- PHP 83.4%
- Python 14.2%
- Shell 2.4%
| game_canceled | ||
| zabbix | ||
| .gitignore | ||
| add_game.php | ||
| database_migration_plan.md | ||
| deploy.sh | ||
| espn_proxy.php | ||
| game-watcher.service | ||
| game_watcher.py | ||
| get_watchlist.php | ||
| index.php | ||
| README.md | ||
Game Watcher
A self-hosted ESPN game monitoring dashboard. A Python service polls the ESPN public API for live game updates and writes a JSON file that a PHP/JS frontend reads to display scores.
How it works
- Drop a game file into
game_scheduled/— one JSON file per game you want to track. - The watcher service (
game_watcher.py) runs in a loop:- Every 60 seconds it checks all scheduled games against the ESPN API.
- When a game goes live it moves the file to
game_live/and polls it every 10 seconds. - When a game ends it moves the file to
game_completed/. - Completed games older than 3 days are automatically deleted.
- Every tick the watcher writes
games_data.json— a snapshot of all games across all three folders. - The frontend (
index.php) pollsgames_data.jsonevery 10 seconds and renders the scoreboard. - On card click, the frontend fetches richer data on-demand from
espn_proxy.phpand displays it in an overlay.
Directory structure
game_watcher/
├── game_watcher.py # background service
├── index.php # web frontend
├── espn_proxy.php # server-side ESPN API proxy (avoids CORS)
├── add_game.php # API endpoint: add a game to the watch list
├── get_watchlist.php # API endpoint: read a user's watch list
├── games_data.json # generated — do not edit
├── game_scheduled/ # drop new game files here
├── game_live/ # managed by the service
├── game_completed/ # managed by the service
├── game_canceled/ # managed by the service
├── user_data/ # per-user watch list JSON files
├── league_helper/ # cached ESPN API data (generated, 10-min TTL)
├── game_watcher.log # service log
└── zabbix/ # Zabbix monitoring integration
├── game_watcher_stats.sh
├── game_watcher.conf
└── template_game_watcher.yaml
Adding a game
Create a JSON file in game_scheduled/ with the event ID, sport, and league:
{
"event_id": "401696853",
"sport": "baseball",
"league": "mlb"
}
The filename can be anything ending in .json — a descriptive name like mlb_401696853.json works well. The event_id is the number at the end of the ESPN scoreboard URL for the game (e.g. espn.com/mlb/game/_/gameId/401696853).
Favorite teams
Teams listed in FAVORITE_ABBRS in game_watcher.py get a gold left border in the UI. Abbreviations are league-qualified (ABBR:league) to avoid collisions between teams that share an abbreviation across sports.
Current favorites:
| Team | Entry |
|---|---|
| Bay FC (NWSL) | BAY:usa.nwsl |
Game card overlays
Clicking any game card opens a full-screen overlay with richer detail. Overlays are sport-aware:
- Soccer (FIFA World Cup): goal timeline, halftime score, match stats (possession, shots, saves, fouls, cards), group standings with advancement indicators, and a cross-group 3rd-place race table showing which third-place teams are currently on track to advance.
- All other sports: scoreboard, linescore (if available), venue, and broadcast info.
The overlay fetches data on-demand from espn_proxy.php when opened, so live game stats are always current. Group standings for all 12 World Cup groups are cached in league_helper/ for 10 minutes to avoid redundant API calls.
June 28 note (World Cup)
Starting June 28, ESPN's API transitions from group-stage to knockout format. Knockout-stage games (identifiable by having no group_name) skip the group advancement UI automatically.
ESPN proxy
espn_proxy.php is a thin server-side proxy that forwards requests to ESPN's public API endpoints. It exists to avoid CORS issues with direct browser fetches. Supported actions:
| Action | Description |
|---|---|
groups |
Division/conference hierarchy for a league (24h cache) |
teams |
Team list, optionally filtered by group/conference |
schedule |
Team schedule, with scoreboard supplement for tournament leagues |
summary |
Full event summary (box score, stats, standings) for a single game |
group_standings |
All-group standings for a tournament league (10-min cache) |
Running the service
sudo systemctl start game-watcher
sudo systemctl enable game-watcher
sudo systemctl status game-watcher
Logs are written to game_watcher.log and also to stdout (captured by journald).
Zabbix monitoring
The zabbix/ directory contains a Zabbix Agent2 integration that exposes service metrics:
- Game counts (live, scheduled, completed, total)
- ESPN API call and error counters
- Age of
games_data.json(staleness check) - nginx visitor hits to
/game_watcher/in the last hour
Deploy
sudo mkdir -p /etc/zabbix/scripts
sudo cp zabbix/game_watcher_stats.sh /etc/zabbix/scripts/
sudo chmod +x /etc/zabbix/scripts/game_watcher_stats.sh
sudo cp zabbix/game_watcher.conf /etc/zabbix/zabbix_agent2.d/
sudo usermod -aG adm zabbix
sudo systemctl restart zabbix-agent2
Then import zabbix/template_game_watcher.yaml into Zabbix (Configuration → Templates → Import) and link it to the host.
Development
This project is actively developed with assistance from Claude (Anthropic). Current version: Claude Sonnet 4.6.