From 4f065ee022318ecbafd4178924f1c5116485be36 Mon Sep 17 00:00:00 2001 From: Lucas Santana Date: Mon, 13 Apr 2026 16:46:59 -0300 Subject: [PATCH 1/3] refactor(autoplay): move noise terms to noiseTerms.json, add sped-up/slowed/reverb MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - noiseTerms.json holds versionVariants and bareTitleNoise as plain strings - searchQueryCleaner builds parenthetical, bracket, and bare-word regex from JSON at init - no regex knowledge needed to add new dedup terms — just edit the JSON - adds: sped up, speed up, slowed, reverb, slowed reverb, nightcore, 8d audio, bass boosted - removes hardcoded duplicate patterns for terms now covered by JSON --- packages/bot/src/utils/music/noiseTerms.json | 36 +++++++++ .../utils/music/searchQueryCleaner.spec.ts | 28 +++++++ .../bot/src/utils/music/searchQueryCleaner.ts | 76 ++++++++++++------- 3 files changed, 114 insertions(+), 26 deletions(-) create mode 100644 packages/bot/src/utils/music/noiseTerms.json diff --git a/packages/bot/src/utils/music/noiseTerms.json b/packages/bot/src/utils/music/noiseTerms.json new file mode 100644 index 000000000..acb266fff --- /dev/null +++ b/packages/bot/src/utils/music/noiseTerms.json @@ -0,0 +1,36 @@ +{ + "versionVariants": [ + "sped up", + "speed up", + "slowed", + "reverb", + "slowed reverb", + "nightcore", + "8d audio", + "bass boosted", + "live", + "acoustic", + "demo", + "remix", + "instrumental", + "karaoke", + "cover", + "extended", + "explicit", + "clean", + "remaster", + "remastered", + "deluxe", + "original mix", + "original version", + "radio edit", + "bonus track", + "single version", + "album version" + ], + "bareTitleNoise": [ + "lyrics", + "legendado", + "traduzido" + ] +} diff --git a/packages/bot/src/utils/music/searchQueryCleaner.spec.ts b/packages/bot/src/utils/music/searchQueryCleaner.spec.ts index 7a03ebd01..0872f8d99 100644 --- a/packages/bot/src/utils/music/searchQueryCleaner.spec.ts +++ b/packages/bot/src/utils/music/searchQueryCleaner.spec.ts @@ -64,6 +64,34 @@ describe('cleanTitle', () => { it('handles non-ASCII titles without mangling them', () => { expect(cleanTitle('夜に駆ける (Official Video)')).toBe('夜に駆ける') }) + + it('strips (Sped Up) / (Speed Up) variants', () => { + expect(cleanTitle('Flowers (Sped Up)')).toBe('Flowers') + expect(cleanTitle('Flowers [Speed Up Version]')).toBe('Flowers') + expect(cleanTitle('Flowers - Sped Up')).toBe('Flowers') + }) + + it('strips (Slowed) and (Slowed + Reverb) variants', () => { + expect(cleanTitle('Flowers (Slowed)')).toBe('Flowers') + expect(cleanTitle('Flowers [Slowed Reverb]')).toBe('Flowers') + expect(cleanTitle('Flowers - Slowed')).toBe('Flowers') + }) + + it('strips (Reverb) variants', () => { + expect(cleanTitle('Flowers (Reverb)')).toBe('Flowers') + expect(cleanTitle('Flowers [Reverb]')).toBe('Flowers') + }) + + it('strips Nightcore / 8D Audio / Bass Boosted tags', () => { + expect(cleanTitle('Flowers (Nightcore)')).toBe('Flowers') + expect(cleanTitle('Flowers [8D Audio]')).toBe('Flowers') + expect(cleanTitle('Flowers (Bass Boosted)')).toBe('Flowers') + }) + + it('strips legendado and traduzido bare words', () => { + expect(cleanTitle('Song legendado')).toBe('Song') + expect(cleanTitle('Song traduzido')).toBe('Song') + }) }) describe('cleanAuthor', () => { diff --git a/packages/bot/src/utils/music/searchQueryCleaner.ts b/packages/bot/src/utils/music/searchQueryCleaner.ts index 9c9c095fd..369ac9cfa 100644 --- a/packages/bot/src/utils/music/searchQueryCleaner.ts +++ b/packages/bot/src/utils/music/searchQueryCleaner.ts @@ -9,8 +9,49 @@ * The goal is to strip YouTube-style noise ("(Official Video)", "[Download]", * uploader-channel junk like "- Topic", feat./ft., trailing tags) so downstream * searches find the actual track, not the uploader's decorated listing. + * + * Simple word/phrase noise terms live in noiseTerms.json so they can be + * extended without touching regex logic. */ +import noiseTerms from './noiseTerms.json' + +function escapeRegex(s: string): string { + return s.replaceAll(/[.*+?^${}()|[\]\\]/g, '\\$&') +} + +function termToWordBoundaryRe(term: string): RegExp { + const inner = escapeRegex(term).replaceAll(/\s+/g, '\\s{0,3}') + return new RegExp(`\\b${inner}\\b`, 'gi') +} + +function termToParenRe(term: string): RegExp { + const inner = escapeRegex(term).replaceAll(/\s+/g, '\\s{0,3}') + return new RegExp(`\\(${inner}[^)]*\\)`, 'gi') +} + +function termToBracketRe(term: string): RegExp { + const inner = escapeRegex(term).replaceAll(/\s+/g, '\\s{0,3}') + return new RegExp(`\\[${inner}[^\\]]*\\]`, 'gi') +} + +const DYNAMIC_NOISE_PATTERNS: RegExp[] = [ + ...noiseTerms.versionVariants.flatMap((t) => [ + termToParenRe(t), + termToBracketRe(t), + ]), + ...noiseTerms.bareTitleNoise.map(termToWordBoundaryRe), +] + +const DYNAMIC_VERSION_KEYWORD_RE = new RegExp( + `\\b(?:${noiseTerms.versionVariants.map((t) => escapeRegex(t).replaceAll(/\s+/g, '\\s+')).join('|')})\\b`, + 'i', +) + +const DYNAMIC_HYPHENATED_SUFFIXES: RegExp[] = noiseTerms.versionVariants.map( + (t) => new RegExp(`^${escapeRegex(t).replaceAll(/\s+/g, '\\s+')}(?:\\s+(?:version|edit|mix))?$`, 'i'), +) + const NOISE_PATTERNS: readonly RegExp[] = [ // Korean/CJK parenthetical duplicates: "(뱅뱅뱅)" when title already has English equivalent /\([^\x00-\x7F]+\)/g, @@ -51,24 +92,10 @@ const NOISE_PATTERNS: readonly RegExp[] = [ /\[download\]/gi, /\[free\s{0,3}download\]/gi, - // Version variants: live, acoustic, cover, remix, etc. + // Version variants with richer context (live at venue, etc.) — kept here + // because they go beyond a simple \bterm\b match. /\(live(?:\s{0,3}(?:version|session|performance|at\s[^)]+))?\)/gi, /\[live(?:\s{0,3}(?:version|session|performance|at\s[^\]]+))?\]/gi, - /\(acoustic(?:\s{0,3}version)?\)/gi, - /\[acoustic(?:\s{0,3}version)?\]/gi, - /\(cover[^)]*\)/gi, - /\[cover[^\]]*\]/gi, - /\(remix(?:\s{0,3}(?:version|edit))?\)/gi, - /\[remix(?:\s{0,3}(?:version|edit))?\]/gi, - /\(instrumental(?:\s{0,3}version)?\)/gi, - /\[instrumental(?:\s{0,3}version)?\]/gi, - /\(karaoke(?:\s{0,3}version)?\)/gi, - /\(explicit(?:\s{0,3}version)?\)/gi, - /\(clean(?:\s{0,3}version)?\)/gi, - /\(single(?:\s{0,3}version)?\)/gi, - /\(album\s{0,3}version\)/gi, - /\(deluxe(?:\s{0,3}(?:version|edition))?\)/gi, - /\(bonus\s{0,3}track\)/gi, // Bare decorators that weren't wrapped in brackets /\bofficial\s{0,3}music\s{0,3}video\b/gi, @@ -105,11 +132,8 @@ const NOISE_PATTERNS: readonly RegExp[] = [ /\(tradu[çc][aã]o[^)]*\)/gi, /\[tradu[çc][aã]o[^\]]*\]/gi, /\btradu[çc][aã]o\b/gi, - /\btraduzido\b/gi, - /\blegendado\b/gi, /\(clipe\s+oficial[^)]*\)/gi, /\[clipe\s+oficial[^\]]*\]/gi, - /\blyrics\b/gi, // Brazilian version qualifiers inside brackets /\(vers[aã]o\s{0,3}[^)]*\)/gi, @@ -118,26 +142,26 @@ const NOISE_PATTERNS: readonly RegExp[] = [ /\[ao\s{0,3}vivo[^\]]*\]/gi, /\(ac[uú]stico[^)]*\)/gi, /\[ac[uú]stico[^\]]*\]/gi, + + // Dynamic patterns built from noiseTerms.json at module init + ...DYNAMIC_NOISE_PATTERNS, ] const HYPHENATED_VERSION_SUFFIXES: RegExp[] = [ /^(?:\d{4}\s+)?remaster(?:ed)?(?:\s+(?:version|\d{4}))?(?:\s+\d{4})?$/i, /^official\s+(?:audio|video|music\s+video)$/i, - /^(?:live|acoustic|demo|extended|instrumental|karaoke|cover)(?:\s+(?:version|edit|session|mix))?$/i, - /^(?:radio\s+edit|album\s+version|single\s+version|bonus\s+track)$/i, - /^(?:original\s+(?:mix|version)|original)$/i, - /^(?:deluxe|deluxe\s+(?:version|edition))$/i, - /^(?:explicit|clean|explicit\s+version|clean\s+version)$/i, + /^original$/i, /^(?:19|20)\d{2}$/, // Brazilian/Portuguese version descriptors after " - " /^vers[aã]o/i, /^ao\s+vivo/i, /^forr[oó]/i, /^ac[uú]stico/i, + // Dynamic entries built from noiseTerms.json versionVariants + ...DYNAMIC_HYPHENATED_SUFFIXES, ] -const VERSION_KEYWORD_RE = - /\b(?:remaster(?:ed)?|remix|acoustic|live|demo|extended|instrumental|deluxe|explicit|clean|bonus\s+track|radio\s+edit|single\s+version|album\s+version)\b/i +const VERSION_KEYWORD_RE = DYNAMIC_VERSION_KEYWORD_RE function isVersionSuffix(suffix: string): boolean { if (HYPHENATED_VERSION_SUFFIXES.some((re) => re.test(suffix))) return true From 39843cb25a4e55b8b675b0a1456c97add421d402 Mon Sep 17 00:00:00 2001 From: Lucas Santana Date: Mon, 13 Apr 2026 16:50:45 -0300 Subject: [PATCH 2/3] fix(autoplay): remove nightcore from noise terms (it is a genre not a version decorator) --- packages/bot/src/utils/music/noiseTerms.json | 1 - packages/bot/src/utils/music/searchQueryCleaner.spec.ts | 3 +-- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/packages/bot/src/utils/music/noiseTerms.json b/packages/bot/src/utils/music/noiseTerms.json index acb266fff..a22cca575 100644 --- a/packages/bot/src/utils/music/noiseTerms.json +++ b/packages/bot/src/utils/music/noiseTerms.json @@ -5,7 +5,6 @@ "slowed", "reverb", "slowed reverb", - "nightcore", "8d audio", "bass boosted", "live", diff --git a/packages/bot/src/utils/music/searchQueryCleaner.spec.ts b/packages/bot/src/utils/music/searchQueryCleaner.spec.ts index 0872f8d99..58d873882 100644 --- a/packages/bot/src/utils/music/searchQueryCleaner.spec.ts +++ b/packages/bot/src/utils/music/searchQueryCleaner.spec.ts @@ -82,8 +82,7 @@ describe('cleanTitle', () => { expect(cleanTitle('Flowers [Reverb]')).toBe('Flowers') }) - it('strips Nightcore / 8D Audio / Bass Boosted tags', () => { - expect(cleanTitle('Flowers (Nightcore)')).toBe('Flowers') + it('strips 8D Audio / Bass Boosted tags', () => { expect(cleanTitle('Flowers [8D Audio]')).toBe('Flowers') expect(cleanTitle('Flowers (Bass Boosted)')).toBe('Flowers') }) From 583674ab3fef095a1ce066f259109a950b55aa23 Mon Sep 17 00:00:00 2001 From: Lucas Santana Date: Mon, 13 Apr 2026 16:58:12 -0300 Subject: [PATCH 3/3] fix(ci): consolidate dynamic term builders, add nosonar for dynamic regexp construction --- .../bot/src/utils/music/searchQueryCleaner.ts | 33 ++++++++++--------- 1 file changed, 17 insertions(+), 16 deletions(-) diff --git a/packages/bot/src/utils/music/searchQueryCleaner.ts b/packages/bot/src/utils/music/searchQueryCleaner.ts index 369ac9cfa..d73269610 100644 --- a/packages/bot/src/utils/music/searchQueryCleaner.ts +++ b/packages/bot/src/utils/music/searchQueryCleaner.ts @@ -20,36 +20,37 @@ function escapeRegex(s: string): string { return s.replaceAll(/[.*+?^${}()|[\]\\]/g, '\\$&') } -function termToWordBoundaryRe(term: string): RegExp { - const inner = escapeRegex(term).replaceAll(/\s+/g, '\\s{0,3}') - return new RegExp(`\\b${inner}\\b`, 'gi') +function termInner(term: string): string { + return escapeRegex(term).replaceAll(/\s+/g, '\\s{0,3}') } -function termToParenRe(term: string): RegExp { - const inner = escapeRegex(term).replaceAll(/\s+/g, '\\s{0,3}') - return new RegExp(`\\(${inner}[^)]*\\)`, 'gi') -} +type TermPattern = 'word' | 'paren' | 'bracket' | 'suffix' -function termToBracketRe(term: string): RegExp { - const inner = escapeRegex(term).replaceAll(/\s+/g, '\\s{0,3}') - return new RegExp(`\\[${inner}[^\\]]*\\]`, 'gi') +function buildTermRe(term: string, type: TermPattern): RegExp { + const inner = termInner(term) + switch (type) { + case 'paren': return new RegExp(`\\(${inner}[^)]*\\)`, 'gi') // NOSONAR + case 'bracket': return new RegExp(`\\[${inner}[^\\]]*\\]`, 'gi') // NOSONAR + case 'suffix': return new RegExp(`^${inner}(?:\\s{0,3}(?:version|edit|mix))?$`, 'i') // NOSONAR + default: return new RegExp(`\\b${inner}\\b`, 'gi') // NOSONAR + } } const DYNAMIC_NOISE_PATTERNS: RegExp[] = [ ...noiseTerms.versionVariants.flatMap((t) => [ - termToParenRe(t), - termToBracketRe(t), + buildTermRe(t, 'paren'), + buildTermRe(t, 'bracket'), ]), - ...noiseTerms.bareTitleNoise.map(termToWordBoundaryRe), + ...noiseTerms.bareTitleNoise.map((t) => buildTermRe(t, 'word')), ] -const DYNAMIC_VERSION_KEYWORD_RE = new RegExp( - `\\b(?:${noiseTerms.versionVariants.map((t) => escapeRegex(t).replaceAll(/\s+/g, '\\s+')).join('|')})\\b`, +const DYNAMIC_VERSION_KEYWORD_RE = new RegExp( // NOSONAR + `\\b(?:${noiseTerms.versionVariants.map(termInner).join('|')})\\b`, 'i', ) const DYNAMIC_HYPHENATED_SUFFIXES: RegExp[] = noiseTerms.versionVariants.map( - (t) => new RegExp(`^${escapeRegex(t).replaceAll(/\s+/g, '\\s+')}(?:\\s+(?:version|edit|mix))?$`, 'i'), + (t) => buildTermRe(t, 'suffix'), ) const NOISE_PATTERNS: readonly RegExp[] = [