"""FastAPI-App: REST-API + statisches Frontend.""" from __future__ import annotations import logging from pathlib import Path from typing import Optional from fastapi import Depends, FastAPI, HTTPException, Query, Request from fastapi.responses import HTMLResponse, RedirectResponse from fastapi.staticfiles import StaticFiles from sqlalchemy.orm import Session from app import crud, schemas from app.database import get_db, init_db from app.external import lookup_book_by_isbn, lookup_book_by_title from app.seed import seed_if_empty logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(name)s: %(message)s") log = logging.getLogger("watchstack") BASE_DIR = Path(__file__).resolve().parent STATIC_DIR = BASE_DIR / "static" app = FastAPI( title="WatchStack", description="Watchlist für Bücher, Serien & mehr.", version="0.1.0", ) @app.on_event("startup") def _startup() -> None: init_db() with next(get_db()) as db: seed_if_empty(db) # ---------- Statische Dateien & UI ------------------------------------------ app.mount("/static", StaticFiles(directory=STATIC_DIR), name="static") @app.get("/", response_class=HTMLResponse, include_in_schema=False) def root(request: Request) -> HTMLResponse: index = (STATIC_DIR / "index.html").read_text(encoding="utf-8") return HTMLResponse(index) # ---------- Health ----------------------------------------------------------- @app.get("/api/health", tags=["meta"]) def health() -> dict: return {"status": "ok", "service": "watchstack", "version": app.version} # ---------- Metadaten-Lookup ------------------------------------------------- @app.get("/api/lookup/book", tags=["lookup"]) def lookup_book( isbn: Optional[str] = Query(None, description="ISBN-10 oder ISBN-13"), title: Optional[str] = Query(None), ) -> dict: """Schlägt Buchdaten extern nach. ISBN hat Vorrang.""" if isbn: result = lookup_book_by_isbn(isbn) elif title: result = lookup_book_by_title(title) else: raise HTTPException(400, "isbn oder title erforderlich") if not result: raise HTTPException(404, "Keine Daten gefunden") return result # ---------- Media ------------------------------------------------------------ @app.get("/api/media", response_model=list[schemas.MediaOut], tags=["media"]) def api_list_media( kind: Optional[str] = Query(None, pattern="^(book|series)$"), status: Optional[str] = Query(None, pattern="^(plan|reading|done|hold|dropped)$"), search: Optional[str] = Query(None), genre: Optional[str] = Query(None), tag: Optional[str] = Query(None), sort: str = Query("updated_desc"), db: Session = Depends(get_db), ): items = crud.list_media(db, kind=kind, status=status, search=search, genre=genre, tag=tag, sort=sort) return items @app.post("/api/media", response_model=schemas.MediaOut, status_code=201, tags=["media"]) def api_create_media(payload: schemas.MediaCreate, db: Session = Depends(get_db)): return crud.create_media(db, payload) @app.get("/api/media/{media_id}", response_model=schemas.MediaOut, tags=["media"]) def api_get_media(media_id: int, db: Session = Depends(get_db)): m = crud.get_media(db, media_id) if not m: raise HTTPException(404, "Eintrag nicht gefunden") return m @app.patch("/api/media/{media_id}", response_model=schemas.MediaOut, tags=["media"]) def api_update_media( media_id: int, payload: schemas.MediaUpdate, db: Session = Depends(get_db), ): m = crud.get_media(db, media_id) if not m: raise HTTPException(404, "Eintrag nicht gefunden") return crud.update_media(db, m, payload) @app.delete("/api/media/{media_id}", status_code=204, tags=["media"]) def api_delete_media(media_id: int, db: Session = Depends(get_db)): m = crud.get_media(db, media_id) if not m: raise HTTPException(404, "Eintrag nicht gefunden") crud.delete_media(db, m) return None @app.post("/api/media/{media_id}/progress", response_model=schemas.MediaOut, tags=["media"]) def api_progress( media_id: int, payload: schemas.ProgressUpdate, db: Session = Depends(get_db), ): m = crud.get_media(db, media_id) if not m: raise HTTPException(404, "Eintrag nicht gefunden") return crud.bump_progress(db, m, payload) # ---------- Taxonomien ------------------------------------------------------- @app.get("/api/genres", response_model=list[schemas.GenreOut], tags=["taxonomy"]) def api_genres(db: Session = Depends(get_db)): return crud.list_genres(db) @app.get("/api/tags", response_model=list[schemas.TagOut], tags=["taxonomy"]) def api_tags(db: Session = Depends(get_db)): return crud.list_tags(db) # ---------- Statistik -------------------------------------------------------- @app.get("/api/stats", response_model=schemas.StatsResponse, tags=["stats"]) def api_stats(db: Session = Depends(get_db)): return crud.stats(db)