Skip to content

fix(bot): drop the audio-features scoring path, spotify returns 403 - #2073

Closed
LucasSantana-Dev wants to merge 2 commits into
fix/drop-dead-spotify-recommendationsfrom
fix/drop-dead-spotify-audio-features
Closed

LucasSantana-Dev wants to merge 2 commits into
fix/drop-dead-spotify-recommendationsfrom
fix/drop-dead-spotify-audio-features

Conversation

@LucasSantana-Dev

@LucasSantana-Dev LucasSantana-Dev commented Aug 21, 2026 •

Copy link
Copy Markdown
Owner

Closes #2072. Stacked on #2071 — review that first; this targets its branch, not main.

Measured, with Lucky own credentials from inside lucky-bot

v1/search / artists/{id} / albums/{id} / tracks/{id}   HTTP 200
v1/audio-features                                      HTTP 403

So currentFeatures was always null and enrichWithAudioFeatures returned its input unchanged on the first line. The 5000-entry LRU cache behind it, with a 24h TTL, could only ever hold null.

Option 3 turned out to be already done

The plan was to replace the Spotify signal with Last.fm tags. That replacement already exists: candidateScorer scores genre-family overlap in-pass via calculateGenreFamilyPenalty on Last.fm tags, and the code says so at the old call site:

Genre-family scoring moved to in-pass calculateRecommendationScore via Last.fm tags so it works without a Spotify token. This pass is now limited to audio-feature (energy/valence) deltas, which the Last.fm-tag path cant substitute for.

What was left is energy, valence, tempo and acousticness. Last.fm does not expose those, so there is nothing to substitute with. The remaining honest move is removal.

Extended access does not bring them back

Ruled out with sources on #2072. Since 2025-05-15 it requires a registered business entity and 250k MAUs, and the deprecation notice says extended mode preserved these endpoints for apps that already had it on 2024-11-27. It is not a door that opens later.

Removed

enrichWithAudioFeatures existed twice, in candidateScorer.ts and in candidateFallback.ts. Both are gone, along with audioFeatures.ts, getAudioFeatures, getBatchAudioFeatures, and the SpotifyAudioFeatures / SpotifyAudioFeatureConstraints types, plus the currentFeatures parameter threaded through the collector chain.

15 files changed, 33 insertions(+), 1163 deletions(-)

Tests retargeted, not deleted

Two regression tests used the removed fetchers only as a vehicle to exercise spotifyFetch shared AbortSignal (#1279) and its 429 retry (PR #808 / Greptile). Those behaviours still matter, so both now run against searchSpotifyTrack and carry a comment saying why.

One test asserted searchSpotifyTrack had been called — but it reached that function only through the audio-features path, so it broke the moment the path went. It now asserts on the queue: the point is that a Spotify-linked user gets tracks, and that survives arms being retired. I had introduced that same brittleness in #2071 by asserting on an internal call; this is the correction.

Verification

jest         3114 passed, 1 skipped, 241 suites
type:check   clean across shared, bot, backend, frontend
eslint       quiet

Summary by cubic

Removes the Spotify audio-features scoring path that always 403’d. Before: we attempted energy/valence/tempo/acousticness boosts; currentFeatures was null and the pass was a no-op. Now: delete the pass and fetchers; scoring relies on Last.fm tags and other signals. Side effect: fewer Spotify calls and simpler code with no change to recommendation outcomes.

  • Delete enrichWithAudioFeatures (both sites), audioFeatures.ts, getAudioFeatures, getBatchAudioFeatures, related types, and the currentFeatures parameter/thread across collectors and replenisher.
  • Stop fetching/using track features in replenisher; keep the selected-candidate flow unchanged.
  • Retarget AbortSignal and 429 retry tests to searchSpotifyTrack; replace brittle internal-call assertion with a queue outcome assertion; no test deletions.
  • Do not reintroduce v1/audio-features (extended access will not restore it).
  • Verified: 3114 tests pass; type-check clean; eslint quiet.

Written for commit 118ae20. Summary will update on new commits.

Review in cubic

Measured from inside lucky-bot with Lucky's own credentials:

    v1/search / artists / albums / tracks   HTTP 200
    v1/audio-features                       HTTP 403

So currentFeatures was always null and enrichWithAudioFeatures returned
its input unchanged on the first line. The LRU cache behind it could only
ever hold null.

The Last.fm substitution this would otherwise need already exists.
candidateScorer already scores genre-family overlap in-pass via
calculateGenreFamilyPenalty on Last.fm tags, and the code says so at the
old call site: "Genre-family scoring moved to in-pass
calculateRecommendationScore via Last.fm tags so it works without a
Spotify token." What was left here is energy, valence, tempo and
acousticness, which Last.fm does not expose and nothing can substitute.

Extended access does not bring these back. Since 2025-05-15 it requires a
registered business entity and 250k MAUs, and the deprecation notice says
extended mode PRESERVED these endpoints for apps that already had it on
2024-11-27; it is not a door that opens later. Recorded on the issue.

enrichWithAudioFeatures existed twice, in candidateScorer and in
candidateFallback. Both are gone, along with audioFeatures.ts, the two
spotifyApi fetchers and the SpotifyAudioFeatures types.

Two regression tests used the removed fetchers only as a vehicle to
exercise spotifyFetch's shared AbortSignal and 429 retry. Those behaviours
still matter, so both are retargeted at searchSpotifyTrack rather than
deleted, and say so in a comment.

One test asserted searchSpotifyTrack had been called, which it reached
only through the audio-features path. It now asserts on the queue instead:
the point is that a Spotify-linked user gets tracks, and that survives
arms being retired.

3114 tests pass, type:check clean across all four workspaces, eslint quiet.

Stacked on #2071.
@coderabbitai

coderabbitai Bot commented Aug 21, 2026 •

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 239c972a-cacb-4822-9f07-c566bd63fae1

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Warning

Billing warning: we have not been able to collect payment for this subscription for more than 72 hours. Please update the payment method or pay any pending invoices in Billing to avoid service interruption.


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.

@github-actions

Copy link
Copy Markdown

Failed to generate code suggestions for PR

@graphify-labs graphify-labs 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.

Graphify reviewed this change.

Worth a look — the grounded gate found no coupling regressions or blocking issues, but 5 advisory finding(s) below merit a look before merge.


Graphify review — findings

Removes the audio-features enrichment path from autoplay: deletes audioFeatures.ts (getTrackAudioFeatures), drops its re-exports from queueManipulation.ts, and strips the now-unused audioFeatures param from candidateCollector. Rewrites the associated specs to assert on queue outcomes rather than searchSpotifyTrack/getBatchAudioFeatures calls, and deletes the energy/valence boost and enrichWithAudioFeatures tests.

Worth a look

  • Removed public getTrackAudioFeatures barrel export — packages/bot/src/services/musicManagement/queueManipulation.ts:17 · Escalate · high
    • agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review
  • Removed exported getAudioFeatures API — packages/bot/src/spotify/spotifyApi.ts · Escalate · high
    • agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review
  • Removed exported getBatchAudioFeatures API — packages/bot/src/spotify/spotifyApi.ts · Escalate · high
    • agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review
  • Removed collectRecommendationCandidates positional parameter — packages/bot/src/services/musicRecommendation/autoplay/candidateCollector.ts:61 · Escalate · medium
    • agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review
  • Removed exported enrichWithAudioFeatures API — packages/bot/src/services/musicRecommendation/candidateFallback.ts · Escalate · medium
    • agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review
Analysis details — impact, health, verification

Impact & health

Graphify review

Impact — 149 functions depend on the 119 functions this change touches.

Health — this change adds coupling hotspots:

  • new: replenishQueue() — 19 callers, 39 callees
  • new: calculateRecommendationScore() — 15 callers, 8 callees
  • new: collectLastFmCandidates() — 4 callers, 13 callees
  • new: collectRecommendationCandidates() — 6 callers, 7 callees
  • new: collectSeedSimilarCandidates() — 4 callers, 8 callees
  • new: setupLifecycleHandlers() — 3 callers, 8 callees
  • new: setupTrackHandlers() — 4 callers, 6 callees
  • new: handlePlayerSkip() — 2 callers, 11 callees
  • …and 10 more — each is listed as a finding

Verification — 149 functions in the blast radius were not formally verified this run (proofs are advisory here).

Health delta baseline: last indexed commit 23de645 (diverged from this PR's base — delta is approximate).

Gate & verification

graphify gate

PASS — objectively clean (no health regressions, tests not run — proofs not run this pass (advisory)). Grounded, not self-assessed.

Advisory (not blocking):

  • verification_scope: 144 function(s) in the blast radius were not formally verified this run

· 1 grounded finding(s) anchored inline below; 17 more finding(s) on lines outside this diff (see the check run).

}
}

export async function getArtistPopularity(

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Health regression — getArtistPopularity()

high coupling complexity (Ca·Ce = 12).

Grounded coupling-delta finding (deterministic), not an LLM guess.

@cubic-dev-ai cubic-dev-ai 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.

2 issues found and verified against the latest diff

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="packages/bot/src/spotify/spotifyApiRetry.spec.ts">

<violation number="1" location="packages/bot/src/spotify/spotifyApiRetry.spec.ts:148">
P3: The two timer-based Retry-After tests now call searchSpotifyTrack but their mocked success response is still the removed getAudioFeatures shape ({ id, energy, valence }), which searchSpotifyTrack cannot parse (it reads tracks.items[0].id) and therefore returns null. Because the tests assert only attemptCount and never the result, they silently no longer exercise a valid successful parse and leave the exact audio-features artifacts this PR removes. Update both mocks to a search response (e.g. { tracks: { items: [{ id: 't1' }] } }) and assert the parsed id.</violation>
</file>

<file name="packages/bot/src/services/musicRecommendation/autoplay/replenisher.ts">

<violation number="1" location="packages/bot/src/services/musicRecommendation/autoplay/replenisher.ts:552">
P3: Since the `enrichWithAudioFeatures` pass is removed, `enriched` is just a redundant alias for `selected` and the name is now misleading. Drop the alias and use `selected` directly (or rename), so the code reflects that no enrichment happens.</violation>
</file>

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

})

const promise = spotifyApi.getAudioFeatures('tok', 't')
const promise = spotifyApi.searchSpotifyTrack(

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P3: The two timer-based Retry-After tests now call searchSpotifyTrack but their mocked success response is still the removed getAudioFeatures shape ({ id, energy, valence }), which searchSpotifyTrack cannot parse (it reads tracks.items[0].id) and therefore returns null. Because the tests assert only attemptCount and never the result, they silently no longer exercise a valid successful parse and leave the exact audio-features artifacts this PR removes. Update both mocks to a search response (e.g. { tracks: { items: [{ id: 't1' }] } }) and assert the parsed id.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/bot/src/spotify/spotifyApiRetry.spec.ts, line 148:

<comment>The two timer-based Retry-After tests now call searchSpotifyTrack but their mocked success response is still the removed getAudioFeatures shape ({ id, energy, valence }), which searchSpotifyTrack cannot parse (it reads tracks.items[0].id) and therefore returns null. Because the tests assert only attemptCount and never the result, they silently no longer exercise a valid successful parse and leave the exact audio-features artifacts this PR removes. Update both mocks to a search response (e.g. { tracks: { items: [{ id: 't1' }] } }) and assert the parsed id.</comment>

<file context>
@@ -306,7 +145,11 @@ describe('Spotify API 429 Retry Logic', () => {
                 })
 
-                const promise = spotifyApi.getAudioFeatures('tok', 't')
+                const promise = spotifyApi.searchSpotifyTrack(
+                    'tok',
+                    'Creep',
</file context>

currentAudioFeatures,
currentTrack.author,
)
const enriched = selected

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P3: Since the enrichWithAudioFeatures pass is removed, enriched is just a redundant alias for selected and the name is now misleading. Drop the alias and use selected directly (or rename), so the code reflects that no enrichment happens.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/bot/src/services/musicRecommendation/autoplay/replenisher.ts, line 552:

<comment>Since the `enrichWithAudioFeatures` pass is removed, `enriched` is just a redundant alias for `selected` and the name is now misleading. Drop the alias and use `selected` directly (or rename), so the code reflects that no enrichment happens.</comment>

<file context>
@@ -562,16 +549,7 @@ async function _replenishQueue(
-            currentAudioFeatures,
-            currentTrack.author,
-        )
+        const enriched = selected
 
         if (requestedBy?.id) {
</file context>

@graphify-labs graphify-labs 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.

Graphify reviewed this change.

Worth a look — the grounded gate found no coupling regressions or blocking issues, but 5 advisory finding(s) below merit a look before merge.


Graphify review — findings

Removes the getTrackAudioFeatures/audio-features enrichment path from autoplay, deleting audioFeatures.ts and its re-exports from queueManipulation.ts, and drops the corresponding enrichWithAudioFeatures and energy/valence-boost specs. Updates collectRecommendationCandidates callers to drop the now-removed audio-features argument, and rewrites the multi-user blend test to assert on queue outcome instead of searchSpotifyTrack being called.

Worth a look

  • Removed exported getTrackAudioFeatures API — packages/bot/src/services/musicManagement/queueManipulation.ts:17 · Escalate · high
    • agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review
  • Removed exported getAudioFeatures API — packages/bot/src/spotify/spotifyApi.ts:159 · Escalate · high
    • agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review
  • Removed exported getBatchAudioFeatures API — packages/bot/src/spotify/spotifyApi.ts:209 · Escalate · high
    • agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review
  • collectRecommendationCandidates positional contract changed — packages/bot/src/services/musicRecommendation/autoplay/candidateCollector.ts:61 · Escalate · medium
    • agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review
  • Removed exported SpotifyAudioFeatureConstraints type — packages/bot/src/spotify/spotifyApi.ts:13 · Escalate · medium
    • agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review
Analysis details — impact, health, verification

Impact & health

Graphify review

Impact — 149 functions depend on the 119 functions this change touches.

Health — this change adds coupling hotspots:

  • new: replenishQueue() — 19 callers, 39 callees
  • new: calculateRecommendationScore() — 15 callers, 8 callees
  • new: collectLastFmCandidates() — 4 callers, 13 callees
  • new: collectRecommendationCandidates() — 6 callers, 7 callees
  • new: collectSeedSimilarCandidates() — 4 callers, 8 callees
  • new: setupLifecycleHandlers() — 3 callers, 8 callees
  • new: setupTrackHandlers() — 4 callers, 6 callees
  • new: handlePlayerSkip() — 2 callers, 11 callees
  • …and 10 more — each is listed as a finding

Verification — 149 functions in the blast radius were not formally verified this run (proofs are advisory here).

Health delta baseline: last indexed commit 23de645 (diverged from this PR's base — delta is approximate).

Gate & verification

graphify gate

PASS — objectively clean (no health regressions, tests not run — proofs not run this pass (advisory)). Grounded, not self-assessed.

Advisory (not blocking):

  • verification_scope: 144 function(s) in the blast radius were not formally verified this run

· 1 grounded finding(s) anchored inline below; 17 more finding(s) on lines outside this diff (see the check run).

}
}

export async function getArtistPopularity(

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Health regression — getArtistPopularity()

high coupling complexity (Ca·Ce = 12).

Grounded coupling-delta finding (deterministic), not an LLM guess.

@LucasSantana-Dev
LucasSantana-Dev deleted the branch fix/drop-dead-spotify-recommendations August 21, 2026 23:30
LucasSantana-Dev added a commit that referenced this pull request Aug 22, 2026
…2074)

Closes #2072. Replaces #2073, which GitHub auto-closed when #2071 merged
with `--delete-branch` and took its base branch with it. Same content,
rebuilt on current `main`.

## Measured, with Lucky own credentials from inside lucky-bot

```
v1/search / artists/{id} / albums/{id} / tracks/{id}   HTTP 200
v1/audio-features                                      HTTP 403
```

So `currentFeatures` was always `null` and `enrichWithAudioFeatures`
returned its input unchanged on the first line. The 5000-entry LRU cache
behind it, with a 24h TTL, could only ever hold `null`.

## The Last.fm replacement already exists

The plan was to substitute the Spotify signal with Last.fm tags. That
work was already done: `candidateScorer` scores genre-family overlap
in-pass via `calculateGenreFamilyPenalty` on Last.fm tags, and the code
says so at the old call site:

> Genre-family scoring moved to in-pass `calculateRecommendationScore`
via Last.fm tags so it works without a Spotify token. This pass is now
limited to audio-feature (energy/valence) deltas, which the Last.fm-tag
path cant substitute for.

What remained is energy, valence, tempo and acousticness. Last.fm does
not expose those, so there is nothing to substitute with.

## Extended access does not bring them back

Ruled out with sources on #2072. Since 2025-05-15 it requires a
registered business entity and 250k MAUs, and the deprecation notice
says extended mode **preserved** these endpoints for apps that already
had it on 2024-11-27. It is not a door that opens later.

## Removed

`enrichWithAudioFeatures` existed **twice**, in `candidateScorer.ts` and
in `candidateFallback.ts`. Both are gone, along with `audioFeatures.ts`,
`getAudioFeatures`, `getBatchAudioFeatures`, the `SpotifyAudioFeatures`
/ `SpotifyAudioFeatureConstraints` types, and the `currentFeatures`
parameter threaded through the collector chain.

```
15 files changed, 29 insertions(+), 1164 deletions(-)
```

## Tests retargeted, not deleted

Two regression tests used the removed fetchers only as a **vehicle** to
exercise `spotifyFetch` shared `AbortSignal` (#1279) and its 429 retry
(PR #808 / Greptile). Those behaviours still matter, so both now run
against `searchSpotifyTrack` and carry a comment saying why.

## Verification

```
jest    3122 passed, 1 skipped, 243 suites
tsc     clean
```

<!-- This is an auto-generated description by cubic. -->
---
## Summary by cubic
Removes Spotify audio-features–based scoring because the endpoint now
returns 403. Previously we boosted/penalized candidates using
energy/valence/tempo/acousticness; in practice this path was a no-op
(always null), so behavior does not change and scoring continues to rely
on Last.fm tag–based genre-family checks.

- Delete the enrichment path: remove `enrichWithAudioFeatures` (from
candidate scorer and fallback) and `audioFeatures.ts`; drop
`getAudioFeatures`, `getBatchAudioFeatures`, and the
`SpotifyAudioFeatures`/`SpotifyAudioFeatureConstraints` types.
- Simplify call sites: remove the `currentFeatures` parameter from
candidate collection and replenisher; delete the related cache and the
export in queue manipulation.
- Keep infra tests: retarget AbortSignal deadline and 429-retry
regressions to `searchSpotifyTrack`; update Retry-After tests to return
a valid search payload and assert non-null results. Remove an unused
`getArtistGenres` import flagged by CodeQL.
- No rollout or user migration needed; compile-time errors will surface
any stale imports.

<sup>Written for commit 7bb2eff.
Summary will update on new commits.</sup>

<a
href="https://cubic.dev/pr/LucasSantana-Dev/Lucky/pull/2074?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>

<!-- End of auto-generated description by cubic. -->
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant