Files
football-api/main.py
2026-03-13 18:02:00 +00:00

196 lines
5.1 KiB
Python

#!/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} <span class='abbr'>({abbr})</span>"
# Row 2 — score or date · channel
if is_past and score:
row2 = f'<span class="score">{score}</span>'
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'<span class="fixture">{short_date}{time_part}{ch_part}</span>'
card_class = "match future"
divider = '<div class="divider"></div>' if idx == 1 else ""
return f"""
{divider}
<div class="{card_class}">
<div class="row1">{row1}</div>
<div class="row2">{row2}</div>
</div>"""
cards = "".join(match_html(m, i) for i, m in enumerate(matches))
return f"""<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<style>
* {{ margin: 0; padding: 0; box-sizing: border-box; }}
body {{
font-family: ui-sans-serif, system-ui, sans-serif;
background: transparent;
color: #e2e8f0;
display: flex;
align-items: center;
height: 100vh;
overflow: hidden;
}}
.container {{
display: flex;
align-items: stretch;
width: 100%;
padding: 0 6px;
}}
.match {{
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 2px 6px;
gap: 3px;
min-width: 0;
}}
.past {{ opacity: 0.72; }}
.future {{ opacity: 1; }}
.row1 {{
font-size: 11px;
font-weight: 600;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
width: 100%;
text-align: center;
color: #f1f5f9;
}}
.future .row1 {{ color: #93c5fd; }}
.abbr {{
font-size: 9px;
font-weight: 400;
color: #64748b;
}}
.row2 {{
font-size: 10px;
text-align: center;
white-space: nowrap;
}}
.score {{
color: #4ade80;
font-weight: 700;
}}
.fixture {{ color: #94a3b8; }}
.divider {{
width: 1px;
background: #1e293b;
margin: 6px 0;
flex-shrink: 0;
align-self: stretch;
}}
</style>
</head>
<body>
<div class="container">
{cards}
</div>
</body>
</html>"""