Files
wordarr/wordarr/importer/mover.py
T

168 lines
7.0 KiB
Python

import shutil
from pathlib import Path
from .. import config
from ..naming import render_template, sanitize
from . import tagger
def _cleanup_sources(src_dir: Path, paths: list[Path]) -> None:
"""Remove emptied source folders: the item itself plus every folder the files
came from, deepest first - "…/100 - Toteninsel/A - Sphinx/CD" contributes all
three. Anything still holding files (cover art, booklets) is left alone, and
the download dir itself is never removed."""
download_root = Path(config.DOWNLOAD_DIR).resolve()
candidates = {src_dir}
for p in paths:
d = p.parent
while d == src_dir or src_dir in d.parents:
candidates.add(d)
if d == src_dir:
break
d = d.parent
for d in sorted(candidates, key=lambda p: len(p.parts), reverse=True):
if d.resolve() == download_root:
continue # merged items can point at the download dir itself
if d.is_dir() and not any(f.is_file() for f in d.rglob("*")):
shutil.rmtree(d, ignore_errors=True)
def import_item(item_path: str, files: list[str], is_dir: bool, request, library) -> str:
"""Move scanned files into the library, renamed per the library's templates.
Returns the destination path (folder for multi-file audiobooks, else the file)."""
root = Path(library.root_path)
folder = root / render_template(library.folder_template, request)
folder.mkdir(parents=True, exist_ok=True)
base = render_template(library.file_template, request)
paths = [Path(f) for f in files]
if is_dir and len(paths) > 1:
width = max(2, len(str(len(paths))))
targets = [
folder / sanitize(f"{base} - Part {i:0{width}d}{src.suffix.lower()}")
for i, src in enumerate(paths, 1)
]
# check every target first: moving file by file would leave a half
# imported folder behind, and without the check a second title rendering
# to the same name would silently overwrite the first one
taken = [d for d in targets if d.exists()]
if taken:
raise FileExistsError(
f"Destination already exists: {taken[0]}"
+ (f" (und {len(taken) - 1} weitere)" if len(taken) > 1 else "")
+ " — trägt ein anderer Titel nach diesem Namensschema denselben Namen?"
)
for i, (src, dest) in enumerate(zip(paths, targets), 1):
shutil.move(str(src), dest)
if request.media_type == "audiobook":
tagger.tag_audio(dest, request, track=i, total=len(paths))
_cleanup_sources(Path(item_path), paths)
return str(folder)
src = paths[0]
dest = folder / sanitize(f"{base}{src.suffix.lower()}")
if dest.exists():
raise FileExistsError(f"Destination already exists: {dest}")
shutil.move(str(src), dest)
if request.media_type == "audiobook":
tagger.tag_audio(dest, request)
return str(dest)
def append_to_import(files: list[str], request, library) -> str:
"""Add more files to an audiobook that was already imported - a part that
arrived late, or one that failed on the first run. Numbering continues after
the files already there, and all tracks are re-tagged with the new total."""
dest_dir = Path(request.imported_path or "")
if not dest_dir.is_dir():
raise FileNotFoundError(
f"Zielordner existiert nicht (mehr): {request.imported_path}"
)
existing = sorted(
f for f in dest_dir.iterdir()
if f.is_file() and f.suffix.lower() in config.AUDIOBOOK_EXTENSIONS
)
paths = [Path(f) for f in files]
total = len(existing) + len(paths)
width = max(2, len(str(total)))
base = render_template(library.file_template, request)
for i, src in enumerate(paths, len(existing) + 1):
dest = dest_dir / sanitize(f"{base} - Part {i:0{width}d}{src.suffix.lower()}")
if dest.exists():
raise FileExistsError(f"Destination already exists: {dest}")
shutil.move(str(src), dest)
if request.media_type == "audiobook":
tagger.tag_audio(dest, request, track=i, total=total)
if request.media_type == "audiobook":
for i, f in enumerate(existing, 1):
tagger.tag_audio(f, request, track=i, total=total)
for src_dir in {p.parent for p in paths}:
_cleanup_sources(src_dir, paths)
return str(dest_dir)
def relocate(request, library) -> str:
"""Move an already imported audiobook to where the library's templates say it
belongs now - after correcting series, title or author. Files are renamed as
on import, extras (cover art) move along, and emptied folders are removed."""
src = Path(request.imported_path or "")
if not src.exists():
raise FileNotFoundError(f"Pfad existiert nicht (mehr): {request.imported_path}")
root = Path(library.root_path)
folder = root / render_template(library.folder_template, request)
base = render_template(library.file_template, request)
if src.is_file():
dest = folder / sanitize(f"{base}{src.suffix.lower()}")
if dest.resolve() == src.resolve():
return str(src)
if dest.exists():
raise FileExistsError(f"Destination already exists: {dest}")
folder.mkdir(parents=True, exist_ok=True)
shutil.move(str(src), dest)
if request.media_type == "audiobook":
tagger.tag_audio(dest, request)
_remove_if_empty(src.parent, root)
return str(dest)
audio = sorted(
(f for f in src.iterdir()
if f.is_file() and f.suffix.lower() in config.AUDIOBOOK_EXTENSIONS),
key=lambda f: f.name,
)
extras = [f for f in src.iterdir() if f.is_file() and f not in audio]
same_folder = folder.resolve() == src.resolve() if folder.exists() else False
if not same_folder and folder.exists() and any(folder.iterdir()):
raise FileExistsError(f"Destination already exists: {folder}")
folder.mkdir(parents=True, exist_ok=True)
width = max(2, len(str(len(audio))))
for i, f in enumerate(audio, 1):
name = sanitize(
f"{base} - Part {i:0{width}d}{f.suffix.lower()}" if len(audio) > 1
else f"{base}{f.suffix.lower()}"
)
dest = folder / name
if dest.resolve() != f.resolve():
shutil.move(str(f), dest)
if request.media_type == "audiobook":
tagger.tag_audio(dest, request, track=i, total=len(audio))
for extra in extras:
target = folder / extra.name
if target.resolve() != extra.resolve() and not target.exists():
shutil.move(str(extra), target)
if not same_folder:
_remove_if_empty(src, root)
return str(folder)
def _remove_if_empty(folder: Path, root: Path) -> None:
"""Drop the folder and its emptied parents, never touching the library root."""
while folder.is_dir() and folder.resolve() != root.resolve() and root in folder.parents:
if any(folder.iterdir()):
return
folder.rmdir()
folder = folder.parent