Arag / app /admin /routers /books.py
AuthorBot
Auto-rescan format prices in background when verified listings have none
9910ee4
Raw
History Blame Contribute Delete
6.14 kB
"""admin/routers/books.py — Book management routes.
Routes:
GET /{slug}/books
DELETE /{slug}/books/{book_id}
POST /{slug}/books/{book_id}/cover
POST /{slug}/books/import-url/preview — Fetch metadata preview from URL
POST /{slug}/books/import-url — Create book from URL metadata
"""
from fastapi import APIRouter, Depends, File, HTTPException, UploadFile
from sqlalchemy.ext.asyncio import AsyncSession
from app.dependencies import get_db, get_current_author_scoped, get_redis
from app.schemas.admin import (
BookActiveRequest,
BookReorderRequest,
ImportURLConfirmRequest,
ImportURLRequest,
PlatformListingUpsertRequest,
)
from app.services.book_service import BookService
router = APIRouter()
def _validate_url(url: str) -> str:
"""Strip and validate URL scheme."""
url = url.strip()
if not url.startswith(("http://", "https://")):
raise HTTPException(400, "Please provide a full URL including https://")
return url
@router.get("/{author_slug}/books")
async def list_books(
author_slug: str,
current_user=Depends(get_current_author_scoped),
db: AsyncSession = Depends(get_db),
):
"""List all books for the author."""
return await BookService(db).list_books(current_user.id)
@router.get("/{author_slug}/books/{book_id}/platform-presence")
async def get_book_platform_presence(
author_slug: str,
book_id: str,
current_user=Depends(get_current_author_scoped),
db: AsyncSession = Depends(get_db),
redis=Depends(get_redis),
):
"""Return stored platform presence scan for a book."""
result = await BookService(db).get_platform_presence(
current_user.id, book_id, redis=redis,
)
if result is None:
raise HTTPException(404, "Book not found")
return result
@router.put("/{author_slug}/books/{book_id}/platforms/{platform_id}")
async def upsert_book_platform_listing(
author_slug: str,
book_id: str,
platform_id: str,
body: PlatformListingUpsertRequest,
current_user=Depends(get_current_author_scoped),
db: AsyncSession = Depends(get_db),
):
"""Create or update one per-platform listing (URL, ISBN/ASIN, format prices)."""
fields = body.model_dump(exclude_unset=True)
result = await BookService(db).upsert_platform_listing(
current_user.id, book_id, platform_id, fields,
)
if result is None:
raise HTTPException(404, "Book not found")
return result
@router.delete("/{author_slug}/books/{book_id}")
async def delete_book(
author_slug: str,
book_id: str,
current_user=Depends(get_current_author_scoped),
db: AsyncSession = Depends(get_db),
):
"""Delete a book and its ChromaDB collection."""
result = await BookService(db).delete_book(current_user.id, book_id)
if result is None:
raise HTTPException(404, "Book not found")
return result
@router.patch("/{author_slug}/books/{book_id}/active")
async def set_book_active(
author_slug: str,
book_id: str,
body: BookActiveRequest,
current_user=Depends(get_current_author_scoped),
db: AsyncSession = Depends(get_db),
):
"""Activate or deactivate a book (inactive books are excluded from RAG)."""
result = await BookService(db).set_book_active(current_user.id, book_id, body.is_active)
if result is None:
raise HTTPException(404, "Book not found")
return result
@router.patch("/{author_slug}/books/reorder")
async def reorder_books(
author_slug: str,
body: BookReorderRequest,
current_user=Depends(get_current_author_scoped),
db: AsyncSession = Depends(get_db),
):
"""Update display order for widget book list."""
return await BookService(db).reorder_books(current_user.id, body.ordered_ids)
@router.post("/{author_slug}/books/{book_id}/reindex")
async def reindex_book(
author_slug: str,
book_id: str,
current_user=Depends(get_current_author_scoped),
db: AsyncSession = Depends(get_db),
redis=Depends(get_redis),
):
"""Re-run indexing for a failed or stuck book."""
result = await BookService(db).reindex_book(current_user.id, book_id, redis)
if result is None:
raise HTTPException(404, "Book not found")
return result
@router.post("/{author_slug}/books/{book_id}/cover")
async def upload_book_cover(
author_slug: str,
book_id: str,
current_user=Depends(get_current_author_scoped),
db: AsyncSession = Depends(get_db),
file: UploadFile = File(...),
):
"""Upload a book cover image (JPG/PNG/WebP, max 5MB)."""
result = await BookService(db).upload_cover(current_user.id, book_id, file)
if result is None:
raise HTTPException(404, "Book not found")
return result
@router.post("/{author_slug}/books/import-url/preview")
async def preview_book_url(
author_slug: str,
body: ImportURLRequest,
current_user=Depends(get_current_author_scoped),
db: AsyncSession = Depends(get_db),
redis=Depends(get_redis),
):
"""Fetch and preview book metadata from a supported platform URL."""
url = _validate_url(body.url)
return await BookService.preview_url_import(
url, redis, force_refresh=body.force_refresh, db=db,
)
@router.post("/{author_slug}/books/import-url", status_code=201)
async def import_book_from_url(
author_slug: str,
body: ImportURLConfirmRequest,
current_user=Depends(get_current_author_scoped),
db: AsyncSession = Depends(get_db),
redis=Depends(get_redis),
):
"""Create a book record from a supported platform URL and index it immediately."""
url = _validate_url(body.url)
return await BookService(db).import_from_url(
author_id=current_user.id,
url=url,
title_override=body.title,
author_override=body.author,
description_override=body.description,
cover_url_override=body.cover_url,
isbn_override=body.isbn,
genre_override=body.genre,
price_override=body.price,
price_hardcover=body.price_hardcover,
price_ebook=body.price_ebook,
price_audiobook=body.price_audiobook,
redis=redis,
)