"""MusicBrainz + Cover Art Archive clients — both free, no API key. MusicBrainz rate limit is 1 req/sec for unauthenticated clients; callers must pace requests. """ import requests from typing import Optional, Dict _HEADERS = {"User-Agent": "StreamHoard/1.0 (self-hosted media library)"} MB_BASE = "https://musicbrainz.org/ws/2" COVER_ART_BASE = "https://coverartarchive.org" def lookup_release(artist: str, album: str) -> Optional[Dict]: """Best-effort match for an artist+album against MusicBrainz's release index.""" query = f'release:"{album}" AND artist:"{artist}"' try: r = requests.get(f"{MB_BASE}/release/", params={"query": query, "fmt": "json", "limit": 1}, headers=_HEADERS, timeout=15) r.raise_for_status() releases = (r.json() or {}).get("releases") or [] except Exception: return None if not releases: return None rel = releases[0] return { "mbid": rel.get("id"), "title": rel.get("title") or album, "artist": (rel.get("artist-credit") or [{}])[0].get("name") or artist, "year": (rel.get("date") or "")[:4] or None, } def cover_art_url(mbid: str) -> Optional[str]: """Returns the front cover URL if the Cover Art Archive has one for this release, else None.""" url = f"{COVER_ART_BASE}/release/{mbid}/front" try: r = requests.head(url, headers=_HEADERS, timeout=10, allow_redirects=True) if r.status_code == 200: return r.url except Exception: pass return None