fix(artist): show fanart in the artist-detail hero, not the Navidrome cover (#1172)

* fix(cover): report a genuine miss for external artist surfaces

When external artwork is enabled, the fanart/banner ensure fell through to
the Navidrome cover and returned it as a hit on a fanart.tv miss. That made
'this artist has no banner' indistinguishable from a real hit, so the
artist-detail hero short-circuited on the ND cover and never reached the
fanart it actually had. Return hit=false on an external-surface miss instead;
the Navidrome fallback is the caller's job, since each surface has its own
chain (hero: banner->fanart->ND, fullscreen player: fanart->ND).

* fix(artist): hero background banner->fanart->Navidrome with raised focal point

The hero header background now steps through banner -> fanart -> Navidrome
artist cover, resolving the ND cover the same way the fullscreen player does.
Previously it had no Navidrome stage at all and, combined with the backend
masking a banner miss as a hit, showed the ND cover where the fanart belonged.

Also raise the focal point (background-position: center 30%) for the portrait-ish
fanart/ND images so the band's heads stay in frame on wide (2K+) viewports,
where 'cover' scales them up and overflows vertically. The wide banner strip
keeps the shared center. Done via a scoped inline style; .album-detail-bg stays
untouched for the album/playlist headers that share it.

* docs(changelog): artist header external-background fallback fix (#1172)
This commit is contained in:
Psychotoxical
2026-06-24 20:41:16 +02:00
committed by GitHub
parent 09f24beb32
commit c3e6be537c
3 changed files with 53 additions and 17 deletions
+20 -9
View File
@@ -302,12 +302,18 @@ impl CoverCacheState {
}
// For an external artist surface (`fanart` 16:9 background or `banner`
// strip), try fanart.tv before the Navidrome fallback. On any miss it
// falls through WITHOUT writing a `.fetch-failed` marker, so Navidrome
// stays the display fallback (§28).
// strip), resolve fanart.tv only. On a hit we return the external image;
// on a miss we report a genuine `hit=false` (no `.fetch-failed` marker)
// and DO NOT fall back to the Navidrome cover here. The fallback is the
// caller's job, because each surface has its own chain: the artist-detail
// hero wants banner → fanart → Navidrome, the fullscreen player wants
// fanart → Navidrome. Falling back to Navidrome at this layer would mask
// "this artist has no banner" as a hit and short-circuit the caller's
// fanart step (the banner ensure would win the `||` with the ND cover and
// the existing fanart would never show).
if args.external_artwork_enabled && !args.library_bulk && args.cache_kind == "artist" {
if let Some(surface) = external_ensure::external_surface(args.surface_kind.as_deref()) {
if let Some(path) = external_ensure::try_external_fanart(
let external = external_ensure::try_external_fanart(
app,
args,
&dir,
@@ -317,14 +323,19 @@ impl CoverCacheState {
args.tier,
surface,
)
.await
{
return Ok(CoverCacheEnsureResult {
.await;
return Ok(match external {
Some(path) => CoverCacheEnsureResult {
hit: true,
path: path.to_string_lossy().into_owned(),
tier: args.tier,
});
}
},
None => CoverCacheEnsureResult {
hit: false,
path: String::new(),
tier: args.tier,
},
});
}
}