ADR-0011: Bundled Async, Typed MusicBrainz Client
Status
Accepted
Date
2026-08-12
Context
The application needs two things from MusicBrainz: recording search (to find the MBID that anchors track identity, see ADR-0010) and cover art from the Cover Art Archive.
The obvious third-party binding, musicbrainzngs, is a poor fit:
- Unmaintained: not meaningfully updated for years.
- Synchronous: it would block the event loop, against the app's async design (ADR-0008).
- Untyped: it returns raw dicts, against the strict typing stance the project holds.
- Potentially stale: no guarantee it tracks the current MusicBrainz API.
The app already follows "Parse, Don't Validate" (Alexis King's essay):
Any values are handled at the boundary, and everything downstream deals with
typed objects. Pydantic is the tool used for this everywhere else (config, state
models), and parsing API responses into typed models at the HTTP boundary is the
same pattern applied to MusicBrainz. Async is strictly better here because
network waits should never idle the event loop.
The needed surface is small: search recordings, look up recordings and releases, and fetch cover art. Nothing else.
Decision
- Bundle a minimal, purpose-built client in
py_musicbrainz/, backed byhttpx.AsyncClient. - Parse at the boundary: every response is validated into a Pydantic model
(
Recording,Release,SearchResults,CoverArt) before it leaves the client. Invalid or unrecognised responses raise immediately rather than propagating malformed data. - Respect MusicBrainz public API etiquette: a descriptive
User-Agent, and a rate limit of one request per second. - Retry transient 503 responses with exponential backoff (three attempts). MusicBrainz returns 503s frequently under load; without the retry, lookups failed spuriously during the matching work (see ADR-0010).
- Expose only what the application uses: search, recording/release lookup, and cover art. No aliases, relationships, browse, or submission endpoints. New endpoints are added when a concrete use case appears.
Consequences
Positive
- A typed, async client that fits the architecture and the strict typing rules.
- No dependency on an unmaintained library.
- Fail-fast parsing surfaces MusicBrainz API shape changes immediately, instead of breaking downstream code subtly.
- Small enough to understand and extend cheaply.
Negative
- We own the integration: API compatibility, rate-limit etiquette, and parsing edge cases are our maintenance burden.
- Any future need beyond the minimal surface requires a code change (though a cheap one).
Neutral
- Adds no new dependencies:
httpxandpydanticare already in the project.
Alternatives Considered
Use musicbrainzngs
Rejected: unmaintained, synchronous, untyped, and potentially out of step with the current API.
Hand-rolled requests returning raw dicts
Rejected: violates Parse-Don't-Validate and forces Any handling throughout the
call sites instead of containing it at the boundary.
Sync httpx.Client (as originally specced)
Rejected in favour of the async client: network waits must not idle the event loop, consistent with the rest of the application.