diff --git a/ios/Podfile.lock b/ios/Podfile.lock
index b352e747..7f1d716c 100644
--- a/ios/Podfile.lock
+++ b/ios/Podfile.lock
@@ -1,9 +1,12 @@
PODS:
+ - connectivity_plus (0.0.1):
+ - Flutter
- Flutter (1.0.0)
- media_kit_libs_ios_video (1.0.4):
- Flutter
- media_kit_video (0.0.1):
- Flutter
+ - media_kit_libs_ios_video
- os_media_controls (0.0.1):
- Flutter
- package_info_plus (0.4.5):
@@ -25,6 +28,7 @@ PODS:
- Flutter
DEPENDENCIES:
+ - connectivity_plus (from `.symlinks/plugins/connectivity_plus/ios`)
- Flutter (from `Flutter`)
- media_kit_libs_ios_video (from `.symlinks/plugins/media_kit_libs_ios_video/ios`)
- media_kit_video (from `.symlinks/plugins/media_kit_video/ios`)
@@ -38,6 +42,8 @@ DEPENDENCIES:
- wakelock_plus (from `.symlinks/plugins/wakelock_plus/ios`)
EXTERNAL SOURCES:
+ connectivity_plus:
+ :path: ".symlinks/plugins/connectivity_plus/ios"
Flutter:
:path: Flutter
media_kit_libs_ios_video:
@@ -62,9 +68,10 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/wakelock_plus/ios"
SPEC CHECKSUMS:
+ connectivity_plus: cb623214f4e1f6ef8fe7403d580fdad517d2f7dd
Flutter: cabc95a1d2626b1b06e7179b784ebcf0c0cde467
media_kit_libs_ios_video: 5a18affdb97d1f5d466dc79988b13eff6c5e2854
- media_kit_video: 1746e198cb697d1ffb734b1d05ec429d1fcd1474
+ media_kit_video: 6235abf1d299037d23692ad47119b89e05346b23
os_media_controls: 86dceab6245a5325af90fc0fdebe243c42d789b4
package_info_plus: af8e2ca6888548050f16fa2f1938db7b5a5df499
path_provider_foundation: bb55f6dbba17d0dccd6737fe6f7f34fbd0376880
diff --git a/ios/Runner/Info.plist b/ios/Runner/Info.plist
index 03b3c3e7..0fc3e407 100644
--- a/ios/Runner/Info.plist
+++ b/ios/Runner/Info.plist
@@ -39,10 +39,6 @@
This app needs to connect to your Plex Media Server on your local network.
UIApplicationSupportsIndirectInputEvents
- UIBackgroundModes
-
- audio
-
UILaunchStoryboardName
LaunchScreen
UIMainStoryboardFile
diff --git a/lib/client/plex_client.dart b/lib/client/plex_client.dart
index ebce3753..0b31a98a 100644
--- a/lib/client/plex_client.dart
+++ b/lib/client/plex_client.dart
@@ -10,9 +10,12 @@ import '../models/plex_library.dart';
import '../models/plex_media_info.dart';
import '../models/plex_media_version.dart';
import '../models/plex_metadata.dart';
+import '../models/plex_playlist.dart';
import '../models/plex_sort.dart';
import '../models/plex_video_playback_data.dart';
+import '../network/endpoint_failover_interceptor.dart';
import '../utils/app_logger.dart';
+import '../utils/log_redaction_manager.dart';
/// Result of testing a connection, including success status and latency
class ConnectionTestResult {
@@ -25,6 +28,8 @@ class ConnectionTestResult {
class PlexClient {
PlexConfig config;
late final Dio _dio;
+ final EndpointFailoverManager? _endpointManager;
+ final Future Function(String newBaseUrl)? _onEndpointChanged;
/// Custom response decoder that handles malformed UTF-8 gracefully
static String _lenientUtf8Decoder(
@@ -35,7 +40,18 @@ class PlexClient {
return utf8.decode(responseBytes, allowMalformed: true);
}
- PlexClient(this.config) {
+ PlexClient(
+ this.config, {
+ List? prioritizedEndpoints,
+ Future Function(String newBaseUrl)? onEndpointChanged,
+ }) : _endpointManager =
+ (prioritizedEndpoints != null && prioritizedEndpoints.isNotEmpty)
+ ? EndpointFailoverManager(prioritizedEndpoints)
+ : null,
+ _onEndpointChanged = onEndpointChanged {
+ LogRedactionManager.registerServerUrl(config.baseUrl);
+ LogRedactionManager.registerToken(config.token);
+
_dio = Dio(
BaseOptions(
baseUrl: config.baseUrl,
@@ -59,6 +75,16 @@ class PlexClient {
responseHeader: false,
),
);
+
+ if (_endpointManager != null) {
+ _dio.interceptors.add(
+ EndpointFailoverInterceptor(
+ dio: _dio,
+ endpointManager: _endpointManager,
+ onEndpointSwitch: _handleEndpointSwitch,
+ ),
+ );
+ }
}
/// Update the token used by this client
@@ -66,9 +92,29 @@ class PlexClient {
// Update both the Dio headers and the config to ensure consistency
_dio.options.headers['X-Plex-Token'] = newToken;
config = config.copyWith(token: newToken);
+ LogRedactionManager.registerToken(newToken);
appLogger.d('PlexClient token updated (headers and config)');
}
+ /// Update endpoint priority list and optionally hop to the new best endpoint.
+ Future updateEndpointPreferences(
+ List prioritizedEndpoints, {
+ bool switchToFirst = false,
+ }) async {
+ if (_endpointManager == null || prioritizedEndpoints.isEmpty) {
+ return;
+ }
+
+ final targetBaseUrl = switchToFirst
+ ? prioritizedEndpoints.first
+ : config.baseUrl;
+ _endpointManager.reset(prioritizedEndpoints, currentBaseUrl: targetBaseUrl);
+
+ if (switchToFirst && targetBaseUrl != config.baseUrl) {
+ await _handleEndpointSwitch(targetBaseUrl);
+ }
+ }
+
/// Test connection to server
Future testConnection() async {
try {
@@ -257,6 +303,30 @@ class PlexClient {
return _extractSingleMetadata(response);
}
+ /// Get the server's machine identifier
+ Future getMachineIdentifier() async {
+ try {
+ final response = await _dio.get('/');
+ final container = _getMediaContainer(response);
+ if (container == null) return null;
+ return container['machineIdentifier'] as String?;
+ } catch (e) {
+ appLogger.e('Failed to get machine identifier', error: e);
+ return null;
+ }
+ }
+
+ /// Build a proper metadata URI for adding to playlists
+ /// Returns URI in format: server://{machineId}/com.plexapp.plugins.library/library/metadata/{ratingKey}
+ Future buildMetadataUri(String ratingKey) async {
+ // Use cached machine identifier from config if available
+ final machineId = config.machineIdentifier ?? await getMachineIdentifier();
+ if (machineId == null) {
+ throw Exception('Could not get server machine identifier');
+ }
+ return 'server://$machineId/com.plexapp.plugins.library/library/metadata/$ratingKey';
+ }
+
/// Get metadata by rating key with images (includes clearLogo and OnDeck)
Future