fix(plex): read external ids from legacy agent and HAMA AniDB guids
Plex only builds the `Guid` array for the Plex Movie / Plex TV Series agents. A library still on a legacy agent answers with the scalar `guid` alone, so `fetchExternalIds` returned nothing for it and every consumer went quiet: trackers logged "no external IDs" and skipped the write, manual ratings showed "Not available", the detail screen dropped its watchlist button, and Continue Watching stopped collapsing duplicate copies. The reverse lookup already read that scalar; only the forward path ignored it. Read both shapes from the one request the method already makes, with the array winning per field and the scalar filling the rest. HAMA identifies anime by AniDB id and nothing else, which no id set could carry. AniDB is the Fribb mapping's own primary key, so it now travels on `ExternalIds` and indexes those rows directly — 7177 of them expose no tvdb/tmdb/imdb at all and were unreachable by any other path. Only plain `anidb-` maps: `anidb2`..`anidb9` group several AniDB entries under one TVDB-numbered show, so the guid names the root entry only. Two guards keep the new id where it means something. It is trusted for season 1, because that mode puts the anime there and its specials in season 0, while a higher season means the library is numbered by TVDB instead. And it resolves nothing for Trakt and Simkl, which never map anime and cannot address an AniDB id, so they keep reporting no ids rather than failing silently further down. `hasCatalogIds` marks the callers that can only speak IMDb/TMDB/TVDB. close #1788
This commit is contained in:
@@ -37,9 +37,10 @@ class _FakeFribb implements FribbMappingLookup {
|
||||
_FakeFribb(this.rows);
|
||||
|
||||
@override
|
||||
Future<List<FribbMappingRow>> lookup({int? tvdbId, int? tmdbId, String? imdbId}) async => [
|
||||
Future<List<FribbMappingRow>> lookup({int? anidbId, int? tvdbId, int? tmdbId, String? imdbId}) async => [
|
||||
for (final row in rows)
|
||||
if ((tvdbId != null && row.tvdbId == tvdbId) ||
|
||||
if ((anidbId != null && row.anidbId == anidbId) ||
|
||||
(tvdbId != null && row.tvdbId == tvdbId) ||
|
||||
(tmdbId != null && (row.tmdbIds?.contains(tmdbId) ?? false)) ||
|
||||
(imdbId != null && (row.imdbIds?.contains(imdbId) ?? false)))
|
||||
row,
|
||||
|
||||
@@ -32,9 +32,10 @@ class _FakeFribb implements FribbMappingLookup {
|
||||
_FakeFribb(this.rows);
|
||||
|
||||
@override
|
||||
Future<List<FribbMappingRow>> lookup({int? tvdbId, int? tmdbId, String? imdbId}) async => [
|
||||
Future<List<FribbMappingRow>> lookup({int? anidbId, int? tvdbId, int? tmdbId, String? imdbId}) async => [
|
||||
for (final row in rows)
|
||||
if ((tvdbId != null && row.tvdbId == tvdbId) ||
|
||||
if ((anidbId != null && row.anidbId == anidbId) ||
|
||||
(tvdbId != null && row.tvdbId == tvdbId) ||
|
||||
(tmdbId != null && (row.tmdbIds?.contains(tmdbId) ?? false)) ||
|
||||
(imdbId != null && (row.imdbIds?.contains(imdbId) ?? false)))
|
||||
row,
|
||||
|
||||
@@ -0,0 +1,113 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:drift/native.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:plezy/database/app_database.dart';
|
||||
import 'package:plezy/services/plex_api_cache.dart';
|
||||
|
||||
import '../test_helpers/backend_client_fixtures.dart';
|
||||
|
||||
http.Response _json(Object body) => http.Response(jsonEncode(body), 200, headers: {'content-type': 'application/json'});
|
||||
|
||||
/// A `/library/metadata/{id}` response carrying whichever guid shapes the
|
||||
/// server's agent produces.
|
||||
http.Response _metadata({List<Object>? guidArray, Object? scalarGuid}) => _json({
|
||||
'MediaContainer': {
|
||||
'Metadata': [
|
||||
{'ratingKey': 'show-1', 'type': 'show', 'title': 'Show', 'guid': ?scalarGuid, 'Guid': ?guidArray},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
void main() {
|
||||
late AppDatabase db;
|
||||
|
||||
setUp(() {
|
||||
db = AppDatabase.forTesting(NativeDatabase.memory());
|
||||
PlexApiCache.initialize(db);
|
||||
});
|
||||
|
||||
tearDown(() async {
|
||||
await db.close();
|
||||
});
|
||||
|
||||
test('reads the modern Guid array and asks for it', () async {
|
||||
late Uri requestUri;
|
||||
final client = testPlexClient(
|
||||
handler: (request) async {
|
||||
requestUri = request.url;
|
||||
return _metadata(
|
||||
guidArray: [
|
||||
{'id': 'imdb://tt12345'},
|
||||
{'id': 'tmdb://456'},
|
||||
{'id': 'tvdb://789'},
|
||||
],
|
||||
scalarGuid: 'plex://show/abc',
|
||||
);
|
||||
},
|
||||
);
|
||||
addTearDown(client.close);
|
||||
|
||||
final ids = await client.fetchExternalIds('show-1');
|
||||
|
||||
expect(requestUri.path, '/library/metadata/show-1');
|
||||
expect(requestUri.queryParameters['includeGuids'], '1');
|
||||
expect((ids.imdb, ids.tmdb, ids.tvdb), ('tt12345', 456, 789));
|
||||
});
|
||||
|
||||
// Plex only builds the `Guid` array for the Plex Movie / Plex TV Series
|
||||
// agents. A library still on a legacy agent answers with the scalar `guid`
|
||||
// alone, and reading only the array left every tracker blind to it (#1788).
|
||||
test('falls back to a legacy agent scalar guid', () async {
|
||||
final client = testPlexClient(
|
||||
handler: (request) async => _metadata(scalarGuid: 'com.plexapp.agents.thetvdb://315500?lang=en'),
|
||||
);
|
||||
addTearDown(client.close);
|
||||
|
||||
final ids = await client.fetchExternalIds('show-1');
|
||||
|
||||
expect(ids.tvdb, 315500);
|
||||
});
|
||||
|
||||
test('maps a HAMA AniDB guid, which carries no catalog id at all', () async {
|
||||
final client = testPlexClient(
|
||||
handler: (request) async => _metadata(scalarGuid: 'com.plexapp.agents.hama://anidb-11905?lang=en'),
|
||||
);
|
||||
addTearDown(client.close);
|
||||
|
||||
final ids = await client.fetchExternalIds('show-1');
|
||||
|
||||
expect(ids.anidb, 11905);
|
||||
expect(ids.hasAny, isTrue);
|
||||
expect(ids.hasCatalogIds, isFalse);
|
||||
});
|
||||
|
||||
test('the Guid array wins per field and the scalar fills the rest', () async {
|
||||
final client = testPlexClient(
|
||||
handler: (request) async => _metadata(
|
||||
guidArray: [
|
||||
{'id': 'tvdb://789'},
|
||||
],
|
||||
scalarGuid: 'com.plexapp.agents.hama://tvdb-315500',
|
||||
),
|
||||
);
|
||||
addTearDown(client.close);
|
||||
|
||||
final ids = await client.fetchExternalIds('show-1');
|
||||
|
||||
expect(ids.tvdb, 789);
|
||||
});
|
||||
|
||||
test('an unmatched item and a failed request both resolve to no ids', () async {
|
||||
final unmatched = testPlexClient(
|
||||
handler: (request) async => _metadata(scalarGuid: 'com.plexapp.agents.none://315500'),
|
||||
);
|
||||
addTearDown(unmatched.close);
|
||||
expect((await unmatched.fetchExternalIds('show-1')).hasAny, isFalse);
|
||||
|
||||
final failing = testPlexClient(handler: (request) async => http.Response('nope', 500));
|
||||
addTearDown(failing.close);
|
||||
expect((await failing.fetchExternalIds('show-1')).hasAny, isFalse);
|
||||
});
|
||||
}
|
||||
@@ -53,6 +53,11 @@ void main() {
|
||||
expect(tv.imdbIds, ['tt2']);
|
||||
expect(index.byTmdb[456]!.single, same(tv));
|
||||
expect(index.byImdb['tt2']!.single, same(tv));
|
||||
|
||||
// AniDB is the dataset's primary key, so it indexes to a single row —
|
||||
// the only handle a HAMA-matched Plex library can offer (#1788).
|
||||
expect(index.byAnidb[7], same(movie));
|
||||
expect(index.byAnidb[8], same(tv));
|
||||
});
|
||||
|
||||
test('an unexpected field shape yields null fields, not a whole-parse crash', () {
|
||||
|
||||
@@ -66,7 +66,7 @@ class _FakeFribbLookup implements FribbMappingLookup {
|
||||
const _FakeFribbLookup(this.rows);
|
||||
|
||||
@override
|
||||
Future<List<FribbMappingRow>> lookup({int? tvdbId, int? tmdbId, String? imdbId}) async => rows;
|
||||
Future<List<FribbMappingRow>> lookup({int? anidbId, int? tvdbId, int? tmdbId, String? imdbId}) async => rows;
|
||||
|
||||
@override
|
||||
Future<FribbMappingRow?> lookupByMal(int malId) async => rows.where((row) => row.malId == malId).firstOrNull;
|
||||
@@ -111,6 +111,20 @@ MediaItem _episode(int number, {int season = 1}) => testMediaItem(
|
||||
index: number,
|
||||
);
|
||||
|
||||
/// An episode that already knows its show, as playback metadata does — the
|
||||
/// resolver reads the show's guids through `grandparentId`.
|
||||
MediaItem _episodeOfShow(int number, {int season = 1}) => testMediaItem(
|
||||
id: 'episode-$season-$number',
|
||||
backend: MediaBackend.plex,
|
||||
kind: MediaKind.episode,
|
||||
title: 'Episode $number',
|
||||
serverId: ServerId('server-1'),
|
||||
libraryId: 'lib-1',
|
||||
grandparentId: 'show-1',
|
||||
parentIndex: season,
|
||||
index: number,
|
||||
);
|
||||
|
||||
MediaItem _show() => testMediaItem(
|
||||
id: 'show-1',
|
||||
backend: MediaBackend.plex,
|
||||
@@ -299,6 +313,72 @@ void main() {
|
||||
expect(anilistSaves, contains(equals({'mediaId': 202, 'progress': 2, 'status': 'COMPLETED'})));
|
||||
});
|
||||
|
||||
// A HAMA-matched library identifies anime by AniDB id and nothing else, so
|
||||
// every tracker used to be skipped with "no external IDs" (#1788).
|
||||
test('a HAMA show identified only by AniDB still reaches MAL and AniList', () async {
|
||||
await simkl.setEnabled(false);
|
||||
await mal.setEnabled(true);
|
||||
await anilist.setEnabled(true);
|
||||
coordinator.debugUseResolverDependencies(
|
||||
store: const _FakeFribbLookup([FribbMappingRow(anidbId: 11905, malId: 21, anilistId: 30, type: 'TV')]),
|
||||
animeLists: const _FakeAnimeListsLookup(),
|
||||
);
|
||||
|
||||
final malUpdates = <int, Map<String, String>>{};
|
||||
final malHttp = MockClient((request) async {
|
||||
final malId = int.parse(request.url.pathSegments[2]);
|
||||
if (request.method == 'GET') return http.Response(json.encode({'num_episodes': 12}), 200);
|
||||
expect(request.method, 'PUT');
|
||||
malUpdates[malId] = Uri.splitQueryString(request.body);
|
||||
return http.Response('{}', 200);
|
||||
});
|
||||
mal.rebindSession(_malSession(), onSessionInvalidated: () {}, httpClient: malHttp);
|
||||
|
||||
final anilistSaves = <Map<String, dynamic>>[];
|
||||
final anilistHttp = MockClient((request) async {
|
||||
final body = json.decode(request.body) as Map<String, dynamic>;
|
||||
final query = body['query'] as String;
|
||||
if (query.contains('Media(id:')) {
|
||||
return http.Response(
|
||||
json.encode({
|
||||
'data': {
|
||||
'Media': {'episodes': 12},
|
||||
},
|
||||
}),
|
||||
200,
|
||||
);
|
||||
}
|
||||
if (query.contains('SaveMediaListEntry')) {
|
||||
anilistSaves.add((body['variables'] as Map).cast<String, dynamic>());
|
||||
return http.Response(
|
||||
json.encode({
|
||||
'data': {
|
||||
'SaveMediaListEntry': {'id': 1},
|
||||
},
|
||||
}),
|
||||
200,
|
||||
);
|
||||
}
|
||||
fail('Unexpected AniList query: $query');
|
||||
});
|
||||
anilist.rebindSession(_anilistSession(), onSessionInvalidated: () {}, httpClient: anilistHttp);
|
||||
|
||||
final client = _FakeMediaServerClient(
|
||||
externalIdsByItem: {'show-1': const ExternalIds(anidb: 11905)},
|
||||
descendantsByParent: const {},
|
||||
);
|
||||
|
||||
await coordinator.markWatched(_episodeOfShow(4), client);
|
||||
|
||||
expect(client.externalIdCalls, ['show-1']);
|
||||
expect(malUpdates, {
|
||||
21: {'status': 'watching', 'num_watched_episodes': '4'},
|
||||
});
|
||||
expect(anilistSaves, [
|
||||
{'mediaId': 30, 'progress': 4, 'status': 'CURRENT'},
|
||||
]);
|
||||
});
|
||||
|
||||
test('groups manually watched same-season split cours by Anime-Lists ranges', () async {
|
||||
await simkl.setEnabled(false);
|
||||
await mal.setEnabled(true);
|
||||
|
||||
@@ -31,12 +31,21 @@ class _FakeMediaServerClient implements MediaServerClient {
|
||||
class _FakeFribbLookup implements FribbMappingLookup {
|
||||
final List<FribbMappingRow> rows;
|
||||
int lookups = 0;
|
||||
int? lastAnidbId;
|
||||
|
||||
_FakeFribbLookup(this.rows);
|
||||
|
||||
/// Mirrors the real store: an AniDB id is the dataset's primary key and
|
||||
/// resolves at most one row, so it short-circuits the tvdb/tmdb/imdb ladder.
|
||||
@override
|
||||
Future<List<FribbMappingRow>> lookup({int? tvdbId, int? tmdbId, String? imdbId}) async {
|
||||
Future<List<FribbMappingRow>> lookup({int? anidbId, int? tvdbId, int? tmdbId, String? imdbId}) async {
|
||||
lookups++;
|
||||
lastAnidbId = anidbId;
|
||||
if (anidbId != null) {
|
||||
final hit = rows.where((row) => row.anidbId == anidbId).firstOrNull;
|
||||
if (hit != null) return [hit];
|
||||
}
|
||||
if (tvdbId == null && tmdbId == null && imdbId == null) return const [];
|
||||
return rows;
|
||||
}
|
||||
|
||||
@@ -110,9 +119,12 @@ TrackerIdResolver _resolver({
|
||||
required _FakeAnimeProgressLookup animeProgress,
|
||||
_FakeFribbLookup? lookup,
|
||||
AnimeListsMappingLookup animeLists = const _FakeAnimeListsLookup(),
|
||||
ExternalIds showIds = const ExternalIds(tvdb: 81797, tmdb: 37854, imdb: 'tt0388629'),
|
||||
bool Function()? needsFribb,
|
||||
}) {
|
||||
return TrackerIdResolver(
|
||||
_FakeMediaServerClient({'show-1': const ExternalIds(tvdb: 81797, tmdb: 37854, imdb: 'tt0388629')}),
|
||||
_FakeMediaServerClient({'show-1': showIds}),
|
||||
needsFribb: needsFribb,
|
||||
store: lookup ?? _FakeFribbLookup(rows),
|
||||
animeLists: animeLists,
|
||||
animeProgress: animeProgress,
|
||||
@@ -293,4 +305,97 @@ void main() {
|
||||
expect(lookup.lookups, 2);
|
||||
});
|
||||
});
|
||||
|
||||
group('TrackerIdResolver AniDB-only items', () {
|
||||
const hamaRow = FribbMappingRow(anidbId: 11905, malId: 21, anilistId: 30, simklId: 40, type: 'TV');
|
||||
|
||||
test('a HAMA show resolves its anime through the AniDB id alone', () async {
|
||||
final lookup = _FakeFribbLookup(const [hamaRow]);
|
||||
final resolver = _resolver(
|
||||
rows: const [hamaRow],
|
||||
lookup: lookup,
|
||||
animeProgress: _FakeAnimeProgressLookup(4),
|
||||
showIds: const ExternalIds(anidb: 11905),
|
||||
);
|
||||
|
||||
final ids = await resolver.resolveShowForEpisode(_episode(season: 1, number: 4));
|
||||
|
||||
expect(lookup.lastAnidbId, 11905);
|
||||
expect(ids?.anime?.mal, 21);
|
||||
expect(ids?.anime?.anilist, 30);
|
||||
expect(ids?.animeProgressScope, AnimeProgressScope.show);
|
||||
expect(ids?.animeProgress, 4);
|
||||
});
|
||||
|
||||
test('an AniDB id does not describe a season beside season 1', () async {
|
||||
final lookup = _FakeFribbLookup(const [hamaRow]);
|
||||
final resolver = _resolver(
|
||||
rows: const [hamaRow],
|
||||
lookup: lookup,
|
||||
animeProgress: _FakeAnimeProgressLookup(null),
|
||||
showIds: const ExternalIds(anidb: 11905),
|
||||
);
|
||||
|
||||
final ids = await resolver.resolveShowForEpisode(_episode(season: 2, number: 4));
|
||||
|
||||
expect(lookup.lastAnidbId, isNull, reason: 'season 2 means the library is TVDB-numbered');
|
||||
expect(ids?.anime?.mal, isNull);
|
||||
});
|
||||
|
||||
test('a catalog id still wins the ladder when both are present', () async {
|
||||
final lookup = _FakeFribbLookup(const [
|
||||
hamaRow,
|
||||
FribbMappingRow(anidbId: 222, tvdbId: 81797, malId: 999, type: 'TV'),
|
||||
]);
|
||||
final resolver = _resolver(
|
||||
rows: const [],
|
||||
lookup: lookup,
|
||||
animeProgress: _FakeAnimeProgressLookup(null),
|
||||
showIds: const ExternalIds(anidb: 11905, tvdb: 81797),
|
||||
);
|
||||
|
||||
final ids = await resolver.resolveShowForEpisode(_episode(season: 1, number: 4));
|
||||
|
||||
expect(ids?.anime?.mal, 21, reason: 'AniDB names exactly one row, so it leads the ladder');
|
||||
});
|
||||
|
||||
test('an item with no ids at all resolves to nothing', () async {
|
||||
final resolver = _resolver(
|
||||
rows: const [hamaRow],
|
||||
animeProgress: _FakeAnimeProgressLookup(null),
|
||||
showIds: const ExternalIds(),
|
||||
);
|
||||
|
||||
expect(await resolver.resolveShowForEpisode(_episode(season: 1, number: 4)), isNull);
|
||||
});
|
||||
|
||||
test('trackers that never map anime get nothing from an AniDB-only item', () async {
|
||||
final resolver = _resolver(
|
||||
rows: const [hamaRow],
|
||||
animeProgress: _FakeAnimeProgressLookup(null),
|
||||
showIds: const ExternalIds(anidb: 11905),
|
||||
needsFribb: () => false,
|
||||
);
|
||||
|
||||
expect(
|
||||
await resolver.resolveShowForEpisode(_episode(season: 1, number: 4)),
|
||||
isNull,
|
||||
reason: 'Trakt and Simkl cannot address an AniDB id',
|
||||
);
|
||||
});
|
||||
|
||||
test('trackers that never map anime still get a catalog-id context', () async {
|
||||
final resolver = _resolver(
|
||||
rows: const [],
|
||||
animeProgress: _FakeAnimeProgressLookup(null),
|
||||
showIds: const ExternalIds(tvdb: 81797),
|
||||
needsFribb: () => false,
|
||||
);
|
||||
|
||||
final ids = await resolver.resolveShowForEpisode(_episode(season: 1, number: 4));
|
||||
|
||||
expect(ids?.external.tvdb, 81797);
|
||||
expect(ids?.anime, isNull);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -69,7 +69,7 @@ class _FakeFribbLookup implements FribbMappingLookup {
|
||||
/// Filters by tvdb id so distinct shows map to distinct anime entries, which is
|
||||
/// what makes their queued rows distinct.
|
||||
@override
|
||||
Future<List<FribbMappingRow>> lookup({int? tvdbId, int? tmdbId, String? imdbId}) async =>
|
||||
Future<List<FribbMappingRow>> lookup({int? anidbId, int? tvdbId, int? tmdbId, String? imdbId}) async =>
|
||||
rows.where((row) => tvdbId == null || row.tvdbId == tvdbId).toList();
|
||||
|
||||
@override
|
||||
|
||||
@@ -45,27 +45,34 @@ void main() {
|
||||
});
|
||||
|
||||
test('normalizes HAMA GUID modes with direct external IDs', () {
|
||||
final cases = <({String guid, String? imdb, int? tmdb, int? tvdb})>[
|
||||
(guid: 'com.plexapp.agents.hama://tvdb-315500', imdb: null, tmdb: null, tvdb: 315500),
|
||||
(guid: 'com.plexapp.agents.hama://tvdb2-315500', imdb: null, tmdb: null, tvdb: 315500),
|
||||
(guid: 'com.plexapp.agents.hama://tvdb9-315500', imdb: null, tmdb: null, tvdb: 315500),
|
||||
(guid: 'com.plexapp.agents.hama://tmdb-69346', imdb: null, tmdb: 69346, tvdb: null),
|
||||
(guid: 'com.plexapp.agents.hama://tsdb-69346?lang=en', imdb: null, tmdb: 69346, tvdb: null),
|
||||
(guid: 'com.plexapp.agents.hama://imdb-6455986', imdb: 'tt6455986', tmdb: null, tvdb: null),
|
||||
(guid: 'com.plexapp.agents.hama://imdb-tt6455986', imdb: 'tt6455986', tmdb: null, tvdb: null),
|
||||
final cases = <({String guid, String? imdb, int? tmdb, int? tvdb, int? anidb})>[
|
||||
(guid: 'com.plexapp.agents.hama://tvdb-315500', imdb: null, tmdb: null, tvdb: 315500, anidb: null),
|
||||
(guid: 'com.plexapp.agents.hama://tvdb2-315500', imdb: null, tmdb: null, tvdb: 315500, anidb: null),
|
||||
(guid: 'com.plexapp.agents.hama://tvdb9-315500', imdb: null, tmdb: null, tvdb: 315500, anidb: null),
|
||||
(guid: 'com.plexapp.agents.hama://tmdb-69346', imdb: null, tmdb: 69346, tvdb: null, anidb: null),
|
||||
(guid: 'com.plexapp.agents.hama://tsdb-69346?lang=en', imdb: null, tmdb: 69346, tvdb: null, anidb: null),
|
||||
(guid: 'com.plexapp.agents.hama://imdb-6455986', imdb: 'tt6455986', tmdb: null, tvdb: null, anidb: null),
|
||||
(guid: 'com.plexapp.agents.hama://imdb-tt6455986', imdb: 'tt6455986', tmdb: null, tvdb: null, anidb: null),
|
||||
(guid: 'com.plexapp.agents.hama://anidb-11905?lang=en', imdb: null, tmdb: null, tvdb: null, anidb: 11905),
|
||||
];
|
||||
|
||||
for (final testCase in cases) {
|
||||
final ids = ExternalIds.fromLegacyPlexGuid(testCase.guid);
|
||||
expect(
|
||||
(imdb: ids.imdb, tmdb: ids.tmdb, tvdb: ids.tvdb),
|
||||
(imdb: testCase.imdb, tmdb: testCase.tmdb, tvdb: testCase.tvdb),
|
||||
(imdb: ids.imdb, tmdb: ids.tmdb, tvdb: ids.tvdb, anidb: ids.anidb),
|
||||
(imdb: testCase.imdb, tmdb: testCase.tmdb, tvdb: testCase.tvdb, anidb: testCase.anidb),
|
||||
reason: testCase.guid,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
test('rejects unsupported agents, AniDB modes, and malformed IDs', () {
|
||||
test('an AniDB id is not a catalog id', () {
|
||||
final ids = ExternalIds.fromLegacyPlexGuid('com.plexapp.agents.hama://anidb-11905');
|
||||
expect(ids.hasAny, isTrue);
|
||||
expect(ids.hasCatalogIds, isFalse, reason: 'IMDb/TMDB/TVDB consumers must not act on an AniDB id');
|
||||
});
|
||||
|
||||
test('rejects unsupported agents, AniDB grouping modes, and malformed IDs', () {
|
||||
final invalid = <Object?>[
|
||||
null,
|
||||
315500,
|
||||
@@ -74,7 +81,12 @@ void main() {
|
||||
'plex://movie/abc',
|
||||
'local://315500',
|
||||
'com.plexapp.agents.none://315500',
|
||||
'com.plexapp.agents.hama://anidb-11905',
|
||||
// anidb2..9 group several AniDB entries under one TVDB-numbered Plex
|
||||
// show, so the guid's id does not describe the seasons beside it.
|
||||
'com.plexapp.agents.hama://anidb2-11905',
|
||||
'com.plexapp.agents.hama://anidb9-11905',
|
||||
'com.plexapp.agents.hama://anidb-not-a-number',
|
||||
'com.plexapp.agents.hama://anidb-',
|
||||
'com.plexapp.agents.hama://tvdb10-315500',
|
||||
'com.plexapp.agents.hama://tvdb-not-a-number',
|
||||
'com.plexapp.agents.hama://tmdb-',
|
||||
@@ -88,6 +100,33 @@ void main() {
|
||||
});
|
||||
});
|
||||
|
||||
group('ExternalIds.fillFrom', () {
|
||||
test('keeps its own ids and only fills the ones it is missing', () {
|
||||
const modern = ExternalIds(tvdb: 315500);
|
||||
const legacy = ExternalIds(imdb: 'tt6455986', tvdb: 999, anidb: 11905);
|
||||
|
||||
final merged = modern.fillFrom(legacy);
|
||||
|
||||
expect(merged.tvdb, 315500, reason: 'the modern Guid array wins per field');
|
||||
expect(merged.imdb, 'tt6455986');
|
||||
expect(merged.anidb, 11905);
|
||||
});
|
||||
|
||||
test('round-trips every id through JSON', () {
|
||||
const ids = ExternalIds(imdb: 'tt1', tmdb: 2, tvdb: 3, anidb: 4);
|
||||
final restored = ExternalIds.fromJson(ids.toJson());
|
||||
|
||||
expect((restored.imdb, restored.tmdb, restored.tvdb, restored.anidb), ('tt1', 2, 3, 4));
|
||||
expect(ExternalIds.fromJson(const ExternalIds().toJson()).hasAny, isFalse);
|
||||
});
|
||||
|
||||
test('intersects matches on an AniDB id alone', () {
|
||||
const hama = ExternalIds(anidb: 11905);
|
||||
expect(hama.intersects(const ExternalIds(anidb: 11905)), isTrue);
|
||||
expect(hama.intersects(const ExternalIds(tvdb: 315500)), isFalse);
|
||||
});
|
||||
});
|
||||
|
||||
group('ExternalIds.fromJellyfinProviderIds', () {
|
||||
test('extracts Tmdb/Imdb/Tvdb (case-insensitive)', () {
|
||||
final ids = ExternalIds.fromJellyfinProviderIds({'Tmdb': '12345', 'Imdb': 'tt99999', 'Tvdb': '777'});
|
||||
|
||||
Reference in New Issue
Block a user