Add 4 new cover/tracklist sources: MB back cover, iTunes, Last.fm, Discogs tracklist
cover_handler.py: - _download_image(): shared helper replaces duplicated download logic - download_back_cover(): fetches back cover from MusicBrainz CAA (/back endpoint), saves as back.jpg; skips if already present - _itunes_cover_url() / download_itunes_cover(): iTunes Search API (no auth), requests 600x600 artwork; fallback after Discogs - _lastfm_cover_url() / download_lastfm_cover(): Last.fm album.getinfo (LASTFM_API_KEY env var); last cover fallback, skips placeholder images - resolve_cover(): extended with iTunes → Last.fm fallback chain metadata_resolver.py: - _discogs_get_tracklist(): fetches full Discogs release via REST API, parses tracklist[] including heading-based disc detection - _lastfm_tracklist(): fetches Last.fm album.getinfo tracks (LASTFM_API_KEY) - resolve(): uses Discogs tracklist → Last.fm tracklist as fallback when MusicBrainz returns no tracks; LASTFM_API_KEY added to env var block music_enricher.py: - process_album(): calls download_back_cover() after execute_album() when MBID known New cover priority: local → MusicBrainz front → Discogs → iTunes → Last.fm New tracklist priority: local → YouTube → MusicBrainz → Discogs → Last.fm → OCR Test suite: 29 → 33 tests (all pass) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
071f4c5e1d
commit
80472653b4
4 changed files with 273 additions and 33 deletions
|
|
@ -322,6 +322,74 @@ def test_normalize_cover_renames_front_jpg() -> str:
|
|||
return "Front.jpg → folder.jpg rename OK"
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# New cover sources Tests
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def test_itunes_cover_url_format() -> str:
|
||||
from cover_handler import _itunes_cover_url
|
||||
# Ohne echten Netzwerkaufruf: testen ob Funktion bei leeren Eingaben None zurückgibt
|
||||
assert _itunes_cover_url(None, None) is None, "None inputs → None"
|
||||
assert _itunes_cover_url("", "") is None, "empty inputs → None"
|
||||
return "iTunes URL helper: None-Handling OK"
|
||||
|
||||
|
||||
def test_discogs_tracklist_format() -> str:
|
||||
from metadata_resolver import _discogs_get_tracklist
|
||||
# Simuliere API-Antwort-Parsing mit einem Testfall
|
||||
import unittest.mock as mock
|
||||
fake_response = {
|
||||
"tracklist": [
|
||||
{"position": "1", "type_": "track", "title": "Song A", "duration": "3:20"},
|
||||
{"type_": "heading", "title": "CD 2"},
|
||||
{"position": "1", "type_": "track", "title": "Song B", "duration": "4:00"},
|
||||
]
|
||||
}
|
||||
with mock.patch("requests.get") as mock_get:
|
||||
mock_get.return_value.status_code = 200
|
||||
mock_get.return_value.json.return_value = fake_response
|
||||
tracks = _discogs_get_tracklist(12345)
|
||||
assert len(tracks) == 2, f"expected 2 tracks, got {len(tracks)}"
|
||||
assert tracks[0]["title"] == "Song A", f"track 0: {tracks[0]}"
|
||||
assert tracks[1]["disc"] == 2, f"disc should be 2 after heading: {tracks[1]}"
|
||||
return f"Discogs tracklist format OK: {len(tracks)} tracks"
|
||||
|
||||
|
||||
def test_lastfm_tracklist_format() -> str:
|
||||
from metadata_resolver import _lastfm_tracklist
|
||||
import unittest.mock as mock, os
|
||||
fake_response = {
|
||||
"album": {
|
||||
"tracks": {
|
||||
"track": [
|
||||
{"name": "Track One", "@attr": {"rank": "1"}, "artist": {"name": "Artist"}},
|
||||
{"name": "Track Two", "@attr": {"rank": "2"}, "artist": {"name": "Artist"}},
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
with mock.patch.dict(os.environ, {"LASTFM_API_KEY": "testkey"}):
|
||||
with mock.patch("requests.get") as mock_get:
|
||||
mock_get.return_value.status_code = 200
|
||||
mock_get.return_value.json.return_value = fake_response
|
||||
tracks = _lastfm_tracklist("Artist", "Album")
|
||||
assert len(tracks) == 2, f"expected 2 tracks, got {len(tracks)}"
|
||||
assert tracks[0]["title"] == "Track One", f"track 0: {tracks[0]}"
|
||||
assert tracks[0]["number"] == 1, f"rank/number: {tracks[0]}"
|
||||
return f"Last.fm tracklist format OK: {len(tracks)} tracks"
|
||||
|
||||
|
||||
def test_back_cover_skips_if_exists() -> str:
|
||||
from cover_handler import download_back_cover
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
root = Path(tmpdir)
|
||||
back = root / "back.jpg"
|
||||
back.write_bytes(b"\xff\xd8" + b"\x00" * 200)
|
||||
result = download_back_cover("fake-mbid", root)
|
||||
assert result == back, f"should return existing back.jpg: {result}"
|
||||
return "back cover skip-if-exists OK"
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# executor Tests
|
||||
# ---------------------------------------------------------------------------
|
||||
|
|
@ -399,6 +467,10 @@ def main() -> None:
|
|||
("UNIT_27_is_classical_false_pop", test_is_classical_false_for_pop),
|
||||
("UNIT_28_is_classical_false_folk", test_is_classical_false_for_folk),
|
||||
("UNIT_29_normalize_cover_renames", test_normalize_cover_renames_front_jpg),
|
||||
("UNIT_30_itunes_url_none_handling", test_itunes_cover_url_format),
|
||||
("UNIT_31_discogs_tracklist_format", test_discogs_tracklist_format),
|
||||
("UNIT_32_lastfm_tracklist_format", test_lastfm_tracklist_format),
|
||||
("UNIT_33_back_cover_skip_if_exists", test_back_cover_skips_if_exists),
|
||||
]
|
||||
|
||||
for test_id, fn in cases:
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue