mirror of
https://github.com/navidrome/navidrome.git
synced 2025-04-04 21:17:37 +03:00
Implements the get/save play queue Subsonic endpoints and bumps API version to 1.12.0
This commit is contained in:
parent
16c38eb344
commit
3000238a3c
10 changed files with 171 additions and 8 deletions
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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 }{}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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())
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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))
|
||||||
|
|
75
server/subsonic/bookmarks.go
Normal file
75
server/subsonic/bookmarks.go
Normal 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
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
|
@ -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"),
|
||||||
)
|
)
|
||||||
|
|
|
@ -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))
|
||||||
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue