dd81a7ef set CanDownload=false to stop Wholphin's screensaver from
404ing on the nonexistent /Items/{id}/Download route — but the flag is
load-bearing for Infuse, which refuses Direct Play (Static=true
streaming) of items it believes it cannot download. With omitempty the
field vanished from the JSON entirely and Infuse playback broke, while
PlaybackInfo-negotiating clients were unaffected.
Resolve the underlying inconsistency instead of trading one client for
the other: implement GET/HEAD /Items/{id}/Download serving the original
file (range support, Content-Disposition, optional mediaSourceId for
multi-version items) under stream-group auth, and restore
CanDownload=true now that the route exists. Fixes Infuse playback and
keeps Wholphin's download callers working.
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
9.4 KiB
9.4 KiB
Wholphin → jellycompat Endpoint Coverage Report
Date: 2026-06-09
Scope: every Jellyfin server endpoint the Wholphin Android TV client (jellyfin-sdk-kotlin based) can call, cross-referenced against the routes registered in internal/jellycompat/router.go. Wholphin source analyzed from a local clone of damontecres/Wholphin (app/src/main). Commands assume the repository root is the cwd.
Wholphin's Seerr integration (/api/v1/... discover/request endpoints) targets a separate Seerr server, not Jellyfin, and is out of scope here.
Methodology
- Grepped Wholphin for every
*Api.method()SDK call (itemsApi,playStateApi, etc.), websocket subscriptions, and raw URL construction; mapped each to its HTTP endpoint per the Jellyfin OpenAPI spec. - Inventoried every route registered in
internal/jellycompat/router.go, including path normalization (path_normalization.gohandles casing and/emby//jellyfinprefixes). - For each endpoint Wholphin uses but silo does not serve, checked whether a server-side gate (user policy flag, DTO field) prevents the client from ever calling it.
Summary
- Covered (full): 22 endpoint families — everything on the core path: auth, browse, item detail, episodes/next-up, resume, playback info, direct/HLS streaming, subtitles, progress reporting, favorites/played, images, genres, persons, suggestions, media segments (skip intro), display preferences.
- Covered (partial/stub): 4 — websocket (keep-alive only, no remote control),
/Studios,/Users/Public,/QuickConnect/Enabled(deliberatefalse). - Missing but safely gated: 8 — the client never calls them because we don't advertise the capability (delete, subtitle download, trickplay, local trailers, additional parts, quick connect auth, live TV, playlists).
- Missing and reachable: 4 — theme songs, universal audio stream, item download, client log upload. These return 404 when the client calls them. None block video playback.
Bottom line: the entire core video path is covered. The reachable gaps are quality-of-life features (theme songs, photo screensaver, debug log upload) plus one advertised-but-broken inconsistency (CanDownload=true with no /Items/{id}/Download route).
Coverage matrix
Fully covered
| Wholphin call | Endpoint | silo handler |
|---|---|---|
userApi.authenticateUserByName |
POST /Users/AuthenticateByName |
handlers_auth.go |
userApi.getCurrentUser |
GET /Users/Me |
handlers_auth.go |
userApi.getUserById |
GET /Users/{id} |
handlers_auth.go |
systemApi.getPublicSystemInfo |
GET /System/Info/Public |
handlers_system.go |
sessionApi.postCapabilities |
POST /Sessions/Capabilities |
handlers_playback.go |
itemsApi.getItems |
GET /Items |
handlers_items.go |
itemsApi.getResumeItems |
GET /UserItems/Resume (+ legacy /Users/{id}/Items/Resume) |
handlers_items.go |
userLibraryApi.getItem |
GET /Items/{id} (+ legacy user-scoped form) |
handlers_items.go |
tvShowsApi.getEpisodes |
GET /Shows/{id}/Episodes |
handlers_items.go |
tvShowsApi.getNextUp |
GET /Shows/NextUp |
handlers_items.go |
suggestionsApi.getSuggestions |
GET /Items/Suggestions |
handlers_items.go |
genresApi.getGenres |
GET /Genres |
handlers_items.go |
personsApi.getPersons |
GET /Persons |
handlers_persons.go (full when PersonRepo is wired; empty stub otherwise) |
searchApi.getSearchHints |
GET /Search/Hints |
handlers_items.go (Wholphin's call is currently commented out upstream) |
mediaInfoApi.getPostedPlaybackInfo |
POST /Items/{id}/PlaybackInfo |
handlers_playback.go |
videosApi.getVideoStreamUrl |
GET/HEAD /Videos/{id}/stream(.{container}) |
streams.go |
| (transcode path from PlaybackInfo) | GET /Videos/{id}/master.m3u8 + HLS playlist/segments |
streams.go |
| (subtitle delivery URLs) | GET /Videos/{id}/{source}/Subtitles/{idx}/stream.{fmt} |
streams.go |
playStateApi.reportPlaybackStart/Progress/Stopped |
`POST /Sessions/Playing[/Progress | /Stopped]` |
playStateApi.markPlayedItem/markUnplayedItem |
POST/DELETE /UserPlayedItems/{id} (+ legacy) |
handlers_userdata.go |
userLibraryApi.markFavoriteItem/unmarkFavoriteItem |
POST/DELETE /UserFavoriteItems/{id} (+ legacy) |
handlers_userdata.go |
mediaSegmentsApi.getItemSegments |
GET /MediaSegments/{id} |
handlers_items.go (skip intro/credits) |
displayPreferencesApi.updateDisplayPreferences |
GET/POST /DisplayPreferences/{id} |
handlers_displayprefs.go |
imageApi.getItemImageUrl |
GET /Items/{id}/Images/{type}[/{index}] |
handlers_images.go |
imageApi.getUserImageUrl |
GET /Users/{id}/Images/Primary |
handlers_images.go (placeholder avatar) |
Partial / stub (graceful degradation)
| Endpoint | Status | Client impact |
|---|---|---|
GET /socket (websocket) |
Accepts upgrade, answers KeepAlive only (handlers_websocket.go) |
Wholphin subscribes to GeneralCommandMessage and PlaystateMessage for server-initiated remote control. Connection stays healthy; remote-control commands are simply never delivered. No errors, feature inert. |
GET /Studios |
Empty-array stub | Studio browse rows render empty. |
GET /Users/Public |
Empty-array stub | No user tiles on the login screen; manual login works. |
GET /QuickConnect/Enabled |
Hardcoded false |
Deliberate: Wholphin hides Quick Connect UI, so the unimplemented POST /QuickConnect/Authorize and POST /Users/AuthenticateWithQuickConnect are never called. |
Missing but safely gated (client never calls them)
| Endpoint | Why it's unreachable |
|---|---|
DELETE /Items/{id} |
We return CanDelete=false per item (mapping.go) and EnableContentDeletion=false in user policy (handlers_auth.go), so Wholphin hides delete UI. |
POST /Items/{id}/RemoteSearch/Subtitles/{subtitleId} |
EnableSubtitleManagement=false in user policy hides subtitle search/download. |
GET /Videos/{id}/Trickplay/... |
We never emit trickplay metadata on item DTOs, so the scrubber never requests tiles. |
GET /Items/{id}/LocalTrailers |
Wholphin gates on localTrailerCount > 0 (TrailerService.kt); we never set LocalTrailerCount. |
GET /Videos/{id}/AdditionalParts |
Gated on partCount > 1 (PlaylistCreator.kt); we never set PartCount. |
| Quick Connect auth endpoints | Gated by /QuickConnect/Enabled = false. |
Live TV suite (/LiveTv/Channels, /Programs, /Recordings, timers) |
We expose no live-TV views or channels, so the feature never activates. |
Playlists (GET/POST/DELETE /Playlists/{id}/Items) |
We expose no playlist items in the catalog, so playlist screens are unreachable. Becomes a real gap the day silo grows playlists. |
Missing and reachable (return 404 today)
| Endpoint | Wholphin caller | Impact | Severity |
|---|---|---|---|
GET /Items/{id}/ThemeSongs |
ThemeSongPlayer.kt |
ThemeMediaResult (including the OwnerId field jellyfin-sdk-kotlin requires). |
Resolved |
GET /Audio/{id}/universal |
ThemeSongPlayer.kt, MusicService.kt |
Audio streaming for theme songs and music playback. No music libraries are exposed today, but this is the second half of the theme-song path and the blocker for any future audio support in jellycompat. | Low (today) |
GET /Items/{id}/Download |
SlideshowViewModel.kt, ScreensaverService.kt |
CanDownload=true while the route 404s./Items/{id}/Download route now serves the original file (streams.go), and CanDownload=true again. The earlier CanDownload=false workaround broke Infuse, which requires the flag for Direct Play. |
Resolved |
POST /ClientLog/Document |
MediaReportService.kt, DebugPage.kt |
"Upload logs to server" debug action fails. | Low |
GET /Audio/{id}/Lyrics |
NowPlayingViewModel.kt |
Music-only; unreachable until audio libraries exist. | Low |
Recommendations (priority order)
- Merge the repeated-
Fieldsfix (PR #110).parseItemsQuerystill readsq.Get("Fields")(internal/jellycompat/query.go), which truncates jellyfin-sdk-kotlin's repeated query params and breaks Wholphin episode auto-advance. This is the only covered endpoint with a known correctness bug for this client. Follow up by sweeping the remaining single-value reads (Ids,GenreIds,PersonIds,Filters,SortBy,SortOrder) toq.Values, matching howIncludeItemTypes/MediaTypes/ImageTypesare already handled. Resolve the— done 2026-06-09: realCanDownloadinconsistency/Items/{id}/Downloadroute added;CanDownloadstaystrue(Infuse requires it for Direct Play).Stub— done 2026-06-09: emptyGET /Items/{id}/ThemeSongsThemeMediaResultstub registered.- Websocket server-push (remote control: server-initiated pause/stop, display messages) — tracked in issue #122.
- Optional, later:
POST /ClientLog/Documentaccept-and-discard stub and real/Studiosdata. Playlists, lyrics, and universal audio only matter once silo exposes those content types through jellycompat.
Notes
- Path normalization (
path_normalization.go) already handles Wholphin's casing and any/emby//jellyfinprefixes, so none of the covered routes are at risk from URL-shape differences. - Unmatched jellycompat paths fall through to chi's default 404 with no body; jellyfin-sdk-kotlin surfaces these as
InvalidStatusException, which Wholphin generally catches per-feature.