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
73 lines
2.9 KiB
Dart
73 lines
2.9 KiB
Dart
import 'ids.dart';
|
|
|
|
/// One coalesced "this server's library content changed" signal from a
|
|
/// server push channel (#1646).
|
|
///
|
|
/// Deliberately coarse for additions and updates: raw notifications arrive
|
|
/// mid-scan when backends disagree about what an id even means (Plex emits
|
|
/// per-state timeline floods, MediaBrowser batches server-side), and every
|
|
/// consumer responds by refetching its own views anyway. The one exact
|
|
/// carve-out is [removedItemIds]: a removal names an item the app may be
|
|
/// displaying right now, so those ids ride along for in-place drops through
|
|
/// `DeletionNotifier` — the same split Plex Web uses (deletions patch stores
|
|
/// directly; additions go through container refetch).
|
|
class LibraryChangeEvent {
|
|
/// Server whose library changed.
|
|
final ServerId serverId;
|
|
|
|
/// Backend-native ids of the affected libraries (Plex section ids,
|
|
/// MediaBrowser collection-folder ids). Empty when the backend did not say.
|
|
final Set<String> libraryIds;
|
|
|
|
/// Backend-native ids of removed items, when the backend named them (Plex
|
|
/// settled `type: -1` timeline entries, MediaBrowser `ItemsRemoved`).
|
|
final Set<String> removedItemIds;
|
|
|
|
/// Advisory flags describing what the change contained. A backend that
|
|
/// cannot distinguish (Plex settles adds and metadata updates identically)
|
|
/// sets the closest superset.
|
|
final bool itemsAdded;
|
|
final bool itemsRemoved;
|
|
final bool itemsUpdated;
|
|
|
|
const LibraryChangeEvent({
|
|
required this.serverId,
|
|
this.libraryIds = const {},
|
|
this.removedItemIds = const {},
|
|
this.itemsAdded = false,
|
|
this.itemsRemoved = false,
|
|
this.itemsUpdated = false,
|
|
});
|
|
|
|
bool get hasChanges => itemsAdded || itemsRemoved || itemsUpdated;
|
|
|
|
@override
|
|
String toString() =>
|
|
'LibraryChangeEvent($serverId, libraries: $libraryIds, '
|
|
'added: $itemsAdded, removed: $itemsRemoved (${removedItemIds.length} ids), updated: $itemsUpdated)';
|
|
}
|
|
|
|
/// A live push subscription to one server's library-change notifications.
|
|
///
|
|
/// Implementations own the websocket, keepalive, parsing, per-backend
|
|
/// debouncing, and reconnect backoff; [LibraryEventService] owns *which*
|
|
/// channels run (server online/offline, app foreground/background). Connect
|
|
/// failures degrade silently — the stale-refresh paths remain the fallback —
|
|
/// so a reverse proxy without websocket upgrade support must never surface
|
|
/// an error to the user.
|
|
abstract class LibraryEventChannel {
|
|
/// Coalesced change events. Broadcast; never errors.
|
|
Stream<LibraryChangeEvent> get events;
|
|
|
|
/// Begin connecting (idempotent while running). Failures retry with
|
|
/// bounded backoff, then go quiet until the next [start].
|
|
void start();
|
|
|
|
/// Drop the connection and all retry state. Safe to call repeatedly;
|
|
/// [start] may be called again afterwards.
|
|
void stop();
|
|
|
|
/// [stop] plus closing [events]. The channel is unusable afterwards.
|
|
void dispose();
|
|
}
|