Files
watchlist/app/main.py
T
ki 67f09363f3 Initial commit: WatchStack v0.1.0
- FastAPI + SQLite backend with REST API
- Polymorphic Media model (book + series)
- Cover lookup via Open Library
- Vanilla HTML/CSS/JS frontend (MAL-inspired)
- Dark theme, status tabs, drawer detail, stats modal
- 8 seed demo entries
2026-07-21 23:11:14 +02:00

154 lines
4.9 KiB
Python

"""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)