← Guides

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:

What you’ll learn

Prerequisites

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.

python
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.

python
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).

python
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.

python
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]
i
No candidate?
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.

python
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”.

python
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:

terminal
$ 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:

Next steps

Full source on GitHub.
A complete, runnable version of this tutorial lives at github.com/tunelab-dev/examples/tree/main/dj-set-builder (coming soon). Pull requests welcome.