@@ -248,6 +248,9 @@ class PlexClient
|
||||
/// Promoted home hub endpoint advertised by /media/providers (usually /hubs/promoted).
|
||||
String? _providerPromotedHubKey;
|
||||
|
||||
/// Dedicated Continue Watching hub endpoint advertised by /media/providers.
|
||||
String? _providerContinueWatchingHubKey;
|
||||
|
||||
/// EPG providers parsed from /media/providers
|
||||
@override
|
||||
List<({String identifier, String gridEndpoint})> _providerEpg = const [];
|
||||
@@ -348,6 +351,7 @@ class PlexClient
|
||||
List<({String identifier, String gridEndpoint})> epgProviders = const [],
|
||||
String? homeHubKey,
|
||||
String? promotedHubKey,
|
||||
String? continueWatchingHubKey,
|
||||
}) {
|
||||
final client = PlexClient._(
|
||||
config,
|
||||
@@ -360,6 +364,7 @@ class PlexClient
|
||||
client._providerEpg = epgProviders;
|
||||
client._providerHomeHubKey = homeHubKey;
|
||||
client._providerPromotedHubKey = promotedHubKey;
|
||||
client._providerContinueWatchingHubKey = continueWatchingHubKey;
|
||||
return client;
|
||||
}
|
||||
|
||||
@@ -459,6 +464,7 @@ class PlexClient
|
||||
_providerEpg = [];
|
||||
_providerHomeHubKey = null;
|
||||
_providerPromotedHubKey = null;
|
||||
_providerContinueWatchingHubKey = null;
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -468,6 +474,7 @@ class PlexClient
|
||||
_providerEpg = [];
|
||||
_providerHomeHubKey = null;
|
||||
_providerPromotedHubKey = null;
|
||||
_providerContinueWatchingHubKey = null;
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -475,6 +482,7 @@ class PlexClient
|
||||
final epg = <({String identifier, String gridEndpoint})>[];
|
||||
String? homeHubKey;
|
||||
String? promotedHubKey;
|
||||
String? continueWatchingHubKey;
|
||||
|
||||
for (final provider in providers) {
|
||||
if (provider is! Map) continue;
|
||||
@@ -493,6 +501,10 @@ class PlexClient
|
||||
promotedHubKey ??= feature['key'] as String?;
|
||||
}
|
||||
|
||||
if (feature['type'] == 'continuewatching') {
|
||||
continueWatchingHubKey ??= feature['key'] as String?;
|
||||
}
|
||||
|
||||
if (feature['type'] != 'content') continue;
|
||||
|
||||
final directories = feature['Directory'] as List?;
|
||||
@@ -552,6 +564,7 @@ class PlexClient
|
||||
_providerEpg = epg;
|
||||
_providerHomeHubKey = homeHubKey;
|
||||
_providerPromotedHubKey = promotedHubKey;
|
||||
_providerContinueWatchingHubKey = continueWatchingHubKey;
|
||||
appLogger.d('Media providers: ${libraries.length} libraries, ${epg.length} EPG provider(s)');
|
||||
} catch (e) {
|
||||
appLogger.w('Failed to fetch /media/providers, will fall back to /library/sections', error: e);
|
||||
@@ -559,6 +572,7 @@ class PlexClient
|
||||
_providerEpg = [];
|
||||
_providerHomeHubKey = null;
|
||||
_providerPromotedHubKey = null;
|
||||
_providerContinueWatchingHubKey = null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1326,15 +1340,22 @@ class PlexClient
|
||||
}
|
||||
|
||||
/// Get continue watching items via the hubs system.
|
||||
/// Uses /hubs?identifier=home.continue,home.ondeck which respects the
|
||||
/// Prefer the provider's dedicated Continue Watching feature key when
|
||||
/// advertised; fall back to Plex Web's legacy hubs query. Both respect the
|
||||
/// server's OnDeckWindow preference (unlike /library/onDeck).
|
||||
Future<List<PlexMetadataDto>> _getContinueWatching({int? count = 20}) async {
|
||||
final continueWatchingHubKey = _providerContinueWatchingHubKey;
|
||||
final queryParameters = <String, dynamic>{'count': ?count, 'includeGuids': 1};
|
||||
if (continueWatchingHubKey == null) {
|
||||
queryParameters['identifier'] = 'home.continue,home.ondeck';
|
||||
}
|
||||
|
||||
final response = await retryTransientMediaServerCall(
|
||||
operation: 'Plex continue watching hubs',
|
||||
attemptTimeouts: MediaServerTimeouts.homeHubAttemptTimeouts,
|
||||
call: (timeout, abort) => _getWithFailover(
|
||||
'/hubs',
|
||||
queryParameters: {'identifier': 'home.continue,home.ondeck', 'count': ?count, 'includeGuids': 1},
|
||||
continueWatchingHubKey ?? '/hubs',
|
||||
queryParameters: queryParameters,
|
||||
timeout: timeout,
|
||||
abort: abort,
|
||||
allowEndpointFailover: false,
|
||||
|
||||
@@ -127,6 +127,40 @@ void main() {
|
||||
expect(httpClient.requests.last.url.queryParameters['count'], '12');
|
||||
});
|
||||
|
||||
test('fetchContinueWatching uses advertised provider feature endpoint', () async {
|
||||
final db = AppDatabase.forTesting(NativeDatabase.memory());
|
||||
PlexApiCache.initialize(db);
|
||||
addTearDown(db.close);
|
||||
|
||||
final httpClient = _SequenceClient([
|
||||
(_) async => _jsonResponse(_mediaProvidersPayload()),
|
||||
(_) async => _jsonResponse(_continueWatchingPayload()),
|
||||
]);
|
||||
final client = await PlexClient.create(
|
||||
PlexConfig(
|
||||
baseUrl: 'http://server:32400',
|
||||
token: 'token',
|
||||
clientIdentifier: 'client-id',
|
||||
product: 'Plezy',
|
||||
version: 'test',
|
||||
),
|
||||
serverId: 'server-id',
|
||||
serverName: 'Server',
|
||||
httpClient: httpClient,
|
||||
seedTranscoderVideoSupport: true,
|
||||
);
|
||||
addTearDown(client.close);
|
||||
|
||||
final items = await client.fetchContinueWatching(count: 21);
|
||||
|
||||
expect(items, hasLength(1));
|
||||
expect(items.single.title, 'Movie A');
|
||||
expect(httpClient.requests.map((r) => r.url.path), ['/media/providers', '/hubs/continueWatching']);
|
||||
expect(httpClient.requests.last.url.queryParameters['count'], '21');
|
||||
expect(httpClient.requests.last.url.queryParameters['includeGuids'], '1');
|
||||
expect(httpClient.requests.last.url.queryParameters.containsKey('identifier'), isFalse);
|
||||
});
|
||||
|
||||
test('fetchContinueWatching omits count when uncapped', () async {
|
||||
final db = AppDatabase.forTesting(NativeDatabase.memory());
|
||||
PlexApiCache.initialize(db);
|
||||
@@ -260,6 +294,7 @@ Map<String, dynamic> _mediaProvidersPayload() => {
|
||||
],
|
||||
},
|
||||
{'type': 'promoted', 'key': '/hubs/promoted'},
|
||||
{'type': 'continuewatching', 'key': '/hubs/continueWatching'},
|
||||
],
|
||||
},
|
||||
],
|
||||
|
||||
Reference in New Issue
Block a user