mirror of
https://github.com/hanzoskill/sports-ticker.git
synced 2026-10-30 21:00:35 +00:00
361 lines
13 KiB
Python
361 lines
13 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
ESPN Sports API - FREE live scores with goal scorers, cards, and more!
|
|
|
|
No API key required. Works for:
|
|
- Football/Soccer (Premier League, Champions League, La Liga, Serie A, etc.)
|
|
- NFL, NBA, MLB, NHL (American sports)
|
|
- And many more...
|
|
|
|
Base URL: https://site.api.espn.com/apis/site/v2/sports/{sport}/{league}
|
|
"""
|
|
|
|
import urllib.request
|
|
import json
|
|
import sys
|
|
from typing import Optional
|
|
from pathlib import Path
|
|
|
|
# ESPN API Base
|
|
BASE_URL = "https://site.api.espn.com/apis/site/v2/sports"
|
|
|
|
# Sport-to-URL mapping
|
|
SPORT_MAPPING = {
|
|
"soccer": "soccer",
|
|
"football": "football", # American football
|
|
"basketball": "basketball",
|
|
"hockey": "hockey",
|
|
"baseball": "baseball",
|
|
"racing": "racing",
|
|
}
|
|
|
|
# League display names by sport
|
|
LEAGUES = {
|
|
"soccer": {
|
|
# European
|
|
"eng.1": "Premier League",
|
|
"eng.2": "Championship",
|
|
"esp.1": "La Liga",
|
|
"ger.1": "Bundesliga",
|
|
"ita.1": "Serie A",
|
|
"fra.1": "Ligue 1",
|
|
"ned.1": "Eredivisie",
|
|
"por.1": "Primeira Liga",
|
|
"aut.1": "Austrian Bundesliga",
|
|
# European competitions
|
|
"uefa.champions": "Champions League",
|
|
"uefa.europa": "Europa League",
|
|
"uefa.europa.conf": "Conference League",
|
|
# Americas
|
|
"usa.1": "MLS",
|
|
"mex.1": "Liga MX",
|
|
"bra.1": "Brasileirão",
|
|
"arg.1": "Argentine Primera",
|
|
# International
|
|
"fifa.world": "World Cup",
|
|
"uefa.euro": "Euros",
|
|
},
|
|
"football": {
|
|
"nfl": "NFL",
|
|
},
|
|
"basketball": {
|
|
"nba": "NBA",
|
|
"wnba": "WNBA",
|
|
"mens-college-basketball": "NCAA Basketball (M)",
|
|
"womens-college-basketball": "NCAA Basketball (W)",
|
|
},
|
|
"hockey": {
|
|
"nhl": "NHL",
|
|
},
|
|
"baseball": {
|
|
"mlb": "MLB",
|
|
},
|
|
"racing": {
|
|
"f1": "Formula 1",
|
|
},
|
|
}
|
|
|
|
# Backward compatibility
|
|
FOOTBALL_LEAGUES = LEAGUES["soccer"]
|
|
|
|
def api_request(endpoint: str) -> dict:
|
|
"""Make request to ESPN API (no auth needed!)."""
|
|
url = f"{BASE_URL}/{endpoint}"
|
|
req = urllib.request.Request(url, headers={"User-Agent": "Mozilla/5.0"})
|
|
return json.loads(urllib.request.urlopen(req, timeout=15).read())
|
|
|
|
def get_scoreboard(sport: str = "soccer", league: str = "eng.1") -> dict:
|
|
"""Get current scoreboard for a league."""
|
|
return api_request(f"{sport}/{league}/scoreboard")
|
|
|
|
def get_match_details(event_id: str, sport: str = "soccer", league: str = "eng.1") -> dict:
|
|
"""Get detailed match info including events."""
|
|
return api_request(f"{sport}/{league}/summary?event={event_id}")
|
|
|
|
def get_all_teams(league: str = "eng.1", sport: str = "soccer") -> list:
|
|
"""Get ALL teams in a league (not just today's matches)."""
|
|
sport_url = SPORT_MAPPING.get(sport, sport)
|
|
data = api_request(f"{sport_url}/{league}/teams")
|
|
teams = data.get("sports", [{}])[0].get("leagues", [{}])[0].get("teams", [])
|
|
return [t.get("team", {}) for t in teams]
|
|
|
|
def search_team(team_name: str, sport: str = "soccer", leagues: list = None) -> list:
|
|
"""Search for a team across leagues using the teams endpoint."""
|
|
if leagues is None:
|
|
# Default leagues based on sport
|
|
if sport == "soccer":
|
|
leagues = ["eng.1", "esp.1", "ger.1", "ita.1", "fra.1", "uefa.champions"]
|
|
elif sport == "football":
|
|
leagues = ["nfl"]
|
|
elif sport == "basketball":
|
|
leagues = ["nba"]
|
|
elif sport == "hockey":
|
|
leagues = ["nhl"]
|
|
elif sport == "baseball":
|
|
leagues = ["mlb"]
|
|
elif sport == "racing":
|
|
leagues = ["f1"]
|
|
else:
|
|
leagues = []
|
|
|
|
team_lower = team_name.lower()
|
|
results = []
|
|
sport_leagues = LEAGUES.get(sport, {})
|
|
|
|
for league in leagues:
|
|
try:
|
|
teams = get_all_teams(league, sport)
|
|
for team in teams:
|
|
if team_lower in team.get("displayName", "").lower() or \
|
|
team_lower in team.get("shortDisplayName", "").lower() or \
|
|
team_lower in team.get("nickname", "").lower():
|
|
results.append({
|
|
"id": team.get("id"),
|
|
"name": team.get("displayName"),
|
|
"short": team.get("shortDisplayName"),
|
|
"sport": sport,
|
|
"league": league,
|
|
"league_name": sport_leagues.get(league, league)
|
|
})
|
|
except Exception:
|
|
continue
|
|
|
|
return results
|
|
|
|
def find_team_match(team_id: str, leagues: list = None, sport: str = "soccer") -> Optional[dict]:
|
|
"""Find a match for a specific team."""
|
|
if leagues is None:
|
|
leagues = ["eng.1", "uefa.champions"]
|
|
|
|
for league in leagues:
|
|
try:
|
|
data = get_scoreboard(sport, league)
|
|
for event in data.get("events", []):
|
|
for comp in event.get("competitions", []):
|
|
for competitor in comp.get("competitors", []):
|
|
if str(competitor.get("team", {}).get("id")) == str(team_id):
|
|
return {
|
|
"event": event,
|
|
"league": league,
|
|
"event_id": event.get("id"),
|
|
"sport": sport
|
|
}
|
|
except Exception:
|
|
continue
|
|
|
|
return None
|
|
|
|
def get_match_events(event_id: str, sport: str = "soccer", league: str = "eng.1") -> list:
|
|
"""Get key events (goals, cards) from a match."""
|
|
details = get_match_details(event_id, sport, league)
|
|
return details.get("keyEvents", [])
|
|
|
|
def format_event(event: dict, sport: str = "soccer") -> str:
|
|
"""Format a match/game event for display."""
|
|
event_type = event.get("type", {}).get("text", "")
|
|
clock = event.get("clock", {}).get("displayValue", "?'")
|
|
team = event.get("team", {}).get("displayName", "")
|
|
|
|
participants = event.get("participants", [])
|
|
player = participants[0].get("athlete", {}).get("displayName", "") if participants else ""
|
|
|
|
# Soccer events
|
|
if sport == "soccer":
|
|
if "Goal" in event_type:
|
|
detail = ""
|
|
if "Own Goal" in event_type:
|
|
detail = " (OG)"
|
|
elif "Penalty" in event_type:
|
|
detail = " (pen)"
|
|
return f"⚽ {clock} {player}{detail} ({team})"
|
|
elif "Yellow" in event_type:
|
|
return f"🟨 {clock} {player} ({team})"
|
|
elif "Red" in event_type:
|
|
return f"🟥 {clock} {player} ({team})"
|
|
elif "Substitution" in event_type:
|
|
return f"🔄 {clock} {player} ({team})"
|
|
|
|
# American Football events
|
|
elif sport == "football":
|
|
if "Touchdown" in event_type:
|
|
return f"🏈 {clock} TOUCHDOWN - {player} ({team})"
|
|
elif "Field Goal" in event_type:
|
|
return f"🎯 {clock} Field Goal - {player} ({team})"
|
|
elif "Interception" in event_type:
|
|
return f"🔒 {clock} INT - {player} ({team})"
|
|
|
|
# Basketball events
|
|
elif sport == "basketball":
|
|
if "Three Point" in event_type or "3PT" in event_type:
|
|
return f"🎯 {clock} 3-pointer - {player} ({team})"
|
|
elif "Dunk" in event_type:
|
|
return f"💪 {clock} DUNK - {player} ({team})"
|
|
|
|
# Hockey events
|
|
elif sport == "hockey":
|
|
if "Goal" in event_type:
|
|
return f"🏒 {clock} GOAL - {player} ({team})"
|
|
elif "Penalty" in event_type:
|
|
return f"⏱️ {clock} Penalty - {player} ({team})"
|
|
|
|
# Baseball events
|
|
elif sport == "baseball":
|
|
if "Home Run" in event_type or "HR" in event_type:
|
|
return f"⚾ {clock} HOME RUN - {player} ({team})"
|
|
|
|
return f"📋 {clock} {event_type}: {player}"
|
|
|
|
def format_match(event: dict, include_events: bool = True, sport: str = "soccer", league: str = "eng.1") -> str:
|
|
"""Format a full match/game summary."""
|
|
lines = []
|
|
|
|
status = event.get("status", {})
|
|
status_desc = status.get("type", {}).get("description", "Unknown")
|
|
clock = status.get("displayClock", "")
|
|
|
|
# Status header (sport-specific terminology)
|
|
if status_desc == "In Progress":
|
|
lines.append(f"🔴 LIVE {clock}")
|
|
elif status_desc == "Halftime":
|
|
if sport in ["football", "basketball"]:
|
|
lines.append("⏸️ HALFTIME")
|
|
else:
|
|
lines.append("⏸️ HALFTIME")
|
|
elif "End of" in status_desc: # Basketball/Hockey periods
|
|
lines.append(f"⏸️ {status_desc}")
|
|
elif status_desc == "Final":
|
|
if sport == "soccer":
|
|
lines.append("🏁 FULL TIME")
|
|
else:
|
|
lines.append("🏁 FINAL")
|
|
else:
|
|
lines.append(f"📅 {status_desc}")
|
|
|
|
# Teams and score
|
|
competitions = event.get("competitions", [{}])[0]
|
|
competitors = competitions.get("competitors", [])
|
|
|
|
home = away = ""
|
|
home_score = away_score = "0"
|
|
|
|
for c in competitors:
|
|
team_name = c.get("team", {}).get("displayName", "?")
|
|
score = c.get("score", "0")
|
|
if c.get("homeAway") == "home":
|
|
home, home_score = team_name, score
|
|
else:
|
|
away, away_score = team_name, score
|
|
|
|
lines.append(f"**{home} {home_score} - {away_score} {away}**")
|
|
|
|
# Events
|
|
if include_events:
|
|
event_id = event.get("id")
|
|
if event_id:
|
|
try:
|
|
events = get_match_events(event_id, sport, league)
|
|
if events:
|
|
lines.append("")
|
|
for e in events[-8:]:
|
|
lines.append(format_event(e, sport))
|
|
except Exception:
|
|
pass
|
|
|
|
return "\n".join(lines)
|
|
|
|
def list_leagues(sport: str = None):
|
|
"""List available leagues by sport."""
|
|
if sport:
|
|
if sport in LEAGUES:
|
|
print(f"Available {sport.title()} Leagues:\n")
|
|
for code, name in sorted(LEAGUES[sport].items(), key=lambda x: x[1]):
|
|
print(f" {code:20} {name}")
|
|
else:
|
|
print(f"Sport '{sport}' not found. Available: {', '.join(LEAGUES.keys())}")
|
|
else:
|
|
print("Available Leagues by Sport:\n")
|
|
for sport_name, leagues in LEAGUES.items():
|
|
print(f"\n{sport_name.upper()}:")
|
|
for code, name in sorted(leagues.items(), key=lambda x: x[1]):
|
|
print(f" {code:20} {name}")
|
|
|
|
if __name__ == "__main__":
|
|
if len(sys.argv) < 2:
|
|
print("ESPN Sports API - Free live data!")
|
|
print("\nUsage:")
|
|
print(" espn.py leagues [sport] - List available leagues")
|
|
print(" espn.py scoreboard [league] [sport] - Get scoreboard (default: eng.1 soccer)")
|
|
print(" espn.py search <team> [sport] - Search for a team (default: soccer)")
|
|
print(" espn.py match <event_id> [league] [sport] - Get match details")
|
|
print("\nSupported sports: soccer, football, basketball, hockey, baseball, racing")
|
|
sys.exit(0)
|
|
|
|
cmd = sys.argv[1]
|
|
|
|
if cmd == "leagues":
|
|
sport = sys.argv[2] if len(sys.argv) > 2 else None
|
|
list_leagues(sport)
|
|
|
|
elif cmd == "scoreboard":
|
|
league = sys.argv[2] if len(sys.argv) > 2 else "eng.1"
|
|
sport = sys.argv[3] if len(sys.argv) > 3 else "soccer"
|
|
data = get_scoreboard(sport, league)
|
|
sport_leagues = LEAGUES.get(sport, {})
|
|
print(f"=== {sport_leagues.get(league, league)} ===\n")
|
|
for event in data.get("events", []):
|
|
print(format_match(event, include_events=False, sport=sport, league=league))
|
|
print()
|
|
|
|
elif cmd == "search":
|
|
if len(sys.argv) < 3:
|
|
print("Usage: espn.py search <team_name> [sport]")
|
|
sys.exit(1)
|
|
# Find where sport might be (last arg if it's a known sport)
|
|
args = sys.argv[2:]
|
|
sport = "soccer"
|
|
if args and args[-1] in SPORT_MAPPING:
|
|
sport = args[-1]
|
|
team = " ".join(args[:-1])
|
|
else:
|
|
team = " ".join(args)
|
|
|
|
results = search_team(team, sport)
|
|
if results:
|
|
print(f"Found teams matching '{team}' in {sport}:\n")
|
|
for r in results:
|
|
print(f" ID: {r['id']:6} | {r['name']:30} | {r['league_name']}")
|
|
else:
|
|
print(f"No teams found matching '{team}' in {sport}")
|
|
|
|
elif cmd == "match":
|
|
if len(sys.argv) < 3:
|
|
print("Usage: espn.py match <event_id> [league] [sport]")
|
|
sys.exit(1)
|
|
event_id = sys.argv[2]
|
|
league = sys.argv[3] if len(sys.argv) > 3 else "eng.1"
|
|
sport = sys.argv[4] if len(sys.argv) > 4 else "soccer"
|
|
details = get_match_details(event_id, sport, league)
|
|
event = details.get("header", {}).get("competitions", [{}])[0]
|
|
# Reconstruct event format
|
|
print(f"Match details for event {event_id}")
|