From 00a6e056b038588eb03937517e8e0e7d68651afc Mon Sep 17 00:00:00 2001 From: edde746 <86283021+edde746@users.noreply.github.com> Date: Mon, 17 Aug 2026 01:50:31 +0200 Subject: [PATCH] refactor(plex): drop the test-only raw-JSON mapper flavours and title obfuscation branches MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit PlexMappers advertised a parse-and-map FromJson flavour of every mapper 'for callers that haven't already parsed a DTO' — no such production caller exists; every call site uses the DTO-typed flavour. Tests now parse the DTO and map it, preserving every assertion. The kBlurArtwork JSON-walking branches in the DTO factories (title/summary vowel rotation on the decode hot path) are deleted with the flag's title half. --- lib/services/plex_mappers.dart | 66 ++------------- test/services/plex_mappers_test.dart | 81 ++++++++++++------- .../plex_rating_payload_shapes_test.dart | 9 ++- test/utils/media_quality_labels_test.dart | 8 +- 4 files changed, 74 insertions(+), 90 deletions(-) diff --git a/lib/services/plex_mappers.dart b/lib/services/plex_mappers.dart index c80ea4921..c8267425a 100644 --- a/lib/services/plex_mappers.dart +++ b/lib/services/plex_mappers.dart @@ -32,7 +32,6 @@ import '../media/media_version.dart'; import '../utils/app_logger.dart'; import '../utils/global_key_utils.dart'; import '../utils/json_utils.dart'; -import '../utils/obfuscation_utils.dart'; import 'file_info_parser.dart'; import 'plex_constants.dart'; @@ -42,14 +41,6 @@ part 'plex_mappers.g.dart'; /// `com.plexapp.agents.none://` and new-style `tv.plex.agents.none://`. const _unmatchedAgentMarker = 'agents.none://'; -Map _obfuscatePlaylistJson(Map json) { - final copy = Map.from(json); - for (final key in const ['title', 'summary']) { - if (copy[key] is String) copy[key] = obfuscateText(copy[key] as String); - } - return copy; -} - Map? _firstPartMap(Object? raw) { final parts = _partMaps(raw); return parts.isEmpty ? null : parts.first; @@ -240,7 +231,7 @@ List _mediaPartsFromReadValue(Object? raw) { String _hubTitleFromJson(Object? raw) { final title = raw as String? ?? t.common.unknown; - return kBlurArtwork ? obfuscateText(title) : title; + return title; } Object? _readHubItems(Map json, String _) { @@ -484,8 +475,7 @@ class PlexPlaylistDto { this.serverName, }); - factory PlexPlaylistDto.fromJson(Map json) => - _$PlexPlaylistDtoFromJson(kBlurArtwork ? _obfuscatePlaylistJson(json) : json); + factory PlexPlaylistDto.fromJson(Map json) => _$PlexPlaylistDtoFromJson(json); PlexPlaylistDto copyWith({ServerId? serverId, String? serverName}) { return PlexPlaylistDto( @@ -751,8 +741,7 @@ class PlexMetadataDto { this.flattenSeasons, }); - factory PlexMetadataDto.fromJson(Map rawJson) { - final json = kBlurArtwork ? _obfuscateJson(rawJson) : rawJson; + factory PlexMetadataDto.fromJson(Map json) { try { return _$PlexMetadataDtoFromJson(json); } on TypeError catch (e, st) { @@ -795,14 +784,6 @@ class PlexMetadataDto { return PlexMetadataDto.fromJson(enriched); } - static Map _obfuscateJson(Map json) { - final copy = Map.from(json); - for (final key in const ['title', 'summary', 'tagline', 'grandparentTitle', 'parentTitle', 'studio']) { - if (copy[key] is String) copy[key] = obfuscateText(copy[key] as String); - } - return copy; - } - String get globalKey => serverId != null ? buildGlobalKey(ServerId(serverId!), ratingKey) : ratingKey; bool get isUnmatched => guid == null || guid!.isEmpty || guid!.contains(_unmatchedAgentMarker); @@ -1038,12 +1019,11 @@ String? _nonEmptyRatingString(Object? value) { /// Pure JSON/DTO→neutral-type mappers for Plex. Mirrors [JellyfinMappers]. /// -/// Methods come in two flavours: -/// * `FromJson` — accept raw Plex JSON and parse + map in one step. -/// Used by tests and by callers that haven't already parsed a DTO. -/// * `` (DTO-typed) — accept an already-parsed DTO. Used by the -/// [PlexClient] which keeps a DTO step internally for caching, copying, -/// and OnDeck composition. +/// Methods accept already-parsed DTOs (`mediaItem`, `mediaHub`, …); the +/// [PlexClient] keeps the DTO step internally for caching, copying, and +/// OnDeck composition. A few raw-JSON helpers remain for callers without a +/// DTO surface: [mediaItemFromCacheJson] (offline cache), +/// [mediaVersionFromJson], and [displayCriteriaFromJson]. /// /// Pure: no HTTP, no client state, no token-aware image-URL resolution. /// Token-aware image URLs are layered on at the [PlexClient] boundary via @@ -1053,12 +1033,6 @@ String? _nonEmptyRatingString(Object? value) { class PlexMappers { PlexMappers._(); - /// Map a Plex `Metadata` JSON entry directly into a [PlexMediaItem]. - static PlexMediaItem mediaItemFromJson(Map json, {ServerId? serverId, String? serverName}) { - final dto = PlexMetadataDto.fromJsonWithImages(json).copyWith(serverId: serverId, serverName: serverName); - return mediaItem(dto); - } - /// Parse a Plex `/library/metadata/{id}` JSON object into a neutral /// [MediaItem]. Used by the offline cache layer to convert persisted Plex /// JSON back into MediaItem without depending on the Plex client surface. @@ -1246,19 +1220,6 @@ class PlexMappers { ); } - /// Map a Plex `/library/sections` Directory entry into a [MediaLibrary]. - static MediaLibrary mediaLibraryFromJson( - Map json, { - ServerId? serverId, - String? serverName, - bool isShared = false, - }) { - final dto = PlexLibraryDto.fromJson( - json, - ).copyWith(serverId: serverIdOrNull(serverId), serverName: serverName, isShared: isShared); - return mediaLibrary(dto); - } - /// Map a parsed [PlexHubDto] into a [MediaHub]. static MediaHub mediaHub(PlexHubDto dto) { return MediaHub( @@ -1274,11 +1235,6 @@ class PlexMappers { ); } - /// Map a Plex `/hubs` Hub JSON entry directly into a [MediaHub]. - static MediaHub mediaHubFromJson(Map json, {ServerId? serverId, String? serverName}) { - return mediaHub(PlexHubDto.fromJson(json, serverId: serverId, serverName: serverName)); - } - /// Map a parsed [PlexPlaylistDto] into a [MediaPlaylist]. static MediaPlaylist mediaPlaylist(PlexPlaylistDto dto) { return MediaPlaylist( @@ -1301,12 +1257,6 @@ class PlexMappers { serverName: dto.serverName, ); } - - /// Map a Plex `/playlists` Metadata entry directly into a [MediaPlaylist]. - static MediaPlaylist mediaPlaylistFromJson(Map json, {ServerId? serverId, String? serverName}) { - final dto = PlexPlaylistDto.fromJson(json).copyWith(serverId: serverId, serverName: serverName); - return mediaPlaylist(dto); - } } /// Build a [MediaSourceInfo] from a Plex `/library/metadata/{id}` JSON diff --git a/test/services/plex_mappers_test.dart b/test/services/plex_mappers_test.dart index 5af2237c7..9643e2846 100644 --- a/test/services/plex_mappers_test.dart +++ b/test/services/plex_mappers_test.dart @@ -5,7 +5,11 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:plezy/media/ids.dart'; import 'package:plezy/media/media_backend.dart'; import 'package:plezy/media/media_display_criteria.dart'; +import 'package:plezy/media/media_hub.dart'; +import 'package:plezy/media/media_item.dart'; import 'package:plezy/media/media_kind.dart'; +import 'package:plezy/media/media_library.dart'; +import 'package:plezy/media/media_playlist.dart'; import 'package:plezy/media/media_stream.dart'; import 'package:plezy/services/plex_mappers.dart'; import 'package:sentry_flutter/sentry_flutter.dart'; @@ -13,6 +17,30 @@ import 'package:sentry_flutter/sentry_flutter.dart'; const _serverId = 'plex-machine-1'; const _serverName = 'Home'; +PlexMediaItem _mediaItemFromJson(Map json, {ServerId? serverId, String? serverName}) { + final dto = PlexMetadataDto.fromJsonWithImages(json).copyWith(serverId: serverId, serverName: serverName); + return PlexMappers.mediaItem(dto); +} + +MediaLibrary _mediaLibraryFromJson( + Map json, { + ServerId? serverId, + String? serverName, + bool isShared = false, +}) { + final dto = PlexLibraryDto.fromJson(json).copyWith(serverId: serverId, serverName: serverName, isShared: isShared); + return PlexMappers.mediaLibrary(dto); +} + +MediaHub _mediaHubFromJson(Map json, {ServerId? serverId, String? serverName}) { + return PlexMappers.mediaHub(PlexHubDto.fromJson(json, serverId: serverId, serverName: serverName)); +} + +MediaPlaylist _mediaPlaylistFromJson(Map json, {ServerId? serverId, String? serverName}) { + final dto = PlexPlaylistDto.fromJson(json).copyWith(serverId: serverId, serverName: serverName); + return PlexMappers.mediaPlaylist(dto); +} + void _expectSafePlexMapperEvent( SentryEvent event, { required Iterable responseMarkers, @@ -111,7 +139,7 @@ void main() { 'guid': 7, }; - final hub = PlexMappers.mediaHubFromJson({ + final hub = _mediaHubFromJson({ 'key': '/hubs/cc004', 'title': 'Synthetic hub', 'type': 'movie', @@ -240,7 +268,7 @@ void main() { ], }; - final item = PlexMappers.mediaItemFromJson(json, serverId: ServerId(_serverId), serverName: _serverName); + final item = _mediaItemFromJson(json, serverId: ServerId(_serverId), serverName: _serverName); expect(item.id, '12345'); expect(item.backend, MediaBackend.plex); @@ -309,7 +337,7 @@ void main() { test('a section listing yields the scalar pair alone', () { // Plex sends no `Rating[]` outside /library/metadata/{id}, and no query // parameter adds it, so listing-backed cards get one or two scores. - final item = PlexMappers.mediaItemFromJson({ + final item = _mediaItemFromJson({ 'ratingKey': 'listing-1', 'type': 'movie', 'rating': 9.4, @@ -323,17 +351,14 @@ void main() { }); test('an unrated item reports no ratings rather than an empty list', () { - final item = PlexMappers.mediaItemFromJson({ - 'ratingKey': 'unrated-1', - 'type': 'movie', - }, serverId: ServerId(_serverId)); + final item = _mediaItemFromJson({'ratingKey': 'unrated-1', 'type': 'movie'}, serverId: ServerId(_serverId)); expect(item.rating, isNull); expect(item.ratings, isNull); }); test('scores outside the reportable range are dropped, not folded', () { - final item = PlexMappers.mediaItemFromJson({ + final item = _mediaItemFromJson({ 'ratingKey': 'noisy-1', 'type': 'movie', 'rating': 140, @@ -349,7 +374,7 @@ void main() { }); test('normalizes aggregate watch counts off leaf items', () { - final item = PlexMappers.mediaItemFromJson({ + final item = _mediaItemFromJson({ 'ratingKey': 'leaf-with-counts', 'type': 'movie', 'viewCount': 0, @@ -377,7 +402,7 @@ void main() { 'year': 2008, }; - final item = PlexMappers.mediaItemFromJson(json, serverId: ServerId(_serverId)); + final item = _mediaItemFromJson(json, serverId: ServerId(_serverId)); expect(item.kind, MediaKind.show); expect(item.leafCount, 62); expect(item.viewedLeafCount, 62); @@ -395,7 +420,7 @@ void main() { 'flattenSeasons': '1', }; - final item = PlexMappers.mediaItemFromJson(json, serverId: ServerId(_serverId)); + final item = _mediaItemFromJson(json, serverId: ServerId(_serverId)); expect(item.raw, containsPair('key', '/library/metadata/500')); expect(item.raw, containsPair('skipChildren', true)); @@ -415,7 +440,7 @@ void main() { 'viewedLeafCount': 3, }; - final item = PlexMappers.mediaItemFromJson(json, serverId: ServerId(_serverId)); + final item = _mediaItemFromJson(json, serverId: ServerId(_serverId)); expect(item.kind, MediaKind.season); expect(item.index, 1); expect(item.parentId, '500'); @@ -445,7 +470,7 @@ void main() { 'viewOffset': 1410000, }; - final item = PlexMappers.mediaItemFromJson(json, serverId: ServerId(_serverId)); + final item = _mediaItemFromJson(json, serverId: ServerId(_serverId)); expect(item.kind, MediaKind.episode); expect(item.index, 1); expect(item.parentIndex, 1); @@ -475,7 +500,7 @@ void main() { 'leafCount': 13, }; - final item = PlexMappers.mediaItemFromJson(json, serverId: ServerId(_serverId)); + final item = _mediaItemFromJson(json, serverId: ServerId(_serverId)); expect(item.kind, MediaKind.album); expect(item.title, 'Random Access Memories'); expect(item.parentId, '699'); @@ -499,7 +524,7 @@ void main() { 'duration': 369000, }; - final item = PlexMappers.mediaItemFromJson(json, serverId: ServerId(_serverId)); + final item = _mediaItemFromJson(json, serverId: ServerId(_serverId)); expect(item.kind, MediaKind.track); expect(item.title, 'Get Lucky'); expect(item.index, 8); @@ -552,7 +577,7 @@ void main() { ], }; - final item = PlexMappers.mediaItemFromJson(json, serverId: ServerId(_serverId)); + final item = _mediaItemFromJson(json, serverId: ServerId(_serverId)); expect(item.mediaVersions, isNotNull); final v = item.mediaVersions!.single; expect(v.id, '1'); @@ -601,7 +626,7 @@ void main() { ], }; - final item = PlexMappers.mediaItemFromJson(json, serverId: ServerId(_serverId)); + final item = _mediaItemFromJson(json, serverId: ServerId(_serverId)); final part = item.mediaVersions!.single.parts.single; final video = part.streams.firstWhere((stream) => stream.kind == MediaStreamKind.video); final audio = part.streams.firstWhere((stream) => stream.kind == MediaStreamKind.audio); @@ -627,7 +652,7 @@ void main() { ], }; - final item = PlexMappers.mediaItemFromJson(json, serverId: ServerId(_serverId)); + final item = _mediaItemFromJson(json, serverId: ServerId(_serverId)); expect(item.clearLogoPath, '/library/metadata/12345/clearLogo'); expect(item.backgroundSquarePath, '/library/metadata/12345/squareBg'); }); @@ -646,7 +671,7 @@ void main() { 'hidden': 0, }; - final lib = PlexMappers.mediaLibraryFromJson(json, serverId: ServerId(_serverId), serverName: _serverName); + final lib = _mediaLibraryFromJson(json, serverId: ServerId(_serverId), serverName: _serverName); expect(lib.id, '1'); expect(lib.backend, MediaBackend.plex); expect(lib.title, 'Movies'); @@ -662,13 +687,13 @@ void main() { test('shared library is marked isShared', () { final json = {'key': 'shared', 'title': 'Shared with you', 'type': 'movie'}; - final lib = PlexMappers.mediaLibraryFromJson(json, serverId: ServerId(_serverId), isShared: true); + final lib = _mediaLibraryFromJson(json, serverId: ServerId(_serverId), isShared: true); expect(lib.isShared, isTrue); }); test('hidden=1 maps to true', () { final json = {'key': '2', 'title': 'Hidden', 'type': 'show', 'hidden': 1}; - final lib = PlexMappers.mediaLibraryFromJson(json, serverId: ServerId(_serverId)); + final lib = _mediaLibraryFromJson(json, serverId: ServerId(_serverId)); expect(lib.hidden, isTrue); }); @@ -701,7 +726,7 @@ void main() { // PlexLibraryDto.fromJson would throw TypeError when Plex omitted // either field. Confirms graceful degradation. final json = {'key': '99'}; - final lib = PlexMappers.mediaLibraryFromJson(json, serverId: ServerId(_serverId)); + final lib = _mediaLibraryFromJson(json, serverId: ServerId(_serverId)); expect(lib.id, '99'); expect(lib.title, ''); expect(lib.kind, MediaKind.unknown); @@ -723,7 +748,7 @@ void main() { ], }; - final hub = PlexMappers.mediaHubFromJson(json, serverId: ServerId(_serverId), serverName: _serverName); + final hub = _mediaHubFromJson(json, serverId: ServerId(_serverId), serverName: _serverName); expect(hub.id, '/hubs/movie.recentlyAdded'); expect(hub.identifier, 'movie.recentlyAdded.1'); expect(hub.title, 'Recently Added Movies'); @@ -753,7 +778,7 @@ void main() { ], }; - final hub = PlexMappers.mediaHubFromJson(json, serverId: ServerId(_serverId)); + final hub = _mediaHubFromJson(json, serverId: ServerId(_serverId)); expect(hub.items.length, 2); expect(hub.items[0].kind, MediaKind.show); expect(hub.items[1].kind, MediaKind.folder); @@ -772,7 +797,7 @@ void main() { ], }; - final hub = PlexMappers.mediaHubFromJson(json, serverId: ServerId(_serverId)); + final hub = _mediaHubFromJson(json, serverId: ServerId(_serverId)); expect(hub.items.length, 2); expect(hub.items[0].kind, MediaKind.movie); expect(hub.items[1].kind, MediaKind.show); @@ -799,7 +824,7 @@ void main() { 'thumb': '/playlists/999/thumb', }; - final p = PlexMappers.mediaPlaylistFromJson(json, serverId: ServerId(_serverId), serverName: _serverName); + final p = _mediaPlaylistFromJson(json, serverId: ServerId(_serverId), serverName: _serverName); expect(p.id, '999'); expect(p.backend, MediaBackend.plex); expect(p.title, 'Date Night'); @@ -828,7 +853,7 @@ void main() { 'playlistType': 'audio', }; - final p = PlexMappers.mediaPlaylistFromJson(json, serverId: ServerId(_serverId)); + final p = _mediaPlaylistFromJson(json, serverId: ServerId(_serverId)); expect(p.smart, isTrue); expect(p.playlistType, 'audio'); }); @@ -838,7 +863,7 @@ void main() { // PlexPlaylistDto.fromJson would throw TypeError when Plex omitted // optional fields. Confirms graceful degradation. final json = {'ratingKey': '777', 'title': 'Bare', 'summary': null}; - final p = PlexMappers.mediaPlaylistFromJson(json, serverId: ServerId(_serverId)); + final p = _mediaPlaylistFromJson(json, serverId: ServerId(_serverId)); expect(p.id, '777'); expect(p.title, 'Bare'); expect(p.smart, isFalse); diff --git a/test/services/plex_rating_payload_shapes_test.dart b/test/services/plex_rating_payload_shapes_test.dart index 0335a1a23..85180cc40 100644 --- a/test/services/plex_rating_payload_shapes_test.dart +++ b/test/services/plex_rating_payload_shapes_test.dart @@ -8,14 +8,19 @@ // dashboard shows fewer scores than the detail screen, and this suite pins it. import 'package:flutter_test/flutter_test.dart'; import 'package:plezy/media/ids.dart'; +import 'package:plezy/media/media_item.dart'; import 'package:plezy/services/plex_mappers.dart'; const _serverId = 'plex-machine-1'; +PlexMediaItem _mediaItemFromJson(Map json, {ServerId? serverId}) { + return PlexMappers.mediaItem(PlexMetadataDto.fromJsonWithImages(json).copyWith(serverId: serverId)); +} + void main() { group('live Plex payloads', () { test('detail response surfaces IMDb alongside Rotten Tomatoes and TMDB', () { - final item = PlexMappers.mediaItemFromJson({ + final item = _mediaItemFromJson({ 'ratingKey': '2254', 'type': 'show', 'title': '【OSHI NO KO】', @@ -35,7 +40,7 @@ void main() { }); test('listing response carries only the scalar the server chose', () { - final item = PlexMappers.mediaItemFromJson({ + final item = _mediaItemFromJson({ 'ratingKey': '2254', 'type': 'show', 'title': '【OSHI NO KO】', diff --git a/test/utils/media_quality_labels_test.dart b/test/utils/media_quality_labels_test.dart index 9cfdb6443..86204bf73 100644 --- a/test/utils/media_quality_labels_test.dart +++ b/test/utils/media_quality_labels_test.dart @@ -87,7 +87,7 @@ void main() { }); test('formats Plex season child fallback metadata', () { - final item = PlexMappers.mediaItemFromJson({ + final item = _mediaItemFromJson({ 'ratingKey': '6048', 'type': 'episode', 'title': 'Hello, Ms. Cobel', @@ -115,7 +115,7 @@ void main() { }); test('formats Plex movie full-detail stream metadata', () { - final item = PlexMappers.mediaItemFromJson({ + final item = _mediaItemFromJson({ 'ratingKey': 'movie-1', 'type': 'movie', 'title': 'Movie', @@ -269,6 +269,10 @@ void main() { }); } +PlexMediaItem _mediaItemFromJson(Map json, {ServerId? serverId}) { + return PlexMappers.mediaItem(PlexMetadataDto.fromJsonWithImages(json).copyWith(serverId: serverId)); +} + MediaItem _episodeWithVersion(MediaVersion? version) { return testMediaItem( id: 'episode-1',