From a870f50c87d2017d5a5c6c3c6336465202923652 Mon Sep 17 00:00:00 2001 From: Honey Date: Fri, 19 Jun 2026 18:13:59 +0200 Subject: [PATCH] =?UTF-8?q?feat(backend):=20Mittelweg=20cascade=20?= =?UTF-8?q?=E2=80=94=20never=20download=20wrong=20song=20on=20no=20duratio?= =?UTF-8?q?n=20match?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When a track's duration is known but nothing matches the duration window, the cascade no longer falls back to blind title-only/no-filter grabs (which pulled same-titled videos by other uploaders, e.g. a "recognize" fan clip). Instead it relaxes only the duration constraint while still REQUIRING the artist channel + title. If nothing matches, the download produces no file and the job now fails cleanly with "Nicht eindeutig gefunden — bitte Direktlink im Importer nutzen" (previously it falsely reported success with no file). Single, album and playlist paths all surface the not-found state. Co-Authored-By: Claude Opus 4.8 --- backend/lib/downloader.js | 4 +++- backend/lib/ytdlp.js | 37 ++++++++++++------------------------- backend/novasonic-server.js | 11 ++++++++--- 3 files changed, 23 insertions(+), 29 deletions(-) diff --git a/backend/lib/downloader.js b/backend/lib/downloader.js index e40695f..af58761 100644 --- a/backend/lib/downloader.js +++ b/backend/lib/downloader.js @@ -33,7 +33,9 @@ async function downloadAlbumTracks({ jobId, artist, albumTitle, tracks, artworkU outputDir: CONFIG.musicPath, durationMs: track.trackTimeMillis || undefined, }); - if (result?.skipped) skipped++; else done++; + if (result?.skipped) skipped++; + else if (result?.notFound) log(jobId, `⚠️ Nicht eindeutig gefunden: "${track.trackName || track.title}" — Direktlink nutzen`); + else done++; updateJob(jobId, { progress: Math.round(((done + skipped) / total) * 90) }); } catch (e) { log(jobId, `⚠️ Fehler bei "${track.trackName || track.title}": ${e.message}`); diff --git a/backend/lib/ytdlp.js b/backend/lib/ytdlp.js index f1289ca..78fa23b 100644 --- a/backend/lib/ytdlp.js +++ b/backend/lib/ytdlp.js @@ -295,32 +295,19 @@ async function downloadTrack({ jobId, artist, rawArtist, albumArtist, title, alb // 4. YT top-10 — no channel check await spawnYtdlp([`ytsearch10:${query}`, ...PI_FILTERED_10, ...mf(dur, true), ...baseArgs]); } + // No duration match. "Mittelweg": relax the duration constraint but REQUIRE the artist + // channel (+ title). This catches legit tracks whose YT upload duration drifts from the + // iTunes value, while never grabbing a same-titled video by someone else. If nothing + // matches title+channel we download NOTHING and the job fails cleanly (see end of fn) — + // far better than silently saving the wrong song (e.g. a "recognize" fan clip). if (!hasAudio()) { - // 6. YTM all — no duration check - log(jobId, '⚠️ Kein Treffer im Dauerbereich — suche ohne Dauerbeschränkung…'); - await tryYtm(0, mf(null, true)); + // 5. YTM all — no duration, but channel + title required + log(jobId, '⚠️ Kein Treffer im Dauerbereich — suche ohne Dauer, aber mit Künstler-Channel…'); + await tryYtm(0, mf(null, true, true)); } if (!hasAudio()) { - // 7. YT top-5 — no duration check - await spawnYtdlp([`ytsearch5:${query}`, ...PI_FILTERED, ...mf(null, true), ...baseArgs]); - } - if (!hasAudio()) { - // 8. YTM all — title only (no variant filter) - log(jobId, '⚠️ Letzter gefilterter Versuch…'); - await tryYtm(0, mf(null, false)); - } - if (!hasAudio()) { - // 9. YT top-5 — title only - await spawnYtdlp([`ytsearch5:${query}`, ...PI_FILTERED, ...mf(null, false), ...baseArgs]); - } - if (!hasAudio()) { - // 10. YTM — no filter at all, take first video result - log(jobId, '⚠️ Letzter Versuch: keine Filter…'); - await tryYtm(3, []); - } - if (!hasAudio()) { - // 11. YT — no filter - await spawnYtdlp([`ytsearch1:${query}`, ...PI_UNFILTERED, ...baseArgs]); + // 6. YT top-10 — no duration, but channel + title required + await spawnYtdlp([`ytsearch10:${query}`, ...PI_FILTERED_10, ...mf(null, true, true), ...baseArgs]); } } else { log(jobId, `🔍 Suche YT Music: "${query}"`); @@ -360,8 +347,8 @@ async function downloadTrack({ jobId, artist, rawArtist, albumArtist, title, alb const audioFile = hasAudio() ? expectedAudio : null; if (!audioFile) { - log(jobId, '⚠️ Keine Audio-Datei gefunden'); - return { success: true, dir }; + log(jobId, '⚠️ Nicht eindeutig gefunden — bitte Direktlink im Importer nutzen'); + return { success: false, notFound: true, dir }; } return embedMetadata({ jobId, audioFile, dir, trackPrefix, safeTitle, rawArtist, artist, mainArtist, album, year, trackNum, totalTracks, discNum, discTotal, compilation, genre, artworkUrl, explicit, title: searchTitle }); diff --git a/backend/novasonic-server.js b/backend/novasonic-server.js index 177bc65..62b74fe 100644 --- a/backend/novasonic-server.js +++ b/backend/novasonic-server.js @@ -601,6 +601,8 @@ app.post('/api/download', async (req, res) => { const result = await downloadTrack({ jobId, artist: finalArtist, rawArtist: artist, title, ...meta, sourceUrl: sourceUrl || sp.sourceUrl || undefined }); if (result?.skipped) { updateJob(jobId, { status: 'done', progress: 100, message: 'Bereits vorhanden – übersprungen', skipped: true }); + } else if (result?.notFound) { + updateJob(jobId, { status: 'error', message: 'Nicht eindeutig gefunden — bitte Direktlink im Importer nutzen' }); } else { updateJob(jobId, { status: 'done', progress: 100, message: 'Fertig!' }); } @@ -653,7 +655,7 @@ app.post('/api/download', async (req, res) => { const plTracks = req.body.tracks || []; if (!plTracks.length) throw new Error('Keine Tracks übergeben'); log(jobId, `🎵 Playlist "${title}" — ${plTracks.length} Tracks`); - let done = 0, skipped = 0; + let done = 0, skipped = 0, notFound = 0; const CONC = CONFIG.maxParallel || 2; for (let i = 0; i < plTracks.length; i += CONC) { const batch = plTracks.slice(i, i + CONC); @@ -709,10 +711,13 @@ app.post('/api/download', async (req, res) => { log(jobId, `⚠️ Fehler bei "${batch[j].trackName || '?'}": ${r.reason?.message || r.reason}`); } else if (r.value?.skipped) { skipped++; + } else if (r.value?.notFound) { + notFound++; + log(jobId, `⚠️ Nicht eindeutig gefunden: "${batch[j].trackName || '?'}" — Direktlink nutzen`); } else { done++; } - updateJob(jobId, { progress: Math.round(((done + skipped) / plTracks.length) * 90) }); + updateJob(jobId, { progress: Math.round(((done + skipped + notFound) / plTracks.length) * 90) }); } } const allSkipped = skipped === plTracks.length; @@ -720,7 +725,7 @@ app.post('/api/download', async (req, res) => { status: 'done', progress: 100, skipped: allSkipped, message: allSkipped ? `Alle ${skipped} Tracks bereits vorhanden` - : `Fertig! ${done} geladen${skipped ? `, ${skipped} übersprungen` : ''}`, + : `Fertig! ${done} geladen${skipped ? `, ${skipped} übersprungen` : ''}${notFound ? `, ${notFound} nicht gefunden` : ''}`, }); } } catch (err) {