diff --git a/server/subsonic/api.go b/server/subsonic/api.go index bb3d20e5c..d08d3eb5b 100644 --- a/server/subsonic/api.go +++ b/server/subsonic/api.go @@ -148,7 +148,9 @@ func (api *Router) routes() http.Handler { h(r, "createBookmark", api.CreateBookmark) h(r, "deleteBookmark", api.DeleteBookmark) h(r, "getPlayQueue", api.GetPlayQueue) + h(r, "getPlayQueueByIndex", api.GetPlayQueueByIndex) h(r, "savePlayQueue", api.SavePlayQueue) + h(r, "savePlayQueueByIndex", api.SavePlayQueueByIndex) }) r.Group(func(r chi.Router) { r.Use(getPlayer(api.players)) diff --git a/server/subsonic/bookmarks.go b/server/subsonic/bookmarks.go index d7286c20c..b1e71b1c7 100644 --- a/server/subsonic/bookmarks.go +++ b/server/subsonic/bookmarks.go @@ -91,7 +91,7 @@ func (api *Router) GetPlayQueue(r *http.Request) (*responses.Subsonic, error) { Current: currentID, Position: pq.Position, Username: user.UserName, - Changed: &pq.UpdatedAt, + Changed: pq.UpdatedAt, ChangedBy: pq.ChangedBy, } return response, nil @@ -135,3 +135,74 @@ func (api *Router) SavePlayQueue(r *http.Request) (*responses.Subsonic, error) { } return newResponse(), nil } + +func (api *Router) GetPlayQueueByIndex(r *http.Request) (*responses.Subsonic, error) { + user, _ := request.UserFrom(r.Context()) + + repo := api.ds.PlayQueue(r.Context()) + pq, err := repo.RetrieveWithMediaFiles(user.ID) + if err != nil && !errors.Is(err, model.ErrNotFound) { + return nil, err + } + if pq == nil || len(pq.Items) == 0 { + return newResponse(), nil + } + + response := newResponse() + + var index *int + if len(pq.Items) > 0 { + index = &pq.Current + } + + response.PlayQueueByIndex = &responses.PlayQueueByIndex{ + Entry: slice.MapWithArg(pq.Items, r.Context(), childFromMediaFile), + CurrentIndex: index, + Position: pq.Position, + Username: user.UserName, + Changed: pq.UpdatedAt, + ChangedBy: pq.ChangedBy, + } + return response, nil +} + +func (api *Router) SavePlayQueueByIndex(r *http.Request) (*responses.Subsonic, error) { + p := req.Params(r) + ids, _ := p.Strings("id") + + position := p.Int64Or("position", 0) + + var err error + var currentIndex int + + if len(ids) > 0 { + currentIndex, err = p.Int("currentIndex") + if err != nil || currentIndex < 0 || currentIndex >= len(ids) { + return nil, newError(responses.ErrorMissingParameter, "missing parameter index, err: %s", err) + } + } + + items := slice.Map(ids, func(id string) model.MediaFile { + return model.MediaFile{ID: id} + }) + + user, _ := request.UserFrom(r.Context()) + client, _ := request.ClientFrom(r.Context()) + + pq := &model.PlayQueue{ + UserID: user.ID, + Current: currentIndex, + Position: position, + ChangedBy: client, + Items: items, + CreatedAt: time.Time{}, + UpdatedAt: time.Time{}, + } + + repo := api.ds.PlayQueue(r.Context()) + err = repo.Store(pq) + if err != nil { + return nil, err + } + return newResponse(), nil +} diff --git a/server/subsonic/opensubsonic.go b/server/subsonic/opensubsonic.go index 17ce3c2b0..a364651c5 100644 --- a/server/subsonic/opensubsonic.go +++ b/server/subsonic/opensubsonic.go @@ -12,6 +12,7 @@ func (api *Router) GetOpenSubsonicExtensions(_ *http.Request) (*responses.Subson {Name: "transcodeOffset", Versions: []int32{1}}, {Name: "formPost", Versions: []int32{1}}, {Name: "songLyrics", Versions: []int32{1}}, + {Name: "indexBasedQueue", Versions: []int32{1}}, } return response, nil } diff --git a/server/subsonic/opensubsonic_test.go b/server/subsonic/opensubsonic_test.go index 3cc680afe..58dca682c 100644 --- a/server/subsonic/opensubsonic_test.go +++ b/server/subsonic/opensubsonic_test.go @@ -35,10 +35,11 @@ var _ = Describe("GetOpenSubsonicExtensions", func() { err := json.Unmarshal(w.Body.Bytes(), &response) Expect(err).NotTo(HaveOccurred()) Expect(*response.Subsonic.OpenSubsonicExtensions).To(SatisfyAll( - HaveLen(3), + HaveLen(4), ContainElement(responses.OpenSubsonicExtension{Name: "transcodeOffset", Versions: []int32{1}}), ContainElement(responses.OpenSubsonicExtension{Name: "formPost", Versions: []int32{1}}), ContainElement(responses.OpenSubsonicExtension{Name: "songLyrics", Versions: []int32{1}}), + ContainElement(responses.OpenSubsonicExtension{Name: "indexBasedQueue", Versions: []int32{1}}), )) }) }) diff --git a/server/subsonic/responses/.snapshots/Responses PlayQueue without data should match .JSON b/server/subsonic/responses/.snapshots/Responses PlayQueue without data should match .JSON index 88eebb276..70b10c059 100644 --- a/server/subsonic/responses/.snapshots/Responses PlayQueue without data should match .JSON +++ b/server/subsonic/responses/.snapshots/Responses PlayQueue without data should match .JSON @@ -6,6 +6,7 @@ "openSubsonic": true, "playQueue": { "username": "", + "changed": "0001-01-01T00:00:00Z", "changedBy": "" } } diff --git a/server/subsonic/responses/.snapshots/Responses PlayQueue without data should match .XML b/server/subsonic/responses/.snapshots/Responses PlayQueue without data should match .XML index 5af3d9157..597781cbd 100644 --- a/server/subsonic/responses/.snapshots/Responses PlayQueue without data should match .XML +++ b/server/subsonic/responses/.snapshots/Responses PlayQueue without data should match .XML @@ -1,3 +1,3 @@ - + diff --git a/server/subsonic/responses/.snapshots/Responses PlayQueueByIndex with data should match .JSON b/server/subsonic/responses/.snapshots/Responses PlayQueueByIndex with data should match .JSON new file mode 100644 index 000000000..efc032ca6 --- /dev/null +++ b/server/subsonic/responses/.snapshots/Responses PlayQueueByIndex with data should match .JSON @@ -0,0 +1,22 @@ +{ + "status": "ok", + "version": "1.16.1", + "type": "navidrome", + "serverVersion": "v0.55.0", + "openSubsonic": true, + "playQueueByIndex": { + "entry": [ + { + "id": "1", + "isDir": false, + "title": "title", + "isVideo": false + } + ], + "currentIndex": 0, + "position": 243, + "username": "user1", + "changed": "0001-01-01T00:00:00Z", + "changedBy": "a_client" + } +} diff --git a/server/subsonic/responses/.snapshots/Responses PlayQueueByIndex with data should match .XML b/server/subsonic/responses/.snapshots/Responses PlayQueueByIndex with data should match .XML new file mode 100644 index 000000000..1d31b334e --- /dev/null +++ b/server/subsonic/responses/.snapshots/Responses PlayQueueByIndex with data should match .XML @@ -0,0 +1,5 @@ + + + + + diff --git a/server/subsonic/responses/.snapshots/Responses PlayQueueByIndex without data should match .JSON b/server/subsonic/responses/.snapshots/Responses PlayQueueByIndex without data should match .JSON new file mode 100644 index 000000000..ad49a35e5 --- /dev/null +++ b/server/subsonic/responses/.snapshots/Responses PlayQueueByIndex without data should match .JSON @@ -0,0 +1,12 @@ +{ + "status": "ok", + "version": "1.16.1", + "type": "navidrome", + "serverVersion": "v0.55.0", + "openSubsonic": true, + "playQueueByIndex": { + "username": "", + "changed": "0001-01-01T00:00:00Z", + "changedBy": "" + } +} diff --git a/server/subsonic/responses/.snapshots/Responses PlayQueueByIndex without data should match .XML b/server/subsonic/responses/.snapshots/Responses PlayQueueByIndex without data should match .XML new file mode 100644 index 000000000..d99681f4c --- /dev/null +++ b/server/subsonic/responses/.snapshots/Responses PlayQueueByIndex without data should match .XML @@ -0,0 +1,3 @@ + + + diff --git a/server/subsonic/responses/responses.go b/server/subsonic/responses/responses.go index ffda2aa43..0724d2fff 100644 --- a/server/subsonic/responses/responses.go +++ b/server/subsonic/responses/responses.go @@ -60,6 +60,7 @@ type Subsonic struct { // OpenSubsonic extensions OpenSubsonicExtensions *OpenSubsonicExtensions `xml:"openSubsonicExtensions,omitempty" json:"openSubsonicExtensions,omitempty"` LyricsList *LyricsList `xml:"lyricsList,omitempty" json:"lyricsList,omitempty"` + PlayQueueByIndex *PlayQueueByIndex `xml:"playQueueByIndex,omitempty" json:"playQueueByIndex,omitempty"` } const ( @@ -439,12 +440,21 @@ type TopSongs struct { } type PlayQueue struct { - Entry []Child `xml:"entry,omitempty" json:"entry,omitempty"` - Current string `xml:"current,attr,omitempty" json:"current,omitempty"` - Position int64 `xml:"position,attr,omitempty" json:"position,omitempty"` - Username string `xml:"username,attr" json:"username"` - Changed *time.Time `xml:"changed,attr,omitempty" json:"changed,omitempty"` - ChangedBy string `xml:"changedBy,attr" json:"changedBy"` + Entry []Child `xml:"entry,omitempty" json:"entry,omitempty"` + Current string `xml:"current,attr,omitempty" json:"current,omitempty"` + Position int64 `xml:"position,attr,omitempty" json:"position,omitempty"` + Username string `xml:"username,attr" json:"username"` + Changed time.Time `xml:"changed,attr" json:"changed"` + ChangedBy string `xml:"changedBy,attr" json:"changedBy"` +} + +type PlayQueueByIndex struct { + Entry []Child `xml:"entry,omitempty" json:"entry,omitempty"` + CurrentIndex *int `xml:"currentIndex,attr,omitempty" json:"currentIndex,omitempty"` + Position int64 `xml:"position,attr,omitempty" json:"position,omitempty"` + Username string `xml:"username,attr" json:"username"` + Changed time.Time `xml:"changed,attr" json:"changed"` + ChangedBy string `xml:"changedBy,attr" json:"changedBy"` } type Bookmark struct { diff --git a/server/subsonic/responses/responses_test.go b/server/subsonic/responses/responses_test.go index 7238665cf..2ee8e080d 100644 --- a/server/subsonic/responses/responses_test.go +++ b/server/subsonic/responses/responses_test.go @@ -768,7 +768,7 @@ var _ = Describe("Responses", func() { response.PlayQueue.Username = "user1" response.PlayQueue.Current = "111" response.PlayQueue.Position = 243 - response.PlayQueue.Changed = &time.Time{} + response.PlayQueue.Changed = time.Time{} response.PlayQueue.ChangedBy = "a_client" child := make([]Child, 1) child[0] = Child{Id: "1", Title: "title", IsDir: false} @@ -783,6 +783,40 @@ var _ = Describe("Responses", func() { }) }) + Describe("PlayQueueByIndex", func() { + BeforeEach(func() { + response.PlayQueueByIndex = &PlayQueueByIndex{} + }) + + Context("without data", func() { + It("should match .XML", func() { + Expect(xml.MarshalIndent(response, "", " ")).To(MatchSnapshot()) + }) + It("should match .JSON", func() { + Expect(json.MarshalIndent(response, "", " ")).To(MatchSnapshot()) + }) + }) + + Context("with data", func() { + BeforeEach(func() { + response.PlayQueueByIndex.Username = "user1" + response.PlayQueueByIndex.CurrentIndex = gg.P(0) + response.PlayQueueByIndex.Position = 243 + response.PlayQueueByIndex.Changed = time.Time{} + response.PlayQueueByIndex.ChangedBy = "a_client" + child := make([]Child, 1) + child[0] = Child{Id: "1", Title: "title", IsDir: false} + response.PlayQueueByIndex.Entry = child + }) + It("should match .XML", func() { + Expect(xml.MarshalIndent(response, "", " ")).To(MatchSnapshot()) + }) + It("should match .JSON", func() { + Expect(json.MarshalIndent(response, "", " ")).To(MatchSnapshot()) + }) + }) + }) + Describe("Shares", func() { BeforeEach(func() { response.Shares = &Shares{}