diff --git a/lib/models/trackers/tracker_context.dart b/lib/models/trackers/tracker_context.dart index 3f0081a6..3470d50d 100644 --- a/lib/models/trackers/tracker_context.dart +++ b/lib/models/trackers/tracker_context.dart @@ -16,7 +16,6 @@ class TrackerContext { final int? season; final int? episodeNumber; final int? animeProgress; - final bool animeProgressComplete; /// Plex ratingKey of the item being played. Used only for logging — not /// sent to any tracker. @@ -35,7 +34,6 @@ class TrackerContext { this.season, this.episodeNumber, this.animeProgress, - this.animeProgressComplete = false, }); factory TrackerContext.movie({ @@ -61,7 +59,6 @@ class TrackerContext { required int season, required int episodeNumber, int? animeProgress, - bool animeProgressComplete = false, }) { return TrackerContext._( external: external, @@ -72,7 +69,6 @@ class TrackerContext { season: season, episodeNumber: episodeNumber, animeProgress: animeProgress, - animeProgressComplete: animeProgressComplete, ); } } diff --git a/lib/services/trackers/anilist/anilist_client.dart b/lib/services/trackers/anilist/anilist_client.dart index 78e235ce..c15536f3 100644 --- a/lib/services/trackers/anilist/anilist_client.dart +++ b/lib/services/trackers/anilist/anilist_client.dart @@ -5,6 +5,7 @@ import 'package:http/http.dart' as http; import '../../../utils/abortable_http_request.dart'; import '../../../utils/app_logger.dart'; +import '../../../utils/json_utils.dart'; import '../../../utils/platform_http_client_stub.dart' if (dart.library.io) '../../../utils/platform_http_client_io.dart' as platform; @@ -49,6 +50,21 @@ class AnilistClient { await query(mutation, variables: {'mediaId': mediaId, 'progress': progress, 'status': status}); } + Future getAnimeEpisodeCount(int mediaId) async { + const mediaQuery = ''' + query(\$mediaId: Int) { + Media(id: \$mediaId, type: ANIME) { + episodes + } + } + '''; + final data = await query(mediaQuery, variables: {'mediaId': mediaId}); + final media = data['Media']; + if (media is! Map) return null; + final count = flexibleInt(media['episodes']); + return count != null && count > 0 ? count : null; + } + Future> query(String query, {Map? variables}) async { final uri = Uri.parse(AnilistConstants.apiBase); final headers = AnilistConstants.headers(accessToken: _session.accessToken); diff --git a/lib/services/trackers/anilist/anilist_tracker.dart b/lib/services/trackers/anilist/anilist_tracker.dart index e0873f0c..fc2dcc1d 100644 --- a/lib/services/trackers/anilist/anilist_tracker.dart +++ b/lib/services/trackers/anilist/anilist_tracker.dart @@ -1,3 +1,5 @@ +import 'package:http/http.dart' as http; + import '../../../models/trackers/tracker_context.dart'; import '../../../utils/app_logger.dart'; import '../../settings_service.dart'; @@ -25,6 +27,7 @@ class AnilistTracker extends TrackerBase { bool get needsFribb => true; AnilistClient? _client; + final Map> _episodeCountLoads = {}; @override bool get hasActiveClient => _client != null; @@ -32,9 +35,16 @@ class AnilistTracker extends TrackerBase { @override bool readEnabledSetting(SettingsService settings) => settings.read(SettingsService.enableAnilistScrobble); - void rebindSession(AnilistSession? session, {required void Function() onSessionInvalidated}) { + void rebindSession( + AnilistSession? session, { + required void Function() onSessionInvalidated, + http.Client? httpClient, + }) { _client?.dispose(); - _client = session == null ? null : AnilistClient(session, onSessionInvalidated: onSessionInvalidated); + _episodeCountLoads.clear(); + _client = session == null + ? null + : AnilistClient(session, onSessionInvalidated: onSessionInvalidated, httpClient: httpClient); } @override @@ -45,9 +55,27 @@ class AnilistTracker extends TrackerBase { final progress = ctx.isMovie ? 1 : (ctx.animeProgress ?? ctx.episodeNumber); if (progress == null || progress <= 0) return; - final status = ctx.isMovie || ctx.animeProgressComplete ? 'COMPLETED' : 'CURRENT'; + final total = ctx.isMovie || ctx.animeProgress == null ? null : await _episodeCount(client, anilistId); + final watched = total != null && progress > total ? total : progress; + final status = ctx.isMovie || (total != null && progress >= total) ? 'COMPLETED' : 'CURRENT'; - await client.saveMediaListEntry(mediaId: anilistId, progress: progress, status: status); - appLogger.d('AniList: saved entry (anilist=$anilistId, progress=$progress, status=$status)'); + await client.saveMediaListEntry(mediaId: anilistId, progress: watched, status: status); + appLogger.d('AniList: saved entry (anilist=$anilistId, progress=$watched, status=$status)'); + } + + Future _episodeCount(AnilistClient client, int anilistId) { + final existing = _episodeCountLoads[anilistId]; + if (existing != null) return existing; + + late final Future loading; + loading = client.getAnimeEpisodeCount(anilistId).catchError((Object e) { + if (identical(_episodeCountLoads[anilistId], loading)) { + final _ = _episodeCountLoads.remove(anilistId); + } + appLogger.d('AniList: failed to fetch anime episode count (anilist=$anilistId)', error: e); + return null; + }); + _episodeCountLoads[anilistId] = loading; + return loading; } } diff --git a/lib/services/trackers/anime_episode_progress_resolver.dart b/lib/services/trackers/anime_episode_progress_resolver.dart index 3ac2aefd..02f08c87 100644 --- a/lib/services/trackers/anime_episode_progress_resolver.dart +++ b/lib/services/trackers/anime_episode_progress_resolver.dart @@ -7,9 +7,8 @@ enum AnimeProgressScope { show, season } class ResolvedAnimeProgress { final int progress; - final bool isComplete; - const ResolvedAnimeProgress({required this.progress, required this.isComplete}); + const ResolvedAnimeProgress({required this.progress}); } /// Resolves watched progress in the MAL/AniList anime entry selected by Fribb. @@ -48,21 +47,20 @@ class AnimeEpisodeProgressResolver implements AnimeEpisodeProgressLookup { final existing = _seasonProgressLoads[showId]; if (existing != null) return existing; - final loading = _loadSeasonProgress(showId); + late final Future?> loading; + loading = _loadSeasonProgress(showId).whenComplete(() { + if (identical(_seasonProgressLoads[showId], loading)) { + final _ = _seasonProgressLoads.remove(showId); + } + }); _seasonProgressLoads[showId] = loading; - - final progress = await loading; - if (progress == null) { - final _ = _seasonProgressLoads.remove(showId); - } - return progress; + return loading; } ResolvedAnimeProgress? _showProgress(Map seasons, bool currentAlreadyWatched) { if (seasons.isEmpty) return null; var watched = 0; var total = 0; - var totalKnown = true; for (final entry in seasons.entries) { final season = entry.key; if (season <= 0) continue; @@ -70,14 +68,11 @@ class AnimeEpisodeProgressResolver implements AnimeEpisodeProgressLookup { final count = entry.value.total; if (count != null && count > 0) { total += count; - } else { - totalKnown = false; } } final progress = watched + (currentAlreadyWatched ? 0 : 1); if (progress <= 0) return null; - final isComplete = totalKnown && total > 0 && progress >= total; - return ResolvedAnimeProgress(progress: isComplete && progress > total ? total : progress, isComplete: isComplete); + return ResolvedAnimeProgress(progress: total > 0 && progress > total ? total : progress); } ResolvedAnimeProgress? _seasonProgress(_SeasonProgress? season, bool currentAlreadyWatched) { @@ -85,8 +80,7 @@ class AnimeEpisodeProgressResolver implements AnimeEpisodeProgressLookup { final progress = season.watched + (currentAlreadyWatched ? 0 : 1); if (progress <= 0) return null; final total = season.total; - final isComplete = total != null && total > 0 && progress >= total; - return ResolvedAnimeProgress(progress: isComplete && progress > total ? total : progress, isComplete: isComplete); + return ResolvedAnimeProgress(progress: total != null && total > 0 && progress > total ? total : progress); } Future?> _loadSeasonProgress(String showId) async { diff --git a/lib/services/trackers/mal/mal_client.dart b/lib/services/trackers/mal/mal_client.dart index df97c821..276cfd6b 100644 --- a/lib/services/trackers/mal/mal_client.dart +++ b/lib/services/trackers/mal/mal_client.dart @@ -5,6 +5,7 @@ import 'package:http/http.dart' as http; import '../../../utils/abortable_http_request.dart'; import '../../../utils/app_logger.dart'; +import '../../../utils/json_utils.dart'; import '../../../utils/platform_http_client_stub.dart' if (dart.library.io) '../../../utils/platform_http_client_io.dart' as platform; @@ -56,7 +57,14 @@ class MalClient { /// ``` Future updateMyListStatus(int animeId, Map fields) async { // MAL's list-status endpoint is form-encoded (not JSON). - await _request('PATCH', '/anime/$animeId/my_list_status', formBody: fields); + await _request('PUT', '/anime/$animeId/my_list_status', formBody: fields); + } + + Future getAnimeEpisodeCount(int animeId) async { + final res = await _request('GET', '/anime/$animeId?fields=num_episodes'); + if (res is! Map) return null; + final count = flexibleInt(res['num_episodes']); + return count != null && count > 0 ? count : null; } Future _refresh() => _refreshCoalescer.run(_doRefresh); diff --git a/lib/services/trackers/mal/mal_tracker.dart b/lib/services/trackers/mal/mal_tracker.dart index a388a0ee..24cae065 100644 --- a/lib/services/trackers/mal/mal_tracker.dart +++ b/lib/services/trackers/mal/mal_tracker.dart @@ -1,3 +1,5 @@ +import 'package:http/http.dart' as http; + import '../../../models/trackers/tracker_context.dart'; import '../../../utils/app_logger.dart'; import '../../settings_service.dart'; @@ -28,6 +30,7 @@ class MalTracker extends TrackerBase { bool get needsFribb => true; MalClient? _client; + final Map> _episodeCountLoads = {}; @override bool get hasActiveClient => _client != null; @@ -39,11 +42,18 @@ class MalTracker extends TrackerBase { MalSession? session, { required void Function() onSessionInvalidated, void Function(MalSession)? onSessionUpdated, + http.Client? httpClient, }) { _client?.dispose(); + _episodeCountLoads.clear(); _client = session == null ? null - : MalClient(session, onSessionInvalidated: onSessionInvalidated, onSessionUpdated: onSessionUpdated); + : MalClient( + session, + onSessionInvalidated: onSessionInvalidated, + onSessionUpdated: onSessionUpdated, + httpClient: httpClient, + ); } @override @@ -58,10 +68,31 @@ class MalTracker extends TrackerBase { } else { final progress = ctx.animeProgress ?? ctx.episodeNumber; if (progress == null || progress <= 0) return; - fields = {'status': ctx.animeProgressComplete ? 'completed' : 'watching', 'num_watched_episodes': '$progress'}; + final total = ctx.animeProgress == null ? null : await _episodeCount(client, malId); + final watched = total != null && progress > total ? total : progress; + fields = { + 'status': total != null && progress >= total ? 'completed' : 'watching', + 'num_watched_episodes': '$watched', + }; } await client.updateMyListStatus(malId, fields); appLogger.d('MAL: updated list status (mal=$malId, fields=$fields)'); } + + Future _episodeCount(MalClient client, int malId) { + final existing = _episodeCountLoads[malId]; + if (existing != null) return existing; + + late final Future loading; + loading = client.getAnimeEpisodeCount(malId).catchError((Object e) { + if (identical(_episodeCountLoads[malId], loading)) { + final _ = _episodeCountLoads.remove(malId); + } + appLogger.d('MAL: failed to fetch anime episode count (mal=$malId)', error: e); + return null; + }); + _episodeCountLoads[malId] = loading; + return loading; + } } diff --git a/lib/services/trackers/tracker_coordinator.dart b/lib/services/trackers/tracker_coordinator.dart index b8b2a29c..fb7a78fc 100644 --- a/lib/services/trackers/tracker_coordinator.dart +++ b/lib/services/trackers/tracker_coordinator.dart @@ -161,7 +161,6 @@ class TrackerCoordinator { season: season, episodeNumber: number, animeProgress: ids.animeProgress, - animeProgressComplete: ids.animeProgressComplete, ); } } diff --git a/lib/services/trackers/tracker_id_resolver.dart b/lib/services/trackers/tracker_id_resolver.dart index 64989ac6..4f603b23 100644 --- a/lib/services/trackers/tracker_id_resolver.dart +++ b/lib/services/trackers/tracker_id_resolver.dart @@ -14,15 +14,8 @@ class TrackerIds { final AnimeIds? anime; final AnimeProgressScope? animeProgressScope; final int? animeProgress; - final bool animeProgressComplete; - const TrackerIds({ - required this.external, - required this.anime, - this.animeProgressScope, - this.animeProgress, - this.animeProgressComplete = false, - }); + const TrackerIds({required this.external, required this.anime, this.animeProgressScope, this.animeProgress}); TrackerIds withAnimeProgress(ResolvedAnimeProgress? animeProgress) { return TrackerIds( @@ -30,7 +23,6 @@ class TrackerIds { anime: anime, animeProgressScope: animeProgressScope, animeProgress: animeProgress?.progress, - animeProgressComplete: animeProgress?.isComplete ?? false, ); } } diff --git a/test/services/trackers/anilist_tracker_test.dart b/test/services/trackers/anilist_tracker_test.dart new file mode 100644 index 00000000..29f929ec --- /dev/null +++ b/test/services/trackers/anilist_tracker_test.dart @@ -0,0 +1,132 @@ +import 'dart:convert'; + +import 'package:flutter_test/flutter_test.dart'; +import 'package:http/testing.dart'; +import 'package:http/http.dart' as http; +import 'package:plezy/models/trackers/anime_ids.dart'; +import 'package:plezy/models/trackers/tracker_context.dart'; +import 'package:plezy/services/trackers/anilist/anilist_session.dart'; +import 'package:plezy/services/trackers/anilist/anilist_tracker.dart'; +import 'package:plezy/utils/external_ids.dart'; + +AnilistSession _session() { + final now = DateTime.now().millisecondsSinceEpoch ~/ 1000; + return AnilistSession(accessToken: 'token', expiresAt: now + 86400, createdAt: now); +} + +TrackerContext _episode({int anilistId = 21, int episodeNumber = 12, int? animeProgress = 12}) { + return TrackerContext.episode( + external: const ExternalIds(tvdb: 1), + anime: AnimeIds(anilist: anilistId), + ratingKey: 'episode-1', + libraryGlobalKey: null, + season: 1, + episodeNumber: episodeNumber, + animeProgress: animeProgress, + ); +} + +void main() { + group('AnilistTracker', () { + final tracker = AnilistTracker.instance; + + tearDown(() { + tracker.rebindSession(null, onSessionInvalidated: () {}); + }); + + test('marks completed when scoped progress reaches AniList total', () async { + final saved = >[]; + final client = MockClient((request) async { + final body = json.decode(request.body) as Map; + 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')) { + saved.add((body['variables'] as Map).cast()); + return http.Response( + json.encode({ + 'data': { + 'SaveMediaListEntry': {'id': 1}, + }, + }), + 200, + ); + } + fail('Unexpected AniList query: $query'); + }); + tracker.rebindSession(_session(), onSessionInvalidated: () {}, httpClient: client); + + await tracker.markWatched(_episode(animeProgress: 13)); + + expect(saved.single, {'mediaId': 21, 'progress': 12, 'status': 'COMPLETED'}); + }); + + test('keeps fallback local progress as current without total lookup', () async { + final saved = >[]; + final client = MockClient((request) async { + final body = json.decode(request.body) as Map; + final query = body['query'] as String; + if (query.contains('SaveMediaListEntry')) { + saved.add((body['variables'] as Map).cast()); + return http.Response( + json.encode({ + 'data': { + 'SaveMediaListEntry': {'id': 1}, + }, + }), + 200, + ); + } + fail('Unexpected AniList query: $query'); + }); + tracker.rebindSession(_session(), onSessionInvalidated: () {}, httpClient: client); + + await tracker.markWatched(_episode(animeProgress: null)); + + expect(saved.single, {'mediaId': 21, 'progress': 12, 'status': 'CURRENT'}); + }); + + test('keeps progress current when AniList total is unknown', () async { + final saved = >[]; + final client = MockClient((request) async { + final body = json.decode(request.body) as Map; + final query = body['query'] as String; + if (query.contains('Media(id:')) { + return http.Response( + json.encode({ + 'data': { + 'Media': {'episodes': null}, + }, + }), + 200, + ); + } + if (query.contains('SaveMediaListEntry')) { + saved.add((body['variables'] as Map).cast()); + return http.Response( + json.encode({ + 'data': { + 'SaveMediaListEntry': {'id': 1}, + }, + }), + 200, + ); + } + fail('Unexpected AniList query: $query'); + }); + tracker.rebindSession(_session(), onSessionInvalidated: () {}, httpClient: client); + + await tracker.markWatched(_episode()); + + expect(saved.single, {'mediaId': 21, 'progress': 12, 'status': 'CURRENT'}); + }); + }); +} diff --git a/test/services/trackers/anime_episode_progress_resolver_test.dart b/test/services/trackers/anime_episode_progress_resolver_test.dart index ad29ad80..e8b48cf4 100644 --- a/test/services/trackers/anime_episode_progress_resolver_test.dart +++ b/test/services/trackers/anime_episode_progress_resolver_test.dart @@ -54,7 +54,6 @@ void main() { final result = await resolver.resolve(_episode(), scope: AnimeProgressScope.show); expect(result?.progress, 56); - expect(result?.isComplete, isFalse); }); test('show scope ignores specials season', () async { @@ -67,7 +66,6 @@ void main() { final result = await resolver.resolve(_episode(season: 1, number: 6), scope: AnimeProgressScope.show); expect(result?.progress, 6); - expect(result?.isComplete, isFalse); }); test('season scope uses only current season watched count', () async { @@ -80,36 +78,33 @@ void main() { final result = await resolver.resolve(_episode(season: 2, number: 6), scope: AnimeProgressScope.season); expect(result?.progress, 6); - expect(result?.isComplete, isFalse); }); - test('season scope marks complete when progress reaches known season total', () async { + test('season scope caps progress at known season total', () async { final resolver = AnimeEpisodeProgressResolver( _FakeMediaServerClient({ - 'show-1': [_season(2, watched: 11, total: 12)], + 'show-1': [_season(2, watched: 12, total: 12)], }), ); final result = await resolver.resolve(_episode(season: 2, number: 12), scope: AnimeProgressScope.season); expect(result?.progress, 12); - expect(result?.isComplete, isTrue); }); - test('show scope marks complete when progress reaches known show total', () async { + test('show scope caps progress at known show total', () async { final resolver = AnimeEpisodeProgressResolver( _FakeMediaServerClient({ - 'show-1': [_season(1, watched: 12, total: 12), _season(2, watched: 11, total: 12)], + 'show-1': [_season(1, watched: 12, total: 12), _season(2, watched: 12, total: 12)], }), ); final result = await resolver.resolve(_episode(season: 2, number: 12), scope: AnimeProgressScope.show); expect(result?.progress, 24); - expect(result?.isComplete, isTrue); }); - test('unknown total does not mark complete', () async { + test('unknown total still returns progress', () async { final resolver = AnimeEpisodeProgressResolver( _FakeMediaServerClient({ 'show-1': [_season(1, watched: 11)], @@ -119,7 +114,6 @@ void main() { final result = await resolver.resolve(_episode(season: 1, number: 12), scope: AnimeProgressScope.season); expect(result?.progress, 12); - expect(result?.isComplete, isFalse); }); test('already watched current episode does not add one', () async { @@ -135,7 +129,6 @@ void main() { ); expect(result?.progress, 5); - expect(result?.isComplete, isFalse); }); test('missing viewedLeafCount returns null', () async { @@ -160,15 +153,30 @@ void main() { expect(result, isNull); }); - test('cache is reused for multiple episodes in the same show', () async { + test('in-flight load is reused for concurrent episodes in the same show', () async { final client = _FakeMediaServerClient({ 'show-1': [_season(1, watched: 10), _season(2, watched: 5)], }); final resolver = AnimeEpisodeProgressResolver(client); - expect((await resolver.resolve(_episode(season: 2, number: 6), scope: AnimeProgressScope.show))?.progress, 16); - expect((await resolver.resolve(_episode(season: 2, number: 7), scope: AnimeProgressScope.show))?.progress, 16); + final first = resolver.resolve(_episode(season: 2, number: 6), scope: AnimeProgressScope.show); + final second = resolver.resolve(_episode(season: 2, number: 7), scope: AnimeProgressScope.show); + + expect((await first)?.progress, 16); + expect((await second)?.progress, 16); expect(client.fetchChildrenCalls, 1); }); + + test('sequential loads refetch watched counts', () async { + final client = _FakeMediaServerClient({ + 'show-1': [_season(1, watched: 5)], + }); + final resolver = AnimeEpisodeProgressResolver(client); + + expect((await resolver.resolve(_episode(season: 1, number: 6), scope: AnimeProgressScope.season))?.progress, 6); + client.childrenByParent['show-1'] = [_season(1, watched: 6)]; + expect((await resolver.resolve(_episode(season: 1, number: 7), scope: AnimeProgressScope.season))?.progress, 7); + expect(client.fetchChildrenCalls, 2); + }); }); } diff --git a/test/services/trackers/mal_tracker_test.dart b/test/services/trackers/mal_tracker_test.dart new file mode 100644 index 00000000..8bb9a1e6 --- /dev/null +++ b/test/services/trackers/mal_tracker_test.dart @@ -0,0 +1,88 @@ +import 'dart:convert'; + +import 'package:flutter_test/flutter_test.dart'; +import 'package:http/http.dart' as http; +import 'package:http/testing.dart'; +import 'package:plezy/models/trackers/anime_ids.dart'; +import 'package:plezy/models/trackers/tracker_context.dart'; +import 'package:plezy/services/trackers/mal/mal_session.dart'; +import 'package:plezy/services/trackers/mal/mal_tracker.dart'; +import 'package:plezy/utils/external_ids.dart'; + +MalSession _session() { + final now = DateTime.now().millisecondsSinceEpoch ~/ 1000; + return MalSession(accessToken: 'token', refreshToken: 'refresh', expiresAt: now + 86400, createdAt: now); +} + +TrackerContext _episode({int malId = 21, int episodeNumber = 12, int? animeProgress = 12}) { + return TrackerContext.episode( + external: const ExternalIds(tvdb: 1), + anime: AnimeIds(mal: malId), + ratingKey: 'episode-1', + libraryGlobalKey: null, + season: 1, + episodeNumber: episodeNumber, + animeProgress: animeProgress, + ); +} + +void main() { + group('MalTracker', () { + final tracker = MalTracker.instance; + + tearDown(() { + tracker.rebindSession(null, onSessionInvalidated: () {}); + }); + + test('marks completed when scoped progress reaches MAL total', () async { + final requests = []; + final client = MockClient((request) async { + requests.add(request); + if (request.method == 'GET') { + expect(request.url.path, '/v2/anime/21'); + expect(request.url.queryParameters['fields'], 'num_episodes'); + return http.Response(json.encode({'num_episodes': 12}), 200); + } + if (request.method == 'PUT') return http.Response('{}', 200); + fail('Unexpected ${request.method} ${request.url}'); + }); + tracker.rebindSession(_session(), onSessionInvalidated: () {}, httpClient: client); + + await tracker.markWatched(_episode(animeProgress: 13)); + + final put = requests.singleWhere((request) => request.method == 'PUT'); + expect(Uri.splitQueryString(put.body), {'status': 'completed', 'num_watched_episodes': '12'}); + }); + + test('keeps fallback local progress as watching without total lookup', () async { + final requests = []; + final client = MockClient((request) async { + requests.add(request); + if (request.method == 'PUT') return http.Response('{}', 200); + fail('Unexpected ${request.method} ${request.url}'); + }); + tracker.rebindSession(_session(), onSessionInvalidated: () {}, httpClient: client); + + await tracker.markWatched(_episode(animeProgress: null)); + + final put = requests.singleWhere((request) => request.method == 'PUT'); + expect(Uri.splitQueryString(put.body), {'status': 'watching', 'num_watched_episodes': '12'}); + }); + + test('keeps progress as watching when MAL total is unknown', () async { + final requests = []; + final client = MockClient((request) async { + requests.add(request); + if (request.method == 'GET') return http.Response(json.encode({'num_episodes': 0}), 200); + if (request.method == 'PUT') return http.Response('{}', 200); + fail('Unexpected ${request.method} ${request.url}'); + }); + tracker.rebindSession(_session(), onSessionInvalidated: () {}, httpClient: client); + + await tracker.markWatched(_episode()); + + final put = requests.singleWhere((request) => request.method == 'PUT'); + expect(Uri.splitQueryString(put.body), {'status': 'watching', 'num_watched_episodes': '12'}); + }); + }); +} diff --git a/test/services/trackers/tracker_id_resolver_test.dart b/test/services/trackers/tracker_id_resolver_test.dart index 5095da98..ea980ad3 100644 --- a/test/services/trackers/tracker_id_resolver_test.dart +++ b/test/services/trackers/tracker_id_resolver_test.dart @@ -45,8 +45,8 @@ class _FakeAnimeProgressLookup implements AnimeEpisodeProgressLookup { MediaItem? lastEpisode; AnimeProgressScope? lastScope; - _FakeAnimeProgressLookup(int? progress, {bool isComplete = false}) - : result = progress == null ? null : ResolvedAnimeProgress(progress: progress, isComplete: isComplete); + _FakeAnimeProgressLookup(int? progress) + : result = progress == null ? null : ResolvedAnimeProgress(progress: progress); @override Future resolve(MediaItem episode, {required AnimeProgressScope scope}) async { @@ -100,14 +100,13 @@ void main() { expect(ids?.anime?.mal, 21); expect(ids?.animeProgressScope, AnimeProgressScope.show); expect(ids?.animeProgress, 6); - expect(ids?.animeProgressComplete, isFalse); 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, isComplete: true); + final animeProgress = _FakeAnimeProgressLookup(18); final resolver = _resolver( animeProgress: animeProgress, rows: const [ @@ -121,7 +120,6 @@ void main() { expect(ids?.anime?.mal, 200); expect(ids?.animeProgressScope, AnimeProgressScope.season); expect(ids?.animeProgress, 18); - expect(ids?.animeProgressComplete, isTrue); expect(animeProgress.resolveCalls, 1); expect(animeProgress.lastScope, AnimeProgressScope.season); });