Generate DMCA takedown evidence
Produce a structured evidence pack — track identification plus exact in-file timestamps — proving copyrighted music appears in a piece of content, ready for a DMCA filing.
When you need to file a takedown over copyrighted music in someone else’s content, the complaint is only as strong as the evidence behind it: which recording, who released it, and exactly where it plays. This recipe runs a piece of offending content through AudD’s enterprise endpoint, then assembles a per-match evidence record — track identification plus precise start/end timestamps in the file — and renders it both as JSON (for your records and tooling) and as a human-readable summary (to paste into a complaint).
What you’ll build
A function that takes a URL or file for the offending content and returns an evidence pack: one record per recognized track, each with the recording’s identification (artist, title, album, label, ISRC, UPC, song_link, score) and its location in the content (file-absolute start and end timestamps), plus the source reference. You’ll render the pack two ways — structured JSON and a plain-text summary.
The enterprise endpoint is the right tool: it handles arbitrary-length content
and finds every track rather than one. A complaint has to say when the
recording plays, and each match answers that with start_seconds and
end_seconds — the song’s file-absolute position in the content.
recognize_enterprise requests accurate offsets by default, so those
positions are precise; there’s no chunk-offset math to do.
AudD provides the identification data, not the legal filing. This recipe produces evidence — what was recognized and where. Deciding whether to file, drafting the complaint, and asserting ownership are yours to handle. The output is input to that process, not legal advice.
Prerequisites
- An API token from dashboard.audd.io. Get your own token there; the enterprise endpoint must be enabled on it. ISRC and UPC in responses require a Startup plan or higher; they’re returned on enterprise calls.
- Python 3.10+ with the SDK:
pip install audd - The offending content as a URL or a file. AudD parses social URLs
server-side, so you can pass a link to live content directly.
https://audd.tech/example.mp3works for a happy-path run.
Walkthrough
Step 1: Recognize the content
Run the content through recognize_enterprise — pass a URL, file bytes, or a
path. It handles any length and returns a flat list[EnterpriseMatch]: one
entry per recognized fragment, in time order, each carrying the precise
start_seconds / end_seconds the evidence is built on (accurate offsets are
requested by default).
from audd import AudD
audd = AudD("your-api-token") # token from dashboard.audd.io
def recognize(content_url: str, limit: int = 25):
"""Return the enterprise matches — one per recognized fragment, in time order."""
return audd.recognize_enterprise(
content_url,
limit=limit, # cap metered fragments while developing
return_metadata=["apple_music", "spotify"], # optional streaming links per match
)
matches = recognize("https://audd.tech/example.mp3", limit=25)
for m in matches:
print(f"{m.start_seconds:.1f}s {m.artist} — {m.title} (score {m.score})")
Running this prints one line per recognized fragment. A recording that plays for several minutes produces a run of consecutive matches all naming it — Step 4 collapses that run into one evidence span. Clean content returns an empty list, not an error.
Always set
limitduring development. The enterprise endpoint bills per 12 seconds of audio processed. An unbounded call on a long video can produce hundreds of metered fragments. Start withlimit=25and raise it only when you understand the cost on your real inputs.
Step 2: The timestamps the evidence cites
The timestamps a reviewer verifies against the content are start_seconds /
end_seconds: where this song plays in the content file, in seconds (e.g.
246.78 to 251.64). They’re file-absolute and go into the evidence record
as-is. They’re None only when a fragment arrived without a usable position —
skip those.
Two other fields are easy to mistake for them. start_offset / end_offset
are the raw offsets the seconds are derived from: milliseconds within AudD’s
internal 12-second scan fragment (0–~12000), not file seconds; you rarely need
them. And timecode is the position inside the matched recording, not the
content file — it tells you which part of the song was playing, so never use
it to locate the infringement.
Step 3: Format an in-file position into HH:MM:SS
A complaint wants a clock time, not raw seconds. Format the file-absolute position:
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(246.78)) # -> "00:04:06"
This always gives a zero-padded HH:MM:SS, which reads cleanly in a filing
even for content that runs past an hour.
Step 4: Collapse a held track into one evidence span
For evidence you want one record with a time range — “this recording plays
from 00:04:06 to 00:06:18” — not ten near-identical records, one per fragment.
Collapse the consecutive matches for a track into a single span: the start is
the first match’s start_seconds, the end is the last match’s end_seconds.
A new (artist, title) opens a new span; a repeat of the one you’re currently
extending stretches its end.
def build_spans(matches) -> list[dict]:
"""Collapse consecutive same-track matches into one span with a time range."""
spans = []
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
extending = (
spans
and spans[-1]["artist"] == m.artist
and spans[-1]["title"] == m.title
)
if extending:
spans[-1]["end"] = max(spans[-1]["end"], end) # same track still playing
continue
spans.append({
"artist": m.artist,
"title": m.title,
"album": m.album,
"release_date": m.release_date,
"label": m.label,
"isrc": m.isrc,
"upc": m.upc,
"song_link": m.song_link,
"score": m.score,
"start": m.start_seconds,
"end": end,
})
return spans
A two-minute play becomes one span with a real start and end, which is exactly
what a reviewer wants to verify against the content. The start_seconds guard
and the end_seconds fallback are there because the SDK parses responses
leniently: any field can be absent or None, and one positionless fragment
shouldn’t sink the whole pack.
Step 5: Keep only the spans worth citing
Not every recognition is takedown-grade evidence. A defensible record points at a commercial release — one with a label and/or an ISRC/UPC. Filter to spans that carry those identifiers, and drop the rest (or route them to manual review).
def is_citable(span: dict) -> bool:
"""A span worth putting in a takedown: identifies a commercial release."""
return bool(span.get("label") or span.get("isrc") or span.get("upc"))
citable = [s for s in build_spans(matches) if is_citable(s)]
A non-null label or a present isrc/upc is your signal that the match is a
released recording you can name precisely. ISRC and UPC require a Startup plan
or higher; if they’re null on every span, check your plan tier before assuming
the content is unidentifiable.
Step 6: Assemble an evidence record per span
For each citable span, build a record with two halves: identification (what the recording is and who released it) and location (where it plays in the file). Add the source reference so the record stands on its own.
from datetime import datetime, timezone
def evidence_record(span: dict, source: str) -> dict:
return {
"identification": {
"artist": span["artist"],
"title": span["title"],
"album": span["album"],
"release_date": span["release_date"],
"label": span["label"],
"isrc": span["isrc"],
"upc": span["upc"],
"song_link": span["song_link"],
"score": span["score"],
},
"location": {
"start_seconds": round(span["start"], 2),
"end_seconds": round(span["end"], 2),
"start_timestamp": fmt_hms(span["start"]),
"end_timestamp": fmt_hms(span["end"]),
},
"source": source,
}
def evidence_pack(matches, source: str) -> dict:
spans = [s for s in build_spans(matches) if is_citable(s)]
return {
"source": source,
"generated_at": datetime.now(timezone.utc).isoformat(),
"match_count": len(spans),
"records": [evidence_record(s, source) for s in spans],
}
source is the URL or filename of the offending content — record it explicitly
so each pack is self-describing. The location times are file-absolute: they
come straight from each match’s start_seconds / end_seconds, so they point
at the exact moment in the content a reviewer can seek to.
Step 7: Generate the pack end to end
def build_pack(source: str) -> dict:
matches = recognize(source, limit=50)
return evidence_pack(matches, source)
pack = build_pack("https://audd.tech/example.mp3")
pack is the structured evidence — store it as JSON alongside your case
records.
Step 8: Render a human-readable summary
The JSON is for your systems. For the complaint itself, render a plain-text summary a human can read and a reviewer can verify against the content.
def render_summary(pack: dict) -> str:
lines = [
f"Evidence pack for: {pack['source']}",
f"Generated (UTC): {pack['generated_at']}",
f"Recognized recordings: {pack['match_count']}",
"",
]
for i, rec in enumerate(pack["records"], 1):
ident = rec["identification"]
loc = rec["location"]
lines += [
f"[{i}] {ident['artist']} — {ident['title']}",
f" Album: {ident['album']}",
f" Label: {ident['label']}",
f" ISRC: {ident['isrc']}",
f" UPC: {ident['upc']}",
f" Link: {ident['song_link']}",
f" Plays: {loc['start_timestamp']}–{loc['end_timestamp']} "
f"({loc['start_seconds']}s–{loc['end_seconds']}s into the content)",
"",
]
return "\n".join(lines)
print(render_summary(pack))
This produces something you can paste straight into a takedown form: each recording named, identified, and located by its position in the file.
What you get back
Every match in the flat list is one recognized fragment, in time order. The fields the evidence uses:
| Field | Type | Role in the evidence |
|---|---|---|
start_seconds, end_seconds | float | None | Where this song plays in the content file, in seconds. The values the evidence is built on. None only when a fragment had no usable position. |
artist, title | str | None | The recording, named. |
album | str | None | The release it appears on. |
release_date | str | None | The release date of the recording. |
label | str | None | The releasing label — establishes the commercial rights holder. |
isrc | str | None | International Standard Recording Code — the recording’s unique identifier. The strongest single identifier in a filing. |
upc | str | None | Universal Product Code — the release’s identifier. |
song_link | str | None | lis.tn universal link to the track. |
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 recording, not the content file. Supporting detail; don’t use it to locate the infringement. |
For the example file, a match might come back with start_seconds = 246.78
(00:04:06) and end_seconds = 251.64 (00:04:11) — the span the evidence
record cites.
isrc and upc populate on enterprise responses for Startup-plan accounts and
higher. A null ISRC/UPC doesn’t mean the match is invalid — label plus
artist/title still identify the recording — but an ISRC makes the strongest
record.
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 content URL or file wasn’t decodable audio/video. For evidence work this matters: a record built on content the API couldn’t actually read is worthless. Treat it as a hard failure for that source, not a silent empty pack.
- Connection errors (
AudDConnectionError) — transient. Retry with backoff.
from audd.errors import AudDError
try:
pack = build_pack(source)
except AudDError as e:
# any AudD API/transport failure — log and decide; retry connection errors
print(f"Recognition failed: {e}")
pack = None
An empty records list (no citable matches) is a valid, distinct outcome from
an error — it means the content recognized nothing commercial, so there’s
nothing to file on.
Going further
- Re-running recognition re-meters every fragment. Generate the pack once per source and persist the JSON; regenerate the human-readable summary from stored JSON rather than re-scanning.
- Reading metadata fields beyond the ones above — including undocumented or
beta fields the API returns — comes through each match’s
model_extra. If a filing needs a field not listed here, read it offm.model_extraon the match. - To find offending content in the first place — scanning uploads as they arrive — see Build a copyright scanner for user-uploaded content.
- For multi-hour content, see Enterprise cost optimization on trading coverage against metered chunks.
Related
Reading this as an AI agent? The raw Markdown is at recipes/dmca-takedown-evidence.md, and the full index is /resources/llms.txt.
