LLM-Parser-Tests, check-Befehl und Cover-Doku

tests/test_llm_parser.py: 13 Tests für _call_ollama, _call_openai_compatible
  und parse_tracklist (Retry-Logik, Markdown-Block, Track-Artist, Mock)

cli: neuer check-Befehl zeigt Tags und Cover-Status aller Audiodateien;
  ♪ markiert Dateien mit eingebettetem Cover

BEDIENUNGSANLEITUNG: neuer Abschnitt 7 (check-Befehl), Cover-Konvention
  (frontcover.jpg/backcover.jpg, Embedding, 500px) in Schritt 3

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
Dieter Schlüter 2026-02-18 00:45:49 +01:00
commit 88b89fbb50
3 changed files with 261 additions and 4 deletions

View file

@ -16,6 +16,9 @@ from musiksammlung.ocr import ocr_images
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 mutagen import File as MutagenFile
from musiksammlung.config import AUDIO_EXTENSIONS
from musiksammlung.tagger import embed_album_cover, tag_album
from musiksammlung.vision_llm import parse_image
@ -373,5 +376,81 @@ def process(
typer.echo(f"Fertig! Album: {album_dir}")
@app.command()
def check(
directory: Path = typer.Argument(..., help="Album- oder Disc-Verzeichnis"),
) -> None:
"""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
vorhanden sind und ob ein Cover eingebettet ist.
"""
if not directory.exists():
typer.echo(f"Fehler: Verzeichnis nicht gefunden: {directory}", err=True)
raise typer.Exit(1)
# Cover-Status auf Album-Ebene
front = find_cover(directory, "front")
back = find_cover(directory, "back")
typer.echo(f"\nVerzeichnis: {directory}")
typer.echo(f" frontcover: {front.name if front else '— (fehlt)'}")
typer.echo(f" backcover: {back.name if back else '— (fehlt)'}")
# Alle Audiodateien finden (flach + Unterverzeichnisse)
audio_files: list[Path] = sorted(
(f for f in directory.rglob("*") if f.suffix.lower() in AUDIO_EXTENSIONS),
key=lambda p: (p.parent.name, p.name),
)
if not audio_files:
typer.echo("\n Keine Audiodateien gefunden.")
return
current_subdir: str | None = None
for path in audio_files:
subdir = path.parent.name if path.parent != directory else ""
if subdir != current_subdir:
current_subdir = subdir
typer.echo(f"\n {subdir or '.'}/" if subdir else "\n ./")
audio = MutagenFile(str(path), easy=True)
if audio is None:
typer.echo(f" {path.name} [unlesbares Format]")
continue
def tag(key: str) -> str:
vals = audio.get(key)
return vals[0] if vals else ""
has_cover = _has_embedded_cover(path)
cover_marker = "" if has_cover else " "
typer.echo(
f" [{cover_marker}] {path.name}\n"
f" Titel: {tag('title')}\n"
f" Künstler: {tag('artist')} | AlbumArtist: {tag('albumartist')}\n"
f" Album: {tag('album')} | Jahr: {tag('date')}\n"
f" Track: {tag('tracknumber')} | Disc: {tag('discnumber')}"
)
def _has_embedded_cover(path: Path) -> bool:
"""Prüft ob eine Audiodatei ein eingebettetes Cover enthält."""
from mutagen.flac import FLAC
from mutagen.mp3 import MP3
suffix = path.suffix.lower()
try:
if suffix == ".flac":
return bool(FLAC(str(path)).pictures)
if suffix == ".mp3":
tags = MP3(str(path)).tags
return tags is not None and any(k.startswith("APIC") for k in tags.keys())
except Exception:
pass
return False
if __name__ == "__main__":
app()