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>
493 lines
21 KiB
Python
Executable file
493 lines
21 KiB
Python
Executable file
#!/usr/bin/env python3
|
|
"""test_suite_enricher.py — Unit- und Integrationstests für music_enricher."""
|
|
from __future__ import annotations
|
|
|
|
import sys
|
|
import tempfile
|
|
import traceback
|
|
from pathlib import Path
|
|
from typing import Callable
|
|
|
|
sys.path.insert(0, str(Path(__file__).parent))
|
|
|
|
from models import AlbumScan, TrackHints, AlbumHints
|
|
|
|
RESULTS: list[dict] = []
|
|
|
|
|
|
def record(test_id: str, passed: bool, detail: str = "") -> None:
|
|
RESULTS.append({"id": test_id, "status": "PASS" if passed else "FAIL", "detail": detail})
|
|
|
|
|
|
def run_case(test_id: str, fn: Callable[[], str]) -> None:
|
|
try:
|
|
detail = fn()
|
|
record(test_id, True, detail)
|
|
except Exception:
|
|
record(test_id, False, traceback.format_exc()[:300])
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# hint_extractor Tests
|
|
# ---------------------------------------------------------------------------
|
|
|
|
def test_parse_dirname_artist_album() -> str:
|
|
from hint_extractor import _parse_dirname
|
|
artist, album, year = _parse_dirname("Pink_Floyd_-_The_Wall")
|
|
assert artist and "Pink" in artist, f"artist: {artist}"
|
|
assert album and "Wall" in album, f"album: {album}"
|
|
return f"artist={artist!r}, album={album!r}"
|
|
|
|
|
|
def test_parse_dirname_with_year() -> str:
|
|
from hint_extractor import _parse_dirname
|
|
artist, album, year = _parse_dirname("Abba_-_Greatest_Hits_1992")
|
|
assert year == "1992", f"year: {year}"
|
|
return f"year={year}"
|
|
|
|
|
|
def test_parse_dirname_album_only() -> str:
|
|
from hint_extractor import _parse_dirname
|
|
artist, album, year = _parse_dirname("Beethoven_Complete_Edition")
|
|
assert album is not None, "album should not be None"
|
|
return f"album={album!r}"
|
|
|
|
|
|
def test_parse_filename_track_artist_title() -> str:
|
|
from hint_extractor import _parse_filename
|
|
r = _parse_filename("07 - ABBA - Dancing Queen")
|
|
assert r.get("track") == "07", f"track: {r}"
|
|
assert "ABBA" in r.get("artist", ""), f"artist: {r}"
|
|
assert "Dancing" in r.get("title", ""), f"title: {r}"
|
|
return str(r)
|
|
|
|
|
|
def test_parse_filename_disc_track_title() -> str:
|
|
from hint_extractor import _parse_filename
|
|
r = _parse_filename("2-07 - Bach - Toccata")
|
|
assert r.get("disc") == "2", f"disc: {r}"
|
|
assert r.get("track") == "07", f"track: {r}"
|
|
return str(r)
|
|
|
|
|
|
def test_parse_filename_track_title() -> str:
|
|
from hint_extractor import _parse_filename
|
|
r = _parse_filename("01 - Dancing Queen")
|
|
assert r.get("track") == "01", f"track: {r}"
|
|
assert "Dancing" in r.get("title", ""), f"title: {r}"
|
|
return str(r)
|
|
|
|
|
|
def test_parse_filename_artist_title() -> str:
|
|
from hint_extractor import _parse_filename
|
|
r = _parse_filename("Miles Davis - So What")
|
|
assert "Miles" in r.get("artist", ""), f"artist: {r}"
|
|
assert "What" in r.get("title", ""), f"title: {r}"
|
|
return str(r)
|
|
|
|
|
|
def test_parse_tracklist_numbered() -> str:
|
|
from hint_extractor import _parse_tracklist
|
|
text = "1. Dancing Queen\n2. Waterloo\n3. Fernando"
|
|
tracks = _parse_tracklist(text)
|
|
assert len(tracks) == 3, f"count: {len(tracks)}"
|
|
assert tracks[0]["title"] == "Dancing Queen", f"title: {tracks[0]}"
|
|
return f"{len(tracks)} tracks parsed"
|
|
|
|
|
|
def test_parse_tracklist_with_duration() -> str:
|
|
from hint_extractor import _parse_tracklist
|
|
text = "1-1 Toccata And Fugue 9:17\n1-2 Heartbeat 2:19\n2-1 Finale 5:00"
|
|
tracks = _parse_tracklist(text)
|
|
assert len(tracks) >= 2, f"count: {len(tracks)}"
|
|
assert tracks[0]["disc"] == "1", f"disc: {tracks[0]}"
|
|
return f"{len(tracks)} tracks parsed"
|
|
|
|
|
|
def test_parse_tracklist_with_disc_sections() -> str:
|
|
from hint_extractor import _parse_tracklist
|
|
text = "CD 1\n1. Track A\n2. Track B\nCD 2\n1. Track C"
|
|
tracks = _parse_tracklist(text)
|
|
disc2 = [t for t in tracks if t.get("disc") == "2"]
|
|
assert len(disc2) >= 1, f"disc2: {disc2}"
|
|
return f"{len(tracks)} total, {len(disc2)} on disc 2"
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Scanner Tests
|
|
# ---------------------------------------------------------------------------
|
|
|
|
def test_scanner_classifies_files() -> str:
|
|
from scanner import scan_album
|
|
with tempfile.TemporaryDirectory() as tmpdir:
|
|
root = Path(tmpdir) / "TestAlbum"
|
|
root.mkdir()
|
|
(root / "01 - Song.mp3").write_bytes(b"\x00" * 100)
|
|
(root / "02 - Song.flac").write_bytes(b"\x00" * 100)
|
|
(root / "front.jpg").write_bytes(b"\xff\xd8" + b"\x00" * 100)
|
|
(root / "tracklist.txt").write_text("1. Track One\n2. Track Two")
|
|
(root / "notes.pdf").write_bytes(b"\x00" * 50)
|
|
|
|
scan = scan_album(root)
|
|
assert len(scan.audio_files) == 2, f"audio: {scan.audio_files}"
|
|
assert len(scan.image_files) == 1, f"images: {scan.image_files}"
|
|
assert len(scan.tracklist_files) == 1, f"tracklists: {scan.tracklist_files}"
|
|
return "scan OK: 2 audio, 1 image, 1 tracklist"
|
|
|
|
|
|
def test_scanner_ignores_hidden() -> str:
|
|
from scanner import scan_album
|
|
with tempfile.TemporaryDirectory() as tmpdir:
|
|
root = Path(tmpdir) / "Album"
|
|
root.mkdir()
|
|
(root / "song.mp3").write_bytes(b"\x00" * 100)
|
|
(root / ".hidden.mp3").write_bytes(b"\x00" * 100)
|
|
(root / "_trash.mp3").write_bytes(b"\x00" * 100)
|
|
scan = scan_album(root)
|
|
assert len(scan.audio_files) == 1, f"should ignore hidden: {scan.audio_files}"
|
|
return "hidden files correctly ignored"
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# extract_hints integration
|
|
# ---------------------------------------------------------------------------
|
|
|
|
def test_extract_hints_from_scan() -> str:
|
|
from scanner import scan_album
|
|
from hint_extractor import extract_hints
|
|
with tempfile.TemporaryDirectory() as tmpdir:
|
|
root = Path(tmpdir) / "ABBA_-_Greatest_Hits"
|
|
root.mkdir()
|
|
(root / "01 - ABBA - Dancing Queen.mp3").write_bytes(b"\x00" * 1024)
|
|
(root / "02 - ABBA - Waterloo.mp3").write_bytes(b"\x00" * 1024)
|
|
(root / "tracklist.txt").write_text("1. Dancing Queen\n2. Waterloo\n")
|
|
|
|
scan = scan_album(root)
|
|
hints = extract_hints(scan)
|
|
assert hints.dir_album is not None, "album hint missing"
|
|
assert len(hints.tracks) == 2, f"tracks: {len(hints.tracks)}"
|
|
assert hints.tracklist_text is not None, "tracklist not read"
|
|
return f"hints OK: album={hints.dir_album!r}, {len(hints.tracks)} tracks"
|
|
|
|
|
|
def test_extract_hints_multi_disc() -> str:
|
|
from scanner import scan_album
|
|
from hint_extractor import extract_hints
|
|
with tempfile.TemporaryDirectory() as tmpdir:
|
|
root = Path(tmpdir) / "Bach_Complete"
|
|
(root / "CD1").mkdir(parents=True)
|
|
(root / "CD2").mkdir()
|
|
(root / "CD1" / "01 - Toccata.mp3").write_bytes(b"\x00" * 1024)
|
|
(root / "CD2" / "01 - Fugue.mp3").write_bytes(b"\x00" * 1024)
|
|
|
|
scan = scan_album(root)
|
|
hints = extract_hints(scan)
|
|
disc_nums = {t.disc_number for t in hints.tracks if t.disc_number}
|
|
assert 1 in disc_nums, f"disc 1 missing: {disc_nums}"
|
|
assert 2 in disc_nums, f"disc 2 missing: {disc_nums}"
|
|
return f"disc numbers detected: {disc_nums}"
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Vertical tracklist parser Tests
|
|
# ---------------------------------------------------------------------------
|
|
|
|
def test_vertical_tracklist_basic() -> str:
|
|
from hint_extractor import _normalize_vertical_tracklist
|
|
text = "1\nKatka dovádí\n3:22\n2\nZáludná\n2:15\n3\nPolka pro trubku\n4:01"
|
|
result = _normalize_vertical_tracklist(text)
|
|
assert result is not None, "should recognize vertical format"
|
|
assert "1. Katka" in result, f"got: {result!r}"
|
|
assert "2. Záludná" in result, f"got: {result!r}"
|
|
return f"normalized: {result[:60]!r}"
|
|
|
|
|
|
def test_vertical_tracklist_without_duration() -> str:
|
|
from hint_extractor import _normalize_vertical_tracklist
|
|
text = "1\nFirst Song\n2\nSecond Song\n3\nThird Song"
|
|
result = _normalize_vertical_tracklist(text)
|
|
assert result is not None, "should work without durations"
|
|
assert "1. First Song" in result, f"got: {result!r}"
|
|
return f"no-duration OK: {result[:60]!r}"
|
|
|
|
|
|
def test_vertical_tracklist_not_triggered_for_normal() -> str:
|
|
from hint_extractor import _normalize_vertical_tracklist
|
|
text = "1. Dancing Queen\n2. Waterloo\n3. Fernando"
|
|
result = _normalize_vertical_tracklist(text)
|
|
assert result is None, f"should return None for normal format, got: {result!r}"
|
|
return "correctly returns None for standard format"
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Single-CD disc handling Tests
|
|
# ---------------------------------------------------------------------------
|
|
|
|
def test_single_cd_tracklist_match() -> str:
|
|
"""Track-Nummer-Match darf nicht disc_num erfordern (Single-CD hat disc=None)."""
|
|
from hint_extractor import _parse_tracklist
|
|
from models import TrackHints, AlbumHints, AlbumScan
|
|
from pathlib import Path
|
|
import tempfile
|
|
|
|
with tempfile.TemporaryDirectory() as tmpdir:
|
|
root = Path(tmpdir) / "Tufaranka_-_Katka_dovadi"
|
|
root.mkdir()
|
|
(root / "01_-_Tufaranka_-_AudioTrack_01.mp3").write_bytes(b"\x00" * 100)
|
|
(root / "tracklist.txt").write_text("1\nKatka dovádí\n3:22\n2\nZáludná\n2:15\n3\nPolka\n4:01")
|
|
|
|
from scanner import scan_album
|
|
from hint_extractor import extract_hints
|
|
scan = scan_album(root)
|
|
hints = extract_hints(scan, use_ocr=False)
|
|
track = hints.tracks[0]
|
|
assert track.title == "Katka dovádí", f"expected tracklist title, got: {track.title!r}"
|
|
return f"single-CD match OK: title={track.title!r}"
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Genre normalization Tests
|
|
# ---------------------------------------------------------------------------
|
|
|
|
def test_genre_normalize_german() -> str:
|
|
from metadata_resolver import normalize_genre
|
|
assert normalize_genre("volksmusik") == "Folk", "volksmusik → Folk"
|
|
assert normalize_genre("klassik") == "Classical", "klassik → Classical"
|
|
assert normalize_genre("marschmusik") == "March", "marschmusik → March"
|
|
return "German genres normalized correctly"
|
|
|
|
|
|
def test_genre_normalize_english_variants() -> str:
|
|
from metadata_resolver import normalize_genre
|
|
assert normalize_genre("rhythm and blues") == "R&B"
|
|
assert normalize_genre("rock and roll") == "Rock 'n' Roll"
|
|
return "English variants normalized correctly"
|
|
|
|
|
|
def test_genre_normalize_titlecase() -> str:
|
|
from metadata_resolver import normalize_genre
|
|
assert normalize_genre("JAZZ") == "Jazz", f"got: {normalize_genre('JAZZ')!r}"
|
|
assert normalize_genre("folk") == "Folk", f"got: {normalize_genre('folk')!r}"
|
|
assert normalize_genre("Big Band") == "Big Band" # unchanged
|
|
return "Titlecase normalization OK"
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# _is_classical() Tests
|
|
# ---------------------------------------------------------------------------
|
|
|
|
def test_is_classical_by_genre() -> str:
|
|
from executor import _is_classical
|
|
assert _is_classical("Gardiner", "Bach", "Classical"), "Classical genre should trigger"
|
|
assert _is_classical("Herreweghe", "Handel", "Baroque"), "Baroque should trigger"
|
|
return "genre-based detection OK"
|
|
|
|
|
|
def test_is_classical_by_composer() -> str:
|
|
from executor import _is_classical
|
|
assert _is_classical("Gardiner", "Bach", ""), "Bach as track_artist should trigger"
|
|
assert _is_classical("Hurford", "beethoven", ""), "beethoven should trigger"
|
|
return "composer-name detection OK"
|
|
|
|
|
|
def test_is_classical_false_for_pop() -> str:
|
|
from executor import _is_classical
|
|
assert not _is_classical("Trini Lopez", "Trini Lopez", "Pop"), "same artist = not classical"
|
|
assert not _is_classical("ABBA", "ABBA", "Pop"), "ABBA is not classical"
|
|
assert not _is_classical("Trini Lopez", "", "R&B"), "empty track_artist = not classical"
|
|
return "pop albums correctly not classical"
|
|
|
|
|
|
def test_is_classical_false_for_folk() -> str:
|
|
from executor import _is_classical
|
|
assert not _is_classical("Tufaranka", "Tufaranka", "Folk"), "Folk is not classical"
|
|
return "Folk correctly not classical"
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# cover normalize Tests
|
|
# ---------------------------------------------------------------------------
|
|
|
|
def test_normalize_cover_renames_front_jpg() -> str:
|
|
from cover_handler import normalize_cover_to_folder_jpg
|
|
import tempfile, shutil
|
|
with tempfile.TemporaryDirectory() as tmpdir:
|
|
root = Path(tmpdir)
|
|
front = root / "Front.jpg"
|
|
front.write_bytes(b"\xff\xd8" + b"\x00" * 200)
|
|
result = normalize_cover_to_folder_jpg(front)
|
|
assert result.name == "folder.jpg", f"expected folder.jpg, got {result.name!r}"
|
|
assert (root / "folder.jpg").exists(), "folder.jpg should exist"
|
|
assert not front.exists(), "Front.jpg should be gone"
|
|
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
|
|
# ---------------------------------------------------------------------------
|
|
|
|
def test_proposed_filename_single_disc() -> str:
|
|
from executor import _proposed_filename
|
|
from models import TrackProposal
|
|
from pathlib import Path
|
|
# Pop schema: albumartist == track artist → TT_-_Artist_-_Title
|
|
tp = TrackProposal(path=Path("dummy.mp3"), title="Dancing Queen",
|
|
artist="ABBA", track_number=1, disc_number=None)
|
|
name = _proposed_filename(tp, ".mp3", albumartist="ABBA")
|
|
assert name == "01_-_ABBA_-_Dancing_Queen.mp3", f"got: {name!r}"
|
|
return name
|
|
|
|
|
|
def test_proposed_filename_multi_disc() -> str:
|
|
from executor import _proposed_filename
|
|
from models import TrackProposal
|
|
from pathlib import Path
|
|
# Classical schema: albumartist (performer) ≠ track artist (composer)
|
|
tp = TrackProposal(path=Path("dummy.flac"), title="Toccata",
|
|
artist="Bach", track_number=7, disc_number=2)
|
|
name = _proposed_filename(tp, ".flac", albumartist="Gardiner")
|
|
assert name == "2-07_-_Gardiner_-_Bach_-_Toccata.flac", f"got: {name!r}"
|
|
return name
|
|
|
|
|
|
def test_proposed_filename_sanitizes_chars() -> str:
|
|
from executor import _proposed_filename
|
|
from models import TrackProposal
|
|
from pathlib import Path
|
|
tp = TrackProposal(path=Path("x.mp3"), title='Track: "Live" / Today',
|
|
artist="Artist?", track_number=3, disc_number=None)
|
|
name = _proposed_filename(tp, ".mp3")
|
|
assert "/" not in name and ":" not in name, f"unsafe chars in: {name!r}"
|
|
return name
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Runner
|
|
# ---------------------------------------------------------------------------
|
|
|
|
def main() -> None:
|
|
print("🧪 Starte Music Metadata Enricher Tests...")
|
|
|
|
cases = [
|
|
("UNIT_01_parse_dirname_artist_album", test_parse_dirname_artist_album),
|
|
("UNIT_02_parse_dirname_with_year", test_parse_dirname_with_year),
|
|
("UNIT_03_parse_dirname_album_only", test_parse_dirname_album_only),
|
|
("UNIT_04_parse_filename_track_artist_title", test_parse_filename_track_artist_title),
|
|
("UNIT_05_parse_filename_disc_track_title", test_parse_filename_disc_track_title),
|
|
("UNIT_06_parse_filename_track_title", test_parse_filename_track_title),
|
|
("UNIT_07_parse_filename_artist_title", test_parse_filename_artist_title),
|
|
("UNIT_08_parse_tracklist_numbered", test_parse_tracklist_numbered),
|
|
("UNIT_09_parse_tracklist_with_duration", test_parse_tracklist_with_duration),
|
|
("UNIT_10_parse_tracklist_disc_sections", test_parse_tracklist_with_disc_sections),
|
|
("UNIT_11_scanner_classifies_files", test_scanner_classifies_files),
|
|
("UNIT_12_scanner_ignores_hidden", test_scanner_ignores_hidden),
|
|
("UNIT_13_extract_hints_from_scan", test_extract_hints_from_scan),
|
|
("UNIT_14_extract_hints_multi_disc", test_extract_hints_multi_disc),
|
|
("UNIT_15_proposed_filename_single_disc", test_proposed_filename_single_disc),
|
|
("UNIT_16_proposed_filename_multi_disc", test_proposed_filename_multi_disc),
|
|
("UNIT_17_proposed_filename_sanitizes_chars", test_proposed_filename_sanitizes_chars),
|
|
# Neue Tests
|
|
("UNIT_18_vertical_tracklist_basic", test_vertical_tracklist_basic),
|
|
("UNIT_19_vertical_tracklist_no_duration", test_vertical_tracklist_without_duration),
|
|
("UNIT_20_vertical_tracklist_no_false_pos", test_vertical_tracklist_not_triggered_for_normal),
|
|
("UNIT_21_single_cd_tracklist_match", test_single_cd_tracklist_match),
|
|
("UNIT_22_genre_normalize_german", test_genre_normalize_german),
|
|
("UNIT_23_genre_normalize_english", test_genre_normalize_english_variants),
|
|
("UNIT_24_genre_normalize_titlecase", test_genre_normalize_titlecase),
|
|
("UNIT_25_is_classical_by_genre", test_is_classical_by_genre),
|
|
("UNIT_26_is_classical_by_composer", test_is_classical_by_composer),
|
|
("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:
|
|
run_case(test_id, fn)
|
|
|
|
print("=" * 70)
|
|
for r in RESULTS:
|
|
icon = "✅" if r["status"] == "PASS" else "❌"
|
|
detail = r["detail"][:100] + "..." if len(r["detail"]) > 100 else r["detail"]
|
|
print(f"{icon} [{r['status']}] {r['id']} {detail}")
|
|
print("=" * 70)
|
|
|
|
passed = sum(1 for r in RESULTS if r["status"] == "PASS")
|
|
total = len(RESULTS)
|
|
print(f"📊 {passed}/{total} Tests erfolgreich")
|
|
sys.exit(0 if passed == total else 1)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|