Skip to content

feat(autoplay): preferred-artist source + remove dead Spotify endpoints - #720

Closed
LucasSantana-Dev wants to merge 3 commits into
mainfrom
feat/autoplay-preferred-artist-source
Closed

LucasSantana-Dev wants to merge 3 commits into
mainfrom
feat/autoplay-preferred-artist-source

Conversation

@LucasSantana-Dev

@LucasSantana-Dev LucasSantana-Dev commented Apr 19, 2026 •

Copy link
Copy Markdown
Owner

Summary

  • Deleted dead Spotify endpoints: Removed /v1/recommendations and /v1/audio-features calls + audio-feature enrichment pipeline (getSpotifyRecommendations, SpotifyAudioFeatures, getAudioFeatures, getBatchAudioFeatures, enrichWithAudioFeatures, getTrackAudioFeatures). These endpoints were silently 404/403'ing in production for months and contributed nothing to autoplay.
  • Added preferred-artist seeder: New collectPreferredArtistCandidates source that fetches top tracks from Last.fm for artists marked as "preferred" on the Musical Taste page, resolves them to real Tracks, and seeds the autoplay pool with a +0.2 score boost so UI preferences actually shape playback.
  • Wired preferred-artist into replenisher: Gathers preferred artist names across all Discord users in the VC, calls seeder immediately after recommendation candidates and before Last.fm sources, so preferred artists get priority.

Files Changed

Deleted:

  • Dead Spotify endpoint code from spotifyApi.ts, spotifyRecommender.ts, queueManipulation.ts, candidateScorer.ts
  • Audio-feature enrichment pipeline from replenisher.ts
  • collectSpotifyRecommendationCandidates from spotifyRecommender.ts

Added:

  • preferredArtistSeeder.ts + preferredArtistSeeder.spec.ts
  • getArtistTopTracks() to lastFmApi.ts
  • getPreferredArtistNames() to feedbackService.ts

Updated:

  • replenisher.ts: gather preferred artist names, call seeder
  • candidateCollector.ts: remove Spotify-recs call, drop currentFeatures param
  • Test files: updated spotifyApi.spec.ts, added feedbackService tests, new seeder tests

Manual Test Plan

  1. Setup: User marks Artist X as "prefer" on Musical Taste page → seeder should fetch Artist X top 6 tracks
  2. Verify seeding: Trigger autoplay → check logs for "preferred artist candidates" entry showing >0 candidates added
  3. Verify score boost: Inspect debug logs → preferred artist tracks should have +0.2 score relative to generic recommendation pool

Summary by CodeRabbit

  • New Features

    • Added support for preferred artist names to influence autoplay music recommendations
    • Integrated Last.fm artist top tracks for autoplay candidate selection
  • Removals

    • Removed Spotify-based recommendation candidate collection engine
    • Removed audio feature analysis and dynamic scoring adjustments
  • Tests

    • Added comprehensive test coverage for preferred artist recommendation seeding
    • Added test coverage for user feedback preference services

… recs/audio-features

This commit addresses two coupled issues:

1. **Remove dead Spotify endpoints**: Spotify's /v1/recommendations and /v1/audio-features endpoints have been silently returning 404/403 for months, rendering the Spotify-recs path useless. Deleted getSpotifyRecommendations, SpotifyRecommendationTrack, SpotifyAudioFeatureConstraints, getAudioFeatures, and getBatchAudioFeatures from spotifyApi.ts along with their audio-feature-only LRUCache. Cleaned up the dead code path from spotifyRecommender.ts and removed the currentFeatures parameter threading throughout the candidate collection pipeline.

2. **Add preferred-artist candidate source**: Artists saved on the Musical Taste page (via userArtistPreference table) now shape autoplay seeding via a new preferred-artist candidate source. New collectPreferredArtistCandidates seeder mirrors lastFmSeeder patterns: fetches artist top tracks via Last.fm, resolves to real Tracks via search, scores with a +0.2 boost so they reliably outrank generic sources. Wired into replenisher.ts to gather union of preferred artist names across all VC members and feed to the seeder immediately after recommendation candidates, before Last.fm sources.

**Files changed**:
- spotifyApi.ts: removed 5 dead functions + types
- spotifyApi.spec.ts: dropped 3 describe blocks
- spotifyRecommender.ts: removed collectSpotifyRecommendationCandidates
- candidateCollector.ts: removed Spotify-recs call, dropped currentFeatures param
- queueManipulation.ts: removed getTrackAudioFeatures, enrichWithAudioFeatures, audioFeatureCache
- candidateScorer.ts: removed enrichWithAudioFeatures definition
- replenisher.ts: removed audio-feature enrichment, wired preferred-artist seeder
- lastFmApi.ts: added getArtistTopTracks (mirrors getSimilarTracks pattern)
- lastfm/index.ts: re-exported getArtistTopTracks
- feedbackService.ts: added getPreferredArtistNames
- **NEW** preferredArtistSeeder.ts: collectPreferredArtistCandidates + searchLastFmQuery
- Test updates: added coverage for new seeder and feedbackService method

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@vercel

vercel Bot commented Apr 19, 2026 •

Copy link
Copy Markdown
Contributor

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
lucky Ready Ready Preview, Comment Apr 19, 2026 4:01pm

Request Review

@LucasSantana-Dev
LucasSantana-Dev enabled auto-merge (squash) April 19, 2026 15:30
@coderabbitai

coderabbitai Bot commented Apr 19, 2026 •

Copy link
Copy Markdown
📝 Walkthrough

Walkthrough

The PR introduces Last.fm-based preferred artist candidate collection for autoplay, adding methods to fetch artist top tracks and user preferred artist names. Simultaneously, it removes Spotify audio-feature enrichment and recommendation logic throughout the recommendation pipeline.

Changes

Cohort / File(s) Summary
Last.fm API Enhancement
packages/bot/src/lastfm/index.ts, packages/bot/src/lastfm/lastFmApi.ts
Added getArtistTopTracks() function to fetch top tracks for an artist via Last.fm API, returning simplified track objects with error handling and a barrel export.
Feedback Service Extension
packages/bot/src/services/musicRecommendation/feedbackService.ts, packages/bot/src/services/musicRecommendation/feedbackService.spec.ts
Added getPreferredArtistNames() method to retrieve user's preferred artist names from the database, filtering by guild and preference type, with comprehensive test coverage for success/failure scenarios.
Preferred Artist Seeder (New Module)
packages/bot/src/utils/music/autoplay/preferredArtistSeeder.ts, packages/bot/src/utils/music/autoplay/preferredArtistSeeder.spec.ts
Introduced new module with collectPreferredArtistCandidates() and searchLastFmQuery() functions to rotate through preferred artists, fetch their top tracks, search for them across engines, score, and populate the candidate pool with preferred-artist-boosted candidates.
Spotify Audio-Features Removal
packages/bot/src/spotify/spotifyApi.ts, packages/bot/src/spotify/spotifyApi.spec.ts, packages/bot/src/utils/music/autoplay/candidateScorer.ts
Removed exported Spotify recommendation/audio-feature functions (getSpotifyRecommendations, getAudioFeatures, getBatchAudioFeatures, enrichWithAudioFeatures) and their test coverage, eliminating audio-feature-based scoring logic from the recommendation pipeline.
Queue Manipulation Audio-Feature Cleanup
packages/bot/src/utils/music/queueManipulation.ts, packages/bot/src/utils/music/queueManipulation.spec.ts
Removed getTrackAudioFeatures() and enrichWithAudioFeatures() exports, audio-feature caching infrastructure (LRUCache), and related Spotify imports; eliminated audio-feature-based track score adjustments.
Autoplay Pipeline Refactoring
packages/bot/src/utils/music/autoplay/candidateCollector.ts, packages/bot/src/utils/music/autoplay/spotifyRecommender.ts, packages/bot/src/utils/music/autoplay/replenisher.ts
Removed Spotify recommendation candidate collection flow and currentFeatures parameter from candidate collection signatures; integrated preferred artist seeding with getPreferredArtistNames() and collectPreferredArtistCandidates() calls; removed audio-feature-based enrichment and Spotify-popularity score adjustments from track selection.

Sequence Diagram

sequenceDiagram
    participant Replenisher
    participant FeedbackService
    participant PreferredArtistSeeder
    participant LastFmAPI
    participant SearchEngine
    participant Scorer

    Replenisher->>FeedbackService: getPreferredArtistNames(guildId, userId)
    FeedbackService->>FeedbackService: Query DB for preferred artists
    FeedbackService-->>Replenisher: Set<artistNames>

    Replenisher->>PreferredArtistSeeder: collectPreferredArtistCandidates(...preferredArtistNames)
    
    loop For each preferred artist (rotated)
        PreferredArtistSeeder->>LastFmAPI: getArtistTopTracks(artist, limit)
        LastFmAPI-->>PreferredArtistSeeder: Track[] {artist, title}
        
        PreferredArtistSeeder->>SearchEngine: searchLastFmQuery(queue, query)
        SearchEngine-->>PreferredArtistSeeder: Track[] (filtered by duration)
        
        loop For each search result
            PreferredArtistSeeder->>Scorer: calculateRecommendationScore(...)
            Scorer-->>PreferredArtistSeeder: score + reason
            PreferredArtistSeeder->>PreferredArtistSeeder: upsertScoredCandidate(+BOOST)
        end
    end
    
    PreferredArtistSeeder-->>Replenisher: (candidates map populated)
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

Suggested labels

enhancement

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The pull request title accurately describes the main changes: it introduces a new preferred-artist source for autoplay and removes dead Spotify endpoints and related audio-feature code, matching the key objectives and substantial refactoring across multiple files.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/autoplay-preferred-artist-source

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
packages/bot/src/utils/music/queueManipulation.spec.ts (2)

92-99: ⚠️ Potential issue | 🟠 Major

Remove stale Spotify recommendation/audio-feature tests and mocks.

This PR removes getAudioFeatures, getBatchAudioFeatures, and getSpotifyRecommendations paths, but this spec still mocks and asserts those calls. These tests will fail or keep validating dead behavior.

🧹 Suggested cleanup direction
 jest.mock('../../spotify/spotifyApi', () => ({
-    getAudioFeatures: jest.fn().mockResolvedValue(null),
     searchSpotifyTrack: jest.fn().mockResolvedValue(null),
-    getBatchAudioFeatures: jest.fn().mockResolvedValue(new Map()),
     getArtistPopularity: jest.fn().mockResolvedValue(null),
     getArtistGenres: jest.fn().mockResolvedValue([]),
-    getSpotifyRecommendations: jest.fn().mockResolvedValue([]),
 }))

Then delete the spec cases that assert audio-feature enrichment or Spotify recommendations are invoked.

Also applies to: 2792-3191, 3529-3597

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/bot/src/utils/music/queueManipulation.spec.ts` around lines 92 - 99,
The spec is still mocking and asserting removed Spotify paths (getAudioFeatures,
getBatchAudioFeatures, getSpotifyRecommendations); remove those mocks from the
jest.mock block and delete or disable the test cases that assert audio-feature
enrichment or Spotify recommendation calls (any tests referencing
getAudioFeatures, getBatchAudioFeatures, or getSpotifyRecommendations) so the
spec no longer validates dead behavior; search for those symbol names in
queueManipulation.spec.ts and remove the related mock entries and the test
blocks that expect or verify those calls.

101-122: ⚠️ Potential issue | 🔴 Critical

Add getPreferredArtistNames to the feedback service mock throughout the spec.

replenisher.ts now calls recommendationFeedbackService.getPreferredArtistNames() in its Promise.all block (line 140). Without it in the mock, any test reaching that code path will fail with getPreferredArtistNames is not a function.

Fix locations

Add to the mock declaration (lines 101–122):

const getPreferredArtistKeysMock = jest.fn()
+const getPreferredArtistNamesMock = jest.fn()
const getBlockedArtistKeysMock = jest.fn()

Add to the mock object definition (within the jest.mock call):

getPreferredArtistKeys: (...args: unknown[]) =>
    getPreferredArtistKeysMock(...args),
+getPreferredArtistNames: (...args: unknown[]) =>
+    getPreferredArtistNamesMock(...args),
getBlockedArtistKeys: (...args: unknown[]) =>

Add to all beforeEach blocks (lines 163–169, 2065–2071, 2158–2164, 2202–2208, 2264–2270, 2458–2465, 2551–2557, 3697–3703, 3789–3795, 4060–4066):

getPreferredArtistKeysMock.mockResolvedValue(new Set())
+getPreferredArtistNamesMock.mockResolvedValue(new Set())
getBlockedArtistKeysMock.mockResolvedValue(new Set())
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/bot/src/utils/music/queueManipulation.spec.ts` around lines 101 -
122, The tests fail because
recommendationFeedbackService.getPreferredArtistNames is now called by
replenisher.ts but isn't mocked; add a new mock function (e.g.
getPreferredArtistNamesMock = jest.fn()) alongside
dislikedTrackWeightsMock/likedTrackWeightsMock/... and expose it in the
jest.mock return under recommendationFeedbackService as getPreferredArtistNames:
(...args: unknown[]) => getPreferredArtistNamesMock(...args); also update every
beforeEach that resets/sets the other feedback mocks to initialize or
mockImplementation for getPreferredArtistNamesMock so tests exercising
replenisher.ts's Promise.all path won't throw "is not a function".
packages/bot/src/services/musicRecommendation/feedbackService.spec.ts (1)

490-558: ⚠️ Potential issue | 🔴 Critical

Move this describe block out of the recordImplicitFeedback trims... test and fix the mocking approach.

The block is currently nested inside the test's for loop (lines 490–558 within 492–560). The tests reference service before it's instantiated (line 563), and they incorrectly spy on module.getPrismaClient instead of the imported function. The suite will fail with ReferenceError and invalid mock registration.

Structural fix

Move the describe('getPreferredArtistNames') block (lines 495–558) outside the recordImplicitFeedback test. Within each test, instantiate a fresh service and use (getPrismaClient as jest.Mock).mockReturnValue(mockDb) instead of jest.spyOn(module, 'getPrismaClient').

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/bot/src/services/musicRecommendation/feedbackService.spec.ts` around
lines 490 - 558, The getPreferredArtistNames test suite is accidentally nested
inside the recordImplicitFeedback test and uses an improper spy; move the entire
describe('getPreferredArtistNames') block out of the recordImplicitFeedback test
(so it sits at the top-level in the file), and in each test instantiate a fresh
service instance before calling service.getPreferredArtistNames; replace
jest.spyOn(module, 'getPrismaClient') with (getPrismaClient as
jest.Mock).mockReturnValue(mockDb) to mock the imported getPrismaClient function
correctly.
🧹 Nitpick comments (1)
packages/bot/src/utils/music/autoplay/replenisher.ts (1)

151-154: Simplify the preferred-name union construction.

allPreferredArtistNames already contains sets, so the extra new Set(set) allocation is unnecessary.

♻️ Proposed simplification
-        const allPreferredArtistNameSets = allPreferredArtistNames.map((set) => new Set(set))
         const preferredArtistNamesUnion = new Set<string>(
-            allPreferredArtistNameSets.flatMap((s) => [...s]),
+            allPreferredArtistNames.flatMap((s) => [...s]),
         )
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/bot/src/utils/music/autoplay/replenisher.ts` around lines 151 - 154,
The code creates intermediate allPreferredArtistNameSets by mapping each set to
new Set(set) even though allPreferredArtistNames already contains Set instances;
remove the unnecessary allPreferredArtistNameSets variable and build
preferredArtistNamesUnion directly from allPreferredArtistNames by flattening
each Set (e.g., flatMap(s => [...s])) into the new Set constructor so
preferredArtistNamesUnion is created in one step.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/bot/src/utils/music/autoplay/preferredArtistSeeder.spec.ts`:
- Around line 104-128: The blocked-key fixture is wrong: the test intends to
exercise the "skip blocked artists" branch in collectPreferredArtistCandidates
but uses 'Artist B' (normalizes to 'artistb') while blockedArtistKeys contains
'artistbkey', so lastfm.getArtistTopTracks is still called; update the
blockedArtistKeys set to contain the normalized key used by the function (e.g.,
'artistb') so collectPreferredArtistCandidates recognizes the artist as blocked
and the expectation that lastfm.getArtistTopTracks was not called will hold.

In `@packages/bot/src/utils/music/autoplay/preferredArtistSeeder.ts`:
- Around line 66-105: The candidate buffer size is only checked after each
artist loop so the inner search loop can overfill work; after calling
upsertScoredCandidate(...) inside the inner loop (the loop iterating over
searchTracks in preferredArtistSeeder.ts that processes searchedTrack),
immediately check if candidates.size >= AUTOPLAY_BUFFER_SIZE and break out of
the inner loop, and then ensure the outer topTracks loop also exits (use a
labeled loop, set a flag, or return early from the enclosing function) so no
further searchLastFmQuery or candidate processing occurs once the buffer is
full.
- Around line 56-61: The inline normalization in preferredArtistSeeder.ts
(calculating normalizedArtistKey from artistName) can miss variants cleaned by
RecommendationFeedbackService.cleanAuthor; change the code to use the same
cleaning function (import or access RecommendationFeedbackService.cleanAuthor)
when producing the artist key (e.g., compute cleaned = cleanAuthor(artistName)
and use that to check blockedArtistKeys.has(cleaned)), ensuring any
trimming/empty handling matches the feedback storage behavior.

---

Outside diff comments:
In `@packages/bot/src/services/musicRecommendation/feedbackService.spec.ts`:
- Around line 490-558: The getPreferredArtistNames test suite is accidentally
nested inside the recordImplicitFeedback test and uses an improper spy; move the
entire describe('getPreferredArtistNames') block out of the
recordImplicitFeedback test (so it sits at the top-level in the file), and in
each test instantiate a fresh service instance before calling
service.getPreferredArtistNames; replace jest.spyOn(module, 'getPrismaClient')
with (getPrismaClient as jest.Mock).mockReturnValue(mockDb) to mock the imported
getPrismaClient function correctly.

In `@packages/bot/src/utils/music/queueManipulation.spec.ts`:
- Around line 92-99: The spec is still mocking and asserting removed Spotify
paths (getAudioFeatures, getBatchAudioFeatures, getSpotifyRecommendations);
remove those mocks from the jest.mock block and delete or disable the test cases
that assert audio-feature enrichment or Spotify recommendation calls (any tests
referencing getAudioFeatures, getBatchAudioFeatures, or
getSpotifyRecommendations) so the spec no longer validates dead behavior; search
for those symbol names in queueManipulation.spec.ts and remove the related mock
entries and the test blocks that expect or verify those calls.
- Around line 101-122: The tests fail because
recommendationFeedbackService.getPreferredArtistNames is now called by
replenisher.ts but isn't mocked; add a new mock function (e.g.
getPreferredArtistNamesMock = jest.fn()) alongside
dislikedTrackWeightsMock/likedTrackWeightsMock/... and expose it in the
jest.mock return under recommendationFeedbackService as getPreferredArtistNames:
(...args: unknown[]) => getPreferredArtistNamesMock(...args); also update every
beforeEach that resets/sets the other feedback mocks to initialize or
mockImplementation for getPreferredArtistNamesMock so tests exercising
replenisher.ts's Promise.all path won't throw "is not a function".

---

Nitpick comments:
In `@packages/bot/src/utils/music/autoplay/replenisher.ts`:
- Around line 151-154: The code creates intermediate allPreferredArtistNameSets
by mapping each set to new Set(set) even though allPreferredArtistNames already
contains Set instances; remove the unnecessary allPreferredArtistNameSets
variable and build preferredArtistNamesUnion directly from
allPreferredArtistNames by flattening each Set (e.g., flatMap(s => [...s])) into
the new Set constructor so preferredArtistNamesUnion is created in one step.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 6ab06d02-a22c-4556-9e74-b9016087f05f

📥 Commits

Reviewing files that changed from the base of the PR and between 923fe82 and 4e67e0d.

📒 Files selected for processing (14)
  • packages/bot/src/lastfm/index.ts
  • packages/bot/src/lastfm/lastFmApi.ts
  • packages/bot/src/services/musicRecommendation/feedbackService.spec.ts
  • packages/bot/src/services/musicRecommendation/feedbackService.ts
  • packages/bot/src/spotify/spotifyApi.spec.ts
  • packages/bot/src/spotify/spotifyApi.ts
  • packages/bot/src/utils/music/autoplay/candidateCollector.ts
  • packages/bot/src/utils/music/autoplay/candidateScorer.ts
  • packages/bot/src/utils/music/autoplay/preferredArtistSeeder.spec.ts
  • packages/bot/src/utils/music/autoplay/preferredArtistSeeder.ts
  • packages/bot/src/utils/music/autoplay/replenisher.ts
  • packages/bot/src/utils/music/autoplay/spotifyRecommender.ts
  • packages/bot/src/utils/music/queueManipulation.spec.ts
  • packages/bot/src/utils/music/queueManipulation.ts
💤 Files with no reviewable changes (4)
  • packages/bot/src/utils/music/autoplay/candidateCollector.ts
  • packages/bot/src/utils/music/autoplay/candidateScorer.ts
  • packages/bot/src/spotify/spotifyApi.ts
  • packages/bot/src/spotify/spotifyApi.spec.ts
📜 Review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: Quality Gates
  • GitHub Check: SonarCloud Scan
🔇 Additional comments (5)
packages/bot/src/lastfm/index.ts (1)

1-15: LGTM.

The new getArtistTopTracks export is correctly wired through the Last.fm barrel module.

packages/bot/src/lastfm/lastFmApi.ts (1)

324-350: LGTM.

The helper follows the existing Last.fm client pattern and returns the simplified { artist, title } shape needed by the seeder.

packages/bot/src/services/musicRecommendation/feedbackService.ts (1)

474-488: LGTM.

The Prisma query shape matches the UserArtistPreference schema, and trimming/filtering avoids blank artist names reaching the autoplay seeder.

packages/bot/src/utils/music/autoplay/replenisher.ts (1)

97-154: LGTM.

Preferred artist names are gathered across the requester/VC members and the new seeder is invoked before the Last.fm source, matching the intended priority.

Also applies to: 219-249

packages/bot/src/utils/music/autoplay/preferredArtistSeeder.ts (1)

7-11: No action needed. The import statement is correct.

All three functions—shouldIncludeCandidate, upsertScoredCandidate, and normalizeTrackKey—are properly exported from ../queueManipulation. While shouldIncludeCandidate and upsertScoredCandidate originate in ./candidateCollector, they are explicitly re-exported from queueManipulation.ts for backward compatibility. The normalizeTrackKey function is defined directly in queueManipulation.ts.

			> Likely an incorrect or invalid review comment.

Comment on lines +104 to +128
it('skips blocked artists', async () => {
const blockedArtistKeys = new Set(['artistbkey'])
const candidates = new Map()

await collectPreferredArtistCandidates(
mockQueue as GuildQueue,
mockUser as User,
new Set(),
new Set(),
new Map(),
new Map(),
new Set(),
blockedArtistKeys,
mockCurrentTrack as Track,
new Set(),
candidates,
'similar',
new Map(),
new Set(),
new Set(),
null,
['Artist B'],
)

expect(jest.mocked(lastfm.getArtistTopTracks)).not.toHaveBeenCalled()

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Fix the blocked-key fixture so this test actually exercises the skip path.

'Artist B' normalizes to 'artistb', not 'artistbkey', so getArtistTopTracks will still be called and this assertion will fail.

✅ Proposed test fix
     it('skips blocked artists', async () => {
-        const blockedArtistKeys = new Set(['artistbkey'])
+        const blockedArtistKeys = new Set(['artistb'])
         const candidates = new Map()
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
it('skips blocked artists', async () => {
const blockedArtistKeys = new Set(['artistbkey'])
const candidates = new Map()
await collectPreferredArtistCandidates(
mockQueue as GuildQueue,
mockUser as User,
new Set(),
new Set(),
new Map(),
new Map(),
new Set(),
blockedArtistKeys,
mockCurrentTrack as Track,
new Set(),
candidates,
'similar',
new Map(),
new Set(),
new Set(),
null,
['Artist B'],
)
expect(jest.mocked(lastfm.getArtistTopTracks)).not.toHaveBeenCalled()
it('skips blocked artists', async () => {
const blockedArtistKeys = new Set(['artistb'])
const candidates = new Map()
await collectPreferredArtistCandidates(
mockQueue as GuildQueue,
mockUser as User,
new Set(),
new Set(),
new Map(),
new Map(),
new Set(),
blockedArtistKeys,
mockCurrentTrack as Track,
new Set(),
candidates,
'similar',
new Map(),
new Set(),
new Set(),
null,
['Artist B'],
)
expect(jest.mocked(lastfm.getArtistTopTracks)).not.toHaveBeenCalled()
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/bot/src/utils/music/autoplay/preferredArtistSeeder.spec.ts` around
lines 104 - 128, The blocked-key fixture is wrong: the test intends to exercise
the "skip blocked artists" branch in collectPreferredArtistCandidates but uses
'Artist B' (normalizes to 'artistb') while blockedArtistKeys contains
'artistbkey', so lastfm.getArtistTopTracks is still called; update the
blockedArtistKeys set to contain the normalized key used by the function (e.g.,
'artistb') so collectPreferredArtistCandidates recognizes the artist as blocked
and the expectation that lastfm.getArtistTopTracks was not called will hold.

Comment on lines +56 to +61
const normalizedArtistKey = artistName
.toLowerCase()
.replaceAll(/[^a-z0-9]+/g, '')
.trim()

if (blockedArtistKeys.has(normalizedArtistKey)) continue

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Use the same artist-key normalization as feedback storage.

Blocked keys are produced via cleanAuthor(...) in RecommendationFeedbackService; this inline normalization can miss blocks for names with suffixes/noise that cleanAuthor strips.

🛡️ Proposed normalization alignment
-import { cleanSearchQuery } from '../searchQueryCleaner'
+import { cleanAuthor, cleanSearchQuery } from '../searchQueryCleaner'
@@
-        const normalizedArtistKey = artistName
+        const normalizedArtistKey = cleanAuthor(artistName)
             .toLowerCase()
             .replaceAll(/[^a-z0-9]+/g, '')
             .trim()
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/bot/src/utils/music/autoplay/preferredArtistSeeder.ts` around lines
56 - 61, The inline normalization in preferredArtistSeeder.ts (calculating
normalizedArtistKey from artistName) can miss variants cleaned by
RecommendationFeedbackService.cleanAuthor; change the code to use the same
cleaning function (import or access RecommendationFeedbackService.cleanAuthor)
when producing the artist key (e.g., compute cleaned = cleanAuthor(artistName)
and use that to check blockedArtistKeys.has(cleaned)), ensuring any
trimming/empty handling matches the feedback storage behavior.

Comment on lines +66 to +105
for (const track of topTracks) {
const query = cleanSearchQuery(track.title, track.artist)
const searchTracks = await searchLastFmQuery(queue, query, requestedBy)
for (const searchedTrack of searchTracks) {
if (!shouldIncludeCandidate(searchedTrack, excludedUrls, excludedKeys))
continue
const normalizedKey = normalizeTrackKey(
searchedTrack.title,
searchedTrack.author,
)
const dislikedWeight = dislikedWeights.get(normalizedKey)
if (dislikedWeight !== undefined && dislikedWeight > 0.5) continue

const rec = calculateRecommendationScore(
searchedTrack,
currentTrack,
recentArtists,
likedWeights,
preferredArtistKeys,
blockedArtistKeys,
autoplayMode,
artistFrequency,
implicitDislikeKeys,
implicitLikeKeys,
dislikedWeights,
sessionMood,
true,
)
if (rec.score === -Infinity) continue

upsertScoredCandidate(candidates, searchedTrack, {
score: rec.score + PREFERRED_ARTIST_SCORE_BOOST,
reason: rec.reason
? `${rec.reason} • preferred artist`
: 'preferred artist',
})
}
}

if (candidates.size >= AUTOPLAY_BUFFER_SIZE) break

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Stop as soon as the candidate buffer is full.

The buffer check only runs after an artist finishes, so one artist can trigger up to topTracks × searchResults candidate work after the buffer is already full. This can add avoidable Last.fm/search latency during autoplay replenishment.

⚡ Proposed early-exit guard
         for (const track of topTracks) {
+            if (candidates.size >= AUTOPLAY_BUFFER_SIZE) break
+
             const query = cleanSearchQuery(track.title, track.artist)
             const searchTracks = await searchLastFmQuery(queue, query, requestedBy)
             for (const searchedTrack of searchTracks) {
+                if (candidates.size >= AUTOPLAY_BUFFER_SIZE) break
+
                 if (!shouldIncludeCandidate(searchedTrack, excludedUrls, excludedKeys))
                     continue
@@
 
         if (candidates.size >= AUTOPLAY_BUFFER_SIZE) break
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
for (const track of topTracks) {
const query = cleanSearchQuery(track.title, track.artist)
const searchTracks = await searchLastFmQuery(queue, query, requestedBy)
for (const searchedTrack of searchTracks) {
if (!shouldIncludeCandidate(searchedTrack, excludedUrls, excludedKeys))
continue
const normalizedKey = normalizeTrackKey(
searchedTrack.title,
searchedTrack.author,
)
const dislikedWeight = dislikedWeights.get(normalizedKey)
if (dislikedWeight !== undefined && dislikedWeight > 0.5) continue
const rec = calculateRecommendationScore(
searchedTrack,
currentTrack,
recentArtists,
likedWeights,
preferredArtistKeys,
blockedArtistKeys,
autoplayMode,
artistFrequency,
implicitDislikeKeys,
implicitLikeKeys,
dislikedWeights,
sessionMood,
true,
)
if (rec.score === -Infinity) continue
upsertScoredCandidate(candidates, searchedTrack, {
score: rec.score + PREFERRED_ARTIST_SCORE_BOOST,
reason: rec.reason
? `${rec.reason} • preferred artist`
: 'preferred artist',
})
}
}
if (candidates.size >= AUTOPLAY_BUFFER_SIZE) break
for (const track of topTracks) {
if (candidates.size >= AUTOPLAY_BUFFER_SIZE) break
const query = cleanSearchQuery(track.title, track.artist)
const searchTracks = await searchLastFmQuery(queue, query, requestedBy)
for (const searchedTrack of searchTracks) {
if (candidates.size >= AUTOPLAY_BUFFER_SIZE) break
if (!shouldIncludeCandidate(searchedTrack, excludedUrls, excludedKeys))
continue
const normalizedKey = normalizeTrackKey(
searchedTrack.title,
searchedTrack.author,
)
const dislikedWeight = dislikedWeights.get(normalizedKey)
if (dislikedWeight !== undefined && dislikedWeight > 0.5) continue
const rec = calculateRecommendationScore(
searchedTrack,
currentTrack,
recentArtists,
likedWeights,
preferredArtistKeys,
blockedArtistKeys,
autoplayMode,
artistFrequency,
implicitDislikeKeys,
implicitLikeKeys,
dislikedWeights,
sessionMood,
true,
)
if (rec.score === -Infinity) continue
upsertScoredCandidate(candidates, searchedTrack, {
score: rec.score + PREFERRED_ARTIST_SCORE_BOOST,
reason: rec.reason
? `${rec.reason} • preferred artist`
: 'preferred artist',
})
}
}
if (candidates.size >= AUTOPLAY_BUFFER_SIZE) break
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/bot/src/utils/music/autoplay/preferredArtistSeeder.ts` around lines
66 - 105, The candidate buffer size is only checked after each artist loop so
the inner search loop can overfill work; after calling
upsertScoredCandidate(...) inside the inner loop (the loop iterating over
searchTracks in preferredArtistSeeder.ts that processes searchedTrack),
immediately check if candidates.size >= AUTOPLAY_BUFFER_SIZE and break out of
the inner loop, and then ensure the outer topTracks loop also exits (use a
labeled loop, set a flag, or return early from the enclosing function) so no
further searchLastFmQuery or candidate processing occurs once the buffer is
full.

@sonarqubecloud

Copy link
Copy Markdown

Quality Gate Failed Quality Gate failed

Failed conditions
20.7% Coverage on New Code (required ≥ 80%)
22.2% Duplication on New Code (required ≤ 3%)

See analysis details on SonarQube Cloud

@LucasSantana-Dev

Copy link
Copy Markdown
Owner Author

Closing — the refactor compiles (0 TS errors) but breaks replenisher tests (0 candidates produced where >=1 expected, ~12 failures across queueManipulation/candidateScorer/candidateCollector/replenisher/preferredArtistSeeder/feedbackService specs). The dead Spotify endpoints (/v1/recommendations, /v1/audio-features) were already failing silently — leaving them in place doesn't actively harm autoplay. Will re-attempt with proper test mock updates after the live UX bugs (no Discover pictures, broken genre autoplay) are addressed in the in-flight ultraplan.

auto-merge was automatically disabled April 19, 2026 16:11

Pull request was closed

This branch was successfully deployed

1 active deployment
Preview — 4e67e0d1 Deployed Apr 19, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bot enhancement New feature or request size/xl

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant