audd-rust
Recognize music in audio clips, long broadcasts, and live streams from Rust — async, tokio-based.
[dependencies]
audd = "1.5"
tokio = { version = "1", features = ["rt-multi-thread", "macros"] }
use audd::AudD;
#[tokio::main]
async fn main() -> Result<(), audd::AudDError> {
let audd = AudD::new("test"); // 10 reqs/day; get a real token at dashboard.audd.io
if let Some(r) = audd.recognize("https://audd.tech/example.mp3").await? {
println!("{} — {}", r.artist.as_deref().unwrap_or(""), r.title.as_deref().unwrap_or(""));
}
Ok(())
}
Authentication
Get your API token at dashboard.audd.io.
The api_token is resolved on construction in this order:
- The argument to
AudD::new,AudD::try_new, orAudD::builder(...). - The
AUDD_API_TOKENenvironment variable. - Otherwise
AudDError::Configuration, with a pointer to dashboard.audd.io.
use audd::AudD;
let audd = AudD::new("your-api-token"); // explicit; panics on no-token
let audd = AudD::try_new("your-api-token")?; // Result-returning sibling
let audd = AudD::from_env()?; // reads AUDD_API_TOKEN
# Ok::<_, audd::AudDError>(())
The public "test" token is capped at 10 requests/day and works only for standard recognition — fine for hello-worlds, not for production.
For long-running services pulling tokens from a secret manager, rotate without rebuilding the client:
audd.set_api_token("new-token")?;
set_api_token is thread-safe and tokio-task-safe — it swaps the token under an RwLock shared by the standard and enterprise transports. In-flight requests finish on the previous token; subsequent calls use the new one. audd.api_token() returns the current value.
Recognize a clip
recognize() takes a 5–25-second clip and returns Some(RecognitionResult) on a match, or Ok(None) when the clip processed but matched nothing.
use audd::{AudD, Source};
use std::path::PathBuf;
# async fn run(audd: AudD) -> Result<(), audd::AudDError> {
// URL — auto-detected from the &str prefix; server fetches the audio.
audd.recognize("https://audd.tech/example.mp3").await?;
// Filesystem path — &str, &Path, or PathBuf.
audd.recognize(PathBuf::from("/clip.mp3")).await?;
// Bytes.
let bytes: Vec<u8> = std::fs::read("/clip.mp3").unwrap();
audd.recognize(bytes).await?;
// Async reader (anything `AsyncRead + Send + Unpin`).
let file = tokio::fs::File::open("/clip.mp3").await.unwrap();
audd.recognize(Source::Reader(Box::new(file))).await?;
# Ok(()) }
recognize is the no-knob default. For provider blocks, region, or per-call timeouts use recognize_with:
# use audd::AudD;
# use std::time::Duration;
# async fn run(audd: AudD) -> Result<(), audd::AudDError> {
let result = audd
.recognize_with(
"https://audd.tech/example.mp3",
Some(&["apple_music".into(), "spotify".into()]),
Some("US"),
Some(Duration::from_secs(10)),
)
.await?;
# let _ = result; Ok(()) }
A RecognitionResult carries timecode, artist, title, album, release_date, label, song_link, isrc / upc (Startup plan or higher), provider blocks (when requested), an extras: HashMap<String, Value> for forward-compat, and helpers — thumbnail_url(), streaming_url(provider), streaming_urls(), preview_url(). Full reference: github.com/AudDMusic/audd-rust#what-you-get-back.
Source-form notes: Path re-reads on each retry, Bytes clones, Reader buffers the bytes on first read so retries replay from memory — very large readers may spike memory; prefer Path or Bytes when audio doesn't fit in RAM. A non-existent path surfaces as AudDError::Source before the first network attempt.
Process a long audio file
For broadcasts, podcasts, and full sets longer than 25 seconds, use the enterprise endpoint. It chunks the file server-side and returns every match.
use audd::{AudD, EnterpriseOptions};
# async fn run(audd: AudD) -> Result<(), audd::AudDError> {
let matches = audd
.recognize_enterprise(
"/path/to/broadcast.mp3",
EnterpriseOptions { limit: Some(10), ..Default::default() },
)
.await?;
for m in matches {
println!(
"{} {} — {} (score={})",
m.timecode,
m.artist.as_deref().unwrap_or(""),
m.title.as_deref().unwrap_or(""),
m.score,
);
}
# Ok(()) }
EnterpriseOptions derives Default — reach for the struct-update form (EnterpriseOptions { limit: Some(N), ..Default::default() }) and only set what you need. Other fields: return_, skip, every, skip_first_seconds, use_timecode, accurate_offsets, timeout. Default read/write timeout is one hour.
Always set limit during development. The unbounded default can ingest many hours of audio on a single call.
Each EnterpriseMatch carries score, timecode, artist, title, album, release_date, label, song_link, isrc/upc (Startup+), start_offset, end_offset, extras, plus thumbnail_url(), streaming_url(provider), streaming_urls().
Get streaming-service metadata
Pass return_ to populate provider sub-objects on the result. Without it, those fields are None.
# use audd::AudD;
# async fn run(audd: AudD) -> Result<(), audd::AudDError> {
let return_ = ["apple_music".into(), "spotify".into()];
let result = audd
.recognize_with("https://audd.tech/example.mp3", Some(&return_), None, None)
.await?;
if let Some(r) = result {
if let Some(am) = &r.apple_music {
println!("Apple Music: {:?}", am.url);
}
if let Some(sp) = &r.spotify {
println!("Spotify URI: {:?}", sp.uri);
}
// Or resolve any provider — direct URL when the metadata block is set,
// else the lis.tn redirect when song_link is on lis.tn.
println!("{:?}", r.streaming_url(audd::StreamingProvider::Spotify));
println!("{:?}", r.streaming_urls()); // every resolvable provider
println!("{:?}", r.preview_url()); // 30-second preview, provider terms apply
}
# Ok(()) }
Valid providers: apple_music, spotify, deezer, napster, musicbrainz. Each provider adds latency. Pass market (ISO 3166 region code, e.g. "GB") to recognize_with to control the Apple Music storefront.
For the field shape of each provider sub-object, see the upstream API reference at docs.audd.io and the GitHub README. Server-side fields not yet typed surface via each model's extras: HashMap<String, Value>.
Monitor a live audio stream
A stream in AudD is a long-running audio source — a radio URL, HLS, Icecast, SHOUTcast, or a twitch:<channel> / youtube:<video_id> shortcut — that AudD ingests continuously and recognizes against. Each stream is identified by a radio_id: i64 you pick.
There are two ways to consume recognition events:
- Callbacks — AudD POSTs each match (and each lifecycle notification) to a URL you control. See Receive callbacks from a stream.
- Longpoll — your code polls AudD's
/longpoll/endpoint and receives matches and notifications synchronously over HTTP. See Poll for stream events (longpoll).
Both modes need a callback URL configured on the account; otherwise events are never delivered. If your consumers will only longpoll — e.g., browser widgets, mobile apps, or scripts that can't expose a public URL — set the account's callback URL to https://audd.tech/empty/ in advance; it's a placeholder URL that discards POSTs. Consumers themselves don't need to configure anything.
Receive callbacks from a stream
The full setup has three steps: configure the callback URL, register a stream, and parse incoming POSTs.
1. Configure the account callback URL
# use audd::AudD;
# async fn run(audd: AudD) -> Result<(), audd::AudDError> {
audd.streams()
.set_callback_url(
"https://your-app.example.com/audd-callback",
Some(&["apple_music".into(), "spotify".into()]), // optional; populates each callback
)
.await?;
# Ok(()) }
return_metadata is serialized into the URL as ?return=apple_music,spotify (the server reads it from the URL). If url already has a ?return= parameter and return_metadata.is_some(), the SDK returns AudDError::Api with ErrorKind::InvalidRequest rather than silently overwriting.
2. Register the stream
# use audd::AudD;
# async fn run(audd: AudD) -> Result<(), audd::AudDError> {
let radio_id = 1i64; // any integer you choose — your handle for this stream
audd.streams()
.add("https://radio.example.com/stream.mp3", radio_id, None)
.await?;
# Ok(()) }
Other stream methods — set_url(radio_id, url), delete(radio_id), list() (returns Vec<Stream>), and get_callback_url(). add takes an optional third argument callbacks: Option<&str> — comma-separated extra callback URLs for this stream only, or Some("before") to deliver callbacks at song start instead of song end.
3. Parse incoming POST bodies — axum
Each callback POST body is JSON with either a result block (recognition) or a notification block (lifecycle event). audd::handle_callback accepts raw bytes and returns a CallbackEvent:
pub enum CallbackEvent {
Match(StreamCallbackMatch),
Notification(StreamCallbackNotification),
}
A canonical axum recipe — pull Bytes from the request and hand them to handle_callback:
use audd::{handle_callback, CallbackEvent};
use axum::{http::StatusCode, routing::post, Router};
use bytes::Bytes;
async fn audd_callback(body: Bytes) -> Result<StatusCode, (StatusCode, String)> {
match handle_callback(&body) {
Ok(CallbackEvent::Match(m)) => {
let song = &m.song;
println!(
"{} — {} (radio={}, score={})",
song.artist, song.title, m.radio_id, song.score,
);
// persist, queue, fan-out, ...
}
Ok(CallbackEvent::Notification(n)) => {
// Stream lifecycle: "stream stopped", "can't connect", etc.
println!("#{}: {}", n.notification_code, n.notification_message);
}
Err(e) => return Err((StatusCode::BAD_REQUEST, format!("malformed: {e}"))),
}
Ok(StatusCode::NO_CONTENT)
}
pub fn app() -> Router {
Router::new().route("/audd-callback", post(audd_callback))
}
handle_callback and parse_callback are exposed as free functions (no AudD instance required), so they work in queue replay tools, webhook proxies, and framework-agnostic tests. Use parse_callback(value: serde_json::Value) when you've already deserialized the body.
A StreamCallbackMatch carries radio_id, timestamp, play_length, song (the top match — StreamCallbackSong with artist, title, score, album, release_date, label, song_link, plus optional provider blocks when return_metadata was set), alternatives (rare extra candidates — may have a different artist or title from the top match), extras, and raw_response (the full unparsed body).
A StreamCallbackNotification carries radio_id, stream_running, notification_code, notification_message, time, extras, and raw_response.
Other frameworks (actix-web, warp, rocket) — see the GitHub README.
Poll for stream events (longpoll)
For consumers that can't expose a public callback URL — desktop apps, scripts behind NAT, anything without a public HTTPS receiver — call streams().longpoll_by_radio_id(...). It returns a LongpollPoll handle whose three streams — matches, notifications, errors — are driven by a background tokio task. Drive them with tokio::select!.
If no callback URL is configured for the account, longpoll might never return any events despite successful song recognition. The SDK preflights this on your first longpoll_by_radio_id(...) call and returns AudDError::Api with ErrorKind::InvalidRequest and guidance if it's missing; pass LongpollOptions::default().skip_callback_check(true) once you've set one.
If you only want longpoll and have no real receiver, set https://audd.tech/empty/ — it's a placeholder URL that discards incoming POSTs:
audd.streams().set_callback_url("https://audd.tech/empty/", None).await?;
use audd::{AudD, LongpollOptions};
use futures_util::StreamExt;
# async fn run(audd: AudD) -> Result<(), audd::AudDError> {
let radio_id = 1i64; // any integer you choose — your handle for this stream
let mut poll = audd
.streams()
.longpoll_by_radio_id(radio_id, LongpollOptions::default().timeout(30))
.await?;
loop {
tokio::select! {
biased;
Some(err) = poll.errors.next() => {
eprintln!("longpoll terminated: {err}");
break;
}
Some(n) = poll.notifications.next() => {
println!("notif: {}", n.notification_message);
}
Some(m) = poll.matches.next() => {
println!("matched: {} — {}", m.song.artist, m.song.title);
}
else => break,
}
}
poll.close().await;
# Ok(()) }
matches carries StreamCallbackMatch, notifications carries StreamCallbackNotification (same shapes as the callback receiver above). errors is single-shot — the first terminal error closes matches and notifications too. LongpollOptions builds with Default::default() and chains .timeout(secs), .since_time(unix_ms), .skip_callback_check(true). close().await shuts down deterministically; dropping the handle aborts the background task best-effort.
Tokenless consumers
For browser widgets, mobile clients, WASM frontends, or any consumer that should not see the api_token: a server with the token derives an opaque per-stream identifier with audd::derive_longpoll_category(api_token, radio_id) and ships only that string to the client. The client subscribes via LongpollConsumer — no api_token ever leaves the server.
use audd::{LongpollConsumer, LongpollIterateOptions};
use futures_util::StreamExt;
# async fn run(category_from_server: String) -> Result<(), audd::AudDError> {
let consumer = LongpollConsumer::new(category_from_server);
let mut poll = consumer.iterate(LongpollIterateOptions::default());
while let Some(m) = poll.matches.next().await {
println!("matched: {} — {}", m.song.artist, m.song.title);
}
poll.close().await;
# Ok(()) }
The server-side caller (which holds the token) is responsible for ensuring a callback URL is configured before handing identifiers out.
Add a song to your custom catalog
The custom-catalog endpoint adds songs to a private fingerprint database for your account. After upload, future recognize() calls on the same account can match against your tracks. It is not how you submit audio for music recognition — for that, use recognize (or recognize_enterprise).
The custom-catalog endpoint requires special access. Contact api@audd.io to enable it. Calls without access return AudDError::Api with ErrorKind::CustomCatalogAccess.
# use audd::AudD;
# async fn run(audd: AudD) -> Result<(), audd::AudDError> {
let audio_id = 1i64; // any integer you choose — your reference to the song
audd.custom_catalog().add(audio_id, "https://my.cdn/song.mp3").await?;
# Ok(()) }
The SDK exposes only add — there is no public list or delete. Track audio_id ↔ song mappings yourself. Re-using an audio_id re-fingerprints that slot.
Handle errors
Every fallible method returns Result<T, AudDError>. AudDError is a single enum derived with thiserror:
| Variant | When raised |
|---|---|
Api { code, message, kind, http_status, request_id, requested_params, branded_message, raw_response, .. } | Server returned status: error with a parseable body. |
Server { http_status, message, .. } | Non-2xx HTTP with a non-JSON body (e.g. 502 with HTML from an upstream gateway). |
Connection { message, source } | Network / TLS / timeout — no response received. Wraps the underlying reqwest::Error via the source chain. |
Serialization { message, raw_text } | 2xx with malformed JSON, or unexpectedly-shaped JSON the typed model couldn't deserialize. |
Source(String) | Caller misuse: nonexistent path, retry against a consumed unbuffered reader, malformed callback URL. |
Configuration { message } | Construction-time misconfiguration — no api_token supplied with AUDD_API_TOKEN unset, or set_api_token(""). |
Idiomatic error handling
The AudDError::Api variant carries an ErrorKind for semantic dispatch — match on it directly, or use the is_* helpers:
use audd::{AudD, AudDError, ErrorKind};
# async fn run(audd: AudD) -> Result<(), AudDError> {
match audd.recognize("/path/to/clip.mp3").await {
Ok(_) => {}
Err(AudDError::Api { kind: ErrorKind::Authentication, code, message, .. }) => {
eprintln!("check your token: [#{code}] {message}");
}
Err(AudDError::Api { kind: ErrorKind::Quota, message, .. }) => {
eprintln!("out of quota: {message}");
}
Err(AudDError::Api { kind: ErrorKind::RateLimit, message, .. }) => {
eprintln!("rate limited: {message}");
}
Err(AudDError::Api { kind: ErrorKind::InvalidAudio, message, .. }) => {
eprintln!("audio rejected: {message}");
}
Err(AudDError::Api { kind: ErrorKind::CustomCatalogAccess, .. }) => {
eprintln!("contact api@audd.io for custom-catalog access");
}
Err(AudDError::Connection { message, .. }) => {
eprintln!("network: {message}");
}
Err(other) => return Err(other),
}
# Ok(()) }
For some codes — 19 (request_blocked, abuse, test_request_scope), 31337 (ip_ban), 903 (token_disabled) — the server populates branded human-readable text alongside the error. The SDK extracts that onto AudDError::Api { branded_message, .. } rather than surfacing it as a fake recognition match:
# use audd::{AudD, AudDError, ErrorKind};
# async fn run(audd: AudD) -> Result<(), AudDError> {
match audd.recognize("https://audd.tech/example.mp3").await {
Ok(_) => {}
Err(AudDError::Api { kind: ErrorKind::Blocked, code, message, branded_message: Some(b), .. }) => {
eprintln!("[#{code}] {message}");
eprintln!("server brand text: {b}");
}
Err(other) => return Err(other),
}
# Ok(()) }
When the server returns code 51 (deprecated parameter) with a usable result, the SDK emits a tracing::warn! on the audd target and returns the result as if the call had succeeded. Code 51 with no result raises AudDError::Api with ErrorKind::InvalidRequest.
Retry behavior
Each endpoint is classified into one of three retry classes:
Read(streams.list,streams.get_callback_url, longpoll polls) — retries on anyreqwest::Error, plus HTTP 408/429/5xx.Recognition(recognize,recognize_enterprise,advanced.*) — retries only on pre-upload network errors (connect, DNS, TLS) to avoid double-billing for in-progress uploads, plus 5xx (skips 429 — cost concern).Mutating(streams.set_callback_url,streams.add,streams.set_url,streams.delete,custom_catalog.add) — retries only on pre-upload errors; 5xx is surfaced because the side effect may have already happened.
Defaults: max_attempts = 3, backoff_factor = 0.5, backoff_max = 30.0. Backoff is min(backoff_factor * 2^attempt, backoff_max) with 0.5x..1.5x deterministic jitter. Override per client:
let audd = AudD::builder("your-api-token")
.max_attempts(5)
.backoff_factor(1.0)
.build()?;
let audd = AudD::builder("your-api-token").max_attempts(1).build()?; // disable retries
# Ok::<_, audd::AudDError>(())
Configuration
TLS feature flags
Pick exactly one TLS backend at compile time:
| Flag | Default | Effect |
|---|---|---|
rustls-tls | yes | TLS via rustls with the Mozilla CA bundle. Pure-Rust, no system dependencies. |
native-tls | no | Platform-native TLS — OpenSSL on Linux, SecureTransport on macOS, SChannel on Windows. Useful for custom corporate CA trust stores or OpenSSL FIPS. |
vendored-openssl | no | OpenSSL via native-tls, statically linked from a vendored source build. Useful when the build host lacks libssl-dev / openssl-devel. |
To swap the default rustls for native-tls:
[dependencies]
audd = { version = "1.5", default-features = false, features = ["native-tls"] }
MSRV is Rust 1.88.
Timeouts
Defaults: standard endpoint 30 s connect / 60 s read & write; enterprise endpoint 30 s connect / 3600 s read & write because long files take that long to fingerprint server-side. Override per call on recognize_with / recognize_enterprise:
# use audd::AudD;
# use std::time::Duration;
# async fn run(audd: AudD) -> Result<(), audd::AudDError> {
audd.recognize_with(
"https://audd.tech/example.mp3",
None,
None,
Some(Duration::from_secs(10)),
)
.await?;
# Ok(()) }
You can also wrap any call with tokio::time::timeout(...) for cooperative cancellation.
Custom HTTP client
Inject your own reqwest::Client for proxies, custom transports, custom TLS, or HTTP/2:
use audd::AudD;
# fn run() -> Result<(), Box<dyn std::error::Error>> {
let http = reqwest::Client::builder()
.proxy(reqwest::Proxy::all("http://corp-proxy:8080")?)
.build()?;
let audd = AudD::builder("your-api-token")
.reqwest_client(http)
.build()?;
# let _ = audd; Ok(()) }
The injected client is shared by both standard and enterprise endpoints — the SDK does not override its timeouts. If your standard timeouts conflict with enterprise's hour-long reads, omit the injection and let the SDK build them.
Observability
The SDK logs through the audd tracing target — install any subscriber to capture deprecation warnings and retry diagnostics:
tracing_subscriber::fmt().with_env_filter("audd=info").init();
For structured per-request observability, register an on_event hook on the builder:
use std::sync::Arc;
use audd::{AudD, AudDEvent, OnEventHook};
let hook: OnEventHook = Arc::new(|event: &AudDEvent| {
eprintln!(
"audd {} {} -> {:?} ({}ms)",
event.method,
event.url,
event.http_status,
event.elapsed.as_millis(),
);
});
let audd = AudD::builder("your-api-token").on_event(hook).build()?;
# Ok::<_, audd::AudDError>(())
AudDEvent carries kind (Request / Response / Exception), method, url, request_id, http_status, elapsed, error_code, and a free-form extras. Events never carry the api_token or request/response bodies. Hook panics are caught and suppressed (std::panic::catch_unwind) so observability can never break the request path.
Concurrency and lifecycle
AudD is Send + Sync + 'static and Clone — clones share the inner reqwest::Client connection pool and the Arc<RwLock<String>> token. Share one instance across tokio tasks; spawn freely. Drop the value (or call audd.close().await for parity with sibling SDKs) when you're done.
tokio::time::timeout and tokio::select! work as expected, but cancelling a recognize() mid-flight might still consume a request on the server — the metered work may already be in progress by the time the cancel signal lands.
Calling undocumented endpoints
For AudD endpoints not yet wrapped by typed methods on this SDK, hit them by name through audd.advanced().raw_request(method, params). It returns the raw JSON body as a serde_json::Value, runs through the same retry policy, and is the supported path for anything beta or one-off — typed wrappers ship as features stabilize. raw_request_strict raises AudDError::Api on status: error bodies; plain raw_request returns the body for inspection.
# use audd::AudD;
# async fn run(audd: AudD) -> Result<(), audd::AudDError> {
let body = audd
.advanced()
.raw_request("getLinks", &[("audd_id", "12345".to_string())])
.await?;
# let _ = body; Ok(()) }