Skip to content

← dj-incarnadine.org

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 install

Importing 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-playlists

list-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 export

export 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-set

Open 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 already

The 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 observedKnob(s)
Cards are near-clones of the current songIDEAL_DELTA ↑, NEAR_DUP ↑
Cards lurch too far, transitions jarIDEAL_DELTA ↓, DELTA_MISS ↑
Tempo moves feel out of scaleBPM_SCALE (↓ = bpm counts for more)
Same artist keeps resurfacingARTIST_REPEAT ↑
Last week's set is back alreadyPAST_SET_RECENCY ↑, RECENCY_HALF_LIFE_DAYS ↑
Favorites never resurfacePAST_SET_RECENCY ↓, RECENCY_HALF_LIFE_DAYS ↓
No cool-down offered after a climbRELEASE_VALVE ↑, CLIMB_LENGTH ↓
Cool-downs offered mid-buildCLIMB_LENGTH ↑, RELEASE_VALVE ↓
Set drifted all-alt or all-trad uncorrectedTRAD_DRIFT ↑, TRAD_DRIFT_THRESHOLD ↓
Engine fights your deliberate arcTRAD_DRIFT ↓, TRAD_DRIFT_THRESHOLD ↑
Boost/bury ratings barely matter, or drown all elseRATING_BOOST
Too few / too many candidate cardsMIN_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.