first commit

This commit is contained in:
Tomas Dvorak
2026-04-13 17:46:58 +02:00
commit 6e8fedf534
234 changed files with 53808 additions and 0 deletions
+354
View File
@@ -0,0 +1,354 @@
"""
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()