From e9218e0a3d5e9f9496e121daf4f1d48fe9725b7d Mon Sep 17 00:00:00 2001 From: Matt Vogel Date: Thu, 26 Feb 2026 12:37:17 +0000 Subject: [PATCH] Fix duplicate Plex notifications and Flutter app name on login Connection probe requests in testConnectionWithLatency were sent without X-Plex-Client-Identifier, X-Plex-Product, or X-Plex-Device-Name headers. Plex treated each anonymous probe as a new unknown device and fired a "New Device" notification for every server tested (one per shared/owned server), while displaying "Flutter" as the device name from the HTTP user-agent. Pass clientIdentifier through findBestWorkingConnection and all connection test helpers so every probe request identifies itself as "Plezy" with the persistent client UUID. This prevents spurious notifications and ensures the device shows the correct app name in Plex's device list. https://claude.ai/code/session_01V5VraujkNmk5GGPLyZ33fN --- lib/services/multi_server_manager.dart | 4 ++-- lib/services/plex_auth_service.dart | 14 ++++++++------ lib/services/plex_client.dart | 21 +++++++++++++++++++-- 3 files changed, 29 insertions(+), 10 deletions(-) diff --git a/lib/services/multi_server_manager.dart b/lib/services/multi_server_manager.dart index 9afd4282..4b79c75d 100644 --- a/lib/services/multi_server_manager.dart +++ b/lib/services/multi_server_manager.dart @@ -83,7 +83,7 @@ class MultiServerManager { final cachedEndpoint = storage.getServerEndpoint(serverId); // Find best working connection, passing cached endpoint for fast-path - final streamIterator = StreamIterator(server.findBestWorkingConnection(preferredUri: cachedEndpoint)); + final streamIterator = StreamIterator(server.findBestWorkingConnection(preferredUri: cachedEndpoint, clientIdentifier: clientIdentifier)); if (!await streamIterator.moveNext()) { throw Exception('No working connection found'); @@ -388,7 +388,7 @@ class MultiServerManager { try { appLogger.d('Starting connection optimization for ${server.name}', error: {'reason': reason}); - await for (final connection in server.findBestWorkingConnection(preferredUri: cachedEndpoint)) { + await for (final connection in server.findBestWorkingConnection(preferredUri: cachedEndpoint, clientIdentifier: _clientIdentifier)) { final newUrl = connection.uri; // Check if this is actually a better connection than current diff --git a/lib/services/plex_auth_service.dart b/lib/services/plex_auth_service.dart index ef715099..574d3151 100644 --- a/lib/services/plex_auth_service.dart +++ b/lib/services/plex_auth_service.dart @@ -392,7 +392,7 @@ class PlexServer { /// 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({String? preferredUri}) async* { + Stream findBestWorkingConnection({String? preferredUri, String? clientIdentifier}) async* { if (connections.isEmpty) { appLogger.w('No connections available for server discovery'); return; @@ -438,6 +438,7 @@ class PlexServer { cachedCandidate.url, accessToken, timeout: preferredTimeout, + clientIdentifier: clientIdentifier, ); if (result.success) { @@ -457,7 +458,7 @@ class PlexServer { appLogger.d('Running connection race to find first working endpoint', error: {'candidateCount': totalCandidates}); for (final candidate in candidates) { - PlexClient.testConnectionWithLatency(candidate.url, accessToken, timeout: raceTimeout).then((result) { + PlexClient.testConnectionWithLatency(candidate.url, accessToken, timeout: raceTimeout, clientIdentifier: clientIdentifier).then((result) { completedTests++; if (!result.success) { @@ -502,7 +503,7 @@ class PlexServer { } // Attempt HTTPS upgrade on the Phase 1 winner before emitting - final upgradedFirstCandidate = await _upgradeCandidateToHttpsIfPossible(firstCandidate); + final upgradedFirstCandidate = await _upgradeCandidateToHttpsIfPossible(firstCandidate, clientIdentifier: clientIdentifier); final emitCandidate = upgradedFirstCandidate ?? firstCandidate; final firstConnection = _updateConnectionUrl(emitCandidate.connection, emitCandidate.url); @@ -524,7 +525,7 @@ class PlexServer { await Future.wait( candidates.map((candidate) async { - final result = await PlexClient.testConnectionWithAverageLatency(candidate.url, accessToken, attempts: 2); + final result = await PlexClient.testConnectionWithAverageLatency(candidate.url, accessToken, attempts: 2, clientIdentifier: clientIdentifier); if (result.success) { candidateResults[candidate] = result; @@ -548,7 +549,7 @@ class PlexServer { // Emit the best connection if it's different from the first one if (bestCandidate != null) { - final upgradedCandidate = await _upgradeCandidateToHttpsIfPossible(bestCandidate) ?? bestCandidate; + final upgradedCandidate = await _upgradeCandidateToHttpsIfPossible(bestCandidate, clientIdentifier: clientIdentifier) ?? bestCandidate; final bestConnection = _updateConnectionUrl(upgradedCandidate.connection, upgradedCandidate.url); if (bestConnection.uri != firstConnection.uri) { @@ -666,7 +667,7 @@ class PlexServer { return urls; } - Future<_ConnectionCandidate?> _upgradeCandidateToHttpsIfPossible(_ConnectionCandidate candidate) async { + Future<_ConnectionCandidate?> _upgradeCandidateToHttpsIfPossible(_ConnectionCandidate candidate, {String? clientIdentifier}) async { final currentUrl = candidate.url; if (currentUrl.startsWith('https://')) { return null; @@ -716,6 +717,7 @@ class PlexServer { httpsUrl, accessToken, timeout: ConnectionTimeouts.connectionRace, + clientIdentifier: clientIdentifier, ); if (!result.success) { diff --git a/lib/services/plex_client.dart b/lib/services/plex_client.dart index 7aac83c8..e9ebd435 100644 --- a/lib/services/plex_client.dart +++ b/lib/services/plex_client.dart @@ -215,6 +215,8 @@ class PlexClient { String baseUrl, String token, { Duration timeout = const Duration(seconds: 5), + String? clientIdentifier, + String appName = 'Plezy', }) async { final stopwatch = Stopwatch()..start(); @@ -230,7 +232,14 @@ class PlexClient { ), ); - final response = await dio.get('/', options: Options(headers: {'X-Plex-Token': token})); + final headers = {'X-Plex-Token': token}; + if (clientIdentifier != null) { + headers['X-Plex-Client-Identifier'] = clientIdentifier; + headers['X-Plex-Product'] = appName; + headers['X-Plex-Device-Name'] = appName; + } + + final response = await dio.get('/', options: Options(headers: headers)); stopwatch.stop(); final success = response.statusCode == 200; @@ -266,11 +275,19 @@ class PlexClient { String token, { int attempts = 3, Duration timeout = const Duration(seconds: 5), + String? clientIdentifier, + String appName = 'Plezy', }) async { final results = []; for (int i = 0; i < attempts; i++) { - final result = await testConnectionWithLatency(baseUrl, token, timeout: timeout); + final result = await testConnectionWithLatency( + baseUrl, + token, + timeout: timeout, + clientIdentifier: clientIdentifier, + appName: appName, + ); // If any attempt fails, return failed result immediately if (!result.success) {