MusicBrainz: Construct Cover Art URLs instead of HEAD-probing them

Cover Art Archive URLs are deterministic from the MBID: a GET either
307-redirects to the image or returns 404. The previous adapter fired
`requests.head(timeout=3)` per search result to probe for the image
first. 10 results × 3s worst-case = up to 30s of blocking HEAD calls
before a search returned.

The probe was defensive overhead — the frontend already handles 404 via
`<img onerror>` fallback. Building the URL deterministically and letting
the browser load it lazily collapses the tail latency to the real MB API
calls (artist-search + browse = ~3s at the 1-rps rate limit).

Also prefer release-group scope over per-release scope when both are
available — release-group covers every edition of an album, so the hit
rate is noticeably higher than pinning to a specific regional release.

Removes now-unused `self._art_cache` and the `requests` import.
This commit is contained in:
Broque Thomas 2026-04-24 08:15:49 -07:00
parent d7e232e01c
commit 73df2951e5

View file

@ -7,7 +7,6 @@ Album art is fetched from Cover Art Archive (free, linked by release MBID).
""" """
import threading import threading
import requests
from dataclasses import dataclass from dataclasses import dataclass
from typing import Any, Dict, List, Optional from typing import Any, Dict, List, Optional
@ -60,29 +59,24 @@ class Album:
external_urls: Optional[Dict[str, str]] = None external_urls: Optional[Dict[str, str]] = None
def _get_cover_art_url(release_mbid: str) -> Optional[str]: def _cover_art_url(mbid: str, scope: str = 'release') -> Optional[str]:
"""Fetch album art URL from Cover Art Archive. Returns None if not available.""" """Build a Cover Art Archive URL without hitting the network.
try:
# CAA redirects to the actual image URL — just get the front image URL
url = f"{COVER_ART_ARCHIVE_URL}/release/{release_mbid}/front-250"
resp = requests.head(url, timeout=3, allow_redirects=True)
if resp.status_code == 200:
return resp.url # The redirect target is the actual image
return None
except Exception:
return None
CAA URLs are deterministic from the MBID: the endpoint either 307-redirects
to the image or returns 404. Previously we fired `requests.head(timeout=3)`
per result during search 10 results × 3s worst-case = up to 30s of
blocking HEAD calls before a search returned. The frontend's <img> tag
handles the 404 case via onerror fallback, so the HEAD round-trip was
pure overhead.
def _get_release_group_art(release_group_mbid: str) -> Optional[str]: `scope` is 'release' (most specific) or 'release-group' (covers all
"""Fetch album art from release group (covers all editions).""" editions better hit rate).
try: """
url = f"{COVER_ART_ARCHIVE_URL}/release-group/{release_group_mbid}/front-250" if not mbid:
resp = requests.head(url, timeout=3, allow_redirects=True)
if resp.status_code == 200:
return resp.url
return None
except Exception:
return None return None
if scope not in ('release', 'release-group'):
scope = 'release'
return f"{COVER_ART_ARCHIVE_URL}/{scope}/{mbid}/front-250"
def _extract_artist_credit(artist_credit) -> List[str]: def _extract_artist_credit(artist_credit) -> List[str]:
@ -121,7 +115,6 @@ class MusicBrainzSearchClient:
# which is what MusicBrainz wants. Version stays generic ("2") — # which is what MusicBrainz wants. Version stays generic ("2") —
# the exact UI minor version would add noise to every request. # the exact UI minor version would add noise to every request.
self._client = MusicBrainzClient("SoulSync", "2") self._client = MusicBrainzClient("SoulSync", "2")
self._art_cache: Dict[str, Optional[str]] = {} # mbid -> url
# Per-instance cache for "top artist MBID for this query". The # Per-instance cache for "top artist MBID for this query". The
# backend fires artists/albums/tracks searches in parallel against # backend fires artists/albums/tracks searches in parallel against
# one client instance, and albums+tracks both need the same artist # one client instance, and albums+tracks both need the same artist
@ -133,15 +126,17 @@ class MusicBrainzSearchClient:
self._artist_mbid_lock = threading.Lock() self._artist_mbid_lock = threading.Lock()
def _cached_art(self, release_mbid: str, release_group_mbid: str = '') -> Optional[str]: def _cached_art(self, release_mbid: str, release_group_mbid: str = '') -> Optional[str]:
"""Get cover art with caching. Tries release first, then release group.""" """Build a Cover Art Archive URL for a release / release-group MBID.
if release_mbid in self._art_cache:
return self._art_cache[release_mbid]
url = _get_cover_art_url(release_mbid) Prefers release-group scope when provided better hit rate because
if not url and release_group_mbid: it covers all editions of the same album. No network call; the
url = _get_release_group_art(release_group_mbid) frontend's <img onerror> fallback handles 404s.
self._art_cache[release_mbid] = url """
return url preferred = release_group_mbid or release_mbid
if not preferred:
return None
scope = 'release-group' if release_group_mbid else 'release'
return _cover_art_url(preferred, scope=scope)
# Score threshold for user-facing search results. MusicBrainz returns a # Score threshold for user-facing search results. MusicBrainz returns a
# Lucene score 0-100 on every match; exact name/alias hits score 100, # Lucene score 0-100 on every match; exact name/alias hits score 100,