Update api/main.py
Some checks failed
Build & Push Football Docker Images / build-push-update (push) Failing after 3s
Some checks failed
Build & Push Football Docker Images / build-push-update (push) Failing after 3s
This commit is contained in:
196
api/main.py
Normal file
196
api/main.py
Normal file
@@ -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} <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>"""
|
||||
Reference in New Issue
Block a user