MusicBrainz: Add browse endpoints for release-groups + recordings

Add `browse_artist_release_groups(mbid)` and `browse_artist_recordings(mbid)`
to MusicBrainzClient. These hit `/ws/2/release-group?artist=<mbid>` and
`/ws/2/recording?artist=<mbid>` respectively — the correct MusicBrainz
pattern for "give me everything linked to this artist."

Why this matters: our current search adapter calls text-search
(`release?query=...` / `recording?query=...`) for albums and tracks,
which matches entity titles literally. Typing "metallica" hits unrelated
releases titled "Metallica" and recordings named "Metallica" by obscure
bands — every garbage match scores 100 because they're all exact title
matches on the wrong field.

Browse walks the artist→release-group and artist→recording links
directly. Once we know the artist's MBID (from `search_artist`), browse
returns their actual discography instead of title collisions.

No behavior change yet — search adapter still uses the old path. Follow-
up commit wires the new endpoints in.

Reference: https://musicbrainz.org/doc/MusicBrainz_API — "Browse queries
retrieve entities linked to a known entity" vs search.
pull/372/head
Broque Thomas 1 month ago
parent 3c48508c3f
commit e2a5a38cd2

@ -200,6 +200,93 @@ class MusicBrainzClient:
logger.error(f"Error searching for recording '{track_name}': {e}")
return []
@rate_limited
def browse_artist_release_groups(self, artist_mbid: str,
release_types: Optional[List[str]] = None,
limit: int = 100,
offset: int = 0) -> List[Dict[str, Any]]:
"""Browse release-groups linked to an artist MBID.
This is the correct MusicBrainz pattern for "give me this artist's
discography" — text-based `/release?query=...` search would look at
release TITLES (matching unrelated releases literally titled after
the artist name), while browse walks the artistrelease-group link
directly.
Args:
artist_mbid: Artist's MusicBrainz ID
release_types: Filter by primary type any of 'album', 'single',
'ep', 'compilation', 'soundtrack', 'live', etc. Combined with
`|` per MB spec, e.g. `['album', 'ep']` `type=album|ep`.
None returns all types.
limit: 1-100 (MB hard cap)
offset: Pagination offset
Returns:
List of release-group dicts. Each has `id`, `title`, `primary-type`,
`secondary-types`, `first-release-date`, `disambiguation`.
"""
try:
params = {'artist': artist_mbid, 'fmt': 'json', 'limit': min(limit, 100), 'offset': offset}
if release_types:
params['type'] = '|'.join(release_types)
response = self.session.get(
f"{self.BASE_URL}/release-group",
params=params,
timeout=10
)
response.raise_for_status()
data = response.json()
rgs = data.get('release-groups', [])
logger.debug(f"Browsed {len(rgs)} release-groups for artist {artist_mbid}")
return rgs
except Exception as e:
logger.error(f"Error browsing release-groups for artist {artist_mbid}: {e}")
return []
@rate_limited
def browse_artist_recordings(self, artist_mbid: str,
limit: int = 100,
offset: int = 0,
includes: Optional[List[str]] = None) -> List[Dict[str, Any]]:
"""Browse recordings (tracks) linked to an artist MBID.
Counterpart to `browse_artist_release_groups` text search on
`/recording?query=...` matches recording TITLES, while browse follows
the artistrecording link directly.
Args:
artist_mbid: Artist's MusicBrainz ID
limit: 1-100 (MB hard cap)
offset: Pagination offset
includes: e.g. ['releases', 'artist-credits'] to embed linked entities
Returns:
List of recording dicts with `id`, `title`, `length`, `disambiguation`,
and optionally `releases` / `artist-credit` per includes.
"""
try:
params = {'artist': artist_mbid, 'fmt': 'json', 'limit': min(limit, 100), 'offset': offset}
if includes:
params['inc'] = '+'.join(includes)
response = self.session.get(
f"{self.BASE_URL}/recording",
params=params,
timeout=10
)
response.raise_for_status()
data = response.json()
recs = data.get('recordings', [])
logger.debug(f"Browsed {len(recs)} recordings for artist {artist_mbid}")
return recs
except Exception as e:
logger.error(f"Error browsing recordings for artist {artist_mbid}: {e}")
return []
@rate_limited
def get_artist(self, mbid: str, includes: Optional[List[str]] = None) -> Optional[Dict[str, Any]]:
"""

Loading…
Cancel
Save