Beatport (crate matching, purchase planning, set prep)
Turn tracks earmarked in Spotify/YouTube into a costed Beatport buy list, then into a harmonically-ordered folder Ableton can chew on.
This skill never buys anything. It produces a reviewed plan and a list of URLs; Idan completes checkout in his browser.
Hard rules (do not skip)
- No purchases, ever. There is no
buycommand and you must not add one, drive a checkout with browser automation, or enter card details.planends the automated part of the job. Hand Idan the list, the total, and the URLs. - Never handle the Beatport password.
POST /v4/auth/login/accepts username+password and it works — do not use it. Auth is a bearer token Idan pastes, or PKCE. If a login is needed, ask him to do it in his browser. - Show the total before he opens the cart.
planprints per-track prices and an estimated total. Beatport returns prices in different units across endpoints, so the plan reports bothestimated_totalandestimated_total_raw_sum— if they disagree, say the total is unverified rather than quoting a number confidently. - Never auto-buy an ambiguous match.
matchsorts results into three piles;needs_review_before_buyingmeans the matcher found a plausible-but-uncertain track. Show those to Idan with the alternatives and let him pick. Buying the wrong remix is the expensive, annoying mistake this skill exists to prevent. - Check
already_ownedfirst. Runpurchases/librarybefore planning a buy so he isn't re-buying tracks. Beatport will happily sell the same track twice. - Don't invent endpoints. The
/my/*surface is not publicly documented. Useprobeandspecto find what actually works on his account, thengetthat path. If a listing command fails, discover — don't guess a path and report its 404 as "no data".
How Beatport is actually reachable (verified 2026-08-27)
Two planes, and mixing them up is the main way this goes wrong:
api.beatport.com/v4— scriptable. Answers clean JSON, returns401 {"detail":"Authentication credentials were not provided."}unauthenticated. No Cloudflare challenge. Everything this skill does goes here.www.beatport.com— not scriptable. Returns a Cloudflare "Just a moment..." interstitial to plain HTTP clients, including/api/auth/session. Anything onwww.(cart, checkout, the downloads page) needs a real browser — Idan's, orplaywright-skillwith a persistent session. Do not try to curl it and do not report the Cloudflare page as "Beatport is down".
Other verified facts:
GET /v4/auth/o/authorize/302s to/v4/auth/login/when the browser has no Beatport session — so PKCE only completes if Idan is already logged in.POST /v4/auth/o/token/returns{"error":"invalid_client"}for an unregisteredclient_id. PKCE therefore needs a real registered client; without one, use a pasted bearer token.GET /v4/swagger-ui/json/is the OpenAPI spec and it is itself auth-gated. Once authenticated it is the authoritative endpoint list — that is whatspecfetches.
Setup
Two modes. Start with (a); it works today with no partner approval.
(a) Pasted bearer token — Idan opens beatport.com logged in, DevTools > Network, clicks
any api.beatport.com request, copies the Authorization header value after Bearer :
python3 ~/.claude/skills/beatport-skill/beatport_skill.py setup --token 'eyJ...'
Tokens are short-lived. On a 401 the skill tells him exactly how to grab a fresh one — ask, don't retry in a loop.
(b) Registered API client (Beatport partner portal), which gets auto-refresh:
python3 ~/.claude/skills/beatport-skill/beatport_skill.py setup --client-id YOUR_ID
python3 ~/.claude/skills/beatport-skill/beatport_skill.py login
Then confirm and discover the account's real endpoints:
python3 ~/.claude/skills/beatport-skill/beatport_skill.py auth-status
python3 ~/.claude/skills/beatport-skill/beatport_skill.py probe
python3 ~/.claude/skills/beatport-skill/beatport_skill.py spec --grep my
probe tries a list of candidate /my/* paths, records which return 200 in cache.json,
and the listing commands then use only the paths known to work. Run it once per account
before the first library/purchases/downloads call.
Commands
Discovery and raw access
beatport_skill.py auth-status # is the token good, whose account
beatport_skill.py probe # which /my/ endpoints work (caches result)
beatport_skill.py spec [--grep my] # authoritative OpenAPI path list
beatport_skill.py get /my/downloads/ --param per_page=10 # raw authenticated GET
Catalog
beatport_skill.py search "artist track" [--limit 20] [--type tracks|releases|artists]
beatport_skill.py track TRACK_ID # BPM, key, Camelot, label, price, URL
Your account
beatport_skill.py library [--all] # saved / My Beatport tracks
beatport_skill.py purchases [--all] # what you already bought
beatport_skill.py downloads [--all] # download entitlements
Crate workflow
beatport_skill.py match CRATE.json [--min-score 0.72] [--refresh]
beatport_skill.py plan CRATE.json # buy list + total, NO purchase
beatport_skill.py report CRATE.json [--target-bpm 128] [--bpm-tolerance 8]
beatport_skill.py organize CRATE.json --dir ~/Music/friday-set
beatport_skill.py camelot "A Minor"
The Friday-set pipeline
Spotify liked / playlist ─┐
├─> crate.json ─> match ─> plan ─> [Idan buys] ─> download
YouTube liked / playlist ─┘ │ │
report organize
(set order) (files + m3u8)
Export what he earmarked (see
spotify-skill/youtube-skill):python3 ~/.claude/skills/spotify-skill/spotify_skill.py export-crate --name friday --playlist "<url>" --all python3 ~/.claude/skills/youtube-skill/youtube_skill.py export-crate --name friday-yt --allCrates land in
~/.claude/skills/beatport-skill/crates/.Check what he already owns —
purchases --all— so step 4 doesn't re-buy.Match to Beatport:
match crates/friday.json. Matching is ISRC-first (exact, from Spotify) then fuzzy on artist+title+mix. It writes results back into the crate.Plan, then hand off:
plan crates/friday.json. Show Idanto_buywith prices and the total,needs_review_before_buyingwith alternatives, andunmatched_on_beatport. Stop there. He opens the URLs and checks out.After he downloads (Beatport delivers ZIPs — unzip into one folder):
beatport_skill.py organize crates/friday.json --dir ~/Music/friday-setMatches files to crate entries, reports anything missing or unexpected, and writes an
.m3u8in harmonic set order.Set order:
report crates/friday.json --target-bpm 128. Gives a greedy Camelot walk (compatible key, smallest BPM step) plus arough_transitionslist of the joins that need work. It is a starting point to hand-tune, not a finished set.
Ableton
Drag the folder (or the .m3u8) into Ableton's browser. Set the project tempo from
report's bpm_range. Warp each clip. Beatport's BPM/key are metadata from the label —
trust them for planning the order, verify by ear before the set.
Crate format (v1)
One shared shape written by all three skills:
{
"crate_version": 1,
"name": "friday",
"origin": "spotify:playlist:37i9...",
"tracks": [
{
"source": "spotify", "source_id": "...", "url": "...",
"artist": "Bicep", "title": "Glue", "mix": "",
"isrc": "GBCFB1700123", "duration_ms": 330000,
"bpm": null, "key": null,
"beatport": {
"beatport_id": 11111, "artist": "Bicep", "title": "Glue",
"mix": "Original Mix", "label": "Ninja Tune",
"bpm": 128, "key": "A Minor", "camelot": "8A",
"price": "$2.49", "price_value": 2.49,
"url": "https://www.beatport.com/track/glue/11111",
"match_method": "isrc", "match_score": 1.0, "purchased": false
}
}
]
}
beatport: null— nothing found; checkbeatport_candidateson the track.match_method:isrc(exact) ·fuzzy(confident) ·fuzzy-ambiguous(review).- Mark
purchased: trueafter a buy so later runs treat it as owned.
Matching notes
- ISRC is the good path. Spotify exposes it, Beatport carries it, and it makes matching exact. YouTube has no ISRC, so YouTube crates are fuzzy-only — review them harder.
- The matcher deliberately rejects unrequested remixes. "Glue" will not silently match "Glue (Chaos In The CBD Remix)"; a remix only matches if the crate side names the remixer. This is asymmetric on purpose — a wrong remix is a wasted purchase and a wrong track in the set.
--min-scoredefaults to 0.72, validated against a labelled set intest_matching.py. Lower it to surface more candidates for review, not to auto-buy more.- Not everything is on Beatport. Bootlegs, edits, and non-dance releases often aren't.
unmatched_on_beatportis a normal outcome, not a failure — those need another source.
Tests
python3 ~/.claude/skills/beatport-skill/test_matching.py
40 checks over the Camelot wheel (all 24 keys, enharmonics, wraparound) and the matcher
(remix discrimination, accents, feat., apostrophes, false positives). No network or
credentials needed. Run it after touching score_match, to_camelot, or
harmonic_order.
Notes
- Standard library only — no pip installs.
config.json,tokens/,cache.jsonare gitignored.crates/is user data, also gitignored.- Rate limiting:
matchsleeps--delay(0.25s) between lookups. A 100-track crate is a couple of hundred requests — don't hammer it.