Files
plezy/test/services/trackers/tracker_id_resolver_test.dart
T

297 lines
11 KiB
Dart

import 'package:flutter_test/flutter_test.dart';
import 'package:plezy/media/media_backend.dart';
import 'package:plezy/media/media_item.dart';
import 'package:plezy/media/media_kind.dart';
import 'package:plezy/media/media_server_client.dart';
import 'package:plezy/models/trackers/anime_lists_mapping.dart';
import 'package:plezy/models/trackers/fribb_mapping_row.dart';
import 'package:plezy/services/trackers/anime_episode_progress_resolver.dart';
import 'package:plezy/services/trackers/anime_lists_mapping_store.dart';
import 'package:plezy/services/trackers/fribb_mapping_store.dart';
import 'package:plezy/services/trackers/tracker_id_resolver.dart';
import 'package:plezy/utils/external_ids.dart';
import '../../test_helpers/media_items.dart';
class _FakeMediaServerClient implements MediaServerClient {
final Map<String, ExternalIds> externalIdsByItem;
final List<String> externalIdCalls = [];
_FakeMediaServerClient(this.externalIdsByItem);
@override
Future<ExternalIds> fetchExternalIds(String itemId) async {
externalIdCalls.add(itemId);
return externalIdsByItem[itemId] ?? const ExternalIds();
}
@override
dynamic noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation);
}
class _FakeFribbLookup implements FribbMappingLookup {
final List<FribbMappingRow> rows;
int lookups = 0;
_FakeFribbLookup(this.rows);
@override
Future<List<FribbMappingRow>> lookup({int? tvdbId, int? tmdbId, String? imdbId}) async {
lookups++;
return rows;
}
@override
Future<FribbMappingRow?> lookupByMal(int malId) async => rows.where((row) => row.malId == malId).firstOrNull;
}
class _FakeAnimeProgressLookup implements AnimeEpisodeProgressLookup {
ResolvedAnimeProgress? result;
int resolveCalls = 0;
int clearCalls = 0;
MediaItem? lastEpisode;
AnimeProgressScope? lastScope;
AnimeEpisodeMatch? lastMatch;
bool? lastIncludeCurrentEpisode;
_FakeAnimeProgressLookup(int? progress)
: result = progress == null ? null : ResolvedAnimeProgress(progress: progress);
@override
Future<ResolvedAnimeProgress?> resolve(
MediaItem episode, {
required AnimeProgressScope scope,
AnimeEpisodeMatch? animeMatch,
Future<AnimeEpisodeMatch?> Function(MediaItem episode)? episodeMatcher,
bool includeCurrentEpisode = true,
}) async {
resolveCalls++;
lastEpisode = episode;
lastScope = scope;
lastMatch = animeMatch;
lastIncludeCurrentEpisode = includeCurrentEpisode;
return result;
}
@override
void clearCache() {
clearCalls++;
}
}
class _FakeAnimeListsLookup implements AnimeListsMappingLookup {
final Map<String, AnimeEpisodeMatch> matches;
const _FakeAnimeListsLookup({this.matches = const {}});
@override
Future<AnimeEpisodeMatch?> lookupEpisode({int? tvdbId, int? tmdbId, int? season, int? episodeNumber}) async {
return matches['$season-$episodeNumber'];
}
@override
Future<Set<int>> lookupAnimeIdsForSeason({int? tvdbId, int? tmdbId, required int season}) async => const <int>{};
@override
Future<Set<int>> lookupAnimeIdsForShow({int? tvdbId, int? tmdbId}) async => const <int>{};
}
MediaItem _episode({int season = 23, int number = 6}) => testMediaItem(
id: 'episode-$season-$number',
backend: MediaBackend.plex,
kind: MediaKind.episode,
title: 'Episode $number',
grandparentId: 'show-1',
parentIndex: season,
index: number,
);
TrackerIdResolver _resolver({
required List<FribbMappingRow> rows,
required _FakeAnimeProgressLookup animeProgress,
_FakeFribbLookup? lookup,
AnimeListsMappingLookup animeLists = const _FakeAnimeListsLookup(),
}) {
return TrackerIdResolver(
_FakeMediaServerClient({'show-1': const ExternalIds(tvdb: 81797, tmdb: 37854, imdb: 'tt0388629')}),
store: lookup ?? _FakeFribbLookup(rows),
animeLists: animeLists,
animeProgress: animeProgress,
);
}
AnimeEpisodeMatch _match({required int anidbId, required int serverEpisode, required int animeEpisode}) =>
AnimeEpisodeMatch(
anidbId: anidbId,
anidbSeason: 1,
anidbEpisode: animeEpisode,
provider: AnimeListProvider.tvdb,
externalSeason: 1,
externalEpisode: serverEpisode,
kind: AnimeListMatchKind.range,
);
void main() {
group('TrackerIdResolver anime progress', () {
test('one unseasoned regular TV row uses show-scope progress', () async {
final animeProgress = _FakeAnimeProgressLookup(6);
final resolver = _resolver(
animeProgress: animeProgress,
rows: const [
FribbMappingRow(
tvdbId: 81797,
tmdbIds: [37854],
imdbIds: ['tt0388629'],
malId: 21,
anilistId: 21,
type: 'TV',
),
],
);
final ids = await resolver.resolveShowForEpisode(_episode());
expect(ids?.anime?.mal, 21);
expect(ids?.animeProgressScope, AnimeProgressScope.show);
expect(ids?.animeProgress, 6);
expect(animeProgress.resolveCalls, 1);
expect(animeProgress.lastEpisode?.id, 'episode-23-6');
expect(animeProgress.lastScope, AnimeProgressScope.show);
});
test('exact season-scoped row uses season-scope progress', () async {
final animeProgress = _FakeAnimeProgressLookup(18);
final resolver = _resolver(
animeProgress: animeProgress,
rows: const [
FribbMappingRow(tvdbId: 81797, malId: 100, tvdbSeason: 1, type: 'TV'),
FribbMappingRow(tvdbId: 81797, malId: 200, tvdbSeason: 2, type: 'TV'),
],
);
final ids = await resolver.resolveShowForEpisode(_episode(season: 2));
expect(ids?.anime?.mal, 200);
expect(ids?.animeProgressScope, AnimeProgressScope.season);
expect(ids?.animeProgress, 18);
expect(animeProgress.resolveCalls, 1);
expect(animeProgress.lastScope, AnimeProgressScope.season);
});
test('does not guess when multiple regular rows are unseasoned', () async {
final animeProgress = _FakeAnimeProgressLookup(1061);
final resolver = _resolver(
animeProgress: animeProgress,
rows: const [
FribbMappingRow(tvdbId: 81797, malId: 1, type: 'TV'),
FribbMappingRow(tvdbId: 81797, malId: 2, type: 'ONA'),
],
);
final ids = await resolver.resolveShowForEpisode(_episode());
expect(ids?.anime?.mal, 1);
expect(ids?.animeProgressScope, isNull);
expect(ids?.animeProgress, isNull);
expect(animeProgress.resolveCalls, 0);
});
test('movie and special rows do not make a regular TV row ambiguous', () async {
final animeProgress = _FakeAnimeProgressLookup(1061);
final resolver = _resolver(
animeProgress: animeProgress,
rows: const [
FribbMappingRow(tvdbId: 81797, malId: 21, type: 'TV'),
FribbMappingRow(tvdbId: 81797, malId: 459, tvdbSeason: 0, type: 'MOVIE'),
FribbMappingRow(tvdbId: 81797, malId: 466, tvdbSeason: 0, type: 'OVA'),
FribbMappingRow(tvdbId: 81797, malId: 492, tvdbSeason: 0, type: 'SPECIAL'),
],
);
final ids = await resolver.resolveShowForEpisode(_episode());
expect(ids?.anime?.mal, 21);
expect(ids?.animeProgressScope, AnimeProgressScope.show);
expect(ids?.animeProgress, 1061);
expect(animeProgress.resolveCalls, 1);
expect(animeProgress.lastScope, AnimeProgressScope.show);
});
test('clearCache clears ID and anime progress caches', () async {
final animeProgress = _FakeAnimeProgressLookup(1061);
final lookup = _FakeFribbLookup(const [FribbMappingRow(tvdbId: 81797, malId: 21, type: 'TV')]);
final resolver = _resolver(rows: lookup.rows, lookup: lookup, animeProgress: animeProgress);
await resolver.resolveShowForEpisode(_episode());
resolver.clearCache();
await resolver.resolveShowForEpisode(_episode(number: 7));
expect(lookup.lookups, 2);
expect(animeProgress.clearCalls, 1);
expect(animeProgress.resolveCalls, 2);
});
test('same server season can select different anime entries by episode range', () async {
final animeProgress = _FakeAnimeProgressLookup(2);
final resolver = _resolver(
animeProgress: animeProgress,
animeLists: _FakeAnimeListsLookup(matches: {'1-14': _match(anidbId: 222, serverEpisode: 14, animeEpisode: 2)}),
rows: const [
FribbMappingRow(anidbId: 111, tvdbId: 81797, malId: 101, tvdbSeason: 1, type: 'TV'),
FribbMappingRow(anidbId: 222, tvdbId: 81797, malId: 102, tvdbSeason: 1, type: 'TV'),
],
);
final ids = await resolver.resolveShowForEpisode(_episode(season: 1, number: 14));
expect(ids?.anime?.mal, 102);
expect(ids?.animeProgressScope, AnimeProgressScope.mapped);
expect(ids?.animeEpisodeNumber, 2);
expect(ids?.animeProgress, 2);
expect(animeProgress.lastScope, AnimeProgressScope.mapped);
expect(animeProgress.lastMatch?.anidbId, 222);
});
test('passes includeCurrentEpisode through for unwatch progress', () async {
final animeProgress = _FakeAnimeProgressLookup(1);
final resolver = _resolver(
animeProgress: animeProgress,
animeLists: _FakeAnimeListsLookup(matches: {'1-14': _match(anidbId: 222, serverEpisode: 14, animeEpisode: 2)}),
rows: const [FribbMappingRow(anidbId: 222, tvdbId: 81797, malId: 102, tvdbSeason: 1, type: 'TV')],
);
final ids = await resolver.resolveShowForEpisode(_episode(season: 1, number: 14), includeCurrentEpisode: false);
expect(ids?.animeProgress, 1);
expect(animeProgress.lastIncludeCurrentEpisode, isFalse);
});
test('episode-aware cache does not reuse a same-season split-cour row', () async {
final animeProgress = _FakeAnimeProgressLookup(null);
final lookup = _FakeFribbLookup(const [
FribbMappingRow(anidbId: 111, tvdbId: 81797, malId: 101, tvdbSeason: 1, type: 'TV'),
FribbMappingRow(anidbId: 222, tvdbId: 81797, malId: 102, tvdbSeason: 1, type: 'TV'),
]);
final client = _FakeMediaServerClient({'show-1': const ExternalIds(tvdb: 81797)});
final resolver = TrackerIdResolver(
client,
store: lookup,
animeLists: _FakeAnimeListsLookup(
matches: {
'1-12': _match(anidbId: 111, serverEpisode: 12, animeEpisode: 12),
'1-13': _match(anidbId: 222, serverEpisode: 13, animeEpisode: 1),
},
),
animeProgress: animeProgress,
);
final first = await resolver.resolveShowForEpisode(_episode(season: 1, number: 12), includeAnimeProgress: false);
final second = await resolver.resolveShowForEpisode(_episode(season: 1, number: 13), includeAnimeProgress: false);
expect(first?.anime?.mal, 101);
expect(second?.anime?.mal, 102);
expect(client.externalIdCalls, ['show-1']);
expect(lookup.lookups, 2);
});
});
}