package subsonic import ( "database/sql" "strconv" "git.noroutes.net/NoRoutes/vibetunes/internal/database" ) func init() { register("getPlaylists", handleGetPlaylists) register("getPlaylist", handleGetPlaylist) register("createPlaylist", handleCreatePlaylist) register("updatePlaylist", handleUpdatePlaylist) register("deletePlaylist", handleDeletePlaylist) } func playlistToModel(db *sql.DB, p *database.Playlist) Playlist { entries, _ := database.ListPlaylistEntries(db, p.ID) duration := 0 for _, e := range entries { if mf, ok := mediaFileForSongID(db, e.SongID); ok { duration += int(mf.DurationSeconds + 0.5) } } owner, _ := database.GetUserByID(db, p.OwnerID) ownerName := "" if owner != nil { ownerName = owner.Username } return Playlist{ ID: p.ID, Name: p.Name, Comment: p.Description, Owner: ownerName, Public: p.IsPublic, SongCount: len(entries), Duration: duration, Created: isoTime(p.CreatedAt), Changed: isoTime(p.UpdatedAt), } } func mediaFileForSongID(db *sql.DB, songID string) (*database.MediaFile, bool) { song, err := database.GetSong(db, songID) if err != nil { return nil, false } mf, err := database.GetMediaFile(db, song.MediaFileID) if err != nil { return nil, false } return mf, true } // canSeePlaylist reports whether user may view playlist p: they own it, // it's public, or they're an admin. func canSeePlaylist(user *database.User, p *database.Playlist) bool { return user.IsAdmin || user.ID == p.OwnerID || p.IsPublic } func handleGetPlaylists(rc *Context) *apiError { db := rc.Server.DB var playlists []*database.Playlist var err error if username := rc.Params.Get("username"); username != "" { if !rc.User.IsAdmin && username != rc.User.Username { return errUnauthorized("viewing another user's playlists") } target, terr := database.GetUserByUsername(db, username) if terr != nil { return errNotFound("User") } playlists, err = database.ListPlaylistsByOwner(db, target.ID) } else { playlists, err = database.ListPlaylistsVisibleToUser(db, rc.User.ID) } if err != nil { return newError(ErrGeneric, err.Error()) } out := &Playlists{} for _, p := range playlists { out.Playlist = append(out.Playlist, playlistToModel(db, p)) } rc.Response = newResponse() rc.Response.Playlists = out return nil } func handleGetPlaylist(rc *Context) *apiError { id, aerr := rc.Params.Required("id") if aerr != nil { return aerr } db := rc.Server.DB p, err := database.GetPlaylist(db, id) if err != nil { return errNotFound("Playlist") } if !canSeePlaylist(rc.User, p) { return errUnauthorized("viewing this playlist") } entries, err := database.ListPlaylistEntries(db, id) if err != nil { return newError(ErrGeneric, err.Error()) } out := &PlaylistWithSongs{Playlist: playlistToModel(db, p)} for _, e := range entries { song, serr := database.GetSong(db, e.SongID) if serr != nil { continue } mf, merr := database.GetMediaFile(db, song.MediaFileID) if merr != nil { continue } out.Entry = append(out.Entry, childFromSong(db, song, mf, albumOfSong(db, song), rc.User.ID)) } rc.Response = newResponse() rc.Response.Playlist = out return nil } func handleCreatePlaylist(rc *Context) *apiError { if !rc.User.Allows(rc.User.Permissions.Playlist) { return errUnauthorized("creating playlists") } db := rc.Server.DB songIDs := rc.Params.GetAll("songId") if playlistID := rc.Params.Get("playlistId"); playlistID != "" { p, err := database.GetPlaylist(db, playlistID) if err != nil { return errNotFound("Playlist") } if p.OwnerID != rc.User.ID && !rc.User.IsAdmin { return errUnauthorized("modifying this playlist") } if err := database.ReplacePlaylistSongs(db, playlistID, songIDs); err != nil { return newError(ErrGeneric, err.Error()) } return respondWithPlaylist(rc, playlistID) } name, aerr := rc.Params.Required("name") if aerr != nil { return aerr } p, err := database.CreatePlaylist(db, rc.User.ID, name, "", false) if err != nil { return newError(ErrGeneric, err.Error()) } if len(songIDs) > 0 { if err := database.ReplacePlaylistSongs(db, p.ID, songIDs); err != nil { return newError(ErrGeneric, err.Error()) } } return respondWithPlaylist(rc, p.ID) } func respondWithPlaylist(rc *Context, playlistID string) *apiError { db := rc.Server.DB p, err := database.GetPlaylist(db, playlistID) if err != nil { return newError(ErrGeneric, err.Error()) } entries, err := database.ListPlaylistEntries(db, playlistID) if err != nil { return newError(ErrGeneric, err.Error()) } out := &PlaylistWithSongs{Playlist: playlistToModel(db, p)} for _, e := range entries { song, serr := database.GetSong(db, e.SongID) if serr != nil { continue } mf, merr := database.GetMediaFile(db, song.MediaFileID) if merr != nil { continue } out.Entry = append(out.Entry, childFromSong(db, song, mf, albumOfSong(db, song), rc.User.ID)) } rc.Response = newResponse() rc.Response.Playlist = out return nil } func handleUpdatePlaylist(rc *Context) *apiError { id, aerr := rc.Params.Required("playlistId") if aerr != nil { return aerr } db := rc.Server.DB p, err := database.GetPlaylist(db, id) if err != nil { return errNotFound("Playlist") } if p.OwnerID != rc.User.ID && !rc.User.IsAdmin { return errUnauthorized("modifying this playlist") } name := p.Name if v := rc.Params.Get("name"); v != "" { name = v } comment := p.Description if rc.Params.Has("comment") { comment = rc.Params.Get("comment") } public := p.IsPublic if rc.Params.Has("public") { public = rc.Params.Bool("public", p.IsPublic) } if err := database.RenamePlaylist(db, id, name, comment, public); err != nil { return newError(ErrGeneric, err.Error()) } toRemove := rc.Params.GetAll("songIndexToRemove") toAdd := rc.Params.GetAll("songIdToAdd") if len(toRemove) > 0 || len(toAdd) > 0 { entries, err := database.ListPlaylistEntries(db, id) if err != nil { return newError(ErrGeneric, err.Error()) } removeSet := map[int]bool{} for _, s := range toRemove { if n, err := strconv.Atoi(s); err == nil { removeSet[n] = true } } var songIDs []string for i, e := range entries { if !removeSet[i] { songIDs = append(songIDs, e.SongID) } } songIDs = append(songIDs, toAdd...) if err := database.ReplacePlaylistSongs(db, id, songIDs); err != nil { return newError(ErrGeneric, err.Error()) } } rc.Response = newResponse() return nil } func handleDeletePlaylist(rc *Context) *apiError { id, aerr := rc.Params.Required("id") if aerr != nil { return aerr } db := rc.Server.DB p, err := database.GetPlaylist(db, id) if err != nil { return errNotFound("Playlist") } if p.OwnerID != rc.User.ID && !rc.User.IsAdmin { return errUnauthorized("deleting this playlist") } if err := database.DeletePlaylist(db, id); err != nil { return newError(ErrGeneric, err.Error()) } rc.Response = newResponse() return nil }