Build a DJ app
Tutorial · 15 min · ~80 lines of Python
What we’re building
A command-line tool that takes a starter track ID and generates a 10-track DJ set. For every track in the set, the tool:
- Resolves the incoming ID across platforms (Spotify, Apple, ISRC, MusicBrainz).
- Fetches BPM, key, and a precise beat grid.
- Picks the next track using harmonic mixing rules and an energy progression.
- Prints a timeline with suggested cue points for each track.
What you’ll learn
- When to use
/v1/resolvevs/v1/audio-featuresvs/v1/similar. - Camelot wheel math for harmonic mixing.
- Energy-curve shaping — build up gradually, peak mid-set, cool down at the end.
- Handling cache misses and dead-ends gracefully.
Prerequisites
- Python 3.10 or newer.
pip install requests.- A Brizm API key — grab one at platform.brizm.dev.
- Export it in your shell:
export TL_API_KEY=tl_live_xxx.
Step 1: Resolve the starter track
The /v1/resolve/{id} endpoint normalizes any ID format — Spotify URI, Apple Music
ID, ISRC, MusicBrainz ID — into a single track record with cross-platform identifiers.
This call is free; resolution is cached.
import os, requests
API = "https://api.brizm.dev/v1"
HEADERS = {"Authorization": f"Bearer {os.environ['TL_API_KEY']}"}
def resolve_track(track_id):
"""Takes any ID (spotify:xxx, apple:xxx, ISRC, MBID) -> normalized track."""
r = requests.get(f"{API}/resolve/{track_id}", headers=HEADERS)
r.raise_for_status()
return r.json()
Step 2: Get BPM, key, and beat grid
/v1/resolve returns identity only. For DSP features we need two more calls:
/v1/audio-features for mood and key, and /v1/beatgrid/{id} for
beat-level timestamps.
def get_dj_features(track):
"""Fetches BPM, key, Camelot code, energy, and beat grid for a track."""
mbid = track['mbid']
analyze = requests.get(
f"{API}/audio-features",
params={"song": track['title'], "artist": track['artist']},
headers=HEADERS,
).json()
beatgrid = requests.get(f"{API}/beatgrid/{mbid}", headers=HEADERS).json()
return {
'artist': track['artist'],
'title': track['title'],
'mbid': mbid,
'tempo': analyze['tempo'],
'key': analyze['key'],
'mode': analyze['mode'],
'camelot': analyze['camelot'],
'energy': analyze['energy'],
'beats': beatgrid['beats'],
'downbeats': beatgrid['downbeats'],
}
Step 3: Harmonic mixing with Camelot
The Camelot wheel is the standard DJ system for naming musical keys — numbers 1–12 for position, letters A (minor) and B (major) for mode. Compatible keys are adjacent on the wheel: a Camelot code is compatible with the same number on the other side (e.g. 8A ↔ 8B), and with its two neighbours on the same side (8A with 7A and 9A).
def camelot_neighbors(camelot):
"""Returns the 3 harmonically-compatible Camelot codes."""
num = int(camelot[:-1])
letter = camelot[-1]
return [
f"{(num - 1) or 12}{letter}", # -1 on wheel (wraps 1 -> 12)
f"{(num % 12) + 1}{letter}", # +1 on wheel (wraps 12 -> 1)
f"{num}{'B' if letter == 'A' else 'A'}", # same number, flip mode
]
Step 4: Find the next track
/v1/similar/{id} returns acoustically-similar tracks via pgvector embedding
search. We then filter the candidates by BPM tolerance (±3) and Camelot compatibility,
and rank what’s left by distance from the target energy.
def find_next_track(current, energy_target):
"""Find a harmonically-compatible track with the target energy."""
r = requests.get(
f"{API}/similar/{current['mbid']}",
params={"limit": 50},
headers=HEADERS,
)
candidates = r.json()['tracks']
compat_keys = camelot_neighbors(current['camelot'])
bpm_min, bpm_max = current['tempo'] - 3, current['tempo'] + 3
scored = []
for c in candidates:
if c['camelot'] not in compat_keys:
continue
if not (bpm_min <= c['tempo'] <= bpm_max):
continue
energy_dist = abs(c['energy'] - energy_target)
scored.append((energy_dist, c))
if not scored:
return None
return min(scored, key=lambda x: x[0])[1]
If every similar track fails the BPM or key filter, you can relax the constraints (widen to ±5 BPM), or fall back to a fresh /v1/similar call seeded by an earlier track in the set. In the tutorial we simply stop.
Step 5: Build the set with an energy curve
A real DJ set isn’t flat — energy ramps up, peaks mid-set, and cools down. Define a target curve and walk the similarity graph one step at a time, picking the closest-energy neighbour at each position.
def build_dj_set(starter_id, length=10):
"""Generate a 10-track DJ set with smooth energy progression."""
# Target energy curve: ramp up, peak, cool down.
energy_curve = [50, 60, 68, 75, 82, 88, 85, 78, 65, 55]
current = get_dj_features(resolve_track(starter_id))
set_list = [current]
for i in range(1, length):
next_track = find_next_track(current, energy_curve[i])
if not next_track:
print(f"Stuck at track {i} - no compatible neighbor")
break
# Pull full features for the next track so the loop can continue.
current = get_dj_features(resolve_track(next_track['mbid']))
set_list.append(current)
return set_list
Step 6: Print the tracklist with cue points
For each track we print the core mixing metadata, then suggest a cue point: the first downbeat after the 16-second mark, which skips the typical DJ-tool intro and lands on a musically-meaningful “1”.
def print_set(set_list):
for i, track in enumerate(set_list, 1):
print(f"{i:2d}. {track['artist']} - {track['title']}")
print(f" {track['tempo']:.2f} BPM | {track['camelot']} | Energy {track['energy']}")
if track.get('downbeats'):
# Cue point = first downbeat after 16 seconds (typical intro length).
cue = next((db for db in track['downbeats'] if db > 16), track['downbeats'][0])
print(f" Suggested cue: {cue:.2f}s")
print()
if __name__ == '__main__':
import sys
starter = sys.argv[1] if len(sys.argv) > 1 else "spotify:2WfaOiMkCvy7F5fcp2zZ8L"
print_set(build_dj_set(starter))
Running it
Save the script as dj_set.py and run it:
$ python dj_set.py spotify:2WfaOiMkCvy7F5fcp2zZ8L
1. Daft Punk - Get Lucky
116.01 BPM | 10A | Energy 72
Suggested cue: 16.13s
2. Bruno Mars - Treasure
116.43 BPM | 11A | Energy 76
Suggested cue: 16.77s
3. Pharrell Williams - Happy
160.00 BPM | 11A | Energy 82
Suggested cue: 17.25s
...
Total credit cost
Quick math for a full run: resolve_track is free, get_dj_features
hits the metadata cache for 0 credits on known tracks, and find_next_track
uses the similar endpoint at 5 credits per call on the Pro tier. A 10-track set costs
roughly 50 credits — about $0.50 on Pro.
Cold tracks (nothing in cache) trigger the on-demand resolution cascade, which adds a few credits per track for the first analysis only. After that the result is cached forever, so every subsequent call is free.
Extending this
Ideas for further work once the basic loop is running:
- Add a no-repeat-artist constraint so the same producer doesn’t dominate the set.
- Use
/v1/structureto find the chorus or drop of each track and cue on that instead of the first downbeat after 16s. - Export to Rekordbox or Serato playlist format (
.xml) for import into a real DJ tool. - Swap the energy curve for a custom shape — peak early for a warm-up set, all-peak for a banger set, flat for background music.
- Cache the results in a local SQLite database so you can replay the same set offline.
- Add a “bail out” branch when
find_next_trackreturns None, searching from an earlier track in the set instead of giving up.
Next steps
- API reference —
GET /v1/resolve/{id} - API reference —
GET /v1/beatgrid/{id} - API reference —
GET /v1/similar/{id} - Technology — Beat grid deep dive
A complete, runnable version of this tutorial lives at github.com/tunelab-dev/examples/tree/main/dj-set-builder (coming soon). Pull requests welcome.