diff --git a/lib/services/plex_auth_service.dart b/lib/services/plex_auth_service.dart index 4792d16c..24d9d07a 100644 --- a/lib/services/plex_auth_service.dart +++ b/lib/services/plex_auth_service.dart @@ -250,9 +250,18 @@ class PlexServer { factory PlexServer.fromJson(Map json) { final List connectionsJson = json['connections'] as List; - final connections = connectionsJson - .map((c) => PlexConnection.fromJson(c as Map)) - .toList(); + final connections = []; + + // Parse connections and generate HTTP fallbacks for HTTPS connections + for (final c in connectionsJson) { + final connection = PlexConnection.fromJson(c as Map); + connections.add(connection); + + // Generate HTTP fallback for HTTPS connections + if (connection.protocol == 'https') { + connections.add(connection.toHttpFallback()); + } + } DateTime? lastSeenAt; if (json['lastSeenAt'] != null) { @@ -317,18 +326,33 @@ class PlexServer { /// 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) + /// Priority: local > remote > relay, then HTTPS > HTTP, then lowest latency /// Tests both plex.direct URI and direct IP for each connection + /// HTTPS connections are tested first, with HTTP as fallback Stream findBestWorkingConnection() async* { if (connections.isEmpty) return; // Create candidates: test both uri and directUrl for each connection - final candidates = <_ConnectionCandidate>[]; + // Separate HTTPS and HTTP candidates to prioritize HTTPS first + final httpsCandidates = <_ConnectionCandidate>[]; + final httpCandidates = <_ConnectionCandidate>[]; + for (final connection in connections) { - candidates.add(_ConnectionCandidate(connection, connection.uri, true)); - candidates.add(_ConnectionCandidate(connection, connection.directUrl, false)); + final uriCandidate = _ConnectionCandidate(connection, connection.uri, true); + final directCandidate = _ConnectionCandidate(connection, connection.directUrl, false); + + if (connection.protocol == 'https') { + httpsCandidates.add(uriCandidate); + httpsCandidates.add(directCandidate); + } else { + httpCandidates.add(uriCandidate); + httpCandidates.add(directCandidate); + } } + // Combine candidates with HTTPS first, then HTTP + final candidates = [...httpsCandidates, ...httpCandidates]; + // Phase 1: Race to find first working connection final completer = Completer<_ConnectionCandidate?>(); _ConnectionCandidate? firstCandidate; @@ -444,18 +468,24 @@ class PlexServer { _findLowestLatencyCandidate(relayCandidates); } - /// Find the candidate with lowest latency, preferring plex.direct URI on tie + /// Find the candidate with lowest latency, preferring HTTPS and 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) + // Sort by latency first, then by protocol (HTTPS > HTTP), 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 latencies are equal, prefer HTTPS over HTTP + final aIsHttps = a.key.connection.protocol == 'https'; + final bIsHttps = b.key.connection.protocol == 'https'; + if (aIsHttps && !bIsHttps) return -1; + if (!aIsHttps && bIsHttps) return 1; + + // If latencies and protocols 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; @@ -518,4 +548,20 @@ class PlexConnection { if (local) return 'Local'; return 'Remote'; } + + /// Create an HTTP fallback version of this HTTPS connection + /// This allows testing HTTP when HTTPS is unavailable (e.g., certificate issues) + PlexConnection toHttpFallback() { + assert(protocol == 'https', 'Can only create HTTP fallback for HTTPS connections'); + + return PlexConnection( + protocol: 'http', + address: address, + port: port, + uri: uri.replaceFirst('https://', 'http://'), + local: local, + relay: relay, + ipv6: ipv6, + ); + } } diff --git a/macos/Podfile.lock b/macos/Podfile.lock index 294f32e9..fbec16e1 100644 --- a/macos/Podfile.lock +++ b/macos/Podfile.lock @@ -90,9 +90,9 @@ SPEC CHECKSUMS: media_kit_libs_macos_video: 85a23e549b5f480e72cae3e5634b5514bc692f65 media_kit_video: fa6564e3799a0a28bff39442334817088b7ca758 package_info_plus: f0052d280d17aa382b932f399edf32507174e870 - path_provider_foundation: 080d55be775b7414fd5a5ef3ac137b97b097e564 + path_provider_foundation: bb55f6dbba17d0dccd6737fe6f7f34fbd0376880 screen_retriever_macos: 452e51764a9e1cdb74b3c541238795849f21557f - shared_preferences_foundation: 9e1978ff2562383bd5676f64ec4e9aa8fa06a6f7 + shared_preferences_foundation: 7036424c3d8ec98dfe75ff1667cb0cd531ec82bb sqflite_darwin: 20b2a3a3b70e43edae938624ce550a3cbf66a3d0 url_launcher_macos: f87a979182d112f911de6820aefddaf56ee9fbfd volume_controller: 5c068e6d085c80dadd33fc2c918d2114b775b3dd