feat: per-tracker library whitelist/blacklist

close #917
This commit is contained in:
edde746
2026-04-24 10:30:16 +02:00
parent e8729a4e27
commit bf2584d90f
47 changed files with 1084 additions and 8 deletions
@@ -2,6 +2,7 @@ import '../../../models/trackers/tracker_context.dart';
import '../../../utils/app_logger.dart';
import '../../settings_service.dart';
import '../tracker.dart';
import '../tracker_constants.dart';
import 'anilist_client.dart';
import 'anilist_session.dart';
@@ -17,6 +18,9 @@ class AnilistTracker extends TrackerBase {
@override
String get name => 'anilist';
@override
TrackerService get service => TrackerService.anilist;
@override
bool get needsFribb => true;
@@ -2,6 +2,7 @@ import '../../../models/trackers/tracker_context.dart';
import '../../../utils/app_logger.dart';
import '../../settings_service.dart';
import '../tracker.dart';
import '../tracker_constants.dart';
import 'mal_client.dart';
import 'mal_session.dart';
@@ -22,6 +23,9 @@ class MalTracker extends TrackerBase {
@override
String get name => 'mal';
@override
TrackerService get service => TrackerService.mal;
@override
bool get needsFribb => true;
@@ -2,6 +2,7 @@ import '../../../models/trackers/tracker_context.dart';
import '../../../utils/app_logger.dart';
import '../../settings_service.dart';
import '../tracker.dart';
import '../tracker_constants.dart';
import 'simkl_client.dart';
import 'simkl_session.dart';
@@ -20,6 +21,9 @@ class SimklTracker extends TrackerBase {
@override
String get name => 'simkl';
@override
TrackerService get service => TrackerService.simkl;
@override
bool get needsFribb => false;
+15
View File
@@ -1,5 +1,6 @@
import '../../models/trackers/tracker_context.dart';
import '../settings_service.dart';
import 'tracker_constants.dart';
/// Abstract tracker contract: the coordinator calls [markWatched] once per
/// playback when progress crosses the watched threshold. Enabled/auth gating
@@ -7,6 +8,10 @@ import '../settings_service.dart';
abstract class Tracker {
String get name;
/// Stable identifier used to persist per-service settings (library filter,
/// scrobble enabled, etc.).
TrackerService get service;
bool get canScrobble;
/// True if this tracker's IDs only come from the Fribb anime mapping
@@ -18,6 +23,12 @@ abstract class Tracker {
Future<void> initialize();
Future<void> setEnabled(bool enabled);
/// Whether an item in the given library should be scrobbled. Applies the
/// per-tracker whitelist/blacklist — callers pass the Plex library
/// `serverId:sectionId` globalKey (null when unknown, in which case the
/// filter is bypassed so we err on the side of syncing).
bool shouldScrobbleForLibrary(String? libraryGlobalKey);
Future<void> markWatched(TrackerContext ctx);
}
@@ -45,4 +56,8 @@ abstract class TrackerBase implements Tracker {
Future<void> setEnabled(bool enabled) async {
_isEnabled = enabled;
}
@override
bool shouldScrobbleForLibrary(String? libraryGlobalKey) =>
SettingsService.instanceOrNull?.isLibraryAllowedForTracker(service, libraryGlobalKey) ?? true;
}
@@ -6,3 +6,11 @@ class TrackerConstants {
/// pushed to each tracker.
static const double watchedThresholdPercent = 80.0;
}
/// Identifier used across the app to disambiguate per-service operations.
/// The enum's `.name` forms part of the persistence key — do not rename
/// without a migration.
enum TrackerService { mal, anilist, simkl, trakt }
/// Blacklist+[] syncs every library (the default); whitelist+[] syncs nothing.
enum TrackerLibraryFilterMode { blacklist, whitelist }
@@ -113,7 +113,9 @@ class TrackerCoordinator {
}
Future<void> _dispatchMarkWatched(TrackerContext ctx) async {
final active = _trackers.where((t) => t.canScrobble);
final active = _trackers.where(
(t) => t.canScrobble && t.shouldScrobbleForLibrary(ctx.libraryGlobalKey),
);
await Future.wait(active.map((t) async {
try {
await t.markWatched(ctx);
@@ -127,6 +129,8 @@ class TrackerCoordinator {
final resolver = _resolver;
if (resolver == null) return null;
final libraryKey = metadata.librarySectionGlobalKey;
if (metadata.mediaType == PlexMediaType.movie) {
final ids = await resolver.resolveForMovie(metadata.ratingKey);
if (ids == null) return null;
@@ -134,6 +138,7 @@ class TrackerCoordinator {
external: ids.external,
anime: ids.anime,
ratingKey: metadata.ratingKey,
libraryGlobalKey: libraryKey,
);
}
@@ -147,6 +152,7 @@ class TrackerCoordinator {
external: ids.external,
anime: ids.anime,
ratingKey: metadata.ratingKey,
libraryGlobalKey: libraryKey,
season: season,
episodeNumber: number,
);