fix(metadata-edit): use 16:9 poster picker for episodes

close #1519
This commit is contained in:
edde746
2026-07-10 12:45:35 +02:00
parent 7509077d63
commit b5d8ff5332
5 changed files with 100 additions and 16 deletions
@@ -5,6 +5,7 @@ import '../media/media_kind.dart';
import '../media/media_server_client.dart'; import '../media/media_server_client.dart';
import '../services/jellyfin_client.dart'; import '../services/jellyfin_client.dart';
import '../utils/jellyfin_time.dart'; import '../utils/jellyfin_time.dart';
import '../utils/media_image_helper.dart';
import 'metadata_edit_models.dart'; import 'metadata_edit_models.dart';
class JellyfinMetadataEditAdapter extends MetadataEditAdapter { class JellyfinMetadataEditAdapter extends MetadataEditAdapter {
@@ -235,11 +236,32 @@ class JellyfinMetadataEditAdapter extends MetadataEditAdapter {
List<MetadataEditField> _artworkFields(MediaKind kind) { List<MetadataEditField> _artworkFields(MediaKind kind) {
final fields = <MetadataEditField>[ final fields = <MetadataEditField>[
_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) { if (kind == MediaKind.movie || kind == MediaKind.show || kind == MediaKind.episode) {
fields.add( 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) { if (kind == MediaKind.movie || kind == MediaKind.show) {
@@ -253,6 +275,7 @@ class JellyfinMetadataEditAdapter extends MetadataEditAdapter {
2, 2,
2.5, 2.5,
fit: MetadataArtworkFit.contain, fit: MetadataArtworkFit.contain,
imageType: ImageType.logo,
), ),
); );
} }
@@ -268,6 +291,7 @@ class JellyfinMetadataEditAdapter extends MetadataEditAdapter {
int columns, int columns,
double aspectRatio, { double aspectRatio, {
MetadataArtworkFit fit = MetadataArtworkFit.cover, MetadataArtworkFit fit = MetadataArtworkFit.cover,
ImageType imageType = ImageType.poster,
}) { }) {
return MetadataEditField( return MetadataEditField(
id: 'artwork:$key', id: 'artwork:$key',
@@ -282,6 +306,7 @@ class JellyfinMetadataEditAdapter extends MetadataEditAdapter {
gridColumns: columns, gridColumns: columns,
gridAspectRatio: aspectRatio, gridAspectRatio: aspectRatio,
fit: fit, fit: fit,
imageType: imageType,
), ),
); );
} }
@@ -2,6 +2,7 @@ import '../media/media_backend.dart';
import '../media/media_item.dart'; import '../media/media_item.dart';
import '../media/media_kind.dart'; import '../media/media_kind.dart';
import '../media/media_server_client.dart'; import '../media/media_server_client.dart';
import '../utils/media_image_helper.dart';
enum MetadataEditFieldType { text, multilineText, date, stringList, choice, artwork } enum MetadataEditFieldType { text, multilineText, date, stringList, choice, artwork }
@@ -24,6 +25,7 @@ class MetadataArtworkConfig {
final int gridColumns; final int gridColumns;
final double gridAspectRatio; final double gridAspectRatio;
final MetadataArtworkFit fit; final MetadataArtworkFit fit;
final ImageType imageType;
const MetadataArtworkConfig({ const MetadataArtworkConfig({
required this.key, required this.key,
@@ -33,6 +35,7 @@ class MetadataArtworkConfig {
required this.gridColumns, required this.gridColumns,
required this.gridAspectRatio, required this.gridAspectRatio,
this.fit = MetadataArtworkFit.cover, this.fit = MetadataArtworkFit.cover,
this.imageType = ImageType.poster,
}); });
} }
@@ -5,6 +5,7 @@ import '../media/media_kind.dart';
import '../media/media_server_client.dart'; import '../media/media_server_client.dart';
import '../services/plex_client.dart'; import '../services/plex_client.dart';
import '../utils/language_codes.dart'; import '../utils/language_codes.dart';
import '../utils/media_image_helper.dart';
import 'metadata_edit_models.dart'; import 'metadata_edit_models.dart';
class PlexMetadataEditAdapter extends MetadataEditAdapter { class PlexMetadataEditAdapter extends MetadataEditAdapter {
@@ -240,10 +241,33 @@ class PlexMetadataEditAdapter extends MetadataEditAdapter {
List<MetadataEditField> _artworkFields(MediaKind kind) { List<MetadataEditField> _artworkFields(MediaKind kind) {
final fields = <MetadataEditField>[ final fields = <MetadataEditField>[
_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) { 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) { if (kind == MediaKind.movie || kind == MediaKind.show || kind == MediaKind.collection) {
fields.add( fields.add(
@@ -256,9 +280,21 @@ class PlexMetadataEditAdapter extends MetadataEditAdapter {
2, 2,
2.5, 2.5,
fit: MetadataArtworkFit.contain, 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; return fields;
} }
@@ -272,6 +308,7 @@ class PlexMetadataEditAdapter extends MetadataEditAdapter {
int columns, int columns,
double aspectRatio, { double aspectRatio, {
MetadataArtworkFit fit = MetadataArtworkFit.cover, MetadataArtworkFit fit = MetadataArtworkFit.cover,
ImageType imageType = ImageType.poster,
}) { }) {
return MetadataEditField( return MetadataEditField(
id: 'artwork:$key', id: 'artwork:$key',
@@ -286,6 +323,7 @@ class PlexMetadataEditAdapter extends MetadataEditAdapter {
gridColumns: columns, gridColumns: columns,
gridAspectRatio: aspectRatio, gridAspectRatio: aspectRatio,
fit: fit, fit: fit,
imageType: imageType,
), ),
); );
} }
+2 -11
View File
@@ -13,7 +13,6 @@ import '../services/file_picker_service.dart';
import '../utils/app_logger.dart'; import '../utils/app_logger.dart';
import '../utils/dialogs.dart'; import '../utils/dialogs.dart';
import '../utils/formatters.dart'; import '../utils/formatters.dart';
import '../utils/media_image_helper.dart';
import '../utils/provider_extensions.dart'; import '../utils/provider_extensions.dart';
import '../utils/snackbar_helper.dart'; import '../utils/snackbar_helper.dart';
import '../widgets/app_icon.dart'; import '../widgets/app_icon.dart';
@@ -373,7 +372,7 @@ class _MetadataEditScreenState extends State<MetadataEditScreen> {
width: artwork.previewWidth, width: artwork.previewWidth,
height: artwork.previewHeight, height: artwork.previewHeight,
fit: artwork.fit == MetadataArtworkFit.contain ? BoxFit.contain : BoxFit.cover, fit: artwork.fit == MetadataArtworkFit.contain ? BoxFit.contain : BoxFit.cover,
imageType: _imageTypeForArtwork(artwork), imageType: artwork.imageType,
), ),
), ),
), ),
@@ -549,7 +548,7 @@ class _ArtworkPickerDialogState extends State<ArtworkPickerDialog> {
client: widget.adapter.mediaClient, client: widget.adapter.mediaClient,
imagePath: artwork.thumbnailPath, imagePath: artwork.thumbnailPath,
fit: _config.fit == MetadataArtworkFit.contain ? BoxFit.contain : BoxFit.cover, fit: _config.fit == MetadataArtworkFit.contain ? BoxFit.contain : BoxFit.cover,
imageType: _imageTypeForArtwork(_config), imageType: _config.imageType,
), ),
), ),
), ),
@@ -571,11 +570,3 @@ class _ArtworkPickerDialogState extends State<ArtworkPickerDialog> {
); );
} }
} }
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;
}
@@ -8,7 +8,9 @@ import 'package:plezy/media/media_backend.dart';
import 'package:plezy/media/media_item.dart'; import 'package:plezy/media/media_item.dart';
import 'package:plezy/media/media_kind.dart'; import 'package:plezy/media/media_kind.dart';
import 'package:plezy/metadata_edit/jellyfin_metadata_edit_adapter.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/services/jellyfin_client.dart';
import 'package:plezy/utils/media_image_helper.dart';
void main() { void main() {
test('load fails when the full editable Jellyfin DTO is unavailable', () async { test('load fails when the full editable Jellyfin DTO is unavailable', () async {
@@ -29,6 +31,31 @@ void main() {
expect(adapter.load(item), throwsA(isA<StateError>())); expect(adapter.load(item), throwsA(isA<StateError>()));
}); });
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 { test('save preserves unchanged Jellyfin people and studio identity data', () async {
String? capturedBody; String? capturedBody;
final client = JellyfinClient.forTesting( final client = JellyfinClient.forTesting(