Apple Music Playlist Toolkit
English | 简体中文 | 繁體中文 | 日本語 | 한국어 | Español | Português do Brasil | Deutsch | Français
Describe the playlist you want. Your MCP agent curates it, verifies every track in Apple Music, previews the match, and creates it for you.
Pure Python standard library — no pip install required to run, and no Apple Developer Program
membership needed. Requires Python 3.10+ and works on Windows / macOS / Linux.
The primary interface is the local am-mcp stdio server. The language model already running in
your MCP client interprets the brief and chooses candidates; this project searches the Apple Music
catalog, resolves exact tracks, and performs account operations. There is no bundled model, LLM
API key, artist list, or fixed theme. The CLI remains available for login, diagnostics, scripting,
audits, and advanced sequencing.
describe → curate → catalog-check → dry-run → create
Quick start
Recommended — install the MCP stdio service:
pip install git+https://github.com/Z-Han-Z/apple-music-playlists.git
am-playlist status
am-playlist login # one-time Apple ID sign-in
Register am-mcp in the client. The common configuration shape is:
{ "mcpServers": { "applemusic": { "command": "am-mcp",
"env": { "PYTHONIOENCODING": "utf-8" } } } }
Then describe the result, not the implementation:
Create a 25-track late-night driving playlist: atmospheric alternative R&B and electronic, mostly from the last ten years, no live versions, with a calm landing.
Clients with MCP Prompt support can select create_playlist_from_description. In every other
client, send the same request in chat: the server instructions and typed tools expose the same
status → candidate pool → catalog grounding → direct comparison → dry-run → create workflow.
From a clone (nothing to install):
git clone https://github.com/Z-Han-Z/apple-music-playlists.git
cd apple-music-playlists
python am_playlist.py status # auto-fetches the developer token
python am_playlist.py login # one-time Apple ID sign-in (~6 months validity)
python am_mcp_server.py # register this absolute script path in the MCP client
Installation also puts the CLI and MCP commands on your PATH:
am-playlist status # the CLI
am-playlist login # one-time Apple ID sign-in
am-mcp # the MCP stdio server
For development, pip install -e . from a clone makes edits take effect without reinstalling.
See SETUP.en.md for the credential walkthrough (three ways to get the user token, including a zero-dependency one).
What's in the box
| File | Purpose |
|---|---|
am_playlist.py |
Core: token management, catalog search, create / edit / delete playlists, track resolution |
am_mcp_server.py |
Primary MCP stdio service: one description-to-playlist prompt plus 13 tools |
playlist_audit.py |
Metadata audit: length, artist concentration, genres, eras, durations, duplicates, interludes |
playlist_flow.py |
Audio-feature audit: BPM / key / loudness / energy / valence, adjacency checks, arc shape |
playlist_optimize.py |
Simulated-annealing track ordering against the measured rules |
listening_stats.py |
Listening history: recently played, and per-track/album/artist play counts (Apple Music Replay backend) |
profile_library.py |
Taste profile: what your most-played music actually sounds like (BPM / energy / valence spread, mood quadrants) |
am_library.py |
Your library: paged export of every catalog-backed song you own, enriched with ISRC / year / genre |
build_pool.py |
Candidate pool: pick a pool out of your library (most-played → favourite artists → variety filler) and fetch its features |
Supporting modules:
| File | Purpose |
|---|---|
playlist_core.py |
Platform-neutral core: Camelot, BPM folding, the four adjacency rules, the six narrative shapes. Imports nothing from this project and nothing third-party |
am_paths.py |
Platform-neutral paths and version — where config and cache live |
am_meta.py |
The single catalog_meta implementation (batched catalog lookups) |
All three analysis modules are importable as libraries:
import playlist_flow, playlist_audit, playlist_optimize
print(playlist_flow.flow_report("My Playlist")) # -> str
print(playlist_audit.audit_report("My Playlist")) # -> str
ids, report = playlist_optimize.optimize("stack.json") # -> (list[str], str)
MCP tools
The standard prompt create_playlist_from_description asks for a natural-language brief and
optional name, track count, and response language. Curation stays in the host model; the tools are
the grounded Apple Music execution layer:
am_status · am_search_songs · am_resolve_candidates · am_list_playlists · am_show_playlist ·
am_create_playlist · am_add_tracks · am_delete_playlist ·
am_audit_playlist · am_analyze_flow · am_optimize_order ·
am_recently_played · am_top_played
am_resolve_candidates grounds a generous LLM-proposed pool in real catalog metadata, flags
duplicates and suspicious versions, and deliberately does not score theme fit. The host model
compares candidates directly with the user's words and explains their playlist roles.
am_analyze_flow diagnoses transitions; am_optimize_order can optionally refine ordering inside
already chosen narrative blocks. It never decides which songs belong in the playlist.
Mount it in a Cordis agent preset with the template in preset/, or wire it into
any other MCP client with:
{ "mcpServers": { "applemusic": {
"command": "python", "args": ["/abs/path/am_mcp_server.py"] } } }
Complete tested examples for Codex, Claude, Cursor, VS Code/Copilot, Gemini CLI, Windsurf, Docker, Cordis/DSH, and Harness are in docs/client-setup.md.
Build the non-root local container with:
docker build -t apple-music-playlists:1.3.0 .
The client must run it attached with docker run --rm -i; mount only the app config directory and
a writable cache as shown in the client guide. Config stays writable so token refresh can persist.
Never bake Apple credentials into the image.
Tests
python -m unittest discover -s tests -v
The suite is entirely offline — no network, no credentials. Two kinds:
- Behaviour: the pure math that decides what "sounds good" — Camelot mapping, BPM folding, arc classification, each adjacency penalty asserted in isolation, annealing determinism and the block-order constraint. All six narrative archetypes must classify as themselves.
- Structural regressions, each pinned to a bug that actually shipped: no hardcoded catalog
region, exactly one
catalog_meta, no non-None--storefrontdefault, cache outside the repo, every MCP tool wired to a handler, and the optimizer free of platform imports.
They earn their keep immediately — the suite caught a syntax error in a file written minutes earlier, before it was ever run.
The interesting part: audio features
Apple's catalog API exposes no audio features at all — no tempo, key, loudness, energy, or
valence. Spotify's audio-features endpoint was shut off for new apps on 2024-11-27, and
AcousticBrainz retired in 2022.
playlist_flow.py bridges the gap with a free, key-less chain built on ISRC, which Apple does
return:
Apple Music track ──► ISRC
│
├─► api.reccobeats.com/v1/track?ids=<ISRC> → track UUID
│ └─► /v1/audio-features?ids=<UUID>
│ → tempo, key, mode, loudness, energy, valence,
│ danceability, acousticness, instrumentalness,
│ liveness, speechiness
└─ (fallback) musicbrainz.org ISRC lookup
Results are cached locally, so the network cost is paid once per playlist.
Coverage is reported, never silently dropped. Every consumer prints a funnel saying why each track could not be measured:
音频特征覆盖:1395/1581 可用(88%)
· 186 首 没有 ISRC —— 特征链的硬边界,换特征源也解决不了
· 1395 首 不在特征缓存里(这批还没抓过)
That distinction is the point. No ISRC means the chain cannot start at all — a different feature source will not help. Not in the source means the ISRC is fine and switching sources (or analysing the audio locally) would fix it. Collapsing both into "missing features" discards the only information that tells you what to do next.
It matters more than it looks: tempo / key / energy / valence are the only things the
adjacency rules and the arc can act on, so coverage is the ceiling on how good an ordering can
be. At 60% coverage, four positions in ten were never evaluated — while the cost number still
looks excellent. The optimizer therefore prints coverage above its cost lines, and warns below 90%.
Sequencing rules the optimizer enforces
Derived from the research collected in docs/ — including a PLOS ONE study in which
130 music professionals sequenced albums, and a randomized trial on mood-adaptive music ordering.
Hard adjacency rules
- No two slow tracks adjacent
- Avoid "only slightly slower" transitions (0–12% drop makes the slower track feel like it drags)
- Adjacent tracks must not be similar in both tempo and key
- No unjustified large BPM jumps (>40%); no jarring energy shifts under incompatible keys
These four have exactly one definition, in playlist_core.check_pair(), and both the audit and
the optimizer call it. They used to be implemented twice, and the copies disagreed: the audit
called a track "slow" below the tempo's 25th percentile while the optimizer used a fixed 100 BPM,
and the audit never checked the BPM-jump rule at all. That is a tool diagnosing against one
standard and repairing against another — so the count it reported could not be trusted.
Note on the "slow" threshold. It is absolute (100 BPM), not data-driven, deliberately: the optimizer evaluates the same sequence thousands of times while annealing, and a percentile threshold would drift as the permutation changes, so the cost would never settle. The cost is that a uniformly slow playlist flags every adjacent pair — that is real, not a sequencing failure, and the report says so.
Global arc — you choose the target shape:
python playlist_optimize.py stack.json --arc cinderella
python playlist_optimize.py --list-shapes
| Axis | Target |
|---|---|
valence, energy, loudness |
the chosen narrative archetype |
tempo |
inverted U — fast in the middle |
Six shapes: rags-to-riches, tragedy, man-in-a-hole (default), icarus, cinderella,
oedipus. The target curve and the shape the audit classifies come from the same table in
playlist_core.ARCHETYPES, so "what shape is this" and "what shape am I aiming for" cannot drift
apart.
Tempo deliberately does not follow the chosen shape. The archetypes describe an emotional trajectory (valence / arousal); "put the fast ones in the middle" is a sequencing convention. Making tempo follow Cinderella too would conflate two independent principles.
Measuring this on a real arc playlist is what justified wiring it up: under man-in-a-hole — the
shape the optimizer used to hardcode — that playlist's opening 30 tracks score an arc cost of
2.95, the worst of the six. The same tracks score 1.19 under cinderella. The tool had
been aiming at the one shape that fit least.
The optimizer preserves your grouping (movements / eras / moods) and only reorders within groups, so thematic structure survives the loudness tuning. Drop the grouping and it reorders freely — measurably "smoother", at the cost of your narrative.
Example from a real run: cost 104.46 → 19.41 with grouping preserved, → 1.28 ungrouped.
Gotchas worth knowing before you debug
- Responses can be gzip-compressed even when you never sent
Accept-Encoding. Decoding the raw bytes as UTF-8 yields garbage that looks like an empty body. (This is fixed inam_playlist.py.) DELETEonly works onamp-api.music.apple.com. The documented hostapi.music.apple.comreturns 401 for playlist and library-song deletion.- Do not send
x-apple-client-versionto amp-api — it turns into a 500. - Creating a playlist also adds its tracks to the library. Deleting the playlist does not remove them.
- BPM estimates have octave ambiguity (90 vs 180 for the same track). Fold into
[70,160)before comparing, or "two slow tracks adjacent" over-reports by ~4×. - Never pass
"Title - Artist"straight into catalog search — you get live/remastered takes. Search on the space-separated form and score versions afterwards. - An artist missing from a storefront's search ≠ the song is unavailable there. Look it up by ISRC.
- Only the client that created a playlist can modify it — Apple-side restriction.
More in docs/apple-music-api-notes.md and
skill/reference.md.
Documentation
| Doc | Contents |
|---|---|
SETUP.en.md |
Credentials: what tokens exist, how to get each one, security notes, troubleshooting |
docs/client-setup.md |
Client-specific MCP, Docker, Cordis/DSH, generic harness, and Harness Platform setup |
docs/apple-music-api-notes.md |
Token model, endpoint contracts, measured API behaviour, eval of 7 automation approaches |
docs/how-to-build-a-good-playlist.md |
Curation methodology: adjacency physics, arc data, six narrative shapes, the ISO principle |
docs/playlist-curation-survey.md |
Survey of published curation guidance (platform rules, DJ methods, academic findings) |
docs/evaluation-signals.md |
LLM-native curation: direct candidate comparison, catalog grounding, readable constraints, and why scalar theme scores stay out of the critical path |
docs/platform-adapters.md |
The platform-adapter boundary: what is platform-neutral, what an adapter must provide, and what breaks on a service that exposes no ISRC |
skill/ |
Agent skill: workflow + the accumulated gotcha list |
CHANGELOG.md |
Release history, including behaviour changes between versions |
preset/ |
Cordis agent preset template that mounts the MCP server |
For the complete Simplified Chinese guide, see README_ZH_CN.md.
License
MIT — see LICENSE.
Unofficial community tooling. Not affiliated with or endorsed by Apple. Uses your own Apple Music account for personal use; follow Apple's terms of service.
Release files for apple-music-playlists 1.3.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| apple_music_playlists-1.3.0.tar.gz | 105.8 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| apple_music_playlists-1.3.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 189.0 kB
Release files / apple_music_playlists-1.3.0.tar.gz
| Download URL | apple_music_playlists-1.3.0.tar.gz |
|---|---|
| Size | 105.8 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
396a2b583a4c177d4ad8b812101565a77768096c0f9831dec21c4d9e0faced8d
|
|
BLAKE2b-256 checksum How to use checksums |
004123fd7ff5774edea16b75f5a9b9fcaf5f80c3c5acbbb9f48fbf44b2dc4831
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 23, 2026.
Transparency logRelease files / apple_music_playlists-1.3.0-py3-none-any.whl
| Download URL | apple_music_playlists-1.3.0-py3-none-any.whl |
|---|---|
| Size | 83.2 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
a89a9a3cda740aa4e1fc02cfe24410182f37d8efe874464a80c94d318f81f189
|
|
BLAKE2b-256 checksum How to use checksums |
3ccb0126970d1b3e4d01fdcb342440eb4d766b7621cf8d9fc50ab9f4531e2206
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 23, 2026.
Transparency log