Add disc count validation before apply
Check audio file count vs JSON track count per disc before processing. Aborts with a clear error showing which discs have discrepancies and whether tracks are missing from the JSON or audio files are missing from the directory. Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
b599c9eb8a
commit
f4e49a3df6
2 changed files with 80 additions and 1 deletions
|
|
@ -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():
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue