perf(detail): paint a show before its on-deck episode is looked up
Jellyfin has no equivalent of Plex's bundled `?includeOnDeck=1`, so a show detail open chained `/Shows/NextUp` behind the item fetch and the screen sat on a spinner for both round trips. The second one is not needed to paint: everything except the play button's episode label comes from the item. `fetchItemWithOnDeck` now takes an `onItemReady` callback and invokes it as soon as the item is known, when that is strictly before on-deck settles. Plex returns both together and never invokes it. Phone and desktop only. TV keeps its own reveal gate — `_isTvDetailReadyToReveal` holds the foreground at opacity 0 until extras, related hubs, seasons and the first episode page have all loaded, and those still run after the on-deck lookup settles, so TV sees no change. Both halves are pinned by tests. Measured on a remote Jellyfin server, 15 interleaved show-detail opens per version: time to content 1264ms -> 1042ms (-18%), with the rest of the load unchanged. Seasons and extras deliberately still start after the whole lookup settles. Starting them at the early paint measured worse (time to settled +21%) because they contend with the on-deck request instead of overlapping it — the same reason `/Shows/NextUp` is not fired in parallel with the item fetch. That trade-off is also why TV was left alone rather than being unblocked by moving those loads earlier. Two ordering hazards the early paint introduces, both covered by `media_detail_screen_test.dart`: - The early call must not write on-deck. `_loadFullMetadata` runs again after playback, and clearing there would blank the play button for the length of the round trip. `onDeckSettled` marks the authoritative write, so a reload that finds the series finished still clears it. - A settled empty on-deck must not drop the episode-derived fallback that `_ensureFallbackOnDeckEpisode` supplies. close #1784
This commit is contained in:
@@ -613,17 +613,22 @@ mixin _JellyfinBrowseMethods on _JellyfinClientInternals {
|
||||
/// OnDeck semantics: returns the resume episode when one exists, or S1E1
|
||||
/// when the user hasn't started. Movies and other kinds short-circuit.
|
||||
///
|
||||
/// Deliberately still chained rather than fired in parallel off a caller
|
||||
/// kind hint: measured against a remote server that saved nothing, because
|
||||
/// the two requests contend rather than overlap (`/Shows/NextUp` went from
|
||||
/// 380ms alone to 1395ms beside the detail fetch), and it would cost a
|
||||
/// wasted request on every movie whose hint was absent or wrong.
|
||||
/// The chain stays sequential — firing both together measured no better,
|
||||
/// because the requests contend rather than overlap (`/Shows/NextUp` went
|
||||
/// from 380ms alone to 1395ms beside the detail fetch) and a movie would pay
|
||||
/// for a request it can never use. Instead the item is handed to
|
||||
/// [onItemReady] the moment it lands, so the caller can paint without
|
||||
/// waiting for the on-deck round trip (#1784).
|
||||
@override
|
||||
Future<({MediaItem? item, MediaItem? onDeckEpisode})> fetchItemWithOnDeck(String id) async {
|
||||
Future<({MediaItem? item, MediaItem? onDeckEpisode})> fetchItemWithOnDeck(
|
||||
String id, {
|
||||
void Function(MediaItem item)? onItemReady,
|
||||
}) async {
|
||||
final item = await fetchItem(id);
|
||||
if (item == null || item.kind != MediaKind.show) {
|
||||
return (item: item, onDeckEpisode: null);
|
||||
}
|
||||
onItemReady?.call(item);
|
||||
final nextUp = await _safeFetchItemsArray('/Shows/NextUp', {
|
||||
'seriesId': id,
|
||||
'userId': connection.userId,
|
||||
|
||||
@@ -3769,10 +3769,14 @@ class PlexClient
|
||||
}
|
||||
|
||||
/// Full item with on-deck episode from a single `/library/metadata/{id}`
|
||||
/// round-trip. Implements [MediaServerClient.fetchItemWithOnDeck];
|
||||
/// Jellyfin has no analogous endpoint and returns onDeck=null there.
|
||||
/// round-trip. Implements [MediaServerClient.fetchItemWithOnDeck]. Both
|
||||
/// halves arrive together, so there is no window in which the item is known
|
||||
/// and on-deck is not — `onItemReady` is intentionally never invoked.
|
||||
@override
|
||||
Future<({MediaItem? item, MediaItem? onDeckEpisode})> fetchItemWithOnDeck(String id) async {
|
||||
Future<({MediaItem? item, MediaItem? onDeckEpisode})> fetchItemWithOnDeck(
|
||||
String id, {
|
||||
void Function(MediaItem item)? onItemReady,
|
||||
}) async {
|
||||
try {
|
||||
final result = await getMetadataWithImagesAndOnDeck(id, shouldFallback: _shouldFallbackPlexItemLookup);
|
||||
final itemDto = result['metadata'] as PlexMetadataDto?;
|
||||
|
||||
Reference in New Issue
Block a user