fix: download notification opened the wrong song (same-name album)

The finished-download notification resolved the album by name via search3, then fell back to
a same-named album by a DIFFERENT artist (or the first result) when the exact artist match
wasn't found — e.g. right after download, before Navidrome rescans. Many artists share an album
title ("I LIKE IT!"), so the notification navigated to a completely different song.

- Web (notify.js) + Android (AppNavigation): require the artist to match when known (fuzzy for
  multi-artist / album-artist drift); drop the name-only and first-result fallbacks. If nothing
  matches, show "wird evtl. gerade gescannt" instead of opening a wrong album.

The download itself was correct all along (verified: it grabbed UPSAHL's official "I LIKE IT!"
visualizer) — only the notification's navigation was wrong. SW ns-v63.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
HoneyandClaude Opus 4.8 committed 2026-08-01 02:30:53 +02:00
1 parent 1e206e8df3
commit 17bb527e1d
4 files changed
+23 -11

No files matched your search

+1
View File
@@ -80,6 +80,7 @@ Big change: **the entire metadata/search stack moved from Apple's iTunes to Deez
- **Backend** — Unit tests for the Deezer mapping/matching logic (`backend/test/deezer.test.js`, run with `npm test`), incl. per-track numbering, mixed-artist album-artist stability, explicit flags and the Japanese-title match guard. A live smoke test runs with `RUN_LIVE=1` - **Backend** — Unit tests for the Deezer mapping/matching logic (`backend/test/deezer.test.js`, run with `npm test`), incl. per-track numbering, mixed-artist album-artist stability, explicit flags and the Japanese-title match guard. A live smoke test runs with `RUN_LIVE=1`
### Fixed ### Fixed
- **Web + Android** — A finished-download **notification led to the wrong song**: it resolved the album by name via `search3`, and when the exact artist match wasn't found (e.g. the new album isn't scanned yet), it fell back to a same-named album by a **different** artist (or the first result). Several artists share an album title ("I LIKE IT!"), so tapping the notification opened a completely different song. Now the artist must match when known (fuzzy for multi-artist/album-artist drift); otherwise it shows "wird evtl. gerade gescannt" instead of opening a wrong album
- **Web** — Downloading a not-yet-owned album from an artist page searched YouTube for "**Unknown Artist** &lt;album&gt;" and failed. Deezer's `/artist/{id}/albums` omits the `artist` object, so the discography cards had an empty artist name → the modal fell back to the literal `"Unknown Artist"`. Fixed by backfilling the known artist name in `deezerLookupArtistAlbums` and defaulting the download modal to **Album** for album cards (was always **Track**) - **Web** — Downloading a not-yet-owned album from an artist page searched YouTube for "**Unknown Artist** &lt;album&gt;" and failed. Deezer's `/artist/{id}/albums` omits the `artist` object, so the discography cards had an empty artist name → the modal fell back to the literal `"Unknown Artist"`. Fixed by backfilling the known artist name in `deezerLookupArtistAlbums` and defaulting the download modal to **Album** for album cards (was always **Track**)
- **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)
@@ -576,13 +576,19 @@ fun AppNavigation() {
val res = repo.search(n.navAlbum) val res = repo.search(n.navAlbum)
val albums = res.album val albums = res.album
val norm = { s: String -> s.lowercase().trim() } val norm = { s: String -> s.lowercase().trim() }
val na = norm(n.navArtist)
// REQUIRE the artist match when we know it — several artists share an
// album title ("I LIKE IT!"); the old name-only / firstOrNull fallback
// opened a completely different artist's album (wrong song).
val match = albums.firstOrNull { val match = albums.firstOrNull {
norm(it.name) == norm(n.navAlbum) && val aa = norm(it.artist ?: "")
(n.navArtist.isBlank() || norm(it.artist ?: "") == norm(n.navArtist)) norm(it.name) == norm(n.navAlbum) && na.isNotBlank() &&
} ?: albums.firstOrNull { norm(it.name) == norm(n.navAlbum) } (aa == na || aa.contains(na) || na.contains(aa))
?: albums.firstOrNull() } ?: if (na.isBlank()) albums.firstOrNull { norm(it.name) == norm(n.navAlbum) } else null
if (match != null) { if (match != null) {
navController.navigate("album/${match.id}") navController.navigate("album/${match.id}")
} else {
Toast.makeText(context, "Album noch nicht gefunden — evtl. wird die Library gerade gescannt", Toast.LENGTH_SHORT).show()
} }
} }
} }
+1 -1
View File
@@ -1,4 +1,4 @@
const CACHE = 'ns-v62'; const CACHE = 'ns-v63';
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.
+11 -6
View File
@@ -121,16 +121,21 @@ export async function _notifNavigate(notifId) {
try { try {
const r = await api('search3', `&query=${encodeURIComponent(albumQuery)}&artistCount=0&albumCount=10&songCount=0`); const r = await api('search3', `&query=${encodeURIComponent(albumQuery)}&artistCount=0&albumCount=10&songCount=0`);
const albums = r.searchResult3?.album || []; const albums = r.searchResult3?.album || [];
const match = albums.find(a => norm(a.name) === norm(albumQuery) && norm(a.artist) === norm(n.navArtist)) const na = norm(n.navArtist);
|| albums.find(a => norm(a.name) === norm(albumQuery)) const nameEq = a => norm(a.name) === norm(albumQuery);
|| albums[0]; // Fuzzy artist compare (multi-artist / album-artist drift), but REQUIRE it when we know the
// artist: several artists share an album title ("I LIKE IT!"), and a name-only fallback used
// to open a completely different artist's album — the notification led to the wrong song.
const artistEq = a => { const aa = norm(a.artist); return !!na && (aa === na || aa.includes(na) || na.includes(aa)); };
const match = albums.find(a => nameEq(a) && artistEq(a))
|| (na ? null : albums.find(nameEq)); // name-only only when the artist is unknown
if (match) { if (match) {
navigate('album-detail', { id: match.id, name: match.name, artist: match.artist || '', artistId: match.artistId || '', coverArt: match.coverArt || '' }); navigate('album-detail', { id: match.id, name: match.name, artist: match.artist || '', artistId: match.artistId || '', coverArt: match.coverArt || '' });
return; return;
} }
} catch {} } catch {}
} }
toast('Album nicht gefunden — Library-Scan nötig?'); toast('Album noch nicht gefunden — evtl. wird die Library gerade gescannt');
return; return;
} }
@@ -138,9 +143,9 @@ export async function _notifNavigate(notifId) {
try { try {
const r = await api('search3', `&query=${encodeURIComponent(n.navArtist)}&artistCount=5&albumCount=0&songCount=0`); const r = await api('search3', `&query=${encodeURIComponent(n.navArtist)}&artistCount=5&albumCount=0&songCount=0`);
const artists = r.searchResult3?.artist || []; const artists = r.searchResult3?.artist || [];
const match = artists.find(a => norm(a.name) === norm(n.navArtist)) || artists[0]; const match = artists.find(a => norm(a.name) === norm(n.navArtist)); // no blind artists[0]
if (match) { navigate('artist-detail', { id: match.id, name: match.name }); return; } if (match) { navigate('artist-detail', { id: match.id, name: match.name }); return; }
} catch {} } catch {}
toast('Künstler nicht gefunden — Library-Scan nötig?'); toast('Künstler noch nicht gefunden — evtl. wird die Library gerade gescannt');
} }
} }