fix(jellyfin): flip server status when all endpoints are exhausted

This commit is contained in:
edde746
2026-06-12 15:26:39 +02:00
parent c849a213f0
commit 0d7dd093c4
3 changed files with 46 additions and 4 deletions
+4
View File
@@ -106,6 +106,7 @@ class JellyfinClient
static Future<JellyfinClient> create(
JellyfinConnection connection, {
FavoriteChannelsRepository? favoritesRepository,
void Function()? onAllEndpointsExhausted,
}) async {
// Register before any HTTP traffic so the very first probe URL doesn't
// leak the token verbatim. `LogRedactionManager.redact()` also has
@@ -142,6 +143,7 @@ class JellyfinClient
logLabel: 'Jellyfin',
prioritizedEndpoints: connection.baseUrls,
onEndpointSwitch: (newBaseUrl, {required persist}) => client._handleEndpointSwitch(newBaseUrl, persist: persist),
onAllEndpointsExhausted: onAllEndpointsExhausted,
);
client = JellyfinClient._(connection: connection, http: http, favoritesRepository: favoritesRepository);
return client;
@@ -154,6 +156,7 @@ class JellyfinClient
required JellyfinConnection connection,
required http.Client httpClient,
FavoriteChannelsRepository? favoritesRepository,
void Function()? onAllEndpointsExhausted,
}) {
late JellyfinClient client;
final mediaHttp = FailoverHttpClient(
@@ -162,6 +165,7 @@ class JellyfinClient
logLabel: 'Jellyfin',
prioritizedEndpoints: connection.baseUrls,
onEndpointSwitch: (newBaseUrl, {required persist}) => client._handleEndpointSwitch(newBaseUrl, persist: persist),
onAllEndpointsExhausted: onAllEndpointsExhausted,
client: httpClient,
);
client = JellyfinClient._(connection: connection, http: mediaHttp, favoritesRepository: favoritesRepository);
+25 -4
View File
@@ -575,7 +575,12 @@ class MultiServerManager {
}
}
final client = await JellyfinClient.create(resolvedConnection);
final exhaustedMachineId = resolvedConnection.serverMachineId;
final exhaustedCompoundId = resolvedConnection.id;
final client = await JellyfinClient.create(
resolvedConnection,
onAllEndpointsExhausted: () => _onJellyfinEndpointsExhausted(exhaustedMachineId, exhaustedCompoundId),
);
// Admin status can change server-side; re-broadcast and persist so
// admin-gated UI survives app restarts without requiring re-auth.
_wireJellyfinConnectionUpdates(client);
@@ -1022,8 +1027,10 @@ class MultiServerManager {
_reconnectDebounce[serverId] = Timer(const Duration(seconds: 5), () {
_reconnectDebounce.remove(serverId);
final server = _plexServers[serverId];
if (server == null) return;
final plexServer = _plexServers[serverId];
final jellyfinCompoundId = _activeJellyfinMachine[serverId];
final jellyfinClient = jellyfinCompoundId != null ? _jellyfinByCompoundId[jellyfinCompoundId] : null;
if (plexServer == null && jellyfinClient == null) return;
appLogger.i('All endpoints exhausted for $serverId, triggering reconnection');
updateServerStatus(serverId, false);
@@ -1031,12 +1038,26 @@ class MultiServerManager {
// Guard with _activeOptimizations to prevent duplicate reconnections
if (_activeOptimizations.containsKey(serverId)) return;
_activeOptimizations[serverId] = _reconnectServer(serverId, server).whenComplete(() {
final reconnect = plexServer != null
? _reconnectServer(serverId, plexServer)
: _reconnectJellyfinServer(serverId, jellyfinClient!);
_activeOptimizations[serverId] = reconnect.whenComplete(() {
_activeOptimizations.remove(serverId);
});
});
}
/// Jellyfin clients outlive their active binding (a previous profile's
/// client stays in [_jellyfinByCompoundId]); only the currently bound
/// client's exhaustion may flip the machine's status.
void _onJellyfinEndpointsExhausted(String machineId, String compoundId) {
if (_activeJellyfinMachine[machineId] != compoundId) {
appLogger.d('Ignoring endpoint exhaustion from inactive Jellyfin client', error: compoundId);
return;
}
_onServerEndpointsExhausted(ServerId(machineId));
}
/// Disconnect all servers
void disconnectAll() {
appLogger.i('Disconnecting all servers');
@@ -165,6 +165,23 @@ void main() {
expect(client.connection.baseUrls, ['https://fallback.example.com', 'https://primary.example.com']);
});
test('exhausting every endpoint fires onAllEndpointsExhausted', () async {
var exhausted = 0;
final client = JellyfinClient.forTesting(
connection: _conn(
baseUrl: 'https://primary.example.com',
baseUrls: const ['https://primary.example.com', 'https://fallback.example.com'],
),
httpClient: MockClient((req) async => throw TimeoutException('endpoint down')),
onAllEndpointsExhausted: () => exhausted++,
);
addTearDown(client.close);
await client.getMachineIdentifier();
expect(exhausted, 1);
});
test('resets live base URL after fallback endpoint is exhausted', () async {
final requests = <Uri>[];
final client = JellyfinClient.forTesting(