Implements the get/save play queue Subsonic endpoints and bumps API version to 1.12.0

This commit is contained in:
Deluan 2020-07-31 13:07:39 -04:00 committed by Deluan Quintão
parent 16c38eb344
commit 3000238a3c
10 changed files with 171 additions and 8 deletions

View file

@ -54,7 +54,7 @@ func CreateSubsonicAPIRouter() (*subsonic.Router, error) {
transcodingCache := core.NewTranscodingCache() transcodingCache := core.NewTranscodingCache()
mediaStreamer := core.NewMediaStreamer(dataStore, transcoderTranscoder, transcodingCache) mediaStreamer := core.NewMediaStreamer(dataStore, transcoderTranscoder, transcodingCache)
players := engine.NewPlayers(dataStore) 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 return router, nil
} }

View file

@ -26,6 +26,7 @@ type DataStore interface {
MediaFolder(ctx context.Context) MediaFolderRepository MediaFolder(ctx context.Context) MediaFolderRepository
Genre(ctx context.Context) GenreRepository Genre(ctx context.Context) GenreRepository
Playlist(ctx context.Context) PlaylistRepository Playlist(ctx context.Context) PlaylistRepository
PlayQueue(ctx context.Context) PlayQueueRepository
Property(ctx context.Context) PropertyRepository Property(ctx context.Context) PropertyRepository
User(ctx context.Context) UserRepository User(ctx context.Context) UserRepository
Transcoding(ctx context.Context) TranscodingRepository Transcoding(ctx context.Context) TranscodingRepository

View file

@ -52,6 +52,10 @@ func (db *MockDataStore) Playlist(context.Context) model.PlaylistRepository {
return struct{ 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 { func (db *MockDataStore) Property(context.Context) model.PropertyRepository {
return struct{ model.PropertyRepository }{} return struct{ model.PropertyRepository }{}
} }

View file

@ -38,6 +38,10 @@ func (s *SQLStore) Genre(ctx context.Context) model.GenreRepository {
return NewGenreRepository(ctx, s.getOrmer()) 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 { func (s *SQLStore) Playlist(ctx context.Context) model.PlaylistRepository {
return NewPlaylistRepository(ctx, s.getOrmer()) return NewPlaylistRepository(ctx, s.getOrmer())
} }

View file

@ -37,8 +37,10 @@ type playQueue struct {
} }
func (r *playQueueRepository) Store(q *model.PlayQueue) error { func (r *playQueueRepository) Store(q *model.PlayQueue) error {
u := loggedUser(r.ctx)
err := r.clearPlayQueue(q.UserID) err := r.clearPlayQueue(q.UserID)
if err != nil { if err != nil {
log.Error(r.ctx, "Error deleting previous playqueue", "user", u.UserName, err)
return err return err
} }
pq := r.fromModel(q) pq := r.fromModel(q)
@ -47,7 +49,11 @@ func (r *playQueueRepository) Store(q *model.PlayQueue) error {
} }
pq.UpdatedAt = time.Now() pq.UpdatedAt = time.Now()
_, err = r.put(pq.ID, pq) _, 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) { 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]} idsFilter := Eq{"id": chunks[i]}
tracks, err := mfRepo.GetAll(model.QueryOptions{Filters: idsFilter}) tracks, err := mfRepo.GetAll(model.QueryOptions{Filters: idsFilter})
if err != nil { 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 { for _, t := range tracks {
trackMap[t.ID] = t trackMap[t.ID] = t

View file

@ -11,13 +11,14 @@ import (
"github.com/deluan/navidrome/core" "github.com/deluan/navidrome/core"
"github.com/deluan/navidrome/engine" "github.com/deluan/navidrome/engine"
"github.com/deluan/navidrome/log" "github.com/deluan/navidrome/log"
"github.com/deluan/navidrome/model"
"github.com/deluan/navidrome/server/subsonic/responses" "github.com/deluan/navidrome/server/subsonic/responses"
"github.com/deluan/navidrome/utils" "github.com/deluan/navidrome/utils"
"github.com/go-chi/chi" "github.com/go-chi/chi"
"github.com/go-chi/chi/middleware" "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) type Handler = func(http.ResponseWriter, *http.Request) (*responses.Subsonic, error)
@ -32,15 +33,17 @@ type Router struct {
Users engine.Users Users engine.Users
Streamer core.MediaStreamer Streamer core.MediaStreamer
Players engine.Players Players engine.Players
DataStore model.DataStore
mux http.Handler mux http.Handler
} }
func New(browser engine.Browser, artwork core.Artwork, listGenerator engine.ListGenerator, users engine.Users, 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, 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, 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() r.mux = r.routes()
return r return r
} }
@ -107,6 +110,12 @@ func (api *Router) routes() http.Handler {
H(withPlayer, "deletePlaylist", c.DeletePlaylist) H(withPlayer, "deletePlaylist", c.DeletePlaylist)
H(withPlayer, "updatePlaylist", c.UpdatePlaylist) 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) { r.Group(func(r chi.Router) {
c := initSearchingController(api) c := initSearchingController(api)
withPlayer := r.With(getPlayer(api.Players)) withPlayer := r.With(getPlayer(api.Players))

View file

@ -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
}

View file

@ -162,3 +162,54 @@ func getTranscoding(ctx context.Context) (format string, bitRate int) {
} }
return 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
}

View file

@ -64,6 +64,12 @@ func initStreamController(router *Router) *StreamController {
return streamController return streamController
} }
func initBookmarksController(router *Router) *BookmarksController {
dataStore := router.DataStore
bookmarksController := NewBookmarksController(dataStore)
return bookmarksController
}
// wire_injectors.go: // wire_injectors.go:
var allProviders = wire.NewSet( var allProviders = wire.NewSet(
@ -75,5 +81,6 @@ var allProviders = wire.NewSet(
NewSearchingController, NewSearchingController,
NewUsersController, NewUsersController,
NewMediaRetrievalController, 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"),
) )

View file

@ -16,7 +16,8 @@ var allProviders = wire.NewSet(
NewUsersController, NewUsersController,
NewMediaRetrievalController, NewMediaRetrievalController,
NewStreamController, 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 { func initSystemController(router *Router) *SystemController {
@ -54,3 +55,7 @@ func initMediaRetrievalController(router *Router) *MediaRetrievalController {
func initStreamController(router *Router) *StreamController { func initStreamController(router *Router) *StreamController {
panic(wire.Build(allProviders)) panic(wire.Build(allProviders))
} }
func initBookmarksController(router *Router) *BookmarksController {
panic(wire.Build(allProviders))
}