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

+4 -3
View File
@@ -19,6 +19,7 @@ const CONFIG = require('./lib/config');
const { jobs, updateJob, log } = require('./lib/jobs');
const { sanitize, findExistingTrack } = require('./lib/metadata');
const dz = require('./lib/deezer');
const tm = require('./lib/trackmeta'); // Deezer → MusicBrainz → clean fallback (single tracks)
const { downloadTrack, downloadAlbumTracks } = require('./lib/downloader');
const { buildRomajiIndex, clearRomajiCache, isTokenizerReady, fetchNdSongs, romajiForStorage } = require('./lib/romaji');
@@ -289,7 +290,7 @@ app.get('/api/meta/lookup', async (req, res) => {
const { artist, title } = req.query;
if (!artist || !title) return res.status(400).json({ error: 'artist and title required' });
try {
const meta = await dz.lookupTrackMeta(artist, title);
const meta = await tm.lookupTrackMeta(artist, title);
if (!meta.album && !meta.genre && !meta.year) return res.json({ found: false });
res.json({
found: true,
@@ -524,7 +525,7 @@ app.post('/api/youtube/resolve', async (req, res) => {
const title = e.title || '';
const sourceUrl = e.webpage_url || url;
let meta = {};
try { meta = await dz.lookupTrackMeta(rawArtist.split(',')[0].trim(), title); } catch {}
try { meta = await tm.lookupTrackMeta(rawArtist.split(',')[0].trim(), title); } catch {}
const artist = meta.artist || rawArtist;
const artworkUrl = meta.artworkUrl || e.thumbnail || '';
@@ -583,7 +584,7 @@ app.post('/api/download', async (req, res) => {
const searchArtist = artist.split(',')[0].trim();
if (!meta.album) {
log(jobId, '📋 Suche Album-Metadaten von Deezer…');
const extra = await dz.lookupTrackMeta(searchArtist, title);
const extra = await tm.lookupTrackMeta(searchArtist, title);
if (extra.album) log(jobId, `📋 Album: "${extra.album}" (${extra.year || '?'})`);
meta = { ...meta, ...extra, artworkUrl: spArt || extra.artworkUrl || '', genre: meta.genre || extra.genre || undefined };
} else {