Skip to content

applemusic

iTunes Search API client for retrieving album release information.

This module provides access to Apple's iTunes Search API, which offers: - Album release dates and metadata - Artist information - No authentication required (public API) - Useful for new releases that may not be in other databases yet

The iTunes Search API is particularly valuable for: - Recent releases (Apple gets data directly from labels) - Albums available on Apple Music/iTunes Store - Official release dates and metadata validation

API Reference: https://developer.apple.com/library/archive/documentation/AudioVideo/Conceptual/iTuneSearchAPI/

AppleMusicClient

AppleMusicClient(
    console_logger,
    error_logger,
    make_api_request_func,
    score_release_func,
    *,
    country_code="US",
    entity="album",
    limit=50
)

Client for iTunes Search API operations.

Provides album search and metadata retrieval using Apple's public iTunes Search API. No authentication required - this is a public API service.

Initialize the iTunes Search API 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[..., Coroutine[Any, Any, dict[str, Any] | None]]

Injected function for making API requests

required
score_release_func Callable[..., float]

Injected function for scoring releases

required
country_code str

Country code for search results (default: US)

'US'
entity str

Type of content to search for (default: album)

'album'
limit int

Maximum number of results to return (default: 50)

50
Source code in src/services/api/applemusic.py
def __init__(
    self,
    console_logger: logging.Logger,
    error_logger: logging.Logger,
    make_api_request_func: Callable[..., Coroutine[Any, Any, dict[str, Any] | None]],
    score_release_func: Callable[..., float],
    *,
    country_code: str = "US",
    entity: str = "album",
    limit: int = 50,
) -> None:
    """Initialize the iTunes Search API client.

    Args:
        console_logger: Logger for console output
        error_logger: Logger for error messages
        make_api_request_func: Injected function for making API requests
        score_release_func: Injected function for scoring releases
        country_code: Country code for search results (default: US)
        entity: Type of content to search for (default: album)
        limit: Maximum number of results to return (default: 50)

    """
    self.console_logger = console_logger
    self.error_logger = error_logger
    self.make_api_request_func = make_api_request_func
    self.score_release_func = score_release_func

    # iTunes Search API configuration
    self.base_url = f"{ITUNES_BASE_URL}/search"
    self.country_code = country_code
    self.entity = entity
    # Ensure limit is positive and within iTunes API bounds [1, 200]
    validated_limit = max(1, limit)
    self.limit = min(validated_limit, 200)

    self.console_logger.debug(
        "iTunes Search API client initialized (country=%s, entity=%s, limit=%d)",
        self.country_code,
        self.entity,
        self.limit,
    )

get_scored_releases async

get_scored_releases(artist_norm, album_norm)

Get scored releases from iTunes Search API with lookup fallback.

Uses the search API first. If no results are found, falls back to looking up the artist and fetching all their albums.

Parameters:

Name Type Description Default
artist_norm str

Normalized artist name

required
album_norm str

Normalized album name

required

Returns:

Type Description
list[ScoredRelease]

List of scored releases from iTunes Search API

Source code in src/services/api/applemusic.py
async def get_scored_releases(
    self,
    artist_norm: str,
    album_norm: str,
) -> list[ScoredRelease]:
    """Get scored releases from iTunes Search API with lookup fallback.

    Uses the search API first. If no results are found, falls back to
    looking up the artist and fetching all their albums.

    Args:
        artist_norm: Normalized artist name
        album_norm: Normalized album name

    Returns:
        List of scored releases from iTunes Search API

    """
    self.console_logger.debug(
        "[itunes] get_scored_releases called with artist='%s', album='%s'",
        artist_norm,
        album_norm,
    )
    try:
        # Build search query - iTunes Search works best with "artist album" format
        search_term = f"{artist_norm} {album_norm}".strip()

        # Build request parameters (aiohttp handles URL encoding automatically)
        params = {
            "term": search_term,
            "country": self.country_code,
            "entity": self.entity,
            "limit": str(self.limit),
        }

        self.console_logger.debug(
            "[itunes] Searching for: '%s' (country=%s)",
            search_term,
            self.country_code,
        )

        self.console_logger.debug(
            "[itunes] About to call make_api_request_func with url=%s, params=%s",
            self.base_url,
            params,
        )

        # Make the API request
        self.console_logger.debug("[itunes] Calling make_api_request_func now...")
        response_data = await self.make_api_request_func(
            api_name="itunes",
            url=self.base_url,
            params=params,
            max_retries=2,
            base_delay=0.5,
        )
        self.console_logger.debug(
            "[itunes] make_api_request_func completed, response_data type: %s",
            type(response_data),
        )

        self.console_logger.debug(
            "[itunes] make_api_request_func returned: %s",
            "data" if response_data else "None/empty",
        )

        # Get results from search API, fallback to artist lookup if empty
        results = (response_data.get("results", []) if response_data else []) or await self._try_lookup_fallback(artist_norm, search_term)

        if not results:
            self.console_logger.info("[itunes] No results found for query: '%s'", search_term)
            return []

        # Filter and score results
        return self._process_api_results(results, artist_norm, album_norm, search_term)

    except (OSError, ValueError, RuntimeError) as e:
        self.error_logger.warning(
            "[itunes] Error fetching data for '%s - %s': %s",
            artist_norm,
            album_norm,
            e,
        )
        return []

get_artist_start_year async

get_artist_start_year(artist_norm)

Get artist's earliest release year from iTunes.

iTunes doesn't have an explicit artist start date, so we use the earliest album release year as a proxy.

Parameters:

Name Type Description Default
artist_norm str

Normalized artist name

required

Returns:

Type Description
int | None

Earliest release year found, or None if no releases found

Source code in src/services/api/applemusic.py
async def get_artist_start_year(self, artist_norm: str) -> int | None:
    """Get artist's earliest release year from iTunes.

    iTunes doesn't have an explicit artist start date, so we use
    the earliest album release year as a proxy.

    Args:
        artist_norm: Normalized artist name

    Returns:
        Earliest release year found, or None if no releases found

    """
    self.console_logger.debug(
        "[itunes] get_artist_start_year called for artist='%s'",
        artist_norm,
    )

    try:
        response_data = await self._fetch_artist_albums(artist_norm)
        if not response_data:
            return None

        results = response_data.get("results", [])
        if not results:
            self.console_logger.debug(
                "[itunes] No albums found for artist: '%s'",
                artist_norm,
            )
            return None

        years = self._extract_release_years(results, artist_norm)
        if not years:
            self.console_logger.debug(
                "[itunes] No valid release years found for artist: '%s'",
                artist_norm,
            )
            return None

        earliest_year = min(years)
        self.console_logger.debug(
            "[itunes] Artist '%s' earliest release year: %d (from %d albums)",
            artist_norm,
            earliest_year,
            len(years),
        )
        return earliest_year

    except (OSError, ValueError, RuntimeError) as e:
        self.error_logger.warning(
            "[itunes] Error fetching artist start year for '%s': %s",
            artist_norm,
            e,
        )
        return None