The Guide
From zero to your first set. The toolkit is open source —browse the code on Forgejo.
1 · Install
You need macOS, Spotify Premium with the desktop app, andBun. Enrichment works best withClaude Code, which drives the bundled tango-library skill.
git clone https://git.celilo.computer/celilo/tango-dj.git
cd tango-dj
bun installImporting playlists needs a (free) Spotify developer app: create one atdeveloper.spotify.com, add http://127.0.0.1:8988/callback as a Redirect URI, and export the client id (SPOTIFY_CLIENT_ID, env or.env). The first import opens a browser once for the OAuth grant; the token is cached after that. Playing mode never touches the API at all.
2 · Build your library
Point the importer at every playlist you DJ from:
bun run library import <playlist-url-or-id>
bun run library list-playlistslist-playlists is your completeness report. Every playlist you have ever imported is tracked with a status: ok,partial (a fetch died partway — sync finishes it), error, or forbidden — Spotify refuses other owners' playlists to new API apps; import-embedscrapes up to ~100 tracks of those from the public embed page. A flaky connection can never silently cost you tracks: re-runbun run library sync on a good connection and it re-walks everything registered, skipping playlists that haven't changed.
3 · Enrich with Claude
Fresh imports have no metadata yet, and the candidate engine ignores unenriched tracks — this step is what turns a pile of songs into a library. Open Claude Code in the repo and ask it to use thetango-library skill. It works in batches of ~10: scoring each track it knows on the five dimensions, assigning one of eight categories, and attaching a one-line rationale as a note. The cardinal rule is flag, don't guess — anything it can't confidently place is flagged for your review instead of silently invented.
golden-age-salon- classic orchestra salon tango, 4/4 or 2/4
golden-age-milonga- Golden Age milonga rhythm (habanera pulse)
vals- tango vals — 3/4, one circular sweep per bar
six-eight- non-tango 6/8 or 3/4 that dancers treat like a vals
electro-tango- Gotan/Bajofondo lineage
neotango- modern acoustic tango composition
pop- pop/indie/blues danceable as tango
house- electronic/house-derived
Then close the loop yourself:
bun run library list --needs-review # what Claude flagged
bun run library confirm <uri> energy 4 # your word is final — never overwritten
bun run library exclude <uri> # not danceable? gone from candidates
bun run library note <uri> dj "great opener"Corrections made with confirm are sticky forever: no re-import or re-enrichment ever touches them. Exclusions survive re-imports too.
4 · Go offline-proof
A venue with bad wifi can kill a streaming set. Spotify plays downloaded tracks offline, but only per playlist — so mirror the whole library into one:
bun run library sync
bun run library exportexport creates (or re-diffs) a private playlist calledIncarnadine DJ Library holding every non-excluded track. Open it in the Spotify app, switch Download on, and wait for the green arrow — no API exposes download state, so that last check is yours. Re-run export after any import or exclusion; additions download automatically.
5 · Set up playing mode
Three one-time steps, all before an event, never at one:
In Spotify's settings, disable Autoplay andCrossfade — the app plays each song to its natural end, holds a few seconds of deliberate silence, then fires the next. Then trigger the macOS automation prompt once and approve it:
osascript -e 'tell application "Spotify" to player state'To DJ:
bun start # runs under caffeinate so the Mac won't sleep mid-setOpen localhost:3000, start the Spotify desktop app, pick an opener, and go. Hide Spotify with ⌘H — the console re-hides it after every track change. If nothing is queued when a song ends, playback stops with a loud warning; the app will never let Spotify wander into a track you didn't choose.
6 · Your first set: calibrate
The candidate engine's structure is tested; its numbers are starting guesses. Your first real set is the calibration pass — don't tune mid-set, just capture observations with the note field on the now-playing card, prefixed calib: (one keystroke away, timestamped, tied to the track that prompted the thought):
calib: all 6 cards felt like clones of this one
calib: wanted a cool-down here, nothing offered
calib: played this last week and it's back alreadyThe morning after, pull your notes back out and map each one to a knob in the WEIGHTS block at the top ofsrc/candidates/engine.ts. Change few knobs at a time, in steps of 25–50%, and leave a dated comment on each — the block is designed to be revisited after every set until it sounds like you.
| You observed | Knob(s) |
|---|---|
| Cards are near-clones of the current song | IDEAL_DELTA ↑, NEAR_DUP ↑ |
| Cards lurch too far, transitions jar | IDEAL_DELTA ↓, DELTA_MISS ↑ |
| Tempo moves feel out of scale | BPM_SCALE (↓ = bpm counts for more) |
| Same artist keeps resurfacing | ARTIST_REPEAT ↑ |
| Last week's set is back already | PAST_SET_RECENCY ↑, RECENCY_HALF_LIFE_DAYS ↑ |
| Favorites never resurface | PAST_SET_RECENCY ↓, RECENCY_HALF_LIFE_DAYS ↓ |
| No cool-down offered after a climb | RELEASE_VALVE ↑, CLIMB_LENGTH ↓ |
| Cool-downs offered mid-build | CLIMB_LENGTH ↑, RELEASE_VALVE ↓ |
| Set drifted all-alt or all-trad uncorrected | TRAD_DRIFT ↑, TRAD_DRIFT_THRESHOLD ↓ |
| Engine fights your deliberate arc | TRAD_DRIFT ↓, TRAD_DRIFT_THRESHOLD ↑ |
| Boost/bury ratings barely matter, or drown all else | RATING_BOOST |
| Too few / too many candidate cards | MIN_CANDIDATES / MAX_CANDIDATES |
One distinction worth internalizing: if a bad suggestion traces to one track's values rather than the weights (energy marked 8, plays like a 4), that's a confirm fix, not calibration.