diff --git a/pyproject.toml b/pyproject.toml index b28abc1..86565f0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -7,6 +7,7 @@ requires-python = ">=3.14" dependencies = [ "fastapi>=0.136.1", "httpx>=0.28.1", + "pillow>=12.2.0", "pyyaml>=6.0.3", "uvicorn[standard]>=0.46.0", ] diff --git a/routes/cards.py b/routes/cards.py index 080c1bc..bf63c80 100644 --- a/routes/cards.py +++ b/routes/cards.py @@ -1,13 +1,16 @@ import asyncio +import io import re from pathlib import Path from urllib.parse import quote import httpx -from fastapi import APIRouter, HTTPException +from fastapi import APIRouter, File, Form, HTTPException, UploadFile from fastapi.responses import FileResponse, StreamingResponse +from PIL import Image import db +from worker import safe_filename from config import OUTPUTS router = APIRouter() @@ -43,6 +46,46 @@ async def deck_cards(slug: str): ] +@router.post("/decks/{slug}/upload-image", status_code=201) +async def upload_image(slug: str, name: str = Form(...), file: UploadFile = File(...)): + slug = valid_slug(slug) + deck = db.get_deck(slug) + if not deck: + raise HTTPException(404) + name = name.strip() + if not name: + raise HTTPException(400, "name required") + + raw = await file.read() + try: + img = Image.open(io.BytesIO(raw)) + img.load() + except Exception: + raise HTTPException(400, "unreadable image") + if img.mode != "RGB": + img = img.convert("RGB") + + deck_dir = OUTPUTS / slug + deck_dir.mkdir(exist_ok=True) + + with db.conn() as c: + max_pos = c.execute( + "SELECT COALESCE(MAX(position), 0) FROM cards WHERE deck_slug=?", (slug,) + ).fetchone()[0] + cur = c.execute( + "INSERT INTO cards (deck_slug, name, position, fetch_status, price_usd) VALUES (?,?,?,?,0)", + (slug, name, max_pos + 1, "done") + ) + card_id = cur.lastrowid + filename = f"{safe_filename(name)}_{card_id}.png" + img.save(deck_dir / filename, "PNG") + c.execute("UPDATE cards SET filename=? WHERE id=?", (filename, card_id)) + c.execute("INSERT INTO logs (deck_slug, line) VALUES (?,?)", (slug, f"[{max_pos + 1}] {name} — UPLOADED")) + + db.recalc_done(slug) + return {"id": card_id, "filename": filename} + + @router.get("/decks/{slug}/stream") async def stream(slug: str): slug = valid_slug(slug) diff --git a/static/index.html b/static/index.html index f9602af..822ba06 100644 --- a/static/index.html +++ b/static/index.html @@ -348,7 +348,12 @@ function renderCards(slug, cards) { addSection = document.createElement("div"); addSection.className = "edit-add-section"; addSection.innerHTML = ` -
Search to Add
+
Upload Image (token / custom card)
+ + +
+ +
Search to Add