Files
plezy/lib/services/trackers/tracker_account_store.dart
T
edde746 eaa1736c4e feat(mdblist): sync watched history, scrobbles and ratings with MDBList
Connects MDBList through its OAuth device-code grant, registered as a
Device Code app so no client secret or redirect URI ships in the binary
and TV, mobile and desktop all use the same flow.

MDBList omits `verification_uri_complete`, but its device page seeds the
code field from a `user_code` query parameter and the sign-in redirect
preserves the query string, so the activation link is built locally and
the dialog's open button lands on a filled-in form instead of an empty
one. A server-supplied complete URL still wins if one ever appears.

Poll state is read from the response body rather than the status code:
`authorization_pending` and `slow_down` both arrive as HTTP 400, and a
missing grant answers 404 `device_not_found`.

Writes go out as real-time `/scrobble/*` reports plus `/sync/watched`
for the marks that never pass through the player, with ratings on
`/sync/ratings`. Matching uses IMDb and TMDb only — MDBList's id block
has no `tvdb` field, so a TVDB-only item is skipped rather than written
under an empty id block.
2026-08-05 12:03:05 +02:00

55 lines
2.1 KiB
Dart

import '../../profiles/profile.dart';
import '../base_shared_preferences_service.dart';
import 'tracker_constants.dart';
import 'tracker_session.dart';
/// Per-Plex-profile session persistence for any tracker service.
///
/// Keyed by `user_{uuid}_{baseKey}` so each Plex Home profile gets its own
/// stored session. The storage key remains service-specific, while the payload
/// is the unified [TrackerSession] JSON shape.
///
/// Pass an empty `userUuid` to fall back to a single global slot (used
/// before a profile has been selected).
class TrackerAccountStore {
static final Map<TrackerService, TrackerAccountStore> _stores = {
TrackerService.mal: TrackerAccountStore._(TrackerService.mal, 'mal_session'),
TrackerService.anilist: TrackerAccountStore._(TrackerService.anilist, 'anilist_session'),
TrackerService.simkl: TrackerAccountStore._(TrackerService.simkl, 'simkl_session'),
TrackerService.trakt: TrackerAccountStore._(TrackerService.trakt, 'trakt_session'),
TrackerService.mdblist: TrackerAccountStore._(TrackerService.mdblist, 'mdblist_session'),
};
static TrackerAccountStore forService(TrackerService service) => _stores[service]!;
final TrackerService service;
final String _baseKey;
TrackerAccountStore._(this.service, this._baseKey);
String _scopedKey(String userUuid) => profileScopedPrefsKey(userUuid, _baseKey);
Future<TrackerSession?> load(String userUuid) async {
final prefs = await BaseSharedPreferencesService.sharedCache();
final raw = readTolerantString(prefs, _scopedKey(userUuid));
if (raw == null) return null;
try {
return TrackerSession.decode(raw, service: service);
} catch (_) {
return null;
}
}
Future<void> save(String userUuid, TrackerSession session) async {
final prefs = await BaseSharedPreferencesService.sharedCache();
await prefs.setString(_scopedKey(userUuid), session.encode());
}
Future<void> clear(String userUuid) async {
final prefs = await BaseSharedPreferencesService.sharedCache();
await prefs.remove(_scopedKey(userUuid));
}
}
TrackerAccountStore trackerAccountStore(TrackerService service) => TrackerAccountStore.forService(service);