#!/usr/bin/env python3 """ FFTCG Card Data Reviewer — Local web tool for browsing and editing scanned card data. Usage: python tools/card_reviewer.py # Start on port 8080 python tools/card_reviewer.py --port 9000 # Custom port Opens a browser showing each card's image alongside its detected data. Edit fields and save changes back to data/cards.json. """ import argparse import json import os import sys import webbrowser from http.server import HTTPServer, BaseHTTPRequestHandler from pathlib import Path from urllib.parse import urlparse, parse_qs PROJECT_ROOT = Path(__file__).parent.parent CARDS_FILE = PROJECT_ROOT / "data" / "cards.json" REVIEWED_FILE = PROJECT_ROOT / "data" / "reviewed.json" SOURCE_CARDS_DIR = PROJECT_ROOT / "source-cards" # Load cards into memory cards_data = None reviewed_data = None def load_cards(): global cards_data with open(CARDS_FILE, "r") as f: cards_data = json.load(f) def save_cards(): with open(CARDS_FILE, "w") as f: json.dump(cards_data, f, indent=2, ensure_ascii=False) def load_reviewed(): global reviewed_data if REVIEWED_FILE.exists(): with open(REVIEWED_FILE, "r") as f: reviewed_data = json.load(f) else: reviewed_data = {"reviewed": []} def save_reviewed(): with open(REVIEWED_FILE, "w") as f: json.dump(reviewed_data, f, indent=2, ensure_ascii=False) HTML_PAGE = r""" FFTCG Card Reviewer
0 cards
""" class CardReviewerHandler(BaseHTTPRequestHandler): def log_message(self, format, *args): # Suppress default access logging pass def _send_json(self, data, status=200): body = json.dumps(data, ensure_ascii=False).encode("utf-8") self.send_response(status) self.send_header("Content-Type", "application/json; charset=utf-8") self.send_header("Content-Length", str(len(body))) self.end_headers() self.wfile.write(body) def _send_html(self, html): body = html.encode("utf-8") self.send_response(200) self.send_header("Content-Type", "text/html; charset=utf-8") self.send_header("Content-Length", str(len(body))) self.end_headers() self.wfile.write(body) def _send_404(self): self.send_response(404) self.send_header("Content-Type", "text/plain") self.end_headers() self.wfile.write(b"Not Found") def do_GET(self): parsed = urlparse(self.path) path = parsed.path # Serve main page if path == "/" or path == "": self._send_html(HTML_PAGE) return # API: get all cards if path == "/api/cards": self._send_json(cards_data) return # API: get reviewed state if path == "/api/reviewed": self._send_json(reviewed_data) return # API: get single card if path.startswith("/api/cards/"): card_id = path[len("/api/cards/"):] card = next((c for c in cards_data["cards"] if c["id"] == card_id), None) if card: self._send_json(card) else: self._send_404() return # Serve card images if path.startswith("/images/"): filename = path[len("/images/"):] filepath = SOURCE_CARDS_DIR / filename if filepath.exists() and filepath.is_file(): with open(filepath, "rb") as f: data = f.read() self.send_response(200) ext = filepath.suffix.lower() if ext in (".jpg", ".jpeg"): self.send_header("Content-Type", "image/jpeg") elif ext == ".png": self.send_header("Content-Type", "image/png") else: self.send_header("Content-Type", "application/octet-stream") self.send_header("Content-Length", str(len(data))) self.send_header("Cache-Control", "public, max-age=86400") self.end_headers() self.wfile.write(data) else: self._send_404() return self._send_404() def do_POST(self): parsed = urlparse(self.path) path = parsed.path # API: save reviewed state if path == "/api/reviewed": content_length = int(self.headers.get("Content-Length", 0)) body = self.rfile.read(content_length) try: global reviewed_data reviewed_data = json.loads(body) save_reviewed() self._send_json({"ok": True}) except json.JSONDecodeError: self._send_json({"error": "Invalid JSON"}, 400) return # API: save card if path.startswith("/api/cards/"): card_id = path[len("/api/cards/"):] content_length = int(self.headers.get("Content-Length", 0)) body = self.rfile.read(content_length) try: new_data = json.loads(body) except json.JSONDecodeError: self._send_json({"error": "Invalid JSON"}, 400) return # Find and update card for i, card in enumerate(cards_data["cards"]): if card["id"] == card_id: # Preserve abilities from existing card if not provided if "abilities" not in new_data: new_data["abilities"] = card.get("abilities", []) cards_data["cards"][i] = new_data save_cards() self._send_json({"ok": True}) return self._send_json({"error": "Card not found"}, 404) return self._send_404() def main(): parser = argparse.ArgumentParser(description="FFTCG Card Data Reviewer") parser.add_argument("--port", type=int, default=8080, help="Port to serve on") parser.add_argument("--no-browser", action="store_true", help="Don't open browser automatically") args = parser.parse_args() load_cards() load_reviewed() print(f"Loaded {len(cards_data['cards'])} cards from {CARDS_FILE}") print(f"Reviewed: {len(reviewed_data.get('reviewed', []))} cards") print(f"Serving card images from {SOURCE_CARDS_DIR}") print(f"Starting server at http://localhost:{args.port}") server = HTTPServer(("localhost", args.port), CardReviewerHandler) if not args.no_browser: webbrowser.open(f"http://localhost:{args.port}") try: server.serve_forever() except KeyboardInterrupt: print("\nShutting down.") server.shutdown() if __name__ == "__main__": main()