Skip to content

Commit 83dcfa4

Browse files
author
AudD
committed
Initial release v1.3.0
0 parents  commit 83dcfa4

54 files changed

Lines changed: 4937 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.9', '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

CHANGELOG.md

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
# Changelog
2+
3+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
4+
5+
## [1.3.0] - 2026-05-05
6+
7+
Coordinated v1.3.0 stable release across the audd-sdks family. No breaking
8+
changes; the version bump signals API stability across all nine SDKs.
9+
10+
The full v0.3.0 polish — env-var auto-pickup, streaming/preview helpers
11+
with metadata fallback, `onEvent` inspection hook, thread-safe token
12+
rotation, plus per-language work (JPMS module-info, Kotlin `Flow<LongpollEvent>`,
13+
Swift `Sendable` + DocC, .NET AOT/source-gen + `IServiceCollection`, Rust
14+
TLS feature flags + `Serialize`, PHP PSR-3 logger, Python `__repr__` +
15+
`pretty_print()`, Go `slog` example) is now the v1.3.0 baseline.
16+
17+
## [0.3.0] - 2026-05-05
18+
19+
### Added
20+
21+
- **`RecognitionResult.__repr__` and `EnterpriseMatch.__repr__`** — useful one-line representations for logs / REPL: `<RecognitionResult artist='X' title='Y' timecode='00:56' song_link='https://lis.tn/abc'>`. Empty fields are omitted; `timecode` is always present (it's required server-side).
22+
- **`RecognitionResult.pretty_print()` / `EnterpriseMatch.pretty_print()`** — debug helper that writes the full model state (typed fields plus any forward-compat extras) as indented JSON to stdout (or a caller-supplied stream). Useful when you want to see exactly what came back over the wire.
23+
24+
### Documentation
25+
26+
- README opens with an explicit "thread-safe / safe for concurrent use" statement.
27+
28+
## [0.2.1] - 2026-05-05
29+
30+
### Improved
31+
32+
- **`streaming_url(provider)` now falls back to the metadata block when `song_link` is non-lis.tn.** Previously returned `None` for YouTube `song_link` values; now picks the direct URL from `apple_music.url` / `spotify.external_urls.spotify` / `deezer.link` / `napster.href` when the corresponding metadata was requested via `return=`. Direct URLs are also now *preferred* over the lis.tn redirect when both paths resolve (no redirect = faster client-side load). YouTube as a provider has no metadata-block fallback (only the lis.tn redirect path).
33+
- `streaming_urls()` correspondingly returns more entries — every provider with either a direct URL or a lis.tn redirect available.
34+
35+
## [0.2.0] - 2026-05-05
36+
37+
DX polish — env-var auto-pickup, streaming/preview helpers, on_event inspection hook, thread-safe token rotation.
38+
39+
### Added
40+
41+
- **`AUDD_API_TOKEN` env-var fallback** (§7.11): `Audd()` / `AsyncAudd()` constructed without an explicit `api_token` now reads it from `os.environ.get("AUDD_API_TOKEN")`. Raises `ValueError` with a dashboard-link hint if both are missing.
42+
- **`RecognitionResult.streaming_url(provider)`** (§4.3): returns the `lis.tn` redirect URL for `spotify` / `apple_music` / `deezer` / `napster` / `youtube` (e.g. `https://lis.tn/abc?spotify` 302-redirects to the song's Spotify page). Returns `None` for non-lis.tn `song_link` values (e.g., YouTube ones).
43+
- **`RecognitionResult.streaming_urls()`** (§4.3): convenience dict of all five providers' redirect URLs.
44+
- **`RecognitionResult.preview_url()`** (§4.3): first available 30-second preview URL from `apple_music.previews[0].url``spotify.preview_url``deezer.preview`. Documented caveat about provider TOS.
45+
- Same `streaming_url` / `streaming_urls` / `thumbnail_url` helpers on `EnterpriseMatch`.
46+
- **`set_api_token(new_token)`** on `Audd` / `AsyncAudd` (§7.10): thread-safe token rotation via a `threading.Lock`. In-flight requests continue with the old token; subsequent requests use the new one. Validates non-empty.
47+
- **`on_event` inspection hook** on `Audd` / `AsyncAudd` (§7.7a): a callable receiving `AuddEvent` lifecycle events (kind=`request`/`response`/`exception`, method, url, request_id, http_status, elapsed_ms, error_code, extras). Off by default. Never logs the api_token or request body. Hook exceptions are swallowed at debug level so observability never breaks the request path.
48+
49+
### Internal
50+
51+
- 26 new regression tests (`tests/unit/test_v0_2_polish.py`) covering all four feature areas.
52+
53+
## [0.1.1] - 2026-05-04
54+
55+
Independent code review caught several issues. v0.1.1 fixes them before they propagate to the other 8 language SDKs being modeled on this implementation.
56+
57+
### Fixed
58+
59+
- **C1 — `prepare_source` now returns a per-attempt re-opener.** *Note: the reviewer flagged this as a "silent zero-byte upload on retry" bug, but verification (running the regression test against v0.1.0 source) showed that httpx auto-seeks file handles between `post()` calls, so audd-python v0.1.0 was actually correct.* The re-opener pattern is kept as **defense in depth**: it doesn't depend on the HTTP library's seeking behavior, raises cleanly on unseekable streams (rather than silently sending an empty body), and matches the mandatory pattern other SDKs need (their HTTP libraries may not auto-seek).
60+
- **C2 — `advanced.find_lyrics` was using READ retry policy.** Fixed to use RECOGNITION (the lyrics endpoint is metered; the spec §7.1 cost-aware retry policy explicitly groups it with `recognize`).
61+
- **C3 — Code 51 (deprecation warning) was raising unconditionally.** Per spec §6.5, when the server returns code 51 with a usable `result`, the SDK now emits a `DeprecationWarning` and returns the result. Only raises if `result` is absent.
62+
- **S2 — Non-JSON HTTP errors mapped to `AuddSerializationError`.** A 502 with an HTML body now correctly raises `AuddServerError` preserving the HTTP status; `AuddSerializationError` is reserved for 2xx-with-bad-JSON.
63+
- **S5 — `LongpollConsumer` silently swallowed HTTP errors.** A 401/403/500 returned `{}` and the consumer looped forever, especially painful in browsers. Now non-2xx raises `AuddServerError`; JSON decode failures raise `AuddSerializationError`.
64+
- **S6 — `LongpollConsumer` had no retry/timeout knobs.** Per spec §4.1 ("same retry, timeout, transport configurability as the authenticated client"), now accepts `max_retries` and `backoff_factor` and applies READ-class retries on connection failures + 5xx.
65+
- **S9 — Confusing `FileNotFoundError` for typo'd URLs.** A string source that's neither HTTP(S) nor an existing path now raises `TypeError` with a hint about the URL prefix.
66+
67+
### Added
68+
69+
- **Context-manager protocol** on `Audd`, `AsyncAudd`, `LongpollConsumer`, `AsyncLongpollConsumer``with Audd(...) as audd:` / `async with AsyncAudd(...) as audd:` now work.
70+
71+
### Tests
72+
73+
- Added 16 regression tests covering each finding (`tests/unit/test_review_fixes.py`, `tests/unit/test_review_fixes_longpoll.py`). Total: 108 unit + contract tests passing.
74+
75+
## [0.1.0] - 2026-05-04
76+
77+
### Added
78+
- Sync (`Audd`) and async (`AsyncAudd`) clients with parity for every capability.
79+
- `recognize(source, *, return_, market, timeout)` — auto-detects URL / path / file-like / bytes sources.
80+
- `recognize_enterprise(source, ...)` with 1-hour read timeout default.
81+
- `streams.*` namespace: `set_callback_url(url, return_metadata=...)`, `get_callback_url`, `add`, `set_url`, `delete`, `list`, `longpoll(category, *, skip_callback_check=False)` with default-on preflight, plus pure helpers `derive_longpoll_category` and `parse_callback`.
82+
- `custom_catalog.add(audio_id, source)` — namespaced with NOT-for-recognition warning docstring; 904 errors raise `AuddCustomCatalogAccessError`.
83+
- `advanced.*` namespace: `find_lyrics(query)`, `raw_request(method, params)` escape hatch.
84+
- Tokenless `LongpollConsumer` / `AsyncLongpollConsumer` for browser/widget contexts.
85+
- Forward-compatible Pydantic v2 models with `extra="allow"` on every type.
86+
- Full error hierarchy mapped to the 25+ AudD error codes.
87+
- Cost-aware retry policy: read endpoints retry 408/429/5xx; recognition endpoints don't retry post-upload read timeouts (cost protection); mutating endpoints retry only pre-upload connection failures.
88+
- CI: ruff + mypy strict + pytest matrix on Py 3.9–3.13 (`ci.yml`); contract tests on push/PR + daily cron + `openapi-updated` repository_dispatch (`contract.yml`); tag-triggered PyPI publishing with Sigstore attestation (`release.yml`).

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: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
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+
`Audd` and `AsyncAudd` are **safe for concurrent use** — share one instance across threads or asyncio tasks. Token rotation via `set_api_token(...)` is thread-safe; in-flight requests continue with the old token, subsequent requests use the new one.
11+
12+
## Quickstart
13+
14+
```bash
15+
pip install audd
16+
```
17+
18+
```python
19+
from audd import Audd
20+
21+
audd = Audd(api_token="test") # use your token from https://dashboard.audd.io
22+
result = audd.recognize("https://audd.tech/example.mp3")
23+
if result:
24+
print(f"{result.artist}{result.title}")
25+
```
26+
27+
## Capabilities
28+
29+
| What | How |
30+
|---|---|
31+
| Recognize a short clip (≤25s) | `audd.recognize(source)` |
32+
| Recognize a long file (hours, days) | `audd.recognize_enterprise(source, limit=...)` |
33+
| Manage real-time stream recognition | `audd.streams.add(url, radio_id)` etc. |
34+
| Long-poll for stream events | `audd.streams.longpoll(category)` |
35+
| Long-poll without exposing your token | `LongpollConsumer(category).iterate()` |
36+
37+
`source` accepts a URL, a file path, a file-like object, or raw bytes — auto-detected.
38+
39+
## Async
40+
41+
Use `AsyncAudd` instead — same surface:
42+
43+
```python
44+
from audd import AsyncAudd
45+
46+
async def main():
47+
audd = AsyncAudd(api_token="test")
48+
try:
49+
result = await audd.recognize("https://audd.tech/example.mp3")
50+
print(result)
51+
finally:
52+
await audd.aclose()
53+
```
54+
55+
## Errors
56+
57+
Every server error becomes a typed exception:
58+
59+
```python
60+
from audd import Audd, AuddAuthenticationError, AuddSubscriptionError
61+
62+
try:
63+
Audd(api_token="bad").recognize("https://x.mp3")
64+
except AuddAuthenticationError as e:
65+
print(f"check your token: {e.error_code} {e.message}")
66+
except AuddSubscriptionError:
67+
print("this endpoint isn't enabled on your token")
68+
```
69+
70+
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`.
71+
72+
## Forward compatibility
73+
74+
Models accept and round-trip unknown server fields via `model_extra`:
75+
76+
```python
77+
result = audd.recognize("https://example.mp3", return_=["apple_music"])
78+
print(result.apple_music.url) # typed
79+
print(result.model_extra) # any other unknown fields
80+
```
81+
82+
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.
83+
84+
## Configuration
85+
86+
```python
87+
import httpx
88+
from audd import Audd
89+
90+
audd = Audd(
91+
api_token="...",
92+
max_retries=3, # retry budget per call
93+
backoff_factor=0.5, # initial backoff seconds (jittered)
94+
httpx_client=httpx.Client(proxies="http://corp-proxy:8080"),
95+
)
96+
```
97+
98+
Default timeouts: 30s connect / 60s read for standard endpoints, 30s connect / **1 hour** read for the enterprise endpoint. Pass `timeout=` per call to override.
99+
100+
## Custom catalog (advanced — not for music recognition)
101+
102+
> **The custom-catalog endpoint is NOT how you submit audio for music recognition.**
103+
> For recognition, use `recognize()` or `recognize_enterprise()`. The custom-catalog
104+
> endpoint adds songs to your private fingerprint database for *your* account.
105+
> Requires special access — contact api@audd.io if you need it.
106+
107+
```python
108+
audd.custom_catalog.add(audio_id=42, source="https://my.song.mp3")
109+
```
110+
111+
## Advanced
112+
113+
For lyrics search and a generic raw-request escape hatch:
114+
115+
```python
116+
hits = audd.advanced.find_lyrics("rule the world")
117+
raw = audd.advanced.raw_request("someNewMethod", {"q": "x"})
118+
```
119+
120+
## Tokenless longpoll
121+
122+
For browser widgets and other contexts where shipping the api_token would leak it:
123+
124+
```python
125+
from audd import LongpollConsumer
126+
127+
# `category` is derived server-side via Audd(...).streams.derive_longpoll_category(radio_id),
128+
# then shared with the browser/widget. The consumer carries no api_token.
129+
consumer = LongpollConsumer(category="abc123def")
130+
for event in consumer.iterate(timeout=30):
131+
print(event)
132+
```
133+
134+
## Spec contract
135+
136+
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.
137+
138+
## License
139+
140+
MIT — see [LICENSE](./LICENSE).
141+
142+
## Support
143+
144+
- Documentation: https://docs.audd.io
145+
- Tokens: https://dashboard.audd.io
146+
- Email: api@audd.io

SECURITY.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
# Security Policy
2+
3+
To report a vulnerability, please email **api@audd.io**. We aim to respond within 2 business days. Do not open public GitHub issues for security reports.

0 commit comments

Comments
 (0)