fix(backend): add ytmsearch5 step + expand YTM pool to 20 for better coverage

ytmsearch5 (yt-dlp native YTM search) added as step 2 — returns different
result ordering than the manual YTM URL, recovering songs that the URL
approach ranks too low. YTM pool expanded from 10 → 20 results to reduce
fallthrough to YouTube.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
HoneyandClaude Sonnet 4.6 committed 2026-06-19 10:25:37 +02:00
1 parent 4804023a98
commit 3c54850cca
1 file changed
+16 -12
+16 -12
View File
@@ -247,7 +247,7 @@ async function downloadTrack({ jobId, artist, rawArtist, albumArtist, title, alb
// Fetch YTM URLs once upfront so every cascade step filters the SAME result set. // Fetch YTM URLs once upfront so every cascade step filters the SAME result set.
// Re-fetching produced different orderings, causing the correct video (found in step 1 // Re-fetching produced different orderings, causing the correct video (found in step 1
// but failing the channel filter) to disappear by step 3 and a wrong one to slip through. // but failing the channel filter) to disappear by step 3 and a wrong one to slip through.
const ytmUrls = await getYtmVideoUrls(10); const ytmUrls = await getYtmVideoUrls(20);
const tryYtm = async (n, mfArgs) => { const tryYtm = async (n, mfArgs) => {
const urls = n ? ytmUrls.slice(0, n) : ytmUrls; const urls = n ? ytmUrls.slice(0, n) : ytmUrls;
if (!urls.length) return; if (!urls.length) return;
@@ -260,47 +260,51 @@ async function downloadTrack({ jobId, artist, rawArtist, albumArtist, title, alb
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 top-5 — strict: duration + title + variant + channel check // 1. YTM top-5 via manual URL — strict: duration + title + variant + channel check
await tryYtm(5, mf(dur, true, true)); await tryYtm(5, mf(dur, true, true));
if (!hasAudio()) { if (!hasAudio()) {
// 2. YT top-5 — strict: same filters including channel check // 2. ytmsearch5 (yt-dlp native YTM) — different result ordering than manual URL
await spawnYtdlp([`ytmsearch5:${query}`, ...PI_FILTERED, ...mf(dur, true, true), ...baseArgs]);
}
if (!hasAudio()) {
// 3. YT top-5 — strict: same filters including channel check
log(jobId, '⚠️ Kein YTM-Treffer — Fallback zu YouTube…'); log(jobId, '⚠️ Kein YTM-Treffer — Fallback zu YouTube…');
await spawnYtdlp([`ytsearch5:${query}`, ...PI_FILTERED, ...mf(dur, true, true), ...baseArgs]); await spawnYtdlp([`ytsearch5:${query}`, ...PI_FILTERED, ...mf(dur, true, true), ...baseArgs]);
} }
if (!hasAudio()) { if (!hasAudio()) {
// 3. YTM all-10 — no channel check (label channels, etc.) // 4. YTM all-20 — no channel check (label channels, etc.)
log(jobId, '⚠️ Erweitere Suche auf top 10 mit Dauerbeschränkung…'); log(jobId, '⚠️ Erweitere Suche auf top 20 mit Dauerbeschränkung…');
await tryYtm(0, mf(dur, true)); await tryYtm(0, mf(dur, true));
} }
if (!hasAudio()) { if (!hasAudio()) {
// 4. YT top-10 — no channel check // 5. YT top-10 — no channel check
await spawnYtdlp([`ytsearch10:${query}`, ...PI_FILTERED_10, ...mf(dur, true), ...baseArgs]); await spawnYtdlp([`ytsearch10:${query}`, ...PI_FILTERED_10, ...mf(dur, true), ...baseArgs]);
} }
if (!hasAudio()) { if (!hasAudio()) {
// 5. YTM all — no duration check // 6. YTM all — no duration check
log(jobId, '⚠️ Kein Treffer im Dauerbereich — suche ohne Dauerbeschränkung…'); log(jobId, '⚠️ Kein Treffer im Dauerbereich — suche ohne Dauerbeschränkung…');
await tryYtm(0, mf(null, true)); await tryYtm(0, mf(null, true));
} }
if (!hasAudio()) { if (!hasAudio()) {
// 6. YT top-5 — no duration check // 7. YT top-5 — no duration check
await spawnYtdlp([`ytsearch5:${query}`, ...PI_FILTERED, ...mf(null, true), ...baseArgs]); await spawnYtdlp([`ytsearch5:${query}`, ...PI_FILTERED, ...mf(null, true), ...baseArgs]);
} }
if (!hasAudio()) { if (!hasAudio()) {
// 7. YTM all — title only (no variant filter) // 8. YTM all — title only (no variant filter)
log(jobId, '⚠️ Letzter gefilterter Versuch…'); log(jobId, '⚠️ Letzter gefilterter Versuch…');
await tryYtm(0, mf(null, false)); await tryYtm(0, mf(null, false));
} }
if (!hasAudio()) { if (!hasAudio()) {
// 8. YT top-5 — title only // 9. YT top-5 — title only
await spawnYtdlp([`ytsearch5:${query}`, ...PI_FILTERED, ...mf(null, false), ...baseArgs]); await spawnYtdlp([`ytsearch5:${query}`, ...PI_FILTERED, ...mf(null, false), ...baseArgs]);
} }
if (!hasAudio()) { if (!hasAudio()) {
// 9. YTM — no filter at all, take first video result // 10. YTM — no filter at all, take first video result
log(jobId, '⚠️ Letzter Versuch: keine Filter…'); log(jobId, '⚠️ Letzter Versuch: keine Filter…');
await tryYtm(3, []); await tryYtm(3, []);
} }
if (!hasAudio()) { if (!hasAudio()) {
// 10. YT — no filter // 11. YT — no filter
await spawnYtdlp([`ytsearch1:${query}`, ...PI_UNFILTERED, ...baseArgs]); await spawnYtdlp([`ytsearch1:${query}`, ...PI_UNFILTERED, ...baseArgs]);
} }
} else { } else {