diff --git a/main.py b/main.py new file mode 100644 index 0000000..15bae7f --- /dev/null +++ b/main.py @@ -0,0 +1,196 @@ +#!/usr/bin/env python3 +""" +FC Porto fixtures API +Endpoints: + GET /health — liveness + GET /data — raw JSON + GET /next — next match (for Homepage customapi) + GET /widget — self-contained HTML widget for Homepage iframe +""" + +import json +from datetime import datetime +from pathlib import Path +from zoneinfo import ZoneInfo + +from fastapi import FastAPI, HTTPException +from fastapi.middleware.cors import CORSMiddleware +from fastapi.responses import HTMLResponse + +DATA_FILE = Path("/data/porto.json") +PT_TZ = ZoneInfo("Europe/Lisbon") + +app = FastAPI(title="FC Porto API", version="1.0.0") +app.add_middleware(CORSMiddleware, allow_origins=["*"], allow_methods=["GET"], allow_headers=["*"]) + + +def load() -> dict: + if not DATA_FILE.exists(): + raise HTTPException(503, "Data not yet available — scraper hasn't run yet") + return json.loads(DATA_FILE.read_text()) + + +@app.get("/health") +def health(): + return {"status": "ok", "data_exists": DATA_FILE.exists()} + + +@app.get("/data") +def data(): + return load() + + +@app.get("/next") +def next_match(): + d = load() + future = d.get("all_future", []) + if not future: + return {"opponent": "No upcoming matches", "date": "—", "competition": "—", "channel": "—"} + m = future[0] + return { + "opponent": m["opponent"], + "date": f"{m['date']} {m['time']}", + "competition": m["competition"], + "channel": m["channel"], + "venue": "Home" if m["is_home"] else "Away", + } + + +@app.get("/widget", response_class=HTMLResponse) +def widget(): + """Self-contained HTML widget for Homepage iframe top bar — 2 row layout.""" + try: + d = load() + matches = d.get("display", []) + except HTTPException: + matches = [] + + # Competition abbreviations + ABBR = { + "Liga Portugal": "LP", + "Champions League": "CL", + "Europa League": "UE", + "Taça de Portugal": "TP", + "Taça da Liga": "TL", + "Supertaça": "ST", + } + + # Pad to exactly 4 + empty = {"home": "—", "away": "—", "competition": "—", + "date": "—", "time": "—", "score": None, "is_past": False, "channel": "—"} + while len(matches) < 4: + matches.append(empty) + matches = matches[:4] + + def match_html(m: dict, idx: int) -> str: + is_past = m.get("is_past", False) + score = m.get("score") + home = m.get("home", "—") + away = m.get("away", "—") + comp = m.get("competition", "—") + date = m.get("date", "—") + time_ = m.get("time", "") + channel = m.get("channel", "") + + abbr = ABBR.get(comp, comp[:2].upper() if comp != "—" else "—") + + # Row 1 — "Porto vs Benfica (LP)" + row1 = f"{home} vs {away} ({abbr})" + + # Row 2 — score or date · channel + if is_past and score: + row2 = f'{score}' + card_class = "match past" + else: + # shorten date: dd/mm/yy → dd/mm + short_date = "/".join(date.split("/")[:2]) if "/" in date else date + time_part = f" {time_}" if time_ and time_ != "TBD" else "" + ch_part = f" · {channel}" if channel and channel not in ("TBD", "—", "") else "" + row2 = f'{short_date}{time_part}{ch_part}' + card_class = "match future" + + divider = '
' if idx == 1 else "" + + return f""" + {divider} +