Files
plezy/lib/models/plex/plex_account_preferences.dart
T
edde746 b43cd81102 chore(profiles): let AccountPreferencesController own the active user's playback preferences
UserProfileProvider kept a second cache of the active user's audio/subtitle
defaults beside AccountPreferencesRepository, re-implemented the active
profile/connection listener wiring and Plex Home token resolution, and for
Plex Home profiles bypassed the shared cache entirely (fetching /user with
its own client), so a write in the Account preferences screen never reached
Home-profile playback.

The controller now resolves the active account (the head of its sorted
account list), loads it through the one repository on attach, on profile
switch, and when the account's token changes, and exposes it as
activePreferences; ensureActiveLoaded replaces initialize() for the startup
gate. Playback, offline waiting, and logout storage clearing read the
controller directly; UserProfileProvider, the per-backend user-profile DTOs,
PlexAuthService.getUserProfile, and JellyfinClient.fetchUserProfile are
deleted.

The ranked defaultAudioLanguages/defaultSubtitleLanguages lists go with
them: Plex Web never reads them, PMS applies them when it stamps `selected`
on streams, and Plezy already trusts that flag ahead of the profile. They
only refined a best-effort audio fallback for Plex items with no media
info, which the primary language covers; on that path a second preferred
language is no longer tried before the file's default track.

Plex Home profiles now read /user/profile with the switched token through
the same cache as local profiles.
2026-09-03 11:36:56 +02:00

132 lines
5.9 KiB
Dart

import '../../media/account_preferences.dart';
import '../../media/media_server_user_profile.dart';
import '../../utils/json_utils.dart';
import '../../utils/language_codes.dart';
/// Wire mapping for plex.tv `/api/v2/user/profile` account preferences.
///
/// The account API has drifted before, so reads tolerate each field independently
/// rather than allowing one unexpected value to discard the whole profile.
class PlexAccountPreferences {
const PlexAccountPreferences._();
/// Maps either a bare profile object or the `profile` envelope returned by
/// plex.tv's broader `/user` endpoint.
static AccountPreferences fromProfileJson(Map<String, dynamic> json) {
final envelope = json['profile'];
final profile = envelope is Map<String, dynamic> ? envelope : json;
return AccountPreferences(
preferredAudioLanguage: _profileLanguage(profile['defaultAudioLanguage']),
playDefaultAudioTrack: flexibleBoolNullable(profile['autoSelectAudio']),
preferredSubtitleLanguage: _profileLanguage(profile['defaultSubtitleLanguage']),
subtitlePlaybackMode: _subtitleMode(profile['autoSelectSubtitle']),
watchedIndicator: WatchedIndicatorScope.fromPlexValue(flexibleInt(profile['watchedIndicator'])),
mediaReviewsVisibility: MediaReviewsVisibility.fromPlexValue(flexibleInt(profile['mediaReviewsVisibility'])),
subtitleAccessibility: SubtitleAccessibilityPreference.fromPlexValue(
flexibleInt(profile['defaultSubtitleAccessibility']),
),
forcedSubtitles: ForcedSubtitlePreference.fromPlexValue(flexibleInt(profile['defaultSubtitleForced'])),
);
}
/// Converts a sparse patch into the query-string shape expected by plex.tv.
///
/// Plex accepts only its eight account-profile keys here. Unsupported keys
/// and subtitle modes that have no Plex representation are programming errors.
static Map<String, String> queryParametersFor(AccountPreferencesPatch patch) {
final parameters = <String, String>{};
for (final entry in patch.values.entries) {
switch (entry.key) {
case AccountPreferenceKey.preferredAudioLanguage:
parameters['defaultAudioLanguage'] = _languageQueryValue(entry.key, entry.value);
case AccountPreferenceKey.autoSelectAudio:
parameters['autoSelectAudio'] = _boolQueryValue(entry.key, entry.value);
case AccountPreferenceKey.preferredSubtitleLanguage:
parameters['defaultSubtitleLanguage'] = _languageQueryValue(entry.key, entry.value);
case AccountPreferenceKey.subtitleMode:
parameters['autoSelectSubtitle'] = _subtitleModeQueryValue(entry.key, entry.value);
case AccountPreferenceKey.watchedIndicator:
parameters['watchedIndicator'] = _requiredValue<WatchedIndicatorScope>(
entry.key,
entry.value,
).plexValue.toString();
case AccountPreferenceKey.mediaReviewsVisibility:
parameters['mediaReviewsVisibility'] = _requiredValue<MediaReviewsVisibility>(
entry.key,
entry.value,
).plexValue.toString();
case AccountPreferenceKey.subtitleAccessibility:
parameters['defaultSubtitleAccessibility'] = _requiredValue<SubtitleAccessibilityPreference>(
entry.key,
entry.value,
).plexValue.toString();
case AccountPreferenceKey.forcedSubtitles:
parameters['defaultSubtitleForced'] = _requiredValue<ForcedSubtitlePreference>(
entry.key,
entry.value,
).plexValue.toString();
case AccountPreferenceKey.rememberAudioSelections:
case AccountPreferenceKey.rememberSubtitleSelections:
case AccountPreferenceKey.autoPlayNextEpisode:
case AccountPreferenceKey.displayMissingEpisodes:
case AccountPreferenceKey.hidePlayedInLatest:
case AccountPreferenceKey.displayCollectionsView:
case AccountPreferenceKey.rewatchingInNextUp:
throw ArgumentError.value(entry.value, entry.key.name, 'is not supported by Plex account preferences');
}
}
// Plex Web's onSubmit forces subtitles back to manual when audio auto-selection
// is disabled; sending both keeps the two server-stored settings consistent.
if (patch[AccountPreferenceKey.autoSelectAudio] == false) {
parameters['autoSelectSubtitle'] = '0';
}
return parameters;
}
/// Singular language fields tolerate the array/CSV drift the account API has
/// shown (#1488) by taking the first entry.
static String? _profileLanguage(Object? value) => flexibleCsvStringList(value)?.first;
static SubtitlePlaybackMode? _subtitleMode(Object? value) => switch (flexibleInt(value)) {
0 => SubtitlePlaybackMode.none,
1 => SubtitlePlaybackMode.smart,
2 => SubtitlePlaybackMode.always,
_ => null,
};
static String _languageQueryValue(AccountPreferenceKey key, Object? value) {
if (value == null) return '';
if (value is! String) {
throw ArgumentError.value(value, key.name, 'must be an ISO 639 language code or null');
}
if (value.isEmpty) return '';
return LanguageCodes.getIso6391Code(value) ?? value;
}
static String _boolQueryValue(AccountPreferenceKey key, Object? value) =>
_requiredValue<bool>(key, value) ? '1' : '0';
static String _subtitleModeQueryValue(AccountPreferenceKey key, Object? value) {
final mode = _requiredValue<SubtitlePlaybackMode>(key, value);
return switch (mode) {
SubtitlePlaybackMode.none => '0',
SubtitlePlaybackMode.smart => '1',
SubtitlePlaybackMode.always => '2',
SubtitlePlaybackMode.defaultMode || SubtitlePlaybackMode.onlyForced => throw ArgumentError.value(
mode,
key.name,
'cannot be represented by Plex autoSelectSubtitle',
),
};
}
static T _requiredValue<T>(AccountPreferenceKey key, Object? value) {
if (value is T) return value;
throw ArgumentError.value(value, key.name, 'must be a non-null $T');
}
}