fix: fetch YTM URLs once and reuse across cascade steps

Each tryYtmSearch() call made a fresh HTTP request, so YouTube could
return different result orderings between steps. The correct video
(right title, wrong channel name) appeared in step 1 but was gone by
step 3 — a worse video with matching keywords slipped through instead.

Now fetch 10 YTM URLs once upfront and filter the same list with
progressively relaxed rules. Also saves 2-4 extra YTM requests per
download.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
HoneyandClaude Sonnet 4.6 committed 2026-05-17 20:39:07 +02:00
1 parent c037ee623d
commit 7bec106350
1 file changed
+24 -23
+24 -23
View File
@@ -145,15 +145,6 @@ async function downloadTrack({ jobId, artist, albumArtist, title, album, year, t
proc.on('close', () => resolve(channelExpanded ? [] : urls));
});
// Search YTM for video URLs (pre-filtered), then pass them all at once to yt-dlp.
// --max-downloads 1 makes yt-dlp stop after the first video that passes mfArgs.
// Filtered/skipped videos do NOT count toward --max-downloads, so the cascade works.
const tryYtmSearch = async (maxItems, mfArgs) => {
const urls = await getYtmVideoUrls(maxItems);
if (!urls.length) return;
await spawnYtdlp([...urls, '--max-downloads', '1', ...mfArgs, ...baseArgs]);
};
const variantFilter = buildVariantFilter(title);
const titleKws = buildTitleKeywords(title);
const variantPart = variantFilter ? ` & ${variantFilter}` : '';
@@ -177,6 +168,16 @@ async function downloadTrack({ jobId, artist, albumArtist, title, album, year, t
const PI_FILTERED_10 = ['--playlist-items', '1-10', '--max-downloads', '1'];
const PI_UNFILTERED = ['--playlist-items', '1'];
// 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
// but failing the channel filter) to disappear by step 3 and a wrong one to slip through.
const ytmUrls = await getYtmVideoUrls(10);
const tryYtm = async (n, mfArgs) => {
const urls = n ? ytmUrls.slice(0, n) : ytmUrls;
if (!urls.length) return;
await spawnYtdlp([...urls, '--max-downloads', '1', ...mfArgs, ...baseArgs]);
};
if (durationMs) {
const durationSec = Math.round(durationMs / 1000);
const margin = Math.max(15, Math.round(durationSec * 0.15));
@@ -184,34 +185,34 @@ async function downloadTrack({ jobId, artist, albumArtist, title, album, year, t
log(jobId, `🔍 Suche YT Music (~${durationSec}s ±${margin}s): "${query}"`);
// 1. YTM top-5 — strict: duration + title + variant + channel check
await tryYtmSearch(5, mf(dur, true, true));
await tryYtm(5, mf(dur, true, true));
if (!hasAudio()) {
// 2. YT top-5 — strict: same filters including channel check
log(jobId, '⚠️ Kein YTM-Treffer — Fallback zu YouTube…');
await spawnYtdlp([`ytsearch5:${query}`, ...PI_FILTERED, ...mf(dur, true, true), ...baseArgs]);
}
if (!hasAudio()) {
// 3. YTM top-10 — no channel check (label channels, etc.)
// 3. YTM all-10 — no channel check (label channels, etc.)
log(jobId, '⚠️ Erweitere Suche auf top 10 mit Dauerbeschränkung…');
await tryYtmSearch(10, mf(dur, true));
await tryYtm(0, mf(dur, true));
}
if (!hasAudio()) {
// 4. YT top-10 — no channel check
await spawnYtdlp([`ytsearch10:${query}`, ...PI_FILTERED_10, ...mf(dur, true), ...baseArgs]);
}
if (!hasAudio()) {
// 5. YTM top-5 — no duration check
// 5. YTM all — no duration check
log(jobId, '⚠️ Kein Treffer im Dauerbereich — suche ohne Dauerbeschränkung…');
await tryYtmSearch(5, mf(null, true));
await tryYtm(0, mf(null, true));
}
if (!hasAudio()) {
// 6. YT top-5 — no duration check
await spawnYtdlp([`ytsearch5:${query}`, ...PI_FILTERED, ...mf(null, true), ...baseArgs]);
}
if (!hasAudio()) {
// 7. YTM top-5 — title only (no variant filter)
// 7. YTM all — title only (no variant filter)
log(jobId, '⚠️ Letzter gefilterter Versuch…');
await tryYtmSearch(5, mf(null, false));
await tryYtm(0, mf(null, false));
}
if (!hasAudio()) {
// 8. YT top-5 — title only
@@ -220,7 +221,7 @@ async function downloadTrack({ jobId, artist, albumArtist, title, album, year, t
if (!hasAudio()) {
// 9. YTM — no filter at all, take first video result
log(jobId, '⚠️ Letzter Versuch: keine Filter…');
await tryYtmSearch(3, []);
await tryYtm(3, []);
}
if (!hasAudio()) {
// 10. YT — no filter
@@ -229,23 +230,23 @@ async function downloadTrack({ jobId, artist, albumArtist, title, album, year, t
} else {
log(jobId, `🔍 Suche YT Music: "${query}"`);
// 1. YTM top-5 — strict: title + variant + channel check
await tryYtmSearch(5, mf(null, true, true));
await tryYtm(5, mf(null, true, true));
if (!hasAudio()) {
// 2. YT top-5 — strict
await spawnYtdlp([`ytsearch5:${query}`, ...PI_FILTERED, ...mf(null, true, true), ...baseArgs]);
}
if (!hasAudio()) {
// 3. YTM top-5 — no channel check
await tryYtmSearch(5, mf(null, true));
// 3. YTM all-10 — no channel check (catches label/distributor channels)
await tryYtm(0, mf(null, true));
}
if (!hasAudio()) {
// 4. YT top-5 — no channel check
await spawnYtdlp([`ytsearch5:${query}`, ...PI_FILTERED, ...mf(null, true), ...baseArgs]);
}
if (!hasAudio()) {
// 5. YTM top-5 — title only
// 5. YTM all — title only
log(jobId, '⚠️ Letzter gefilterter Versuch…');
await tryYtmSearch(5, mf(null, false));
await tryYtm(0, mf(null, false));
}
if (!hasAudio()) {
// 6. YT top-5 — title only
@@ -254,7 +255,7 @@ async function downloadTrack({ jobId, artist, albumArtist, title, album, year, t
if (!hasAudio()) {
// 7. YTM — first video result, no filter
log(jobId, '⚠️ Letzter Versuch: keine Filter…');
await tryYtmSearch(3, []);
await tryYtm(3, []);
}
if (!hasAudio()) {
// 8. YT — no filter