Recipe

Turn a DJ set or mix into a tracklist

Build a timestamped tracklist from a recorded DJ set or mix with the AudD enterprise endpoint, reading each match's file-absolute start time to place tracks and surface blends.

view .md audddj set tracklistmix tracklist1001tracklists

A recorded DJ set or continuous mix is one long file with dozens of tracks beat-matched and blended into each other. This recipe sends that file to AudD’s enterprise endpoint and produces a timestamped tracklist — the kind you’d post to a forum or a site like 1001Tracklists, with each track placed at the time it comes in.

What you’ll build

A script that recognizes a mix, reads the matches, and formats each distinct track as HH:MM:SS Artist — Title. You’ll also handle the thing that makes mixes harder than albums: tracks overlap. During a transition the outgoing and incoming track both fingerprint, so the server can match two different songs at the same moment. The recipe shows how to present those blends instead of dropping one side.

Each tracklist line is anchored at the moment its track comes in, and the SDK gives you that moment directly: recognize_enterprise returns a flat list[EnterpriseMatch], and each match carries start_seconds / end_seconds — its position in your mix, in file-absolute seconds. The call requests accurate offsets by default, so those positions are precise; there’s no chunk-offset math to do.

Prerequisites

  • An API token from dashboard.audd.io. Get your own token there; the enterprise endpoint must be enabled on it. label, isrc, and upc come back on enterprise responses (ISRC/UPC require a Startup plan or higher) — useful if you want to enrich the tracklist, but not required for a basic one.
  • Python 3.10+ with the SDK: pip install audd
  • A mix file or URL. https://audd.tech/example.mp3 is a public, reproducible file you can run against end-to-end.

Walkthrough

Step 1: Recognize the mix

Hand the mix to recognize_enterprise as a URL, file bytes, or a path. However long the set runs, you get back a flat list[EnterpriseMatch]: one entry per recognized fragment, in time order, each with a precise start_seconds / end_seconds (accurate offsets are requested by default).

from audd import AudD

audd = AudD("your-api-token")  # token from dashboard.audd.io

# limit caps metered fragments while you get formatting and blends right
matches = audd.recognize_enterprise(
    "https://audd.tech/example.mp3",
    limit=25,
    return_metadata=["apple_music", "spotify"],  # optional streaming links per match
)

for m in matches:
    print(f"{m.start_seconds:.1f}s  {m.artist} — {m.title}  (score {m.score})")

When you run this you’ll see one line per match, in time order. A track that plays for three minutes shows up as a run of consecutive matches naming the same song; transitions show up either as adjacent matches that switch from one song to the next, or — during a crossfade — as two matches sharing the same start_seconds, because the server matched both tracks in the same moment of the mix.

Always set limit during development. The enterprise endpoint bills per 12 seconds of audio processed. A 60-minute mix is hundreds of fragments; an unbounded call meters all of them. Run with limit=25 until your formatting and blend handling are right, then raise the cap for the full mix.

Step 2: Pick the right time field

Timestamps are the whole product here, so be precise about which field is which. Each EnterpriseMatch carries three kinds of position:

  • start_seconds / end_seconds — where this track plays in your mix, in seconds (e.g. 288.0). This is the value you place the track at. It’s None only when a fragment arrived without a usable position.
  • start_offset / end_offset — the raw offsets the seconds are derived from: milliseconds within AudD’s internal 12-second scan fragment, not file seconds. You rarely need them.
  • timecode — position inside the matched track, not your mix. It tells you which part of the song was playing; never use it to place a track.

Place tracks by start_seconds, not timecode. start_seconds is the position-in-your-file; the song’s timecode is a position inside that song’s own recording. Mixing them up puts every track at the wrong time.

Step 3: Format the position into HH:MM:SS

A tracklist wants a clock time, not raw seconds. Format it:

def fmt_hms(total_seconds: float) -> str:
    s = int(total_seconds)
    h, rem = divmod(s, 3600)
    m, sec = divmod(rem, 60)
    return f"{h:02d}:{m:02d}:{sec:02d}"


print(fmt_hms(288.0))  # -> "00:04:48"

This always gives a zero-padded HH:MM:SS, which sorts and aligns cleanly in a tracklist even for sets that run past an hour.

Step 4: Collapse a held track, but keep the transitions

Within a single track you get many consecutive matches naming it. Collapse those to one tracklist entry, anchored at the first match the track appears in. But when the track changes from one match to the next, that’s a real transition, not noise, and each new track earns its own line.

The rule: a new (artist, title) starts a new entry; the same (artist, title) as the entry you’re currently extending does not.

def build_tracklist(matches) -> list[dict]:
    entries = []
    for m in matches:
        if m.start_seconds is None:
            continue  # no usable position for this match — skip it
        same_as_last = (
            entries
            and entries[-1]["artist"] == m.artist
            and entries[-1]["title"] == m.title
        )
        if same_as_last:
            continue  # still the same track playing; first appearance wins
        entries.append({
            "start": m.start_seconds,
            "artist": m.artist,
            "title": m.title,
            "score": m.score,
        })
    return entries


def render(entries: list[dict]) -> str:
    return "\n".join(
        f"{fmt_hms(e['start'])}  {e['artist']} — {e['title']}"
        for e in entries
    )


print(render(build_tracklist(matches)))

Any field on a match can come back absent or None (the SDK parses responses leniently), which is what the start_seconds guard is for: a fragment without a position gets skipped instead of crashing the scan.

This prints a tracklist like:

00:00:00  Tears For Fears — Everybody Wants To Rule The World
00:00:48  Barely Alive — Keyboard Killer
00:04:00  Eptic — Like a Boss Barely (Alive Remix)

Anchoring each track at its first match is what you want for a tracklist: the listed time is when the track comes in, even though it keeps playing across the next several matches.

Step 5: Present blended transitions

In a real mix two tracks share airtime during a crossfade. The server can match both in the same ~12-second moment of the mix, and the SDK returns them as two separate matches in the flat list, sharing the same start_seconds, because they come from the same point in the file. That shared start time is the signal for a blend: the dominant track and the one bleeding in or out are pinned to the same instant. The Step 4 logic keeps only the first track at each new position, so it shows the cleaner cut. If you want a 1001Tracklists-style overlap marker, look for matches that share a start_seconds.

def transitions(matches, score_floor: int = 50) -> list[dict]:
    """Find blends: distinct tracks the server matched at the same moment."""
    # Group matches by their file position; a shared start is one moment in the mix.
    by_start: dict[float, list] = {}
    for m in matches:
        if m.start_seconds is None:
            continue
        by_start.setdefault(round(m.start_seconds, 1), []).append(m)

    out = []
    for start, group in by_start.items():
        if len(group) < 2:
            continue
        a, b = group[0], group[1]
        # A blend: two different tracks at the same moment, both above the floor.
        if a.title != b.title and (b.score or 0) >= score_floor:
            out.append({
                "at": start,
                "out_track": f"{a.artist} — {a.title}",
                "in_track": f"{b.artist} — {b.title}",
            })
    return out

You can render those as a w/ (with) line, the way tracklist sites mark a blend:

00:04:48  Eptic — Like a Boss Barely (Alive Remix)
          w/ Eptic — Like A Boss (Dubstep Sector)

A reasonable confidence floor (here score >= 50) keeps faint, incidental matches out of the transitions. Raise it if your mix is producing spurious “w/” lines; lower it if a known blend isn’t showing up. The (b.score or 0) guard handles a match whose score came back None.

The blend view here and the tracklist view from Step 4 read the same matches from one recognition call — you don’t pay twice. Both fall out of the flat list: the tracklist walks it in order, and a blend is simply the case where two matches land on the same start_seconds.

Step 6: Sample a long set when you don’t need every track

For a quick “what’s the rough tracklist” pass on a multi-hour set, sample instead of recognizing every fragment. Pass the sampling parameters to the same call:

matches = audd.recognize_enterprise(
    "https://audd.tech/example.mp3",
    every=1,
    skip=3,     # recognize 12s, skip 48s, repeat — ~1 match per minute
    limit=60,
)

every=1, skip=3 meters one fragment a minute, roughly a quarter of the audio. Most tracks in a set run longer than a minute, so you’ll still catch them — but short edits and quick doubles can slip through the gaps. Use a full pass (no skip, higher limit) when you want every track placed exactly.

What you get back

recognize_enterprise returns a flat list[EnterpriseMatch], one per recognized fragment in time order. The fields you use here:

FieldTypeMeaning for a tracklist
start_seconds, end_secondsfloat | NoneWhere this track plays in your mix, in seconds. start_seconds is the position you place the track at; two matches sharing it are a blend. None only when a fragment had no usable position.
artist, titlestr | NoneThe track.
album, release_datestr | NoneThe release it appears on, and when.
scoreint | NoneMatch confidence. Use it as the floor for whether a second track at the same moment is a real blend or noise.
labelstr | NoneReleasing label, if you want to enrich the line.
song_linkstr | Nonelis.tn universal link to the track.
isrc, upcstr | NoneIdentifiers (Startup plan or higher).
start_offset, end_offsetint | NoneRaw milliseconds within the 12-second scan fragment that start_seconds/end_seconds are derived from. Rarely needed directly.
timecodestr | NonePosition inside the matched track, not your mix. Don’t use it to place the track.

The shared start_seconds is what makes blend handling possible: two tracks playing at the same moment of the mix come back as two matches pinned to the same file position, not something you have to correlate yourself.

Handling errors

The SDK raises typed exceptions; catch the ones you can act on.

  • Authentication errors (AudDAuthenticationError) — bad or missing token. Fail at startup.
  • Quota / subscription errors (AudDSubscriptionError) — request limit hit, or the enterprise endpoint isn’t enabled on your token. The enterprise endpoint and ISRC/UPC both depend on plan tier; surface these to the account owner rather than retrying.
  • Invalid-audio errors — the mix URL or file wasn’t decodable. Treat it as a hard failure for that source, not a silent empty tracklist.
  • Connection errors (AudDConnectionError) — transient; retry with backoff.
from audd.errors import AudDError

try:
    matches = audd.recognize_enterprise(mix_url, limit=60)
except AudDError as e:
    # any AudD API/transport failure — log and decide; retry connection errors
    print(f"Recognition failed: {e}")
    matches = []

Quiet intros, ambient breakdowns, or a track not in the 160-million-song database simply produce no match for that stretch — that’s a gap in the tracklist, not an error.

Going further


Related

Reading this as an AI agent? The raw Markdown is at recipes/dj-set-tracklist.md, and the full index is /resources/llms.txt.