MusicBrainz: Switch track lookup from browse to arid: search
The previous commit's `browse_artist_recordings` call passed `inc=releases+artist-credits` — but MusicBrainz's recording browse endpoint rejects `inc=releases` with HTTP 400. The adapter's error handler returned an empty list, so the Tracks section stayed empty even though the fix was supposed to populate it. Browse without release info is useless for our search UI (tracks would render with no album), so swap to the fielded Lucene search `arid:<mbid>` on the `/recording` endpoint. That's the canonical MB pattern for "find recordings by this artist WITH release context": - arid: search accepts the artist MBID and returns recordings with `releases` (release-group, date, media) embedded in each result. - One API call per lookup, same as browse would have been. Renamed the method to `search_recordings_by_artist_mbid` so the name matches its behaviour — it's a search, not a browse. Adapter updated to call the new name; tests updated to match. Verified against the live API: Metallica's MBID returns 5 recordings in ~1.8 seconds (vs the previous 400 error).
This commit is contained in:
parent
394ac73877
commit
8523724b03
3 changed files with 31 additions and 27 deletions
|
|
@ -264,30 +264,35 @@ class MusicBrainzClient:
|
||||||
return []
|
return []
|
||||||
|
|
||||||
@rate_limited
|
@rate_limited
|
||||||
def browse_artist_recordings(self, artist_mbid: str,
|
def search_recordings_by_artist_mbid(self, artist_mbid: str,
|
||||||
limit: int = 100,
|
limit: int = 100) -> List[Dict[str, Any]]:
|
||||||
offset: int = 0,
|
"""Search for recordings linked to an artist via Lucene `arid:` query.
|
||||||
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
|
This is the counterpart to `browse_artist_release_groups` for tracks.
|
||||||
`/recording?query=...` matches recording TITLES, while browse follows
|
The proper "browse" endpoint (`/recording?artist=<mbid>`) rejects
|
||||||
the artist→recording link directly.
|
`inc=releases`, so we can't get album context per recording from
|
||||||
|
browse — only the track title/length/MBID. Without release info the
|
||||||
|
user would see tracks with no album, which is useless.
|
||||||
|
|
||||||
|
The search endpoint with a fielded `arid:<mbid>` query returns
|
||||||
|
recordings with the `releases` array already embedded (including
|
||||||
|
release-group, date, and media info), which is what the search-tab
|
||||||
|
UI needs.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
artist_mbid: Artist's MusicBrainz ID
|
artist_mbid: Artist's MusicBrainz ID
|
||||||
limit: 1-100 (MB hard cap)
|
limit: 1-100 (MB hard cap)
|
||||||
offset: Pagination offset
|
|
||||||
includes: e.g. ['releases', 'artist-credits'] to embed linked entities
|
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
List of recording dicts with `id`, `title`, `length`, `disambiguation`,
|
List of recording dicts with `id`, `title`, `length`, `score`,
|
||||||
and optionally `releases` / `artist-credit` per includes.
|
`artist-credit`, and `releases` (each with release-group + date).
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
params = {'artist': artist_mbid, 'fmt': 'json', 'limit': min(limit, 100), 'offset': offset}
|
params = {
|
||||||
if includes:
|
'query': f'arid:{artist_mbid}',
|
||||||
params['inc'] = '+'.join(includes)
|
'fmt': 'json',
|
||||||
|
'limit': min(limit, 100),
|
||||||
|
}
|
||||||
|
|
||||||
response = self.session.get(
|
response = self.session.get(
|
||||||
f"{self.BASE_URL}/recording",
|
f"{self.BASE_URL}/recording",
|
||||||
|
|
@ -298,10 +303,10 @@ class MusicBrainzClient:
|
||||||
|
|
||||||
data = response.json()
|
data = response.json()
|
||||||
recs = data.get('recordings', [])
|
recs = data.get('recordings', [])
|
||||||
logger.debug(f"Browsed {len(recs)} recordings for artist {artist_mbid}")
|
logger.debug(f"Found {len(recs)} recordings for artist {artist_mbid}")
|
||||||
return recs
|
return recs
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Error browsing recordings for artist {artist_mbid}: {e}")
|
logger.error(f"Error searching recordings for artist {artist_mbid}: {e}")
|
||||||
return []
|
return []
|
||||||
|
|
||||||
@rate_limited
|
@rate_limited
|
||||||
|
|
|
||||||
|
|
@ -429,16 +429,15 @@ class MusicBrainzSearchClient:
|
||||||
if artist_name:
|
if artist_name:
|
||||||
return self._search_tracks_text(title, artist_name, limit)
|
return self._search_tracks_text(title, artist_name, limit)
|
||||||
|
|
||||||
# Bare name → artist-first → browse.
|
# Bare name → artist-first → arid: search.
|
||||||
top = self._resolve_top_artist(query)
|
top = self._resolve_top_artist(query)
|
||||||
if top:
|
if top:
|
||||||
mbid = top.get('id', '')
|
mbid = top.get('id', '')
|
||||||
tname = top.get('name', '') or query
|
tname = top.get('name', '') or query
|
||||||
recs = self._client.browse_artist_recordings(
|
# /recording?artist=<mbid> (browse) rejects inc=releases,
|
||||||
mbid,
|
# so we use the fielded Lucene search arid:<mbid> instead —
|
||||||
limit=100,
|
# that returns recordings with release context inline.
|
||||||
includes=['releases', 'artist-credits'],
|
recs = self._client.search_recordings_by_artist_mbid(mbid, limit=100)
|
||||||
)
|
|
||||||
# Browse returns recordings unsorted. Dedupe by normalized
|
# Browse returns recordings unsorted. Dedupe by normalized
|
||||||
# title (MB has many live/compilation variants of the same
|
# title (MB has many live/compilation variants of the same
|
||||||
# song), then sort by release date desc so "newest" tracks
|
# song), then sort by release date desc so "newest" tracks
|
||||||
|
|
|
||||||
|
|
@ -274,7 +274,7 @@ def test_search_tracks_bare_query_uses_browse_path():
|
||||||
client = MusicBrainzSearchClient()
|
client = MusicBrainzSearchClient()
|
||||||
client._client = MagicMock()
|
client._client = MagicMock()
|
||||||
client._client.search_artist.return_value = [_mk_artist('Metallica', 'mb-1', score=100)]
|
client._client.search_artist.return_value = [_mk_artist('Metallica', 'mb-1', score=100)]
|
||||||
client._client.browse_artist_recordings.return_value = [
|
client._client.search_recordings_by_artist_mbid.return_value = [
|
||||||
{'id': 'rec-1', 'title': 'One', 'length': 446000,
|
{'id': 'rec-1', 'title': 'One', 'length': 446000,
|
||||||
'releases': [{'id': 'rel-1', 'title': '...And Justice for All', 'date': '1988',
|
'releases': [{'id': 'rel-1', 'title': '...And Justice for All', 'date': '1988',
|
||||||
'release-group': {'id': 'rg-1', 'primary-type': 'Album'}}],
|
'release-group': {'id': 'rg-1', 'primary-type': 'Album'}}],
|
||||||
|
|
@ -287,7 +287,7 @@ def test_search_tracks_bare_query_uses_browse_path():
|
||||||
|
|
||||||
tracks = client.search_tracks('metallica', limit=10)
|
tracks = client.search_tracks('metallica', limit=10)
|
||||||
|
|
||||||
client._client.browse_artist_recordings.assert_called_once()
|
client._client.search_recordings_by_artist_mbid.assert_called_once()
|
||||||
client._client.search_recording.assert_not_called()
|
client._client.search_recording.assert_not_called()
|
||||||
assert len(tracks) == 2
|
assert len(tracks) == 2
|
||||||
assert {t.name for t in tracks} == {'One', 'Battery'}
|
assert {t.name for t in tracks} == {'One', 'Battery'}
|
||||||
|
|
@ -300,7 +300,7 @@ def test_search_tracks_dedupes_by_title():
|
||||||
client = MusicBrainzSearchClient()
|
client = MusicBrainzSearchClient()
|
||||||
client._client = MagicMock()
|
client._client = MagicMock()
|
||||||
client._client.search_artist.return_value = [_mk_artist('Metallica', 'mb-1', score=100)]
|
client._client.search_artist.return_value = [_mk_artist('Metallica', 'mb-1', score=100)]
|
||||||
client._client.browse_artist_recordings.return_value = [
|
client._client.search_recordings_by_artist_mbid.return_value = [
|
||||||
{'id': 'rec-1', 'title': 'One', 'length': 446000,
|
{'id': 'rec-1', 'title': 'One', 'length': 446000,
|
||||||
'releases': [{'id': 'rel-1', 'title': '...And Justice for All', 'date': '1988'}],
|
'releases': [{'id': 'rel-1', 'title': '...And Justice for All', 'date': '1988'}],
|
||||||
'artist-credit': [{'name': 'Metallica'}]},
|
'artist-credit': [{'name': 'Metallica'}]},
|
||||||
|
|
@ -328,7 +328,7 @@ def test_search_tracks_structured_query_uses_text_path():
|
||||||
|
|
||||||
client._client.search_recording.assert_called_once()
|
client._client.search_recording.assert_called_once()
|
||||||
client._client.search_artist.assert_not_called()
|
client._client.search_artist.assert_not_called()
|
||||||
client._client.browse_artist_recordings.assert_not_called()
|
client._client.search_recordings_by_artist_mbid.assert_not_called()
|
||||||
assert len(tracks) == 1
|
assert len(tracks) == 1
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue