fix(jellyfin): keep unreachable endpoints when saving a connection
Persist every user-entered URL that is not positively known to belong to a different server, matching reconcilePreviouslyStoredBaseUrls. Requiring a successful identity probe deleted a stored LAN endpoint whenever the box was asleep or the user saved from outside the network.
This commit is contained in:
@@ -25,8 +25,9 @@ class JellyfinServerInfo {
|
||||
class JellyfinEndpointRaceResult {
|
||||
final String activeBaseUrl;
|
||||
|
||||
/// Trusted, active-first endpoints. Every fallback completed an
|
||||
/// unauthenticated public probe and reported [serverInfo]'s exact machine ID.
|
||||
/// Active-first endpoints selected for persistence. Candidates that reported
|
||||
/// another machine ID are excluded; candidates that returned no trustworthy
|
||||
/// identity are retained for a later retry.
|
||||
final List<String> baseUrls;
|
||||
final JellyfinServerInfo serverInfo;
|
||||
final Map<String, String> _verifiedEffectiveBaseUrls;
|
||||
@@ -159,11 +160,11 @@ class JellyfinEndpointDiscovery {
|
||||
}
|
||||
}
|
||||
|
||||
/// Races public Jellyfin probes and returns only identity-verified endpoints.
|
||||
/// Races public Jellyfin probes and returns persistence-safe endpoints.
|
||||
///
|
||||
/// [baseUrlsToPersist] contains caller-selected persistence candidates, not
|
||||
/// pre-trusted URLs. Unreachable candidates and candidates for another
|
||||
/// machine are never included in the returned [JellyfinEndpointRaceResult].
|
||||
/// [baseUrlsToPersist] contains caller-selected persistence candidates.
|
||||
/// Candidates that reported another machine are excluded; candidates that
|
||||
/// returned no trustworthy identity are retained for a later retry.
|
||||
Future<JellyfinEndpointRaceResult> raceEndpoints(
|
||||
Iterable<String> baseUrls, {
|
||||
String? preferredUrl,
|
||||
@@ -288,14 +289,12 @@ class JellyfinEndpointDiscovery {
|
||||
}
|
||||
|
||||
final effectiveUrls = <String, String>{};
|
||||
final matchingBaseUrls = <String>{};
|
||||
final machineMismatchBaseUrls = <String>{};
|
||||
for (final entry in identityResults.entries) {
|
||||
if (entry.value.serverInfo?.machineId != expected) {
|
||||
machineMismatchBaseUrls.add(entry.key.url);
|
||||
continue;
|
||||
}
|
||||
matchingBaseUrls.add(entry.key.url);
|
||||
final effectiveBaseUrl = entry.value.effectiveBaseUrl;
|
||||
if (effectiveBaseUrl != null) {
|
||||
effectiveUrls[entry.key.url] = effectiveBaseUrl;
|
||||
@@ -305,7 +304,7 @@ class JellyfinEndpointDiscovery {
|
||||
effectiveUrls[selectedCandidate.url] = activeBaseUrl;
|
||||
final persistedUrls = [
|
||||
for (final url in persistUrls)
|
||||
if (matchingBaseUrls.contains(url)) effectiveUrls[url] ?? url,
|
||||
if (!machineMismatchBaseUrls.contains(url)) effectiveUrls[url] ?? url,
|
||||
];
|
||||
|
||||
return JellyfinEndpointRaceResult._(
|
||||
|
||||
@@ -157,7 +157,7 @@ void main() {
|
||||
);
|
||||
});
|
||||
|
||||
test('persists only explicit URLs that proved the selected machine identity', () async {
|
||||
test('persists explicit URLs while probing without authentication', () async {
|
||||
final probeRequests = <http.Request>[];
|
||||
final discovery = JellyfinEndpointDiscovery(
|
||||
testHttpClientFactory: () => MockClient((req) async {
|
||||
@@ -180,7 +180,7 @@ void main() {
|
||||
);
|
||||
|
||||
expect(result.activeBaseUrl, 'https://jf.example.com');
|
||||
expect(result.baseUrls, ['https://jf.example.com']);
|
||||
expect(result.baseUrls, ['https://jf.example.com', 'https://offline.example.com']);
|
||||
expect(probeRequests, isNotEmpty);
|
||||
for (final request in probeRequests) {
|
||||
final headerNames = request.headers.keys.map((name) => name.toLowerCase());
|
||||
@@ -209,20 +209,42 @@ void main() {
|
||||
expect(result.serverInfo.machineId, 'srv-1');
|
||||
});
|
||||
|
||||
test('excludes unreachable URLs from the trusted failover list', () async {
|
||||
test('an unreachable persisted endpoint survives a save', () async {
|
||||
const offlineUrl = 'https://offline.example.com';
|
||||
const activeUrl = 'https://jf.example.com';
|
||||
final discovery = JellyfinEndpointDiscovery(
|
||||
testHttpClientFactory: () => MockClient((req) async {
|
||||
if (req.url.host == 'offline.example.com') {
|
||||
testHttpClientFactory: () => MockClient((request) async {
|
||||
if (request.url.host == 'offline.example.com') {
|
||||
throw TimeoutException('offline');
|
||||
}
|
||||
return _info(id: 'srv-1');
|
||||
}),
|
||||
);
|
||||
|
||||
final result = await discovery.raceEndpoints(['https://offline.example.com', 'https://jf.example.com']);
|
||||
final result = await discovery.raceEndpoints([offlineUrl, activeUrl], baseUrlsToPersist: [offlineUrl, activeUrl]);
|
||||
|
||||
expect(result.activeBaseUrl, 'https://jf.example.com');
|
||||
expect(result.baseUrls, ['https://jf.example.com']);
|
||||
expect(result.activeBaseUrl, activeUrl);
|
||||
expect(result.baseUrls, [activeUrl, offlineUrl]);
|
||||
});
|
||||
|
||||
test('a different-machine persisted endpoint remains excluded', () async {
|
||||
const activeUrl = 'https://jf.example.com';
|
||||
const differentMachineUrl = 'https://other.example.com';
|
||||
final discovery = JellyfinEndpointDiscovery(
|
||||
testHttpClientFactory: () =>
|
||||
MockClient((request) async => _info(id: request.url.host == 'other.example.com' ? 'srv-2' : 'srv-1')),
|
||||
);
|
||||
|
||||
final result = await discovery.raceEndpoints(
|
||||
[activeUrl, differentMachineUrl],
|
||||
expectedMachineId: 'srv-1',
|
||||
baseUrlsToPersist: [activeUrl, differentMachineUrl],
|
||||
baseUrlsToValidate: const [],
|
||||
);
|
||||
|
||||
expect(result.activeBaseUrl, activeUrl);
|
||||
expect(result.baseUrls, [activeUrl]);
|
||||
expect(result.reconcilePreviouslyStoredBaseUrls([activeUrl, differentMachineUrl]), [activeUrl]);
|
||||
});
|
||||
|
||||
test('rejects reachable URLs that point to different Jellyfin servers', () async {
|
||||
@@ -249,7 +271,7 @@ void main() {
|
||||
);
|
||||
});
|
||||
|
||||
test('expected machine ID keeps only reachable matching candidates', () async {
|
||||
test('expected machine ID retains candidates that returned no identity', () async {
|
||||
final discovery = JellyfinEndpointDiscovery(
|
||||
testHttpClientFactory: () => MockClient((request) async {
|
||||
if (request.url.host == 'offline.example.com') {
|
||||
@@ -265,7 +287,7 @@ void main() {
|
||||
], expectedMachineId: 'srv-1');
|
||||
|
||||
expect(result.activeBaseUrl, 'https://matching.example.com');
|
||||
expect(result.baseUrls, ['https://matching.example.com']);
|
||||
expect(result.baseUrls, ['https://matching.example.com', 'https://offline.example.com']);
|
||||
});
|
||||
|
||||
test('reconciles stored endpoints without pruning candidates that returned no identity', () async {
|
||||
@@ -285,7 +307,7 @@ void main() {
|
||||
baseUrlsToValidate: const [],
|
||||
);
|
||||
|
||||
expect(result.baseUrls, ['https://active.example.com']);
|
||||
expect(result.baseUrls, ['https://active.example.com', 'https://offline.example.com']);
|
||||
expect(result.reconcilePreviouslyStoredBaseUrls(storedBaseUrls), [
|
||||
'https://active.example.com',
|
||||
'https://offline.example.com',
|
||||
|
||||
Reference in New Issue
Block a user