Rename cover files: frontcover.jpg → front.jpg, backcover.jpg → back.jpg
Shorter, cleaner filenames consistent with Jellyfin conventions. Updated all references in source, tests, and documentation. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
fd8de16bdd
commit
1ca88b0d6d
9 changed files with 145 additions and 68 deletions
|
|
@ -73,8 +73,8 @@ EAN/Barcode eingeben ODER CD-Hülle fotografieren
|
|||
│
|
||||
├─ MusicBrainz-Treffer → Auto-Rip
|
||||
│ Zeige: Artist – Album (Year), N Discs, M Tracks
|
||||
│ CAA-Cover herunterladen (frontcover.jpg + backcover.jpg)
|
||||
│ Vision-LLM im Hintergrund starten (analysiert backcover.jpg) ←─┐
|
||||
│ CAA-Cover herunterladen (front.jpg + back.jpg)
|
||||
│ Vision-LLM im Hintergrund starten (analysiert back.jpg) ←─┐
|
||||
│ Für jede Disc: "CD 1/3 einlegen, Enter" → Rip → Rename │ parallel
|
||||
│ Vision-LLM-Ergebnis abwarten (max. 120 s, oft schon fertig) ───┘
|
||||
│ Priorität: Vision-LLM > MusicBrainz
|
||||
|
|
@ -154,8 +154,8 @@ EAN/Barcode (Enter = überspringen): 028943753227
|
|||
MusicBrainz-Suche nach Barcode 028943753227 ...
|
||||
✓ Herbert von Karajan – Beethoven: 9 Symphonies (1963, 5 Disc(s), 50 Tracks)
|
||||
|
||||
Cover-Download: frontcover.jpg, backcover.jpg
|
||||
[Vision-LLM analysiert backcover.jpg im Hintergrund...]
|
||||
Cover-Download: front.jpg, back.jpg
|
||||
[Vision-LLM analysiert back.jpg im Hintergrund...]
|
||||
|
||||
CD 1/5 einlegen und Enter drücken (9 Tracks) ...
|
||||
|
||||
|
|
@ -263,7 +263,7 @@ Im Fallback-Modus (kein MusicBrainz-Treffer) kann derselbe laufende Scanner-Serv
|
|||
|
||||
Nach dem Hochladen fährt das Programm automatisch fort und startet die Vision-LLM-Analyse im Hintergrund. Sobald der Ripping-Prozess abgeschlossen ist, wird das Ergebnis übernommen.
|
||||
|
||||
Bei MusicBrainz-Treffern wird `backcover.jpg` aus dem Cover Art Archive heruntergeladen — kein Foto nötig.
|
||||
Bei MusicBrainz-Treffern wird `back.jpg` aus dem Cover Art Archive heruntergeladen — kein Foto nötig.
|
||||
|
||||
### Ergebnis-Verzeichnis
|
||||
|
||||
|
|
@ -275,8 +275,8 @@ Bei MusicBrainz-Treffern wird `backcover.jpg` aus dem Cover Art Archive herunter
|
|||
01_-_Allegro_con_brio_-_Karajan.flac
|
||||
02_-_Andante_con_moto_-_Karajan.flac
|
||||
...
|
||||
frontcover.jpg ← aus CAA oder automatisch hinzugefügt
|
||||
backcover.jpg ← aus CAA oder Smartphone-Foto
|
||||
front.jpg ← aus CAA oder automatisch hinzugefügt
|
||||
back.jpg ← aus CAA oder Smartphone-Foto
|
||||
album.json ← automatisch gespeichert
|
||||
```
|
||||
→ Am Ende zeigt das Programm den fertigen `apply`-Befehl an (copy-paste-fähig).
|
||||
|
|
@ -469,10 +469,10 @@ Im Album-Verzeichnis werden folgende Dateinamen erwartet:
|
|||
|
||||
| Datei | Zweck |
|
||||
|-------|-------|
|
||||
| `frontcover.jpg` oder `frontcover.png` | Front-Cover |
|
||||
| `backcover.jpg` oder `backcover.png` | Rückseiten-Cover |
|
||||
| `front.jpg` oder `front.png` | Front-Cover |
|
||||
| `back.jpg` oder `back.png` | Rückseiten-Cover |
|
||||
|
||||
Symbolische Links auf diese Namen sind erlaubt. `apply` kopiert die mit `--front`/`--back` angegebenen Bilder automatisch als `frontcover.jpg` bzw. `backcover.jpg` ins Album-Verzeichnis und bettet das Frontcover anschließend in alle Audio-Dateien ein (skaliert auf max. 500 px).
|
||||
Symbolische Links auf diese Namen sind erlaubt. `apply` kopiert die mit `--front`/`--back` angegebenen Bilder automatisch als `front.jpg` bzw. `back.jpg` ins Album-Verzeichnis und bettet das Frontcover anschließend in alle Audio-Dateien ein (skaliert auf max. 500 px).
|
||||
|
||||
Ist bereits ein `frontcover.*` vorhanden (z.B. bei erneutem `apply`), wird es ohne `--front`-Option verwendet.
|
||||
|
||||
|
|
@ -505,8 +505,8 @@ Ausgabe:
|
|||
|
||||
```
|
||||
Verzeichnis: ~/rip/Beethoven_Sinfonien
|
||||
frontcover: frontcover.jpg
|
||||
backcover: backcover.jpg
|
||||
frontcover: front.jpg
|
||||
backcover: back.jpg
|
||||
|
||||
CD1/
|
||||
[♪] 01_-_Allegro_con_brio_-_Karajan.flac
|
||||
|
|
|
|||
77
docs/refactoring_plan.md
Normal file
77
docs/refactoring_plan.md
Normal file
|
|
@ -0,0 +1,77 @@
|
|||
# Refactoring-Plan: Workflow-Phasen in ripper.py
|
||||
|
||||
*Stand: 2026-02-19 — Entwurf zur Diskussion*
|
||||
|
||||
## Aktueller Zustand
|
||||
|
||||
`interactive_rip` ist eine ~400-Zeilen-Funktion mit zwei verschränkten Pfaden
|
||||
(MB-Hit vs. Fallback), die alles in einem monolithischen Block erledigt.
|
||||
Die `apply`-Seite ist bereits sauber getrennt (organizer → tagger → playlist).
|
||||
|
||||
## Vorschlag: 7 logische Phasen
|
||||
|
||||
### Phase 1: Album-Identifikation (EAN / Barcode)
|
||||
- EAN-Eingabe per Tastatur oder Foto-Scan
|
||||
- Vision-LLM: Barcode aus Foto extrahieren
|
||||
- MusicBrainz-Lookup per EAN → Album-Struktur + MBID
|
||||
- Ergebnis: `ean`, `mb_album`, `mb_mbid` (oder alles None)
|
||||
|
||||
### Phase 2: Cover-Beschaffung
|
||||
- CAA-Download (front.jpg, back.jpg) per MBID
|
||||
- Foto-Upload per Scanner-Server (Backcover, ggf. Frontcover)
|
||||
- `prepare_cover()` für Jellyfin-Format
|
||||
- Ergebnis: Cover-Dateien im album_root
|
||||
|
||||
### Phase 3: Backcover-Analyse (parallel zu Phase 4)
|
||||
- Vision-LLM auf Backcover-Foto → vollständige Album-Metadaten
|
||||
- Läuft als Background-Thread während des Rippens
|
||||
- Ergebnis: `vision_album: Album | None`
|
||||
|
||||
### Phase 4: Audio-Ripping (pro Disc)
|
||||
- Disc einlegen → abcde starten (cdparanoia + Encoder)
|
||||
- CDDB-Lookup als Nebenprodukt von abcde
|
||||
- CDDB-Bestätigung durch User (Fallback-Pfad)
|
||||
- Audio-Dateien in album_root/CDn/
|
||||
- Ergebnis: Audio-Dateien + `cddb_tracks` pro Disc
|
||||
|
||||
### Phase 5: Metadaten-Zusammenführung (Priorität: Vision > MB > CDDB)
|
||||
- Vision-LLM-Ergebnis einsammeln (Timeout 120s)
|
||||
- Beste Quelle auswählen nach Priorität
|
||||
- Album-Name, Artist, Year, Genre, Tracklist konsolidieren
|
||||
- Ergebnis: `final_album: Album`
|
||||
|
||||
### Phase 6: album.json erzeugen + Apply-Hint
|
||||
- `final_album` serialisieren → album.json
|
||||
- Kopierbaren `apply`-Befehl ausgeben
|
||||
- **Hier endet `rip`** — User prüft/editiert JSON manuell
|
||||
|
||||
### Phase 7: Apply (bereits separater CLI-Befehl)
|
||||
- Datei-Mapping erstellen (organizer)
|
||||
- Umbenennen/Verschieben (sanitize_filename)
|
||||
- Audio-Tagging (mutagen)
|
||||
- Cover-Embedding
|
||||
- M3U-Playlist generieren
|
||||
|
||||
## Strukturelle Beobachtungen
|
||||
|
||||
### Was heute vermischt ist
|
||||
- Phase 1–6 stecken alle in `interactive_rip()` mit if/else-Verzweigung
|
||||
für MB-Hit vs. Fallback
|
||||
- Die Disc-Schleife (Phase 4) enthält Logik aus Phase 2 (Cover-Upload
|
||||
während Disc-Insert) und Phase 3 (Vision-Thread starten)
|
||||
- Phase 5 ist dupliziert: einmal im MB-Pfad (Zeile ~791), einmal im
|
||||
Fallback-Pfad (Zeile ~956)
|
||||
|
||||
### Was eine Refaktorierung bringen würde
|
||||
- Die zwei Pfade (MB-Hit / Fallback) konvergieren nach Phase 4 — ab Phase 5
|
||||
ist die Logik identisch, aber heute kopiert
|
||||
- Cover-Beschaffung ist über den ganzen Code verstreut (CAA-Download,
|
||||
Scanner-Upload während Disc-Insert, Backcover-Save am Ende)
|
||||
- Die Vision-LLM-Steuerung (Thread starten, Ergebnis einsammeln) könnte ein
|
||||
eigenes Objekt/Kontext sein
|
||||
|
||||
### Offene Frage
|
||||
Soll die Disc-Schleife (Phase 4) eine einheitliche Schnittstelle haben, die
|
||||
beide Pfade bedient? Im MB-Pfad ist die Disc-Anzahl bekannt, im Fallback-Pfad
|
||||
offen (while-Schleife mit "Nächste CD?"). Das ist der größte strukturelle
|
||||
Unterschied.
|
||||
|
|
@ -509,7 +509,7 @@ def check(
|
|||
"""Zeigt Audio-Tags und Cover-Status aller Dateien in einem Verzeichnis.
|
||||
|
||||
Durchsucht das Verzeichnis rekursiv nach Audiodateien und gibt für jede
|
||||
Datei die wichtigsten Tags aus. Zeigt außerdem ob frontcover.jpg/backcover.jpg
|
||||
Datei die wichtigsten Tags aus. Zeigt außerdem ob front.jpg/back.jpg
|
||||
vorhanden sind und ob ein Cover eingebettet ist.
|
||||
"""
|
||||
if not directory.exists():
|
||||
|
|
|
|||
|
|
@ -13,15 +13,15 @@ logger = logging.getLogger(__name__)
|
|||
|
||||
# Standard-Dateinamen für Cover im Album-Verzeichnis.
|
||||
# Symbolische Links auf diese Namen sind erlaubt.
|
||||
FRONT_COVER_STEMS = ["frontcover"]
|
||||
BACK_COVER_STEMS = ["backcover"]
|
||||
FRONT_COVER_STEMS = ["front"]
|
||||
BACK_COVER_STEMS = ["back"]
|
||||
COVER_EXTENSIONS = [".jpg", ".png"]
|
||||
|
||||
|
||||
def find_cover(album_dir: Path, kind: str = "front") -> Path | None:
|
||||
"""Sucht das Standard-Coverbild im Album-Verzeichnis.
|
||||
|
||||
Prüft frontcover.jpg, frontcover.png (bzw. backcover.*) in dieser Reihenfolge.
|
||||
Prüft front.jpg, front.png (bzw. back.*) in dieser Reihenfolge.
|
||||
Folgt symbolischen Links.
|
||||
|
||||
Args:
|
||||
|
|
@ -65,20 +65,20 @@ def copy_covers(
|
|||
back_image: Path | None,
|
||||
album_dir: Path,
|
||||
) -> None:
|
||||
"""Kopiert Front- und Rückseiten-Cover als frontcover.jpg / backcover.jpg
|
||||
"""Kopiert Front- und Rückseiten-Cover als front.jpg / back.jpg
|
||||
in das Album-Verzeichnis.
|
||||
|
||||
Bereits vorhandene frontcover.*/backcover.*-Dateien werden nicht überschrieben,
|
||||
Bereits vorhandene front.*/back.*-Dateien werden nicht überschrieben,
|
||||
wenn kein Quellbild angegeben wurde.
|
||||
"""
|
||||
if front_image and front_image.exists():
|
||||
prepare_cover(front_image, album_dir / "frontcover.jpg")
|
||||
prepare_cover(front_image, album_dir / "front.jpg")
|
||||
else:
|
||||
if not find_cover(album_dir, "front"):
|
||||
logger.debug("Kein Front-Cover in %s", album_dir)
|
||||
|
||||
if back_image and back_image.exists():
|
||||
prepare_cover(back_image, album_dir / "backcover.jpg")
|
||||
prepare_cover(back_image, album_dir / "back.jpg")
|
||||
else:
|
||||
logger.debug("Kein Back-Cover angegeben")
|
||||
|
||||
|
|
@ -95,9 +95,9 @@ def download_caa_covers(mbid: str, album_dir: Path) -> None:
|
|||
|
||||
Args:
|
||||
mbid: MusicBrainz Release-MBID
|
||||
album_dir: Zielverzeichnis für frontcover.jpg / backcover.jpg
|
||||
album_dir: Zielverzeichnis für front.jpg / back.jpg
|
||||
"""
|
||||
for kind, filename in [("front", "frontcover.jpg"), ("back", "backcover.jpg")]:
|
||||
for kind, filename in [("front", "front.jpg"), ("back", "back.jpg")]:
|
||||
target = album_dir / filename
|
||||
if target.exists():
|
||||
logger.info("CAA: %s existiert bereits, überspringe.", filename)
|
||||
|
|
|
|||
|
|
@ -73,7 +73,7 @@ def _print_apply_hint(album_root: Path, json_path: Path, num_discs: int) -> None
|
|||
"""Gibt einen kopierbaren apply-Befehl aus."""
|
||||
input_dir = album_root / "CD1" if num_discs == 1 else album_root
|
||||
parts = ["musiksammlung apply", str(input_dir), str(json_path)]
|
||||
if not (album_root / "frontcover.jpg").exists():
|
||||
if not (album_root / "front.jpg").exists():
|
||||
parts.append("--front <cover.jpg>")
|
||||
print(f" → {' '.join(parts)}")
|
||||
|
||||
|
|
@ -713,7 +713,7 @@ def interactive_rip(config: RipperConfig) -> None:
|
|||
total_discs = len(mb_album.discs)
|
||||
|
||||
# Album-Root und Cover VOR dem Ripping anlegen,
|
||||
# damit backcover.jpg für Vision-LLM verfügbar ist.
|
||||
# damit back.jpg für Vision-LLM verfügbar ist.
|
||||
album_root = config.output_dir / sanitize_filename(album_name)
|
||||
album_root.mkdir(parents=True, exist_ok=True)
|
||||
if mb_mbid:
|
||||
|
|
@ -722,7 +722,7 @@ def interactive_rip(config: RipperConfig) -> None:
|
|||
# Vision-LLM im Hintergrund starten, falls CAA-Backcover vorhanden
|
||||
vision_queue = None
|
||||
uploaded_photo: Path | None = None
|
||||
backcover = album_root / "backcover.jpg"
|
||||
backcover = album_root / "back.jpg"
|
||||
if backcover.exists():
|
||||
print(
|
||||
" Backcover verfügbar → Vision-LLM-Analyse im Hintergrund...",
|
||||
|
|
@ -809,9 +809,9 @@ def interactive_rip(config: RipperConfig) -> None:
|
|||
|
||||
# Hochgeladenes Backcover speichern (Handy-Foto hat Vorrang vor CAA)
|
||||
if uploaded_photo and uploaded_photo.exists():
|
||||
dest = album_root / "backcover.jpg"
|
||||
dest = album_root / "back.jpg"
|
||||
prepare_cover(uploaded_photo, dest)
|
||||
print(f" backcover.jpg gespeichert: {dest}", flush=True)
|
||||
print(f" back.jpg gespeichert: {dest}", flush=True)
|
||||
|
||||
json_path = album_root / "album.json"
|
||||
json_path.write_text(
|
||||
|
|
@ -1010,9 +1010,9 @@ def interactive_rip(config: RipperConfig) -> None:
|
|||
# Hochgeladenes Backcover ins Album-Verzeichnis kopieren
|
||||
# (überschreibt ggf. das CAA-Backcover — das Handy-Foto hat Vorrang)
|
||||
if uploaded_photo and uploaded_photo.exists():
|
||||
dest = album_root / "backcover.jpg"
|
||||
dest = album_root / "back.jpg"
|
||||
prepare_cover(uploaded_photo, dest)
|
||||
print(f" backcover.jpg gespeichert: {dest}", flush=True)
|
||||
print(f" back.jpg gespeichert: {dest}", flush=True)
|
||||
|
||||
json_path = album_root / "album.json"
|
||||
json_path.write_text(
|
||||
|
|
@ -1038,7 +1038,7 @@ def interactive_rip(config: RipperConfig) -> None:
|
|||
for i, (album_root, json_path, num_discs) in enumerate(processed_albums, 1):
|
||||
input_dir = album_root / "CD1" if num_discs == 1 else album_root
|
||||
front_flag = (
|
||||
"" if (album_root / "frontcover.jpg").exists()
|
||||
"" if (album_root / "front.jpg").exists()
|
||||
else " --front <cover.jpg>"
|
||||
)
|
||||
print(f"\n Album {i}: {album_root.name}")
|
||||
|
|
|
|||
|
|
@ -227,7 +227,7 @@ class ScannerServer:
|
|||
_, b64 = img_data.split(",", 1)
|
||||
img_bytes = base64.b64decode(b64)
|
||||
|
||||
path = server_instance._upload_dir / f"backcover{ext}"
|
||||
path = server_instance._upload_dir / f"back{ext}"
|
||||
path.write_bytes(img_bytes)
|
||||
logger.info("Backcover hochgeladen: %s (%d bytes)", path, len(img_bytes))
|
||||
server_instance._queue.put(path)
|
||||
|
|
|
|||
|
|
@ -141,12 +141,12 @@ class TestApplyCommand:
|
|||
|
||||
class TestCheckCommand:
|
||||
def test_check_shows_cover_status(self, tmp_path: Path) -> None:
|
||||
(tmp_path / "frontcover.jpg").write_bytes(b"\xff\xd8\xff\xe0") # minimal JPEG magic
|
||||
(tmp_path / "front.jpg").write_bytes(b"\xff\xd8\xff\xe0") # minimal JPEG magic
|
||||
|
||||
result = runner.invoke(app, ["check", str(tmp_path)])
|
||||
|
||||
assert result.exit_code == 0
|
||||
assert "frontcover.jpg" in result.output
|
||||
assert "front.jpg" in result.output
|
||||
|
||||
def test_check_shows_missing_cover(self, tmp_path: Path) -> None:
|
||||
result = runner.invoke(app, ["check", str(tmp_path)])
|
||||
|
|
|
|||
|
|
@ -23,22 +23,22 @@ class TestFindCover:
|
|||
"""Tests für find_cover."""
|
||||
|
||||
def test_finds_frontcover_jpg(self, tmp_path: Path) -> None:
|
||||
(tmp_path / "frontcover.jpg").touch()
|
||||
assert find_cover(tmp_path, "front") == tmp_path / "frontcover.jpg"
|
||||
(tmp_path / "front.jpg").touch()
|
||||
assert find_cover(tmp_path, "front") == tmp_path / "front.jpg"
|
||||
|
||||
def test_finds_frontcover_png(self, tmp_path: Path) -> None:
|
||||
(tmp_path / "frontcover.png").touch()
|
||||
assert find_cover(tmp_path, "front") == tmp_path / "frontcover.png"
|
||||
(tmp_path / "front.png").touch()
|
||||
assert find_cover(tmp_path, "front") == tmp_path / "front.png"
|
||||
|
||||
def test_jpg_preferred_over_png(self, tmp_path: Path) -> None:
|
||||
(tmp_path / "frontcover.jpg").touch()
|
||||
(tmp_path / "frontcover.png").touch()
|
||||
(tmp_path / "front.jpg").touch()
|
||||
(tmp_path / "front.png").touch()
|
||||
# .jpg wird zuerst geprüft
|
||||
assert find_cover(tmp_path, "front") == tmp_path / "frontcover.jpg"
|
||||
assert find_cover(tmp_path, "front") == tmp_path / "front.jpg"
|
||||
|
||||
def test_finds_backcover(self, tmp_path: Path) -> None:
|
||||
(tmp_path / "backcover.jpg").touch()
|
||||
assert find_cover(tmp_path, "back") == tmp_path / "backcover.jpg"
|
||||
(tmp_path / "back.jpg").touch()
|
||||
assert find_cover(tmp_path, "back") == tmp_path / "back.jpg"
|
||||
|
||||
def test_returns_none_if_missing(self, tmp_path: Path) -> None:
|
||||
assert find_cover(tmp_path, "front") is None
|
||||
|
|
@ -47,7 +47,7 @@ class TestFindCover:
|
|||
def test_follows_symlink(self, tmp_path: Path) -> None:
|
||||
real = tmp_path / "original.jpg"
|
||||
real.touch()
|
||||
link = tmp_path / "frontcover.jpg"
|
||||
link = tmp_path / "front.jpg"
|
||||
link.symlink_to(real)
|
||||
assert find_cover(tmp_path, "front") == link
|
||||
|
||||
|
|
@ -95,33 +95,33 @@ class TestCopyCovers:
|
|||
def test_copies_front_cover(self, tmp_path: Path) -> None:
|
||||
src = _make_image(tmp_path / "src.jpg")
|
||||
copy_covers(src, None, tmp_path)
|
||||
assert (tmp_path / "frontcover.jpg").exists()
|
||||
assert (tmp_path / "front.jpg").exists()
|
||||
|
||||
def test_copies_back_cover(self, tmp_path: Path) -> None:
|
||||
src = _make_image(tmp_path / "src.jpg")
|
||||
copy_covers(None, src, tmp_path)
|
||||
assert (tmp_path / "backcover.jpg").exists()
|
||||
assert (tmp_path / "back.jpg").exists()
|
||||
|
||||
def test_copies_both_covers(self, tmp_path: Path) -> None:
|
||||
front = _make_image(tmp_path / "front.jpg")
|
||||
back = _make_image(tmp_path / "back.jpg")
|
||||
copy_covers(front, back, tmp_path)
|
||||
assert (tmp_path / "frontcover.jpg").exists()
|
||||
assert (tmp_path / "backcover.jpg").exists()
|
||||
assert (tmp_path / "front.jpg").exists()
|
||||
assert (tmp_path / "back.jpg").exists()
|
||||
|
||||
def test_skips_nonexistent_front(self, tmp_path: Path) -> None:
|
||||
copy_covers(tmp_path / "nope.jpg", None, tmp_path)
|
||||
assert not (tmp_path / "frontcover.jpg").exists()
|
||||
assert not (tmp_path / "front.jpg").exists()
|
||||
|
||||
def test_skips_nonexistent_back(self, tmp_path: Path) -> None:
|
||||
copy_covers(None, tmp_path / "nope.jpg", tmp_path)
|
||||
assert not (tmp_path / "backcover.jpg").exists()
|
||||
assert not (tmp_path / "back.jpg").exists()
|
||||
|
||||
def test_existing_frontcover_not_overwritten_when_no_source(self, tmp_path: Path) -> None:
|
||||
existing = _make_image(tmp_path / "frontcover.jpg")
|
||||
existing = _make_image(tmp_path / "front.jpg")
|
||||
original_mtime = existing.stat().st_mtime
|
||||
copy_covers(None, None, tmp_path)
|
||||
assert (tmp_path / "frontcover.jpg").stat().st_mtime == original_mtime
|
||||
assert (tmp_path / "front.jpg").stat().st_mtime == original_mtime
|
||||
|
||||
|
||||
def _fake_image_bytes() -> bytes:
|
||||
|
|
@ -151,10 +151,10 @@ class TestDownloadCaaCovers:
|
|||
with patch("musiksammlung.cover.httpx.get", return_value=resp):
|
||||
download_caa_covers("test-mbid", tmp_path)
|
||||
|
||||
assert (tmp_path / "frontcover.jpg").exists()
|
||||
assert (tmp_path / "backcover.jpg").exists()
|
||||
assert (tmp_path / "front.jpg").exists()
|
||||
assert (tmp_path / "back.jpg").exists()
|
||||
# Ergebnis ist ein gültiges JPEG
|
||||
assert Image.open(tmp_path / "frontcover.jpg").format == "JPEG"
|
||||
assert Image.open(tmp_path / "front.jpg").format == "JPEG"
|
||||
|
||||
def test_404_skips_cover(self, tmp_path: Path) -> None:
|
||||
"""404 → kein Cover, kein Fehler."""
|
||||
|
|
@ -163,8 +163,8 @@ class TestDownloadCaaCovers:
|
|||
with patch("musiksammlung.cover.httpx.get", return_value=resp_404):
|
||||
download_caa_covers("no-cover-mbid", tmp_path)
|
||||
|
||||
assert not (tmp_path / "frontcover.jpg").exists()
|
||||
assert not (tmp_path / "backcover.jpg").exists()
|
||||
assert not (tmp_path / "front.jpg").exists()
|
||||
assert not (tmp_path / "back.jpg").exists()
|
||||
|
||||
def test_http_error_continues(self, tmp_path: Path) -> None:
|
||||
"""Netzwerkfehler → Warnung, kein Abbruch."""
|
||||
|
|
@ -174,12 +174,12 @@ class TestDownloadCaaCovers:
|
|||
):
|
||||
download_caa_covers("error-mbid", tmp_path)
|
||||
|
||||
assert not (tmp_path / "frontcover.jpg").exists()
|
||||
assert not (tmp_path / "backcover.jpg").exists()
|
||||
assert not (tmp_path / "front.jpg").exists()
|
||||
assert not (tmp_path / "back.jpg").exists()
|
||||
|
||||
def test_skips_existing_cover(self, tmp_path: Path) -> None:
|
||||
"""Bereits vorhandene Cover werden nicht überschrieben."""
|
||||
existing = _make_image(tmp_path / "frontcover.jpg")
|
||||
existing = _make_image(tmp_path / "front.jpg")
|
||||
original_size = existing.stat().st_size
|
||||
|
||||
img_bytes = _fake_image_bytes()
|
||||
|
|
@ -188,15 +188,15 @@ class TestDownloadCaaCovers:
|
|||
with patch("musiksammlung.cover.httpx.get", return_value=resp) as mock_get:
|
||||
download_caa_covers("test-mbid", tmp_path)
|
||||
|
||||
# frontcover.jpg bleibt unverändert
|
||||
assert (tmp_path / "frontcover.jpg").stat().st_size == original_size
|
||||
# backcover.jpg wird heruntergeladen (war nicht vorhanden)
|
||||
assert (tmp_path / "backcover.jpg").exists()
|
||||
# front.jpg bleibt unverändert
|
||||
assert (tmp_path / "front.jpg").stat().st_size == original_size
|
||||
# back.jpg wird heruntergeladen (war nicht vorhanden)
|
||||
assert (tmp_path / "back.jpg").exists()
|
||||
# Nur ein HTTP-Request (für back), nicht zwei
|
||||
assert mock_get.call_count == 1
|
||||
|
||||
def test_front_only_on_back_404(self, tmp_path: Path) -> None:
|
||||
"""Front 200, Back 404 → nur frontcover.jpg erstellt."""
|
||||
"""Front 200, Back 404 → nur front.jpg erstellt."""
|
||||
img_bytes = _fake_image_bytes()
|
||||
resp_ok = _mock_caa_response(200, img_bytes)
|
||||
resp_404 = _mock_caa_response(404)
|
||||
|
|
@ -207,5 +207,5 @@ class TestDownloadCaaCovers:
|
|||
):
|
||||
download_caa_covers("mixed-mbid", tmp_path)
|
||||
|
||||
assert (tmp_path / "frontcover.jpg").exists()
|
||||
assert not (tmp_path / "backcover.jpg").exists()
|
||||
assert (tmp_path / "front.jpg").exists()
|
||||
assert not (tmp_path / "back.jpg").exists()
|
||||
|
|
|
|||
|
|
@ -351,7 +351,7 @@ class TestEmbedAlbumCover:
|
|||
album = _make_album(tracks=2)
|
||||
_make_flac(tmp_path / "01_-_Track_1_-_TestArtist.flac")
|
||||
_make_flac(tmp_path / "02_-_Track_2_-_TestArtist.flac")
|
||||
cover = _make_cover(tmp_path / "frontcover.jpg")
|
||||
cover = _make_cover(tmp_path / "front.jpg")
|
||||
|
||||
embed_album_cover(album, tmp_path, cover)
|
||||
|
||||
|
|
@ -374,7 +374,7 @@ class TestEmbedAlbumCover:
|
|||
cd2.mkdir()
|
||||
_make_flac(cd1 / "01_-_T1_-_A.flac")
|
||||
_make_flac(cd2 / "01_-_T2_-_A.flac")
|
||||
cover = _make_cover(tmp_path / "frontcover.jpg")
|
||||
cover = _make_cover(tmp_path / "front.jpg")
|
||||
|
||||
embed_album_cover(album, tmp_path, cover)
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue