Spaces:
Running
Running
| """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 | |
| 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) | |
| 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 | |
| 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 | |
| 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 | |
| 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 | |
| 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) | |
| 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 | |
| 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 | |
| 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, | |
| ) | |
| 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, | |
| ) | |