Files
gonic/server/ctrlsubsonic/handlers_playlist.go
Nadia Santalla ac798ac2d2 fix(playlist): fix non-admin users not being able to create playlists (#524)
* fix(playlist): fail early if playlist path is a directory

* fix(playlist): check error before assuming playlist loaded
2024-09-15 14:57:23 +00:00

248 lines
7.3 KiB
Go

package ctrlsubsonic
import (
"encoding/base64"
"errors"
"fmt"
"log"
"net/http"
"sort"
"time"
"github.com/jinzhu/gorm"
"go.senan.xyz/gonic/db"
playlistp "go.senan.xyz/gonic/playlist"
"go.senan.xyz/gonic/server/ctrlsubsonic/params"
"go.senan.xyz/gonic/server/ctrlsubsonic/spec"
"go.senan.xyz/gonic/server/ctrlsubsonic/specid"
"go.senan.xyz/gonic/server/ctrlsubsonic/specidpaths"
)
func (c *Controller) ServeGetPlaylists(r *http.Request) *spec.Response {
params := r.Context().Value(CtxParams).(params.Params)
user := r.Context().Value(CtxUser).(*db.User)
paths, err := c.playlistStore.List()
if err != nil {
return spec.NewError(0, "error listing playlists: %v", err)
}
sub := spec.NewResponse()
sub.Playlists = &spec.Playlists{
List: []*spec.Playlist{},
}
for _, path := range paths {
playlist, err := c.playlistStore.Read(path)
if err != nil {
return spec.NewError(0, "error reading playlist %q: %v", path, err)
}
if playlist.UserID != user.ID && !playlist.IsPublic {
continue
}
playlistID := playlistIDEncode(path)
rendered, err := playlistRender(c, params, playlistID, playlist, false)
if err != nil {
return spec.NewError(0, "error rendering playlist %q: %v", path, err)
}
sub.Playlists.List = append(sub.Playlists.List, rendered)
}
return sub
}
func (c *Controller) ServeGetPlaylist(r *http.Request) *spec.Response {
params := r.Context().Value(CtxParams).(params.Params)
playlistID, err := params.GetFirst("id", "playlistId")
if err != nil {
return spec.NewError(10, "please provide an `id` parameter")
}
playlist, err := c.playlistStore.Read(playlistIDDecode(playlistID))
if err != nil {
return spec.NewError(70, "playlist with id %s not found", playlistID)
}
sub := spec.NewResponse()
rendered, err := playlistRender(c, params, playlistID, playlist, true)
if err != nil {
return spec.NewError(0, "error rendering playlist: %v", err)
}
sub.Playlist = rendered
return sub
}
func (c *Controller) ServeCreateOrUpdatePlaylist(r *http.Request) *spec.Response {
user := r.Context().Value(CtxUser).(*db.User)
params := r.Context().Value(CtxParams).(params.Params)
playlistID := params.GetFirstOr( /* default */ "", "id", "playlistId")
playlistPath := playlistIDDecode(playlistID)
var playlist playlistp.Playlist
if playlistPath != "" {
if pl, err := c.playlistStore.Read(playlistPath); err != nil && pl != nil {
playlist = *pl
}
}
if playlist.UserID != 0 && playlist.UserID != user.ID {
return spec.NewError(50, "you aren't allowed update that user's playlist")
}
playlist.UserID = user.ID
playlist.UpdatedAt = time.Now()
if val, err := params.Get("name"); err == nil {
playlist.Name = val
}
playlist.Items = nil
ids := params.GetOrIDList("songId", nil)
for _, id := range ids {
r, err := specidpaths.Locate(c.dbc, id)
if err != nil {
return spec.NewError(0, "lookup id %v: %v", id, err)
}
playlist.Items = append(playlist.Items, r.AbsPath())
}
if playlistPath == "" {
playlistPath = playlistp.NewPath(user.ID, fmt.Sprint(time.Now().UnixMilli()))
playlistID = playlistIDEncode(playlistPath)
}
if err := c.playlistStore.Write(playlistPath, &playlist); err != nil {
return spec.NewError(0, "save playlist: %v", err)
}
sub := spec.NewResponse()
rendered, err := playlistRender(c, params, playlistID, &playlist, true)
if err != nil {
return spec.NewError(0, "error rendering playlist: %v", err)
}
sub.Playlist = rendered
return sub
}
func (c *Controller) ServeUpdatePlaylist(r *http.Request) *spec.Response {
user := r.Context().Value(CtxUser).(*db.User)
params := r.Context().Value(CtxParams).(params.Params)
playlistID := params.GetFirstOr( /* default */ "", "id", "playlistId")
playlistPath := playlistIDDecode(playlistID)
playlist, err := c.playlistStore.Read(playlistPath)
if err != nil {
return spec.NewError(0, "find playlist: %v", err)
}
// update meta info
if playlist.UserID != 0 && playlist.UserID != user.ID {
return spec.NewResponse()
}
if val, err := params.Get("name"); err == nil {
playlist.Name = val
}
if val, err := params.Get("comment"); err == nil {
playlist.Comment = val
}
if val, err := params.GetBool("public"); err == nil {
playlist.IsPublic = val
}
// delete items
if indexes, err := params.GetIntList("songIndexToRemove"); err == nil {
sort.Sort(sort.Reverse(sort.IntSlice(indexes)))
for _, i := range indexes {
playlist.Items = append(playlist.Items[:i], playlist.Items[i+1:]...)
}
}
// add items
if ids, err := params.GetIDList("songIdToAdd"); err == nil {
for _, id := range ids {
item, err := specidpaths.Locate(c.dbc, id)
if err != nil {
return spec.NewError(0, "locate id %q: %v", id, err)
}
playlist.Items = append(playlist.Items, item.AbsPath())
}
}
if err := c.playlistStore.Write(playlistPath, playlist); err != nil {
return spec.NewError(0, "save playlist: %v", err)
}
return spec.NewResponse()
}
func (c *Controller) ServeDeletePlaylist(r *http.Request) *spec.Response {
params := r.Context().Value(CtxParams).(params.Params)
playlistID := params.GetFirstOr( /* default */ "", "id", "playlistId")
if err := c.playlistStore.Delete(playlistIDDecode(playlistID)); err != nil {
return spec.NewError(0, "delete playlist: %v", err)
}
return spec.NewResponse()
}
func playlistIDEncode(path string) string {
return base64.URLEncoding.EncodeToString([]byte(path))
}
func playlistIDDecode(id string) string {
path, _ := base64.URLEncoding.DecodeString(id)
return string(path)
}
func playlistRender(c *Controller, params params.Params, playlistID string, playlist *playlistp.Playlist, withItems bool) (*spec.Playlist, error) {
user := &db.User{}
if err := c.dbc.Where("id=?", playlist.UserID).Find(user).Error; err != nil {
return nil, fmt.Errorf("find user by id: %w", err)
}
resp := &spec.Playlist{
ID: playlistID,
Name: playlist.Name,
Comment: playlist.Comment,
Created: playlist.UpdatedAt,
Changed: playlist.UpdatedAt,
SongCount: len(playlist.Items),
Public: playlist.IsPublic,
Owner: user.Name,
}
if !withItems {
return resp, nil
}
transcodeMeta := streamGetTranscodeMeta(c.dbc, user.ID, params.GetOr("c", ""))
for _, path := range playlist.Items {
file, err := specidpaths.Lookup(c.dbc, MusicPaths(c.musicPaths), c.podcastsPath, path)
if err != nil {
log.Printf("error looking up path %q: %s", path, err)
continue
}
var trch *spec.TrackChild
switch id := file.SID(); id.Type {
case specid.Track:
var track db.Track
if err := c.dbc.Where("id=?", id.Value).Preload("Album").Preload("Album.Artists").Preload("Artists").Preload("TrackStar", "user_id=?", user.ID).Preload("TrackRating", "user_id=?", user.ID).Find(&track).Error; errors.Is(err, gorm.ErrRecordNotFound) {
return nil, fmt.Errorf("load track by id: %w", err)
}
trch = spec.NewTCTrackByFolder(&track, track.Album)
resp.Duration += track.Length
case specid.PodcastEpisode:
var pe db.PodcastEpisode
if err := c.dbc.Preload("Podcast").Where("id=?", id.Value).Find(&pe).Error; errors.Is(err, gorm.ErrRecordNotFound) {
return nil, fmt.Errorf("load podcast episode by id: %w", err)
}
trch = spec.NewTCPodcastEpisode(&pe)
resp.Duration += pe.Length
default:
continue
}
trch.TranscodeMeta = transcodeMeta
resp.List = append(resp.List, trch)
}
resp.SongCount = len(resp.List)
return resp, nil
}