perf(home): load the home screen once instead of twice per cold start
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
This commit is contained in:
@@ -453,29 +453,35 @@ class DataAggregationService {
|
||||
final serverLibraries = libraries?[serverId];
|
||||
final shouldUseGlobalHubs = useGlobalHubs && client.capabilities.richHubs;
|
||||
final hubItemLimit = limit ?? defaultHubPreviewLimit;
|
||||
final hubs = shouldUseGlobalHubs
|
||||
? [
|
||||
...await client.fetchGlobalHubs(limit: hubItemLimit, includePlaybackHubs: includePlaybackHubs),
|
||||
// Plex's promoted/global hub endpoint never includes music
|
||||
// libraries — append their per-library hubs so music rows
|
||||
// reach home. No-op (zero extra calls) without a visible
|
||||
// music library.
|
||||
...await _fetchLibraryHubsForClient(
|
||||
client,
|
||||
limit: hubItemLimit,
|
||||
hiddenLibraryKeys: hiddenLibraryKeys,
|
||||
includePlaybackHubs: includePlaybackHubs,
|
||||
libraries: serverLibraries ?? const [],
|
||||
kinds: const {MediaKind.artist},
|
||||
),
|
||||
]
|
||||
: await _fetchLibraryHubsForClient(
|
||||
client,
|
||||
limit: hubItemLimit,
|
||||
hiddenLibraryKeys: hiddenLibraryKeys,
|
||||
includePlaybackHubs: includePlaybackHubs,
|
||||
libraries: useGlobalHubs ? serverLibraries : null,
|
||||
);
|
||||
List<MediaHub> hubs;
|
||||
if (shouldUseGlobalHubs) {
|
||||
// Both legs are independent, so start them before awaiting either.
|
||||
// Spreading `...await a, ...await b` into one list literal evaluates
|
||||
// them in order, which serialised the music rows behind the global
|
||||
// hub round trip.
|
||||
final globalFuture = client.fetchGlobalHubs(limit: hubItemLimit, includePlaybackHubs: includePlaybackHubs);
|
||||
// Plex's promoted/global hub endpoint never includes music
|
||||
// libraries — append their per-library hubs so music rows
|
||||
// reach home. No-op (zero extra calls) without a visible
|
||||
// music library.
|
||||
final musicFuture = _fetchLibraryHubsForClient(
|
||||
client,
|
||||
limit: hubItemLimit,
|
||||
hiddenLibraryKeys: hiddenLibraryKeys,
|
||||
includePlaybackHubs: includePlaybackHubs,
|
||||
libraries: serverLibraries ?? const [],
|
||||
kinds: const {MediaKind.artist},
|
||||
);
|
||||
hubs = [...await globalFuture, ...await musicFuture];
|
||||
} else {
|
||||
hubs = await _fetchLibraryHubsForClient(
|
||||
client,
|
||||
limit: hubItemLimit,
|
||||
hiddenLibraryKeys: hiddenLibraryKeys,
|
||||
includePlaybackHubs: includePlaybackHubs,
|
||||
libraries: useGlobalHubs ? serverLibraries : null,
|
||||
);
|
||||
}
|
||||
return _postProcessHubs(hubs, serverId: ServerId(serverId), hiddenLibraryKeys: hiddenLibraryKeys);
|
||||
},
|
||||
);
|
||||
@@ -506,31 +512,39 @@ class DataAggregationService {
|
||||
return true;
|
||||
}).toList();
|
||||
|
||||
// Sliding window rather than batches of three separated by a barrier: a
|
||||
// batch waits for its slowest member before the next one starts, so wall
|
||||
// time was the sum of per-batch maxima and one slow library stalled every
|
||||
// library queued behind it (#1784). Starting the next request the moment
|
||||
// any slot frees keeps the same peak concurrency with no head-of-line
|
||||
// blocking. Results are written back by index so hub order stays the
|
||||
// library order regardless of completion order.
|
||||
const concurrency = 3;
|
||||
final all = <MediaHub>[];
|
||||
for (var start = 0; start < visible.length; start += concurrency) {
|
||||
final batch = visible.skip(start).take(concurrency);
|
||||
final results = await Future.wait(
|
||||
batch.map((l) async {
|
||||
try {
|
||||
return await client.fetchLibraryHubs(
|
||||
l.id,
|
||||
libraryName: l.title,
|
||||
limit: limit,
|
||||
includePlaybackHubs: includePlaybackHubs,
|
||||
libraryKind: l.kind,
|
||||
);
|
||||
} catch (e, st) {
|
||||
appLogger.e('Failed to fetch library hubs for ${l.globalKey}', error: e, stackTrace: st);
|
||||
return <MediaHub>[];
|
||||
}
|
||||
}),
|
||||
);
|
||||
for (final list in results) {
|
||||
all.addAll(list);
|
||||
final results = List<List<MediaHub>>.filled(visible.length, const []);
|
||||
var next = 0;
|
||||
|
||||
Future<void> worker() async {
|
||||
while (true) {
|
||||
final index = next++;
|
||||
if (index >= visible.length) return;
|
||||
final library = visible[index];
|
||||
try {
|
||||
results[index] = await client.fetchLibraryHubs(
|
||||
library.id,
|
||||
libraryName: library.title,
|
||||
limit: limit,
|
||||
includePlaybackHubs: includePlaybackHubs,
|
||||
libraryKind: library.kind,
|
||||
);
|
||||
} catch (e, st) {
|
||||
appLogger.e('Failed to fetch library hubs for ${library.globalKey}', error: e, stackTrace: st);
|
||||
}
|
||||
}
|
||||
}
|
||||
return all;
|
||||
|
||||
await Future.wait([for (var i = 0; i < concurrency && i < visible.length; i++) worker()]);
|
||||
|
||||
return [for (final list in results) ...list];
|
||||
}
|
||||
|
||||
/// Filter hidden-library items and drop empty hubs.
|
||||
|
||||
@@ -177,6 +177,11 @@ class JellyfinClient
|
||||
baseUrl: connection.baseUrl,
|
||||
defaultHeaders: headers,
|
||||
logLabel: 'Jellyfin',
|
||||
// Same pool tuning Plex uses: the home fan-out issues several concurrent
|
||||
// requests per pass, and the untuned dart:io default drops idle
|
||||
// connections after 15s — a fresh TLS handshake per request on a
|
||||
// high-RTT/CDN link.
|
||||
usePlexApiClient: true,
|
||||
prioritizedEndpoints: connection.baseUrls,
|
||||
onEndpointSwitch: (newBaseUrl, {required persist}) => client._handleEndpointSwitch(newBaseUrl, persist: persist),
|
||||
onAllEndpointsExhausted: onAllEndpointsExhausted,
|
||||
|
||||
@@ -2,23 +2,21 @@ part of '../../jellyfin_client.dart';
|
||||
|
||||
String _segment(String value) => Uri.encodeComponent(value);
|
||||
|
||||
/// Transport policy for a hub surface: bounded transient retries, no
|
||||
/// endpoint failover. See `_getItemsResponse`.
|
||||
typedef _HubRetryPolicy = ({String operation, List<Duration> attemptTimeouts});
|
||||
/// Transport policy for a hub surface: one whole-request deadline, retries
|
||||
/// only on immediate connection errors, no endpoint failover. See
|
||||
/// `_getItemsResponse` and [retryTransientMediaServerCall].
|
||||
typedef _HubRetryPolicy = ({String operation, Duration deadline});
|
||||
|
||||
const _HubRetryPolicy _homeHubRetry = (
|
||||
operation: 'Jellyfin home hubs',
|
||||
attemptTimeouts: MediaServerTimeouts.homeHubAttemptTimeouts,
|
||||
);
|
||||
const _HubRetryPolicy _homeHubRetry = (operation: 'Jellyfin home hubs', deadline: MediaServerTimeouts.homeHubDeadline);
|
||||
|
||||
const _HubRetryPolicy _libraryHubRetry = (
|
||||
operation: 'Jellyfin library hubs',
|
||||
attemptTimeouts: MediaServerTimeouts.libraryHubAttemptTimeouts,
|
||||
deadline: MediaServerTimeouts.libraryHubDeadline,
|
||||
);
|
||||
|
||||
const _HubRetryPolicy _continueWatchingRetry = (
|
||||
operation: 'Jellyfin continue watching',
|
||||
attemptTimeouts: MediaServerTimeouts.homeHubAttemptTimeouts,
|
||||
deadline: MediaServerTimeouts.homeHubDeadline,
|
||||
);
|
||||
|
||||
List<Map<String, dynamic>> _itemsArray(Object? data) {
|
||||
@@ -58,15 +56,47 @@ LibraryPage<T> _pagedItems<T>(
|
||||
/// list calls; we ask for the minimum extras needed to drive the
|
||||
/// MediaItem mapper:
|
||||
/// - `RecursiveItemCount`/`ChildCount` for series leaf count
|
||||
/// - `UserData` is included in defaults but pinned for safety
|
||||
/// - `PremiereDate` for sort-by-release-date and episode metadata
|
||||
/// - `OriginalTitle`/`SortName` for sort + alphabetised display
|
||||
/// - `Overview` so list rows can show their description
|
||||
///
|
||||
/// Heavier fields (`MediaSources`, `People`, `Genres`, `Tags`, `Studios`,
|
||||
/// `Taglines`, `ProviderIds`, `Chapters`) stay in [_detailFields] — together
|
||||
/// they added seconds to large-library pages on small home servers.
|
||||
const _browseFields = 'RecursiveItemCount,ChildCount,UserData,PremiereDate,OriginalTitle,SortName,Overview';
|
||||
///
|
||||
/// `UserData` and `PremiereDate` are deliberately absent: neither is a member
|
||||
/// of Jellyfin's `ItemFields` enum, so the server's
|
||||
/// `CommaDelimitedCollectionModelBinder` drops them element-by-element and
|
||||
/// they never did anything. `UserData` is governed by `EnableUserData`
|
||||
/// (default true) and `dto.PremiereDate` is set unconditionally.
|
||||
const _browseFields = 'RecursiveItemCount,ChildCount,OriginalTitle,SortName,Overview';
|
||||
|
||||
/// Field set for the home / per-library hub rows (Recently Added, Continue
|
||||
/// Watching, Next Up). Poster cards render artwork, title, year and the
|
||||
/// watch badge; only `Overview` needs asking for, to feed the mobile hero
|
||||
/// (`discover_screen`) and the TV spotlight blurb.
|
||||
///
|
||||
/// Crucially this drops `RecursiveItemCount`/`ChildCount`. `/Items/Latest`
|
||||
/// groups a TV library by series, so those rows are Series FOLDER dtos and
|
||||
/// each count field costs a per-row DB query server-side
|
||||
/// (`Folder.GetRecursiveChildCount` / `Folder.GetChildCount`) — the same cost
|
||||
/// already documented on [_folderBrowseFields] and [_musicAlbumRowFields],
|
||||
/// which the home rows never got (#1784).
|
||||
///
|
||||
/// Watch state survives intact: Jellyfin computes `UserData.Played` from
|
||||
/// `UnplayedItemCount` when `RecursiveItemCount` is absent
|
||||
/// (`Folder.FillUserDataDtoValues`), and [MediaItem.unwatchedCount] falls back
|
||||
/// to `UserData.UnplayedItemCount`. Only the season progress bar needs real
|
||||
/// leaf totals, and seasons never appear on a hub row.
|
||||
const _hubRowFields = 'Overview';
|
||||
|
||||
/// How far back `/Shows/NextUp` looks for a series to resume, mirroring
|
||||
/// Jellyfin web's `maxDaysForNextUp` default. Without it the server's
|
||||
/// `GetNextUpSeriesKeys` scan is unbounded over every series the user has ever
|
||||
/// played an episode of.
|
||||
const _nextUpDateCutoffDays = 365;
|
||||
|
||||
String _nextUpDateCutoff() =>
|
||||
DateTime.now().toUtc().subtract(const Duration(days: _nextUpDateCutoffDays)).toIso8601String();
|
||||
|
||||
/// Existing episode-row requests can show Plex-style quality labels when the
|
||||
/// response includes `MediaSources`. Keep this off broad library/search/latest
|
||||
@@ -213,11 +243,32 @@ mixin _JellyfinBrowseMethods on _JellyfinClientInternals {
|
||||
/// `_providerLibraries`.
|
||||
List<MediaLibrary>? _loadedLibraryViews;
|
||||
|
||||
/// In-flight `/Views` request, shared by concurrent callers.
|
||||
///
|
||||
/// At cold start `LibrariesProvider.loadLibraries()` and
|
||||
/// `DataAggregationService.getHubsFromAllServers` both ask for libraries at
|
||||
/// the same time, and the hub fan-out sits *serially* behind its copy. Two
|
||||
/// identical uncached round trips is a full RTT of pure cold-start latency on
|
||||
/// a remote server (#1784). Plex has never paid it — its library list comes
|
||||
/// from the `/media/providers` response cached at client creation.
|
||||
///
|
||||
/// Single-flight only: once the request settles the next caller re-fetches,
|
||||
/// so a library added server-side still shows up on the next refresh.
|
||||
Future<List<MediaLibrary>>? _inFlightLibraries;
|
||||
|
||||
@override
|
||||
Future<List<MediaLibrary>> fetchLibraries() async {
|
||||
final libraries = await _fetchLibraries();
|
||||
_loadedLibraryViews = libraries;
|
||||
return libraries;
|
||||
Future<List<MediaLibrary>> fetchLibraries() {
|
||||
final inFlight = _inFlightLibraries;
|
||||
if (inFlight != null) return inFlight;
|
||||
|
||||
final request = _fetchLibraries().then((libraries) {
|
||||
_loadedLibraryViews = libraries;
|
||||
return libraries;
|
||||
});
|
||||
_inFlightLibraries = request;
|
||||
return request.whenComplete(() {
|
||||
if (identical(_inFlightLibraries, request)) _inFlightLibraries = null;
|
||||
});
|
||||
}
|
||||
|
||||
/// [abort] tears the view fetch down with the pass that owns it — a
|
||||
@@ -1414,7 +1465,7 @@ mixin _JellyfinBrowseMethods on _JellyfinClientInternals {
|
||||
_fetchItemsArray('/UserItems/Resume', {
|
||||
'userId': connection.userId,
|
||||
'Limit': ?count?.toString(),
|
||||
'Fields': _browseFields,
|
||||
'Fields': _hubRowFields,
|
||||
'MediaTypes': 'Video',
|
||||
'Recursive': 'true',
|
||||
'EnableTotalRecordCount': 'false',
|
||||
@@ -1423,8 +1474,9 @@ mixin _JellyfinBrowseMethods on _JellyfinClientInternals {
|
||||
_safeFetchItemsArray('/Shows/NextUp', {
|
||||
'userId': connection.userId,
|
||||
'Limit': ?count?.toString(),
|
||||
'Fields': _browseFields,
|
||||
'Fields': _hubRowFields,
|
||||
'EnableResumable': 'false',
|
||||
'NextUpDateCutoff': _nextUpDateCutoff(),
|
||||
'EnableTotalRecordCount': 'false',
|
||||
...jellyfinImageQueryParameters,
|
||||
}, retry: _continueWatchingRetry),
|
||||
@@ -1519,7 +1571,7 @@ mixin _JellyfinBrowseMethods on _JellyfinClientInternals {
|
||||
final latestFuture = _safeFetchItemsArray('/Users/${_segment(connection.userId)}/Items/Latest', {
|
||||
'Limit': limit.toString(),
|
||||
'ParentId': ?parentId,
|
||||
'Fields': _browseFields,
|
||||
'Fields': _hubRowFields,
|
||||
'IncludeItemTypes': ?latestItemTypes,
|
||||
...jellyfinImageQueryParameters,
|
||||
}, retry: retry);
|
||||
@@ -1547,7 +1599,7 @@ mixin _JellyfinBrowseMethods on _JellyfinClientInternals {
|
||||
'userId': connection.userId,
|
||||
'ParentId': ?parentId,
|
||||
'Limit': limit.toString(),
|
||||
'Fields': _browseFields,
|
||||
'Fields': _hubRowFields,
|
||||
'MediaTypes': 'Video',
|
||||
'Recursive': 'true',
|
||||
'EnableTotalRecordCount': 'false',
|
||||
@@ -1558,8 +1610,9 @@ mixin _JellyfinBrowseMethods on _JellyfinClientInternals {
|
||||
'userId': connection.userId,
|
||||
'ParentId': ?parentId,
|
||||
'Limit': limit.toString(),
|
||||
'Fields': _browseFields,
|
||||
'Fields': _hubRowFields,
|
||||
'EnableResumable': 'false',
|
||||
'NextUpDateCutoff': _nextUpDateCutoff(),
|
||||
'EnableTotalRecordCount': 'false',
|
||||
...jellyfinImageQueryParameters,
|
||||
}, retry: retry)
|
||||
@@ -1701,7 +1754,7 @@ mixin _JellyfinBrowseMethods on _JellyfinClientInternals {
|
||||
'IncludeItemTypes': 'Movie,Series,Episode,Video,MusicVideo,Photo',
|
||||
'SortBy': 'DateCreated,SortName,ProductionYear',
|
||||
'SortOrder': 'Descending,Descending,Descending',
|
||||
'Fields': _browseFields,
|
||||
'Fields': _hubRowFields,
|
||||
...jellyfinImageQueryParameters,
|
||||
},
|
||||
offset: offset,
|
||||
@@ -1732,7 +1785,7 @@ mixin _JellyfinBrowseMethods on _JellyfinClientInternals {
|
||||
'userId': connection.userId,
|
||||
'StartIndex': offset.toString(),
|
||||
'Limit': effectiveLimit,
|
||||
'Fields': _browseFields,
|
||||
'Fields': _hubRowFields,
|
||||
'Recursive': 'true',
|
||||
'EnableTotalRecordCount': 'true',
|
||||
if (parentId != null) 'ParentId': parentId else 'MediaTypes': 'Video',
|
||||
@@ -1749,9 +1802,10 @@ mixin _JellyfinBrowseMethods on _JellyfinClientInternals {
|
||||
'userId': connection.userId,
|
||||
'StartIndex': offset.toString(),
|
||||
'Limit': effectiveLimit,
|
||||
'Fields': _browseFields,
|
||||
'Fields': _hubRowFields,
|
||||
'ParentId': ?parentId,
|
||||
'EnableResumable': 'false',
|
||||
'NextUpDateCutoff': _nextUpDateCutoff(),
|
||||
'EnableTotalRecordCount': 'true',
|
||||
...jellyfinImageQueryParameters,
|
||||
},
|
||||
@@ -2036,14 +2090,13 @@ mixin _JellyfinBrowseMethods on _JellyfinClientInternals {
|
||||
}
|
||||
|
||||
/// GET [path], optionally under a hub-surface transport policy ([retry]):
|
||||
/// bounded transient retries with per-attempt timeouts and **no endpoint
|
||||
/// failover** — a slow hub row must not move the whole client off an
|
||||
/// otherwise working endpoint (same policy as Plex's three hub fetches;
|
||||
/// see [retryTransientMediaServerCall] / [FailoverHttpClient]).
|
||||
/// one whole-request deadline, retries only on immediate connection errors,
|
||||
/// and **no endpoint failover** — a slow hub row must not move the whole
|
||||
/// client off an otherwise working endpoint (same policy as Plex's three hub
|
||||
/// fetches; see [retryTransientMediaServerCall] / [FailoverHttpClient]).
|
||||
///
|
||||
/// [timeout] and [allowEndpointFailover] configure the un-retried path only; a
|
||||
/// [retry] policy carries its own per-attempt timeouts and always disables
|
||||
/// failover.
|
||||
/// [retry] policy carries its own deadline and always disables failover.
|
||||
Future<MediaServerResponse> _getItemsResponse(
|
||||
String path,
|
||||
Map<String, dynamic> queryParameters,
|
||||
@@ -2064,7 +2117,7 @@ mixin _JellyfinBrowseMethods on _JellyfinClientInternals {
|
||||
abort?.throwIfAborted();
|
||||
return retryTransientMediaServerCall(
|
||||
operation: retry.operation,
|
||||
attemptTimeouts: retry.attemptTimeouts,
|
||||
deadline: retry.deadline,
|
||||
call: (timeout, attemptAbort) => _http.get(
|
||||
path,
|
||||
queryParameters: queryParameters,
|
||||
|
||||
@@ -5,8 +5,13 @@ import 'plex_constants.dart';
|
||||
/// Browse responses retain up to three backdrops so hero surfaces can rotate
|
||||
/// artwork without allowing image-tag payloads to grow without bound.
|
||||
const jellyfinBackdropImageLimit = 3;
|
||||
|
||||
/// `Thumb` is deliberately absent: `JellyfinMappers` never reads
|
||||
/// `ImageTags['Thumb']`, and `parentThumbPath`/`grandparentThumbPath` are built
|
||||
/// from the season/series *Primary* tags. Asking for it added a dead image type
|
||||
/// to ~40 requests and widened the server's inherited-image parent walk.
|
||||
const jellyfinImageQueryParameters = <String, String>{
|
||||
'EnableImageTypes': 'Primary,Backdrop,Thumb,Logo',
|
||||
'EnableImageTypes': 'Primary,Backdrop,Logo',
|
||||
'ImageTypeLimit': '$jellyfinBackdropImageLimit',
|
||||
};
|
||||
|
||||
|
||||
@@ -1564,7 +1564,7 @@ class PlexClient
|
||||
|
||||
final response = await retryTransientMediaServerCall(
|
||||
operation: 'Plex continue watching hubs',
|
||||
attemptTimeouts: MediaServerTimeouts.homeHubAttemptTimeouts,
|
||||
deadline: MediaServerTimeouts.homeHubDeadline,
|
||||
call: (timeout, abort) => _getWithFailover(
|
||||
continueWatchingHubKey ?? '/hubs',
|
||||
queryParameters: queryParameters,
|
||||
@@ -2244,7 +2244,7 @@ class PlexClient
|
||||
required String path,
|
||||
required Map<String, dynamic> queryParameters,
|
||||
required String operation,
|
||||
required List<Duration> attemptTimeouts,
|
||||
required Duration deadline,
|
||||
required String failureLabel,
|
||||
int? librarySectionID,
|
||||
String? librarySectionTitle,
|
||||
@@ -2253,7 +2253,7 @@ class PlexClient
|
||||
try {
|
||||
final response = await retryTransientMediaServerCall(
|
||||
operation: operation,
|
||||
attemptTimeouts: attemptTimeouts,
|
||||
deadline: deadline,
|
||||
call: (timeout, abort) => _getWithFailover(
|
||||
path,
|
||||
queryParameters: queryParameters,
|
||||
@@ -2291,7 +2291,7 @@ class PlexClient
|
||||
path: '/hubs/sections/$sectionId',
|
||||
queryParameters: {'count': limit, 'includeGuids': 1},
|
||||
operation: 'Plex library hubs',
|
||||
attemptTimeouts: MediaServerTimeouts.libraryHubAttemptTimeouts,
|
||||
deadline: MediaServerTimeouts.libraryHubDeadline,
|
||||
failureLabel: 'library hubs',
|
||||
librarySectionID: _librarySectionIdFromString(sectionId),
|
||||
librarySectionTitle: libraryName,
|
||||
@@ -2305,7 +2305,7 @@ class PlexClient
|
||||
path: _providerPromotedHubKey ?? _providerHomeHubKey ?? '/hubs',
|
||||
queryParameters: {'count': limit, 'includeGuids': 1},
|
||||
operation: 'Plex global hubs',
|
||||
attemptTimeouts: MediaServerTimeouts.homeHubAttemptTimeouts,
|
||||
deadline: MediaServerTimeouts.homeHubDeadline,
|
||||
failureLabel: 'global hubs',
|
||||
);
|
||||
|
||||
@@ -2314,7 +2314,7 @@ class PlexClient
|
||||
path: '/hubs/metadata/$ratingKey/related',
|
||||
queryParameters: {'count': count},
|
||||
operation: 'Plex related hubs',
|
||||
attemptTimeouts: MediaServerTimeouts.libraryHubAttemptTimeouts,
|
||||
deadline: MediaServerTimeouts.libraryHubDeadline,
|
||||
failureLabel: 'related hubs',
|
||||
filter: _videoOrCollectionHubItem,
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user