- TypeScript 73.6%
- CSS 14%
- Astro 12%
- HTML 0.2%
- JavaScript 0.2%
| .claude | ||
| .cursor/rules | ||
| .forgejo/workflows | ||
| docs | ||
| openspec | ||
| scripts | ||
| src | ||
| website | ||
| .gitignore | ||
| bun.lock | ||
| CLAUDE.md | ||
| globals.d.ts | ||
| lasertag.d.ts | ||
| mise.toml | ||
| package.json | ||
| README.md | ||
| tsconfig.json | ||
incarnadine
An alt-tango DJ app for macOS. Two modes:
- Building mode — a Claude skill (
.claude/skills/tango-library/) imports your Spotify playlists into a local SQLite library and enriches each track with tango metadata (traditionality, energy, romance, juiciness, BPM) for your review. - Playing mode — a local web console that drives the Spotify desktop app via AppleScript: pick from scored candidate cards, manage the queue, auto-advance with a hard gap (never a crossfade), capture notes, watch set stats.
Requires: macOS, Spotify Premium, the Spotify desktop app, Bun.
Setup
bun install
1. Spotify app credentials (building mode only)
Playing mode needs no Spotify API auth. To import playlists you need a Spotify app:
- Create an app at https://developer.spotify.com/dashboard
- Add
http://127.0.0.1:8988/callbackas a Redirect URI - Export the client id:
export SPOTIFY_CLIENT_ID=...(or put it in.env)
The first bun run library import ... opens a browser for a one-time OAuth
(PKCE) grant; the token is cached in .spotify-token.json.
2. Spotify desktop app settings
In Spotify → Settings:
- Disable Autoplay ("Autoplay similar songs when your music ends") — otherwise Spotify may start an unrelated track in the gap before this app advances.
- Disable Crossfade — hard gaps between songs are the point. Each song plays
to its end, then the app holds a few seconds of silence (
POST_TRACK_GAP_Sinsrc/playback/engine.ts) before firing the next one.
3. First-run AppleScript permission
The first time the server sends a command to Spotify, macOS prompts to allow Terminal (or whatever runs Bun) to control Spotify. Do this before an event:
osascript -e 'tell application "Spotify" to player state'
Approve the automation prompt (System Settings → Privacy & Security → Automation if you need to fix it later).
4. Run a set
bun start # runs under caffeinate so the Mac won't sleep mid-set
Open http://localhost:3000, start the Spotify desktop app, and DJ.
(Port taken? PORT=3210 bun start. Different library file? TANGO_DB=path.)
Spotify grabs focus when a new track starts; the app gives focus back automatically. To keep Spotify off-screen entirely, hide it with ⌘H (not minimize) — the app re-hides it after every track change.
Building the library
Ask Claude to use the tango-library skill, or run the helper directly:
bun run library import <playlist-url-or-id> # import a playlist
bun run library sync # re-walk everything imported
bun run library list-playlists # completeness report
bun run library export # mirror library → one playlist
bun run library list --needs-review # tracks awaiting review
bun run library set <uri> energy 7 # set a value (Claude enrichment)
bun run library confirm <uri> energy 4 # DJ correction — never overwritten
bun run library note <uri> claude "text" # attach a note
Before an event: offline readiness
A venue with bad internet can kill a set. Spotify Premium plays downloaded tracks offline, but only per playlist — so mirror the whole library into one playlist and download that:
bun run library sync # re-walk every playlist; recovers anything a flaky
# connection left half-imported
bun run library export # mirror every non-excluded track into one playlist
export creates (or updates) a private playlist called Incarnadine DJ
Library and prints its link. In the Spotify app, open it and switch
Download on, then wait for the green arrow — Spotify exposes no API or
AppleScript for download state, so that check is yours to make.
Re-run export after importing or excluding tracks; it re-diffs every time,
so the playlist always matches the library. That also means hand edits made
in Spotify get reverted on the next run.
To see whether anything is missing:
bun run library list-playlists
Every playlist you've imported is listed with a status: ok, partial (a
fetch died partway — sync will finish it), error, or forbidden (Spotify
refuses other owners' playlists to new apps; import-embed gets ~100 tracks
of those, and sync skips them unless you pass --force).
Tests
bun test