Files
wordarr/wordarr/api/imports.py
T

111 lines
4.5 KiB
Python

from pathlib import Path
from fastapi import APIRouter, Depends, HTTPException
from pydantic import BaseModel
from sqlalchemy import select
from sqlalchemy.orm import Session
from .. import config
from ..db import BookRequest, get_session
from ..fsnames import decode_path, readable
from ..importer import matcher, mover, scanner
router = APIRouter(prefix="/api/import", tags=["import"])
def _orphaned(session: Session) -> list[BookRequest]:
"""Imported requests whose files are gone from the library - moved away or
deleted by hand. They are effectively missing again, so the scan should
suggest them just like an open request."""
imported = session.scalars(
select(BookRequest).where(BookRequest.status == "imported")
).all()
return [r for r in imported
if not r.imported_path or not Path(r.imported_path).exists()]
@router.get("/scan")
def scan(split_dirs: bool = False, session: Session = Depends(get_session)):
"""Scan the download dir and suggest matches against open requests."""
items = scanner.scan(Path(config.DOWNLOAD_DIR), split_dirs=split_dirs)
missing = session.scalars(
select(BookRequest).where(BookRequest.status == "missing")
).all()
orphaned = _orphaned(session)
return {
"download_dir": str(config.DOWNLOAD_DIR),
"items": matcher.best_matches(items, missing + orphaned),
# the UI offers these like open requests instead of "append parts"
"orphaned_request_ids": [r.id for r in orphaned],
}
class ImportItem(BaseModel):
path: str
is_dir: bool
files: list[str]
request_id: int
# add to an already imported audiobook instead of importing a new one
append: bool = False
class ImportIn(BaseModel):
items: list[ImportItem]
@router.post("")
def do_import(data: ImportIn, session: Session = Depends(get_session)):
results = []
download_root = Path(config.DOWNLOAD_DIR).resolve()
for item in data.items:
# the client hands back whatever the scan produced, base64 included
item.path = decode_path(item.path)
item.files = [decode_path(f) for f in item.files]
shown = readable(item.path)
req = session.get(BookRequest, item.request_id)
if not req:
results.append({"path": shown, "ok": False,
"error": f"Anfrage #{item.request_id} existiert nicht mehr"})
continue
path_gone = not req.imported_path or not Path(req.imported_path).exists()
if req.status == "imported" and path_gone:
# the library copy is gone; treat this as a fresh import
pass
elif item.append and req.status == "imported":
try:
dest = mover.append_to_import(item.files, req, req.library)
except Exception as exc:
results.append({"path": shown, "ok": False, "error": str(exc)})
continue
req.imported_path = dest
session.commit()
results.append({"path": shown, "ok": True, "dest": dest, "appended": True})
continue
elif req.status != "missing":
# usually several entries pointing at the same request: the first one
# imported and flipped it, the rest land here
results.append({"path": shown, "ok": False, "error": (
f"Anfrage „{req.title}“ (#{req.id}) ist bereits importiert"
f"{' nach ' + req.imported_path if req.imported_path else ''}"
" — mehrere Einträge auf dieselbe Anfrage? Dann vorher zusammenfassen."
)})
continue
# every source path must stay inside the download dir - merged items
# carry files from several folders, so check them all
paths = [Path(p).resolve() for p in [item.path, *item.files]]
if any(download_root not in p.parents and p != download_root for p in paths):
results.append({"path": shown, "ok": False, "error": "path outside download dir"})
continue
try:
dest = mover.import_item(item.path, item.files, item.is_dir, req, req.library)
except Exception as exc:
results.append({"path": shown, "ok": False, "error": str(exc)})
continue
req.status = "imported"
req.imported_path = dest
session.commit()
results.append({"path": shown, "ok": True, "dest": dest})
if not results:
raise HTTPException(400, "nothing to import")
return {"results": results}