Update api/main.py
All checks were successful
Build & Push Football Docker Images / build-push-update (push) Successful in 7s

This commit is contained in:
2026-04-12 14:04:11 +00:00
parent 2f8e0a517a
commit a319b77d2e

View File

@@ -20,8 +20,13 @@ from fastapi.responses import HTMLResponse
DATA_FILE = Path("/data/porto.json") DATA_FILE = Path("/data/porto.json")
PT_TZ = ZoneInfo("Europe/Lisbon") PT_TZ = ZoneInfo("Europe/Lisbon")
app = FastAPI(title="FC Porto API", version="1.0.0") app = FastAPI(title="FC Porto API", version="2.0.0")
app.add_middleware(CORSMiddleware, allow_origins=["*"], allow_methods=["GET"], allow_headers=["*"]) app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_methods=["GET"],
allow_headers=["*"],
)
def load() -> dict: def load() -> dict:
@@ -30,6 +35,34 @@ def load() -> dict:
return json.loads(DATA_FILE.read_text()) 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") @app.get("/health")
def health(): def health():
return {"status": "ok", "data_exists": DATA_FILE.exists()} return {"status": "ok", "data_exists": DATA_FILE.exists()}
@@ -43,154 +76,187 @@ def data():
@app.get("/next") @app.get("/next")
def next_match(): def next_match():
d = load() d = load()
future = d.get("all_future", []) _, future = sorted_matches(d)
if not future: if not future:
return {"opponent": "No upcoming matches", "date": "", "competition": "", "channel": ""} return {"opponent": "No upcoming matches", "date": "", "competition": "", "venue": ""}
m = future[0] m = future[0]
return { return {
"opponent": m["opponent"], "opponent": m["opponent"],
"date": f"{m['date']} {m['time']}", "date": f"{m['date']} {m['time']}",
"competition": m["competition"], "competition": m["competition"],
"channel": m["channel"],
"venue": "Home" if m["is_home"] else "Away", "venue": "Home" if m["is_home"] else "Away",
"home": m["home"],
"away": m["away"],
} }
@app.get("/widget", response_class=HTMLResponse) @app.get("/widget", response_class=HTMLResponse)
def widget(): def widget():
"""Self-contained HTML widget for Homepage iframe top bar — 2 row layout.""" """
Layout: 3 past results + 2 upcoming fixtures
For each match card:
[ home badge ]
[ home name ]
[ score/date ]
[ away badge ]
[ away name ]
[ comp abbr ]
"""
try: try:
d = load() d = load()
matches = d.get("display", []) past, future = sorted_matches(d)
except HTTPException: except HTTPException:
matches = [] past, future = [], []
# Competition abbreviations # Last 3 past + next 2 future
ABBR = { last3 = past[-3:] if len(past) >= 3 else past
"Liga Portugal": "LP", next2 = future[:2] if len(future) >= 2 else future
"Champions League": "CL",
"Europa League": "UE", # Pad with empties
"Taça de Portugal": "TP", empty = {
"Taça da Liga": "TL", "home": "", "away": "",
"Supertaça": "ST", "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))
# Pad to exactly 4 all_cards = last3 + next2
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: PORTO_LOGO = "https://a.espncdn.com/i/teamlogos/soccer/500/437.png"
is_past = m.get("is_past", False)
score = m.get("score") def card(m: dict, is_past: bool) -> str:
home = m.get("home", "") home = m.get("home", "")
away = m.get("away", "") away = m.get("away", "")
comp = m.get("competition", "") 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", "") date = m.get("date", "")
time_ = m.get("time", "") time_ = m.get("time", "")
channel = m.get("channel", "")
abbr = ABBR.get(comp, comp[:2].upper() if comp != "" else "") # 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]
# Row 1 — "Porto vs Benfica (LP)" home_s = shorten(home)
row1 = f"{home} vs {away} <span class='abbr'>({abbr})</span>" away_s = shorten(away)
# Row 2 — score or date · channel
if is_past and score: if is_past and score:
row2 = f'<span class="score">{score}</span>' middle = f'<div class="score">{score}</div>'
card_class = "match past" cls = "card past"
else: else:
# shorten date: dd/mm/yy → dd/mm date_s = "/".join(date.split("/")[:2]) if "/" in date else date
short_date = "/".join(date.split("/")[:2]) if "/" in date else date time_s = f"<br>{time_}" if time_ and time_ != "TBD" else ""
time_part = f" {time_}" if time_ and time_ != "TBD" else "" middle = f'<div class="fixture">{date_s}{time_s}</div>'
ch_part = f" · {channel}" if channel and channel not in ("TBD", "", "") else "" cls = "card future"
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""" return f"""
{divider} <div class="{cls}">
<div class="{card_class}"> <img src="{home_logo}" class="badge" alt="{home}">
<div class="row1">{row1}</div> <div class="name home-name">{home_s}</div>
<div class="row2">{row2}</div> {middle}
<img src="{away_logo}" class="badge" alt="{away}">
<div class="name away-name">{away_s}</div>
<div class="comp">{abbr}</div>
</div>""" </div>"""
cards = "".join(match_html(m, i) for i, m in enumerate(matches)) past_cards = "".join(card(m, True) for m in last3)
future_cards = "".join(card(m, False) for m in next2)
return f"""<!DOCTYPE html> return f"""<!DOCTYPE html>
<html> <html>
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<style> <style>
* {{ margin: 0; padding: 0; box-sizing: border-box; }} * {{ margin:0; padding:0; box-sizing:border-box; }}
body {{ body {{
font-family: ui-sans-serif, system-ui, sans-serif; font-family: ui-sans-serif, system-ui, sans-serif;
background: transparent; background: transparent;
color: #e2e8f0; color: #e2e8f0;
height: 100vh;
display: flex; display: flex;
align-items: center; align-items: center;
height: 100vh;
overflow: hidden; overflow: hidden;
}} }}
.section {{
display: flex;
align-items: stretch;
gap: 2px;
}}
.divider {{
width: 1px;
background: #334155;
margin: 4px 6px;
align-self: stretch;
flex-shrink: 0;
}}
.container {{ .container {{
display: flex; display: flex;
align-items: stretch; align-items: center;
width: 100%; width: 100%;
padding: 0 6px; padding: 0 4px;
}} }}
.match {{ .card {{
flex: 1; flex: 1;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
padding: 2px 6px; padding: 2px 4px;
gap: 3px; gap: 1px;
min-width: 0; min-width: 0;
}} }}
.past {{ opacity: 0.72; }} .past {{ opacity: 0.7; }}
.future {{ opacity: 1; }} .future {{ opacity: 1; }}
.row1 {{ .badge {{
font-size: 11px; width: 22px;
height: 22px;
object-fit: contain;
flex-shrink: 0;
}}
.name {{
font-size: 9px;
font-weight: 600; font-weight: 600;
white-space: nowrap; white-space: nowrap;
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
width: 100%; max-width: 100%;
text-align: center; 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;
}} }}
.home-name {{ color: #cbd5e1; }}
.away-name {{ color: #94a3b8; }}
.score {{ .score {{
font-size: 11px;
font-weight: 800;
color: #4ade80; color: #4ade80;
font-weight: 700; padding: 1px 0;
}} }}
.fixture {{ color: #94a3b8; }} .fixture {{
.divider {{ font-size: 9px;
width: 1px; color: #93c5fd;
background: #1e293b; text-align: center;
margin: 6px 0; line-height: 1.3;
flex-shrink: 0; }}
align-self: stretch; .comp {{
font-size: 8px;
color: #475569;
font-weight: 500;
}} }}
</style> </style>
</head> </head>
<body> <body>
<div class="container"> <div class="container">
{cards} <div class="section">{past_cards}</div>
<div class="divider"></div>
<div class="section">{future_cards}</div>
</div> </div>
</body> </body>
</html>""" </html>"""