297 lines
11 KiB
Dart
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);
|
|
});
|
|
});
|
|
}
|