Add --from-photo to scan, retry in MB disc loop, temp/ to .gitignore

- scan: new --from-photo <img> option extracts EAN via Vision-LLM,
  then falls through to existing MusicBrainz barcode lookup
- ripper: MB disc loop now retries the same disc on rip failure instead
  of printing "Bitte Album neu starten"; user decline raises RuntimeError
- .gitignore: suppress temp/ directory
- tests: 4 new tests for scan --from-photo (225 total)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Dieter Schlüter 2026-02-19 14:43:37 +01:00
commit a32b0229f5
4 changed files with 147 additions and 49 deletions

View file

@ -211,6 +211,82 @@ class TestScanCommand:
result = runner.invoke(app, ["scan"])
assert result.exit_code == 1
# --- --from-photo ---
def test_scan_from_photo_creates_json(self, tmp_path: Path) -> None:
"""Foto → EAN extrahiert → MusicBrainz-Lookup → JSON."""
img = tmp_path / "cover.jpg"
img.write_bytes(b"fake")
output = tmp_path / "album.json"
fake_album = Album(
artist="Beatles",
album="Abbey Road",
year=1969,
discs=[Disc(disc_number=1, tracks=[Track(track_number=1, title="Come Together")])],
)
mbid = "some-mbid"
with (
patch("musiksammlung.cli.extract_barcode_from_image", return_value="4006408262121"),
patch("musiksammlung.cli.lookup_by_barcode", return_value=(fake_album, mbid)),
):
result = runner.invoke(app, [
"scan", "--from-photo", str(img), "--output", str(output),
])
assert result.exit_code == 0, result.output
assert output.exists()
data = json.loads(output.read_text())
assert data["artist"] == "Beatles"
assert "4006408262121" in result.output
def test_scan_from_photo_file_not_found(self, tmp_path: Path) -> None:
"""Foto existiert nicht → Exit 1."""
result = runner.invoke(app, [
"scan", "--from-photo", str(tmp_path / "nope.jpg"),
])
assert result.exit_code == 1
def test_scan_from_photo_no_barcode_recognized(self, tmp_path: Path) -> None:
"""Vision-LLM erkennt keinen Barcode → Exit 1 mit Fehlermeldung."""
img = tmp_path / "cover.jpg"
img.write_bytes(b"fake")
with patch("musiksammlung.cli.extract_barcode_from_image", return_value=None):
result = runner.invoke(app, [
"scan", "--from-photo", str(img),
])
assert result.exit_code == 1
assert "Kein EAN" in result.output or "Kein EAN" in (result.stderr or "")
def test_scan_from_photo_passes_model_and_url(self, tmp_path: Path) -> None:
"""--vision-model und --url werden an extract_barcode_from_image weitergegeben."""
img = tmp_path / "cover.jpg"
img.write_bytes(b"fake")
fake_album = Album(
artist="A", album="B", year=2000,
discs=[Disc(disc_number=1, tracks=[Track(track_number=1, title="T")])],
)
with (
patch("musiksammlung.cli.extract_barcode_from_image", return_value="1234567890123")
as mock_extract,
patch("musiksammlung.cli.lookup_by_barcode", return_value=(fake_album, "mbid")),
):
runner.invoke(app, [
"scan", "--from-photo", str(img),
"--vision-model", "my-vlm",
"--url", "http://myhost:11434",
"--output", str(tmp_path / "out.json"),
])
mock_extract.assert_called_once_with(
img, model="my-vlm", base_url="http://myhost:11434"
)
# ---------------------------------------------------------------------------
# _rename_album_dir_inplace (via apply --in-place)