mirror of
https://github.com/navidrome/navidrome.git
synced 2026-08-31 07:30:32 +00:00
fix(jellyfin): honor repeated Fields query params (#5811)
The Fields param was read with StringOr, which keeps only one value, so a request sending it as repeated params (Fields=Genres&Fields=MediaSources) — as Finamp and Feishin do — lost all but the first. Field-gated data like MediaSources was then omitted for Audio items even though the client asked for it; the comma-separated form happened to work because ParseFields splits on commas. Real Jellyfin accepts both forms. ParseFields is now variadic and a parseFields helper reads every repeated value via req.Values.Strings, applied to the item list, single-item, and playlist endpoints.
This commit is contained in:
parent
09ac342f5a
commit
1e82f515c4
@ -7,12 +7,15 @@ import "strings"
|
||||
// omits those unless the client asks for them.
|
||||
type Fields map[string]struct{}
|
||||
|
||||
// ParseFields splits the comma-separated Fields param into a lowercased set.
|
||||
func ParseFields(csv string) Fields {
|
||||
// ParseFields builds a lowercased set from the Fields param. It accepts each value comma-separated
|
||||
// (Fields=a,b) and across repeated params (Fields=a&Fields=b), both of which real Jellyfin honors.
|
||||
func ParseFields(values ...string) Fields {
|
||||
f := Fields{}
|
||||
for name := range strings.SplitSeq(csv, ",") {
|
||||
if name = strings.TrimSpace(strings.ToLower(name)); name != "" {
|
||||
f[name] = struct{}{}
|
||||
for _, csv := range values {
|
||||
for name := range strings.SplitSeq(csv, ",") {
|
||||
if name = strings.TrimSpace(strings.ToLower(name)); name != "" {
|
||||
f[name] = struct{}{}
|
||||
}
|
||||
}
|
||||
}
|
||||
return f
|
||||
|
||||
26
server/jellyfin/dto/fields_test.go
Normal file
26
server/jellyfin/dto/fields_test.go
Normal file
@ -0,0 +1,26 @@
|
||||
package dto
|
||||
|
||||
import (
|
||||
. "github.com/onsi/ginkgo/v2"
|
||||
. "github.com/onsi/gomega"
|
||||
)
|
||||
|
||||
var _ = Describe("ParseFields", func() {
|
||||
It("parses a single comma-separated value", func() {
|
||||
f := ParseFields("Genres,MediaSources")
|
||||
Expect(f.Has("Genres")).To(BeTrue())
|
||||
Expect(f.Has("MediaSources")).To(BeTrue())
|
||||
})
|
||||
|
||||
It("parses fields spread across repeated params", func() {
|
||||
f := ParseFields("Genres", "MediaSources", "SortName")
|
||||
Expect(f.Has("Genres")).To(BeTrue())
|
||||
Expect(f.Has("MediaSources")).To(BeTrue())
|
||||
Expect(f.Has("SortName")).To(BeTrue())
|
||||
})
|
||||
|
||||
It("returns an empty set for no values", func() {
|
||||
Expect(ParseFields()).To(BeEmpty())
|
||||
Expect(ParseFields("")).To(BeEmpty())
|
||||
})
|
||||
})
|
||||
@ -62,6 +62,16 @@ var _ = Describe("Browsing", func() {
|
||||
}
|
||||
})
|
||||
|
||||
// Clients (Finamp, Feishin) send Fields as repeated params rather than one comma-separated
|
||||
// value; real Jellyfin accepts both, so a later Fields=MediaSources must still take effect.
|
||||
It("honors MediaSources when Fields is sent as repeated params", func() {
|
||||
q := queryResult(get("/Items?IncludeItemTypes=Audio&Recursive=true&Fields=Genres&Fields=MediaSources"))
|
||||
Expect(q.Items).ToNot(BeEmpty())
|
||||
for _, it := range q.Items {
|
||||
Expect(it.MediaSources).To(HaveLen(1))
|
||||
}
|
||||
})
|
||||
|
||||
It("lists all album artists", func() {
|
||||
q := queryResult(get("/Items?IncludeItemTypes=MusicArtist&Recursive=true"))
|
||||
Expect(q.TotalRecordCount).To(Equal(4))
|
||||
|
||||
@ -224,13 +224,20 @@ type itemsQuery struct {
|
||||
albumIds []string
|
||||
}
|
||||
|
||||
// parseFields reads the Fields param, accepting both repeated params (Fields=a&Fields=b) and a
|
||||
// comma-separated value (Fields=a,b), matching real Jellyfin. StringOr would keep only one value.
|
||||
func parseFields(p *req.Values) dto.Fields {
|
||||
values, _ := p.Strings("fields")
|
||||
return dto.ParseFields(values...)
|
||||
}
|
||||
|
||||
// parseItemsQuery also resolves the entity types (inferring them from the parent when
|
||||
// IncludeItemTypes is absent) and the library scope. Query keys are read lowercase because
|
||||
// normalizeQueryKeys folded them (Jellyfin binds case-insensitively).
|
||||
func (api *Router) parseItemsQuery(ctx context.Context, r *http.Request) itemsQuery {
|
||||
p := req.Params(r)
|
||||
q := itemsQuery{
|
||||
fields: dto.ParseFields(p.StringOr("fields", "")),
|
||||
fields: parseFields(p),
|
||||
ids: decodedQueryIDs(r, "ids"),
|
||||
rawTypes: p.StringOr("includeitemtypes", ""),
|
||||
search: searchTerm(p),
|
||||
@ -730,7 +737,7 @@ func (api *Router) itemsByIDs(ctx context.Context, ids []string, fields dto.Fiel
|
||||
|
||||
func (api *Router) getItem(w http.ResponseWriter, r *http.Request) {
|
||||
id := api.resolveItemID(r.Context(), dto.DecodeID(chi.URLParam(r, "itemId")))
|
||||
fields := dto.ParseFields(req.Params(r).StringOr("fields", ""))
|
||||
fields := parseFields(req.Params(r))
|
||||
if item, ok := api.resolveItemByID(r.Context(), id, fields); ok {
|
||||
api.ok(w, r, item)
|
||||
return
|
||||
|
||||
@ -191,7 +191,7 @@ func (api *Router) getPlaylistItems(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
p := req.Params(r)
|
||||
fields := dto.ParseFields(p.StringOr("fields", ""))
|
||||
fields := parseFields(p)
|
||||
res, err := api.playlistTrackPage(repo, fields, p.IntOr("startindex", 0), p.IntOr("limit", 0))
|
||||
if err != nil {
|
||||
api.internalError(w, r, err)
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user