feat(server): add index-based play queue endpoints to native API (#4210)
* Add migration converting playqueue current to index * refactor Signed-off-by: Deluan <deluan@navidrome.org> * fix(queue): ensure valid current index and improve test coverage Signed-off-by: Deluan <deluan@navidrome.org> --------- Signed-off-by: Deluan <deluan@navidrome.org>
This commit is contained in:
@@ -0,0 +1,80 @@
|
|||||||
|
package migrations
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"database/sql"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/pressly/goose/v3"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
goose.AddMigrationContext(upPlayQueueCurrentToIndex, downPlayQueueCurrentToIndex)
|
||||||
|
}
|
||||||
|
|
||||||
|
func upPlayQueueCurrentToIndex(ctx context.Context, tx *sql.Tx) error {
|
||||||
|
_, err := tx.ExecContext(ctx, `
|
||||||
|
create table playqueue_dg_tmp(
|
||||||
|
id varchar(255) not null,
|
||||||
|
user_id varchar(255) not null
|
||||||
|
references user(id)
|
||||||
|
on update cascade on delete cascade,
|
||||||
|
current integer not null default 0,
|
||||||
|
position real,
|
||||||
|
changed_by varchar(255),
|
||||||
|
items varchar(255),
|
||||||
|
created_at datetime,
|
||||||
|
updated_at datetime
|
||||||
|
);`)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
rows, err := tx.QueryContext(ctx, `select id, user_id, current, position, changed_by, items, created_at, updated_at from playqueue`)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
|
||||||
|
stmt, err := tx.PrepareContext(ctx, `insert into playqueue_dg_tmp(id, user_id, current, position, changed_by, items, created_at, updated_at) values(?,?,?,?,?,?,?,?)`)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer stmt.Close()
|
||||||
|
|
||||||
|
for rows.Next() {
|
||||||
|
var id, userID, currentID, changedBy, items string
|
||||||
|
var position sql.NullFloat64
|
||||||
|
var createdAt, updatedAt sql.NullString
|
||||||
|
if err = rows.Scan(&id, &userID, ¤tID, &position, &changedBy, &items, &createdAt, &updatedAt); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
index := 0
|
||||||
|
if currentID != "" && items != "" {
|
||||||
|
parts := strings.Split(items, ",")
|
||||||
|
for i, p := range parts {
|
||||||
|
if p == currentID {
|
||||||
|
index = i
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_, err = stmt.Exec(id, userID, index, position, changedBy, items, createdAt, updatedAt)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err = rows.Err(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err = tx.ExecContext(ctx, `drop table playqueue;`); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
_, err = tx.ExecContext(ctx, `alter table playqueue_dg_tmp rename to playqueue;`)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func downPlayQueueCurrentToIndex(ctx context.Context, tx *sql.Tx) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
+1
-1
@@ -7,7 +7,7 @@ import (
|
|||||||
type PlayQueue struct {
|
type PlayQueue struct {
|
||||||
ID string `structs:"id" json:"id"`
|
ID string `structs:"id" json:"id"`
|
||||||
UserID string `structs:"user_id" json:"userId"`
|
UserID string `structs:"user_id" json:"userId"`
|
||||||
Current string `structs:"current" json:"current"`
|
Current int `structs:"current" json:"current"`
|
||||||
Position int64 `structs:"position" json:"position"`
|
Position int64 `structs:"position" json:"position"`
|
||||||
ChangedBy string `structs:"changed_by" json:"changedBy"`
|
ChangedBy string `structs:"changed_by" json:"changedBy"`
|
||||||
Items MediaFiles `structs:"-" json:"items,omitempty"`
|
Items MediaFiles `structs:"-" json:"items,omitempty"`
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ func NewPlayQueueRepository(ctx context.Context, db dbx.Builder) model.PlayQueue
|
|||||||
type playQueue struct {
|
type playQueue struct {
|
||||||
ID string `structs:"id"`
|
ID string `structs:"id"`
|
||||||
UserID string `structs:"user_id"`
|
UserID string `structs:"user_id"`
|
||||||
Current string `structs:"current"`
|
Current int `structs:"current"`
|
||||||
Position int64 `structs:"position"`
|
Position int64 `structs:"position"`
|
||||||
ChangedBy string `structs:"changed_by"`
|
ChangedBy string `structs:"changed_by"`
|
||||||
Items string `structs:"items"`
|
Items string `structs:"items"`
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ var _ = Describe("PlayQueueRepository", func() {
|
|||||||
It("stores and retrieves the playqueue for the user", func() {
|
It("stores and retrieves the playqueue for the user", func() {
|
||||||
By("Storing a playqueue for the user")
|
By("Storing a playqueue for the user")
|
||||||
|
|
||||||
expected := aPlayQueue("userid", songDayInALife.ID, 123, songComeTogether, songDayInALife)
|
expected := aPlayQueue("userid", 1, 123, songComeTogether, songDayInALife)
|
||||||
Expect(repo.Store(expected)).To(Succeed())
|
Expect(repo.Store(expected)).To(Succeed())
|
||||||
|
|
||||||
actual, err := repo.Retrieve("userid")
|
actual, err := repo.Retrieve("userid")
|
||||||
@@ -42,7 +42,7 @@ var _ = Describe("PlayQueueRepository", func() {
|
|||||||
|
|
||||||
By("Storing a new playqueue for the same user")
|
By("Storing a new playqueue for the same user")
|
||||||
|
|
||||||
another := aPlayQueue("userid", songRadioactivity.ID, 321, songAntenna, songRadioactivity)
|
another := aPlayQueue("userid", 1, 321, songAntenna, songRadioactivity)
|
||||||
Expect(repo.Store(another)).To(Succeed())
|
Expect(repo.Store(another)).To(Succeed())
|
||||||
|
|
||||||
actual, err = repo.Retrieve("userid")
|
actual, err = repo.Retrieve("userid")
|
||||||
@@ -62,7 +62,7 @@ var _ = Describe("PlayQueueRepository", func() {
|
|||||||
Expect(mfRepo.Put(&newSong)).To(Succeed())
|
Expect(mfRepo.Put(&newSong)).To(Succeed())
|
||||||
|
|
||||||
// Create a playqueue with the new song
|
// Create a playqueue with the new song
|
||||||
pq := aPlayQueue("userid", newSong.ID, 0, newSong, songAntenna)
|
pq := aPlayQueue("userid", 0, 0, newSong, songAntenna)
|
||||||
Expect(repo.Store(pq)).To(Succeed())
|
Expect(repo.Store(pq)).To(Succeed())
|
||||||
|
|
||||||
// Retrieve the playqueue
|
// Retrieve the playqueue
|
||||||
@@ -107,7 +107,7 @@ func AssertPlayQueue(expected, actual *model.PlayQueue) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func aPlayQueue(userId, current string, position int64, items ...model.MediaFile) *model.PlayQueue {
|
func aPlayQueue(userId string, current int, position int64, items ...model.MediaFile) *model.PlayQueue {
|
||||||
createdAt := time.Now()
|
createdAt := time.Now()
|
||||||
updatedAt := createdAt.Add(time.Minute)
|
updatedAt := createdAt.Add(time.Minute)
|
||||||
return &model.PlayQueue{
|
return &model.PlayQueue{
|
||||||
|
|||||||
@@ -60,6 +60,7 @@ func (n *Router) routes() http.Handler {
|
|||||||
n.addPlaylistRoute(r)
|
n.addPlaylistRoute(r)
|
||||||
n.addPlaylistTrackRoute(r)
|
n.addPlaylistTrackRoute(r)
|
||||||
n.addSongPlaylistsRoute(r)
|
n.addSongPlaylistsRoute(r)
|
||||||
|
n.addQueueRoute(r)
|
||||||
n.addMissingFilesRoute(r)
|
n.addMissingFilesRoute(r)
|
||||||
n.addInspectRoute(r)
|
n.addInspectRoute(r)
|
||||||
n.addConfigRoute(r)
|
n.addConfigRoute(r)
|
||||||
@@ -152,6 +153,13 @@ func (n *Router) addSongPlaylistsRoute(r chi.Router) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (n *Router) addQueueRoute(r chi.Router) {
|
||||||
|
r.Route("/queue", func(r chi.Router) {
|
||||||
|
r.Get("/", getQueue(n.ds))
|
||||||
|
r.Post("/", saveQueue(n.ds))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
func (n *Router) addMissingFilesRoute(r chi.Router) {
|
func (n *Router) addMissingFilesRoute(r chi.Router) {
|
||||||
r.Route("/missing", func(r chi.Router) {
|
r.Route("/missing", func(r chi.Router) {
|
||||||
n.RX(r, "/", newMissingRepository(n.ds), false)
|
n.RX(r, "/", newMissingRepository(n.ds), false)
|
||||||
|
|||||||
@@ -0,0 +1,76 @@
|
|||||||
|
package nativeapi
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/navidrome/navidrome/log"
|
||||||
|
"github.com/navidrome/navidrome/model"
|
||||||
|
"github.com/navidrome/navidrome/model/request"
|
||||||
|
"github.com/navidrome/navidrome/utils/slice"
|
||||||
|
)
|
||||||
|
|
||||||
|
type queuePayload struct {
|
||||||
|
Ids []string `json:"ids"`
|
||||||
|
Current int `json:"current"`
|
||||||
|
Position int64 `json:"position"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func getQueue(ds model.DataStore) http.HandlerFunc {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
ctx := r.Context()
|
||||||
|
user, _ := request.UserFrom(ctx)
|
||||||
|
repo := ds.PlayQueue(ctx)
|
||||||
|
pq, err := repo.Retrieve(user.ID)
|
||||||
|
if err != nil && !errors.Is(err, model.ErrNotFound) {
|
||||||
|
log.Error(ctx, "Error retrieving queue", err)
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if pq == nil {
|
||||||
|
pq = &model.PlayQueue{}
|
||||||
|
}
|
||||||
|
resp, err := json.Marshal(pq)
|
||||||
|
if err != nil {
|
||||||
|
log.Error(ctx, "Error marshalling queue", err)
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
_, _ = w.Write(resp)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func saveQueue(ds model.DataStore) http.HandlerFunc {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
ctx := r.Context()
|
||||||
|
var payload queuePayload
|
||||||
|
if err := json.NewDecoder(r.Body).Decode(&payload); err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
user, _ := request.UserFrom(ctx)
|
||||||
|
client, _ := request.ClientFrom(ctx)
|
||||||
|
items := slice.Map(payload.Ids, func(id string) model.MediaFile {
|
||||||
|
return model.MediaFile{ID: id}
|
||||||
|
})
|
||||||
|
if len(payload.Ids) > 0 && (payload.Current < 0 || payload.Current >= len(payload.Ids)) {
|
||||||
|
http.Error(w, "current index out of bounds", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
pq := &model.PlayQueue{
|
||||||
|
UserID: user.ID,
|
||||||
|
Current: payload.Current,
|
||||||
|
Position: max(payload.Position, 0),
|
||||||
|
ChangedBy: client,
|
||||||
|
Items: items,
|
||||||
|
}
|
||||||
|
if err := ds.PlayQueue(ctx).Store(pq); err != nil {
|
||||||
|
log.Error(ctx, "Error saving queue", err)
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
w.WriteHeader(http.StatusNoContent)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,164 @@
|
|||||||
|
package nativeapi
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
|
||||||
|
"github.com/navidrome/navidrome/model"
|
||||||
|
"github.com/navidrome/navidrome/model/request"
|
||||||
|
"github.com/navidrome/navidrome/tests"
|
||||||
|
. "github.com/onsi/ginkgo/v2"
|
||||||
|
. "github.com/onsi/gomega"
|
||||||
|
)
|
||||||
|
|
||||||
|
var _ = Describe("Queue Endpoints", func() {
|
||||||
|
var (
|
||||||
|
ds *tests.MockDataStore
|
||||||
|
repo *tests.MockPlayQueueRepo
|
||||||
|
user model.User
|
||||||
|
userRepo *tests.MockedUserRepo
|
||||||
|
)
|
||||||
|
|
||||||
|
BeforeEach(func() {
|
||||||
|
repo = &tests.MockPlayQueueRepo{}
|
||||||
|
user = model.User{ID: "u1", UserName: "user"}
|
||||||
|
userRepo = tests.CreateMockUserRepo()
|
||||||
|
_ = userRepo.Put(&user)
|
||||||
|
ds = &tests.MockDataStore{MockedPlayQueue: repo, MockedUser: userRepo, MockedProperty: &tests.MockedPropertyRepo{}}
|
||||||
|
})
|
||||||
|
|
||||||
|
Describe("POST /queue", func() {
|
||||||
|
It("saves the queue", func() {
|
||||||
|
payload := queuePayload{Ids: []string{"s1", "s2"}, Current: 1, Position: 10}
|
||||||
|
body, _ := json.Marshal(payload)
|
||||||
|
req := httptest.NewRequest("POST", "/queue", bytes.NewReader(body))
|
||||||
|
ctx := request.WithUser(req.Context(), user)
|
||||||
|
ctx = request.WithClient(ctx, "TestClient")
|
||||||
|
req = req.WithContext(ctx)
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
|
||||||
|
saveQueue(ds)(w, req)
|
||||||
|
Expect(w.Code).To(Equal(http.StatusNoContent))
|
||||||
|
Expect(repo.Queue).ToNot(BeNil())
|
||||||
|
Expect(repo.Queue.Current).To(Equal(1))
|
||||||
|
Expect(repo.Queue.Items).To(HaveLen(2))
|
||||||
|
Expect(repo.Queue.Items[1].ID).To(Equal("s2"))
|
||||||
|
Expect(repo.Queue.ChangedBy).To(Equal("TestClient"))
|
||||||
|
})
|
||||||
|
|
||||||
|
It("saves an empty queue", func() {
|
||||||
|
payload := queuePayload{Ids: []string{}, Current: 0, Position: 0}
|
||||||
|
body, _ := json.Marshal(payload)
|
||||||
|
req := httptest.NewRequest("POST", "/queue", bytes.NewReader(body))
|
||||||
|
req = req.WithContext(request.WithUser(req.Context(), user))
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
|
||||||
|
saveQueue(ds)(w, req)
|
||||||
|
Expect(w.Code).To(Equal(http.StatusNoContent))
|
||||||
|
Expect(repo.Queue).ToNot(BeNil())
|
||||||
|
Expect(repo.Queue.Items).To(HaveLen(0))
|
||||||
|
})
|
||||||
|
|
||||||
|
It("returns bad request for invalid current index (negative)", func() {
|
||||||
|
payload := queuePayload{Ids: []string{"s1", "s2"}, Current: -1, Position: 10}
|
||||||
|
body, _ := json.Marshal(payload)
|
||||||
|
req := httptest.NewRequest("POST", "/queue", bytes.NewReader(body))
|
||||||
|
req = req.WithContext(request.WithUser(req.Context(), user))
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
|
||||||
|
saveQueue(ds)(w, req)
|
||||||
|
Expect(w.Code).To(Equal(http.StatusBadRequest))
|
||||||
|
Expect(w.Body.String()).To(ContainSubstring("current index out of bounds"))
|
||||||
|
})
|
||||||
|
|
||||||
|
It("returns bad request for invalid current index (too large)", func() {
|
||||||
|
payload := queuePayload{Ids: []string{"s1", "s2"}, Current: 2, Position: 10}
|
||||||
|
body, _ := json.Marshal(payload)
|
||||||
|
req := httptest.NewRequest("POST", "/queue", bytes.NewReader(body))
|
||||||
|
req = req.WithContext(request.WithUser(req.Context(), user))
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
|
||||||
|
saveQueue(ds)(w, req)
|
||||||
|
Expect(w.Code).To(Equal(http.StatusBadRequest))
|
||||||
|
Expect(w.Body.String()).To(ContainSubstring("current index out of bounds"))
|
||||||
|
})
|
||||||
|
|
||||||
|
It("returns bad request for malformed JSON", func() {
|
||||||
|
req := httptest.NewRequest("POST", "/queue", bytes.NewReader([]byte("invalid json")))
|
||||||
|
req = req.WithContext(request.WithUser(req.Context(), user))
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
|
||||||
|
saveQueue(ds)(w, req)
|
||||||
|
Expect(w.Code).To(Equal(http.StatusBadRequest))
|
||||||
|
})
|
||||||
|
|
||||||
|
It("returns internal server error when store fails", func() {
|
||||||
|
repo.Err = true
|
||||||
|
payload := queuePayload{Ids: []string{"s1"}, Current: 0, Position: 10}
|
||||||
|
body, _ := json.Marshal(payload)
|
||||||
|
req := httptest.NewRequest("POST", "/queue", bytes.NewReader(body))
|
||||||
|
req = req.WithContext(request.WithUser(req.Context(), user))
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
|
||||||
|
saveQueue(ds)(w, req)
|
||||||
|
Expect(w.Code).To(Equal(http.StatusInternalServerError))
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
Describe("GET /queue", func() {
|
||||||
|
It("returns the queue", func() {
|
||||||
|
queue := &model.PlayQueue{
|
||||||
|
UserID: user.ID,
|
||||||
|
Current: 1,
|
||||||
|
Position: 55,
|
||||||
|
Items: model.MediaFiles{
|
||||||
|
{ID: "track1", Title: "Song 1"},
|
||||||
|
{ID: "track2", Title: "Song 2"},
|
||||||
|
{ID: "track3", Title: "Song 3"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
repo.Queue = queue
|
||||||
|
req := httptest.NewRequest("GET", "/queue", nil)
|
||||||
|
req = req.WithContext(request.WithUser(req.Context(), user))
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
|
||||||
|
getQueue(ds)(w, req)
|
||||||
|
Expect(w.Code).To(Equal(http.StatusOK))
|
||||||
|
Expect(w.Header().Get("Content-Type")).To(Equal("application/json"))
|
||||||
|
var resp model.PlayQueue
|
||||||
|
Expect(json.Unmarshal(w.Body.Bytes(), &resp)).To(Succeed())
|
||||||
|
Expect(resp.Current).To(Equal(1))
|
||||||
|
Expect(resp.Position).To(Equal(int64(55)))
|
||||||
|
Expect(resp.Items).To(HaveLen(3))
|
||||||
|
Expect(resp.Items[0].ID).To(Equal("track1"))
|
||||||
|
Expect(resp.Items[1].ID).To(Equal("track2"))
|
||||||
|
Expect(resp.Items[2].ID).To(Equal("track3"))
|
||||||
|
})
|
||||||
|
|
||||||
|
It("returns empty queue when user has no queue", func() {
|
||||||
|
req := httptest.NewRequest("GET", "/queue", nil)
|
||||||
|
req = req.WithContext(request.WithUser(req.Context(), user))
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
|
||||||
|
getQueue(ds)(w, req)
|
||||||
|
Expect(w.Code).To(Equal(http.StatusOK))
|
||||||
|
var resp model.PlayQueue
|
||||||
|
Expect(json.Unmarshal(w.Body.Bytes(), &resp)).To(Succeed())
|
||||||
|
Expect(resp.Items).To(BeEmpty())
|
||||||
|
Expect(resp.Current).To(Equal(0))
|
||||||
|
Expect(resp.Position).To(Equal(int64(0)))
|
||||||
|
})
|
||||||
|
|
||||||
|
It("returns internal server error when retrieve fails", func() {
|
||||||
|
repo.Err = true
|
||||||
|
req := httptest.NewRequest("GET", "/queue", nil)
|
||||||
|
req = req.WithContext(request.WithUser(req.Context(), user))
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
|
||||||
|
getQueue(ds)(w, req)
|
||||||
|
Expect(w.Code).To(Equal(http.StatusInternalServerError))
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -82,9 +82,13 @@ func (api *Router) GetPlayQueue(r *http.Request) (*responses.Subsonic, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
response := newResponse()
|
response := newResponse()
|
||||||
|
var currentID string
|
||||||
|
if pq.Current >= 0 && pq.Current < len(pq.Items) {
|
||||||
|
currentID = pq.Items[pq.Current].ID
|
||||||
|
}
|
||||||
response.PlayQueue = &responses.PlayQueue{
|
response.PlayQueue = &responses.PlayQueue{
|
||||||
Entry: slice.MapWithArg(pq.Items, r.Context(), childFromMediaFile),
|
Entry: slice.MapWithArg(pq.Items, r.Context(), childFromMediaFile),
|
||||||
Current: pq.Current,
|
Current: currentID,
|
||||||
Position: pq.Position,
|
Position: pq.Position,
|
||||||
Username: user.UserName,
|
Username: user.UserName,
|
||||||
Changed: &pq.UpdatedAt,
|
Changed: &pq.UpdatedAt,
|
||||||
@@ -96,20 +100,27 @@ func (api *Router) GetPlayQueue(r *http.Request) (*responses.Subsonic, error) {
|
|||||||
func (api *Router) SavePlayQueue(r *http.Request) (*responses.Subsonic, error) {
|
func (api *Router) SavePlayQueue(r *http.Request) (*responses.Subsonic, error) {
|
||||||
p := req.Params(r)
|
p := req.Params(r)
|
||||||
ids, _ := p.Strings("id")
|
ids, _ := p.Strings("id")
|
||||||
current, _ := p.String("current")
|
currentID, _ := p.String("current")
|
||||||
position := p.Int64Or("position", 0)
|
position := p.Int64Or("position", 0)
|
||||||
|
|
||||||
user, _ := request.UserFrom(r.Context())
|
user, _ := request.UserFrom(r.Context())
|
||||||
client, _ := request.ClientFrom(r.Context())
|
client, _ := request.ClientFrom(r.Context())
|
||||||
|
|
||||||
var items model.MediaFiles
|
items := slice.Map(ids, func(id string) model.MediaFile {
|
||||||
for _, id := range ids {
|
return model.MediaFile{ID: id}
|
||||||
items = append(items, model.MediaFile{ID: id})
|
})
|
||||||
|
|
||||||
|
currentIndex := 0
|
||||||
|
for i, id := range ids {
|
||||||
|
if id == currentID {
|
||||||
|
currentIndex = i
|
||||||
|
break
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pq := &model.PlayQueue{
|
pq := &model.PlayQueue{
|
||||||
UserID: user.ID,
|
UserID: user.ID,
|
||||||
Current: current,
|
Current: currentIndex,
|
||||||
Position: position,
|
Position: position,
|
||||||
ChangedBy: client,
|
ChangedBy: client,
|
||||||
Items: items,
|
Items: items,
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ type MockDataStore struct {
|
|||||||
MockedProperty model.PropertyRepository
|
MockedProperty model.PropertyRepository
|
||||||
MockedPlayer model.PlayerRepository
|
MockedPlayer model.PlayerRepository
|
||||||
MockedPlaylist model.PlaylistRepository
|
MockedPlaylist model.PlaylistRepository
|
||||||
|
MockedPlayQueue model.PlayQueueRepository
|
||||||
MockedShare model.ShareRepository
|
MockedShare model.ShareRepository
|
||||||
MockedTranscoding model.TranscodingRepository
|
MockedTranscoding model.TranscodingRepository
|
||||||
MockedUserProps model.UserPropsRepository
|
MockedUserProps model.UserPropsRepository
|
||||||
@@ -115,10 +116,14 @@ func (db *MockDataStore) Playlist(ctx context.Context) model.PlaylistRepository
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (db *MockDataStore) PlayQueue(ctx context.Context) model.PlayQueueRepository {
|
func (db *MockDataStore) PlayQueue(ctx context.Context) model.PlayQueueRepository {
|
||||||
if db.RealDS != nil {
|
if db.MockedPlayQueue == nil {
|
||||||
return db.RealDS.PlayQueue(ctx)
|
if db.RealDS != nil {
|
||||||
|
db.MockedPlayQueue = db.RealDS.PlayQueue(ctx)
|
||||||
|
} else {
|
||||||
|
db.MockedPlayQueue = &MockPlayQueueRepo{}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return struct{ model.PlayQueueRepository }{}
|
return db.MockedPlayQueue
|
||||||
}
|
}
|
||||||
|
|
||||||
func (db *MockDataStore) UserProps(ctx context.Context) model.UserPropsRepository {
|
func (db *MockDataStore) UserProps(ctx context.Context) model.UserPropsRepository {
|
||||||
|
|||||||
@@ -0,0 +1,39 @@
|
|||||||
|
package tests
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
|
||||||
|
"github.com/navidrome/navidrome/model"
|
||||||
|
)
|
||||||
|
|
||||||
|
type MockPlayQueueRepo struct {
|
||||||
|
model.PlayQueueRepository
|
||||||
|
Queue *model.PlayQueue
|
||||||
|
Err bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MockPlayQueueRepo) Store(q *model.PlayQueue) error {
|
||||||
|
if m.Err {
|
||||||
|
return errors.New("error")
|
||||||
|
}
|
||||||
|
copyItems := make(model.MediaFiles, len(q.Items))
|
||||||
|
copy(copyItems, q.Items)
|
||||||
|
qCopy := *q
|
||||||
|
qCopy.Items = copyItems
|
||||||
|
m.Queue = &qCopy
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MockPlayQueueRepo) Retrieve(userId string) (*model.PlayQueue, error) {
|
||||||
|
if m.Err {
|
||||||
|
return nil, errors.New("error")
|
||||||
|
}
|
||||||
|
if m.Queue == nil || m.Queue.UserID != userId {
|
||||||
|
return nil, model.ErrNotFound
|
||||||
|
}
|
||||||
|
copyItems := make(model.MediaFiles, len(m.Queue.Items))
|
||||||
|
copy(copyItems, m.Queue.Items)
|
||||||
|
qCopy := *m.Queue
|
||||||
|
qCopy.Items = copyItems
|
||||||
|
return &qCopy, nil
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user