diff --git a/src/base_classes/baseball.py b/src/base_classes/baseball.py index 56ffd78e..10c0b9af 100644 --- a/src/base_classes/baseball.py +++ b/src/base_classes/baseball.py @@ -13,28 +13,12 @@ import logging class Baseball(SportsCore): """Base class for baseball sports with common functionality.""" - - # Baseball sport configuration (moved from sport_configs.py) - SPORT_CONFIG = { - 'update_cadence': 'daily', - 'season_length': 162, - 'games_per_week': 6, - 'api_endpoints': ['scoreboard', 'standings', 'stats'], - 'sport_specific_fields': ['inning', 'outs', 'bases', 'strikes', 'balls', 'pitcher', 'batter'], - 'update_interval_seconds': 30, - 'logo_dir': 'assets/sports/mlb_logos', - 'show_records': True, - 'show_ranking': True, - 'show_odds': True, - 'data_source_type': 'espn', # Can be overridden for MLB API - 'api_base_url': 'https://site.api.espn.com/apis/site/v2/sports/baseball' - } + def __init__(self, config: Dict[str, Any], display_manager, cache_manager, logger: logging.Logger, sport_key: str): super().__init__(config, display_manager, cache_manager, logger, sport_key) # Initialize baseball-specific architecture components - self.sport_config = self.get_sport_config() self.api_extractor = ESPNBaseballExtractor(logger) # Choose data source based on sport (MLB uses MLB API, others use ESPN) @@ -49,10 +33,7 @@ class Baseball(SportsCore): self.show_bases = self.mode_config.get("show_bases", True) self.show_count = self.mode_config.get("show_count", True) self.show_pitcher_batter = self.mode_config.get("show_pitcher_batter", False) - - def get_sport_config(self) -> Dict[str, Any]: - """Get baseball sport configuration.""" - return self.SPORT_CONFIG.copy() + self.sport = "baseball" def _get_baseball_display_text(self, game: Dict) -> str: """Get baseball-specific display text.""" diff --git a/src/base_classes/data_sources.py b/src/base_classes/data_sources.py index 6132b536..1f3dd813 100644 --- a/src/base_classes/data_sources.py +++ b/src/base_classes/data_sources.py @@ -65,8 +65,10 @@ class ESPNDataSource(DataSource): def fetch_live_games(self, sport: str, league: str) -> List[Dict]: """Fetch live games from ESPN API.""" try: + now = datetime.now() + formatted_date = now.strftime("%Y%m%d") url = f"{self.base_url}/{sport}/{league}/scoreboard" - response = self.session.get(url, headers=self.get_headers(), timeout=15) + response = self.session.get(url, params={"dates": formatted_date, "limit": 1000}, headers=self.get_headers(), timeout=15) response.raise_for_status() data = response.json() @@ -90,7 +92,8 @@ class ESPNDataSource(DataSource): url = f"{self.base_url}/{sport}/{league}/scoreboard" params = { - 'dates': f"{start_date.strftime('%Y%m%d')}-{end_date.strftime('%Y%m%d')}" + 'dates': f"{start_date.strftime('%Y%m%d')}-{end_date.strftime('%Y%m%d')}", + "limit": 1000 } response = self.session.get(url, headers=self.get_headers(), params=params, timeout=15) @@ -109,7 +112,7 @@ class ESPNDataSource(DataSource): def fetch_standings(self, sport: str, league: str) -> Dict: """Fetch standings from ESPN API.""" try: - url = f"{self.base_url}/{sport}/{league}/standings" + url = f"{self.base_url}/{sport}/{league}/rankings" response = self.session.get(url, headers=self.get_headers(), timeout=15) response.raise_for_status() diff --git a/src/base_classes/football.py b/src/base_classes/football.py index cf67ca6c..01f4cc55 100644 --- a/src/base_classes/football.py +++ b/src/base_classes/football.py @@ -14,39 +14,13 @@ import requests class Football(SportsCore): """Base class for football sports with common functionality.""" - # Football sport configuration (moved from sport_configs.py) - SPORT_CONFIG = { - 'update_cadence': 'weekly', - 'season_length': 17, # NFL default - 'games_per_week': 1, - 'api_endpoints': ['scoreboard', 'standings'], - 'sport_specific_fields': ['down', 'distance', 'possession', 'timeouts', 'is_redzone'], - 'update_interval_seconds': 60, - 'logo_dir': 'assets/sports/nfl_logos', - 'show_records': True, - 'show_ranking': True, - 'show_odds': True, - 'data_source_type': 'espn', - 'api_base_url': 'https://site.api.espn.com/apis/site/v2/sports/football' - } - def __init__(self, config: Dict[str, Any], display_manager: DisplayManager, cache_manager: CacheManager, logger: logging.Logger, sport_key: str): super().__init__(config, display_manager, cache_manager, logger, sport_key) # Initialize football-specific architecture components - self.sport_config = self.get_sport_config() self.api_extractor = ESPNFootballExtractor(logger) self.data_source = ESPNDataSource(logger) - - def get_sport_config(self) -> Dict[str, Any]: - """Get football sport configuration.""" - return self.SPORT_CONFIG.copy() - - def _fetch_game_odds(self, _: Dict) -> None: - pass - - def _fetch_odds(self, game: Dict, league: str) -> None: - super()._fetch_odds(game, "football", league) + self.sport = "football" def _extract_game_details(self, game_event: Dict) -> Optional[Dict]: """Extract relevant game details from ESPN NCAA FB API response.""" @@ -122,13 +96,6 @@ class Football(SportsCore): elif status["type"]["state"] == "pre": period_text = details.get("game_time", "") # Show time for upcoming - # Timeouts (assuming max 3 per half, not carried over well in standard API) - # API often provides 'timeouts' directly under team, but reset logic is tricky - # We might need to simplify this or just use a fixed display if API is unreliable - # For upcoming games, we'll show based on number of games, not time window - # For recent games, we'll show based on number of games, not time window - is_within_window = True # Always include games, let the managers filter by count - details.update({ "period": period, "period_text": period_text, # Formatted quarter/status @@ -155,17 +122,6 @@ class Football(SportsCore): logging.error(f"Error extracting game details: {e} from event: {game_event.get('id')}", exc_info=True) return None - def _fetch_todays_games(self, league: str) -> Optional[Dict]: - """Fetch only today's games for live updates (not entire season).""" - return super()._fetch_todays_games("football", league) - - def _get_weeks_data(self, league: str) -> Optional[Dict]: - """ - Get partial data for immediate display while background fetch is in progress. - This fetches current/recent games only for quick response. - """ - return super()._get_weeks_data("football", league) - class FootballLive(Football): def __init__(self, config: Dict[str, Any], display_manager: DisplayManager, cache_manager: CacheManager, logger: logging.Logger, sport_key: str): super().__init__(config, display_manager, cache_manager, logger, sport_key) @@ -250,21 +206,19 @@ class FootballLive(Football): data = self._fetch_data() new_live_games = [] if data and "events" in data: - for event in data["events"]: - details = self._extract_game_details(event) + for game in data["events"]: + details = self._extract_game_details(game) if details and (details["is_live"] or details["is_halftime"]): # If show_favorite_teams_only is true, only add if it's a favorite. # Otherwise, add all games. - if self.mode_config.get("show_favorite_teams_only", False): + if self.show_favorite_teams_only: if details["home_abbr"] in self.favorite_teams or details["away_abbr"] in self.favorite_teams: - if self.show_odds: - self._fetch_game_odds(details) new_live_games.append(details) else: - if self.show_odds: - self._fetch_game_odds(details) new_live_games.append(details) - + for game in new_live_games: + if self.show_odds: + self._fetch_odds(game) # Log changes or periodically current_time_for_log = time.time() # Use a consistent time for logging comparison should_log = ( @@ -276,12 +230,12 @@ class FootballLive(Football): if should_log: if new_live_games: - filter_text = "favorite teams" if self.mode_config.get("show_favorite_teams_only", False) else "all teams" + filter_text = "favorite teams" if self.show_favorite_teams_only else "all teams" self.logger.info(f"Found {len(new_live_games)} live/halftime games for {filter_text}.") for game_info in new_live_games: # Renamed game to game_info self.logger.info(f" - {game_info['away_abbr']}@{game_info['home_abbr']} ({game_info.get('status_text', 'N/A')})") else: - filter_text = "favorite teams" if self.mode_config.get("show_favorite_teams_only", False) else "criteria" + filter_text = "favorite teams" if self.show_favorite_teams_only else "criteria" self.logger.info(f"No live/halftime games found for {filter_text}.") self.last_log_time = current_time_for_log @@ -504,8 +458,7 @@ class FootballLive(Football): if away_abbr: if self.show_ranking and self.show_records: # When both rankings and records are enabled, rankings replace records completely - rankings = self._fetch_team_rankings() - away_rank = rankings.get(away_abbr, 0) + away_rank = self._team_rankings_cache.get(away_abbr, 0) if away_rank > 0: away_text = f"#{away_rank}" else: @@ -513,8 +466,7 @@ class FootballLive(Football): away_text = '' elif self.show_ranking: # Show ranking only if available - rankings = self._fetch_team_rankings() - away_rank = rankings.get(away_abbr, 0) + away_rank = self._team_rankings_cache.get(away_abbr, 0) if away_rank > 0: away_text = f"#{away_rank}" else: @@ -534,8 +486,7 @@ class FootballLive(Football): if home_abbr: if self.show_ranking and self.show_records: # When both rankings and records are enabled, rankings replace records completely - rankings = self._fetch_team_rankings() - home_rank = rankings.get(home_abbr, 0) + home_rank = self._team_rankings_cache.get(home_abbr, 0) if home_rank > 0: home_text = f"#{home_rank}" else: @@ -543,8 +494,7 @@ class FootballLive(Football): home_text = '' elif self.show_ranking: # Show ranking only if available - rankings = self._fetch_team_rankings() - home_rank = rankings.get(home_abbr, 0) + home_rank = self._team_rankings_cache.get(home_abbr, 0) if home_rank > 0: home_text = f"#{home_rank}" else: diff --git a/src/base_classes/hockey.py b/src/base_classes/hockey.py index c5743471..eb600aca 100644 --- a/src/base_classes/hockey.py +++ b/src/base_classes/hockey.py @@ -12,36 +12,13 @@ from src.base_classes.data_sources import ESPNDataSource class Hockey(SportsCore): """Base class for hockey sports with common functionality.""" - # Hockey sport configuration (moved from sport_configs.py) - SPORT_CONFIG = { - 'update_cadence': 'daily', - 'season_length': 82, # NHL default - 'games_per_week': 3, - 'api_endpoints': ['scoreboard', 'standings'], - 'sport_specific_fields': ['period', 'power_play', 'penalties', 'shots_on_goal'], - 'update_interval_seconds': 30, - 'logo_dir': 'assets/sports/nhl_logos', - 'show_records': True, - 'show_ranking': True, - 'show_odds': True, - 'data_source_type': 'espn', - 'api_base_url': 'https://site.api.espn.com/apis/site/v2/sports/hockey' - } - def __init__(self, config: Dict[str, Any], display_manager: DisplayManager, cache_manager: CacheManager, logger: logging.Logger, sport_key: str): super().__init__(config, display_manager, cache_manager, logger, sport_key) # Initialize hockey-specific architecture components - self.sport_config = self.get_sport_config() self.api_extractor = ESPNHockeyExtractor(logger) self.data_source = ESPNDataSource(logger) - - def get_sport_config(self) -> Dict[str, Any]: - """Get hockey sport configuration.""" - return self.SPORT_CONFIG.copy() - - def _fetch_odds(self, game: Dict, league: str) -> None: - super()._fetch_odds(game, "hockey", league) + self.sport = "hockey" def _extract_game_details(self, game_event: Dict) -> Optional[Dict]: @@ -53,11 +30,20 @@ class Hockey(SportsCore): try: competition = game_event["competitions"][0] status = competition["status"] + powerplay = False + penalties = "" + shots_on_goal = {"home": 0, "away": 0} if situation and status["type"]["state"] == "in": # Detect scoring events from status detail status_detail = status["type"].get("detail", "").lower() status_short = status["type"].get("shortDetail", "").lower() + powerplay = situation.get("isPowerPlay", False) + penalties = situation.get("penalties", "") + shots_on_goal = { + "home": situation.get("homeShots", 0), + "away": situation.get("awayShots", 0) + } # Format period/quarter period = status.get("period", 0) @@ -79,7 +65,10 @@ class Hockey(SportsCore): details.update({ "period": period, "period_text": period_text, # Formatted quarter/status - "clock": status.get("displayClock", "0:00") + "clock": status.get("displayClock", "0:00"), + "power_play": powerplay, + "penalties": penalties, + "shots_on_goal": shots_on_goal }) # Basic validation (can be expanded) @@ -118,6 +107,9 @@ class HockeyLive(Hockey): if current_time - self.last_update >= interval: self.last_update = current_time + if self.show_ranking: + self._fetch_team_rankings() + if self.test_mode: # For testing, we'll just update the clock to show it's working if self.current_game: @@ -149,7 +141,7 @@ class HockeyLive(Hockey): new_live_games.append(details) # Filter for favorite teams only if the config is set - if self.mode_config.get("show_favorite_teams_only", False): + if self.show_favorite_teams_only: new_live_games = [game for game in new_live_games if game['home_abbr'] in self.favorite_teams or game['away_abbr'] in self.favorite_teams] @@ -163,12 +155,12 @@ class HockeyLive(Hockey): if should_log: if new_live_games: - filter_text = "favorite teams" if self.mode_config.get("show_favorite_teams_only", False) else "all teams" + filter_text = "favorite teams" if self.show_favorite_teams_only else "all teams" self.logger.info(f"[NCAAMH] Found {len(new_live_games)} live games involving {filter_text}") for game in new_live_games: self.logger.info(f"[NCAAMH] Live game: {game['away_abbr']} vs {game['home_abbr']} - Period {game['period']}, {game['clock']}") else: - filter_text = "favorite teams" if self.mode_config.get("show_favorite_teams_only", False) else "criteria" + filter_text = "favorite teams" if self.show_favorite_teams_only else "criteria" self.logger.info(f"[NCAAMH] No live games found matching {filter_text}") self.last_log_time = current_time @@ -287,8 +279,7 @@ class HockeyLive(Hockey): if away_abbr: if self.show_ranking and self.show_records: # When both rankings and records are enabled, rankings replace records completely - rankings = self._fetch_team_rankings() - away_rank = rankings.get(away_abbr, 0) + away_rank = self._team_rankings_cache.get(away_abbr, 0) if away_rank > 0: away_text = f"#{away_rank}" else: @@ -296,8 +287,7 @@ class HockeyLive(Hockey): away_text = '' elif self.show_ranking: # Show ranking only if available - rankings = self._fetch_team_rankings() - away_rank = rankings.get(away_abbr, 0) + away_rank = self._team_rankings_cache.get(away_abbr, 0) if away_rank > 0: away_text = f"#{away_rank}" else: @@ -317,8 +307,7 @@ class HockeyLive(Hockey): if home_abbr: if self.show_ranking and self.show_records: # When both rankings and records are enabled, rankings replace records completely - rankings = self._fetch_team_rankings() - home_rank = rankings.get(home_abbr, 0) + home_rank = self._team_rankings_cache.get(home_abbr, 0) if home_rank > 0: home_text = f"#{home_rank}" else: @@ -326,8 +315,7 @@ class HockeyLive(Hockey): home_text = '' elif self.show_ranking: # Show ranking only if available - rankings = self._fetch_team_rankings() - home_rank = rankings.get(home_abbr, 0) + home_rank = self._team_rankings_cache.get(home_abbr, 0) if home_rank > 0: home_text = f"#{home_rank}" else: diff --git a/src/base_classes/sports.py b/src/base_classes/sports.py index 16bf8aad..7aa29404 100644 --- a/src/base_classes/sports.py +++ b/src/base_classes/sports.py @@ -16,8 +16,8 @@ from src.logo_downloader import download_missing_logo, LogoDownloader from pathlib import Path # Import new architecture components (individual classes will import what they need) -from .api_extractors import ESPNFootballExtractor, ESPNBaseballExtractor, ESPNHockeyExtractor -from .data_sources import ESPNDataSource, MLBAPIDataSource +from src.base_classes.api_extractors import APIDataExtractor +from src.base_classes.data_sources import DataSource from src.dynamic_team_resolver import DynamicTeamResolver class SportsCore: @@ -33,11 +33,13 @@ class SportsCore: self.display_height = self.display_manager.matrix.height self.sport_key = sport_key + self.sport = None + self.league = None # Initialize new architecture components (will be overridden by sport-specific classes) self.sport_config = None - self.api_extractor = None - self.data_source = None + self.api_extractor: APIDataExtractor + self.data_source: DataSource self.mode_config = config.get(f"{sport_key}_scoreboard", {}) # Changed config key self.is_enabled = self.mode_config.get("enabled", False) self.show_odds = self.mode_config.get("show_odds", False) @@ -52,6 +54,7 @@ class SportsCore: "recent_games_to_show", 5) # Show last 5 games self.upcoming_games_to_show = self.mode_config.get( "upcoming_games_to_show", 10) # Show next 10 games + self.show_favorite_teams_only = self.mode_config.get("show_favorite_teams_only", False) self.session = requests.Session() retry_strategy = Retry( @@ -257,8 +260,10 @@ class SportsCore: def _load_and_resize_logo(self, team_id: str, team_abbrev: str, logo_path: Path, logo_url: str | None ) -> Optional[Image.Image]: """Load and resize a team logo, with caching and automatic download if missing.""" - self.logger.debug(f"Logo path: {logo_path}") + if team_abbrev in self._logo_cache: + self.logger.debug(f"Using cached logo for {team_abbrev}") + return self._logo_cache[team_abbrev] try: # Try different filename variations first (for cases like TA&M vs TAANDM) @@ -302,118 +307,15 @@ class SportsCore: except Exception as e: self.logger.error(f"Error loading logo for {team_abbrev}: {e}", exc_info=True) return None - - def _fetch_data(self) -> Optional[Dict]: - """Fetch data using the new architecture components.""" - try: - # Use the data source to fetch live games - live_games = self.data_source.fetch_live_games(self.sport_key, self.sport_key) - - if not live_games: - self.logger.debug(f"No live games found for {self.sport_key}") - return None - - # Use the API extractor to process each game - processed_games = [] - for game_event in live_games: - game_details = self.api_extractor.extract_game_details(game_event) - if game_details: - # Add sport-specific fields - sport_fields = self.api_extractor.get_sport_specific_fields(game_event) - game_details.update(sport_fields) - - # Fetch odds if enabled - if self.show_odds: - self._fetch_odds(game_details, self.sport_key, self.sport_key) - - processed_games.append(game_details) - - if processed_games: - self.logger.debug(f"Successfully processed {len(processed_games)} games for {self.sport_key}") - return { - 'games': processed_games, - 'sport': self.sport_key, - 'timestamp': time.time() - } - else: - self.logger.debug(f"No valid games processed for {self.sport_key}") - return None - - except Exception as e: - self.logger.error(f"Error fetching data for {self.sport_key}: {e}") - return None - def _get_partial_schedule_data(self, year: int) -> List[Dict]: - """Get schedule data using the new architecture components.""" - try: - # Calculate date range for the year - start_date = datetime(year, 1, 1) - end_date = datetime(year, 12, 31) - - # Use the data source to fetch schedule - schedule_games = self.data_source.fetch_schedule( - self.sport_key, - self.sport_key, - (start_date, end_date) - ) - - if not schedule_games: - self.logger.debug(f"No schedule data found for {self.sport_key} in {year}") - return [] - - # Use the API extractor to process each game - processed_games = [] - for game_event in schedule_games: - game_details = self.api_extractor.extract_game_details(game_event) - if game_details: - # Add sport-specific fields - sport_fields = self.api_extractor.get_sport_specific_fields(game_event) - game_details.update(sport_fields) - processed_games.append(game_details) - - self.logger.debug(f"Successfully processed {len(processed_games)} schedule games for {self.sport_key} in {year}") - return processed_games - - except Exception as e: - self.logger.error(f"Error fetching schedule data for {self.sport_key} in {year}: {e}") - return [] - - def _fetch_immediate_games(self) -> List[Dict]: - """Fetch immediate games using the new architecture components.""" - try: - # Use the data source to fetch live games - live_games = self.data_source.fetch_live_games(self.sport_key, self.sport_key) - - if not live_games: - self.logger.debug(f"No immediate games found for {self.sport_key}") - return [] - - # Use the API extractor to process each game - processed_games = [] - for game_event in live_games: - game_details = self.api_extractor.extract_game_details(game_event) - if game_details: - # Add sport-specific fields - sport_fields = self.api_extractor.get_sport_specific_fields(game_event) - game_details.update(sport_fields) - processed_games.append(game_details) - - self.logger.debug(f"Successfully processed {len(processed_games)} immediate games for {self.sport_key}") - return processed_games - - except Exception as e: - self.logger.error(f"Error fetching immediate games for {self.sport_key}: {e}") - return [] - - def _fetch_game_odds(self, game: Dict) -> None: + def _fetch_odds(self, game: Dict) -> None: """Fetch odds for a specific game using the new architecture.""" try: if not self.show_odds: return # Check if we should only fetch for favorite teams - is_favorites_only = self.mode_config.get("show_favorite_teams_only", False) - if is_favorites_only: + if self.show_favorite_teams_only: home_abbr = game.get('home_abbr') away_abbr = game.get('away_abbr') if not (home_abbr in self.favorite_teams or away_abbr in self.favorite_teams): @@ -427,8 +329,8 @@ class SportsCore: # Fetch odds using OddsManager odds_data = self.odds_manager.get_odds( - sport=self.sport_key, - league=self.sport_key, + sport=self.sport, + league=self.league, event_id=game['id'], update_interval_seconds=update_interval ) @@ -442,47 +344,6 @@ class SportsCore: except Exception as e: self.logger.error(f"Error fetching odds for game {game.get('id', 'N/A')}: {e}") - def _fetch_odds(self, game: Dict, sport: str, league: str) -> None: - """Fetch odds for a specific game if conditions are met.""" - # Check if odds should be shown for this sport - if not self.show_odds: - return - - # Check if we should only fetch for favorite teams - is_favorites_only = self.mode_config.get("show_favorite_teams_only", False) - if is_favorites_only: - home_abbr = game.get('home_abbr') - away_abbr = game.get('away_abbr') - if not (home_abbr in self.favorite_teams or away_abbr in self.favorite_teams): - self.logger.debug(f"Skipping odds fetch for non-favorite game in favorites-only mode: {away_abbr}@{home_abbr}") - return - - self.logger.debug(f"Proceeding with odds fetch for game: {game.get('id', 'N/A')}") - - # Fetch odds using OddsManager (ESPN API) - try: - # Determine update interval based on game state - is_live = game.get('status', '').lower() == 'in' - update_interval = self.mode_config.get("live_odds_update_interval", 60) if is_live \ - else self.mode_config.get("odds_update_interval", 3600) - - odds_data = self.odds_manager.get_odds( - sport=sport, - league=league, - event_id=game['id'], - update_interval_seconds=update_interval - ) - - if odds_data: - game['odds'] = odds_data - self.logger.debug(f"Successfully fetched and attached odds for game {game['id']}") - else: - self.logger.debug(f"No odds data returned for game {game['id']}") - - except Exception as e: - self.logger.error(f"Error fetching odds for game {game.get('id', 'N/A')}: {e}") - - def _get_timezone(self): try: timezone_str = self.config.get('timezone', 'UTC') @@ -500,24 +361,41 @@ class SportsCore: def _fetch_team_rankings(self) -> Dict[str, int]: """Fetch team rankings using the new architecture components.""" + current_time = time.time() + + # Check if we have cached rankings that are still valid + if (self._team_rankings_cache and + current_time - self._rankings_cache_timestamp < self._rankings_cache_duration): + return self._team_rankings_cache + try: - # Use the data source to fetch standings - standings_data = self.data_source.fetch_standings(self.sport_key, self.sport_key) + data = self.data_source.fetch_standings(self.sport, self.league) - if not standings_data: - self.logger.debug(f"No standings data found for {self.sport_key}") - return {} - - # Extract rankings from standings data rankings = {} - # This would need to be implemented based on the specific data structure - # returned by each data source + rankings_data = data.get('rankings', []) - self.logger.debug(f"Successfully fetched rankings for {self.sport_key}") + if rankings_data: + # Use the first ranking (usually AP Top 25) + first_ranking = rankings_data[0] + teams = first_ranking.get('ranks', []) + + for team_data in teams: + team_info = team_data.get('team', {}) + team_abbr = team_info.get('abbreviation', '') + current_rank = team_data.get('current', 0) + + if team_abbr and current_rank > 0: + rankings[team_abbr] = current_rank + + # Cache the results + self._team_rankings_cache = rankings + self._rankings_cache_timestamp = current_time + + self.logger.debug(f"Fetched rankings for {len(rankings)} teams") return rankings except Exception as e: - self.logger.error(f"Error fetching team rankings for {self.sport_key}: {e}") + self.logger.error(f"Error fetching team rankings: {e}") return {} def _extract_game_details_common(self, game_event: Dict) -> tuple[Dict | None, Dict | None, Dict | None, Dict | None, Dict | None]: @@ -617,25 +495,28 @@ class SportsCore: # def display(self, force_clear=False): # pass - def _fetch_todays_games(self, sport: str, league: str) -> Optional[Dict]: + def _fetch_data(self) -> Optional[Dict]: + pass + + def _fetch_todays_games(self) -> Optional[Dict]: """Fetch only today's games for live updates (not entire season).""" try: now = datetime.now() formatted_date = now.strftime("%Y%m%d") # Fetch todays games only - url = f"https://site.api.espn.com/apis/site/v2/sports/{sport}/{league}/scoreboard" + url = f"https://site.api.espn.com/apis/site/v2/sports/{self.sport}/{self.league}/scoreboard" response = self.session.get(url, params={"dates": formatted_date, "limit": 1000}, headers=self.headers, timeout=10) response.raise_for_status() data = response.json() events = data.get('events', []) - self.logger.info(f"Fetched {len(events)} todays games for {sport} - {league}") + self.logger.info(f"Fetched {len(events)} todays games for {self.sport} - {self.league}") return {'events': events} except requests.exceptions.RequestException as e: - self.logger.error(f"API error fetching todays games for {sport} - {league}: {e}") + self.logger.error(f"API error fetching todays games for {self.sport} - {self.league}: {e}") return None - def _get_weeks_data(self, sport: str, league: str) -> Optional[Dict]: + def _get_weeks_data(self) -> Optional[Dict]: """ Get partial data for immediate display while background fetch is in progress. This fetches current/recent games only for quick response. @@ -648,7 +529,7 @@ class SportsCore: start_date = now + timedelta(weeks=-2) end_date = now + timedelta(weeks=1) date_str = f"{start_date.strftime('%Y%m%d')}-{end_date.strftime('%Y%m%d')}" - url = f"https://site.api.espn.com/apis/site/v2/sports/{sport}/{league}/scoreboard" + url = f"https://site.api.espn.com/apis/site/v2/sports/{self.sport}/{self.league}/scoreboard" response = self.session.get(url, params={"dates": date_str, "limit": 1000},headers=self.headers, timeout=10) response.raise_for_status() data = response.json() @@ -659,7 +540,7 @@ class SportsCore: return {'events': immediate_events} except requests.exceptions.RequestException as e: - self.logger.warning(f"Error fetching this weeks games for {sport} - {league} - {date_str}: {e}") + self.logger.warning(f"Error fetching this weeks games for {self.sport} - {self.league} - {date_str}: {e}") return None class SportsUpcoming(SportsCore): @@ -713,7 +594,7 @@ class SportsUpcoming(SportsCore): # Filter criteria: must be upcoming ('pre' state) if game and game['is_upcoming']: # Only fetch odds for games that will be displayed - if self.mode_config.get("show_favorite_teams_only", False): + if self.show_favorite_teams_only: if not self.favorite_teams: continue if game['home_abbr'] not in self.favorite_teams and game['away_abbr'] not in self.favorite_teams: @@ -724,7 +605,7 @@ class SportsUpcoming(SportsCore): game['away_abbr'] in self.favorite_teams): favorite_games_found += 1 if self.show_odds: - self._fetch_game_odds(game) + self._fetch_odds(game) # Enhanced logging for debugging self.logger.info(f"Found {all_upcoming_games} total upcoming games in data") @@ -771,7 +652,7 @@ class SportsUpcoming(SportsCore): self.logger.info(f"Found {favorite_games_found} favorite team upcoming games") # Filter for favorite teams only if the config is set - if self.mode_config.get("show_favorite_teams_only", False): + if self.show_favorite_teams_only: # Get all games involving favorite teams favorite_team_games = [game for game in processed_games if game['home_abbr'] in self.favorite_teams or @@ -926,8 +807,7 @@ class SportsUpcoming(SportsCore): if away_abbr: if self.show_ranking and self.show_records: # When both rankings and records are enabled, rankings replace records completely - rankings = self._fetch_team_rankings() - away_rank = rankings.get(away_abbr, 0) + away_rank = self._team_rankings_cache.get(away_abbr, 0) if away_rank > 0: away_text = f"#{away_rank}" else: @@ -935,8 +815,7 @@ class SportsUpcoming(SportsCore): away_text = '' elif self.show_ranking: # Show ranking only if available - rankings = self._fetch_team_rankings() - away_rank = rankings.get(away_abbr, 0) + away_rank = rankself._team_rankings_cacheings.get(away_abbr, 0) if away_rank > 0: away_text = f"#{away_rank}" else: @@ -956,8 +835,7 @@ class SportsUpcoming(SportsCore): if home_abbr: if self.show_ranking and self.show_records: # When both rankings and records are enabled, rankings replace records completely - rankings = self._fetch_team_rankings() - home_rank = rankings.get(home_abbr, 0) + home_rank = self._team_rankings_cache.get(home_abbr, 0) if home_rank > 0: home_text = f"#{home_rank}" else: @@ -965,8 +843,7 @@ class SportsUpcoming(SportsCore): home_text = '' elif self.show_ranking: # Show ranking only if available - rankings = self._fetch_team_rankings() - home_rank = rankings.get(home_abbr, 0) + home_rank = self._team_rankings_cache.get(home_abbr, 0) if home_rank > 0: home_text = f"#{home_rank}" else: @@ -1227,8 +1104,7 @@ class SportsRecent(SportsCore): if away_abbr: if self.show_ranking and self.show_records: # When both rankings and records are enabled, rankings replace records completely - rankings = self._fetch_team_rankings() - away_rank = rankings.get(away_abbr, 0) + away_rank = self._team_rankings_cache.get(away_abbr, 0) if away_rank > 0: away_text = f"#{away_rank}" else: @@ -1236,8 +1112,7 @@ class SportsRecent(SportsCore): away_text = '' elif self.show_ranking: # Show ranking only if available - rankings = self._fetch_team_rankings() - away_rank = rankings.get(away_abbr, 0) + away_rank = self._team_rankings_cache.get(away_abbr, 0) if away_rank > 0: away_text = f"#{away_rank}" else: @@ -1257,8 +1132,7 @@ class SportsRecent(SportsCore): if home_abbr: if self.show_ranking and self.show_records: # When both rankings and records are enabled, rankings replace records completely - rankings = self._fetch_team_rankings() - home_rank = rankings.get(home_abbr, 0) + home_rank = self._team_rankings_cache.get(home_abbr, 0) if home_rank > 0: home_text = f"#{home_rank}" else: @@ -1266,8 +1140,7 @@ class SportsRecent(SportsCore): home_text = '' elif self.show_ranking: # Show ranking only if available - rankings = self._fetch_team_rankings() - home_rank = rankings.get(home_abbr, 0) + home_rank = self._team_rankings_cache.get(home_abbr, 0) if home_rank > 0: home_text = f"#{home_rank}" else: diff --git a/src/mlb_manager.py b/src/mlb_manager.py index 6ec1398a..7537bc80 100644 --- a/src/mlb_manager.py +++ b/src/mlb_manager.py @@ -15,8 +15,8 @@ from src.odds_manager import OddsManager from src.background_data_service import get_background_service # Import baseball and standard sports classes -from .base_classes.baseball import Baseball, BaseballLive -from .base_classes.sports import SportsRecent, SportsUpcoming +from src.base_classes.baseball import Baseball, BaseballLive +from src.base_classes.sports import SportsRecent, SportsUpcoming # Import the API counter function from web interface try: @@ -40,7 +40,7 @@ class BaseMLBManager(Baseball): self.show_odds = self.mlb_config.get("show_odds", False) self.favorite_teams = self.mlb_config.get('favorite_teams', []) self.show_records = self.mlb_config.get('show_records', False) - + self.league = "mlb" # Store reference to config instead of creating new ConfigManager self.config_manager = None # Not used in this class self.odds_manager = OddsManager(self.cache_manager, self.config_manager) @@ -83,58 +83,6 @@ class BaseMLBManager(Baseball): self.background_enabled = False self.logger.info("[MLB] Background service disabled") - def _fetch_odds(self, game: Dict) -> None: - """Fetch odds for a game and attach it to the game dictionary.""" - # Check if odds should be shown for this sport - if not self.show_odds: - return - - # Check if we should only fetch for favorite teams - is_favorites_only = self.mlb_config.get("show_favorite_teams_only", False) - if is_favorites_only: - home_team = game.get('home_team') - away_team = game.get('away_team') - if not (home_team in self.favorite_teams or away_team in self.favorite_teams): - self.logger.debug(f"Skipping odds fetch for non-favorite game in favorites-only mode: {away_team}@{home_team}") - return - - self.logger.debug(f"Proceeding with odds fetch for game: {game.get('id', 'N/A')}") - - # Skip if odds are already attached to this game - if 'odds' in game and game['odds']: - return - - try: - game_id = game.get('id', 'N/A') - self.logger.info(f"Requesting odds for game ID: {game_id}") - - odds_data = self.odds_manager.get_odds( - sport="baseball", - league="mlb", - event_id=game_id - ) - if odds_data: - game['odds'] = odds_data - self.logger.info(f"Successfully attached odds to game {game_id}") - - # Check if the odds data has any non-null values - has_odds = False - if odds_data.get('spread') is not None: - has_odds = True - if odds_data.get('home_team_odds', {}).get('spread_odds') is not None: - has_odds = True - if odds_data.get('away_team_odds', {}).get('spread_odds') is not None: - has_odds = True - - if not has_odds: - self.logger.warning(f"Odds data returned for game {game_id} but all values are null") - else: - self.logger.info(f"Found actual odds data for game {game_id}") - else: - self.logger.warning(f"No odds data returned for game {game_id}") - except Exception as e: - self.logger.error(f"Error fetching odds for game {game.get('id', 'N/A')}: {e}") - def _get_team_logo(self, team_abbr: str) -> Optional[Image.Image]: """Get team logo from the configured directory.""" try: diff --git a/src/ncaa_baseball_managers.py b/src/ncaa_baseball_managers.py index 37b0d380..62a7c76c 100644 --- a/src/ncaa_baseball_managers.py +++ b/src/ncaa_baseball_managers.py @@ -7,15 +7,15 @@ from datetime import datetime, timedelta, timezone import os from PIL import Image, ImageDraw, ImageFont import numpy as np -from .cache_manager import CacheManager +from src.cache_manager import CacheManager from requests.adapters import HTTPAdapter from urllib3.util.retry import Retry from src.odds_manager import OddsManager import pytz # Import baseball and standard sports classes -from .base_classes.baseball import Baseball, BaseballLive -from .base_classes.sports import SportsRecent, SportsUpcoming +from src.base_classes.baseball import Baseball, BaseballLive +from src.base_classes.sports import SportsRecent, SportsUpcoming # Get logger logger = logging.getLogger(__name__) @@ -34,7 +34,8 @@ class BaseNCAABaseballManager(Baseball): self.show_odds = self.ncaa_baseball_config.get('show_odds', False) self.show_records = self.ncaa_baseball_config.get('show_records', False) self.favorite_teams = self.ncaa_baseball_config.get('favorite_teams', []) - + self.league = "college-baseball" + # Store reference to config instead of creating new ConfigManager self.config_manager = None # Not used in this class self.odds_manager = OddsManager(self.cache_manager, self.config_manager) @@ -63,34 +64,6 @@ class BaseNCAABaseballManager(Baseball): 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36' } - def _fetch_odds(self, game: Dict) -> None: - """Fetch odds for a game and attach it to the game dictionary.""" - # Check if odds should be shown for this sport - if not self.show_odds: - return - - # Check if we should only fetch for favorite teams - is_favorites_only = self.ncaa_baseball_config.get("show_favorite_teams_only", False) - if is_favorites_only: - home_team = game.get('home_team') - away_team = game.get('away_team') - if not (home_team in self.favorite_teams or away_team in self.favorite_teams): - self.logger.debug(f"Skipping odds fetch for non-favorite game in favorites-only mode: {away_team}@{home_team}") - return - - self.logger.debug(f"Proceeding with odds fetch for game: {game.get('id', 'N/A')}") - - try: - odds_data = self.odds_manager.get_odds( - sport="baseball", - league="college-baseball", - event_id=game["id"] - ) - if odds_data: - game['odds'] = odds_data - except Exception as e: - self.logger.error(f"Error fetching odds for game {game.get('id', 'N/A')}: {e}") - def _get_team_logo(self, team_abbr: str) -> Optional[Image.Image]: """Get team logo from the configured directory or generate a fallback.""" try: diff --git a/src/ncaa_fb_managers.py b/src/ncaa_fb_managers.py index be613cc0..8054fdd0 100644 --- a/src/ncaa_fb_managers.py +++ b/src/ncaa_fb_managers.py @@ -37,53 +37,11 @@ class BaseNCAAFBManager(Football): # Renamed class self.recent_enabled = display_modes.get("ncaa_fb_recent", False) self.upcoming_enabled = display_modes.get("ncaa_fb_upcoming", False) self.live_enabled = display_modes.get("ncaa_fb_live", False) - + self.league = "college-football" self.logger.info(f"Initialized NCAAFB manager with display dimensions: {self.display_width}x{self.display_height}") self.logger.info(f"Logo directory: {self.logo_dir}") self.logger.info(f"Display modes - Recent: {self.recent_enabled}, Upcoming: {self.upcoming_enabled}, Live: {self.live_enabled}") - - def _fetch_team_rankings(self) -> Dict[str, int]: - """Fetch current team rankings from ESPN API.""" - current_time = time.time() - - # Check if we have cached rankings that are still valid - if (self._team_rankings_cache and - current_time - self._rankings_cache_timestamp < self._rankings_cache_duration): - return self._team_rankings_cache - - try: - rankings_url = "https://site.api.espn.com/apis/site/v2/sports/football/college-football/rankings" - response = self.session.get(rankings_url, headers=self.headers, timeout=30) - response.raise_for_status() - data = response.json() - - rankings = {} - rankings_data = data.get('rankings', []) - - if rankings_data: - # Use the first ranking (usually AP Top 25) - first_ranking = rankings_data[0] - teams = first_ranking.get('ranks', []) - - for team_data in teams: - team_info = team_data.get('team', {}) - team_abbr = team_info.get('abbreviation', '') - current_rank = team_data.get('current', 0) - - if team_abbr and current_rank > 0: - rankings[team_abbr] = current_rank - - # Cache the results - self._team_rankings_cache = rankings - self._rankings_cache_timestamp = current_time - - self.logger.debug(f"Fetched rankings for {len(rankings)} teams") - return rankings - - except Exception as e: - self.logger.error(f"Error fetching team rankings: {e}") - return {} def _fetch_ncaa_fb_api_data(self, use_cache: bool = True) -> Optional[Dict]: """ @@ -159,7 +117,7 @@ class BaseNCAAFBManager(Football): # Renamed class self.background_fetch_requests[season_year] = request_id # For immediate response, try to get partial data - partial_data = self._get_weeks_data("college-football") + partial_data = self._get_weeks_data() if partial_data: return partial_data return None @@ -191,14 +149,10 @@ class BaseNCAAFBManager(Football): # Renamed class def _fetch_data(self) -> Optional[Dict]: """Fetch data using shared data mechanism or direct fetch for live.""" if isinstance(self, NCAAFBLiveManager): - return self._fetch_todays_games("college-football") + return self._fetch_todays_games() else: return self._fetch_ncaa_fb_api_data(use_cache=True) - - def _fetch_football_odds(self, game: Dict) -> None: - super()._fetch_odds(game, "college-football") - class NCAAFBLiveManager(BaseNCAAFBManager, FootballLive): # Renamed class """Manager for live NCAA FB games.""" # Updated docstring def __init__(self, config: Dict[str, Any], display_manager: DisplayManager, cache_manager: CacheManager): diff --git a/src/ncaam_hockey_managers.py b/src/ncaam_hockey_managers.py index e9edc5c3..2c6bd65a 100644 --- a/src/ncaam_hockey_managers.py +++ b/src/ncaam_hockey_managers.py @@ -42,53 +42,12 @@ class BaseNCAAMHockeyManager(Hockey): # Renamed class self.recent_enabled = display_modes.get("ncaam_hockey_recent", False) self.upcoming_enabled = display_modes.get("ncaam_hockey_upcoming", False) self.live_enabled = display_modes.get("ncaam_hockey_live", False) + self.league = "mens-college-hockey" self.logger.info(f"Initialized NCAAMHockey manager with display dimensions: {self.display_width}x{self.display_height}") self.logger.info(f"Logo directory: {self.logo_dir}") self.logger.info(f"Display modes - Recent: {self.recent_enabled}, Upcoming: {self.upcoming_enabled}, Live: {self.live_enabled}") - - def _fetch_team_rankings(self) -> Dict[str, int]: - """Fetch current team rankings from ESPN API.""" - current_time = time.time() - - # Check if we have cached rankings that are still valid - if (self._team_rankings_cache and - current_time - self._rankings_cache_timestamp < self._rankings_cache_duration): - return self._team_rankings_cache - - try: - rankings_url = "https://site.api.espn.com/apis/site/v2/sports/hockey/mens-college-hockey/rankings" - response = self.session.get(rankings_url, headers=self.headers, timeout=30) - response.raise_for_status() - data = response.json() - - rankings = {} - rankings_data = data.get('rankings', []) - - if rankings_data: - # Use the first ranking (usually AP Top 25) - first_ranking = rankings_data[0] - teams = first_ranking.get('ranks', []) - - for team_data in teams: - team_info = team_data.get('team', {}) - team_abbr = team_info.get('abbreviation', '') - current_rank = team_data.get('current', 0) - - if team_abbr and current_rank > 0: - rankings[team_abbr] = current_rank - - # Cache the results - self._team_rankings_cache = rankings - self._rankings_cache_timestamp = current_time - - self.logger.debug(f"Fetched rankings for {len(rankings)} teams") - return rankings - - except Exception as e: - self.logger.error(f"Error fetching team rankings: {e}") - return {} - + def _get_timezone(self): try: timezone_str = self.config.get('timezone', 'UTC') @@ -103,9 +62,6 @@ class BaseNCAAMHockeyManager(Hockey): # Renamed class self._last_warning_time = current_time return True return False - - def _fetch_odds(self, game: Dict) -> None: - super()._fetch_odds(game, "mens-college-hockey") def _fetch_ncaa_fb_api_data(self, use_cache: bool = True) -> Optional[Dict]: """ diff --git a/src/nfl_managers.py b/src/nfl_managers.py index 917cf42f..5557e57a 100644 --- a/src/nfl_managers.py +++ b/src/nfl_managers.py @@ -38,10 +38,7 @@ class BaseNFLManager(Football): # Renamed class self.logger.info(f"Initialized NFL manager with display dimensions: {self.display_width}x{self.display_height}") self.logger.info(f"Logo directory: {self.logo_dir}") self.logger.info(f"Display modes - Recent: {self.recent_enabled}, Upcoming: {self.upcoming_enabled}, Live: {self.live_enabled}") - - - def _fetch_football_odds(self, game: Dict) -> None: - super()._fetch_odds(game, "nfl") + self.league = "nfl" def _fetch_nfl_api_data(self, use_cache: bool = True) -> Optional[Dict]: """ @@ -114,7 +111,7 @@ class BaseNFLManager(Football): # Renamed class self.background_fetch_requests[season_year] = request_id # For immediate response, try to get partial data - partial_data = self._get_weeks_data("nfl") + partial_data = self._get_weeks_data() if partial_data: return partial_data @@ -148,7 +145,7 @@ class BaseNFLManager(Football): # Renamed class """Fetch data using shared data mechanism or direct fetch for live.""" if isinstance(self, NFLLiveManager): # Live games should fetch only current games, not entire season - return self._fetch_todays_games("nfl") + return self._fetch_todays_games() else: # Recent and Upcoming managers should use cached season data return self._fetch_nfl_api_data(use_cache=True) diff --git a/src/odds_manager.py b/src/odds_manager.py index 705d60f6..00fa2cd0 100644 --- a/src/odds_manager.py +++ b/src/odds_manager.py @@ -22,7 +22,9 @@ class OddsManager: self.logger = logging.getLogger(__name__) self.base_url = "https://sports.core.api.espn.com/v2/sports" - def get_odds(self, sport: str, league: str, event_id: str, update_interval_seconds=3600): + def get_odds(self, sport: str | None, league: str | None, event_id: str, update_interval_seconds=3600): + if sport is None or league is None: + raise ValueError("Sport and League cannot be None") cache_key = f"odds_espn_{sport}_{league}_{event_id}" # Check cache first