#!/usr/bin/env python3 """ FC Porto fixtures API Endpoints: GET /health — liveness GET /data — raw JSON GET /next — next match GET /widget — HTML widget for Homepage iframe """ import json import re 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(): try: d = load() past, future = sorted_matches(d) except HTTPException: past, future = [], [] # Most recent 3 past (newest leftmost = reversed) + next 2 future last3 = list(reversed(past[-3:])) if len(past) >= 3 else list(reversed(past)) next2 = future[:2] if len(future) >= 2 else future empty_past = {"home": "—", "away": "—", "home_logo": None, "away_logo": None, "abbr": "—", "date": "—", "time": "", "score": None} empty_future = {**empty_past, "score": None} while len(last3) < 3: last3.append(dict(empty_past)) while len(next2) < 2: next2.append(dict(empty_future)) PORTO_LOGO = "https://a.espncdn.com/i/teamlogos/soccer/500/437.png" def clean_time(t: str) -> str: """Remove ESPN artifacts like 'v', 'v2nd Leg' etc from time field.""" t = t.strip() # If it looks like a time (contains :) keep it, otherwise TBD if re.search(r'\d+:\d+', t): return re.search(r'\d+:\d+\s*(?:AM|PM)?', t).group(0).strip() return "TBD" def shorten(name: str) -> str: if len(name) <= 10: return name parts = name.split() return parts[-1] if parts else name[:10] def card(m: dict, is_past: bool) -> str: home_logo = m.get("home_logo") or PORTO_LOGO away_logo = m.get("away_logo") or PORTO_LOGO home_s = shorten(m.get("home", "—")) away_s = shorten(m.get("away", "—")) abbr = m.get("abbr", "—") score = m.get("score") date = m.get("date", "—") time_raw = m.get("time", "") if is_past and score: middle = f'