From d586c8c067b08193ab70d65acf50df5a5785ac52 Mon Sep 17 00:00:00 2001 From: edde746 <86283021+edde746@users.noreply.github.com> Date: Tue, 4 Nov 2025 09:07:04 +0100 Subject: [PATCH] fix: allow connections to local only servers --- lib/services/plex_auth_service.dart | 156 ++++++++++++++++++++-------- 1 file changed, 111 insertions(+), 45 deletions(-) diff --git a/lib/services/plex_auth_service.dart b/lib/services/plex_auth_service.dart index 3bde3830..4792d16c 100644 --- a/lib/services/plex_auth_service.dart +++ b/lib/services/plex_auth_service.dart @@ -215,6 +215,15 @@ class PlexAuthService { } } +/// Helper class to track connection candidates during testing +class _ConnectionCandidate { + final PlexConnection connection; + final String url; + final bool isPlexDirectUri; + + _ConnectionCandidate(this.connection, this.url, this.isPlexDirectUri); +} + /// Represents a Plex Media Server class PlexServer { final String name; @@ -304,103 +313,156 @@ class PlexServer { return _selectBest(connections); } - PlexConnection? _findLowestLatency( - List> entries, - ) { - if (entries.isEmpty) return null; - final bestEntry = entries.reduce( - (a, b) => a.value.latencyMs < b.value.latencyMs ? a : b, - ); - return bestEntry.key; - } - - PlexConnection? _selectBestWithLatency( - Map results, - ) { - final localEntries = results.entries - .where((e) => e.key.local && !e.key.relay) - .toList(); - final remoteEntries = results.entries - .where((e) => !e.key.local && !e.key.relay) - .toList(); - final relayEntries = results.entries.where((e) => e.key.relay).toList(); - - return _findLowestLatency(localEntries) ?? - _findLowestLatency(remoteEntries) ?? - _findLowestLatency(relayEntries); - } - /// Find the best working connection by testing them /// Returns a Stream that emits connections progressively: /// 1. First emission: The first connection that responds successfully /// 2. Second emission (optional): The best connection after latency testing /// Priority: local > remote > relay (from successful connections) + /// Tests both plex.direct URI and direct IP for each connection Stream findBestWorkingConnection() async* { if (connections.isEmpty) return; + // Create candidates: test both uri and directUrl for each connection + final candidates = <_ConnectionCandidate>[]; + for (final connection in connections) { + candidates.add(_ConnectionCandidate(connection, connection.uri, true)); + candidates.add(_ConnectionCandidate(connection, connection.directUrl, false)); + } + // Phase 1: Race to find first working connection - final completer = Completer(); - PlexConnection? firstConnection; + final completer = Completer<_ConnectionCandidate?>(); + _ConnectionCandidate? firstCandidate; int completedTests = 0; - // Start testing all connections simultaneously - for (final connection in connections) { - PlexClient.testConnectionWithLatency(connection.uri, accessToken).then(( + // Start testing all candidates simultaneously + for (final candidate in candidates) { + PlexClient.testConnectionWithLatency(candidate.url, accessToken).then(( result, ) { completedTests++; // If this is the first successful connection, emit it immediately if (result.success && !completer.isCompleted) { - completer.complete(connection); + completer.complete(candidate); } // If all tests complete without success, complete with null - if (completedTests == connections.length && !completer.isCompleted) { + if (completedTests == candidates.length && !completer.isCompleted) { completer.complete(null); } }); } // Wait for and emit the first successful connection - firstConnection = await completer.future; - if (firstConnection == null) { + firstCandidate = await completer.future; + if (firstCandidate == null) { return; // No working connections found } + // Update the connection object to use the working URL + final firstConnection = _updateConnectionUrl( + firstCandidate.connection, + firstCandidate.url, + ); yield firstConnection; // Phase 2: Continue testing in background to find best connection - // Test each connection 2-3 times and average the latency - final connectionResults = {}; + // Test each candidate 2-3 times and average the latency + final candidateResults = <_ConnectionCandidate, ConnectionTestResult>{}; await Future.wait( - connections.map((connection) async { + candidates.map((candidate) async { final result = await PlexClient.testConnectionWithAverageLatency( - connection.uri, + candidate.url, accessToken, attempts: 2, ); if (result.success) { - connectionResults[connection] = result; + candidateResults[candidate] = result; } }), ); // If no connections succeeded, we're done - if (connectionResults.isEmpty) { + if (candidateResults.isEmpty) { return; } - // Find the best connection considering both priority and latency - final bestConnection = _selectBestWithLatency(connectionResults); + // Find the best connection considering priority, latency, and URL type + final bestCandidate = _selectBestCandidateWithLatency(candidateResults); // Emit the best connection if it's different from the first one - if (bestConnection != null && bestConnection.uri != firstConnection.uri) { - yield bestConnection; + if (bestCandidate != null) { + final bestConnection = _updateConnectionUrl( + bestCandidate.connection, + bestCandidate.url, + ); + if (bestConnection.uri != firstConnection.uri) { + yield bestConnection; + } } } + + /// Update a connection's URI to use the specified URL + PlexConnection _updateConnectionUrl(PlexConnection connection, String url) { + // If the URL matches the original URI, return as-is + if (url == connection.uri) { + return connection; + } + + // Otherwise, create a new connection with the directUrl as the uri + return PlexConnection( + protocol: connection.protocol, + address: connection.address, + port: connection.port, + uri: url, + local: connection.local, + relay: connection.relay, + ipv6: connection.ipv6, + ); + } + + /// Select the best candidate considering priority, latency, and URL type preference + _ConnectionCandidate? _selectBestCandidateWithLatency( + Map<_ConnectionCandidate, ConnectionTestResult> results, + ) { + // Group candidates by connection type (local/remote/relay) + final localCandidates = results.entries + .where((e) => e.key.connection.local && !e.key.connection.relay) + .toList(); + final remoteCandidates = results.entries + .where((e) => !e.key.connection.local && !e.key.connection.relay) + .toList(); + final relayCandidates = results.entries + .where((e) => e.key.connection.relay) + .toList(); + + // Find best in each category + return _findLowestLatencyCandidate(localCandidates) ?? + _findLowestLatencyCandidate(remoteCandidates) ?? + _findLowestLatencyCandidate(relayCandidates); + } + + /// Find the candidate with lowest latency, preferring plex.direct URI on tie + _ConnectionCandidate? _findLowestLatencyCandidate( + List> entries, + ) { + if (entries.isEmpty) return null; + + // Sort by latency first, then by URL type (prefer plex.direct) + entries.sort((a, b) { + final latencyCompare = a.value.latencyMs.compareTo(b.value.latencyMs); + if (latencyCompare != 0) return latencyCompare; + + // If latencies are equal, prefer plex.direct URI (isPlexDirectUri = true) + if (a.key.isPlexDirectUri && !b.key.isPlexDirectUri) return -1; + if (!a.key.isPlexDirectUri && b.key.isPlexDirectUri) return 1; + return 0; + }); + + return entries.first.key; + } } /// Represents a connection to a Plex server @@ -447,6 +509,10 @@ class PlexConnection { }; } + /// Get the direct URL constructed from address and port + /// This bypasses plex.direct DNS and connects directly to the IP + String get directUrl => '$protocol://$address:$port'; + String get displayType { if (relay) return 'Relay'; if (local) return 'Local';