Files
plezy/lib/services/trackers/tracker_session.dart
edde746 a33c423985 refactor(models): stop parsing wire fields and enum members nothing consumes
Parse-only DTO surface deleted across trackers and catalog models: quality-preset storageKey/fromStorage round-trips (persistence uses EnumPref on .name), PlexHome's placeholder identity fields (id stays — deriveHomeSecret reads it), SeerrUser.email/avatar, SimklSearchResult.endpointType, SimklDetail.type, SimklAllItemsEntry.status/isShow, TraktCatalogEntry.rank, CatalogItem.relevance, and the never-produced CatalogRankScope.favorited/.seasonal members with their label arms. Tracker root: OAuthProxyStart.expiresIn, TrackerSession.scope/isExpired, copyWith slimmed to its one real parameter, TrackerHttpClient.service. TrackerAccountStore.service and the trackerAccountStore() alias are live (persisted-blob validation, callers) and stay.
2026-08-17 19:02:24 +02:00

146 lines
5.5 KiB
Dart

import 'oauth_proxy_client.dart';
import 'tracker_constants.dart';
import 'tracker_exceptions.dart';
import 'tracker_session_utils.dart';
class TrackerSession {
final String accessToken;
final String? refreshToken;
final int? expiresAt;
final String? username;
final int createdAt;
const TrackerSession({
required this.accessToken,
required this.createdAt,
this.refreshToken,
this.expiresAt,
this.username,
});
bool get needsRefresh => expiresAt != null && trackerTokenNeedsRefresh(expiresAt!);
String requireRefreshToken(TrackerService service) => _requireRefreshToken(service, refreshToken);
TrackerSession copyWith({String? username}) {
return TrackerSession(
accessToken: accessToken,
refreshToken: refreshToken,
expiresAt: expiresAt,
username: username ?? this.username,
createdAt: createdAt,
);
}
Map<String, dynamic> toJson() => {
'access_token': accessToken,
'refresh_token': refreshToken,
'expires_at': expiresAt,
'username': username,
'created_at': createdAt,
};
String encode() => encodeTrackerSessionJson(toJson());
factory TrackerSession.fromJson(Map<String, dynamic> json, {TrackerService? service}) {
final session = TrackerSession(
accessToken: json['access_token'] as String,
refreshToken: json['refresh_token'] as String?,
expiresAt: (json['expires_at'] as num?)?.toInt(),
username: json['username'] as String?,
createdAt: (json['created_at'] as num).toInt(),
);
// When decoding a persisted blob we know the service, so re-impose the
// per-service invariants the old json_serializable decoders guaranteed.
// A corrupt/truncated blob then throws (and TrackerAccountStore.load's
// catch falls back to a clean re-auth) rather than loading a broken,
// never-expiring session.
if (service != null) session._validatePersisted(service);
return session;
}
void _validatePersisted(TrackerService service) {
void requireExpiry() {
if (expiresAt == null) {
throw TrackerAuthException(service: service, message: 'Corrupt persisted session', isPermanent: true);
}
}
switch (service) {
case TrackerService.mal:
case TrackerService.trakt:
case TrackerService.mdblist:
_validateRefreshToken(service, refreshToken);
requireExpiry();
case TrackerService.anilist:
requireExpiry();
case TrackerService.simkl:
return;
}
}
factory TrackerSession.fromOAuthProxyResult(TrackerService service, OAuthProxyResult result) {
final createdAt = trackerSessionNowEpochSeconds();
return switch (service) {
TrackerService.anilist => TrackerSession(
accessToken: result.accessToken,
expiresAt: createdAt + (result.expiresIn ?? 365 * 24 * 60 * 60),
createdAt: createdAt,
),
TrackerService.mal => TrackerSession(
accessToken: result.accessToken,
refreshToken: _requireRefreshToken(service, result.refreshToken),
expiresAt: createdAt + (result.expiresIn ?? 31 * 24 * 60 * 60),
createdAt: createdAt,
),
_ => throw ArgumentError('OAuth proxy sessions are not supported for ${service.name}'),
};
}
factory TrackerSession.fromTokenResponse(TrackerService service, Map<String, dynamic> json) {
final createdAt = (json['created_at'] as num?)?.toInt() ?? trackerSessionNowEpochSeconds();
return switch (service) {
TrackerService.mal => TrackerSession(
accessToken: json['access_token'] as String,
refreshToken: _requireRefreshToken(service, json['refresh_token'] as String?),
expiresAt: createdAt + (json['expires_in'] as num).toInt(),
createdAt: createdAt,
),
TrackerService.simkl => TrackerSession(accessToken: json['access_token'] as String, createdAt: createdAt),
// MDBList issues a 30-day access token plus a refresh token.
TrackerService.mdblist => TrackerSession(
accessToken: json['access_token'] as String,
refreshToken: _requireRefreshToken(service, json['refresh_token'] as String?),
expiresAt: createdAt + (json['expires_in'] as num).toInt(),
createdAt: createdAt,
),
TrackerService.trakt => TrackerSession(
accessToken: json['access_token'] as String,
refreshToken: _requireRefreshToken(service, json['refresh_token'] as String?),
expiresAt: createdAt + (json['expires_in'] as num).toInt(),
createdAt: createdAt,
),
_ => throw ArgumentError('Token-response sessions are not supported for ${service.name}'),
};
}
static void _validateRefreshToken(TrackerService service, String? token) {
// Reject empty as well as null: a blank refresh token can never refresh, so
// failing fast (at connect, or as a clean re-auth on load) is more correct
// than persisting a session that is silently doomed at the next expiry. The
// connect-time factories route through here too, so new sessions can never
// reach the persisted empty-token state the old decoders tolerated.
if (token == null || token.isEmpty) {
throw TrackerAuthException(service: service, message: 'Missing refresh token', isPermanent: true);
}
}
static String _requireRefreshToken(TrackerService service, String? token) {
_validateRefreshToken(service, token);
return token!;
}
static TrackerSession decode(String raw, {TrackerService? service}) =>
decodeTrackerSessionJson(raw, (json) => TrackerSession.fromJson(json, service: service));
}