Skip to content

Commit 87c622f

Browse files
author
AudD
committed
Initial release v1.4.2
0 parents  commit 87c622f

56 files changed

Lines changed: 4882 additions & 0 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/ci.yml

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
name: CI
2+
3+
on:
4+
push:
5+
branches: [main]
6+
pull_request:
7+
branches: [main]
8+
9+
jobs:
10+
test:
11+
name: tests + mypy + ruff
12+
runs-on: ubuntu-latest
13+
strategy:
14+
fail-fast: false
15+
matrix:
16+
python: ['3.10', '3.11', '3.12', '3.13']
17+
steps:
18+
- uses: actions/checkout@v4
19+
- uses: actions/setup-python@v5
20+
with:
21+
python-version: ${{ matrix.python }}
22+
cache: pip
23+
- name: Install
24+
run: |
25+
pip install --upgrade pip
26+
pip install -e '.[dev]'
27+
- name: Ruff
28+
run: python -m ruff check src/audd/ tests/
29+
- name: Mypy
30+
run: python -m mypy src/audd/
31+
- name: Unit tests
32+
run: python -m pytest tests/unit/ -v --tb=short

.github/workflows/contract.yml

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
name: Contract tests
2+
3+
on:
4+
push:
5+
branches: [main]
6+
pull_request:
7+
branches: [main]
8+
schedule:
9+
- cron: '0 6 * * *' # daily at 06:00 UTC
10+
repository_dispatch:
11+
types: [openapi-updated]
12+
13+
jobs:
14+
contract:
15+
name: validate parser against latest audd-openapi fixtures
16+
runs-on: ubuntu-latest
17+
steps:
18+
- uses: actions/checkout@v4
19+
with:
20+
path: audd-python
21+
- name: Check out audd-openapi
22+
uses: actions/checkout@v4
23+
with:
24+
repository: AudDMusic/audd-openapi
25+
path: audd-openapi
26+
ref: main
27+
- uses: actions/setup-python@v5
28+
with:
29+
python-version: '3.12'
30+
cache: pip
31+
cache-dependency-path: audd-python/pyproject.toml
32+
- name: Install
33+
working-directory: audd-python
34+
run: pip install -e '.[dev]'
35+
- name: Run contract tests
36+
working-directory: audd-python
37+
env:
38+
AUDD_OPENAPI_FIXTURES: ${{ github.workspace }}/audd-openapi/fixtures
39+
run: python -m pytest tests/contract/ -v --tb=short
40+
- name: Open issue on failure (dispatched runs only)
41+
if: failure() && github.event_name == 'repository_dispatch'
42+
env:
43+
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
44+
TRIGGER_SHA: ${{ github.event.client_payload.trigger_sha }}
45+
run: |
46+
gh issue create \
47+
--title "Contract drift: audd-openapi spec change broke parser" \
48+
--body "An openapi-updated dispatch (trigger SHA: $TRIGGER_SHA) caused contract tests to fail. Investigate and update parsers." \
49+
--label "contract-drift" || true

.github/workflows/release.yml

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
name: Release
2+
3+
on:
4+
push:
5+
tags:
6+
- 'audd-python/v*'
7+
- 'v*'
8+
9+
permissions:
10+
contents: read
11+
id-token: write
12+
attestations: write
13+
14+
jobs:
15+
build-and-publish:
16+
name: Build and publish to PyPI
17+
runs-on: ubuntu-latest
18+
environment: pypi
19+
steps:
20+
- uses: actions/checkout@v4
21+
- uses: actions/setup-python@v5
22+
with:
23+
python-version: '3.12'
24+
- name: Install build tools
25+
run: |
26+
pip install --upgrade pip build
27+
- name: Build sdist + wheel
28+
run: python -m build
29+
- name: Generate Sigstore attestations
30+
uses: actions/attest-build-provenance@v1
31+
with:
32+
subject-path: 'dist/*'
33+
- name: Publish to PyPI
34+
uses: pypa/gh-action-pypi-publish@release/v1
35+
with:
36+
attestations: true

.gitignore

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
__pycache__/
2+
*.py[cod]
3+
.venv/
4+
venv/
5+
build/
6+
dist/
7+
*.egg-info/
8+
.pytest_cache/
9+
.mypy_cache/
10+
.ruff_cache/
11+
.coverage
12+
htmlcov/
13+
.env
14+
.DS_Store

LICENSE

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
MIT License
2+
3+
Copyright (c) 2026 AudD (https://audd.io)
4+
5+
Permission is hereby granted, free of charge, to any person obtaining a copy
6+
of this software and associated documentation files (the "Software"), to deal
7+
in the Software without restriction, including without limitation the rights
8+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
copies of the Software, and to permit persons to whom the Software is
10+
furnished to do so, subject to the following conditions:
11+
12+
The above copyright notice and this permission notice shall be included in all
13+
copies or substantial portions of the Software.
14+
15+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21+
SOFTWARE.

README.md

Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
# audd
2+
3+
[![CI](https://github.com/AudDMusic/audd-python/actions/workflows/ci.yml/badge.svg)](https://github.com/AudDMusic/audd-python/actions/workflows/ci.yml)
4+
[![Contract](https://github.com/AudDMusic/audd-python/actions/workflows/contract.yml/badge.svg)](https://github.com/AudDMusic/audd-python/actions/workflows/contract.yml)
5+
[![PyPI](https://img.shields.io/pypi/v/audd.svg)](https://pypi.org/project/audd/)
6+
[![Python versions](https://img.shields.io/pypi/pyversions/audd.svg)](https://pypi.org/project/audd/)
7+
8+
Official Python SDK for the [AudD](https://audd.io) music recognition API.
9+
10+
## Quickstart
11+
12+
```bash
13+
pip install audd
14+
```
15+
16+
```python
17+
from audd import AudD
18+
19+
audd = AudD(api_token="test") # use your token from https://dashboard.audd.io
20+
result = audd.recognize("https://audd.tech/example.mp3")
21+
if result:
22+
print(f"{result.artist}{result.title}")
23+
```
24+
25+
## Capabilities
26+
27+
| What | How |
28+
|---|---|
29+
| Recognize a short clip (≤25s) | `audd.recognize(source)` |
30+
| Recognize a long file (hours, days) | `audd.recognize_enterprise(source, limit=...)` |
31+
| Manage stream recognition (set callback, longpoll for events) | `audd.streams.*` |
32+
33+
`source` accepts a URL, a file path, a file-like object, or raw bytes — auto-detected.
34+
35+
## Async
36+
37+
Use `AsyncAudD` instead — same surface:
38+
39+
```python
40+
from audd import AsyncAudD
41+
42+
async def main():
43+
audd = AsyncAudD(api_token="test")
44+
try:
45+
result = await audd.recognize("https://audd.tech/example.mp3")
46+
print(result)
47+
finally:
48+
await audd.aclose()
49+
```
50+
51+
## Errors
52+
53+
Every server error becomes a typed exception:
54+
55+
```python
56+
from audd import AudD, AudDAuthenticationError, AudDSubscriptionError
57+
58+
try:
59+
AudD(api_token="bad").recognize("https://x.mp3")
60+
except AudDAuthenticationError as e:
61+
print(f"check your token: {e.error_code} {e.message}")
62+
except AudDSubscriptionError:
63+
print("this endpoint isn't enabled on your token")
64+
```
65+
66+
The full hierarchy is documented in [`src/audd/errors.py`](src/audd/errors.py). Every `AudDAPIError` carries `error_code`, `message`, `http_status`, `request_id`, `requested_params`, `request_method`, `branded_message`, and `raw_response`.
67+
68+
## Forward compatibility
69+
70+
Models accept and round-trip unknown server fields via `model_extra`:
71+
72+
```python
73+
result = audd.recognize("https://example.mp3", return_=["apple_music"])
74+
print(result.apple_music.url) # typed
75+
print(result.model_extra) # any other unknown fields
76+
```
77+
78+
If AudD adds a new metadata block tomorrow (e.g., `tidal`), you can read it as `result.model_extra["tidal"]` *today* — no SDK release needed. The next SDK release adds the typed `tidal` field, and both paths keep working.
79+
80+
## Streams
81+
82+
Manage real-time stream recognition (set callback, longpoll for events):
83+
84+
```python
85+
audd.streams.set_callback_url("https://your.server/cb")
86+
audd.streams.add("https://your.stream.url", radio_id=42)
87+
for event in audd.streams.list():
88+
print(event)
89+
```
90+
91+
### Receiving events without exposing your token
92+
93+
For browser widgets and other contexts where shipping the api_token would leak it,
94+
derive a `category` server-side and share that with the consumer:
95+
96+
```python
97+
from audd import LongpollConsumer
98+
99+
# `category` is derived server-side via AudD(...).streams.derive_longpoll_category(radio_id),
100+
# then shared with the browser/widget. The consumer carries no api_token.
101+
consumer = LongpollConsumer(category="abc123def")
102+
for event in consumer.iterate(timeout=30):
103+
print(event)
104+
```
105+
106+
## Configuration
107+
108+
```python
109+
import httpx
110+
from audd import AudD
111+
112+
audd = AudD(
113+
api_token="...",
114+
max_retries=3, # retry budget per call
115+
backoff_factor=0.5, # initial backoff seconds (jittered)
116+
httpx_client=httpx.Client(proxies="http://corp-proxy:8080"),
117+
)
118+
```
119+
120+
Default timeouts: 30s connect / 60s read for standard endpoints, 30s connect / **1 hour** read for the enterprise endpoint. Pass `timeout=` per call to override.
121+
122+
**Concurrency:** `AudD` and `AsyncAudD` are safe for concurrent use — share one instance across threads or asyncio tasks. `set_api_token(...)` rotates the token safely; in-flight requests continue with the old token, subsequent requests use the new one.
123+
124+
## Custom catalog (advanced — not for music recognition)
125+
126+
> **The custom-catalog endpoint is NOT how you submit audio for music recognition.**
127+
> For recognition, use `recognize()` or `recognize_enterprise()`. The custom-catalog
128+
> endpoint adds songs to your private fingerprint database for *your* account.
129+
> Requires special access — contact api@audd.io if you need it.
130+
131+
```python
132+
audd.custom_catalog.add(audio_id=42, source="https://my.song.mp3")
133+
```
134+
135+
## Advanced
136+
137+
For endpoints not yet wrapped by typed methods on this SDK, use the raw-request escape hatch:
138+
139+
```python
140+
raw = audd.advanced.raw_request("someNewMethod", {"q": "x"})
141+
```
142+
143+
## Spec contract
144+
145+
This SDK builds against the [`audd-openapi`](https://github.com/AudDMusic/audd-openapi) spec. The contract tests in `tests/contract/` validate the parser against the canonical fixture set on every push, on a daily cron, and on every spec update.
146+
147+
## License
148+
149+
MIT — see [LICENSE](./LICENSE).
150+
151+
## Support
152+
153+
- Documentation: https://docs.audd.io
154+
- Tokens: https://dashboard.audd.io
155+
- Email: api@audd.io

SECURITY.md

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
# Security Policy
2+
3+
## Reporting a vulnerability
4+
5+
If you discover a security issue in this SDK, please email **api@audd.io** privately. Do not open a public GitHub issue for security reports.
6+
7+
We will acknowledge receipt within 2 business days and coordinate disclosure with you.
8+
9+
## Scope
10+
11+
- **In scope:** vulnerabilities in this SDK's source code.
12+
- **Out of scope:** issues in upstream dependencies (file those with the upstream maintainer), or issues in the AudD service or API itself (email **api@audd.io** with subject `AudD service: <summary>`).
13+
14+
## Hardening practices
15+
16+
This SDK never logs `api_token`, request bodies, or response bodies. The `onEvent` inspection hook receives request / response / exception lifecycle events with method, URL, HTTP status, elapsed time, and request_id — but never the token or payload bytes.
17+

examples/custom_catalog_add.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
"""Add a song to your private fingerprint catalog.
2+
3+
NOTE: this is NOT music recognition — for that, use recognize() instead.
4+
This adds a song to YOUR private catalog so AudD can later identify it for
5+
YOUR account only.
6+
7+
Run: AUDD_API_TOKEN=... python examples/custom_catalog_add.py https://my.song.mp3 42
8+
"""
9+
import os
10+
import sys
11+
12+
from audd import AudD
13+
14+
15+
def main() -> None:
16+
if len(sys.argv) != 3:
17+
sys.exit("usage: custom_catalog_add.py <song_url> <audio_id>")
18+
audd = AudD(api_token=os.environ["AUDD_API_TOKEN"])
19+
audd.custom_catalog.add(audio_id=int(sys.argv[2]), source=sys.argv[1])
20+
print("ok")
21+
22+
23+
if __name__ == "__main__":
24+
main()

examples/recognize_enterprise.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
"""Recognize music in a long file via the enterprise endpoint.
2+
3+
Always pass `limit=` in dev to bound the chunk count and request cost.
4+
Run: AUDD_API_TOKEN=... python examples/recognize_enterprise.py https://example.mp3
5+
"""
6+
import os
7+
import sys
8+
9+
from audd import AudD
10+
11+
12+
def main() -> None:
13+
token = os.environ.get("AUDD_API_TOKEN")
14+
if not token:
15+
sys.exit("AUDD_API_TOKEN required")
16+
if len(sys.argv) != 2:
17+
sys.exit("usage: recognize_enterprise.py <url>")
18+
audd = AudD(api_token=token)
19+
matches = audd.recognize_enterprise(sys.argv[1], limit=5, accurate_offsets=True)
20+
for m in matches:
21+
print(f"{m.timecode} {m.artist}{m.title} (score {m.score})")
22+
23+
24+
if __name__ == "__main__":
25+
main()

0 commit comments

Comments
 (0)