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; }