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:
parent
67b8653b3c
commit
070a0573ae
2 changed files with 37 additions and 12 deletions
|
|
@ -133,19 +133,36 @@ def apply(
|
||||||
..., help="Verzeichnis mit gerippten Audiodateien"
|
..., help="Verzeichnis mit gerippten Audiodateien"
|
||||||
),
|
),
|
||||||
album_json: Path = typer.Argument(..., help="Album-JSON aus 'scan'"),
|
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"),
|
front: Path | None = typer.Option(None, "--front", help="Front-Cover-Bild"),
|
||||||
back: Path | None = typer.Option(
|
back: Path | None = typer.Option(
|
||||||
None, "--back", help="Rückseiten-Cover-Bild"
|
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(
|
dry_run: bool = typer.Option(
|
||||||
False, "--dry-run", help="Nur anzeigen, nichts ändern"
|
False, "--dry-run", help="Nur anzeigen, nichts ändern"
|
||||||
),
|
),
|
||||||
) -> None:
|
) -> 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"))
|
raw = json.loads(album_json.read_text(encoding="utf-8"))
|
||||||
album = Album.model_validate(raw)
|
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
|
# Prüfe Track-Anzahl pro Disc
|
||||||
checks = check_disc_counts(album, input_dir)
|
checks = check_disc_counts(album, input_dir)
|
||||||
problems = [c for c in checks if not c.ok]
|
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)
|
typer.echo(f"\nBitte {album_json} korrigieren und erneut aufrufen.", err=True)
|
||||||
raise typer.Exit(1)
|
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")
|
typer.echo(f"Mapping: {len(mapping)} Dateien")
|
||||||
|
display_root = input_dir if in_place else output_dir
|
||||||
for src, dst in mapping.items():
|
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:
|
if dry_run:
|
||||||
typer.echo("[DRY-RUN] Keine Änderungen vorgenommen.")
|
typer.echo("[DRY-RUN] Keine Änderungen vorgenommen.")
|
||||||
|
|
@ -187,11 +205,11 @@ def apply(
|
||||||
|
|
||||||
apply_mapping(mapping)
|
apply_mapping(mapping)
|
||||||
|
|
||||||
first_target = next(iter(mapping.values()))
|
if in_place:
|
||||||
if len(album.discs) > 1:
|
album_dir = input_dir
|
||||||
album_dir = first_target.parent.parent
|
|
||||||
else:
|
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...")
|
typer.echo("Setze Audio-Tags...")
|
||||||
tag_album(album, album_dir)
|
tag_album(album, album_dir)
|
||||||
|
|
|
||||||
|
|
@ -86,20 +86,27 @@ def check_disc_counts(album: Album, input_dir: Path) -> list[DiscCheck]:
|
||||||
def build_mapping(
|
def build_mapping(
|
||||||
album: Album,
|
album: Album,
|
||||||
input_dir: Path,
|
input_dir: Path,
|
||||||
output_root: Path,
|
output_root: Path | None = None,
|
||||||
|
in_place: bool = False,
|
||||||
) -> dict[Path, Path]:
|
) -> dict[Path, Path]:
|
||||||
"""Berechnet das Quell→Ziel-Mapping für alle Audiodateien.
|
"""Berechnet das Quell→Ziel-Mapping für alle Audiodateien.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
album: Validiertes Album-Modell
|
album: Validiertes Album-Modell
|
||||||
input_dir: Verzeichnis mit den gerippten Dateien
|
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:
|
Returns:
|
||||||
Dict von Quellpfad → Zielpfad
|
Dict von Quellpfad → Zielpfad
|
||||||
"""
|
"""
|
||||||
artist_dir = _sanitize_filename(album.artist)
|
if in_place:
|
||||||
album_dir = output_root / artist_dir / _sanitize_filename(album.folder_name)
|
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] = {}
|
mapping: dict[Path, Path] = {}
|
||||||
multi_disc = len(album.discs) > 1
|
multi_disc = len(album.discs) > 1
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue