@@ -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<int?> 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<Map<String, dynamic>> query(String query, {Map<String, dynamic>? variables}) async {
|
||||
final uri = Uri.parse(AnilistConstants.apiBase);
|
||||
final headers = AnilistConstants.headers(accessToken: _session.accessToken);
|
||||
|
||||
@@ -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<int, Future<int?>> _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<int?> _episodeCount(AnilistClient client, int anilistId) {
|
||||
final existing = _episodeCountLoads[anilistId];
|
||||
if (existing != null) return existing;
|
||||
|
||||
late final Future<int?> 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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<Map<int, _SeasonProgress>?> 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<int, _SeasonProgress> 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<Map<int, _SeasonProgress>?> _loadSeasonProgress(String showId) async {
|
||||
|
||||
@@ -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<void> updateMyListStatus(int animeId, Map<String, String> 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<int?> 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<MalSession> _refresh() => _refreshCoalescer.run(_doRefresh);
|
||||
|
||||
@@ -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<int, Future<int?>> _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<int?> _episodeCount(MalClient client, int malId) {
|
||||
final existing = _episodeCountLoads[malId];
|
||||
if (existing != null) return existing;
|
||||
|
||||
late final Future<int?> 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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -161,7 +161,6 @@ class TrackerCoordinator {
|
||||
season: season,
|
||||
episodeNumber: number,
|
||||
animeProgress: ids.animeProgress,
|
||||
animeProgressComplete: ids.animeProgressComplete,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user