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.
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.
labelcomes 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.mp3is 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
limitduring 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 againstlimit=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.2to71.6). These are the values you put in the show notes; a fragment that arrived without a usable position hasNonehere.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:41means 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_secondsstarts recognition partway in — handy if every episode opens with the same dynamically-inserted ad you don’t want to credit.everyandskipsample the file:every=1, skip=4recognizes 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:
| Field | Type | Meaning for a credits list |
|---|---|---|
start_seconds, end_seconds | float | None | Where 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, title | str | None | The credit. |
album, release_date | str | None | The release the song appears on, and when. |
label | str | None | The releasing label — what you cite for licensing. |
isrc, upc | str | None | Recording / release identifiers for licensing records (Startup plan or higher). |
song_link | str | None | A lis.tn universal link to the track; handy as a clickable credit. |
score | int | None | Match confidence for the fragment. |
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 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, andlimittrade 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.
