The Discover tab fanned out its whole request set twice on every cold
start and replayed slow rows on a shrinking timeout ladder, so a healthy
remote server produced anywhere from 4s to 15s of loading.
Measured against a remote Jellyfin server with four libraries, 24
interleaved cold-start samples per side:
requests 19 -> 9 payload 219 KB -> 94 KB
settled 5231ms -> 2502ms median, 13222ms -> 5927ms p95
Four independent causes:
- Retry policy. `Client.send` resolves on response headers, so the
connect budget covers the server's think time and a slow-but-alive
query raises `connectionTimeout`. Replaying it made the server re-run
the query with a shorter budget than the one it just missed; the
`[10s, 8s, 5s]` ladder turned an 11s answer into an empty row after
23s. Hub surfaces now get one whole-request deadline, retry only
immediate connection errors, and the deadline bounds the whole call
including the request still in flight.
- Request shape. `/Items/Latest` groups a TV library by series, so its
rows are Series folder dtos and `RecursiveItemCount`/`ChildCount` cost
a DB count each, per row. Hub rows now ask for `Overview` only; watch
state survives because Jellyfin derives `UserData.Played` from
`UnplayedItemCount` when the count fields are absent. `/Shows/NextUp`
sends `NextUpDateCutoff` to bound the server's series-key scan, and
`Thumb` leaves `EnableImageTypes` since nothing reads it. `UserData`
and `PremiereDate` leave the browse set: neither is an `ItemFields`
member, so the server dropped them anyway.
- Fan-out. Per-library hubs ran in batches of three separated by a
barrier, so one slow library stalled every library behind it. A
sliding window keeps the same peak concurrency without head-of-line
blocking. Concurrent `fetchLibraries` calls now share one `/Views`
instead of racing two identical round trips, Plex's global and music
hub legs start together, and Jellyfin gets Plex's pool tuning.
- Duplicate pass. `DiscoverScreen.initState` starts a load and the
online-entry hook asked for a full refresh on top of it, which
`CoalescedLoadCoordinator` correctly queued as a trailing pass. The
hook now calls `primeRefresh`, which rides along with a load already
in flight; profile switches still go through `fullRefresh`.
Refs #1784
Jellyfin search rows do not expose their owning collection, so hidden libraries cannot be filtered after the response. Scope searches to visible libraries, stamp the returned rows, reuse the latest loaded views, propagate cancellation, and fail closed when views cannot be loaded.
Keep full candidate budgets and split music libraries into parallel album, audio, and artist requests. Album requests disable UserData and use the existing album field set to avoid recursive per-album work from #1552; audio requests retain cheap leaf play state.
close#1770
searchAcrossServers was the only aggregation entry point without a
hiddenLibraryKeys parameter, so libraries hidden from home hubs, Continue
Watching and the library rail still surfaced their contents in the Search
tab. Thread the profile's hidden keys from SearchScreen through to the
aggregation, and drop matching items between the fan-out and the ranking
pass so hidden hits cannot spend the result limit and shrink what is
shown. Items the backend cannot attribute to a library, such as Plex
shared and external media, are kept.
The screen re-runs the visible query when a library is hidden or unhidden
while results are on screen. Its listener is attached only after the
provider has hydrated, so the initial load notification cannot race the
first query into running twice.
Plex search rows now go through the library-aware tagger, so a response
that names its section only via librarySectionKey or
targetLibrarySectionID is still attributable, and therefore filterable.
Jellyfin search results carry no library id at all: the mapper's
ParentLibraryId is not a Jellyfin field, and ParentId resolves to a
season or physical folder rather than a CollectionFolder. Filtering there
needs server-side ParentId scoping and is left for a follow-up.
close#1770
close#1552
Latest Albums returns MusicAlbum folder dtos, where the browse count/user-data
fields each cost a recursive per-album COUNT query; request it with the slim
album fields + EnableUserData=false under a dedicated latestalbums identifier,
and drop the folder fields and Overview from the played-track rows.
Album/artist downloads expand to tracks with pinned parent metadata,
aggregate progress, container deletes with reference-counted album
covers, and a Music tab on the downloads screen with fully offline
album playback. Home rows include music libraries (fixes plex hub
items being filtered to video types) and audio playlists join
download/sync rules.
Signing in triggered two back-to-back profile rebinds; the second re-added
the same Jellyfin connection, which tore down the live client and aborted
the home screen's in-flight fetches. The aborted pass was committed as
loaded-empty, flashing 'no content available' until the follow-up load
landed. Fix at the root instead of patching the sign-in window:
- addJellyfinConnection now reuses the live client when the connection is
unchanged (token, deviceId, URL set), matching the existing Plex
refreshTokensForProfile behavior; material changes still recreate it.
- Cancelled requests are classified end-to-end: the client's
treat-as-empty helpers rethrow cancellations, and the aggregation
fan-outs report cancelledServerIds alongside succeededServerIds.
- A fetch pass in which zero servers succeeded is never authoritative:
it keeps existing content instead of wiping it (also fixes the
pre-existing blanking of home/sidebar on a totally failed refresh),
stays in loading while disrupted (cancellation or binding in flight),
and only commits loaded-empty on a settled failure.