From 4436efea72c68bb5bd64be8e684675da936b29e8 Mon Sep 17 00:00:00 2001 From: edde746 <86283021+edde746@users.noreply.github.com> Date: Mon, 17 Aug 2026 01:49:03 +0200 Subject: [PATCH] refactor(models): drop parse-only wire fields nothing reads Four model surfaces parsed, coerced, and re-serialized data with zero readers: PlayQueueResponse's selectedItemIndex/offset/sourceURI/version (version stays as a parse-time validity gate), PlexUserProfile's seven non-track-selection account fields, MediaSubscriptionCreateRequest's hints/params/providers, and the whole CatalogPlayState pipeline (parsed by the Plex catalog source, merged and round-tripped on every CatalogItem, read by nobody). --- lib/models/catalog/catalog_item.dart | 7 ------ lib/models/catalog/catalog_metadata.dart | 24 ------------------- lib/models/media_subscription.dart | 6 ----- lib/models/plex/play_queue_response.dart | 13 ---------- lib/models/plex/plex_user_profile.dart | 22 ----------------- lib/models/plex/plex_user_profile.g.dart | 18 -------------- lib/services/catalog/plex_catalog_source.dart | 6 ----- .../plex_client/parts/play_queues.dart | 4 ---- test/models/plex_user_profile_test.dart | 20 ++-------------- .../catalog/plex_catalog_source_test.dart | 14 ++--------- test/services/play_queue_launcher_test.dart | 1 - .../plex_client_http_contract_test.dart | 3 --- .../track_selection_service_test.dart | 7 ------ 13 files changed, 4 insertions(+), 141 deletions(-) diff --git a/lib/models/catalog/catalog_item.dart b/lib/models/catalog/catalog_item.dart index 2fbebd49d..f0caf140d 100644 --- a/lib/models/catalog/catalog_item.dart +++ b/lib/models/catalog/catalog_item.dart @@ -313,9 +313,6 @@ class CatalogItem { /// answer different questions and neither can be derived from the other. final double? recommendationPercent; - /// Play state the catalog provider itself reports. - final CatalogPlayState? playState; - /// Provider search relevance. Ordering input only — never rendered. final double? relevance; @@ -381,7 +378,6 @@ class CatalogItem { this.isAdult, this.recommendationCount, this.recommendationPercent, - this.playState, this.relevance, this.background, this.cast, @@ -447,7 +443,6 @@ class CatalogItem { budget: detail.budget ?? budget, revenue: detail.revenue ?? revenue, isAdult: detail.isAdult ?? isAdult, - playState: detail.playState ?? playState, unairedEpisodeCount: detail.unairedEpisodeCount ?? unairedEpisodeCount, // Row-only context: a detail endpoint cannot know these. ranks: ranks ?? detail.ranks, @@ -556,7 +551,6 @@ class CatalogItem { if (isAdult != null) 'isAdult': isAdult, if (recommendationCount != null) 'recommendationCount': recommendationCount, if (recommendationPercent != null) 'recommendationPercent': recommendationPercent, - if (playState != null) 'playState': playState!.toJson(), if (relevance != null) 'relevance': relevance, if (background != null) 'background': background, if (cast != null) 'cast': [for (final c in cast!) c.toJson()], @@ -624,7 +618,6 @@ class CatalogItem { isAdult: json['isAdult'] as bool?, recommendationCount: json['recommendationCount'] as int?, recommendationPercent: (json['recommendationPercent'] as num?)?.toDouble(), - playState: _decodeObject(json['playState'], CatalogPlayState.fromJson), unairedEpisodeCount: json['unairedEpisodeCount'] as int?, recommenders: decodeCatalogList(json['recommenders'], CatalogRecommender.fromJson), relevance: (json['relevance'] as num?)?.toDouble(), diff --git a/lib/models/catalog/catalog_metadata.dart b/lib/models/catalog/catalog_metadata.dart index 120c7b5d8..223d7b32b 100644 --- a/lib/models/catalog/catalog_metadata.dart +++ b/lib/models/catalog/catalog_metadata.dart @@ -405,30 +405,6 @@ class CatalogLink { } } -/// Play state the catalog provider itself knows about (Plex Discover -/// `includeUserState=1`). Independent of any connected media server's state. -class CatalogPlayState { - final int? viewCount; - final int? viewOffsetMs; - final int? viewedLeafCount; - - const CatalogPlayState({this.viewCount, this.viewOffsetMs, this.viewedLeafCount}); - - bool get isEmpty => viewCount == null && viewOffsetMs == null && viewedLeafCount == null; - - Map toJson() => { - if (viewCount != null) 'viewCount': viewCount, - if (viewOffsetMs != null) 'viewOffsetMs': viewOffsetMs, - if (viewedLeafCount != null) 'viewedLeafCount': viewedLeafCount, - }; - - factory CatalogPlayState.fromJson(Map json) => CatalogPlayState( - viewCount: json['viewCount'] as int?, - viewOffsetMs: json['viewOffsetMs'] as int?, - viewedLeafCount: json['viewedLeafCount'] as int?, - ); -} - /// Decodes a JSON list of objects into value objects, dropping malformed /// siblings rather than failing the whole item — catalog payloads are /// best-effort API data, not a strict persisted discriminator. diff --git a/lib/models/media_subscription.dart b/lib/models/media_subscription.dart index 317e30bf3..a4f80403c 100644 --- a/lib/models/media_subscription.dart +++ b/lib/models/media_subscription.dart @@ -146,20 +146,14 @@ class MediaSubscriptionCreateRequest { final int? targetSectionLocationID; final int? type; final String? parameters; - final Map hints; final Map prefs; - final Map params; - final String? providers; const MediaSubscriptionCreateRequest({ this.targetLibrarySectionID, this.targetSectionLocationID, this.type, this.parameters, - this.hints = const {}, this.prefs = const {}, - this.params = const {}, - this.providers, }); factory MediaSubscriptionCreateRequest.fromTemplate( diff --git a/lib/models/plex/play_queue_response.dart b/lib/models/plex/play_queue_response.dart index 53c299a23..a001a3772 100644 --- a/lib/models/plex/play_queue_response.dart +++ b/lib/models/plex/play_queue_response.dart @@ -5,24 +5,16 @@ import '../../media/media_item.dart'; class PlayQueueResponse { final int playQueueID; final int? playQueueSelectedItemID; - final int? playQueueSelectedItemOffset; - final String? playQueueSelectedMetadataItemID; final bool playQueueShuffled; - final String? playQueueSourceURI; final int? playQueueTotalCount; - final int playQueueVersion; final int? size; // Number of items in this response window final List? items; PlayQueueResponse({ required this.playQueueID, this.playQueueSelectedItemID, - this.playQueueSelectedItemOffset, - this.playQueueSelectedMetadataItemID, required this.playQueueShuffled, - this.playQueueSourceURI, required this.playQueueTotalCount, - required this.playQueueVersion, this.size, this.items, }); @@ -37,9 +29,4 @@ class PlayQueueResponse { return null; } } - - int? get selectedItemIndex { - if (items == null || playQueueSelectedItemID == null) return null; - return items!.indexWhere((item) => item is PlexMediaItem && item.playQueueItemId == playQueueSelectedItemID); - } } diff --git a/lib/models/plex/plex_user_profile.dart b/lib/models/plex/plex_user_profile.dart index 88773fa7b..1efa10d85 100644 --- a/lib/models/plex/plex_user_profile.dart +++ b/lib/models/plex/plex_user_profile.dart @@ -16,8 +16,6 @@ class PlexUserProfile implements MediaServerUserProfile { @JsonKey(fromJson: _boolOrTrue) @override final bool autoSelectAudio; - @JsonKey(fromJson: flexibleIntOrZero) - final int defaultAudioAccessibility; @JsonKey(fromJson: _flexibleLanguage) @override final String? defaultAudioLanguage; @@ -30,35 +28,16 @@ class PlexUserProfile implements MediaServerUserProfile { @JsonKey(fromJson: flexibleCsvStringList) @override final List? defaultSubtitleLanguages; - @JsonKey(fromJson: flexibleIntOrZero) - final int autoSelectSubtitle; - @JsonKey(fromJson: flexibleIntOrZero) - final int defaultSubtitleAccessibility; - @JsonKey(fromJson: _intOr1) - final int defaultSubtitleForced; - @JsonKey(fromJson: _intOr1) - final int watchedIndicator; - @JsonKey(fromJson: flexibleIntOrZero) - final int mediaReviewsVisibility; - @JsonKey(fromJson: flexibleCsvStringList) - final List? mediaReviewsLanguages; @override SubtitlePlaybackMode? get subtitleMode => null; PlexUserProfile({ required this.autoSelectAudio, - required this.defaultAudioAccessibility, this.defaultAudioLanguage, this.defaultAudioLanguages, this.defaultSubtitleLanguage, this.defaultSubtitleLanguages, - required this.autoSelectSubtitle, - required this.defaultSubtitleAccessibility, - required this.defaultSubtitleForced, - required this.watchedIndicator, - required this.mediaReviewsVisibility, - this.mediaReviewsLanguages, }); factory PlexUserProfile.fromJson(Map json) { @@ -74,4 +53,3 @@ class PlexUserProfile implements MediaServerUserProfile { String? _flexibleLanguage(Object? v) => flexibleCsvStringList(v)?.first; bool _boolOrTrue(Object? v) => flexibleBoolNullable(v) ?? true; -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 f309a78bf..958034b6c 100644 --- a/lib/models/plex/plex_user_profile.g.dart +++ b/lib/models/plex/plex_user_profile.g.dart @@ -10,37 +10,19 @@ PlexUserProfile _$PlexUserProfileFromJson( Map json, ) => PlexUserProfile( autoSelectAudio: _boolOrTrue(json['autoSelectAudio']), - defaultAudioAccessibility: flexibleIntOrZero( - json['defaultAudioAccessibility'], - ), defaultAudioLanguage: _flexibleLanguage(json['defaultAudioLanguage']), defaultAudioLanguages: flexibleCsvStringList(json['defaultAudioLanguages']), defaultSubtitleLanguage: _flexibleLanguage(json['defaultSubtitleLanguage']), defaultSubtitleLanguages: flexibleCsvStringList( json['defaultSubtitleLanguages'], ), - autoSelectSubtitle: flexibleIntOrZero(json['autoSelectSubtitle']), - defaultSubtitleAccessibility: flexibleIntOrZero( - json['defaultSubtitleAccessibility'], - ), - defaultSubtitleForced: _intOr1(json['defaultSubtitleForced']), - watchedIndicator: _intOr1(json['watchedIndicator']), - mediaReviewsVisibility: flexibleIntOrZero(json['mediaReviewsVisibility']), - mediaReviewsLanguages: flexibleCsvStringList(json['mediaReviewsLanguages']), ); Map _$PlexUserProfileToJson(PlexUserProfile instance) => { 'autoSelectAudio': instance.autoSelectAudio, - 'defaultAudioAccessibility': instance.defaultAudioAccessibility, 'defaultAudioLanguage': instance.defaultAudioLanguage, 'defaultAudioLanguages': instance.defaultAudioLanguages, 'defaultSubtitleLanguage': instance.defaultSubtitleLanguage, 'defaultSubtitleLanguages': instance.defaultSubtitleLanguages, - 'autoSelectSubtitle': instance.autoSelectSubtitle, - 'defaultSubtitleAccessibility': instance.defaultSubtitleAccessibility, - 'defaultSubtitleForced': instance.defaultSubtitleForced, - 'watchedIndicator': instance.watchedIndicator, - 'mediaReviewsVisibility': instance.mediaReviewsVisibility, - 'mediaReviewsLanguages': instance.mediaReviewsLanguages, }; diff --git a/lib/services/catalog/plex_catalog_source.dart b/lib/services/catalog/plex_catalog_source.dart index 3952516df..93c2fd3a7 100644 --- a/lib/services/catalog/plex_catalog_source.dart +++ b/lib/services/catalog/plex_catalog_source.dart @@ -231,11 +231,6 @@ class PlexCatalogSource with CatalogWatchlistMachinery implements CatalogSource, final nextAirDate = kind == MediaKind.show ? _date(metadata['nextEpisodeOriginallyAvailableAt']) ?? _date(metadata['nextSeasonOriginallyAvailableAt']) : null; - final playState = CatalogPlayState( - viewCount: flexibleInt(metadata['viewCount']), - viewOffsetMs: flexibleInt(metadata['viewOffset']), - viewedLeafCount: flexibleInt(metadata['viewedLeafCount']), - ); String? coverPoster; String? coverArt; @@ -301,7 +296,6 @@ class PlexCatalogSource with CatalogWatchlistMachinery implements CatalogSource, contentAdvisory: _contentAdvisoryFor(metadata), budget: flexibleInt(metadata['budget']), revenue: flexibleInt(metadata['revenue']), - playState: playState.isEmpty ? null : playState, relevance: relevance, ); } diff --git a/lib/services/plex_client/parts/play_queues.dart b/lib/services/plex_client/parts/play_queues.dart index 795461cb6..ad24db52f 100644 --- a/lib/services/plex_client/parts/play_queues.dart +++ b/lib/services/plex_client/parts/play_queues.dart @@ -36,12 +36,8 @@ mixin _PlexPlayQueueMethods on _PlexClientInternals { return PlayQueueResponse( playQueueID: playQueueID, playQueueSelectedItemID: flexibleInt(container['playQueueSelectedItemID']), - playQueueSelectedItemOffset: flexibleInt(container['playQueueSelectedItemOffset']), - playQueueSelectedMetadataItemID: container['playQueueSelectedMetadataItemID'] as String?, playQueueShuffled: flexibleBool(container['playQueueShuffled']), - playQueueSourceURI: container['playQueueSourceURI'] as String?, playQueueTotalCount: flexibleInt(container['playQueueTotalCount']), - playQueueVersion: playQueueVersion, size: flexibleInt(container['size']), items: items, ); diff --git a/test/models/plex_user_profile_test.dart b/test/models/plex_user_profile_test.dart index 5c5a2c265..c1a5207fd 100644 --- a/test/models/plex_user_profile_test.dart +++ b/test/models/plex_user_profile_test.dart @@ -7,24 +7,17 @@ void main() { final profile = PlexUserProfile.fromJson({ 'defaultAudioLanguages': ['en', 'sv'], 'defaultSubtitleLanguages': ['sv'], - 'mediaReviewsLanguages': ['en'], }); expect(profile.defaultAudioLanguages, ['en', 'sv']); expect(profile.defaultSubtitleLanguages, ['sv']); - expect(profile.mediaReviewsLanguages, ['en']); }); test('parses the July 2026 CSV string shape for language lists (#1488)', () { - final profile = PlexUserProfile.fromJson({ - 'defaultAudioLanguages': 'en,sv', - 'defaultSubtitleLanguages': 'en,sv', - 'mediaReviewsLanguages': 'en', - }); + final profile = PlexUserProfile.fromJson({'defaultAudioLanguages': 'en,sv', 'defaultSubtitleLanguages': 'en,sv'}); expect(profile.defaultAudioLanguages, ['en', 'sv']); expect(profile.defaultSubtitleLanguages, ['en', 'sv']); - expect(profile.mediaReviewsLanguages, ['en']); }); test('parses absent and null language lists as null', () { @@ -32,7 +25,6 @@ void main() { expect(profile.defaultAudioLanguages, isNull); expect(profile.defaultSubtitleLanguages, isNull); - expect(profile.mediaReviewsLanguages, isNull); }); test('unwraps the profile envelope and tolerates a non-map envelope', () { @@ -58,17 +50,9 @@ void main() { }); test('scalar fields coerce from drifted types', () { - final profile = PlexUserProfile.fromJson({ - 'autoSelectAudio': 0, - 'autoSelectSubtitle': '1', - 'watchedIndicator': '2', - 'defaultSubtitleForced': {}, - }); + final profile = PlexUserProfile.fromJson({'autoSelectAudio': 0}); expect(profile.autoSelectAudio, isFalse); - expect(profile.autoSelectSubtitle, 1); - expect(profile.watchedIndicator, 2); - expect(profile.defaultSubtitleForced, 1); }); }); } diff --git a/test/services/catalog/plex_catalog_source_test.dart b/test/services/catalog/plex_catalog_source_test.dart index e1a2aa3fb..5f5d00595 100644 --- a/test/services/catalog/plex_catalog_source_test.dart +++ b/test/services/catalog/plex_catalog_source_test.dart @@ -168,7 +168,6 @@ void main() { expect(absent.rating, isNull); expect(absent.ratings, isNull); expect(absent.releaseDate, isNull); - expect(absent.playState, isNull); expect(absent.posterVariants, isNull); expect(absent.backdropVariants, isNull); }); @@ -221,14 +220,7 @@ void main() { case '/hubs/sections/home/platforms': return jsonResponse({ 'MediaContainer': { - 'Metadata': [ - { - ..._metadata(ratingKey: 'platform-1', title: 'A Platform Title'), - 'viewCount': 2, - 'viewOffset': 12345, - 'viewedLeafCount': 7, - }, - ], + 'Metadata': [_metadata(ratingKey: 'platform-1', title: 'A Platform Title')], }, }); case '/hubs/sections/home/chris-nolan': @@ -277,9 +269,7 @@ void main() { expect(show.endDate, isNull); final platformItem = hubs[1].page.items.single; - expect(platformItem.playState?.viewCount, 2); - expect(platformItem.playState?.viewOffsetMs, 12345); - expect(platformItem.playState?.viewedLeafCount, 7); + expect(platformItem.title, 'A Platform Title'); expect(hubs.last.page.items.single.title, 'The Prestige'); expect(hubs.last.page.hasMore, isFalse); }); diff --git a/test/services/play_queue_launcher_test.dart b/test/services/play_queue_launcher_test.dart index 8b0b5fce9..a431e3d59 100644 --- a/test/services/play_queue_launcher_test.dart +++ b/test/services/play_queue_launcher_test.dart @@ -65,7 +65,6 @@ PlayQueueResponse _queueWith(MediaItem item) { playQueueSelectedItemID: 41, playQueueShuffled: false, playQueueTotalCount: 1, - playQueueVersion: 1, items: [item], ); } diff --git a/test/services/plex_client_http_contract_test.dart b/test/services/plex_client_http_contract_test.dart index 3909529aa..e93399c74 100644 --- a/test/services/plex_client_http_contract_test.dart +++ b/test/services/plex_client_http_contract_test.dart @@ -442,7 +442,6 @@ void main() { 'MediaContainer': { 'playQueueID': '42', 'playQueueSelectedItemID': '7', - 'playQueueSelectedItemOffset': '1', 'playQueueTotalCount': '3', 'playQueueVersion': '5', 'size': '3', @@ -459,9 +458,7 @@ void main() { expect(queue?.playQueueID, 42); expect(queue?.playQueueSelectedItemID, 7); - expect(queue?.playQueueSelectedItemOffset, 1); expect(queue?.playQueueTotalCount, 3); - expect(queue?.playQueueVersion, 5); expect(queue?.size, 3); }); diff --git a/test/services/track_selection_service_test.dart b/test/services/track_selection_service_test.dart index f11ce3157..5bbc12ff1 100644 --- a/test/services/track_selection_service_test.dart +++ b/test/services/track_selection_service_test.dart @@ -30,20 +30,13 @@ PlexUserProfile _profile({ List? defaultAudioLanguages, String? defaultSubtitleLanguage, List? defaultSubtitleLanguages, - int autoSelectSubtitle = 0, }) { return PlexUserProfile( autoSelectAudio: autoSelectAudio, - defaultAudioAccessibility: 0, defaultAudioLanguage: defaultAudioLanguage, defaultAudioLanguages: defaultAudioLanguages, defaultSubtitleLanguage: defaultSubtitleLanguage, defaultSubtitleLanguages: defaultSubtitleLanguages, - autoSelectSubtitle: autoSelectSubtitle, - defaultSubtitleAccessibility: 0, - defaultSubtitleForced: 1, - watchedIndicator: 1, - mediaReviewsVisibility: 0, ); }