diff --git a/api/main.py b/api/main.py index 15bae7f..74d4885 100644 --- a/api/main.py +++ b/api/main.py @@ -2,10 +2,10 @@ """ 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 + 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 @@ -18,10 +18,15 @@ from fastapi.middleware.cors import CORSMiddleware from fastapi.responses import HTMLResponse 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.add_middleware(CORSMiddleware, allow_origins=["*"], allow_methods=["GET"], allow_headers=["*"]) +app = FastAPI(title="FC Porto API", version="2.0.0") +app.add_middleware( + CORSMiddleware, + allow_origins=["*"], + allow_methods=["GET"], + allow_headers=["*"], +) def load() -> dict: @@ -30,6 +35,34 @@ def load() -> dict: 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()} @@ -43,154 +76,187 @@ def data(): @app.get("/next") def next_match(): d = load() - future = d.get("all_future", []) + _, future = sorted_matches(d) if not future: - return {"opponent": "No upcoming matches", "date": "—", "competition": "—", "channel": "—"} + return {"opponent": "No upcoming matches", "date": "—", "competition": "—", "venue": "—"} m = future[0] return { - "opponent": m["opponent"], - "date": f"{m['date']} {m['time']}", + "opponent": m["opponent"], + "date": f"{m['date']} {m['time']}", "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) 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: d = load() - matches = d.get("display", []) + past, future = sorted_matches(d) except HTTPException: - matches = [] + past, future = [], [] - # 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", + # 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)) - # 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] + all_cards = last3 + next2 - 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", "") + PORTO_LOGO = "https://a.espncdn.com/i/teamlogos/soccer/500/437.png" - abbr = ABBR.get(comp, comp[:2].upper() if comp != "—" else "—") + 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", "") - # Row 1 — "Porto vs Benfica (LP)" - row1 = f"{home} vs {away} ({abbr})" + # 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) - # Row 2 — score or date · channel if is_past and score: - row2 = f'{score}' - card_class = "match past" + middle = f'
{score}
' + cls = "card 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'{short_date}{time_part}{ch_part}' - card_class = "match future" - - divider = '
' if idx == 1 else "" + date_s = "/".join(date.split("/")[:2]) if "/" in date else date + time_s = f"
{time_}" if time_ and time_ != "TBD" else "" + middle = f'
{date_s}{time_s}
' + cls = "card future" return f""" - {divider} -
-
{row1}
-
{row2}
+
+ {home} +
{home_s}
+ {middle} + {away} +
{away_s}
+
{abbr}
""" - 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"""
- {cards} +
{past_cards}
+
+
{future_cards}
""" \ No newline at end of file