#!/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): """Return all matches sorted by timestamp, fixing any year-rollover issues.""" now_ts = datetime.now(PT_TZ).timestamp() past = [] future = [] for m in d.get("all_past", []): if m.get("timestamp", 0) > 0: past.append(m) for m in d.get("all_future", []): if m.get("timestamp", 0) > 0: future.append(m) # Sort both past.sort(key=lambda x: x["timestamp"]) future.sort(key=lambda x: x["timestamp"]) # Edge case: some "future" matches may have wrong year from scraper # Keep only those with timestamp > now future = [m for m in future if m["timestamp"] > now_ts] # Keep only past with timestamp <= now past = [m for m in past if m["timestamp"] <= now_ts] 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", "") # 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'