Merge the deduplication and dead-code removal pass
Consolidates duplicated logic behind shared implementations — paginated grid tabs, focus chrome, cached remote stores, sheet selection columns, the server artifact store and a test fixture layer — and removes code that had become unreachable. Net reduction of about 5,500 lines with no behaviour change. Where a fix had landed separately in code that moved into a shared helper, the fix was re-applied inside the helper rather than left behind in the copy that went away.
This commit is contained in:
@@ -3,38 +3,13 @@ part of '../../plex_client.dart';
|
||||
const _favoriteChannelsUrl = 'https://epg.provider.plex.tv/settings/favoriteChannels';
|
||||
const _providerVersionHeader = {'X-Plex-Provider-Version': '5.1'};
|
||||
|
||||
mixin _PlexLiveTvClientMethods on MediaServerCacheMixin implements LiveTvSupport, LiveTvDvrSupport {
|
||||
mixin _PlexLiveTvClientMethods on _PlexClientInternals implements LiveTvSupport, LiveTvDvrSupport {
|
||||
PlexConfig get config;
|
||||
MediaServerHttpClient get _http;
|
||||
|
||||
@override
|
||||
ServerId get serverId;
|
||||
|
||||
@override
|
||||
String? get serverName;
|
||||
|
||||
List<({String identifier, String gridEndpoint})> get _providerEpg;
|
||||
|
||||
Future<MediaServerResponse> _getWithFailover(
|
||||
String path, {
|
||||
Map<String, dynamic>? queryParameters,
|
||||
// ignore: unused_element_parameter
|
||||
Map<String, String>? headers,
|
||||
Duration? timeout,
|
||||
// ignore: unused_element_parameter
|
||||
AbortController? abort,
|
||||
bool allowEndpointFailover = true,
|
||||
});
|
||||
|
||||
Map<String, dynamic>? _getMediaContainer(MediaServerResponse response);
|
||||
PlexMetadataDto _createTaggedMetadata(Map<String, dynamic> json);
|
||||
|
||||
Future<List<T>> _wrapListApiCall<T>(
|
||||
Future<MediaServerResponse> Function() apiCall,
|
||||
List<T> Function(MediaServerResponse response) parseResponse,
|
||||
String errorMessage,
|
||||
);
|
||||
|
||||
/// POST the tune endpoint with one retry on transient HTTP failure.
|
||||
Future<MediaServerResponse> _postTuneWithRetry(String path, String sessionIdentifier) async {
|
||||
final query = {'X-Plex-Session-Identifier': sessionIdentifier};
|
||||
@@ -96,7 +71,7 @@ mixin _PlexLiveTvClientMethods on MediaServerCacheMixin implements LiveTvSupport
|
||||
for (final entry in request.prefs.entries) 'prefs[${entry.key}]': entry.value,
|
||||
for (final entry in request.params.entries) 'params[${entry.key}]': entry.value,
|
||||
};
|
||||
final encoded = MediaServerHttpClient.encodeQueryParameters(flat);
|
||||
final encoded = encodeQueryParameters(flat);
|
||||
if (encoded.isNotEmpty) parts.add(encoded);
|
||||
return parts.join('&');
|
||||
}
|
||||
@@ -197,73 +172,6 @@ mixin _PlexLiveTvClientMethods on MediaServerCacheMixin implements LiveTvSupport
|
||||
return dvrs.isNotEmpty;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<LiveTvServerStatus> fetchLiveTvServerStatus() async {
|
||||
final response = await _getWithFailover('/');
|
||||
final container = _getMediaContainer(response);
|
||||
return LiveTvServerStatus.fromJson(container ?? const <String, dynamic>{});
|
||||
}
|
||||
|
||||
@override
|
||||
Future<LiveTvDvr?> fetchDvr(String dvrId) async {
|
||||
final response = await _getWithFailover('/livetv/dvrs/$dvrId');
|
||||
final container = _getMediaContainer(response);
|
||||
final rootMappings = container?['ChannelMapping'];
|
||||
return _extractFirst(response, const ['Dvr'], (json) {
|
||||
final map = Map<String, dynamic>.from(json)..putIfAbsent('ChannelMapping', () => rootMappings);
|
||||
return LiveTvDvr.fromJson(map);
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Future<LiveTvActivityResult<LiveTvDvr?>> createDvr({
|
||||
required List<String> devices,
|
||||
required List<String> lineups,
|
||||
String? language,
|
||||
String? country,
|
||||
String? postalCode,
|
||||
}) async {
|
||||
final response = await _http.post(
|
||||
'/livetv/dvrs',
|
||||
queryParameters: {
|
||||
'device': devices,
|
||||
'lineup': lineups,
|
||||
...?(language == null ? null : {'language': language}),
|
||||
...?(country == null ? null : {'country': country}),
|
||||
...?(postalCode == null ? null : {'postalCode': postalCode}),
|
||||
},
|
||||
timeout: MediaServerTimeouts.receive,
|
||||
);
|
||||
_throwIfFailed(response);
|
||||
return LiveTvActivityResult(
|
||||
value: _extractFirst(response, const ['Dvr'], LiveTvDvr.fromJson),
|
||||
activityUuid: _activityUuid(response),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> deleteDvr(String dvrId) => _expectOk(() => _http.delete('/livetv/dvrs/$dvrId'));
|
||||
|
||||
@override
|
||||
Future<void> updateDvrPrefs(String dvrId, Map<String, Object?> prefs) =>
|
||||
_expectOk(() => _http.put('/livetv/dvrs/$dvrId/prefs', queryParameters: prefs));
|
||||
|
||||
@override
|
||||
Future<void> attachDeviceToDvr(String dvrId, String deviceId) =>
|
||||
_expectOk(() => _http.put('/livetv/dvrs/$dvrId/devices/$deviceId'));
|
||||
|
||||
@override
|
||||
Future<void> detachDeviceFromDvr(String dvrId, String deviceId) =>
|
||||
_expectOk(() => _http.delete('/livetv/dvrs/$dvrId/devices/$deviceId'));
|
||||
|
||||
@override
|
||||
Future<void> addLineupToDvr(String dvrId, String lineupUri) =>
|
||||
_expectOk(() => _http.put('/livetv/dvrs/$dvrId/lineups', queryParameters: {'lineup': lineupUri}));
|
||||
|
||||
@override
|
||||
Future<void> removeLineupFromDvr(String dvrId, String lineupUri) =>
|
||||
_expectOk(() => _http.delete('/livetv/dvrs/$dvrId/lineups', queryParameters: {'lineup': lineupUri}));
|
||||
|
||||
@override
|
||||
Future<LiveTvActivityResult<void>> reloadGuide(String dvrId) async {
|
||||
final response = await _http.post('/livetv/dvrs/$dvrId/reloadGuide', timeout: MediaServerTimeouts.receive);
|
||||
@@ -271,229 +179,23 @@ mixin _PlexLiveTvClientMethods on MediaServerCacheMixin implements LiveTvSupport
|
||||
return LiveTvActivityResult(value: null, activityUuid: _activityUuid(response));
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> cancelGuideReload(String dvrId) => _expectOk(() => _http.delete('/livetv/dvrs/$dvrId/reloadGuide'));
|
||||
|
||||
@override
|
||||
Future<List<MediaGrabber>> fetchGrabbers({String? protocol}) async {
|
||||
final response = await _getWithFailover(
|
||||
'/media/grabbers',
|
||||
queryParameters: {
|
||||
...?(protocol == null ? null : {'protocol': protocol}),
|
||||
},
|
||||
);
|
||||
return _extractContainerList(response, const ['MediaGrabber'], MediaGrabber.fromJson);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<MediaGrabberDevice>> fetchGrabberDevices() async {
|
||||
final response = await _getWithFailover('/media/grabbers/devices');
|
||||
return _extractContainerList(response, const ['Device', 'Devices'], MediaGrabberDevice.fromJson);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<LiveTvActivityResult<List<MediaGrabberDevice>>> discoverGrabberDevices() async {
|
||||
final response = await _http.post('/media/grabbers/devices/discover', timeout: MediaServerTimeouts.receive);
|
||||
_throwIfFailed(response);
|
||||
return LiveTvActivityResult(
|
||||
value: _extractContainerList(response, const ['Device', 'Devices'], MediaGrabberDevice.fromJson),
|
||||
activityUuid: _activityUuid(response),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<MediaGrabberDevice?> fetchGrabberDevice(String deviceId) async {
|
||||
final response = await _getWithFailover('/media/grabbers/devices/$deviceId');
|
||||
return _extractFirst(response, const ['Device', 'Devices'], MediaGrabberDevice.fromJson);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<MediaGrabberDevice?> addGrabberDevice(String uri, {String? grabberId}) async {
|
||||
final path = grabberId == null ? '/media/grabbers/devices' : '/media/grabbers/$grabberId/devices';
|
||||
final response = await _http.post(path, queryParameters: {'uri': uri});
|
||||
_throwIfFailed(response);
|
||||
return _extractFirst(response, const ['Device', 'Devices'], MediaGrabberDevice.fromJson);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> updateGrabberDevice(String deviceId, {bool? enabled, String? title}) => _expectOk(
|
||||
() => _http.put(
|
||||
'/media/grabbers/devices/$deviceId',
|
||||
queryParameters: {
|
||||
...?(enabled == null ? null : {'enabled': enabled ? 1 : 0}),
|
||||
...?(title == null ? null : {'title': title}),
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
@override
|
||||
Future<void> deleteGrabberDevice(String deviceId) =>
|
||||
_expectOk(() => _http.delete('/media/grabbers/devices/$deviceId'));
|
||||
|
||||
@override
|
||||
Future<List<MediaGrabberDeviceChannel>> fetchGrabberDeviceChannels(String deviceId) async {
|
||||
final response = await _getWithFailover('/media/grabbers/devices/$deviceId/channels');
|
||||
return _extractContainerList(response, const ['DeviceChannel'], MediaGrabberDeviceChannel.fromJson);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<LiveTvActivityResult<MediaGrabberDevice?>> scanGrabberDevice(
|
||||
String deviceId, {
|
||||
String? source,
|
||||
Map<String, Object?> prefs = const {},
|
||||
String? network,
|
||||
String? country,
|
||||
}) async {
|
||||
final response = await _http.post(
|
||||
'/media/grabbers/devices/$deviceId/scan',
|
||||
queryParameters: {
|
||||
...?(source == null ? null : {'source': source}),
|
||||
for (final entry in prefs.entries) 'prefs[${entry.key}]': entry.value,
|
||||
...?(network == null ? null : {'network': network}),
|
||||
...?(country == null ? null : {'country': country}),
|
||||
},
|
||||
timeout: MediaServerTimeouts.receive,
|
||||
);
|
||||
_throwIfFailed(response);
|
||||
return LiveTvActivityResult(
|
||||
value: _extractFirst(response, const ['Device', 'Devices'], MediaGrabberDevice.fromJson),
|
||||
activityUuid: _activityUuid(response),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<MediaGrabberDevice?> cancelGrabberDeviceScan(String deviceId) async {
|
||||
final response = await _http.delete('/media/grabbers/devices/$deviceId/scan');
|
||||
_throwIfFailed(response);
|
||||
return _extractFirst(response, const ['Device', 'Devices'], MediaGrabberDevice.fromJson);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<MediaGrabberDevice?> saveGrabberDeviceChannelMap(
|
||||
String deviceId,
|
||||
MediaGrabberChannelMapRequest request,
|
||||
) async {
|
||||
final response = await _http.put(
|
||||
'/media/grabbers/devices/$deviceId/channelmap',
|
||||
queryParameters: {
|
||||
if (request.channelsEnabled.isNotEmpty) 'channelsEnabled': request.channelsEnabled.join(','),
|
||||
for (final entry in request.channelMapping.entries) 'channelMapping[${entry.key}]': entry.value,
|
||||
for (final entry in request.channelMappingByKey.entries) 'channelMappingByKey[${entry.key}]': entry.value,
|
||||
},
|
||||
);
|
||||
_throwIfFailed(response);
|
||||
return _extractFirst(response, const ['Device', 'Devices'], MediaGrabberDevice.fromJson);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> updateGrabberDevicePrefs(String deviceId, Map<String, Object?> prefs) =>
|
||||
_expectOk(() => _http.put('/media/grabbers/devices/$deviceId/prefs', queryParameters: prefs));
|
||||
|
||||
@override
|
||||
String buildGrabberDeviceThumbUrl(String deviceId, int version) =>
|
||||
'${config.baseUrl}/media/grabbers/devices/$deviceId/thumb/$version'.withPlexToken(config.token);
|
||||
|
||||
@override
|
||||
Future<List<LiveTvCountry>> fetchEpgCountries() async {
|
||||
final response = await _getWithFailover('/livetv/epg/countries');
|
||||
return _extractContainerList(response, const ['Country'], LiveTvCountry.fromJson);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<LiveTvLanguage>> fetchEpgLanguages() async {
|
||||
final response = await _getWithFailover('/livetv/epg/languages');
|
||||
return _extractContainerList(response, const ['Language'], LiveTvLanguage.fromJson);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<LiveTvRegion>> fetchEpgRegions(String country, String epgId) async {
|
||||
final response = await _getWithFailover('/livetv/epg/countries/$country/$epgId/regions');
|
||||
return _extractContainerList(response, const ['Region'], LiveTvRegion.fromJson);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<LiveTvLineupResult> fetchEpgLineups(String country, String epgId, {String? postalCode, String? region}) async {
|
||||
final path = region == null
|
||||
? '/livetv/epg/countries/$country/$epgId/lineups'
|
||||
: '/livetv/epg/countries/$country/$epgId/regions/$region/lineups';
|
||||
final response = await _getWithFailover(
|
||||
path,
|
||||
queryParameters: {
|
||||
...?(postalCode == null ? null : {'postalCode': postalCode}),
|
||||
},
|
||||
);
|
||||
final container = _getMediaContainer(response);
|
||||
return LiveTvLineupResult(
|
||||
lineupGroupUuid: container?['uuid'] as String?,
|
||||
lineups: _extractContainerList(response, const ['Lineup'], LiveTvLineup.fromJson),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<LiveTvChannel>> fetchEpgChannelsForLineup(String lineupUri) async {
|
||||
final response = await _getWithFailover('/livetv/epg/channels', queryParameters: {'lineup': lineupUri});
|
||||
return _extractContainerList(response, const [
|
||||
'Channel',
|
||||
], (json) => LiveTvChannel.fromJson(json).copyWith(serverId: serverId, serverName: serverName));
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<LiveTvLineup>> fetchEpgChannelsForLineups(List<String> lineupUris) async {
|
||||
final response = await _getWithFailover('/livetv/epg/lineupchannels', queryParameters: {'lineup': lineupUris});
|
||||
return _extractContainerList(response, const ['Lineup'], LiveTvLineup.fromJson);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<ChannelMapping>> computeEpgChannelMap({required String deviceUri, required String lineupUri}) async {
|
||||
final response = await _getWithFailover(
|
||||
'/livetv/epg/channelmap',
|
||||
queryParameters: {'device': deviceUri, 'lineup': lineupUri},
|
||||
);
|
||||
return _extractContainerList(response, const ['ChannelMapping'], ChannelMapping.fromJson);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<LiveTvActivityResult<Map<String, dynamic>?>> findBestLineup({
|
||||
required String deviceUri,
|
||||
required String lineupGroupUri,
|
||||
}) async {
|
||||
final response = await _getWithFailover(
|
||||
'/livetv/epg/lineup',
|
||||
queryParameters: {'device': deviceUri, 'lineupGroup': lineupGroupUri},
|
||||
timeout: MediaServerTimeouts.receive,
|
||||
);
|
||||
return LiveTvActivityResult(value: _getMediaContainer(response), activityUuid: _activityUuid(response));
|
||||
}
|
||||
|
||||
/// Get EPG channels using provider lineup endpoints (matches official Plex web client)
|
||||
Future<List<LiveTvChannel>> getEpgChannels({String? lineup}) async {
|
||||
List<LiveTvChannel> parseChannels(MediaServerResponse response) {
|
||||
final container = _getMediaContainer(response);
|
||||
if (container != null && container['Channel'] is List && (container['Channel'] as List).isNotEmpty) {
|
||||
appLogger.d('EPG channel sample: ${(container['Channel'] as List).first}');
|
||||
if (container == null || (container['Channel'] == null && container['Metadata'] == null)) {
|
||||
appLogger.d('EPG channels: container keys=${container?.keys.toList()}, size=${container?['size']}');
|
||||
return [];
|
||||
}
|
||||
if (container != null && container['Channel'] != null) {
|
||||
return (container['Channel'] as List)
|
||||
.map(
|
||||
(json) => LiveTvChannel.fromJson(
|
||||
json as Map<String, dynamic>,
|
||||
).copyWith(serverId: serverId, serverName: serverName),
|
||||
)
|
||||
.where((ch) => ch.key.isNotEmpty)
|
||||
.toList();
|
||||
final rawChannels = container['Channel'];
|
||||
if (rawChannels is List && rawChannels.isNotEmpty) {
|
||||
appLogger.d('EPG channel sample: ${rawChannels.first}');
|
||||
}
|
||||
if (container != null && container['Metadata'] != null) {
|
||||
return (container['Metadata'] as List)
|
||||
.map(
|
||||
(json) => LiveTvChannel.fromJson(
|
||||
json as Map<String, dynamic>,
|
||||
).copyWith(serverId: serverId, serverName: serverName),
|
||||
)
|
||||
.where((ch) => ch.key.isNotEmpty)
|
||||
.toList();
|
||||
}
|
||||
appLogger.d('EPG channels: container keys=${container?.keys.toList()}, size=${container?['size']}');
|
||||
return [];
|
||||
return _extractContainerList(
|
||||
response,
|
||||
const ['Channel', 'Metadata'],
|
||||
(json) => LiveTvChannel.fromJson(json).copyWith(serverId: serverId, serverName: serverName),
|
||||
).where((ch) => ch.key.isNotEmpty).toList();
|
||||
}
|
||||
|
||||
final allChannels = <LiveTvChannel>[];
|
||||
@@ -744,19 +446,6 @@ mixin _PlexLiveTvClientMethods on MediaServerCacheMixin implements LiveTvSupport
|
||||
return _extractContainerList(response, const ['MediaSubscription'], MediaSubscription.fromJson);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<MediaSubscription?> fetchRecordingRule(
|
||||
String subscriptionId, {
|
||||
bool includeGrabs = true,
|
||||
bool includeStorage = true,
|
||||
}) async {
|
||||
final response = await _getWithFailover(
|
||||
'/media/subscriptions/$subscriptionId',
|
||||
queryParameters: {'includeGrabs': includeGrabs ? 1 : 0, 'includeStorage': includeStorage ? 1 : 0},
|
||||
);
|
||||
return _extractFirst(response, const ['MediaSubscription'], MediaSubscription.fromJson);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<MediaSubscription?> createRecordingRule(MediaSubscriptionCreateRequest request) async {
|
||||
final response = await _http.post(_withQuery('/media/subscriptions', _subscriptionCreateQuery(request)));
|
||||
@@ -778,18 +467,6 @@ mixin _PlexLiveTvClientMethods on MediaServerCacheMixin implements LiveTvSupport
|
||||
Future<void> deleteRecordingRule(String subscriptionId) =>
|
||||
_expectOk(() => _http.delete('/media/subscriptions/$subscriptionId'));
|
||||
|
||||
@override
|
||||
Future<MediaSubscription?> moveRecordingRule(String subscriptionId, {String? afterSubscriptionId}) async {
|
||||
final response = await _http.put(
|
||||
'/media/subscriptions/$subscriptionId/move',
|
||||
queryParameters: {
|
||||
...?(afterSubscriptionId == null ? null : {'after': afterSubscriptionId}),
|
||||
},
|
||||
);
|
||||
_throwIfFailed(response);
|
||||
return _extractFirst(response, const ['MediaSubscription'], MediaSubscription.fromJson);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> processRecordingRules() => _expectOk(() => _http.post('/media/subscriptions/process'));
|
||||
|
||||
@@ -824,23 +501,6 @@ mixin _PlexLiveTvClientMethods on MediaServerCacheMixin implements LiveTvSupport
|
||||
return _extractContainerList(response, const ['MediaSubscription'], MediaSubscription.fromJson);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<MediaProviderInfo>> fetchMediaProviders() async {
|
||||
final response = await _getWithFailover('/media/providers');
|
||||
return _extractContainerList(response, const ['MediaProvider'], MediaProviderInfo.fromJson);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> registerMediaProvider(String url) =>
|
||||
_expectOk(() => _http.post('/media/providers', queryParameters: {'url': url}));
|
||||
|
||||
@override
|
||||
Future<void> refreshMediaProviders() => _expectOk(() => _http.post('/media/providers/refresh'));
|
||||
|
||||
@override
|
||||
Future<void> unregisterMediaProvider(String providerId) =>
|
||||
_expectOk(() => _http.delete('/media/providers/$providerId'));
|
||||
|
||||
/// Tune to a live TV channel.
|
||||
///
|
||||
/// POSTs to the tune endpoint and extracts metadata, session info, and
|
||||
@@ -873,11 +533,7 @@ mixin _PlexLiveTvClientMethods on MediaServerCacheMixin implements LiveTvSupport
|
||||
if (container == null) return null;
|
||||
|
||||
final containerStatus = container['status'];
|
||||
final statusInt = containerStatus is num
|
||||
? containerStatus.toInt()
|
||||
: containerStatus is String
|
||||
? int.tryParse(containerStatus)
|
||||
: null;
|
||||
final statusInt = flexibleInt(containerStatus);
|
||||
if (statusInt != null && statusInt != 0 && statusInt != 200) {
|
||||
final msg = container['message'] ?? t.liveTv.unknownError;
|
||||
appLogger.w('Tune channel error: $msg (status: $containerStatus)');
|
||||
@@ -907,14 +563,7 @@ mixin _PlexLiveTvClientMethods on MediaServerCacheMixin implements LiveTvSupport
|
||||
if (op is Map) {
|
||||
if (op['Metadata'] case [final Map firstMetadata, ...]) {
|
||||
if (firstMetadata['Media'] case [final Map firstMedia, ...]) {
|
||||
final rawBeginsAt = firstMedia['beginsAt'];
|
||||
|
||||
beginsAt = switch (rawBeginsAt) {
|
||||
final num n => n.toInt(),
|
||||
final String s => int.tryParse(s),
|
||||
_ => null,
|
||||
};
|
||||
|
||||
beginsAt = flexibleInt(firstMedia['beginsAt']);
|
||||
appLogger.d('beginsAt=$beginsAt');
|
||||
}
|
||||
}
|
||||
@@ -980,12 +629,7 @@ mixin _PlexLiveTvClientMethods on MediaServerCacheMixin implements LiveTvSupport
|
||||
if (media is List && media.isNotEmpty) {
|
||||
final firstMedia = media.first;
|
||||
if (firstMedia is Map<String, dynamic>) {
|
||||
final rawBeginsAt = firstMedia['beginsAt'];
|
||||
beginsAt = switch (rawBeginsAt) {
|
||||
final num n => n.toInt(),
|
||||
final String s => int.tryParse(s),
|
||||
_ => null,
|
||||
};
|
||||
beginsAt = flexibleInt(firstMedia['beginsAt']);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1053,10 +697,9 @@ mixin _PlexLiveTvClientMethods on MediaServerCacheMixin implements LiveTvSupport
|
||||
if (config.token != null) 'X-Plex-Token': config.token!,
|
||||
};
|
||||
|
||||
// Manual query encoding — use '%20' for spaces as Plex requires.
|
||||
final queryString = allParams.entries
|
||||
.map((e) => '${Uri.encodeComponent(e.key)}=${Uri.encodeComponent(e.value)}')
|
||||
.join('&');
|
||||
// '%20' for spaces as Plex requires — not `Uri.queryParameters`, which
|
||||
// emits `+`.
|
||||
final queryString = encodeQueryParameters(allParams);
|
||||
|
||||
// Decision — wrapper around the same transport so no default X-Plex-*
|
||||
// HTTP headers leak through (everything travels in the query string).
|
||||
@@ -1086,9 +729,7 @@ mixin _PlexLiveTvClientMethods on MediaServerCacheMixin implements LiveTvSupport
|
||||
|
||||
// Token is added by the caller via .withPlexToken()
|
||||
final startParams = Map<String, String>.from(allParams)..remove('X-Plex-Token');
|
||||
final startQuery = startParams.entries
|
||||
.map((e) => '${Uri.encodeComponent(e.key)}=${Uri.encodeComponent(e.value)}')
|
||||
.join('&');
|
||||
final startQuery = encodeQueryParameters(startParams);
|
||||
|
||||
return '$_plexVideoHlsStartEndpoint?$startQuery';
|
||||
} catch (e, st) {
|
||||
@@ -1106,53 +747,6 @@ mixin _PlexLiveTvClientMethods on MediaServerCacheMixin implements LiveTvSupport
|
||||
return '${config.baseUrl}$streamPath'.withPlexToken(config.token);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<LiveTvSession>> fetchLiveTvSessionsDetailed() async {
|
||||
final response = await _getWithFailover('/livetv/sessions');
|
||||
return _extractContainerList(response, const [
|
||||
'LiveTVSession',
|
||||
'LiveTvSession',
|
||||
'Session',
|
||||
'Metadata',
|
||||
], LiveTvSession.fromJson);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<LiveTvSession?> fetchLiveTvSession(String sessionId) async {
|
||||
final response = await _getWithFailover('/livetv/sessions/$sessionId');
|
||||
return _extractFirst(response, const [
|
||||
'LiveTVSession',
|
||||
'LiveTvSession',
|
||||
'Session',
|
||||
'Metadata',
|
||||
], LiveTvSession.fromJson);
|
||||
}
|
||||
|
||||
@override
|
||||
Uri buildNotificationWebSocketUri({List<String>? filters}) {
|
||||
final base = Uri.parse(config.baseUrl);
|
||||
return base.replace(
|
||||
scheme: base.scheme == 'https' ? 'wss' : 'ws',
|
||||
path: '/:/websocket/notifications',
|
||||
queryParameters: {
|
||||
if (config.token != null) 'X-Plex-Token': config.token!,
|
||||
if (filters != null) 'filters': filters.join(','),
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Uri buildNotificationEventSourceUri({List<String>? filters}) {
|
||||
final base = Uri.parse(config.baseUrl);
|
||||
return base.replace(
|
||||
path: '/:/eventsource/notifications',
|
||||
queryParameters: {
|
||||
if (config.token != null) 'X-Plex-Token': config.token!,
|
||||
if (filters != null) 'filters': filters.join(','),
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/// Build the source URI for favorite channels: `server://{machineIdentifier}/{providerIdentifier}`
|
||||
@override
|
||||
Future<String> buildFavoriteChannelSource({String? lineup}) async {
|
||||
|
||||
Reference in New Issue
Block a user