Add Discogs as cover source fallback after MusicBrainz

- _discogs_cover_url(): searches Discogs database/search API by artist+album,
  returns primary image URL; uses DISCOGS_TOKEN if set, else anonymous
- download_discogs_cover(): downloads and saves as folder.jpg (PNG→JPEG via PIL)
- resolve_cover() priority: local → MusicBrainz → Discogs
- music_enricher: pass artist/album to resolve_cover() for Discogs lookup

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Dieter Schlüter 2026-04-29 08:44:04 +02:00
commit 031e595ff7
2 changed files with 64 additions and 0 deletions

View file

@ -221,10 +221,67 @@ def embed_cover(audio_path: Path, cover_path: Path) -> bool:
return False
def _discogs_cover_url(artist: Optional[str], album: Optional[str]) -> Optional[str]:
"""Sucht auf Discogs nach artist+album und gibt die primäre Image-URL zurück."""
if not HAS_REQUESTS or not artist or not album:
return None
import os
token = os.getenv("DISCOGS_TOKEN", "")
headers = {"User-Agent": "MusicMetadataEnricher/1.0"}
if token:
headers["Authorization"] = f"Discogs token={token}"
try:
r = requests.get(
"https://api.discogs.com/database/search",
params={"artist": artist, "release_title": album, "type": "release", "per_page": 3},
headers=headers,
timeout=10,
)
if r.status_code != 200:
return None
results = r.json().get("results", [])
for result in results:
cover = result.get("cover_image") or result.get("thumb")
if cover and "spacer" not in cover:
return cover
except Exception as e:
print(f" ⚠️ Discogs-Suchfehler: {e}", file=sys.stderr)
return None
def download_discogs_cover(artist: Optional[str], album: Optional[str], dest_dir: Path) -> Optional[Path]:
url = _discogs_cover_url(artist, album)
if not url:
return None
dest = dest_dir / "folder.jpg"
try:
r = requests.get(url, timeout=15, headers={"User-Agent": "MusicMetadataEnricher/1.0"})
if r.status_code != 200:
return None
ct = r.headers.get("content-type", "")
if ("png" in ct or url.lower().endswith(".png")) and HAS_PIL:
import io
with Image.open(io.BytesIO(r.content)) as img:
buf = io.BytesIO()
img.convert("RGB").save(buf, format="JPEG", quality=92)
dest.write_bytes(buf.getvalue())
else:
dest.write_bytes(r.content)
if _image_ok(dest):
return dest
dest.unlink(missing_ok=True)
except Exception as e:
print(f" ⚠️ Discogs-Cover-Fehler: {e}", file=sys.stderr)
dest.unlink(missing_ok=True)
return None
def resolve_cover(
image_files: List[Path],
release_mbid: Optional[str],
album_dir: Path,
artist: Optional[str] = None,
album: Optional[str] = None,
) -> tuple[Optional[Path], Optional[str]]:
"""Returns (cover_path, source_label)."""
local = find_local_cover(image_files)
@ -237,4 +294,9 @@ def resolve_cover(
if downloaded:
return downloaded, "musicbrainz"
if artist or album:
downloaded = download_discogs_cover(artist, album, album_dir)
if downloaded:
return downloaded, "discogs"
return None, None

View file

@ -148,6 +148,8 @@ def process_album(
hints.cover_images,
proposal.mbid,
album_dir,
artist=proposal.albumartist,
album=proposal.album,
)
if cover_path and not args.no_cover:
proposal.cover_path = cover_path