Files
cardpuller/routes/cards.py
T

110 lines
3.2 KiB
Python

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