fix(backend): scan top 5 results per search instead of only item 1

--playlist-items 1-5 --max-downloads 1 on all filtered steps so yt-dlp
skips non-matching results (wrong artist, sped up, wrong duration) and
lands on the first result that actually passes all active filters.
Unfiltered last-resort fallbacks keep --playlist-items 1.
YouTube fallback uses ytsearch5: to match the 5-result window.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
HoneyandClaude Sonnet 4.6 committed 2026-05-02 12:00:49 +02:00
1 parent cf8ba70af8
commit 5db9938f49
1 file changed
+19 -16
+19 -16
View File
@@ -218,10 +218,13 @@ async function downloadTrack({ jobId, artist, albumArtist, title, album, year, t
'--audio-quality', CONFIG.audioQuality, '--audio-quality', CONFIG.audioQuality,
'-o', outputTemplate, '-o', outputTemplate,
'--no-playlist', '--no-playlist',
'--playlist-items', '1',
'--no-warnings', '--no-warnings',
'--newline', '--newline',
]; ];
// Filtered searches scan top 5 results and stop at first match.
// Unfiltered fallback only needs item 1 (no filter to skip past).
const PI_FILTERED = ['--playlist-items', '1-5', '--max-downloads', '1'];
const PI_UNFILTERED = ['--playlist-items', '1'];
const spawnYtdlp = (args) => new Promise((resolve, reject) => { const spawnYtdlp = (args) => new Promise((resolve, reject) => {
const proc = spawn(CONFIG.ytdlpBin, args, { env: { ...process.env, PYTHONUNBUFFERED: '1' } }); const proc = spawn(CONFIG.ytdlpBin, args, { env: { ...process.env, PYTHONUNBUFFERED: '1' } });
@@ -274,61 +277,61 @@ async function downloadTrack({ jobId, artist, albumArtist, title, album, year, t
const dur = { min: Math.max(30, durationSec - margin), max: durationSec + margin }; const dur = { min: Math.max(30, durationSec - margin), max: durationSec + margin };
log(jobId, `🔍 Suche YT Music (~${durationSec}s ±${margin}s): "${query}"`); log(jobId, `🔍 Suche YT Music (~${durationSec}s ±${margin}s): "${query}"`);
// 1. YTM — Dauer + Artist + Varianten-Filter (streng) // 1. YTM — Dauer + Artist + Varianten-Filter (Top 5, erster Treffer)
await tryYtdlp([ytmUrl, '--playlist-items', '1', ...mf(dur, true, true), ...baseArgs]); await tryYtdlp([ytmUrl, ...PI_FILTERED, ...mf(dur, true, true), ...baseArgs]);
if (!hasAudio()) { if (!hasAudio()) {
// 2. YT — Dauer + Artist + Varianten-Filter // 2. YT — Dauer + Artist + Varianten-Filter
log(jobId, '⚠️ Kein YTM-Treffer — Fallback zu YouTube…'); log(jobId, '⚠️ Kein YTM-Treffer — Fallback zu YouTube…');
await tryYtdlp([`ytsearch1:${query}`, ...mf(dur, true, true), ...baseArgs]); await tryYtdlp([`ytsearch5:${query}`, ...PI_FILTERED, ...mf(dur, true, true), ...baseArgs]);
} }
if (!hasAudio()) { if (!hasAudio()) {
// 3. YTM — Dauer + Varianten-Filter (kein Artist) // 3. YTM — Dauer + Varianten-Filter (kein Artist)
log(jobId, '⚠️ Kein Treffer mit Artist-Filter — ohne Artist…'); log(jobId, '⚠️ Kein Treffer mit Artist-Filter — ohne Artist…');
await tryYtdlp([ytmUrl, '--playlist-items', '1', ...mf(dur, false, true), ...baseArgs]); await tryYtdlp([ytmUrl, ...PI_FILTERED, ...mf(dur, false, true), ...baseArgs]);
} }
if (!hasAudio()) { if (!hasAudio()) {
// 4. YT — Dauer + Varianten-Filter // 4. YT — Dauer + Varianten-Filter
await tryYtdlp([`ytsearch1:${query}`, ...mf(dur, false, true), ...baseArgs]); await tryYtdlp([`ytsearch5:${query}`, ...PI_FILTERED, ...mf(dur, false, true), ...baseArgs]);
} }
if (!hasAudio()) { if (!hasAudio()) {
// 5. YTM — nur Varianten-Filter (kein Dauer-Check) // 5. YTM — nur Varianten-Filter (kein Dauer-Check)
log(jobId, '⚠️ Kein Treffer im Dauerbereich — suche ohne Dauerbeschränkung…'); log(jobId, '⚠️ Kein Treffer im Dauerbereich — suche ohne Dauerbeschränkung…');
await tryYtdlp([ytmUrl, '--playlist-items', '1', ...mf(null, false, true), ...baseArgs]); await tryYtdlp([ytmUrl, ...PI_FILTERED, ...mf(null, false, true), ...baseArgs]);
} }
if (!hasAudio()) { if (!hasAudio()) {
// 6. YT — nur Varianten-Filter // 6. YT — nur Varianten-Filter
await tryYtdlp([`ytsearch1:${query}`, ...mf(null, false, true), ...baseArgs]); await tryYtdlp([`ytsearch5:${query}`, ...PI_FILTERED, ...mf(null, false, true), ...baseArgs]);
} }
if (!hasAudio()) { if (!hasAudio()) {
// 7+8. Letzter Ausweg: komplett ungefiltert // 7+8. Letzter Ausweg: komplett ungefiltert
log(jobId, '⚠️ Letzter Versuch: keine Filter…'); log(jobId, '⚠️ Letzter Versuch: keine Filter…');
await tryYtdlp([ytmUrl, '--playlist-items', '1', ...baseArgs]); await tryYtdlp([ytmUrl, ...PI_UNFILTERED, ...baseArgs]);
} }
if (!hasAudio()) { if (!hasAudio()) {
await tryYtdlp([`ytsearch1:${query}`, ...baseArgs]); await tryYtdlp([`ytsearch1:${query}`, ...PI_UNFILTERED, ...baseArgs]);
} }
} else { } else {
log(jobId, `🔍 Suche YT Music: "${query}"`); log(jobId, `🔍 Suche YT Music: "${query}"`);
// Ohne Dauer: Artist + Varianten → ohne Artist → ohne alles // Ohne Dauer: Artist + Varianten → ohne Artist → ohne alles
await tryYtdlp([ytmUrl, '--playlist-items', '1', ...mf(null, true, true), ...baseArgs]); await tryYtdlp([ytmUrl, ...PI_FILTERED, ...mf(null, true, true), ...baseArgs]);
if (!hasAudio()) { if (!hasAudio()) {
await tryYtdlp([`ytsearch1:${query}`, ...mf(null, true, true), ...baseArgs]); await tryYtdlp([`ytsearch5:${query}`, ...PI_FILTERED, ...mf(null, true, true), ...baseArgs]);
} }
if (!hasAudio()) { if (!hasAudio()) {
log(jobId, '⚠️ Kein Treffer mit Artist — ohne Artist-Filter…'); log(jobId, '⚠️ Kein Treffer mit Artist — ohne Artist-Filter…');
await tryYtdlp([ytmUrl, '--playlist-items', '1', ...mf(null, false, true), ...baseArgs]); await tryYtdlp([ytmUrl, ...PI_FILTERED, ...mf(null, false, true), ...baseArgs]);
} }
if (!hasAudio()) { if (!hasAudio()) {
await tryYtdlp([`ytsearch1:${query}`, ...mf(null, false, true), ...baseArgs]); await tryYtdlp([`ytsearch5:${query}`, ...PI_FILTERED, ...mf(null, false, true), ...baseArgs]);
} }
if (!hasAudio()) { if (!hasAudio()) {
log(jobId, '⚠️ Letzter Versuch: keine Filter…'); log(jobId, '⚠️ Letzter Versuch: keine Filter…');
await tryYtdlp([ytmUrl, '--playlist-items', '1', ...baseArgs]); await tryYtdlp([ytmUrl, ...PI_UNFILTERED, ...baseArgs]);
} }
if (!hasAudio()) { if (!hasAudio()) {
await tryYtdlp([`ytsearch1:${query}`, ...baseArgs]); await tryYtdlp([`ytsearch1:${query}`, ...PI_UNFILTERED, ...baseArgs]);
} }
} }