103 lines
3.4 KiB
Python
103 lines
3.4 KiB
Python
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) |