refactor: type server identifiers

This commit is contained in:
edde746
2026-06-01 11:06:03 +02:00
parent 7b59dac26b
commit 74b8dc4561
258 changed files with 2296 additions and 2080 deletions
+38 -33
View File
@@ -1,4 +1,5 @@
import 'dart:async';
import '../media/ids.dart';
import 'package:connectivity_plus/connectivity_plus.dart';
import 'package:flutter/foundation.dart';
@@ -59,7 +60,7 @@ class MultiServerManager {
/// device row on plex.tv).
final Map<String, String> _clientIdByServer = {};
String? _resolveClientIdentifier(String serverId) => _clientIdByServer[serverId];
String? _resolveClientIdentifier(ServerId serverId) => _clientIdByServer[serverId];
/// All Jellyfin clients ever added, keyed by the compound connection id
/// (`{serverMachineId}/{userId}`). Lets two users on the same Jellyfin
@@ -99,13 +100,13 @@ class MultiServerManager {
List<String> get offlineServerIds => _serverStatus.entries.where((e) => !e.value).map((e) => e.key).toList();
/// Get client for specific server.
MediaServerClient? getClient(String serverId) => _clients[serverId];
MediaServerClient? getClient(ServerId serverId) => _clients[serverId];
/// Get the [PlexClient] for a server, or `null` if the server is Jellyfin
/// (or not registered). Use for Plex-only flows (Live TV, server prefs,
/// endpoint optimization) that don't yet have a backend-neutral
/// equivalent on [MediaServerClient].
PlexClient? getPlexClient(String serverId) {
PlexClient? getPlexClient(ServerId serverId) {
final client = _clients[serverId];
return client is PlexClient ? client : null;
}
@@ -129,7 +130,7 @@ class MultiServerManager {
}
@visibleForTesting
void debugMarkAuthErrorForTesting(String serverId) {
void debugMarkAuthErrorForTesting(ServerId serverId) {
_serverStatus[serverId] = false;
_authErrorServers.add(serverId);
_statusController.add(Map.from(_serverStatus));
@@ -153,9 +154,9 @@ class MultiServerManager {
/// `owned` flag). Returns `null` for Jellyfin server ids — Jellyfin has no
/// `PlexServer` analogue. For "is this server registered?" use
/// [getClient] (works for both backends).
PlexServer? getPlexServer(String serverId) => _plexServers[serverId];
PlexServer? getPlexServer(ServerId serverId) => _plexServers[serverId];
String serverDisplayName(String serverId) =>
String serverDisplayName(ServerId serverId) =>
_clients[serverId]?.serverName ?? _plexServers[serverId]?.name ?? serverId;
/// Backend-neutral "is this user an owner/admin on [serverId]?" probe used
@@ -166,7 +167,7 @@ class MultiServerManager {
/// `ActiveProfileProvider`).
/// - Jellyfin: `JellyfinConnection.isAdministrator` captured at sign-in.
/// - Unknown server: `false`.
bool isOwnerOrAdmin(String serverId) {
bool isOwnerOrAdmin(ServerId serverId) {
final client = _clients[serverId];
if (client is PlexClient) {
return _plexServers[serverId]?.owned == true;
@@ -196,10 +197,10 @@ class MultiServerManager {
Map<String, PlexServer> get plexServers => Map.unmodifiable(_plexServers);
/// Check if a server is online
bool isServerOnline(String serverId) => _serverStatus[serverId] ?? false;
bool isServerOnline(ServerId serverId) => _serverStatus[serverId] ?? false;
/// Check whether the active or scoped client for [serverId] is online.
bool isClientOnline(String serverId, {String? clientScopeId}) {
bool isClientOnline(ServerId serverId, {String? clientScopeId}) {
if (clientScopeId != null && clientScopeId.isNotEmpty) {
return _jellyfinHealthByCompoundId[clientScopeId] == HealthStatus.online;
}
@@ -215,7 +216,7 @@ class MultiServerManager {
// Get storage and load cached endpoint for this server
final storage = await StorageService.getInstance();
final cachedEndpoint = storage.getServerEndpoint(serverId);
final cachedEndpoint = storage.getServerEndpoint(ServerId(serverId));
// The connection race already hits `/` on the winning endpoint — capture
// `transcoderVideo` from that response so PlexClient.create can skip the
@@ -248,19 +249,19 @@ class MultiServerManager {
final client = await PlexClient.create(
config,
serverId: serverId,
serverId: ServerId(serverId),
serverName: server.name,
prioritizedEndpoints: prioritizedEndpoints,
onEndpointChanged: (newUrl) async {
await storage.saveServerEndpoint(serverId, newUrl);
await storage.saveServerEndpoint(ServerId(serverId), newUrl);
appLogger.i('Updated endpoint for ${server.name} after failover: $newUrl');
},
onAllEndpointsExhausted: () => _onServerEndpointsExhausted(serverId),
onAllEndpointsExhausted: () => _onServerEndpointsExhausted(ServerId(serverId)),
seedTranscoderVideoSupport: observedTranscoderVideo,
);
// Save the initial endpoint
await storage.saveServerEndpoint(serverId, baseUrl);
await storage.saveServerEndpoint(ServerId(serverId), baseUrl);
// Drain remaining stream values in background to apply better connections
_drainOptimizationStream(streamIterator, client: client, server: server, storage: storage);
@@ -275,7 +276,7 @@ class MultiServerManager {
required StorageService storage,
required String newUrl,
}) async {
await storage.saveServerEndpoint(server.clientIdentifier, newUrl);
await storage.saveServerEndpoint(ServerId(server.clientIdentifier), newUrl);
final newEndpoints = server.prioritizedEndpointUrls(preferredFirst: newUrl);
await client.updateEndpointPreferences(newEndpoints, switchToFirst: true);
}
@@ -315,7 +316,7 @@ class MultiServerManager {
}
/// Remove a server connection
void removeServer(String serverId) {
void removeServer(ServerId serverId) {
final jellyfinCompoundIds = _jellyfinByCompoundId.entries
.where((entry) => entry.value.connection.serverMachineId == serverId)
.map((entry) => entry.key)
@@ -369,7 +370,7 @@ class MultiServerManager {
Future<int> addPlexAccount(
PlexAccountConnection connection, {
Duration timeout = MediaServerTimeouts.perServerConnect,
Function(String serverId, bool success)? onServerStatus,
Function(ServerId serverId, bool success)? onServerStatus,
}) async {
if (connection.servers.isEmpty) return 0;
appLogger.i(
@@ -391,12 +392,12 @@ class MultiServerManager {
if (oldClient != null) _closeClient(oldClient);
_clients[serverId] = client;
_serverStatus[serverId] = true;
onServerStatus?.call(serverId, true);
onServerStatus?.call(ServerId(serverId), true);
connected++;
} catch (e, stackTrace) {
appLogger.e('Failed to connect ${server.name}', error: e, stackTrace: stackTrace);
_serverStatus[serverId] = false;
onServerStatus?.call(serverId, false);
onServerStatus?.call(ServerId(serverId), false);
}
});
@@ -540,7 +541,7 @@ class MultiServerManager {
final health = await client.checkHealth();
final healthy = health == HealthStatus.online;
_jellyfinHealthByCompoundId[compoundId] = health;
_applyHealth(machineId, health);
_applyHealth(ServerId(machineId), health);
appLogger.i('Added Jellyfin server: ${resolvedConnection.serverName}${healthy ? '' : ' (unhealthy)'}');
if (_connectivitySubscription == null && healthy) {
@@ -599,7 +600,7 @@ class MultiServerManager {
///
/// Clears the auth-error flag — callers that observed an auth failure
/// should use [_applyHealth] instead.
void updateServerStatus(String serverId, bool isOnline) {
void updateServerStatus(ServerId serverId, bool isOnline) {
final prevOnline = _serverStatus[serverId];
final hadAuthError = _authErrorServers.remove(serverId);
if (prevOnline != isOnline || hadAuthError) {
@@ -612,7 +613,7 @@ class MultiServerManager {
/// Apply a health-probe outcome to both online state and auth-error
/// tracking. Used by the manager's own health checks; external callers
/// without an auth-distinct signal should use [updateServerStatus].
void _applyHealth(String serverId, HealthStatus status) {
void _applyHealth(ServerId serverId, HealthStatus status) {
final isOnline = status == HealthStatus.online;
final isAuthError = status == HealthStatus.authError;
final prevOnline = _serverStatus[serverId];
@@ -668,7 +669,7 @@ class MultiServerManager {
return;
}
}
_applyHealth(serverId, status);
_applyHealth(ServerId(serverId), status);
if (status != HealthStatus.online) {
appLogger.w('Server $serverId health check failed: ${status.name}');
}
@@ -745,14 +746,14 @@ class MultiServerManager {
continue;
}
if (!isServerOnline(serverId)) {
if (!isServerOnline(ServerId(serverId))) {
// Attempt reconnection for offline servers
_activeOptimizations[serverId] = _reconnectServer(serverId, server).whenComplete(() {
_activeOptimizations[serverId] = _reconnectServer(ServerId(serverId), server).whenComplete(() {
_activeOptimizations.remove(serverId);
});
} else {
// Re-optimize online servers
_activeOptimizations[serverId] = _reoptimizeServer(serverId: serverId, server: server, reason: reason)
_activeOptimizations[serverId] = _reoptimizeServer(serverId: ServerId(serverId), server: server, reason: reason)
.whenComplete(() {
_activeOptimizations.remove(serverId);
});
@@ -764,7 +765,7 @@ class MultiServerManager {
for (final entry in _activeJellyfinMachine.entries) {
final serverId = entry.key;
if (_activeOptimizations.containsKey(serverId)) continue;
if (isServerOnline(serverId)) continue;
if (isServerOnline(ServerId(serverId))) continue;
final client = _jellyfinByCompoundId[entry.value];
if (client == null) continue;
@@ -780,7 +781,11 @@ class MultiServerManager {
/// Today this only runs against Plex servers — the connection-racing logic
/// is built around [PlexServer.findBestWorkingConnection]. Non-Plex
/// clients short-circuit until a backend-agnostic equivalent lands.
Future<void> _reoptimizeServer({required String serverId, required PlexServer server, required String reason}) async {
Future<void> _reoptimizeServer({
required ServerId serverId,
required PlexServer server,
required String reason,
}) async {
final storage = await StorageService.getInstance();
final raw = _clients[serverId];
final client = raw is PlexClient ? raw : null;
@@ -819,7 +824,7 @@ class MultiServerManager {
}
/// Attempt full reconnection for a single offline server
Future<void> _reconnectServer(String serverId, PlexServer server) async {
Future<void> _reconnectServer(ServerId serverId, PlexServer server) async {
final clientId = _resolveClientIdentifier(serverId);
if (clientId == null) {
appLogger.w('Cannot reconnect ${server.name}: no client identifier cached');
@@ -858,7 +863,7 @@ class MultiServerManager {
appLogger.d('Ignoring stale Jellyfin reconnection result for ${client.connection.serverName}');
return;
}
_applyHealth(machineId, status);
_applyHealth(ServerId(machineId), status);
if (status == HealthStatus.online) {
appLogger.i('Successfully reconnected to ${client.connection.serverName}');
} else {
@@ -901,7 +906,7 @@ class MultiServerManager {
if (forceRediscovery) {
final storage = await StorageService.getInstance();
await Future.wait(offline.map(storage.clearServerEndpoint));
await Future.wait(offline.map((id) => storage.clearServerEndpoint(ServerId(id))));
}
final futures = offline.map((serverId) {
@@ -910,7 +915,7 @@ class MultiServerManager {
final server = _plexServers[serverId];
if (server != null) {
final future = _reconnectServer(serverId, server)
final future = _reconnectServer(ServerId(serverId), server)
.timeout(
const Duration(seconds: 15),
onTimeout: () {
@@ -950,7 +955,7 @@ class MultiServerManager {
/// Called when all failover endpoints are exhausted for a server.
/// Debounced per-server to prevent cascading reconnections from parallel failures.
void _onServerEndpointsExhausted(String serverId) {
void _onServerEndpointsExhausted(ServerId serverId) {
// Cancel any existing debounce timer for this server
_reconnectDebounce[serverId]?.cancel();