Skip to content

feat: source last month's top 5 from Spotify official top tracks API#88

Merged
kaiiiichen merged 1 commit into
mainfrom
feat/spotify-official-top-tracks
Jul 5, 2026
Merged

feat: source last month's top 5 from Spotify official top tracks API#88
kaiiiichen merged 1 commit into
mainfrom
feat/spotify-official-top-tracks

Conversation

@kaiiiichen

@kaiiiichen kaiiiichen commented Jul 5, 2026

Copy link
Copy Markdown
Owner

Summary

  • /api/spotify/last-month-top now calls Spotify's official GET /me/top/tracks?time_range=short_term&limit=5 (rolling ~4 weeks, computed by Spotify across all devices) instead of aggregating Supabase listening_history over the previous UTC calendar month — which both lagged behind (early-July views showed June data) and only captured plays observed while the site was being polled.
  • Reuses the existing refresh-token flow (getSpotifyAccessToken) with a 401 invalidate-and-retry; returns { tracks: [] } on any failure. New lib/spotify-top-tracks.ts maps the response (affinity order preserved) with unit tests; removed the now-unused lib/listening-monthly-top.ts and Supabase aggregation helpers. now-playing's Supabase writes are untouched.
  • Docs updated (README EN/中文, CLAUDE.md, .env.example): the refresh token now needs the user-top-read scope.

Deployment note

The current SPOTIFY_REFRESH_TOKEN lacks user-top-read, so the endpoint returns an empty list (tooltip shows the empty state) until a re-authorized token is set in Vercel env.

Test plan

  • npm run lint && npm run typecheck && npm run test && npm run build all green
  • After updating SPOTIFY_REFRESH_TOKEN (with user-top-read), verify /api/spotify/last-month-top returns 5 tracks matching the Spotify app's last-month top

Made with Cursor

Summary by CodeRabbit

  • New Features

    • Spotify-based “last month top” results now come directly from Spotify, showing your recent top tracks more accurately.
    • Added better fallback behavior so the section stays usable when Spotify data can’t be retrieved.
  • Documentation

    • Updated setup and API documentation to reflect the new Spotify-powered behavior and required permissions.
    • Clarified the Spotify scope and environment variable guidance in both English and Chinese.

Replace the Supabase listening_history aggregation (previous UTC calendar
month, only recorded while the site is polled) with Spotify's
GET /me/top/tracks?time_range=short_term, which reflects the user's real
top 5 over the last ~4 weeks across all devices. Requires the
user-top-read scope on the refresh token.

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Claude <noreply@anthropic.com>
@vercel

vercel Bot commented Jul 5, 2026

Copy link
Copy Markdown

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

Project Deployment Actions Updated (UTC)
kaichen.dev Ready Ready Preview, Comment Jul 5, 2026 3:08am

@coderabbitai

coderabbitai Bot commented Jul 5, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Replaces the Supabase-driven "last month top five tracks" feature with a direct Spotify Web API integration: adds a new mapping module (spotify-top-tracks.ts), rewrites the API route to call Spotify with token refresh/retry logic, removes the old Supabase-based module and helper, updates a component import, and updates documentation.

Changes

Spotify Top Tracks Migration

Layer / File(s) Summary
Spotify top tracks contract and mapping
lib/spotify-top-tracks.ts, lib/spotify-top-tracks.test.ts
Adds ListeningHighlightTrack and SpotifyTopTracksPayload types plus mapSpotifyTopTracks to convert Spotify API items into up to 5 card-row tracks, with unit tests covering ordering, missing id, and empty payloads.
Route handler switched to Spotify API
app/api/spotify/last-month-top/route.ts, app/components/listening-last-month-top.tsx
Rewrites GET() to fetch a Spotify access token, request top tracks, retry once on 401 with a refreshed token, return { tracks: [] } on failure, and map successful responses via mapSpotifyTopTracks; updates the component's type import path accordingly.
Removal of Supabase monthly top tracks logic
lib/listening-monthly-top.ts, lib/listening-monthly-top.test.ts, lib/listening-supabase.ts
Deletes the old Supabase-based module and its tests, and removes getLastMonthTopFiveTracks plus its pagination helper from listening-supabase.ts.
Documentation updates
.env.example, CLAUDE.md, README.md
Updates env variable comments, architecture notes, and English/Chinese API docs to describe the Spotify short_term top tracks endpoint and the user-top-read scope requirement.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Sequence Diagram(s)

sequenceDiagram
    participant Client
    participant Route as API Route (/api/spotify/last-month-top)
    participant TokenCache as Spotify Access Token
    participant SpotifyAPI as Spotify Web API
    participant Mapper as mapSpotifyTopTracks

    Client->>Route: GET /api/spotify/last-month-top
    Route->>TokenCache: get access token
    TokenCache-->>Route: token (or none)
    alt no token
        Route-->>Client: { tracks: [] }
    else token available
        Route->>SpotifyAPI: fetch top tracks (Bearer token)
        SpotifyAPI-->>Route: response
        alt 401 Unauthorized
            Route->>TokenCache: invalidate token
            Route->>TokenCache: get refreshed token
            Route->>SpotifyAPI: retry fetch top tracks
            SpotifyAPI-->>Route: response
        end
        alt non-OK or JSON parse failure
            Route-->>Client: { tracks: [] }
        else success
            Route->>Mapper: mapSpotifyTopTracks(payload)
            Mapper-->>Route: ListeningHighlightTrack[]
            Route-->>Client: { tracks } with cache headers
        end
    end
Loading
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly describes the main change: switching the last-month top tracks endpoint to Spotify's official top tracks API.
Description check ✅ Passed The summary and test plan are present and specific, though the type, related issues, notes, and checklist sections are missing.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/spotify-official-top-tracks

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.

@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 (2)
app/api/spotify/last-month-top/route.ts (2)

27-53: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Consider logging failures for observability.

All failure branches (missing token, non-OK Spotify response, JSON parse error, exhausted 401 retry) silently return { tracks: [] } with no logging. Given the PR notes this endpoint currently returns empty due to a missing scope, some server-side logging would make it easier to distinguish "no plays" from "auth/scope failure" in production.

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

In `@app/api/spotify/last-month-top/route.ts` around lines 27 - 53, Add
server-side logging in GET for each empty-response path so failures are
observable. In app/api/spotify/last-month-top/route.ts, use the existing GET
flow, getSpotifyAccessToken, fetchTopTracks, and
invalidateSpotifyAccessTokenCache to log when the access token is missing, when
the first or retried Spotify response is 401, when res.ok is false, and when
res.json() throws. Keep returning the same emptyResponse payload, but make each
branch emit a distinct message so auth/scope failures can be distinguished from
a true no-data case.

14-23: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Remove the redundant revalidate export

fetch(..., { cache: "no-store" }) already makes this route dynamic, so export const revalidate = 600 doesn’t change the handler’s behavior. Keep the Cache-Control header if CDN caching is the goal; otherwise drop revalidate to avoid implying Next is handling revalidation here.

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

In `@app/api/spotify/last-month-top/route.ts` around lines 14 - 23, Remove the
redundant revalidate export from the last-month-top route, since fetchTopTracks
already uses fetch with cache: "no-store" and the route is therefore dynamic.
Keep responseHeaders and its Cache-Control behavior as-is if CDN caching is
still intended, but eliminate the export const revalidate declaration so the
route doesn’t imply Next.js revalidation is being used here.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@app/api/spotify/last-month-top/route.ts`:
- Around line 20-25: The fetchTopTracks helper currently has no timeout, so
Spotify calls can hang on both the initial request and the 401 retry path.
Update fetchTopTracks to use an AbortController with a timeout, and make sure
the caller in the last-month-top route passes/handles the same timeout behavior
for retries so stalled requests are aborted promptly.

---

Nitpick comments:
In `@app/api/spotify/last-month-top/route.ts`:
- Around line 27-53: Add server-side logging in GET for each empty-response path
so failures are observable. In app/api/spotify/last-month-top/route.ts, use the
existing GET flow, getSpotifyAccessToken, fetchTopTracks, and
invalidateSpotifyAccessTokenCache to log when the access token is missing, when
the first or retried Spotify response is 401, when res.ok is false, and when
res.json() throws. Keep returning the same emptyResponse payload, but make each
branch emit a distinct message so auth/scope failures can be distinguished from
a true no-data case.
- Around line 14-23: Remove the redundant revalidate export from the
last-month-top route, since fetchTopTracks already uses fetch with cache:
"no-store" and the route is therefore dynamic. Keep responseHeaders and its
Cache-Control behavior as-is if CDN caching is still intended, but eliminate the
export const revalidate declaration so the route doesn’t imply Next.js
revalidation is being used here.
🪄 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: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: b6feeb17-2a20-4e57-9e8b-7e66f4fd587b

📥 Commits

Reviewing files that changed from the base of the PR and between d874a04 and 9e1e19e.

📒 Files selected for processing (10)
  • .env.example
  • CLAUDE.md
  • README.md
  • app/api/spotify/last-month-top/route.ts
  • app/components/listening-last-month-top.tsx
  • lib/listening-monthly-top.test.ts
  • lib/listening-monthly-top.ts
  • lib/listening-supabase.ts
  • lib/spotify-top-tracks.test.ts
  • lib/spotify-top-tracks.ts
💤 Files with no reviewable changes (3)
  • lib/listening-monthly-top.test.ts
  • lib/listening-monthly-top.ts
  • lib/listening-supabase.ts

Comment on lines +20 to +25
function fetchTopTracks(accessToken: string) {
return fetch(TOP_TRACKS_URL, {
headers: { Authorization: `Bearer ${accessToken}` },
cache: "no-store",
});
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Add a timeout to the Spotify fetch.

fetchTopTracks has no AbortController/timeout. If Spotify's API stalls, the request will hang until the platform's function timeout kicks in, tying up the request thread on every retry path (initial call and the 401 retry).

🕐 Proposed fix: add a request timeout
 function fetchTopTracks(accessToken: string) {
-  return fetch(TOP_TRACKS_URL, {
-    headers: { Authorization: `Bearer ${accessToken}` },
-    cache: "no-store",
-  });
+  return fetch(TOP_TRACKS_URL, {
+    headers: { Authorization: `Bearer ${accessToken}` },
+    cache: "no-store",
+    signal: AbortSignal.timeout(5000),
+  });
 }

Also applies to: 34-41

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

In `@app/api/spotify/last-month-top/route.ts` around lines 20 - 25, The
fetchTopTracks helper currently has no timeout, so Spotify calls can hang on
both the initial request and the 401 retry path. Update fetchTopTracks to use an
AbortController with a timeout, and make sure the caller in the last-month-top
route passes/handles the same timeout behavior for retries so stalled requests
are aborted promptly.

@kaiiiichen kaiiiichen self-assigned this Jul 5, 2026
@kaiiiichen kaiiiichen merged commit 0ff4525 into main Jul 5, 2026
5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant