From 070a0573aef67c22aaefefa41b1478f13c3c3742 Mon Sep 17 00:00:00 2001 From: dschlueter Date: Tue, 17 Feb 2026 23:41:29 +0100 Subject: [PATCH] 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 --- src/musiksammlung/cli.py | 34 ++++++++++++++++++++++++++-------- src/musiksammlung/organizer.py | 15 +++++++++++---- 2 files changed, 37 insertions(+), 12 deletions(-) diff --git a/src/musiksammlung/cli.py b/src/musiksammlung/cli.py index 6b2cc32..e3c63c3 100644 --- a/src/musiksammlung/cli.py +++ b/src/musiksammlung/cli.py @@ -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) diff --git a/src/musiksammlung/organizer.py b/src/musiksammlung/organizer.py index 93f4ac5..d0ab508 100644 --- a/src/musiksammlung/organizer.py +++ b/src/musiksammlung/organizer.py @@ -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