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{}