fix(explore): list every library copy of a title, not one per server
`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
This commit is contained in:
@@ -4,6 +4,7 @@ import 'package:plezy/media/media_backend.dart';
|
||||
import 'package:plezy/media/media_item.dart';
|
||||
import 'package:plezy/media/media_item_merge.dart';
|
||||
import 'package:plezy/media/media_kind.dart';
|
||||
import 'package:plezy/media/media_version.dart';
|
||||
import '../test_helpers/media_items.dart';
|
||||
|
||||
void main() {
|
||||
@@ -47,4 +48,69 @@ void main() {
|
||||
expect(merged.libraryId, 'old-lib');
|
||||
expect(merged.libraryTitle, 'Old');
|
||||
});
|
||||
|
||||
MediaItem copy({
|
||||
required String id,
|
||||
String? libraryTitle,
|
||||
String? videoResolution,
|
||||
List<MediaVersion>? mediaVersions,
|
||||
}) => testMediaItem(
|
||||
id: id,
|
||||
serverId: 'server-1',
|
||||
serverName: 'Living Room',
|
||||
libraryId: libraryTitle == null ? null : '$id-lib',
|
||||
libraryTitle: libraryTitle,
|
||||
mediaVersions:
|
||||
mediaVersions ??
|
||||
(videoResolution == null ? null : [MediaVersion(id: '$id-v', videoResolution: videoResolution)]),
|
||||
);
|
||||
|
||||
test('orders library copies by resolution, best first', () {
|
||||
final merged = mergeLibraryCopies(const [], [
|
||||
copy(id: 'hd', libraryTitle: 'Movies', videoResolution: '1080'),
|
||||
copy(id: 'uhd', libraryTitle: '4K Movies', videoResolution: '4k'),
|
||||
]);
|
||||
|
||||
expect(merged.map((item) => item.id), ['uhd', 'hd']);
|
||||
});
|
||||
|
||||
test('keeps copies a later pass did not return', () {
|
||||
final merged = mergeLibraryCopies([copy(id: 'hd', libraryTitle: 'Movies')], const []);
|
||||
|
||||
expect(merged.map((item) => item.id), ['hd']);
|
||||
});
|
||||
|
||||
test('an unstamped re-resolve does not strip a copy of its library', () {
|
||||
// Jellyfin's library stamp is a best-effort ancestors lookup that returns
|
||||
// the item unstamped when it fails. Losing the title would make this copy
|
||||
// indistinguishable from its sibling in the server's other library.
|
||||
final merged = mergeLibraryCopies(
|
||||
[copy(id: 'hd', libraryTitle: 'Movies', videoResolution: '1080')],
|
||||
[copy(id: 'hd')],
|
||||
);
|
||||
|
||||
expect(merged.single.libraryId, 'hd-lib');
|
||||
expect(merged.single.libraryTitle, 'Movies');
|
||||
expect(merged.single.serverName, 'Living Room');
|
||||
expect(merged.single.mediaVersions?.single.videoResolution, '1080');
|
||||
});
|
||||
|
||||
test('a re-resolve carrying fresher library context wins', () {
|
||||
final merged = mergeLibraryCopies(
|
||||
[copy(id: 'hd', libraryTitle: 'Movies', videoResolution: '1080')],
|
||||
[copy(id: 'hd', libraryTitle: 'Renamed Movies', videoResolution: '4k')],
|
||||
);
|
||||
|
||||
expect(merged.single.libraryTitle, 'Renamed Movies');
|
||||
expect(merged.single.mediaVersions?.single.videoResolution, '4k');
|
||||
});
|
||||
|
||||
test('an empty version list is treated as absent, not as a downgrade', () {
|
||||
final merged = mergeLibraryCopies(
|
||||
[copy(id: 'hd', libraryTitle: 'Movies', videoResolution: '1080')],
|
||||
[copy(id: 'hd', libraryTitle: 'Movies', mediaVersions: const [])],
|
||||
);
|
||||
|
||||
expect(merged.single.mediaVersions?.single.videoResolution, '1080');
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user