diff --git a/model/playlist.go b/model/playlist.go index 8e4f552f7..9ae775dfe 100644 --- a/model/playlist.go +++ b/model/playlist.go @@ -1,6 +1,10 @@ package model -import "time" +import ( + "time" + + "github.com/deluan/rest" +) type Playlist struct { ID string `json:"id" orm:"column(id)"` @@ -22,6 +26,18 @@ type PlaylistRepository interface { Get(id string) (*Playlist, error) GetAll(options ...QueryOptions) (Playlists, error) Delete(id string) error + Tracks(playlistId string) PlaylistTracksRepository +} + +type PlaylistTracks struct { + ID string `json:"id" orm:"column(id)"` + MediaFileID string `json:"mediaFileId" orm:"column(media_file_id)"` + MediaFile +} + +type PlaylistTracksRepository interface { + rest.Repository + //rest.Persistable } type Playlists []Playlist diff --git a/persistence/playlist_repository.go b/persistence/playlist_repository.go index b586eb3c8..63e501a1d 100644 --- a/persistence/playlist_repository.go +++ b/persistence/playlist_repository.go @@ -77,10 +77,20 @@ func (r *playlistRepository) GetAll(options ...model.QueryOptions) (model.Playli if err != nil { return nil, err } + // TODO Maybe the tracks are not required when retrieving all playlists? err = r.loadAllTracks(res) return res, err } +func (r *playlistRepository) Tracks(playlistId string) model.PlaylistTracksRepository { + p := &playlistTracksRepository{} + p.playlistId = playlistId + p.ctx = r.ctx + p.ormer = r.ormer + p.tableName = "playlist_tracks" + return p +} + func (r *playlistRepository) updateTracks(id string, tracks model.MediaFiles) error { // Remove old tracks del := Delete("playlist_tracks").Where(Eq{"playlist_id": id}) @@ -128,9 +138,14 @@ func (r *playlistRepository) loadAllTracks(all model.Playlists) error { } func (r *playlistRepository) loadTracks(pls *model.Playlist) (err error) { - tracksQuery := Select("media_file.*").From("media_file"). - Join("playlist_tracks f on f.media_file_id = media_file.id"). - Where(Eq{"f.playlist_id": pls.ID}).OrderBy("f.id") + tracksQuery := Select().From("playlist_tracks"). + LeftJoin("annotation on ("+ + "annotation.item_id = media_file_id"+ + " AND annotation.item_type = 'media_file'"+ + " AND annotation.user_id = '"+userId(r.ctx)+"')"). + Columns("starred", "starred_at", "play_count", "play_date", "rating", "f.*"). + Join("media_file f on f.id = media_file_id"). + Where(Eq{"playlist_id": pls.ID}).OrderBy("playlist_tracks.id") err = r.queryAll(tracksQuery, &pls.Tracks) if err != nil { log.Error("Error loading playlist tracks", "playlist", pls.Name, "id", pls.ID) diff --git a/persistence/playlist_tracks_repository.go b/persistence/playlist_tracks_repository.go new file mode 100644 index 000000000..5459cd639 --- /dev/null +++ b/persistence/playlist_tracks_repository.go @@ -0,0 +1,56 @@ +package persistence + +import ( + . "github.com/Masterminds/squirrel" + "github.com/deluan/navidrome/model" + "github.com/deluan/rest" +) + +type playlistTracksRepository struct { + sqlRepository + sqlRestful + playlistId string +} + +func (r *playlistTracksRepository) Count(options ...rest.QueryOptions) (int64, error) { + return r.count(Select().Where(Eq{"playlist_id": r.playlistId}), r.parseRestOptions(options...)) +} + +func (r *playlistTracksRepository) Read(id string) (interface{}, error) { + sel := r.newSelect(). + LeftJoin("annotation on ("+ + "annotation.item_id = media_file_id"+ + " AND annotation.item_type = 'media_file'"+ + " AND annotation.user_id = '"+userId(r.ctx)+"')"). + Columns("starred", "starred_at", "play_count", "play_date", "rating", "f.*", "playlist_tracks.*"). + Join("media_file f on f.id = media_file_id"). + Where(And{Eq{"playlist_id": r.playlistId}, Eq{"id": id}}) + var trk model.PlaylistTracks + err := r.queryOne(sel, &trk) + return &trk, err +} + +func (r *playlistTracksRepository) ReadAll(options ...rest.QueryOptions) (interface{}, error) { + sel := r.newSelect(r.parseRestOptions(options...)). + LeftJoin("annotation on ("+ + "annotation.item_id = media_file_id"+ + " AND annotation.item_type = 'media_file'"+ + " AND annotation.user_id = '"+userId(r.ctx)+"')"). + Columns("starred", "starred_at", "play_count", "play_date", "rating", "f.*", "playlist_tracks.*"). + Join("media_file f on f.id = media_file_id"). + Where(Eq{"playlist_id": r.playlistId}) + var res []model.PlaylistTracks + err := r.queryAll(sel, &res) + return res, err +} + +func (r *playlistTracksRepository) EntityName() string { + return "playlist_tracks" +} + +func (r *playlistTracksRepository) NewInstance() interface{} { + return &model.PlaylistTracks{} +} + +var _ model.PlaylistTracksRepository = (*playlistTracksRepository)(nil) +var _ model.ResourceRepository = (*playlistTracksRepository)(nil) diff --git a/server/app/app.go b/server/app/app.go index 5945649e2..340f347ba 100644 --- a/server/app/app.go +++ b/server/app/app.go @@ -49,7 +49,9 @@ func (app *Router) routes(path string) http.Handler { app.R(r, "/player", model.Player{}, true) app.R(r, "/playlist", model.Playlist{}, true) app.R(r, "/transcoding", model.Transcoding{}, conf.Server.EnableTranscodingConfig) - app.addResource(r, "/translation", newTranslationRepository, false) + app.RX(r, "/translation", newTranslationRepository, false) + + app.addPlaylistTracksRoute(r) // Keepalive endpoint to be used to keep the session valid (ex: while playing songs) r.Get("/keepalive/*", func(w http.ResponseWriter, r *http.Request) { _, _ = w.Write([]byte(`{"response":"ok"}`)) }) @@ -66,10 +68,10 @@ func (app *Router) R(r chi.Router, pathPrefix string, model interface{}, persist constructor := func(ctx context.Context) rest.Repository { return app.ds.Resource(ctx, model) } - app.addResource(r, pathPrefix, constructor, persistable) + app.RX(r, pathPrefix, constructor, persistable) } -func (app *Router) addResource(r chi.Router, pathPrefix string, constructor rest.RepositoryConstructor, persistable bool) { +func (app *Router) RX(r chi.Router, pathPrefix string, constructor rest.RepositoryConstructor, persistable bool) { r.Route(pathPrefix, func(r chi.Router) { r.Get("/", rest.GetAll(constructor)) if persistable { @@ -86,6 +88,31 @@ func (app *Router) addResource(r chi.Router, pathPrefix string, constructor rest }) } +type restHandler = func(rest.RepositoryConstructor, ...rest.Logger) http.HandlerFunc + +func (app *Router) addPlaylistTracksRoute(r chi.Router) { + // Add a middleware to capture the playlisId + wrapper := func(f restHandler) http.HandlerFunc { + return func(res http.ResponseWriter, req *http.Request) { + c := func(ctx context.Context) rest.Repository { + plsRepo := app.ds.Resource(ctx, model.Playlist{}) + plsId := chi.URLParam(req, "playlistId") + return plsRepo.(model.PlaylistRepository).Tracks(plsId) + } + + f(c).ServeHTTP(res, req) + } + } + + r.Route("/playlist/{playlistId}/tracks", func(r chi.Router) { + r.Get("/", wrapper(rest.GetAll)) + r.Route("/{id}", func(r chi.Router) { + r.Use(UrlParams) + r.Get("/", wrapper(rest.Get)) + }) + }) +} + // Middleware to convert Chi URL params (from Context) to query params, as expected by our REST package func UrlParams(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {