Spaces:
Paused
Paused
| from __future__ import annotations | |
| import time | |
| from datetime import datetime, timezone | |
| import requests | |
| SCOREBOARD = "https://site.api.espn.com/apis/site/v2/sports/soccer/fifa.world/scoreboard?dates=2026&limit=200" | |
| SUMMARY = "https://site.api.espn.com/apis/site/v2/sports/soccer/fifa.world/summary?event={}" | |
| MATCH_PAGE = "https://www.espn.com/soccer/match/_/gameId/{}" | |
| _session = requests.Session() | |
| _session.headers["User-Agent"] = "DramaMeter2026/2.0 (+https://github.com/yava-code/DramaMeter-2026)" | |
| _cache: dict[str, tuple[float, dict, str]] = {} | |
| class FeedError(RuntimeError): | |
| pass | |
| def _get(url: str, ttl: int, force: bool = False) -> tuple[dict, str, bool]: | |
| now = time.monotonic() | |
| cached = _cache.get(url) | |
| if cached and not force and now - cached[0] < ttl: | |
| return cached[1], cached[2], False | |
| try: | |
| response = _session.get(url, timeout=12) | |
| response.raise_for_status() | |
| fetched_at = datetime.now(timezone.utc).isoformat(timespec="seconds") | |
| data = response.json() | |
| _cache[url] = (now, data, fetched_at) | |
| return data, fetched_at, False | |
| except (requests.RequestException, ValueError) as exc: | |
| if cached: | |
| return cached[1], cached[2], True | |
| raise FeedError("The live football feed is unavailable. Try refresh in a moment.") from exc | |
| def tournament(force: bool = False) -> tuple[list[dict], dict]: | |
| data, fetched_at, stale = _get(SCOREBOARD, 45, force) | |
| return data.get("events", []), {"fetched_at": fetched_at, "stale": stale} | |
| def summary(event_id: str, force: bool = False) -> tuple[dict, dict]: | |
| data, fetched_at, stale = _get(SUMMARY.format(event_id), 30, force) | |
| return data, {"fetched_at": fetched_at, "stale": stale} | |
| def find_event(events: list[dict], event_id: str) -> dict: | |
| for event in events: | |
| if str(event.get("id")) == str(event_id): | |
| return event | |
| raise FeedError("That fixture is no longer in the tournament feed.") | |
| def fixture_choices(events: list[dict]) -> tuple[list[tuple[str, str]], str | None]: | |
| live = [event for event in events if event["status"]["type"]["state"] == "in"] | |
| upcoming = [event for event in events if event["status"]["type"]["state"] == "pre"] | |
| finished = [event for event in events if event["status"]["type"]["state"] == "post"][-10:] | |
| visible = live + upcoming + list(reversed(finished)) | |
| choices = [(fixture_label(event), str(event["id"])) for event in visible] | |
| latest = finished[-1] if finished else None | |
| recent = latest and ( | |
| datetime.now(timezone.utc) - datetime.fromisoformat(latest["date"].replace("Z", "+00:00")) | |
| ).total_seconds() < 8 * 60 * 60 | |
| preferred = live or ([latest] if recent else []) or upcoming or list(reversed(finished)) | |
| default = str(preferred[0]["id"]) if preferred else None | |
| return choices, default | |
| def fixture_label(event: dict) -> str: | |
| teams = sorted(event["competitions"][0]["competitors"], key=lambda item: item["homeAway"] != "home") | |
| home, away = teams | |
| home_name, away_name = home["team"]["displayName"], away["team"]["displayName"] | |
| state = event["status"]["type"]["state"] | |
| if state == "in": | |
| return f"LIVE {event['status']['displayClock']} · {home_name} {home['score']}–{away['score']} {away_name}" | |
| if state == "post": | |
| return f"FT · {home_name} {home['score']}–{away['score']} {away_name}" | |
| kickoff = datetime.fromisoformat(event["date"].replace("Z", "+00:00")) | |
| return f"{kickoff:%b %d · %H:%M UTC} · {home_name} vs {away_name}" | |