mirror of
https://github.com/agessaman/meshcore-bot.git
synced 2026-08-28 05:14:12 +00:00
feat: Introduce ESPN and TheSportsDB clients and enhance command code quality with type hints and docstrings.
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
API Clients for MeshCore Bot
|
||||
Encapsulates external API interactions
|
||||
"""
|
||||
@@ -0,0 +1,320 @@
|
||||
import aiohttp
|
||||
import logging
|
||||
import asyncio
|
||||
from datetime import datetime, timezone
|
||||
from typing import List, Dict, Optional
|
||||
from .sports_mappings import is_womens_league, get_team_abbreviation, format_clean_date, format_clean_date_time
|
||||
|
||||
class ESPNClient:
|
||||
"""Client for ESPN API using aiohttp for asynchronous requests"""
|
||||
|
||||
BASE_URL = "http://site.api.espn.com/apis/site/v2/sports"
|
||||
|
||||
def __init__(self, logger=None, timeout: int = 10, session: Optional[aiohttp.ClientSession] = None):
|
||||
self.logger = logger or logging.getLogger(__name__)
|
||||
self.timeout = aiohttp.ClientTimeout(total=timeout)
|
||||
self.session = session
|
||||
|
||||
async def _get_session(self) -> aiohttp.ClientSession:
|
||||
"""Get or create an aiohttp session"""
|
||||
if self.session is None or self.session.closed:
|
||||
self.session = aiohttp.ClientSession(timeout=self.timeout)
|
||||
return self.session
|
||||
|
||||
async def fetch_scoreboard(self, sport: str, league: str) -> List[Dict]:
|
||||
"""Fetch and parse scoreboard data for a league"""
|
||||
url = f"{self.BASE_URL}/{sport}/{league}/scoreboard"
|
||||
try:
|
||||
session = await self._get_session()
|
||||
async with session.get(url) as response:
|
||||
response.raise_for_status()
|
||||
data = await response.json()
|
||||
events = data.get('events', [])
|
||||
|
||||
parsed_events = []
|
||||
for event in events:
|
||||
parsed = self.parse_league_game_event(event, sport, league)
|
||||
if parsed:
|
||||
parsed_events.append(parsed)
|
||||
return parsed_events
|
||||
except Exception as e:
|
||||
self.logger.error(f"ESPN fetch_scoreboard error for {sport}/{league}: {e}")
|
||||
return []
|
||||
|
||||
async def fetch_team_schedule(self, sport: str, league: str, team_id: str) -> List[Dict]:
|
||||
"""Fetch and parse schedule data for a team
|
||||
|
||||
For soccer teams, if the team schedule has no upcoming games, we fall back
|
||||
to searching the league scoreboard for games involving this team.
|
||||
"""
|
||||
url = f"{self.BASE_URL}/{sport}/{league}/teams/{team_id}/schedule"
|
||||
try:
|
||||
session = await self._get_session()
|
||||
async with session.get(url) as response:
|
||||
response.raise_for_status()
|
||||
data = await response.json()
|
||||
events = data.get('events', [])
|
||||
|
||||
parsed_events = []
|
||||
for event in events:
|
||||
parsed = self.parse_game_event_with_timestamp(event, team_id, sport, league)
|
||||
if parsed:
|
||||
parsed_events.append(parsed)
|
||||
|
||||
# For soccer, if no upcoming games found, check league scoreboard
|
||||
if sport == 'soccer':
|
||||
from datetime import datetime, timezone
|
||||
now = datetime.now(timezone.utc).timestamp()
|
||||
has_upcoming = any(
|
||||
g.get('event_timestamp', 0) > now for g in parsed_events
|
||||
)
|
||||
|
||||
if not has_upcoming:
|
||||
# Fall back to league scoreboard to find this team's games
|
||||
scoreboard_games = await self._find_team_in_scoreboard(sport, league, team_id)
|
||||
if scoreboard_games:
|
||||
parsed_events.extend(scoreboard_games)
|
||||
|
||||
return parsed_events
|
||||
except Exception as e:
|
||||
self.logger.error(f"ESPN fetch_team_schedule error for {team_id}: {e}")
|
||||
return []
|
||||
|
||||
async def _find_team_in_scoreboard(self, sport: str, league: str, team_id: str) -> List[Dict]:
|
||||
"""Find games for a specific team in the league scoreboard"""
|
||||
url = f"{self.BASE_URL}/{sport}/{league}/scoreboard"
|
||||
try:
|
||||
session = await self._get_session()
|
||||
async with session.get(url) as response:
|
||||
response.raise_for_status()
|
||||
data = await response.json()
|
||||
events = data.get('events', [])
|
||||
|
||||
team_games = []
|
||||
for event in events:
|
||||
# Check if this team is in this event
|
||||
competitions = event.get('competitions', [])
|
||||
if not competitions:
|
||||
continue
|
||||
|
||||
competition = competitions[0]
|
||||
competitors = competition.get('competitors', [])
|
||||
|
||||
# Check if our team is in this game
|
||||
team_in_game = False
|
||||
for competitor in competitors:
|
||||
if str(competitor.get('team', {}).get('id', '')) == str(team_id):
|
||||
team_in_game = True
|
||||
break
|
||||
|
||||
if team_in_game:
|
||||
parsed = self.parse_game_event_with_timestamp(event, team_id, sport, league)
|
||||
if parsed:
|
||||
team_games.append(parsed)
|
||||
|
||||
return team_games
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error finding team in scoreboard: {e}")
|
||||
return []
|
||||
|
||||
async def fetch_live_event_data(self, event_id: str, sport: str, league: str) -> Optional[Dict]:
|
||||
"""Fetch live event data from the scoreboard endpoint for real-time scores
|
||||
|
||||
The scoreboard endpoint provides more up-to-date scores for live games than the schedule endpoint.
|
||||
We fetch the scoreboard and find the matching event by ID.
|
||||
"""
|
||||
url = f"{self.BASE_URL}/{sport}/{league}/scoreboard"
|
||||
try:
|
||||
session = await self._get_session()
|
||||
async with session.get(url) as response:
|
||||
response.raise_for_status()
|
||||
data = await response.json()
|
||||
|
||||
# Find the event with matching ID in the scoreboard
|
||||
# Convert event_id to string for comparison (API may return IDs as strings or ints)
|
||||
event_id_str = str(event_id)
|
||||
events = data.get('events', [])
|
||||
for event in events:
|
||||
event_id_from_api = str(event.get('id', ''))
|
||||
if event_id_from_api == event_id_str:
|
||||
return event
|
||||
|
||||
# If not found in scoreboard, return None (event might not be live anymore)
|
||||
return None
|
||||
except Exception as e:
|
||||
self.logger.error(f"ESPN fetch_live_event_data error for {event_id}: {e}")
|
||||
return None
|
||||
|
||||
def extract_score(self, competitor: Dict) -> str:
|
||||
"""Extract score value from competitor data"""
|
||||
score = competitor.get('score', '0')
|
||||
if isinstance(score, dict):
|
||||
if 'displayValue' in score:
|
||||
return str(score['displayValue'])
|
||||
elif 'value' in score:
|
||||
value = score['value']
|
||||
if isinstance(value, float) and value.is_integer():
|
||||
return str(int(value))
|
||||
return str(value)
|
||||
return '0'
|
||||
if isinstance(score, str):
|
||||
return score
|
||||
if isinstance(score, (int, float)):
|
||||
if isinstance(score, float) and score.is_integer():
|
||||
return str(int(score))
|
||||
return str(score)
|
||||
return '0'
|
||||
|
||||
def extract_shootout_score(self, competitor: Dict) -> Optional[int]:
|
||||
"""Extract penalty shootout score from competitor data"""
|
||||
score = competitor.get('score', {})
|
||||
if isinstance(score, dict) and 'shootoutScore' in score:
|
||||
shootout = score['shootoutScore']
|
||||
if isinstance(shootout, (int, float)):
|
||||
return int(shootout)
|
||||
return None
|
||||
|
||||
def parse_game_event_with_timestamp(self, event: Dict, team_id: str, sport: str, league: str) -> Optional[Dict]:
|
||||
"""Parse a game event and return structured data with timestamp for sorting"""
|
||||
try:
|
||||
competitions = event.get('competitions', [])
|
||||
if not competitions:
|
||||
return None
|
||||
|
||||
competition = competitions[0]
|
||||
competitors = competition.get('competitors', [])
|
||||
|
||||
if len(competitors) != 2:
|
||||
return None
|
||||
|
||||
# Extract team info
|
||||
team1 = competitors[0]
|
||||
team2 = competitors[1]
|
||||
|
||||
# Determine home/away
|
||||
home_team = team1 if team1.get('homeAway') == 'home' else team2
|
||||
away_team = team2 if team1.get('homeAway') == 'home' else team1
|
||||
|
||||
home_id = home_team.get('team', {}).get('id', '')
|
||||
away_id = away_team.get('team', {}).get('id', '')
|
||||
home_abbr = home_team.get('team', {}).get('abbreviation', 'UNK')
|
||||
away_abbr = away_team.get('team', {}).get('abbreviation', 'UNK')
|
||||
|
||||
home_name = get_team_abbreviation(home_id, home_abbr, sport, league)
|
||||
away_name = get_team_abbreviation(away_id, away_abbr, sport, league)
|
||||
|
||||
home_score = self.extract_score(home_team)
|
||||
away_score = self.extract_score(away_team)
|
||||
|
||||
# Get game status
|
||||
status_obj = competition.get('status', event.get('status', {}))
|
||||
status_type = status_obj.get('type', {})
|
||||
status_name = status_type.get('name', 'UNKNOWN')
|
||||
|
||||
# Get timestamp for sorting
|
||||
date_str = event.get('date', '')
|
||||
timestamp = 0
|
||||
event_timestamp = None
|
||||
if date_str:
|
||||
try:
|
||||
dt = datetime.fromisoformat(date_str.replace('Z', '+00:00'))
|
||||
if dt.tzinfo is None:
|
||||
dt = dt.replace(tzinfo=timezone.utc)
|
||||
event_timestamp = dt.timestamp()
|
||||
timestamp = event_timestamp
|
||||
except:
|
||||
pass
|
||||
|
||||
# Format based on game status
|
||||
formatted = ""
|
||||
if status_name in ['STATUS_IN_PROGRESS', 'STATUS_FIRST_HALF', 'STATUS_SECOND_HALF', 'STATUS_END_PERIOD']:
|
||||
# Game is live
|
||||
clock = status_obj.get('displayClock', '')
|
||||
period = status_obj.get('period', 0)
|
||||
is_end_period = (status_name == 'STATUS_END_PERIOD')
|
||||
|
||||
if sport == 'soccer':
|
||||
# Soccer: @Home Score-Score Away (Clock)
|
||||
period_str = clock if (clock and clock != '0:00' and clock != "0'") else f"{period}H"
|
||||
formatted = f"@{home_name} {home_score}-{away_score} {away_name} ({period_str})"
|
||||
elif sport == 'baseball':
|
||||
short_detail = status_type.get('shortDetail', '')
|
||||
period_str = short_detail if ('Top' in short_detail or 'Bottom' in short_detail) else f"{period}I"
|
||||
if is_end_period: period_str = f"End {period_str}"
|
||||
formatted = f"{away_name} {away_score}-{home_score} @{home_name} ({period_str})"
|
||||
elif sport == 'football':
|
||||
period_str = f"Q{period}"
|
||||
if is_end_period: period_str = f"End {period_str}"
|
||||
formatted = f"{away_name} {away_score}-{home_score} @{home_name} ({clock} {period_str})"
|
||||
else:
|
||||
period_str = f"P{period}"
|
||||
if is_end_period: period_str = f"End {period_str}"
|
||||
formatted = f"{away_name} {away_score}-{home_score} @{home_name} ({clock} {period_str})"
|
||||
|
||||
timestamp = -1 # Live games first
|
||||
|
||||
elif status_name == 'STATUS_SCHEDULED':
|
||||
# Scheduled
|
||||
if event_timestamp:
|
||||
dt = datetime.fromtimestamp(event_timestamp, tz=timezone.utc).astimezone()
|
||||
time_str = format_clean_date_time(dt)
|
||||
if sport == 'soccer':
|
||||
formatted = f"@{home_name} vs. {away_name} ({time_str})"
|
||||
else:
|
||||
formatted = f"{away_abbr} @ {home_abbr} ({time_str})"
|
||||
else:
|
||||
formatted = f"{away_abbr} @ {home_abbr} (TBD)" if sport != 'soccer' else f"@{home_name} vs. {away_name} (TBD)"
|
||||
timestamp = 9999999999
|
||||
|
||||
elif status_name == 'STATUS_HALFTIME':
|
||||
if sport == 'soccer':
|
||||
formatted = f"@{home_name} {home_score}-{away_score} {away_name} (HT)"
|
||||
else:
|
||||
formatted = f"{away_abbr} {away_score}-{home_score} @{home_abbr} (HT)"
|
||||
timestamp = -2
|
||||
|
||||
elif status_name in ['STATUS_FINAL', 'STATUS_FULL_TIME', 'STATUS_FINAL_PEN', 'STATUS_POSTPONED']:
|
||||
date_suffix = ""
|
||||
if event_timestamp:
|
||||
dt = datetime.fromtimestamp(event_timestamp, tz=timezone.utc).astimezone()
|
||||
if dt.date() != datetime.now().date():
|
||||
date_suffix = f", {format_clean_date(dt)}"
|
||||
|
||||
if status_name == 'STATUS_FINAL_PEN':
|
||||
home_shootout = self.extract_shootout_score(home_team)
|
||||
away_shootout = self.extract_shootout_score(away_team)
|
||||
pen_str = f"FT-PEN {home_shootout}-{away_shootout}" if home_shootout is not None else "FT-PEN"
|
||||
formatted = f"@{home_name} {home_score}-{away_score} {away_name} ({pen_str}{date_suffix})"
|
||||
elif status_name == 'STATUS_FULL_TIME':
|
||||
formatted = f"@{home_name} {home_score}-{away_score} {away_name} (FT{date_suffix})"
|
||||
elif status_name == 'STATUS_POSTPONED':
|
||||
formatted = f"{away_abbr} @ {home_abbr} (Postponed{date_suffix})"
|
||||
else:
|
||||
formatted = f"{away_abbr} {away_score}-{home_score} @{home_abbr} (F{date_suffix})"
|
||||
|
||||
timestamp = 9999999998
|
||||
else:
|
||||
prefix = "@" if sport == 'soccer' else ""
|
||||
suffix = " vs. " if sport == 'soccer' else " @ "
|
||||
if sport == 'soccer':
|
||||
formatted = f"@{home_name} {home_score}-{away_score} {away_name} ({status_name})"
|
||||
else:
|
||||
formatted = f"{away_name} {away_score}-{home_score} @{home_name} ({status_name})"
|
||||
timestamp = 9999999997
|
||||
|
||||
return {
|
||||
'id': event.get('id'),
|
||||
'timestamp': timestamp,
|
||||
'event_timestamp': event_timestamp,
|
||||
'formatted': formatted,
|
||||
'sport': sport,
|
||||
'league': league,
|
||||
'status': status_name
|
||||
}
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error parsing ESPN event {event.get('id')}: {e}")
|
||||
return None
|
||||
|
||||
def parse_league_game_event(self, event: Dict, sport: str, league: str) -> Optional[Dict]:
|
||||
"""Parse a league game event (scoreboard)"""
|
||||
return self.parse_game_event_with_timestamp(event, "", sport, league)
|
||||
@@ -0,0 +1,802 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Sports Team and League Mappings
|
||||
Contains team IDs and custom abbreviations for various sports APIs
|
||||
"""
|
||||
|
||||
# Sport emojis for easy identification
|
||||
SPORT_EMOJIS = {
|
||||
'football': '🏈',
|
||||
'baseball': '⚾',
|
||||
'basketball': '🏀',
|
||||
'hockey': '🏒',
|
||||
'soccer': '⚽'
|
||||
}
|
||||
|
||||
# Custom team abbreviations to distinguish between leagues
|
||||
# Only use -W suffixes for women's leagues
|
||||
WOMENS_TEAM_ABBREVIATIONS = {
|
||||
# NWSL teams - use custom abbreviations to distinguish from MLS
|
||||
'21422': 'LA-W', # Angel City FC (Women's)
|
||||
'22187': 'BAY-W', # Bay FC (Women's)
|
||||
'15360': 'CHI-W', # Chicago Stars FC (Women's)
|
||||
'15364': 'GFC-W', # Gotham FC (Women's)
|
||||
'17346': 'HOU-W', # Houston Dash (Women's)
|
||||
'20907': 'KC-W', # Kansas City Current (Women's)
|
||||
'15366': 'NC-W', # North Carolina Courage (Women's)
|
||||
'18206': 'ORL-W', # Orlando Pride (Women's)
|
||||
'15362': 'POR-W', # Portland Thorns FC (Women's)
|
||||
'20905': 'LOU-W', # Racing Louisville FC (Women's)
|
||||
'21423': 'SD-W', # San Diego Wave FC (Women's)
|
||||
'15363': 'SEA-W', # Seattle Reign FC (Women's)
|
||||
'19141': 'UTA-W', # Utah Royals (Women's)
|
||||
'15365': 'WAS-W', # Washington Spirit (Women's)
|
||||
# WNBA teams - use custom abbreviations to distinguish from NBA
|
||||
'14': 'SEA-W', # Seattle Storm (Women's)
|
||||
'9': 'NY-W', # New York Liberty (Women's)
|
||||
'6': 'LA-W', # Los Angeles Sparks (Women's)
|
||||
'19': 'CHI-W', # Chicago Sky (Women's)
|
||||
'20': 'ATL-W', # Atlanta Dream (Women's)
|
||||
'18': 'CON-W', # Connecticut Sun (Women's)
|
||||
'3': 'DAL-W', # Dallas Wings (Women's)
|
||||
'129689': 'GS-W', # Golden State Valkyries (Women's)
|
||||
'5': 'IND-W', # Indiana Fever (Women's)
|
||||
'17': 'LV-W', # Las Vegas Aces (Women's)
|
||||
'8': 'MIN-W', # Minnesota Lynx (Women's)
|
||||
'11': 'PHX-W', # Phoenix Mercury (Women's)
|
||||
'16': 'WSH-W', # Washington Mystics (Women's)
|
||||
}
|
||||
|
||||
# Team mappings for common searches
|
||||
TEAM_MAPPINGS = {
|
||||
# NFL Teams
|
||||
'seahawks': {'sport': 'football', 'league': 'nfl', 'team_id': '26'},
|
||||
'hawks': {'sport': 'football', 'league': 'nfl', 'team_id': '26'},
|
||||
'49ers': {'sport': 'football', 'league': 'nfl', 'team_id': '25'},
|
||||
'niners': {'sport': 'football', 'league': 'nfl', 'team_id': '25'},
|
||||
'sf': {'sport': 'football', 'league': 'nfl', 'team_id': '25'},
|
||||
'bears': {'sport': 'football', 'league': 'nfl', 'team_id': '3'},
|
||||
'chicago': {'sport': 'football', 'league': 'nfl', 'team_id': '3'},
|
||||
'chi': {'sport': 'football', 'league': 'nfl', 'team_id': '3'},
|
||||
'bengals': {'sport': 'football', 'league': 'nfl', 'team_id': '4'},
|
||||
'cincinnati': {'sport': 'football', 'league': 'nfl', 'team_id': '4'},
|
||||
'cin': {'sport': 'football', 'league': 'nfl', 'team_id': '4'},
|
||||
'bills': {'sport': 'football', 'league': 'nfl', 'team_id': '2'},
|
||||
'buffalo': {'sport': 'football', 'league': 'nfl', 'team_id': '2'},
|
||||
'buf': {'sport': 'football', 'league': 'nfl', 'team_id': '2'},
|
||||
'broncos': {'sport': 'football', 'league': 'nfl', 'team_id': '7'},
|
||||
'denver': {'sport': 'football', 'league': 'nfl', 'team_id': '7'},
|
||||
'den': {'sport': 'football', 'league': 'nfl', 'team_id': '7'},
|
||||
'browns': {'sport': 'football', 'league': 'nfl', 'team_id': '5'},
|
||||
'cleveland': {'sport': 'football', 'league': 'nfl', 'team_id': '5'},
|
||||
'cle': {'sport': 'football', 'league': 'nfl', 'team_id': '5'},
|
||||
'buccaneers': {'sport': 'football', 'league': 'nfl', 'team_id': '27'},
|
||||
'bucs': {'sport': 'football', 'league': 'nfl', 'team_id': '27'},
|
||||
'tampa bay': {'sport': 'football', 'league': 'nfl', 'team_id': '27'},
|
||||
'tb': {'sport': 'football', 'league': 'nfl', 'team_id': '27'},
|
||||
'arizona cardinals': {'sport': 'football', 'league': 'nfl', 'team_id': '22'},
|
||||
'az cardinals': {'sport': 'football', 'league': 'nfl', 'team_id': '22'},
|
||||
'chargers': {'sport': 'football', 'league': 'nfl', 'team_id': '24'},
|
||||
'lac': {'sport': 'football', 'league': 'nfl', 'team_id': '24'},
|
||||
'la chargers': {'sport': 'football', 'league': 'nfl', 'team_id': '24'},
|
||||
'los angeles chargers': {'sport': 'football', 'league': 'nfl', 'team_id': '24'},
|
||||
'chiefs': {'sport': 'football', 'league': 'nfl', 'team_id': '12'},
|
||||
'kansas city': {'sport': 'football', 'league': 'nfl', 'team_id': '12'},
|
||||
'kc': {'sport': 'football', 'league': 'nfl', 'team_id': '12'},
|
||||
'colts': {'sport': 'football', 'league': 'nfl', 'team_id': '11'},
|
||||
'indianapolis': {'sport': 'football', 'league': 'nfl', 'team_id': '11'},
|
||||
'ind': {'sport': 'football', 'league': 'nfl', 'team_id': '11'},
|
||||
'commanders': {'sport': 'football', 'league': 'nfl', 'team_id': '28'},
|
||||
'washington': {'sport': 'football', 'league': 'nfl', 'team_id': '28'},
|
||||
'wsh': {'sport': 'football', 'league': 'nfl', 'team_id': '28'},
|
||||
'cowboys': {'sport': 'football', 'league': 'nfl', 'team_id': '6'},
|
||||
'dallas': {'sport': 'football', 'league': 'nfl', 'team_id': '6'},
|
||||
'dal': {'sport': 'football', 'league': 'nfl', 'team_id': '6'},
|
||||
'dolphins': {'sport': 'football', 'league': 'nfl', 'team_id': '15'},
|
||||
'miami': {'sport': 'football', 'league': 'nfl', 'team_id': '15'},
|
||||
'mia': {'sport': 'football', 'league': 'nfl', 'team_id': '15'},
|
||||
'eagles': {'sport': 'football', 'league': 'nfl', 'team_id': '21'},
|
||||
'philadelphia': {'sport': 'football', 'league': 'nfl', 'team_id': '21'},
|
||||
'phi': {'sport': 'football', 'league': 'nfl', 'team_id': '21'},
|
||||
'falcons': {'sport': 'football', 'league': 'nfl', 'team_id': '1'},
|
||||
'atlanta': {'sport': 'football', 'league': 'nfl', 'team_id': '1'},
|
||||
'atl': {'sport': 'football', 'league': 'nfl', 'team_id': '1'},
|
||||
'giants': {'sport': 'football', 'league': 'nfl', 'team_id': '19'},
|
||||
'nyg': {'sport': 'football', 'league': 'nfl', 'team_id': '19'},
|
||||
'jaguars': {'sport': 'football', 'league': 'nfl', 'team_id': '30'},
|
||||
'jax': {'sport': 'football', 'league': 'nfl', 'team_id': '30'},
|
||||
'jacksonville': {'sport': 'football', 'league': 'nfl', 'team_id': '30'},
|
||||
'jets': {'sport': 'football', 'league': 'nfl', 'team_id': '20'},
|
||||
'nyj': {'sport': 'football', 'league': 'nfl', 'team_id': '20'},
|
||||
'lions': {'sport': 'football', 'league': 'nfl', 'team_id': '8'},
|
||||
'detroit': {'sport': 'football', 'league': 'nfl', 'team_id': '8'},
|
||||
'det': {'sport': 'football', 'league': 'nfl', 'team_id': '8'},
|
||||
'packers': {'sport': 'football', 'league': 'nfl', 'team_id': '9'},
|
||||
'green bay': {'sport': 'football', 'league': 'nfl', 'team_id': '9'},
|
||||
'gb': {'sport': 'football', 'league': 'nfl', 'team_id': '9'},
|
||||
'carolina panthers': {'sport': 'football', 'league': 'nfl', 'team_id': '29'},
|
||||
'patriots': {'sport': 'football', 'league': 'nfl', 'team_id': '17'},
|
||||
'new england': {'sport': 'football', 'league': 'nfl', 'team_id': '17'},
|
||||
'ne': {'sport': 'football', 'league': 'nfl', 'team_id': '17'},
|
||||
'raiders': {'sport': 'football', 'league': 'nfl', 'team_id': '13'},
|
||||
'las vegas': {'sport': 'football', 'league': 'nfl', 'team_id': '13'},
|
||||
'lv': {'sport': 'football', 'league': 'nfl', 'team_id': '13'},
|
||||
'rams': {'sport': 'football', 'league': 'nfl', 'team_id': '14'},
|
||||
'lar': {'sport': 'football', 'league': 'nfl', 'team_id': '14'},
|
||||
'la rams': {'sport': 'football', 'league': 'nfl', 'team_id': '14'},
|
||||
'los angeles rams': {'sport': 'football', 'league': 'nfl', 'team_id': '14'},
|
||||
'ravens': {'sport': 'football', 'league': 'nfl', 'team_id': '33'},
|
||||
'baltimore': {'sport': 'football', 'league': 'nfl', 'team_id': '33'},
|
||||
'bal': {'sport': 'football', 'league': 'nfl', 'team_id': '33'},
|
||||
'saints': {'sport': 'football', 'league': 'nfl', 'team_id': '18'},
|
||||
'new orleans': {'sport': 'football', 'league': 'nfl', 'team_id': '18'},
|
||||
'no': {'sport': 'football', 'league': 'nfl', 'team_id': '18'},
|
||||
'steelers': {'sport': 'football', 'league': 'nfl', 'team_id': '23'},
|
||||
'pittsburgh': {'sport': 'football', 'league': 'nfl', 'team_id': '23'},
|
||||
'pit': {'sport': 'football', 'league': 'nfl', 'team_id': '23'},
|
||||
'texans': {'sport': 'football', 'league': 'nfl', 'team_id': '34'},
|
||||
'houston': {'sport': 'football', 'league': 'nfl', 'team_id': '34'},
|
||||
'hou': {'sport': 'football', 'league': 'nfl', 'team_id': '34'},
|
||||
'titans': {'sport': 'football', 'league': 'nfl', 'team_id': '10'},
|
||||
'tennessee': {'sport': 'football', 'league': 'nfl', 'team_id': '10'},
|
||||
'ten': {'sport': 'football', 'league': 'nfl', 'team_id': '10'},
|
||||
'vikings': {'sport': 'football', 'league': 'nfl', 'team_id': '16'},
|
||||
'minnesota': {'sport': 'football', 'league': 'nfl', 'team_id': '16'},
|
||||
'min': {'sport': 'football', 'league': 'nfl', 'team_id': '16'},
|
||||
|
||||
# CFL Teams (Canadian Football League)
|
||||
'bc lions': {'sport': 'football', 'league': 'cfl', 'team_id': '79'},
|
||||
'bcl': {'sport': 'football', 'league': 'cfl', 'team_id': '79'},
|
||||
'calgary stampeders': {'sport': 'football', 'league': 'cfl', 'team_id': '80'},
|
||||
'stampeders': {'sport': 'football', 'league': 'cfl', 'team_id': '80'},
|
||||
'csp': {'sport': 'football', 'league': 'cfl', 'team_id': '80'},
|
||||
'edmonton elks': {'sport': 'football', 'league': 'cfl', 'team_id': '81'},
|
||||
'elks': {'sport': 'football', 'league': 'cfl', 'team_id': '81'},
|
||||
'ees': {'sport': 'football', 'league': 'cfl', 'team_id': '81'},
|
||||
'hamilton tiger-cats': {'sport': 'football', 'league': 'cfl', 'team_id': '82'},
|
||||
'tiger-cats': {'sport': 'football', 'league': 'cfl', 'team_id': '82'},
|
||||
'tigercats': {'sport': 'football', 'league': 'cfl', 'team_id': '82'},
|
||||
'htc': {'sport': 'football', 'league': 'cfl', 'team_id': '82'},
|
||||
'montreal alouettes': {'sport': 'football', 'league': 'cfl', 'team_id': '83'},
|
||||
'alouettes': {'sport': 'football', 'league': 'cfl', 'team_id': '83'},
|
||||
'mta': {'sport': 'football', 'league': 'cfl', 'team_id': '83'},
|
||||
'ottawa redblacks': {'sport': 'football', 'league': 'cfl', 'team_id': '87'},
|
||||
'redblacks': {'sport': 'football', 'league': 'cfl', 'team_id': '87'},
|
||||
'red blacks': {'sport': 'football', 'league': 'cfl', 'team_id': '87'},
|
||||
'orb': {'sport': 'football', 'league': 'cfl', 'team_id': '87'},
|
||||
'saskatchewan roughriders': {'sport': 'football', 'league': 'cfl', 'team_id': '84'},
|
||||
'roughriders': {'sport': 'football', 'league': 'cfl', 'team_id': '84'},
|
||||
'riders': {'sport': 'football', 'league': 'cfl', 'team_id': '84'},
|
||||
'srr': {'sport': 'football', 'league': 'cfl', 'team_id': '84'},
|
||||
'toronto argonauts': {'sport': 'football', 'league': 'cfl', 'team_id': '85'},
|
||||
'argonauts': {'sport': 'football', 'league': 'cfl', 'team_id': '85'},
|
||||
'argos': {'sport': 'football', 'league': 'cfl', 'team_id': '85'},
|
||||
'tat': {'sport': 'football', 'league': 'cfl', 'team_id': '85'},
|
||||
'winnipeg blue bombers': {'sport': 'football', 'league': 'cfl', 'team_id': '86'},
|
||||
'blue bombers': {'sport': 'football', 'league': 'cfl', 'team_id': '86'},
|
||||
'bombers': {'sport': 'football', 'league': 'cfl', 'team_id': '86'},
|
||||
'wbb': {'sport': 'football', 'league': 'cfl', 'team_id': '86'},
|
||||
|
||||
# MLB Teams
|
||||
'mariners': {'sport': 'baseball', 'league': 'mlb', 'team_id': '12'},
|
||||
'seattle': {'sport': 'baseball', 'league': 'mlb', 'team_id': '12'},
|
||||
'sea': {'sport': 'baseball', 'league': 'mlb', 'team_id': '12'},
|
||||
'angels': {'sport': 'baseball', 'league': 'mlb', 'team_id': '3'},
|
||||
'laa': {'sport': 'baseball', 'league': 'mlb', 'team_id': '3'},
|
||||
'astros': {'sport': 'baseball', 'league': 'mlb', 'team_id': '18'},
|
||||
'houston': {'sport': 'baseball', 'league': 'mlb', 'team_id': '18'},
|
||||
'hou': {'sport': 'baseball', 'league': 'mlb', 'team_id': '18'},
|
||||
'athletics': {'sport': 'baseball', 'league': 'mlb', 'team_id': '11'},
|
||||
'a\'s': {'sport': 'baseball', 'league': 'mlb', 'team_id': '11'},
|
||||
'oakland': {'sport': 'baseball', 'league': 'mlb', 'team_id': '11'},
|
||||
'oak': {'sport': 'baseball', 'league': 'mlb', 'team_id': '11'},
|
||||
'blue jays': {'sport': 'baseball', 'league': 'mlb', 'team_id': '14'},
|
||||
'toronto': {'sport': 'baseball', 'league': 'mlb', 'team_id': '14'},
|
||||
'tor': {'sport': 'baseball', 'league': 'mlb', 'team_id': '14'},
|
||||
'braves': {'sport': 'baseball', 'league': 'mlb', 'team_id': '15'},
|
||||
'atlanta': {'sport': 'baseball', 'league': 'mlb', 'team_id': '15'},
|
||||
'atl': {'sport': 'baseball', 'league': 'mlb', 'team_id': '15'},
|
||||
'brewers': {'sport': 'baseball', 'league': 'mlb', 'team_id': '8'},
|
||||
'milwaukee': {'sport': 'baseball', 'league': 'mlb', 'team_id': '8'},
|
||||
'mil': {'sport': 'baseball', 'league': 'mlb', 'team_id': '8'},
|
||||
'cardinals': {'sport': 'baseball', 'league': 'mlb', 'team_id': '24'},
|
||||
'st louis': {'sport': 'baseball', 'league': 'mlb', 'team_id': '24'},
|
||||
'stl': {'sport': 'baseball', 'league': 'mlb', 'team_id': '24'},
|
||||
'cubs': {'sport': 'baseball', 'league': 'mlb', 'team_id': '16'},
|
||||
'chicago': {'sport': 'baseball', 'league': 'mlb', 'team_id': '16'},
|
||||
'chc': {'sport': 'baseball', 'league': 'mlb', 'team_id': '16'},
|
||||
'diamondbacks': {'sport': 'baseball', 'league': 'mlb', 'team_id': '29'},
|
||||
'arizona': {'sport': 'baseball', 'league': 'mlb', 'team_id': '29'},
|
||||
'ari': {'sport': 'baseball', 'league': 'mlb', 'team_id': '29'},
|
||||
'dodgers': {'sport': 'baseball', 'league': 'mlb', 'team_id': '19'},
|
||||
'lad': {'sport': 'baseball', 'league': 'mlb', 'team_id': '19'},
|
||||
'giants': {'sport': 'baseball', 'league': 'mlb', 'team_id': '26'},
|
||||
'san francisco': {'sport': 'baseball', 'league': 'mlb', 'team_id': '26'},
|
||||
'sf': {'sport': 'baseball', 'league': 'mlb', 'team_id': '26'},
|
||||
'guardians': {'sport': 'baseball', 'league': 'mlb', 'team_id': '5'},
|
||||
'cleveland': {'sport': 'baseball', 'league': 'mlb', 'team_id': '5'},
|
||||
'cle': {'sport': 'baseball', 'league': 'mlb', 'team_id': '5'},
|
||||
'marlins': {'sport': 'baseball', 'league': 'mlb', 'team_id': '28'},
|
||||
'miami': {'sport': 'baseball', 'league': 'mlb', 'team_id': '28'},
|
||||
'mia': {'sport': 'baseball', 'league': 'mlb', 'team_id': '28'},
|
||||
'mets': {'sport': 'baseball', 'league': 'mlb', 'team_id': '21'},
|
||||
'nym': {'sport': 'baseball', 'league': 'mlb', 'team_id': '21'},
|
||||
'nationals': {'sport': 'baseball', 'league': 'mlb', 'team_id': '20'},
|
||||
'washington': {'sport': 'baseball', 'league': 'mlb', 'team_id': '20'},
|
||||
'was': {'sport': 'baseball', 'league': 'mlb', 'team_id': '20'},
|
||||
'orioles': {'sport': 'baseball', 'league': 'mlb', 'team_id': '1'},
|
||||
'baltimore': {'sport': 'baseball', 'league': 'mlb', 'team_id': '1'},
|
||||
'bal': {'sport': 'baseball', 'league': 'mlb', 'team_id': '1'},
|
||||
'padres': {'sport': 'baseball', 'league': 'mlb', 'team_id': '25'},
|
||||
'san diego': {'sport': 'baseball', 'league': 'mlb', 'team_id': '25'},
|
||||
'sd': {'sport': 'baseball', 'league': 'mlb', 'team_id': '25'},
|
||||
'phillies': {'sport': 'baseball', 'league': 'mlb', 'team_id': '22'},
|
||||
'philadelphia': {'sport': 'baseball', 'league': 'mlb', 'team_id': '22'},
|
||||
'phi': {'sport': 'baseball', 'league': 'mlb', 'team_id': '22'},
|
||||
'pirates': {'sport': 'baseball', 'league': 'mlb', 'team_id': '23'},
|
||||
'pittsburgh': {'sport': 'baseball', 'league': 'mlb', 'team_id': '23'},
|
||||
'pit': {'sport': 'baseball', 'league': 'mlb', 'team_id': '23'},
|
||||
'rangers': {'sport': 'baseball', 'league': 'mlb', 'team_id': '13'},
|
||||
'texas': {'sport': 'baseball', 'league': 'mlb', 'team_id': '13'},
|
||||
'tex': {'sport': 'baseball', 'league': 'mlb', 'team_id': '13'},
|
||||
'rays': {'sport': 'baseball', 'league': 'mlb', 'team_id': '30'},
|
||||
'tampa bay': {'sport': 'baseball', 'league': 'mlb', 'team_id': '30'},
|
||||
'tb': {'sport': 'baseball', 'league': 'mlb', 'team_id': '30'},
|
||||
'red sox': {'sport': 'baseball', 'league': 'mlb', 'team_id': '2'},
|
||||
'boston': {'sport': 'baseball', 'league': 'mlb', 'team_id': '2'},
|
||||
'bos': {'sport': 'baseball', 'league': 'mlb', 'team_id': '2'},
|
||||
'reds': {'sport': 'baseball', 'league': 'mlb', 'team_id': '17'},
|
||||
'cincinnati': {'sport': 'baseball', 'league': 'mlb', 'team_id': '17'},
|
||||
'cin': {'sport': 'baseball', 'league': 'mlb', 'team_id': '17'},
|
||||
'rockies': {'sport': 'baseball', 'league': 'mlb', 'team_id': '27'},
|
||||
'colorado': {'sport': 'baseball', 'league': 'mlb', 'team_id': '27'},
|
||||
'col': {'sport': 'baseball', 'league': 'mlb', 'team_id': '27'},
|
||||
'royals': {'sport': 'baseball', 'league': 'mlb', 'team_id': '7'},
|
||||
'kansas city': {'sport': 'baseball', 'league': 'mlb', 'team_id': '7'},
|
||||
'kc': {'sport': 'baseball', 'league': 'mlb', 'team_id': '7'},
|
||||
'tigers': {'sport': 'baseball', 'league': 'mlb', 'team_id': '6'},
|
||||
'detroit': {'sport': 'baseball', 'league': 'mlb', 'team_id': '6'},
|
||||
'det': {'sport': 'baseball', 'league': 'mlb', 'team_id': '6'},
|
||||
'twins': {'sport': 'baseball', 'league': 'mlb', 'team_id': '9'},
|
||||
'minnesota': {'sport': 'baseball', 'league': 'mlb', 'team_id': '9'},
|
||||
'min': {'sport': 'baseball', 'league': 'mlb', 'team_id': '9'},
|
||||
'white sox': {'sport': 'baseball', 'league': 'mlb', 'team_id': '4'},
|
||||
'chw': {'sport': 'baseball', 'league': 'mlb', 'team_id': '4'},
|
||||
'yankees': {'sport': 'baseball', 'league': 'mlb', 'team_id': '10'},
|
||||
'new york': {'sport': 'baseball', 'league': 'mlb', 'team_id': '10'},
|
||||
'nyy': {'sport': 'baseball', 'league': 'mlb', 'team_id': '10'},
|
||||
|
||||
# NBA Teams
|
||||
'hawks': {'sport': 'basketball', 'league': 'nba', 'team_id': '1'},
|
||||
'atlanta hawks': {'sport': 'basketball', 'league': 'nba', 'team_id': '1'},
|
||||
'celtics': {'sport': 'basketball', 'league': 'nba', 'team_id': '2'},
|
||||
'boston celtics': {'sport': 'basketball', 'league': 'nba', 'team_id': '2'},
|
||||
'nets': {'sport': 'basketball', 'league': 'nba', 'team_id': '17'},
|
||||
'brooklyn nets': {'sport': 'basketball', 'league': 'nba', 'team_id': '17'},
|
||||
'hornets': {'sport': 'basketball', 'league': 'nba', 'team_id': '30'},
|
||||
'charlotte hornets': {'sport': 'basketball', 'league': 'nba', 'team_id': '30'},
|
||||
'bulls': {'sport': 'basketball', 'league': 'nba', 'team_id': '4'},
|
||||
'chicago bulls': {'sport': 'basketball', 'league': 'nba', 'team_id': '4'},
|
||||
'cavaliers': {'sport': 'basketball', 'league': 'nba', 'team_id': '5'},
|
||||
'cavs': {'sport': 'basketball', 'league': 'nba', 'team_id': '5'},
|
||||
'cleveland cavaliers': {'sport': 'basketball', 'league': 'nba', 'team_id': '5'},
|
||||
'mavericks': {'sport': 'basketball', 'league': 'nba', 'team_id': '6'},
|
||||
'mavs': {'sport': 'basketball', 'league': 'nba', 'team_id': '6'},
|
||||
'dallas mavericks': {'sport': 'basketball', 'league': 'nba', 'team_id': '6'},
|
||||
'nuggets': {'sport': 'basketball', 'league': 'nba', 'team_id': '7'},
|
||||
'denver nuggets': {'sport': 'basketball', 'league': 'nba', 'team_id': '7'},
|
||||
'pistons': {'sport': 'basketball', 'league': 'nba', 'team_id': '8'},
|
||||
'detroit pistons': {'sport': 'basketball', 'league': 'nba', 'team_id': '8'},
|
||||
'warriors': {'sport': 'basketball', 'league': 'nba', 'team_id': '9'},
|
||||
'golden state warriors': {'sport': 'basketball', 'league': 'nba', 'team_id': '9'},
|
||||
'rockets': {'sport': 'basketball', 'league': 'nba', 'team_id': '10'},
|
||||
'houston rockets': {'sport': 'basketball', 'league': 'nba', 'team_id': '10'},
|
||||
'pacers': {'sport': 'basketball', 'league': 'nba', 'team_id': '11'},
|
||||
'indiana pacers': {'sport': 'basketball', 'league': 'nba', 'team_id': '11'},
|
||||
'clippers': {'sport': 'basketball', 'league': 'nba', 'team_id': '12'},
|
||||
'la clippers': {'sport': 'basketball', 'league': 'nba', 'team_id': '12'},
|
||||
'lakers': {'sport': 'basketball', 'league': 'nba', 'team_id': '13'},
|
||||
'la lakers': {'sport': 'basketball', 'league': 'nba', 'team_id': '13'},
|
||||
'heat': {'sport': 'basketball', 'league': 'nba', 'team_id': '14'},
|
||||
'miami heat': {'sport': 'basketball', 'league': 'nba', 'team_id': '14'},
|
||||
'bucks': {'sport': 'basketball', 'league': 'nba', 'team_id': '15'},
|
||||
'milwaukee bucks': {'sport': 'basketball', 'league': 'nba', 'team_id': '15'},
|
||||
'timberwolves': {'sport': 'basketball', 'league': 'nba', 'team_id': '16'},
|
||||
'twolves': {'sport': 'basketball', 'league': 'nba', 'team_id': '16'},
|
||||
'minnesota timberwolves': {'sport': 'basketball', 'league': 'nba', 'team_id': '16'},
|
||||
'pelicans': {'sport': 'basketball', 'league': 'nba', 'team_id': '3'},
|
||||
'new orleans pelicans': {'sport': 'basketball', 'league': 'nba', 'team_id': '3'},
|
||||
'knicks': {'sport': 'basketball', 'league': 'nba', 'team_id': '18'},
|
||||
'new york knicks': {'sport': 'basketball', 'league': 'nba', 'team_id': '18'},
|
||||
'magic': {'sport': 'basketball', 'league': 'nba', 'team_id': '19'},
|
||||
'orlando magic': {'sport': 'basketball', 'league': 'nba', 'team_id': '19'},
|
||||
'76ers': {'sport': 'basketball', 'league': 'nba', 'team_id': '20'},
|
||||
'sixers': {'sport': 'basketball', 'league': 'nba', 'team_id': '20'},
|
||||
'philadelphia 76ers': {'sport': 'basketball', 'league': 'nba', 'team_id': '20'},
|
||||
'suns': {'sport': 'basketball', 'league': 'nba', 'team_id': '21'},
|
||||
'phoenix suns': {'sport': 'basketball', 'league': 'nba', 'team_id': '21'},
|
||||
'trail blazers': {'sport': 'basketball', 'league': 'nba', 'team_id': '22'},
|
||||
'trailblazers': {'sport': 'basketball', 'league': 'nba', 'team_id': '22'},
|
||||
'blazers': {'sport': 'basketball', 'league': 'nba', 'team_id': '22'},
|
||||
'portland trail blazers': {'sport': 'basketball', 'league': 'nba', 'team_id': '22'},
|
||||
'kings': {'sport': 'basketball', 'league': 'nba', 'team_id': '23'},
|
||||
'sacramento kings': {'sport': 'basketball', 'league': 'nba', 'team_id': '23'},
|
||||
'spurs': {'sport': 'basketball', 'league': 'nba', 'team_id': '24'},
|
||||
'san antonio spurs': {'sport': 'basketball', 'league': 'nba', 'team_id': '24'},
|
||||
'thunder': {'sport': 'basketball', 'league': 'nba', 'team_id': '25'},
|
||||
'okc thunder': {'sport': 'basketball', 'league': 'nba', 'team_id': '25'},
|
||||
'oklahoma city thunder': {'sport': 'basketball', 'league': 'nba', 'team_id': '25'},
|
||||
'jazz': {'sport': 'basketball', 'league': 'nba', 'team_id': '26'},
|
||||
'utah jazz': {'sport': 'basketball', 'league': 'nba', 'team_id': '26'},
|
||||
'wizards': {'sport': 'basketball', 'league': 'nba', 'team_id': '27'},
|
||||
'washington wizards': {'sport': 'basketball', 'league': 'nba', 'team_id': '27'},
|
||||
'raptors': {'sport': 'basketball', 'league': 'nba', 'team_id': '28'},
|
||||
'toronto raptors': {'sport': 'basketball', 'league': 'nba', 'team_id': '28'},
|
||||
'grizzlies': {'sport': 'basketball', 'league': 'nba', 'team_id': '29'},
|
||||
'memphis grizzlies': {'sport': 'basketball', 'league': 'nba', 'team_id': '29'},
|
||||
|
||||
# WNBA Teams
|
||||
'storm': {'sport': 'basketball', 'league': 'wnba', 'team_id': '14'},
|
||||
'seattle storm': {'sport': 'basketball', 'league': 'wnba', 'team_id': '14'},
|
||||
'liberty': {'sport': 'basketball', 'league': 'wnba', 'team_id': '9'},
|
||||
'new york liberty': {'sport': 'basketball', 'league': 'wnba', 'team_id': '9'},
|
||||
'sparks': {'sport': 'basketball', 'league': 'wnba', 'team_id': '6'},
|
||||
'los angeles sparks': {'sport': 'basketball', 'league': 'wnba', 'team_id': '6'},
|
||||
'sky': {'sport': 'basketball', 'league': 'wnba', 'team_id': '19'},
|
||||
'chicago sky': {'sport': 'basketball', 'league': 'wnba', 'team_id': '19'},
|
||||
'dream': {'sport': 'basketball', 'league': 'wnba', 'team_id': '20'},
|
||||
'atlanta dream': {'sport': 'basketball', 'league': 'wnba', 'team_id': '20'},
|
||||
'sun': {'sport': 'basketball', 'league': 'wnba', 'team_id': '18'},
|
||||
'connecticut sun': {'sport': 'basketball', 'league': 'wnba', 'team_id': '18'},
|
||||
'wings': {'sport': 'basketball', 'league': 'wnba', 'team_id': '3'},
|
||||
'dallas wings': {'sport': 'basketball', 'league': 'wnba', 'team_id': '3'},
|
||||
'valkyries': {'sport': 'basketball', 'league': 'wnba', 'team_id': '129689'},
|
||||
'golden state valkyries': {'sport': 'basketball', 'league': 'wnba', 'team_id': '129689'},
|
||||
'fever': {'sport': 'basketball', 'league': 'wnba', 'team_id': '5'},
|
||||
'indiana fever': {'sport': 'basketball', 'league': 'wnba', 'team_id': '5'},
|
||||
'aces': {'sport': 'basketball', 'league': 'wnba', 'team_id': '17'},
|
||||
'las vegas aces': {'sport': 'basketball', 'league': 'wnba', 'team_id': '17'},
|
||||
'lynx': {'sport': 'basketball', 'league': 'wnba', 'team_id': '8'},
|
||||
'minnesota lynx': {'sport': 'basketball', 'league': 'wnba', 'team_id': '8'},
|
||||
'mercury': {'sport': 'basketball', 'league': 'wnba', 'team_id': '11'},
|
||||
'phoenix mercury': {'sport': 'basketball', 'league': 'wnba', 'team_id': '11'},
|
||||
'mystics': {'sport': 'basketball', 'league': 'wnba', 'team_id': '16'},
|
||||
'washington mystics': {'sport': 'basketball', 'league': 'wnba', 'team_id': '16'},
|
||||
|
||||
# NHL Teams
|
||||
'ducks': {'sport': 'hockey', 'league': 'nhl', 'team_id': '25'},
|
||||
'anaheim': {'sport': 'hockey', 'league': 'nhl', 'team_id': '25'},
|
||||
'ana': {'sport': 'hockey', 'league': 'nhl', 'team_id': '25'},
|
||||
'bruins': {'sport': 'hockey', 'league': 'nhl', 'team_id': '1'},
|
||||
'boston bruins': {'sport': 'hockey', 'league': 'nhl', 'team_id': '1'},
|
||||
'bos': {'sport': 'hockey', 'league': 'nhl', 'team_id': '1'},
|
||||
'sabres': {'sport': 'hockey', 'league': 'nhl', 'team_id': '2'},
|
||||
'buffalo': {'sport': 'hockey', 'league': 'nhl', 'team_id': '2'},
|
||||
'buf': {'sport': 'hockey', 'league': 'nhl', 'team_id': '2'},
|
||||
'flames': {'sport': 'hockey', 'league': 'nhl', 'team_id': '3'},
|
||||
'calgary': {'sport': 'hockey', 'league': 'nhl', 'team_id': '3'},
|
||||
'cgy': {'sport': 'hockey', 'league': 'nhl', 'team_id': '3'},
|
||||
'hurricanes': {'sport': 'hockey', 'league': 'nhl', 'team_id': '7'},
|
||||
'carolina': {'sport': 'hockey', 'league': 'nhl', 'team_id': '7'},
|
||||
'car': {'sport': 'hockey', 'league': 'nhl', 'team_id': '7'},
|
||||
'blackhawks': {'sport': 'hockey', 'league': 'nhl', 'team_id': '4'},
|
||||
'chicago blackhawks': {'sport': 'hockey', 'league': 'nhl', 'team_id': '4'},
|
||||
'chi': {'sport': 'hockey', 'league': 'nhl', 'team_id': '4'},
|
||||
'avalanche': {'sport': 'hockey', 'league': 'nhl', 'team_id': '17'},
|
||||
'colorado': {'sport': 'hockey', 'league': 'nhl', 'team_id': '17'},
|
||||
'col': {'sport': 'hockey', 'league': 'nhl', 'team_id': '17'},
|
||||
'blue jackets': {'sport': 'hockey', 'league': 'nhl', 'team_id': '29'},
|
||||
'columbus': {'sport': 'hockey', 'league': 'nhl', 'team_id': '29'},
|
||||
'cbj': {'sport': 'hockey', 'league': 'nhl', 'team_id': '29'},
|
||||
'stars': {'sport': 'hockey', 'league': 'nhl', 'team_id': '9'},
|
||||
'dallas': {'sport': 'hockey', 'league': 'nhl', 'team_id': '9'},
|
||||
'dal': {'sport': 'hockey', 'league': 'nhl', 'team_id': '9'},
|
||||
'red wings': {'sport': 'hockey', 'league': 'nhl', 'team_id': '5'},
|
||||
'detroit': {'sport': 'hockey', 'league': 'nhl', 'team_id': '5'},
|
||||
'det': {'sport': 'hockey', 'league': 'nhl', 'team_id': '5'},
|
||||
'oilers': {'sport': 'hockey', 'league': 'nhl', 'team_id': '6'},
|
||||
'edmonton': {'sport': 'hockey', 'league': 'nhl', 'team_id': '6'},
|
||||
'edm': {'sport': 'hockey', 'league': 'nhl', 'team_id': '6'},
|
||||
'panthers': {'sport': 'hockey', 'league': 'nhl', 'team_id': '26'},
|
||||
'florida': {'sport': 'hockey', 'league': 'nhl', 'team_id': '26'},
|
||||
'fla': {'sport': 'hockey', 'league': 'nhl', 'team_id': '26'},
|
||||
'kings': {'sport': 'hockey', 'league': 'nhl', 'team_id': '8'},
|
||||
'los angeles': {'sport': 'hockey', 'league': 'nhl', 'team_id': '8'},
|
||||
'la': {'sport': 'hockey', 'league': 'nhl', 'team_id': '8'},
|
||||
'wild': {'sport': 'hockey', 'league': 'nhl', 'team_id': '30'},
|
||||
'minnesota': {'sport': 'hockey', 'league': 'nhl', 'team_id': '30'},
|
||||
'min': {'sport': 'hockey', 'league': 'nhl', 'team_id': '30'},
|
||||
'canadiens': {'sport': 'hockey', 'league': 'nhl', 'team_id': '10'},
|
||||
'montreal': {'sport': 'hockey', 'league': 'nhl', 'team_id': '10'},
|
||||
'mtl': {'sport': 'hockey', 'league': 'nhl', 'team_id': '10'},
|
||||
'predators': {'sport': 'hockey', 'league': 'nhl', 'team_id': '27'},
|
||||
'nashville': {'sport': 'hockey', 'league': 'nhl', 'team_id': '27'},
|
||||
'nsh': {'sport': 'hockey', 'league': 'nhl', 'team_id': '27'},
|
||||
'devils': {'sport': 'hockey', 'league': 'nhl', 'team_id': '11'},
|
||||
'new jersey': {'sport': 'hockey', 'league': 'nhl', 'team_id': '11'},
|
||||
'nj': {'sport': 'hockey', 'league': 'nhl', 'team_id': '11'},
|
||||
'islanders': {'sport': 'hockey', 'league': 'nhl', 'team_id': '12'},
|
||||
'new york islanders': {'sport': 'hockey', 'league': 'nhl', 'team_id': '12'},
|
||||
'nyi': {'sport': 'hockey', 'league': 'nhl', 'team_id': '12'},
|
||||
'rangers': {'sport': 'hockey', 'league': 'nhl', 'team_id': '13'},
|
||||
'new york rangers': {'sport': 'hockey', 'league': 'nhl', 'team_id': '13'},
|
||||
'nyr': {'sport': 'hockey', 'league': 'nhl', 'team_id': '13'},
|
||||
'senators': {'sport': 'hockey', 'league': 'nhl', 'team_id': '14'},
|
||||
'ottawa': {'sport': 'hockey', 'league': 'nhl', 'team_id': '14'},
|
||||
'ott': {'sport': 'hockey', 'league': 'nhl', 'team_id': '14'},
|
||||
'flyers': {'sport': 'hockey', 'league': 'nhl', 'team_id': '15'},
|
||||
'philadelphia': {'sport': 'hockey', 'league': 'nhl', 'team_id': '15'},
|
||||
'phi': {'sport': 'hockey', 'league': 'nhl', 'team_id': '15'},
|
||||
'penguins': {'sport': 'hockey', 'league': 'nhl', 'team_id': '16'},
|
||||
'pittsburgh': {'sport': 'hockey', 'league': 'nhl', 'team_id': '16'},
|
||||
'pit': {'sport': 'hockey', 'league': 'nhl', 'team_id': '16'},
|
||||
'sharks': {'sport': 'hockey', 'league': 'nhl', 'team_id': '18'},
|
||||
'san jose': {'sport': 'hockey', 'league': 'nhl', 'team_id': '18'},
|
||||
'sj': {'sport': 'hockey', 'league': 'nhl', 'team_id': '18'},
|
||||
'kraken': {'sport': 'hockey', 'league': 'nhl', 'team_id': '124292'},
|
||||
'seattle kraken': {'sport': 'hockey', 'league': 'nhl', 'team_id': '124292'},
|
||||
'seattle': {'sport': 'hockey', 'league': 'nhl', 'team_id': '124292'},
|
||||
'blues': {'sport': 'hockey', 'league': 'nhl', 'team_id': '19'},
|
||||
'st louis': {'sport': 'hockey', 'league': 'nhl', 'team_id': '19'},
|
||||
'stl': {'sport': 'hockey', 'league': 'nhl', 'team_id': '19'},
|
||||
'lightning': {'sport': 'hockey', 'league': 'nhl', 'team_id': '20'},
|
||||
'tampa bay': {'sport': 'hockey', 'league': 'nhl', 'team_id': '20'},
|
||||
'tb': {'sport': 'hockey', 'league': 'nhl', 'team_id': '20'},
|
||||
'maple leafs': {'sport': 'hockey', 'league': 'nhl', 'team_id': '21'},
|
||||
'toronto': {'sport': 'hockey', 'league': 'nhl', 'team_id': '21'},
|
||||
'tor': {'sport': 'hockey', 'league': 'nhl', 'team_id': '21'},
|
||||
'mammoth': {'sport': 'hockey', 'league': 'nhl', 'team_id': '129764'},
|
||||
'utah': {'sport': 'hockey', 'league': 'nhl', 'team_id': '129764'},
|
||||
'utah mammoth': {'sport': 'hockey', 'league': 'nhl', 'team_id': '129764'},
|
||||
'canucks': {'sport': 'hockey', 'league': 'nhl', 'team_id': '22'},
|
||||
'vancouver': {'sport': 'hockey', 'league': 'nhl', 'team_id': '22'},
|
||||
'van': {'sport': 'hockey', 'league': 'nhl', 'team_id': '22'},
|
||||
'golden knights': {'sport': 'hockey', 'league': 'nhl', 'team_id': '37'},
|
||||
'vegas': {'sport': 'hockey', 'league': 'nhl', 'team_id': '37'},
|
||||
'vgk': {'sport': 'hockey', 'league': 'nhl', 'team_id': '37'},
|
||||
'capitals': {'sport': 'hockey', 'league': 'nhl', 'team_id': '23'},
|
||||
'washington': {'sport': 'hockey', 'league': 'nhl', 'team_id': '23'},
|
||||
'wsh': {'sport': 'hockey', 'league': 'nhl', 'team_id': '23'},
|
||||
'jets': {'sport': 'hockey', 'league': 'nhl', 'team_id': '28'},
|
||||
'winnipeg': {'sport': 'hockey', 'league': 'nhl', 'team_id': '28'},
|
||||
'wpg': {'sport': 'hockey', 'league': 'nhl', 'team_id': '28'},
|
||||
|
||||
# WHL Teams (Western Hockey League) - using TheSportsDB API
|
||||
'thunderbirds': {'sport': 'hockey', 'league': 'whl', 'team_id': '144380', 'api_source': 'thesportsdb'},
|
||||
'seattle thunderbirds': {'sport': 'hockey', 'league': 'whl', 'team_id': '144380', 'api_source': 'thesportsdb'},
|
||||
't-birds': {'sport': 'hockey', 'league': 'whl', 'team_id': '144380', 'api_source': 'thesportsdb'},
|
||||
'winterhawks': {'sport': 'hockey', 'league': 'whl', 'team_id': '144379', 'api_source': 'thesportsdb'},
|
||||
'portland winterhawks': {'sport': 'hockey', 'league': 'whl', 'team_id': '144379', 'api_source': 'thesportsdb'},
|
||||
'silvertips': {'sport': 'hockey', 'league': 'whl', 'team_id': '144378', 'api_source': 'thesportsdb'},
|
||||
'everett silvertips': {'sport': 'hockey', 'league': 'whl', 'team_id': '144378', 'api_source': 'thesportsdb'},
|
||||
'everett': {'sport': 'hockey', 'league': 'whl', 'team_id': '144378', 'api_source': 'thesportsdb'},
|
||||
'spokane chiefs': {'sport': 'hockey', 'league': 'whl', 'team_id': '144381', 'api_source': 'thesportsdb'},
|
||||
'spokane': {'sport': 'hockey', 'league': 'whl', 'team_id': '144381', 'api_source': 'thesportsdb'},
|
||||
'vancouver giants': {'sport': 'hockey', 'league': 'whl', 'team_id': '144376', 'api_source': 'thesportsdb'},
|
||||
'blazers': {'sport': 'hockey', 'league': 'whl', 'team_id': '144373', 'api_source': 'thesportsdb'},
|
||||
'kamloops blazers': {'sport': 'hockey', 'league': 'whl', 'team_id': '144373', 'api_source': 'thesportsdb'},
|
||||
'kamloops': {'sport': 'hockey', 'league': 'whl', 'team_id': '144373', 'api_source': 'thesportsdb'},
|
||||
'cougars': {'sport': 'hockey', 'league': 'whl', 'team_id': '144375', 'api_source': 'thesportsdb'},
|
||||
'prince george cougars': {'sport': 'hockey', 'league': 'whl', 'team_id': '144375', 'api_source': 'thesportsdb'},
|
||||
'prince george': {'sport': 'hockey', 'league': 'whl', 'team_id': '144375', 'api_source': 'thesportsdb'},
|
||||
'rockets': {'sport': 'hockey', 'league': 'whl', 'team_id': '144374', 'api_source': 'thesportsdb'},
|
||||
'kelowna rockets': {'sport': 'hockey', 'league': 'whl', 'team_id': '144374', 'api_source': 'thesportsdb'},
|
||||
'kelowna': {'sport': 'hockey', 'league': 'whl', 'team_id': '144374', 'api_source': 'thesportsdb'},
|
||||
'tri-city americans': {'sport': 'hockey', 'league': 'whl', 'team_id': '144382', 'api_source': 'thesportsdb'},
|
||||
'americans': {'sport': 'hockey', 'league': 'whl', 'team_id': '144382', 'api_source': 'thesportsdb'},
|
||||
'tri city': {'sport': 'hockey', 'league': 'whl', 'team_id': '144382', 'api_source': 'thesportsdb'},
|
||||
'tricity': {'sport': 'hockey', 'league': 'whl', 'team_id': '144382', 'api_source': 'thesportsdb'},
|
||||
'wenatchee wild': {'sport': 'hockey', 'league': 'whl', 'team_id': '144372', 'api_source': 'thesportsdb'},
|
||||
'wenatchee': {'sport': 'hockey', 'league': 'whl', 'team_id': '144372', 'api_source': 'thesportsdb'},
|
||||
'victoria royals': {'sport': 'hockey', 'league': 'whl', 'team_id': '144377', 'api_source': 'thesportsdb'},
|
||||
'victoria': {'sport': 'hockey', 'league': 'whl', 'team_id': '144377', 'api_source': 'thesportsdb'},
|
||||
'edmonton oil kings': {'sport': 'hockey', 'league': 'whl', 'team_id': '144362', 'api_source': 'thesportsdb'},
|
||||
'oil kings': {'sport': 'hockey', 'league': 'whl', 'team_id': '144362', 'api_source': 'thesportsdb'},
|
||||
'calgary hitmen': {'sport': 'hockey', 'league': 'whl', 'team_id': '144361', 'api_source': 'thesportsdb'},
|
||||
'hitmen': {'sport': 'hockey', 'league': 'whl', 'team_id': '144361', 'api_source': 'thesportsdb'},
|
||||
'red deer rebels': {'sport': 'hockey', 'league': 'whl', 'team_id': '144365', 'api_source': 'thesportsdb'},
|
||||
'red deer': {'sport': 'hockey', 'league': 'whl', 'team_id': '144365', 'api_source': 'thesportsdb'},
|
||||
'medicine hat tigers': {'sport': 'hockey', 'league': 'whl', 'team_id': '144364', 'api_source': 'thesportsdb'},
|
||||
'medicine hat': {'sport': 'hockey', 'league': 'whl', 'team_id': '144364', 'api_source': 'thesportsdb'},
|
||||
'lethbridge hurricanes': {'sport': 'hockey', 'league': 'whl', 'team_id': '144363', 'api_source': 'thesportsdb'},
|
||||
'lethbridge': {'sport': 'hockey', 'league': 'whl', 'team_id': '144363', 'api_source': 'thesportsdb'},
|
||||
'swift current broncos': {'sport': 'hockey', 'league': 'whl', 'team_id': '144366', 'api_source': 'thesportsdb'},
|
||||
'swift current': {'sport': 'hockey', 'league': 'whl', 'team_id': '144366', 'api_source': 'thesportsdb'},
|
||||
'moose jaw warriors': {'sport': 'hockey', 'league': 'whl', 'team_id': '144368', 'api_source': 'thesportsdb'},
|
||||
'moose jaw': {'sport': 'hockey', 'league': 'whl', 'team_id': '144368', 'api_source': 'thesportsdb'},
|
||||
'regina pats': {'sport': 'hockey', 'league': 'whl', 'team_id': '144370', 'api_source': 'thesportsdb'},
|
||||
'pats': {'sport': 'hockey', 'league': 'whl', 'team_id': '144370', 'api_source': 'thesportsdb'},
|
||||
'regina': {'sport': 'hockey', 'league': 'whl', 'team_id': '144370', 'api_source': 'thesportsdb'},
|
||||
'saskatoon blades': {'sport': 'hockey', 'league': 'whl', 'team_id': '144371', 'api_source': 'thesportsdb'},
|
||||
'blades': {'sport': 'hockey', 'league': 'whl', 'team_id': '144371', 'api_source': 'thesportsdb'},
|
||||
'saskatoon': {'sport': 'hockey', 'league': 'whl', 'team_id': '144371', 'api_source': 'thesportsdb'},
|
||||
'prince albert raiders': {'sport': 'hockey', 'league': 'whl', 'team_id': '144369', 'api_source': 'thesportsdb'},
|
||||
'prince albert': {'sport': 'hockey', 'league': 'whl', 'team_id': '144369', 'api_source': 'thesportsdb'},
|
||||
'brandon wheat kings': {'sport': 'hockey', 'league': 'whl', 'team_id': '144367', 'api_source': 'thesportsdb'},
|
||||
'wheat kings': {'sport': 'hockey', 'league': 'whl', 'team_id': '144367', 'api_source': 'thesportsdb'},
|
||||
'brandon': {'sport': 'hockey', 'league': 'whl', 'team_id': '144367', 'api_source': 'thesportsdb'},
|
||||
|
||||
# MLS Teams
|
||||
'sounders': {'sport': 'soccer', 'league': 'usa.1', 'team_id': '9726'},
|
||||
'seattle sounders': {'sport': 'soccer', 'league': 'usa.1', 'team_id': '9726'},
|
||||
|
||||
# NWSL Teams
|
||||
'reign': {'sport': 'soccer', 'league': 'usa.nwsl', 'team_id': '15363'},
|
||||
'seattle reign': {'sport': 'soccer', 'league': 'usa.nwsl', 'team_id': '15363'},
|
||||
'racing': {'sport': 'soccer', 'league': 'usa.nwsl', 'team_id': '20905'},
|
||||
'racing louisville': {'sport': 'soccer', 'league': 'usa.nwsl', 'team_id': '20905'},
|
||||
'louisville': {'sport': 'soccer', 'league': 'usa.nwsl', 'team_id': '20905'},
|
||||
'atlanta united': {'sport': 'soccer', 'league': 'usa.1', 'team_id': '18418'},
|
||||
'atl': {'sport': 'soccer', 'league': 'usa.1', 'team_id': '18418'},
|
||||
'austin fc': {'sport': 'soccer', 'league': 'usa.1', 'team_id': '20906'},
|
||||
'atx': {'sport': 'soccer', 'league': 'usa.1', 'team_id': '20906'},
|
||||
'cf montreal': {'sport': 'soccer', 'league': 'usa.1', 'team_id': '9720'},
|
||||
'montreal': {'sport': 'soccer', 'league': 'usa.1', 'team_id': '9720'},
|
||||
'mtl': {'sport': 'soccer', 'league': 'usa.1', 'team_id': '9720'},
|
||||
'charlotte fc': {'sport': 'soccer', 'league': 'usa.1', 'team_id': '21300'},
|
||||
'clt': {'sport': 'soccer', 'league': 'usa.1', 'team_id': '21300'},
|
||||
'chicago fire': {'sport': 'soccer', 'league': 'usa.1', 'team_id': '182'},
|
||||
'fire': {'sport': 'soccer', 'league': 'usa.1', 'team_id': '182'},
|
||||
'chi': {'sport': 'soccer', 'league': 'usa.1', 'team_id': '182'},
|
||||
'rapids': {'sport': 'soccer', 'league': 'usa.1', 'team_id': '184'},
|
||||
'colorado': {'sport': 'soccer', 'league': 'usa.1', 'team_id': '184'},
|
||||
'col': {'sport': 'soccer', 'league': 'usa.1', 'team_id': '184'},
|
||||
'crew': {'sport': 'soccer', 'league': 'usa.1', 'team_id': '183'},
|
||||
'columbus': {'sport': 'soccer', 'league': 'usa.1', 'team_id': '183'},
|
||||
'clb': {'sport': 'soccer', 'league': 'usa.1', 'team_id': '183'},
|
||||
'dc united': {'sport': 'soccer', 'league': 'usa.1', 'team_id': '193'},
|
||||
'dc': {'sport': 'soccer', 'league': 'usa.1', 'team_id': '193'},
|
||||
'fc cincinnati': {'sport': 'soccer', 'league': 'usa.1', 'team_id': '18267'},
|
||||
'cincinnati': {'sport': 'soccer', 'league': 'usa.1', 'team_id': '18267'},
|
||||
'cin': {'sport': 'soccer', 'league': 'usa.1', 'team_id': '18267'},
|
||||
'fc dallas': {'sport': 'soccer', 'league': 'usa.1', 'team_id': '185'},
|
||||
'dallas': {'sport': 'soccer', 'league': 'usa.1', 'team_id': '185'},
|
||||
'dal': {'sport': 'soccer', 'league': 'usa.1', 'team_id': '185'},
|
||||
'dynamo': {'sport': 'soccer', 'league': 'usa.1', 'team_id': '6077'},
|
||||
'houston': {'sport': 'soccer', 'league': 'usa.1', 'team_id': '6077'},
|
||||
'hou': {'sport': 'soccer', 'league': 'usa.1', 'team_id': '6077'},
|
||||
'inter miami': {'sport': 'soccer', 'league': 'usa.1', 'team_id': '20232'},
|
||||
'miami': {'sport': 'soccer', 'league': 'usa.1', 'team_id': '20232'},
|
||||
'mia': {'sport': 'soccer', 'league': 'usa.1', 'team_id': '20232'},
|
||||
'la galaxy': {'sport': 'soccer', 'league': 'usa.1', 'team_id': '187'},
|
||||
'galaxy': {'sport': 'soccer', 'league': 'usa.1', 'team_id': '187'},
|
||||
'la': {'sport': 'soccer', 'league': 'usa.1', 'team_id': '187'},
|
||||
'lafc': {'sport': 'soccer', 'league': 'usa.1', 'team_id': '18966'},
|
||||
'minnesota united': {'sport': 'soccer', 'league': 'usa.1', 'team_id': '17362'},
|
||||
'minnesota': {'sport': 'soccer', 'league': 'usa.1', 'team_id': '17362'},
|
||||
'min': {'sport': 'soccer', 'league': 'usa.1', 'team_id': '17362'},
|
||||
'nashville sc': {'sport': 'soccer', 'league': 'usa.1', 'team_id': '18986'},
|
||||
'nashville': {'sport': 'soccer', 'league': 'usa.1', 'team_id': '18986'},
|
||||
'nsh': {'sport': 'soccer', 'league': 'usa.1', 'team_id': '18986'},
|
||||
'revolution': {'sport': 'soccer', 'league': 'usa.1', 'team_id': '189'},
|
||||
'new england': {'sport': 'soccer', 'league': 'usa.1', 'team_id': '189'},
|
||||
'ne': {'sport': 'soccer', 'league': 'usa.1', 'team_id': '189'},
|
||||
'nyc fc': {'sport': 'soccer', 'league': 'usa.1', 'team_id': '17606'},
|
||||
'nyc': {'sport': 'soccer', 'league': 'usa.1', 'team_id': '17606'},
|
||||
'red bulls': {'sport': 'soccer', 'league': 'usa.1', 'team_id': '190'},
|
||||
'ny': {'sport': 'soccer', 'league': 'usa.1', 'team_id': '190'},
|
||||
'orlando city': {'sport': 'soccer', 'league': 'usa.1', 'team_id': '12011'},
|
||||
'orlando': {'sport': 'soccer', 'league': 'usa.1', 'team_id': '12011'},
|
||||
'orl': {'sport': 'soccer', 'league': 'usa.1', 'team_id': '12011'},
|
||||
'union': {'sport': 'soccer', 'league': 'usa.1', 'team_id': '10739'},
|
||||
'philadelphia': {'sport': 'soccer', 'league': 'usa.1', 'team_id': '10739'},
|
||||
'phi': {'sport': 'soccer', 'league': 'usa.1', 'team_id': '10739'},
|
||||
'timbers': {'sport': 'soccer', 'league': 'usa.1', 'team_id': '9723'},
|
||||
'portland': {'sport': 'soccer', 'league': 'usa.1', 'team_id': '9723'},
|
||||
'por': {'sport': 'soccer', 'league': 'usa.1', 'team_id': '9723'},
|
||||
'real salt lake': {'sport': 'soccer', 'league': 'usa.1', 'team_id': '4771'},
|
||||
'salt lake': {'sport': 'soccer', 'league': 'usa.1', 'team_id': '4771'},
|
||||
'rsl': {'sport': 'soccer', 'league': 'usa.1', 'team_id': '4771'},
|
||||
'san diego fc': {'sport': 'soccer', 'league': 'usa.1', 'team_id': '22529'},
|
||||
'san diego': {'sport': 'soccer', 'league': 'usa.1', 'team_id': '22529'},
|
||||
'sd': {'sport': 'soccer', 'league': 'usa.1', 'team_id': '22529'},
|
||||
'earthquakes': {'sport': 'soccer', 'league': 'usa.1', 'team_id': '191'},
|
||||
'san jose': {'sport': 'soccer', 'league': 'usa.1', 'team_id': '191'},
|
||||
'sj': {'sport': 'soccer', 'league': 'usa.1', 'team_id': '191'},
|
||||
'sporting kc': {'sport': 'soccer', 'league': 'usa.1', 'team_id': '186'},
|
||||
'sporting kansas city': {'sport': 'soccer', 'league': 'usa.1', 'team_id': '186'},
|
||||
'skc': {'sport': 'soccer', 'league': 'usa.1', 'team_id': '186'},
|
||||
'st louis city': {'sport': 'soccer', 'league': 'usa.1', 'team_id': '21812'},
|
||||
'st louis': {'sport': 'soccer', 'league': 'usa.1', 'team_id': '21812'},
|
||||
'stl': {'sport': 'soccer', 'league': 'usa.1', 'team_id': '21812'},
|
||||
'toronto fc': {'sport': 'soccer', 'league': 'usa.1', 'team_id': '7318'},
|
||||
'toronto': {'sport': 'soccer', 'league': 'usa.1', 'team_id': '7318'},
|
||||
'tor': {'sport': 'soccer', 'league': 'usa.1', 'team_id': '7318'},
|
||||
'whitecaps': {'sport': 'soccer', 'league': 'usa.1', 'team_id': '9727'},
|
||||
'vancouver': {'sport': 'soccer', 'league': 'usa.1', 'team_id': '9727'},
|
||||
'van': {'sport': 'soccer', 'league': 'usa.1', 'team_id': '9727'},
|
||||
|
||||
# Premier League Teams
|
||||
'lfc': {'sport': 'soccer', 'league': 'eng.1', 'team_id': '364'},
|
||||
'liverpool': {'sport': 'soccer', 'league': 'eng.1', 'team_id': '364'},
|
||||
'manchester united': {'sport': 'soccer', 'league': 'eng.1', 'team_id': '360'},
|
||||
'man united': {'sport': 'soccer', 'league': 'eng.1', 'team_id': '360'},
|
||||
'arsenal': {'sport': 'soccer', 'league': 'eng.1', 'team_id': '359'},
|
||||
'chelsea': {'sport': 'soccer', 'league': 'eng.1', 'team_id': '363'},
|
||||
'manchester city': {'sport': 'soccer', 'league': 'eng.1', 'team_id': '382'},
|
||||
'man city': {'sport': 'soccer', 'league': 'eng.1', 'team_id': '382'},
|
||||
}
|
||||
|
||||
# Helper to check for women's leagues
|
||||
WOMENS_LEAGUES = {
|
||||
('basketball', 'wnba'),
|
||||
('soccer', 'usa.nwsl'),
|
||||
('hockey', 'pwhl')
|
||||
}
|
||||
|
||||
# League mappings for league-wide queries
|
||||
LEAGUE_MAPPINGS = {
|
||||
# NFL
|
||||
'nfl': {'sport': 'football', 'league': 'nfl'},
|
||||
'football': {'sport': 'football', 'league': 'nfl'},
|
||||
|
||||
# CFL
|
||||
'cfl': {'sport': 'football', 'league': 'cfl'},
|
||||
'canadian football': {'sport': 'football', 'league': 'cfl'},
|
||||
|
||||
# MLB
|
||||
'mlb': {'sport': 'baseball', 'league': 'mlb'},
|
||||
'baseball': {'sport': 'baseball', 'league': 'mlb'},
|
||||
|
||||
# NBA
|
||||
'nba': {'sport': 'basketball', 'league': 'nba'},
|
||||
'basketball': {'sport': 'basketball', 'league': 'nba'},
|
||||
|
||||
# WNBA
|
||||
'wnba': {'sport': 'basketball', 'league': 'wnba'},
|
||||
|
||||
# NHL
|
||||
'nhl': {'sport': 'hockey', 'league': 'nhl'},
|
||||
'hockey': {'sport': 'hockey', 'league': 'nhl'},
|
||||
|
||||
# PWHL
|
||||
'pwhl': {'sport': 'hockey', 'league': 'pwhl'},
|
||||
|
||||
# WHL
|
||||
'whl': {'sport': 'hockey', 'league': 'whl', 'league_id': '5160', 'api_source': 'thesportsdb'},
|
||||
|
||||
# MLS
|
||||
'mls': {'sport': 'soccer', 'league': 'usa.1'},
|
||||
'soccer': {'sport': 'soccer', 'league': 'usa.1'},
|
||||
|
||||
# NWSL
|
||||
'nwsl': {'sport': 'soccer', 'league': 'usa.nwsl'},
|
||||
|
||||
# Premier League
|
||||
'epl': {'sport': 'soccer', 'league': 'eng.1'},
|
||||
'premier league': {'sport': 'soccer', 'league': 'eng.1'}
|
||||
}
|
||||
|
||||
def format_clean_date_time(dt) -> str:
|
||||
"""Format date and time without leading zeros"""
|
||||
month = dt.month
|
||||
day = dt.day
|
||||
minute = dt.minute
|
||||
ampm = dt.strftime("%p")
|
||||
|
||||
# Convert to 12-hour format
|
||||
hour_12 = dt.hour
|
||||
if hour_12 == 0:
|
||||
hour_12 = 12
|
||||
elif hour_12 > 12:
|
||||
hour_12 = hour_12 - 12
|
||||
|
||||
# Remove leading zeros
|
||||
time_str = f"{month}/{day} {hour_12}:{minute:02d} {ampm}"
|
||||
return time_str
|
||||
|
||||
def format_clean_date(dt) -> str:
|
||||
"""Format date without leading zeros"""
|
||||
month = dt.month
|
||||
day = dt.day
|
||||
return f"{month}/{day}"
|
||||
|
||||
def get_team_abbreviation_from_name(team_name: str) -> str:
|
||||
"""Extract a short abbreviation from a team name
|
||||
|
||||
Uses common city abbreviations for WHL teams.
|
||||
"""
|
||||
if not team_name:
|
||||
return 'UNK'
|
||||
|
||||
# WHL team abbreviation mappings
|
||||
whl_abbreviations = {
|
||||
'seattle thunderbirds': 'SEA',
|
||||
'portland winterhawks': 'POR',
|
||||
'everett silvertips': 'EVE',
|
||||
'spokane chiefs': 'SPO',
|
||||
'vancouver giants': 'VAN',
|
||||
'kamloops blazers': 'KAM',
|
||||
'prince george cougars': 'PG',
|
||||
'kelowna rockets': 'KEL',
|
||||
'tri-city americans': 'TC',
|
||||
'wenatchee wild': 'WEN',
|
||||
'victoria royals': 'VIC',
|
||||
'edmonton oil kings': 'EDM',
|
||||
'calgary hitmen': 'CGY',
|
||||
'red deer rebels': 'RD',
|
||||
'medicine hat tigers': 'MH',
|
||||
'lethbridge hurricanes': 'LET',
|
||||
'swift current broncos': 'SC',
|
||||
'moose jaw warriors': 'MJ',
|
||||
'regina pats': 'REG',
|
||||
'saskatoon blades': 'SAS',
|
||||
'prince albert raiders': 'PA',
|
||||
'brandon wheat kings': 'BDN',
|
||||
'winnipeg ice': 'WPG',
|
||||
}
|
||||
|
||||
team_lower = team_name.lower()
|
||||
if team_lower in whl_abbreviations:
|
||||
return whl_abbreviations[team_lower]
|
||||
|
||||
# Try to extract from city name (first one or two words)
|
||||
words = team_name.lower().split()
|
||||
if len(words) >= 2:
|
||||
# Check for two-word cities first
|
||||
two_word_city = f"{words[0]} {words[1]}"
|
||||
# Use common city abbreviations
|
||||
city_abbr = {
|
||||
'seattle': 'SEA',
|
||||
'portland': 'POR',
|
||||
'everett': 'EVE',
|
||||
'spokane': 'SPO',
|
||||
'vancouver': 'VAN',
|
||||
'kamloops': 'KAM',
|
||||
'prince george': 'PG',
|
||||
'prince albert': 'PA',
|
||||
'kelowna': 'KEL',
|
||||
'tri-city': 'TC',
|
||||
'tri city': 'TC',
|
||||
'tricity': 'TC',
|
||||
'wenatchee': 'WEN',
|
||||
'victoria': 'VIC',
|
||||
'edmonton': 'EDM',
|
||||
'calgary': 'CGY',
|
||||
'red deer': 'RD',
|
||||
'medicine hat': 'MH',
|
||||
'lethbridge': 'LET',
|
||||
'swift current': 'SC',
|
||||
'moose jaw': 'MJ',
|
||||
'regina': 'REG',
|
||||
'saskatoon': 'SAS',
|
||||
'brandon': 'BDN',
|
||||
'winnipeg': 'WPG',
|
||||
}
|
||||
|
||||
if two_word_city in city_abbr:
|
||||
return city_abbr[two_word_city]
|
||||
|
||||
# Then check for one-word cities
|
||||
city = words[0]
|
||||
if city in city_abbr:
|
||||
return city_abbr[city]
|
||||
|
||||
# Fallback for one-word city: use first 3 letters
|
||||
if len(city) >= 3:
|
||||
return city[:3].upper()
|
||||
|
||||
# Final fallback: use first 3 letters of team name
|
||||
return team_name[:3].upper() if len(team_name) >= 3 else team_name.upper()
|
||||
|
||||
def is_womens_league(sport: str, league: str) -> bool:
|
||||
"""Check if the league is a women's league"""
|
||||
return (sport, league) in WOMENS_LEAGUES
|
||||
|
||||
def is_soccer(sport: str) -> bool:
|
||||
"""Check if the sport is soccer"""
|
||||
return sport.lower() == 'soccer'
|
||||
|
||||
def get_team_abbreviation(team_id: str, team_abbreviation: str, sport: str, league: str) -> str:
|
||||
"""Get team abbreviation, using -W suffix only for women's leagues"""
|
||||
if is_womens_league(sport, league):
|
||||
return WOMENS_TEAM_ABBREVIATIONS.get(team_id, team_abbreviation)
|
||||
return team_abbreviation
|
||||
|
||||
# Export all public functions and constants
|
||||
__all__ = [
|
||||
'SPORT_EMOJIS', 'WOMENS_TEAM_ABBREVIATIONS', 'TEAM_MAPPINGS',
|
||||
'WOMENS_LEAGUES', 'LEAGUE_MAPPINGS', 'is_womens_league', 'is_soccer',
|
||||
'get_team_abbreviation', 'get_team_abbreviation_from_name',
|
||||
'format_clean_date_time', 'format_clean_date'
|
||||
]
|
||||
@@ -0,0 +1,321 @@
|
||||
import aiohttp
|
||||
import asyncio
|
||||
import time
|
||||
import logging
|
||||
from datetime import datetime, timezone
|
||||
from typing import List, Dict, Optional
|
||||
from .sports_mappings import (
|
||||
get_team_abbreviation_from_name, format_clean_date,
|
||||
format_clean_date_time, is_soccer
|
||||
)
|
||||
|
||||
class TheSportsDBClient:
|
||||
"""Client for TheSportsDB API with rate limiting and parsing logic"""
|
||||
|
||||
BASE_URL = "https://www.thesportsdb.com/api/v1/json"
|
||||
FREE_API_KEY = "123" # Free public API key
|
||||
|
||||
def __init__(self, logger=None, timeout: int = 10, session: Optional[aiohttp.ClientSession] = None):
|
||||
self.logger = logger or logging.getLogger(__name__)
|
||||
self.timeout = aiohttp.ClientTimeout(total=timeout)
|
||||
self.session = session
|
||||
self.last_request_time = 0
|
||||
self.min_request_interval = 2.1 # Slightly more than 2 seconds for safety
|
||||
|
||||
async def _get_session(self) -> aiohttp.ClientSession:
|
||||
"""Get or create an aiohttp session"""
|
||||
if self.session is None or self.session.closed:
|
||||
self.session = aiohttp.ClientSession(timeout=self.timeout)
|
||||
return self.session
|
||||
|
||||
async def _rate_limit(self):
|
||||
"""Enforce rate limiting asynchronously"""
|
||||
current_time = time.time()
|
||||
time_since_last = current_time - self.last_request_time
|
||||
if time_since_last < self.min_request_interval:
|
||||
sleep_time = self.min_request_interval - time_since_last
|
||||
await asyncio.sleep(sleep_time)
|
||||
self.last_request_time = time.time()
|
||||
|
||||
async def search_team(self, team_name: str) -> Optional[Dict]:
|
||||
"""Search for a team by name"""
|
||||
await self._rate_limit()
|
||||
url = f"{self.BASE_URL}/{self.FREE_API_KEY}/searchteams.php"
|
||||
params = {'t': team_name}
|
||||
try:
|
||||
session = await self._get_session()
|
||||
async with session.get(url, params=params) as response:
|
||||
response.raise_for_status()
|
||||
data = await response.json()
|
||||
teams = data.get('teams', [])
|
||||
return teams[0] if teams else None
|
||||
except Exception as e:
|
||||
self.logger.error(f"TheSportsDB search_team error: {e}")
|
||||
return None
|
||||
|
||||
async def get_team_events_last(self, team_id: str, limit: int = 5) -> List[Dict]:
|
||||
"""Get last N events for a team"""
|
||||
await self._rate_limit()
|
||||
url = f"{self.BASE_URL}/{self.FREE_API_KEY}/eventslast.php"
|
||||
params = {'id': team_id}
|
||||
try:
|
||||
session = await self._get_session()
|
||||
async with session.get(url, params=params) as response:
|
||||
response.raise_for_status()
|
||||
data = await response.json()
|
||||
events = data.get('results', [])
|
||||
return events[:limit] if events else []
|
||||
except Exception as e:
|
||||
self.logger.error(f"TheSportsDB get_team_events_last error: {e}")
|
||||
return []
|
||||
|
||||
async def get_team_events_next(self, team_id: str, limit: int = 5) -> List[Dict]:
|
||||
"""Get next N events for a team"""
|
||||
await self._rate_limit()
|
||||
url = f"{self.BASE_URL}/{self.FREE_API_KEY}/eventsnext.php"
|
||||
params = {'id': team_id}
|
||||
try:
|
||||
session = await self._get_session()
|
||||
async with session.get(url, params=params) as response:
|
||||
response.raise_for_status()
|
||||
data = await response.json()
|
||||
events = data.get('events', [])
|
||||
return events[:limit] if events else []
|
||||
except Exception as e:
|
||||
self.logger.error(f"TheSportsDB get_team_events_next error: {e}")
|
||||
return []
|
||||
|
||||
async def fetch_team_games(self, sport: str, league: str, team_id: str) -> List[Dict]:
|
||||
"""Fetch and parse team games (last and next)"""
|
||||
last_events_task = self.get_team_events_last(team_id, limit=5)
|
||||
next_events_task = self.get_team_events_next(team_id, limit=5)
|
||||
|
||||
last_events, next_events = await asyncio.gather(last_events_task, next_events_task)
|
||||
|
||||
all_games = []
|
||||
# Parse last events (completed games)
|
||||
for event in last_events:
|
||||
game_data = self.parse_event(event, team_id, sport, league)
|
||||
if game_data:
|
||||
all_games.append(game_data)
|
||||
|
||||
# Parse next events (upcoming games)
|
||||
for event in next_events:
|
||||
game_data = self.parse_event(event, team_id, sport, league)
|
||||
if game_data:
|
||||
all_games.append(game_data)
|
||||
|
||||
return all_games
|
||||
|
||||
async def fetch_team_schedule(self, sport: str, league: str, team_id: str) -> List[Dict]:
|
||||
"""Fetch upcoming scheduled games for a team"""
|
||||
next_events = await self.get_team_events_next(team_id, limit=10)
|
||||
|
||||
upcoming_games = []
|
||||
for event in next_events:
|
||||
game_data = self.parse_event(event, team_id, sport, league)
|
||||
if game_data:
|
||||
upcoming_games.append(game_data)
|
||||
|
||||
return upcoming_games
|
||||
|
||||
def parse_event(self, event: Dict, team_id: str, sport: str, league: str) -> Optional[Dict]:
|
||||
"""Parse a TheSportsDB event and return structured data with timestamp for sorting"""
|
||||
try:
|
||||
# Extract team info
|
||||
home_team = event.get('strHomeTeam', '')
|
||||
away_team = event.get('strAwayTeam', '')
|
||||
home_score = event.get('intHomeScore', '')
|
||||
away_score = event.get('intAwayScore', '')
|
||||
status = event.get('strStatus', 'UNKNOWN')
|
||||
timestamp_str = event.get('strTimestamp', '')
|
||||
date_str = event.get('dateEvent', '')
|
||||
time_str = event.get('strTime', '')
|
||||
|
||||
# Determine if our team is home or away
|
||||
our_team_id = str(team_id)
|
||||
event_home_id = str(event.get('idHomeTeam', ''))
|
||||
is_home = (event_home_id == our_team_id)
|
||||
|
||||
# Get team abbreviations
|
||||
home_abbr = get_team_abbreviation_from_name(home_team)
|
||||
away_abbr = get_team_abbreviation_from_name(away_team)
|
||||
|
||||
# Get timestamp for sorting
|
||||
timestamp = 0
|
||||
event_timestamp = None
|
||||
if timestamp_str:
|
||||
try:
|
||||
# fromisoformat handles 'Z' in Python 3.11+, but we force UTC if naive
|
||||
dt = datetime.fromisoformat(timestamp_str.replace('Z', '+00:00'))
|
||||
if dt.tzinfo is None:
|
||||
dt = dt.replace(tzinfo=timezone.utc)
|
||||
event_timestamp = dt.timestamp()
|
||||
timestamp = event_timestamp
|
||||
except:
|
||||
if date_str and time_str:
|
||||
try:
|
||||
dt_str = f"{date_str} {time_str}"
|
||||
dt = datetime.strptime(dt_str, "%Y-%m-%d %H:%M:%S")
|
||||
dt = dt.replace(tzinfo=timezone.utc)
|
||||
event_timestamp = dt.timestamp()
|
||||
timestamp = event_timestamp
|
||||
except: pass
|
||||
|
||||
formatted = ""
|
||||
if status == 'Match Finished':
|
||||
date_suffix = ""
|
||||
if event_timestamp:
|
||||
dt = datetime.fromtimestamp(event_timestamp, tz=timezone.utc).astimezone()
|
||||
if dt.date() != datetime.now().date():
|
||||
date_suffix = f", {format_clean_date(dt)}"
|
||||
|
||||
if home_score and away_score:
|
||||
if is_soccer(sport):
|
||||
formatted = f"@{home_abbr} {home_score}-{away_score} {away_abbr} (F{date_suffix})"
|
||||
elif is_home:
|
||||
formatted = f"{away_abbr} {away_score}-{home_score} @{home_abbr} (F{date_suffix})"
|
||||
else:
|
||||
formatted = f"@{home_abbr} {home_score}-{away_score} {away_abbr} (F{date_suffix})"
|
||||
else:
|
||||
if is_soccer(sport):
|
||||
formatted = f"@{home_abbr} vs. {away_abbr} (Final{date_suffix})"
|
||||
else:
|
||||
formatted = f"{away_abbr} vs. {home_abbr} (Final{date_suffix})"
|
||||
timestamp = 9999999998
|
||||
else:
|
||||
# Scheduled or TBD
|
||||
if event_timestamp:
|
||||
dt = datetime.fromtimestamp(event_timestamp, tz=timezone.utc).astimezone()
|
||||
time_str_formatted = format_clean_date_time(dt)
|
||||
if is_soccer(sport):
|
||||
formatted = f"@{home_abbr} vs. {away_abbr} ({time_str_formatted})"
|
||||
else:
|
||||
formatted = f"{away_abbr} @ {home_abbr} ({time_str_formatted})"
|
||||
else:
|
||||
if is_soccer(sport):
|
||||
formatted = f"@{home_abbr} vs. {away_abbr} (TBD)"
|
||||
else:
|
||||
formatted = f"{away_abbr} @ {home_abbr} (TBD)"
|
||||
timestamp = 9999999999
|
||||
|
||||
return {
|
||||
'id': event.get('idEvent'),
|
||||
'timestamp': timestamp,
|
||||
'event_timestamp': event_timestamp,
|
||||
'formatted': formatted,
|
||||
'sport': sport,
|
||||
'league': league,
|
||||
'status': status
|
||||
}
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error parsing TheSportsDB event {event.get('idEvent')}: {e}")
|
||||
return None
|
||||
async def get_league_teams(self, league_id: str) -> List[Dict]:
|
||||
"""Get all teams in a league"""
|
||||
await self._rate_limit()
|
||||
url = f"{self.BASE_URL}/{self.FREE_API_KEY}/lookup_all_teams.php"
|
||||
params = {'id': league_id}
|
||||
try:
|
||||
session = await self._get_session()
|
||||
async with session.get(url, params=params) as response:
|
||||
response.raise_for_status()
|
||||
data = await response.json()
|
||||
teams = data.get('teams', [])
|
||||
return teams if teams else []
|
||||
except Exception as e:
|
||||
self.logger.error(f"TheSportsDB get_league_teams error: {e}")
|
||||
return []
|
||||
|
||||
async def get_league_events_next(self, league_id: str, limit: int = 10) -> List[Dict]:
|
||||
"""Get next N events for a league"""
|
||||
await self._rate_limit()
|
||||
url = f"{self.BASE_URL}/{self.FREE_API_KEY}/eventsnextleague.php"
|
||||
params = {'id': league_id}
|
||||
try:
|
||||
session = await self._get_session()
|
||||
async with session.get(url, params=params) as response:
|
||||
response.raise_for_status()
|
||||
data = await response.json()
|
||||
events = data.get('events', [])
|
||||
return events[:limit] if events else []
|
||||
except Exception as e:
|
||||
self.logger.error(f"TheSportsDB get_league_events_next error: {e}")
|
||||
return []
|
||||
|
||||
async def get_league_events_past(self, league_id: str, limit: int = 10) -> List[Dict]:
|
||||
"""Get past N events for a league"""
|
||||
await self._rate_limit()
|
||||
url = f"{self.BASE_URL}/{self.FREE_API_KEY}/eventspastleague.php"
|
||||
params = {'id': league_id}
|
||||
try:
|
||||
session = await self._get_session()
|
||||
async with session.get(url, params=params) as response:
|
||||
response.raise_for_status()
|
||||
data = await response.json()
|
||||
events = data.get('results', [])
|
||||
return events[:limit] if events else []
|
||||
except Exception as e:
|
||||
self.logger.error(f"TheSportsDB get_league_events_past error: {e}")
|
||||
return []
|
||||
|
||||
async def get_events_by_day(self, date_str: str, league_id: str = None) -> List[Dict]:
|
||||
"""Get events for a specific day"""
|
||||
await self._rate_limit()
|
||||
url = f"{self.BASE_URL}/{self.FREE_API_KEY}/eventsday.php"
|
||||
params = {'d': date_str}
|
||||
if league_id:
|
||||
params['l'] = league_id
|
||||
try:
|
||||
session = await self._get_session()
|
||||
async with session.get(url, params=params) as response:
|
||||
response.raise_for_status()
|
||||
data = await response.json()
|
||||
events = data.get('events', [])
|
||||
if events is None: return []
|
||||
return events if isinstance(events, list) else []
|
||||
except Exception as e:
|
||||
self.logger.error(f"TheSportsDB get_events_by_day error: {e}")
|
||||
return []
|
||||
|
||||
async def fetch_league_scores(self, sport: str, league_name: str, league_id: str) -> List[Dict]:
|
||||
"""Fetch and parse league scores from multiple sources"""
|
||||
from datetime import timedelta
|
||||
|
||||
# Get today's date and next few days
|
||||
today = datetime.now().date()
|
||||
date_strings = [today.strftime('%Y-%m-%d')]
|
||||
for i in range(1, 7): # Next 6 days
|
||||
date_strings.append((today + timedelta(days=i)).strftime('%Y-%m-%d'))
|
||||
|
||||
# Fetch events from multiple sources in parallel
|
||||
next_events_task = self.get_league_events_next(league_id, limit=15)
|
||||
past_events_task = self.get_league_events_past(league_id, limit=5)
|
||||
day_events_tasks = [self.get_events_by_day(d, league_id) for d in date_strings]
|
||||
|
||||
# Wait for all requests
|
||||
results = await asyncio.gather(next_events_task, past_events_task, *day_events_tasks)
|
||||
next_events = results[0]
|
||||
past_events = results[1]
|
||||
day_events_list = results[2:]
|
||||
|
||||
# Combine and parse
|
||||
all_event_data = []
|
||||
seen_event_ids = set()
|
||||
|
||||
# Helper to add parsed events
|
||||
def add_parsed(events):
|
||||
for event in events:
|
||||
event_id = event.get('idEvent')
|
||||
if event_id and event_id not in seen_event_ids:
|
||||
parsed = self.parse_event(event, "", sport, league_name)
|
||||
if parsed:
|
||||
all_event_data.append(parsed)
|
||||
seen_event_ids.add(event_id)
|
||||
|
||||
add_parsed(next_events)
|
||||
add_parsed(past_events)
|
||||
for day_events in day_events_list:
|
||||
add_parsed(day_events)
|
||||
|
||||
return all_event_data
|
||||
@@ -5,6 +5,7 @@ Handles the 'advert' command for sending flood adverts
|
||||
"""
|
||||
|
||||
import time
|
||||
from typing import Any
|
||||
from .base_command import BaseCommand
|
||||
from ..models import MeshMessage
|
||||
|
||||
@@ -25,6 +26,14 @@ class AdvertCommand(BaseCommand):
|
||||
cooldown_seconds = 3600 # 1 hour
|
||||
category = "special"
|
||||
|
||||
def __init__(self, bot: Any):
|
||||
"""Initialize the advert command.
|
||||
|
||||
Args:
|
||||
bot: The bot instance.
|
||||
"""
|
||||
super().__init__(bot)
|
||||
|
||||
def get_help_text(self) -> str:
|
||||
"""Get help text for the advert command.
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ import sqlite3
|
||||
import time
|
||||
import asyncio
|
||||
from datetime import datetime, timedelta
|
||||
from typing import Optional, Dict, Any, List
|
||||
from typing import Optional, Dict, Any, List, Tuple
|
||||
from .base_command import BaseCommand
|
||||
from ..models import MeshMessage
|
||||
|
||||
@@ -22,7 +22,12 @@ class GreeterCommand(BaseCommand):
|
||||
description = "Greets users on their first public channel message (once globally by default, or per-channel if configured)"
|
||||
category = "system"
|
||||
|
||||
def __init__(self, bot):
|
||||
def __init__(self, bot: Any):
|
||||
"""Initialize the greeter command.
|
||||
|
||||
Args:
|
||||
bot: The bot instance.
|
||||
"""
|
||||
super().__init__(bot)
|
||||
self._init_greeter_tables()
|
||||
self._load_config()
|
||||
@@ -114,8 +119,8 @@ class GreeterCommand(BaseCommand):
|
||||
import traceback
|
||||
self.logger.error(traceback.format_exc())
|
||||
|
||||
def _load_config(self):
|
||||
"""Load configuration for greeter command"""
|
||||
def _load_config(self) -> None:
|
||||
"""Load configuration for greeter command."""
|
||||
self.enabled = self.get_config_value('Greeter_Command', 'enabled', fallback=False, value_type='bool')
|
||||
self.greeting_message = self.get_config_value('Greeter_Command', 'greeting_message',
|
||||
fallback='Welcome to the mesh, {sender}!')
|
||||
@@ -179,8 +184,8 @@ class GreeterCommand(BaseCommand):
|
||||
self.levenshtein_distance = self.get_config_value('Greeter_Command', 'levenshtein_distance',
|
||||
fallback=0, value_type='int')
|
||||
|
||||
def _init_greeter_tables(self):
|
||||
"""Initialize database tables for greeter tracking"""
|
||||
def _init_greeter_tables(self) -> None:
|
||||
"""Initialize database tables for greeter tracking."""
|
||||
try:
|
||||
with self.bot.db_manager.get_connection() as conn:
|
||||
cursor = conn.cursor()
|
||||
@@ -226,8 +231,8 @@ class GreeterCommand(BaseCommand):
|
||||
self.logger.error(f"Failed to initialize greeter tables: {e}")
|
||||
raise
|
||||
|
||||
def _check_rollout_period(self):
|
||||
"""Check if we're in a rollout period and mark active users if needed"""
|
||||
def _check_rollout_period(self) -> None:
|
||||
"""Check if we're in a rollout period and mark active users if needed."""
|
||||
if not self.enabled:
|
||||
return
|
||||
|
||||
@@ -281,8 +286,12 @@ class GreeterCommand(BaseCommand):
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error checking rollout period: {e}")
|
||||
|
||||
def _mark_active_users_as_greeted(self, rollout_id: int):
|
||||
"""Mark all users who have posted on public channels during rollout period as greeted"""
|
||||
def _mark_active_users_as_greeted(self, rollout_id: int) -> None:
|
||||
"""Mark all users who have posted on public channels during rollout period as greeted.
|
||||
|
||||
Args:
|
||||
rollout_id: The ID of the active rollout.
|
||||
"""
|
||||
try:
|
||||
with self.bot.db_manager.get_connection() as conn:
|
||||
cursor = conn.cursor()
|
||||
@@ -355,17 +364,16 @@ class GreeterCommand(BaseCommand):
|
||||
self.logger.error(f"Error marking active users as greeted: {e}")
|
||||
|
||||
def backfill_greeted_users(self, lookback_days: Optional[int] = None) -> Dict[str, Any]:
|
||||
"""
|
||||
Backfill greeted_users table from historical message_stats data
|
||||
"""Backfill greeted_users table from historical message_stats data.
|
||||
|
||||
This allows marking all users who have posted on public channels in the past,
|
||||
which can shorten or eliminate the rollout period.
|
||||
|
||||
Args:
|
||||
lookback_days: Number of days to look back (None = all time)
|
||||
lookback_days: Number of days to look back (None = all time).
|
||||
|
||||
Returns:
|
||||
Dictionary with backfill results (marked_count, total_users, etc.)
|
||||
Dict[str, Any]: Dictionary with backfill results (marked_count, total_users, etc.)
|
||||
"""
|
||||
if not self.enabled:
|
||||
self.logger.warning("Greeter is disabled - cannot backfill")
|
||||
@@ -464,15 +472,14 @@ class GreeterCommand(BaseCommand):
|
||||
}
|
||||
|
||||
def start_rollout(self, days: Optional[int] = None, backfill_first: bool = True) -> bool:
|
||||
"""
|
||||
Start a rollout period where all active users are marked as greeted
|
||||
"""Start a rollout period where all active users are marked as greeted.
|
||||
|
||||
Args:
|
||||
days: Number of days for rollout period (uses config default if None)
|
||||
backfill_first: If True, backfill from historical data before starting rollout
|
||||
days: Number of days for rollout period (uses config default if None).
|
||||
backfill_first: If True, backfill from historical data before starting rollout.
|
||||
|
||||
Returns:
|
||||
True if rollout started successfully
|
||||
bool: True if rollout started successfully.
|
||||
"""
|
||||
if not self.enabled:
|
||||
self.logger.warning("Greeter is disabled - cannot start rollout")
|
||||
@@ -521,15 +528,14 @@ class GreeterCommand(BaseCommand):
|
||||
return False
|
||||
|
||||
def _levenshtein_distance(self, s1: str, s2: str) -> int:
|
||||
"""
|
||||
Calculate Levenshtein distance between two strings
|
||||
"""Calculate Levenshtein distance between two strings.
|
||||
|
||||
Args:
|
||||
s1: First string
|
||||
s2: Second string
|
||||
s1: First string.
|
||||
s2: Second string.
|
||||
|
||||
Returns:
|
||||
Levenshtein distance (number of edits needed)
|
||||
int: Levenshtein distance (number of edits needed).
|
||||
"""
|
||||
if len(s1) < len(s2):
|
||||
return self._levenshtein_distance(s2, s1)
|
||||
@@ -550,15 +556,14 @@ class GreeterCommand(BaseCommand):
|
||||
return previous_row[-1]
|
||||
|
||||
def _find_similar_greeted_user(self, sender_id: str, channel: str) -> Optional[str]:
|
||||
"""
|
||||
Find if a user with a similar name (within Levenshtein distance) has been greeted
|
||||
"""Find if a user with a similar name has been greeted.
|
||||
|
||||
Args:
|
||||
sender_id: The user's ID to check
|
||||
channel: The channel name (used only if per_channel_greetings is True)
|
||||
sender_id: The user's ID to check.
|
||||
channel: The channel name (used only if per_channel_greetings is True).
|
||||
|
||||
Returns:
|
||||
The greeted sender_id if a similar one is found, None otherwise
|
||||
Optional[str]: The greeted sender_id if a similar one is found, None otherwise.
|
||||
"""
|
||||
if self.levenshtein_distance <= 0:
|
||||
return None
|
||||
@@ -595,15 +600,14 @@ class GreeterCommand(BaseCommand):
|
||||
return None
|
||||
|
||||
def has_been_greeted(self, sender_id: str, channel: str) -> bool:
|
||||
"""
|
||||
Check if a user has been greeted (with optional Levenshtein distance matching)
|
||||
"""Check if a user has been greeted.
|
||||
|
||||
Args:
|
||||
sender_id: The user's ID
|
||||
channel: The channel name (used only if per_channel_greetings is True)
|
||||
sender_id: The user's ID.
|
||||
channel: The channel name (used only if per_channel_greetings is True).
|
||||
|
||||
Returns:
|
||||
True if user has been greeted (globally or on this channel, depending on config)
|
||||
bool: True if user has been greeted (globally or on this channel), False otherwise.
|
||||
"""
|
||||
try:
|
||||
with self.bot.db_manager.get_connection() as conn:
|
||||
@@ -638,19 +642,16 @@ class GreeterCommand(BaseCommand):
|
||||
return False
|
||||
|
||||
def mark_as_greeted(self, sender_id: str, channel: str) -> bool:
|
||||
"""
|
||||
Mark a user as greeted atomically.
|
||||
"""Mark a user as greeted atomically.
|
||||
|
||||
Uses INSERT OR IGNORE with UNIQUE constraint to handle race conditions.
|
||||
Returns True if user was successfully marked (or already marked).
|
||||
Returns False only on actual errors (not on duplicate attempts).
|
||||
|
||||
Args:
|
||||
sender_id: The user's ID
|
||||
channel: The channel name (stored only if per_channel_greetings is True)
|
||||
sender_id: The user's ID.
|
||||
channel: The channel name (stored only if per_channel_greetings is True).
|
||||
|
||||
Returns:
|
||||
True if user was marked (or already marked), False on error
|
||||
bool: True if user was marked (or already marked), False on error.
|
||||
"""
|
||||
try:
|
||||
self.logger.debug(f"Marking {sender_id} as greeted (channel: {channel})")
|
||||
@@ -780,7 +781,11 @@ class GreeterCommand(BaseCommand):
|
||||
return False
|
||||
|
||||
def get_greeted_users_count(self) -> int:
|
||||
"""Get count of users who have been greeted (for verification)"""
|
||||
"""Get count of users who have been greeted.
|
||||
|
||||
Returns:
|
||||
int: The total count of greeted users.
|
||||
"""
|
||||
try:
|
||||
with self.bot.db_manager.get_connection() as conn:
|
||||
cursor = conn.cursor()
|
||||
@@ -791,8 +796,8 @@ class GreeterCommand(BaseCommand):
|
||||
self.logger.error(f"Error getting greeted users count: {e}")
|
||||
return 0
|
||||
|
||||
def _cleanup_duplicate_greetings(self):
|
||||
"""Remove duplicate entries from greeted_users table, keeping the earliest (first) greeting"""
|
||||
def _cleanup_duplicate_greetings(self) -> None:
|
||||
"""Remove duplicate entries from greeted_users table."""
|
||||
try:
|
||||
with self.bot.db_manager.get_connection() as conn:
|
||||
cursor = conn.cursor()
|
||||
@@ -852,7 +857,14 @@ class GreeterCommand(BaseCommand):
|
||||
# Don't raise - allow initialization to continue even if cleanup fails
|
||||
|
||||
def get_recent_greeted_users(self, limit: int = 10) -> List[Dict[str, Any]]:
|
||||
"""Get recent greeted users (for verification) - ordered by first greeting time"""
|
||||
"""Get recent greeted users.
|
||||
|
||||
Args:
|
||||
limit: Maximum number of users to return.
|
||||
|
||||
Returns:
|
||||
List[Dict[str, Any]]: A list of dictionaries containing greeted user info.
|
||||
"""
|
||||
try:
|
||||
with self.bot.db_manager.get_connection() as conn:
|
||||
cursor = conn.cursor()
|
||||
@@ -871,7 +883,11 @@ class GreeterCommand(BaseCommand):
|
||||
return []
|
||||
|
||||
async def _get_mesh_info(self) -> Dict[str, Any]:
|
||||
"""Get mesh network information for greeting"""
|
||||
"""Get mesh network information for greeting.
|
||||
|
||||
Returns:
|
||||
Dict[str, Any]: A dictionary containing mesh statistics.
|
||||
"""
|
||||
info = {
|
||||
'total_contacts': 0,
|
||||
'repeaters': 0,
|
||||
@@ -933,30 +949,28 @@ class GreeterCommand(BaseCommand):
|
||||
return info
|
||||
|
||||
def _get_greeting_for_channel(self, channel: str) -> str:
|
||||
"""
|
||||
Get greeting message for a specific channel
|
||||
"""Get greeting message for a specific channel.
|
||||
|
||||
Args:
|
||||
channel: Channel name
|
||||
channel: Channel name.
|
||||
|
||||
Returns:
|
||||
Greeting message template for the channel, or default if not specified
|
||||
str: Greeting message template for the channel, or default if not specified.
|
||||
"""
|
||||
if channel and channel.lower() in self.channel_greetings:
|
||||
return self.channel_greetings[channel.lower()]['greeting']
|
||||
return self.greeting_message
|
||||
|
||||
async def _format_greeting_parts(self, sender_id: str, channel: str = None, mesh_info: Optional[Dict[str, Any]] = None) -> list:
|
||||
"""
|
||||
Format greeting message parts with mesh information
|
||||
async def _format_greeting_parts(self, sender_id: str, channel: Optional[str] = None, mesh_info: Optional[Dict[str, Any]] = None) -> List[str]:
|
||||
"""Format greeting message parts with mesh information.
|
||||
|
||||
Args:
|
||||
sender_id: The user's ID
|
||||
channel: Channel name (for channel-specific greetings)
|
||||
mesh_info: Optional mesh info dict (will be fetched if None)
|
||||
sender_id: The user's ID.
|
||||
channel: Channel name (for channel-specific greetings).
|
||||
mesh_info: Optional mesh info dict (will be fetched if None).
|
||||
|
||||
Returns:
|
||||
List of greeting message strings (for multi-part greetings)
|
||||
List[str]: List of greeting message strings (for multi-part greetings).
|
||||
"""
|
||||
if mesh_info is None:
|
||||
mesh_info = await self._get_mesh_info()
|
||||
@@ -993,15 +1007,33 @@ class GreeterCommand(BaseCommand):
|
||||
return formatted_parts
|
||||
|
||||
def matches_keyword(self, message: MeshMessage) -> bool:
|
||||
"""Greeter doesn't match keywords - it's triggered automatically"""
|
||||
"""Greeter doesn't match keywords - it's triggered automatically.
|
||||
|
||||
Args:
|
||||
message: The message to check.
|
||||
|
||||
Returns:
|
||||
bool: Always False.
|
||||
"""
|
||||
return False
|
||||
|
||||
def matches_custom_syntax(self, message: MeshMessage) -> bool:
|
||||
"""Greeter doesn't match custom syntax"""
|
||||
"""Greeter doesn't match custom syntax.
|
||||
|
||||
Args:
|
||||
message: The message to check.
|
||||
|
||||
Returns:
|
||||
bool: Always False.
|
||||
"""
|
||||
return False
|
||||
|
||||
def _is_rollout_active(self) -> bool:
|
||||
"""Check if there's an active rollout period"""
|
||||
"""Check if there's an active rollout period.
|
||||
|
||||
Returns:
|
||||
bool: True if a rollout is active, False otherwise.
|
||||
"""
|
||||
try:
|
||||
with self.bot.db_manager.get_connection() as conn:
|
||||
cursor = conn.cursor()
|
||||
@@ -1051,16 +1083,15 @@ class GreeterCommand(BaseCommand):
|
||||
return False
|
||||
|
||||
def _check_human_greeting(self, new_user_id: str, channel: str, since_timestamp: int) -> bool:
|
||||
"""
|
||||
Check if a human has greeted the new user by mentioning their name in a message
|
||||
"""Check if a human has greeted the new user.
|
||||
|
||||
Args:
|
||||
new_user_id: The new user's ID to check for
|
||||
channel: The channel to check
|
||||
since_timestamp: Only check messages after this timestamp
|
||||
new_user_id: The new user's ID to check for.
|
||||
channel: The channel to check.
|
||||
since_timestamp: Only check messages after this timestamp.
|
||||
|
||||
Returns:
|
||||
True if a human (not the new user) has mentioned the new user's name
|
||||
bool: True if a human has mentioned the new user, False otherwise.
|
||||
"""
|
||||
if not self.defer_to_human_greeting:
|
||||
return False
|
||||
@@ -1115,8 +1146,13 @@ class GreeterCommand(BaseCommand):
|
||||
self.logger.error(f"Error checking for human greeting: {e}")
|
||||
return False
|
||||
|
||||
def _cancel_pending_greeting(self, sender_id: str, channel: str):
|
||||
"""Cancel a pending greeting if it exists"""
|
||||
def _cancel_pending_greeting(self, sender_id: str, channel: str) -> None:
|
||||
"""Cancel a pending greeting if it exists.
|
||||
|
||||
Args:
|
||||
sender_id: The user's ID.
|
||||
channel: The channel name.
|
||||
"""
|
||||
key = (sender_id, channel)
|
||||
if key in self.pending_greetings:
|
||||
task = self.pending_greetings[key]
|
||||
@@ -1125,12 +1161,11 @@ class GreeterCommand(BaseCommand):
|
||||
self.logger.info(f"Cancelled pending greeting for {sender_id} on {channel}")
|
||||
del self.pending_greetings[key]
|
||||
|
||||
async def _send_delayed_greeting(self, message: MeshMessage):
|
||||
"""
|
||||
Send a greeting after the dead air delay, checking for human greetings during the delay
|
||||
async def _send_delayed_greeting(self, message: MeshMessage) -> None:
|
||||
"""Send a greeting after the dead air delay.
|
||||
|
||||
Args:
|
||||
message: The original message that triggered the greeting
|
||||
message: The original message that triggered the greeting.
|
||||
"""
|
||||
key = (message.sender_id, message.channel)
|
||||
original_timestamp = message.timestamp or int(time.time())
|
||||
@@ -1182,14 +1217,13 @@ class GreeterCommand(BaseCommand):
|
||||
del self.pending_greetings[key]
|
||||
|
||||
async def _send_greeting(self, message: MeshMessage) -> bool:
|
||||
"""
|
||||
Actually send the greeting message (extracted from execute for reuse)
|
||||
"""Actually send the greeting message.
|
||||
|
||||
Args:
|
||||
message: The message that triggered the greeting
|
||||
message: The message that triggered the greeting.
|
||||
|
||||
Returns:
|
||||
True if greeting was sent successfully
|
||||
bool: True if greeting was sent successfully.
|
||||
"""
|
||||
try:
|
||||
# Format greeting parts (may be single or multi-part)
|
||||
@@ -1227,9 +1261,13 @@ class GreeterCommand(BaseCommand):
|
||||
return False
|
||||
|
||||
def should_execute(self, message: MeshMessage) -> bool:
|
||||
"""
|
||||
Check if greeter should execute for this message
|
||||
Only executes for public channel messages (not DMs) on monitored channels
|
||||
"""Check if greeter should execute for this message.
|
||||
|
||||
Args:
|
||||
message: The message to check.
|
||||
|
||||
Returns:
|
||||
bool: True if the greeter should execute, False otherwise.
|
||||
"""
|
||||
if not self.enabled:
|
||||
return False
|
||||
@@ -1277,7 +1315,14 @@ class GreeterCommand(BaseCommand):
|
||||
return True
|
||||
|
||||
async def execute(self, message: MeshMessage) -> bool:
|
||||
"""Execute the greeter command - greet the user on their first public message"""
|
||||
"""Execute the greeter command.
|
||||
|
||||
Args:
|
||||
message: The message triggering the greeting.
|
||||
|
||||
Returns:
|
||||
bool: True if executed successfully, False otherwise.
|
||||
"""
|
||||
try:
|
||||
# Double-check we should greet (race condition protection)
|
||||
if not self.should_execute(message):
|
||||
@@ -1353,13 +1398,11 @@ class GreeterCommand(BaseCommand):
|
||||
self.logger.error(f"Error executing greeter command: {e}")
|
||||
return False
|
||||
|
||||
def check_message_for_human_greeting(self, message: MeshMessage):
|
||||
"""
|
||||
Check if an incoming message should cancel a pending greeting
|
||||
Called from message handler when new messages arrive
|
||||
def check_message_for_human_greeting(self, message: MeshMessage) -> None:
|
||||
"""Check if an incoming message should cancel a pending greeting.
|
||||
|
||||
Args:
|
||||
message: The incoming message to check
|
||||
message: The incoming message to check.
|
||||
"""
|
||||
if not self.defer_to_human_greeting or not self.dead_air_delay_seconds > 0:
|
||||
return
|
||||
@@ -1397,6 +1440,11 @@ class GreeterCommand(BaseCommand):
|
||||
self.mark_as_greeted(key[0], key[1])
|
||||
|
||||
def get_help_text(self) -> str:
|
||||
"""Get help text for the greeter command.
|
||||
|
||||
Returns:
|
||||
str: The help text for this command.
|
||||
"""
|
||||
mode = "per-channel" if self.per_channel_greetings else "global (once total)"
|
||||
return f"Greeter automatically welcomes new users on public channels ({mode} mode). Configure in [Greeter_Command] section."
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ Responds to Linux commands with hilarious supervillain mainframe error messages
|
||||
"""
|
||||
|
||||
import random
|
||||
from typing import Any
|
||||
from .base_command import BaseCommand
|
||||
from ..models import MeshMessage
|
||||
|
||||
@@ -21,15 +22,32 @@ class HackerCommand(BaseCommand):
|
||||
description = "Simulates hacking a supervillain's mainframe with hilarious error messages"
|
||||
category = "fun"
|
||||
|
||||
def __init__(self, bot):
|
||||
def __init__(self, bot: Any):
|
||||
"""Initialize the hacker command.
|
||||
|
||||
Args:
|
||||
bot: The bot instance.
|
||||
"""
|
||||
super().__init__(bot)
|
||||
self.enabled = self.get_config_value('Hacker_Command', 'hacker_enabled', fallback=False, value_type='bool')
|
||||
|
||||
def get_help_text(self) -> str:
|
||||
"""Get help text for the hacker command.
|
||||
|
||||
Returns:
|
||||
str: The help text for this command.
|
||||
"""
|
||||
return self.description
|
||||
|
||||
async def execute(self, message: MeshMessage) -> bool:
|
||||
"""Execute the hacker command"""
|
||||
"""Execute the hacker command.
|
||||
|
||||
Args:
|
||||
message: The message triggering the command.
|
||||
|
||||
Returns:
|
||||
bool: True if executed successfully, False otherwise.
|
||||
"""
|
||||
if not self.enabled:
|
||||
return False
|
||||
|
||||
@@ -45,7 +63,14 @@ class HackerCommand(BaseCommand):
|
||||
return await self.send_response(message, error_msg)
|
||||
|
||||
def get_hacker_error(self, command: str) -> str:
|
||||
"""Get a hilarious error message for the given command"""
|
||||
"""Get a hilarious error message for the given command.
|
||||
|
||||
Args:
|
||||
command: The command that triggered the error.
|
||||
|
||||
Returns:
|
||||
str: A randomized hacker-themed error message.
|
||||
"""
|
||||
command_lower = command.lower()
|
||||
|
||||
# Try to get errors from translations, fallback to hardcoded if not available
|
||||
@@ -440,7 +465,14 @@ class HackerCommand(BaseCommand):
|
||||
return get_random_error('commands.hacker.generic_errors', fallback)
|
||||
|
||||
def matches_keyword(self, message: MeshMessage) -> bool:
|
||||
"""Override to check for command matches (exact for some, prefix for others)"""
|
||||
"""Check if message matches any of the hacker keywords.
|
||||
|
||||
Args:
|
||||
message: The received message.
|
||||
|
||||
Returns:
|
||||
bool: True if it matches, False otherwise.
|
||||
"""
|
||||
if not self.enabled:
|
||||
return False
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ Responds to various greetings with robot-themed responses
|
||||
"""
|
||||
|
||||
import random
|
||||
from typing import Any, List, Dict
|
||||
from .base_command import BaseCommand
|
||||
from ..models import MeshMessage
|
||||
|
||||
@@ -18,14 +19,19 @@ class HelloCommand(BaseCommand):
|
||||
description = "Responds to greetings with robot-themed responses"
|
||||
category = "basic"
|
||||
|
||||
def __init__(self, bot):
|
||||
def __init__(self, bot: Any):
|
||||
"""Initialize the hello command.
|
||||
|
||||
Args:
|
||||
bot: The bot instance.
|
||||
"""
|
||||
super().__init__(bot)
|
||||
|
||||
# Fallback arrays if translations not available
|
||||
self._init_fallback_arrays()
|
||||
|
||||
def _init_fallback_arrays(self):
|
||||
"""Initialize fallback arrays for when translations are not available"""
|
||||
def _init_fallback_arrays(self) -> None:
|
||||
"""Initialize fallback arrays for when translations are not available."""
|
||||
# Time-neutral greeting openings
|
||||
self.greeting_openings_fallback = [
|
||||
"Hello", "Greetings", "Salutations", "Hi", "Hey", "Howdy", "Yo", "Sup",
|
||||
@@ -171,57 +177,100 @@ class HelloCommand(BaseCommand):
|
||||
]
|
||||
}
|
||||
|
||||
def get_greeting_openings(self) -> list:
|
||||
"""Get greeting openings from translations or fallback"""
|
||||
def get_greeting_openings(self) -> List[str]:
|
||||
"""Get greeting openings from translations or fallback.
|
||||
|
||||
Returns:
|
||||
List[str]: A list of greeting opening strings.
|
||||
"""
|
||||
openings = self.translate_get_value('commands.hello.greeting_openings')
|
||||
if openings and isinstance(openings, list) and len(openings) > 0:
|
||||
return openings
|
||||
return self.greeting_openings_fallback
|
||||
|
||||
def get_morning_greetings(self) -> list:
|
||||
"""Get morning greetings from translations or fallback"""
|
||||
def get_morning_greetings(self) -> List[str]:
|
||||
"""Get morning greetings from translations or fallback.
|
||||
|
||||
Returns:
|
||||
List[str]: A list of morning greeting strings.
|
||||
"""
|
||||
greetings = self.translate_get_value('commands.hello.morning_greetings')
|
||||
if greetings and isinstance(greetings, list) and len(greetings) > 0:
|
||||
return greetings
|
||||
return self.morning_greetings_fallback
|
||||
|
||||
def get_afternoon_greetings(self) -> list:
|
||||
"""Get afternoon greetings from translations or fallback"""
|
||||
def get_afternoon_greetings(self) -> List[str]:
|
||||
"""Get afternoon greetings from translations or fallback.
|
||||
|
||||
Returns:
|
||||
List[str]: A list of afternoon greeting strings.
|
||||
"""
|
||||
greetings = self.translate_get_value('commands.hello.afternoon_greetings')
|
||||
if greetings and isinstance(greetings, list) and len(greetings) > 0:
|
||||
return greetings
|
||||
return self.afternoon_greetings_fallback
|
||||
|
||||
def get_evening_greetings(self) -> list:
|
||||
"""Get evening greetings from translations or fallback"""
|
||||
def get_evening_greetings(self) -> List[str]:
|
||||
"""Get evening greetings from translations or fallback.
|
||||
|
||||
Returns:
|
||||
List[str]: A list of evening greeting strings.
|
||||
"""
|
||||
greetings = self.translate_get_value('commands.hello.evening_greetings')
|
||||
if greetings and isinstance(greetings, list) and len(greetings) > 0:
|
||||
return greetings
|
||||
return self.evening_greetings_fallback
|
||||
|
||||
def get_human_descriptors(self) -> list:
|
||||
"""Get human descriptors from translations or fallback"""
|
||||
def get_human_descriptors(self) -> List[str]:
|
||||
"""Get human descriptors from translations or fallback.
|
||||
|
||||
Returns:
|
||||
List[str]: A list of human descriptor strings.
|
||||
"""
|
||||
descriptors = self.translate_get_value('commands.hello.human_descriptors')
|
||||
if descriptors and isinstance(descriptors, list) and len(descriptors) > 0:
|
||||
return descriptors
|
||||
return self.human_descriptors_fallback
|
||||
|
||||
def get_emoji_responses(self) -> dict:
|
||||
"""Get emoji responses from translations or fallback"""
|
||||
def get_emoji_responses(self) -> Dict[str, List[str]]:
|
||||
"""Get emoji responses from translations or fallback.
|
||||
|
||||
Returns:
|
||||
Dict[str, List[str]]: A dictionary mapping emojis to lists of response strings.
|
||||
"""
|
||||
responses = self.translate_get_value('commands.hello.emoji_responses')
|
||||
if responses and isinstance(responses, dict) and len(responses) > 0:
|
||||
return responses
|
||||
return self.emoji_responses_fallback
|
||||
|
||||
def get_help_text(self) -> str:
|
||||
"""Get help text for the hello command.
|
||||
|
||||
Returns:
|
||||
str: The help text for this command.
|
||||
"""
|
||||
return self.translate('commands.hello.help')
|
||||
|
||||
def matches_custom_syntax(self, message: MeshMessage) -> bool:
|
||||
"""Check if message contains only defined emojis"""
|
||||
"""Check if message contains only defined emojis.
|
||||
|
||||
Args:
|
||||
message: The message to check.
|
||||
|
||||
Returns:
|
||||
bool: True if it's an emoji-only message, False otherwise.
|
||||
"""
|
||||
return self.is_emoji_only_message(message.content)
|
||||
|
||||
async def execute(self, message: MeshMessage) -> bool:
|
||||
"""Execute the hello command"""
|
||||
"""Execute the hello command.
|
||||
|
||||
Args:
|
||||
message: The message triggering the command.
|
||||
|
||||
Returns:
|
||||
bool: True if executed successfully, False otherwise.
|
||||
"""
|
||||
# Get bot name from config
|
||||
bot_name = self.bot.config.get('Bot', 'bot_name', fallback='Bot')
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ Provides help information for commands and general usage
|
||||
|
||||
import sqlite3
|
||||
from collections import defaultdict
|
||||
from typing import Any, Optional
|
||||
from .base_command import BaseCommand
|
||||
from ..models import MeshMessage
|
||||
|
||||
|
||||
@@ -20,6 +20,7 @@ class HfcondCommand(BaseCommand):
|
||||
keywords = ['hfcond']
|
||||
description = "Get HF band conditions for ham radio"
|
||||
category = "solar"
|
||||
requires_internet = True # Requires internet access for hamqsl.com API
|
||||
|
||||
def __init__(self, bot):
|
||||
"""Initialize the hfcond command.
|
||||
|
||||
@@ -7,7 +7,7 @@ Provides clean, family-friendly jokes from the JokeAPI
|
||||
import aiohttp
|
||||
import asyncio
|
||||
import logging
|
||||
from typing import Optional, Dict, Any
|
||||
from typing import Optional, Dict, Any, List
|
||||
from .base_command import BaseCommand
|
||||
from ..models import MeshMessage
|
||||
|
||||
@@ -40,7 +40,12 @@ class JokeCommand(BaseCommand):
|
||||
BLACKLIST_FLAGS = "nsfw,religious,political,racist,sexist,explicit"
|
||||
TIMEOUT = 10 # seconds
|
||||
|
||||
def __init__(self, bot):
|
||||
def __init__(self, bot: Any):
|
||||
"""Initialize the joke command.
|
||||
|
||||
Args:
|
||||
bot: The bot instance.
|
||||
"""
|
||||
super().__init__(bot)
|
||||
|
||||
# Load configuration
|
||||
@@ -61,7 +66,14 @@ class JokeCommand(BaseCommand):
|
||||
return f"Usage: joke [category] - Get a random joke or from categories: {categories}"
|
||||
|
||||
def matches_keyword(self, message: MeshMessage) -> bool:
|
||||
"""Check if message starts with a joke keyword"""
|
||||
"""Check if message starts with a joke keyword.
|
||||
|
||||
Args:
|
||||
message: The message to check.
|
||||
|
||||
Returns:
|
||||
bool: True if a joke keyword matches, False otherwise.
|
||||
"""
|
||||
content = message.content.strip()
|
||||
if content.startswith('!'):
|
||||
content = content[1:].strip()
|
||||
@@ -123,7 +135,14 @@ class JokeCommand(BaseCommand):
|
||||
return None
|
||||
|
||||
async def execute(self, message: MeshMessage) -> bool:
|
||||
"""Execute the joke command"""
|
||||
"""Execute the joke command.
|
||||
|
||||
Args:
|
||||
message: The message triggering the command.
|
||||
|
||||
Returns:
|
||||
bool: True if executed successfully, False otherwise.
|
||||
"""
|
||||
content = message.content.strip()
|
||||
|
||||
# Parse the command to extract category
|
||||
@@ -166,8 +185,15 @@ class JokeCommand(BaseCommand):
|
||||
await self.send_response(message, "Sorry, something went wrong getting a joke!")
|
||||
return True
|
||||
|
||||
async def get_joke_from_api(self, category: str = None) -> dict:
|
||||
"""Get a joke from the JokeAPI"""
|
||||
async def get_joke_from_api(self, category: Optional[str] = None) -> Optional[Dict[str, Any]]:
|
||||
"""Get a joke from the JokeAPI.
|
||||
|
||||
Args:
|
||||
category: The joke category to fetch.
|
||||
|
||||
Returns:
|
||||
Optional[Dict[str, Any]]: The joke data from the API, or None if it fails.
|
||||
"""
|
||||
try:
|
||||
# Build the API URL
|
||||
# For dark jokes, don't use safe-mode since users expect dark humor
|
||||
@@ -251,8 +277,13 @@ class JokeCommand(BaseCommand):
|
||||
self.logger.warning(f"Could not get short joke after {max_attempts} attempts")
|
||||
return joke_data
|
||||
|
||||
async def send_joke_with_length_handling(self, message: MeshMessage, joke_data: Dict[str, Any]):
|
||||
"""Send joke with length handling - split if necessary"""
|
||||
async def send_joke_with_length_handling(self, message: MeshMessage, joke_data: Dict[str, Any]) -> None:
|
||||
"""Send joke with length handling - split if necessary.
|
||||
|
||||
Args:
|
||||
message: The original message to respond to.
|
||||
joke_data: The joke data from the API.
|
||||
"""
|
||||
joke_text = self.format_joke(joke_data)
|
||||
|
||||
if len(joke_text) <= 130:
|
||||
@@ -272,8 +303,15 @@ class JokeCommand(BaseCommand):
|
||||
# Cannot be split properly, send as single message (user will see truncation)
|
||||
await self.send_response(message, joke_text)
|
||||
|
||||
def split_joke(self, joke_text: str) -> list:
|
||||
"""Split a long joke at a logical point"""
|
||||
def split_joke(self, joke_text: str) -> List[str]:
|
||||
"""Split a long joke at a logical point.
|
||||
|
||||
Args:
|
||||
joke_text: The full text of the joke.
|
||||
|
||||
Returns:
|
||||
List[str]: A list of joke parts.
|
||||
"""
|
||||
# Remove emoji for splitting
|
||||
clean_joke = joke_text[2:] if joke_text.startswith('🎭 ') else joke_text
|
||||
|
||||
@@ -307,8 +345,15 @@ class JokeCommand(BaseCommand):
|
||||
|
||||
return [f"🎭 {part1}", f"🎭 {part2}"]
|
||||
|
||||
def format_joke(self, joke_data: dict) -> str:
|
||||
"""Format the joke data into a readable string"""
|
||||
def format_joke(self, joke_data: Dict[str, Any]) -> str:
|
||||
"""Format the joke data into a readable string.
|
||||
|
||||
Args:
|
||||
joke_data: The joke data from the API.
|
||||
|
||||
Returns:
|
||||
str: The formatted joke string.
|
||||
"""
|
||||
try:
|
||||
joke_type = joke_data.get('type', 'single')
|
||||
|
||||
|
||||
@@ -27,7 +27,12 @@ class PrefixCommand(BaseCommand):
|
||||
cooldown_seconds = 2
|
||||
requires_internet = False # Will be set to True in __init__ if API is configured
|
||||
|
||||
def __init__(self, bot):
|
||||
def __init__(self, bot: Any):
|
||||
"""Initialize the prefix command.
|
||||
|
||||
Args:
|
||||
bot: The bot instance.
|
||||
"""
|
||||
super().__init__(bot)
|
||||
# Get API URL from config, no fallback to regional API
|
||||
self.api_url = self.bot.config.get('External_Data', 'repeater_prefix_api_url', fallback="")
|
||||
@@ -63,6 +68,11 @@ class PrefixCommand(BaseCommand):
|
||||
)
|
||||
|
||||
def get_help_text(self) -> str:
|
||||
"""Get help text for the prefix command.
|
||||
|
||||
Returns:
|
||||
str: The help text for this command.
|
||||
"""
|
||||
location_note = self.translate('commands.prefix.location_note') if self.show_repeater_locations else ""
|
||||
if not self.api_url or self.api_url.strip() == "":
|
||||
return self.translate('commands.prefix.help_no_api', location_note=location_note)
|
||||
@@ -81,7 +91,14 @@ class PrefixCommand(BaseCommand):
|
||||
return content_lower == 'prefix' or content_lower.startswith('prefix ')
|
||||
|
||||
async def execute(self, message: MeshMessage) -> bool:
|
||||
"""Execute the prefix command"""
|
||||
"""Execute the prefix command.
|
||||
|
||||
Args:
|
||||
message: The message triggering the command.
|
||||
|
||||
Returns:
|
||||
bool: True if executed successfully, False otherwise.
|
||||
"""
|
||||
content = message.content.strip()
|
||||
|
||||
# Handle exclamation prefix
|
||||
@@ -258,8 +275,8 @@ class PrefixCommand(BaseCommand):
|
||||
# Return original API data if enhancement fails
|
||||
return api_data
|
||||
|
||||
async def refresh_cache(self):
|
||||
"""Refresh the cache from the API"""
|
||||
async def refresh_cache(self) -> None:
|
||||
"""Refresh the cache from the API."""
|
||||
try:
|
||||
# Check if API URL is configured
|
||||
if not self.api_url or self.api_url.strip() == "":
|
||||
@@ -546,7 +563,15 @@ class PrefixCommand(BaseCommand):
|
||||
return [], 0, False
|
||||
|
||||
def format_free_prefixes_response(self, free_prefixes: List[str], total_free: int) -> str:
|
||||
"""Format the free prefixes response"""
|
||||
"""Format the free prefixes response.
|
||||
|
||||
Args:
|
||||
free_prefixes: List of free prefixes to display.
|
||||
total_free: Total count of free prefixes.
|
||||
|
||||
Returns:
|
||||
str: Formatted response string.
|
||||
"""
|
||||
if not free_prefixes:
|
||||
return self.translate('commands.prefix.no_free_prefixes')
|
||||
|
||||
@@ -569,7 +594,15 @@ class PrefixCommand(BaseCommand):
|
||||
return response
|
||||
|
||||
def format_prefix_response(self, prefix: str, data: Dict[str, Any]) -> str:
|
||||
"""Format the prefix response"""
|
||||
"""Format the prefix response.
|
||||
|
||||
Args:
|
||||
prefix: The prefix being queried.
|
||||
data: The prefix data dictionary.
|
||||
|
||||
Returns:
|
||||
str: Formatted response string.
|
||||
"""
|
||||
node_count = data['node_count']
|
||||
node_names = data['node_names']
|
||||
source = data.get('source', 'api')
|
||||
@@ -617,8 +650,13 @@ class PrefixCommand(BaseCommand):
|
||||
|
||||
return response
|
||||
|
||||
async def _send_prefix_response(self, message: MeshMessage, response: str):
|
||||
"""Send prefix response, splitting into multiple messages if necessary"""
|
||||
async def _send_prefix_response(self, message: MeshMessage, response: str) -> None:
|
||||
"""Send prefix response, splitting into multiple messages if necessary.
|
||||
|
||||
Args:
|
||||
message: The original message to respond to.
|
||||
response: The complete response string.
|
||||
"""
|
||||
# Store the complete response for web viewer integration BEFORE splitting
|
||||
# command_manager will prioritize command.last_response over _last_response
|
||||
# This ensures capture_command gets the full response, not just the last split message
|
||||
|
||||
@@ -24,6 +24,7 @@ class RepeaterCommand(BaseCommand):
|
||||
requires_dm = True
|
||||
cooldown_seconds = 0
|
||||
category = "management"
|
||||
requires_internet = True # Requires internet access for geocoding (Nominatim)
|
||||
|
||||
def __init__(self, bot):
|
||||
super().__init__(bot)
|
||||
|
||||
@@ -16,6 +16,7 @@ class SatpassCommand(BaseCommand):
|
||||
keywords = ['satpass']
|
||||
description = "Get satellite pass info: satpass <NORAD_number_or_shortcut> [visual]"
|
||||
category = "solar"
|
||||
requires_internet = True # Requires internet access for N2YO API
|
||||
|
||||
# Common satellite shortcuts
|
||||
SATELLITE_SHORTCUTS = {
|
||||
|
||||
@@ -18,8 +18,9 @@ class SolarCommand(BaseCommand):
|
||||
# Plugin metadata
|
||||
name = "solar"
|
||||
keywords = ['solar']
|
||||
description = "Get solar conditions and HF band status"
|
||||
description = "Get current solar conditions and HF band info"
|
||||
category = "solar"
|
||||
requires_internet = True # Requires internet access for hamqsl.com API
|
||||
|
||||
def __init__(self, bot):
|
||||
"""Initialize the solar command.
|
||||
|
||||
+119
-2119
File diff suppressed because it is too large
Load Diff
@@ -25,7 +25,12 @@ class StatsCommand(BaseCommand):
|
||||
description = "Show statistics for past 24 hours. Use 'stats messages', 'stats channels', or 'stats paths' for specific stats."
|
||||
category = "analytics"
|
||||
|
||||
def __init__(self, bot):
|
||||
def __init__(self, bot: Any):
|
||||
"""Initialize the stats command.
|
||||
|
||||
Args:
|
||||
bot: The bot instance.
|
||||
"""
|
||||
super().__init__(bot)
|
||||
self._load_config()
|
||||
self._init_stats_tables()
|
||||
@@ -295,6 +300,11 @@ class StatsCommand(BaseCommand):
|
||||
return path
|
||||
|
||||
def get_help_text(self) -> str:
|
||||
"""Get help text for the stats command.
|
||||
|
||||
Returns:
|
||||
str: The help text for this command.
|
||||
"""
|
||||
return self.translate('commands.stats.help')
|
||||
|
||||
async def execute(self, message: MeshMessage) -> bool:
|
||||
|
||||
Reference in New Issue
Block a user