Recipe

Generate music credits for a podcast episode

Produce a timestamped music credits list for a podcast episode with the AudD enterprise endpoint, collapsing repeated matches into one credit per song.

view .md auddpodcast music creditsshow notesmusic licensing

Podcasts use music: intros, stingers, bumpers, beds under the host, a song played in full during a segment. This recipe takes one episode audio file and produces a credits list — every song that appears, with a timestamp, artist, title, and label — ready to drop into your show notes or your licensing records.

What you’ll build

A script that sends a podcast episode to AudD’s enterprise endpoint, walks the matches it returns, collapses runs of the same song into a single credit with a time range, and prints a clean credits block.

The enterprise endpoint is the right tool here (not the standard endpoint): an episode is arbitrary length, and you want every song in it, not one. recognize_enterprise returns a flat list[EnterpriseMatch] — one entry per recognized fragment, in time order — and each match carries its position in your episode directly as start_seconds / end_seconds. A two-minute outro song spans several consecutive matches, so you’ll get several near-identical entries for it; the dedupe step turns those back into one line that reads “used from 48:00 to 50:00.”

The show-notes timestamps come straight from those matches: start_seconds and end_seconds are the song’s position in your episode, in seconds. recognize_enterprise requests accurate offsets by default, so there’s no offset math to do — you read those values and group on them.

Prerequisites

  • An API token from dashboard.audd.io. Get your own token there; the enterprise endpoint must be enabled on it. label comes back on enterprise responses; ISRC and UPC require a Startup plan or higher if you also want to log those for licensing.
  • Python 3.10+ with the SDK: pip install audd
  • A podcast episode file or URL. https://audd.tech/example.mp3 is a public, reproducible file you can run against to see the full flow before you point it at your own audio.

Walkthrough

Step 1: Recognize the episode

Point recognize_enterprise at the episode — a URL, file bytes, or a path. It scans the whole file and returns a flat list[EnterpriseMatch], one entry per recognized fragment in time order, each with a file-absolute 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 the credits logic right
matches = audd.recognize_enterprise(
    "https://audd.tech/example.mp3",
    limit=20,
    return_metadata=["apple_music", "spotify"],  # optional streaming links per match
)

for m in matches:
    if m.start_seconds is None:
        continue
    print(f"{m.start_seconds:.0f}s  {m.artist} — {m.title}")

The output is one line per recognized fragment. Stretches of pure speech (the host talking with no music bed) produce no match — that’s a gap, not an error. A song that plays for two minutes shows up as a run of consecutive matches with the same artist and title but advancing start_seconds values; that repetition is exactly what Step 3 folds away.

Always set limit during development. The enterprise endpoint bills per 12 seconds of audio processed. A 90-minute episode is hundreds of fragments; an unbounded call meters all of them. Develop against limit=20, confirm your credits logic, and only then raise the cap for a full pass.

Step 2: Understand the time fields

Before deduping, get the time fields straight — they are easy to confuse and the credits block depends on using the right one.

  • start_seconds / end_seconds — where this song plays in your episode, in seconds (e.g. 64.2 to 71.6). These are the values you put in the show notes; a fragment that arrived without a usable position has None here.
  • start_offset / end_offset — raw milliseconds within AudD’s internal 12-second scan fragment, which the seconds are derived from. Not episode seconds, and rarely needed.
  • timecode — the position inside the matched song where the fragment lined up (e.g. 00:41 means the fragment matched 41 seconds into that song). Useful for knowing which part of a song was used, but it is not a position in your episode.

Everything below keys off start_seconds and end_seconds.

Step 3: Collapse consecutive matches into one credit

The dedupe rule: walk the matches in time order; whenever the current match has the same artist and title as the credit you’re currently building, extend that credit’s end time instead of starting a new one. A new artist/title starts a new credit.

def build_credits(matches) -> list[dict]:
    """Collapse runs of the same song into one credit with a time range."""
    credits = []
    for m in matches:
        if m.start_seconds is None:
            continue  # no usable position for this match — skip it
        end = m.end_seconds if m.end_seconds is not None else m.start_seconds + 12
        same_as_last = (
            credits
            and credits[-1]["artist"] == m.artist
            and credits[-1]["title"] == m.title
        )
        if same_as_last:
            # Same song still playing — extend the running credit.
            credits[-1]["end"] = max(credits[-1]["end"], end)
        else:
            credits.append({
                "artist": m.artist,
                "title": m.title,
                "label": m.label,
                "start": m.start_seconds,
                "end": end,
                "song_link": m.song_link,
            })
    return credits

The result is one entry per distinct song, in the order it first appears, with the span it occupied. The SDK parses responses leniently — any field can be absent or None — hence the start_seconds guard and the end_seconds fallback, which keep a positionless fragment from crashing the pass.

A song that recurs gets two credits, on purpose. If the same theme plays at the open and again at the close, two non-adjacent runs produce two credits. That’s usually what you want in show notes — “used at 00:00 and again at 48:00.” If you’d rather merge all appearances of a song into one line, group the finished credits by (artist, title) afterward.

Step 4: Render the credits block

Format each credit as a timestamp range plus artist, title, and label.

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


def render(credits: list[dict]) -> str:
    lines = ["Music credits", ""]
    for c in credits:
        span = f"{fmt_time(c['start'])}–{fmt_time(c['end'])}"
        label = f" ({c['label']})" if c["label"] else ""
        lines.append(f"{span}  {c['artist']} — {c['title']}{label}")
    return "\n".join(lines)


matches = audd.recognize_enterprise("https://audd.tech/example.mp3", limit=20)
print(render(build_credits(matches)))

This prints a block like:

Music credits

00:00–00:57  Tears For Fears — Everybody Wants To Rule The World (UMC (Universal Music Catalogue))

Songs that played only behind a few seconds of speech collapse to a short span; a full segment track collapses to its real run. Either way you get one line per song.

Step 5: Skip the cold open, sample long beds

Two parameters keep the cost and the noise down on real episodes. Pass them as keyword arguments to recognize_enterprise:

matches = audd.recognize_enterprise(
    "https://audd.tech/example.mp3",
    skip_first_seconds=30,  # ignore a fixed ad slot or cold open
    every=1,
    skip=0,
    limit=50,
)
  • skip_first_seconds starts recognition partway in — handy if every episode opens with the same dynamically-inserted ad you don’t want to credit.
  • every and skip sample the file: every=1, skip=4 recognizes one fragment then skips the next four (one match per minute). For credits you usually want full coverage so you don’t miss a short stinger, but for a rough “what music is in here” pass, sampling cuts metered fragments several-fold.

Note skip_first_seconds must not be combined with use_timecode; use one or the other.

What you get back

Each entry in the returned list is one recognized fragment, in time order. The fields a credits list cares about:

FieldTypeMeaning for a credits list
start_seconds, end_secondsfloat | NoneWhere this song plays in your episode, in seconds. These are the show-notes timestamps you group and render on. None only when a fragment had no usable position.
artist, titlestr | NoneThe credit.
album, release_datestr | NoneThe release the song appears on, and when.
labelstr | NoneThe releasing label — what you cite for licensing.
isrc, upcstr | NoneRecording / release identifiers for licensing records (Startup plan or higher).
song_linkstr | NoneA lis.tn universal link to the track; handy as a clickable credit.
scoreint | NoneMatch confidence for the fragment.
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 song, not your episode. Don’t put this in show notes.

For the example file, a match might come back with start_seconds = 0.0, end_seconds = 57.0, artist = "Tears For Fears", title = "Everybody Wants To Rule The World", and label = "UMC (Universal Music Catalogue)" — which renders as the single credit line above.

Handling errors

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

  • Authentication errors (AudDAuthenticationError) — bad or missing token. Fail at startup, not per episode.
  • Quota / subscription errors (AudDSubscriptionError) — you’ve hit a request limit, 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 URL or file wasn’t decodable audio. Treat as a bad-input case: report which episode failed and move on.
  • Connection errors (AudDConnectionError) — transient; retry with backoff.
from audd.errors import AudDError

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

A clean stretch with no music isn’t an error — those fragments simply produce no match, so they contribute no credits.

Going further

  • Persist the finished credits keyed by episode ID so you never re-scan (a re-scan re-meters every fragment).
  • If your show always opens and closes with the same theme, post-process the credits to merge non-adjacent runs of the same (artist, title) into one line.
  • Recognizing a back catalog of episodes? See Enterprise cost optimization for how every, skip, and limit trade coverage against metered fragments.
  • Turning a DJ set or continuous mix into a tracklist instead of a credits block? See Turn a DJ set or mix into a tracklist.

Related

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