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:
1 parent
c037ee623d
commit
7bec106350
1 file changed
+24
-23
+24
-23
@@ -145,15 +145,6 @@ async function downloadTrack({ jobId, artist, albumArtist, title, album, year, t
|
|||||||
proc.on('close', () => resolve(channelExpanded ? [] : urls));
|
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 variantFilter = buildVariantFilter(title);
|
||||||
const titleKws = buildTitleKeywords(title);
|
const titleKws = buildTitleKeywords(title);
|
||||||
const variantPart = variantFilter ? ` & ${variantFilter}` : '';
|
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_FILTERED_10 = ['--playlist-items', '1-10', '--max-downloads', '1'];
|
||||||
const PI_UNFILTERED = ['--playlist-items', '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) {
|
if (durationMs) {
|
||||||
const durationSec = Math.round(durationMs / 1000);
|
const durationSec = Math.round(durationMs / 1000);
|
||||||
const margin = Math.max(15, Math.round(durationSec * 0.15));
|
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}"`);
|
log(jobId, `🔍 Suche YT Music (~${durationSec}s ±${margin}s): "${query}"`);
|
||||||
// 1. YTM top-5 — strict: duration + title + variant + channel check
|
// 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()) {
|
if (!hasAudio()) {
|
||||||
// 2. YT top-5 — strict: same filters including channel check
|
// 2. 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 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…');
|
log(jobId, '⚠️ Erweitere Suche auf top 10 mit Dauerbeschränkung…');
|
||||||
await tryYtmSearch(10, mf(dur, true));
|
await tryYtm(0, mf(dur, true));
|
||||||
}
|
}
|
||||||
if (!hasAudio()) {
|
if (!hasAudio()) {
|
||||||
// 4. YT top-10 — no channel check
|
// 4. 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 top-5 — no duration check
|
// 5. 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 tryYtmSearch(5, mf(null, true));
|
await tryYtm(0, mf(null, true));
|
||||||
}
|
}
|
||||||
if (!hasAudio()) {
|
if (!hasAudio()) {
|
||||||
// 6. YT top-5 — no duration check
|
// 6. 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 top-5 — title only (no variant filter)
|
// 7. YTM all — title only (no variant filter)
|
||||||
log(jobId, '⚠️ Letzter gefilterter Versuch…');
|
log(jobId, '⚠️ Letzter gefilterter Versuch…');
|
||||||
await tryYtmSearch(5, mf(null, false));
|
await tryYtm(0, mf(null, false));
|
||||||
}
|
}
|
||||||
if (!hasAudio()) {
|
if (!hasAudio()) {
|
||||||
// 8. YT top-5 — title only
|
// 8. YT top-5 — title only
|
||||||
@@ -220,7 +221,7 @@ async function downloadTrack({ jobId, artist, albumArtist, title, album, year, t
|
|||||||
if (!hasAudio()) {
|
if (!hasAudio()) {
|
||||||
// 9. YTM — no filter at all, take first video result
|
// 9. YTM — no filter at all, take first video result
|
||||||
log(jobId, '⚠️ Letzter Versuch: keine Filter…');
|
log(jobId, '⚠️ Letzter Versuch: keine Filter…');
|
||||||
await tryYtmSearch(3, []);
|
await tryYtm(3, []);
|
||||||
}
|
}
|
||||||
if (!hasAudio()) {
|
if (!hasAudio()) {
|
||||||
// 10. YT — no filter
|
// 10. YT — no filter
|
||||||
@@ -229,23 +230,23 @@ async function downloadTrack({ jobId, artist, albumArtist, title, album, year, t
|
|||||||
} else {
|
} else {
|
||||||
log(jobId, `🔍 Suche YT Music: "${query}"`);
|
log(jobId, `🔍 Suche YT Music: "${query}"`);
|
||||||
// 1. YTM top-5 — strict: title + variant + channel check
|
// 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()) {
|
if (!hasAudio()) {
|
||||||
// 2. YT top-5 — strict
|
// 2. YT top-5 — strict
|
||||||
await spawnYtdlp([`ytsearch5:${query}`, ...PI_FILTERED, ...mf(null, true, true), ...baseArgs]);
|
await spawnYtdlp([`ytsearch5:${query}`, ...PI_FILTERED, ...mf(null, true, true), ...baseArgs]);
|
||||||
}
|
}
|
||||||
if (!hasAudio()) {
|
if (!hasAudio()) {
|
||||||
// 3. YTM top-5 — no channel check
|
// 3. YTM all-10 — no channel check (catches label/distributor channels)
|
||||||
await tryYtmSearch(5, mf(null, true));
|
await tryYtm(0, mf(null, true));
|
||||||
}
|
}
|
||||||
if (!hasAudio()) {
|
if (!hasAudio()) {
|
||||||
// 4. YT top-5 — no channel check
|
// 4. YT top-5 — no channel 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()) {
|
||||||
// 5. YTM top-5 — title only
|
// 5. YTM all — title only
|
||||||
log(jobId, '⚠️ Letzter gefilterter Versuch…');
|
log(jobId, '⚠️ Letzter gefilterter Versuch…');
|
||||||
await tryYtmSearch(5, mf(null, false));
|
await tryYtm(0, mf(null, false));
|
||||||
}
|
}
|
||||||
if (!hasAudio()) {
|
if (!hasAudio()) {
|
||||||
// 6. YT top-5 — title only
|
// 6. YT top-5 — title only
|
||||||
@@ -254,7 +255,7 @@ async function downloadTrack({ jobId, artist, albumArtist, title, album, year, t
|
|||||||
if (!hasAudio()) {
|
if (!hasAudio()) {
|
||||||
// 7. YTM — first video result, no filter
|
// 7. YTM — first video result, no filter
|
||||||
log(jobId, '⚠️ Letzter Versuch: keine Filter…');
|
log(jobId, '⚠️ Letzter Versuch: keine Filter…');
|
||||||
await tryYtmSearch(3, []);
|
await tryYtm(3, []);
|
||||||
}
|
}
|
||||||
if (!hasAudio()) {
|
if (!hasAudio()) {
|
||||||
// 8. YT — no filter
|
// 8. YT — no filter
|
||||||
|
|||||||
Reference in new issue
Block a user