diff --git a/lib/services/plex_client.dart b/lib/services/plex_client.dart index b8b1337e..99df1bbd 100644 --- a/lib/services/plex_client.dart +++ b/lib/services/plex_client.dart @@ -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> _getContinueWatching({int? count = 20}) async { + final continueWatchingHubKey = _providerContinueWatchingHubKey; + final queryParameters = {'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, diff --git a/test/services/plex_home_retry_test.dart b/test/services/plex_home_retry_test.dart index 10598916..847e3afa 100644 --- a/test/services/plex_home_retry_test.dart +++ b/test/services/plex_home_retry_test.dart @@ -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 _mediaProvidersPayload() => { ], }, {'type': 'promoted', 'key': '/hubs/promoted'}, + {'type': 'continuewatching', 'key': '/hubs/continueWatching'}, ], }, ],