Files
2026-06-13 12:28:58 -05:00

170 lines
5.5 KiB
Python

import re
import shutil
from fastapi import APIRouter, HTTPException, Request
import db
import worker
from config import OUTPUTS
from worker import is_scry_url
router = APIRouter()
def slugify(name: str) -> str:
return re.sub(r"[^a-z0-9]+", "_", name.lower().strip()).strip("_")
def valid_slug(slug: str) -> str:
if not re.match(r"^[a-z0-9_]+$", slug):
raise HTTPException(400, "invalid slug")
return slug
def line_to_entry(line: str) -> dict:
line = line.strip()
if is_scry_url(line):
return {"name": "", "scry_url": line}
return {"name": line, "scry_url": None}
@router.get("/decks")
async def list_decks():
return [dict(d) for d in db.get_decks()]
@router.get("/decks/{slug}")
async def get_deck(slug: str):
slug = valid_slug(slug)
d = db.get_deck(slug)
if not d:
raise HTTPException(404)
return {**dict(d), "log": db.get_logs(slug)}
@router.post("/decks", status_code=201)
async def create_deck(request: Request):
body = await request.json()
name = body.get("deck_name", "").strip()
card_text = body.get("cards", "").strip()
commander = body.get("commander", "").strip()
is_tokens = bool(body.get("is_tokens"))
if not name:
raise HTTPException(400, "deck_name required")
entries = [line_to_entry(l) for l in card_text.splitlines() if l.strip()]
if not is_tokens and not entries:
raise HTTPException(400, "cards required")
if is_tokens:
commander = ""
else:
if not commander:
commander = next((e["name"] for e in entries if e["name"]), "")
if commander:
idx = next((i for i, e in enumerate(entries) if e["name"] == commander), None)
if idx is not None:
entries.insert(0, entries.pop(idx))
else:
entries.insert(0, {"name": commander, "scry_url": None})
slug = slugify(name)
existing = db.get_deck(slug)
if existing and existing["status"] in ("queued", "running"):
raise HTTPException(409, "deck already processing")
with db.conn() as c:
c.execute(
"INSERT OR REPLACE INTO decks (slug, deck_name, status, commander, price_usd, done, total) VALUES (?,?,?,?,0,0,?)",
(slug, name, "queued", commander or None, len(entries))
)
c.execute("DELETE FROM cards WHERE deck_slug=?", (slug,))
c.execute("DELETE FROM logs WHERE deck_slug=?", (slug,))
for i, e in enumerate(entries):
c.execute(
"INSERT INTO cards (deck_slug, name, position, scry_url, fetch_status) VALUES (?,?,?,?,'pending')",
(slug, e["name"], i + 1, e["scry_url"])
)
await worker.queue.put(slug)
return {"slug": slug}
@router.patch("/decks/{slug}")
async def edit_deck(slug: str, request: Request):
slug = valid_slug(slug)
deck = db.get_deck(slug)
if not deck:
raise HTTPException(404)
if deck["status"] in ("queued", "running"):
raise HTTPException(409, "deck is processing")
body = await request.json()
remove_ids: list[int] = body.get("remove", [])
add_entries = [line_to_entry(n) for n in body.get("add", []) if n.strip()]
new_name: str = body.get("deck_name", deck["deck_name"]).strip() or deck["deck_name"]
raw_cmd = body.get("commander", None)
if raw_cmd is not None:
new_commander = raw_cmd.strip() or (deck["commander"] or None)
else:
new_commander = deck["commander"]
with db.conn() as c:
for cid in remove_ids:
row = c.execute(
"SELECT filename, back_filename FROM cards WHERE id=? AND deck_slug=?", (cid, slug)
).fetchone()
if row:
for fn in (row["filename"], row["back_filename"]):
if fn and (OUTPUTS / slug / fn).exists():
(OUTPUTS / slug / fn).unlink()
c.execute("DELETE FROM cards WHERE id=? AND deck_slug=?", (cid, slug))
max_pos = c.execute(
"SELECT COALESCE(MAX(position), 0) FROM cards WHERE deck_slug=?", (slug,)
).fetchone()[0]
for i, e in enumerate(add_entries):
c.execute(
"INSERT INTO cards (deck_slug, name, position, scry_url, fetch_status) VALUES (?,?,?,?,'pending')",
(slug, e["name"], max_pos + i + 1, e["scry_url"])
)
if new_commander:
row = c.execute(
"SELECT id FROM cards WHERE deck_slug=? AND name=? LIMIT 1", (slug, new_commander)
).fetchone()
if not row:
c.execute(
"INSERT INTO cards (deck_slug, name, position, fetch_status) VALUES (?,?,0,'pending')",
(slug, new_commander)
)
c.execute(
"UPDATE decks SET deck_name=?, commander=?, status='queued' WHERE slug=?",
(new_name, new_commander, slug)
)
c.execute("INSERT INTO logs (deck_slug, line) VALUES (?,?)", (slug, "Deck edited — re-queued."))
db.recalc_done(slug)
await worker.queue.put(slug)
return {"slug": slug}
@router.delete("/decks/{slug}", status_code=204)
async def delete_deck(slug: str):
slug = valid_slug(slug)
deck = db.get_deck(slug)
if not deck:
raise HTTPException(404)
if deck["status"] in ("queued", "running"):
raise HTTPException(409, "can't delete while processing")
deck_dir = OUTPUTS / slug
if deck_dir.exists():
shutil.rmtree(deck_dir)
with db.conn() as c:
c.execute("DELETE FROM decks WHERE slug=?", (slug,))
return None