feat(subsonic): implement indexBasedQueue extension (#4244)
* redo this whole PR, but clearner now that better errata is in * update play queue types
This commit is contained in:
@@ -148,7 +148,9 @@ func (api *Router) routes() http.Handler {
|
|||||||
h(r, "createBookmark", api.CreateBookmark)
|
h(r, "createBookmark", api.CreateBookmark)
|
||||||
h(r, "deleteBookmark", api.DeleteBookmark)
|
h(r, "deleteBookmark", api.DeleteBookmark)
|
||||||
h(r, "getPlayQueue", api.GetPlayQueue)
|
h(r, "getPlayQueue", api.GetPlayQueue)
|
||||||
|
h(r, "getPlayQueueByIndex", api.GetPlayQueueByIndex)
|
||||||
h(r, "savePlayQueue", api.SavePlayQueue)
|
h(r, "savePlayQueue", api.SavePlayQueue)
|
||||||
|
h(r, "savePlayQueueByIndex", api.SavePlayQueueByIndex)
|
||||||
})
|
})
|
||||||
r.Group(func(r chi.Router) {
|
r.Group(func(r chi.Router) {
|
||||||
r.Use(getPlayer(api.players))
|
r.Use(getPlayer(api.players))
|
||||||
|
|||||||
@@ -91,7 +91,7 @@ func (api *Router) GetPlayQueue(r *http.Request) (*responses.Subsonic, error) {
|
|||||||
Current: currentID,
|
Current: currentID,
|
||||||
Position: pq.Position,
|
Position: pq.Position,
|
||||||
Username: user.UserName,
|
Username: user.UserName,
|
||||||
Changed: &pq.UpdatedAt,
|
Changed: pq.UpdatedAt,
|
||||||
ChangedBy: pq.ChangedBy,
|
ChangedBy: pq.ChangedBy,
|
||||||
}
|
}
|
||||||
return response, nil
|
return response, nil
|
||||||
@@ -135,3 +135,74 @@ func (api *Router) SavePlayQueue(r *http.Request) (*responses.Subsonic, error) {
|
|||||||
}
|
}
|
||||||
return newResponse(), nil
|
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
|
||||||
|
}
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ func (api *Router) GetOpenSubsonicExtensions(_ *http.Request) (*responses.Subson
|
|||||||
{Name: "transcodeOffset", Versions: []int32{1}},
|
{Name: "transcodeOffset", Versions: []int32{1}},
|
||||||
{Name: "formPost", Versions: []int32{1}},
|
{Name: "formPost", Versions: []int32{1}},
|
||||||
{Name: "songLyrics", Versions: []int32{1}},
|
{Name: "songLyrics", Versions: []int32{1}},
|
||||||
|
{Name: "indexBasedQueue", Versions: []int32{1}},
|
||||||
}
|
}
|
||||||
return response, nil
|
return response, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -35,10 +35,11 @@ var _ = Describe("GetOpenSubsonicExtensions", func() {
|
|||||||
err := json.Unmarshal(w.Body.Bytes(), &response)
|
err := json.Unmarshal(w.Body.Bytes(), &response)
|
||||||
Expect(err).NotTo(HaveOccurred())
|
Expect(err).NotTo(HaveOccurred())
|
||||||
Expect(*response.Subsonic.OpenSubsonicExtensions).To(SatisfyAll(
|
Expect(*response.Subsonic.OpenSubsonicExtensions).To(SatisfyAll(
|
||||||
HaveLen(3),
|
HaveLen(4),
|
||||||
ContainElement(responses.OpenSubsonicExtension{Name: "transcodeOffset", Versions: []int32{1}}),
|
ContainElement(responses.OpenSubsonicExtension{Name: "transcodeOffset", Versions: []int32{1}}),
|
||||||
ContainElement(responses.OpenSubsonicExtension{Name: "formPost", Versions: []int32{1}}),
|
ContainElement(responses.OpenSubsonicExtension{Name: "formPost", Versions: []int32{1}}),
|
||||||
ContainElement(responses.OpenSubsonicExtension{Name: "songLyrics", Versions: []int32{1}}),
|
ContainElement(responses.OpenSubsonicExtension{Name: "songLyrics", Versions: []int32{1}}),
|
||||||
|
ContainElement(responses.OpenSubsonicExtension{Name: "indexBasedQueue", Versions: []int32{1}}),
|
||||||
))
|
))
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
+1
@@ -6,6 +6,7 @@
|
|||||||
"openSubsonic": true,
|
"openSubsonic": true,
|
||||||
"playQueue": {
|
"playQueue": {
|
||||||
"username": "",
|
"username": "",
|
||||||
|
"changed": "0001-01-01T00:00:00Z",
|
||||||
"changedBy": ""
|
"changedBy": ""
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -1,3 +1,3 @@
|
|||||||
<subsonic-response xmlns="http://subsonic.org/restapi" status="ok" version="1.16.1" type="navidrome" serverVersion="v0.55.0" openSubsonic="true">
|
<subsonic-response xmlns="http://subsonic.org/restapi" status="ok" version="1.16.1" type="navidrome" serverVersion="v0.55.0" openSubsonic="true">
|
||||||
<playQueue username="" changedBy=""></playQueue>
|
<playQueue username="" changed="0001-01-01T00:00:00Z" changedBy=""></playQueue>
|
||||||
</subsonic-response>
|
</subsonic-response>
|
||||||
|
|||||||
+22
@@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
+5
@@ -0,0 +1,5 @@
|
|||||||
|
<subsonic-response xmlns="http://subsonic.org/restapi" status="ok" version="1.16.1" type="navidrome" serverVersion="v0.55.0" openSubsonic="true">
|
||||||
|
<playQueueByIndex currentIndex="0" position="243" username="user1" changed="0001-01-01T00:00:00Z" changedBy="a_client">
|
||||||
|
<entry id="1" isDir="false" title="title" isVideo="false"></entry>
|
||||||
|
</playQueueByIndex>
|
||||||
|
</subsonic-response>
|
||||||
+12
@@ -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": ""
|
||||||
|
}
|
||||||
|
}
|
||||||
+3
@@ -0,0 +1,3 @@
|
|||||||
|
<subsonic-response xmlns="http://subsonic.org/restapi" status="ok" version="1.16.1" type="navidrome" serverVersion="v0.55.0" openSubsonic="true">
|
||||||
|
<playQueueByIndex username="" changed="0001-01-01T00:00:00Z" changedBy=""></playQueueByIndex>
|
||||||
|
</subsonic-response>
|
||||||
@@ -60,6 +60,7 @@ type Subsonic struct {
|
|||||||
// OpenSubsonic extensions
|
// OpenSubsonic extensions
|
||||||
OpenSubsonicExtensions *OpenSubsonicExtensions `xml:"openSubsonicExtensions,omitempty" json:"openSubsonicExtensions,omitempty"`
|
OpenSubsonicExtensions *OpenSubsonicExtensions `xml:"openSubsonicExtensions,omitempty" json:"openSubsonicExtensions,omitempty"`
|
||||||
LyricsList *LyricsList `xml:"lyricsList,omitempty" json:"lyricsList,omitempty"`
|
LyricsList *LyricsList `xml:"lyricsList,omitempty" json:"lyricsList,omitempty"`
|
||||||
|
PlayQueueByIndex *PlayQueueByIndex `xml:"playQueueByIndex,omitempty" json:"playQueueByIndex,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -443,7 +444,16 @@ type PlayQueue struct {
|
|||||||
Current string `xml:"current,attr,omitempty" json:"current,omitempty"`
|
Current string `xml:"current,attr,omitempty" json:"current,omitempty"`
|
||||||
Position int64 `xml:"position,attr,omitempty" json:"position,omitempty"`
|
Position int64 `xml:"position,attr,omitempty" json:"position,omitempty"`
|
||||||
Username string `xml:"username,attr" json:"username"`
|
Username string `xml:"username,attr" json:"username"`
|
||||||
Changed *time.Time `xml:"changed,attr,omitempty" json:"changed,omitempty"`
|
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"`
|
ChangedBy string `xml:"changedBy,attr" json:"changedBy"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -768,7 +768,7 @@ var _ = Describe("Responses", func() {
|
|||||||
response.PlayQueue.Username = "user1"
|
response.PlayQueue.Username = "user1"
|
||||||
response.PlayQueue.Current = "111"
|
response.PlayQueue.Current = "111"
|
||||||
response.PlayQueue.Position = 243
|
response.PlayQueue.Position = 243
|
||||||
response.PlayQueue.Changed = &time.Time{}
|
response.PlayQueue.Changed = time.Time{}
|
||||||
response.PlayQueue.ChangedBy = "a_client"
|
response.PlayQueue.ChangedBy = "a_client"
|
||||||
child := make([]Child, 1)
|
child := make([]Child, 1)
|
||||||
child[0] = Child{Id: "1", Title: "title", IsDir: false}
|
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() {
|
Describe("Shares", func() {
|
||||||
BeforeEach(func() {
|
BeforeEach(func() {
|
||||||
response.Shares = &Shares{}
|
response.Shares = &Shares{}
|
||||||
|
|||||||
Reference in New Issue
Block a user