diff --git a/backend/novasonic-server.js b/backend/novasonic-server.js index 02c2fa7..c6505ff 100644 --- a/backend/novasonic-server.js +++ b/backend/novasonic-server.js @@ -68,25 +68,30 @@ function artistFilterWord(name) { } // Extract distinctive words from a track title for case-insensitive yt-dlp title ~= (?i) filters. -// Returns an array: [mainWord, optionalMixQualifier]. -// "Memories" → ["Memories"] (matches "MEMORIES", "memories", etc.) -// "Eclipse - Radio Mix" → ["Eclipse", "radio"] +// Returns up to 2 of the most distinctive words so the filter is precise: +// "Echoes of Eternity" → ["Eternity", "Echoes"] → title ~= (?i)Eternity & title ~= (?i)Echoes +// "Amethyst Flowers" → ["Amethyst", "Flowers?"] → title ~= (?i)Amethyst & title ~= (?i)Flowers? +// "Whispers" → ["Whispers?"] → title ~= (?i)Whispers? +// "Eclipse - Radio Mix"→ ["Eclipse", "Radio"] → title ~= (?i)Eclipse & title ~= (?i)Radio +// Trailing 's' is made optional (Whispers? matches "Whisper" or "Whispers") to handle +// cases where the YouTube video title drops the plural form. function buildTitleKeywords(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(); - // Keep original case so the word looks right in logs, case-insensitivity is handled by (?i) flag const words = (title || '') .replace(/[^\w\s]/g, ' ') .split(/\s+/) .filter(w => w.length >= 3 && !VARIANT_WORDS.has(w.toLowerCase()) && !STOPWORDS.has(w.toLowerCase())); - const main = words.sort((a, b) => b.length - a.length)[0] || ''; - const qualifier = lower.includes('mix') ? MIX_QUALIFIERS.find(q => lower.includes(q)) : null; - // Make trailing 's' optional so "Whispers?" matches both "Whisper" and "Whispers" in yt-dlp regex + // Take top 2 by length — two words together are far more precise than one alone + const top2 = words.sort((a, b) => b.length - a.length).slice(0, 2); + // Make trailing 's' optional so "Whispers?" matches "Whisper" or "Whispers" const toPattern = w => (w.length >= 4 && w.endsWith('s')) ? w + '?' : w; - const result = main ? [toPattern(main)] : []; - if (qualifier && main.toLowerCase() !== qualifier) result.push(qualifier); + const result = top2.map(toPattern); + // Add mix qualifier only if neither top word already covers it + const qualifier = lower.includes('mix') ? MIX_QUALIFIERS.find(q => lower.includes(q)) : null; + if (qualifier && !top2.some(w => w.toLowerCase() === qualifier)) result.push(qualifier); return result; }