diff --git a/src/musiksammlung/cli.py b/src/musiksammlung/cli.py index 820d078..6b2cc32 100644 --- a/src/musiksammlung/cli.py +++ b/src/musiksammlung/cli.py @@ -13,7 +13,7 @@ from musiksammlung.cover import copy_covers from musiksammlung.llm_parser import parse_tracklist from musiksammlung.models import Album from musiksammlung.ocr import ocr_images -from musiksammlung.organizer import apply_mapping, build_mapping +from musiksammlung.organizer import apply_mapping, build_mapping, check_disc_counts from musiksammlung.playlist import generate_playlist from musiksammlung.ripper import RipperConfig, interactive_rip from musiksammlung.tagger import tag_album @@ -146,6 +146,36 @@ def apply( raw = json.loads(album_json.read_text(encoding="utf-8")) album = Album.model_validate(raw) + # Prüfe Track-Anzahl pro Disc + checks = check_disc_counts(album, input_dir) + problems = [c for c in checks if not c.ok] + if problems: + typer.echo( + "\nFEHLER: Track-Diskrepanz zwischen gerippten Dateien und album.json:\n", + err=True, + ) + for c in checks: + status = "OK" if c.ok else "!!" + typer.echo( + f" [{status}] Disc {c.disc_number}: " + f"{c.audio_file_count} Datei(en), {c.json_track_count} JSON-Track(s)", + err=True, + ) + if c.surplus_files: + typer.echo( + f" → {c.surplus_files} Track(s) fehlen im JSON " + f"(Tracks {c.json_track_count + 1}–{c.audio_file_count} eintragen)", + err=True, + ) + elif c.surplus_json: + typer.echo( + f" → {c.surplus_json} JSON-Eintrag/Einträge ohne Audiodatei " + f"(Tracks {c.audio_file_count + 1}–{c.json_track_count} prüfen)", + err=True, + ) + typer.echo(f"\nBitte {album_json} korrigieren und erneut aufrufen.", err=True) + raise typer.Exit(1) + mapping = build_mapping(album, input_dir, output_dir) typer.echo(f"Mapping: {len(mapping)} Dateien") for src, dst in mapping.items(): diff --git a/src/musiksammlung/organizer.py b/src/musiksammlung/organizer.py index 22a6a3e..1c00419 100644 --- a/src/musiksammlung/organizer.py +++ b/src/musiksammlung/organizer.py @@ -5,6 +5,7 @@ from __future__ import annotations import logging import re import shutil +from dataclasses import dataclass from pathlib import Path from musiksammlung.config import AUDIO_EXTENSIONS @@ -13,6 +14,29 @@ from musiksammlung.models import Album logger = logging.getLogger(__name__) +@dataclass +class DiscCheck: + """Ergebnis der Track-Zählung für eine einzelne Disc.""" + + disc_number: int + audio_file_count: int + json_track_count: int + + @property + def ok(self) -> bool: + return self.audio_file_count == self.json_track_count + + @property + def surplus_files(self) -> int: + """Dateien ohne JSON-Eintrag (Tracks fehlen im JSON).""" + return max(0, self.audio_file_count - self.json_track_count) + + @property + def surplus_json(self) -> int: + """JSON-Einträge ohne Datei (Dateien fehlen im Verzeichnis).""" + return max(0, self.json_track_count - self.audio_file_count) + + def _sanitize_filename(name: str) -> str: """Entfernt problematische Zeichen aus Dateinamen.""" return re.sub(r'[<>:"/\\|?*]', "_", name).strip() @@ -28,6 +52,31 @@ def discover_audio_files(directory: Path) -> list[Path]: return sorted(files, key=extract_number) +def check_disc_counts(album: Album, input_dir: Path) -> list[DiscCheck]: + """Vergleicht Dateianzahl und JSON-Track-Anzahl pro Disc. + + Args: + album: Validiertes Album-Modell + input_dir: Verzeichnis mit gerippten Dateien (enthält CD1/, CD2/, ... bei Multi-Disc) + + Returns: + Liste von DiscCheck-Objekten — auch für korrekte Discs (ok=True). + """ + multi_disc = len(album.discs) > 1 + results: list[DiscCheck] = [] + + for disc in album.discs: + source_dir = input_dir / f"CD{disc.disc_number}" if multi_disc else input_dir + file_count = len(discover_audio_files(source_dir)) if source_dir.exists() else 0 + results.append(DiscCheck( + disc_number=disc.disc_number, + audio_file_count=file_count, + json_track_count=len(disc.tracks), + )) + + return results + + def build_mapping( album: Album, input_dir: Path,