From 17bb527e1d8d4e8d3e07577b23bfac99bd510e7e Mon Sep 17 00:00:00 2001 From: Honey Date: Sat, 1 Aug 2026 02:30:53 +0200 Subject: [PATCH] fix: download notification opened the wrong song (same-name album) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- CHANGELOG.md | 1 + .../com/novasonic/app/ui/AppNavigation.kt | 14 ++++++++++---- public/sw.js | 2 +- src/ui/notify.js | 17 +++++++++++------ 4 files changed, 23 insertions(+), 11 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 51d52ce..c8ea1d1 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 - **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 +- **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** <album>" 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) diff --git a/android/app/src/main/kotlin/com/novasonic/app/ui/AppNavigation.kt b/android/app/src/main/kotlin/com/novasonic/app/ui/AppNavigation.kt index 7683be0..d432533 100644 --- a/android/app/src/main/kotlin/com/novasonic/app/ui/AppNavigation.kt +++ b/android/app/src/main/kotlin/com/novasonic/app/ui/AppNavigation.kt @@ -576,13 +576,19 @@ fun AppNavigation() { val res = repo.search(n.navAlbum) val albums = res.album 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 { - norm(it.name) == norm(n.navAlbum) && - (n.navArtist.isBlank() || norm(it.artist ?: "") == norm(n.navArtist)) - } ?: albums.firstOrNull { norm(it.name) == norm(n.navAlbum) } - ?: albums.firstOrNull() + val aa = norm(it.artist ?: "") + norm(it.name) == norm(n.navAlbum) && na.isNotBlank() && + (aa == na || aa.contains(na) || na.contains(aa)) + } ?: if (na.isBlank()) albums.firstOrNull { norm(it.name) == norm(n.navAlbum) } else null if (match != null) { navController.navigate("album/${match.id}") + } else { + Toast.makeText(context, "Album noch nicht gefunden — evtl. wird die Library gerade gescannt", Toast.LENGTH_SHORT).show() } } } diff --git a/public/sw.js b/public/sw.js index b320312..594c816 100644 --- a/public/sw.js +++ b/public/sw.js @@ -1,4 +1,4 @@ -const CACHE = 'ns-v62'; +const CACHE = 'ns-v63'; 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/ui/notify.js b/src/ui/notify.js index 34b26b0..2919335 100644 --- a/src/ui/notify.js +++ b/src/ui/notify.js @@ -121,16 +121,21 @@ export async function _notifNavigate(notifId) { try { const r = await api('search3', `&query=${encodeURIComponent(albumQuery)}&artistCount=0&albumCount=10&songCount=0`); const albums = r.searchResult3?.album || []; - const match = albums.find(a => norm(a.name) === norm(albumQuery) && norm(a.artist) === norm(n.navArtist)) - || albums.find(a => norm(a.name) === norm(albumQuery)) - || albums[0]; + const na = norm(n.navArtist); + const nameEq = a => norm(a.name) === norm(albumQuery); + // 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) { navigate('album-detail', { id: match.id, name: match.name, artist: match.artist || '', artistId: match.artistId || '', coverArt: match.coverArt || '' }); return; } } catch {} } - toast('Album nicht gefunden — Library-Scan nötig?'); + toast('Album noch nicht gefunden — evtl. wird die Library gerade gescannt'); return; } @@ -138,9 +143,9 @@ export async function _notifNavigate(notifId) { try { const r = await api('search3', `&query=${encodeURIComponent(n.navArtist)}&artistCount=5&albumCount=0&songCount=0`); 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; } } catch {} - toast('Künstler nicht gefunden — Library-Scan nötig?'); + toast('Künstler noch nicht gefunden — evtl. wird die Library gerade gescannt'); } }