From d59551799b3d5673bfcf8619f2c1e82f66b4c88b Mon Sep 17 00:00:00 2001 From: Kendall Garner <17521368+kgarner7@users.noreply.github.com> Date: Thu, 11 Sep 2025 20:38:03 -0700 Subject: [PATCH 1/2] redo this whole PR, but clearner now that better errata is in --- server/subsonic/api.go | 2 + server/subsonic/bookmarks.go | 71 +++++++++++++++++++ server/subsonic/opensubsonic.go | 1 + server/subsonic/opensubsonic_test.go | 3 +- ...yQueueByIndex with data should match .JSON | 22 ++++++ ...ayQueueByIndex with data should match .XML | 5 ++ ...eueByIndex without data should match .JSON | 11 +++ ...ueueByIndex without data should match .XML | 3 + server/subsonic/responses/responses.go | 10 +++ server/subsonic/responses/responses_test.go | 34 +++++++++ 10 files changed, 161 insertions(+), 1 deletion(-) create mode 100644 server/subsonic/responses/.snapshots/Responses PlayQueueByIndex with data should match .JSON create mode 100644 server/subsonic/responses/.snapshots/Responses PlayQueueByIndex with data should match .XML create mode 100644 server/subsonic/responses/.snapshots/Responses PlayQueueByIndex without data should match .JSON create mode 100644 server/subsonic/responses/.snapshots/Responses PlayQueueByIndex without data should match .XML 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..00c1f2934 100644 --- a/server/subsonic/bookmarks.go +++ b/server/subsonic/bookmarks.go @@ -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 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..c360e422e --- /dev/null +++ b/server/subsonic/responses/.snapshots/Responses PlayQueueByIndex without data should match .JSON @@ -0,0 +1,11 @@ +{ + "status": "ok", + "version": "1.16.1", + "type": "navidrome", + "serverVersion": "v0.55.0", + "openSubsonic": true, + "playQueueByIndex": { + "username": "", + "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..80aa146be --- /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..150214096 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 ( @@ -447,6 +448,15 @@ type PlayQueue struct { 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,omitempty" json:"changed,omitempty"` + ChangedBy string `xml:"changedBy,attr" json:"changedBy"` +} + type Bookmark struct { Entry Child `xml:"entry,omitempty" json:"entry,omitempty"` Position int64 `xml:"position,attr,omitempty" json:"position,omitempty"` diff --git a/server/subsonic/responses/responses_test.go b/server/subsonic/responses/responses_test.go index 7238665cf..1081c8382 100644 --- a/server/subsonic/responses/responses_test.go +++ b/server/subsonic/responses/responses_test.go @@ -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{} From 72eb314267739bcd2cd0a9cbdb050a8a0d0352b5 Mon Sep 17 00:00:00 2001 From: Kendall Garner <17521368+kgarner7@users.noreply.github.com> Date: Fri, 7 Nov 2025 16:33:22 -0800 Subject: [PATCH 2/2] update play queue types --- server/subsonic/bookmarks.go | 4 ++-- ... PlayQueue without data should match .JSON | 1 + ...s PlayQueue without data should match .XML | 2 +- ...eueByIndex without data should match .JSON | 1 + ...ueueByIndex without data should match .XML | 2 +- server/subsonic/responses/responses.go | 24 +++++++++---------- server/subsonic/responses/responses_test.go | 4 ++-- 7 files changed, 20 insertions(+), 18 deletions(-) diff --git a/server/subsonic/bookmarks.go b/server/subsonic/bookmarks.go index 00c1f2934..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 @@ -160,7 +160,7 @@ func (api *Router) GetPlayQueueByIndex(r *http.Request) (*responses.Subsonic, er CurrentIndex: index, Position: pq.Position, Username: user.UserName, - Changed: &pq.UpdatedAt, + Changed: pq.UpdatedAt, ChangedBy: pq.ChangedBy, } return response, nil 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 without data should match .JSON b/server/subsonic/responses/.snapshots/Responses PlayQueueByIndex without data should match .JSON index c360e422e..ad49a35e5 100644 --- a/server/subsonic/responses/.snapshots/Responses PlayQueueByIndex without data should match .JSON +++ b/server/subsonic/responses/.snapshots/Responses PlayQueueByIndex without data should match .JSON @@ -6,6 +6,7 @@ "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 index 80aa146be..d99681f4c 100644 --- a/server/subsonic/responses/.snapshots/Responses PlayQueueByIndex without data should match .XML +++ b/server/subsonic/responses/.snapshots/Responses PlayQueueByIndex without data should match .XML @@ -1,3 +1,3 @@ - + diff --git a/server/subsonic/responses/responses.go b/server/subsonic/responses/responses.go index 150214096..0724d2fff 100644 --- a/server/subsonic/responses/responses.go +++ b/server/subsonic/responses/responses.go @@ -440,21 +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,omitempty" json:"changed,omitempty"` - ChangedBy string `xml:"changedBy,attr" json:"changedBy"` + 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 1081c8382..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} @@ -802,7 +802,7 @@ var _ = Describe("Responses", func() { response.PlayQueueByIndex.Username = "user1" response.PlayQueueByIndex.CurrentIndex = gg.P(0) response.PlayQueueByIndex.Position = 243 - response.PlayQueueByIndex.Changed = &time.Time{} + response.PlayQueueByIndex.Changed = time.Time{} response.PlayQueueByIndex.ChangedBy = "a_client" child := make([]Child, 1) child[0] = Child{Id: "1", Title: "title", IsDir: false}