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:
1 parent
33b3df609a
commit
eee98d193f
3 files changed
+68
-19
No files matched your search
@@ -110,6 +110,14 @@ On the deploy host the music lives at `/opt/navidrome/music`.
|
||||
`live` rule like remix/cover/…. It self-disables when the searched title itself says live, so
|
||||
"Live Forever" or a deliberately requested live version still works — that check uses a `\blive\b`
|
||||
regex (`skipRe`), because a plain substring test would let "deliver"/"delivery" switch the rule off.
|
||||
- **A channel hit in the YTM results is a line to skip, not a reason to abort.** `getYtmVideoUrls`
|
||||
used to treat any `/browse/UC…` in the output as "yt-dlp expanded an artist channel" and returned
|
||||
`[]` — discarding the song URLs it had already collected. Measured on 8 real searches, that fired
|
||||
in **7 of 8**, so step 1 (YTM, where the official Art Tracks live) was effectively dead and almost
|
||||
every download came from the YouTube fallback. With `--flat-playlist` a channel is printed as ONE
|
||||
line, so expansion cannot happen; `pickYtmVideoUrls()` now keeps every `watch?v=` line and skips
|
||||
`browse/UC…` (channel) and `browse/MPREb_…` (album) alike. It is a strict superset of the old
|
||||
behaviour — fuzz-checked over 20k random outputs — so it can only add candidates, never remove any.
|
||||
- **Size cap:** `--max-filesize 30M` on every yt-dlp call; Mittelweg steps also cap at 2×
|
||||
expected duration. Prevents saving a 28-min video (52 MB) when the wrong result slips through.
|
||||
- **Leading aesthetic characters:** artists often style their name with leading punctuation
|
||||
|
||||
+29
-19
@@ -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 };
|
||||
@@ -76,3 +76,34 @@ test('"live" inside another word does not disable the rule', () => {
|
||||
assert.equal(variantAccepts('Deliver', 'Artist - Deliver (Live at Wembley)'), false);
|
||||
assert.equal(variantAccepts('Deliver', 'Artist - Deliver'), true);
|
||||
});
|
||||
|
||||
const { pickYtmVideoUrls } = require('../lib/ytdlp.js');
|
||||
|
||||
// Echte Ausgabe der YTM-Suche "DAT ADAM UFO": Songs, ein Album (MPREb_) und der
|
||||
// Kuenstlerkanal (UC…) auf Platz 9.
|
||||
const YTM_OUT = [
|
||||
'https://music.youtube.com/watch?v=ZG4XhPn3Yb4',
|
||||
'https://music.youtube.com/watch?v=2KVU6aikZF0',
|
||||
'https://music.youtube.com/browse/MPREb_IhxJPixC1QT',
|
||||
'https://music.youtube.com/watch?v=phLOTtJaqZk',
|
||||
'https://music.youtube.com/watch?v=p935nH6aj8Y',
|
||||
'https://music.youtube.com/watch?v=Pb2YcDBm5fA',
|
||||
'https://music.youtube.com/browse/MPREb_S7MMKVMduma',
|
||||
'https://music.youtube.com/watch?v=mNfsX2RbZ2M',
|
||||
'https://music.youtube.com/browse/UC5hilNPCUHEPf4VpG5_YpYA',
|
||||
'https://music.youtube.com/watch?v=O034JUlf1J4',
|
||||
].join('\n');
|
||||
|
||||
test('a channel hit no longer discards the whole result list', () => {
|
||||
const { urls, skipped } = pickYtmVideoUrls(YTM_OUT);
|
||||
assert.equal(urls.length, 7);
|
||||
assert.equal(skipped, 3); // 2 Alben + 1 Kanal
|
||||
assert.match(urls[0], /ZG4XhPn3Yb4/); // der offizielle Art Track
|
||||
assert.ok(!urls.some(u => u.includes('/browse/')));
|
||||
});
|
||||
|
||||
test('nothing but list entries yields no candidates', () => {
|
||||
const only = 'https://music.youtube.com/browse/UC5hilNPCUHEPf4VpG5_YpYA';
|
||||
assert.deepEqual(pickYtmVideoUrls(only).urls, []);
|
||||
assert.deepEqual(pickYtmVideoUrls('').urls, []);
|
||||
});
|
||||
Reference in new issue
Block a user