Alt-tango DJ app for macOS: Spotify-driven playing console + Claude-assisted library builder. Ships the dj-incarnadine.org site as a celilo module (website/celilo-module/).
  • TypeScript 73.6%
  • CSS 14%
  • Astro 12%
  • HTML 0.2%
  • JavaScript 0.2%
Find a file
Peter Banka 7be9fff6e8
All checks were successful
release / version (push) Successful in 0s
release / publish (push) Successful in 6s
Merge pull request 'chore: version dj-incarnadine-org' (#5) from changeset-release/main into main
2026-08-06 06:30:07 +00:00
.claude Fold import/sync/export lifecycle into tango-library skill 2026-08-04 21:47:20 -07:00
.cursor/rules Foundation: scaffold, Drizzle schema on bun:sqlite, db module + tests 2026-08-02 09:39:32 -07:00
.forgejo/workflows release: tolerate PR-create 404 on private repos 2026-08-05 12:07:50 -07:00
docs Add UAT doc for task 6.2 weight calibration 2026-08-05 10:45:35 -07:00
openspec Merge pull request 'Implement add-tango-dj-app + sync-and-offline-readiness' (#1) from implement-tango-dj-app into main 2026-08-05 14:51:33 +00:00
scripts Export uses the endpoints Spotify actually serves new apps (task 3.4 live) 2026-08-04 21:39:43 -07:00
src Pause just before a track ends, so the album never bleeds through 2026-08-04 22:20:06 -07:00
website chore: version dj-incarnadine-org 2026-08-06 06:28:06 +00:00
.gitignore Ignore generated .astro directory 2026-08-05 11:17:24 -07:00
bun.lock Foundation: scaffold, Drizzle schema on bun:sqlite, db module + tests 2026-08-02 09:39:32 -07:00
CLAUDE.md Focus-preserving playback, candidate filters/directions, DJ ratings, embed import 2026-08-02 10:58:15 -07:00
globals.d.ts Server + DJ console: tRPC router, lasertag-styled React console 2026-08-02 09:57:10 -07:00
lasertag.d.ts Server + DJ console: tRPC router, lasertag-styled React console 2026-08-02 09:57:10 -07:00
mise.toml Wire website release CI/CD (changesets -> celilo-registry) 2026-08-05 11:47:45 -07:00
package.json Rename the app to incarnadine, give it a fire favicon, wrap text in the set view 2026-08-03 17:20:15 -07:00
README.md Playlist registry, sync and offline export (tasks 1.1-3.3) 2026-08-04 21:30:47 -07:00
tsconfig.json Library builder: PKCE OAuth helper script, enrichment subcommands, Claude skill, e2e test 2026-08-02 09:41:32 -07:00

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:

  1. Create an app at https://developer.spotify.com/dashboard
  2. Add http://127.0.0.1:8988/callback as a Redirect URI
  3. 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_S in src/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