#!/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="2.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()) def sorted_matches(d: dict): """ Split by score: has score = past, no score = future. Combines all_past + all_future, deduplicates, filters year-rollover ghosts. """ now_ts = datetime.now(PT_TZ).timestamp() max_future = now_ts + 60 * 60 * 24 * 180 # 6 months ahead all_matches = [] for m in d.get("all_past", []) + d.get("all_future", []): ts = m.get("timestamp", 0) if ts > 0 and ts < max_future: all_matches.append(m) # Deduplicate by (home, away, date) seen, unique = set(), [] for m in all_matches: key = (m.get("home"), m.get("away"), m.get("date")) if key not in seen: seen.add(key) unique.append(m) unique.sort(key=lambda x: x["timestamp"]) past = [m for m in unique if m.get("score")] future = [m for m in unique if not m.get("score")] return past, future @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 = sorted_matches(d) if not future: return {"opponent": "No upcoming matches", "date": "—", "competition": "—", "venue": "—"} m = future[0] return { "opponent": m["opponent"], "date": f"{m['date']} {m['time']}", "competition": m["competition"], "venue": "Home" if m["is_home"] else "Away", "home": m["home"], "away": m["away"], } @app.get("/widget", response_class=HTMLResponse) def widget(): """ Layout: 3 past results + 2 upcoming fixtures For each match card: [ home badge ] [ home name ] [ score/date ] [ away badge ] [ away name ] [ comp abbr ] """ try: d = load() past, future = sorted_matches(d) except HTTPException: past, future = [], [] # Last 3 past + next 2 future last3 = past[-3:] if len(past) >= 3 else past next2 = future[:2] if len(future) >= 2 else future # Pad with empties empty = { "home": "—", "away": "—", "home_logo": None, "away_logo": None, "competition": "—", "abbr": "—", "date": "—", "time": "", "score": None, "is_past": True, } while len(last3) < 3: last3.insert(0, dict(empty)) while len(next2) < 2: next2.append(dict(empty, is_past=False)) all_cards = last3 + next2 PORTO_LOGO = "https://a.espncdn.com/i/teamlogos/soccer/500/437.png" def card(m: dict, is_past: bool) -> str: home = m.get("home", "—") away = m.get("away", "—") home_logo = m.get("home_logo") or PORTO_LOGO away_logo = m.get("away_logo") or PORTO_LOGO abbr = m.get("abbr", "—") score = m.get("score") date = m.get("date", "—") time_ = m.get("time", "") time_str = m.get("time", "") time_str = re.sub(r'^v\s*', '', time_str).strip() time_str = time_str if time_str not in ("", "TBD", "v") else "TBD" # Shorten names for display def shorten(name: str) -> str: if len(name) <= 10: return name parts = name.split() return parts[-1] if parts else name[:10] home_s = shorten(home) away_s = shorten(away) if is_past and score: middle = f'