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:
edde746
2026-03-15 18:23:32 +01:00
parent a5629c54f6
commit 59b3e6ce81
7 changed files with 167 additions and 235 deletions
+121 -50
View File
@@ -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.