Skip to content

fix(autoplay): prioritize user tracks, Spotify liked seeds, sertanejo filter, expand Last.fm limits - #817

Merged
LucasSantana-Dev merged 22 commits into
release/v2.10.0from
fix/autoplay-queue-priority-spotify-seeds
May 8, 2026
Merged

LucasSantana-Dev merged 22 commits into
release/v2.10.0from
fix/autoplay-queue-priority-spotify-seeds

Conversation

@LucasSantana-Dev

@LucasSantana-Dev LucasSantana-Dev commented May 7, 2026 •

Copy link
Copy Markdown
Owner

Summary

  • Queue priority: Buffer calculation now counts only autoplay-tagged tracks (isAutoplay: true in metadata), so user-added songs are never displaced to make room for autoplay
  • Wuthering Heights loop fix: Lowered fuzzy-dedup threshold (0.82 → 0.75) and strip remaster/remix/live/acoustic suffixes before comparing — songs that are "effectively the same" are excluded as duplicates
  • Spotify liked seeds: getUserSavedTracks fetches up to 200 liked tracks from Spotify; up to 3 are used as priority seeds in the Recommendations API call (ahead of queue history seeds, max 5 total per API limit)
  • Sertanejo filter: Block sertanejo/forró genre family from autoplay via Last.fm artist tags unless the current seed track is itself sertanejo (fail-open — only filters when tags are present)
  • Last.fm variety: LASTFM_SEED_COUNT raised 3 → 15 and MAX_SIMILAR_LOOKUPS 5 → 15 for broader candidate pool

Changed files

File Change
spotifyApi.ts Add getUserSavedTracks (paged, up to 200 tracks)
spotifyUserSeeds.ts Add likedTrackIds field, fetch saved tracks, cache TTL 5min → 30min
spotifyRecommender.ts Use liked track IDs as priority seeds
replenisher.ts Count only autoplay-tagged tracks for buffer; detect & pass blockSertanejo
diversitySelector.ts Lower fuzzy threshold, strip variant suffixes from both sides of dedup check
candidateCollector.ts Accept blockSertanejo param, filter sertanejo candidates via Last.fm tags
artistTagCache.ts Export hasGenreTag helper
lastFmSeeder.ts / lastFmSeeds.ts Raise seed count constants 3/5 → 15

Test plan

  • All 75 bot autoplay/spotify tests pass (spotifyUserSeeds, diversitySelector, replenisher, candidateCollector, lastFmSeeder)
  • spotifyUserSeeds.spec.ts: covers cache hit/miss, likedTrackIds, null-token, API failure, caching, normalization
  • replenisher.spec.ts: covers full queue skip with autoplay-tagged tracks, telemetry emission, error handling
  • lastFmSeeder.spec.ts: updated expectations to reflect new seed count of 15

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Prioritize a user’s liked Spotify tracks as recommendation seeds (up to 3); saved-track fetch now supports paginated retrieval.
    • Added explicit Sertanejo tag set and an option to block that genre from candidate selection.
  • Improvements

    • Larger Last.fm seed slices and more similar-track lookups for broader recommendations.
    • Improved duplicate detection (strip variant suffixes) and relaxed title fuzzy threshold.
    • Replenisher counts only autoplay-marked tracks; cache TTL standardized to 30 minutes.
    • Fallback candidate scoring enriched with per-artist genre tags and resilient tag fallbacks.
  • Tests

    • Expanded test coverage for new behaviors, edge cases, and failure modes.

Greptile Summary

  • Pagination & priority seeds: getUserSavedTracks now paginates up to 200 liked tracks (default limit changed 50 → 200, resolving the prior P1 where the call site passed no argument and silently capped at 50); up to 3 liked IDs are prepended as priority seeds before queue-history seeds in the Recommendations call.
  • Autoplay buffer & dedup: Replenisher counts only isAutoplay-tagged tracks for the buffer so user-added songs are never displaced; fuzzy threshold lowered to 0.75 and stripVariantSuffix strips remaster/remix/live/acoustic suffixes before duplicate comparison.
  • Sertanejo filter & Last.fm breadth: blockSertanejo flag (fail-open at candidate level) gates sertanejo/forró candidates via Last.fm tags; LASTFM_SEED_COUNT unified and raised 3/5 → 15, MAX_SIMILAR_LOOKUPS 5 → 15 for a broader pool. Note: the existing P2 — sertanejo filter does not apply to the collectSpotifyRecommendationCandidates path — remains open.

Confidence Score: 5/5

Safe to merge — the prior P1 (50-track cap) is resolved by the default-limit change and all new logic is well-tested.

Both previous P1 findings are fixed by changing the default limit to 200. No new P1 or P0 issues found. The only open item is the pre-existing P2 that sertanejo blocking does not cover the Spotify Recommendations code path, already flagged in the prior review. P2s do not lower the confidence score.

candidateCollector.ts — sertanejo filter gap on the Spotify Recommendations path (pre-existing P2, not introduced by this PR).

Important Files Changed

Filename Overview
packages/bot/src/spotify/spotifyApi.ts Rewrote getUserSavedTracks to paginate up to 200 tracks; changed default limit from 50 to 200, fixing the previous P1 where the call site passed no argument and silently capped at 50.
packages/bot/src/spotify/spotifyUserSeeds.ts Adds likedTrackIds field to UserSpotifySeeds, fetches saved tracks, bumps cache TTL from 5 min to 30 min. getUserSavedTracks(token) now correctly uses the updated default of 200.
packages/bot/src/utils/music/autoplay/replenisher.ts Buffer calculation now counts only autoplay-tagged tracks; adds blockSertanejo flag derived from seed track tags and threads it into collectRecommendationCandidates.
packages/bot/src/utils/music/autoplay/candidateCollector.ts Exports SERTANEJO_TAGS and applies sertanejo blocking in the searchSeedCandidates path. Spotify Recommendations path still bypasses the filter — existing P2 from prior review remains unfixed.
packages/bot/src/utils/music/autoplay/diversitySelector.ts Lowers fuzzy dedup threshold 0.82 to 0.75 and adds stripVariantSuffix to normalize remaster/remix/live/acoustic suffixes before comparison.
packages/bot/src/utils/music/autoplay/spotifyRecommender.ts Liked-track IDs prepended as priority seeds (up to 3) before queue-history seeds; combined list deduplicated and capped at 5 per Spotify API limit.
packages/bot/src/utils/music/autoplay/lastFmSeeder.ts Removes private LASTFM_SEED_COUNT=3, imports shared constant (now 15); raises MAX_SIMILAR_LOOKUPS 5 to 15; adds early break when candidates.size >= AUTOPLAY_BUFFER_SIZE.

Comments Outside Diff (3)

  1. packages/bot/src/spotify/spotifyApi.spec.ts, line 102-114 (link)

    P1 Test will fail: maxTracks is 50, not 200

    getUserSavedTracks('token') uses the default limit = 50, giving maxTracks = Math.min(50, 200) = 50. With 50 items returned per page, the loop breaks after a single request (savedTrackIds.length >= maxTracks → 50 >= 50 → true). The assertion expect(fetchMock).toHaveBeenCalledTimes(4) will never be reached — only 1 call is made.

    To test the 200-track cap, the call must pass the explicit limit: getUserSavedTracks('token', 200).

    Fix in Claude Code Fix in Codex

  2. packages/bot/src/spotify/spotifyUserSeeds.ts, line 61 (link)

    P1 Only 50 liked tracks fetched, not 200

    getUserSavedTracks(token) is called without a limit argument, so limit defaults to 50 and maxTracks = Math.min(50, 200) = 50. Only a single Spotify page (50 tracks) is ever fetched, despite the PR description stating "fetches up to 200 liked tracks from Spotify".

    To use the new pagination logic and match the stated intent, pass the explicit maximum: getUserSavedTracks(token, 200).

    Fix in Claude Code Fix in Codex

  3. packages/bot/src/utils/music/autoplay/candidateCollector.ts, line 122-143 (link)

    P2 Sertanejo filter does not apply to Spotify Recommendations path

    The blockSertanejo guard (line 172) is applied only to candidates that come from searchSeedCandidates. Candidates added by collectSpotifyRecommendationCandidates (line 123) skip this check entirely, so sertanejo tracks recommended by the Spotify API can still enter the pool regardless of blockSertanejo. Consider passing or applying the same filter inside collectSpotifyRecommendationCandidates.

    Fix in Claude Code Fix in Codex

Reviews (3): Last reviewed commit: "fix(spotify): drain response body on non..." | Re-trigger Greptile

… sertanejo, expand Last.fm limits

- Buffer calculation now counts only autoplay-tagged tracks so user-added songs are never displaced
- Lower fuzzy-dedup threshold (0.82→0.75) and strip remaster/remix/live suffixes to break Wuthering Heights loop
- Fetch up to 200 liked Spotify tracks (getUserSavedTracks); use up to 3 as priority seeds in Spotify Recommendations API
- Block sertanejo/forró genre family via Last.fm artist tags unless seed track is itself sertanejo (fail-open)
- Raise LASTFM_SEED_COUNT 3→15 and MAX_SIMILAR_LOOKUPS 5→15 for broader Last.fm variety
- Export hasGenreTag helper from artistTagCache; add likedTrackIds field to UserSpotifySeeds
- Update tests: mock toArray on queue tracks map, reset getUserSavedTracks mock in beforeEach, update Last.fm seed count expectations

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@vercel

vercel Bot commented May 7, 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 May 8, 2026 6:45pm

Request Review

@github-actions github-actions Bot added bot size/m and removed bot labels May 7, 2026
@coderabbitai

coderabbitai Bot commented May 7, 2026 •

Copy link
Copy Markdown
📝 Walkthrough

Walkthrough

Integrates paginated Spotify saved-track fetching and liked-track seeds, raises Last.fm seed/lookup counts, adds hasGenreTag and Sertanejo blocking, improves duplicate-title normalization and fuzzy threshold, counts only autoplay-marked tracks for runway, and updates tests and cache TTLs.

Changes

Autoplay recommendation pipeline

Layer / File(s) Summary
Constants & Utilities
packages/bot/src/utils/music/autoplay/lastFmSeeds.ts, packages/bot/src/utils/music/autoplay/artistTagCache.ts, packages/bot/src/utils/music/languageHeuristics.ts
LASTFM_SEED_COUNT increased to 15; added hasGenreTag(tags, genreVariants); added Spanish tokens and word-boundary matching via hasWordMatch/LETTER_RE.
Duplicate Detection
packages/bot/src/utils/music/autoplay/diversitySelector.ts
Lowered FUZZY_TITLE_THRESHOLD 0.82→0.75; replaced stripFeaturing and added stripVariantSuffix()/VARIANT_KEYWORDS; exclusion keys now include variant-stripped title-only.
Last.fm Seeding
packages/bot/src/utils/music/autoplay/lastFmSeeder.ts, packages/bot/src/utils/music/autoplay/lastFmSeeds.ts
Increased LASTFM_SEED_COUNT defaults to 15 and MAX_SIMILAR_LOOKUPS to 15; added per-seed candidate-size guard; tests updated for larger pools.
Spotify API & Seeds
packages/bot/src/spotify/spotifyApi.ts, packages/bot/src/spotify/spotifyUserSeeds.ts
getUserSavedTracks(accessToken, limit=200) now paginates /v1/me/tracks (pageLimit=50), caps results and handles errors; UserSpotifySeeds includes likedTrackIds; CACHE_TTL_MS set to 30 minutes and tests updated.
Seed Selection / Recommender
packages/bot/src/utils/music/autoplay/spotifyRecommender.ts
Recommender fetches userSpotifySeeds and merges up to 3 liked track IDs (deduped) with queue-derived seeds, capping seeds to 5 for Spotify requests.
Candidate Fallback & Enrichment
packages/bot/src/utils/music/candidateFallback.ts
collectBroadFallbackCandidates accepts optional genreContext.getArtistTags, falls back to getArtistGenres if Last.fm tags empty and token available, logs failures, and passes per-candidate candidateTags into scoring.
Candidate Collection & Genre Gating
packages/bot/src/utils/music/autoplay/candidateCollector.ts
Adds SERTANEJO_TAGS; collectRecommendationCandidates accepts genreContext.blockSertanejo and filters candidates whose fetched artist tags match Sertanejo (tag fetches fail-open and are logged); tests added for block and fail-open scenarios.
Autoplay Replenisher
packages/bot/src/utils/music/autoplay/replenisher.ts
Autoplay runway computed by counting metadata.isAutoplay === true; computes blockSertanejo per seed via hasGenreTag() and forwards into candidate collection context.
Tests & Fixtures
packages/bot/src/spotify/*.spec.ts, packages/bot/src/utils/music/autoplay/*.spec.ts, packages/bot/src/utils/music/*.spec.ts
Added/updated tests: getUserSavedTracks suite, liked-track caching tests, artistTagCache tests, sertanejo-blocking and fail-open tests, diversity variant-suffix cases, larger Last.fm pool tests, replenisher queue helpers, and various fixture updates.

Sequence Diagram(s)

sequenceDiagram
    participant User as User/Requester
    participant Cache as Seeds Cache
    participant SpotifyAPI as Spotify API (spotifyApi)
    participant SeedsSvc as spotifyUserSeeds
    participant Recommender as SpotifyRecommender
    participant CandidateColl as CandidateCollector
    participant Replenisher as Replenisher
    participant Queue as Guild Queue

    User->>Recommender: request recommendations
    Recommender->>SeedsSvc: getUserSpotifySeeds(userId)
    SeedsSvc->>Cache: check cache
    alt cache miss
      SeedsSvc->>SpotifyAPI: getUserSavedTracks(accessToken) (paged)
      SpotifyAPI-->>SeedsSvc: saved track IDs
      SeedsSvc->>Cache: store seeds (likedTrackIds, artistIds, trackIds)
    end
    SeedsSvc-->>Recommender: userSpotifySeeds (incl. likedTrackIds)
    Recommender->>CandidateColl: collectRecommendationCandidates(seeds, genreContext)
    CandidateColl->>CandidateColl: getArtistTags(candidate.author) (optional)
    CandidateColl-->>Recommender: candidate list (filtered by genreContext.blockSertanejo)
    Recommender->>Replenisher: provide candidates
    Replenisher->>Queue: inspect tracks (count metadata.isAutoplay)
    Replenisher-->>Queue: add selected candidates
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 4.55% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title comprehensively summarizes all five major improvements: queue priority fix (user tracks), Spotify liked seeds, sertanejo filter, Last.fm limit expansion, and fuzzy-dedup threshold adjustment.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ 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 fix/autoplay-queue-priority-spotify-seeds

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[bot]
coderabbitai Bot previously requested changes May 7, 2026

@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: 2

Caution

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

⚠️ Outside diff range comments (2)
packages/bot/src/utils/music/autoplay/lastFmSeeder.ts (1)

100-188: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Add an early-exit guard to the outer seed loop — missing break dramatically amplifies API call volume.

The break at Line 186 only exits the inner similar-tracks loop. Once candidates.size >= AUTOPLAY_BUFFER_SIZE (8) is satisfied for the first time, the outer for (const seed of seedSlice) loop continues over all remaining seeds. Each remaining seed still fires getSimilarTracks (1 Last.fm call) plus up to 15 × searchLastFmQuery (each tries up to 3 engines sequentially). With LASTFM_SEED_COUNT raised from 3 → 15, the worst-case wasted calls scale from ~12 to ~224 per replenishment cycle. Given that Last.fm warns that "Your account may be suspended if your application is continuously making several calls per second," this is a real API-key suspension risk under load.

🐛 Proposed fix
 for (const seed of seedSlice) {
+    if (candidates.size >= AUTOPLAY_BUFFER_SIZE) break
     const lovedBoost = isLovedSeed(requestedBy.id, seed.artist, seed.title)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/bot/src/utils/music/autoplay/lastFmSeeder.ts` around lines 100 -
188, The outer for (const seed of seedSlice) loop lacks an early-exit when
candidates have reached AUTOPLAY_BUFFER_SIZE, causing excess Last.fm calls; add
a guard that checks candidates.size >= AUTOPLAY_BUFFER_SIZE and breaks the outer
loop (e.g., immediately after processing each seed or right after the inner
similar-tracks loop) so no further calls to getSimilarTracks or
searchLastFmQuery are issued once the buffer is full.
packages/bot/src/spotify/spotifyUserSeeds.spec.ts (1)

142-142: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Stale test description: TTL is 30 minutes, not 5.

CACHE_TTL_MS in spotifyUserSeeds.ts is 30 * 60 * 1000 (30 minutes), but the test description still says "5 minutes".

📝 Fix
-    it('should cache results for 5 minutes', async () => {
+    it('should cache results for 30 minutes', async () => {
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/bot/src/spotify/spotifyUserSeeds.spec.ts` at line 142, Update the
stale test description for the caching TTL in the spec: the actual TTL constant
is CACHE_TTL_MS = 30 * 60 * 1000 in spotifyUserSeeds.ts, so change the test
description in the failing spec ("it('should cache results for 5 minutes'...)"
in spotifyUserSeeds.spec.ts) to reflect 30 minutes (or derive the expected
minutes from CACHE_TTL_MS to keep them in sync). Ensure the test text and any
assertions that reference the TTL use the correct 30-minute value or compute it
from CACHE_TTL_MS.
🧹 Nitpick comments (5)
packages/bot/src/utils/music/autoplay/lastFmSeeder.ts (1)

25-25: ⚡ Quick win

Import LASTFM_SEED_COUNT from ./lastFmSeeds instead of re-declaring it locally.

Both lastFmSeeds.ts and lastFmSeeder.ts now independently declare const LASTFM_SEED_COUNT = 15. If one is updated without the other, they silently diverge — seed slices consumed in lastFmSeeder could be sized differently from the buffer advance in lastFmSeeds.

♻️ Proposed fix
-const LASTFM_SEED_COUNT = 15
 const LASTFM_SCORE_BOOST = 0.20

In the imports:

 import {
     consumeLastFmSeedSlice,
     consumeBlendedSeedSlice,
     isLovedSeed,
+    LASTFM_SEED_COUNT,
 } from './lastFmSeeds'
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/bot/src/utils/music/autoplay/lastFmSeeder.ts` at line 25, The
constant LASTFM_SEED_COUNT is duplicated in lastFmSeeder.ts causing drift;
remove the local declaration in lastFmSeeder.ts and import LASTFM_SEED_COUNT
from ./lastFmSeeds instead, updating the file's imports so usages inside
functions (e.g., any seed slicing or loop logic that currently references
LASTFM_SEED_COUNT) use the single shared constant from lastFmSeeds.ts; ensure
there are no remaining local declarations and run tests to confirm behavior
unchanged.
packages/bot/src/utils/music/autoplay/candidateCollector.ts (1)

109-114: ⚡ Quick win

blockSertanejo belongs in genreContext, not as a separate positional parameter.

All other genre-related signals (getArtistTags, currentTrackTags, sessionGenreFamilies) are grouped in genreContext. Adding blockSertanejo as its own trailing positional parameter breaks that convention and makes the already-long function signature harder to extend.

♻️ Proposed refactor
     genreContext: {
         getArtistTags?: ArtistTagFetcher
         currentTrackTags?: string[]
         sessionGenreFamilies?: Set<string>
+        blockSertanejo?: boolean
     } = {},
-    blockSertanejo = false,

Update callers to pass blockSertanejo inside the genreContext object.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/bot/src/utils/music/autoplay/candidateCollector.ts` around lines 109
- 114, The parameter blockSertanejo should be moved into the existing
genreContext object to keep all genre-related flags together: update the
function signature in candidateCollector (and any exported types/interfaces used
there) to remove the trailing blockSertanejo positional parameter and add
blockSertanejo?: boolean to the genreContext shape (alongside
getArtistTags/currentTrackTags/sessionGenreFamilies) with the same default
value; then update all callers to stop passing blockSertanejo as a separate
argument and instead include it as genreContext.blockSertanejo, and adjust any
local references inside the function to read genreContext.blockSertanejo instead
of the old parameter.
packages/bot/src/spotify/spotifyUserSeeds.ts (1)

18-23: ⚡ Quick win

Cache TTL is declared twice and will silently diverge if one is changed independently.

LRUCache is configured with ttl: 30 * 60 * 1000 inline while CACHE_TTL_MS = 30 * 60 * 1000 is declared separately for the manual freshness check. If the constant is updated, the LRU eviction boundary stays stale (or vice-versa).

♻️ Proposed fix — single source of truth
+const CACHE_TTL_MS = 30 * 60 * 1000
+
 const userSeedsCache = new LRUCache<string, SeededUserEntry>({
     max: 500,
-    ttl: 30 * 60 * 1000,
+    ttl: CACHE_TTL_MS,
 })
-
-const CACHE_TTL_MS = 30 * 60 * 1000
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/bot/src/spotify/spotifyUserSeeds.ts` around lines 18 - 23, The code
defines CACHE_TTL_MS but also hardcodes the same TTL in the LRUCache
instantiation, creating two sources of truth; update the LRUCache creation
(userSeedsCache) to use the CACHE_TTL_MS constant for its ttl option so both the
cache configuration and any manual freshness checks use the single CACHE_TTL_MS
symbol (ensure you reference userSeedsCache and LRUCache instantiation to make
the change).
packages/bot/src/spotify/spotifyApi.ts (1)

468-519: ⚡ Quick win

data.total could short-circuit the last empty-page round-trip.

The Spotify paging response includes a total field for the total number of items available. The response type already includes total?: number (line 494), but it is never consulted. When a user has, say, 90 saved tracks, the paginator makes a third request (offset=100) to discover there are no more items, then breaks — one avoidable HTTP call.

🔧 Optional early-exit using `total`
- if (savedTrackIds.length >= maxTracks || !data.items.length) {
+ if (savedTrackIds.length >= maxTracks || !data.items.length ||
+     (data.total !== undefined && savedTrackIds.length >= data.total)) {
      break
  }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/bot/src/spotify/spotifyApi.ts` around lines 468 - 519, The paginator
in getUserSavedTracks performs an unnecessary extra fetch because it never
checks the response's total field; use the returned data.total together with
limit/offset and maxTracks to early-exit when offset + data.items.length >=
data.total (or when offset >= data.total) so the loop stops once we've fetched
all available items (or reached maxTracks). Update the while-loop
condition/logic to consult data.total after the first response and break before
incrementing offset if there are no more pages to fetch; reference
getUserSavedTracks, the variables limit, offset, maxTracks, and the response
property data.total when implementing the early-exit.
packages/bot/src/utils/music/autoplay/replenisher.spec.ts (1)

204-204: ⚡ Quick win

Avoid as unknown as never in test track metadata setup.

This cast suppresses type safety and can hide breakages if Track metadata contracts change. Prefer a typed helper/intersection for autoplay test tracks instead of forcing never.

Suggested cleanup
- const track = createTrack({ id: `track${i}`, metadata: { isAutoplay: true } as unknown as never })
+ const track = createTrack({
+   id: `track${i}`,
+   metadata: { isAutoplay: true } as Track['metadata'],
+ })
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/bot/src/utils/music/autoplay/replenisher.spec.ts` at line 204, The
test is bypassing type checks by casting metadata to `as unknown as never` when
calling createTrack; update the test and/or the createTrack helper to accept a
properly typed autoplay metadata instead of forcing `never`—either change the
test to pass a correctly typed metadata object matching the Track metadata shape
(e.g., using a Partial/Intersection type for autoplay) or overload/adjust the
createTrack test helper to accept a partial metadata parameter so you can pass {
isAutoplay: true } without casting; reference createTrack and the Track metadata
type when making the change.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/bot/src/utils/music/autoplay/candidateCollector.ts`:
- Around line 19-25: The SERTANEJO_TAGS array is duplicated in
candidateCollector.ts and replenisher.ts; move it into the existing
artistTagCache module and import it from there. Add a named export
SERTANEJO_TAGS (or a suitably named constant) in artistTagCache.ts, remove the
local SERTANEJO_TAGS declarations from candidateCollector.ts and replenisher.ts,
and replace their usages with the imported constant; ensure any references
(e.g., places noted at lines ~169–171) are updated to use the imported symbol so
future tag edits are made in one place.

In `@packages/bot/src/utils/music/autoplay/replenisher.ts`:
- Around line 247-250: The blockSertanejo logic is currently fail-closed when
currentTrackTags is empty; change it to fail-open by treating an absent tag set
as "unknown/allow" — e.g., compute seedIsSertanejo (or blockSertanejo directly)
so that if currentTrackTags.length === 0 you default to allowing sertanejo
(seedIsSertanejo = true or blockSertanejo = false) instead of blocking; update
the lines referencing currentTrackTags, hasGenreTag, SERTANEJO_TAGS,
seedIsSertanejo and blockSertanejo accordingly so the filter only applies when
tags are present.

---

Outside diff comments:
In `@packages/bot/src/spotify/spotifyUserSeeds.spec.ts`:
- Line 142: Update the stale test description for the caching TTL in the spec:
the actual TTL constant is CACHE_TTL_MS = 30 * 60 * 1000 in spotifyUserSeeds.ts,
so change the test description in the failing spec ("it('should cache results
for 5 minutes'...)" in spotifyUserSeeds.spec.ts) to reflect 30 minutes (or
derive the expected minutes from CACHE_TTL_MS to keep them in sync). Ensure the
test text and any assertions that reference the TTL use the correct 30-minute
value or compute it from CACHE_TTL_MS.

In `@packages/bot/src/utils/music/autoplay/lastFmSeeder.ts`:
- Around line 100-188: The outer for (const seed of seedSlice) loop lacks an
early-exit when candidates have reached AUTOPLAY_BUFFER_SIZE, causing excess
Last.fm calls; add a guard that checks candidates.size >= AUTOPLAY_BUFFER_SIZE
and breaks the outer loop (e.g., immediately after processing each seed or right
after the inner similar-tracks loop) so no further calls to getSimilarTracks or
searchLastFmQuery are issued once the buffer is full.

---

Nitpick comments:
In `@packages/bot/src/spotify/spotifyApi.ts`:
- Around line 468-519: The paginator in getUserSavedTracks performs an
unnecessary extra fetch because it never checks the response's total field; use
the returned data.total together with limit/offset and maxTracks to early-exit
when offset + data.items.length >= data.total (or when offset >= data.total) so
the loop stops once we've fetched all available items (or reached maxTracks).
Update the while-loop condition/logic to consult data.total after the first
response and break before incrementing offset if there are no more pages to
fetch; reference getUserSavedTracks, the variables limit, offset, maxTracks, and
the response property data.total when implementing the early-exit.

In `@packages/bot/src/spotify/spotifyUserSeeds.ts`:
- Around line 18-23: The code defines CACHE_TTL_MS but also hardcodes the same
TTL in the LRUCache instantiation, creating two sources of truth; update the
LRUCache creation (userSeedsCache) to use the CACHE_TTL_MS constant for its ttl
option so both the cache configuration and any manual freshness checks use the
single CACHE_TTL_MS symbol (ensure you reference userSeedsCache and LRUCache
instantiation to make the change).

In `@packages/bot/src/utils/music/autoplay/candidateCollector.ts`:
- Around line 109-114: The parameter blockSertanejo should be moved into the
existing genreContext object to keep all genre-related flags together: update
the function signature in candidateCollector (and any exported types/interfaces
used there) to remove the trailing blockSertanejo positional parameter and add
blockSertanejo?: boolean to the genreContext shape (alongside
getArtistTags/currentTrackTags/sessionGenreFamilies) with the same default
value; then update all callers to stop passing blockSertanejo as a separate
argument and instead include it as genreContext.blockSertanejo, and adjust any
local references inside the function to read genreContext.blockSertanejo instead
of the old parameter.

In `@packages/bot/src/utils/music/autoplay/lastFmSeeder.ts`:
- Line 25: The constant LASTFM_SEED_COUNT is duplicated in lastFmSeeder.ts
causing drift; remove the local declaration in lastFmSeeder.ts and import
LASTFM_SEED_COUNT from ./lastFmSeeds instead, updating the file's imports so
usages inside functions (e.g., any seed slicing or loop logic that currently
references LASTFM_SEED_COUNT) use the single shared constant from
lastFmSeeds.ts; ensure there are no remaining local declarations and run tests
to confirm behavior unchanged.

In `@packages/bot/src/utils/music/autoplay/replenisher.spec.ts`:
- Line 204: The test is bypassing type checks by casting metadata to `as unknown
as never` when calling createTrack; update the test and/or the createTrack
helper to accept a properly typed autoplay metadata instead of forcing
`never`—either change the test to pass a correctly typed metadata object
matching the Track metadata shape (e.g., using a Partial/Intersection type for
autoplay) or overload/adjust the createTrack test helper to accept a partial
metadata parameter so you can pass { isAutoplay: true } without casting;
reference createTrack and the Track metadata type when making the change.
🪄 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: e87fe665-491c-4846-b959-14f850ef7b92

📥 Commits

Reviewing files that changed from the base of the PR and between 85777be and c2efc30.

📒 Files selected for processing (12)
  • packages/bot/src/spotify/spotifyApi.ts
  • packages/bot/src/spotify/spotifyUserSeeds.spec.ts
  • packages/bot/src/spotify/spotifyUserSeeds.ts
  • packages/bot/src/utils/music/autoplay/artistTagCache.ts
  • packages/bot/src/utils/music/autoplay/candidateCollector.ts
  • packages/bot/src/utils/music/autoplay/diversitySelector.ts
  • packages/bot/src/utils/music/autoplay/lastFmSeeder.spec.ts
  • packages/bot/src/utils/music/autoplay/lastFmSeeder.ts
  • packages/bot/src/utils/music/autoplay/lastFmSeeds.ts
  • packages/bot/src/utils/music/autoplay/replenisher.spec.ts
  • packages/bot/src/utils/music/autoplay/replenisher.ts
  • packages/bot/src/utils/music/autoplay/spotifyRecommender.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 context used
📓 Path-based instructions (6)
**/*.{ts,tsx}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

**/*.{ts,tsx}: Use the isPrisma*Error() helper functions to check for specific Prisma error types (e.g., isPrismaForeignKeyError, isPrismaUniqueConstraintError) instead of manually checking error codes
Use Prisma's $transaction() method to ensure database operations are atomic and avoid partial updates when multiple related tables are modified
Always use select or include in Prisma queries to explicitly specify which fields to return, avoiding unnecessary data transfer
For Redis operations, use connection pooling and implement exponential backoff retry logic for transient failures
Always use logAndRethrow() or logAndSwallow() utilities when handling errors to ensure errors are logged with context before propagating or suppressing

Files:

  • packages/bot/src/utils/music/autoplay/artistTagCache.ts
  • packages/bot/src/utils/music/autoplay/lastFmSeeds.ts
  • packages/bot/src/utils/music/autoplay/spotifyRecommender.ts
  • packages/bot/src/utils/music/autoplay/lastFmSeeder.spec.ts
  • packages/bot/src/spotify/spotifyUserSeeds.spec.ts
  • packages/bot/src/utils/music/autoplay/diversitySelector.ts
  • packages/bot/src/utils/music/autoplay/candidateCollector.ts
  • packages/bot/src/utils/music/autoplay/lastFmSeeder.ts
  • packages/bot/src/utils/music/autoplay/replenisher.ts
  • packages/bot/src/spotify/spotifyUserSeeds.ts
  • packages/bot/src/spotify/spotifyApi.ts
  • packages/bot/src/utils/music/autoplay/replenisher.spec.ts
packages/bot/src/utils/music/**/*.ts

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

Implement error recovery in music queue operations by using rescueQueue() when tracks fail to load or dependencies are unavailable

Files:

  • packages/bot/src/utils/music/autoplay/artistTagCache.ts
  • packages/bot/src/utils/music/autoplay/lastFmSeeds.ts
  • packages/bot/src/utils/music/autoplay/spotifyRecommender.ts
  • packages/bot/src/utils/music/autoplay/lastFmSeeder.spec.ts
  • packages/bot/src/utils/music/autoplay/diversitySelector.ts
  • packages/bot/src/utils/music/autoplay/candidateCollector.ts
  • packages/bot/src/utils/music/autoplay/lastFmSeeder.ts
  • packages/bot/src/utils/music/autoplay/replenisher.ts
  • packages/bot/src/utils/music/autoplay/replenisher.spec.ts
packages/{bot,backend,shared}/src/**/*.ts

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

packages/{bot,backend,shared}/src/**/*.ts: For feature toggles, check both global and guild-specific toggles using FeatureToggleService.isEnabledForGuild() rather than checking them separately
Use branded types (e.g., GuildId, UserId, ChannelId) for Discord IDs throughout the codebase to prevent type-level ID confusion

Files:

  • packages/bot/src/utils/music/autoplay/artistTagCache.ts
  • packages/bot/src/utils/music/autoplay/lastFmSeeds.ts
  • packages/bot/src/utils/music/autoplay/spotifyRecommender.ts
  • packages/bot/src/utils/music/autoplay/lastFmSeeder.spec.ts
  • packages/bot/src/spotify/spotifyUserSeeds.spec.ts
  • packages/bot/src/utils/music/autoplay/diversitySelector.ts
  • packages/bot/src/utils/music/autoplay/candidateCollector.ts
  • packages/bot/src/utils/music/autoplay/lastFmSeeder.ts
  • packages/bot/src/utils/music/autoplay/replenisher.ts
  • packages/bot/src/spotify/spotifyUserSeeds.ts
  • packages/bot/src/spotify/spotifyApi.ts
  • packages/bot/src/utils/music/autoplay/replenisher.spec.ts
packages/bot/src/**/*.ts

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

When building Discord embeds, use EmbedBuilderService.createTemplate() or EmbedBuilderService.getTemplate() instead of constructing embeds directly

Files:

  • packages/bot/src/utils/music/autoplay/artistTagCache.ts
  • packages/bot/src/utils/music/autoplay/lastFmSeeds.ts
  • packages/bot/src/utils/music/autoplay/spotifyRecommender.ts
  • packages/bot/src/utils/music/autoplay/lastFmSeeder.spec.ts
  • packages/bot/src/spotify/spotifyUserSeeds.spec.ts
  • packages/bot/src/utils/music/autoplay/diversitySelector.ts
  • packages/bot/src/utils/music/autoplay/candidateCollector.ts
  • packages/bot/src/utils/music/autoplay/lastFmSeeder.ts
  • packages/bot/src/utils/music/autoplay/replenisher.ts
  • packages/bot/src/spotify/spotifyUserSeeds.ts
  • packages/bot/src/spotify/spotifyApi.ts
  • packages/bot/src/utils/music/autoplay/replenisher.spec.ts
packages/bot/src/utils/music/autoplay/**/*.ts

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

For autoplay candidate selection, use genre-family-based penalties via getGenreFamilies() to avoid monotonous genre clustering

Files:

  • packages/bot/src/utils/music/autoplay/artistTagCache.ts
  • packages/bot/src/utils/music/autoplay/lastFmSeeds.ts
  • packages/bot/src/utils/music/autoplay/spotifyRecommender.ts
  • packages/bot/src/utils/music/autoplay/lastFmSeeder.spec.ts
  • packages/bot/src/utils/music/autoplay/diversitySelector.ts
  • packages/bot/src/utils/music/autoplay/candidateCollector.ts
  • packages/bot/src/utils/music/autoplay/lastFmSeeder.ts
  • packages/bot/src/utils/music/autoplay/replenisher.ts
  • packages/bot/src/utils/music/autoplay/replenisher.spec.ts
packages/bot/src/{spotify,utils/music}/**/*.ts

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

Extract Spotify track IDs using extractSpotifyTrackId() before passing to Spotify API calls to prevent malformed requests

Files:

  • packages/bot/src/utils/music/autoplay/artistTagCache.ts
  • packages/bot/src/utils/music/autoplay/lastFmSeeds.ts
  • packages/bot/src/utils/music/autoplay/spotifyRecommender.ts
  • packages/bot/src/utils/music/autoplay/lastFmSeeder.spec.ts
  • packages/bot/src/spotify/spotifyUserSeeds.spec.ts
  • packages/bot/src/utils/music/autoplay/diversitySelector.ts
  • packages/bot/src/utils/music/autoplay/candidateCollector.ts
  • packages/bot/src/utils/music/autoplay/lastFmSeeder.ts
  • packages/bot/src/utils/music/autoplay/replenisher.ts
  • packages/bot/src/spotify/spotifyUserSeeds.ts
  • packages/bot/src/spotify/spotifyApi.ts
  • packages/bot/src/utils/music/autoplay/replenisher.spec.ts
🔇 Additional comments (5)
packages/bot/src/utils/music/autoplay/lastFmSeeds.ts (1)

14-14: LGTM — constant increase is clean.

The LASTFM_SEED_COUNT bump from 5 → 15 in lastFmSeeds.ts is straightforward and aligns with the broader pool expansion described in the PR.

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

182-182: Assertion updates are correct.

Both toHaveBeenCalledWith('user-1', 15) assertions correctly reflect the new LASTFM_SEED_COUNT = 15 in lastFmSeeder.ts for the single-user and single-linked-VC-member paths.

Also applies to: 392-392

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

5-9: LGTM — clean, well-scoped helper.

Normalization of both sides before comparison is correct, and the early tags.length === 0 guard avoids unnecessary iteration.

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

63-64: LGTM — regex is correctly anchored and bounded.

The $ anchor prevents backtracking on non-matching titles, and all alternations use fixed-length or bounded quantifiers. No catastrophic backtracking risk.

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

84-91: LGTM — seed prioritization is correct.

likedSeedIds are raw Spotify track IDs (returned directly from getUserSavedTracks), so no extractSpotifyTrackId call is needed. Dedup via indexOf is fine at ≤8 elements.

Comment thread packages/bot/src/utils/music/autoplay/candidateCollector.ts Outdated
Comment thread packages/bot/src/utils/music/autoplay/replenisher.ts
… test pools

- Add .catch() guards on getArtistTags() in candidateCollector.ts and
  replenisher.ts to prevent unhandled rejections from Last.fm calls
- Export SERTANEJO_TAGS from candidateCollector.ts as single source of truth;
  remove duplicate local declaration in replenisher.ts
- Log HTTP errors and JSON parse failures in getUserSavedTracks instead of
  silently breaking (spotifyApi.ts)
- Pass shared getArtistTags fetcher instance from replenisher into
  collectRecommendationCandidates via genreContext
- Fix lastFmSeeds.spec.ts: expand test pools to 20 tracks so LASTFM_SEED_COUNT=15
  advances don't wrap offset, and default-count tests can return 15 items
- Fix queueManipulation.spec.ts: update placeholder track names (Song A/B/C/D/E)
  to distinct real-world titles so fuzzy dedup threshold 0.75 doesn't exclude
  all candidates; add isAutoplay:true metadata to buffer-full guard test;
  update replenishWithSingleCandidate to use non-generic track names

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…inked

Three-layer fix for Spanish/gospel songs appearing in non-Spanish sessions:

1. `languageHeuristics.ts` — expand SPANISH_DISTINCT_TOKENS with 14 Spanish
   worship-music words whose Portuguese spellings differ (fuego/fogo,
   cielo/céu, presencia/presença, alabanza/louvor, gracia/graça, eres/és,
   nuevo/novo, pueblo/povo, tierra/terra ie-diphthong, llena/cheia,
   noche/noite, hoy/hoje). Catches titles like "Eres Fiel", "Tu Gracia",
   "Fuego de Tu Presencia" that have no ñ/¿/¡ but are clearly Spanish.

2. `candidateFallback.ts` — `collectBroadFallbackCandidates` now accepts an
   optional `genreContext` (getArtistTags, currentTrackTags,
   sessionGenreFamilies). Fetches Last.fm tags per candidate and threads them
   into calculateRecommendationScore so the cross-locale and cross-genre-
   family vetoes fire in the broad-fallback path.

3. `replenisher.ts` — passes `candidateGenreContext` (including the shared
   per-pass ArtistTagFetcher) to collectBroadFallbackCandidates so Last.fm
   lookups are de-duplicated across all fallback candidates.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ejo veto tests

- Add debugLog to silent .catch() blocks in candidateFallback and candidateCollector
  so tag-fetch failures surface in debug output instead of swallowing silently
- Fix comment on 'gracias' token (Portuguese equivalent is graça/obrigado, not obrigado-as-equivalent)
- Add fail-open comment on blockSertanejo derivation in replenisher
- Add 3 sertanejo veto tests to candidateCollector.spec (block/allow/fail-open-on-empty-tags)
- Add 3 VARIANT_SUFFIX_RE tests to diversitySelector.spec via isDuplicateCandidate
  (remastered, live, mid-title variant word safety)
- Mock @lucky/shared/utils in candidateFallback.spec to fix uuid ESM parse error

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
coderabbitai[bot]
coderabbitai Bot previously requested changes May 7, 2026

@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: 1

🧹 Nitpick comments (3)
packages/bot/src/utils/music/autoplay/candidateCollector.spec.ts (2)

568-599: ⚡ Quick win

Assert tag lookup is skipped when blockSertanejo=false.

Line 568 validates result behavior, but it should also assert no tag fetch is performed when blocking is disabled to prevent unnecessary Last.fm calls.

Suggested assertion
         it('allows sertanejo candidates when blockSertanejo=false', async () => {
             collectSpotifyRecommendationCandidatesMock.mockResolvedValue(undefined)
             const serTanejoTrack = createTrack({ title: 'Saudade do Nordeste', author: 'Jorge e Mateus' })
             searchSeedCandidatesMock.mockResolvedValue([serTanejoTrack])

             const getArtistTags = jest.fn().mockResolvedValue(['sertanejo'])

             const result = await collectRecommendationCandidates(
                 createGuildQueue(),
                 [createTrack()],
                 null,
                 new Set(),
                 new Set(),
                 new Map(),
                 new Map(),
                 new Set(),
                 new Set(),
                 createTrack(),
                 new Set(),
                 0,
                 'similar',
                 new Map(),
                 new Set(),
                 new Set(),
                 null,
                 null,
                 { getArtistTags },
                 false, // blockSertanejo
             )

             expect(result.size).toBeGreaterThan(0)
+            expect(getArtistTags).not.toHaveBeenCalled()
         })
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/bot/src/utils/music/autoplay/candidateCollector.spec.ts` around
lines 568 - 599, The test should also assert that tag lookup is skipped when
blockSertanejo=false; update the spec in candidateCollector.spec.ts for the
'allows sertanejo candidates when blockSertanejo=false' case by adding an
assertion that the mocked getArtistTags function was not called (e.g.
expect(getArtistTags).not.toHaveBeenCalled()) after awaiting
collectRecommendationCandidates; this ensures collectRecommendationCandidates
(the function under test) does not invoke getArtistTags when the blockSertanejo
flag is false.

601-632: ⚡ Quick win

Add a true Last.fm failure-path assertion for fail-open behavior.

Line 601 describes “Last.fm unavailable,” but the mock on Line 606 returns an empty array rather than simulating a failure. Add a case with mockRejectedValue(...) to verify candidates are still allowed when tag fetch throws.

Suggested test addition
+        it('fails open when getArtistTags throws (Last.fm error)', async () => {
+            collectSpotifyRecommendationCandidatesMock.mockResolvedValue(undefined)
+            const serTanejoTrack = createTrack({ title: 'Saudade do Nordeste', author: 'Jorge e Mateus' })
+            searchSeedCandidatesMock.mockResolvedValue([serTanejoTrack])
+
+            const getArtistTags = jest.fn().mockRejectedValue(new Error('lastfm unavailable'))
+
+            const result = await collectRecommendationCandidates(
+                createGuildQueue(),
+                [createTrack()],
+                null,
+                new Set(),
+                new Set(),
+                new Map(),
+                new Map(),
+                new Set(),
+                new Set(),
+                createTrack(),
+                new Set(),
+                0,
+                'similar',
+                new Map(),
+                new Set(),
+                new Set(),
+                null,
+                null,
+                { getArtistTags },
+                true,
+            )
+
+            expect(result.size).toBeGreaterThan(0)
+        })
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/bot/src/utils/music/autoplay/candidateCollector.spec.ts` around
lines 601 - 632, The test currently simulates Last.fm returning empty tags but
not an actual failure; update or add a test that uses
getArtistTags.mockRejectedValue(new Error(...)) so the
collectRecommendationCandidates call (the same test case "does not block
sertanejo when tags are empty (fail-open when Last.fm unavailable)" or a new
test with that name) exercises the failure path; keep blockSertanejo true, pass
the mocked { getArtistTags } into collectRecommendationCandidates, await the
result, and assert result.size > 0 to confirm fail-open behavior when
getArtistTags throws.
packages/bot/src/utils/music/autoplay/lastFmSeeds.spec.ts (1)

269-287: 💤 Low value

Test description contradicts the scenario being tested.

The test is named "increments offset using modulo wrap-around", yet the pool (20) is larger than LASTFM_SEED_COUNT (15), so (0 + 15) % 20 = 15 — no wrap-around occurs. The inline comment on line 271 even confirms this. The test effectively verifies the no-wrap, forward-advance case.

Consider renaming the test to reflect what it actually exercises, and adding a separate case that demonstrates the actual wrap path at the new seed count (two advances: (15 + 15) % 20 = 10):

✏️ Suggested rename + additional wrap test
-    it('increments offset using modulo wrap-around', async () => {
+    it('advances offset by LASTFM_SEED_COUNT when pool is larger than step', async () => {
         getByDiscordIdMock.mockResolvedValue({ lastFmUsername: 'user123' })
-        // Pool of 20 so (0+15)%20 = 15, no wrap
+        // Pool of 20, step=15: (0+15)%20 = 15 (no wrap on first advance)
         getTopTracksMock.mockResolvedValue(
             Array.from({ length: 20 }, (_, i) => ({
+    it('wraps offset when cumulative advances exceed pool (LASTFM_SEED_COUNT=15, pool=20)', async () => {
+        getByDiscordIdMock.mockResolvedValue({ lastFmUsername: 'user123' })
+        getTopTracksMock.mockResolvedValue(
+            Array.from({ length: 20 }, (_, i) => ({
+                artist: `A${i + 1}`,
+                title: `S${i + 1}`,
+                playCount: i + 1,
+            })),
+        )
+
+        await getLastFmSeedTracks('user-double-advance')
+
+        advanceLastFmSeedOffset('user-double-advance') // offset → 15
+        advanceLastFmSeedOffset('user-double-advance') // offset → (15+15)%20 = 10
+
+        expect(getLastFmCacheOffset('user-double-advance')).toBe(10)
+    })
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/bot/src/utils/music/autoplay/lastFmSeeds.spec.ts` around lines 269 -
287, The test name "increments offset using modulo wrap-around" is misleading
because the scenario (pool size 20 > LASTFM_SEED_COUNT 15) does not wrap; update
the spec: rename this test to something like "increments offset without
wrap-around" and keep its current assertions using getLastFmSeedTracks,
getLastFmCacheOffset and advanceLastFmSeedOffset; then add a new test that
demonstrates actual wrap-around by advancing twice (e.g., call
advanceLastFmSeedOffset twice after seeding) and asserting the offset equals
(LASTFM_SEED_COUNT * 2) % poolSize (use LASTFM_SEED_COUNT and the same
mocks/getTopTracksMock to construct a pool of 20) so the second test verifies
the modulo wrap behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/bot/src/utils/music/candidateFallback.ts`:
- Around line 132-137: Replace the inline .catch handlers that call debugLog and
return fallbacks with the project's logAndSwallow utility: for the candidateTags
assignment that awaits genreContext.getArtistTags(track.author) and for the
per-track getArtistGenres call (the getArtistGenres catch at the 259-262
region), wrap the awaited promise in logAndSwallow so errors are logged with
context and swallowed consistently; keep the same fallback return values (empty
string array) and preserve the context data (author/track info) when calling
logAndSwallow; ensure you import/ensure availability of logAndSwallow in
candidateFallback.ts if not already present.

---

Nitpick comments:
In `@packages/bot/src/utils/music/autoplay/candidateCollector.spec.ts`:
- Around line 568-599: The test should also assert that tag lookup is skipped
when blockSertanejo=false; update the spec in candidateCollector.spec.ts for the
'allows sertanejo candidates when blockSertanejo=false' case by adding an
assertion that the mocked getArtistTags function was not called (e.g.
expect(getArtistTags).not.toHaveBeenCalled()) after awaiting
collectRecommendationCandidates; this ensures collectRecommendationCandidates
(the function under test) does not invoke getArtistTags when the blockSertanejo
flag is false.
- Around line 601-632: The test currently simulates Last.fm returning empty tags
but not an actual failure; update or add a test that uses
getArtistTags.mockRejectedValue(new Error(...)) so the
collectRecommendationCandidates call (the same test case "does not block
sertanejo when tags are empty (fail-open when Last.fm unavailable)" or a new
test with that name) exercises the failure path; keep blockSertanejo true, pass
the mocked { getArtistTags } into collectRecommendationCandidates, await the
result, and assert result.size > 0 to confirm fail-open behavior when
getArtistTags throws.

In `@packages/bot/src/utils/music/autoplay/lastFmSeeds.spec.ts`:
- Around line 269-287: The test name "increments offset using modulo
wrap-around" is misleading because the scenario (pool size 20 >
LASTFM_SEED_COUNT 15) does not wrap; update the spec: rename this test to
something like "increments offset without wrap-around" and keep its current
assertions using getLastFmSeedTracks, getLastFmCacheOffset and
advanceLastFmSeedOffset; then add a new test that demonstrates actual
wrap-around by advancing twice (e.g., call advanceLastFmSeedOffset twice after
seeding) and asserting the offset equals (LASTFM_SEED_COUNT * 2) % poolSize (use
LASTFM_SEED_COUNT and the same mocks/getTopTracksMock to construct a pool of 20)
so the second test verifies the modulo wrap behavior.
🪄 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: 491b1ca2-1587-4f7e-8424-7fc32e7be4e5

📥 Commits

Reviewing files that changed from the base of the PR and between c2efc30 and 042872e.

📒 Files selected for processing (12)
  • packages/bot/src/spotify/spotifyApi.ts
  • packages/bot/src/utils/music/autoplay/candidateCollector.spec.ts
  • packages/bot/src/utils/music/autoplay/candidateCollector.ts
  • packages/bot/src/utils/music/autoplay/diversitySelector.spec.ts
  • packages/bot/src/utils/music/autoplay/lastFmSeeds.spec.ts
  • packages/bot/src/utils/music/autoplay/replenisher.spec.ts
  • packages/bot/src/utils/music/autoplay/replenisher.ts
  • packages/bot/src/utils/music/candidateFallback.spec.ts
  • packages/bot/src/utils/music/candidateFallback.ts
  • packages/bot/src/utils/music/languageHeuristics.spec.ts
  • packages/bot/src/utils/music/languageHeuristics.ts
  • packages/bot/src/utils/music/queueManipulation.spec.ts
✅ Files skipped from review due to trivial changes (2)
  • packages/bot/src/utils/music/languageHeuristics.ts
  • packages/bot/src/utils/music/queueManipulation.spec.ts
🚧 Files skipped from review as they are similar to previous changes (3)
  • packages/bot/src/spotify/spotifyApi.ts
  • packages/bot/src/utils/music/autoplay/replenisher.ts
  • packages/bot/src/utils/music/autoplay/candidateCollector.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). (1)
  • GitHub Check: SonarCloud Scan
🧰 Additional context used
📓 Path-based instructions (6)
**/*.{ts,tsx}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

**/*.{ts,tsx}: Use the isPrisma*Error() helper functions to check for specific Prisma error types (e.g., isPrismaForeignKeyError, isPrismaUniqueConstraintError) instead of manually checking error codes
Use Prisma's $transaction() method to ensure database operations are atomic and avoid partial updates when multiple related tables are modified
Always use select or include in Prisma queries to explicitly specify which fields to return, avoiding unnecessary data transfer
For Redis operations, use connection pooling and implement exponential backoff retry logic for transient failures
Always use logAndRethrow() or logAndSwallow() utilities when handling errors to ensure errors are logged with context before propagating or suppressing

Files:

  • packages/bot/src/utils/music/autoplay/diversitySelector.spec.ts
  • packages/bot/src/utils/music/autoplay/candidateCollector.spec.ts
  • packages/bot/src/utils/music/autoplay/replenisher.spec.ts
  • packages/bot/src/utils/music/candidateFallback.spec.ts
  • packages/bot/src/utils/music/languageHeuristics.spec.ts
  • packages/bot/src/utils/music/candidateFallback.ts
  • packages/bot/src/utils/music/autoplay/lastFmSeeds.spec.ts
packages/bot/src/utils/music/**/*.ts

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

Implement error recovery in music queue operations by using rescueQueue() when tracks fail to load or dependencies are unavailable

Files:

  • packages/bot/src/utils/music/autoplay/diversitySelector.spec.ts
  • packages/bot/src/utils/music/autoplay/candidateCollector.spec.ts
  • packages/bot/src/utils/music/autoplay/replenisher.spec.ts
  • packages/bot/src/utils/music/candidateFallback.spec.ts
  • packages/bot/src/utils/music/languageHeuristics.spec.ts
  • packages/bot/src/utils/music/candidateFallback.ts
  • packages/bot/src/utils/music/autoplay/lastFmSeeds.spec.ts
packages/{bot,backend,shared}/src/**/*.ts

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

packages/{bot,backend,shared}/src/**/*.ts: For feature toggles, check both global and guild-specific toggles using FeatureToggleService.isEnabledForGuild() rather than checking them separately
Use branded types (e.g., GuildId, UserId, ChannelId) for Discord IDs throughout the codebase to prevent type-level ID confusion

Files:

  • packages/bot/src/utils/music/autoplay/diversitySelector.spec.ts
  • packages/bot/src/utils/music/autoplay/candidateCollector.spec.ts
  • packages/bot/src/utils/music/autoplay/replenisher.spec.ts
  • packages/bot/src/utils/music/candidateFallback.spec.ts
  • packages/bot/src/utils/music/languageHeuristics.spec.ts
  • packages/bot/src/utils/music/candidateFallback.ts
  • packages/bot/src/utils/music/autoplay/lastFmSeeds.spec.ts
packages/bot/src/**/*.ts

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

When building Discord embeds, use EmbedBuilderService.createTemplate() or EmbedBuilderService.getTemplate() instead of constructing embeds directly

Files:

  • packages/bot/src/utils/music/autoplay/diversitySelector.spec.ts
  • packages/bot/src/utils/music/autoplay/candidateCollector.spec.ts
  • packages/bot/src/utils/music/autoplay/replenisher.spec.ts
  • packages/bot/src/utils/music/candidateFallback.spec.ts
  • packages/bot/src/utils/music/languageHeuristics.spec.ts
  • packages/bot/src/utils/music/candidateFallback.ts
  • packages/bot/src/utils/music/autoplay/lastFmSeeds.spec.ts
packages/bot/src/utils/music/autoplay/**/*.ts

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

For autoplay candidate selection, use genre-family-based penalties via getGenreFamilies() to avoid monotonous genre clustering

Files:

  • packages/bot/src/utils/music/autoplay/diversitySelector.spec.ts
  • packages/bot/src/utils/music/autoplay/candidateCollector.spec.ts
  • packages/bot/src/utils/music/autoplay/replenisher.spec.ts
  • packages/bot/src/utils/music/autoplay/lastFmSeeds.spec.ts
packages/bot/src/{spotify,utils/music}/**/*.ts

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

Extract Spotify track IDs using extractSpotifyTrackId() before passing to Spotify API calls to prevent malformed requests

Files:

  • packages/bot/src/utils/music/autoplay/diversitySelector.spec.ts
  • packages/bot/src/utils/music/autoplay/candidateCollector.spec.ts
  • packages/bot/src/utils/music/autoplay/replenisher.spec.ts
  • packages/bot/src/utils/music/candidateFallback.spec.ts
  • packages/bot/src/utils/music/languageHeuristics.spec.ts
  • packages/bot/src/utils/music/candidateFallback.ts
  • packages/bot/src/utils/music/autoplay/lastFmSeeds.spec.ts
🔇 Additional comments (6)
packages/bot/src/utils/music/autoplay/diversitySelector.spec.ts (1)

251-297: Good coverage for variant-suffix dedup edge cases.

These new cases cleanly validate remastered/live suffix handling and the mid-title boundary behavior. This matches the intended duplicate-detection change well.

packages/bot/src/utils/music/languageHeuristics.spec.ts (1)

77-98: Strong coverage addition for Spanish/Portuguese boundary cases.

These new assertions directly protect the regression scenario and improve confidence in token-based detection when diacritics/tags are missing.

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

83-87: Good test-fixture alignment with queue track API.

createTracksMap() + toArray() makes the test queue shape consistent with replenisher expectations and avoids brittle mocks.

Also applies to: 92-92


202-207: Nice coverage of autoplay runway semantics.

These tests now explicitly validate that only autoplay-tagged tracks count toward buffer fullness, and that user-added tracks do not suppress replenishment.

Also applies to: 219-236

packages/bot/src/utils/music/autoplay/lastFmSeeds.spec.ts (2)

202-222: LGTM — boundary arithmetic is correct.

Pool of 20 → offset after one advance = 15 → 5 remaining items (indices 15–19) → A16…A20. Assertions at lines 219–221 are all correct.


242-255: LGTM — all three Array.from fixture updates are correct.

Pool of 20 satisfies LASTFM_SEED_COUNT = 15 in every case, so the toHaveLength(LASTFM_SEED_COUNT) and toBe(LASTFM_SEED_COUNT) assertions hold without special-casing.

Also applies to: 342-360, 437-448

Comment thread packages/bot/src/utils/music/candidateFallback.ts Outdated
…p constant, cache TTL

- Fix TS2339 in spotifyApi.ts: replace `as typeof data` (narrows to never in
  while loop) with explicit `SavedTracksPage` type alias
- Add data.total early-exit in getUserSavedTracks to avoid trailing empty fetch
- lastFmSeeder: add outer seed loop early-exit guard (prevents ~224 wasted
  Last.fm calls when buffer fills before all 15 seeds are processed)
- lastFmSeeder: remove duplicate LASTFM_SEED_COUNT local const; import from
  lastFmSeeds (single source of truth); add constant to lastFmSeeds mock in
  lastFmSeeder.spec and queueManipulation.spec
- spotifyUserSeeds: declare CACHE_TTL_MS before LRU; use it for ttl option
- spotifyUserSeeds.spec: correct stale test description "5 minutes" → "30 minutes"
- replenisher.spec: replace `as unknown as never` metadata cast with
  `as Record<string, unknown>` (type-safe, doesn't suppress errors)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@LucasSantana-Dev
LucasSantana-Dev dismissed stale reviews from coderabbitai[bot] and coderabbitai[bot] May 7, 2026 01:54

All actionable items addressed: TS type error fixed, outer loop guard added, LASTFM_SEED_COUNT deduplicated, cache TTL unified, test description corrected.

…d candidateCollector catch path

Brings new-code coverage above the 80% SonarCloud gate threshold:
- spotifyApi.spec.ts: 11 tests for getUserSavedTracks (pagination, error, filtering)
- artistTagCache.spec.ts: new file, 11 tests covering hasGenreTag + createArtistTagFetcher at 100%
- candidateCollector.spec.ts: test for getArtistTags rejection catch path (fail-open)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
coderabbitai[bot]
coderabbitai Bot previously approved these changes May 8, 2026
@coderabbitai

coderabbitai Bot commented May 8, 2026

Copy link
Copy Markdown
✅ Actions performed

Comments resolved and changes approved.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@github-actions github-actions Bot added the dependencies Pull requests that update a dependency file label May 8, 2026
Keeps Spotify genre fallback for Spanish gospel veto; uses local
getArtistTags var (createArtistTagFetcher default) from #818.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@LucasSantana-Dev

Copy link
Copy Markdown
Owner Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented May 8, 2026

Copy link
Copy Markdown
✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@github-actions

github-actions Bot commented May 8, 2026 •

Copy link
Copy Markdown

Size Change: 0 B

Total Size: 367 kB

ℹ️ View Unchanged
Filename Size
packages/frontend/dist/assets/ActionPanel-dTlZUgqe.js 402 B
packages/frontend/dist/assets/Admin-DnWz1MOA.js 1.97 kB
packages/frontend/dist/assets/AutoMessages-vRB62tJd.js 2.66 kB
packages/frontend/dist/assets/AutoMod-K_viJkXQ.js 4.07 kB
packages/frontend/dist/assets/badge-8qbHQXXg.js 504 B
packages/frontend/dist/assets/Card-PloSFCea.js 506 B
packages/frontend/dist/assets/CommandsConfig-CESed-fz.js 1.44 kB
packages/frontend/dist/assets/Config-By5IL35T.js 1.64 kB
packages/frontend/dist/assets/CustomCommands-CXfqmyJo.js 2.14 kB
packages/frontend/dist/assets/DashboardOverview-CL541_VM.js 3.36 kB
packages/frontend/dist/assets/dialog-BCLK2JMT.js 952 B
packages/frontend/dist/assets/EmbedBuilder-BdtZhzFK.js 3.31 kB
packages/frontend/dist/assets/Features-ByYF18ea.js 756 B
packages/frontend/dist/assets/GuildAutomation-Dc7me2lV.js 2.9 kB
packages/frontend/dist/assets/index-DDLMHTiW.css 17.1 kB
packages/frontend/dist/assets/index-DyCTxeYW.js 56 kB
packages/frontend/dist/assets/input-cOH1PQVi.js 467 B
packages/frontend/dist/assets/label-Dh1uOFi2.js 478 B
packages/frontend/dist/assets/Landing-CD9QzN7X.js 3.71 kB
packages/frontend/dist/assets/LastFm-blvMf2ay.js 1.74 kB
packages/frontend/dist/assets/Levels-B96wzwSM.js 2.61 kB
packages/frontend/dist/assets/Login-tEr-uYLk.js 2.48 kB
packages/frontend/dist/assets/Lyrics-Dvgco87j.js 1.32 kB
packages/frontend/dist/assets/Moderation-Bqlfkaz_.js 3.81 kB
packages/frontend/dist/assets/Music-Dqx9eZ6w.js 7.11 kB
packages/frontend/dist/assets/MusicConfig-C1cDkQXP.js 1.59 kB
packages/frontend/dist/assets/PreferredArtists-CcbRnFlR.js 3.67 kB
packages/frontend/dist/assets/PrivacyPolicy-BmZtXGg8.js 1.38 kB
packages/frontend/dist/assets/ReactionRoles-Dm64nzHC.js 1.86 kB
packages/frontend/dist/assets/rolldown-runtime-BYbx6iT9.js 471 B
packages/frontend/dist/assets/SectionHeader-CtzacEAY.js 895 B
packages/frontend/dist/assets/select-FOgY7B8V.js 1.23 kB
packages/frontend/dist/assets/ServerLogs-QqqQW9cg.js 2.87 kB
packages/frontend/dist/assets/ServerSettings-sBfBpHL0.js 4.19 kB
packages/frontend/dist/assets/ServersPage-CuWU6B6g.js 2.87 kB
packages/frontend/dist/assets/Skeleton-BKxDqjuF.js 237 B
packages/frontend/dist/assets/Spotify-ChhuqXVt.js 1.75 kB
packages/frontend/dist/assets/Starboard-CReFRkuG.js 2.05 kB
packages/frontend/dist/assets/StatTile-B31RaVZ2.js 638 B
packages/frontend/dist/assets/switch-BE_uObSf.js 543 B
packages/frontend/dist/assets/TermsOfService-BolVjdlH.js 1.37 kB
packages/frontend/dist/assets/TrackHistory-Dv_7dEQE.js 2.15 kB
packages/frontend/dist/assets/TwitchNotifications-NRZsAuz4.js 2.43 kB
packages/frontend/dist/assets/useFeatures-BSC8EiID.js 1.99 kB
packages/frontend/dist/assets/usePageMetadata-T1T1Fi3u.js 329 B
packages/frontend/dist/assets/vendor-forms-DNkRbYRs.js 25.6 kB
packages/frontend/dist/assets/vendor-radix-hBYIp2YC.js 38.9 kB
packages/frontend/dist/assets/vendor-react-CFDOOXi7.js 55.7 kB
packages/frontend/dist/assets/vendor-state-BDM_FkZa.js 24.1 kB
packages/frontend/dist/assets/vendor-ui-CbZ_GPii.js 64.7 kB

compressed-size-action

Tests expected 4 pages of 50 (= 200 max), matching the pagination
cap. Was defaulting to 50 which stopped after the first page.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

@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: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/bot/src/spotify/spotifyApi.ts`:
- Around line 493-500: When handling the non-OK response in the pagination loop
(the block that logs via 'spotify.getUserSavedTracks.request' and uses the local
variable res), ensure the response body is drained or cancelled before breaking
to free the underlying TCP connection; e.g., call a safe drain like awaiting
res.arrayBuffer() or calling res.body?.cancel() (or the appropriate cancel/close
for the runtime) and await it, then call logAndSwallow and break. Update the
code around res in the getUserSavedTracks pagination logic to perform this
drain/cancel before the break so connections aren't held open on errors.
🪄 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: ae51effb-de3d-43c7-ab71-817636a944fc

📥 Commits

Reviewing files that changed from the base of the PR and between 647be19 and 798b5a6.

⛔ Files ignored due to path filters (1)
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (7)
  • packages/bot/src/spotify/spotifyApi.ts
  • packages/bot/src/spotify/spotifyUserSeeds.spec.ts
  • packages/bot/src/spotify/spotifyUserSeeds.ts
  • packages/bot/src/utils/music/autoplay/diversitySelector.ts
  • packages/bot/src/utils/music/autoplay/replenisher.ts
  • packages/bot/src/utils/music/candidateFallback.spec.ts
  • packages/bot/src/utils/music/candidateFallback.ts
🚧 Files skipped from review as they are similar to previous changes (4)
  • packages/bot/src/utils/music/autoplay/diversitySelector.ts
  • packages/bot/src/utils/music/candidateFallback.ts
  • packages/bot/src/spotify/spotifyUserSeeds.spec.ts
  • packages/bot/src/spotify/spotifyUserSeeds.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). (4)
  • GitHub Check: Greptile Review
  • GitHub Check: SonarCloud Scan
  • GitHub Check: compressed-size
  • GitHub Check: Quality Gates
🧰 Additional context used
📓 Path-based instructions (6)
**/*.{ts,tsx}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

**/*.{ts,tsx}: Use the isPrisma*Error() helper functions to check for specific Prisma error types (e.g., isPrismaForeignKeyError, isPrismaUniqueConstraintError) instead of manually checking error codes
Use Prisma's $transaction() method to ensure database operations are atomic and avoid partial updates when multiple related tables are modified
Always use select or include in Prisma queries to explicitly specify which fields to return, avoiding unnecessary data transfer
For Redis operations, use connection pooling and implement exponential backoff retry logic for transient failures
Always use logAndRethrow() or logAndSwallow() utilities when handling errors to ensure errors are logged with context before propagating or suppressing

Files:

  • packages/bot/src/spotify/spotifyApi.ts
  • packages/bot/src/utils/music/autoplay/replenisher.ts
  • packages/bot/src/utils/music/candidateFallback.spec.ts
packages/{bot,backend,shared}/src/**/*.ts

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

packages/{bot,backend,shared}/src/**/*.ts: For feature toggles, check both global and guild-specific toggles using FeatureToggleService.isEnabledForGuild() rather than checking them separately
Use branded types (e.g., GuildId, UserId, ChannelId) for Discord IDs throughout the codebase to prevent type-level ID confusion

Files:

  • packages/bot/src/spotify/spotifyApi.ts
  • packages/bot/src/utils/music/autoplay/replenisher.ts
  • packages/bot/src/utils/music/candidateFallback.spec.ts
packages/bot/src/**/*.ts

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

When building Discord embeds, use EmbedBuilderService.createTemplate() or EmbedBuilderService.getTemplate() instead of constructing embeds directly

Files:

  • packages/bot/src/spotify/spotifyApi.ts
  • packages/bot/src/utils/music/autoplay/replenisher.ts
  • packages/bot/src/utils/music/candidateFallback.spec.ts
packages/bot/src/{spotify,utils/music}/**/*.ts

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

Extract Spotify track IDs using extractSpotifyTrackId() before passing to Spotify API calls to prevent malformed requests

Files:

  • packages/bot/src/spotify/spotifyApi.ts
  • packages/bot/src/utils/music/autoplay/replenisher.ts
  • packages/bot/src/utils/music/candidateFallback.spec.ts
packages/bot/src/utils/music/**/*.ts

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

Implement error recovery in music queue operations by using rescueQueue() when tracks fail to load or dependencies are unavailable

Files:

  • packages/bot/src/utils/music/autoplay/replenisher.ts
  • packages/bot/src/utils/music/candidateFallback.spec.ts
packages/bot/src/utils/music/autoplay/**/*.ts

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

For autoplay candidate selection, use genre-family-based penalties via getGenreFamilies() to avoid monotonous genre clustering

Files:

  • packages/bot/src/utils/music/autoplay/replenisher.ts
🔇 Additional comments (4)
packages/bot/src/utils/music/autoplay/replenisher.ts (2)

241-247: Fail-open sertanejo gating is still inverted when tags are missing.

If currentTrackTags is empty, seedIsSertanejo is set to false, so blockSertanejo becomes true (fail-closed), which contradicts the intended fail-open behavior in the comment.

🐛 Minimal fix
-        const seedIsSertanejo = currentTrackTags.length > 0
-            ? hasGenreTag(currentTrackTags, SERTANEJO_TAGS)
-            : false
-        // Block sertanejo candidates unless the seed itself is sertanejo — fail-open
-        // when tags are absent (Last.fm unlinked) to avoid over-filtering.
-        const blockSertanejo = !seedIsSertanejo
+        const blockSertanejo =
+            currentTrackTags.length > 0 &&
+            !hasGenreTag(currentTrackTags, SERTANEJO_TAGS)

105-109: Autoplay runway calculation now correctly excludes user-added tracks.

Nice change: counting only metadata.isAutoplay === true prevents user-queued songs from shrinking autoplay buffer capacity.

packages/bot/src/spotify/spotifyApi.ts (1)

468-534: AI summary incorrectly states non-OK returns [].

The summary says the function "returns [] when the response is non-OK", but the code breaks out of the while loop and falls through to return savedTrackIds.slice(0, maxTracks) — returning whatever was accumulated in prior pages. The outer catch (line 530) is the only path that returns []. The actual behavior (return partial results on mid-pagination failure) is reasonable; the summary description is just inaccurate.

packages/bot/src/utils/music/candidateFallback.spec.ts (1)

19-23: LGTM — suppressing log noise in tests is the right call.

Comment thread packages/bot/src/spotify/spotifyApi.ts
Prevents TCP connection pool exhaustion when paginating with a
non-OK status response.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@LucasSantana-Dev

Copy link
Copy Markdown
Owner Author

@coderabbitai approve

@coderabbitai

coderabbitai Bot commented May 8, 2026

Copy link
Copy Markdown
✅ Actions performed

Comments resolved and changes approved.

@sonarqubecloud

sonarqubecloud Bot commented May 8, 2026

Copy link
Copy Markdown

@LucasSantana-Dev
LucasSantana-Dev merged commit ef2bc6c into release/v2.10.0 May 8, 2026
17 checks passed
LucasSantana-Dev added a commit that referenced this pull request May 8, 2026
Keep replenisher.spec.ts and backend index.test.ts from release (both
were modified upstream; PR deletion is superseded by the coverage work
in #817).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
LucasSantana-Dev added a commit that referenced this pull request May 13, 2026
… filter, expand Last.fm limits (#817)

* fix(autoplay): prioritize user tracks, add Spotify liked seeds, block sertanejo, expand Last.fm limits

- Buffer calculation now counts only autoplay-tagged tracks so user-added songs are never displaced
- Lower fuzzy-dedup threshold (0.82→0.75) and strip remaster/remix/live suffixes to break Wuthering Heights loop
- Fetch up to 200 liked Spotify tracks (getUserSavedTracks); use up to 3 as priority seeds in Spotify Recommendations API
- Block sertanejo/forró genre family via Last.fm artist tags unless seed track is itself sertanejo (fail-open)
- Raise LASTFM_SEED_COUNT 3→15 and MAX_SIMILAR_LOOKUPS 5→15 for broader Last.fm variety
- Export hasGenreTag helper from artistTagCache; add likedTrackIds field to UserSpotifySeeds
- Update tests: mock toArray on queue tracks map, reset getUserSavedTracks mock in beforeEach, update Last.fm seed count expectations

* fix(autoplay): harden error handling, deduplicate SERTANEJO_TAGS, fix test pools

- Add .catch() guards on getArtistTags() in candidateCollector.ts and
  replenisher.ts to prevent unhandled rejections from Last.fm calls
- Export SERTANEJO_TAGS from candidateCollector.ts as single source of truth;
  remove duplicate local declaration in replenisher.ts
- Log HTTP errors and JSON parse failures in getUserSavedTracks instead of
  silently breaking (spotifyApi.ts)
- Pass shared getArtistTags fetcher instance from replenisher into
  collectRecommendationCandidates via genreContext
- Fix lastFmSeeds.spec.ts: expand test pools to 20 tracks so LASTFM_SEED_COUNT=15
  advances don't wrap offset, and default-count tests can return 15 items
- Fix queueManipulation.spec.ts: update placeholder track names (Song A/B/C/D/E)
  to distinct real-world titles so fuzzy dedup threshold 0.75 doesn't exclude
  all candidates; add isAutoplay:true metadata to buffer-full guard test;
  update replenishWithSingleCandidate to use non-generic track names

* fix(autoplay): block Spanish gospel contamination when Last.fm is unlinked

Three-layer fix for Spanish/gospel songs appearing in non-Spanish sessions:

1. `languageHeuristics.ts` — expand SPANISH_DISTINCT_TOKENS with 14 Spanish
   worship-music words whose Portuguese spellings differ (fuego/fogo,
   cielo/céu, presencia/presença, alabanza/louvor, gracia/graça, eres/és,
   nuevo/novo, pueblo/povo, tierra/terra ie-diphthong, llena/cheia,
   noche/noite, hoy/hoje). Catches titles like "Eres Fiel", "Tu Gracia",
   "Fuego de Tu Presencia" that have no ñ/¿/¡ but are clearly Spanish.

2. `candidateFallback.ts` — `collectBroadFallbackCandidates` now accepts an
   optional `genreContext` (getArtistTags, currentTrackTags,
   sessionGenreFamilies). Fetches Last.fm tags per candidate and threads them
   into calculateRecommendationScore so the cross-locale and cross-genre-
   family vetoes fire in the broad-fallback path.

3. `replenisher.ts` — passes `candidateGenreContext` (including the shared
   per-pass ArtistTagFetcher) to collectBroadFallbackCandidates so Last.fm
   lookups are de-duplicated across all fallback candidates.

* fix(autoplay): address PR review findings — logging, comments, sertanejo veto tests

- Add debugLog to silent .catch() blocks in candidateFallback and candidateCollector
  so tag-fetch failures surface in debug output instead of swallowing silently
- Fix comment on 'gracias' token (Portuguese equivalent is graça/obrigado, not obrigado-as-equivalent)
- Add fail-open comment on blockSertanejo derivation in replenisher
- Add 3 sertanejo veto tests to candidateCollector.spec (block/allow/fail-open-on-empty-tags)
- Add 3 VARIANT_SUFFIX_RE tests to diversitySelector.spec via isDuplicateCandidate
  (remastered, live, mid-title variant word safety)
- Mock @lucky/shared/utils in candidateFallback.spec to fix uuid ESM parse error

* fix(autoplay): resolve CodeRabbit review — TS error, loop guard, dedup constant, cache TTL

- Fix TS2339 in spotifyApi.ts: replace `as typeof data` (narrows to never in
  while loop) with explicit `SavedTracksPage` type alias
- Add data.total early-exit in getUserSavedTracks to avoid trailing empty fetch
- lastFmSeeder: add outer seed loop early-exit guard (prevents ~224 wasted
  Last.fm calls when buffer fills before all 15 seeds are processed)
- lastFmSeeder: remove duplicate LASTFM_SEED_COUNT local const; import from
  lastFmSeeds (single source of truth); add constant to lastFmSeeds mock in
  lastFmSeeder.spec and queueManipulation.spec
- spotifyUserSeeds: declare CACHE_TTL_MS before LRU; use it for ttl option
- spotifyUserSeeds.spec: correct stale test description "5 minutes" → "30 minutes"
- replenisher.spec: replace `as unknown as never` metadata cast with
  `as Record<string, unknown>` (type-safe, doesn't suppress errors)

* ci: retrigger SonarCloud scan

* test: add missing coverage for getUserSavedTracks, artistTagCache, and candidateCollector catch path

Brings new-code coverage above the 80% SonarCloud gate threshold:
- spotifyApi.spec.ts: 11 tests for getUserSavedTracks (pagination, error, filtering)
- artistTagCache.spec.ts: new file, 11 tests covering hasGenreTag + createArtistTagFetcher at 100%
- candidateCollector.spec.ts: test for getArtistTags rejection catch path (fail-open)

* refactor(languageHeuristics): precompile word-boundary patterns at module init

Eliminates dynamic new RegExp(variable) inside countMatches, removing the
SonarCloud S5852 security hotspot. Patterns are now compiled once from the
hardcoded token lists and reused on every call (also a minor perf win).

* fix(languageHeuristics): replace dynamic RegExp with indexOf+boundary check

Eliminates new RegExp(variable) entirely — no dynamic pattern compilation at
all. Uses indexOf loop with LETTER_RE (literal static regex) to check word
boundaries, which resolves the SonarCloud S5852 security hotspot on new code.

* fix(spotifyApi): use URLSearchParams in getUserSavedTracks to eliminate S5144 SSRF hotspot

Matches the URLSearchParams pattern used by all other fetch calls in this file.

* fix(autoplay): use Spotify genres as fallback tag source when Last.fm is not linked

When Last.fm is not linked, getArtistTags returns [] for every candidate,
leaving the cross-locale Spanish veto in candidateScorer with no tags to
check. Spanish gospel artists with English-looking names (e.g. "Felipe
Dutra", "Marcos Witt") passed through undetected into non-Spanish sessions.

In collectBroadFallbackCandidates, obtain a Spotify token once and call
getArtistGenres for any candidate whose Last.fm tags are empty. The
resulting genre strings ("latin gospel", "latin christian", "spanish pop")
are fed into calculateRecommendationScore as candidateTags, allowing the
existing -Infinity cross-locale veto to fire without any new code paths.

getArtistGenres already has a 24h LRU cache so repeated lookups per artist
are cheap. Token fetch is skipped when no requestedBy user is present.

Three new tests cover: Spotify genre fallback active, skipped when Last.fm
tags are present, and skipped when no Spotify token is available.

* fix(candidateFallback): wrap getValidAccessToken in Promise.resolve for resetMocks safety

With resetMocks:true in jest.config.cjs, jest.fn().mockResolvedValue() is
cleared between tests leaving the mock returning undefined synchronously.
Calling .catch() directly on undefined throws TypeError, causing
collectBroadFallbackCandidates to reject silently and the broad-fallback
search queries to never fire.

Matches the Promise.resolve() guard pattern already used throughout
replenisher.ts and enrichWithAudioFeatures.

* fix(candidateFallback): suppress NOSONAR S5144 on getArtistGenres call — URLSearchParams used internally

* fix(candidateFallback): broaden NOSONAR suppressions — bare comment suppresses any hotspot rule

* fix(sonar): rewrite stripFeaturing to eliminate S5852 hotspot

Replace the double-quantifier regex [^)]*feat[^)]* (polynomial
backtracking per SonarCloud S5852) with an indexOf loop + single
[^)]* guard, and replace the (?:\s|$) alternation with explicit
indexOf markers. Behavior is identical for all real music metadata.

* fix(sonar): replace VARIANT_SUFFIX_RE with indexOf-based approach

The complex nested-alternation regex triggered SonarCloud S5852 twice
(once at declaration, once at usage). Replace with a plain keyword array
+ BRACKET_INNER_RE (single [a-z ]+ quantifier, unambiguous) + indexOf
loop for dash-prefix variants. All 26 diversitySelector tests pass.

* fix(sonar): eliminate BRACKET_INNER_RE — rewrite with startsWithYear+indexOf

Replace the polynomial-backtracking BRACKET_INNER_RE pattern with pure
indexOf/char-comparison logic to clear the 2 S5852 Security Hotspots.
Also replace /\s*\([^)]*\)\s*/gi in stripFeaturing with an explicit
indexOf loop for consistency and to pre-empt any future flag.

* fix(deps): patch fast-uri (high) and hono (moderate) vulnerabilities

* fix(spotify): default getUserSavedTracks limit to 200

Tests expected 4 pages of 50 (= 200 max), matching the pagination
cap. Was defaulting to 50 which stopped after the first page.

* fix(spotify): drain response body on non-OK to release connection

Prevents TCP connection pool exhaustion when paginating with a
non-OK status response.

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
LucasSantana-Dev added a commit that referenced this pull request May 13, 2026
Promote [Unreleased] entries into the v2.10.0 section, bump root + workspace
versions from 2.9.0 to 2.10.0, and update the lockfile.

Release highlights:
- Spotify 429 retry hardening (#808)
- Last.fm canonical metadata + multi-artist scrobble fix (#821)
- Autoplay Spanish-gospel-block + sertanejo prioritization series
  (#817-#820, #827, #829, #830)
- Review-tools revamp: Claude review + Danger + chilled CodeRabbit
  via org-level reusable workflows (#838)
- Coverage threshold pinned for phase-2 test cleanup (#835)
- CI extended to release/** branches (#816)
@LucasSantana-Dev
LucasSantana-Dev deleted the fix/autoplay-queue-priority-spotify-seeds branch May 23, 2026 02:21

This branch was successfully deployed

1 active deployment
Preview — 5f54cc6f Deployed May 8, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bot dependencies Pull requests that update a dependency file enhancement New feature or request size/xl

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant