Initial commit: party-mix-app with prefetch cache, audio preload optimizations
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
283
apps/backend/internal/handlers/playlists.go
Normal file
283
apps/backend/internal/handlers/playlists.go
Normal file
@@ -0,0 +1,283 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/google/uuid"
|
||||
"github.com/toyffee/party-mix/internal/models"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
func parseTags(s string) []string {
|
||||
if s == "" {
|
||||
return []string{}
|
||||
}
|
||||
result := []string{}
|
||||
for _, t := range strings.Split(s, ",") {
|
||||
if t = strings.TrimSpace(t); t != "" {
|
||||
result = append(result, t)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func joinTags(tags []string) string {
|
||||
cleaned := make([]string, 0, len(tags))
|
||||
for _, t := range tags {
|
||||
if t = strings.TrimSpace(t); t != "" {
|
||||
cleaned = append(cleaned, t)
|
||||
}
|
||||
}
|
||||
return strings.Join(cleaned, ",")
|
||||
}
|
||||
|
||||
type playlistResp struct {
|
||||
models.Playlist
|
||||
Tags []string `json:"tags"`
|
||||
}
|
||||
|
||||
type pubPlaylistResp struct {
|
||||
models.Playlist
|
||||
Tags []string `json:"tags"`
|
||||
Username string `json:"username"`
|
||||
}
|
||||
|
||||
func toResp(p models.Playlist) playlistResp {
|
||||
return playlistResp{Playlist: p, Tags: parseTags(p.Tags)}
|
||||
}
|
||||
|
||||
func GetPublicPlaylists(db *gorm.DB) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
var playlists []models.Playlist
|
||||
db.Preload("Tracks", func(tx *gorm.DB) *gorm.DB {
|
||||
return tx.Order("position asc")
|
||||
}).Where("is_public = ?", true).Order("created_at desc").Find(&playlists)
|
||||
|
||||
type UserRow struct {
|
||||
ID string
|
||||
Username string
|
||||
}
|
||||
userIDs := make([]string, 0, len(playlists))
|
||||
for _, p := range playlists {
|
||||
userIDs = append(userIDs, p.UserID)
|
||||
}
|
||||
var users []UserRow
|
||||
db.Model(&models.User{}).Select("id, username").Where("id IN ?", userIDs).Find(&users)
|
||||
userMap := make(map[string]string, len(users))
|
||||
for _, u := range users {
|
||||
userMap[u.ID] = u.Username
|
||||
}
|
||||
|
||||
resp := make([]pubPlaylistResp, 0, len(playlists))
|
||||
for _, p := range playlists {
|
||||
resp = append(resp, pubPlaylistResp{
|
||||
Playlist: p,
|
||||
Tags: parseTags(p.Tags),
|
||||
Username: userMap[p.UserID],
|
||||
})
|
||||
}
|
||||
c.JSON(http.StatusOK, resp)
|
||||
}
|
||||
}
|
||||
|
||||
func GetPlaylists(db *gorm.DB) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
userID := currentUserID(c)
|
||||
var playlists []models.Playlist
|
||||
db.Preload("Tracks", func(tx *gorm.DB) *gorm.DB {
|
||||
return tx.Order("position asc")
|
||||
}).Where("user_id = ?", userID).Order("created_at desc").Find(&playlists)
|
||||
resp := make([]playlistResp, 0, len(playlists))
|
||||
for _, p := range playlists {
|
||||
resp = append(resp, toResp(p))
|
||||
}
|
||||
c.JSON(http.StatusOK, resp)
|
||||
}
|
||||
}
|
||||
|
||||
type playlistTrackInput struct {
|
||||
Title string `json:"title"`
|
||||
Position int `json:"position"`
|
||||
}
|
||||
|
||||
type playlistReq struct {
|
||||
Name string `json:"name" binding:"required"`
|
||||
Tracks []playlistTrackInput `json:"tracks"`
|
||||
IsPublic bool `json:"is_public"`
|
||||
Tags []string `json:"tags"`
|
||||
}
|
||||
|
||||
func CreatePlaylist(db *gorm.DB) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
var req playlistReq
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
playlist := models.Playlist{
|
||||
ID: uuid.NewString(),
|
||||
UserID: currentUserID(c),
|
||||
Name: req.Name,
|
||||
IsPublic: req.IsPublic,
|
||||
Tags: joinTags(req.Tags),
|
||||
CreatedAt: time.Now(),
|
||||
}
|
||||
tracks := buildTracks(playlist.ID, req.Tracks)
|
||||
|
||||
if err := db.Transaction(func(tx *gorm.DB) error {
|
||||
if err := tx.Create(&playlist).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if len(tracks) > 0 {
|
||||
return tx.Create(&tracks).Error
|
||||
}
|
||||
return nil
|
||||
}); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "could not create playlist"})
|
||||
return
|
||||
}
|
||||
|
||||
playlist.Tracks = tracks
|
||||
c.JSON(http.StatusCreated, toResp(playlist))
|
||||
}
|
||||
}
|
||||
|
||||
func GetPlaylist(db *gorm.DB) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
userID := currentUserID(c)
|
||||
var playlist models.Playlist
|
||||
if err := db.Preload("Tracks", func(tx *gorm.DB) *gorm.DB {
|
||||
return tx.Order("position asc")
|
||||
}).First(&playlist, "id = ?", c.Param("id")).Error; err != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "playlist not found"})
|
||||
return
|
||||
}
|
||||
if playlist.UserID != userID {
|
||||
c.JSON(http.StatusForbidden, gin.H{"error": "forbidden"})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, toResp(playlist))
|
||||
}
|
||||
}
|
||||
|
||||
func UpdatePlaylist(db *gorm.DB) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
userID := currentUserID(c)
|
||||
var playlist models.Playlist
|
||||
if err := db.First(&playlist, "id = ?", c.Param("id")).Error; err != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "playlist not found"})
|
||||
return
|
||||
}
|
||||
if playlist.UserID != userID {
|
||||
c.JSON(http.StatusForbidden, gin.H{"error": "forbidden"})
|
||||
return
|
||||
}
|
||||
|
||||
var req playlistReq
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
playlist.Name = req.Name
|
||||
playlist.IsPublic = req.IsPublic
|
||||
playlist.Tags = joinTags(req.Tags)
|
||||
newTracks := buildTracks(playlist.ID, req.Tracks)
|
||||
|
||||
if err := db.Transaction(func(tx *gorm.DB) error {
|
||||
if err := tx.Save(&playlist).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if err := tx.Where("playlist_id = ?", playlist.ID).Delete(&models.PlaylistTrack{}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if len(newTracks) > 0 {
|
||||
return tx.Create(&newTracks).Error
|
||||
}
|
||||
return nil
|
||||
}); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "could not update playlist"})
|
||||
return
|
||||
}
|
||||
|
||||
playlist.Tracks = newTracks
|
||||
c.JSON(http.StatusOK, toResp(playlist))
|
||||
}
|
||||
}
|
||||
|
||||
func AddTrackToPlaylist(db *gorm.DB) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
userID := currentUserID(c)
|
||||
var playlist models.Playlist
|
||||
if err := db.Preload("Tracks").First(&playlist, "id = ?", c.Param("id")).Error; err != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "playlist not found"})
|
||||
return
|
||||
}
|
||||
if playlist.UserID != userID {
|
||||
c.JSON(http.StatusForbidden, gin.H{"error": "forbidden"})
|
||||
return
|
||||
}
|
||||
var req struct {
|
||||
Title string `json:"title" binding:"required"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
track := models.PlaylistTrack{
|
||||
ID: uuid.NewString(),
|
||||
PlaylistID: playlist.ID,
|
||||
Title: req.Title,
|
||||
Position: len(playlist.Tracks),
|
||||
CreatedAt: time.Now(),
|
||||
}
|
||||
if err := db.Create(&track).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "could not add track"})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusCreated, track)
|
||||
}
|
||||
}
|
||||
|
||||
func DeletePlaylist(db *gorm.DB) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
userID := currentUserID(c)
|
||||
var playlist models.Playlist
|
||||
if err := db.First(&playlist, "id = ?", c.Param("id")).Error; err != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "playlist not found"})
|
||||
return
|
||||
}
|
||||
if playlist.UserID != userID {
|
||||
c.JSON(http.StatusForbidden, gin.H{"error": "forbidden"})
|
||||
return
|
||||
}
|
||||
db.Where("playlist_id = ?", playlist.ID).Delete(&models.PlaylistTrack{})
|
||||
db.Delete(&playlist)
|
||||
c.Status(http.StatusNoContent)
|
||||
}
|
||||
}
|
||||
|
||||
func buildTracks(playlistID string, inputs []playlistTrackInput) []models.PlaylistTrack {
|
||||
tracks := make([]models.PlaylistTrack, 0, len(inputs))
|
||||
for i, t := range inputs {
|
||||
if t.Title == "" {
|
||||
continue
|
||||
}
|
||||
pos := t.Position
|
||||
if pos == 0 {
|
||||
pos = i
|
||||
}
|
||||
tracks = append(tracks, models.PlaylistTrack{
|
||||
ID: uuid.NewString(),
|
||||
PlaylistID: playlistID,
|
||||
Title: t.Title,
|
||||
Position: pos,
|
||||
CreatedAt: time.Now(),
|
||||
})
|
||||
}
|
||||
return tracks
|
||||
}
|
||||
Reference in New Issue
Block a user