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()
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
}

View file

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

View file

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

View file

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

View file

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

View file

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

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
}
// 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
}
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"),
)

View file

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