Files
silo-server/internal/jellycompat/query_repeated_fields_test.go
T
c5f21cb10d fix(jellycompat): parse repeated Fields query params (#110)
* fix(jellycompat): parse repeated Fields query params

parseItemsQuery read the Fields parameter via q.Get("Fields"), which
returns only the first value when a client sends Fields as repeated
query params (Fields=A&Fields=B&...) instead of comma-separated in a
single param (Fields=A,B,C).

The jellyfin-sdk-kotlin (used by Wholphin) sends repeated params. When
such a request listed a detail-only field like MediaSources after other
fields — e.g. the episode-playlist request
  /Shows/{id}/Episodes?Fields=PrimaryImageAspectRatio&...&Fields=MediaSources&...
silo saw only the first value (PrimaryImageAspectRatio), so
needsDetailFields stayed false, the request took the list path, and the
response came back without MediaSources. Clients then could not start
playback of the returned episodes ("no media sources").

Join all repeated Fields values before splitting on commas so field
order and delimiter style no longer matter. Comma-separated single-param
clients (e.g. VidHub) are unaffected.

* fix(jellycompat): stop advertising CanDownload and stub ThemeSongs

Wholphin (jellyfin-sdk-kotlin) audit surfaced two reachable gaps:

- mapping.go set CanDownload=true on every playable item while no
  /Items/{id}/Download route exists, sending clients that honor the flag
  (e.g. Wholphin's screensaver/slideshow) into 404s. Advertise false until
  a download route exists.
- GET /Items/{id}/ThemeSongs 404'd, so enabling theme songs in Wholphin
  silently failed on every detail page. Stub it with an empty
  ThemeMediaResult. This cannot reuse the generic item stub: the SDK
  models OwnerId as non-nullable, so the response must include it even
  when empty.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* docs(architecture): add Wholphin endpoint coverage audit

Cross-references every Jellyfin endpoint the Wholphin client can call
against the routes jellycompat serves, with gating evidence for each
missing-but-unreachable endpoint and prioritized recommendations.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Quick <31828688+Quick104@users.noreply.github.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-06-09 20:42:46 -04:00

53 lines
2.2 KiB
Go

package jellycompat
import (
"net/http/httptest"
"testing"
)
// TestRepeatedFieldsParamTriggersDetail covers clients (e.g. the
// jellyfin-sdk-kotlin / Wholphin) that send Fields as repeated query params
// (Fields=A&Fields=B&...) rather than comma-separated in a single param.
// q.Get("Fields") returns only the first repeated value, which silently
// dropped every field after the first — so a request whose first Fields entry
// is a list-servable field but which also asks for a detail-only field (e.g.
// MediaSources) later never triggered the detail path and came back without
// MediaSources. parseItemsQuery must join all repeated values before splitting.
func TestRepeatedFieldsParamTriggersDetail(t *testing.T) {
url := "/Shows/abc/Episodes?Fields=PrimaryImageAspectRatio&Fields=SeasonUserData" +
"&Fields=ChildCount&Fields=Overview&Fields=Trickplay&Fields=SortName" +
"&Fields=Chapters&Fields=MediaSources&Fields=MediaSourceCount" +
"&Fields=ParentId&Fields=CanDelete&startItemId=x&limit=100"
req := httptest.NewRequest("GET", url, nil)
query := parseItemsQuery(req, NewResourceIDCodec())
if !query.needsDetailFields {
t.Fatal("repeated Fields params with a detail-only field not-first must set needsDetailFields=true")
}
if !query.requestedFields["mediasources"] {
t.Error("mediasources must be parsed from repeated Fields params")
}
if !query.requestedFields["chapters"] {
t.Error("chapters must be parsed from repeated Fields params")
}
if !query.fieldsExplicit {
t.Error("fieldsExplicit must be true when repeated Fields params are present")
}
}
// TestCommaSeparatedFieldsStillParsed guards the original single-param,
// comma-separated form (e.g. VidHub: Fields=A,B,C) which must keep working.
func TestCommaSeparatedFieldsStillParsed(t *testing.T) {
req := httptest.NewRequest("GET", "/Items?Fields=Overview,MediaSources,People", nil)
query := parseItemsQuery(req, NewResourceIDCodec())
if !query.needsDetailFields {
t.Fatal("comma-separated Fields with MediaSources must set needsDetailFields=true")
}
for _, f := range []string{"overview", "mediasources", "people"} {
if !query.requestedFields[f] {
t.Errorf("field %q must be parsed from comma-separated Fields", f)
}
}
}