168 lines
7.0 KiB
Python
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
|