Skip to content

feat(bot): intelligent autoplay — skip/completion signals, loved tracks, artist frequency, mood matching - #577

Merged
LucasSantana-Dev merged 1 commit into
mainfrom
feat/smart-autoplay-signals
Apr 13, 2026
Merged

LucasSantana-Dev merged 1 commit into
mainfrom
feat/smart-autoplay-signals

Conversation

@LucasSantana-Dev

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

Copy link
Copy Markdown
Owner

What this does

Upgrades autoplay from simple feedback to a fully implicit intelligence system — no commands required, everything inferred from listening behaviour.

New signals

Skip & completion tracking

  • guildTrackStartTimes tracks when each guild's current track started
  • Early skip (<30% duration, track >20s) → implicit_dislike stored in Redis (14d TTL)
  • Track completed >80% → implicit_like stored in Redis (14d TTL)
  • Scoring: previously skipped → -0.35, previously completed → +0.25

Last.fm loved tracks

  • getLovedTracks(username, 50) added to Last.fm API wrapper
  • Loved tracks are merged first in the seed list (above top/recent) — highest priority signal
  • Autoplay seeds now = loved → top 3-month → recent (deduplicated)

Artist frequency from manual plays

  • Counts how many times each artist appears in manually-added play history (not autoplay)
  • Replaces binary preferred/not-preferred with a gradient:
    • 1+ manual plays → +0.10 (known artist)
    • 3+ manual plays → +0.20 (liked artist)
    • 5+ manual plays → +0.30 (favourite artist)

Spotify audio features

  • getAudioFeatures(token, trackId) + searchSpotifyTrack(token, title, artist) added to Spotify API
  • getTrackAudioFeatures() helper caches results per normalized track key
  • Spotify-source candidates near the current Spotify track get a +0.08 mood match boost
  • Foundation for deeper energy/valence matching in future

Config fix

  • isSpotifyConfigured() no longer requires SPOTIFY_REDIRECT_URI — backend derives it from WEBAPP_BACKEND_URL

Score summary (new weights)

Signal Delta
Favourite artist (5+ manual plays) +0.30
Liked track (explicit) +0.30
Preferred artist (explicit) +0.30
Completed before (implicit) +0.25
Liked artist (3+ plays) +0.20
Novel artist (not in session) +0.15
Known artist (1+ plays) +0.10
Spotify mood match +0.08
Skipped before (implicit) -0.35
Recent artist fatigue -0.25
Blocked artist -∞

Summary by CodeRabbit

  • New Features

    • Recommendations learn from listening: completed tracks boost preference; early skips reduce it.
    • Per-user implicit feedback is now recorded and used to tailor recommendations.
    • Spotify mood matching via audio features and improved Spotify track lookup.
    • Last.fm “loved” tracks are included as additional seeds for autoplay.
    • Improved artist-frequency awareness for more personalized suggestions.
  • Bug Fixes

    • Spotify configuration no longer requires a redirect URI.

@vercel

vercel Bot commented Apr 12, 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 13, 2026 0:01am

@github-actions github-actions Bot added the bot label Apr 12, 2026
@coderabbitai

coderabbitai Bot commented Apr 12, 2026 •

Copy link
Copy Markdown

Warning

Rate limit exceeded

@LucasSantana-Dev has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 20 minutes and 8 seconds before requesting another review.

Your organization is not enrolled in usage-based pricing. Contact your admin to enable usage-based pricing to continue reviews beyond the rate limit, or try again in 20 minutes and 8 seconds.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 9f682ffb-aeda-4ff2-8ccd-8bd5cdfb72bc

📥 Commits

Reviewing files that changed from the base of the PR and between f5f9c41 and 490b457.

📒 Files selected for processing (16)
  • packages/bot/src/handlers/player/trackHandlers.spec.ts
  • packages/bot/src/handlers/player/trackHandlers.ts
  • packages/bot/src/lastfm/index.ts
  • packages/bot/src/lastfm/lastFmApi.spec.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/spotify/spotifyConfig.spec.ts
  • packages/bot/src/spotify/spotifyConfig.ts
  • packages/bot/src/utils/music/autoplay/lastFmSeeds.spec.ts
  • packages/bot/src/utils/music/autoplay/lastFmSeeds.ts
  • packages/bot/src/utils/music/queueManipulation.spec.ts
  • packages/bot/src/utils/music/queueManipulation.ts
  • sonar-project.properties
📝 Walkthrough

Walkthrough

Adds per-user implicit like/dislike recording from player events, Redis-backed implicit-feedback APIs with TTL/size cap, Spotify audio-feature lookup and caching, Last.fm “loved tracks” ingestion for seeds, and integrates these signals into queue replenishment and recommendation scoring.

Changes

Cohort / File(s) Summary
Player handlers & feedback recording
packages/bot/src/handlers/player/trackHandlers.ts, packages/bot/src/handlers/player/trackHandlers.spec.ts
Record per-guild track start times, compute played ratio on finish/skip, normalize title::author, and call `recommendationFeedbackService.recordImplicitFeedback(userId, trackKey, 'implicit_like'
Recommendation feedback service
packages/bot/src/services/musicRecommendation/feedbackService.ts, packages/bot/src/services/musicRecommendation/feedbackService.spec.ts
New implicit feedback storage in Redis under music:implicit_feedback:${userId} with 14‑day TTL and 200-entry trimming; APIs recordImplicitFeedback, getImplicitLikeKeys, getImplicitDislikeKeys; tests validate persistence, trimming, and error handling.
Queue recommendation & scoring
packages/bot/src/utils/music/queueManipulation.ts, packages/bot/src/utils/music/queueManipulation.spec.ts
Fetch implicit like/dislike sets and build artist-frequency map; add Spotify audio-feature retrieval with process cache; propagate artistFrequency/implicit keys through candidate collection and update calculateRecommendationScore to apply artist boosts, implicit-feedback adjustments, and a Spotify-source mood-match boost; tests updated/mocked.
Spotify API & config
packages/bot/src/spotify/spotifyApi.ts, packages/bot/src/spotify/spotifyConfig.ts, packages/bot/src/spotify/spotifyConfig.spec.ts, packages/bot/src/spotify/spotifyApi.spec.ts
New getAudioFeatures and searchSpotifyTrack helpers and SpotifyAudioFeatures type; robust null-on-error behavior; isSpotifyConfigured() treats redirect URI as optional; comprehensive tests added.
Last.fm loved-tracks & seeds
packages/bot/src/lastfm/lastFmApi.ts, packages/bot/src/lastfm/index.ts, packages/bot/src/utils/music/autoplay/lastFmSeeds.ts, packages/bot/src/utils/music/autoplay/lastFmSeeds.spec.ts
Add getLovedTracks() API, re-export it, and include loved tracks (up to 50) in autoplay seed aggregation; tests mock/reset the new function.
Tests, mocks & sonar config
packages/bot/src/lastfm/lastFmApi.spec.ts, packages/bot/src/spotify/spotifyApi.spec.ts, packages/bot/src/services/musicRecommendation/feedbackService.spec.ts, sonar-project.properties
New/extended tests for implicit feedback, Spotify API, Last.fm loved-tracks; mocks for implicit key getters and getLovedTracks; new files excluded from Sonar CPD.

Sequence Diagram(s)

sequenceDiagram
    participant Player as Music Player
    participant Handler as Track Handler
    participant Feedback as RecommendationFeedbackService
    participant Redis as Redis Storage
    participant Queue as Queue Manager
    participant Spotify as Spotify API

    autonumber

    rect rgba(100, 150, 200, 0.5)
      Note over Player,Handler: Implicit feedback recording
      Player->>Handler: playerStart(guildId, track)
      Handler->>Redis: store startTime[guildId] = now
      Player->>Handler: playerFinish/playerSkip(guildId, track)
      Handler->>Handler: compute playedMs / durationMS
      Handler->>Feedback: recordImplicitFeedback(userId, trackKey, implicit_like/implicit_dislike)
      Feedback->>Redis: load/persist implicit map (setex 14d, cap 200)
      Handler->>Redis: delete startTime[guildId]
    end

    rect rgba(150, 200, 100, 0.5)
      Note over Queue,Spotify: Replenish & scoring
      Queue->>Feedback: getImplicitLikeKeys(userId)
      Queue->>Feedback: getImplicitDislikeKeys(userId)
      Queue->>Queue: buildArtistFrequency(history)
      Queue->>Spotify: searchSpotifyTrack/getAudioFeatures (as needed)
      Spotify->>Queue: return SpotifyAudioFeatures (cached)
      Queue->>Queue: calculateRecommendationScore(candidate, ..., artistFrequency, implicitLikeKeys, implicitDislikeKeys)
      Queue->>Queue: select top candidates for enqueue
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Possibly related PRs

Suggested labels

shared

🚥 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 title accurately summarizes the main features added: skip/completion signals (implicit feedback), loved tracks integration, artist frequency tracking, and mood matching via Spotify audio features.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/smart-autoplay-signals

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: 5

Caution

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

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

935-947: ⚠️ Potential issue | 🔴 Critical

Keep the blocked-artist hard stop in these paths too.

Both call sites upsert the candidate even when calculateRecommendationScore() returns -Infinity. That breaks the explicit “blocked artist = never” rule when the pool is thin.

Suggested fix
             const rec = calculateRecommendationScore(
                 track,
                 currentTrack,
                 recentArtists,
                 likedTrackKeys,
@@
                 implicitDislikeKeys,
                 implicitLikeKeys,
             )
+            if (rec.score === -Infinity) continue
             upsertScoredCandidate(candidates, track, {
                 score: (rec.score + LASTFM_SCORE_BOOST) * (s.match / 100),
                 reason: rec.reason
                     ? `${rec.reason} • similar to your taste`
                     : 'similar to your taste',
@@
     const rec = calculateRecommendationScore(
         track,
         ctx.currentTrack,
         ctx.recentArtists,
         ctx.likedTrackKeys,
@@
         ctx.artistFrequency,
         ctx.implicitDislikeKeys,
         ctx.implicitLikeKeys,
     )
+    if (rec.score === -Infinity) return
     upsertScoredCandidate(ctx.candidates, track, {
         score: rec.score + GENRE_SCORE_BOOST,
         reason: rec.reason ? `${rec.reason} • ${tag} vibes` : `${tag} vibes`,
     })
 }

Also applies to: 1019-1034

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

In `@packages/bot/src/utils/music/queueManipulation.ts` around lines 935 - 947,
The code currently upserts candidates even when
calculateRecommendationScore(...) returns -Infinity, which violates the "blocked
artist = never" rule; update both call sites where calculateRecommendationScore
is used (the one that computes rec and then calls
upsertScoredCandidate(candidates, track, ...)) to check the returned score and
skip the upsert when rec === -Infinity (or isFinite(rec) is false), ensuring
blocked artists are never added to candidates; apply the same guard to the other
identical call site where rec is computed (also referenced in this file) so
upsertScoredCandidate is only invoked for finite scores.
🧹 Nitpick comments (7)
packages/bot/src/utils/music/queueManipulation.spec.ts (1)

2114-2126: Missing implicit feedback mock initialization in this describe block.

Unlike other beforeEach blocks (lines 152-153, 1921-1922, etc.), this one doesn't initialize getImplicitDislikeKeysMock and getImplicitLikeKeysMock. While the tests likely still pass due to Jest's mock behavior, adding the initialization would be consistent with other blocks.

🔧 Proposed fix for consistency
     beforeEach(() => {
         dislikedTrackKeysMock.mockResolvedValue(new Set())
         likedTrackKeysMock.mockResolvedValue(new Set())
         getPreferredArtistKeysMock.mockResolvedValue(new Set())
         getBlockedArtistKeysMock.mockResolvedValue(new Set())
+        getImplicitDislikeKeysMock.mockResolvedValue(new Set())
+        getImplicitLikeKeysMock.mockResolvedValue(new Set())
         consumeLastFmSeedSliceMock.mockResolvedValue([])
🤖 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 2114 -
2126, In the describe block for "queueManipulation.addSelectedTracks async
writes" add initialization for the implicit feedback mocks: call
getImplicitDislikeKeysMock.mockResolvedValue(new Set()) and
getImplicitLikeKeysMock.mockResolvedValue(new Set()) in the beforeEach so they
match other blocks; locate the beforeEach in that describe and add these two
mockResolvedValue(new Set()) calls alongside the existing mocks like
dislikedTrackKeysMock and likedTrackKeysMock.
packages/bot/src/handlers/player/trackHandlers.ts (3)

54-64: Duplicate normalization logic with feedbackService.buildTrackKey.

This function duplicates the track key normalization logic from RecommendationFeedbackService.buildTrackKey (lines 56-67 in feedbackService.ts). Both perform the same lowercase alphanumeric normalization with :: delimiter. Consider reusing the service method to avoid drift.

♻️ Proposed refactor to reuse service method
-function normalizeTrackKeyForFeedback(title: string, author: string): string {
-    const normalizedTitle = cleanTitle(title)
-        .toLowerCase()
-        .replaceAll(/[^a-z0-9]+/g, '')
-        .trim()
-    const normalizedAuthor = cleanAuthor(author)
-        .toLowerCase()
-        .replaceAll(/[^a-z0-9]+/g, '')
-        .trim()
-    return `${normalizedTitle}::${normalizedAuthor}`
-}

Then use recommendationFeedbackService.buildTrackKey(cleanTitle(track.title), cleanAuthor(track.author)) at the call sites.

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

In `@packages/bot/src/handlers/player/trackHandlers.ts` around lines 54 - 64, The
normalizeTrackKeyForFeedback function duplicates
RecommendationFeedbackService.buildTrackKey's logic; remove the local function
and replace its call sites to use the service method instead (e.g.,
recommendationFeedbackService.buildTrackKey(cleanTitle(track.title),
cleanAuthor(track.author))). Ensure the service instance
(recommendationFeedbackService or RecommendationFeedbackService) is
imported/available in the module and adapt any call signatures to pass the
cleaned title and author strings so behavior remains identical.

242-244: Duplicate requester extraction logic.

The inline requester lookup duplicates the existing getTrackRequesterId helper defined at lines 42-45. Consider reusing it:

♻️ Proposed refactor
-                const requesterId = track.requestedBy?.id
-                    ?? (track.metadata as { requestedById?: string } | undefined)
-                        ?.requestedById
+                const requesterId = getTrackRequesterId(track)

Also applies to: 299-301

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

In `@packages/bot/src/handlers/player/trackHandlers.ts` around lines 242 - 244,
The duplicate logic extracting the requesterId (using track.requestedBy?.id ??
(track.metadata as { requestedById?: string } | undefined)?.requestedById)
should be replaced with the existing helper getTrackRequesterId; update the
occurrences that set requesterId (e.g., the variable named requesterId around
the track handling blocks) to call getTrackRequesterId(track) instead, removing
the inline fallback extraction and keeping a single source of truth for
requester resolution.

30-30: Potential memory growth for disconnected guilds.

guildTrackStartTimes entries are deleted on finish/skip, but if a guild disconnects mid-track without triggering these events, entries may accumulate. Consider adding cleanup on disconnect or emptyChannel events, or periodically evicting stale entries.

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

In `@packages/bot/src/handlers/player/trackHandlers.ts` at line 30, The
guildTrackStartTimes map can leak if a guild disconnects mid-track; add cleanup
logic that removes entries when a guild's voice session ends by hooking into the
relevant disconnect/emptyChannel/voiceStateUpdate handlers (or your player
disconnect/emptyChannel events) and calling
guildTrackStartTimes.delete(guildId), and additionally implement a periodic
eviction routine that scans guildTrackStartTimes and deletes entries older than
a safe TTL (compare stored timestamp to Date.now()) to guard against missed
events.
packages/bot/src/utils/music/autoplay/lastFmSeeds.spec.ts (1)

107-127: Consider adding test coverage for loved tracks prioritization.

The existing tests mock getLovedTracks to [] but don't verify the new behavior where loved tracks are merged with highest priority. Consider adding a test case that verifies loved tracks appear before top/recent tracks in the merged result.

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

In `@packages/bot/src/utils/music/autoplay/lastFmSeeds.spec.ts` around lines 107 -
127, Add a new spec that mocks getByDiscordIdMock to return a lastFmUsername and
stubs getLovedTracksMock to return one or more loved tracks, while keeping
getTopTracksMock and getRecentTracksMock returning their sample tracks, then
call getLastFmSeedTracks('discord-user-loved') and assert the loved track(s)
appear first in the returned array (before top and recent tracks); reference and
update the existing test helpers/mocks named getByDiscordIdMock,
getLovedTracksMock, getTopTracksMock, getRecentTracksMock and the function under
test getLastFmSeedTracks to implement this check.
packages/bot/src/lastfm/lastFmApi.ts (1)

289-292: Missing HTTP status check before parsing response.

Other API functions in this file (e.g., getTopTracks at line 128, getRecentTracks at line 203) check res.ok before parsing JSON. This function parses the response directly, which could lead to parsing errors on non-2xx responses instead of gracefully returning an empty array.

🔧 Proposed fix to add status check
         const response = await fetch(
             `${API_BASE}?method=user.getlovedtracks&user=${encodeURIComponent(username)}&limit=${limit}&format=json&api_key=${config.apiKey}`,
         )
+        if (!response.ok) return []
         const data = (await response.json()) as {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/bot/src/lastfm/lastFmApi.ts` around lines 289 - 292, The
getLovedTracks fetch call parses the body without checking HTTP status; update
the getLovedTracks implementation so the response is checked (like
getTopTracks/getRecentTracks): after awaiting the fetch (the response variable),
if !response.ok return an empty array (or the function's empty result) and only
call response.json() when response.ok is true; keep existing variable names
(response) and downstream logic intact and optionally log the non-2xx status for
debugging.
packages/bot/src/services/musicRecommendation/feedbackService.ts (1)

337-338: Hardcoded TTL inconsistent with configurable explicit feedback TTL.

The implicit feedback TTL is hardcoded to 14 days, while explicit feedback uses this.ttlDays (configurable via AUTOPLAY_FEEDBACK_TTL_DAYS). Consider extracting this to a separate configurable parameter or documenting why implicit feedback has a shorter, fixed TTL.

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

In `@packages/bot/src/services/musicRecommendation/feedbackService.ts` around
lines 337 - 338, The implicit feedback TTL is hardcoded to 14 days in
feedbackService (the local ttlSeconds near getImplicitFeedbackRedisKey), causing
inconsistency with explicit feedback which uses this.ttlDays (driven by
AUTOPLAY_FEEDBACK_TTL_DAYS); update the implementation to use a configurable
value instead—either reuse this.ttlDays for implicit feedback or add a new
config property (e.g., this.implicitTtlDays) initialized from a new env/config
entry, and replace the hardcoded 14*24*60*60 with the computed seconds from that
property so both TTLs are configurable and consistent.
🤖 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/spotify/spotifyApi.ts`:
- Around line 36-38: The current truthy check rejects valid energy values like
0; change the conditional that reads "!data?.energy || typeof data.valence !==
'number'" to perform a type check for energy instead (e.g., check typeof
data.energy !== 'number' while keeping the existing valence check) so that valid
numeric 0 values are accepted; locate the conditional using the data object and
update it accordingly in the function handling Spotify audio feature responses.
- Around line 14-22: Add timeout protection to Spotify fetches by using
AbortController in both getAudioFeatures and searchSpotifyTrack: create an
AbortController, start a setTimeout (e.g., 3000ms) that calls
controller.abort(), pass controller.signal into the fetch options (headers +
signal), and clear the timeout when the fetch resolves successfully; handle
abort errors appropriately (propagate or convert to a timeout error) so slow
Spotify responses won't block _replenishQueue().

In `@packages/bot/src/spotify/spotifyConfig.spec.ts`:
- Around line 45-50: The test in spotifyConfig.spec.ts is simulating a missing
env var by assigning process.env.SPOTIFY_REDIRECT_URI = undefined which leaves
the string "undefined" in the env; modify the test that calls
isSpotifyConfigured() to actually remove the variable using delete
process.env.SPOTIFY_REDIRECT_URI so the env truly is absent and the test intent
(missing redirect URI) is correctly represented.

In `@packages/bot/src/utils/music/queueManipulation.ts`:
- Around line 43-59: The global audioFeatureCache is being populated with null
on user-specific token failures or transient lookup errors, causing permanent
misses for all users; in getTrackAudioFeatures (and the similar later block that
sets cache to null) only write to audioFeatureCache when you have a definitive
API result (i.e., non-null SpotifyAudioFeatures) or an explicit “not found”
response from Spotify, and avoid caching null when
spotifyLinkService.getValidAccessToken(userId) returns no token or when the
fetch fails transiently—so remove or guard the audioFeatureCache.set(cacheKey,
null) calls behind checks for definitive negative responses and only set the
cache after a successful features fetch.
- Around line 1383-1386: The current block always awards +0.08 for any
Spotify→Spotify pair and labels it "spotify mood match" without checking audio
features; change it to first check the actual Spotify audio feature fields
(e.g., candidate.audioFeatures and currentTrack.audioFeatures — valence, energy,
tempo, etc.) and only add the +0.08 and push the "spotify mood match" reason
when the feature-distance (choose a small threshold on valence/energy or a
combined distance) indicates mood similarity; if you still want a smaller source
bias, use a separate smaller score bump with a different reason like "spotify
source match" only when features are absent. Target the code that references
candidate.source, currentTrack.source, score, and reasons in
queueManipulation.ts.

---

Outside diff comments:
In `@packages/bot/src/utils/music/queueManipulation.ts`:
- Around line 935-947: The code currently upserts candidates even when
calculateRecommendationScore(...) returns -Infinity, which violates the "blocked
artist = never" rule; update both call sites where calculateRecommendationScore
is used (the one that computes rec and then calls
upsertScoredCandidate(candidates, track, ...)) to check the returned score and
skip the upsert when rec === -Infinity (or isFinite(rec) is false), ensuring
blocked artists are never added to candidates; apply the same guard to the other
identical call site where rec is computed (also referenced in this file) so
upsertScoredCandidate is only invoked for finite scores.

---

Nitpick comments:
In `@packages/bot/src/handlers/player/trackHandlers.ts`:
- Around line 54-64: The normalizeTrackKeyForFeedback function duplicates
RecommendationFeedbackService.buildTrackKey's logic; remove the local function
and replace its call sites to use the service method instead (e.g.,
recommendationFeedbackService.buildTrackKey(cleanTitle(track.title),
cleanAuthor(track.author))). Ensure the service instance
(recommendationFeedbackService or RecommendationFeedbackService) is
imported/available in the module and adapt any call signatures to pass the
cleaned title and author strings so behavior remains identical.
- Around line 242-244: The duplicate logic extracting the requesterId (using
track.requestedBy?.id ?? (track.metadata as { requestedById?: string } |
undefined)?.requestedById) should be replaced with the existing helper
getTrackRequesterId; update the occurrences that set requesterId (e.g., the
variable named requesterId around the track handling blocks) to call
getTrackRequesterId(track) instead, removing the inline fallback extraction and
keeping a single source of truth for requester resolution.
- Line 30: The guildTrackStartTimes map can leak if a guild disconnects
mid-track; add cleanup logic that removes entries when a guild's voice session
ends by hooking into the relevant disconnect/emptyChannel/voiceStateUpdate
handlers (or your player disconnect/emptyChannel events) and calling
guildTrackStartTimes.delete(guildId), and additionally implement a periodic
eviction routine that scans guildTrackStartTimes and deletes entries older than
a safe TTL (compare stored timestamp to Date.now()) to guard against missed
events.

In `@packages/bot/src/lastfm/lastFmApi.ts`:
- Around line 289-292: The getLovedTracks fetch call parses the body without
checking HTTP status; update the getLovedTracks implementation so the response
is checked (like getTopTracks/getRecentTracks): after awaiting the fetch (the
response variable), if !response.ok return an empty array (or the function's
empty result) and only call response.json() when response.ok is true; keep
existing variable names (response) and downstream logic intact and optionally
log the non-2xx status for debugging.

In `@packages/bot/src/services/musicRecommendation/feedbackService.ts`:
- Around line 337-338: The implicit feedback TTL is hardcoded to 14 days in
feedbackService (the local ttlSeconds near getImplicitFeedbackRedisKey), causing
inconsistency with explicit feedback which uses this.ttlDays (driven by
AUTOPLAY_FEEDBACK_TTL_DAYS); update the implementation to use a configurable
value instead—either reuse this.ttlDays for implicit feedback or add a new
config property (e.g., this.implicitTtlDays) initialized from a new env/config
entry, and replace the hardcoded 14*24*60*60 with the computed seconds from that
property so both TTLs are configurable and consistent.

In `@packages/bot/src/utils/music/autoplay/lastFmSeeds.spec.ts`:
- Around line 107-127: Add a new spec that mocks getByDiscordIdMock to return a
lastFmUsername and stubs getLovedTracksMock to return one or more loved tracks,
while keeping getTopTracksMock and getRecentTracksMock returning their sample
tracks, then call getLastFmSeedTracks('discord-user-loved') and assert the loved
track(s) appear first in the returned array (before top and recent tracks);
reference and update the existing test helpers/mocks named getByDiscordIdMock,
getLovedTracksMock, getTopTracksMock, getRecentTracksMock and the function under
test getLastFmSeedTracks to implement this check.

In `@packages/bot/src/utils/music/queueManipulation.spec.ts`:
- Around line 2114-2126: In the describe block for
"queueManipulation.addSelectedTracks async writes" add initialization for the
implicit feedback mocks: call getImplicitDislikeKeysMock.mockResolvedValue(new
Set()) and getImplicitLikeKeysMock.mockResolvedValue(new Set()) in the
beforeEach so they match other blocks; locate the beforeEach in that describe
and add these two mockResolvedValue(new Set()) calls alongside the existing
mocks like dislikedTrackKeysMock and likedTrackKeysMock.
🪄 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: 072d8f0e-0cc7-4b25-b447-4d5a339f5dc3

📥 Commits

Reviewing files that changed from the base of the PR and between eeebb0c and 5a88a20.

📒 Files selected for processing (11)
  • packages/bot/src/handlers/player/trackHandlers.ts
  • packages/bot/src/lastfm/index.ts
  • packages/bot/src/lastfm/lastFmApi.ts
  • packages/bot/src/services/musicRecommendation/feedbackService.ts
  • packages/bot/src/spotify/spotifyApi.ts
  • packages/bot/src/spotify/spotifyConfig.spec.ts
  • packages/bot/src/spotify/spotifyConfig.ts
  • packages/bot/src/utils/music/autoplay/lastFmSeeds.spec.ts
  • packages/bot/src/utils/music/autoplay/lastFmSeeds.ts
  • packages/bot/src/utils/music/queueManipulation.spec.ts
  • packages/bot/src/utils/music/queueManipulation.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: SonarCloud Scan
  • GitHub Check: Quality Gates
🔇 Additional comments (6)
packages/bot/src/lastfm/index.ts (1)

8-8: LGTM!

The re-export of getLovedTracks follows the established pattern for exposing Last.fm API functions from the module's public interface.

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

53-63: LGTM!

The implementation correctly fetches loved tracks in parallel with existing seed sources and merges them with highest priority before deduplication, as intended by the PR objectives.

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

343-374: LGTM!

The recordImplicitFeedback implementation correctly handles concurrent updates, enforces the 200-entry cap with LRU-style trimming by updatedAt, and has proper error handling that logs failures without throwing.

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

1-6: LGTM!

The simplified check correctly requires only SPOTIFY_CLIENT_ID and SPOTIFY_CLIENT_SECRET, aligning with the PR objective that SPOTIFY_REDIRECT_URI is now derived from WEBAPP_BACKEND_URL instead of being required.

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

22-22: LGTM - Mock setup is correct.

The mock for getLovedTracks is properly wired and initialized to [] in all relevant beforeEach blocks, ensuring existing tests remain unaffected.

Also applies to: 38-38, 44-44

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

90-106: LGTM!

The new mocks for implicit feedback methods are correctly defined and wired into the recommendationFeedbackService mock, enabling tests to control implicit like/dislike behavior.

Comment on lines +14 to +22
const res = await fetch(
`https://api.spotify.com/v1/audio-features/${spotifyTrackId}`,
{
method: 'GET',
headers: {
Authorization: `Bearer ${accessToken}`,
},
},
)

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

🧩 Analysis chain

🏁 Script executed:

cat -n packages/bot/src/spotify/spotifyApi.ts

Repository: LucasSantana-Dev/Lucky

Length of output: 2808


🏁 Script executed:

rg "getAudioFeatures|searchSpotifyTrack" --type ts --type tsx -B 2 -A 2

Repository: LucasSantana-Dev/Lucky

Length of output: 93


🏁 Script executed:

rg "getAudioFeatures|searchSpotifyTrack" -B 2 -A 2

Repository: LucasSantana-Dev/Lucky

Length of output: 1894


🏁 Script executed:

cat -n packages/bot/src/utils/music/queueManipulation.ts

Repository: LucasSantana-Dev/Lucky

Length of output: 50378


🏁 Script executed:

sed -n '45,87p' packages/bot/src/utils/music/queueManipulation.ts

Repository: LucasSantana-Dev/Lucky

Length of output: 1232


Add a timeout to both Spotify API requests to prevent slow responses from blocking queue replenishment.

These calls are used during autoplay recommendation scoring. Without an abort/timeout, a slow Spotify response can block the entire _replenishQueue() process and stall queue refill for autoplay.

Both getAudioFeatures() and searchSpotifyTrack() should implement timeout protection using AbortController with a reasonable timeout (e.g., 3000ms).

Suggested direction
+const SPOTIFY_FETCH_TIMEOUT_MS = 3000
+
 export async function getAudioFeatures(
     accessToken: string,
     spotifyTrackId: string,
 ): Promise<SpotifyAudioFeatures | null> {
     try {
-        const res = await fetch(
-            `https://api.spotify.com/v1/audio-features/${spotifyTrackId}`,
-            {
-                method: 'GET',
-                headers: {
-                    Authorization: `Bearer ${accessToken}`,
-                },
-            },
-        )
+        const controller = new AbortController()
+        const timeout = setTimeout(() => controller.abort(), SPOTIFY_FETCH_TIMEOUT_MS)
+        const res = await fetch(
+            `https://api.spotify.com/v1/audio-features/${spotifyTrackId}`,
+            {
+                method: 'GET',
+                headers: {
+                    Authorization: `Bearer ${accessToken}`,
+                },
+                signal: controller.signal,
+            },
+        ).finally(() => clearTimeout(timeout))
📝 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
const res = await fetch(
`https://api.spotify.com/v1/audio-features/${spotifyTrackId}`,
{
method: 'GET',
headers: {
Authorization: `Bearer ${accessToken}`,
},
},
)
const controller = new AbortController()
const timeout = setTimeout(() => controller.abort(), SPOTIFY_FETCH_TIMEOUT_MS)
const res = await fetch(
`https://api.spotify.com/v1/audio-features/${spotifyTrackId}`,
{
method: 'GET',
headers: {
Authorization: `Bearer ${accessToken}`,
},
signal: controller.signal,
},
).finally(() => clearTimeout(timeout))
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/bot/src/spotify/spotifyApi.ts` around lines 14 - 22, Add timeout
protection to Spotify fetches by using AbortController in both getAudioFeatures
and searchSpotifyTrack: create an AbortController, start a setTimeout (e.g.,
3000ms) that calls controller.abort(), pass controller.signal into the fetch
options (headers + signal), and clear the timeout when the fetch resolves
successfully; handle abort errors appropriately (propagate or convert to a
timeout error) so slow Spotify responses won't block _replenishQueue().

Comment on lines +36 to +38
if (!data?.energy || typeof data.valence !== 'number') {
return null
}

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

🧩 Analysis chain

🌐 Web query:

In Spotify's Audio Features API, what is the valid range for the energyfield, and is0 a valid value?

💡 Result:

The valid range for the energy field in Spotify's Audio Features API is 0.0 to 1.0. Yes, 0 is a valid value, representing the lowest possible energy (e.g., a track with minimal intensity and activity, such as a calm Bach prelude).

Citations:


🏁 Script executed:

cat -n packages/bot/src/spotify/spotifyApi.ts | head -50

Repository: LucasSantana-Dev/Lucky

Length of output: 1652


Replace falsy check with type check for energy field.

Line 36-38: !data?.energy incorrectly rejects valid energy: 0 payloads. Spotify's Audio Features API returns energy values in the range 0.0–1.0, where 0 is a valid value representing the lowest energy level. This check should validate the field's type, not truthiness, consistent with how valence is validated on the same line.

Fix
-        if (!data?.energy || typeof data.valence !== 'number') {
+        if (
+            typeof data?.energy !== 'number' ||
+            typeof data.valence !== 'number'
+        ) {
             return null
         }
📝 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
if (!data?.energy || typeof data.valence !== 'number') {
return null
}
if (
typeof data?.energy !== 'number' ||
typeof data.valence !== 'number'
) {
return null
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/bot/src/spotify/spotifyApi.ts` around lines 36 - 38, The current
truthy check rejects valid energy values like 0; change the conditional that
reads "!data?.energy || typeof data.valence !== 'number'" to perform a type
check for energy instead (e.g., check typeof data.energy !== 'number' while
keeping the existing valence check) so that valid numeric 0 values are accepted;
locate the conditional using the data object and update it accordingly in the
function handling Spotify audio feature responses.

Comment on lines +45 to +50
it('returns true when SPOTIFY_REDIRECT_URI is missing (optional)', () => {
process.env.SPOTIFY_CLIENT_ID = 'test-client-id'
process.env.SPOTIFY_CLIENT_SECRET = 'test-secret'
process.env.SPOTIFY_REDIRECT_URI = undefined

expect(isSpotifyConfigured()).toBe(false)
expect(isSpotifyConfigured()).toBe(true)

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 | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
node - <<'NODE'
process.env.SPOTIFY_REDIRECT_URI = 'https://example.com/callback'
process.env.SPOTIFY_REDIRECT_URI = undefined
console.log('after assignment:', JSON.stringify(process.env.SPOTIFY_REDIRECT_URI))
delete process.env.SPOTIFY_REDIRECT_URI
console.log('after delete:', JSON.stringify(process.env.SPOTIFY_REDIRECT_URI))
NODE

Repository: LucasSantana-Dev/Lucky

Length of output: 119


🏁 Script executed:

cat packages/bot/src/spotify/spotifyConfig.ts

Repository: LucasSantana-Dev/Lucky

Length of output: 220


Use delete instead of assigning undefined.

Line 48 does not reliably simulate a missing env var in Node.js. Assigning undefined to process.env results in the string "undefined" remaining in the environment, while the test comment claims the redirect URI is "missing". Use delete to actually remove the variable and match the test intent.

Suggested fix
 it('returns true when SPOTIFY_REDIRECT_URI is missing (optional)', () => {
     process.env.SPOTIFY_CLIENT_ID = 'test-client-id'
     process.env.SPOTIFY_CLIENT_SECRET = 'test-secret'
-    process.env.SPOTIFY_REDIRECT_URI = undefined
+    delete process.env.SPOTIFY_REDIRECT_URI

     expect(isSpotifyConfigured()).toBe(true)
 })
📝 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('returns true when SPOTIFY_REDIRECT_URI is missing (optional)', () => {
process.env.SPOTIFY_CLIENT_ID = 'test-client-id'
process.env.SPOTIFY_CLIENT_SECRET = 'test-secret'
process.env.SPOTIFY_REDIRECT_URI = undefined
expect(isSpotifyConfigured()).toBe(false)
expect(isSpotifyConfigured()).toBe(true)
it('returns true when SPOTIFY_REDIRECT_URI is missing (optional)', () => {
process.env.SPOTIFY_CLIENT_ID = 'test-client-id'
process.env.SPOTIFY_CLIENT_SECRET = 'test-secret'
delete process.env.SPOTIFY_REDIRECT_URI
expect(isSpotifyConfigured()).toBe(true)
})
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/bot/src/spotify/spotifyConfig.spec.ts` around lines 45 - 50, The
test in spotifyConfig.spec.ts is simulating a missing env var by assigning
process.env.SPOTIFY_REDIRECT_URI = undefined which leaves the string "undefined"
in the env; modify the test that calls isSpotifyConfigured() to actually remove
the variable using delete process.env.SPOTIFY_REDIRECT_URI so the env truly is
absent and the test intent (missing redirect URI) is correctly represented.

Comment on lines +43 to +59
const audioFeatureCache = new Map<string, SpotifyAudioFeatures | null>()

async function getTrackAudioFeatures(
track: Track,
userId: string,
): Promise<SpotifyAudioFeatures | null> {
const cacheKey = normalizeTrackKey(track.title, track.author)

const cached = audioFeatureCache.get(cacheKey)
if (cached !== undefined) {
return cached
}

const token = await spotifyLinkService.getValidAccessToken(userId)
if (!token) {
audioFeatureCache.set(cacheKey, null)
return null

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

Don't cache user-specific failures in a global track cache.

This memoizes null by title/artist even when the miss happened because the current user had no Spotify token or because a lookup transiently failed. After that, every other user gets a permanent miss for the same track.

Suggested fix
 async function getTrackAudioFeatures(
     track: Track,
     userId: string,
 ): Promise<SpotifyAudioFeatures | null> {
     const cacheKey = normalizeTrackKey(track.title, track.author)
-    
+
     const cached = audioFeatureCache.get(cacheKey)
     if (cached !== undefined) {
         return cached
     }
-    
+
     const token = await spotifyLinkService.getValidAccessToken(userId)
     if (!token) {
-        audioFeatureCache.set(cacheKey, null)
         return null
     }
-    
+
     let spotifyId: string | null = null
-    
+
     if (track.url && track.url.includes('open.spotify.com/track/')) {
         const match = track.url.match(/track\/([a-zA-Z0-9]+)/)
         if (match) {
             spotifyId = match[1]
@@
-    if (!spotifyId) {
-        audioFeatureCache.set(cacheKey, null)
+    if (!spotifyId) {
         return null
     }
-    
+
     const features = await getAudioFeatures(token, spotifyId).catch(() => null)
-    audioFeatureCache.set(cacheKey, features)
+    if (features) {
+        audioFeatureCache.set(cacheKey, features)
+    }
     return features
 }

Also applies to: 79-86

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

In `@packages/bot/src/utils/music/queueManipulation.ts` around lines 43 - 59, The
global audioFeatureCache is being populated with null on user-specific token
failures or transient lookup errors, causing permanent misses for all users; in
getTrackAudioFeatures (and the similar later block that sets cache to null) only
write to audioFeatureCache when you have a definitive API result (i.e., non-null
SpotifyAudioFeatures) or an explicit “not found” response from Spotify, and
avoid caching null when spotifyLinkService.getValidAccessToken(userId) returns
no token or when the fetch fails transiently—so remove or guard the
audioFeatureCache.set(cacheKey, null) calls behind checks for definitive
negative responses and only set the cache after a successful features fetch.

Comment on lines +1383 to +1386
if (candidate.source === 'spotify' && currentTrack.source === 'spotify') {
score += 0.08
reasons.push('spotify mood match')
}

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

This bonus is source-based, not mood-based.

Right now every Spotify→Spotify pair gets +0.08 and the "spotify mood match" label without comparing any audio features. That biases rankings toward Spotify candidates even when they are not actually similar in mood.

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

In `@packages/bot/src/utils/music/queueManipulation.ts` around lines 1383 - 1386,
The current block always awards +0.08 for any Spotify→Spotify pair and labels it
"spotify mood match" without checking audio features; change it to first check
the actual Spotify audio feature fields (e.g., candidate.audioFeatures and
currentTrack.audioFeatures — valence, energy, tempo, etc.) and only add the
+0.08 and push the "spotify mood match" reason when the feature-distance (choose
a small threshold on valence/energy or a combined distance) indicates mood
similarity; if you still want a smaller source bias, use a separate smaller
score bump with a different reason like "spotify source match" only when
features are absent. Target the code that references candidate.source,
currentTrack.source, score, and reasons in queueManipulation.ts.

@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 the current code and only fix it if needed.

Inline comments:
In `@packages/bot/src/spotify/spotifyApi.spec.ts`:
- Around line 93-121: The tests for getAudioFeatures lack a boundary case for
energy === 0 which can be misinterpreted as "missing" if the implementation uses
falsy checks; add a new spec that mocks fetch to return ok:true and json
resolving to an object with energy: 0 (number) plus valid valence/danceability
(e.g., 0.5) and assert that getAudioFeatures('test-token','track-123') does not
return null but returns the expected parsed features; reference getAudioFeatures
and the existing specs "returns null when energy is missing" / "returns null
when valence is not a number" to place the new test alongside them.
🪄 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: 41eb28d2-72d8-4ca9-8f56-ad33fba3a8a1

📥 Commits

Reviewing files that changed from the base of the PR and between 5a88a20 and cedbca1.

📒 Files selected for processing (1)
  • 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: SonarCloud Scan
  • GitHub Check: Quality Gates
🔇 Additional comments (2)
packages/bot/src/spotify/spotifyApi.spec.ts (2)

15-91: Strong coverage for getAudioFeatures happy/error paths.

This block does a solid job validating success mapping, optional defaults, HTTP failures, JSON failures, and network exceptions.


134-266: searchSpotifyTrack tests are comprehensive and practical.

Good validation of success, empty/malformed payloads, transport failures, and special-character query encoding.

Comment on lines +93 to +121
it('returns null when energy is missing', async () => {
const mockResponse = {
ok: true,
json: jest.fn().mockResolvedValue({
valence: 0.75,
danceability: 0.65,
}),
}
global.fetch = jest.fn().mockResolvedValue(mockResponse)

const result = await getAudioFeatures('test-token', 'track-123')

expect(result).toBeNull()
})

it('returns null when valence is not a number', async () => {
const mockResponse = {
ok: true,
json: jest.fn().mockResolvedValue({
energy: 0.8,
valence: 'high',
}),
}
global.fetch = jest.fn().mockResolvedValue(mockResponse)

const result = await getAudioFeatures('test-token', 'track-123')

expect(result).toBeNull()
})

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

Add a boundary test for energy = 0 to prevent false “missing field” behavior.

Right now the suite checks missing energy, but not zero-valued energy. A zero numeric value is a valid boundary and should be tested explicitly; otherwise a falsy check can slip through undetected.

Proposed test addition
+        it('accepts energy=0 as a valid numeric value', async () => {
+            const mockResponse = {
+                ok: true,
+                json: jest.fn().mockResolvedValue({
+                    energy: 0,
+                    valence: 0.4,
+                }),
+            }
+            global.fetch = jest.fn().mockResolvedValue(mockResponse)
+
+            const result = await getAudioFeatures('test-token', 'track-123')
+
+            expect(result).toEqual({
+                energy: 0,
+                valence: 0.4,
+                danceability: 0,
+                tempo: 0,
+                acousticness: 0,
+            })
+        })
📝 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('returns null when energy is missing', async () => {
const mockResponse = {
ok: true,
json: jest.fn().mockResolvedValue({
valence: 0.75,
danceability: 0.65,
}),
}
global.fetch = jest.fn().mockResolvedValue(mockResponse)
const result = await getAudioFeatures('test-token', 'track-123')
expect(result).toBeNull()
})
it('returns null when valence is not a number', async () => {
const mockResponse = {
ok: true,
json: jest.fn().mockResolvedValue({
energy: 0.8,
valence: 'high',
}),
}
global.fetch = jest.fn().mockResolvedValue(mockResponse)
const result = await getAudioFeatures('test-token', 'track-123')
expect(result).toBeNull()
})
it('returns null when energy is missing', async () => {
const mockResponse = {
ok: true,
json: jest.fn().mockResolvedValue({
valence: 0.75,
danceability: 0.65,
}),
}
global.fetch = jest.fn().mockResolvedValue(mockResponse)
const result = await getAudioFeatures('test-token', 'track-123')
expect(result).toBeNull()
})
it('returns null when valence is not a number', async () => {
const mockResponse = {
ok: true,
json: jest.fn().mockResolvedValue({
energy: 0.8,
valence: 'high',
}),
}
global.fetch = jest.fn().mockResolvedValue(mockResponse)
const result = await getAudioFeatures('test-token', 'track-123')
expect(result).toBeNull()
})
it('accepts energy=0 as a valid numeric value', async () => {
const mockResponse = {
ok: true,
json: jest.fn().mockResolvedValue({
energy: 0,
valence: 0.4,
}),
}
global.fetch = jest.fn().mockResolvedValue(mockResponse)
const result = await getAudioFeatures('test-token', 'track-123')
expect(result).toEqual({
energy: 0,
valence: 0.4,
danceability: 0,
tempo: 0,
acousticness: 0,
})
})
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/bot/src/spotify/spotifyApi.spec.ts` around lines 93 - 121, The tests
for getAudioFeatures lack a boundary case for energy === 0 which can be
misinterpreted as "missing" if the implementation uses falsy checks; add a new
spec that mocks fetch to return ok:true and json resolving to an object with
energy: 0 (number) plus valid valence/danceability (e.g., 0.5) and assert that
getAudioFeatures('test-token','track-123') does not return null but returns the
expected parsed features; reference getAudioFeatures and the existing specs
"returns null when energy is missing" / "returns null when valence is not a
number" to place the new test alongside them.

@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 (4)
packages/bot/src/services/musicRecommendation/feedbackService.spec.ts (3)

358-370: Assert the implicit-feedback TTL explicitly (14 days).

Using expect.any(Number) on Line 367 is too loose for a behavior that the PR defines as 14-day retention.

Suggested test tightening
         expect(setexMock).toHaveBeenCalledWith(
             'music:implicit_feedback:user-1',
-            expect.any(Number),
+            14 * 24 * 60 * 60,
             expect.stringContaining('implicit_dislike'),
         )
🤖 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 358 - 370, The test currently uses expect.any(Number) when asserting the
TTL for setexMock in the recordImplicitFeedback test; change that to assert the
explicit 14-day TTL (14*24*60*60 = 1209600 seconds) so the retention policy is
enforced. Locate the test case for
RecommendationFeedbackService.recordImplicitFeedback and replace
expect.any(Number) with the explicit numeric value (1209600) in the
expect(setexMock).toHaveBeenCalledWith(...) assertion so the TTL is asserted
directly.

431-436: This test does not cover Redis write failure in recordImplicitFeedback.

Currently it only rejects get, but that path is already absorbed by getImplicitFeedbackMap. Add a setex rejection case to verify graceful handling of save errors too.

Suggested additional case
     it('recordImplicitFeedback handles redis error gracefully', async () => {
-        getMock.mockRejectedValue(new Error('redis down'))
+        getMock.mockResolvedValue(null)
+        setexMock.mockRejectedValue(new Error('redis down'))
         const service = new RecommendationFeedbackService(30)

         await expect(service.recordImplicitFeedback('user-1', 'key', 'implicit_like')).resolves.toBeUndefined()
     })
🤖 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 431 - 436, The test currently only mocks get to reject; add (or modify) a
case to also mock the Redis save call to reject so recordImplicitFeedback's
save-error branch is covered: when constructing the test where getMock rejects
or returns a value, also set the Redis client's setex mock (e.g., setexMock /
client.setex) to mockRejectedValue(new Error('redis down')) and assert await
expect(service.recordImplicitFeedback('user-1', 'key',
'implicit_like')).resolves.toBeUndefined() to verify the method swallows setex
errors gracefully; reference recordImplicitFeedback and the Redis setex mock
used in the spec.

372-385: Strengthen trim assertions to validate eviction policy, not just upper bound.

Line 384 only checks <= 200, which can pass even if trimming is wrong. Since feedbackService.ts keeps the latest 200 (entries.slice(-200)), assert exact count and key retention/eviction.

Suggested assertions
         const saved = JSON.parse(setexMock.mock.calls[0][2] as string)
-        expect(Object.keys(saved).length).toBeLessThanOrEqual(200)
+        expect(Object.keys(saved)).toHaveLength(200)
+        expect(saved).toHaveProperty('newtrack::artist')
+        expect(saved).not.toHaveProperty('track0::artist')
+        expect(saved).not.toHaveProperty('track1::artist')
🤖 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 372 - 385, Change the weak trim assertion to verify the eviction policy:
after calling RecommendationFeedbackService.recordImplicitFeedback, assert the
saved map has exactly 200 entries (not just <=200), assert the newly added key
('newtrack::artist') is present in the saved object, assert the oldest key
(e.g., 'track0::artist') is evicted (not present), and optionally assert a
recent existing key (e.g., 'track200::artist') remains; locate these checks
around the existing setexMock usage and the saved variable in the test that
creates bigMap and instantiates RecommendationFeedbackService.
packages/bot/src/lastfm/lastFmApi.spec.ts (1)

499-538: Add missing “API not configured” coverage for getLovedTracks.

This suite does not cover the getApiConfig() null branch (LASTFM_API_KEY / LASTFM_API_SECRET missing), which other API methods in this file already validate.

Suggested additional test
+        it('returns empty array when api_key is not configured', async () => {
+            delete process.env.LASTFM_API_KEY
+            const result = await getLovedTracks('testuser')
+            expect(result).toEqual([])
+        })
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/bot/src/lastfm/lastFmApi.spec.ts` around lines 499 - 538, Add a test
covering the getApiConfig() null branch for getLovedTracks: mock getApiConfig()
to return null (simulating missing LASTFM_API_KEY / LASTFM_API_SECRET), call
getLovedTracks('testuser'), and assert it returns an empty array; place this new
spec in the existing describe('getLovedTracks') block alongside the other cases
so the API-not-configured branch in getLovedTracks is exercised.
🤖 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/lastfm/lastFmApi.spec.ts`:
- Around line 521-525: The test for non-OK responses is fragile because it
relied on fetch returning { ok: false } without a json() implementation; update
the test in lastFmApi.spec.ts to mock fetch to resolve to an object with ok:
false, a realistic status (e.g., 500), and a json() function (e.g., jest.fn(()
=> Promise.resolve({ error: 'err' }))) so the test fails only if non-OK handling
is missing, and assert getLovedTracks('testuser') returns []; then fix the
production code in lastFmApi.ts by adding an explicit guard in getLovedTracks
that checks response.ok and immediately returns [] (or the appropriate empty
result) before calling response.json(), ensuring non-OK responses are handled
deterministically.

---

Nitpick comments:
In `@packages/bot/src/lastfm/lastFmApi.spec.ts`:
- Around line 499-538: Add a test covering the getApiConfig() null branch for
getLovedTracks: mock getApiConfig() to return null (simulating missing
LASTFM_API_KEY / LASTFM_API_SECRET), call getLovedTracks('testuser'), and assert
it returns an empty array; place this new spec in the existing
describe('getLovedTracks') block alongside the other cases so the
API-not-configured branch in getLovedTracks is exercised.

In `@packages/bot/src/services/musicRecommendation/feedbackService.spec.ts`:
- Around line 358-370: The test currently uses expect.any(Number) when asserting
the TTL for setexMock in the recordImplicitFeedback test; change that to assert
the explicit 14-day TTL (14*24*60*60 = 1209600 seconds) so the retention policy
is enforced. Locate the test case for
RecommendationFeedbackService.recordImplicitFeedback and replace
expect.any(Number) with the explicit numeric value (1209600) in the
expect(setexMock).toHaveBeenCalledWith(...) assertion so the TTL is asserted
directly.
- Around line 431-436: The test currently only mocks get to reject; add (or
modify) a case to also mock the Redis save call to reject so
recordImplicitFeedback's save-error branch is covered: when constructing the
test where getMock rejects or returns a value, also set the Redis client's setex
mock (e.g., setexMock / client.setex) to mockRejectedValue(new Error('redis
down')) and assert await expect(service.recordImplicitFeedback('user-1', 'key',
'implicit_like')).resolves.toBeUndefined() to verify the method swallows setex
errors gracefully; reference recordImplicitFeedback and the Redis setex mock
used in the spec.
- Around line 372-385: Change the weak trim assertion to verify the eviction
policy: after calling RecommendationFeedbackService.recordImplicitFeedback,
assert the saved map has exactly 200 entries (not just <=200), assert the newly
added key ('newtrack::artist') is present in the saved object, assert the oldest
key (e.g., 'track0::artist') is evicted (not present), and optionally assert a
recent existing key (e.g., 'track200::artist') remains; locate these checks
around the existing setexMock usage and the saved variable in the test that
creates bigMap and instantiates RecommendationFeedbackService.
🪄 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: 7c186043-24e3-48e5-a3fc-adf31523419d

📥 Commits

Reviewing files that changed from the base of the PR and between cedbca1 and da6368f.

📒 Files selected for processing (3)
  • packages/bot/src/lastfm/lastFmApi.spec.ts
  • packages/bot/src/services/musicRecommendation/feedbackService.spec.ts
  • sonar-project.properties
✅ Files skipped from review due to trivial changes (1)
  • sonar-project.properties
📜 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 (1)
packages/bot/src/lastfm/lastFmApi.spec.ts (1)

19-19: Import update looks correct.

getLovedTracks is correctly added to the API test imports.

Comment on lines +521 to +525
it('returns empty array on non-ok response', async () => {
fetchMock.mockResolvedValueOnce({ ok: false })
const result = await getLovedTracks('testuser')
expect(result).toEqual([])
})

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

Harden the non-OK test; current setup can pass for the wrong reason.

At Line 521, { ok: false } without json() makes this pass via thrown response.json + catch, so it does not prove explicit non-OK handling.

Suggested test fix
 it('returns empty array on non-ok response', async () => {
-    fetchMock.mockResolvedValueOnce({ ok: false })
+    fetchMock.mockResolvedValueOnce({
+        ok: false,
+        json: async () => ({
+            lovedtracks: {
+                track: [{ name: 'Should Not Be Returned', artist: { name: 'Ignored' } }],
+            },
+        }),
+    })
     const result = await getLovedTracks('testuser')
     expect(result).toEqual([])
 })

This will also expose the missing response.ok guard in packages/bot/src/lastfm/lastFmApi.ts (Line 283-308).

📝 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('returns empty array on non-ok response', async () => {
fetchMock.mockResolvedValueOnce({ ok: false })
const result = await getLovedTracks('testuser')
expect(result).toEqual([])
})
it('returns empty array on non-ok response', async () => {
fetchMock.mockResolvedValueOnce({
ok: false,
json: async () => ({
lovedtracks: {
track: [{ name: 'Should Not Be Returned', artist: { name: 'Ignored' } }],
},
}),
})
const result = await getLovedTracks('testuser')
expect(result).toEqual([])
})
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/bot/src/lastfm/lastFmApi.spec.ts` around lines 521 - 525, The test
for non-OK responses is fragile because it relied on fetch returning { ok: false
} without a json() implementation; update the test in lastFmApi.spec.ts to mock
fetch to resolve to an object with ok: false, a realistic status (e.g., 500),
and a json() function (e.g., jest.fn(() => Promise.resolve({ error: 'err' })))
so the test fails only if non-OK handling is missing, and assert
getLovedTracks('testuser') returns []; then fix the production code in
lastFmApi.ts by adding an explicit guard in getLovedTracks that checks
response.ok and immediately returns [] (or the appropriate empty result) before
calling response.json(), ensuring non-OK responses are handled
deterministically.

@gitguardian

gitguardian Bot commented Apr 12, 2026 •

Copy link
Copy Markdown

️✅ There are no secrets present in this pull request anymore.

If these secrets were true positive and are still valid, we highly recommend you to revoke them.
While these secrets were previously flagged, we no longer have a reference to the
specific commits where they were detected. Once a secret has been leaked into a git
repository, you should consider it compromised, even if it was deleted immediately.
Find here more information about risks.


🦉 GitGuardian detects secrets in your source code to help developers and security teams secure the modern development process. You are seeing this because you or someone else with access to this repository has authorized GitGuardian to scan your pull request.

@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

♻️ Duplicate comments (2)
packages/bot/src/utils/music/queueManipulation.ts (2)

361-363: ⚠️ Potential issue | 🟠 Major

The Spotify feature lookup is still dead work here.

currentFeatures is fetched, but it never participates in candidate collection or scoring. At the same time, Line 1386 still grants +0.08 to every Spotify→Spotify pair and labels it spotify mood match without comparing any audio features. Right now this adds Spotify API latency without producing an actual mood-based score.

Also applies to: 1386-1389

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

In `@packages/bot/src/utils/music/queueManipulation.ts` around lines 361 - 363,
The code fetches currentFeatures via getTrackAudioFeatures(requestedBy.id) but
never uses it in candidate collection or scoring, while the scoring still
unconditionally grants "+0.08" labeled "spotify mood match"; either remove the
unused Spotify lookup to avoid API latency or actually use currentFeatures in
the scoring path: fetch candidate features (or include them from existing
candidate metadata), compute a similarity/distance between currentFeatures and
candidateFeatures, and only apply the +0.08 "spotify mood match" bonus when the
similarity meets a threshold; update getTrackAudioFeatures calls to be
conditional (skip when currentFeatures is null) and preserve existing .catch
handling to fall back to null if the API fails.

43-86: ⚠️ Potential issue | 🟠 Major

Don't cache null for token misses or transient Spotify lookup failures.

This still turns user-specific or temporary failures into a global negative cache entry keyed only by title::author. After one user without a token hits this path, every later user gets a permanent miss for the same track.

Suggested fix
 const audioFeatureCache = new Map<string, SpotifyAudioFeatures | null>()

 async function getTrackAudioFeatures(
     track: Track,
     userId: string,
 ): Promise<SpotifyAudioFeatures | null> {
     const cacheKey = normalizeTrackKey(track.title, track.author)
-    
+
     const cached = audioFeatureCache.get(cacheKey)
     if (cached !== undefined) {
         return cached
     }
-    
+
     const token = await spotifyLinkService.getValidAccessToken(userId)
     if (!token) {
-        audioFeatureCache.set(cacheKey, null)
         return null
     }
-    
+
     let spotifyId: string | null = null
-    
+
     if (track.url && track.url.includes('open.spotify.com/track/')) {
         const match = track.url.match(/track\/([a-zA-Z0-9]+)/)
         if (match) {
             spotifyId = match[1]
@@
-    if (!spotifyId) {
-        audioFeatureCache.set(cacheKey, null)
+    if (!spotifyId) {
         return null
     }
-    
+
     const features = await getAudioFeatures(token, spotifyId).catch(() => null)
-    audioFeatureCache.set(cacheKey, features)
+    if (features) {
+        audioFeatureCache.set(cacheKey, features)
+    }
     return features
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/bot/src/utils/music/queueManipulation.ts` around lines 43 - 86, The
code currently writes null into audioFeatureCache for token misses and failed
Spotify lookups, causing global negative caching; in getTrackAudioFeatures stop
setting audioFeatureCache.set(cacheKey, null) when
spotifyLinkService.getValidAccessToken(userId) returns falsy or when spotifyId
resolution fails — simply return null in those branches without writing the
cache; instead only call audioFeatureCache.set(cacheKey, features) when features
is a non-null SpotifyAudioFeatures (i.e., after await getAudioFeatures(...)
resolves successfully), so only successful lookups are cached; use the existing
symbols audioFeatureCache, getTrackAudioFeatures, normalizeTrackKey,
spotifyLinkService.getValidAccessToken, searchSpotifyTrack, and getAudioFeatures
to locate and change the logic.
🤖 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/handlers/player/trackHandlers.ts`:
- Around line 265-279: The scrobbleAndRecord and recordPlayBehavior calls are
currently awaited inside the same try so failures in one can abort the other and
downstream cleanup; split them so each runs independently and failures only log:
call scrobbleAndRecord(queue, track) and recordPlayBehavior(queue.guild.id,
track, requesterId, 0, 0.8, 'implicit_like') in separate try/catch blocks (or
use Promise.allSettled) so exceptions from scrobbleAndRecord do not prevent
recordPlayBehavior and vice versa, log errors using the existing logger instead
of rethrowing, and apply the same change to the duplicate block around the
recordPlayBehavior usage at the other occurrence (the block at ~314-326).
- Around line 76-78: The duration gate currently uses `track.durationMS <
minDurationMs`, which lets tracks exactly equal to 20_000ms slip through; change
the condition to `track.durationMS <= minDurationMs` so tracks of 20.0s or
shorter are excluded; update the early-return branch around `startTime`,
`track.durationMS`, and `minDurationMs` (the block that calls
`guildTrackStartTimes.delete(guildId)` in trackHandlers.ts) to use `<=` instead
of `<`.

---

Duplicate comments:
In `@packages/bot/src/utils/music/queueManipulation.ts`:
- Around line 361-363: The code fetches currentFeatures via
getTrackAudioFeatures(requestedBy.id) but never uses it in candidate collection
or scoring, while the scoring still unconditionally grants "+0.08" labeled
"spotify mood match"; either remove the unused Spotify lookup to avoid API
latency or actually use currentFeatures in the scoring path: fetch candidate
features (or include them from existing candidate metadata), compute a
similarity/distance between currentFeatures and candidateFeatures, and only
apply the +0.08 "spotify mood match" bonus when the similarity meets a
threshold; update getTrackAudioFeatures calls to be conditional (skip when
currentFeatures is null) and preserve existing .catch handling to fall back to
null if the API fails.
- Around line 43-86: The code currently writes null into audioFeatureCache for
token misses and failed Spotify lookups, causing global negative caching; in
getTrackAudioFeatures stop setting audioFeatureCache.set(cacheKey, null) when
spotifyLinkService.getValidAccessToken(userId) returns falsy or when spotifyId
resolution fails — simply return null in those branches without writing the
cache; instead only call audioFeatureCache.set(cacheKey, features) when features
is a non-null SpotifyAudioFeatures (i.e., after await getAudioFeatures(...)
resolves successfully), so only successful lookups are cached; use the existing
symbols audioFeatureCache, getTrackAudioFeatures, normalizeTrackKey,
spotifyLinkService.getValidAccessToken, searchSpotifyTrack, and getAudioFeatures
to locate and change the logic.
🪄 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: 4be3085c-3951-4f03-ae8e-d54cd4dfa62f

📥 Commits

Reviewing files that changed from the base of the PR and between da6368f and f5f9c41.

📒 Files selected for processing (5)
  • packages/bot/src/handlers/player/trackHandlers.spec.ts
  • packages/bot/src/handlers/player/trackHandlers.ts
  • packages/bot/src/services/musicRecommendation/feedbackService.ts
  • packages/bot/src/utils/music/queueManipulation.spec.ts
  • packages/bot/src/utils/music/queueManipulation.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/bot/src/services/musicRecommendation/feedbackService.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: SonarCloud Scan
  • GitHub Check: Quality Gates
🧰 Additional context used
🪛 Betterleaks (1.1.1)
packages/bot/src/utils/music/queueManipulation.spec.ts

[high] 2662-2662: Detected a Generic API Key, potentially exposing access to various services and sensitive operations.

(generic-api-key)

Comment on lines +76 to +78
if (!startTime || !track.durationMS || track.durationMS < minDurationMs) {
guildTrackStartTimes.delete(guildId)
return

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 | 🟡 Minor

Use a strict > 20s gate for skip feedback.

The current check allows durationMS === 20_000, but the feature is described as recording early skips only for tracks longer than 20 seconds. This should be <= minDurationMs here, otherwise 20.0s tracks are misclassified.

Suggested fix
-    if (!startTime || !track.durationMS || track.durationMS < minDurationMs) {
+    if (!startTime || !track.durationMS || track.durationMS <= minDurationMs) {
         guildTrackStartTimes.delete(guildId)
         return
     }
📝 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
if (!startTime || !track.durationMS || track.durationMS < minDurationMs) {
guildTrackStartTimes.delete(guildId)
return
if (!startTime || !track.durationMS || track.durationMS <= minDurationMs) {
guildTrackStartTimes.delete(guildId)
return
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/bot/src/handlers/player/trackHandlers.ts` around lines 76 - 78, The
duration gate currently uses `track.durationMS < minDurationMs`, which lets
tracks exactly equal to 20_000ms slip through; change the condition to
`track.durationMS <= minDurationMs` so tracks of 20.0s or shorter are excluded;
update the early-return branch around `startTime`, `track.durationMS`, and
`minDurationMs` (the block that calls `guildTrackStartTimes.delete(guildId)` in
trackHandlers.ts) to use `<=` instead of `<`.

Comment on lines 265 to +279
await scrobbleAndRecord(queue, track)

if (track) {
const requesterId = track.requestedBy?.id
?? (track.metadata as { requestedById?: string } | undefined)
?.requestedById
await recordPlayBehavior(
queue.guild.id,
track,
requesterId,
0,
0.8,
'implicit_like',
)
}

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

Decouple implicit feedback from scrobble/history failures.

Both handlers await scrobbleAndRecord(...) before recordPlayBehavior(...) inside the same outer try. A transient Last.fm/history error now drops the implicit like/dislike signal entirely, and a Redis write failure from recordPlayBehavior(...) can abort replenish/snapshot/cleanup for the whole event. These side effects should fail independently and only log.

Also applies to: 314-326

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

In `@packages/bot/src/handlers/player/trackHandlers.ts` around lines 265 - 279,
The scrobbleAndRecord and recordPlayBehavior calls are currently awaited inside
the same try so failures in one can abort the other and downstream cleanup;
split them so each runs independently and failures only log: call
scrobbleAndRecord(queue, track) and recordPlayBehavior(queue.guild.id, track,
requesterId, 0, 0.8, 'implicit_like') in separate try/catch blocks (or use
Promise.allSettled) so exceptions from scrobbleAndRecord do not prevent
recordPlayBehavior and vice versa, log errors using the existing logger instead
of rethrowing, and apply the same change to the duplicate block around the
recordPlayBehavior usage at the other occurrence (the block at ~314-326).

…oved tracks, artist frequency, spotify audio features

signals added (all implicit, no commands needed):
- skip tracking: guildTrackStartTimes records when track starts; early skip (<30%, track >20s) → implicit_dislike in redis (14d)
- completion tracking: >80% played → implicit_like in redis (14d); both feed calculateRecommendationScore
- implicit dislike: -0.35 penalty; implicit like: +0.25 boost
- last.fm loved tracks: getLovedTracks() merged first in seed list (highest priority over top/recent)
- artist frequency: manual-play count from persistent history → +0.10/+0.20/+0.30 gradient
- spotify audio features: getAudioFeatures() + searchSpotifyTrack() wired into replenish flow
  via getTrackAudioFeatures(); results cached per normalized track key
- spotify-source candidate boost: +0.08 when both current and candidate are from spotify

code quality:
- recordPlayBehavior() helper eliminates duplication between skip/finish tracking handlers
- getImplicitKeysByType() helper eliminates duplication between dislike/like key getters
- sonar cpd exclusions updated for api/seed files
@sonarqubecloud

Copy link
Copy Markdown

@LucasSantana-Dev
LucasSantana-Dev merged commit ad8a57b into main Apr 13, 2026
12 checks passed
LucasSantana-Dev added a commit that referenced this pull request May 13, 2026
…ks, artist frequency, audio features (#577)

signals added (all implicit, no commands needed):
- skip tracking: guildTrackStartTimes records when track starts; early skip (<30%, track >20s) → implicit_dislike in redis (14d)
- completion tracking: >80% played → implicit_like in redis (14d); both feed calculateRecommendationScore
- implicit dislike: -0.35 penalty; implicit like: +0.25 boost
- last.fm loved tracks: getLovedTracks() merged first in seed list (highest priority over top/recent)
- artist frequency: manual-play count from persistent history → +0.10/+0.20/+0.30 gradient
- spotify audio features: getAudioFeatures() + searchSpotifyTrack() wired into replenish flow
  via getTrackAudioFeatures(); results cached per normalized track key
- spotify-source candidate boost: +0.08 when both current and candidate are from spotify

code quality:
- recordPlayBehavior() helper eliminates duplication between skip/finish tracking handlers
- getImplicitKeysByType() helper eliminates duplication between dislike/like key getters
- sonar cpd exclusions updated for api/seed files
@LucasSantana-Dev
LucasSantana-Dev deleted the feat/smart-autoplay-signals branch May 23, 2026 02:21

This branch was successfully deployed

1 active deployment
Preview — 490b4573 Deployed Apr 13, 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