From ab5ad3c8392c2f9986db4be6ef56a4c4e396d4f4 Mon Sep 17 00:00:00 2001 From: edde746 <86283021+edde746@users.noreply.github.com> Date: Thu, 26 Mar 2026 18:57:47 +0100 Subject: [PATCH] fix: use hubs API for continue watching close #748 --- lib/models/plex_hub.dart | 2 +- lib/services/data_aggregation_service.dart | 2 +- lib/services/plex_client.dart | 55 +++++++++++++--------- 3 files changed, 36 insertions(+), 23 deletions(-) diff --git a/lib/models/plex_hub.dart b/lib/models/plex_hub.dart index 51471904..8c19c85a 100644 --- a/lib/models/plex_hub.dart +++ b/lib/models/plex_hub.dart @@ -47,7 +47,7 @@ class PlexHub { final String type = (item.containsKey('leafCount') || item.containsKey('childCount')) ? 'show' : 'folder'; item['type'] = type; } - metadataList.add(PlexMetadata.fromJson(item as Map)); + metadataList.add(PlexMetadata.fromJsonWithImages(item as Map)); } catch (e) { // Skip items that fail to parse } diff --git a/lib/services/data_aggregation_service.dart b/lib/services/data_aggregation_service.dart index bca00a23..d6dcc13c 100644 --- a/lib/services/data_aggregation_service.dart +++ b/lib/services/data_aggregation_service.dart @@ -32,7 +32,7 @@ class DataAggregationService { final allOnDeck = await _perServer( operationName: 'fetching on deck', operation: (serverId, client, server) async { - return await client.getOnDeck(); + return await client.getContinueWatching(); }, ); diff --git a/lib/services/plex_client.dart b/lib/services/plex_client.dart index 4111a9ff..988b8807 100644 --- a/lib/services/plex_client.dart +++ b/lib/services/plex_client.dart @@ -79,23 +79,6 @@ List _processHubResponse(Map decoded, String serverId, return hubs; } -/// Process on-deck response in an isolate. -/// Top-level function so it can be passed to [Isolate.run]. -List _processOnDeckResponse(Map decoded, String serverId, String? serverName) { - final container = decoded['MediaContainer'] as Map?; - if (container == null || container['Metadata'] == null) return []; - - final allItems = (container['Metadata'] as List) - .map( - (json) => PlexMetadata.fromJsonWithImages( - json as Map, - ).copyWith(serverId: serverId, serverName: serverName), - ) - .toList(); - - return allItems.where((item) => !item.isMusicContent).toList(); -} - /// Constants for Plex stream types class PlexStreamType { static const int video = 1; @@ -988,12 +971,42 @@ class PlexClient { return allItems.where((item) => !item.isMusicContent).toList(); } - /// Get on deck items (continue watching, filtered to video content only) - Future> getOnDeck() async { - final response = await _dio.get('/library/onDeck'); + /// Get continue watching items via the hubs system. + /// Uses /hubs?identifier=home.continue,home.ondeck which respects the + /// server's OnDeckWindow preference (unlike /library/onDeck). + Future> getContinueWatching({int count = 20}) async { + final response = await _dio.get('/hubs', queryParameters: { + 'identifier': 'home.continue,home.ondeck', + 'count': count, + 'includeGuids': 1, + }); final sid = serverId; final sname = serverName; - return await tryIsolateRun(() => _processOnDeckResponse(response.data as Map, sid, sname)); + final hubs = await tryIsolateRun( + () => _processHubResponse(response.data as Map, sid, sname), + ); + // Deduplicate across home.continue and home.ondeck hubs. + // Like plex-web, episodes from the same show (same grandparentRatingKey) + // are deduplicated, preferring the in-progress item (has viewOffset). + final items = hubs.expand((hub) => hub.items).toList(); + final result = []; + for (final item in items) { + final isEpisode = item.type?.toLowerCase() == 'episode'; + final gpKey = item.grandparentRatingKey; + if (isEpisode && gpKey != null) { + final idx = result.indexWhere((e) => + e.type?.toLowerCase() == 'episode' && + e.grandparentRatingKey == gpKey); + if (idx != -1) { + if (result[idx].viewOffset == null && item.viewOffset != null) { + result[idx] = item; + } + continue; + } + } + result.add(item); + } + return result; } /// Get children of a metadata item (e.g., seasons for a show, episodes for a season)