mirror of
https://github.com/navidrome/navidrome.git
synced 2025-04-03 20:47:35 +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()
|
||||
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
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 }{}
|
||||
}
|
||||
|
|
|
@ -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())
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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))
|
||||
|
|
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
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
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"),
|
||||
)
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue