`MediaServerClient.findByExternalIds` returned `MediaItem?`, so the Explore "In these libraries" chooser could never show more than one copy per server. A movie held by both a 4K library and an HD library on one Plex server therefore resolved to whichever copy came back first, with no way to reach the other. Return every id-verified match instead. `/library/all` is already server-wide and each `Metadata` entry carries its own `librarySectionID`, so both copies come back labelled with no extra request; Plex was simply taking `Metadata[0]` and the title ladder was returning on its first hit. An exact-guid hit no longer short-circuits the title search either — a library still on a legacy agent has a different primary guid and is invisible to the `guid=` filter. Copies are deduped by global key and ordered best-first, and each row now states its resolution, since library names need not mention it. Resolution passes merge rather than replace: the cross-server fan-out logs and skips per-server failures, so a later pass can come back short a server that answered an earlier one, and a failed pass no longer claims the title left the library. Duplicate keys fold field by field, because Jellyfin's library stamp is a best-effort ancestors lookup that returns the item bare when it fails and an unstamped row is indistinguishable from its sibling. Focus nodes are keyed by copy and reclaimed after a merge re-sorts the rows, so a dpad user is not thrown to a different copy. close #1754
73 lines
2.5 KiB
Dart
73 lines
2.5 KiB
Dart
import 'package:flutter_test/flutter_test.dart';
|
|
import 'package:plezy/utils/external_ids.dart';
|
|
|
|
void main() {
|
|
group('ExternalIds.intersects', () {
|
|
test('matches when any shared id form is equal', () {
|
|
const trakt = ExternalIds(imdb: 'tt0133093', tmdb: 603);
|
|
expect(trakt.intersects(const ExternalIds(tmdb: 603)), isTrue);
|
|
expect(trakt.intersects(const ExternalIds(imdb: 'tt0133093', tvdb: 999)), isTrue);
|
|
});
|
|
|
|
test('never matches on absent or differing ids', () {
|
|
const trakt = ExternalIds(imdb: 'tt0133093');
|
|
expect(trakt.intersects(const ExternalIds(tmdb: 603)), isFalse);
|
|
expect(trakt.intersects(const ExternalIds(imdb: 'tt9999999')), isFalse);
|
|
expect(const ExternalIds().intersects(const ExternalIds()), isFalse);
|
|
});
|
|
});
|
|
|
|
group('ExternalIds.jellyfinCandidatesMatching', () {
|
|
const target = ExternalIds(imdb: 'tt0133093', tmdb: 603);
|
|
|
|
test('picks the candidate whose ProviderIds intersect, skipping others', () {
|
|
final candidates = <Map<String, dynamic>>[
|
|
{
|
|
'Name': 'The Matrix Reloaded',
|
|
'ProviderIds': {'Imdb': 'tt0234215', 'Tmdb': '604'},
|
|
},
|
|
{'Name': 'No provider ids'},
|
|
{
|
|
'Name': 'The Matrix',
|
|
'ProviderIds': {'Tmdb': '603'},
|
|
},
|
|
];
|
|
final matches = ExternalIds.jellyfinCandidatesMatching(candidates, target);
|
|
expect(matches, hasLength(1));
|
|
expect(matches.single['Name'], 'The Matrix');
|
|
});
|
|
|
|
test('returns every library copy of the same title, in response order', () {
|
|
final candidates = <Map<String, dynamic>>[
|
|
{
|
|
'Id': 'movie-4k',
|
|
'Name': 'The Matrix (4K)',
|
|
'ProviderIds': {'Tmdb': '603'},
|
|
},
|
|
{
|
|
'Id': 'movie-other',
|
|
'Name': 'The Matrix Reloaded',
|
|
'ProviderIds': {'Imdb': 'tt0234215'},
|
|
},
|
|
{
|
|
'Id': 'movie-hd',
|
|
'Name': 'The Matrix',
|
|
'ProviderIds': {'Imdb': 'tt0133093'},
|
|
},
|
|
];
|
|
final matches = ExternalIds.jellyfinCandidatesMatching(candidates, target);
|
|
expect(matches.map((candidate) => candidate['Id']), ['movie-4k', 'movie-hd']);
|
|
});
|
|
|
|
test('returns no candidates when nothing verifies', () {
|
|
final candidates = <Map<String, dynamic>>[
|
|
{
|
|
'Name': 'Similar title, different film',
|
|
'ProviderIds': {'Imdb': 'tt0234215'},
|
|
},
|
|
];
|
|
expect(ExternalIds.jellyfinCandidatesMatching(candidates, target), isEmpty);
|
|
});
|
|
});
|
|
}
|