#!/usr/bin/env python3 """Somewhere, a whistle blows — the world's sporting day via TheSportsDB. OPT-IN (run.py --sports): the free tier allows 30 requests a minute on a shared key and limited leagues, so this worker stays off until invited. It is also polite by design: the day's schedule arrives in a few calls per hour and everything else is computed at home, no live polling — sport.events_today how many fixtures the planet plays today sport.inplay matches being played right now (a stepped curve climbing through the world's evenings) sport.kickoff an impulse the moment any match begins, its magnitude the number kicking off together — three o'clock on an English Saturday rings like a carillon Live scores are the paid tier; the schedule is free, and kickoffs are the musical part anyway. """ import argparse import datetime as dt import json import time import urllib.parse import urllib.request from pythonosc.udp_client import SimpleUDPClient NAME = "world_sport" API = "https://www.thesportsdb.com/api/v1/json/{key}/eventsday.php?d={day}&s={sport}" SPORTS = ["Soccer", "Basketball", "Ice Hockey"] POLL_S = 900.0 # schedule refresh: 3 calls / 15 min — miles under the limit TICK_S = 10.0 # local clock: kickoff detection costs no API calls INPLAY_S = 2 * 3600 # a match "in play" for ~2h from kickoff USER_AGENT = "godstrument/1.0 (+world_sport)" def fetch_day(key, sport, day): url = API.format(key=urllib.parse.quote(key), day=day, sport=urllib.parse.quote(sport)) req = urllib.request.Request(url, headers={"User-Agent": USER_AGENT}) with urllib.request.urlopen(req, timeout=15) as resp: return json.loads(resp.read().decode()).get("events") or [] def kickoff_times(events): """-> sorted UTC epoch seconds; events without a timestamp are skipped.""" out = [] for e in events: ts = e.get("strTimestamp") if not ts: continue try: d = dt.datetime.fromisoformat(ts) if d.tzinfo is None: # thesportsdb timestamps are UTC d = d.replace(tzinfo=dt.timezone.utc) out.append(d.timestamp()) except ValueError: continue return sorted(out) def inplay(times, now): return sum(1 for t in times if t <= now < t + INPLAY_S) def kicked_between(times, t0, t1): return sum(1 for t in times if t0 < t <= t1) def refresh(key): day = dt.datetime.now(dt.timezone.utc).strftime("%Y-%m-%d") total, times = 0, [] for sport in SPORTS: ev = fetch_day(key, sport, day) total += len(ev) times += kickoff_times(ev) time.sleep(2.1) # 30/min shared key: stay courteous return total, sorted(times) def main(): parser = argparse.ArgumentParser(description="TheSportsDB fixtures -> OSC (opt-in)") parser.add_argument("--host", default="127.0.0.1") parser.add_argument("--port", type=int, default=9000) parser.add_argument("--key", default="123", help="thesportsdb API key (free shared key: 123)") parser.add_argument("--once", action="store_true", help="one fetch+emit, then exit (doubles as the self-check)") args = parser.parse_args() client = SimpleUDPClient(args.host, args.port) print(f"[{NAME}] emitting /gs/sport/events_today,/gs/sport/inplay,/gs/sport/kickoff " f"· schedule every {POLL_S:g}s, whistle watched every {TICK_S:g}s") total, times, next_poll, last_tick = 0, [], 0.0, time.time() while True: now = time.time() if now >= next_poll: try: total, times = refresh(args.key) client.send_message("/gs/sport/events_today", float(total)) print(f"[{NAME}] {total} fixtures today, {inplay(times, now)} in play") next_poll = now + POLL_S except Exception as e: # transient network / parse error — keep going print(f"[{NAME}] refresh error: {e}; retrying in {POLL_S / 10:g}s") next_poll = now + POLL_S / 10 k = kicked_between(times, last_tick, now) if k: client.send_message("/gs/sport/kickoff", float(k)) print(f"[{NAME}] the whistle blows — {k} match{'es' if k > 1 else ''} kicking off") client.send_message("/gs/sport/inplay", float(inplay(times, now))) last_tick = now if args.once: assert total >= 0 and all(isinstance(t, float) for t in times), "nonsense schedule" print(f"[{NAME}] once: ok — {total} fixtures, {len(times)} timestamped") return time.sleep(TICK_S) if __name__ == "__main__": try: main() except KeyboardInterrupt: pass