diff --git a/packages/bot/src/utils/music/queueManipulation.spec.ts b/packages/bot/src/utils/music/queueManipulation.spec.ts index 47f2b775a..04d13527e 100644 --- a/packages/bot/src/utils/music/queueManipulation.spec.ts +++ b/packages/bot/src/utils/music/queueManipulation.spec.ts @@ -3415,6 +3415,48 @@ describe('queueManipulation — Spotify priority', () => { expect(firstCallQuery).toContain('The Weeknd') }) + it('never appends query modifiers to the Spotify engine query on subsequent replenish cycles', async () => { + const capturedQueries: Array<{ query: string; engine: unknown }> = [] + const searchMock = jest.fn().mockImplementation((query: string, opts: { searchEngine: unknown }) => { + capturedQueries.push({ query, engine: opts?.searchEngine }) + return Promise.resolve({ + tracks: [ + { + title: 'Shape of You', + author: 'Ed Sheeran', + url: 'https://open.spotify.com/track/shapeofyou', + source: 'spotify', + durationMS: 234000, + }, + ], + }) + }) + + const queue = createQueueMock({ + guild: { id: 'guild-spotify-modifier-test' }, + currentTrack: { + title: 'Ed Sheeran - Shape of You', + author: 'Ed SheeranVEVO', + url: 'https://youtube.com/watch?v=seed001', + requestedBy: { id: 'user-1' }, + } as unknown as Track, + metadata: { requestedBy: { id: 'user-1' } }, + tracks: { size: 0, toArray: jest.fn().mockReturnValue([]) }, + player: { search: searchMock }, + }) + + // First call: replenishCount=0, no modifier + await replenishQueue(queue as unknown as GuildQueue) + // Second call: replenishCount=1, modifier='similar' — must NOT appear in Spotify query + await replenishQueue(queue as unknown as GuildQueue) + + // Spotify returns results on every call, so YouTube/AUTO are never reached. + // All captured queries are Spotify queries — none should contain text modifiers. + for (const { query } of capturedQueries) { + expect(query).not.toMatch(/\b(similar|like|playlist|mix)\b/) + } + }) + it('uses right side as artist when song core is on the left of the separator', async () => { const searchMock = jest.fn().mockResolvedValue({ tracks: [ @@ -3508,6 +3550,49 @@ describe('queueManipulation — diversity improvements', () => { expect(firstAdded?.title).toBe('Eu sei que é você') }) + it('deduplicates cover variant of now-playing song so it is not queued', async () => { + const addedTracks: unknown[] = [] + const coverTrack = { + title: 'ANATOMIA - Água viva (Cover - ao vivo)', + author: 'ANATOMIA', + url: 'https://youtube.com/watch?v=cover001', + source: 'youtube', + durationMS: 230000, + } + const differentTrack = { + title: 'Outra Música', + author: 'Other Artist', + url: 'https://open.spotify.com/track/other001', + source: 'spotify', + durationMS: 210000, + } + + const queue = createQueueMock({ + guild: { id: 'guild-cover-dedup-test' }, + currentTrack: { + title: 'ANATOMIA - Água viva', + author: 'ANATOMIA', + url: 'https://youtube.com/watch?v=original', + source: 'youtube', + requestedBy: { id: 'user-1' }, + } as unknown as Track, + metadata: { requestedBy: { id: 'user-1' } }, + tracks: { size: 7, toArray: jest.fn().mockReturnValue([]) }, + addTrack: jest.fn((t: unknown) => addedTracks.push(t)), + player: { + search: jest.fn().mockResolvedValue({ + tracks: [coverTrack, differentTrack], + }), + }, + }) + + await replenishQueue(queue as unknown as GuildQueue) + + const titles = addedTracks.map((t) => (t as { title: string }).title) + expect(titles).not.toContain('ANATOMIA - Água viva (Cover - ao vivo)') + expect(titles).toContain('Outra Música') + }) + it('limits current-track artist to 1 more track when currentTrack counts as first', async () => { const addedTracks: unknown[] = [] const makeTrack = (id: string, artist: string) => ({ diff --git a/packages/bot/src/utils/music/queueManipulation.ts b/packages/bot/src/utils/music/queueManipulation.ts index 36b0f4d46..229042a81 100644 --- a/packages/bot/src/utils/music/queueManipulation.ts +++ b/packages/bot/src/utils/music/queueManipulation.ts @@ -823,7 +823,7 @@ async function searchSeedCandidates( spotifyBase = baseQuery } } - const spotifyQuery = modifier ? `${spotifyBase} ${modifier}` : spotifyBase + const spotifyQuery = spotifyBase const engines: QueryType[] = [ QueryType.SPOTIFY_SEARCH, diff --git a/packages/bot/src/utils/music/searchQueryCleaner.spec.ts b/packages/bot/src/utils/music/searchQueryCleaner.spec.ts index ad0c2d61e..7a03ebd01 100644 --- a/packages/bot/src/utils/music/searchQueryCleaner.spec.ts +++ b/packages/bot/src/utils/music/searchQueryCleaner.spec.ts @@ -410,3 +410,29 @@ describe('cleanTitle — Acústico variants', () => { expect(cleanTitle('Música - Acústico ao vivo')).toBe('Música') }) }) + +describe('cleanTitle — Cover variants', () => { + it('strips "(Cover)" parenthetical', () => { + expect(cleanTitle('Hallelujah (Cover)')).toBe('Hallelujah') + }) + + it('strips "(Cover Version)" parenthetical', () => { + expect(cleanTitle('Hallelujah (Cover Version)')).toBe('Hallelujah') + }) + + it('strips "(Cover by Someone)" parenthetical with extra content', () => { + expect(cleanTitle('Água viva (Cover by Carlos)')).toBe('Água viva') + }) + + it('strips "(Cover - Ao vivo)" parenthetical with dash and extra content', () => { + expect(cleanTitle('ANATOMIA - Água viva (Cover - Ao vivo)')).toBe('ANATOMIA - Água viva') + }) + + it('strips "[Cover]" bracketed', () => { + expect(cleanTitle('Song Title [Cover]')).toBe('Song Title') + }) + + it('strips "- Cover" hyphenated suffix', () => { + expect(cleanTitle('Água viva - Cover')).toBe('Água viva') + }) +}) diff --git a/packages/bot/src/utils/music/searchQueryCleaner.ts b/packages/bot/src/utils/music/searchQueryCleaner.ts index 847ed7392..cfd5495d3 100644 --- a/packages/bot/src/utils/music/searchQueryCleaner.ts +++ b/packages/bot/src/utils/music/searchQueryCleaner.ts @@ -56,8 +56,8 @@ const NOISE_PATTERNS: readonly RegExp[] = [ /\[live(?:\s{0,3}(?:version|session|performance|at\s[^\]]+))?\]/gi, /\(acoustic(?:\s{0,3}version)?\)/gi, /\[acoustic(?:\s{0,3}version)?\]/gi, - /\(cover(?:\s{0,3}version)?\)/gi, - /\[cover(?:\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, @@ -121,7 +121,7 @@ const NOISE_PATTERNS: readonly RegExp[] = [ 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)(?:\s+(?:version|edit|session|mix))?$/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,