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 <noreply@anthropic.com>
This commit is contained in:
HoneyandClaude Sonnet 4.6 committed 2026-05-02 12:46:20 +02:00
1 parent 10a85e0497
commit 42658b3cbf
1 file changed
+32 -8
+32 -8
View File
@@ -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) {