From b5d8ff5332f56ff88517f5c09989471d711f0fd7 Mon Sep 17 00:00:00 2001 From: edde746 <86283021+edde746@users.noreply.github.com> Date: Fri, 10 Jul 2026 12:42:27 +0200 Subject: [PATCH] fix(metadata-edit): use 16:9 poster picker for episodes close #1519 --- .../jellyfin_metadata_edit_adapter.dart | 29 +++++++++++- lib/metadata_edit/metadata_edit_models.dart | 3 ++ .../plex_metadata_edit_adapter.dart | 44 +++++++++++++++++-- lib/screens/metadata_edit_screen.dart | 13 +----- .../jellyfin_metadata_edit_adapter_test.dart | 27 ++++++++++++ 5 files changed, 100 insertions(+), 16 deletions(-) diff --git a/lib/metadata_edit/jellyfin_metadata_edit_adapter.dart b/lib/metadata_edit/jellyfin_metadata_edit_adapter.dart index 1f9457d9..55e40b0f 100644 --- a/lib/metadata_edit/jellyfin_metadata_edit_adapter.dart +++ b/lib/metadata_edit/jellyfin_metadata_edit_adapter.dart @@ -5,6 +5,7 @@ import '../media/media_kind.dart'; import '../media/media_server_client.dart'; import '../services/jellyfin_client.dart'; import '../utils/jellyfin_time.dart'; +import '../utils/media_image_helper.dart'; import 'metadata_edit_models.dart'; class JellyfinMetadataEditAdapter extends MetadataEditAdapter { @@ -235,11 +236,32 @@ class JellyfinMetadataEditAdapter extends MetadataEditAdapter { List _artworkFields(MediaKind kind) { final fields = [ - _artworkField('Primary', t.metadataEdit.poster, t.metadataEdit.selectPoster, 40, 60, 3, 2 / 3), + // Episode "posters" are 16:9 thumbnails, not 2:3 poster art. + kind == MediaKind.episode + ? _artworkField( + 'Primary', + t.metadataEdit.poster, + t.metadataEdit.selectPoster, + 80, + 45, + 2, + 16 / 9, + imageType: ImageType.thumb, + ) + : _artworkField('Primary', t.metadataEdit.poster, t.metadataEdit.selectPoster, 40, 60, 3, 2 / 3), ]; if (kind == MediaKind.movie || kind == MediaKind.show || kind == MediaKind.episode) { fields.add( - _artworkField('Backdrop', t.metadataEdit.background, t.metadataEdit.selectBackground, 80, 45, 2, 16 / 9), + _artworkField( + 'Backdrop', + t.metadataEdit.background, + t.metadataEdit.selectBackground, + 80, + 45, + 2, + 16 / 9, + imageType: ImageType.art, + ), ); } if (kind == MediaKind.movie || kind == MediaKind.show) { @@ -253,6 +275,7 @@ class JellyfinMetadataEditAdapter extends MetadataEditAdapter { 2, 2.5, fit: MetadataArtworkFit.contain, + imageType: ImageType.logo, ), ); } @@ -268,6 +291,7 @@ class JellyfinMetadataEditAdapter extends MetadataEditAdapter { int columns, double aspectRatio, { MetadataArtworkFit fit = MetadataArtworkFit.cover, + ImageType imageType = ImageType.poster, }) { return MetadataEditField( id: 'artwork:$key', @@ -282,6 +306,7 @@ class JellyfinMetadataEditAdapter extends MetadataEditAdapter { gridColumns: columns, gridAspectRatio: aspectRatio, fit: fit, + imageType: imageType, ), ); } diff --git a/lib/metadata_edit/metadata_edit_models.dart b/lib/metadata_edit/metadata_edit_models.dart index 207bd48f..bc817887 100644 --- a/lib/metadata_edit/metadata_edit_models.dart +++ b/lib/metadata_edit/metadata_edit_models.dart @@ -2,6 +2,7 @@ import '../media/media_backend.dart'; import '../media/media_item.dart'; import '../media/media_kind.dart'; import '../media/media_server_client.dart'; +import '../utils/media_image_helper.dart'; enum MetadataEditFieldType { text, multilineText, date, stringList, choice, artwork } @@ -24,6 +25,7 @@ class MetadataArtworkConfig { final int gridColumns; final double gridAspectRatio; final MetadataArtworkFit fit; + final ImageType imageType; const MetadataArtworkConfig({ required this.key, @@ -33,6 +35,7 @@ class MetadataArtworkConfig { required this.gridColumns, required this.gridAspectRatio, this.fit = MetadataArtworkFit.cover, + this.imageType = ImageType.poster, }); } diff --git a/lib/metadata_edit/plex_metadata_edit_adapter.dart b/lib/metadata_edit/plex_metadata_edit_adapter.dart index 3052293e..19d607b2 100644 --- a/lib/metadata_edit/plex_metadata_edit_adapter.dart +++ b/lib/metadata_edit/plex_metadata_edit_adapter.dart @@ -5,6 +5,7 @@ import '../media/media_kind.dart'; import '../media/media_server_client.dart'; import '../services/plex_client.dart'; import '../utils/language_codes.dart'; +import '../utils/media_image_helper.dart'; import 'metadata_edit_models.dart'; class PlexMetadataEditAdapter extends MetadataEditAdapter { @@ -240,10 +241,33 @@ class PlexMetadataEditAdapter extends MetadataEditAdapter { List _artworkFields(MediaKind kind) { final fields = [ - _artworkField('posters', t.metadataEdit.poster, t.metadataEdit.selectPoster, 40, 60, 3, 2 / 3), + // Episode "posters" are 16:9 thumbnails, not 2:3 poster art. + kind == MediaKind.episode + ? _artworkField( + 'posters', + t.metadataEdit.poster, + t.metadataEdit.selectPoster, + 80, + 45, + 2, + 16 / 9, + imageType: ImageType.thumb, + ) + : _artworkField('posters', t.metadataEdit.poster, t.metadataEdit.selectPoster, 40, 60, 3, 2 / 3), ]; if (kind == MediaKind.movie || kind == MediaKind.show || kind == MediaKind.episode) { - fields.add(_artworkField('arts', t.metadataEdit.background, t.metadataEdit.selectBackground, 80, 45, 2, 16 / 9)); + fields.add( + _artworkField( + 'arts', + t.metadataEdit.background, + t.metadataEdit.selectBackground, + 80, + 45, + 2, + 16 / 9, + imageType: ImageType.art, + ), + ); } if (kind == MediaKind.movie || kind == MediaKind.show || kind == MediaKind.collection) { fields.add( @@ -256,9 +280,21 @@ class PlexMetadataEditAdapter extends MetadataEditAdapter { 2, 2.5, fit: MetadataArtworkFit.contain, + imageType: ImageType.logo, + ), + ); + fields.add( + _artworkField( + 'squareArts', + t.metadataEdit.squareArt, + t.metadataEdit.selectSquareArt, + 50, + 50, + 3, + 1, + imageType: ImageType.avatar, ), ); - fields.add(_artworkField('squareArts', t.metadataEdit.squareArt, t.metadataEdit.selectSquareArt, 50, 50, 3, 1)); } return fields; } @@ -272,6 +308,7 @@ class PlexMetadataEditAdapter extends MetadataEditAdapter { int columns, double aspectRatio, { MetadataArtworkFit fit = MetadataArtworkFit.cover, + ImageType imageType = ImageType.poster, }) { return MetadataEditField( id: 'artwork:$key', @@ -286,6 +323,7 @@ class PlexMetadataEditAdapter extends MetadataEditAdapter { gridColumns: columns, gridAspectRatio: aspectRatio, fit: fit, + imageType: imageType, ), ); } diff --git a/lib/screens/metadata_edit_screen.dart b/lib/screens/metadata_edit_screen.dart index e493ce5b..f62cd81e 100644 --- a/lib/screens/metadata_edit_screen.dart +++ b/lib/screens/metadata_edit_screen.dart @@ -13,7 +13,6 @@ import '../services/file_picker_service.dart'; import '../utils/app_logger.dart'; import '../utils/dialogs.dart'; import '../utils/formatters.dart'; -import '../utils/media_image_helper.dart'; import '../utils/provider_extensions.dart'; import '../utils/snackbar_helper.dart'; import '../widgets/app_icon.dart'; @@ -373,7 +372,7 @@ class _MetadataEditScreenState extends State { width: artwork.previewWidth, height: artwork.previewHeight, fit: artwork.fit == MetadataArtworkFit.contain ? BoxFit.contain : BoxFit.cover, - imageType: _imageTypeForArtwork(artwork), + imageType: artwork.imageType, ), ), ), @@ -549,7 +548,7 @@ class _ArtworkPickerDialogState extends State { client: widget.adapter.mediaClient, imagePath: artwork.thumbnailPath, fit: _config.fit == MetadataArtworkFit.contain ? BoxFit.contain : BoxFit.cover, - imageType: _imageTypeForArtwork(_config), + imageType: _config.imageType, ), ), ), @@ -571,11 +570,3 @@ class _ArtworkPickerDialogState extends State { ); } } - -ImageType _imageTypeForArtwork(MetadataArtworkConfig artwork) { - final key = artwork.key.toLowerCase(); - if (key == 'arts' || key == 'backdrop') return ImageType.art; - if (key == 'clearlogos' || key == 'logo') return ImageType.logo; - if (key == 'squarearts') return ImageType.avatar; - return ImageType.poster; -} diff --git a/test/metadata_edit/jellyfin_metadata_edit_adapter_test.dart b/test/metadata_edit/jellyfin_metadata_edit_adapter_test.dart index d8c8ce0b..c214a654 100644 --- a/test/metadata_edit/jellyfin_metadata_edit_adapter_test.dart +++ b/test/metadata_edit/jellyfin_metadata_edit_adapter_test.dart @@ -8,7 +8,9 @@ import 'package:plezy/media/media_backend.dart'; import 'package:plezy/media/media_item.dart'; import 'package:plezy/media/media_kind.dart'; import 'package:plezy/metadata_edit/jellyfin_metadata_edit_adapter.dart'; +import 'package:plezy/metadata_edit/metadata_edit_models.dart'; import 'package:plezy/services/jellyfin_client.dart'; +import 'package:plezy/utils/media_image_helper.dart'; void main() { test('load fails when the full editable Jellyfin DTO is unavailable', () async { @@ -29,6 +31,31 @@ void main() { expect(adapter.load(item), throwsA(isA())); }); + test('episode poster artwork uses 16:9 thumbnail geometry', () async { + final client = JellyfinClient.forTesting( + connection: _connection(), + httpClient: MockClient((request) async => http.Response('', 500)), + ); + addTearDown(client.close); + final adapter = JellyfinMetadataEditAdapter(client); + + MetadataArtworkConfig posterConfig(MediaKind kind) { + final item = MediaItem(id: 'item-1', backend: MediaBackend.jellyfin, kind: kind); + final draft = MetadataEditDraft(sourceItem: item, currentItem: item, values: {}); + final artwork = adapter.buildSchema(draft).singleWhere((section) => section.id == 'artwork'); + return artwork.fields.singleWhere((field) => field.id == 'artwork:Primary').artwork!; + } + + final episode = posterConfig(MediaKind.episode); + expect(episode.gridAspectRatio, 16 / 9); + expect(episode.imageType, ImageType.thumb); + expect(episode.gridColumns, 2); + + final movie = posterConfig(MediaKind.movie); + expect(movie.gridAspectRatio, 2 / 3); + expect(movie.imageType, ImageType.poster); + }); + test('save preserves unchanged Jellyfin people and studio identity data', () async { String? capturedBody; final client = JellyfinClient.forTesting(