diff --git a/lib/media/media_part.dart b/lib/media/media_part.dart index 6e28d386..f764e6ff 100644 --- a/lib/media/media_part.dart +++ b/lib/media/media_part.dart @@ -11,7 +11,7 @@ part 'media_part.g.dart'; @JsonSerializable(includeIfNull: false) class MediaPart { /// Backend-opaque part identifier. - @JsonKey(fromJson: _stringFromJson) + @JsonKey(fromJson: stringOrEmpty) final String id; /// Backend-specific path used to construct a direct stream URL — e.g. Plex's @@ -58,5 +58,3 @@ class MediaPart { /// metadata request includes `checkFiles=1`. bool get isPlayable => accessible != false && exists != false; } - -String _stringFromJson(Object? raw) => (raw ?? '').toString(); diff --git a/lib/media/media_part.g.dart b/lib/media/media_part.g.dart index f154d6e5..42a2cf5f 100644 --- a/lib/media/media_part.g.dart +++ b/lib/media/media_part.g.dart @@ -7,7 +7,7 @@ part of 'media_part.dart'; // ************************************************************************** MediaPart _$MediaPartFromJson(Map json) => MediaPart( - id: _stringFromJson(json['id']), + id: stringOrEmpty(json['id']), streamPath: json['streamPath'] as String?, file: json['file'] as String?, sizeBytes: flexibleInt(json['sizeBytes']), diff --git a/lib/media/media_role.dart b/lib/media/media_role.dart index 7e0ec8a2..57b27d48 100644 --- a/lib/media/media_role.dart +++ b/lib/media/media_role.dart @@ -1,12 +1,14 @@ import 'package:json_annotation/json_annotation.dart'; +import '../utils/json_utils.dart'; + part 'media_role.g.dart'; /// A cast or crew member attached to a media item. @JsonSerializable(includeIfNull: false) class MediaRole { final String? id; - @JsonKey(fromJson: _stringFromJson) + @JsonKey(fromJson: stringOrEmpty) final String tag; final String? role; final String? thumbPath; @@ -17,5 +19,3 @@ class MediaRole { Map toJson() => _$MediaRoleToJson(this); } - -String _stringFromJson(Object? raw) => (raw ?? '').toString(); diff --git a/lib/media/media_role.g.dart b/lib/media/media_role.g.dart index 8a87d731..42a0d8e5 100644 --- a/lib/media/media_role.g.dart +++ b/lib/media/media_role.g.dart @@ -8,7 +8,7 @@ part of 'media_role.dart'; MediaRole _$MediaRoleFromJson(Map json) => MediaRole( id: json['id'] as String?, - tag: _stringFromJson(json['tag']), + tag: stringOrEmpty(json['tag']), role: json['role'] as String?, thumbPath: json['thumbPath'] as String?, ); diff --git a/lib/media/media_version.dart b/lib/media/media_version.dart index e035bf47..844a97ff 100644 --- a/lib/media/media_version.dart +++ b/lib/media/media_version.dart @@ -28,7 +28,7 @@ String _videoResolutionDisplayLabel(String resolution) { @JsonSerializable(includeIfNull: false, explicitToJson: true) class MediaVersion { /// Backend-opaque version identifier. - @JsonKey(fromJson: _stringFromJson) + @JsonKey(fromJson: stringOrEmpty) final String id; @JsonKey(fromJson: flexibleInt) final int? width; @@ -140,7 +140,6 @@ class MediaVersion { } } -String _stringFromJson(Object? raw) => (raw ?? '').toString(); List _partsFromJson(Object? raw) { return raw is List diff --git a/lib/media/media_version.g.dart b/lib/media/media_version.g.dart index d09885d2..cc686d16 100644 --- a/lib/media/media_version.g.dart +++ b/lib/media/media_version.g.dart @@ -7,7 +7,7 @@ part of 'media_version.dart'; // ************************************************************************** MediaVersion _$MediaVersionFromJson(Map json) => MediaVersion( - id: _stringFromJson(json['id']), + id: stringOrEmpty(json['id']), width: flexibleInt(json['width']), height: flexibleInt(json['height']), videoResolution: json['videoResolution'] as String?, diff --git a/lib/models/plex/plex_home.dart b/lib/models/plex/plex_home.dart index dcccfb8f..69924b7b 100644 --- a/lib/models/plex/plex_home.dart +++ b/lib/models/plex/plex_home.dart @@ -7,7 +7,7 @@ part 'plex_home.g.dart'; @JsonSerializable() class PlexHome { - @JsonKey(fromJson: _intOr0) + @JsonKey(fromJson: flexibleIntOrZero) final int id; @JsonKey(readValue: readStringField, defaultValue: '') final String name; @@ -52,5 +52,3 @@ class PlexHome { bool get hasMultipleUsers => users.length > 1; } - -int _intOr0(Object? value) => flexibleInt(value) ?? 0; diff --git a/lib/models/plex/plex_home.g.dart b/lib/models/plex/plex_home.g.dart index 8bad821b..79b24f08 100644 --- a/lib/models/plex/plex_home.g.dart +++ b/lib/models/plex/plex_home.g.dart @@ -7,7 +7,7 @@ part of 'plex_home.dart'; // ************************************************************************** PlexHome _$PlexHomeFromJson(Map json) => PlexHome( - id: _intOr0(json['id']), + id: flexibleIntOrZero(json['id']), name: readStringField(json, 'name') as String? ?? '', guestUserID: flexibleInt(json['guestUserID']), guestUserUUID: readStringField(json, 'guestUserUUID') as String? ?? '', diff --git a/lib/models/plex/plex_home_user.dart b/lib/models/plex/plex_home_user.dart index 89d7cc14..80d0ebfb 100644 --- a/lib/models/plex/plex_home_user.dart +++ b/lib/models/plex/plex_home_user.dart @@ -9,7 +9,7 @@ part 'plex_home_user.g.dart'; /// coerce tolerantly instead of hard-casting. @JsonSerializable() class PlexHomeUser { - @JsonKey(fromJson: _intOr0) + @JsonKey(fromJson: flexibleIntOrZero) final int id; @JsonKey(readValue: readStringField, defaultValue: '') final String uuid; @@ -63,5 +63,3 @@ class PlexHomeUser { bool get isGuestUser => guest; bool get requiresPassword => protected; } - -int _intOr0(Object? v) => flexibleInt(v) ?? 0; diff --git a/lib/models/plex/plex_home_user.g.dart b/lib/models/plex/plex_home_user.g.dart index 94a011ac..7994c978 100644 --- a/lib/models/plex/plex_home_user.g.dart +++ b/lib/models/plex/plex_home_user.g.dart @@ -7,7 +7,7 @@ part of 'plex_home_user.dart'; // ************************************************************************** PlexHomeUser _$PlexHomeUserFromJson(Map json) => PlexHomeUser( - id: _intOr0(json['id']), + id: flexibleIntOrZero(json['id']), uuid: readStringField(json, 'uuid') as String? ?? '', title: readStringField(json, 'title') as String? ?? 'Unknown', username: readStringField(json, 'username') as String?, diff --git a/lib/models/plex/plex_subtitle_search_result.dart b/lib/models/plex/plex_subtitle_search_result.dart index a7d4ab85..e653070e 100644 --- a/lib/models/plex/plex_subtitle_search_result.dart +++ b/lib/models/plex/plex_subtitle_search_result.dart @@ -4,11 +4,9 @@ import '../../utils/json_utils.dart'; part 'plex_subtitle_search_result.g.dart'; -int _flexibleIntOrZero(Object? v) => flexibleInt(v) ?? 0; - @JsonSerializable() class PlexSubtitleSearchResult { - @JsonKey(fromJson: _flexibleIntOrZero) + @JsonKey(fromJson: flexibleIntOrZero) final int id; @JsonKey(readValue: readStringField, defaultValue: '') final String key; diff --git a/lib/models/plex/plex_subtitle_search_result.g.dart b/lib/models/plex/plex_subtitle_search_result.g.dart index 20fd2b45..81a89216 100644 --- a/lib/models/plex/plex_subtitle_search_result.g.dart +++ b/lib/models/plex/plex_subtitle_search_result.g.dart @@ -9,7 +9,7 @@ part of 'plex_subtitle_search_result.dart'; PlexSubtitleSearchResult _$PlexSubtitleSearchResultFromJson( Map json, ) => PlexSubtitleSearchResult( - id: _flexibleIntOrZero(json['id']), + id: flexibleIntOrZero(json['id']), key: readStringField(json, 'key') as String? ?? '', codec: readStringField(json, 'codec') as String?, language: readStringField(json, 'language') as String?, diff --git a/lib/models/plex/plex_user_profile.dart b/lib/models/plex/plex_user_profile.dart index 1487d1a2..9e19b3da 100644 --- a/lib/models/plex/plex_user_profile.dart +++ b/lib/models/plex/plex_user_profile.dart @@ -17,7 +17,7 @@ class PlexUserProfile implements MediaServerUserProfile { @JsonKey(fromJson: _boolOrTrue) @override final bool autoSelectAudio; - @JsonKey(fromJson: _intOr0) + @JsonKey(fromJson: flexibleIntOrZero) final int defaultAudioAccessibility; @JsonKey(fromJson: _flexibleLanguage) @override @@ -31,15 +31,15 @@ class PlexUserProfile implements MediaServerUserProfile { @JsonKey(fromJson: flexibleCsvStringList) @override final List? defaultSubtitleLanguages; - @JsonKey(fromJson: _intOr0) + @JsonKey(fromJson: flexibleIntOrZero) final int autoSelectSubtitle; - @JsonKey(fromJson: _intOr0) + @JsonKey(fromJson: flexibleIntOrZero) final int defaultSubtitleAccessibility; @JsonKey(fromJson: _intOr1) final int defaultSubtitleForced; @JsonKey(fromJson: _intOr1) final int watchedIndicator; - @JsonKey(fromJson: _intOr0) + @JsonKey(fromJson: flexibleIntOrZero) final int mediaReviewsVisibility; @JsonKey(fromJson: flexibleCsvStringList) final List? mediaReviewsLanguages; @@ -88,5 +88,4 @@ class PlexUserProfile implements MediaServerUserProfile { 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; diff --git a/lib/models/plex/plex_user_profile.g.dart b/lib/models/plex/plex_user_profile.g.dart index fafe0f7d..f309a78b 100644 --- a/lib/models/plex/plex_user_profile.g.dart +++ b/lib/models/plex/plex_user_profile.g.dart @@ -10,18 +10,22 @@ PlexUserProfile _$PlexUserProfileFromJson( Map json, ) => PlexUserProfile( autoSelectAudio: _boolOrTrue(json['autoSelectAudio']), - defaultAudioAccessibility: _intOr0(json['defaultAudioAccessibility']), + defaultAudioAccessibility: flexibleIntOrZero( + 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']), + autoSelectSubtitle: flexibleIntOrZero(json['autoSelectSubtitle']), + defaultSubtitleAccessibility: flexibleIntOrZero( + json['defaultSubtitleAccessibility'], + ), defaultSubtitleForced: _intOr1(json['defaultSubtitleForced']), watchedIndicator: _intOr1(json['watchedIndicator']), - mediaReviewsVisibility: _intOr0(json['mediaReviewsVisibility']), + mediaReviewsVisibility: flexibleIntOrZero(json['mediaReviewsVisibility']), mediaReviewsLanguages: flexibleCsvStringList(json['mediaReviewsLanguages']), ); diff --git a/lib/services/plex_mappers.dart b/lib/services/plex_mappers.dart index 8d59ba6c..233519ef 100644 --- a/lib/services/plex_mappers.dart +++ b/lib/services/plex_mappers.dart @@ -48,8 +48,6 @@ Map _obfuscatePlaylistJson(Map json) { return copy; } -int _flexibleIntOrZero(Object? v) => flexibleInt(v) ?? 0; - Map? _firstPartMap(Object? raw) { final parts = _partMaps(raw); return parts.isEmpty ? null : parts.first; @@ -307,7 +305,7 @@ class PlexRoleDto { @JsonSerializable(createToJson: false) class PlexMediaVersionDto { - @JsonKey(fromJson: _flexibleIntOrZero) + @JsonKey(fromJson: flexibleIntOrZero) final int id; @JsonKey(readValue: readStringField) final String? videoResolution; @@ -506,7 +504,7 @@ class PlexHubDto { @JsonKey(defaultValue: 'hub') final String type; final String? hubIdentifier; - @JsonKey(fromJson: _flexibleIntOrZero) + @JsonKey(fromJson: flexibleIntOrZero) final int size; @JsonKey(fromJson: flexibleBool) final bool more; diff --git a/lib/services/plex_mappers.g.dart b/lib/services/plex_mappers.g.dart index 7495e63e..ca421f38 100644 --- a/lib/services/plex_mappers.g.dart +++ b/lib/services/plex_mappers.g.dart @@ -18,7 +18,7 @@ PlexRoleDto _$PlexRoleDtoFromJson(Map json) => PlexRoleDto( PlexMediaVersionDto _$PlexMediaVersionDtoFromJson(Map json) => PlexMediaVersionDto( - id: _flexibleIntOrZero(json['id']), + id: flexibleIntOrZero(json['id']), videoResolution: readStringField(json, 'videoResolution') as String?, videoCodec: readStringField(json, 'videoCodec') as String?, bitrate: flexibleInt(json['bitrate']), @@ -73,7 +73,7 @@ PlexHubDto _$PlexHubDtoFromJson(Map json) => PlexHubDto( title: _hubTitleFromJson(json['title']), type: json['type'] as String? ?? 'hub', hubIdentifier: json['hubIdentifier'] as String?, - size: _flexibleIntOrZero(json['size']), + size: flexibleIntOrZero(json['size']), more: flexibleBool(json['more']), items: _hubItemsFromJson(_readHubItems(json, 'items')), ); diff --git a/lib/utils/json_utils.dart b/lib/utils/json_utils.dart index 76e835e6..476c0226 100644 --- a/lib/utils/json_utils.dart +++ b/lib/utils/json_utils.dart @@ -8,6 +8,10 @@ int? flexibleInt(Object? v) => switch (v) { _ => null, }; +int flexibleIntOrZero(Object? v) => flexibleInt(v) ?? 0; + +String stringOrEmpty(Object? v) => (v ?? '').toString(); + /// Parse a value that may be [bool], [int] (0/1), or [String] ('1'/'true'/'false') to [bool]. /// Returns `false` for `null` or unrecognised values. /// Handles Plex API responses where boolean fields may arrive as integers. diff --git a/test/utils/json_utils_test.dart b/test/utils/json_utils_test.dart index 4d67bad4..ba2e8b2d 100644 --- a/test/utils/json_utils_test.dart +++ b/test/utils/json_utils_test.dart @@ -33,6 +33,21 @@ void main() { }); }); + test('flexibleIntOrZero defaults unsupported values to zero', () { + expect(flexibleIntOrZero(3.9), 3); + expect(flexibleIntOrZero('42'), 42); + expect(flexibleIntOrZero(null), 0); + expect(flexibleIntOrZero('bad'), 0); + expect(flexibleIntOrZero(true), 0); + }); + + test('stringOrEmpty stringifies values and defaults null', () { + expect(stringOrEmpty('value'), 'value'); + expect(stringOrEmpty(42), '42'); + expect(stringOrEmpty(true), 'true'); + expect(stringOrEmpty(null), ''); + }); + group('flexibleBool', () { test('returns bool as-is', () { expect(flexibleBool(true), isTrue);