---
title: "Turn a DJ set or mix into a tracklist"
description: "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."
slug: "/resources/recipes/dj-set-tracklist"
section: "recipes"
keywords: [audd, dj set tracklist, mix tracklist, 1001tracklists, blends, enterprise endpoint, start_seconds]
---

# Turn a DJ set or mix into a tracklist

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](https://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).

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

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

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

```text
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`.

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

```text
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:

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

```python
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](/resources/recipes/podcast-music-credits).
- For multi-hour sets, see
  [Enterprise cost optimization](/resources/concepts/enterprise-cost-control)
  on trading `every`/`skip` coverage against metered chunks.

---

**Related**

- [Generate music credits for a podcast episode](/resources/recipes/podcast-music-credits)
- [Enterprise cost optimization](/resources/concepts/enterprise-cost-control)
- [Standard, enterprise, or streams: how to choose](/resources/concepts/standard-vs-enterprise-vs-streams)
- [Python SDK docs](https://docs.audd.io/sdks/python)
- [API reference](https://docs.audd.io)