From ebba3dfa0940cab78b6b1b4e0a9fe293c0b4ddff Mon Sep 17 00:00:00 2001
From: Frank Stellmacher <171614930+Psychotoxical@users.noreply.github.com>
Date: Sun, 17 May 2026 13:20:50 +0200
Subject: [PATCH] fix(fullscreen): preflight artist portrait URL so broken-img
glyph never shows (#748)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Navidrome / Subsonic `getArtistInfo2` often returns a `largeImageUrl`
that 404s when the artist has no scraped image. The hook returned the
URL as-is, the FullscreenPlayer's `artistBgUrl || coverUrl` chain saw
a non-empty string and skipped the cover-art fallback, and FsPortrait
rendered an `
` whose `src` failed — leaving the browser's default
broken-image glyph in the middle of the screen.
Preflight the URL via an `Image()` probe and only expose it once it
actually loads; on error leave the hook's state empty so the cover-art
fallback in the caller takes effect.
---
src/hooks/useFsArtistPortrait.ts | 21 ++++++++++++++++++---
1 file changed, 18 insertions(+), 3 deletions(-)
diff --git a/src/hooks/useFsArtistPortrait.ts b/src/hooks/useFsArtistPortrait.ts
index 47751ddb..aedbb127 100644
--- a/src/hooks/useFsArtistPortrait.ts
+++ b/src/hooks/useFsArtistPortrait.ts
@@ -3,17 +3,32 @@ import { getArtistInfo } from '../api/subsonicArtists';
/** Fetches the large artist image for the given artist id, returning '' until
* the request resolves (or when there is no artist id). Falls through silently
- * on network failures — the caller should layer a cover-art fallback on top. */
+ * on network failures — the caller should layer a cover-art fallback on top.
+ *
+ * Navidrome / Subsonic backends often return a `largeImageUrl` that 404s when
+ * the artist has no scraped image (Last.fm placeholder), so the URL is
+ * preflighted via an Image() probe and only exposed when it actually loads.
+ * Otherwise the caller would render a broken-img glyph (`?`) on top of the
+ * fullscreen background — see zunoz report on the Psysonic Discord. */
export function useFsArtistPortrait(artistId: string | undefined): string {
const [artistBgUrl, setArtistBgUrl] = useState('');
useEffect(() => {
setArtistBgUrl('');
if (!artistId) return;
let cancelled = false;
+ let probe: HTMLImageElement | null = null;
getArtistInfo(artistId).then(info => {
- if (!cancelled && info.largeImageUrl) setArtistBgUrl(info.largeImageUrl);
+ if (cancelled || !info.largeImageUrl) return;
+ const url = info.largeImageUrl;
+ probe = new Image();
+ probe.onload = () => { if (!cancelled) setArtistBgUrl(url); };
+ probe.onerror = () => { /* leave empty so caller falls back to cover */ };
+ probe.src = url;
}).catch(() => {});
- return () => { cancelled = true; };
+ return () => {
+ cancelled = true;
+ if (probe) probe.onload = probe.onerror = null;
+ };
}, [artistId]);
return artistBgUrl;
}