diff --git a/lib/services/data_aggregation_service.dart b/lib/services/data_aggregation_service.dart index 0995337c..de2337f1 100644 --- a/lib/services/data_aggregation_service.dart +++ b/lib/services/data_aggregation_service.dart @@ -87,10 +87,10 @@ class DataAggregationService { } /// Fetch recommendation hubs from all servers as neutral [MediaHub]s. - /// When useGlobalHubs is true (default), uses the global /hubs endpoint - /// to get the true home page hubs like "Recently Added Movies", "Recently - /// Added TV"; when false, uses per-library hubs from - /// /hubs/sections/{sectionId}. + /// When useGlobalHubs is true (default), rich-hub backends use the global + /// /hubs endpoint to get true home page hubs like "Recently Added Movies". + /// Backends without rich home hubs fall back to per-library hubs so one + /// capped "Latest" response cannot hide whole library types. Future> getHubsFromAllServers({ int? limit, Set? hiddenLibraryKeys, @@ -111,20 +111,23 @@ class DataAggregationService { final serverId = entry.key; final client = entry.value; try { - final hubs = useGlobalHubs + final serverLibraries = libraries?[serverId]; + final shouldUseGlobalHubs = useGlobalHubs && client.capabilities.richHubs; + final hubs = shouldUseGlobalHubs ? await client.fetchGlobalHubs(limit: limit ?? 10, includePlaybackHubs: includePlaybackHubs) : await _fetchLibraryHubsForClient( client, limit: limit ?? 10, hiddenLibraryKeys: hiddenLibraryKeys, includePlaybackHubs: includePlaybackHubs, + libraries: useGlobalHubs ? serverLibraries : null, ); return _postProcessHubs( hubs, serverId: serverId, hiddenLibraryKeys: hiddenLibraryKeys, - libraries: libraries?[serverId], - splitRecentlyAdded: useGlobalHubs, + libraries: serverLibraries, + splitRecentlyAdded: shouldUseGlobalHubs, ); } catch (e, stackTrace) { appLogger.e('Failed to fetch hubs from server $serverId', error: e, stackTrace: stackTrace); @@ -148,8 +151,9 @@ class DataAggregationService { required int limit, Set? hiddenLibraryKeys, required bool includePlaybackHubs, + List? libraries, }) async { - final libs = await client.fetchLibraries(); + final libs = libraries ?? await client.fetchLibraries(); final visible = libs.where((l) { if (l.kind != MediaKind.movie && l.kind != MediaKind.show) return false; if (l.hidden) return false; diff --git a/test/services/data_aggregation_bridge_test.dart b/test/services/data_aggregation_bridge_test.dart index 729b2def..77ffc40c 100644 --- a/test/services/data_aggregation_bridge_test.dart +++ b/test/services/data_aggregation_bridge_test.dart @@ -111,5 +111,53 @@ void main() { ['lib-1', 'lib-2', 'lib-3', 'lib-4'], ); }); + + test('global home layout falls back to per-library hubs for Jellyfin', () async { + final captured = []; + + final client = JellyfinClient.forTesting( + connection: _conn(), + httpClient: MockClient((req) async { + captured.add(req.url); + if (req.url.path == '/Users/user-1/Views') { + return _json({ + 'Items': [ + {'Id': 'movies', 'Name': 'Movies', 'CollectionType': 'movies'}, + {'Id': 'shows', 'Name': 'Shows', 'CollectionType': 'tvshows'}, + ], + }); + } + if (req.url.path == '/Users/user-1/Items/Latest') { + final parentId = req.url.queryParameters['ParentId']; + return switch (parentId) { + 'movies' => _json({ + 'Items': [ + {'Id': 'movie-1', 'Type': 'Movie', 'Name': 'Latest Movie', 'ParentLibraryId': 'movies'}, + ], + }), + 'shows' => _json({ + 'Items': [ + {'Id': 'show-1', 'Type': 'Series', 'Name': 'Latest Show', 'ParentLibraryId': 'shows'}, + ], + }), + _ => http.Response('mixed latest should not be requested', 500), + }; + } + return http.Response('unexpected request', 500); + }), + ); + addTearDown(client.close); + manager.debugRegisterJellyfinClientForTesting(client); + + final hubs = await service.getHubsFromAllServers(useGlobalHubs: true, includePlaybackHubs: false); + + expect(hubs.map((h) => h.identifier), ['library.movies.recent', 'library.shows.recent']); + expect(hubs.map((h) => h.items.single.id), ['movie-1', 'show-1']); + expect(captured.where((uri) => uri.path == '/Users/user-1/Views'), hasLength(1)); + expect( + captured.where((uri) => uri.path == '/Users/user-1/Items/Latest').map((uri) => uri.queryParameters['ParentId']), + ['movies', 'shows'], + ); + }); }); }