fix(artwork): stop memoizing posters that merely have no client yet

Distinguish an unresolvable URL from a failed load at the error-widget
boundary. A transiently null media client during a profile switch or
reconnect marked the primary poster dead in a process-global set, pinning the
item to fallback artwork for the rest of the session.
This commit is contained in:
edde746
2026-07-27 17:44:52 +02:00
parent 5f77da93d1
commit c64e6fc519
3 changed files with 68 additions and 8 deletions
+6 -2
View File
@@ -944,8 +944,12 @@ Widget _buildPosterImage(
// Remember the dead primary URL so later builds go straight to the fallback.
Widget Function(BuildContext, String, dynamic)? retryWithFallback(ImageType type) {
if (posterFallbackUrl == null || useRememberedFallback) return null;
return (_, _, _) {
_rememberFailedPosterUrl(primaryPosterUrl);
return (_, _, error) {
// Only an attempted-and-failed load proves the primary artwork is dead.
// An unresolvable URL just means there is no client right now (offline,
// profile switch, reconnect); memoizing it would pin this item to
// fallback artwork for the process lifetime.
if (error is! UnresolvedImageUrl) _rememberFailedPosterUrl(primaryPosterUrl);
return buildImage(posterFallbackUrl, type);
};
}
+16 -6
View File
@@ -18,6 +18,20 @@ int _imageFailureCount = 0;
DateTime _lastFailureLog = DateTime.now();
const _logInterval = Duration(seconds: 10);
/// Passed to [OptimizedMediaImage.errorWidget] when no URL could be built for
/// the image *at this build*, as opposed to a load that was attempted and
/// failed. Reaching [OptimizedMediaImage._buildCachedImage] already implies a
/// non-empty [OptimizedMediaImage.imagePath], and the only remaining way
/// [MediaImageHelper.getOptimizedImageUrl] returns '' for one is a null
/// [MediaServerClient] (offline mode, profile switch, server reconnect), so the
/// path is not known-bad and callers that memoize failures MUST NOT record it.
class UnresolvedImageUrl implements Exception {
const UnresolvedImageUrl(this.imagePath);
final String imagePath;
@override
String toString() => 'No image URL could be built for $imagePath';
}
Widget blurArtwork(Widget child, {double sigma = 30, bool clip = true}) {
if (!kBlurArtwork) return child;
final filtered = ImageFiltered(
@@ -239,11 +253,7 @@ class OptimizedMediaImage extends StatelessWidget {
if (!hasLocal && (imagePath == null || imagePath!.isEmpty)) {
if (errorWidget != null) {
return errorWidget!(
context,
localFilePath ?? '',
FileSystemException('Local image file is unavailable', localFilePath),
);
return errorWidget!(context, localFilePath ?? '', UnresolvedImageUrl(localFilePath ?? imagePath ?? ''));
}
return _buildFallback(context);
}
@@ -335,7 +345,7 @@ class OptimizedMediaImage extends StatelessWidget {
// load failure from the caller's point of view, so honour its own
// failure UI rather than the generic broken-image tile.
if (errorWidget != null) {
return errorWidget!(context, imagePath ?? '', StateError('No image URL could be built for $imagePath'));
return errorWidget!(context, imagePath ?? '', UnresolvedImageUrl(imagePath ?? ''));
}
return _buildFallback(context);
}