Files
plezy/lib/services/trackers/mal/mal_tracker.dart
T
edde746 78090d8bd6 refactor(trackers): unify session and account store across services
Replace the per-tracker json_serializable session classes and account stores with a shared TrackerSession (service-aware persisted validation), a single TrackerAccountStore, a shared TrackerHttpClient, and an AnimeListTrackerBase mixin for MAL/AniList. Scope the per-service rebind guard so a disconnect can't abort a racing profile load. Add migration-safety, MAL refresh, episode-count cache, and content-type tests.
2026-06-27 10:05:03 +02:00

99 lines
3.0 KiB
Dart

import 'package:http/http.dart' as http;
import '../../../models/trackers/anime_ids.dart';
import '../../../utils/app_logger.dart';
import '../../settings_service.dart';
import '../anime_list_tracker_base.dart';
import '../tracker.dart';
import '../tracker_constants.dart';
import '../tracker_exceptions.dart';
import '../tracker_session.dart';
import 'mal_client.dart';
/// MyAnimeList scrobble tracker. Marks `num_watched_episodes` on the user's
/// list entry once playback crosses the watched threshold.
///
/// MAL is anime-only: no-op when [TrackerContext.anime] is null.
///
/// For anime episodes, MAL receives watched progress in the mapped anime entry
/// when Fribb can define that scope, otherwise local episode progress.
class MalTracker extends TrackerBase with ClientBackedTracker<MalClient>, AnimeListTrackerBase<MalClient> {
static MalTracker? _instance;
static MalTracker get instance => _instance ??= MalTracker._();
MalTracker._();
@override
String get name => 'mal';
@override
TrackerService get service => TrackerService.mal;
@override
bool readEnabledSetting(SettingsService settings) => settings.read(SettingsService.enableMalScrobble);
@override
String get logLabel => 'MAL';
@override
String get idLogName => 'mal';
@override
String get ratingUnavailableName => 'MAL';
void rebindSession(
TrackerSession? session, {
required void Function() onSessionInvalidated,
void Function(TrackerSession)? onSessionUpdated,
http.Client? httpClient,
}) {
rebindTrackerClient(
session,
onBeforeBind: clearAnimeListTrackerCache,
createClient: (session) => MalClient(
session,
onSessionInvalidated: onSessionInvalidated,
onSessionUpdated: onSessionUpdated,
httpClient: httpClient,
),
);
}
@override
int? animeId(AnimeIds? anime) => anime?.mal;
@override
Future<int?> loadAnimeEpisodeCount(MalClient client, int malId) => client.getAnimeEpisodeCount(malId);
@override
Future<void> saveAnimeProgress(
MalClient client, {
required int animeId,
required int progress,
required bool completed,
}) async {
final fields = {'status': completed ? 'completed' : 'watching', 'num_watched_episodes': '$progress'};
await client.updateMyListStatus(animeId, fields);
appLogger.d('MAL: updated list status (mal=$animeId, fields=$fields)');
}
@override
Future<void> deleteAnimeEntry(MalClient client, int malId) async {
try {
await client.deleteMyListStatus(malId);
appLogger.d('MAL: deleted list status (mal=$malId)');
} on TrackerApiException catch (e) {
if (e.statusCode == 404) return;
rethrow;
}
}
@override
Future<void> setAnimeRating(MalClient client, int malId, int score) async {
await client.updateMyListStatus(malId, {'score': '$score'});
appLogger.d(score == 0 ? 'MAL: cleared score (mal=$malId)' : 'MAL: updated score (mal=$malId, score=$score)');
}
@override
Future<int?> loadAnimeRating(MalClient client, int malId) => client.getMyListScore(malId);
}