diff --git a/lib/services/multi_server_manager.dart b/lib/services/multi_server_manager.dart index e3478fd9..70a24212 100644 --- a/lib/services/multi_server_manager.dart +++ b/lib/services/multi_server_manager.dart @@ -103,7 +103,7 @@ class MultiServerManager { final baseUrl = workingConnection.uri; // Create PlexClient with failover support - final prioritizedEndpoints = server.prioritizedEndpointUrls(preferredFirst: cachedEndpoint ?? baseUrl); + final prioritizedEndpoints = server.prioritizedEndpointUrls(preferredFirst: baseUrl); final config = await PlexConfig.create( baseUrl: baseUrl, token: server.accessToken, @@ -131,6 +131,18 @@ class MultiServerManager { return client; } + /// Persists a new endpoint, rebuilds the failover list, and switches the client. + Future _promoteEndpoint({ + required PlexClient client, + required PlexServer server, + required StorageService storage, + required String newUrl, + }) async { + await storage.saveServerEndpoint(server.clientIdentifier, newUrl); + final newEndpoints = server.prioritizedEndpointUrls(preferredFirst: newUrl); + await client.updateEndpointPreferences(newEndpoints, switchToFirst: true); + } + /// Continues draining the connection optimization stream in the background, /// switching the client to any better endpoint found. void _drainOptimizationStream( @@ -139,8 +151,6 @@ class MultiServerManager { required PlexServer server, required StorageService storage, }) { - final serverId = server.clientIdentifier; - () async { try { while (await streamIterator.moveNext()) { @@ -157,9 +167,7 @@ class MultiServerManager { error: {'from': client.config.baseUrl, 'to': newUrl, 'type': connection.displayType}, ); - await storage.saveServerEndpoint(serverId, newUrl); - final newEndpoints = server.prioritizedEndpointUrls(preferredFirst: newUrl); - await client.updateEndpointPreferences(newEndpoints, switchToFirst: true); + await _promoteEndpoint(client: client, server: server, storage: storage, newUrl: newUrl); } } catch (e, stackTrace) { appLogger.w('Background connection optimization failed for ${server.name}', error: e, stackTrace: stackTrace); @@ -174,7 +182,7 @@ class MultiServerManager { Future connectToAllServers( List servers, { String? clientIdentifier, - Duration timeout = ConnectionTimeouts.connectAll, + Duration timeout = ConnectionTimeouts.perServerConnect, Function(String serverId, PlexClient client)? onServerConnected, Function(String serverId, Object error)? onServerFailed, }) async { @@ -190,14 +198,15 @@ class MultiServerManager { final effectiveClientId = clientIdentifier ?? DateTime.now().millisecondsSinceEpoch.toString(); _clientIdentifier = effectiveClientId; - // Create connection tasks for all servers + // Create connection tasks for all servers (timeout is inside each task + // so a timed-out task cannot keep mutating manager state). final connectionFutures = servers.map((server) async { final serverId = server.clientIdentifier; try { appLogger.d('Attempting connection to server: ${server.name}'); - final client = await _createClientForServer(server: server, clientIdentifier: effectiveClientId); + final client = await _createClientForServer(server: server, clientIdentifier: effectiveClientId).timeout(timeout); // Store the client and server info _clients[serverId] = client; @@ -208,6 +217,12 @@ class MultiServerManager { appLogger.i('Successfully connected to ${server.name}'); return serverId; + } on TimeoutException { + appLogger.w('Server connection timed out for ${server.name}'); + _servers[serverId] = server; + _serverStatus[serverId] = false; + onServerFailed?.call(serverId, TimeoutException('Connection to ${server.name} timed out')); + return null; } catch (e, stackTrace) { appLogger.e('Failed to connect to ${server.name}', error: e, stackTrace: stackTrace); @@ -220,18 +235,7 @@ class MultiServerManager { } }); - // Wait for all connections with timeout - final results = await Future.wait( - connectionFutures.map( - (f) => f.timeout( - timeout, - onTimeout: () { - appLogger.w('Server connection timed out'); - return null; - }, - ), - ), - ); + final results = await Future.wait(connectionFutures); // Count successful connections final successCount = results.where((id) => id != null).length; @@ -432,15 +436,11 @@ class MultiServerManager { continue; } - // Save the new endpoint - await storage.saveServerEndpoint(serverId, newUrl); - - // Actively switch the running client to the better endpoint if (client != null) { - final newEndpoints = server.prioritizedEndpointUrls(preferredFirst: newUrl); - await client.updateEndpointPreferences(newEndpoints, switchToFirst: true); + await _promoteEndpoint(client: client, server: server, storage: storage, newUrl: newUrl); appLogger.i('Switched ${server.name} to better endpoint: $newUrl', error: {'type': connection.displayType}); } else { + await storage.saveServerEndpoint(serverId, newUrl); appLogger.i('Updated optimal endpoint for ${server.name}: $newUrl', error: {'type': connection.displayType}); } } diff --git a/lib/services/plex_auth_service.dart b/lib/services/plex_auth_service.dart index 09b8778a..5dcb728b 100644 --- a/lib/services/plex_auth_service.dart +++ b/lib/services/plex_auth_service.dart @@ -365,26 +365,6 @@ class PlexServer { /// Check if server is online using the presence field bool get isOnline => presence; - PlexConnection? _selectBest(Iterable candidates) { - final local = candidates.where((c) => c.local && !c.relay).toList(); - if (local.isNotEmpty) return local.first; - - final remote = candidates.where((c) => !c.local && !c.relay).toList(); - if (remote.isNotEmpty) return remote.first; - - final relay = candidates.where((c) => c.relay).toList(); - if (relay.isNotEmpty) return relay.first; - - if (candidates.isNotEmpty) return candidates.first; - return null; - } - - /// Get the best connection URL - /// Priority: local > remote > relay - PlexConnection? getBestConnection() { - return _selectBest(connections); - } - /// Find the best working connection by testing them /// Returns a Stream that emits connections progressively: /// 1. First emission: The first connection that responds successfully diff --git a/lib/services/server_connection_orchestrator.dart b/lib/services/server_connection_orchestrator.dart index 3cc2d690..37782285 100644 --- a/lib/services/server_connection_orchestrator.dart +++ b/lib/services/server_connection_orchestrator.dart @@ -36,7 +36,7 @@ class ServerConnectionOrchestrator { required LibrariesProvider librariesProvider, required OfflineWatchSyncService syncService, String? clientIdentifier, - Duration timeout = ConnectionTimeouts.connectAll, + Duration timeout = ConnectionTimeouts.perServerConnect, void Function(String serverId, bool success)? onServerStatus, }) async { appLogger.i('Connecting to ${servers.length} servers...'); diff --git a/lib/utils/connection_constants.dart b/lib/utils/connection_constants.dart index cb09b759..56589c6f 100644 --- a/lib/utils/connection_constants.dart +++ b/lib/utils/connection_constants.dart @@ -11,9 +11,8 @@ class ConnectionTimeouts { /// Dio connect timeout for individual HTTP requests to a Plex server. static const connect = Duration(seconds: 10); - /// Timeout for [MultiServerManager.connectToAllServers] — the maximum time - /// to wait for each server's connection future. - static const connectAll = Duration(seconds: 4); + /// Per-server connection budget: preferred probe + race + HTTPS upgrade attempt + 1s buffer. + static const perServerConnect = Duration(milliseconds: 1500 + 2000 + 2000 + 1000); /// Dio receive timeout for streaming/large responses from a Plex server. static const receive = Duration(seconds: 120);