mirror of
https://github.com/agessaman/meshcore-bot.git
synced 2026-06-04 14:51:23 +00:00
836af3341a
- Updated routes in 'app.py' to reflect the new contacts template. - Enhanced 'sports_command.py' adding support for additional Portland teams - Improved game data fetching logic to check multiple dates for recent and upcoming games.
1399 lines
72 KiB
Python
1399 lines
72 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
Sports command for the MeshCore Bot
|
|
Provides sports scores and schedules using ESPN API
|
|
API description via https://github.com/zuplo/espn-openapi/
|
|
"""
|
|
|
|
import re
|
|
import json
|
|
import requests
|
|
from datetime import datetime, timezone
|
|
from typing import List, Dict, Optional, Tuple
|
|
from .base_command import BaseCommand
|
|
from ..models import MeshMessage
|
|
|
|
|
|
class SportsCommand(BaseCommand):
|
|
"""Handles sports commands with ESPN API integration"""
|
|
|
|
# Plugin metadata
|
|
name = "sports"
|
|
keywords = ['sports', 'score', 'scores']
|
|
description = "Get sports scores and schedules (usage: sports [team/league])"
|
|
category = "sports"
|
|
cooldown_seconds = 3 # 3 second cooldown per user to prevent API abuse
|
|
|
|
# ESPN API base URL
|
|
ESPN_BASE_URL = "http://site.api.espn.com/apis/site/v2/sports"
|
|
|
|
# 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'},
|
|
'cardinals': {'sport': 'football', 'league': 'nfl', 'team_id': '22'},
|
|
'arizona': {'sport': 'football', 'league': 'nfl', 'team_id': '22'},
|
|
'ari': {'sport': 'football', 'league': 'nfl', 'team_id': '22'},
|
|
'chargers': {'sport': 'football', 'league': 'nfl', 'team_id': '24'},
|
|
'lac': {'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'},
|
|
'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'},
|
|
'panthers': {'sport': 'football', 'league': 'nfl', 'team_id': '29'},
|
|
'carolina': {'sport': 'football', 'league': 'nfl', 'team_id': '29'},
|
|
'car': {'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'},
|
|
'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'},
|
|
|
|
# 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 (limited data available from API)
|
|
'lakers': {'sport': 'basketball', 'league': 'nba', 'team_id': '13'},
|
|
'warriors': {'sport': 'basketball', 'league': 'nba', 'team_id': '9'},
|
|
'celtics': {'sport': 'basketball', 'league': 'nba', 'team_id': '2'},
|
|
'heat': {'sport': 'basketball', 'league': 'nba', 'team_id': '14'},
|
|
'76ers': {'sport': 'basketball', 'league': 'nba', 'team_id': '20'},
|
|
'knicks': {'sport': 'basketball', 'league': 'nba', 'team_id': '18'},
|
|
'pelicans': {'sport': 'basketball', 'league': 'nba', 'team_id': '3'},
|
|
'trail blazers': {'sport': 'basketball', 'league': 'nba', 'team_id': '22'},
|
|
'blazers': {'sport': 'basketball', 'league': 'nba', 'team_id': '22'},
|
|
|
|
# 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 (limited data available from API)
|
|
'kraken': {'sport': 'hockey', 'league': 'nhl', 'team_id': '58'},
|
|
'seattle kraken': {'sport': 'hockey', 'league': 'nhl', 'team_id': '58'},
|
|
'blues': {'sport': 'hockey', 'league': 'nhl', 'team_id': '19'},
|
|
'stars': {'sport': 'hockey', 'league': 'nhl', 'team_id': '9'},
|
|
|
|
# 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'},
|
|
}
|
|
|
|
def __init__(self, bot):
|
|
super().__init__(bot)
|
|
self.url_timeout = 10 # seconds
|
|
|
|
# Per-user cooldown tracking
|
|
self.user_cooldowns = {} # user_id -> last_execution_time
|
|
|
|
# Load default teams from config
|
|
self.default_teams = self.load_default_teams()
|
|
self.sports_channels = self.load_sports_channels()
|
|
self.channel_overrides = self.load_channel_overrides()
|
|
|
|
def load_default_teams(self) -> List[str]:
|
|
"""Load default teams from config"""
|
|
teams_str = self.bot.config.get('Sports', 'teams', fallback='seahawks,mariners,sounders,kraken')
|
|
return [team.strip().lower() for team in teams_str.split(',') if team.strip()]
|
|
|
|
def load_sports_channels(self) -> List[str]:
|
|
"""Load sports channels from config"""
|
|
channels_str = self.bot.config.get('Sports', 'channels', fallback='')
|
|
return [channel.strip() for channel in channels_str.split(',') if channel.strip()]
|
|
|
|
def load_channel_overrides(self) -> Dict[str, str]:
|
|
"""Load channel overrides from config"""
|
|
overrides_str = self.bot.config.get('Sports', 'channel_override', fallback='')
|
|
overrides = {}
|
|
if overrides_str:
|
|
for override in overrides_str.split(','):
|
|
if '=' in override:
|
|
channel, team = override.strip().split('=', 1)
|
|
overrides[channel.strip()] = team.strip().lower()
|
|
return overrides
|
|
|
|
def is_womens_league(self, sport: str, league: str) -> bool:
|
|
"""Check if the league is a women's league"""
|
|
womens_leagues = {
|
|
('basketball', 'wnba'),
|
|
('soccer', 'usa.nwsl')
|
|
}
|
|
return (sport, league) in womens_leagues
|
|
|
|
def get_team_abbreviation(self, team_id: str, team_abbreviation: str, sport: str, league: str) -> str:
|
|
"""Get team abbreviation, using -W suffix only for women's leagues"""
|
|
if self.is_womens_league(sport, league):
|
|
return self.WOMENS_TEAM_ABBREVIATIONS.get(team_id, team_abbreviation)
|
|
else:
|
|
return team_abbreviation
|
|
|
|
def format_clean_date_time(self, 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(self, dt) -> str:
|
|
"""Format date without leading zeros"""
|
|
month = dt.month
|
|
day = dt.day
|
|
return f"{month}/{day}"
|
|
|
|
def matches_keyword(self, message: MeshMessage) -> bool:
|
|
"""Check if this command matches the message content - sports must be first word"""
|
|
if not self.keywords:
|
|
return False
|
|
|
|
# Strip exclamation mark if present (for command-style messages)
|
|
content = message.content.strip()
|
|
if content.startswith('!'):
|
|
content = content[1:].strip()
|
|
|
|
# Split into words and check if first word matches any keyword
|
|
words = content.split()
|
|
if not words:
|
|
return False
|
|
|
|
first_word = words[0].lower()
|
|
|
|
for keyword in self.keywords:
|
|
if first_word == keyword.lower():
|
|
return True
|
|
|
|
return False
|
|
|
|
def can_execute(self, message: MeshMessage) -> bool:
|
|
"""Check if this command can execute with the given message"""
|
|
# Check if sports command is enabled
|
|
sports_enabled = self.bot.config.getboolean('Sports', 'sports_enabled', fallback=True)
|
|
if not sports_enabled:
|
|
return False
|
|
|
|
# Check if command requires DM and message is not DM
|
|
if self.requires_dm and not message.is_dm:
|
|
return False
|
|
|
|
# Check if command requires specific channels (only for channel messages, not DMs)
|
|
if not message.is_dm and self.sports_channels and message.channel not in self.sports_channels:
|
|
# Check if this channel has an override (allows sports command even if not in main channels list)
|
|
if message.channel not in self.channel_overrides:
|
|
return False
|
|
|
|
# Check per-user cooldown (don't set it here, just check)
|
|
if self.cooldown_seconds > 0:
|
|
import time
|
|
current_time = time.time()
|
|
user_id = message.sender_id or "unknown"
|
|
|
|
if user_id in self.user_cooldowns:
|
|
time_since_last = current_time - self.user_cooldowns[user_id]
|
|
if time_since_last < self.cooldown_seconds:
|
|
remaining = self.cooldown_seconds - time_since_last
|
|
self.logger.info(f"Sports command cooldown active for user {user_id}, {remaining:.1f}s remaining")
|
|
return False
|
|
|
|
return True
|
|
|
|
def get_help_text(self) -> str:
|
|
return "Get sports scores & schedules. Use 'sports' for default teams, 'sports [team]' for specific team, or 'sports [league]' for league games."
|
|
|
|
async def execute(self, message: MeshMessage) -> bool:
|
|
"""Execute the sports command"""
|
|
try:
|
|
# Set cooldown for this user
|
|
if self.cooldown_seconds > 0:
|
|
import time
|
|
current_time = time.time()
|
|
user_id = message.sender_id or "unknown"
|
|
self.user_cooldowns[user_id] = current_time
|
|
|
|
# Parse the command
|
|
content = message.content.strip()
|
|
if content.startswith('!'):
|
|
content = content[1:].strip()
|
|
|
|
# Extract team name if provided
|
|
parts = content.split()
|
|
if len(parts) > 1:
|
|
team_name = ' '.join(parts[1:]).lower()
|
|
response = await self.get_team_scores(team_name)
|
|
else:
|
|
# Check if this channel has an override team
|
|
if not message.is_dm and message.channel in self.channel_overrides:
|
|
override_team = self.channel_overrides[message.channel]
|
|
response = await self.get_team_scores(override_team)
|
|
else:
|
|
response = await self.get_default_teams_scores()
|
|
|
|
# Send response
|
|
return await self.send_response(message, response)
|
|
|
|
except Exception as e:
|
|
self.logger.error(f"Error in sports command: {e}")
|
|
return await self.send_response(message, "Error fetching sports data")
|
|
|
|
async def get_default_teams_scores(self) -> str:
|
|
"""Get scores for default teams, sorted by game time"""
|
|
if not self.default_teams:
|
|
return "No default teams configured"
|
|
|
|
game_data = []
|
|
for team in self.default_teams:
|
|
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)
|
|
except Exception as e:
|
|
self.logger.warning(f"Error fetching score for {team}: {e}")
|
|
|
|
if not game_data:
|
|
return "No games found for default teams"
|
|
|
|
# Sort by game time (earliest first)
|
|
game_data.sort(key=lambda x: x['timestamp'])
|
|
|
|
# Format responses with sport emojis
|
|
responses = []
|
|
for game in game_data:
|
|
sport_emoji = self.SPORT_EMOJIS.get(game['sport'], '🏆')
|
|
responses.append(f"{sport_emoji} {game['formatted']}")
|
|
|
|
# Join responses with newlines and ensure under 130 characters
|
|
result = "\n".join(responses)
|
|
if len(result) > 130:
|
|
# If still too long, truncate the last response
|
|
while len(result) > 130 and len(responses) > 1:
|
|
responses.pop()
|
|
result = "\n".join(responses)
|
|
if len(result) > 130:
|
|
result = result[:127] + "..."
|
|
|
|
return result
|
|
|
|
def get_league_info(self, league_name: str) -> Optional[Dict[str, str]]:
|
|
"""Get league information for league queries"""
|
|
league_mappings = {
|
|
# NFL
|
|
'nfl': {'sport': 'football', 'league': 'nfl'},
|
|
'football': {'sport': 'football', 'league': 'nfl'},
|
|
|
|
# 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'},
|
|
'womens basketball': {'sport': 'basketball', 'league': 'wnba'},
|
|
'womens': {'sport': 'basketball', 'league': 'wnba'},
|
|
|
|
# NHL
|
|
'nhl': {'sport': 'hockey', 'league': 'nhl'},
|
|
'hockey': {'sport': 'hockey', 'league': 'nhl'},
|
|
|
|
# MLS
|
|
'mls': {'sport': 'soccer', 'league': 'usa.1'},
|
|
'soccer': {'sport': 'soccer', 'league': 'usa.1'},
|
|
|
|
# NWSL
|
|
'nwsl': {'sport': 'soccer', 'league': 'usa.nwsl'},
|
|
'womens soccer': {'sport': 'soccer', 'league': 'usa.nwsl'},
|
|
'womens': {'sport': 'soccer', 'league': 'usa.nwsl'},
|
|
|
|
# Premier League
|
|
'epl': {'sport': 'soccer', 'league': 'eng.1'},
|
|
'premier league': {'sport': 'soccer', 'league': 'eng.1'},
|
|
'premier': {'sport': 'soccer', 'league': 'eng.1'},
|
|
}
|
|
|
|
return league_mappings.get(league_name.lower())
|
|
|
|
def get_city_teams(self, city_name: str) -> List[Dict[str, str]]:
|
|
"""Get all teams for a given city"""
|
|
city_name_lower = city_name.lower()
|
|
|
|
# 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'],
|
|
'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'],
|
|
'philadelphia': ['eagles', 'phillies', '76ers', 'union'],
|
|
'philadelphia': ['eagles', 'phillies', '76ers', 'union'],
|
|
'atlanta': ['falcons', 'braves', 'hawks', 'atlanta united', 'dream'],
|
|
'houston': ['texans', 'astros', 'dynamo'],
|
|
'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'],
|
|
'cleveland': ['browns', 'guardians', 'cavaliers'],
|
|
'cincinnati': ['bengals', 'reds', 'fc cincinnati'],
|
|
'pittsburgh': ['steelers', 'pirates', 'penguins'],
|
|
'baltimore': ['ravens', 'orioles'],
|
|
'tampa': ['buccaneers', 'rays', 'lightning'],
|
|
'tampa bay': ['buccaneers', 'rays', 'lightning'],
|
|
'kansas city': ['chiefs', 'royals', 'sporting kc'],
|
|
'kc': ['chiefs', 'royals', 'sporting kc'],
|
|
'washington': ['commanders', 'nationals', 'wizards', 'dc united', 'mystics'],
|
|
'dc': ['commanders', 'nationals', 'wizards', 'dc united', 'mystics'],
|
|
'phoenix': ['cardinals', 'diamondbacks', 'suns', 'mercury'],
|
|
'indiana': ['colts', 'pacers', 'fever'],
|
|
'indianapolis': ['colts', 'pacers', 'fever'],
|
|
'las vegas': ['raiders', 'aces', 'golden knights'],
|
|
'connecticut': ['sun'],
|
|
'arizona': ['cardinals', 'diamondbacks', 'coyotes'],
|
|
'golden state': ['warriors', 'valkyries'],
|
|
'san francisco': ['49ers', 'giants', 'warriors', 'earthquakes', 'valkyries'],
|
|
'sf': ['49ers', 'giants', 'warriors', 'earthquakes', 'valkyries'],
|
|
'san diego': ['chargers', 'padres', 'san diego fc'],
|
|
'sd': ['chargers', 'padres', 'san diego fc'],
|
|
'ind': ['colts', 'pacers'],
|
|
'nashville': ['titans', 'predators', 'nashville sc'],
|
|
'tennessee': ['titans', 'predators', 'nashville sc'],
|
|
'ten': ['titans', 'predators', 'nashville sc'],
|
|
'lv': ['raiders', 'golden knights'],
|
|
'louisville': ['racing'],
|
|
'carolina': ['panthers', 'hornets'],
|
|
'charlotte': ['panthers', 'hornets', 'charlotte fc'],
|
|
'new orleans': ['saints', 'pelicans'],
|
|
'no': ['saints', 'pelicans'],
|
|
'green bay': ['packers'],
|
|
'gb': ['packers'],
|
|
'buffalo': ['bills', 'sabres'],
|
|
'buf': ['bills', 'sabres'],
|
|
'milwaukee': ['bucks', 'brewers'],
|
|
'mil': ['bucks', 'brewers'],
|
|
'portland': ['trail blazers', 'timbers'],
|
|
'por': ['trail blazers', 'timbers'],
|
|
'pdx': ['trail blazers', 'timbers'],
|
|
'salt lake': ['jazz', 'real salt lake'],
|
|
'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'],
|
|
'vancouver': ['canucks', 'whitecaps'],
|
|
'van': ['canucks', 'whitecaps'],
|
|
'montreal': ['canadiens', 'cf montreal'],
|
|
'mtl': ['canadiens', 'cf montreal'],
|
|
'calgary': ['flames'],
|
|
'edmonton': ['oilers'],
|
|
'winnipeg': ['jets'],
|
|
'ottawa': ['senators'],
|
|
'columbus': ['blue jackets', 'crew'],
|
|
'clb': ['blue jackets', 'crew'],
|
|
'st louis': ['blues', 'st louis city'],
|
|
'stl': ['blues', 'st louis city'],
|
|
'colorado': ['avalanche', 'rockies', 'rapids'],
|
|
'col': ['avalanche', 'rockies', 'rapids'],
|
|
'san jose': ['sharks', 'earthquakes'],
|
|
'sj': ['sharks', 'earthquakes'],
|
|
'anaheim': ['ducks', 'angels'],
|
|
'austin': ['austin fc'],
|
|
'atx': ['austin fc'],
|
|
}
|
|
|
|
# Get team names for this city
|
|
team_names = city_mappings.get(city_name_lower, [])
|
|
if not team_names:
|
|
return []
|
|
|
|
# Get team info for each team name
|
|
city_teams = []
|
|
for team_name in team_names:
|
|
team_info = self.TEAM_MAPPINGS.get(team_name)
|
|
if team_info:
|
|
city_teams.append(team_info)
|
|
|
|
return city_teams
|
|
|
|
async def get_city_scores(self, city_teams: List[Dict[str, str]], city_name: str) -> str:
|
|
"""Get scores for all teams in a city"""
|
|
if not city_teams:
|
|
return f"No teams found for {city_name}"
|
|
|
|
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)
|
|
except Exception as e:
|
|
self.logger.warning(f"Error fetching score for {team_info}: {e}")
|
|
|
|
if not game_data:
|
|
return f"No games found for {city_name} teams"
|
|
|
|
# Sort by game time (earliest first)
|
|
game_data.sort(key=lambda x: x['timestamp'])
|
|
|
|
# Format responses with sport emojis
|
|
responses = []
|
|
for game in game_data:
|
|
sport_emoji = self.SPORT_EMOJIS.get(game['sport'], '🏆')
|
|
responses.append(f"{sport_emoji} {game['formatted']}")
|
|
|
|
# Join responses with newlines and ensure under 130 characters
|
|
result = "\n".join(responses)
|
|
if len(result) > 130:
|
|
# If still too long, truncate the last response
|
|
while len(result) > 130 and len(responses) > 1:
|
|
responses.pop()
|
|
result = "\n".join(responses)
|
|
if len(result) > 130:
|
|
result = result[:127] + "..."
|
|
|
|
return result
|
|
|
|
async def get_league_scores(self, league_info: Dict[str, str]) -> str:
|
|
"""Get upcoming games for a league"""
|
|
try:
|
|
# Construct API URL
|
|
url = f"{self.ESPN_BASE_URL}/{league_info['sport']}/{league_info['league']}/scoreboard"
|
|
|
|
# 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 f"No games found for {league_info['sport']}"
|
|
|
|
# Parse all games and sort by time
|
|
game_data = []
|
|
for event in events:
|
|
game_info = self.parse_league_game_event(event, league_info['sport'], league_info['league'])
|
|
if game_info:
|
|
game_data.append(game_info)
|
|
|
|
if not game_data:
|
|
return f"No games found for {league_info['sport']}"
|
|
|
|
# Sort by game time (earliest first)
|
|
game_data.sort(key=lambda x: x['timestamp'])
|
|
|
|
# Format responses with sport emojis
|
|
responses = []
|
|
for game in game_data[:5]: # Limit to 5 games to keep under 130 chars
|
|
sport_emoji = self.SPORT_EMOJIS.get(game['sport'], '🏆')
|
|
responses.append(f"{sport_emoji} {game['formatted']}")
|
|
|
|
# Join responses with newlines and ensure under 130 characters
|
|
result = "\n".join(responses)
|
|
if len(result) > 130:
|
|
# If still too long, truncate the last response
|
|
while len(result) > 130 and len(responses) > 1:
|
|
responses.pop()
|
|
result = "\n".join(responses)
|
|
if len(result) > 130:
|
|
result = result[:127] + "..."
|
|
|
|
return result
|
|
|
|
except Exception as e:
|
|
self.logger.error(f"Error fetching league scores: {e}")
|
|
return f"Error fetching {league_info['sport']} data"
|
|
|
|
def parse_league_game_event(self, event: Dict, sport: str, league: str) -> Optional[Dict]:
|
|
"""Parse a league 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 teams for all sports
|
|
home_team = team1 if team1.get('homeAway') == 'home' else team2
|
|
away_team = team2 if team1.get('homeAway') == 'home' else team1
|
|
home_team_id = home_team.get('team', {}).get('id', '')
|
|
away_team_id = away_team.get('team', {}).get('id', '')
|
|
home_abbreviation = home_team.get('team', {}).get('abbreviation', 'UNK')
|
|
away_abbreviation = away_team.get('team', {}).get('abbreviation', 'UNK')
|
|
home_name = self.get_team_abbreviation(home_team_id, home_abbreviation, sport, league)
|
|
away_name = self.get_team_abbreviation(away_team_id, away_abbreviation, sport, league)
|
|
home_score = home_team.get('score', '0')
|
|
away_score = away_team.get('score', '0')
|
|
|
|
# Keep original variables for backward compatibility
|
|
team1_name = away_name # away team first
|
|
team2_name = home_name # home team second (gets @ symbol)
|
|
team1_score = away_score
|
|
team2_score = home_score
|
|
|
|
# Get game status
|
|
status = event.get('status', {})
|
|
status_type = status.get('type', {})
|
|
status_name = status_type.get('name', 'UNKNOWN')
|
|
|
|
# Get timestamp for sorting
|
|
date_str = event.get('date', '')
|
|
timestamp = 0 # Default for sorting
|
|
if date_str:
|
|
try:
|
|
dt = datetime.fromisoformat(date_str.replace('Z', '+00:00'))
|
|
timestamp = dt.timestamp()
|
|
except:
|
|
pass
|
|
|
|
# Format based on game status
|
|
if status_name in ['STATUS_IN_PROGRESS', 'STATUS_FIRST_HALF', 'STATUS_SECOND_HALF']:
|
|
# Game is live - prioritize these (use negative timestamp)
|
|
clock = status.get('displayClock', '')
|
|
period = status.get('period', 0)
|
|
|
|
# Format period based on sport
|
|
if sport == 'soccer':
|
|
# For soccer, use displayClock if available (e.g., "90'+5'"), otherwise use half
|
|
# For soccer, show home team first (traditional soccer format)
|
|
if clock and clock != '0:00' and clock != "0'":
|
|
period_str = clock # Use displayClock directly (e.g., "90'+5'")
|
|
formatted = f"@{home_name} {home_score}-{away_score} {away_name} ({period_str})"
|
|
else:
|
|
period_str = f"{period}H" # Fallback to half
|
|
formatted = f"@{home_name} {home_score}-{away_score} {away_name} ({clock} {period_str})"
|
|
elif sport == 'baseball':
|
|
# Use shortDetail for ongoing baseball games to show top/bottom of inning
|
|
short_detail = status.get('type', {}).get('shortDetail', '')
|
|
if short_detail and ('Top' in short_detail or 'Bottom' in short_detail):
|
|
period_str = short_detail # e.g., "Top 14th", "Bottom 9th"
|
|
else:
|
|
period_str = f"{period}I" # Fallback to inning number only
|
|
formatted = f"{away_name} {away_score}-{home_score} @{home_name} ({period_str})"
|
|
elif sport == 'football':
|
|
period_str = f"Q{period}" # Quarters
|
|
formatted = f"{away_name} {away_score}-{home_score} @{home_name} ({clock} {period_str})"
|
|
else:
|
|
period_str = f"P{period}" # Generic periods
|
|
formatted = f"{away_name} {away_score}-{home_score} @{home_name} ({clock} {period_str})"
|
|
|
|
timestamp = -1 # Live games first
|
|
|
|
elif status_name == 'STATUS_SCHEDULED':
|
|
# Game is scheduled
|
|
if date_str:
|
|
try:
|
|
dt = datetime.fromisoformat(date_str.replace('Z', '+00:00'))
|
|
local_dt = dt.astimezone()
|
|
time_str = self.format_clean_date_time(local_dt)
|
|
if sport == 'soccer':
|
|
formatted = f"@{home_name} vs. {away_name} ({time_str})"
|
|
else:
|
|
formatted = f"{away_name} @ {home_name} ({time_str})"
|
|
except:
|
|
if sport == 'soccer':
|
|
formatted = f"@{home_name} vs. {away_name} (TBD)"
|
|
else:
|
|
formatted = f"{away_name} @ {home_name} (TBD)"
|
|
timestamp = 9999999999 # Put TBD games last
|
|
else:
|
|
if sport == 'soccer':
|
|
formatted = f"@{home_name} vs. {away_name} (TBD)"
|
|
else:
|
|
formatted = f"{away_name} @ {home_name} (TBD)"
|
|
timestamp = 9999999999 # Put TBD games last
|
|
|
|
elif status_name == 'STATUS_HALFTIME':
|
|
# Game is at halftime
|
|
if sport == 'soccer':
|
|
formatted = f"@{home_name} {home_score}-{away_score} {away_name} (HT)"
|
|
else:
|
|
formatted = f"{away_name} {away_score}-{home_score} @{home_name} (HT)"
|
|
timestamp = -2 # Halftime games second priority after live games
|
|
elif status_name == 'STATUS_FULL_TIME':
|
|
# Soccer game is finished - put these last
|
|
# Check if game was played today or on a different day
|
|
date_suffix = ""
|
|
if date_str:
|
|
try:
|
|
dt = datetime.fromisoformat(date_str.replace('Z', '+00:00'))
|
|
local_dt = dt.astimezone()
|
|
today = datetime.now().date()
|
|
game_date = local_dt.date()
|
|
if game_date != today:
|
|
date_suffix = f", {self.format_clean_date(local_dt)}"
|
|
except:
|
|
pass
|
|
formatted = f"@{home_name} {home_score}-{away_score} {away_name} (FT{date_suffix})"
|
|
timestamp = 9999999998 # Final games second to last
|
|
elif status_name == 'STATUS_FINAL':
|
|
# Other sports game is finished - put these last
|
|
# Check if game was played today or on a different day
|
|
date_suffix = ""
|
|
if date_str:
|
|
try:
|
|
dt = datetime.fromisoformat(date_str.replace('Z', '+00:00'))
|
|
local_dt = dt.astimezone()
|
|
today = datetime.now().date()
|
|
game_date = local_dt.date()
|
|
if game_date != today:
|
|
date_suffix = f", {self.format_clean_date(local_dt)}"
|
|
except:
|
|
pass
|
|
formatted = f"{away_name} {away_score}-{home_score} @{home_name} (F{date_suffix})"
|
|
timestamp = 9999999998 # Final games second to last
|
|
|
|
else:
|
|
# Other status
|
|
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 # Other statuses third to last
|
|
|
|
return {
|
|
'timestamp': timestamp,
|
|
'formatted': formatted,
|
|
'sport': sport,
|
|
'status': status_name
|
|
}
|
|
|
|
except Exception as e:
|
|
self.logger.error(f"Error parsing league game event: {e}")
|
|
return None
|
|
|
|
async def get_team_scores(self, team_name: str) -> str:
|
|
"""Get scores for a specific team or league"""
|
|
# Check if this is a league query
|
|
league_info = self.get_league_info(team_name)
|
|
if league_info:
|
|
return await self.get_league_scores(league_info)
|
|
|
|
# Check if this is a city search that should return multiple teams
|
|
city_teams = self.get_city_teams(team_name)
|
|
if city_teams:
|
|
return await self.get_city_scores(city_teams, team_name)
|
|
|
|
# Otherwise, treat as single team query
|
|
team_info = self.TEAM_MAPPINGS.get(team_name)
|
|
if not team_info:
|
|
return f"Team/League '{team_name}' not found. Try: seahawks, mariners, sounders, kraken, storm, chiefs, lfc, mlb, nfl, mls, wnba, epl, etc."
|
|
|
|
try:
|
|
score_info = await self.fetch_team_score(team_info)
|
|
if score_info:
|
|
# Add sport emoji to the score info
|
|
sport_emoji = self.SPORT_EMOJIS.get(team_info['sport'], '🏆')
|
|
return f"{sport_emoji} {score_info}"
|
|
else:
|
|
return f"No games found for {team_name}"
|
|
except Exception as e:
|
|
self.logger.error(f"Error fetching score for {team_name}: {e}")
|
|
return f"Error fetching data for {team_name}"
|
|
|
|
async def fetch_team_score(self, team_info: Dict[str, str]) -> Optional[str]:
|
|
"""Fetch score information for a team (legacy method for individual team queries)"""
|
|
game_data = await self.fetch_team_game_data(team_info)
|
|
return game_data['formatted'] if game_data else 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"""
|
|
try:
|
|
from datetime import datetime, timedelta
|
|
|
|
# Check multiple dates to catch recent games and upcoming games
|
|
dates_to_check = []
|
|
today = datetime.now()
|
|
|
|
# Check yesterday, today, and tomorrow
|
|
for days_offset in [-1, 0, 1]:
|
|
check_date = today + timedelta(days=days_offset)
|
|
dates_to_check.append(check_date.strftime('%Y%m%d'))
|
|
|
|
# Also check current scoreboard (no date filter) for upcoming games
|
|
dates_to_check.append(None)
|
|
|
|
for date_str in dates_to_check:
|
|
if date_str:
|
|
url = f"{self.ESPN_BASE_URL}/{team_info['sport']}/{team_info['league']}/scoreboard?dates={date_str}"
|
|
else:
|
|
url = f"{self.ESPN_BASE_URL}/{team_info['sport']}/{team_info['league']}/scoreboard"
|
|
|
|
# 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:
|
|
continue
|
|
|
|
# Find games involving the team
|
|
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:
|
|
return game_data
|
|
|
|
return None
|
|
|
|
except Exception as e:
|
|
self.logger.error(f"Error fetching team game data: {e}")
|
|
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
|
|
|
|
# Check if our team is in this game
|
|
our_team = None
|
|
other_team = None
|
|
|
|
for competitor in competitors:
|
|
if competitor.get('team', {}).get('id') == team_id:
|
|
our_team = competitor
|
|
else:
|
|
other_team = competitor
|
|
|
|
if not our_team or not other_team:
|
|
return None
|
|
|
|
# Determine home/away teams for all sports
|
|
home_team = our_team if our_team.get('homeAway') == 'home' else other_team
|
|
away_team = other_team if our_team.get('homeAway') == 'home' else our_team
|
|
home_team_id = home_team.get('team', {}).get('id', '')
|
|
away_team_id = away_team.get('team', {}).get('id', '')
|
|
home_abbreviation = home_team.get('team', {}).get('abbreviation', 'UNK')
|
|
away_abbreviation = away_team.get('team', {}).get('abbreviation', 'UNK')
|
|
home_name = self.get_team_abbreviation(home_team_id, home_abbreviation, sport, league)
|
|
away_name = self.get_team_abbreviation(away_team_id, away_abbreviation, sport, league)
|
|
home_score = home_team.get('score', '0')
|
|
away_score = away_team.get('score', '0')
|
|
|
|
# For individual team queries, we still want to show our team first
|
|
# but in the correct home/away order for each sport
|
|
if our_team.get('homeAway') == 'home':
|
|
our_team_name = home_name
|
|
other_team_name = away_name
|
|
our_score = home_score
|
|
other_score = away_score
|
|
else:
|
|
our_team_name = away_name
|
|
other_team_name = home_name
|
|
our_score = away_score
|
|
other_score = home_score
|
|
|
|
# Get game status
|
|
status = event.get('status', {})
|
|
status_type = status.get('type', {})
|
|
status_name = status_type.get('name', 'UNKNOWN')
|
|
|
|
# Get timestamp for sorting
|
|
date_str = event.get('date', '')
|
|
timestamp = 0 # Default for sorting
|
|
if date_str:
|
|
try:
|
|
dt = datetime.fromisoformat(date_str.replace('Z', '+00:00'))
|
|
timestamp = dt.timestamp()
|
|
except:
|
|
pass
|
|
|
|
# Format based on game status
|
|
if status_name in ['STATUS_IN_PROGRESS', 'STATUS_FIRST_HALF', 'STATUS_SECOND_HALF']:
|
|
# Game is live - prioritize these (use negative timestamp)
|
|
clock = status.get('displayClock', '')
|
|
period = status.get('period', 0)
|
|
|
|
# Format period based on sport
|
|
if sport == 'soccer':
|
|
# For soccer, use displayClock if available (e.g., "90'+5'"), otherwise use half
|
|
# For soccer, show home team first (traditional soccer format)
|
|
if clock and clock != '0:00' and clock != "0'":
|
|
period_str = clock # Use displayClock directly (e.g., "90'+5'")
|
|
formatted = f"@{home_name} {home_score}-{away_score} {away_name} ({period_str})"
|
|
else:
|
|
period_str = f"{period}H" # Fallback to half
|
|
formatted = f"@{home_name} {home_score}-{away_score} {away_name} ({clock} {period_str})"
|
|
elif sport == 'baseball':
|
|
# Use shortDetail for ongoing baseball games to show top/bottom of inning
|
|
short_detail = status.get('type', {}).get('shortDetail', '')
|
|
if short_detail and ('Top' in short_detail or 'Bottom' in short_detail):
|
|
period_str = short_detail # e.g., "Top 14th", "Bottom 9th"
|
|
else:
|
|
period_str = f"{period}I" # Fallback to inning number only
|
|
formatted = f"{away_name} {away_score}-{home_score} @{home_name} ({period_str})"
|
|
elif sport == 'football':
|
|
period_str = f"Q{period}" # Quarters
|
|
formatted = f"{away_name} {away_score}-{home_score} @{home_name} ({clock} {period_str})"
|
|
else:
|
|
period_str = f"P{period}" # Generic periods
|
|
formatted = f"{away_name} {away_score}-{home_score} @{home_name} ({clock} {period_str})"
|
|
|
|
timestamp = -1 # Live games first
|
|
|
|
elif status_name == 'STATUS_SCHEDULED':
|
|
# Game is scheduled
|
|
if date_str:
|
|
try:
|
|
dt = datetime.fromisoformat(date_str.replace('Z', '+00:00'))
|
|
local_dt = dt.astimezone()
|
|
time_str = self.format_clean_date_time(local_dt)
|
|
if sport == 'soccer':
|
|
formatted = f"@{home_name} vs. {away_name} ({time_str})"
|
|
else:
|
|
formatted = f"{away_name} @ {home_name} ({time_str})"
|
|
except:
|
|
if sport == 'soccer':
|
|
formatted = f"@{home_name} vs. {away_name} (TBD)"
|
|
else:
|
|
formatted = f"{away_name} @ {home_name} (TBD)"
|
|
timestamp = 9999999999 # Put TBD games last
|
|
else:
|
|
if sport == 'soccer':
|
|
formatted = f"@{home_name} vs. {away_name} (TBD)"
|
|
else:
|
|
formatted = f"{away_name} @ {home_name} (TBD)"
|
|
timestamp = 9999999999 # Put TBD games last
|
|
|
|
elif status_name == 'STATUS_HALFTIME':
|
|
# Game is at halftime
|
|
if sport == 'soccer':
|
|
formatted = f"@{home_name} {home_score}-{away_score} {away_name} (HT)"
|
|
else:
|
|
formatted = f"{away_name} {away_score}-{home_score} @{home_name} (HT)"
|
|
timestamp = -2 # Halftime games second priority after live games
|
|
elif status_name == 'STATUS_FULL_TIME':
|
|
# Soccer game is finished - put these last
|
|
# Check if game was played today or on a different day
|
|
date_suffix = ""
|
|
if date_str:
|
|
try:
|
|
dt = datetime.fromisoformat(date_str.replace('Z', '+00:00'))
|
|
local_dt = dt.astimezone()
|
|
today = datetime.now().date()
|
|
game_date = local_dt.date()
|
|
if game_date != today:
|
|
date_suffix = f", {self.format_clean_date(local_dt)}"
|
|
except:
|
|
pass
|
|
formatted = f"@{home_name} {home_score}-{away_score} {away_name} (FT{date_suffix})"
|
|
timestamp = 9999999998 # Final games second to last
|
|
elif status_name == 'STATUS_FINAL':
|
|
# Other sports game is finished - put these last
|
|
# Check if game was played today or on a different day
|
|
date_suffix = ""
|
|
if date_str:
|
|
try:
|
|
dt = datetime.fromisoformat(date_str.replace('Z', '+00:00'))
|
|
local_dt = dt.astimezone()
|
|
today = datetime.now().date()
|
|
game_date = local_dt.date()
|
|
if game_date != today:
|
|
date_suffix = f", {self.format_clean_date(local_dt)}"
|
|
except:
|
|
pass
|
|
formatted = f"{away_name} {away_score}-{home_score} @{home_name} (F{date_suffix})"
|
|
timestamp = 9999999998 # Final games second to last
|
|
|
|
else:
|
|
# Other status
|
|
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 # Other statuses third to last
|
|
|
|
return {
|
|
'timestamp': timestamp,
|
|
'formatted': formatted,
|
|
'sport': sport,
|
|
'status': status_name
|
|
}
|
|
|
|
except Exception as e:
|
|
self.logger.error(f"Error parsing game event with timestamp: {e}")
|
|
return None
|
|
|
|
def parse_game_event(self, event: Dict, team_id: str) -> Optional[str]:
|
|
"""Parse a game event and return formatted score info"""
|
|
try:
|
|
competitions = event.get('competitions', [])
|
|
if not competitions:
|
|
return None
|
|
|
|
competition = competitions[0]
|
|
competitors = competition.get('competitors', [])
|
|
|
|
if len(competitors) != 2:
|
|
return None
|
|
|
|
# Check if our team is in this game
|
|
our_team = None
|
|
other_team = None
|
|
|
|
for competitor in competitors:
|
|
if competitor.get('team', {}).get('id') == team_id:
|
|
our_team = competitor
|
|
else:
|
|
other_team = competitor
|
|
|
|
if not our_team or not other_team:
|
|
return None
|
|
|
|
# Extract team info
|
|
our_team_name = our_team.get('team', {}).get('abbreviation', 'UNK')
|
|
other_team_name = other_team.get('team', {}).get('abbreviation', 'UNK')
|
|
|
|
# Determine home/away teams
|
|
our_home_away = our_team.get('homeAway', '')
|
|
other_home_away = other_team.get('homeAway', '')
|
|
|
|
if our_home_away == 'home':
|
|
home_team_name = our_team_name
|
|
away_team_name = other_team_name
|
|
elif other_home_away == 'home':
|
|
home_team_name = other_team_name
|
|
away_team_name = our_team_name
|
|
else:
|
|
# Fallback if homeAway is not available
|
|
home_team_name = other_team_name
|
|
away_team_name = our_team_name
|
|
|
|
# Get scores
|
|
our_score = our_team.get('score', '0')
|
|
other_score = other_team.get('score', '0')
|
|
|
|
# Get game status
|
|
status = event.get('status', {})
|
|
status_type = status.get('type', {})
|
|
status_name = status_type.get('name', 'UNKNOWN')
|
|
|
|
# Format based on game status
|
|
if status_name in ['STATUS_IN_PROGRESS', 'STATUS_FIRST_HALF', 'STATUS_SECOND_HALF']:
|
|
# Game is live
|
|
clock = status.get('displayClock', '')
|
|
period = status.get('period', 0)
|
|
|
|
# Format period based on sport (need to determine sport from team_info)
|
|
# This is a legacy method, so we'll use a generic approach
|
|
if period <= 2:
|
|
period_str = f"{period}H" # Likely soccer (halves)
|
|
elif period <= 4:
|
|
period_str = f"Q{period}" # Likely football (quarters)
|
|
else:
|
|
period_str = f"{period}I" # Likely baseball (innings)
|
|
|
|
return f"{our_team_name} {our_score}-{other_score} @{other_team_name} ({clock} {period_str})"
|
|
|
|
elif status_name == 'STATUS_SCHEDULED':
|
|
# Game is scheduled
|
|
date_str = event.get('date', '')
|
|
if date_str:
|
|
try:
|
|
# Parse date and format
|
|
dt = datetime.fromisoformat(date_str.replace('Z', '+00:00'))
|
|
# Convert to local time (assuming Pacific for Seattle teams)
|
|
local_dt = dt.astimezone()
|
|
time_str = self.format_clean_date_time(local_dt)
|
|
return f"{away_team_name} @ {home_team_name} ({time_str})"
|
|
except:
|
|
return f"{away_team_name} @ {home_team_name} (TBD)"
|
|
else:
|
|
return f"{away_team_name} @ {home_team_name} (TBD)"
|
|
|
|
elif status_name == 'STATUS_HALFTIME':
|
|
# Game is at halftime
|
|
return f"{our_team_name} {our_score}-{other_score} @{other_team_name} (HT)"
|
|
elif status_name == 'STATUS_FULL_TIME':
|
|
# Soccer game is finished
|
|
# Check if game was played today or on a different day
|
|
date_str = event.get('date', '')
|
|
date_suffix = ""
|
|
if date_str:
|
|
try:
|
|
dt = datetime.fromisoformat(date_str.replace('Z', '+00:00'))
|
|
local_dt = dt.astimezone()
|
|
today = datetime.now().date()
|
|
game_date = local_dt.date()
|
|
if game_date != today:
|
|
date_suffix = f", {self.format_clean_date(local_dt)}"
|
|
except:
|
|
pass
|
|
return f"{our_team_name} {our_score}-{other_score} @{other_team_name} (FT{date_suffix})"
|
|
elif status_name == 'STATUS_FINAL':
|
|
# Other sports game is finished
|
|
# Check if game was played today or on a different day
|
|
date_str = event.get('date', '')
|
|
date_suffix = ""
|
|
if date_str:
|
|
try:
|
|
dt = datetime.fromisoformat(date_str.replace('Z', '+00:00'))
|
|
local_dt = dt.astimezone()
|
|
today = datetime.now().date()
|
|
game_date = local_dt.date()
|
|
if game_date != today:
|
|
date_suffix = f", {self.format_clean_date(local_dt)}"
|
|
except:
|
|
pass
|
|
return f"{our_team_name} {our_score}-{other_score} @{other_team_name} (F{date_suffix})"
|
|
|
|
else:
|
|
# Other status
|
|
return f"{our_team_name} {our_score}-{other_score} {other_team_name} ({status_name})"
|
|
|
|
except Exception as e:
|
|
self.logger.error(f"Error parsing game event: {e}")
|
|
return None
|