import asyncio import json import secrets from collections import Counter from contextlib import asynccontextmanager from fastapi import Depends, FastAPI, HTTPException from fastapi.responses import FileResponse from fastapi.security import HTTPBasic, HTTPBasicCredentials import db import worker from config import OUTPUTS, USERNAME, PASSWORD, PORT from routes.decks import router as decks_router from routes.cards import router as cards_router, public_router from worker import safe_filename security = HTTPBasic() def auth(creds: HTTPBasicCredentials = Depends(security)): ok = ( secrets.compare_digest(creds.username.encode(), USERNAME.encode()) and secrets.compare_digest(creds.password.encode(), PASSWORD.encode()) ) if not ok: raise HTTPException(401, headers={"WWW-Authenticate": "Basic"}) @asynccontextmanager async def lifespan(app: FastAPI): db.init() OUTPUTS.mkdir(exist_ok=True) for d in OUTPUTS.iterdir(): if not d.is_dir() or not (d / "cards.txt").exists(): continue slug = d.name if db.get_deck(slug): continue card_names = [l for l in (d / "cards.txt").read_text().splitlines() if l] status = "failed" deck_name = slug.replace("_", " ").title() if (d / "deck.tts.json").exists(): status = "complete" try: deck_name = json.loads((d / "deck.tts.json").read_text()).get("SaveName", deck_name) except Exception: pass counts = Counter(card_names) seen: Counter = Counter() with db.conn() as c: c.execute( "INSERT OR IGNORE INTO decks (slug, deck_name, status, commander, price_usd, done, total) VALUES (?,?,?,?,0,?,?)", (slug, deck_name, status, card_names[0] if card_names else None, len(card_names), len(card_names)) ) for i, name in enumerate(card_names): seen[name] += 1 occ = seen[name] total = counts[name] base = safe_filename(name) suffix = f"_{occ}" if total > 1 else "" filename = f"{base}{suffix}.png" exists = (d / filename).exists() c.execute( "INSERT INTO cards (deck_slug, name, position, filename, fetch_status) VALUES (?,?,?,?,?)", (slug, name, i + 1, filename if exists else None, "done" if exists else "failed") ) c.execute("INSERT INTO logs (deck_slug, line) VALUES (?,?)", (slug, "Loaded from disk.")) asyncio.create_task(worker.run()) yield app = FastAPI(lifespan=lifespan) app.include_router(decks_router, dependencies=[Depends(auth)]) app.include_router(cards_router, dependencies=[Depends(auth)]) app.include_router(public_router) # no auth — TTS app fetches card images directly @app.get("/decks/{slug}/tts.json") async def tts_json(slug: str): path = OUTPUTS / slug / "deck.tts.json" if not path.exists(): raise HTTPException(404, "TTS file not found") return FileResponse(path, media_type="application/json") @app.get("/") async def index(_=Depends(auth)): return FileResponse("static/index.html") @app.get("/card_back.jpg") async def card_back(): return FileResponse("static/card_back.jpg", media_type="image/jpeg") if __name__ == "__main__": import uvicorn uvicorn.run(app, host="0.0.0.0", port=PORT)