The sheet collapsed an item to `Media.first` / `MediaSources.first` and
rendered a fixed set of rows, so split files, extra versions, per-track
properties, HDR classification and Dolby Vision were all invisible.
Model the payload the way both servers shape it — versions own parts,
parts own streams — and project every property either backend populates
onto `MediaStreamDetails`. The field set comes from sweeping both test
servers in full through the clients' own request shapes (Plex 6842
Media / 6842 Part / 39864 Stream entries, Jellyfin 6349 sources / 37763
streams / 61224 attachments), so file presence, Dolby Vision layers,
dynamic range, sample rate, spatial audio, sidecar provenance, embedded
attachments, rotation and the lyric stream type all survive. A coverage
test fails when a server key is neither carried, folded into a sibling,
nor excluded with a reason.
File Info also stopped trusting the shared `/library/metadata/{id}`
cache row: `getPlaybackExtras` writes it without `includeStreams` /
`checkFiles`, so the sheet could render with no stream table at all.
Detect that shape and refetch once under the request context captured
before the cache read, so the outgoing token and the cache namespace
stay on one profile.
Rework the layout to match: a summary chip row, then flat tonal cards
with a two-column field grid that collapses to one column on narrow
viewports, per-stream cards with flag chips, and a copyable monospace
path row. The card fill is a tonal step off the text colour rather than
the `bg` token, which is one shade from the sheet surface on OLED.
MAL/AniList season entries never matched the library show they belong to.
Both backends use the catalog title as a server-side filter before verifying
external ids, and a title like "Mushoku Tensei: Jobless Reincarnation Season 2"
cannot reach a show stored as "Mushoku Tensei: Jobless Reincarnation". Measured
against a 267-show Plex library, 3 of 113 mapped sequel entries matched.
The reverse lookup now takes two ordered title candidates instead of one: the
entry's own title and its season-stripped form, with typographic punctuation
normalised because both servers miss on a curly apostrophe. That matches 77 of
113. Widening it further to romaji/native/synonym variants reached only 81, so
the cap stays at two rather than spending up to five more requests per lookup
that finds nothing.
A sequel's year is its own season's, not the parent show's, so the +/-1 year
window is dropped for one - it would exclude the very show being looked for.
That also keeps a miss at the two requests the single-title lookup already
spent. A Plex Discover item additionally skips the title search entirely by
filtering on the plex:// guid its own rating key already is, which costs no
extra request and needs no cloud lookup.
A season 2+ entry only matches when the server really has that season, which
costs one children fetch on a match. Only a season both TVDB and TMDB agree on
is gated: which provider a library numbers its seasons by is a server setting
no dataset supplies and none of it is inferable from the ids an item exposes,
so a disagreeing reference is left ungated rather than gated on a guess.
The match cache is keyed per source and per entry rather than by canonical id,
which every season of a series shares: all five Mushoku Tensei entries collapse
to imdb:tt13293588, so one season-gated result would have poisoned the rest.
Entries whose Fribb row carries no provider id at all remain unmatched. That is
an upstream mapping gap, not something to guess around with extra lookups.
close#1704
Plezy's device profile declares every subtitle format with
`Method: External`, so Jellyfin answers PlaybackInfo with
`DeliveryMethod: External` and a `DeliveryUrl` even for streams embedded
in a direct-played container. Direct play never fetches those URLs, but
the rows kept the delivery URL as `MediaSubtitleTrack.key`, and keyed
rows only match a native track loaded from the same URL. No embedded
track could satisfy that, so `selectSubtitleTrack` reported "still
pending" forever: playback started with subtitles off and logged the
five- and thirty-second waits, and the server's default subtitle had to
be picked by hand on every item.
Restrict sidecar identity to the rows an open actually fetched as
sidecars. A row that stays in the container loses `key` and
`usesExternalDelivery` and matches on metadata again; genuine
`IsExternal` files keep theirs, and remuxed or transcoded renditions
still resolve their sidecars by URL.
Also declare every subtitle format Embed-first so a direct-played
container reports embedded delivery in the first place, and make the
pending contract match its purpose on every backend. The
complete-catalog escape is no longer Plex-only, so a Jellyfin row the
native player has not produced keeps the pass pending instead of
committing an unrelated default and retiring the listener that was
waiting for the real track. A source id absent from the catalog no
longer defers a decision that can never change, and the thirty-second
deadline resolves from what has arrived instead of re-deriving the same
deferral and applying nothing.
close#1696
Cycling backdrops reach a fallback path only once every rotating path
has failed to load, but every hero passed the rotation-agnostic backdrop
list as the rotation set and the aspect-ordered candidates as the
fallback. One servable wide backdrop was therefore enough to hide the
square background for good, so phone detail and Discover heroes
cover-fitted a 16:9 backdrop into a portrait box instead of showing the
square image Plex supplies.
Give the rotation set the same aspect-aware preference the candidate
list already has: near-square containers rotate the square background
alone and keep the backdrops behind it as fallbacks.
close#1700
Consolidates duplicated logic behind shared implementations — paginated
grid tabs, focus chrome, cached remote stores, sheet selection columns,
the server artifact store and a test fixture layer — and removes code
that had become unreachable. Net reduction of about 5,500 lines with no
behaviour change.
Where a fix had landed separately in code that moved into a shared
helper, the fix was re-applied inside the helper rather than left behind
in the copy that went away.
The search screens, the out-of-band auth dialogs, the live TV guide and the
list-download paths each carried their own copy of the same shell. Extracts
SearchInputField and PendingAuthDialog and routes the duplicated download
and guide helpers through one implementation.
Focus chrome was implemented twice, once in the focusable wrapper and once
in the focus builders; both now go through FocusChrome. TvColorPicker's
channel row was a copy of TvNumberSpinner and is now that widget in compact
density.
Also trims unused helpers and fields and simplifies the Jellyfin browse
paths.
- PaginatedCardGridTabState: the collections and playlists tabs were 95%
identical; they now supply only pageSize/fetchPage/idOf instead of each
duplicating the grid, memo, inflation budget and focus wiring.
- EtagCachedRemoteStore: the anime-lists and fribb mapping stores now share
one download/cache/isolate-parse/conditional-GET lifecycle.
- FocusableTileStateMixin manages its own initState/didUpdateWidget/dispose
instead of requiring every caller to forward three lifecycle hooks.
Also drops unused ServerCapabilities entries and dead code in
focusable_list_tile and music/track_row.
Introduces shared seams for paginated views, D-pad reorder, media control
routing, async singletons and the device method channel, then points the
open-coded copies at them.
Also removes unused models and duplicated provider/server plumbing, folds
the twice-implemented artifact store in the server, and factors the
repeated Flutter toolchain prologue in CI into a composite action.
Drops dead code across services, models, utils and widgets, including the
connection auth service, which had no implementer, and the Live TV DVR
provisioning models, which had no caller.
Tests that only covered deleted behaviour are removed or trimmed. No
behaviour change.
Un-gates music libraries; adds CardShape.square plumbing (circular
artists), artists/albums/tracks browse groupings with per-grouping
sorts, audio playlists tab, artist + album detail screens with dpad
track rows, music context-menu actions, and a stub MusicPlaybackService
seam for the upcoming engine.
The aired-order rework can sweep correctly-placed Specials into "download
next N"; this toggle lets users opt out. Shown only for whole shows
(reusing FocusableSwitchListTile via an optional toggle on the shared
option-picker dialog), remembered across opens via a BoolPref
(default on = unchanged behavior). Filters Specials at the single
collect choke point (_collectPlayable), with a guard so explicitly
downloading the Specials season still queues its episodes.
Extends the Plex play-queue fix (#1416) to the shared episode comparator
so Specials interleave by air date everywhere, not just Plex streaming:
offline next/prev, the Jellyfin online queue, offline OnDeck, and the
download/sync "next N" selection. Undated episodes fall back to
Specials-last, preserving the #1414 "never front-load the Specials
folder" guarantee. Jellyfin queue now requests PremiereDate.