The job "which Plex token represents the active profile right now" was implemented three times against the same registries — the Discover session supplier, the Seerr token supplier, and UserProfileProvider's settings refresh — and the copies had already diverged on whether a Plex Home profile may fall back to the account token. resolveActivePlexToken in lib/profiles/active_plex_token.dart now owns the policy: the per-user ProfileConnection token wins when present, else the account-owner token, with an explicit allowAccountTokenForHomeUser flag (true for Discover/Seerr, false for the settings refresh, which must not impersonate the owner). The three call sites keep only their genuine differences.
161 lines
6.1 KiB
Dart
161 lines
6.1 KiB
Dart
import 'dart:async';
|
|
|
|
import 'package:flutter/foundation.dart';
|
|
|
|
import '../connection/connection_registry.dart';
|
|
import '../mixins/disposable_change_notifier_mixin.dart';
|
|
import '../models/seerr/seerr_session.dart';
|
|
import '../profiles/active_plex_token.dart';
|
|
import '../profiles/active_profile_provider.dart';
|
|
import '../profiles/profile_connection_registry.dart';
|
|
import '../services/seerr/seerr_auth_service.dart';
|
|
import '../services/seerr/seerr_client.dart';
|
|
import '../services/seerr/seerr_session_store.dart';
|
|
import '../utils/app_logger.dart';
|
|
|
|
/// Resolve the active profile's Plex token for Seerr sign-in/re-auth:
|
|
/// the profile's per-user token when a bind exists (a Home user's Seerr
|
|
/// account maps to their own plex.tv user), else the account token.
|
|
SeerrPlexTokenSupplier buildSeerrPlexTokenSupplier({
|
|
required ActiveProfileProvider activeProfile,
|
|
required ConnectionRegistry connections,
|
|
required ProfileConnectionRegistry profileConnections,
|
|
}) {
|
|
return () async {
|
|
final resolved = await resolveActivePlexToken(
|
|
activeProfile: activeProfile,
|
|
connections: connections,
|
|
profileConnections: profileConnections,
|
|
allowAccountTokenForHomeUser: true,
|
|
);
|
|
return resolved?.token;
|
|
};
|
|
}
|
|
|
|
/// Owns the active Seerr session for the currently-selected profile,
|
|
/// mirroring [TrackersProvider]'s rebind shape: `onActiveProfileChanged` loads
|
|
/// the profile's stored session and rebuilds the catalog client.
|
|
///
|
|
/// Unlike the OAuth trackers there is no in-provider connect flow — the
|
|
/// connect screen drives [SeerrAuthService] itself and hands the finished
|
|
/// session to [adoptSession].
|
|
class SeerrAccountProvider extends ChangeNotifier with DisposableChangeNotifierMixin {
|
|
SeerrAccountProvider({SeerrSessionStore? store, SeerrAuthService? authService})
|
|
: _store = store ?? const SeerrSessionStore(),
|
|
authService = authService ?? SeerrAuthService();
|
|
|
|
final SeerrSessionStore _store;
|
|
final SeerrAuthService authService;
|
|
SeerrPlexTokenSupplier? _plexTokenSupplier;
|
|
|
|
/// Store writes go through one queue: save() awaits an AES-GCM protect
|
|
/// step, so two rapid unawaited writes could otherwise persist
|
|
/// last-started-first (and a clear could lose to a still-pending save).
|
|
Future<void> _pendingPersistence = Future<void>.value();
|
|
|
|
Future<void> _enqueuePersistence(Future<void> Function() op) {
|
|
final run = _pendingPersistence.then((_) => op());
|
|
_pendingPersistence = run.then<void>(
|
|
(_) {},
|
|
onError: (Object e) => appLogger.w('Seerr: session persistence failed', error: e),
|
|
);
|
|
return run;
|
|
}
|
|
|
|
SeerrSession? _session;
|
|
String _activeUserUuid = '';
|
|
int _bindingGeneration = 0;
|
|
SeerrClient? _catalogClient;
|
|
|
|
SeerrSession? get session => _session;
|
|
bool get isConnected => _session != null;
|
|
String? get displayName => _session?.displayName;
|
|
|
|
/// Client for the catalog/request surfaces; null when disconnected.
|
|
SeerrClient? get catalogClient => _catalogClient;
|
|
|
|
/// Wired once from the provider tree (the registries live above the
|
|
/// profile session subtree).
|
|
void bindPlexTokenSupplier(SeerrPlexTokenSupplier supplier) => _plexTokenSupplier = supplier;
|
|
|
|
/// The connect screen's "Sign in with Plex" needs the same token the
|
|
/// silent re-auth path would use. Null on Jellyfin-only setups.
|
|
Future<String?> resolvePlexToken() async {
|
|
try {
|
|
return await _plexTokenSupplier?.call();
|
|
} catch (e) {
|
|
appLogger.w('Seerr: Plex token resolution failed', error: e);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/// Called whenever the active profile changes (or on initial load).
|
|
Future<void> onActiveProfileChanged(String? newUserUuid) async {
|
|
if (isDisposed) return;
|
|
final userUuid = newUserUuid ?? '';
|
|
final generation = ++_bindingGeneration;
|
|
_activeUserUuid = userUuid;
|
|
final loaded = await _store.load(userUuid);
|
|
_setSessionAndRebind(userUuid, generation, loaded);
|
|
}
|
|
|
|
/// Persist and bind a session the connect screen established.
|
|
Future<void> adoptSession(SeerrSession session) async {
|
|
final userUuid = _activeUserUuid;
|
|
await _enqueuePersistence(() => _store.save(userUuid, session));
|
|
_setSessionAndRebind(userUuid, ++_bindingGeneration, session);
|
|
}
|
|
|
|
/// Sign out server-side (best effort) and clear local state.
|
|
Future<void> disconnect() async {
|
|
final userUuid = _activeUserUuid;
|
|
final session = _session;
|
|
_setSessionAndRebind(userUuid, ++_bindingGeneration, null);
|
|
await _enqueuePersistence(() => _store.clear(userUuid));
|
|
if (session != null) await authService.signOut(session);
|
|
}
|
|
|
|
void _setSessionAndRebind(String userUuid, int generation, SeerrSession? session) {
|
|
if (!_isCurrentBinding(userUuid, generation)) return;
|
|
_session = session;
|
|
_catalogClient?.dispose();
|
|
_catalogClient = session == null
|
|
? null
|
|
: SeerrClient(
|
|
session,
|
|
onSessionInvalidated: () => _handleSessionInvalidated(userUuid, generation),
|
|
onSessionUpdated: (next) => _handleSessionUpdated(userUuid, generation, next),
|
|
plexTokenSupplier: () async => _plexTokenSupplier?.call(),
|
|
authService: authService,
|
|
);
|
|
safeNotifyListeners();
|
|
}
|
|
|
|
bool _isCurrentBinding(String userUuid, int generation) {
|
|
return !isDisposed && userUuid == _activeUserUuid && generation == _bindingGeneration;
|
|
}
|
|
|
|
void _handleSessionUpdated(String userUuid, int generation, SeerrSession session) {
|
|
if (!_isCurrentBinding(userUuid, generation)) return;
|
|
_session = session;
|
|
unawaited(_enqueuePersistence(() => _store.save(userUuid, session)));
|
|
safeNotifyListeners();
|
|
}
|
|
|
|
/// Called by [SeerrClient] when silent re-auth fails permanently: clear
|
|
/// local state so the UI shows "not connected" and the user can re-link.
|
|
void _handleSessionInvalidated(String userUuid, int generation) {
|
|
if (!_isCurrentBinding(userUuid, generation)) return;
|
|
final nextGeneration = ++_bindingGeneration;
|
|
unawaited(_enqueuePersistence(() => _store.clear(userUuid)));
|
|
_setSessionAndRebind(userUuid, nextGeneration, null);
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
_catalogClient?.dispose();
|
|
_catalogClient = null;
|
|
super.dispose();
|
|
}
|
|
}
|