fix(backend): fix getYtmVideoUrls always returning empty + remove unsupported ytmsearch5

[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 <noreply@anthropic.com>
This commit is contained in:
HoneyandClaude Sonnet 4.6 committed 2026-06-19 10:32:22 +02:00
1 parent 3c54850cca
commit bb0be52f46
1 file changed
+17 -20
+17 -20
View File
@@ -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()) {