fix(plex): tolerate account-API schema drift in profile and switch parsing
Around July 3 plex.tv started returning the profile language-list fields (defaultAudioLanguages, defaultSubtitleLanguages, mediaReviewsLanguages) as comma-separated strings instead of arrays. The generated cast threw on the successful 201 /switch response, dropping the freshly minted Home user token: every rebind failed, the binder retried a /switch mint every 1-2s, and the app sat permanently in offline mode even after re-signing in. Accounts without language prefs set were unaffected, which is why the breakage looked sporadic. Parse the language lists with a CSV-aware coercion, and make UserSwitchResponse.fromJson strict only about authToken: decorative fields now coerce tolerantly and a broken profile blob falls back to defaults, so account-API drift can never brick token minting again. close #1488
This commit is contained in:
@@ -1,36 +1,47 @@
|
||||
import 'package:json_annotation/json_annotation.dart';
|
||||
|
||||
import '../../media/media_server_user_profile.dart';
|
||||
import '../../utils/json_utils.dart';
|
||||
|
||||
part 'plex_user_profile.g.dart';
|
||||
|
||||
/// Represents a Plex user's profile preferences
|
||||
/// Fetched from https://clients.plex.tv/api/v2/user
|
||||
///
|
||||
/// Every field parses tolerantly: the account API drifts (~July 2026 the
|
||||
/// language-list fields switched from arrays to CSV strings, #1488), and a
|
||||
/// profile blob must never fail to parse — token minting embeds it (see
|
||||
/// UserSwitchResponse.fromJson).
|
||||
@JsonSerializable()
|
||||
class PlexUserProfile implements MediaServerUserProfile {
|
||||
@JsonKey(defaultValue: true)
|
||||
@JsonKey(fromJson: _boolOrTrue)
|
||||
@override
|
||||
final bool autoSelectAudio;
|
||||
@JsonKey(defaultValue: 0)
|
||||
@JsonKey(fromJson: _intOr0)
|
||||
final int defaultAudioAccessibility;
|
||||
@JsonKey(fromJson: _flexibleLanguage)
|
||||
@override
|
||||
final String? defaultAudioLanguage;
|
||||
@JsonKey(fromJson: flexibleCsvStringList)
|
||||
@override
|
||||
final List<String>? defaultAudioLanguages;
|
||||
@JsonKey(fromJson: _flexibleLanguage)
|
||||
@override
|
||||
final String? defaultSubtitleLanguage;
|
||||
@JsonKey(fromJson: flexibleCsvStringList)
|
||||
@override
|
||||
final List<String>? defaultSubtitleLanguages;
|
||||
@JsonKey(defaultValue: 0)
|
||||
@JsonKey(fromJson: _intOr0)
|
||||
final int autoSelectSubtitle;
|
||||
@JsonKey(defaultValue: 0)
|
||||
@JsonKey(fromJson: _intOr0)
|
||||
final int defaultSubtitleAccessibility;
|
||||
@JsonKey(defaultValue: 1)
|
||||
@JsonKey(fromJson: _intOr1)
|
||||
final int defaultSubtitleForced;
|
||||
@JsonKey(defaultValue: 1)
|
||||
@JsonKey(fromJson: _intOr1)
|
||||
final int watchedIndicator;
|
||||
@JsonKey(defaultValue: 0)
|
||||
@JsonKey(fromJson: _intOr0)
|
||||
final int mediaReviewsVisibility;
|
||||
@JsonKey(fromJson: flexibleCsvStringList)
|
||||
final List<String>? mediaReviewsLanguages;
|
||||
|
||||
@override
|
||||
@@ -51,10 +62,31 @@ class PlexUserProfile implements MediaServerUserProfile {
|
||||
this.mediaReviewsLanguages,
|
||||
});
|
||||
|
||||
/// Neutral fallback matching the generated defaults — used when the account
|
||||
/// API returns a profile blob that cannot be parsed at all (schema drift
|
||||
/// must never break token minting, see UserSwitchResponse.fromJson).
|
||||
factory PlexUserProfile.defaults() => PlexUserProfile(
|
||||
autoSelectAudio: true,
|
||||
defaultAudioAccessibility: 0,
|
||||
autoSelectSubtitle: 0,
|
||||
defaultSubtitleAccessibility: 0,
|
||||
defaultSubtitleForced: 1,
|
||||
watchedIndicator: 1,
|
||||
mediaReviewsVisibility: 0,
|
||||
);
|
||||
|
||||
factory PlexUserProfile.fromJson(Map<String, dynamic> json) {
|
||||
final profile = json['profile'] as Map<String, dynamic>? ?? json;
|
||||
final envelope = json['profile'];
|
||||
final profile = envelope is Map<String, dynamic> ? envelope : json;
|
||||
return _$PlexUserProfileFromJson(profile);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() => {'profile': _$PlexUserProfileToJson(this)};
|
||||
}
|
||||
|
||||
/// Singular language fields tolerate the inverse drift (array/CSV → first entry).
|
||||
String? _flexibleLanguage(Object? v) => flexibleCsvStringList(v)?.first;
|
||||
|
||||
bool _boolOrTrue(Object? v) => flexibleBoolNullable(v) ?? true;
|
||||
int _intOr0(Object? v) => flexibleInt(v) ?? 0;
|
||||
int _intOr1(Object? v) => flexibleInt(v) ?? 1;
|
||||
|
||||
@@ -9,27 +9,20 @@ part of 'plex_user_profile.dart';
|
||||
PlexUserProfile _$PlexUserProfileFromJson(
|
||||
Map<String, dynamic> json,
|
||||
) => PlexUserProfile(
|
||||
autoSelectAudio: json['autoSelectAudio'] as bool? ?? true,
|
||||
defaultAudioAccessibility:
|
||||
(json['defaultAudioAccessibility'] as num?)?.toInt() ?? 0,
|
||||
defaultAudioLanguage: json['defaultAudioLanguage'] as String?,
|
||||
defaultAudioLanguages: (json['defaultAudioLanguages'] as List<dynamic>?)
|
||||
?.map((e) => e as String)
|
||||
.toList(),
|
||||
defaultSubtitleLanguage: json['defaultSubtitleLanguage'] as String?,
|
||||
defaultSubtitleLanguages: (json['defaultSubtitleLanguages'] as List<dynamic>?)
|
||||
?.map((e) => e as String)
|
||||
.toList(),
|
||||
autoSelectSubtitle: (json['autoSelectSubtitle'] as num?)?.toInt() ?? 0,
|
||||
defaultSubtitleAccessibility:
|
||||
(json['defaultSubtitleAccessibility'] as num?)?.toInt() ?? 0,
|
||||
defaultSubtitleForced: (json['defaultSubtitleForced'] as num?)?.toInt() ?? 1,
|
||||
watchedIndicator: (json['watchedIndicator'] as num?)?.toInt() ?? 1,
|
||||
mediaReviewsVisibility:
|
||||
(json['mediaReviewsVisibility'] as num?)?.toInt() ?? 0,
|
||||
mediaReviewsLanguages: (json['mediaReviewsLanguages'] as List<dynamic>?)
|
||||
?.map((e) => e as String)
|
||||
.toList(),
|
||||
autoSelectAudio: _boolOrTrue(json['autoSelectAudio']),
|
||||
defaultAudioAccessibility: _intOr0(json['defaultAudioAccessibility']),
|
||||
defaultAudioLanguage: _flexibleLanguage(json['defaultAudioLanguage']),
|
||||
defaultAudioLanguages: flexibleCsvStringList(json['defaultAudioLanguages']),
|
||||
defaultSubtitleLanguage: _flexibleLanguage(json['defaultSubtitleLanguage']),
|
||||
defaultSubtitleLanguages: flexibleCsvStringList(
|
||||
json['defaultSubtitleLanguages'],
|
||||
),
|
||||
autoSelectSubtitle: _intOr0(json['autoSelectSubtitle']),
|
||||
defaultSubtitleAccessibility: _intOr0(json['defaultSubtitleAccessibility']),
|
||||
defaultSubtitleForced: _intOr1(json['defaultSubtitleForced']),
|
||||
watchedIndicator: _intOr1(json['watchedIndicator']),
|
||||
mediaReviewsVisibility: _intOr0(json['mediaReviewsVisibility']),
|
||||
mediaReviewsLanguages: flexibleCsvStringList(json['mediaReviewsLanguages']),
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$PlexUserProfileToJson(PlexUserProfile instance) =>
|
||||
|
||||
Reference in New Issue
Block a user