fix(tv): query all EPG providers for multi-DVR support

This commit is contained in:
edde746
2026-02-13 14:16:44 +01:00
parent 426f6ab6ba
commit a4d4e2e1f8
4 changed files with 121 additions and 94 deletions
+7 -1
View File
@@ -107,6 +107,7 @@ class _LiveTvScreenState extends State<LiveTvScreen> with SingleTickerProviderSt
}
final allChannels = <LiveTvChannel>[];
final seenChannels = <String>{};
for (final serverInfo in liveTvServers) {
try {
@@ -114,7 +115,12 @@ class _LiveTvScreenState extends State<LiveTvScreen> with SingleTickerProviderSt
if (client == null) continue;
final channels = await client.getEpgChannels(lineup: serverInfo.lineup);
allChannels.addAll(channels);
for (final channel in channels) {
final dedupKey = '${serverInfo.serverId}:${channel.identifier ?? channel.key}';
if (seenChannels.add(dedupKey)) {
allChannels.add(channel);
}
}
} catch (e) {
appLogger.e('Failed to load channels from server ${serverInfo.serverId}', error: e);
}
+2 -1
View File
@@ -198,8 +198,10 @@ class GuideTabState extends State<GuideTab> {
final multiServer = context.read<MultiServerProvider>();
final liveTvServers = multiServer.liveTvServers;
final allPrograms = <LiveTvProgram>[];
final queriedServers = <String>{};
for (final serverInfo in liveTvServers) {
if (!queriedServers.add(serverInfo.serverId)) continue;
try {
final client = multiServer.getClientForServer(serverInfo.serverId);
if (client == null) continue;
@@ -208,7 +210,6 @@ class GuideTabState extends State<GuideTab> {
final endEpoch = _gridEnd.millisecondsSinceEpoch ~/ 1000;
final programs = await client.getEpgGrid(
lineup: serverInfo.lineup,
beginsAt: startEpoch,
endsAt: endEpoch,
);
@@ -76,8 +76,10 @@ class WhatsOnTabState extends State<WhatsOnTab> {
final multiServer = context.read<MultiServerProvider>();
final liveTvServers = multiServer.liveTvServers;
final allHubs = <LiveTvHubResult>[];
final queriedServers = <String>{};
for (final serverInfo in liveTvServers) {
if (!queriedServers.add(serverInfo.serverId)) continue;
try {
final client = multiServer.getClientForServer(serverInfo.serverId);
if (client == null) continue;
+53 -35
View File
@@ -2023,67 +2023,75 @@ class PlexClient {
);
}
/// Cached EPG grid endpoint path (discovered from /media/providers)
String? _epgGridEndpoint;
/// Cached EPG providers (discovered from /media/providers)
List<({String identifier, String gridEndpoint})>? _epgProviders;
/// Cached EPG provider identifier (e.g. "tv.plex.providers.epg.xmltv:21")
String? _epgProviderIdentifier;
/// Discover the EPG grid endpoint from media providers
Future<String?> _getEpgGridEndpoint() async {
if (_epgGridEndpoint != null) return _epgGridEndpoint;
/// Discover all EPG providers from media providers
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 null;
if (container == null) return [];
final providers = container['MediaProvider'] as List?;
if (providers == null) return null;
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;
_epgProviderIdentifier = provider['identifier'] as String?;
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') {
_epgGridEndpoint = feature['key'] as String?;
appLogger.d('Discovered EPG grid endpoint: $_epgGridEndpoint (provider: $_epgProviderIdentifier)');
return _epgGridEndpoint;
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;
if (results.isEmpty) {
appLogger.w('No EPG providers found');
}
return results;
} catch (e) {
appLogger.e('Failed to discover EPG grid endpoint', error: e);
appLogger.e('Failed to discover EPG providers', error: e);
}
return null;
return [];
}
/// Get guide/program data for channels (EPG grid data)
/// Discovers the grid endpoint from /media/providers on first call
/// Discovers grid endpoints from /media/providers on first call and queries all providers
Future<List<LiveTvProgram>> getEpgGrid({
String? lineup,
int? beginsAt,
int? endsAt,
}) async {
final gridEndpoint = await _getEpgGridEndpoint();
if (gridEndpoint == null) {
appLogger.w('No EPG grid endpoint found');
return [];
}
final providers = await _discoverEpgProviders();
if (providers.isEmpty) return [];
final queryParams = <String, dynamic>{};
if (beginsAt != null) queryParams['beginsAt>'] = beginsAt;
if (endsAt != null) queryParams['endsAt<'] = endsAt;
return _wrapListApiCall<LiveTvProgram>(
() => _dio.get(gridEndpoint, queryParameters: queryParams),
final allPrograms = <LiveTvProgram>[];
for (final provider in providers) {
try {
final programs = await _wrapListApiCall<LiveTvProgram>(
() => _dio.get(provider.gridEndpoint, queryParameters: queryParams),
(response) {
final container = _getMediaContainer(response);
final programs = <LiveTvProgram>[];
@@ -2108,19 +2116,29 @@ class PlexClient {
}
return programs;
},
'Failed to get EPG grid',
'Failed to get EPG grid from ${provider.identifier}',
);
allPrograms.addAll(programs);
} catch (e) {
appLogger.e('Failed to get EPG grid from provider ${provider.identifier}', error: e);
}
}
/// Get live TV hubs (What's On Now, etc.) from the EPG provider's discover endpoint.
return allPrograms;
}
/// Get live TV hubs (What's On Now, etc.) from all EPG providers' discover endpoints.
/// Returns hubs with both display metadata and EPG timing/channel data per item.
Future<List<LiveTvHubResult>> getLiveTvHubs({int count = 12}) async {
await _getEpgGridEndpoint();
if (_epgProviderIdentifier == null) return [];
final providers = await _discoverEpgProviders();
if (providers.isEmpty) return [];
final allHubs = <LiveTvHubResult>[];
for (final provider in providers) {
try {
final response = await _dio.get(
'/$_epgProviderIdentifier/hubs/discover',
'/${provider.identifier}/hubs/discover',
queryParameters: {
'count': count,
'includeStations': 1,
@@ -2132,7 +2150,6 @@ class PlexClient {
final container = _getMediaContainer(response);
if (container != null && container['Hub'] != null) {
final hubs = <LiveTvHubResult>[];
for (final hubJson in container['Hub'] as List) {
try {
final metadataList = hubJson['Metadata'] as List?;
@@ -2154,7 +2171,7 @@ class PlexClient {
}
if (entries.isNotEmpty) {
hubs.add(LiveTvHubResult(
allHubs.add(LiveTvHubResult(
title: hubJson['title'] as String? ?? 'Unknown',
hubKey: hubJson['key'] as String? ?? '',
entries: entries,
@@ -2164,12 +2181,13 @@ class PlexClient {
appLogger.w('Failed to parse live TV hub', error: e);
}
}
return hubs;
}
} catch (e) {
appLogger.e('Failed to get live TV hubs', error: e);
appLogger.e('Failed to get live TV hubs from provider ${provider.identifier}', error: e);
}
return [];
}
return allHubs;
}
/// Extract poster/art URLs from the Image array in EPG metadata items.