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 }