Skip to content
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
sudoesnothing merged 1 commit into
mainfrom
feat/create-playlist-module
May 23, 2026
Merged

feat: add create-playlist module for shuffle#56
sudoesnothing merged 1 commit into
mainfrom
feat/create-playlist-module

Conversation

@sudoesnothing

Copy link
Copy Markdown
Contributor

What

New src/lib/spotify/create-playlist.ts library module for the upcoming /api/shuffle route's write path:

  • createShuffledPlaylist(token, sourceName, uris, now?): public entry. Creates an empty private playlist at POST /v1/me/playlists then writes URIs into it in sequential 100-URI batches against POST /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, carrying playlistId, playlistName, chunksAdded, totalChunks, and the underlying cause.
  • 32 vitest cases covering chunking boundaries (0/100/101/199/200/250 URIs), order preservation, partial-write semantics, and boundary error mapping. Suite grows from 82 to 114.

Plus a sibling helper in src/lib/spotify/playlists.ts:

  • fetchPlaylistName(token, playlistId): GET /v1/playlists/{id}?fields=name projection. Lives in the playlist-metadata module alongside fetchUserPlaylists.

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/playlists is the post-Feb-2026 canonical create-playlist 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.

Sequential over parallel chunk writes preserves source order and lets a mid-batch failure surface which chunks landed via PartialPlaylistWriteError. Default visibility is public: false, deliberately flipped from Spotify's API default since the product has no public-shuffle UI yet.

Closes #18.

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.
@sudoesnothing sudoesnothing added the enhancement New feature or request label May 23, 2026
@sudoesnothing sudoesnothing self-assigned this May 23, 2026
@sudoesnothing
sudoesnothing merged commit 41d87bd into main May 23, 2026
1 check passed
@sudoesnothing
sudoesnothing deleted the feat/create-playlist-module branch May 23, 2026 09:20
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>
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Batch add-tracks calls into 100-URI chunks (Spotify API cap)

1 participant