File size: 3,552 Bytes
9645b79
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
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}"