fix(download): stop wrong album/cover from same-titled tracks; add MusicBrainz fallback

Root cause: deezer pickTrackMatch fell back to a blind title-only match, grabbing a
same-titled song by a completely different artist (e.g. 'Memories' by Rab Noakes for
'☆NoAki☆') and applying its album/cover/year. The download's clean fallback existed
but was never reached because the lookup almost always returned *something*.

Stufe A — deezer.js: the title-only fallback now REQUIRES an artist correspondence
(equal, or one name contained in the other after normalization). No match → {} →
the existing clean fallback finally runs (album_artist = song artist, album =
'Singles', cover = source thumbnail, year/genre untouched). Test that encoded the old
blind behaviour replaced; NoAki + loose-match regression tests added.

Stufe B — musicbrainz.js: a rate-limited (~1/s, User-Agent) MusicBrainz recording
lookup with the SAME artist discipline + Cover Art Archive cover. trackmeta.js is the
single resolver (Deezer → MusicBrainz → clean fallback); the three single-track call
sites use it (album batch stays Deezer-only to avoid MB rate-limit slowdowns).

Unit-tested (17/17) + live-verified: NoAki→clean fallback, real track→Deezer, MB finds
real albums when Deezer misses. Backend-only; no frontend/Android change.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
HoneyandClaude Opus 4.8 committed 2026-08-12 01:51:29 +02:00
1 parent 16f76a4236
commit 6d084a7b7a
6 files changed
+200 -9

No files matched your search

+14 -2
View File
@@ -69,8 +69,20 @@ function pickTrackMatch(results, artist, title) {
const strict = results.find(t => normalizeTitle(t.title) === normT && firstWord(t.artist?.name) === firstWord(normA));
const JP_RE = /[぀-ヿ㐀-䶿一-鿿]/;
const isJp = JP_RE.test(normT) || JP_RE.test(normA);
const titleOnly = results.find(t => normalizeTitle(t.title) === normT);
return strict || (!isJp ? titleOnly || null : null);
// Loose artist match: same title AND the artist names correspond (equal, or one contains the
// other after stripping all non-alphanumerics — covers "Okawari" vs "DJ Okawari"). This REPLACES
// the old blind title-only fallback, which grabbed a same-titled song by a completely different
// artist (e.g. "Memories" by Rab Noakes for "☆NoAki☆") and applied its wrong album/cover/year.
const _n = s => stripLeadSym((s || '').toLowerCase()).replace(/[^\p{L}\p{N}]+/gu, '');
const wantA = _n(normA);
const artistOk = candA => {
if (!wantA || !candA) return false;
if (candA === wantA) return true;
const short = Math.min(candA.length, wantA.length);
return short >= 3 && (candA.includes(wantA) || wantA.includes(candA));
};
const loose = results.find(t => normalizeTitle(t.title) === normT && artistOk(_n(t.artist?.name)));
return strict || (isJp ? null : loose) || null;
}
// Single-track metadata — the Deezer equivalent of the old iTunes lookupTrackMeta.