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:
1 parent
3c54850cca
commit
bb0be52f46
1 file changed
+17
-20
+17
-20
@@ -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
|
// actual video URLs (watch?v=…). This prevents yt-dlp from following an artist channel
|
||||||
// result and treating its uploads as the search result set.
|
// result and treating its uploads as the search result set.
|
||||||
//
|
//
|
||||||
// yt-dlp expands "url_transparent" artist channel entries even under --flat-playlist,
|
// When YTM returns an artist channel as the top hit, yt-dlp prints the channel's
|
||||||
// printing the channel's video URLs instead of the channel URL. We detect this by
|
// browse URL (/browse/UC…) instead of video URLs. We detect this on stdout and
|
||||||
// watching for [youtube:tab] in output (which only fires during channel/tab extraction)
|
// return [] so the caller falls through to the ytsearch fallback.
|
||||||
// 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 getYtmVideoUrls = (maxItems) => new Promise(resolve => {
|
||||||
const urls = [];
|
const urls = [];
|
||||||
let channelExpanded = false;
|
let channelExpanded = false;
|
||||||
const checkExpansion = (text) => {
|
|
||||||
if (text.includes('[youtube:tab]') || text.includes('/browse/UC')) channelExpanded = true;
|
|
||||||
};
|
|
||||||
const proc = spawn(CONFIG.ytdlpBin, [
|
const proc = spawn(CONFIG.ytdlpBin, [
|
||||||
ytmUrl,
|
ytmUrl,
|
||||||
'--flat-playlist',
|
'--flat-playlist',
|
||||||
@@ -221,7 +219,7 @@ async function downloadTrack({ jobId, artist, rawArtist, albumArtist, title, alb
|
|||||||
], { env: { ...process.env, PYTHONUNBUFFERED: '1' } });
|
], { env: { ...process.env, PYTHONUNBUFFERED: '1' } });
|
||||||
proc.stdout.on('data', d => {
|
proc.stdout.on('data', d => {
|
||||||
const text = d.toString();
|
const text = d.toString();
|
||||||
checkExpansion(text);
|
if (text.includes('/browse/UC')) { channelExpanded = true; return; }
|
||||||
if (!channelExpanded) {
|
if (!channelExpanded) {
|
||||||
text.split('\n').forEach(line => {
|
text.split('\n').forEach(line => {
|
||||||
const u = line.trim();
|
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('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) => {
|
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.
|
// 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(20);
|
const ytmUrls = await getYtmVideoUrls(10);
|
||||||
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,24 +261,20 @@ 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 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));
|
await tryYtm(5, mf(dur, true, true));
|
||||||
if (!hasAudio()) {
|
if (!hasAudio()) {
|
||||||
// 2. ytmsearch5 (yt-dlp native YTM) — different result ordering than manual URL
|
// 2. YT top-5 — strict: same filters including channel check
|
||||||
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()) {
|
||||||
// 4. YTM all-20 — no channel check (label channels, etc.)
|
// 3. YTM all-10 — no channel check (label channels, etc.)
|
||||||
log(jobId, '⚠️ Erweitere Suche auf top 20 mit Dauerbeschränkung…');
|
log(jobId, '⚠️ Erweitere Suche auf top 10 mit Dauerbeschränkung…');
|
||||||
await tryYtm(0, mf(dur, true));
|
await tryYtm(0, mf(dur, true));
|
||||||
}
|
}
|
||||||
if (!hasAudio()) {
|
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]);
|
await spawnYtdlp([`ytsearch10:${query}`, ...PI_FILTERED_10, ...mf(dur, true), ...baseArgs]);
|
||||||
}
|
}
|
||||||
if (!hasAudio()) {
|
if (!hasAudio()) {
|
||||||
|
|||||||
Reference in new issue
Block a user