fix(jellyfin): fall back from missing season posters

close #981
This commit is contained in:
edde746
2026-05-05 07:53:36 +02:00
parent 955bc9548c
commit ef2db78310
4 changed files with 48 additions and 2 deletions
+8
View File
@@ -442,6 +442,14 @@ sealed class MediaItem {
return thumbPath;
}
/// Secondary poster path to try when [posterThumb] returns an image URL that
/// exists syntactically but the server cannot serve it.
String? posterThumbFallback({EpisodePosterMode mode = EpisodePosterMode.seriesPoster, bool mixedHubContext = false}) {
if (kind != MediaKind.episode || mode != EpisodePosterMode.seasonPoster) return null;
final fallback = grandparentThumbPath ?? thumbPath;
return fallback != null && fallback != posterThumb(mode: mode, mixedHubContext: mixedHubContext) ? fallback : null;
}
/// True when the item should render in 16:9.
/// - Clips are always 16:9.
/// - Episodes are 16:9 in `episodeThumbnail` mode.
+13 -2
View File
@@ -661,13 +661,15 @@ Widget _buildPosterImage(
final shouldBlur =
hideSpoilers && item.shouldHideSpoiler && episodePosterMode == EpisodePosterMode.episodeThumbnail;
posterUrl = item.posterThumb(mode: episodePosterMode, mixedHubContext: mixedHubContext);
final posterFallbackUrl = item.posterThumbFallback(mode: episodePosterMode, mixedHubContext: mixedHubContext);
final mediaClient = isOffline ? null : context.tryGetMediaClientWithFallback(item.serverId);
Widget image;
// Use thumb image type for 16:9 content (episodes, or movies in mixed hubs)
if (item.usesWideAspectRatio(episodePosterMode, mixedHubContext: mixedHubContext)) {
image = OptimizedMediaImage.thumb(
client: isOffline ? null : context.tryGetMediaClientWithFallback(item.serverId),
client: mediaClient,
imagePath: posterUrl,
width: knownWidth ?? double.infinity,
height: knownHeight ?? double.infinity,
@@ -676,11 +678,20 @@ Widget _buildPosterImage(
);
} else {
image = OptimizedMediaImage.poster(
client: isOffline ? null : context.tryGetMediaClientWithFallback(item.serverId),
client: mediaClient,
imagePath: posterUrl,
width: knownWidth ?? double.infinity,
height: knownHeight ?? double.infinity,
fit: BoxFit.cover,
errorWidget: posterFallbackUrl == null
? null
: (_, _, _) => OptimizedMediaImage.poster(
client: mediaClient,
imagePath: posterFallbackUrl,
width: knownWidth ?? double.infinity,
height: knownHeight ?? double.infinity,
fit: BoxFit.cover,
),
localFilePath: localPosterPath,
);
}
+3
View File
@@ -319,6 +319,9 @@ class OptimizedMediaImage extends StatelessWidget {
_imageFailureCount = 0;
_lastFailureLog = now;
}
if (errorWidget != null) {
return errorWidget!(context, imageUrl, error);
}
return _buildErrorWidget(context, error);
},
frameBuilder: (context, child, frame, wasSynchronouslyLoaded) {
+24
View File
@@ -3,6 +3,7 @@ import 'package:plezy/media/media_backend.dart';
import 'package:plezy/media/media_kind.dart';
import 'package:plezy/media/media_stream.dart';
import 'package:plezy/services/jellyfin_mappers.dart';
import 'package:plezy/services/settings_service.dart' show EpisodePosterMode;
const _serverId = 'jf-machine-1';
@@ -123,6 +124,29 @@ void main() {
expect(item.grandparentArtPath, '/Items/series-1/Images/Backdrop/0');
});
test('episode season poster falls back to series poster when season image tag is absent', () {
final json = {
'Id': 'ep1',
'Name': 'Pilot',
'Type': 'Episode',
'SeriesId': 'series-1',
'SeriesName': 'Breaking Bad',
'SeriesPrimaryImageTag': 'seriesPrimary',
'SeasonId': 'season-1',
'SeasonName': 'Season 1',
};
final item = JellyfinMappers.mediaItem(json, serverId: _serverId, absolutizer: null)!;
expect(item.parentThumbPath, '/Items/season-1/Images/Primary');
expect(item.grandparentThumbPath, '/Items/series-1/Images/Primary?tag=seriesPrimary');
expect(item.posterThumb(mode: EpisodePosterMode.seasonPoster), '/Items/season-1/Images/Primary');
expect(
item.posterThumbFallback(mode: EpisodePosterMode.seasonPoster),
'/Items/series-1/Images/Primary?tag=seriesPrimary',
);
});
test('series viewedLeafCount derived from total - UnplayedItemCount', () {
final json = {
'Id': 's1',