Compare commits
2 commits
8765e991b0
...
f4e49a3df6
| Author | SHA1 | Date | |
|---|---|---|---|
| f4e49a3df6 | |||
| b599c9eb8a |
4 changed files with 87 additions and 5 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
|
||||
|
|
@ -90,7 +90,7 @@ def scan(
|
|||
),
|
||||
languages: str = typer.Option("deu+eng", "--lang", "-l", help="OCR-Sprachen"),
|
||||
backend: str = typer.Option("ollama", "--backend", "-b", help="LLM-Backend"),
|
||||
model: str = typer.Option("llama3", "--model", "-m", help="Text-LLM-Modell"),
|
||||
model: str = typer.Option("gemma3:12b", "--model", "-m", help="Text-LLM-Modell"),
|
||||
base_url: str = typer.Option(
|
||||
"http://localhost:11434", "--url", help="LLM-API-URL"
|
||||
),
|
||||
|
|
@ -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():
|
||||
|
|
@ -262,7 +292,7 @@ def process(
|
|||
),
|
||||
languages: str = typer.Option("deu+eng", "--lang", "-l"),
|
||||
backend: str = typer.Option("ollama", "--backend", "-b"),
|
||||
model: str = typer.Option("llama3", "--model", "-m"),
|
||||
model: str = typer.Option("gemma3:12b", "--model", "-m"),
|
||||
base_url: str = typer.Option("http://localhost:11434", "--url"),
|
||||
dry_run: bool = typer.Option(False, "--dry-run"),
|
||||
) -> None:
|
||||
|
|
|
|||
|
|
@ -47,7 +47,7 @@ def _call_ollama(text: str, model: str, base_url: str) -> str:
|
|||
],
|
||||
"stream": False,
|
||||
},
|
||||
timeout=120.0,
|
||||
timeout=300.0,
|
||||
)
|
||||
response.raise_for_status()
|
||||
return response.json()["message"]["content"]
|
||||
|
|
@ -71,7 +71,7 @@ def _call_openai_compatible(
|
|||
{"role": "user", "content": text},
|
||||
],
|
||||
},
|
||||
timeout=120.0,
|
||||
timeout=300.0,
|
||||
)
|
||||
response.raise_for_status()
|
||||
return response.json()["choices"][0]["message"]["content"]
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -28,6 +28,9 @@ WICHTIG:
|
|||
- Wenn "CD 1", "CD 2", "Disc 1" etc. sichtbar sind, erstelle mehrere Einträge in "discs".
|
||||
- Ohne Disc-Angabe: eine Disc mit disc_number=1.
|
||||
- Lasse Zeitangaben (z.B. "3:12") und Interpretenangaben pro Track weg.
|
||||
- MEHRSPALTIGE LAYOUTS: CD-Rückseiten haben oft 2, 3 oder 4 Spalten nebeneinander.
|
||||
Lies ALLE Spalten vollständig von oben nach unten, bevor du zur nächsten Spalte gehst.
|
||||
Überspringen oder Auslassen von Spalten ist ein häufiger Fehler — lies jede Spalte komplett.
|
||||
|
||||
Antworte NUR mit dem JSON, ohne Erklärung. Beispiel:
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue