New media added server-side was invisible while the app was open - the
client had no channel for library-change notifications, so the home
screen and library tabs sat stale until an app restart.
Each online server now runs a reconnecting websocket push channel
(Plex `/:/websockets/notifications`; Jellyfin/Emby `/socket` with the
dialect deltas, including Emby's capabilities registration), owned per
server by a LibraryEventService supervisor that follows server
online/offline/replaced-client transitions and app lifecycle. Channels
emit one coalesced LibraryChangeEvent per burst on a leading-edge
throttle modeled on Plex Web's repopulate pacing: the first settled
change surfaces immediately and a library scan's flood merges behind
it. Connection failure degrades silently to the stale-refresh paths -
bounded reconnect backoff, re-armed on every status sync.
Consumers pace their reaction through one shared RefreshPacer: burst
debounce, blocked retry while video playback or an active scroll owns
the surface, a cooldown bounding pass frequency during bulk imports,
and credit for committed pull passes so a push landing right after a
fresh load defers to the cooldown's trailing edge.
- Discover runs a debounced full pass and swaps the result in place;
the hero carousel resets only when a pass lands first content, so a
push never yanks a screen the user is reading. Pushed removals drop
from every visible list immediately via the deletion bus, scoped to
the emitting server.
- The visible library tab swaps its data in place: transaction tabs
reload without clearing, and the browse and paginated card grids
refetch their loaded span (Plex Web's repopulateRange) with the
scroll offset anchored on the first visible item and the span
clamped after alpha jumps. Hidden tabs - and tabs behind another
main tab - mark per-library staleness epochs instead and reload when
next shown; epochs are snapshotted at load start so a push racing an
in-flight fetch stays stale.
- The stale-resume and tab-shown paths refetch home hubs (previously
Continue Watching only), covering setups where the socket cannot
connect.
close#1646
Companion-remote reconnects derived their intent from a session status the peer overwrites mid-join, so a failed reconnect attempt often never rescheduled; the attempt now carries its own intent flag.
Library loads dropped retained state on failure. A delta load that failed for every requested server removed their libraries while adding nothing, and an in-place refresh where one server succeeded and another timed out replaced the whole list with only the successful response, wiping the failed server's sidebar entries even though it stayed out of the loaded set. Both paths now key retention to the servers that actually responded: unreachable servers keep their entries and are refetched on the next status emission, removed servers still drop.
Discord RPC kept the dead client after a disconnect, making every 30s reconnect a no-op for the rest of the session; disconnect now tears the client down so the timer builds a fresh one. Rapid disable/enable while a connect was still initializing also let the old client's failure dispose the fresh one, so each attempt binds its listeners and cleanup to its own client instance and stands down when it is no longer current.
hidden_libraries' constructor always seeds _initFuture, so ensureInitialized's ?? _initialize() fallback could never run (field now late final); _loadLibrariesInternal returned a bool no caller read; OfflineModeProvider.refresh() had zero call sites; ExploreScreen's Refreshable application was dispatched by nobody (only the discover tab and library tabs are dispatched); the discover null-serverId branch stays — the TV spotlight resolver passes null items during initial load.
One server with overlapping libraries (movies in both an HD and a 4K
library, shows duplicated per language) returns search results that
cannot be told apart, forcing users to open each copy to find the
right one (#1970).
Each search row now carries a source line under the summary: backend
icon, server name, and library name, shown whenever the owning server
has more than one library. Plex rows name their section inline, with
sectionKey-only rows back-filled from the loaded libraries; Jellyfin
and Emby send no library field on search hits, so search always runs
the per-library scoped fan-out that previously only served
hidden-library exclusion and stamps every hit with its library. The
Jellyfin detail lookup adds a best-effort /Items/{id}/Ancestors stamp
so full metadata stays attributed, row refreshes merge instead of
dropping the stamp, and the mapper no longer misreads SeriesStudio or
ParentId as library identity.
Episode rows trade one summary line for the source line so the
wide-thumb row keeps its height, and the badge paints slightly below
the line-box center to sit on the text's optical middle.
close#1970
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.
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.
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.
Discover and libraries now fetch only the servers that came online since
the last pass and merge the results; already-loaded servers are not
refetched. Both providers also own their online-servers listener
(registered in the constructor, removed in dispose), so profile switches
no longer leak listeners that kept disposed providers refetching.