Skip to content

musicbrainz

MusicBrainz API client for music metadata retrieval.

This module provides the MusicBrainz-specific implementation for fetching and scoring music releases from the MusicBrainz database.

LifeSpan

Bases: TypedDict

Type definition for artist life span data from MusicBrainz.

Area

Bases: TypedDict

Type definition for area data from MusicBrainz.

Alias

Bases: TypedDict

Type definition for artist alias from MusicBrainz.

ArtistCredit

Bases: TypedDict

Type definition for artist credit from MusicBrainz.

TextRepresentation

Bases: TypedDict

Type definition for text representation from MusicBrainz.

ReleaseEvent

Bases: TypedDict

Type definition for release event from MusicBrainz.

Label

Bases: TypedDict

Type definition for a label from MusicBrainz.

LabelInfo

Bases: TypedDict

Type definition for label info from MusicBrainz.

CoverArtArchive

Bases: TypedDict

Type definition for cover arts archive from MusicBrainz.

Recording

Bases: TypedDict

Type definition for recording from MusicBrainz.

Track

Bases: TypedDict

Type definition for a track from MusicBrainz.

Medium

Bases: TypedDict

Type definition for medium from MusicBrainz.

Release

Bases: TypedDict

Type definition for release from MusicBrainz.

ReleaseGroup

Bases: TypedDict

Type definition for a release group from MusicBrainz.

MusicBrainzReleasesResponse

Bases: TypedDict

Type definition for MusicBrainz releases API response.

MusicBrainzClient

MusicBrainzClient(
    console_logger,
    error_logger,
    make_api_request_func,
    score_release_func,
    analytics,
)

Bases: BaseApiClient

MusicBrainz API client for fetching music metadata.

Initialize MusicBrainz client.

Parameters:

Name Type Description Default
console_logger Logger

Logger for console output

required
error_logger Logger

Logger for error messages

required
make_api_request_func Callable[..., Awaitable[dict[str, Any] | None]]

Function to make API requests with rate limiting

required
score_release_func Callable[..., float]

Function to score releases for originality

required
analytics Analytics

Analytics instance for performance tracking

required
Source code in src/services/api/musicbrainz.py
def __init__(
    self,
    console_logger: logging.Logger,
    error_logger: logging.Logger,
    make_api_request_func: Callable[..., Awaitable[dict[str, Any] | None]],
    score_release_func: Callable[..., float],
    analytics: Analytics,
) -> None:
    """Initialize MusicBrainz client.

    Args:
        console_logger: Logger for console output
        error_logger: Logger for error messages
        make_api_request_func: Function to make API requests with rate limiting
        score_release_func: Function to score releases for originality
        analytics: Analytics instance for performance tracking

    """
    super().__init__(console_logger, error_logger)
    self._make_api_request = make_api_request_func
    self._score_original_release = score_release_func
    self.analytics = analytics

get_artist_info async

get_artist_info(artist_norm, include_aliases=False)

Get artist information from MusicBrainz.

Uses non-fielded search to match both canonical names and aliases. Combined with script_detection.detect_primary_script(), enables canonical name resolution for non-Latin artists. Issue #102.

Parameters:

Name Type Description Default
artist_norm str

Normalized artist name

required
include_aliases bool

Whether to include aliases in the response

False

Returns:

Type Description
dict[str, Any] | None

Artist information or None if not found

Source code in src/services/api/musicbrainz.py
@track_instance_method("musicbrainz_artist_search")
async def get_artist_info(self, artist_norm: str, include_aliases: bool = False) -> dict[str, Any] | None:
    """Get artist information from MusicBrainz.

    Uses non-fielded search to match both canonical names and aliases.
    Combined with script_detection.detect_primary_script(), enables
    canonical name resolution for non-Latin artists. Issue #102.

    Args:
        artist_norm: Normalized artist name
        include_aliases: Whether to include aliases in the response

    Returns:
        Artist information or None if not found

    """
    search_url = f"{MUSICBRAINZ_BASE_URL}/artist/"
    # Non-fielded search matches aliases; see _perform_primary_search for script detection
    params = {
        "query": self._escape_lucene(artist_norm),
        "fmt": "json",
        "limit": "1",
    }

    if include_aliases:
        params["inc"] = "aliases"

    try:
        response = await self._make_api_request("musicbrainz", search_url, params=params)
        if response and response.get("artists"):
            return cast(MBApiData, response["artists"][0])
    except (OSError, ValueError, RuntimeError, KeyError, TypeError) as e:
        self.error_logger.exception("Failed to get artist info for '%s': %s", artist_norm, e)

    return None

get_artist_activity_period async

get_artist_activity_period(artist_norm)

Get artist's activity period from MusicBrainz.

Parameters:

Name Type Description Default
artist_norm str

Normalized artist name

required

Returns:

Type Description
tuple[str | None, str | None]

Tuple of (begin_year, end_year) or (None, None) if not found

Source code in src/services/api/musicbrainz.py
@track_instance_method("musicbrainz_artist_period")
async def get_artist_activity_period(self, artist_norm: str) -> tuple[str | None, str | None]:
    """Get artist's activity period from MusicBrainz.

    Args:
        artist_norm: Normalized artist name

    Returns:
        Tuple of (begin_year, end_year) or (None, None) if not found

    """
    artist_info = await self.get_artist_info(artist_norm)

    if not artist_info:
        return None, None

    life_span = artist_info.get("life-span", {})
    begin = life_span.get("begin")
    end = life_span.get("end")

    # Extract years from dates
    begin_year = self._extract_year_from_date(begin) if begin else None
    end_year = self._extract_year_from_date(end) if end else None

    return begin_year, end_year

get_artist_region async

get_artist_region(artist_norm)

Get an artist's region/country from MusicBrainz.

Parameters:

Name Type Description Default
artist_norm str

Normalized artist name

required

Returns:

Type Description
str | None

Region/country name or None if not found

Source code in src/services/api/musicbrainz.py
@track_instance_method("musicbrainz_artist_region")
async def get_artist_region(self, artist_norm: str) -> str | None:
    """Get an artist's region/country from MusicBrainz.

    Args:
        artist_norm: Normalized artist name

    Returns:
        Region/country name or None if not found

    """
    artist_info = await self.get_artist_info(artist_norm)

    if not artist_info:
        return None

    # Try different area fields
    for area_field in ["area", "begin-area", "end-area"]:
        area = artist_info.get(area_field)
        if area and area.get("name"):
            return cast("str", area["name"])

    return None

get_canonical_artist_name async

get_canonical_artist_name(artist_norm)

Get the canonical MusicBrainz artist name from an alias.

Used by _perform_primary_search for non-Latin scripts (detected via script_detection.detect_primary_script). Issue #102.

Parameters:

Name Type Description Default
artist_norm str

Normalized artist name (may be alias)

required

Returns:

Type Description
str | None

Canonical artist name or None if not found

Source code in src/services/api/musicbrainz.py
async def get_canonical_artist_name(self, artist_norm: str) -> str | None:
    """Get the canonical MusicBrainz artist name from an alias.

    Used by _perform_primary_search for non-Latin scripts (detected via
    script_detection.detect_primary_script). Issue #102.

    Args:
        artist_norm: Normalized artist name (may be alias)

    Returns:
        Canonical artist name or None if not found

    """
    artist_info = await self.get_artist_info(artist_norm)
    return artist_info.get("name") if artist_info else None

get_scored_releases async

get_scored_releases(
    artist_norm,
    album_norm,
    artist_region,
    *,
    artist_orig=None,
    album_orig=None
)

Retrieve and score releases from MusicBrainz.

Uses multiple search strategies with fallbacks if precise queries fail.

Parameters:

Name Type Description Default
artist_norm str

Normalized artist name

required
album_norm str

Normalized album name

required
artist_region str | None

Artist's region for scoring

required
artist_orig str | None

Original artist name (before normalization)

None
album_orig str | None

Original album name (before normalization)

None

Returns:

Type Description
list[ScoredRelease]

List of scored releases sorted by score

Source code in src/services/api/musicbrainz.py
@track_instance_method("musicbrainz_release_search")
async def get_scored_releases(
    self,
    artist_norm: str,
    album_norm: str,
    artist_region: str | None,
    *,
    artist_orig: str | None = None,
    album_orig: str | None = None,
) -> list[ScoredRelease]:
    """Retrieve and score releases from MusicBrainz.

    Uses multiple search strategies with fallbacks if precise queries fail.

    Args:
        artist_norm: Normalized artist name
        album_norm: Normalized album name
        artist_region: Artist's region for scoring
        artist_orig: Original artist name (before normalization)
        album_orig: Original album name (before normalization)

    Returns:
        List of scored releases sorted by score

    """
    self.console_logger.debug(
        "[musicbrainz] Start search | artist_orig='%s' artist_norm='%s', album_orig='%s', album_norm='%s'",
        artist_orig or artist_norm,
        artist_norm,
        album_orig or album_norm,
        album_norm,
    )

    try:
        # Attempt primary search first
        all_release_groups = await self._perform_primary_search(artist_norm, album_norm) or await self._perform_fallback_searches(
            artist_norm, album_norm, artist_orig, album_orig
        )

        if not all_release_groups:
            self.console_logger.warning("[musicbrainz] All search attempts failed for '%s - %s'.", artist_norm, album_norm)
            return []

        # Fetch releases for found release groups
        release_results = await self._fetch_releases_for_groups(all_release_groups)

        # Process and score the releases
        scored_releases = self._process_and_score_releases(release_results, artist_norm, album_norm, artist_region)

    except (OSError, ValueError, RuntimeError, KeyError, TypeError, AttributeError, IndexError) as e:
        self.error_logger.exception("Error fetching from MusicBrainz for '%s - %s': %s", artist_norm, album_norm, e)
        return []

    return sorted(scored_releases, key=lambda x: x["score"], reverse=True)