import re import shutil from fastapi import APIRouter, HTTPException, Request import db import worker from config import OUTPUTS 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 @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() if not name or not card_text: raise HTTPException(400, "deck_name and cards required") slug = slugify(name) card_names = [l.strip() for l in card_text.splitlines() if l.strip()] if not card_names: raise HTTPException(400, "no cards") if not commander: commander = card_names[0] # Commander always at position 0; remove any existing occurrence first card_names = [n for n in card_names if n != commander] card_names.insert(0, commander) 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, len(card_names)) ) c.execute("DELETE FROM cards WHERE deck_slug=?", (slug,)) c.execute("DELETE FROM logs WHERE deck_slug=?", (slug,)) for i, card_name in enumerate(card_names): c.execute( "INSERT INTO cards (deck_slug, name, position, fetch_status) VALUES (?,?,?,'pending')", (slug, card_name, i + 1) ) 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_names: list[str] = [n.strip() for n in body.get("add", []) if n.strip()] new_name: str = body.get("deck_name", deck["deck_name"]).strip() or deck["deck_name"] new_commander: str = body.get("commander", deck["commander"]).strip() or deck["commander"] with db.conn() as c: for cid in remove_ids: row = c.execute( "SELECT filename FROM cards WHERE id=? AND deck_slug=?", (cid, slug) ).fetchone() if row and row["filename"]: p = OUTPUTS / slug / row["filename"] if p.exists(): p.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, card_name in enumerate(add_names): c.execute( "INSERT INTO cards (deck_slug, name, position, fetch_status) VALUES (?,?,?,'pending')", (slug, card_name, max_pos + i + 1) ) # Ensure commander has a card row; if not, insert at position 0 so it sorts first existing_commander = c.execute( "SELECT id FROM cards WHERE deck_slug=? AND name=? LIMIT 1", (slug, new_commander) ).fetchone() if not existing_commander: 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