@@ -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.
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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',
|
||||
|
||||
Reference in New Issue
Block a user