From 5ed2532bf15745296e298c8780d26d3f3c03becb Mon Sep 17 00:00:00 2001 From: edde746 <86283021+edde746@users.noreply.github.com> Date: Tue, 6 Jan 2026 23:23:03 +0100 Subject: [PATCH] feat: global hubs with settings toggle close #216 & #217 --- lib/i18n/strings.g.dart | 32 ++++++- lib/i18n/strings.i18n.json | 2 + lib/i18n/strings_de.i18n.json | 2 + lib/i18n/strings_it.i18n.json | 2 + lib/i18n/strings_ko.i18n.json | 2 + lib/i18n/strings_nl.i18n.json | 2 + lib/i18n/strings_sv.i18n.json | 2 + lib/i18n/strings_zh.i18n.json | 2 + lib/providers/settings_provider.dart | 13 +++ lib/screens/discover_screen.dart | 33 ++++--- lib/screens/settings/settings_screen.dart | 13 +++ lib/services/data_aggregation_service.dart | 100 ++++++++++++++++++++- lib/services/plex_client.dart | 51 +++++++++++ lib/services/settings_service.dart | 10 +++ 14 files changed, 243 insertions(+), 23 deletions(-) diff --git a/lib/i18n/strings.g.dart b/lib/i18n/strings.g.dart index a0b233cf..54225ec1 100644 --- a/lib/i18n/strings.g.dart +++ b/lib/i18n/strings.g.dart @@ -4,9 +4,9 @@ /// To regenerate, run: `dart run slang` /// /// Locales: 7 -/// Strings: 3554 (507 per locale) +/// Strings: 3568 (509 per locale) /// -/// Built on 2025-12-31 at 06:54 UTC +/// Built on 2026-01-06 at 22:09 UTC // coverage:ignore-file // ignore_for_file: type=lint @@ -313,6 +313,8 @@ class _StringsSettingsEn { String get listViewDescription => 'Display items in a list layout'; String get useSeasonPosters => 'Use Season Posters'; String get showHeroSection => 'Show Hero Section'; + String get useGlobalHubs => 'Use Plex Home Layout'; + String get useGlobalHubsDescription => 'Show home page hubs like the official Plex client. When off, shows per-library recommendations instead.'; String get hardwareDecoding => 'Hardware Decoding'; String get hardwareDecodingDescription => 'Use hardware acceleration when available'; String get bufferSize => 'Buffer Size'; @@ -1186,6 +1188,8 @@ class _StringsSettingsDe implements _StringsSettingsEn { @override String get listViewDescription => 'Elemente in Listenansicht anzeigen'; @override String get useSeasonPosters => 'Staffelposter verwenden'; @override String get showHeroSection => 'Hero-Bereich anzeigen'; + @override String get useGlobalHubs => 'Plex-Startseiten-Layout verwenden'; + @override String get useGlobalHubsDescription => 'Zeigt Startseiten-Hubs wie der offizielle Plex-Client. Wenn deaktiviert, werden stattdessen Empfehlungen pro Bibliothek angezeigt.'; @override String get hardwareDecoding => 'Hardware-Decodierung'; @override String get hardwareDecodingDescription => 'Hardwarebeschleunigung verwenden, sofern verfügbar'; @override String get bufferSize => 'Puffergröße'; @@ -2060,6 +2064,8 @@ class _StringsSettingsIt implements _StringsSettingsEn { @override String get listViewDescription => 'Visualizza gli elementi in un layout a elenco'; @override String get useSeasonPosters => 'Usa poster delle stagioni'; @override String get showHeroSection => 'Mostra sezione principale'; + @override String get useGlobalHubs => 'Usa layout Home di Plex'; + @override String get useGlobalHubsDescription => 'Mostra gli hub della home page come il client Plex ufficiale. Se disattivato, mostra invece i suggerimenti per libreria.'; @override String get hardwareDecoding => 'Decodifica Hardware'; @override String get hardwareDecodingDescription => 'Utilizza l\'accelerazione hardware quando disponibile'; @override String get bufferSize => 'Dimensione buffer'; @@ -2934,6 +2940,8 @@ class _StringsSettingsKo implements _StringsSettingsEn { @override String get listViewDescription => '항목을 목록 레이아웃으로 표시합니다'; @override String get useSeasonPosters => '시즌 포스터 사용'; @override String get showHeroSection => '주요 추천 영역 표시'; + @override String get useGlobalHubs => 'Plex 홈 레이아웃 사용'; + @override String get useGlobalHubsDescription => '공식 Plex 클라이언트처럼 홈 페이지 허브를 표시합니다. 끄면 라이브러리별 추천이 대신 표시됩니다.'; @override String get hardwareDecoding => '하드웨어 디코딩'; @override String get hardwareDecodingDescription => '가능한 경우 하드웨어 가속을 사용합니다'; @override String get bufferSize => '버퍼 크기'; @@ -3808,6 +3816,8 @@ class _StringsSettingsNl implements _StringsSettingsEn { @override String get listViewDescription => 'Items weergeven in een lijstindeling'; @override String get useSeasonPosters => 'Gebruik seizoenposters'; @override String get showHeroSection => 'Toon hoofdsectie'; + @override String get useGlobalHubs => 'Plex Home-indeling gebruiken'; + @override String get useGlobalHubsDescription => 'Toon startpagina-hubs zoals de officiële Plex-client. Indien uitgeschakeld, worden in plaats daarvan aanbevelingen per bibliotheek getoond.'; @override String get hardwareDecoding => 'Hardware decodering'; @override String get hardwareDecodingDescription => 'Gebruik hardware versnelling indien beschikbaar'; @override String get bufferSize => 'Buffer grootte'; @@ -4682,6 +4692,8 @@ class _StringsSettingsSv implements _StringsSettingsEn { @override String get listViewDescription => 'Visa objekt i listlayout'; @override String get useSeasonPosters => 'Använd säsongsaffischer'; @override String get showHeroSection => 'Visa hjältesektion'; + @override String get useGlobalHubs => 'Använd Plex hem-layout'; + @override String get useGlobalHubsDescription => 'Visar startsidans hubbar som den officiella Plex-klienten. När av visas rekommendationer per bibliotek istället.'; @override String get hardwareDecoding => 'Hårdvaruavkodning'; @override String get hardwareDecodingDescription => 'Använd hårdvaruacceleration när tillgängligt'; @override String get bufferSize => 'Bufferstorlek'; @@ -5556,6 +5568,8 @@ class _StringsSettingsZh implements _StringsSettingsEn { @override String get listViewDescription => '以列表布局显示项目'; @override String get useSeasonPosters => '使用季海报'; @override String get showHeroSection => '显示主要精选区'; + @override String get useGlobalHubs => '使用 Plex 主页布局'; + @override String get useGlobalHubsDescription => '显示与官方 Plex 客户端相同的主页推荐。关闭时将显示按媒体库分类的推荐。'; @override String get hardwareDecoding => '硬件解码'; @override String get hardwareDecodingDescription => '如果可用,使用硬件加速'; @override String get bufferSize => '缓冲区大小'; @@ -6325,6 +6339,8 @@ extension on Translations { case 'settings.listViewDescription': return 'Display items in a list layout'; case 'settings.useSeasonPosters': return 'Use Season Posters'; case 'settings.showHeroSection': return 'Show Hero Section'; + case 'settings.useGlobalHubs': return 'Use Plex Home Layout'; + case 'settings.useGlobalHubsDescription': return 'Show home page hubs like the official Plex client. When off, shows per-library recommendations instead.'; case 'settings.hardwareDecoding': return 'Hardware Decoding'; case 'settings.hardwareDecodingDescription': return 'Use hardware acceleration when available'; case 'settings.bufferSize': return 'Buffer Size'; @@ -6840,6 +6856,8 @@ extension on _StringsDe { case 'settings.listViewDescription': return 'Elemente in Listenansicht anzeigen'; case 'settings.useSeasonPosters': return 'Staffelposter verwenden'; case 'settings.showHeroSection': return 'Hero-Bereich anzeigen'; + case 'settings.useGlobalHubs': return 'Plex-Startseiten-Layout verwenden'; + case 'settings.useGlobalHubsDescription': return 'Zeigt Startseiten-Hubs wie der offizielle Plex-Client. Wenn deaktiviert, werden stattdessen Empfehlungen pro Bibliothek angezeigt.'; case 'settings.hardwareDecoding': return 'Hardware-Decodierung'; case 'settings.hardwareDecodingDescription': return 'Hardwarebeschleunigung verwenden, sofern verfügbar'; case 'settings.bufferSize': return 'Puffergröße'; @@ -7356,6 +7374,8 @@ extension on _StringsIt { case 'settings.listViewDescription': return 'Visualizza gli elementi in un layout a elenco'; case 'settings.useSeasonPosters': return 'Usa poster delle stagioni'; case 'settings.showHeroSection': return 'Mostra sezione principale'; + case 'settings.useGlobalHubs': return 'Usa layout Home di Plex'; + case 'settings.useGlobalHubsDescription': return 'Mostra gli hub della home page come il client Plex ufficiale. Se disattivato, mostra invece i suggerimenti per libreria.'; case 'settings.hardwareDecoding': return 'Decodifica Hardware'; case 'settings.hardwareDecodingDescription': return 'Utilizza l\'accelerazione hardware quando disponibile'; case 'settings.bufferSize': return 'Dimensione buffer'; @@ -7872,6 +7892,8 @@ extension on _StringsKo { case 'settings.listViewDescription': return '항목을 목록 레이아웃으로 표시합니다'; case 'settings.useSeasonPosters': return '시즌 포스터 사용'; case 'settings.showHeroSection': return '주요 추천 영역 표시'; + case 'settings.useGlobalHubs': return 'Plex 홈 레이아웃 사용'; + case 'settings.useGlobalHubsDescription': return '공식 Plex 클라이언트처럼 홈 페이지 허브를 표시합니다. 끄면 라이브러리별 추천이 대신 표시됩니다.'; case 'settings.hardwareDecoding': return '하드웨어 디코딩'; case 'settings.hardwareDecodingDescription': return '가능한 경우 하드웨어 가속을 사용합니다'; case 'settings.bufferSize': return '버퍼 크기'; @@ -8388,6 +8410,8 @@ extension on _StringsNl { case 'settings.listViewDescription': return 'Items weergeven in een lijstindeling'; case 'settings.useSeasonPosters': return 'Gebruik seizoenposters'; case 'settings.showHeroSection': return 'Toon hoofdsectie'; + case 'settings.useGlobalHubs': return 'Plex Home-indeling gebruiken'; + case 'settings.useGlobalHubsDescription': return 'Toon startpagina-hubs zoals de officiële Plex-client. Indien uitgeschakeld, worden in plaats daarvan aanbevelingen per bibliotheek getoond.'; case 'settings.hardwareDecoding': return 'Hardware decodering'; case 'settings.hardwareDecodingDescription': return 'Gebruik hardware versnelling indien beschikbaar'; case 'settings.bufferSize': return 'Buffer grootte'; @@ -8904,6 +8928,8 @@ extension on _StringsSv { case 'settings.listViewDescription': return 'Visa objekt i listlayout'; case 'settings.useSeasonPosters': return 'Använd säsongsaffischer'; case 'settings.showHeroSection': return 'Visa hjältesektion'; + case 'settings.useGlobalHubs': return 'Använd Plex hem-layout'; + case 'settings.useGlobalHubsDescription': return 'Visar startsidans hubbar som den officiella Plex-klienten. När av visas rekommendationer per bibliotek istället.'; case 'settings.hardwareDecoding': return 'Hårdvaruavkodning'; case 'settings.hardwareDecodingDescription': return 'Använd hårdvaruacceleration när tillgängligt'; case 'settings.bufferSize': return 'Bufferstorlek'; @@ -9420,6 +9446,8 @@ extension on _StringsZh { case 'settings.listViewDescription': return '以列表布局显示项目'; case 'settings.useSeasonPosters': return '使用季海报'; case 'settings.showHeroSection': return '显示主要精选区'; + case 'settings.useGlobalHubs': return '使用 Plex 主页布局'; + case 'settings.useGlobalHubsDescription': return '显示与官方 Plex 客户端相同的主页推荐。关闭时将显示按媒体库分类的推荐。'; case 'settings.hardwareDecoding': return '硬件解码'; case 'settings.hardwareDecodingDescription': return '如果可用,使用硬件加速'; case 'settings.bufferSize': return '缓冲区大小'; diff --git a/lib/i18n/strings.i18n.json b/lib/i18n/strings.i18n.json index f32d6e06..14dee7a9 100644 --- a/lib/i18n/strings.i18n.json +++ b/lib/i18n/strings.i18n.json @@ -84,6 +84,8 @@ "listViewDescription": "Display items in a list layout", "useSeasonPosters": "Use Season Posters", "showHeroSection": "Show Hero Section", + "useGlobalHubs": "Use Plex Home Layout", + "useGlobalHubsDescription": "Show home page hubs like the official Plex client. When off, shows per-library recommendations instead.", "hardwareDecoding": "Hardware Decoding", "hardwareDecodingDescription": "Use hardware acceleration when available", "bufferSize": "Buffer Size", diff --git a/lib/i18n/strings_de.i18n.json b/lib/i18n/strings_de.i18n.json index 241e0dd0..874cbca3 100644 --- a/lib/i18n/strings_de.i18n.json +++ b/lib/i18n/strings_de.i18n.json @@ -85,6 +85,8 @@ "listViewDescription": "Elemente in Listenansicht anzeigen", "useSeasonPosters": "Staffelposter verwenden", "showHeroSection": "Hero-Bereich anzeigen", + "useGlobalHubs": "Plex-Startseiten-Layout verwenden", + "useGlobalHubsDescription": "Zeigt Startseiten-Hubs wie der offizielle Plex-Client. Wenn deaktiviert, werden stattdessen Empfehlungen pro Bibliothek angezeigt.", "hardwareDecoding": "Hardware-Decodierung", "hardwareDecodingDescription": "Hardwarebeschleunigung verwenden, sofern verfügbar", "bufferSize": "Puffergröße", diff --git a/lib/i18n/strings_it.i18n.json b/lib/i18n/strings_it.i18n.json index f53857ef..b7ea6220 100644 --- a/lib/i18n/strings_it.i18n.json +++ b/lib/i18n/strings_it.i18n.json @@ -85,6 +85,8 @@ "listViewDescription": "Visualizza gli elementi in un layout a elenco", "useSeasonPosters": "Usa poster delle stagioni", "showHeroSection": "Mostra sezione principale", + "useGlobalHubs": "Usa layout Home di Plex", + "useGlobalHubsDescription": "Mostra gli hub della home page come il client Plex ufficiale. Se disattivato, mostra invece i suggerimenti per libreria.", "hardwareDecoding": "Decodifica Hardware", "hardwareDecodingDescription": "Utilizza l'accelerazione hardware quando disponibile", "bufferSize": "Dimensione buffer", diff --git a/lib/i18n/strings_ko.i18n.json b/lib/i18n/strings_ko.i18n.json index 5d97b07c..50e2ccea 100644 --- a/lib/i18n/strings_ko.i18n.json +++ b/lib/i18n/strings_ko.i18n.json @@ -85,6 +85,8 @@ "listViewDescription": "항목을 목록 레이아웃으로 표시합니다", "useSeasonPosters": "시즌 포스터 사용", "showHeroSection": "주요 추천 영역 표시", + "useGlobalHubs": "Plex 홈 레이아웃 사용", + "useGlobalHubsDescription": "공식 Plex 클라이언트처럼 홈 페이지 허브를 표시합니다. 끄면 라이브러리별 추천이 대신 표시됩니다.", "hardwareDecoding": "하드웨어 디코딩", "hardwareDecodingDescription": "가능한 경우 하드웨어 가속을 사용합니다", "bufferSize": "버퍼 크기", diff --git a/lib/i18n/strings_nl.i18n.json b/lib/i18n/strings_nl.i18n.json index 0ff52cb7..04370240 100644 --- a/lib/i18n/strings_nl.i18n.json +++ b/lib/i18n/strings_nl.i18n.json @@ -85,6 +85,8 @@ "listViewDescription": "Items weergeven in een lijstindeling", "useSeasonPosters": "Gebruik seizoenposters", "showHeroSection": "Toon hoofdsectie", + "useGlobalHubs": "Plex Home-indeling gebruiken", + "useGlobalHubsDescription": "Toon startpagina-hubs zoals de officiële Plex-client. Indien uitgeschakeld, worden in plaats daarvan aanbevelingen per bibliotheek getoond.", "hardwareDecoding": "Hardware decodering", "hardwareDecodingDescription": "Gebruik hardware versnelling indien beschikbaar", "bufferSize": "Buffer grootte", diff --git a/lib/i18n/strings_sv.i18n.json b/lib/i18n/strings_sv.i18n.json index af8a48ed..2866267f 100644 --- a/lib/i18n/strings_sv.i18n.json +++ b/lib/i18n/strings_sv.i18n.json @@ -85,6 +85,8 @@ "listViewDescription": "Visa objekt i listlayout", "useSeasonPosters": "Använd säsongsaffischer", "showHeroSection": "Visa hjältesektion", + "useGlobalHubs": "Använd Plex hem-layout", + "useGlobalHubsDescription": "Visar startsidans hubbar som den officiella Plex-klienten. När av visas rekommendationer per bibliotek istället.", "hardwareDecoding": "Hårdvaruavkodning", "hardwareDecodingDescription": "Använd hårdvaruacceleration när tillgängligt", "bufferSize": "Bufferstorlek", diff --git a/lib/i18n/strings_zh.i18n.json b/lib/i18n/strings_zh.i18n.json index f914c73a..8bee6551 100644 --- a/lib/i18n/strings_zh.i18n.json +++ b/lib/i18n/strings_zh.i18n.json @@ -85,6 +85,8 @@ "listViewDescription": "以列表布局显示项目", "useSeasonPosters": "使用季海报", "showHeroSection": "显示主要精选区", + "useGlobalHubs": "使用 Plex 主页布局", + "useGlobalHubsDescription": "显示与官方 Plex 客户端相同的主页推荐。关闭时将显示按媒体库分类的推荐。", "hardwareDecoding": "硬件解码", "hardwareDecodingDescription": "如果可用,使用硬件加速", "bufferSize": "缓冲区大小", diff --git a/lib/providers/settings_provider.dart b/lib/providers/settings_provider.dart index ec7c9d82..4f114bcb 100644 --- a/lib/providers/settings_provider.dart +++ b/lib/providers/settings_provider.dart @@ -7,6 +7,7 @@ class SettingsProvider extends ChangeNotifier { ViewMode _viewMode = ViewMode.grid; bool _useSeasonPoster = false; bool _showHeroSection = true; + bool _useGlobalHubs = true; bool _isInitialized = false; Future? _initFuture; @@ -27,6 +28,7 @@ class SettingsProvider extends ChangeNotifier { _viewMode = _settingsService!.getViewMode(); _useSeasonPoster = _settingsService!.getUseSeasonPoster(); _showHeroSection = _settingsService!.getShowHeroSection(); + _useGlobalHubs = _settingsService!.getUseGlobalHubs(); _isInitialized = true; notifyListeners(); } @@ -42,6 +44,8 @@ class SettingsProvider extends ChangeNotifier { bool get showHeroSection => _showHeroSection; + bool get useGlobalHubs => _useGlobalHubs; + Future setLibraryDensity(LibraryDensity density) async { if (!_isInitialized) await _initializeSettings(); if (_libraryDensity != density) { @@ -78,6 +82,15 @@ class SettingsProvider extends ChangeNotifier { } } + Future setUseGlobalHubs(bool value) async { + if (!_isInitialized) await _initializeSettings(); + if (_useGlobalHubs != value) { + _useGlobalHubs = value; + await _settingsService!.setUseGlobalHubs(value); + notifyListeners(); + } + } + String get libraryDensityDisplayName { switch (_libraryDensity) { case LibraryDensity.compact: diff --git a/lib/screens/discover_screen.dart b/lib/screens/discover_screen.dart index 6d6e62ea..a4bc711f 100644 --- a/lib/screens/discover_screen.dart +++ b/lib/screens/discover_screen.dart @@ -479,16 +479,25 @@ class _DiscoverScreenState extends State }); try { - appLogger.d('Fetching onDeck and hubs from all Plex servers'); + appLogger.d('Fetching onDeck and global hubs from all Plex servers'); final multiServerProvider = Provider.of(context, listen: false); if (!multiServerProvider.hasConnectedServers) { throw Exception('No servers available'); } - // Start OnDeck and libraries fetch in parallel + // Get hidden libraries for filtering + final hiddenLibrariesProvider = Provider.of(context, listen: false); + + // Get settings for hub mode preference + final settingsProvider = Provider.of(context, listen: false); + + // Start OnDeck and hubs fetch in parallel final onDeckFuture = multiServerProvider.aggregationService.getOnDeckFromAllServers(limit: 20); - final librariesFuture = multiServerProvider.aggregationService.getLibrariesFromAllServersGrouped(); + final hubsFuture = multiServerProvider.aggregationService.getHubsFromAllServers( + hiddenLibraryKeys: hiddenLibrariesProvider.hiddenLibraryKeys, + useGlobalHubs: settingsProvider.useGlobalHubs, + ); // Wait for OnDeck to complete and show it immediately final onDeck = await onDeckFuture; @@ -511,32 +520,22 @@ class _DiscoverScreenState extends State _heroController.jumpToPage(0); } - // Wait for libraries and then fetch hubs - final librariesByServer = await librariesFuture; + // Wait for global hubs + final allHubs = await hubsFuture; if (!mounted) return; - // Get hidden libraries to filter from hubs - final hiddenLibrariesProvider = Provider.of(context, listen: false); - - // Fetch hubs using the pre-fetched libraries and hidden keys - final allHubs = await multiServerProvider.aggregationService.getHubsFromAllServers( - librariesByServer: librariesByServer, - hiddenLibraryKeys: hiddenLibrariesProvider.hiddenLibraryKeys, - ); - - // Filter out duplicate hubs that we already fetch separately + // Filter out Continue Watching / On Deck hubs (handled separately in hero section) final filteredHubs = allHubs.where((hub) { final hubId = hub.hubIdentifier?.toLowerCase() ?? ''; final title = hub.title.toLowerCase(); - // Skip "Continue Watching" and "On Deck" hubs (we handle these separately) return !hubId.contains('ondeck') && !hubId.contains('continue') && !title.contains('continue watching') && !title.contains('on deck'); }).toList(); - appLogger.d('Received ${onDeck.length} on deck items and ${filteredHubs.length} hubs from all servers'); + appLogger.d('Received ${onDeck.length} on deck items and ${filteredHubs.length} global hubs from all servers'); if (!mounted) return; setState(() { _hubs = filteredHubs; diff --git a/lib/screens/settings/settings_screen.dart b/lib/screens/settings/settings_screen.dart index 25774b59..67dcd6d5 100644 --- a/lib/screens/settings/settings_screen.dart +++ b/lib/screens/settings/settings_screen.dart @@ -213,6 +213,19 @@ class _SettingsScreenState extends State { ); }, ), + Consumer( + builder: (context, settingsProvider, child) { + return SwitchListTile( + secondary: const AppIcon(Symbols.home_rounded, fill: 1), + title: Text(t.settings.useGlobalHubs), + subtitle: Text(t.settings.useGlobalHubsDescription), + value: settingsProvider.useGlobalHubs, + onChanged: (value) async { + await settingsProvider.setUseGlobalHubs(value); + }, + ); + }, + ), ], ), ); diff --git a/lib/services/data_aggregation_service.dart b/lib/services/data_aggregation_service.dart index 2ef24851..22bdc8ad 100644 --- a/lib/services/data_aggregation_service.dart +++ b/lib/services/data_aggregation_service.dart @@ -98,11 +98,15 @@ class DataAggregationService { return librariesByServer; } - /// Fetch recommendation hubs from all servers using pre-fetched libraries + /// Fetch recommendation hubs from all servers + /// 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} Future> getHubsFromAllServers({ int? limit, - Map>? librariesByServer, Set? hiddenLibraryKeys, + Map>? librariesByServer, + bool useGlobalHubs = true, }) async { final clients = _serverManager.onlineClients; @@ -111,10 +115,98 @@ class DataAggregationService { return []; } + if (useGlobalHubs) { + return _fetchGlobalHubs(clients, limit: limit, hiddenLibraryKeys: hiddenLibraryKeys); + } else { + return _fetchLibraryHubs( + clients, + limit: limit, + hiddenLibraryKeys: hiddenLibraryKeys, + librariesByServer: librariesByServer, + ); + } + } + + /// Fetch global hubs using /hubs endpoint (matches official Plex client) + Future> _fetchGlobalHubs( + Map clients, { + int? limit, + Set? hiddenLibraryKeys, + }) async { + appLogger.d('Fetching global hubs from ${clients.length} servers'); + + final allHubs = []; + + // Fetch global hubs from all servers in parallel + final hubFutures = clients.entries.map((entry) async { + final serverId = entry.key; + final client = entry.value; + + try { + final hubs = await client.getGlobalHubs(limit: limit ?? 10); + appLogger.d('Fetched ${hubs.length} global hubs from server $serverId'); + + // Filter out items from hidden libraries if specified + if (hiddenLibraryKeys != null && hiddenLibraryKeys.isNotEmpty) { + return hubs.map((hub) { + final filteredItems = hub.items.where((item) { + // Build the global key for the item's library section + final librarySectionId = item.librarySectionID; + if (librarySectionId == null) return true; // Keep if no section ID + final globalKey = '$serverId:$librarySectionId'; + return !hiddenLibraryKeys.contains(globalKey); + }).toList(); + + if (filteredItems.isEmpty) return null; + + return PlexHub( + hubKey: hub.hubKey, + title: hub.title, + type: hub.type, + hubIdentifier: hub.hubIdentifier, + size: filteredItems.length, + more: hub.more, + items: filteredItems, + serverId: hub.serverId, + serverName: hub.serverName, + ); + }).whereType().toList(); + } + + return hubs; + } catch (e, stackTrace) { + appLogger.e('Failed to fetch hubs from server $serverId', error: e, stackTrace: stackTrace); + _serverManager.updateServerStatus(serverId, false); + return []; + } + }); + + final results = await Future.wait(hubFutures); + + // Flatten results + for (final hubs in results) { + allHubs.addAll(hubs); + } + + // Apply limit if specified (applied after merging from all servers) + final result = limit != null && limit < allHubs.length ? allHubs.sublist(0, limit) : allHubs; + + appLogger.i('Fetched ${result.length} global hubs from all servers'); + + return result; + } + + /// Fetch per-library hubs using /hubs/sections/{sectionId} endpoint + Future> _fetchLibraryHubs( + Map clients, { + int? limit, + Set? hiddenLibraryKeys, + Map>? librariesByServer, + }) async { // Use pre-fetched libraries or fetch them if not provided final libraries = librariesByServer ?? await getLibrariesFromAllServersGrouped(); - appLogger.d('Fetching hubs from ${clients.length} servers'); + appLogger.d('Fetching per-library hubs from ${clients.length} servers'); final allHubs = []; @@ -185,7 +277,7 @@ class DataAggregationService { // Apply limit if specified final result = limit != null && limit < allHubs.length ? allHubs.sublist(0, limit) : allHubs; - appLogger.i('Fetched ${result.length} hubs from all servers'); + appLogger.i('Fetched ${result.length} library hubs from all servers'); return result; } diff --git a/lib/services/plex_client.dart b/lib/services/plex_client.dart index d4c0a00e..f2ff1eba 100644 --- a/lib/services/plex_client.dart +++ b/lib/services/plex_client.dart @@ -1249,6 +1249,57 @@ class PlexClient { return []; } + /// Get global hubs (home page recommendations) + /// Returns actual home page hubs like "Recently Added Movies", "Recently Added TV", etc. + /// This matches the official Plex client's home page layout. + Future> getGlobalHubs({int limit = 10}) async { + try { + final response = await _dio.get( + '/hubs', + queryParameters: {'count': limit, 'includeGuids': 1}, + ); + + final container = _getMediaContainer(response); + if (container != null && container['Hub'] != null) { + final hubs = []; + for (final hubJson in container['Hub'] as List) { + try { + final hub = PlexHub.fromJson(hubJson); + if (hub.items.isEmpty) continue; + + // Filter to only video content (movies, shows, episodes) and tag with server info + final videoItems = hub.items + .where((item) => item.isMovie || item.isShow || item.isEpisode) + .map((item) => item.copyWith(serverId: serverId, serverName: serverName)) + .toList(); + + if (videoItems.isNotEmpty) { + hubs.add( + PlexHub( + hubKey: hub.hubKey, + title: hub.title, + type: hub.type, + hubIdentifier: hub.hubIdentifier, + size: hub.size, + more: hub.more, + items: videoItems, + serverId: serverId, + serverName: serverName, + ), + ); + } + } catch (e) { + appLogger.w('Failed to parse global hub', error: e); + } + } + return hubs; + } + } catch (e) { + appLogger.e('Failed to get global hubs: $e'); + } + return []; + } + /// Get full content from a hub using its hub key /// Returns the complete list of metadata items in the hub Future> getHubContent(String hubKey) async { diff --git a/lib/services/settings_service.dart b/lib/services/settings_service.dart index cf297e9b..8f40371b 100644 --- a/lib/services/settings_service.dart +++ b/lib/services/settings_service.dart @@ -30,6 +30,7 @@ class SettingsService extends BaseSharedPreferencesService { static const String _keySeekTimeLarge = 'seek_time_large'; static const String _keyMediaVersionPreferences = 'media_version_preferences'; static const String _keyShowHeroSection = 'show_hero_section'; + static const String _keyUseGlobalHubs = 'use_global_hubs'; static const String _keySleepTimerDuration = 'sleep_timer_duration'; static const String _keyAudioSyncOffset = 'audio_sync_offset'; static const String _keySubtitleSyncOffset = 'subtitle_sync_offset'; @@ -170,6 +171,15 @@ class SettingsService extends BaseSharedPreferencesService { return prefs.getBool(_keyShowHeroSection) ?? true; // Default: true (show hero section) } + // Use Global Hubs (true = global /hubs endpoint, false = per-library hubs) + Future setUseGlobalHubs(bool enabled) async { + await prefs.setBool(_keyUseGlobalHubs, enabled); + } + + bool getUseGlobalHubs() { + return prefs.getBool(_keyUseGlobalHubs) ?? true; // Default: true (use global hubs like official Plex) + } + // Seek Time Small (in seconds) Future setSeekTimeSmall(int seconds) async { await prefs.setInt(_keySeekTimeSmall, seconds);