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.
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, andupccome 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.mp3is 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
limitduring 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 withlimit=25until 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’sNoneonly 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, nottimecode.start_secondsis the position-in-your-file; the song’stimecodeis 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:
| Field | Type | Meaning for a tracklist |
|---|---|---|
start_seconds, end_seconds | float | None | Where 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, title | str | None | The track. |
album, release_date | str | None | The release it appears on, and when. |
score | int | None | Match confidence. Use it as the floor for whether a second track at the same moment is a real blend or noise. |
label | str | None | Releasing label, if you want to enrich the line. |
song_link | str | None | lis.tn universal link to the track. |
isrc, upc | str | None | Identifiers (Startup plan or higher). |
start_offset, end_offset | int | None | Raw milliseconds within the 12-second scan fragment that start_seconds/end_seconds are derived from. Rarely needed directly. |
timecode | str | None | Position 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
- Persist the tracklist keyed by mix ID; re-running re-meters every fragment.
- Generating a music credits list for a podcast instead, where you want one credit per song with a time range? See Generate music credits for a podcast episode.
- For multi-hour sets, see
Enterprise cost optimization
on trading
every/skipcoverage against metered chunks.
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.
