fix(backend): add uploader/artist filter to yt-dlp match-filter

Strips special chars from artist name (☆NoAki☆ → NoAki) and adds
uploader *= check so wrong-artist results are rejected early.
Falls back to duration-only, then no-filter if artist filter yields nothing.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
HoneyandClaude Sonnet 4.6 committed 2026-05-02 10:53:32 +02:00
1 parent 801a3ae0fd
commit 34afeb77ba
1 file changed
+33 -7
+33 -7
View File
@@ -56,6 +56,15 @@ function sanitize(s) {
return (s || 'Unknown').replace(/[\\/:*?"<>|]/g, '_').trim().slice(0, 100);
}
// Extract a plain ASCII-ish word from an artist name for yt-dlp --match-filter uploader check.
// "☆NoAki☆" → "NoAki", "Chiru-san" → "Chiru", "A, B" → "A"
function artistFilterWord(name) {
const first = (name || '').split(/[,/•]/)[0].trim();
const clean = first.replace(/[^\w\s-]/g, '').trim(); // strip ☆ etc.
const word = clean.split(/[\s-]+/)[0]; // first word/segment
return word.length >= 3 ? word : '';
}
// Convert multi-artist strings to " / " separator so Navidrome splits correctly.
// Handles: "feat." / "ft." / "featuring" and comma-separated artists from Spotify/iTunes.
function navidromeArtist(name) {
@@ -218,33 +227,50 @@ async function downloadTrack({ jobId, artist, albumArtist, title, album, year, t
const hasAudio = () => fs.readdirSync(dir).some(f => AUDIO_EXTS.some(e => f.endsWith(`.${e}`)));
const ytmUrl = `https://music.youtube.com/search?q=${encodeURIComponent(query)}`;
const uploaderWord = artistFilterWord(mainArtist);
const uploaderFilter = uploaderWord ? ` & uploader *= ${uploaderWord}` : '';
if (durationMs) {
const durationSec = Math.round(durationMs / 1000);
const minDur = Math.max(30, Math.round(durationSec * 0.6));
const maxDur = Math.round(durationSec * 1.5);
const durFilter = ['--match-filter', `duration > ${minDur} & duration < ${maxDur} & !is_live`];
const durArtistFilter = ['--match-filter', `duration > ${minDur} & duration < ${maxDur} & !is_live${uploaderFilter}`];
const durOnlyFilter = ['--match-filter', `duration > ${minDur} & duration < ${maxDur} & !is_live`];
log(jobId, `🔍 Suche YT Music (~${durationSec}s): "${query}"`);
await spawnYtdlp([ytmUrl, '--playlist-items', '1', ...durFilter, ...baseArgs]);
await spawnYtdlp([ytmUrl, '--playlist-items', '1', ...durArtistFilter, ...baseArgs]);
if (!hasAudio()) {
log(jobId, '⚠️ Kein YT-Music-Treffer im Dauerbereich — Fallback zu YouTube…');
await spawnYtdlp([`ytsearch1:${query}`, ...durFilter, ...baseArgs]);
log(jobId, '⚠️ Kein YT-Music-Treffer (Dauer+Artist) — Fallback zu YouTube…');
await spawnYtdlp([`ytsearch1:${query}`, ...durArtistFilter, ...baseArgs]);
}
if (!hasAudio()) {
log(jobId, '⚠️ Kein YouTube-Treffer im Dauerbereich — Suche YT Music ohne Filter…');
log(jobId, '⚠️ Kein Treffer mit Artist-Filter — Suche nur nach Dauer…');
await spawnYtdlp([ytmUrl, '--playlist-items', '1', ...durOnlyFilter, ...baseArgs]);
}
if (!hasAudio()) {
await spawnYtdlp([`ytsearch1:${query}`, ...durOnlyFilter, ...baseArgs]);
}
if (!hasAudio()) {
log(jobId, '⚠️ Kein Treffer im Dauerbereich — Suche ohne Filter…');
await spawnYtdlp([ytmUrl, '--playlist-items', '1', ...baseArgs]);
}
if (!hasAudio()) {
log(jobId, '⚠️ Letzter Versuch: YouTube ohne Filter…');
await spawnYtdlp([`ytsearch1:${query}`, ...baseArgs]);
}
} else {
log(jobId, `🔍 Suche YT Music: "${query}"`);
await spawnYtdlp([ytmUrl, '--playlist-items', '1', ...baseArgs]);
await spawnYtdlp([ytmUrl, '--playlist-items', '1', ...(uploaderFilter ? ['--match-filter', `!is_live${uploaderFilter}`] : []), ...baseArgs]);
if (!hasAudio()) {
log(jobId, '⚠️ Kein YT-Music-Treffer — Fallback zu YouTube…');
await spawnYtdlp([`ytsearch1:${query}`, ...(uploaderFilter ? ['--match-filter', `!is_live${uploaderFilter}`] : []), ...baseArgs]);
}
if (!hasAudio() && uploaderFilter) {
log(jobId, '⚠️ Kein Treffer mit Artist-Filter — Suche ohne…');
await spawnYtdlp([ytmUrl, '--playlist-items', '1', ...baseArgs]);
}
if (!hasAudio()) {
await spawnYtdlp([`ytsearch1:${query}`, ...baseArgs]);
}
}