252 lines
8.4 KiB
Python
252 lines
8.4 KiB
Python
from pathlib import Path
|
|
|
|
from fastapi import APIRouter, Depends, HTTPException
|
|
from pydantic import BaseModel, ConfigDict
|
|
from sqlalchemy import select
|
|
from sqlalchemy.orm import Session
|
|
|
|
from .. import config
|
|
from ..db import BookRequest, Library, get_session
|
|
from ..importer import mover, tagger
|
|
|
|
router = APIRouter(prefix="/api/requests", tags=["requests"])
|
|
|
|
|
|
class BulkItem(BaseModel):
|
|
title: str
|
|
authors: str = ""
|
|
narrator: str = ""
|
|
external_id: str = ""
|
|
year: int | None = None
|
|
series: str = ""
|
|
volume: int | None = None
|
|
cover_url: str = ""
|
|
|
|
|
|
class RequestIn(BulkItem):
|
|
library_id: int
|
|
|
|
|
|
class RequestOut(RequestIn):
|
|
id: int
|
|
media_type: str
|
|
status: str
|
|
imported_path: str
|
|
library_name: str = ""
|
|
|
|
model_config = ConfigDict(from_attributes=True)
|
|
|
|
|
|
def _out(r: BookRequest) -> RequestOut:
|
|
out = RequestOut.model_validate(r)
|
|
out.library_name = r.library.name if r.library else ""
|
|
return out
|
|
|
|
|
|
@router.get("", response_model=list[RequestOut])
|
|
def list_requests(status: str | None = None, media_type: str | None = None,
|
|
library_id: int | None = None, session: Session = Depends(get_session)):
|
|
stmt = select(BookRequest).order_by(BookRequest.created_at.desc())
|
|
if status:
|
|
stmt = stmt.where(BookRequest.status == status)
|
|
if media_type:
|
|
stmt = stmt.where(BookRequest.media_type == media_type)
|
|
if library_id:
|
|
stmt = stmt.where(BookRequest.library_id == library_id)
|
|
return [_out(r) for r in session.scalars(stmt).all()]
|
|
|
|
|
|
@router.post("", response_model=RequestOut)
|
|
def create_request(data: RequestIn, allow_duplicate: bool = False,
|
|
session: Session = Depends(get_session)):
|
|
lib = session.get(Library, data.library_id)
|
|
if not lib:
|
|
raise HTTPException(404, "library not found")
|
|
if not allow_duplicate:
|
|
open_reqs = session.scalars(
|
|
select(BookRequest).where(
|
|
BookRequest.status == "missing",
|
|
BookRequest.media_type == lib.media_type,
|
|
)
|
|
).all()
|
|
for r in open_reqs:
|
|
same_id = data.external_id and r.external_id == data.external_id
|
|
same_title = (
|
|
r.title.strip().lower() == data.title.strip().lower()
|
|
and (r.narrator or "") == (data.narrator or "")
|
|
and r.volume == data.volume
|
|
)
|
|
if same_id or same_title:
|
|
raise HTTPException(
|
|
409, f"Bereits als missing vorhanden: „{r.title}“ (#{r.id})"
|
|
)
|
|
req = BookRequest(media_type=lib.media_type, **data.model_dump())
|
|
session.add(req)
|
|
session.commit()
|
|
session.refresh(req)
|
|
return _out(req)
|
|
|
|
|
|
class BulkRequestIn(BaseModel):
|
|
library_id: int
|
|
series: str
|
|
authors: str = ""
|
|
narrator: str = ""
|
|
volume_from: int
|
|
volume_to: int
|
|
year: int | None = None
|
|
cover_url: str = ""
|
|
|
|
|
|
@router.post("/bulk", response_model=list[RequestOut])
|
|
def create_bulk(data: BulkRequestIn, session: Session = Depends(get_session)):
|
|
lib = session.get(Library, data.library_id)
|
|
if not lib:
|
|
raise HTTPException(404, "library not found")
|
|
if data.volume_from > data.volume_to:
|
|
raise HTTPException(400, "volume_from must be <= volume_to")
|
|
if data.volume_to - data.volume_from > 999:
|
|
raise HTTPException(400, "at most 1000 requests per bulk call")
|
|
width = max(2, len(str(data.volume_to)))
|
|
reqs = []
|
|
for n in range(data.volume_from, data.volume_to + 1):
|
|
req = BookRequest(
|
|
media_type=lib.media_type,
|
|
library_id=lib.id,
|
|
title=f"{data.series} Folge {n:0{width}d}",
|
|
authors=data.authors,
|
|
narrator=data.narrator,
|
|
series=data.series,
|
|
volume=n,
|
|
year=data.year,
|
|
cover_url=data.cover_url,
|
|
)
|
|
session.add(req)
|
|
reqs.append(req)
|
|
session.commit()
|
|
for r in reqs:
|
|
session.refresh(r)
|
|
return [_out(r) for r in reqs]
|
|
|
|
|
|
class BulkItemsIn(BaseModel):
|
|
library_id: int
|
|
items: list[BulkItem]
|
|
|
|
|
|
@router.post("/bulk-items")
|
|
def create_bulk_items(data: BulkItemsIn, session: Session = Depends(get_session)):
|
|
"""Create many requests at once from ready-made metadata (e.g. a whole
|
|
Audible series). Entries that already exist as missing are skipped."""
|
|
lib = session.get(Library, data.library_id)
|
|
if not lib:
|
|
raise HTTPException(404, "library not found")
|
|
if len(data.items) > 1000:
|
|
raise HTTPException(400, "at most 1000 requests per bulk call")
|
|
|
|
open_reqs = session.scalars(
|
|
select(BookRequest).where(
|
|
BookRequest.status == "missing",
|
|
BookRequest.media_type == lib.media_type,
|
|
)
|
|
).all()
|
|
known_ids = {r.external_id for r in open_reqs if r.external_id}
|
|
known_titles = {
|
|
(r.title.strip().lower(), r.narrator or "", r.volume) for r in open_reqs
|
|
}
|
|
|
|
created, skipped = [], 0
|
|
for item in data.items:
|
|
key = (item.title.strip().lower(), item.narrator or "", item.volume)
|
|
if (item.external_id and item.external_id in known_ids) or key in known_titles:
|
|
skipped += 1
|
|
continue
|
|
known_ids.add(item.external_id)
|
|
known_titles.add(key)
|
|
req = BookRequest(
|
|
media_type=lib.media_type,
|
|
library_id=lib.id,
|
|
**item.model_dump(),
|
|
)
|
|
session.add(req)
|
|
created.append(req)
|
|
session.commit()
|
|
for r in created:
|
|
session.refresh(r)
|
|
return {"created": [_out(r) for r in created], "skipped": skipped}
|
|
|
|
|
|
@router.put("/{request_id}", response_model=RequestOut)
|
|
def update_request(request_id: int, data: RequestIn, session: Session = Depends(get_session)):
|
|
req = session.get(BookRequest, request_id)
|
|
if not req:
|
|
raise HTTPException(404, "request not found")
|
|
lib = session.get(Library, data.library_id)
|
|
if not lib:
|
|
raise HTTPException(404, "library not found")
|
|
for field, value in data.model_dump().items():
|
|
setattr(req, field, value)
|
|
req.media_type = lib.media_type
|
|
session.commit()
|
|
session.refresh(req)
|
|
return _out(req)
|
|
|
|
|
|
@router.post("/{request_id}/retag")
|
|
def retag_request(request_id: int, session: Session = Depends(get_session)):
|
|
"""Rewrite the audio tags of an already imported request from its current
|
|
metadata (e.g. after adding series/volume/narrator later)."""
|
|
req = session.get(BookRequest, request_id)
|
|
if not req:
|
|
raise HTTPException(404, "request not found")
|
|
if req.media_type != "audiobook":
|
|
raise HTTPException(400, "retag is only supported for audiobooks")
|
|
if req.status != "imported" or not req.imported_path:
|
|
raise HTTPException(400, "request is not imported yet")
|
|
path = Path(req.imported_path)
|
|
if path.is_dir():
|
|
files = sorted(
|
|
f for f in path.iterdir()
|
|
if f.is_file() and f.suffix.lower() in config.AUDIOBOOK_EXTENSIONS
|
|
)
|
|
if not files:
|
|
raise HTTPException(404, f"no audio files in {path}")
|
|
for i, f in enumerate(files, 1):
|
|
tagger.tag_audio(f, req, track=i, total=len(files))
|
|
return {"ok": True, "files": len(files)}
|
|
if path.is_file():
|
|
tagger.tag_audio(path, req)
|
|
return {"ok": True, "files": 1}
|
|
raise HTTPException(404, f"imported path no longer exists: {path}")
|
|
|
|
|
|
@router.post("/{request_id}/relocate")
|
|
def relocate_request(request_id: int, session: Session = Depends(get_session)):
|
|
"""Re-apply the library's naming scheme to an imported title, e.g. after
|
|
fixing a series that Audible had filed differently."""
|
|
req = session.get(BookRequest, request_id)
|
|
if not req:
|
|
raise HTTPException(404, "request not found")
|
|
if req.status != "imported" or not req.imported_path:
|
|
raise HTTPException(400, "request is not imported yet")
|
|
old = req.imported_path
|
|
try:
|
|
dest = mover.relocate(req, req.library)
|
|
except FileNotFoundError as exc:
|
|
raise HTTPException(404, str(exc))
|
|
except FileExistsError as exc:
|
|
raise HTTPException(409, str(exc))
|
|
req.imported_path = dest
|
|
session.commit()
|
|
return {"ok": True, "moved": dest != old, "from": old, "dest": dest}
|
|
|
|
|
|
@router.delete("/{request_id}")
|
|
def delete_request(request_id: int, session: Session = Depends(get_session)):
|
|
req = session.get(BookRequest, request_id)
|
|
if not req:
|
|
raise HTTPException(404, "request not found")
|
|
session.delete(req)
|
|
session.commit()
|
|
return {"ok": True}
|