EAN-first workflow in interactive_rip + GnuDB DYEAR/DGENRE parsing
EAN is now asked before the album name. On MusicBrainz hit, the ripper enters an auto-rip flow (no album name prompt, no CDDB confirm, disc count from MB data). On miss/empty EAN, the previous fallback flow (album name → CDDB confirm) is preserved. GnuDB responses now parse DYEAR and DGENRE fields into a new CddbResult NamedTuple. Album model gains an optional genre field. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
90713452c2
commit
8b449493cd
5 changed files with 510 additions and 196 deletions
|
|
@ -292,7 +292,7 @@ class TestRenameFiles:
|
|||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# interactive_rip – EAN/Barcode-Integration
|
||||
# interactive_rip – EAN-First Workflow
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
_MB_ALBUM = Album(
|
||||
|
|
@ -310,87 +310,43 @@ _MB_ALBUM = Album(
|
|||
],
|
||||
)
|
||||
|
||||
_MB_ALBUM_2DISC = Album(
|
||||
artist="The Beatles",
|
||||
album="White Album",
|
||||
year=1968,
|
||||
discs=[
|
||||
Disc(disc_number=1, tracks=[
|
||||
Track(track_number=1, title="Back in the U.S.S.R."),
|
||||
]),
|
||||
Disc(disc_number=2, tracks=[
|
||||
Track(track_number=1, title="Birthday"),
|
||||
]),
|
||||
],
|
||||
)
|
||||
|
||||
_CDDB_TRACKS = [
|
||||
TrackInfo(1, "The Beatles", "Come Together"),
|
||||
TrackInfo(2, "The Beatles", "Something"),
|
||||
]
|
||||
|
||||
|
||||
def _make_rip_disc_mock(tracks: list[TrackInfo] | None = None):
|
||||
"""Erstellt ein Mock für rip_disc, das sofort zurückgibt."""
|
||||
mock = MagicMock(return_value=(Path("/tmp/disc"), "Album1", tracks or []))
|
||||
return mock
|
||||
class TestInteractiveRipEanFirst:
|
||||
"""Tests für EAN-First Workflow in interactive_rip.
|
||||
|
||||
Neuer Flow:
|
||||
1. EAN abfragen
|
||||
2. MB-Treffer → Auto-Rip (kein Albumname, kein CDDB-Confirm)
|
||||
3. Kein Treffer → Fallback (Albumname, CDDB-Confirm wie bisher)
|
||||
"""
|
||||
|
||||
class TestInteractiveRipBarcode:
|
||||
"""Tests für EAN/Barcode-Abfrage in interactive_rip."""
|
||||
# ── Auto-Rip (MusicBrainz-Treffer) ──
|
||||
|
||||
def _run(self, tmp_path: Path, inputs: list[str], mb_album=None, mb_error=None):
|
||||
"""Führt interactive_rip mit gemocktem I/O aus."""
|
||||
config = RipperConfig(output_dir=tmp_path)
|
||||
input_iter = iter(inputs)
|
||||
|
||||
with (
|
||||
patch("musiksammlung.ripper.rip_disc", return_value=(tmp_path, None, _CDDB_TRACKS)),
|
||||
patch("builtins.input", side_effect=input_iter),
|
||||
patch(
|
||||
"musiksammlung.ripper.lookup_by_barcode",
|
||||
side_effect=mb_error if mb_error else (lambda _: mb_album) if mb_album else None,
|
||||
) as mock_lookup,
|
||||
):
|
||||
interactive_rip(config)
|
||||
|
||||
return mock_lookup
|
||||
|
||||
def test_ean_skipped_does_not_call_musicbrainz(self, tmp_path: Path) -> None:
|
||||
"""Kein EAN → lookup_by_barcode wird nicht aufgerufen."""
|
||||
def test_mb_hit_auto_rip_single_disc(self, tmp_path: Path) -> None:
|
||||
"""MB-Treffer → Auto-Rip: eine Disc, kein Albumname-Prompt, album.json aus MB."""
|
||||
inputs = [
|
||||
"Abbey Road", # album name
|
||||
"", # EAN: leer → überspringen
|
||||
"1", # disc number
|
||||
"j", # CDDB korrekt?
|
||||
"n", # next CD?
|
||||
"n", # next album?
|
||||
]
|
||||
config = RipperConfig(output_dir=tmp_path)
|
||||
with (
|
||||
patch("musiksammlung.ripper.rip_disc", return_value=(tmp_path, None, _CDDB_TRACKS)),
|
||||
patch("builtins.input", side_effect=iter(inputs)),
|
||||
patch("musiksammlung.ripper.lookup_by_barcode") as mock_lookup,
|
||||
):
|
||||
interactive_rip(config)
|
||||
|
||||
mock_lookup.assert_not_called()
|
||||
|
||||
def test_ean_triggers_musicbrainz_lookup(self, tmp_path: Path) -> None:
|
||||
"""EAN eingegeben → lookup_by_barcode wird mit der EAN aufgerufen."""
|
||||
inputs = [
|
||||
"Abbey Road", # album name
|
||||
"0602557360561", # EAN
|
||||
"1", # disc number
|
||||
"j", # CDDB korrekt?
|
||||
"n", # next CD?
|
||||
"n", # next album?
|
||||
]
|
||||
config = RipperConfig(output_dir=tmp_path)
|
||||
with (
|
||||
patch("musiksammlung.ripper.rip_disc", return_value=(tmp_path, None, _CDDB_TRACKS)),
|
||||
patch("builtins.input", side_effect=iter(inputs)),
|
||||
patch("musiksammlung.ripper.lookup_by_barcode", return_value=_MB_ALBUM) as mock_lookup,
|
||||
):
|
||||
interactive_rip(config)
|
||||
|
||||
mock_lookup.assert_called_once_with("0602557360561")
|
||||
|
||||
def test_musicbrainz_data_saved_to_json(self, tmp_path: Path) -> None:
|
||||
"""MusicBrainz-Daten werden in album.json gespeichert."""
|
||||
inputs = [
|
||||
"", # album name: leer → Default
|
||||
"0602557360561", # EAN
|
||||
"1", # disc number
|
||||
"j", # CDDB korrekt?
|
||||
"n", # next CD?
|
||||
"n", # next album?
|
||||
"0602557360561", # EAN → MB-Treffer
|
||||
"", # "CD 1/1 einlegen und Enter drücken"
|
||||
"n", # next album?
|
||||
]
|
||||
config = RipperConfig(output_dir=tmp_path)
|
||||
with (
|
||||
|
|
@ -408,15 +364,105 @@ class TestInteractiveRipBarcode:
|
|||
assert data["album"] == "Abbey Road"
|
||||
assert data["year"] == 1969
|
||||
|
||||
def test_musicbrainz_failure_falls_back_to_cddb(self, tmp_path: Path) -> None:
|
||||
"""MusicBrainz-Fehler → CDDB-Daten werden verwendet, kein Absturz."""
|
||||
def test_mb_hit_auto_rip_multi_disc(self, tmp_path: Path) -> None:
|
||||
"""MB-Treffer mit 2 Discs → 2x Disc-Insert-Prompt, album.json aus MB."""
|
||||
inputs = [
|
||||
"Abbey Road", # album name
|
||||
"0000000000000", # EAN (kein Treffer)
|
||||
"1", # disc number
|
||||
"j", # CDDB korrekt?
|
||||
"n", # next CD?
|
||||
"n", # next album?
|
||||
"1234567890123", # EAN → MB-Treffer (2 Discs)
|
||||
"", # "CD 1/2 einlegen und Enter drücken"
|
||||
"", # "CD 2/2 einlegen und Enter drücken"
|
||||
"n", # next album?
|
||||
]
|
||||
config = RipperConfig(output_dir=tmp_path)
|
||||
with (
|
||||
patch("musiksammlung.ripper.rip_disc", return_value=(tmp_path, None, _CDDB_TRACKS)),
|
||||
patch("builtins.input", side_effect=iter(inputs)),
|
||||
patch("musiksammlung.ripper.lookup_by_barcode", return_value=_MB_ALBUM_2DISC),
|
||||
):
|
||||
interactive_rip(config)
|
||||
|
||||
json_path = tmp_path / "White_Album" / "album.json"
|
||||
assert json_path.exists()
|
||||
import json
|
||||
data = json.loads(json_path.read_text())
|
||||
assert data["artist"] == "The Beatles"
|
||||
assert data["album"] == "White Album"
|
||||
assert data["year"] == 1968
|
||||
assert len(data["discs"]) == 2
|
||||
|
||||
def test_mb_hit_triggers_lookup(self, tmp_path: Path) -> None:
|
||||
"""EAN eingegeben → lookup_by_barcode wird aufgerufen."""
|
||||
inputs = [
|
||||
"0602557360561", # EAN
|
||||
"", # disc insert
|
||||
"n", # next album?
|
||||
]
|
||||
config = RipperConfig(output_dir=tmp_path)
|
||||
with (
|
||||
patch("musiksammlung.ripper.rip_disc", return_value=(tmp_path, None, _CDDB_TRACKS)),
|
||||
patch("builtins.input", side_effect=iter(inputs)),
|
||||
patch("musiksammlung.ripper.lookup_by_barcode", return_value=_MB_ALBUM) as mock_lookup,
|
||||
):
|
||||
interactive_rip(config)
|
||||
|
||||
mock_lookup.assert_called_once_with("0602557360561")
|
||||
|
||||
def test_mb_hit_renames_with_cddb_tracks(self, tmp_path: Path) -> None:
|
||||
"""Auto-Rip nutzt CDDB-Tracks zum Umbenennen der Dateien."""
|
||||
inputs = [
|
||||
"0602557360561",
|
||||
"", # disc insert
|
||||
"n",
|
||||
]
|
||||
config = RipperConfig(output_dir=tmp_path)
|
||||
disc_dir = tmp_path / "Abbey_Road" / "CD1"
|
||||
disc_dir.mkdir(parents=True)
|
||||
(disc_dir / "track01.flac").touch()
|
||||
(disc_dir / "track02.flac").touch()
|
||||
|
||||
with (
|
||||
patch("musiksammlung.ripper.rip_disc", return_value=(disc_dir, None, _CDDB_TRACKS)),
|
||||
patch("builtins.input", side_effect=iter(inputs)),
|
||||
patch("musiksammlung.ripper.lookup_by_barcode", return_value=_MB_ALBUM),
|
||||
):
|
||||
interactive_rip(config)
|
||||
|
||||
# _rename_files wurde aufgerufen (tracks vorhanden)
|
||||
# Dateien existieren schon vorher, rename findet in _rename_files statt
|
||||
assert (tmp_path / "Abbey_Road" / "album.json").exists()
|
||||
|
||||
# ── Fallback (kein MB-Treffer / EAN leer) ──
|
||||
|
||||
def test_ean_empty_falls_back_to_album_prompt(self, tmp_path: Path) -> None:
|
||||
"""Leere EAN → Fallback: Albumname wird abgefragt."""
|
||||
inputs = [
|
||||
"", # EAN: leer → überspringen
|
||||
"Abbey Road", # album name (Fallback-Flow)
|
||||
"1", # disc number
|
||||
"j", # CDDB korrekt?
|
||||
"n", # next CD?
|
||||
"n", # next album?
|
||||
]
|
||||
config = RipperConfig(output_dir=tmp_path)
|
||||
with (
|
||||
patch("musiksammlung.ripper.rip_disc", return_value=(tmp_path, None, _CDDB_TRACKS)),
|
||||
patch("builtins.input", side_effect=iter(inputs)),
|
||||
patch("musiksammlung.ripper.lookup_by_barcode") as mock_lookup,
|
||||
):
|
||||
interactive_rip(config)
|
||||
|
||||
mock_lookup.assert_not_called()
|
||||
json_path = tmp_path / "Abbey_Road" / "album.json"
|
||||
assert json_path.exists()
|
||||
|
||||
def test_mb_miss_falls_back_to_album_prompt(self, tmp_path: Path) -> None:
|
||||
"""MB-Fehler → Fallback: Albumname wird abgefragt, CDDB-Daten genutzt."""
|
||||
inputs = [
|
||||
"0000000000000", # EAN (kein Treffer)
|
||||
"Abbey Road", # album name (Fallback-Flow)
|
||||
"1", # disc number
|
||||
"j", # CDDB korrekt?
|
||||
"n", # next CD?
|
||||
"n", # next album?
|
||||
]
|
||||
config = RipperConfig(output_dir=tmp_path)
|
||||
with (
|
||||
|
|
@ -429,53 +475,54 @@ class TestInteractiveRipBarcode:
|
|||
):
|
||||
interactive_rip(config) # darf nicht werfen
|
||||
|
||||
# CDDB-basierte album.json wurde erstellt
|
||||
json_path = tmp_path / "Abbey_Road" / "album.json"
|
||||
assert json_path.exists()
|
||||
|
||||
def test_album_name_taken_from_musicbrainz_when_default(self, tmp_path: Path) -> None:
|
||||
"""Albumnamen wird von MusicBrainz übernommen wenn kein Name manuell eingegeben."""
|
||||
def test_fallback_cddb_confirm_rejected(self, tmp_path: Path) -> None:
|
||||
"""Fallback: CDDB-Daten abgelehnt → keine album.json."""
|
||||
inputs = [
|
||||
"", # album name: leer → Default (Album1)
|
||||
"0602557360561", # EAN
|
||||
"1", # disc number
|
||||
"j", # CDDB korrekt?
|
||||
"n",
|
||||
"n",
|
||||
"", # EAN: leer
|
||||
"TestAlbum", # album name
|
||||
"1", # disc number
|
||||
"n", # CDDB korrekt? → nein
|
||||
"n", # next CD?
|
||||
"n", # next album?
|
||||
]
|
||||
config = RipperConfig(output_dir=tmp_path)
|
||||
with (
|
||||
patch("musiksammlung.ripper.rip_disc", return_value=(tmp_path, None, _CDDB_TRACKS)),
|
||||
patch("builtins.input", side_effect=iter(inputs)),
|
||||
patch("musiksammlung.ripper.lookup_by_barcode", return_value=_MB_ALBUM),
|
||||
patch("musiksammlung.ripper.lookup_by_barcode"),
|
||||
):
|
||||
interactive_rip(config)
|
||||
|
||||
# Verzeichnis und JSON nach MusicBrainz-Namen benannt
|
||||
assert (tmp_path / "Abbey_Road" / "album.json").exists()
|
||||
# Keine album.json, da CDDB-Daten verworfen
|
||||
json_path = tmp_path / "TestAlbum" / "album.json"
|
||||
assert not json_path.exists()
|
||||
|
||||
def test_manual_album_name_kept_when_not_default(self, tmp_path: Path) -> None:
|
||||
"""Manuell eingegebener Albumname → album.json liegt im manuellen Verzeichnis."""
|
||||
def test_fallback_multiple_discs(self, tmp_path: Path) -> None:
|
||||
"""Fallback: Mehrere CDs für ein Album."""
|
||||
inputs = [
|
||||
"Mein Album", # manuell eingegebener Name
|
||||
"0602557360561", # EAN
|
||||
"1", # disc number
|
||||
"j", # CDDB korrekt?
|
||||
"n",
|
||||
"n",
|
||||
"", # EAN: leer
|
||||
"TestAlbum", # album name
|
||||
"1", # disc 1
|
||||
"j", # CDDB korrekt?
|
||||
"y", # next CD? → ja
|
||||
"2", # disc 2
|
||||
"j", # CDDB korrekt?
|
||||
"n", # next CD?
|
||||
"n", # next album?
|
||||
]
|
||||
config = RipperConfig(output_dir=tmp_path)
|
||||
with (
|
||||
patch("musiksammlung.ripper.rip_disc", return_value=(tmp_path, None, _CDDB_TRACKS)),
|
||||
patch("builtins.input", side_effect=iter(inputs)),
|
||||
patch("musiksammlung.ripper.lookup_by_barcode", return_value=_MB_ALBUM),
|
||||
patch("musiksammlung.ripper.lookup_by_barcode"),
|
||||
):
|
||||
interactive_rip(config)
|
||||
|
||||
# album.json liegt im Verzeichnis mit dem manuellen Namen (dort liegen die Dateien),
|
||||
# Inhalt stammt aber aus MusicBrainz
|
||||
json_path = tmp_path / "Mein_Album" / "album.json"
|
||||
json_path = tmp_path / "TestAlbum" / "album.json"
|
||||
assert json_path.exists()
|
||||
import json
|
||||
data = json.loads(json_path.read_text())
|
||||
assert data["artist"] == "The Beatles"
|
||||
assert len(data["discs"]) == 2
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue