mirror of
https://github.com/Dvorinka/SpotifyRecAlg.git
synced 2026-06-04 04:23:02 +00:00
355 lines
11 KiB
Python
355 lines
11 KiB
Python
"""
|
|
MusicBrainz API v2 Client for Universal Music Downloader
|
|
Provides comprehensive music metadata from MusicBrainz database
|
|
"""
|
|
|
|
import logging
|
|
from dataclasses import dataclass
|
|
from typing import Any
|
|
|
|
import aiohttp
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
@dataclass
|
|
class MusicBrainzRecording:
|
|
"""MusicBrainz recording metadata"""
|
|
|
|
mbid: str
|
|
title: str
|
|
artist: str
|
|
artist_mbid: str | None = None
|
|
release: str | None = None
|
|
release_mbid: str | None = None
|
|
isrc: str | None = None
|
|
duration: int | None = None
|
|
position: int | None = None
|
|
genres: list[str] = None
|
|
release_date: str | None = None
|
|
country: str | None = None
|
|
tags: list[str] = None
|
|
cover_art: str | None = None
|
|
|
|
|
|
@dataclass
|
|
class MusicBrainzArtist:
|
|
"""MusicBrainz artist metadata"""
|
|
|
|
mbid: str
|
|
name: str
|
|
sort_name: str | None = None
|
|
disambiguation: str | None = None
|
|
country: str | None = None
|
|
life_span: dict[str, str] | None = None
|
|
genres: list[str] = None
|
|
tags: list[str] = None
|
|
rating: float | None = None
|
|
|
|
|
|
class MusicBrainzClient:
|
|
"""MusicBrainz API v2 client"""
|
|
|
|
def __init__(self, app_name: str = "SwingMusic", app_version: str = "1.0.0"):
|
|
self.base_url = "https://musicbrainz.org/ws/2"
|
|
self.app_name = app_name
|
|
self.app_version = app_version
|
|
self.session = None
|
|
|
|
async def _get_session(self) -> aiohttp.ClientSession:
|
|
"""Get or create aiohttp session"""
|
|
if self.session is None:
|
|
self.session = aiohttp.ClientSession()
|
|
return self.session
|
|
|
|
def _build_url(self, endpoint: str, params: dict[str, str] = None) -> str:
|
|
"""Build MusicBrainz API URL"""
|
|
url = f"{self.base_url}/{endpoint}"
|
|
if params:
|
|
param_string = "&".join([f"{k}={v}" for k, v in params.items()])
|
|
url += f"?{param_string}"
|
|
return url
|
|
|
|
async def lookup_recording(
|
|
self, mbid: str, includes: list[str] = None
|
|
) -> MusicBrainzRecording | None:
|
|
"""Lookup detailed recording information"""
|
|
try:
|
|
session = await self._get_session()
|
|
|
|
params = {}
|
|
if includes:
|
|
params["inc"] = ",".join(includes)
|
|
|
|
url = self._build_url(f"recording/{mbid}", params)
|
|
|
|
headers = {
|
|
"User-Agent": f"{self.app_name}/{self.app_version}",
|
|
"Accept": "application/json",
|
|
}
|
|
|
|
async with session.get(url, headers=headers) as response:
|
|
if response.status == 200:
|
|
data = await response.json()
|
|
return self._parse_recording_response(data)
|
|
else:
|
|
logger.warning(
|
|
f"MusicBrainz recording lookup failed: {response.status}"
|
|
)
|
|
return None
|
|
|
|
except Exception as e:
|
|
logger.error(f"Error looking up MusicBrainz recording: {e}")
|
|
return None
|
|
|
|
async def lookup_artist(
|
|
self, mbid: str, includes: list[str] = None
|
|
) -> MusicBrainzArtist | None:
|
|
"""Lookup detailed artist information"""
|
|
try:
|
|
session = await self._get_session()
|
|
|
|
params = {}
|
|
if includes:
|
|
params["inc"] = ",".join(includes)
|
|
|
|
url = self._build_url(f"artist/{mbid}", params)
|
|
|
|
headers = {
|
|
"User-Agent": f"{self.app_name}/{self.app_version}",
|
|
"Accept": "application/json",
|
|
}
|
|
|
|
async with session.get(url, headers=headers) as response:
|
|
if response.status == 200:
|
|
data = await response.json()
|
|
return self._parse_artist_response(data)
|
|
else:
|
|
logger.warning(
|
|
f"MusicBrainz artist lookup failed: {response.status}"
|
|
)
|
|
return None
|
|
|
|
except Exception as e:
|
|
logger.error(f"Error looking up MusicBrainz artist: {e}")
|
|
return None
|
|
|
|
async def search_recordings(
|
|
self, query: str, artist: str = None, limit: int = 25
|
|
) -> list[MusicBrainzRecording]:
|
|
"""Search for recordings"""
|
|
try:
|
|
session = await self._get_session()
|
|
|
|
params = {"query": f'"{query}"', "limit": str(limit)}
|
|
|
|
if artist:
|
|
params["artist"] = f'"{artist}"'
|
|
|
|
url = self._build_url("recording", params)
|
|
|
|
headers = {
|
|
"User-Agent": f"{self.app_name}/{self.app_version}",
|
|
"Accept": "application/json",
|
|
}
|
|
|
|
async with session.get(url, headers=headers) as response:
|
|
if response.status == 200:
|
|
data = await response.json()
|
|
return self._parse_recording_list_response(data)
|
|
else:
|
|
logger.warning(
|
|
f"MusicBrainz recording search failed: {response.status}"
|
|
)
|
|
return []
|
|
|
|
except Exception as e:
|
|
logger.error(f"Error searching MusicBrainz recordings: {e}")
|
|
return []
|
|
|
|
async def get_artist_releases(
|
|
self, mbid: str, release_types: list[str] = None
|
|
) -> list[str]:
|
|
"""Get all releases for an artist"""
|
|
try:
|
|
session = await self._get_session()
|
|
|
|
params = {}
|
|
if release_types:
|
|
params["type"] = ",".join(release_types)
|
|
|
|
url = self._build_url("release", {"artist": mbid, **params})
|
|
|
|
headers = {
|
|
"User-Agent": f"{self.app_name}/{self.app_version}",
|
|
"Accept": "application/json",
|
|
}
|
|
|
|
async with session.get(url, headers=headers) as response:
|
|
if response.status == 200:
|
|
data = await response.json()
|
|
releases = data.get("releases", [])
|
|
return [release.get("id", "") for release in releases]
|
|
else:
|
|
logger.warning(
|
|
f"MusicBrainz artist releases failed: {response.status}"
|
|
)
|
|
return []
|
|
|
|
except Exception as e:
|
|
logger.error(f"Error getting MusicBrainz artist releases: {e}")
|
|
return []
|
|
|
|
def _parse_recording_response(
|
|
self, data: dict[str, Any]
|
|
) -> MusicBrainzRecording | None:
|
|
"""Parse MusicBrainz recording response"""
|
|
try:
|
|
recording_data = data.get("recording")
|
|
if not recording_data:
|
|
return None
|
|
|
|
# Extract basic info
|
|
title = recording_data.get("title", "")
|
|
|
|
# Extract artist info
|
|
artist_credit = recording_data.get("artist-credit", [])
|
|
artist = (
|
|
artist_credit[0].get("artist", {}).get("name", "")
|
|
if artist_credit
|
|
else ""
|
|
)
|
|
artist_mbid = (
|
|
artist_credit[0].get("artist", {}).get("id") if artist_credit else None
|
|
)
|
|
|
|
# Extract release info
|
|
release_list = recording_data.get("release-list", [])
|
|
release = release_list[0] if release_list else None
|
|
release_title = release.get("title", "") if release else None
|
|
release_mbid = release.get("id") if release else None
|
|
|
|
# Extract ISRC
|
|
isrc_list = recording_data.get("isrc-list", [])
|
|
isrc = isrc_list[0] if isrc_list else None
|
|
|
|
# Extract duration
|
|
duration = recording_data.get("length")
|
|
|
|
# Extract tags and genres
|
|
tag_list = recording_data.get("tag-list", [])
|
|
tags = [tag.get("name", "") for tag in tag_list]
|
|
|
|
# Extract release info
|
|
release_info = recording_data.get("release", {})
|
|
release_date = release_info.get("date")
|
|
country = release_info.get("country")
|
|
|
|
# Extract cover art
|
|
cover_art = None
|
|
if release:
|
|
cover_art_archive = release.get("cover-art-archive", [])
|
|
if cover_art_archive:
|
|
cover_art = cover_art_archive[0].get("image")
|
|
|
|
return MusicBrainzRecording(
|
|
mbid=data.get("id", ""),
|
|
title=title,
|
|
artist=artist,
|
|
artist_mbid=artist_mbid,
|
|
release=release_title,
|
|
release_mbid=release_mbid,
|
|
isrc=isrc,
|
|
duration=duration,
|
|
position=recording_data.get("position"),
|
|
genres=tags,
|
|
release_date=release_date,
|
|
country=country,
|
|
tags=tags,
|
|
cover_art=cover_art,
|
|
)
|
|
|
|
except Exception as e:
|
|
logger.error(f"Error parsing MusicBrainz recording response: {e}")
|
|
return None
|
|
|
|
def _parse_artist_response(self, data: dict[str, Any]) -> MusicBrainzArtist | None:
|
|
"""Parse MusicBrainz artist response"""
|
|
try:
|
|
artist_data = data.get("artist")
|
|
if not artist_data:
|
|
return None
|
|
|
|
name = artist_data.get("name", "")
|
|
sort_name = artist_data.get("sort-name")
|
|
disambiguation = artist_data.get("disambiguation")
|
|
country = artist_data.get("country")
|
|
|
|
# Extract life span
|
|
life_span = artist_data.get("life-span")
|
|
|
|
# Extract tags and genres
|
|
tag_list = artist_data.get("tag-list", [])
|
|
tags = [tag.get("name", "") for tag in tag_list]
|
|
|
|
# Extract rating
|
|
rating = artist_data.get("rating", {}).get("value")
|
|
|
|
return MusicBrainzArtist(
|
|
mbid=data.get("id", ""),
|
|
name=name,
|
|
sort_name=sort_name,
|
|
disambiguation=disambiguation,
|
|
country=country,
|
|
life_span=life_span,
|
|
genres=tags,
|
|
tags=tags,
|
|
rating=rating,
|
|
)
|
|
|
|
except Exception as e:
|
|
logger.error(f"Error parsing MusicBrainz artist response: {e}")
|
|
return None
|
|
|
|
def _parse_recording_list_response(
|
|
self, data: dict[str, Any]
|
|
) -> list[MusicBrainzRecording]:
|
|
"""Parse MusicBrainz recording list response"""
|
|
try:
|
|
recordings = []
|
|
recording_list = data.get("recordings", [])
|
|
|
|
for recording_data in recording_list:
|
|
recording = self._parse_recording_response(
|
|
{"recording": recording_data}
|
|
)
|
|
if recording:
|
|
recordings.append(recording)
|
|
|
|
return recordings
|
|
|
|
except Exception as e:
|
|
logger.error(f"Error parsing MusicBrainz recording list: {e}")
|
|
return []
|
|
|
|
async def close(self):
|
|
"""Close the aiohttp session"""
|
|
if self.session:
|
|
await self.session.close()
|
|
|
|
|
|
# Singleton instance for easy access
|
|
_musicbrainz_client: MusicBrainzClient | None = None
|
|
|
|
|
|
def get_musicbrainz_client() -> MusicBrainzClient:
|
|
"""Get or create the MusicBrainz client"""
|
|
global _musicbrainz_client
|
|
if _musicbrainz_client is None:
|
|
_musicbrainz_client = MusicBrainzClient()
|
|
return _musicbrainz_client
|
|
|
|
|
|
# Global instance
|
|
musicbrainz_client = MusicBrainzClient()
|