feat: Deezer artist photo fallback for local artists with no Navidrome image
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 <noreply@anthropic.com>
This commit is contained in:
1 parent
640e475c51
commit
569af968c5
5 files changed
+26
-12
No files matched your search
@@ -80,6 +80,7 @@ Big change: **the entire metadata/search stack moved from Apple's iTunes to Deez
|
|||||||
|
|
||||||
### Fixed
|
### 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** — 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
|
### 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`
|
- 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`
|
||||||
|
|||||||
@@ -218,7 +218,10 @@ fun ArtistDetailScreen(
|
|||||||
|
|
||||||
// Hero card
|
// Hero card
|
||||||
item {
|
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() }
|
val imgUrl = state.info?.largeImageUrl?.takeIf { it.isNotBlank() }
|
||||||
|
?: state.deezerImage
|
||||||
?: coverUrl(artist.coverArt)
|
?: coverUrl(artist.coverArt)
|
||||||
Box(
|
Box(
|
||||||
Modifier
|
Modifier
|
||||||
|
|||||||
@@ -43,6 +43,8 @@ data class ArtistUiState(
|
|||||||
val showEventsSheet: Boolean = false,
|
val showEventsSheet: Boolean = false,
|
||||||
// Deezer fan count of the matched artist (shown under the name), null if unknown
|
// Deezer fan count of the matched artist (shown under the name), null if unknown
|
||||||
val fanCount: Long? = null,
|
val fanCount: Long? = null,
|
||||||
|
// Deezer artist photo — hero fallback when Navidrome's agents have no image (niche artists)
|
||||||
|
val deezerImage: String? = null,
|
||||||
// Preview + download
|
// Preview + download
|
||||||
val previewingId: Long? = null,
|
val previewingId: Long? = null,
|
||||||
val downloadSheetItem: ItunesItem? = null,
|
val downloadSheetItem: ItunesItem? = null,
|
||||||
@@ -121,7 +123,10 @@ class ArtistViewModel(app: Application, savedState: SavedStateHandle) : AndroidV
|
|||||||
?: artists.firstOrNull()
|
?: artists.firstOrNull()
|
||||||
)
|
)
|
||||||
val iArtistId = iArtist?.artistId
|
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) {
|
if (iArtistId != null) {
|
||||||
coroutineScope {
|
coroutineScope {
|
||||||
|
|||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
const CACHE = 'ns-v55';
|
const CACHE = 'ns-v56';
|
||||||
const PRECACHE = ['/', 'index.html', 'manifest.json', 'icon.svg'];
|
const PRECACHE = ['/', 'index.html', 'manifest.json', 'icon.svg'];
|
||||||
const SKIP_PATHS = ['/rest/stream', '/rest/download', '/config.js'];
|
const SKIP_PATHS = ['/rest/stream', '/rest/download', '/config.js'];
|
||||||
// Auth token params stripped from cache keys so artwork survives across sessions/token refreshes.
|
// Auth token params stripped from cache keys so artwork survives across sessions/token refreshes.
|
||||||
|
|||||||
+15
-10
@@ -178,16 +178,21 @@ export async function loadArtistDetail(artist) {
|
|||||||
const itunesArtistId = knownItunes ?? iArtistMatch?.artistId ?? null;
|
const itunesArtistId = knownItunes ?? iArtistMatch?.artistId ?? null;
|
||||||
if (!knownFans) _showFans(iArtistMatch?.fanCount); // local artist: show the matched Deezer artist's fans
|
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.
|
// Artist photo: Navidrome (from its metadata agents) first; if it has none, fall back to the
|
||||||
if (!hasND && itunesArtistId && pfpWrap) {
|
// matched Deezer artist's picture. Covers non-local Deezer artists AND niche local artists
|
||||||
const dzImg = iArtistMatch?.artworkUrl100 || await deezerArtistImage(itunesArtistId);
|
// (e.g. N!GHT) that Navidrome's agents have no art for — the hero was blank for those before.
|
||||||
if (dzImg) {
|
if (itunesArtistId && pfpWrap) {
|
||||||
const pfpImg = document.createElement('img');
|
const ndImg = await imgP.catch(() => null);
|
||||||
pfpImg.className = 'artist-hero-pfp';
|
if (!ndImg) {
|
||||||
pfpImg.onload = pfpImg.onerror = () => { if (bgEl) bgEl.style.backgroundImage = `url('${dzImg}')`; _revealHero(); };
|
const dzImg = iArtistMatch?.artworkUrl100 || await deezerArtistImage(itunesArtistId);
|
||||||
pfpImg.src = dzImg;
|
if (dzImg) {
|
||||||
pfpWrap.prepend(pfpImg);
|
const pfpImg = document.createElement('img');
|
||||||
document.getElementById('artist-hero-pfp-initial')?.style.setProperty('display', 'none');
|
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');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user