refactor(json): share scalar adapters

This commit is contained in:
edde746
2026-07-12 08:42:21 +02:00
parent 0ddd0a6c25
commit f08fceaee0
18 changed files with 49 additions and 38 deletions
+1 -3
View File
@@ -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();
+1 -1
View File
@@ -7,7 +7,7 @@ part of 'media_part.dart';
// **************************************************************************
MediaPart _$MediaPartFromJson(Map<String, dynamic> json) => MediaPart(
id: _stringFromJson(json['id']),
id: stringOrEmpty(json['id']),
streamPath: json['streamPath'] as String?,
file: json['file'] as String?,
sizeBytes: flexibleInt(json['sizeBytes']),
+3 -3
View File
@@ -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<String, dynamic> toJson() => _$MediaRoleToJson(this);
}
String _stringFromJson(Object? raw) => (raw ?? '').toString();
+1 -1
View File
@@ -8,7 +8,7 @@ part of 'media_role.dart';
MediaRole _$MediaRoleFromJson(Map<String, dynamic> 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?,
);
+1 -2
View File
@@ -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<MediaPart> _partsFromJson(Object? raw) {
return raw is List
+1 -1
View File
@@ -7,7 +7,7 @@ part of 'media_version.dart';
// **************************************************************************
MediaVersion _$MediaVersionFromJson(Map<String, dynamic> json) => MediaVersion(
id: _stringFromJson(json['id']),
id: stringOrEmpty(json['id']),
width: flexibleInt(json['width']),
height: flexibleInt(json['height']),
videoResolution: json['videoResolution'] as String?,
+1 -3
View File
@@ -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;
+1 -1
View File
@@ -7,7 +7,7 @@ part of 'plex_home.dart';
// **************************************************************************
PlexHome _$PlexHomeFromJson(Map<String, dynamic> 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? ?? '',
+1 -3
View File
@@ -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;
+1 -1
View File
@@ -7,7 +7,7 @@ part of 'plex_home_user.dart';
// **************************************************************************
PlexHomeUser _$PlexHomeUserFromJson(Map<String, dynamic> 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?,
@@ -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;
@@ -9,7 +9,7 @@ part of 'plex_subtitle_search_result.dart';
PlexSubtitleSearchResult _$PlexSubtitleSearchResultFromJson(
Map<String, dynamic> 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?,
+4 -5
View File
@@ -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<String>? 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<String>? 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;
+8 -4
View File
@@ -10,18 +10,22 @@ PlexUserProfile _$PlexUserProfileFromJson(
Map<String, dynamic> 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']),
);
+2 -4
View File
@@ -48,8 +48,6 @@ Map<String, dynamic> _obfuscatePlaylistJson(Map<String, dynamic> 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;
+2 -2
View File
@@ -18,7 +18,7 @@ PlexRoleDto _$PlexRoleDtoFromJson(Map<String, dynamic> json) => PlexRoleDto(
PlexMediaVersionDto _$PlexMediaVersionDtoFromJson(Map<String, dynamic> 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<String, dynamic> 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')),
);
+4
View File
@@ -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.
+15
View File
@@ -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);