Recipe

Detect samples and reuse of your own tracks

Detect when uploaded content reuses your own tracks — samples, leaks, or re-uploads — by matching against a private custom catalog with the AudD API.

view .md auddsample detectioncustom catalogleak detection

If you own a catalog — a label, a sample pack, unreleased masters, a production library — you often need the opposite of commercial-music detection. The question is “is my track in here”, not “is there any copyrighted music here”. This recipe uploads your own recordings to a private custom catalog on your account, then runs incoming content through recognition and identifies the matches that came from your catalog rather than the public database.

What you’ll build

A two-part workflow. First, a one-time (or ongoing) ingestion step: your tracks go into your account’s private fingerprint database via POST api.audd.io/upload/. Second, a recognition path that sends incoming content through the API and inspects each match to answer one question — did this match come from my private catalog, or from the public 160-million-song database?

The distinguishing signal is the audio_id field. A custom-catalog match carries an audio_id (the ID of the track you uploaded), and its artist and title may be null, because a private master isn’t a public release with public tags. A public-database match has artist/title populated and no audio_id. That single check is what separates “someone reused my track” from “someone used a commercial song” — and the latter is the copyright-scanner recipe, not this one.

Prerequisites

  • An API token from dashboard.audd.io.
  • Custom-catalog upload access. The upload endpoint requires special access on your account — email [email protected] to have it enabled. Recognition against your catalog works on your normal token once the tracks are ingested.
  • Python 3.10+ with the official SDK: pip install audd
  • The tracks you want to detect, as audio files (MP3, WAV, FLAC, M4A, OGG, AAC, WMA, or AIFF).

Walkthrough

Step 1: Ingest your tracks into the custom catalog

Uploading a track fingerprints it and stores it in your account’s private database. From then on, any recognition call on your token can match against it. This is a fingerprint store, not file storage — you’re teaching the API what your recordings sound like, not hosting the files.

Upload requires special access on your account (email [email protected] to enable it), but the call itself is simple: you send each track with an integer audio_id that you assign — your own track ID — and AudD fingerprints it under that ID. The ID is yours to choose; it’s what comes back on every later recognition that matches this track. The upload response itself carries no payload ({"status": "success", "result": null}), so there’s nothing to read back from it — you already hold the ID, because you set it.

custom_catalog.add(audio_id, source) takes the ID you assign and the track (a path, URL, or bytes):

from audd import AudD

audd = AudD("your-api-token")  # token from dashboard.audd.io

# You assign each track an integer audio_id — your own track ID. It's what
# comes back on a later recognition that matches this track.
catalog = {
    51885: {"path": "masters/unreleased-01.wav", "title": "Unreleased 01"},
    51886: {"path": "masters/unreleased-02.wav", "title": "Unreleased 02"},
}

for audio_id, meta in catalog.items():
    audd.custom_catalog.add(audio_id, meta["path"])  # fingerprint under your id

The custom catalog is private to your account. Tracks you upload are only matchable from your own token. They are not added to the public 160-million-song database, and other accounts cannot recognize against them.

Maintain your own mapping from audio_id to whatever your track means to you (release name, internal ID, rights holder). The API stores the fingerprint and the audio_id; the business meaning lives on your side.

Step 2: Recognize incoming content

Now run a piece of incoming content through recognition. A single recognition call checks the audio against both the public database and your private catalog at once — you don’t choose one or the other. What differs is how you read the result.

For short clips, use the standard endpoint. For full tracks, DJ sets, videos, or anything of arbitrary length, use the enterprise endpoint (it chunks server-side and returns one match per recognized segment). Always set limit on enterprise calls during development.

from audd import AudD

audd = AudD("your-api-token")

# Short clip (under ~25 s of audio): standard endpoint, one match or None.
match = audd.recognize("https://audd.tech/example.mp3")

# Arbitrary-length content: enterprise endpoint, list of matches.
matches = audd.recognize_enterprise(
    "https://audd.tech/example.mp3",
    limit=10,  # cap matches while developing; enterprise bills per 12 s
)

The standard call returns a single match or None on no match. The enterprise call returns a list (empty on no match). Neither raises on “nothing recognized” — an empty/None result is a clean verdict, distinct from an error.

Step 3: Classify each match — yours or public

This is the core of the recipe. For every match, check audio_id. If it’s set, the match came from your private catalog; look it up in your local map. If it’s absent, it’s a public-database match (commercial music), which this recipe ignores.

def classify(match, catalog):
    """Return ('mine', meta) for a custom-catalog hit, ('public', tags) otherwise."""
    audio_id = getattr(match, "audio_id", None)
    if audio_id:
        # Custom-catalog match. artist/title are often null on private tracks,
        # so rely on audio_id + your own mapping for identification.
        meta = catalog.get(audio_id, {"title": "(unknown — not in local map)"})
        return "mine", {
            "audio_id": audio_id,
            "my_track": meta["title"],
            "timecode": match.timecode,
            "artist": match.artist,   # may be null
            "title": match.title,     # may be null
        }
    # No audio_id => public-database match (commercial release).
    return "public", {
        "artist": match.artist,
        "title": match.title,
        "label": match.label,
    }


def find_my_tracks(matches, catalog):
    return [info for kind, info in (classify(m, catalog) for m in matches)
            if kind == "mine"]

audio_id is the only reliable discriminator. Don’t key off artist/title being null — a public match can have sparse tags too. The presence of audio_id is what means “this is from your catalog”.

Step 4: Wire it into an upload handler

Putting it together: accept incoming content, recognize it, and report which of your tracks (if any) it reuses.

from fastapi import FastAPI, UploadFile
from audd import AudD

app = FastAPI()
audd = AudD("your-api-token")
# catalog: your audio_id -> metadata map from Step 1, loaded at startup.

@app.post("/check-reuse")
async def check_reuse(file: UploadFile):
    data = await file.read()
    matches = audd.recognize_enterprise(data, limit=25)
    mine = find_my_tracks(matches, catalog)
    return {
        "reuses_my_catalog": len(mine) > 0,
        "my_matches": mine,
    }

reuses_my_catalog: true with a populated my_matches list is your “this content contains one of my tracks” verdict — a sample, a leak of an unreleased master, or a straight re-upload, depending on what you put in the catalog and where the content came from.

What you get back

A custom-catalog match and a public match differ in exactly the fields you key on. Here are both, side by side, as they appear in the result:

{
  "custom_catalog_match": {
    "audio_id": "742918",
    "artist": null,
    "title": null,
    "album": null,
    "label": null,
    "timecode": "00:14",
    "song_link": null
  },
  "public_match": {
    "audio_id": null,
    "artist": "Imagine Dragons",
    "title": "Warriors",
    "album": "Smoke + Mirrors (Deluxe)",
    "label": "KIDinaKORNER/Interscope Records",
    "timecode": "00:31",
    "song_link": "https://lis.tn/Warriors"
  }
}
FieldTypeMeaning for sample/reuse detection
audio_idstring | nullThe discriminator. Set only on custom-catalog matches; it’s the ID of the track you uploaded. Null means the match came from the public database.
artist, titlestring | nullMay be null on a custom-catalog match — a private master has no public tags. Don’t rely on these to identify your track; use audio_id against your own map.
album, labelstring | nullTypically null for private uploads, populated for public commercial releases.
timecodestringPosition within the matched track at the match point — i.e. how far into your track the reused segment starts.
song_linkstring | nullA lis.tn universal link for public releases; null for private catalog tracks (they aren’t public).

On enterprise responses each match additionally carries start_offset and end_offset — the seconds into the incoming content where the match begins and ends. Use those to point at where in the upload your track was reused; use timecode to know which part of your track was lifted.

Handling errors

This workflow has two distinct failure surfaces — ingestion and recognition.

  • Authentication errors (AudDAuthenticationError) — bad or missing token. Fail at startup.
  • Subscription / access errors (AudDSubscriptionError) — the upload endpoint isn’t enabled on your token. Custom-catalog upload needs special access; email [email protected]. This surfaces on ingestion, not on recognition.
  • Invalid-audio errors (AudDInvalidAudioError) — the file (whether a track you’re ingesting or content you’re scanning) wasn’t decodable audio/video. Treat as a user/input error, not a server error.
  • Connection errors (AudDConnectionError) — transient. Retry with backoff. Do not blindly auto-retry an upload/ingest call — a retried ingest can fingerprint the same track twice and hand you two audio_ids. Make ingestion idempotent on your side (track which files you’ve already uploaded).
from audd.errors import AudDInvalidAudioError, AudDAPIError

try:
    matches = audd.recognize_enterprise(data, limit=25)
except AudDInvalidAudioError:
    return {"error": "unreadable_file"}, 422
except AudDAPIError as e:
    # log e.error_code, e.request_id for support
    return {"error": "recognition_failed"}, 502

Going further

  • Reading additional fields beyond the typed result properties: every result and per-provider block round-trips unknown fields through Pydantic’s model_extra map. If a custom-catalog response carries metadata you want that isn’t a typed property, read it from model_extra.
  • To detect commercial music in user uploads (the inverse of this recipe — someone using a song you don’t own), see Build a copyright scanner for user-uploaded content.
  • To turn a confirmed match into a filing for a takedown, see Generate DMCA takedown evidence.
  • Monitoring live streams for reuse of your catalog (a Twitch or radio stream playing your tracks) uses the same audio_id classification on stream callbacks — see the streams endpoints.

Related

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