diff --git a/cmd/wire_gen.go b/cmd/wire_gen.go index 7fcfd5b9e..e77343ce5 100644 --- a/cmd/wire_gen.go +++ b/cmd/wire_gen.go @@ -54,7 +54,7 @@ func CreateSubsonicAPIRouter() (*subsonic.Router, error) { transcodingCache := core.NewTranscodingCache() mediaStreamer := core.NewMediaStreamer(dataStore, transcoderTranscoder, transcodingCache) players := engine.NewPlayers(dataStore) - router := subsonic.New(browser, artwork, listGenerator, users, playlists, ratings, scrobbler, search, mediaStreamer, players) + router := subsonic.New(browser, artwork, listGenerator, users, playlists, ratings, scrobbler, search, mediaStreamer, players, dataStore) return router, nil } diff --git a/model/datastore.go b/model/datastore.go index a2fa14bdd..819323ec6 100644 --- a/model/datastore.go +++ b/model/datastore.go @@ -26,6 +26,7 @@ type DataStore interface { MediaFolder(ctx context.Context) MediaFolderRepository Genre(ctx context.Context) GenreRepository Playlist(ctx context.Context) PlaylistRepository + PlayQueue(ctx context.Context) PlayQueueRepository Property(ctx context.Context) PropertyRepository User(ctx context.Context) UserRepository Transcoding(ctx context.Context) TranscodingRepository diff --git a/persistence/mock_persistence.go b/persistence/mock_persistence.go index d18243cb0..880cd1711 100644 --- a/persistence/mock_persistence.go +++ b/persistence/mock_persistence.go @@ -52,6 +52,10 @@ func (db *MockDataStore) Playlist(context.Context) model.PlaylistRepository { return struct{ model.PlaylistRepository }{} } +func (db *MockDataStore) PlayQueue(context.Context) model.PlayQueueRepository { + return struct{ model.PlayQueueRepository }{} +} + func (db *MockDataStore) Property(context.Context) model.PropertyRepository { return struct{ model.PropertyRepository }{} } diff --git a/persistence/persistence.go b/persistence/persistence.go index d0b37dbd0..4cf676d58 100644 --- a/persistence/persistence.go +++ b/persistence/persistence.go @@ -38,6 +38,10 @@ func (s *SQLStore) Genre(ctx context.Context) model.GenreRepository { return NewGenreRepository(ctx, s.getOrmer()) } +func (s *SQLStore) PlayQueue(ctx context.Context) model.PlayQueueRepository { + return NewPlayQueueRepository(ctx, s.getOrmer()) +} + func (s *SQLStore) Playlist(ctx context.Context) model.PlaylistRepository { return NewPlaylistRepository(ctx, s.getOrmer()) } diff --git a/persistence/playqueue_repository.go b/persistence/playqueue_repository.go index cbd67a8dd..5389e46b2 100644 --- a/persistence/playqueue_repository.go +++ b/persistence/playqueue_repository.go @@ -37,8 +37,10 @@ type playQueue struct { } func (r *playQueueRepository) Store(q *model.PlayQueue) error { + u := loggedUser(r.ctx) err := r.clearPlayQueue(q.UserID) if err != nil { + log.Error(r.ctx, "Error deleting previous playqueue", "user", u.UserName, err) return err } pq := r.fromModel(q) @@ -47,7 +49,11 @@ func (r *playQueueRepository) Store(q *model.PlayQueue) error { } pq.UpdatedAt = time.Now() _, err = r.put(pq.ID, pq) - return err + if err != nil { + log.Error(r.ctx, "Error saving playqueue", "user", u.UserName, err) + return err + } + return nil } func (r *playQueueRepository) Retrieve(userId string) (*model.PlayQueue, error) { @@ -129,7 +135,8 @@ func (r *playQueueRepository) loadTracks(p *model.PlayQueue) model.MediaFiles { idsFilter := Eq{"id": chunks[i]} tracks, err := mfRepo.GetAll(model.QueryOptions{Filters: idsFilter}) if err != nil { - log.Error(r.ctx, "Could not load playqueue's tracks", "userId", p.UserID, err) + u := loggedUser(r.ctx) + log.Error(r.ctx, "Could not load playqueue's tracks", "user", u.UserName, err) } for _, t := range tracks { trackMap[t.ID] = t diff --git a/server/subsonic/api.go b/server/subsonic/api.go index 8d6b1e8cc..65cfe9d97 100644 --- a/server/subsonic/api.go +++ b/server/subsonic/api.go @@ -11,13 +11,14 @@ import ( "github.com/deluan/navidrome/core" "github.com/deluan/navidrome/engine" "github.com/deluan/navidrome/log" + "github.com/deluan/navidrome/model" "github.com/deluan/navidrome/server/subsonic/responses" "github.com/deluan/navidrome/utils" "github.com/go-chi/chi" "github.com/go-chi/chi/middleware" ) -const Version = "1.10.2" +const Version = "1.12.0" type Handler = func(http.ResponseWriter, *http.Request) (*responses.Subsonic, error) @@ -32,15 +33,17 @@ type Router struct { Users engine.Users Streamer core.MediaStreamer Players engine.Players + DataStore model.DataStore mux http.Handler } func New(browser engine.Browser, artwork core.Artwork, listGenerator engine.ListGenerator, users engine.Users, playlists engine.Playlists, ratings engine.Ratings, scrobbler engine.Scrobbler, search engine.Search, - streamer core.MediaStreamer, players engine.Players) *Router { + streamer core.MediaStreamer, players engine.Players, ds model.DataStore) *Router { r := &Router{Browser: browser, Artwork: artwork, ListGenerator: listGenerator, Playlists: playlists, - Ratings: ratings, Scrobbler: scrobbler, Search: search, Users: users, Streamer: streamer, Players: players} + Ratings: ratings, Scrobbler: scrobbler, Search: search, Users: users, Streamer: streamer, Players: players, + DataStore: ds} r.mux = r.routes() return r } @@ -107,6 +110,12 @@ func (api *Router) routes() http.Handler { H(withPlayer, "deletePlaylist", c.DeletePlaylist) H(withPlayer, "updatePlaylist", c.UpdatePlaylist) }) + r.Group(func(r chi.Router) { + c := initBookmarksController(api) + withPlayer := r.With(getPlayer(api.Players)) + H(withPlayer, "getPlayQueue", c.GetPlayQueue) + H(withPlayer, "savePlayQueue", c.SavePlayQueue) + }) r.Group(func(r chi.Router) { c := initSearchingController(api) withPlayer := r.With(getPlayer(api.Players)) diff --git a/server/subsonic/bookmarks.go b/server/subsonic/bookmarks.go new file mode 100644 index 000000000..4ffa29fda --- /dev/null +++ b/server/subsonic/bookmarks.go @@ -0,0 +1,75 @@ +package subsonic + +import ( + "net/http" + "time" + + "github.com/deluan/navidrome/model" + "github.com/deluan/navidrome/model/request" + "github.com/deluan/navidrome/server/subsonic/responses" + "github.com/deluan/navidrome/utils" +) + +type BookmarksController struct { + ds model.DataStore +} + +func NewBookmarksController(ds model.DataStore) *BookmarksController { + return &BookmarksController{ds: ds} +} + +func (c *BookmarksController) GetPlayQueue(w http.ResponseWriter, r *http.Request) (*responses.Subsonic, error) { + user, _ := request.UserFrom(r.Context()) + + repo := c.ds.PlayQueue(r.Context()) + pq, err := repo.Retrieve(user.ID) + if err != nil { + return nil, NewError(responses.ErrorGeneric, "Internal Error") + } + + response := NewResponse() + response.PlayQueue = &responses.PlayQueue{ + Entry: ChildrenFromMediaFiles(r.Context(), pq.Items), + Current: pq.Current, + Position: int64(pq.Position), + Username: user.UserName, + Changed: &pq.UpdatedAt, + ChangedBy: pq.ChangedBy, + } + return response, nil +} + +func (c *BookmarksController) SavePlayQueue(w http.ResponseWriter, r *http.Request) (*responses.Subsonic, error) { + ids, err := RequiredParamStrings(r, "id", "id parameter required") + if err != nil { + return nil, err + } + + current := utils.ParamString(r, "current") + position := utils.ParamInt(r, "position", 0) + + user, _ := request.UserFrom(r.Context()) + client, _ := request.ClientFrom(r.Context()) + + var items model.MediaFiles + for _, id := range ids { + items = append(items, model.MediaFile{ID: id}) + } + + pq := &model.PlayQueue{ + UserID: user.ID, + Current: current, + Position: float32(position), + ChangedBy: client, + Items: items, + CreatedAt: time.Time{}, + UpdatedAt: time.Time{}, + } + + repo := c.ds.PlayQueue(r.Context()) + err = repo.Store(pq) + if err != nil { + return nil, NewError(responses.ErrorGeneric, "Internal Error") + } + return NewResponse(), nil +} diff --git a/server/subsonic/helpers.go b/server/subsonic/helpers.go index 019601e4a..1d82cedfb 100644 --- a/server/subsonic/helpers.go +++ b/server/subsonic/helpers.go @@ -162,3 +162,54 @@ func getTranscoding(ctx context.Context) (format string, bitRate int) { } return } + +// This seems to be duplicated, but it is an initial step into merging `engine` and the `subsonic` packages, +// In the future there won't be any conversion to/from `engine. Entry` anymore +func ChildFromMediaFile(ctx context.Context, mf *model.MediaFile) responses.Child { + child := responses.Child{} + child.Id = mf.ID + child.Title = mf.Title + child.IsDir = false + child.Parent = mf.AlbumID + child.Album = mf.Album + child.Year = mf.Year + child.Artist = mf.Artist + child.Genre = mf.Genre + child.Track = mf.TrackNumber + child.Duration = int(mf.Duration) + child.Size = mf.Size + child.Suffix = mf.Suffix + child.BitRate = mf.BitRate + if mf.HasCoverArt { + child.CoverArt = mf.ID + } else { + child.CoverArt = "al-" + mf.AlbumID + } + child.ContentType = mf.ContentType() + child.Path = mf.Path + child.DiscNumber = mf.DiscNumber + child.Created = &mf.CreatedAt + child.AlbumId = mf.AlbumID + child.ArtistId = mf.ArtistID + child.Type = "music" + child.PlayCount = mf.PlayCount + if mf.Starred { + child.Starred = &mf.StarredAt + } + child.UserRating = mf.Rating + + format, _ := getTranscoding(ctx) + if mf.Suffix != "" && format != "" && mf.Suffix != format { + child.TranscodedSuffix = format + child.TranscodedContentType = mime.TypeByExtension("." + format) + } + return child +} + +func ChildrenFromMediaFiles(ctx context.Context, mfs model.MediaFiles) []responses.Child { + children := make([]responses.Child, len(mfs)) + for i, mf := range mfs { + children[i] = ChildFromMediaFile(ctx, &mf) + } + return children +} diff --git a/server/subsonic/wire_gen.go b/server/subsonic/wire_gen.go index 872c02c24..335d09306 100644 --- a/server/subsonic/wire_gen.go +++ b/server/subsonic/wire_gen.go @@ -64,6 +64,12 @@ func initStreamController(router *Router) *StreamController { return streamController } +func initBookmarksController(router *Router) *BookmarksController { + dataStore := router.DataStore + bookmarksController := NewBookmarksController(dataStore) + return bookmarksController +} + // wire_injectors.go: var allProviders = wire.NewSet( @@ -75,5 +81,6 @@ var allProviders = wire.NewSet( NewSearchingController, NewUsersController, NewMediaRetrievalController, - NewStreamController, wire.FieldsOf(new(*Router), "Browser", "Artwork", "ListGenerator", "Playlists", "Ratings", "Scrobbler", "Search", "Streamer"), + NewStreamController, + NewBookmarksController, wire.FieldsOf(new(*Router), "Browser", "Artwork", "ListGenerator", "Playlists", "Ratings", "Scrobbler", "Search", "Streamer", "DataStore"), ) diff --git a/server/subsonic/wire_injectors.go b/server/subsonic/wire_injectors.go index 0a6761707..f869bb6ed 100644 --- a/server/subsonic/wire_injectors.go +++ b/server/subsonic/wire_injectors.go @@ -16,7 +16,8 @@ var allProviders = wire.NewSet( NewUsersController, NewMediaRetrievalController, NewStreamController, - wire.FieldsOf(new(*Router), "Browser", "Artwork", "ListGenerator", "Playlists", "Ratings", "Scrobbler", "Search", "Streamer"), + NewBookmarksController, + wire.FieldsOf(new(*Router), "Browser", "Artwork", "ListGenerator", "Playlists", "Ratings", "Scrobbler", "Search", "Streamer", "DataStore"), ) func initSystemController(router *Router) *SystemController { @@ -54,3 +55,7 @@ func initMediaRetrievalController(router *Router) *MediaRetrievalController { func initStreamController(router *Router) *StreamController { panic(wire.Build(allProviders)) } + +func initBookmarksController(router *Router) *BookmarksController { + panic(wire.Build(allProviders)) +}