This repository was archived by the owner on May 25, 2026. It is now read-only.
feat: add create-playlist module for shuffle - #56
Merged
Conversation
Library module for the /api/shuffle route's write path, plus a
source-name read helper in playlists.ts. No callers in this commit;
the route lands separately. Mirrors the module patterns in
playlists.ts and tracks.ts.
createShuffledPlaylist creates an empty playlist at POST
/v1/me/playlists, then writes the URIs in sequential batches of 100
against POST /v1/playlists/{id}/items (the documented per-request
cap). Sequential over parallel preserves source order and lets a
mid-batch failure surface which chunks landed.
POST /v1/me/playlists is the post-Feb-2026 canonical path; the older
POST /v1/users/{user_id}/playlists was deprecated in the same
migration that renamed track-object keys on
/v1/playlists/{id}/items (handled read-side in #54). The /me-prefixed
path uses the token's bound identity, so no prior GET /v1/me call
is needed.
PartialPlaylistWriteError is a distinct class for the "playlist exists
but write was interrupted mid-chunk" state, carrying playlist id and
name, chunks-added and total-chunks counts, and the underlying cause.
Tests grow from 82 to 114 with 32 cases covering chunking boundaries
(0 / 100 / 101 / 199 / 200 / 250 URIs), order preservation across and
within chunks, partial-write semantics, and boundary error mapping for
401 / 429 / 5xx / other / network / response-shape on the create call.
Closes #18.
This was referenced May 23, 2026
ivorycrayon
added a commit
that referenced
this pull request
May 23, 2026
* feat: add /api/shuffle route POST handler at src/app/api/shuffle/route.ts that orchestrates the shuffle write path. Consumes the create-playlist module landed in #56 and the read-side fetchTracksForShuffle from #54. Flow: validate POST body { playlistId }, refresh the access token via ensureFreshToken, then fetch the source URIs and source playlist name in parallel. LIKED_SONGS_ID resolves to a hard-coded "Liked Songs" name (Saved Tracks has no playlist endpoint); every other id reads GET /v1/playlists/{id}?fields=name. If the URI list comes back empty (an empty playlist, or a followed playlist post-Feb-2026 visibility restriction), the route returns 400 unshuffleable_playlist rather than create a hollow playlist. Otherwise: Fisher-Yates shuffle, then createShuffledPlaylist writes the URIs in 100-URI chunks. Hardening mirrors /api/me/playlists/route.ts: Sec-Fetch-Site cross- site refusal, runtime "nodejs", dynamic "force-dynamic", Cache-Control private no-store, Vary Cookie. Error envelope maps the boundary error classes to HTTP statuses: SpotifyUnauthorizedError destroys the session and returns 401 session_expired; SpotifyRateLimitError -> 429 rate_limited; unavailable / unexpected-status / network / response-shape / page- budget errors -> 502 upstream_unavailable or upstream_invalid; unmapped exceptions -> 500 internal_error. PartialPlaylistWriteError gets a distinct console.error log carrying playlistId, chunksAdded, totalChunks, and cause.name, then returns 502 upstream_unavailable. The partial playlist persists in the user's library; the client retries and gets a fresh one. Standard side effect of an interrupted Spotify write. * fix: catch 403/404 reads as unshuffleable The original route comment treated the empty-URI 400 envelope as the catch for followed playlists hit by Spotify's Feb-2026 content- visibility restriction. Smoke test against the live API today shows Spotify returns 403 on `/v1/playlists/{id}/items` for non-owned playlists, not an empty success — see the table in #55. New `SpotifyPlaylistInaccessibleError` carries the raw status (403 or 404). Thrown at the boundary in both `fetchPlaylistName` (playlists.ts) and `fetchPage` (tracks.ts) ahead of the 5xx and catch-all checks, so the more specific case wins. The route catches the new class and returns the same 400 `unshuffleable_playlist` envelope the empty branch already uses — both reduce to "this playlist isn't shuffleable" from the user's perspective. 404 covers missing or editorial ids; the picker should never feed those in, but the defense-in-depth is cheap. Empty-URI branch comment reworded to reflect actual mechanics. Tests cover the 403 (followed) and 404 (missing/editorial) cases against `fetchTracksForShuffle`'s playlist branch; `/v1/me/tracks` is unaffected (caller owns Liked Songs by definition). --------- Co-authored-by: ivorycrayon <ivorycrayon@users.noreply.github.com>
3 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to subscribe to this conversation on GitHub.
Already have an account?
Sign in.
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
New
src/lib/spotify/create-playlist.tslibrary module for the upcoming/api/shuffleroute's write path:createShuffledPlaylist(token, sourceName, uris, now?): public entry. Creates an empty private playlist atPOST /v1/me/playliststhen writes URIs into it in sequential 100-URI batches againstPOST /v1/playlists/{id}/items. Composes the new name as[sourceName] — Shuffled YYYY-MM-DD.PartialPlaylistWriteError: distinct class for the "playlist exists but write interrupted mid-chunk" state, carryingplaylistId,playlistName,chunksAdded,totalChunks, and the underlyingcause.Plus a sibling helper in
src/lib/spotify/playlists.ts:fetchPlaylistName(token, playlistId):GET /v1/playlists/{id}?fields=nameprojection. Lives in the playlist-metadata module alongsidefetchUserPlaylists.No callers in this PR; the route lands in PR B.
Why
Shuffle backend needs 100-URI chunking (Spotify's documented per-request cap, the requirement on #18) baked into the write path rather than retrofitted.
POST /v1/me/playlistsis the post-Feb-2026 canonical create-playlist path; the olderPOST /v1/users/{user_id}/playlistswas deprecated in the same migration that renamed track-object keys on/v1/playlists/{id}/items(handled read-side in #54). The/me-prefixed path uses the token's bound identity, so no priorGET /v1/mecall is needed.Sequential over parallel chunk writes preserves source order and lets a mid-batch failure surface which chunks landed via
PartialPlaylistWriteError. Default visibility ispublic: false, deliberately flipped from Spotify's API default since the product has no public-shuffle UI yet.Closes #18.