Add --in-place mode to apply: rename and tag without moving files

When no output_dir is given (or --in-place is set), files are renamed
and tagged directly in the source directory instead of being moved into
a separate Jellyfin library hierarchy.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
Dieter Schlüter 2026-02-17 23:41:29 +01:00
commit 070a0573ae
2 changed files with 37 additions and 12 deletions

View file

@ -133,19 +133,36 @@ def apply(
..., help="Verzeichnis mit gerippten Audiodateien"
),
album_json: Path = typer.Argument(..., help="Album-JSON aus 'scan'"),
output_dir: Path = typer.Argument(..., help="Jellyfin-Musikverzeichnis"),
output_dir: Path | None = typer.Argument(
None, help="Jellyfin-Musikverzeichnis (weggelassen = --in-place)"
),
front: Path | None = typer.Option(None, "--front", help="Front-Cover-Bild"),
back: Path | None = typer.Option(
None, "--back", help="Rückseiten-Cover-Bild"
),
in_place: bool = typer.Option(
False, "--in-place", help="Dateien im Quellverzeichnis umbenennen/taggen"
),
dry_run: bool = typer.Option(
False, "--dry-run", help="Nur anzeigen, nichts ändern"
),
) -> None:
"""Album-JSON + Audiodateien → Jellyfin-Struktur aufbauen."""
"""Album-JSON + Audiodateien → Jellyfin-Struktur aufbauen.
Ohne output_dir (oder mit --in-place): Dateien werden im Quellverzeichnis
umbenannt und getaggt, nicht in eine separate Bibliothek verschoben.
"""
raw = json.loads(album_json.read_text(encoding="utf-8"))
album = Album.model_validate(raw)
# --in-place wenn kein output_dir angegeben
if output_dir is None:
in_place = True
if not in_place and output_dir is None:
typer.echo("Fehler: output_dir oder --in-place angeben.", err=True)
raise typer.Exit(1)
# Prüfe Track-Anzahl pro Disc
checks = check_disc_counts(album, input_dir)
problems = [c for c in checks if not c.ok]
@ -176,10 +193,11 @@ def apply(
typer.echo(f"\nBitte {album_json} korrigieren und erneut aufrufen.", err=True)
raise typer.Exit(1)
mapping = build_mapping(album, input_dir, output_dir)
mapping = build_mapping(album, input_dir, output_dir, in_place=in_place)
typer.echo(f"Mapping: {len(mapping)} Dateien")
display_root = input_dir if in_place else output_dir
for src, dst in mapping.items():
typer.echo(f" {src.name}{dst.relative_to(output_dir)}")
typer.echo(f" {src.name}{dst.relative_to(display_root)}")
if dry_run:
typer.echo("[DRY-RUN] Keine Änderungen vorgenommen.")
@ -187,11 +205,11 @@ def apply(
apply_mapping(mapping)
first_target = next(iter(mapping.values()))
if len(album.discs) > 1:
album_dir = first_target.parent.parent
if in_place:
album_dir = input_dir
else:
album_dir = first_target.parent
first_target = next(iter(mapping.values()))
album_dir = first_target.parent.parent if len(album.discs) > 1 else first_target.parent
typer.echo("Setze Audio-Tags...")
tag_album(album, album_dir)

View file

@ -86,20 +86,27 @@ def check_disc_counts(album: Album, input_dir: Path) -> list[DiscCheck]:
def build_mapping(
album: Album,
input_dir: Path,
output_root: Path,
output_root: Path | None = None,
in_place: bool = False,
) -> dict[Path, Path]:
"""Berechnet das Quell→Ziel-Mapping für alle Audiodateien.
Args:
album: Validiertes Album-Modell
input_dir: Verzeichnis mit den gerippten Dateien
output_root: Jellyfin-Musikverzeichnis
output_root: Jellyfin-Musikverzeichnis (wird bei in_place ignoriert)
in_place: Wenn True, Dateien nur innerhalb von input_dir umbenennen
Returns:
Dict von Quellpfad Zielpfad
"""
artist_dir = _sanitize_filename(album.artist)
album_dir = output_root / artist_dir / _sanitize_filename(album.folder_name)
if in_place:
album_dir = input_dir
else:
assert output_root is not None, "output_root erforderlich wenn nicht in_place"
artist_dir = _sanitize_filename(album.artist)
album_dir = output_root / artist_dir / _sanitize_filename(album.folder_name)
mapping: dict[Path, Path] = {}
multi_disc = len(album.discs) > 1