From bb0be52f46d02a5e99cd0225085e44be7e23faba Mon Sep 17 00:00:00 2001 From: Honey Date: Fri, 19 Jun 2026 10:32:22 +0200 Subject: [PATCH] fix(backend): fix getYtmVideoUrls always returning empty + remove unsupported ytmsearch5 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [youtube:tab] in stderr fires for all YTM search page processing, not just artist channel expansion, causing channelExpanded=true for every search and returning [] — bypassing all YTM cascade steps entirely. Fix: only check stdout for /browse/UC (actual channel URL in results). Remove ytmsearch5: scheme which is not supported by this yt-dlp version. Revert pool to 10. Co-Authored-By: Claude Sonnet 4.6 --- backend/lib/ytdlp.js | 37 +++++++++++++++++-------------------- 1 file changed, 17 insertions(+), 20 deletions(-) diff --git a/backend/lib/ytdlp.js b/backend/lib/ytdlp.js index dc5dd58..9ff4e29 100644 --- a/backend/lib/ytdlp.js +++ b/backend/lib/ytdlp.js @@ -202,16 +202,14 @@ async function downloadTrack({ jobId, artist, rawArtist, albumArtist, title, alb // actual video URLs (watch?v=…). This prevents yt-dlp from following an artist channel // result and treating its uploads as the search result set. // - // yt-dlp expands "url_transparent" artist channel entries even under --flat-playlist, - // printing the channel's video URLs instead of the channel URL. We detect this by - // watching for [youtube:tab] in output (which only fires during channel/tab extraction) - // and return [] so the caller falls through to the ytsearch fallback. + // When YTM returns an artist channel as the top hit, yt-dlp prints the channel's + // browse URL (/browse/UC…) instead of video URLs. We detect this on stdout and + // return [] so the caller falls through to the ytsearch fallback. + // NOTE: do NOT check for [youtube:tab] in stderr — that fires for all YTM search + // page processing, not just channel expansion, and would wrongly return [] always. const getYtmVideoUrls = (maxItems) => new Promise(resolve => { const urls = []; let channelExpanded = false; - const checkExpansion = (text) => { - if (text.includes('[youtube:tab]') || text.includes('/browse/UC')) channelExpanded = true; - }; const proc = spawn(CONFIG.ytdlpBin, [ ytmUrl, '--flat-playlist', @@ -221,7 +219,7 @@ async function downloadTrack({ jobId, artist, rawArtist, albumArtist, title, alb ], { env: { ...process.env, PYTHONUNBUFFERED: '1' } }); proc.stdout.on('data', d => { const text = d.toString(); - checkExpansion(text); + if (text.includes('/browse/UC')) { channelExpanded = true; return; } if (!channelExpanded) { text.split('\n').forEach(line => { const u = line.trim(); @@ -229,9 +227,12 @@ async function downloadTrack({ jobId, artist, rawArtist, albumArtist, title, alb }); } }); - proc.stderr.on('data', d => checkExpansion(d.toString())); proc.on('error', () => resolve([])); - proc.on('close', () => resolve(channelExpanded ? [] : urls)); + proc.on('close', () => { + if (channelExpanded) log(jobId, `⚠️ YTM: Artist-Channel in Suchergebnissen — überspringe`); + else log(jobId, `📡 YTM: ${urls.length} Ergebnis(se) gefunden`); + resolve(channelExpanded ? [] : urls); + }); }); const mf = (dur, variant, strict = false) => { @@ -247,7 +248,7 @@ async function downloadTrack({ jobId, artist, rawArtist, albumArtist, title, alb // 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(20); + const ytmUrls = await getYtmVideoUrls(10); const tryYtm = async (n, mfArgs) => { const urls = n ? ytmUrls.slice(0, n) : ytmUrls; if (!urls.length) return; @@ -260,24 +261,20 @@ async function downloadTrack({ jobId, artist, rawArtist, albumArtist, title, alb const dur = { min: Math.max(30, durationSec - margin), max: durationSec + margin }; log(jobId, `🔍 Suche YT Music (~${durationSec}s ±${margin}s): "${query}"`); - // 1. YTM top-5 via manual URL — strict: duration + title + variant + channel check + // 1. YTM top-5 — strict: duration + title + variant + channel check await tryYtm(5, mf(dur, true, true)); if (!hasAudio()) { - // 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 + // 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()) { - // 4. YTM all-20 — no channel check (label channels, etc.) - log(jobId, '⚠️ Erweitere Suche auf top 20 mit Dauerbeschränkung…'); + // 3. YTM all-10 — no channel check (label channels, etc.) + log(jobId, '⚠️ Erweitere Suche auf top 10 mit Dauerbeschränkung…'); await tryYtm(0, mf(dur, true)); } if (!hasAudio()) { - // 5. YT top-10 — no channel check + // 4. YT top-10 — no channel check await spawnYtdlp([`ytsearch10:${query}`, ...PI_FILTERED_10, ...mf(dur, true), ...baseArgs]); } if (!hasAudio()) {