A UI-free backend package: give it a Spotify track URL (or a local audio file), it gathers whatever metadata/credits it can, hands that to an AI agent to research official licensing across the web, and returns everything as one JSON object. Meant to be the engine behind a separate Tauri UI later — nothing here renders anything.
Spotify URL ──┐
├─► normalize metadata/credits ─► JSON ─► AI research ─► JSON result
Audio file ───┘
-
Source
spotify_source.py— parses aopen.spotify.com/track/...URL, aspotify:track:...URI, or a bare id, then uses SpotAPI (spotapi.Song.get_track_info) to pull name/artists/album/ISRC/etc. It also makes a best-effort attempt at Spotify's undocumented track-credits data; when that's unavailable (it often will be — it's an unofficial, unstable endpoint) it falls back to treating the listed artists as "performers" and leaves songwriter/ producer credits for the AI step to find from public sources.renderdragon_source.py— parses arenderdragon.org/api/music-link?...URL, resolves it against the RenderDragon endpoint (withAccept: application/json), and mapsname/credits/category/direct_urlonto normalized metadata. It can also generate a link from resource data (build_music_link). Only allowlistedraw.githubusercontent.comfiles are accepted as theurlparameter.file_source.py— reads a local audio file's tags (MP3/FLAC/M4A/OGG/WAV/...) viamutagen, including format-specific ISRC frames (e.g. ID3TSRC).
-
Normalize — both sources are folded into the same
LookupRequest/TrackMetadata/TrackCreditsshape (seemodels.py), so the AI step doesn't need to know where the data came from. -
AI research —
prompts.pybuilds a prompt that hands the AI the normalized JSON and asks it to search multiple sources (MusicBrainz, ASCAP/BMI/PRS, label/publisher sites, Discogs, ...) for the real rights holder and how to license the track, then reply with strict JSON. By default this runs throughopenrouter_researcher.py, which calls the OpenRouter REST API (openrouter_client.py) with the free model router (openrouter/free) and OpenRouter's server-sideopenrouter:web_searchtool for live research — no local agent/browser process. SetOPENROUTER_API_KEYto enable it. Other backends:opencode-go(same REST shape),openai-compatible(any OpenAI-compatible endpoint, using client-side Exa web search viaEXA_API_KEY), andopencode(the bundledopencode-harnessCLI agent). Multiple backends can be chained as a primary/secondary fallback (ai_fallback_backends=). The response is parsed back into aResearchResult. -
Result —
pipeline.py'sPipeline.check_spotify_url()/.check_youtube_url()/.check_file()return aCopyrightCheckResultwith.to_dict()giving you the full JSON: original request, normalized metadata/credits, and the AI's research findings (matches, confidence, licensing contacts, warnings).
Each GitHub Release ships a self-contained onedir archive for every major
platform — no Python, no pip, no build step. Extract it and run the launcher
inside:
| Platform | Asset |
|---|---|
| Linux x86_64 | music-copyright-checker-<version>-linux-x86_64.tar.gz |
| Linux arm64 | music-copyright-checker-<version>-linux-aarch64.tar.gz |
| macOS (Intel) | music-copyright-checker-<version>-macos-x86_64.tar.gz |
| macOS (Apple Silicon) | music-copyright-checker-<version>-macos-aarch64.tar.gz |
| Windows x86_64 | music-copyright-checker-<version>-windows-x86_64.zip |
tar -xzf music-copyright-checker-<version>-<target>.tar.gz
cd music-copyright-checker
# CLI
./music-copyright-checker --spotify-url https://open.spotify.com/track/xxxx --pretty
./music-copyright-checker --youtube-url https://www.youtube.com/watch?v=xxxxxxxxxxx --pretty
./music-copyright-checker --file ./song.mp3 --pretty
# Local JSON server for integrating into other apps (no /jobs endpoint)
./music-copyright-checker server --host 127.0.0.1 --port 8080On Windows, unzip the archive and run music-copyright-checker\music-copyright-checker.exe.
The archive is a directory bundle (not a single file) on purpose: a onefile build re-extracts its whole bundle to a temp directory on every launch, which makes warm startup slow (~17s on macOS). The onedir launcher starts in well under a second.
The default AI backend is the OpenRouter REST API, so the only thing you need
is an OpenRouter key: export OPENROUTER_API_KEY. Other backends are
--ai-backend opencode-go, --ai-backend openai-compatible (needs
OPENAI_COMPAT_API_KEY/OPENAI_COMPAT_MODEL plus EXA_API_KEY for web
search), or --ai-backend opencode (the opencode CLI agent, downloaded via
the official installer into ~/.opencode/bin on first run). Add
--fallback-ai-backend BACKEND (repeatable) to fail over to a secondary
endpoint.
Checksums are published next to the archives in the release (SHA256SUMS).
For everything else — backend/model flags, the full flag reference, embedding the local JSON server into another app, building from source, and the CI/release pipeline — see the binaries user guide.
pip install -e . # or: uv pip install -e .The default AI backend calls OpenRouter directly and needs OPENROUTER_API_KEY.
The opencode_harness package is vendored for the optional
--ai-backend opencode path; using it requires the opencode CLI binary on
PATH (checked at AIResearcher construction), or a running opencode serve
via --opencode-server.
The pipeline persistently caches Spotify metadata, local-file metadata, and the
structured AI research result in SQLite. The default cache is
~/.cache/music-copyright-checker/cache.sqlite3; configure another location with
MUSIC_CHECKER_CACHE_PATH, --cache-path, or disable it with --no-cache.
Spotify URLs are keyed by their canonical track ID and YouTube videos by their 11-character video ID (free-text YouTube queries cache their resolved video ID too). Audio files are identified by a streaming SHA-256 hash, so temporary upload paths and renamed files do not cause repeated AI research. Audio bytes are never stored in the cache.
Cached research is versioned by the prompt and model, expires after seven days
by default, and reports ai_meta.cache_hit, ai_meta.cache_age_seconds, and
ai_meta.cached_at. Send {"refresh":true} to a JSON API request or use the
CLI --refresh flag when a fresh lookup is required. Cached research is a
timestamped research result, not a guarantee that licensing information remains
legally current.
from music_copyright_checker import Pipeline
pipeline = Pipeline() # OpenRouter backend, model openrouter/free (needs OPENROUTER_API_KEY)
# Fail over to other endpoints if the primary fails:
# pipeline = Pipeline(ai_backend="openrouter", ai_fallback_backends=["opencode-go", "openai-compatible"],
# openai_compatible_model="gpt-4.1-mini")
result = pipeline.check_spotify_url("https://open.spotify.com/track/6rqhFgbbKwnb9MLmUQDhG6")
print(result.to_dict())
# YouTube Data API v3: paste a URL, a bare 11-char video id, or a search query.
# Set YOUTUBE_API_KEY (e.g. in your .env); required for the YouTube source.
result_yt = pipeline.check_youtube_url("https://www.youtube.com/watch?v=dQw4w9WgXcQ")
print(result_yt.to_dict())
# Or search first and let the caller pick from up to 5 candidates (with thumbnails).
for candidate in pipeline.search_youtube("Rick Astley - Never Gonna Give You Up", limit=5):
print(candidate["title"], candidate["thumbnail_url"], candidate["url"])
result2 = pipeline.check_file("/path/to/song.mp3")
print(result2.to_dict())
# Resolve a RenderDragon music link, then run the same research.
result3 = pipeline.check_renderdragon_url(
"https://renderdragon.org/api/music-link?name=Song&url=...&id=12"
)
print(result3.to_dict())Or from the command line:
python -m music_copyright_checker.cli --spotify-url https://open.spotify.com/track/6rqhFgbbKwnb9MLmUQDhG6 --pretty
python -m music_copyright_checker.cli --youtube-url https://www.youtube.com/watch?v=dQw4w9WgXcQ --pretty
python -m music_copyright_checker.cli --youtube-url "Rick Astley - Never Gonna Give You Up" --pretty
python -m music_copyright_checker.cli --renderdragon-url "https://renderdragon.org/api/music-link?..." --pretty
python -m music_copyright_checker.cli --file ./song.mp3 --pretty
python -m music_copyright_checker.cli --spotify-url spotify:track:xxxx --no-ai # skip AI, just inspect normalized metadata
python -m music_copyright_checker.cli --spotify-url spotify:track:xxxx --model openrouter/free --pretty- AI backends — OpenRouter, OpenCode Go, OpenAI-compatible, and opencode; Exa web search, fallback chains, keys, models,
.env. - YouTube Data API v3 source — key setup, accepted inputs, normalized fields, quota.
- RenderDragon music links — building/resolving links, allowlist, normalized fields.
- Server API guide —
/check,/youtube/search,/jobs,/docs, deployment. - Downloadable binaries — prebuilt binaries, CLI flags, CI/release.
- v0.4.0 release notes — RenderDragon music-link source.
- v0.3.1 release notes — onedir archives for fast startup.
- v0.3.0 changes and migration — what changed, upgrading from 0.2.x.
For complete installation, API, upload, client-integration, and deployment steps, see the server API implementation guide.
Start the server on localhost. The async /jobs queue is off by default —
local client integrations only need the synchronous /check endpoint; add
--jobs for proxy/Cloudflare deployments:
uv run music-copyright-checker-server --host 127.0.0.1 --port 8080
uv run music-copyright-checker-server --host 127.0.0.1 --port 8080 --jobs # proxy deploymentsFor a persistent Linux user service, install
music-copyright-checker.service under ~/.config/systemd/user/, then run:
systemctl --user daemon-reload
systemctl --user enable --now music-copyright-checker.service
systemctl --user status music-copyright-checker.serviceCheck readiness:
curl http://127.0.0.1:8080/healthRead the complete integration contract and implementation steps from the running server:
curl http://127.0.0.1:8080/docsSend a Spotify request. The response is the same structured JSON returned by
Pipeline:
curl -X POST http://127.0.0.1:8080/check \
-H 'Content-Type: application/json' \
-d '{"spotify_url":"https://open.spotify.com/track/4cOdK2wGLETKBW3PvgPWqT"}' \
> results/server-result.jsonLet the caller choose a YouTube video by searching first: /youtube/search
returns up to five candidates with thumbnails, then send the chosen
video_id/url to /check:
curl -X POST http://127.0.0.1:8080/youtube/search \
-H 'Content-Type: application/json' \
-d '{"query":"Rick Astley - Never Gonna Give You Up","limit":5}'When the server is behind Cloudflare or another proxy, use the asynchronous job route so the AI research does not exceed the proxy request timeout:
curl -X POST http://127.0.0.1:8080/jobs \
-H 'Content-Type: application/json' \
-d '{"spotify_url":"https://open.spotify.com/track/4cOdK2wGLETKBW3PvgPWqT"}'
curl http://127.0.0.1:8080/jobs/JOB_IDFor live progress, connect to the job's SSE stream:
const events = new EventSource(`/jobs/${jobId}/events`);
events.addEventListener("progress", (event) => {
const update = JSON.parse(event.data);
showProgress(update.stage, update.message);
});Local audio files can be routed with {"file":"/absolute/path/to/song.mp3"}.
File paths are read by the server process, so keep the server bound to localhost
unless the deployment explicitly protects this endpoint.
For a file on the client machine, upload it as multipart form data. The server temporarily stores the upload, extracts its tags, sends the normalized metadata to the AI, returns the result, and deletes the temporary file:
curl -X POST http://127.0.0.1:8080/check \
-F 'file=@/path/to/song.mp3' \
> results/upload-result.jsonIf the file has no title tag, the original filename stem is used as a search hint. The AI must corroborate that hint before treating the track as identified; an arbitrary filename is never treated as proof of identity.
Direct public audio URLs are also supported through the async API:
curl -X POST http://127.0.0.1:8080/jobs \
-H 'Content-Type: application/json' \
-d '{"file_url":"https://cdn.example.com/song.mp3"}'Only public HTTP/HTTPS URLs are accepted, with a 100 MB download limit.
{
"request": {
"source": "spotify",
"input": "https://open.spotify.com/track/...",
"track": { "name": "...", "artists": ["..."], "album": "...", "isrc": "...", "...": "..." },
"credits": { "available": true, "performers": [...], "songwriters": [...], "producers": [...] }
},
"research": {
"status": "complete",
"summary": "...",
"matches": [
{
"source_name": "MusicBrainz",
"source_url": "https://...",
"confidence": "high",
"rights_holder": "...",
"publisher": "...",
"label": "...",
"license_type": "...",
"notes": "..."
}
],
"sources": [
{
"name": "MusicBrainz",
"url": "https://...",
"source_type": "database",
"supports": "track identity and credits"
}
],
"usage_assessment": {
"video_verdict": "clearance_required",
"social_media_verdict": "clearance_required",
"reality_tv_verdict": "clearance_required",
"sync_license_required": true,
"master_license_required": true,
"creator_declared_license": "royalty-free, credit appreciated",
"platform_exception": "A platform claim or creator-library license is not general permission.",
"caveats": ["Clear both composition and master rights."]
},
"official_licensing_contacts": ["https://..."],
"warnings": ["..."]
},
"ai_meta": { "mode": "process", "model": "...", "cost": 0.01, "tokens": {...}, "session": null }
}music_copyright_checker/
├── __init__.py # public exports: Pipeline, models, errors
├── models.py # dataclasses: TrackMetadata, TrackCredits, LookupRequest,
│ # LicenseMatch, ResearchResult, CopyrightCheckResult
├── spotify_source.py # Spotify URL/URI parsing + SpotAPI lookup + normalization
├── youtube_source.py # YouTube Data API v3 video lookup + search + normalization
├── renderdragon_source.py # RenderDragon music-link build/resolve + normalization
├── file_source.py # local audio file tag extraction (mutagen)
├── prompts.py # the licensing-research prompt template
├── openrouter_client.py # OpenAI-compatible REST client (server tool or Exa tool loop)
├── openrouter_researcher.py # OpenRouter backend (prompt -> research + parsing)
├── opencode_go_client.py # OpenCode Go REST backend (Cloudflare headers)
├── opencode_go_researcher.py # OpenCode Go backend
├── openai_compatible_client.py # generic OpenAI-compatible backend (Exa search)
├── openai_compatible_researcher.py # generic OpenAI-compatible backend
├── exa_search.py # Exa web search + function-tool definition
├── fallback_researcher.py # primary/secondary AI failover chain
├── ai_researcher.py # optional opencode-harness backend + response parsing
├── pipeline.py # Pipeline: wires sources + AI backend(s) together
├── bootstrap.py # downloads/installs the opencode CLI when missing
├── cli.py # thin CLI wrapper, no UI
├── server.py # JSON HTTP API around Pipeline (/jobs opt-in with --jobs)
├── entrypoints.py # dispatcher behind the packaged binaries (CLI + server)
└── errors.py # exception hierarchy
opencode_harness/ # vendored opencode-harness package (drives the opencode CLI/serve API)
packaging/
├── music_copyright_checker.spec # PyInstaller spec (one binary per platform)
└── smoke_test.py # CI smoke test for built binaries
tests/
└── test_basic.py # dependency-free unit tests (URL parsing, normalization, JSON parsing)
docs/opencode-harness/ # reference docs for the vendored opencode-harness API
- SpotAPI is an unofficial wrapper around Spotify's private web
endpoints; it can break when Spotify changes internal APIs (see its GitHub
issues).
SpotifyLookupErrorwraps any failure so callers can surface a clean error instead of a raw traceback. - SpotAPI's documented
get_track_info()method returns track metadata, not songwriter/producer credits._try_fetch_spotify_credits()makes a best-effort request to Spotify's undocumented private credits endpoint using SpotAPI's session; that endpoint can be unavailable, reshaped, blocked, or absent from the installed SpotAPI session. Thesource_noteexplains this fallback, and the AI research step fills the gap from public sources. - A track being playable, unclaimed, monetized through Content ID, or available
in a creator music library does not automatically grant general video or
reality-TV permission. The usage assessment distinguishes those platform
mechanisms from composition/sync and master recording clearance. Conversely,
licensed_content: truedoes not mean reuse is restricted: when the uploader or rights holder declares free-use terms (e.g. in a pinned comment or the description), the YouTube source surfaces those statements and the research reflects them instead of defaulting toclearance_required. - The AI is explicitly instructed to only report what it actually found
while searching, and to flag disagreement between sources in
warningsrather than silently picking one. Treatmatches[].confidenceas a signal, not a guarantee — this is research assistance, not a legal opinion.