A local-only, bring-your-own-app toolkit for applying a human-authored Spotify playlist migration with protected-playlist, stale-state, approval, and verification guards.
The repository contains no shared Spotify account, client ID, hosted service, personal library data, or reusable authorization. Every user creates and controls their own Spotify developer app, OAuth grant, local files, and API quota.
Do not give a Spotify inventory, track/artist metadata, playlist contents, or a migration plan derived from that content to an AI model. Spotify's current Developer Policy prohibits analyzing Spotify Content and prohibits ingesting Spotify Content into an AI model. Its Building with AI guidance permits AI assistance with code and documentation, not content ingestion.
An agent may help install this repository, run its guarded commands, and report
content-free counts and pass/fail receipts. It must not open, print, summarize,
classify, or upload .spotify-organizer/*.json. The human account owner authors
and reviews the plan. The original idea—having an agent deeply classify a
Spotify library by genre, language, mood, or similarity—requires written
Spotify approval before it can be offered or distributed.
This project is a technical implementation, not a statement of legal approval. Spotify's rules can change; re-check them before distribution. Each person who operates a fork is responsible for their own app registration, disclosures, and compliance; see the local-data statement in PRIVACY.md.
A shared client ID is not a public distribution model. Spotify Development Mode currently ties access and quota to the developer account, requires the app owner to meet its Premium requirement, and limits which users can authenticate. See Spotify's current quota-mode documentation and July 2026 quota update.
The client ID is not a password, but reusing one would still couple everyone to one owner's allowlist, subscription eligibility, quota, and policy status. This tool therefore uses bring your own Spotify app and PKCE; it never asks for a client secret.
- Reads Liked Song and accessible playlist IDs into a private local snapshot.
- Creates playlists and populates them with the exact ordered track URIs in a human-authored manifest.
- Keeps Liked Songs independent from playlist membership; there is no unlike action.
- Excludes configured immutable playlist IDs from item reads and all writes.
- Verifies exact destinations, unchanged Liked Song IDs, and protected boundaries before approved source playlists can be removed.
It does not stream or download music, enrich artist genres, classify content, make recommendations, expose a public server, or share credentials with an agent.
-
Sign in to the Spotify Developer Dashboard.
-
Create an app using values like these:
- App name: any unique personal name, such as
My Local Playlist Migration. - Description:
Local, personal playlist migration using the Spotify Web API. - Website: your fork's repository URL, if the dashboard asks for one.
- API/SDK selection: Web API only.
- App name: any unique personal name, such as
-
Register this redirect URI exactly (use neither
localhostnor a trailing slash):http://127.0.0.1:8787/auth/callback -
In Settings → Users Management, add the name and Spotify email of every account that will authorize this local tool. Development Mode currently permits up to five authenticated users, and the app owner must have Premium.
-
Copy the client ID. Do not create, paste, or publish a client secret; this project uses Authorization Code with PKCE.
There is no production or "live" callback URL. This toolkit intentionally supports only the loopback callback above and must not be deployed as a hosted service.
Requirements: Node.js 22+, Bun, npm, and an account that satisfies Spotify's current Development Mode requirements.
git clone YOUR_FORK_OR_REPOSITORY_URL
cd spotify-playlist-migration-toolkit
npm ci
npm run setup -- --client-id YOUR_SPOTIFY_CLIENT_ID --protected NONE
npm run preflightReplace NONE with comma-separated immutable playlist IDs when any playlist's
items must never be fetched and the playlist must never be changed:
npm run setup -- --client-id YOUR_SPOTIFY_CLIENT_ID --protected ID1,ID2Setup creates .dev.vars with mode 0600 and random local encryption/admin
keys. It refuses to overwrite an existing file. .dev.vars, local KV state,
inventories, plans, and receipts are gitignored.
Keep the local service running in one terminal:
npm run dev -- --ip 127.0.0.1 --port 8787In a second terminal:
npm run auth:urlOpen the printed accounts.spotify.com URL yourself and approve the requested
scopes. Password, passkey, SSO, consent, and 2FA are human-only. Confirm the
connection afterward:
npm run auth:statusThe refresh token is encrypted before Wrangler's local KV emulator stores it. The encryption key is on the same computer, so this protects against accidental file disclosure—not a malicious process running as the same OS user.
Export a fresh local snapshot:
npm run inventoryThe command writes .spotify-organizer/inventory.json and prints only its path
and counts. Do not give that file to an AI system or commit it. The account owner
may use their own Spotify interface and private local snapshot to author
.spotify-organizer/plan.json following
the manifest contract.
Use examples/ only to understand the schema.
Validate without printing account content:
npm run plan:validate -- \
.spotify-organizer/inventory.json \
.spotify-organizer/plan.jsonThe validator requires every unique track present in Liked Songs or an owned playlist to appear exactly once, rejects followed-only tracks, and allows only explicitly listed owned, non-protected source playlists to be removed.
After the human reviews and approves the exact manifest:
npm run plan:apply -- create \
.spotify-organizer/inventory.json \
.spotify-organizer/plan.json \
.spotify-organizer/state.json
npm run plan:apply -- populate \
.spotify-organizer/inventory.json \
.spotify-organizer/plan.json \
.spotify-organizer/state.jsonExport a new snapshot and verify before any removal:
npm run inventory -- --output .spotify-organizer/post-populate.json
npm run plan:apply -- verify \
.spotify-organizer/inventory.json \
.spotify-organizer/plan.json \
.spotify-organizer/state.json \
.spotify-organizer/post-populate.jsonSource removal is a separate destructive phase. The human must review the
manifest's exact sourcePlaylistIdsToRemove list and approve it separately.
Then run:
npm run plan:apply -- delete \
.spotify-organizer/inventory.json \
.spotify-organizer/plan.json \
.spotify-organizer/state.json \
.spotify-organizer/post-populate.json \
REMOVE_APPROVED_SOURCE_PLAYLISTS
npm run inventory -- --output .spotify-organizer/final.json
npm run plan:apply -- complete \
.spotify-organizer/inventory.json \
.spotify-organizer/plan.json \
.spotify-organizer/state.json \
.spotify-organizer/final.jsonAll live inputs, outputs, state, and receipts are confined to
.spotify-organizer/; path traversal and symlink escapes are rejected.
The included
$organize-spotify-playlists
skill is an operations guide, not a content-analysis skill. Give an agent this
prompt:
Use skills/organize-spotify-playlists/SKILL.md to set up and operate the guarded
local migration workflow. Never open, print, summarize, or upload any file under
.spotify-organizer/. I will author and approve the plan myself.
The agent can run setup checks, start the local server, generate the authorization URL, execute validation/migration commands by path, and report count-only receipts. It must stop for human OAuth, plan approval, and the separate source removal approval.
- Inventory and mutation routes require a bearer token; query-string tokens are rejected.
- The service binds to
127.0.0.1; never expose it through a tunnel or LAN bind. - Protected playlist contents remain
items: nulland are never fetched. - Spotify's playlist-list endpoint may return summaries for protected IDs during create reconciliation; the code checks only the ID and discards every other field before parsing.
user-library-modifyis required by Spotify's currentDELETE /me/libraryendpoint to remove an approved playlist URI. The executor never sends track URIs to that endpoint and has no unlike action.- Writes use input hashes, idempotency markers, ownership checks, and Spotify snapshot preconditions.
- Never commit
.dev.vars,.wrangler/,.spotify-organizer/, or real account exports.npm run publication:checkrejects known private artifacts and any unexpected JSON file. - Never zip or upload the raw working directory, because ignored local files are still physically present. Follow PUBLICATION.md to create a clean-history public copy.
- To disconnect, revoke the app in Spotify account settings, stop the exact local server process, and remove only your own ignored local state.
See SECURITY.md for the complete local trust boundary.
npm run checkThis runs strict TypeScript checks and the test suite. A clean source tree can
add the publication gate with npm run release:check; it intentionally fails if
.dev.vars, .wrangler/, or .spotify-organizer/ physically exists. No deploy
command is included; the supported distribution is local-only.
MIT. See LICENSE.