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:
1 parent
10a85e0497
commit
42658b3cbf
1 file changed
+32
-8
@@ -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) {
|
||||
|
||||
Reference in new issue
Block a user