fix: eliminate duplicate API requests on startup
- Skip redundant notifyListeners in HiddenLibrariesProvider init when set is empty (empty→empty is a no-op for consumers) - Track last-seen hidden keys in DiscoverScreen to avoid reloading content when the listener fires without an actual change - Track previous online server set in MultiServerProvider; only re-check DVR availability when a new server comes online - Remove dead content-loading pipeline from libraries_screen (sorts, pagination, filters, items) that was never rendered - Move /media/providers fetch into PlexClient.create() init, replacing lazy EPG cache and providing libraries including shared items
This commit is contained in:
@@ -31,8 +31,12 @@ class HiddenLibrariesProvider extends ChangeNotifier {
|
||||
_storageService = await StorageService.getInstance();
|
||||
_hiddenLibraryKeys = _storageService.getHiddenLibraries();
|
||||
_isInitialized = true;
|
||||
// Only notify if there are actually hidden libraries — the default empty
|
||||
// set is already visible to consumers, so empty→empty is a no-op.
|
||||
if (_hiddenLibraryKeys.isNotEmpty) {
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
/// Hide a library by its key
|
||||
/// Updates both in-memory state and persistent storage
|
||||
|
||||
@@ -2,6 +2,7 @@ import 'dart:async';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
|
||||
import '../models/livetv_dvr.dart';
|
||||
import '../services/plex_client.dart';
|
||||
import '../services/data_aggregation_service.dart';
|
||||
import '../services/multi_server_manager.dart';
|
||||
@@ -14,7 +15,10 @@ class LiveTvServerInfo {
|
||||
final String dvrKey;
|
||||
final String? lineup;
|
||||
|
||||
LiveTvServerInfo({required this.serverId, required this.dvrKey, this.lineup});
|
||||
/// Full DVR objects including channel mappings (avoids re-fetching in LiveTvScreen)
|
||||
final List<LiveTvDvr> dvrs;
|
||||
|
||||
LiveTvServerInfo({required this.serverId, required this.dvrKey, this.lineup, this.dvrs = const []});
|
||||
}
|
||||
|
||||
/// Provider for multi-server Plex connections
|
||||
@@ -32,12 +36,22 @@ class MultiServerProvider extends ChangeNotifier {
|
||||
final List<LiveTvServerInfo> _liveTvServers = [];
|
||||
List<LiveTvServerInfo> get liveTvServers => List.unmodifiable(_liveTvServers);
|
||||
|
||||
/// Previously-seen set of online server IDs, used to detect new servers
|
||||
Set<String> _previousOnlineServerIds = {};
|
||||
|
||||
MultiServerProvider(this._serverManager, this._aggregationService) {
|
||||
// Listen to server status changes
|
||||
_statusSubscription = _serverManager.statusStream.listen((_) {
|
||||
final currentOnline = Set<String>.from(onlineServerIds);
|
||||
final hasNewServer = currentOnline.any((id) => !_previousOnlineServerIds.contains(id));
|
||||
_previousOnlineServerIds = currentOnline;
|
||||
|
||||
notifyListeners();
|
||||
// Re-check live TV availability when servers come online
|
||||
|
||||
// Only re-check live TV when a new server came online
|
||||
if (hasNewServer) {
|
||||
checkLiveTvAvailability();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -123,7 +137,7 @@ class MultiServerProvider extends ChangeNotifier {
|
||||
try {
|
||||
final dvrs = await client.getDvrs();
|
||||
for (final dvr in dvrs) {
|
||||
newLiveTvServers.add(LiveTvServerInfo(serverId: serverId, dvrKey: dvr.key, lineup: dvr.lineup));
|
||||
newLiveTvServers.add(LiveTvServerInfo(serverId: serverId, dvrKey: dvr.key, lineup: dvr.lineup, dvrs: dvrs));
|
||||
}
|
||||
} catch (e) {
|
||||
appLogger.d('LiveTV check failed for server $serverId', error: e);
|
||||
|
||||
@@ -84,6 +84,7 @@ class _DiscoverScreenState extends State<DiscoverScreen>
|
||||
final ValueNotifier<double> _indicatorProgress = ValueNotifier(0.0);
|
||||
bool _isAutoScrollPaused = false;
|
||||
HiddenLibrariesProvider? _hiddenLibrariesProvider;
|
||||
Set<String> _lastSeenHiddenKeys = {};
|
||||
|
||||
// WatchStateAware: watch on-deck items and their parent shows/seasons
|
||||
@override
|
||||
@@ -258,6 +259,11 @@ class _DiscoverScreenState extends State<DiscoverScreen>
|
||||
}
|
||||
|
||||
void _onHiddenLibrariesChanged() {
|
||||
final currentKeys = _hiddenLibrariesProvider?.hiddenLibraryKeys ?? {};
|
||||
if (currentKeys.length == _lastSeenHiddenKeys.length && currentKeys.containsAll(_lastSeenHiddenKeys)) {
|
||||
return; // No actual change
|
||||
}
|
||||
_lastSeenHiddenKeys = Set.of(currentKeys);
|
||||
_loadContent();
|
||||
}
|
||||
|
||||
@@ -451,9 +457,17 @@ class _DiscoverScreenState extends State<DiscoverScreen>
|
||||
|
||||
// Get hidden libraries for filtering
|
||||
final hiddenLibrariesProvider = Provider.of<HiddenLibrariesProvider>(context, listen: false);
|
||||
_lastSeenHiddenKeys = Set.of(hiddenLibrariesProvider.hiddenLibraryKeys);
|
||||
|
||||
// Get settings for hub mode preference (ensure initialized before accessing)
|
||||
final settingsProvider = Provider.of<SettingsProvider>(context, listen: false);
|
||||
|
||||
// Reuse already-loaded libraries to avoid a redundant API call inside hub fetching
|
||||
final librariesProvider = context.read<LibrariesProvider>();
|
||||
final librariesByServer = librariesProvider.libraries.isNotEmpty
|
||||
? multiServerProvider.aggregationService.groupLibrariesByServer(librariesProvider.libraries)
|
||||
: null;
|
||||
|
||||
await settingsProvider.ensureInitialized();
|
||||
|
||||
// Start OnDeck and hubs fetch in parallel
|
||||
@@ -464,6 +478,7 @@ class _DiscoverScreenState extends State<DiscoverScreen>
|
||||
final hubsFuture = multiServerProvider.aggregationService.getHubsFromAllServers(
|
||||
hiddenLibraryKeys: hiddenLibrariesProvider.hiddenLibraryKeys,
|
||||
useGlobalHubs: settingsProvider.useGlobalHubs,
|
||||
librariesByServer: librariesByServer,
|
||||
);
|
||||
|
||||
// Wait for OnDeck to complete and show it immediately
|
||||
|
||||
@@ -3,7 +3,6 @@ import 'package:plezy/widgets/app_icon.dart';
|
||||
import 'package:material_symbols_icons/symbols.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:dio/dio.dart';
|
||||
import '../../focus/focus_theme.dart';
|
||||
import '../../focus/focusable_action_bar.dart';
|
||||
import '../../focus/focusable_button.dart';
|
||||
@@ -14,7 +13,6 @@ import '../../mixins/tab_navigation_mixin.dart';
|
||||
import '../../../services/plex_client.dart';
|
||||
import '../../models/plex_library.dart';
|
||||
import '../../models/plex_metadata.dart';
|
||||
import '../../models/plex_sort.dart';
|
||||
import '../../providers/hidden_libraries_provider.dart';
|
||||
import '../../providers/libraries_provider.dart';
|
||||
import '../../providers/multi_server_provider.dart';
|
||||
@@ -30,7 +28,6 @@ import '../../services/storage_service.dart';
|
||||
import '../../mixins/refreshable.dart';
|
||||
import '../../mixins/item_updatable.dart';
|
||||
import '../../i18n/strings.g.dart';
|
||||
import '../../utils/error_message_utils.dart';
|
||||
import 'state_messages.dart';
|
||||
import 'tabs/library_browse_tab.dart';
|
||||
import 'tabs/library_recommended_tab.dart';
|
||||
@@ -96,15 +93,6 @@ class _LibrariesScreenState extends State<LibrariesScreen>
|
||||
String? _selectedLibraryGlobalKey;
|
||||
bool _isInitialLoad = true;
|
||||
|
||||
Map<String, String> _selectedFilters = {};
|
||||
PlexSort? _selectedSort;
|
||||
bool _isSortDescending = false;
|
||||
List<PlexMetadata> _items = [];
|
||||
int _currentPage = 0;
|
||||
bool _hasMoreItems = true;
|
||||
CancelToken? _cancelToken;
|
||||
int _requestId = 0;
|
||||
static const int _pageSize = 1000;
|
||||
|
||||
/// Flag to prevent onTabChanged from focusing when we're programmatically changing tabs
|
||||
bool _isRestoringTab = false;
|
||||
@@ -182,10 +170,6 @@ class _LibrariesScreenState extends State<LibrariesScreen>
|
||||
}
|
||||
|
||||
if (libraryGlobalKeyToLoad != null && mounted) {
|
||||
final savedFilters = storage.getLibraryFilters(sectionId: libraryGlobalKeyToLoad);
|
||||
if (savedFilters.isNotEmpty) {
|
||||
_selectedFilters = Map.from(savedFilters);
|
||||
}
|
||||
_loadLibraryContent(libraryGlobalKeyToLoad);
|
||||
}
|
||||
}
|
||||
@@ -337,7 +321,6 @@ class _LibrariesScreenState extends State<LibrariesScreen>
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_cancelToken?.cancel();
|
||||
_outerScrollController.dispose();
|
||||
_recommendedTabChipFocusNode.dispose();
|
||||
_browseTabChipFocusNode.dispose();
|
||||
@@ -352,15 +335,6 @@ class _LibrariesScreenState extends State<LibrariesScreen>
|
||||
setState(fn);
|
||||
}
|
||||
|
||||
/// Helper method to get user-friendly error message from exception
|
||||
String _getErrorMessage(dynamic error, String context) {
|
||||
if (error is DioException) {
|
||||
return mapDioErrorToMessage(error, context: context);
|
||||
}
|
||||
|
||||
return mapUnexpectedErrorToMessage(error, context: context);
|
||||
}
|
||||
|
||||
/// Check if libraries come from multiple servers
|
||||
bool _hasMultipleServers(List<PlexLibrary> libraries) {
|
||||
final uniqueServerIds = libraries.where((lib) => lib.serverId != null).map((lib) => lib.serverId).toSet();
|
||||
@@ -392,22 +366,11 @@ class _LibrariesScreenState extends State<LibrariesScreen>
|
||||
final libraryIndex = visibleLibraries.indexWhere((lib) => lib.globalKey == libraryGlobalKey);
|
||||
if (libraryIndex == -1) return; // Library not found or hidden
|
||||
|
||||
final library = visibleLibraries[libraryIndex];
|
||||
|
||||
final isChangingLibrary = !_isInitialLoad && _selectedLibraryGlobalKey != libraryGlobalKey;
|
||||
|
||||
// Get the correct client for this library's server
|
||||
final client = context.getClientForLibrary(library);
|
||||
|
||||
_updateState(() {
|
||||
_selectedLibraryGlobalKey = libraryGlobalKey;
|
||||
_errorMessage = null;
|
||||
// Clear loaded tabs tracking for new library
|
||||
_loadedTabs.clear();
|
||||
// Only clear filters when explicitly changing library (not on initial load)
|
||||
if (isChangingLibrary) {
|
||||
_selectedFilters.clear();
|
||||
}
|
||||
});
|
||||
|
||||
// Mark that initial load is complete
|
||||
@@ -439,137 +402,11 @@ class _LibrariesScreenState extends State<LibrariesScreen>
|
||||
}
|
||||
});
|
||||
|
||||
// Cancel any existing requests
|
||||
_cancelToken?.cancel();
|
||||
_cancelToken = CancelToken();
|
||||
final currentRequestId = ++_requestId;
|
||||
|
||||
// Reset pagination state
|
||||
_updateState(() {
|
||||
_currentPage = 0;
|
||||
_hasMoreItems = true;
|
||||
_items = [];
|
||||
});
|
||||
|
||||
try {
|
||||
// Load sort options for the new library
|
||||
await _loadSortOptions(library);
|
||||
|
||||
final filtersWithSort = _buildFiltersWithSort();
|
||||
|
||||
// Load pages sequentially
|
||||
await _loadAllPagesSequentially(library, filtersWithSort, currentRequestId, client);
|
||||
} catch (e) {
|
||||
// Ignore cancellation errors
|
||||
if (e is DioException && e.type == DioExceptionType.cancel) {
|
||||
return;
|
||||
}
|
||||
|
||||
_updateState(() {
|
||||
_errorMessage = _getErrorMessage(e, 'library content');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// Load all pages sequentially until all items are fetched
|
||||
Future<void> _loadAllPagesSequentially(
|
||||
PlexLibrary library,
|
||||
Map<String, String> filtersWithSort,
|
||||
int requestId,
|
||||
PlexClient client,
|
||||
) async {
|
||||
while (_hasMoreItems && requestId == _requestId) {
|
||||
try {
|
||||
final result = await client.getLibraryContent(
|
||||
library.key,
|
||||
start: _currentPage * _pageSize,
|
||||
size: _pageSize,
|
||||
filters: filtersWithSort,
|
||||
cancelToken: _cancelToken,
|
||||
);
|
||||
|
||||
// Tag items with server info for multi-server support
|
||||
final taggedItems = result.items
|
||||
.map((item) => item.copyWith(serverId: library.serverId, serverName: library.serverName))
|
||||
.toList();
|
||||
|
||||
// Check if request is still valid
|
||||
if (requestId != _requestId) {
|
||||
return; // Request was superseded
|
||||
}
|
||||
|
||||
_updateState(() {
|
||||
_items.addAll(taggedItems);
|
||||
_currentPage++;
|
||||
_hasMoreItems = taggedItems.length >= _pageSize;
|
||||
});
|
||||
} catch (e) {
|
||||
// Check if it's a cancellation
|
||||
if (e is DioException && e.type == DioExceptionType.cancel) {
|
||||
return;
|
||||
}
|
||||
|
||||
// For other errors, update state and rethrow
|
||||
_updateState(() {
|
||||
_hasMoreItems = false;
|
||||
});
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _loadSortOptions(PlexLibrary library) async {
|
||||
try {
|
||||
final client = context.getClientForLibrary(library);
|
||||
|
||||
final sortOptions = await client.getLibrarySorts(library.key);
|
||||
|
||||
// Load saved sort preference for this library
|
||||
final storage = await StorageService.getInstance();
|
||||
final savedSortData = storage.getLibrarySort(library.globalKey);
|
||||
|
||||
// Find the saved sort in the options
|
||||
PlexSort? savedSort;
|
||||
bool descending = false;
|
||||
|
||||
if (savedSortData != null) {
|
||||
final sortKey = savedSortData['key'] as String?;
|
||||
if (sortKey != null) {
|
||||
savedSort = sortOptions.firstWhere((s) => s.key == sortKey, orElse: () => sortOptions.first);
|
||||
descending = (savedSortData['descending'] as bool?) ?? false;
|
||||
} else {
|
||||
savedSort = sortOptions.first;
|
||||
}
|
||||
} else {
|
||||
savedSort = sortOptions.first;
|
||||
}
|
||||
|
||||
_updateState(() {
|
||||
_selectedSort = savedSort;
|
||||
_isSortDescending = descending;
|
||||
});
|
||||
} catch (e) {
|
||||
_updateState(() {
|
||||
_selectedSort = null;
|
||||
_isSortDescending = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Map<String, String> _buildFiltersWithSort() {
|
||||
final filtersWithSort = Map<String, String>.from(_selectedFilters);
|
||||
if (_selectedSort != null) {
|
||||
filtersWithSort['sort'] = _selectedSort!.getSortKey(descending: _isSortDescending);
|
||||
}
|
||||
return filtersWithSort;
|
||||
}
|
||||
|
||||
@override
|
||||
void updateItemInLists(String ratingKey, PlexMetadata updatedMetadata) {
|
||||
final index = _items.indexWhere((item) => item.ratingKey == ratingKey);
|
||||
if (index != -1) {
|
||||
_items[index] = updatedMetadata;
|
||||
}
|
||||
// Delegate to the active tab — parent doesn't maintain its own item list
|
||||
}
|
||||
|
||||
// Public method to refresh content (for normal navigation)
|
||||
@@ -597,8 +434,6 @@ class _LibrariesScreenState extends State<LibrariesScreen>
|
||||
appLogger.d('LibrariesScreen.fullRefresh() called - reloading all content');
|
||||
setState(() {
|
||||
_selectedLibraryGlobalKey = null;
|
||||
_selectedFilters.clear();
|
||||
_items.clear();
|
||||
_errorMessage = null;
|
||||
});
|
||||
|
||||
|
||||
@@ -130,22 +130,15 @@ class _LiveTvScreenState extends State<LiveTvScreen>
|
||||
'Live TV DVRs: ${liveTvServers.map((s) => '${s.serverId}/${s.dvrKey} lineup=${s.lineup}').join(', ')}',
|
||||
);
|
||||
|
||||
// Build a set of enabled channel keys per server from DVR mappings
|
||||
// Build a set of enabled channel keys per server from cached DVR data
|
||||
final enabledKeysByServer = <String, Set<String>>{};
|
||||
final queriedServers = <String>{};
|
||||
final processedServers = <String>{};
|
||||
for (final serverInfo in liveTvServers) {
|
||||
if (!queriedServers.add(serverInfo.serverId)) continue;
|
||||
try {
|
||||
final client = multiServer.getClientForServer(serverInfo.serverId);
|
||||
if (client == null) continue;
|
||||
final dvrs = await client.getDvrs();
|
||||
final enabledKeys = _extractEnabledChannelKeys(dvrs);
|
||||
if (!processedServers.add(serverInfo.serverId)) continue;
|
||||
final enabledKeys = _extractEnabledChannelKeys(serverInfo.dvrs);
|
||||
if (enabledKeys != null) {
|
||||
enabledKeysByServer[serverInfo.serverId] = enabledKeys;
|
||||
}
|
||||
} catch (e) {
|
||||
appLogger.e('Failed to load DVR mappings for server ${serverInfo.serverId}', error: e);
|
||||
}
|
||||
}
|
||||
|
||||
for (final serverInfo in liveTvServers) {
|
||||
|
||||
@@ -110,7 +110,7 @@ class MultiServerManager {
|
||||
clientIdentifier: clientIdentifier,
|
||||
);
|
||||
|
||||
final client = PlexClient(
|
||||
final client = await PlexClient.create(
|
||||
config,
|
||||
serverId: serverId,
|
||||
serverName: server.name,
|
||||
|
||||
+121
-50
@@ -136,6 +136,12 @@ class PlexClient {
|
||||
/// Whether to operate in offline mode (use cache only)
|
||||
bool _offlineMode = false;
|
||||
|
||||
/// Libraries parsed from /media/providers (includes individually shared items)
|
||||
late final List<PlexLibrary> _providerLibraries;
|
||||
|
||||
/// EPG providers parsed from /media/providers
|
||||
late final List<({String identifier, String gridEndpoint})> _providerEpg;
|
||||
|
||||
/// Set offline mode - when true, only cached responses are returned
|
||||
void setOfflineMode(bool offline) {
|
||||
_offlineMode = offline;
|
||||
@@ -157,7 +163,29 @@ class PlexClient {
|
||||
return utf8.decode(responseBytes, allowMalformed: true);
|
||||
}
|
||||
|
||||
PlexClient(
|
||||
/// Create a fully initialized PlexClient.
|
||||
/// Fetches /media/providers to discover libraries (including individually shared items) and EPG providers.
|
||||
static Future<PlexClient> create(
|
||||
PlexConfig config, {
|
||||
required String serverId,
|
||||
String? serverName,
|
||||
List<String>? prioritizedEndpoints,
|
||||
Future<void> Function(String newBaseUrl)? onEndpointChanged,
|
||||
VoidCallback? onAllEndpointsExhausted,
|
||||
}) async {
|
||||
final client = PlexClient._(
|
||||
config,
|
||||
serverId: serverId,
|
||||
serverName: serverName,
|
||||
prioritizedEndpoints: prioritizedEndpoints,
|
||||
onEndpointChanged: onEndpointChanged,
|
||||
onAllEndpointsExhausted: onAllEndpointsExhausted,
|
||||
);
|
||||
await client._initMediaProviders();
|
||||
return client;
|
||||
}
|
||||
|
||||
PlexClient._(
|
||||
this.config, {
|
||||
required this.serverId,
|
||||
this.serverName,
|
||||
@@ -203,6 +231,91 @@ class PlexClient {
|
||||
}
|
||||
}
|
||||
|
||||
/// Fetch /media/providers and parse libraries + EPG providers from the response.
|
||||
/// This discovers individually shared items that don't appear in /library/sections.
|
||||
Future<void> _initMediaProviders() async {
|
||||
try {
|
||||
final response = await _dio.get('/media/providers');
|
||||
final container = _getMediaContainer(response);
|
||||
if (container == null) {
|
||||
_providerLibraries = [];
|
||||
_providerEpg = [];
|
||||
return;
|
||||
}
|
||||
|
||||
final providers = container['MediaProvider'] as List?;
|
||||
if (providers == null) {
|
||||
_providerLibraries = [];
|
||||
_providerEpg = [];
|
||||
return;
|
||||
}
|
||||
|
||||
// Parse libraries from the library provider
|
||||
final libraries = <PlexLibrary>[];
|
||||
final epg = <({String identifier, String gridEndpoint})>[];
|
||||
|
||||
for (final provider in providers) {
|
||||
if (provider is! Map) continue;
|
||||
final identifier = provider['identifier'] as String?;
|
||||
if (identifier == null) continue;
|
||||
|
||||
final features = provider['Feature'] as List?;
|
||||
if (features == null) continue;
|
||||
|
||||
// Library provider — extract directories as libraries
|
||||
if (identifier == 'com.plexapp.plugins.library') {
|
||||
for (final feature in features) {
|
||||
if (feature is! Map) continue;
|
||||
if (feature['type'] != 'content') continue;
|
||||
|
||||
final directories = feature['Directory'] as List?;
|
||||
if (directories == null) continue;
|
||||
|
||||
for (final dir in directories) {
|
||||
if (dir is! Map<String, dynamic>) continue;
|
||||
|
||||
// Skip entries without id (Home hub) and playlists
|
||||
final id = dir['id'] as String?;
|
||||
if (id == null) continue;
|
||||
if (dir['type'] == 'playlist') continue;
|
||||
|
||||
// Set key = id so downstream code gets a plain section ID (e.g. "1")
|
||||
final json = Map<String, dynamic>.from(dir);
|
||||
json['key'] = id;
|
||||
|
||||
libraries.add(
|
||||
PlexLibrary.fromJson(json).copyWith(serverId: serverId, serverName: serverName),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// EPG provider — extract grid endpoints
|
||||
final protocols = provider['protocols'] as String?;
|
||||
if (protocols != null && protocols.contains('livetv')) {
|
||||
for (final feature in features) {
|
||||
if (feature is! Map) continue;
|
||||
if (feature['type'] == 'grid') {
|
||||
final gridEndpoint = feature['key'] as String?;
|
||||
if (gridEndpoint != null) {
|
||||
epg.add((identifier: identifier, gridEndpoint: gridEndpoint));
|
||||
appLogger.d('Discovered EPG provider: $identifier (grid: $gridEndpoint)');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_providerLibraries = libraries;
|
||||
_providerEpg = epg;
|
||||
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);
|
||||
_providerLibraries = [];
|
||||
_providerEpg = [];
|
||||
}
|
||||
}
|
||||
|
||||
/// Update the token used by this client
|
||||
void updateToken(String newToken) {
|
||||
// Update both the Dio headers and the config to ensure consistency
|
||||
@@ -413,8 +526,12 @@ class PlexClient {
|
||||
}
|
||||
|
||||
/// Get library sections
|
||||
/// Returns libraries automatically tagged with this client's serverId and serverName
|
||||
/// Returns libraries automatically tagged with this client's serverId and serverName.
|
||||
/// Prefers /media/providers data (includes individually shared items),
|
||||
/// falls back to /library/sections for old servers.
|
||||
Future<List<PlexLibrary>> getLibraries() async {
|
||||
if (_providerLibraries.isNotEmpty) return _providerLibraries;
|
||||
// Fallback for old servers that don't support /media/providers
|
||||
final response = await _dio.get('/library/sections');
|
||||
return _extractLibraryList(response);
|
||||
}
|
||||
@@ -2112,55 +2229,9 @@ class PlexClient {
|
||||
}, 'Failed to get EPG channels');
|
||||
}
|
||||
|
||||
/// Cached EPG providers (discovered from /media/providers)
|
||||
List<({String identifier, String gridEndpoint})>? _epgProviders;
|
||||
|
||||
/// Discover all EPG providers from media providers
|
||||
/// Return EPG providers (already parsed from /media/providers during initialization)
|
||||
Future<List<({String identifier, String gridEndpoint})>> _discoverEpgProviders() async {
|
||||
if (_epgProviders != null) return _epgProviders!;
|
||||
|
||||
try {
|
||||
final response = await _dio.get('/media/providers');
|
||||
final container = _getMediaContainer(response);
|
||||
if (container == null) return [];
|
||||
|
||||
final providers = container['MediaProvider'] as List?;
|
||||
if (providers == null) return [];
|
||||
|
||||
final results = <({String identifier, String gridEndpoint})>[];
|
||||
|
||||
for (final provider in providers) {
|
||||
if (provider is! Map) continue;
|
||||
final protocols = provider['protocols'] as String?;
|
||||
if (protocols == null || !protocols.contains('livetv')) continue;
|
||||
|
||||
final identifier = provider['identifier'] as String?;
|
||||
if (identifier == null) continue;
|
||||
|
||||
final features = provider['Feature'] as List?;
|
||||
if (features == null) continue;
|
||||
for (final feature in features) {
|
||||
if (feature is! Map) continue;
|
||||
if (feature['type'] == 'grid') {
|
||||
final gridEndpoint = feature['key'] as String?;
|
||||
if (gridEndpoint != null) {
|
||||
results.add((identifier: identifier, gridEndpoint: gridEndpoint));
|
||||
appLogger.d('Discovered EPG provider: $identifier (grid: $gridEndpoint)');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_epgProviders = results;
|
||||
appLogger.d('Discovered ${results.length} EPG provider(s)');
|
||||
if (results.isEmpty) {
|
||||
appLogger.w('No EPG providers found');
|
||||
}
|
||||
return results;
|
||||
} catch (e) {
|
||||
appLogger.e('Failed to discover EPG providers', error: e);
|
||||
}
|
||||
return [];
|
||||
return _providerEpg;
|
||||
}
|
||||
|
||||
/// Parse a list of JSON items into [LiveTvProgram] objects, skipping any that fail.
|
||||
|
||||
Reference in New Issue
Block a user