From 6bf16c9895e46fd654359ce95248c1940e029b29 Mon Sep 17 00:00:00 2001 From: Lucas Santana Date: Mon, 8 Jun 2026 13:21:44 -0300 Subject: [PATCH 1/2] fix(autoplay): weight popularity over name similarity in similar mode MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Listen-test feedback: autoplay was picking by title/name similarity (and surfacing near-duplicate name variants) rather than musical fit. Two changes: - Remove the cross-artist title-token bonus ("similar title mood", +0.2). Rewarding shared title words across different artists is name similarity, not musical similarity, and it pulled in near-identically-named tracks (often variants of the same song → duplicate-ish entries). Same-artist album coherence still comes from the 'album match' signal; musical similarity from Last.fm match + genre families. - Extend the post-selection artist-popularity re-rank to 'similar' mode (was discover/popular only): a mild gradient (popularity/100 × 0.12) over the top of the already-selected, genre-safe, de-duped candidates. Because it reorders vetted candidates and never introduces new ones, it favours well-known songs/artists WITHOUT reintroducing off-genre mainstream drift. Note: only artist-level popularity is available cheaply; song-level popularity would need track-popularity plumbing (follow-up if finer control is wanted). --- .../music/autoplay/candidateScorer.spec.ts | 16 ++++++++++++ .../utils/music/autoplay/candidateScorer.ts | 14 +++++------ .../src/utils/music/autoplay/replenisher.ts | 25 +++++++++++++++---- 3 files changed, 42 insertions(+), 13 deletions(-) diff --git a/packages/bot/src/utils/music/autoplay/candidateScorer.spec.ts b/packages/bot/src/utils/music/autoplay/candidateScorer.spec.ts index 32c0278bb..a833fa3ab 100644 --- a/packages/bot/src/utils/music/autoplay/candidateScorer.spec.ts +++ b/packages/bot/src/utils/music/autoplay/candidateScorer.spec.ts @@ -182,6 +182,22 @@ describe('candidateScorer', () => { expect(result.signals).toContain(signal) }) + it('does not reward cross-artist title-word overlap (name similarity removed)', () => { + const result = calculateRecommendationScore({ + candidate: createTrack({ + title: 'Purple Rain Forever', + author: 'Different Artist', + source: 'youtube', + }), + currentTrack: createTrack({ + title: 'Purple Rain', + author: 'Prince', + }), + recentArtists: new Set(), + }) + expect(result.signals).not.toContain('similar title mood') + }) + it('boosts popular mode based on liked weight', () => { const result = calculateRecommendationScore({ candidate: createTrack({ diff --git a/packages/bot/src/utils/music/autoplay/candidateScorer.ts b/packages/bot/src/utils/music/autoplay/candidateScorer.ts index 8d1438784..12f3a7c5c 100644 --- a/packages/bot/src/utils/music/autoplay/candidateScorer.ts +++ b/packages/bot/src/utils/music/autoplay/candidateScorer.ts @@ -399,14 +399,12 @@ export function calculateRecommendationScore(ctx: ScoringContext): { } else if (candidate.source && candidate.source !== currentTrack.source) { signals.push('source variety') } - const tokenScore = sharedTitleTokenScore( - candidate.title, - currentTrack.title, - ) - score += tokenScore - if (tokenScore > 0) { - signals.push('similar title mood') - } + // NOTE: the cross-artist title-token bonus ("similar title mood") was + // removed — rewarding shared title words across different artists is name + // similarity, not musical similarity, and it pulled in near-identically + // named tracks (often variants of the same song → duplicate-ish entries). + // Same-artist album coherence is still captured by the 'album match' signal + // above; musical similarity comes from Last.fm match + genre families. if ( currentTrack.durationMS && candidate.durationMS && diff --git a/packages/bot/src/utils/music/autoplay/replenisher.ts b/packages/bot/src/utils/music/autoplay/replenisher.ts index 2107d741f..8cbaff983 100644 --- a/packages/bot/src/utils/music/autoplay/replenisher.ts +++ b/packages/bot/src/utils/music/autoplay/replenisher.ts @@ -55,6 +55,11 @@ const AUTOPLAY_BUFFER_SIZE_PREMIUM = 16 const HISTORY_SEED_LIMIT = 3 const MAX_TRACKS_PER_ARTIST = 2 const MAX_TRACKS_PER_SOURCE = 3 +// 'similar' mode popularity re-rank: a mild gradient (popularity/100 × weight) +// applied POST-selection to the genre-safe candidates, so well-known songs are +// favoured over obscure name-matches without pulling in new (off-genre) tracks. +const SIMILAR_POPULARITY_WEIGHT = 0.12 +const POPULARITY_RERANK_HEAD = 5 // Concurrency lock: prevents duplicate queue insertions when playerStart and // playerFinish events fire within milliseconds (playerStart + playerFinish) cannot both read @@ -464,16 +469,20 @@ async function _replenishQueue( currentTrack.author, ) - if ( - (autoplayMode === 'discover' || autoplayMode === 'popular') && - requestedBy?.id - ) { + if (requestedBy?.id) { const token = await Promise.resolve( spotifyLinkService.getValidAccessToken(requestedBy.id), ).catch(() => null) if (token) { + // Re-rank a bounded head of the already-selected (genre-safe, + // de-duped) candidates by artist popularity. Post-selection + // only: it reorders vetted candidates and never introduces new + // ones, so popularity weighting can't reintroduce off-genre + // mainstream drift. + const rerankHead = + autoplayMode === 'similar' ? POPULARITY_RERANK_HEAD : 3 await Promise.all( - enriched.slice(0, 3).map(async (track) => { + enriched.slice(0, rerankHead).map(async (track) => { const popularity = await getArtistPopularity( token, track.track.author, @@ -486,6 +495,12 @@ async function _replenishQueue( popularity <= 40 ) { track.score += 0.12 + } else if (autoplayMode === 'similar') { + // Mild gradient — favour well-known songs/artists + // among related candidates (tie-breaker, not an + // override). + track.score += + (popularity / 100) * SIMILAR_POPULARITY_WEIGHT } }), ) From f6dd468148515e1fe8194d1287ac746aafedc02d Mon Sep 17 00:00:00 2001 From: Lucas Santana Date: Mon, 8 Jun 2026 13:32:47 -0300 Subject: [PATCH 2/2] test(autoplay): extract popularityBoost as a pure, unit-tested fn MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The 'similar'-mode popularity branch was token-gated, so the integration suite (token=null) never executed it → SonarCloud new-code coverage 75%. Extract the per-mode boost into a pure exported popularityBoost() and unit-test all three mode branches. No behavior change. --- .../utils/music/autoplay/replenisher.spec.ts | 20 ++++++++++- .../src/utils/music/autoplay/replenisher.ts | 34 +++++++++++-------- 2 files changed, 39 insertions(+), 15 deletions(-) diff --git a/packages/bot/src/utils/music/autoplay/replenisher.spec.ts b/packages/bot/src/utils/music/autoplay/replenisher.spec.ts index a9af40481..fbe41d4ca 100644 --- a/packages/bot/src/utils/music/autoplay/replenisher.spec.ts +++ b/packages/bot/src/utils/music/autoplay/replenisher.spec.ts @@ -1,6 +1,6 @@ import { describe, expect, it, jest, beforeEach } from '@jest/globals' import type { Track, GuildQueue } from 'discord-player' -import { replenishQueue } from './replenisher' +import { replenishQueue, popularityBoost } from './replenisher' jest.mock('@lucky/shared/utils', () => ({ debugLog: jest.fn(), @@ -426,3 +426,21 @@ describe('clearSessionMoodCache', () => { expect(typeof clearSessionMoodCache).toBe('function') }) }) + +describe('popularityBoost', () => { + it('boosts high-popularity artists in popular mode, nothing below the threshold', () => { + expect(popularityBoost('popular', 80)).toBeCloseTo(0.12, 5) + expect(popularityBoost('popular', 50)).toBe(0) + }) + + it('boosts low-popularity artists in discover mode, nothing above the threshold', () => { + expect(popularityBoost('discover', 30)).toBeCloseTo(0.12, 5) + expect(popularityBoost('discover', 60)).toBe(0) + }) + + it('applies a mild popularity gradient in similar mode', () => { + expect(popularityBoost('similar', 100)).toBeCloseTo(0.12, 5) + expect(popularityBoost('similar', 50)).toBeCloseTo(0.06, 5) + expect(popularityBoost('similar', 0)).toBe(0) + }) +}) diff --git a/packages/bot/src/utils/music/autoplay/replenisher.ts b/packages/bot/src/utils/music/autoplay/replenisher.ts index 8cbaff983..870f52f11 100644 --- a/packages/bot/src/utils/music/autoplay/replenisher.ts +++ b/packages/bot/src/utils/music/autoplay/replenisher.ts @@ -60,6 +60,25 @@ const MAX_TRACKS_PER_SOURCE = 3 // favoured over obscure name-matches without pulling in new (off-genre) tracks. const SIMILAR_POPULARITY_WEIGHT = 0.12 const POPULARITY_RERANK_HEAD = 5 +// Flat boost for the discover/popular mode thresholds. +const POPULARITY_MODE_BOOST = 0.12 + +/** + * Post-selection popularity boost for a candidate, by autoplay mode. Applied to + * already-selected (genre-safe, de-duped) candidates only, so it reorders + * vetted tracks without introducing new (off-genre) ones. + * - popular: flat boost for high-popularity (≥70) artists + * - discover: flat boost for low-popularity (≤40) artists + * - similar: mild gradient favouring well-known artists (tie-breaker) + */ +export function popularityBoost( + mode: 'similar' | 'discover' | 'popular', + popularity: number, +): number { + if (mode === 'popular') return popularity >= 70 ? POPULARITY_MODE_BOOST : 0 + if (mode === 'discover') return popularity <= 40 ? POPULARITY_MODE_BOOST : 0 + return (popularity / 100) * SIMILAR_POPULARITY_WEIGHT +} // Concurrency lock: prevents duplicate queue insertions when playerStart and // playerFinish events fire within milliseconds (playerStart + playerFinish) cannot both read @@ -488,20 +507,7 @@ async function _replenishQueue( track.track.author, ).catch(() => null) if (popularity === null) return - if (autoplayMode === 'popular' && popularity >= 70) { - track.score += 0.12 - } else if ( - autoplayMode === 'discover' && - popularity <= 40 - ) { - track.score += 0.12 - } else if (autoplayMode === 'similar') { - // Mild gradient — favour well-known songs/artists - // among related candidates (tie-breaker, not an - // override). - track.score += - (popularity / 100) * SIMILAR_POPULARITY_WEIGHT - } + track.score += popularityBoost(autoplayMode, popularity) }), ) enriched.sort((a, b) => b.score - a.score)