From 34afeb77ba7f8d0716743d00b6c9a24f2c0e1588 Mon Sep 17 00:00:00 2001 From: Honey Date: Sat, 2 May 2026 10:53:32 +0200 Subject: [PATCH] fix(backend): add uploader/artist filter to yt-dlp match-filter MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- backend/novasonic-server.js | 40 ++++++++++++++++++++++++++++++------- 1 file changed, 33 insertions(+), 7 deletions(-) diff --git a/backend/novasonic-server.js b/backend/novasonic-server.js index e1506fb..74386fb 100644 --- a/backend/novasonic-server.js +++ b/backend/novasonic-server.js @@ -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]); } }