From 569af968c542aa136d91cc27c84b3d50952218ab Mon Sep 17 00:00:00 2001 From: Honey Date: Fri, 31 Jul 2026 17:07:53 +0200 Subject: [PATCH] feat: Deezer artist photo fallback for local artists with no Navidrome image MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Niche local artists (e.g. N!GHT) whose Navidrome metadata agents have no image showed a blank hero. Now, when Navidrome has no artist image, the hero falls back to the matched Deezer artist's photo (Navidrome image → Deezer photo → album cover), on web + Android. - Web (detail.js): the Deezer-photo fallback previously ran only for non-local artists; now it runs whenever the Navidrome image is absent, local or not. - Android: ArtistViewModel captures the matched Deezer artist's photo (deezerImage); ArtistDetailScreen uses it as the hero fallback. SW ns-v56. Co-Authored-By: Claude Opus 4.8 --- CHANGELOG.md | 1 + .../app/ui/screen/ArtistDetailScreen.kt | 3 +++ .../app/viewmodel/ArtistViewModel.kt | 7 +++++- public/sw.js | 2 +- src/views/detail.js | 25 +++++++++++-------- 5 files changed, 26 insertions(+), 12 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8210235..c2e5aff 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -80,6 +80,7 @@ Big change: **the entire metadata/search stack moved from Apple's iTunes to Deez ### Fixed - **Web + Android** — Owned albums were wrongly shown as **"Unvollständig 1 / 0 Tracks"** on the artist page. Deezer's `/artist/{id}/albums` list omits `nb_tracks`, so the total read as 0 and every matched album looked incomplete. Web now resolves the real track count from the Deezer album endpoint for **matched (owned) albums only** and treats an unknown total as complete; Android treats an unknown total as complete (never a false partial) +- **Web + Android** — Local Navidrome artist pages with **no artist image** (niche artists like N!GHT that Navidrome's metadata agents have no art for) showed a blank hero. They now fall back to the **matched Deezer artist photo** (Navidrome image → Deezer photo → album cover) ### Notes - Deploy: `git pull`, then rebuild the frontend (`docker compose up -d --build novasonic-frontend`) and restart the backend (`docker compose restart novasonic-backend`). Service worker bumped to `ns-v54` diff --git a/android/app/src/main/kotlin/com/novasonic/app/ui/screen/ArtistDetailScreen.kt b/android/app/src/main/kotlin/com/novasonic/app/ui/screen/ArtistDetailScreen.kt index fa95f72..a92360e 100644 --- a/android/app/src/main/kotlin/com/novasonic/app/ui/screen/ArtistDetailScreen.kt +++ b/android/app/src/main/kotlin/com/novasonic/app/ui/screen/ArtistDetailScreen.kt @@ -218,7 +218,10 @@ fun ArtistDetailScreen( // Hero card item { + // Navidrome's own artist image first; if its agents have none (niche artists like N!GHT), + // fall back to the matched Deezer artist photo, then the album cover. val imgUrl = state.info?.largeImageUrl?.takeIf { it.isNotBlank() } + ?: state.deezerImage ?: coverUrl(artist.coverArt) Box( Modifier diff --git a/android/app/src/main/kotlin/com/novasonic/app/viewmodel/ArtistViewModel.kt b/android/app/src/main/kotlin/com/novasonic/app/viewmodel/ArtistViewModel.kt index d9075c9..11a7fa0 100644 --- a/android/app/src/main/kotlin/com/novasonic/app/viewmodel/ArtistViewModel.kt +++ b/android/app/src/main/kotlin/com/novasonic/app/viewmodel/ArtistViewModel.kt @@ -43,6 +43,8 @@ data class ArtistUiState( val showEventsSheet: Boolean = false, // Deezer fan count of the matched artist (shown under the name), null if unknown val fanCount: Long? = null, + // Deezer artist photo — hero fallback when Navidrome's agents have no image (niche artists) + val deezerImage: String? = null, // Preview + download val previewingId: Long? = null, val downloadSheetItem: ItunesItem? = null, @@ -121,7 +123,10 @@ class ArtistViewModel(app: Application, savedState: SavedStateHandle) : AndroidV ?: artists.firstOrNull() ) val iArtistId = iArtist?.artistId - if (iArtist?.fanCount != null) _state.value = _state.value.copy(fanCount = iArtist.fanCount) + if (iArtist != null) _state.value = _state.value.copy( + fanCount = iArtist.fanCount ?: _state.value.fanCount, + deezerImage = iArtist.artworkUrl100?.takeIf { it.isNotBlank() } ?: _state.value.deezerImage, + ) if (iArtistId != null) { coroutineScope { diff --git a/public/sw.js b/public/sw.js index 932a2da..5a7e44c 100644 --- a/public/sw.js +++ b/public/sw.js @@ -1,4 +1,4 @@ -const CACHE = 'ns-v55'; +const CACHE = 'ns-v56'; const PRECACHE = ['/', 'index.html', 'manifest.json', 'icon.svg']; const SKIP_PATHS = ['/rest/stream', '/rest/download', '/config.js']; // Auth token params stripped from cache keys so artwork survives across sessions/token refreshes. diff --git a/src/views/detail.js b/src/views/detail.js index 3e46658..3b0b6fc 100644 --- a/src/views/detail.js +++ b/src/views/detail.js @@ -178,16 +178,21 @@ export async function loadArtistDetail(artist) { const itunesArtistId = knownItunes ?? iArtistMatch?.artistId ?? null; if (!knownFans) _showFans(iArtistMatch?.fanCount); // local artist: show the matched Deezer artist's fans - // Non-local (Deezer) artist has no Navidrome photo — fetch the Deezer artist picture for the hero. - if (!hasND && itunesArtistId && pfpWrap) { - const dzImg = iArtistMatch?.artworkUrl100 || await deezerArtistImage(itunesArtistId); - if (dzImg) { - const pfpImg = document.createElement('img'); - pfpImg.className = 'artist-hero-pfp'; - pfpImg.onload = pfpImg.onerror = () => { if (bgEl) bgEl.style.backgroundImage = `url('${dzImg}')`; _revealHero(); }; - pfpImg.src = dzImg; - pfpWrap.prepend(pfpImg); - document.getElementById('artist-hero-pfp-initial')?.style.setProperty('display', 'none'); + // Artist photo: Navidrome (from its metadata agents) first; if it has none, fall back to the + // matched Deezer artist's picture. Covers non-local Deezer artists AND niche local artists + // (e.g. N!GHT) that Navidrome's agents have no art for — the hero was blank for those before. + if (itunesArtistId && pfpWrap) { + const ndImg = await imgP.catch(() => null); + if (!ndImg) { + const dzImg = iArtistMatch?.artworkUrl100 || await deezerArtistImage(itunesArtistId); + if (dzImg) { + const pfpImg = document.createElement('img'); + pfpImg.className = 'artist-hero-pfp'; + pfpImg.onload = pfpImg.onerror = () => { if (bgEl) bgEl.style.backgroundImage = `url('${dzImg}')`; _revealHero(); }; + pfpImg.src = dzImg; + pfpWrap.prepend(pfpImg); + document.getElementById('artist-hero-pfp-initial')?.style.setProperty('display', 'none'); + } } }