fix(backend): Kanaltreffer in der YTM-Suche verwirft nicht mehr die Trefferliste

getYtmVideoUrls wertete jedes /browse/UC in der Ausgabe als "yt-dlp hat einen
Artist-Channel aufgeklappt" und gab [] zurueck — samt der bereits gesammelten
Song-URLs. Bei "DAT ADAM UFO" stand der Kanal auf Platz 9 und riss sieben gute
Treffer mit, darunter den offiziellen Art Track auf Platz 1; uebrig blieb der
YouTube-Fallback mit einem Live-Mitschnitt. An 8 echten Suchen gemessen feuerte
das in 7 Faellen, Schritt 1 der Kaskade war also praktisch tot.

Mit --flat-playlist kann gar nichts aufgeklappt werden, ein Kanal ist EINE Zeile.
pickYtmVideoUrls() behaelt daher alle watch?v=-Zeilen und ueberspringt browse/UC
(Kanal) wie browse/MPREb_ (Album). Ueber 20k Zufallsausgaben geprueft: die neue
Funktion ist eine Obermenge der alten, kann also nur Kandidaten hinzufuegen.
Drei echte Downloads liefen end-to-end sauber durch (Dauer sekundengenau auf
Deezer, korrekte Tags), alle aus Schritt 1 statt aus dem Fallback.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
marcandClaude Opus 5 committed 2026-09-04 22:51:32 +02:00
1 parent 33b3df609a
commit eee98d193f
3 files changed
+68 -19

No files matched your search

+29 -19
View File
@@ -55,6 +55,28 @@ function buildTitleKeywords(title) {
return result;
}
// Aus der YTM-Suchausgabe (--flat-playlist --print webpage_url) die Song-URLs herausziehen.
// Eine Suche liefert drei Sorten Zeilen: watch?v=… (ein Song), browse/MPREb_… (ein Album) und
// browse/UC… (ein Kuenstlerkanal). Nur die erste ist ein Kandidat; die anderen beiden sind
// Listeneintraege und werden uebersprungen.
//
// Frueher galt eine browse/UC-Zeile als "yt-dlp hat einen Kanal aufgeklappt" und die GESAMTE
// Trefferliste wurde verworfen — auch die bereits gesammelten Songs davor. Bei "DAT ADAM UFO" stand
// der Kanal auf Platz 9 und riss sieben gute Treffer mit, darunter den offiziellen Art Track auf
// Platz 1; uebrig blieb der YouTube-Fallback, der einen Live-Mitschnitt zog. Aufklappen kann hier
// ohnehin nicht passieren: mit --flat-playlist druckt yt-dlp einen Kanal als EINE Zeile.
function pickYtmVideoUrls(stdout) {
const urls = [];
let skipped = 0;
for (const line of (stdout || '').split('\n')) {
const u = line.trim();
if (!u) continue;
if (u.includes('watch?v=')) urls.push(u);
else if (u.includes('/browse/')) skipped++;
}
return { urls, skipped };
}
function buildVariantFilter(title) {
const t = (title || '').toLowerCase();
// !~= (?i) = case-insensitive regex "does not match". Use \b word boundaries so that
@@ -231,14 +253,10 @@ 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.
//
// 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;
let out = '';
const proc = spawn(CONFIG.ytdlpBin, [
ytmUrl,
'--flat-playlist',
@@ -250,21 +268,13 @@ async function downloadTrack({ jobId, artist, rawArtist, albumArtist, title, alb
// native Songs shelf (where the actual artist track lives).
'--extractor-args', 'youtube:player_client=web_music,default',
], { env: { ...process.env, PYTHONUNBUFFERED: '1' } });
proc.stdout.on('data', d => {
const text = d.toString();
if (text.includes('/browse/UC')) { channelExpanded = true; return; }
if (!channelExpanded) {
text.split('\n').forEach(line => {
const u = line.trim();
if (u.includes('watch?v=')) urls.push(u);
});
}
});
proc.stdout.on('data', d => { out += d.toString(); });
proc.on('error', () => resolve([]));
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 { urls, skipped } = pickYtmVideoUrls(out);
if (!urls.length) log(jobId, `⚠️ YTM: keine Song-Treffer (${skipped} Listeneintrag/-eintraege)`);
else log(jobId, `📡 YTM: ${urls.length} Ergebnis(se) gefunden${skipped ? ` (${skipped} Album/Kanal uebersprungen)` : ''}`);
resolve(urls);
});
});
@@ -453,4 +463,4 @@ async function embedMetadata({ jobId, audioFile, dir, trackPrefix, safeTitle, ra
return { success: true, dir };
}
module.exports = { downloadTrack, embedMetadata, buildTitleKeywords, buildVariantFilter };
module.exports = { downloadTrack, embedMetadata, buildTitleKeywords, buildVariantFilter, pickYtmVideoUrls };