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
This commit is contained in:
Matt Vogel
2026-02-26 08:21:06 -05:00
parent 2b5923d80b
commit e9218e0a3d
3 changed files with 29 additions and 10 deletions
+2 -2
View File
@@ -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
+8 -6
View File
@@ -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<PlexConnection> findBestWorkingConnection({String? preferredUri}) async* {
Stream<PlexConnection> 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) {
+19 -2
View File
@@ -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 = <String, String>{'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 = <ConnectionTestResult>[];
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) {