A MusicBrainz album resolves its art at RELEASE-GROUP scope even for a concrete release (musicbrainz_search _release_to_album -> _cached_art prefers the rg mbid). On the Cover Art Archive a release-group front is a single REPRESENTATIVE cover (CAA picks one release to stand for the group, ~always the standard edition), so a special edition like "Clair Obscur: Expedition 33 (Gustave Edition)" got the standard art baked into cover.jpg + embedded tags at download time. Add core/metadata/caa_art.fetch_release_preferred_art: try the specific release own /release/<mbid>/front first, fall back to the existing release-group/provider URL only when the release has no art of its own (404 -> None). Wire it into both download_cover_art (cover.jpg) and embed_album_art_metadata (tags) so they stay in sync. min_bytes defaults to 0 so the fallback keeps its prior behavior — this can only ever ADD a better edition match, never strip a cover that showed before. Caveat (documented, not fixed here): this only helps when the resolved release IS the right edition. If the upstream match picked the standard release/group, that is the separate canonical-version matching problem. Tests: pure helper (prefers release art, falls back on 404/tiny/exception, no-mbid passes through, nothing-available -> None). 667 metadata/artwork/deezer/hifi tests pass.
74 lines
3.2 KiB
Python
74 lines
3.2 KiB
Python
"""Cover Art Archive helper: prefer a pinned release's OWN cover over the
|
|
release-group representative.
|
|
|
|
On the Cover Art Archive a release-group ``front`` is a single REPRESENTATIVE
|
|
cover — CAA designates one release in the group to stand for the whole thing,
|
|
which is almost always the standard / most-common edition. So when a download
|
|
has pinned a SPECIFIC release (e.g. a "Gustave Edition" the user picked), using
|
|
the release-group cover silently swaps in the standard art.
|
|
|
|
This helper tries the specific release's own ``/release/<mbid>/front`` first and
|
|
only falls back to the caller's existing URL (a release-group representative or a
|
|
provider cover) when the release has no art of its own — so it can only ever
|
|
*improve* on today's behaviour, never strip a cover that was already showing.
|
|
|
|
Pure: the network fetch is injected, so the preference logic is unit-testable.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from typing import Callable, Optional, Tuple
|
|
|
|
COVER_ART_ARCHIVE_URL = "https://coverartarchive.org"
|
|
|
|
|
|
def caa_front_url(mbid: Optional[str], scope: str = "release", size: int = 1200) -> Optional[str]:
|
|
"""Build a Cover Art Archive front-cover URL, or None for a falsy mbid.
|
|
``scope`` is 'release' (a specific edition) or 'release-group' (the group's
|
|
representative). ``size`` selects the CDN thumbnail (e.g. 250/500/1200); 0
|
|
requests the bare ``/front`` original."""
|
|
if not mbid:
|
|
return None
|
|
if scope not in ("release", "release-group"):
|
|
scope = "release"
|
|
suffix = f"-{size}" if size else ""
|
|
return f"{COVER_ART_ARCHIVE_URL}/{scope}/{mbid}/front{suffix}"
|
|
|
|
|
|
def fetch_release_preferred_art(
|
|
release_mbid: Optional[str],
|
|
fallback_url: Optional[str],
|
|
*,
|
|
fetch_fn: Callable[[str], Tuple[Optional[bytes], Optional[str]]],
|
|
size: int = 1200,
|
|
min_bytes: int = 0,
|
|
) -> Tuple[Optional[bytes], Optional[str], Optional[str]]:
|
|
"""Fetch the best cover, preferring the specific release's own art.
|
|
|
|
Tries ``/release/<release_mbid>/front`` first; on any miss (no such art,
|
|
404, or smaller than ``min_bytes``) falls back to ``fallback_url`` (a
|
|
release-group / provider cover). ``fetch_fn(url) -> (bytes|None, mime|None)``;
|
|
it is expected to return ``(None, None)`` on a 404, which is how a release
|
|
with no art of its own advances to the fallback. ``min_bytes`` defaults to 0
|
|
(accept any non-empty image) to preserve the fallback path's prior behaviour;
|
|
callers can raise it to reject placeholder/error images. Returns
|
|
``(bytes|None, mime|None, url_used|None)``. Never raises for a missing cover —
|
|
a failed candidate just advances to the next, so coverage never regresses."""
|
|
candidates = []
|
|
release_url = caa_front_url(release_mbid, "release", size) if release_mbid else None
|
|
if release_url:
|
|
candidates.append(release_url)
|
|
if fallback_url and fallback_url not in candidates:
|
|
candidates.append(fallback_url)
|
|
|
|
for url in candidates:
|
|
try:
|
|
data, mime = fetch_fn(url)
|
|
except Exception:
|
|
data, mime = None, None
|
|
if data and len(data) > min_bytes:
|
|
return data, mime, url
|
|
return None, None, None
|
|
|
|
|
|
__all__ = ["COVER_ART_ARCHIVE_URL", "caa_front_url", "fetch_release_preferred_art"]
|