From 42658b3cbfcf3059ded94da6438ba9dfe6f70cc8 Mon Sep 17 00:00:00 2001 From: Honey Date: Sat, 2 May 2026 12:46:20 +0200 Subject: [PATCH] fix(backend): improve yt-dlp match filters for accuracy - artistFilterWord: use longest \w+ run instead of stripping special chars, fixes artists like N!GHT where stripping gave NGHT (not a substring) - buildTitleKeyword: new fn extracts most distinctive title word, added as title *= filter to all cascade steps to block unrelated fallback downloads - Mix version specificity: Radio/Extended/Club Mix titles also filter on the qualifier word so plain versions are rejected - Switch uploader *= to artist *= so label-uploaded tracks (Monstercat etc.) match by music metadata artist tag, not channel name Co-Authored-By: Claude Sonnet 4.6 --- backend/novasonic-server.js | 40 +++++++++++++++++++++++++++++-------- 1 file changed, 32 insertions(+), 8 deletions(-) diff --git a/backend/novasonic-server.js b/backend/novasonic-server.js index 9486b4f..e3c73d8 100644 --- a/backend/novasonic-server.js +++ b/backend/novasonic-server.js @@ -56,13 +56,35 @@ 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. +// Extract a plain word from an artist name for yt-dlp --match-filter uploader check. +// Uses longest contiguous \w+ run so special chars like "N!GHT" → "N" + "GHT" picks "GHT", +// but actually picks longest: "N!GHT" → runs ["N","GHT"] → "GHT" (3 chars). // "☆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 : ''; + const runs = first.match(/\w+/g) || []; + const best = runs.filter(r => r.length >= 3).sort((a, b) => b.length - a.length)[0]; + return best || ''; +} + +// Extract the most distinctive word from a track title to use as a title relevance filter. +// Prevents fallback steps from downloading completely unrelated tracks. +// Also preserves version qualifiers ("radio mix", "extended mix", "club mix", "vip mix") so +// "Eclipse - Radio Mix" filters for "eclipse" AND "radio" to avoid downloading the plain version. +function buildTitleKeyword(title) { + const VARIANT_WORDS = new Set(['sped','speedup','slowed','reverb','nightcore','karaoke','parody','remix','cover','acoustic','instrumental']); + const STOPWORDS = new Set(['the','a','an','and','or','of','in','on','at','to','for','feat','ft','with']); + const MIX_QUALIFIERS = ['radio', 'extended', 'club', 'vip', 'original', 'edit']; + const lower = (title || '').toLowerCase(); + const words = lower + .replace(/[^\w\s]/g, ' ') + .split(/\s+/) + .filter(w => w.length >= 3 && !VARIANT_WORDS.has(w) && !STOPWORDS.has(w)); + const main = words.sort((a, b) => b.length - a.length)[0] || ''; + // If the title has a mix qualifier AND "mix", include the qualifier word too + const qualifier = lower.includes('mix') ? MIX_QUALIFIERS.find(q => lower.includes(q)) : null; + if (qualifier && qualifier !== main) return `${main} & title *= ${qualifier}`; + return main; } // Build yt-dlp --match-filter conditions to reject variant versions (speed up, slowed, remix…) @@ -261,15 +283,17 @@ async function downloadTrack({ jobId, artist, albumArtist, title, album, year, t const uploaderWord = artistFilterWord(mainArtist); const variantFilter = buildVariantFilter(title); - const uploaderPart = uploaderWord ? ` & uploader *= ${uploaderWord}` : ''; + const titleKeyword = buildTitleKeyword(title); + const uploaderPart = uploaderWord ? ` & artist *= ${uploaderWord}` : ''; const variantPart = variantFilter ? ` & ${variantFilter}` : ''; + const titleKwPart = titleKeyword ? ` & title *= ${titleKeyword}` : ''; - // Build a --match-filter arg from active parts (duration, artist, variant). - // !is_live is always included. + // Build a --match-filter arg from active parts (duration, artist, variant, title keyword). + // !is_live is always included. titleKw always included to avoid totally unrelated downloads. const mf = (dur, artist, variant) => { const durPart = dur ? `duration > ${dur.min} & duration < ${dur.max} & ` : ''; return ['--match-filter', - `${durPart}!is_live${artist ? uploaderPart : ''}${variant ? variantPart : ''}`]; + `${durPart}!is_live${titleKwPart}${artist ? uploaderPart : ''}${variant ? variantPart : ''}`]; }; if (durationMs) {