import asyncio import re from pathlib import Path from urllib.parse import quote import httpx from fastapi import APIRouter, HTTPException from fastapi.responses import FileResponse, StreamingResponse import db from config import OUTPUTS router = APIRouter() public_router = APIRouter() def valid_slug(slug: str) -> str: if not re.match(r"^[a-z0-9_]+$", slug): raise HTTPException(400, "invalid slug") return slug @router.get("/decks/{slug}/cards") async def deck_cards(slug: str): slug = valid_slug(slug) if not db.get_deck(slug): raise HTTPException(404) cards = db.get_cards(slug) return [ { "id": c["id"], "name": c["name"], "position": c["position"], "filename": c["filename"], "exists": bool(c["filename"] and (OUTPUTS / slug / c["filename"]).exists()), "fetch_status": c["fetch_status"], "price_usd": c["price_usd"], } for c in cards ] @router.get("/decks/{slug}/stream") async def stream(slug: str): slug = valid_slug(slug) async def gen(): sent = 0 while True: deck = db.get_deck(slug) if not deck: yield "data: not found\n\n" return logs = db.get_logs(slug) while sent < len(logs): yield f"data: {logs[sent]}\n\n" sent += 1 if deck["status"] in ("complete", "failed"): yield "data: __DONE__\n\n" return await asyncio.sleep(0.5) return StreamingResponse(gen(), media_type="text/event-stream") @router.get("/decks/{slug}/tts") async def download_tts(slug: str): slug = valid_slug(slug) p = OUTPUTS / slug / "deck.tts.json" if not p.exists(): raise HTTPException(404) return FileResponse(p, filename=f"{slug}.json", media_type="application/json") @router.get("/search") async def search_cards(q: str = ""): if len(q) < 2: raise HTTPException(400, "query too short") url = f"https://api.scryfall.com/cards/search?q=name:{quote(q)}&unique=cards&order=name" async with httpx.AsyncClient(timeout=10, headers={"User-Agent": "card-server/1.0"}) as client: r = await client.get(url) if r.status_code == 404: return [] r.raise_for_status() results = [] for card in r.json().get("data", [])[:12]: img = None if "image_uris" in card: img = card["image_uris"].get("normal") elif card.get("card_faces") and card["card_faces"][0].get("image_uris"): img = card["card_faces"][0]["image_uris"]["normal"] results.append({ "name": card["name"], "set_name": card.get("set_name", ""), "price": card.get("prices", {}).get("usd"), "image": img, }) return results # No auth — TTS desktop app fetches these directly @public_router.get("/cards/{slug}/{filename}") async def serve_card(slug: str, filename: str): slug = Path(slug).name filename = Path(filename).name p = OUTPUTS / slug / filename if not p.exists(): raise HTTPException(404) return FileResponse(p, media_type="image/png")