Recipe

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.

view .md audddmcatakedowncopyright evidence

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.mp3 works 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 limit during 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 with limit=25 and 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:

FieldTypeRole in the evidence
start_seconds, end_secondsfloat | NoneWhere 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, titlestr | NoneThe recording, named.
albumstr | NoneThe release it appears on.
release_datestr | NoneThe release date of the recording.
labelstr | NoneThe releasing label — establishes the commercial rights holder.
isrcstr | NoneInternational Standard Recording Code — the recording’s unique identifier. The strongest single identifier in a filing.
upcstr | NoneUniversal Product Code — the release’s identifier.
song_linkstr | Nonelis.tn universal link to the track.
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 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 off m.model_extra on 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.