mirror of
https://github.com/agessaman/meshcore-bot.git
synced 2026-10-22 05:52:10 +00:00
Enhance SportsCommand with improved game fetching logic
- Added mappings for Professional Women's Hockey League (PWHL) teams with placeholders for team IDs. - Updated city mappings to include PWHL teams for relevant cities. - Enhanced game fetching logic to prioritize live games, recent past games, and upcoming games within specified timeframes. - Introduced methods for fetching and formatting team schedules, improving user experience for schedule queries. - Improved handling of live event data to ensure real-time score updates for ongoing games.
This commit is contained in:
1 parent
4abd6fff69
commit
bdd3a0fd41
1 file changed
+396
-71
@@ -82,6 +82,18 @@ class SportsCommand(BaseCommand):
|
||||
'8': 'MIN-W', # Minnesota Lynx (Women's)
|
||||
'11': 'PHX-W', # Phoenix Mercury (Women's)
|
||||
'16': 'WSH-W', # Washington Mystics (Women's)
|
||||
# PWHL teams - use custom abbreviations to distinguish from NHL
|
||||
# NOTE: Team IDs need to be verified using test_scripts/find_espn_team_id.py hockey pwhl <team_name>
|
||||
# Once verified, uncomment and replace 'VERIFY_TEAM_ID' with the actual ESPN team ID
|
||||
# Format: 'ACTUAL_TEAM_ID': 'ABBREV-W', # Team Name (Women's)
|
||||
# Example: '123456': 'BOS-W', # Boston (Women's)
|
||||
# 'VERIFY_BOS': 'BOS-W', # Boston (Women's) - verify team_id
|
||||
# 'VERIFY_MIN': 'MIN-W', # Minnesota (Women's) - verify team_id
|
||||
# 'VERIFY_MTL': 'MTL-W', # Montreal (Women's) - verify team_id
|
||||
# 'VERIFY_NY': 'NY-W', # New York (Women's) - verify team_id
|
||||
# 'VERIFY_OTT': 'OTT-W', # Ottawa (Women's) - verify team_id
|
||||
# 'VERIFY_TOR': 'TOR-W', # Toronto (Women's) - verify team_id
|
||||
# 'VERIFY_SEA': 'SEA-W', # Seattle Torrent (Women's) - verify team_id
|
||||
}
|
||||
|
||||
# Team mappings for common searches
|
||||
@@ -118,6 +130,8 @@ class SportsCommand(BaseCommand):
|
||||
'ari': {'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'},
|
||||
@@ -143,6 +157,7 @@ class SportsCommand(BaseCommand):
|
||||
'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'},
|
||||
@@ -162,6 +177,8 @@ class SportsCommand(BaseCommand):
|
||||
'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'},
|
||||
@@ -309,11 +326,117 @@ class SportsCommand(BaseCommand):
|
||||
'mystics': {'sport': 'basketball', 'league': 'wnba', 'team_id': '16'},
|
||||
'washington mystics': {'sport': 'basketball', 'league': 'wnba', 'team_id': '16'},
|
||||
|
||||
# NHL Teams (limited data available from API)
|
||||
# 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'},
|
||||
'stars': {'sport': 'hockey', 'league': 'nhl', 'team_id': '9'},
|
||||
'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'},
|
||||
|
||||
# PWHL Teams (Professional Women's Hockey League)
|
||||
# NOTE: As of now, PWHL data may not be available in ESPN's API yet.
|
||||
# ESPN may not have added PWHL teams to their API endpoints.
|
||||
# Team IDs need to be verified using test_scripts/find_espn_team_id.py hockey pwhl <team_name>
|
||||
# Once ESPN adds PWHL support and team IDs are verified, uncomment and update the team_id values below
|
||||
# 'torrent': {'sport': 'hockey', 'league': 'pwhl', 'team_id': 'VERIFY_TEAM_ID'},
|
||||
# 'seattle torrent': {'sport': 'hockey', 'league': 'pwhl', 'team_id': 'VERIFY_TEAM_ID'},
|
||||
# 'boston pwhl': {'sport': 'hockey', 'league': 'pwhl', 'team_id': 'VERIFY_TEAM_ID'},
|
||||
# 'minnesota pwhl': {'sport': 'hockey', 'league': 'pwhl', 'team_id': 'VERIFY_TEAM_ID'},
|
||||
# 'montreal pwhl': {'sport': 'hockey', 'league': 'pwhl', 'team_id': 'VERIFY_TEAM_ID'},
|
||||
# 'new york pwhl': {'sport': 'hockey', 'league': 'pwhl', 'team_id': 'VERIFY_TEAM_ID'},
|
||||
# 'ottawa pwhl': {'sport': 'hockey', 'league': 'pwhl', 'team_id': 'VERIFY_TEAM_ID'},
|
||||
# 'toronto pwhl': {'sport': 'hockey', 'league': 'pwhl', 'team_id': 'VERIFY_TEAM_ID'},
|
||||
|
||||
# MLS Teams
|
||||
'sounders': {'sport': 'soccer', 'league': 'usa.1', 'team_id': '9726'},
|
||||
@@ -455,7 +578,8 @@ class SportsCommand(BaseCommand):
|
||||
"""Check if the league is a women's league"""
|
||||
womens_leagues = {
|
||||
('basketball', 'wnba'),
|
||||
('soccer', 'usa.nwsl')
|
||||
('soccer', 'usa.nwsl'),
|
||||
('hockey', 'pwhl')
|
||||
}
|
||||
return (sport, league) in womens_leagues
|
||||
|
||||
@@ -608,6 +732,7 @@ class SportsCommand(BaseCommand):
|
||||
# Extract team name if provided
|
||||
parts = content.split()
|
||||
if len(parts) > 1:
|
||||
# Join all parts after 'sports' keyword, preserving "schedule" if present
|
||||
team_name = ' '.join(parts[1:]).lower()
|
||||
response = await self.get_team_scores(team_name)
|
||||
else:
|
||||
@@ -635,9 +760,10 @@ class SportsCommand(BaseCommand):
|
||||
try:
|
||||
team_info = self.TEAM_MAPPINGS.get(team)
|
||||
if team_info:
|
||||
game_info = await self.fetch_team_game_data(team_info)
|
||||
if game_info:
|
||||
game_data.append(game_info)
|
||||
# Get all relevant games for this team (live, past within 8 days, upcoming within 6 weeks)
|
||||
games = await self.fetch_team_games(team_info)
|
||||
if games:
|
||||
game_data.extend(games)
|
||||
except Exception as e:
|
||||
self.logger.warning(f"Error fetching score for {team}: {e}")
|
||||
|
||||
@@ -689,6 +815,10 @@ class SportsCommand(BaseCommand):
|
||||
'nhl': {'sport': 'hockey', 'league': 'nhl'},
|
||||
'hockey': {'sport': 'hockey', 'league': 'nhl'},
|
||||
|
||||
# PWHL
|
||||
'pwhl': {'sport': 'hockey', 'league': 'pwhl'},
|
||||
'womens hockey': {'sport': 'hockey', 'league': 'pwhl'},
|
||||
|
||||
# MLS
|
||||
'mls': {'sport': 'soccer', 'league': 'usa.1'},
|
||||
'soccer': {'sport': 'soccer', 'league': 'usa.1'},
|
||||
@@ -712,14 +842,14 @@ class SportsCommand(BaseCommand):
|
||||
|
||||
# Define city mappings to team names
|
||||
city_mappings = {
|
||||
'seattle': ['seahawks', 'mariners', 'sounders', 'kraken', 'reign', 'storm'],
|
||||
'chicago': ['bears', 'cubs', 'white sox', 'fire', 'sky'],
|
||||
'new york': ['giants', 'jets', 'yankees', 'mets', 'knicks', 'nyc fc', 'red bulls', 'liberty'],
|
||||
'ny': ['giants', 'jets', 'yankees', 'mets', 'knicks', 'nyc fc', 'red bulls', 'liberty'],
|
||||
'seattle': ['seahawks', 'mariners', 'sounders', 'kraken', 'reign', 'storm', 'torrent'],
|
||||
'chicago': ['bears', 'cubs', 'white sox', 'fire', 'sky', 'blackhawks'],
|
||||
'new york': ['giants', 'jets', 'yankees', 'mets', 'knicks', 'nyc fc', 'red bulls', 'liberty', 'rangers', 'islanders'], # Add PWHL New York when team_id verified
|
||||
'ny': ['giants', 'jets', 'yankees', 'mets', 'knicks', 'nyc fc', 'red bulls', 'liberty', 'rangers', 'islanders'], # Add PWHL New York when team_id verified
|
||||
'los angeles': ['rams', 'dodgers', 'lakers', 'la galaxy', 'lafc', 'sparks'],
|
||||
'la': ['rams', 'dodgers', 'lakers', 'la galaxy', 'lafc', 'sparks'],
|
||||
'miami': ['dolphins', 'marlins', 'heat', 'inter miami'],
|
||||
'boston': ['patriots', 'red sox', 'celtics', 'revolution'],
|
||||
'boston': ['patriots', 'red sox', 'celtics', 'revolution', 'bruins'], # Add PWHL Boston when team_id verified
|
||||
'philadelphia': ['eagles', 'phillies', '76ers', 'union'],
|
||||
'philadelphia': ['eagles', 'phillies', '76ers', 'union'],
|
||||
'atlanta': ['falcons', 'braves', 'hawks', 'atlanta united', 'dream'],
|
||||
@@ -727,8 +857,8 @@ class SportsCommand(BaseCommand):
|
||||
'dallas': ['cowboys', 'rangers', 'stars', 'fc dallas', 'wings'],
|
||||
'denver': ['broncos', 'rockies', 'rapids'],
|
||||
'detroit': ['lions', 'tigers', 'pistons'],
|
||||
'minnesota': ['vikings', 'twins', 'timberwolves', 'minnesota united', 'lynx'],
|
||||
'minneapolis': ['vikings', 'twins', 'timberwolves', 'minnesota united', 'lynx'],
|
||||
'minnesota': ['vikings', 'twins', 'timberwolves', 'minnesota united', 'lynx', 'wild'], # Add PWHL Minnesota when team_id verified
|
||||
'minneapolis': ['vikings', 'twins', 'timberwolves', 'minnesota united', 'lynx'], # Add PWHL Minnesota when team_id verified
|
||||
'cleveland': ['browns', 'guardians', 'cavaliers'],
|
||||
'cincinnati': ['bengals', 'reds', 'fc cincinnati'],
|
||||
'pittsburgh': ['steelers', 'pirates', 'penguins'],
|
||||
@@ -773,16 +903,16 @@ class SportsCommand(BaseCommand):
|
||||
'utah': ['jazz', 'real salt lake'],
|
||||
'orlando': ['magic', 'orlando city'],
|
||||
'orl': ['magic', 'orlando city'],
|
||||
'toronto': ['raptors', 'blue jays', 'toronto fc', 'maple leafs'],
|
||||
'tor': ['raptors', 'blue jays', 'toronto fc', 'maple leafs'],
|
||||
'toronto': ['raptors', 'blue jays', 'toronto fc', 'maple leafs'], # Add PWHL Toronto when team_id verified
|
||||
'tor': ['raptors', 'blue jays', 'toronto fc', 'maple leafs'], # Add PWHL Toronto when team_id verified
|
||||
'vancouver': ['canucks', 'whitecaps'],
|
||||
'van': ['canucks', 'whitecaps'],
|
||||
'montreal': ['canadiens', 'cf montreal'],
|
||||
'mtl': ['canadiens', 'cf montreal'],
|
||||
'montreal': ['canadiens', 'cf montreal'], # Add PWHL Montreal when team_id verified
|
||||
'mtl': ['canadiens', 'cf montreal'], # Add PWHL Montreal when team_id verified
|
||||
'calgary': ['flames'],
|
||||
'edmonton': ['oilers'],
|
||||
'winnipeg': ['jets'],
|
||||
'ottawa': ['senators'],
|
||||
'ottawa': ['senators'], # Add PWHL Ottawa when team_id verified
|
||||
'columbus': ['blue jackets', 'crew'],
|
||||
'clb': ['blue jackets', 'crew'],
|
||||
'st louis': ['blues', 'st louis city'],
|
||||
@@ -818,9 +948,10 @@ class SportsCommand(BaseCommand):
|
||||
game_data = []
|
||||
for team_info in city_teams:
|
||||
try:
|
||||
game_info = await self.fetch_team_game_data(team_info)
|
||||
if game_info:
|
||||
game_data.append(game_info)
|
||||
# Get all relevant games for this team (live, past within 8 days, upcoming within 6 weeks)
|
||||
games = await self.fetch_team_games(team_info)
|
||||
if games:
|
||||
game_data.extend(games)
|
||||
except Exception as e:
|
||||
self.logger.warning(f"Error fetching score for {team_info}: {e}")
|
||||
|
||||
@@ -953,10 +1084,12 @@ class SportsCommand(BaseCommand):
|
||||
pass
|
||||
|
||||
# Format based on game status
|
||||
if status_name in ['STATUS_IN_PROGRESS', 'STATUS_FIRST_HALF', 'STATUS_SECOND_HALF']:
|
||||
if status_name in ['STATUS_IN_PROGRESS', 'STATUS_FIRST_HALF', 'STATUS_SECOND_HALF', 'STATUS_END_PERIOD']:
|
||||
# Game is live - prioritize these (use negative timestamp)
|
||||
# STATUS_END_PERIOD means a period just ended but game is still ongoing
|
||||
clock = status.get('displayClock', '')
|
||||
period = status.get('period', 0)
|
||||
is_end_period = (status_name == 'STATUS_END_PERIOD')
|
||||
|
||||
# Format period based on sport
|
||||
if sport == 'soccer':
|
||||
@@ -975,12 +1108,18 @@ class SportsCommand(BaseCommand):
|
||||
period_str = short_detail # e.g., "Top 14th", "Bottom 9th"
|
||||
else:
|
||||
period_str = f"{period}I" # Fallback to inning number only
|
||||
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}" # Quarters
|
||||
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}" # Generic periods
|
||||
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
|
||||
@@ -1097,6 +1236,23 @@ class SportsCommand(BaseCommand):
|
||||
|
||||
async def get_team_scores(self, team_name: str) -> str:
|
||||
"""Get scores for a specific team or league"""
|
||||
# Check if this is a schedule query (team_name ends with " schedule")
|
||||
if team_name.endswith(' schedule'):
|
||||
team_name_clean = team_name[:-9].strip() # Remove " schedule"
|
||||
team_info = self.TEAM_MAPPINGS.get(team_name_clean)
|
||||
if not team_info:
|
||||
return self.translate('commands.sports.team_not_found', team=team_name_clean)
|
||||
|
||||
try:
|
||||
schedule_info = await self.fetch_team_schedule_formatted(team_info)
|
||||
if schedule_info:
|
||||
return schedule_info
|
||||
else:
|
||||
return self.translate('commands.sports.no_games_team', team=team_name_clean)
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error fetching schedule for {team_name_clean}: {e}")
|
||||
return self.translate('commands.sports.error_fetching_team', team=team_name_clean)
|
||||
|
||||
# Check if this is a league query
|
||||
league_info = self.get_league_info(team_name)
|
||||
if league_info:
|
||||
@@ -1173,8 +1329,163 @@ class SportsCommand(BaseCommand):
|
||||
Uses the team schedule endpoint which returns both past and upcoming games
|
||||
in a single API call. Returns games sorted by relevance:
|
||||
- Live games first
|
||||
- Then upcoming games
|
||||
- Then recent past games (most recent first)
|
||||
- Last completed game (if within last 8 days)
|
||||
- Next scheduled game (if known)
|
||||
"""
|
||||
try:
|
||||
# Use team schedule endpoint - returns both past and upcoming games
|
||||
url = f"{self.ESPN_BASE_URL}/{team_info['sport']}/{team_info['league']}/teams/{team_info['team_id']}/schedule"
|
||||
|
||||
# Make API request
|
||||
response = requests.get(url, timeout=self.url_timeout)
|
||||
response.raise_for_status()
|
||||
|
||||
data = response.json()
|
||||
events = data.get('events', [])
|
||||
|
||||
if not events:
|
||||
return []
|
||||
|
||||
# Parse all games
|
||||
all_games = []
|
||||
live_event_ids = [] # Track event IDs for live games
|
||||
for event in events:
|
||||
game_data = self.parse_game_event_with_timestamp(event, team_info['team_id'], team_info['sport'], team_info['league'])
|
||||
if game_data:
|
||||
all_games.append(game_data)
|
||||
# If this is a live game, store the event ID to fetch live data
|
||||
if game_data['timestamp'] < 0: # Negative timestamp indicates live game
|
||||
event_id = event.get('id')
|
||||
if event_id:
|
||||
live_event_ids.append((event_id, len(all_games) - 1)) # Store index too
|
||||
|
||||
if not all_games:
|
||||
return []
|
||||
|
||||
# Fetch live event data for live games to get real-time scores
|
||||
for event_id, game_index in live_event_ids:
|
||||
try:
|
||||
live_event_data = await self.fetch_live_event_data(event_id, team_info['sport'], team_info['league'])
|
||||
if live_event_data:
|
||||
# The event endpoint returns the event directly (not in an array)
|
||||
# Update the game data with live scores
|
||||
updated_game = self.parse_game_event_with_timestamp(
|
||||
live_event_data, team_info['team_id'], team_info['sport'], team_info['league']
|
||||
)
|
||||
if updated_game:
|
||||
all_games[game_index] = updated_game
|
||||
except Exception as e:
|
||||
self.logger.warning(f"Error fetching live data for event {event_id}: {e}")
|
||||
|
||||
# Sort by timestamp (negative for live games, then by actual timestamp)
|
||||
# This prioritizes: live games > upcoming games > recent past games
|
||||
all_games.sort(key=lambda x: x['timestamp'])
|
||||
|
||||
# Get current time for comparison
|
||||
now = datetime.now(timezone.utc).timestamp()
|
||||
# 8 days in seconds
|
||||
eight_days_ago = now - (8 * 24 * 60 * 60)
|
||||
# 6 weeks in seconds (6 * 7 * 24 * 60 * 60)
|
||||
six_weeks_from_now = now + (6 * 7 * 24 * 60 * 60)
|
||||
|
||||
# Separate into categories
|
||||
live_games = [g for g in all_games if g['timestamp'] < 0] # Negative timestamps = live
|
||||
upcoming_games = []
|
||||
past_games = []
|
||||
|
||||
# Categorize games with positive timestamps
|
||||
for game in all_games:
|
||||
if game['timestamp'] < 0:
|
||||
continue # Already in live_games
|
||||
|
||||
game_event_ts = game.get('event_timestamp')
|
||||
effective_ts = game_event_ts if game_event_ts is not None else game['timestamp']
|
||||
|
||||
if game['timestamp'] >= 9999999990 and game_event_ts is None:
|
||||
# No real timestamp available, treat as past
|
||||
past_games.append((effective_ts, game))
|
||||
elif effective_ts is None:
|
||||
past_games.append((effective_ts, game))
|
||||
elif effective_ts > now:
|
||||
# Future game - only include if within next 6 weeks
|
||||
if effective_ts is not None and effective_ts <= six_weeks_from_now:
|
||||
upcoming_games.append((effective_ts, game))
|
||||
else:
|
||||
# Past game - only include if within last 8 days
|
||||
if effective_ts is not None and effective_ts >= eight_days_ago:
|
||||
past_games.append((effective_ts, game))
|
||||
|
||||
# Sort upcoming games by soonest first, past games by most recent first
|
||||
upcoming_games.sort(key=lambda x: x[0] if x[0] is not None else float('inf'))
|
||||
past_games.sort(key=lambda x: x[0] if x[0] is not None else -float('inf'), reverse=True)
|
||||
|
||||
# Build result with new priority:
|
||||
# 1. Live games (if any)
|
||||
# 2. Last completed game (if within last 8 days)
|
||||
# 3. Next scheduled game (if known and within 6 weeks)
|
||||
result = []
|
||||
|
||||
# Add live games (if any)
|
||||
if live_games:
|
||||
result.extend(live_games)
|
||||
|
||||
# Add last completed game (if within last 8 days)
|
||||
if past_games:
|
||||
result.append(past_games[0][1]) # Most recent past game
|
||||
|
||||
# Add next scheduled game (if known and within 6 weeks)
|
||||
if upcoming_games:
|
||||
result.append(upcoming_games[0][1])
|
||||
|
||||
return result
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error fetching team games: {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.
|
||||
"""
|
||||
try:
|
||||
# Use scoreboard endpoint which has live scores
|
||||
url = f"{self.ESPN_BASE_URL}/{sport}/{league}/scoreboard"
|
||||
response = requests.get(url, timeout=self.url_timeout)
|
||||
response.raise_for_status()
|
||||
data = 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.warning(f"Error fetching live event data for {event_id}: {e}")
|
||||
return None
|
||||
|
||||
async def fetch_team_game_data(self, team_info: Dict[str, str]) -> Optional[Dict]:
|
||||
"""Fetch structured game data for a team with timestamp for sorting
|
||||
|
||||
Uses the team schedule endpoint which returns both past and upcoming games
|
||||
in a single API call, eliminating the need for multiple scoreboard requests.
|
||||
Returns only the most relevant game (for backward compatibility).
|
||||
"""
|
||||
games = await self.fetch_team_games(team_info)
|
||||
return games[0] if games else None
|
||||
|
||||
async def fetch_team_schedule(self, team_info: Dict[str, str]) -> List[Dict]:
|
||||
"""Fetch upcoming scheduled games for a team
|
||||
|
||||
Returns as many upcoming games as available from the schedule endpoint.
|
||||
Used for 'sports <teamname> schedule' command.
|
||||
"""
|
||||
try:
|
||||
# Use team schedule endpoint - returns both past and upcoming games
|
||||
@@ -1200,71 +1511,79 @@ class SportsCommand(BaseCommand):
|
||||
if not all_games:
|
||||
return []
|
||||
|
||||
# Sort by timestamp (negative for live games, then by actual timestamp)
|
||||
# This prioritizes: live games > upcoming games > recent past games
|
||||
all_games.sort(key=lambda x: x['timestamp'])
|
||||
|
||||
# Get current time for comparison
|
||||
now = datetime.now(timezone.utc).timestamp()
|
||||
|
||||
# Separate into categories
|
||||
live_games = [g for g in all_games if g['timestamp'] < 0] # Negative timestamps = live
|
||||
# Filter to only upcoming games
|
||||
upcoming_games = []
|
||||
past_games = []
|
||||
|
||||
# Categorize games with positive timestamps
|
||||
for game in all_games:
|
||||
# Skip live games (negative timestamps)
|
||||
if game['timestamp'] < 0:
|
||||
continue # Already in live_games
|
||||
continue
|
||||
|
||||
game_event_ts = game.get('event_timestamp')
|
||||
effective_ts = game_event_ts if game_event_ts is not None else game['timestamp']
|
||||
|
||||
if game['timestamp'] >= 9999999990 and game_event_ts is None:
|
||||
# No real timestamp available, treat as past
|
||||
past_games.append((effective_ts, game))
|
||||
elif effective_ts is None:
|
||||
past_games.append((effective_ts, game))
|
||||
elif effective_ts > now:
|
||||
# Future game
|
||||
# Only include games with valid future timestamps
|
||||
if effective_ts is not None and effective_ts > now:
|
||||
upcoming_games.append((effective_ts, game))
|
||||
else:
|
||||
# Past game
|
||||
past_games.append((effective_ts, game))
|
||||
|
||||
# Sort upcoming games by soonest first, past games by most recent first
|
||||
# Sort by soonest first
|
||||
upcoming_games.sort(key=lambda x: x[0] if x[0] is not None else float('inf'))
|
||||
past_games.sort(key=lambda x: x[0] if x[0] is not None else -float('inf'), reverse=True)
|
||||
|
||||
# Build result: live games + next upcoming + recent past
|
||||
result = []
|
||||
|
||||
# Add live games (if any)
|
||||
if live_games:
|
||||
result.extend(live_games)
|
||||
|
||||
# Add next upcoming game (if no live games)
|
||||
if not live_games and upcoming_games:
|
||||
result.append(upcoming_games[0][1])
|
||||
|
||||
# Add recent past games (most recent first, up to what fits)
|
||||
result.extend([g for _, g in past_games])
|
||||
|
||||
return result
|
||||
# Return all upcoming games (caller will limit by message length)
|
||||
return [g for _, g in upcoming_games]
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error fetching team games: {e}")
|
||||
self.logger.error(f"Error fetching team schedule: {e}")
|
||||
return []
|
||||
|
||||
async def fetch_team_game_data(self, team_info: Dict[str, str]) -> Optional[Dict]:
|
||||
"""Fetch structured game data for a team with timestamp for sorting
|
||||
async def fetch_team_schedule_formatted(self, team_info: Dict[str, str]) -> Optional[str]:
|
||||
"""Fetch and format upcoming scheduled games for a team
|
||||
|
||||
Uses the team schedule endpoint which returns both past and upcoming games
|
||||
in a single API call, eliminating the need for multiple scoreboard requests.
|
||||
Returns only the most relevant game (for backward compatibility).
|
||||
Returns formatted schedule with as many games as fit in 130 characters.
|
||||
"""
|
||||
games = await self.fetch_team_games(team_info)
|
||||
return games[0] if games else None
|
||||
games = await self.fetch_team_schedule(team_info)
|
||||
if not games:
|
||||
return None
|
||||
|
||||
# Format games to fit within message limit (130 characters)
|
||||
# Use 125 as a buffer to avoid cutting off mid-game
|
||||
sport_emoji = self.SPORT_EMOJIS.get(team_info['sport'], '🏆')
|
||||
formatted_games = []
|
||||
current_length = 0
|
||||
max_length = 125 # Leave buffer to avoid cutoff
|
||||
|
||||
for game in games:
|
||||
# Ensure game['formatted'] doesn't already have an emoji
|
||||
game_formatted = game['formatted'].strip()
|
||||
# Remove emoji if it's at the start (some games might have it)
|
||||
if game_formatted and game_formatted[0] in self.SPORT_EMOJIS.values():
|
||||
game_formatted = game_formatted[1:].strip()
|
||||
|
||||
game_str = f"{sport_emoji} {game_formatted}"
|
||||
# Check if adding this game would exceed limit
|
||||
if formatted_games:
|
||||
# Account for newline separator
|
||||
test_length = current_length + len("\n") + len(game_str)
|
||||
else:
|
||||
test_length = len(game_str)
|
||||
|
||||
if test_length <= max_length:
|
||||
formatted_games.append(game_str)
|
||||
current_length = test_length
|
||||
else:
|
||||
# Can't fit more games - stop before exceeding limit
|
||||
break
|
||||
|
||||
if not formatted_games:
|
||||
# If even the first game doesn't fit, return it anyway (truncated)
|
||||
game_formatted = games[0]['formatted'].strip()
|
||||
if game_formatted and game_formatted[0] in self.SPORT_EMOJIS.values():
|
||||
game_formatted = game_formatted[1:].strip()
|
||||
return f"{sport_emoji} {game_formatted[:120]}"
|
||||
|
||||
return "\n".join(formatted_games)
|
||||
|
||||
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"""
|
||||
@@ -1336,10 +1655,12 @@ class SportsCommand(BaseCommand):
|
||||
pass
|
||||
|
||||
# Format based on game status
|
||||
if status_name in ['STATUS_IN_PROGRESS', 'STATUS_FIRST_HALF', 'STATUS_SECOND_HALF']:
|
||||
if status_name in ['STATUS_IN_PROGRESS', 'STATUS_FIRST_HALF', 'STATUS_SECOND_HALF', 'STATUS_END_PERIOD']:
|
||||
# Game is live - prioritize these (use negative timestamp)
|
||||
# STATUS_END_PERIOD means a period just ended but game is still ongoing
|
||||
clock = status.get('displayClock', '')
|
||||
period = status.get('period', 0)
|
||||
is_end_period = (status_name == 'STATUS_END_PERIOD')
|
||||
|
||||
# Format period based on sport
|
||||
if sport == 'soccer':
|
||||
@@ -1361,9 +1682,13 @@ class SportsCommand(BaseCommand):
|
||||
formatted = f"{away_name} {away_score}-{home_score} @{home_name} ({period_str})"
|
||||
elif sport == 'football':
|
||||
period_str = f"Q{period}" # Quarters
|
||||
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}" # Generic periods
|
||||
period_str = f"P{period}" # Generic periods (hockey, etc.)
|
||||
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
|
||||
|
||||
Reference in new issue
Block a user