Fix odds and more (#81)

* fix(odds): Resolve incorrect sport and league parameters in _fetch_odds calls

- Fixed SportsCore._fetch_data() to call _fetch_odds(game) instead of _fetch_odds(game, sport_key, sport_key)
- Updated _fetch_odds method signature to accept only game parameter
- Added _fetch_odds_with_params helper method for sport-specific implementations
- Updated sport-specific managers to use correct sport and league parameters:
  - NFL: football/nfl
  - NCAA Football: football/college-football
  - NCAA Hockey: hockey/mens-college-hockey
- Ensures odds are fetched with correct ESPN API endpoints

Fixes #79

* Fix odds and other things

* update hockey

* fix rankings

* update imports

* Fix Logo Cache

* Add show_favorite_team_only attribute

---------

Co-authored-by: ChuckBuilds <33324927+ChuckBuilds@users.noreply.github.com>
Co-authored-by: Alex Resnick <adr8282@gmail.com>
This commit is contained in:
Alex Resnick
2025-09-28 15:34:21 -05:00
committed by GitHub
parent a98760f4d9
commit 1ee805901f
11 changed files with 127 additions and 502 deletions

View File

@@ -14,27 +14,11 @@ import logging
class Baseball(SportsCore): class Baseball(SportsCore):
"""Base class for baseball sports with common functionality.""" """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): 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) super().__init__(config, display_manager, cache_manager, logger, sport_key)
# Initialize baseball-specific architecture components # Initialize baseball-specific architecture components
self.sport_config = self.get_sport_config()
self.api_extractor = ESPNBaseballExtractor(logger) self.api_extractor = ESPNBaseballExtractor(logger)
# Choose data source based on sport (MLB uses MLB API, others use ESPN) # 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_bases = self.mode_config.get("show_bases", True)
self.show_count = self.mode_config.get("show_count", True) self.show_count = self.mode_config.get("show_count", True)
self.show_pitcher_batter = self.mode_config.get("show_pitcher_batter", False) self.show_pitcher_batter = self.mode_config.get("show_pitcher_batter", False)
self.sport = "baseball"
def get_sport_config(self) -> Dict[str, Any]:
"""Get baseball sport configuration."""
return self.SPORT_CONFIG.copy()
def _get_baseball_display_text(self, game: Dict) -> str: def _get_baseball_display_text(self, game: Dict) -> str:
"""Get baseball-specific display text.""" """Get baseball-specific display text."""

View File

@@ -65,8 +65,10 @@ class ESPNDataSource(DataSource):
def fetch_live_games(self, sport: str, league: str) -> List[Dict]: def fetch_live_games(self, sport: str, league: str) -> List[Dict]:
"""Fetch live games from ESPN API.""" """Fetch live games from ESPN API."""
try: try:
now = datetime.now()
formatted_date = now.strftime("%Y%m%d")
url = f"{self.base_url}/{sport}/{league}/scoreboard" 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() response.raise_for_status()
data = response.json() data = response.json()
@@ -90,7 +92,8 @@ class ESPNDataSource(DataSource):
url = f"{self.base_url}/{sport}/{league}/scoreboard" url = f"{self.base_url}/{sport}/{league}/scoreboard"
params = { 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) 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: def fetch_standings(self, sport: str, league: str) -> Dict:
"""Fetch standings from ESPN API.""" """Fetch standings from ESPN API."""
try: 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 = self.session.get(url, headers=self.get_headers(), timeout=15)
response.raise_for_status() response.raise_for_status()

View File

@@ -14,39 +14,13 @@ import requests
class Football(SportsCore): class Football(SportsCore):
"""Base class for football sports with common functionality.""" """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): 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) super().__init__(config, display_manager, cache_manager, logger, sport_key)
# Initialize football-specific architecture components # Initialize football-specific architecture components
self.sport_config = self.get_sport_config()
self.api_extractor = ESPNFootballExtractor(logger) self.api_extractor = ESPNFootballExtractor(logger)
self.data_source = ESPNDataSource(logger) self.data_source = ESPNDataSource(logger)
self.sport = "football"
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)
def _extract_game_details(self, game_event: Dict) -> Optional[Dict]: def _extract_game_details(self, game_event: Dict) -> Optional[Dict]:
"""Extract relevant game details from ESPN NCAA FB API response.""" """Extract relevant game details from ESPN NCAA FB API response."""
@@ -122,13 +96,6 @@ class Football(SportsCore):
elif status["type"]["state"] == "pre": elif status["type"]["state"] == "pre":
period_text = details.get("game_time", "") # Show time for upcoming 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({ details.update({
"period": period, "period": period,
"period_text": period_text, # Formatted quarter/status "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) logging.error(f"Error extracting game details: {e} from event: {game_event.get('id')}", exc_info=True)
return None 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): class FootballLive(Football):
def __init__(self, config: Dict[str, Any], display_manager: DisplayManager, cache_manager: CacheManager, logger: logging.Logger, sport_key: str): 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) super().__init__(config, display_manager, cache_manager, logger, sport_key)
@@ -250,21 +206,19 @@ class FootballLive(Football):
data = self._fetch_data() data = self._fetch_data()
new_live_games = [] new_live_games = []
if data and "events" in data: if data and "events" in data:
for event in data["events"]: for game in data["events"]:
details = self._extract_game_details(event) details = self._extract_game_details(game)
if details and (details["is_live"] or details["is_halftime"]): if details and (details["is_live"] or details["is_halftime"]):
# If show_favorite_teams_only is true, only add if it's a favorite. # If show_favorite_teams_only is true, only add if it's a favorite.
# Otherwise, add all games. # 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 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) new_live_games.append(details)
else: else:
if self.show_odds:
self._fetch_game_odds(details)
new_live_games.append(details) new_live_games.append(details)
for game in new_live_games:
if self.show_odds:
self._fetch_odds(game)
# Log changes or periodically # Log changes or periodically
current_time_for_log = time.time() # Use a consistent time for logging comparison current_time_for_log = time.time() # Use a consistent time for logging comparison
should_log = ( should_log = (
@@ -276,12 +230,12 @@ class FootballLive(Football):
if should_log: if should_log:
if new_live_games: 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}.") 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 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')})") self.logger.info(f" - {game_info['away_abbr']}@{game_info['home_abbr']} ({game_info.get('status_text', 'N/A')})")
else: 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.logger.info(f"No live/halftime games found for {filter_text}.")
self.last_log_time = current_time_for_log self.last_log_time = current_time_for_log
@@ -504,8 +458,7 @@ class FootballLive(Football):
if away_abbr: if away_abbr:
if self.show_ranking and self.show_records: if self.show_ranking and self.show_records:
# When both rankings and records are enabled, rankings replace records completely # When both rankings and records are enabled, rankings replace records completely
rankings = self._fetch_team_rankings() away_rank = self._team_rankings_cache.get(away_abbr, 0)
away_rank = rankings.get(away_abbr, 0)
if away_rank > 0: if away_rank > 0:
away_text = f"#{away_rank}" away_text = f"#{away_rank}"
else: else:
@@ -513,8 +466,7 @@ class FootballLive(Football):
away_text = '' away_text = ''
elif self.show_ranking: elif self.show_ranking:
# Show ranking only if available # Show ranking only if available
rankings = self._fetch_team_rankings() away_rank = self._team_rankings_cache.get(away_abbr, 0)
away_rank = rankings.get(away_abbr, 0)
if away_rank > 0: if away_rank > 0:
away_text = f"#{away_rank}" away_text = f"#{away_rank}"
else: else:
@@ -534,8 +486,7 @@ class FootballLive(Football):
if home_abbr: if home_abbr:
if self.show_ranking and self.show_records: if self.show_ranking and self.show_records:
# When both rankings and records are enabled, rankings replace records completely # When both rankings and records are enabled, rankings replace records completely
rankings = self._fetch_team_rankings() home_rank = self._team_rankings_cache.get(home_abbr, 0)
home_rank = rankings.get(home_abbr, 0)
if home_rank > 0: if home_rank > 0:
home_text = f"#{home_rank}" home_text = f"#{home_rank}"
else: else:
@@ -543,8 +494,7 @@ class FootballLive(Football):
home_text = '' home_text = ''
elif self.show_ranking: elif self.show_ranking:
# Show ranking only if available # Show ranking only if available
rankings = self._fetch_team_rankings() home_rank = self._team_rankings_cache.get(home_abbr, 0)
home_rank = rankings.get(home_abbr, 0)
if home_rank > 0: if home_rank > 0:
home_text = f"#{home_rank}" home_text = f"#{home_rank}"
else: else:

View File

@@ -12,36 +12,13 @@ from src.base_classes.data_sources import ESPNDataSource
class Hockey(SportsCore): class Hockey(SportsCore):
"""Base class for hockey sports with common functionality.""" """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): 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) super().__init__(config, display_manager, cache_manager, logger, sport_key)
# Initialize hockey-specific architecture components # Initialize hockey-specific architecture components
self.sport_config = self.get_sport_config()
self.api_extractor = ESPNHockeyExtractor(logger) self.api_extractor = ESPNHockeyExtractor(logger)
self.data_source = ESPNDataSource(logger) self.data_source = ESPNDataSource(logger)
self.sport = "hockey"
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)
def _extract_game_details(self, game_event: Dict) -> Optional[Dict]: def _extract_game_details(self, game_event: Dict) -> Optional[Dict]:
@@ -53,11 +30,20 @@ class Hockey(SportsCore):
try: try:
competition = game_event["competitions"][0] competition = game_event["competitions"][0]
status = competition["status"] status = competition["status"]
powerplay = False
penalties = ""
shots_on_goal = {"home": 0, "away": 0}
if situation and status["type"]["state"] == "in": if situation and status["type"]["state"] == "in":
# Detect scoring events from status detail # Detect scoring events from status detail
status_detail = status["type"].get("detail", "").lower() status_detail = status["type"].get("detail", "").lower()
status_short = status["type"].get("shortDetail", "").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 # Format period/quarter
period = status.get("period", 0) period = status.get("period", 0)
@@ -79,7 +65,10 @@ class Hockey(SportsCore):
details.update({ details.update({
"period": period, "period": period,
"period_text": period_text, # Formatted quarter/status "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) # Basic validation (can be expanded)
@@ -118,6 +107,9 @@ class HockeyLive(Hockey):
if current_time - self.last_update >= interval: if current_time - self.last_update >= interval:
self.last_update = current_time self.last_update = current_time
if self.show_ranking:
self._fetch_team_rankings()
if self.test_mode: if self.test_mode:
# For testing, we'll just update the clock to show it's working # For testing, we'll just update the clock to show it's working
if self.current_game: if self.current_game:
@@ -149,7 +141,7 @@ class HockeyLive(Hockey):
new_live_games.append(details) new_live_games.append(details)
# Filter for favorite teams only if the config is set # 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 new_live_games = [game for game in new_live_games
if game['home_abbr'] in self.favorite_teams or if game['home_abbr'] in self.favorite_teams or
game['away_abbr'] in self.favorite_teams] game['away_abbr'] in self.favorite_teams]
@@ -163,12 +155,12 @@ class HockeyLive(Hockey):
if should_log: if should_log:
if new_live_games: 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}") self.logger.info(f"[NCAAMH] Found {len(new_live_games)} live games involving {filter_text}")
for game in new_live_games: 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']}") self.logger.info(f"[NCAAMH] Live game: {game['away_abbr']} vs {game['home_abbr']} - Period {game['period']}, {game['clock']}")
else: 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.logger.info(f"[NCAAMH] No live games found matching {filter_text}")
self.last_log_time = current_time self.last_log_time = current_time
@@ -287,8 +279,7 @@ class HockeyLive(Hockey):
if away_abbr: if away_abbr:
if self.show_ranking and self.show_records: if self.show_ranking and self.show_records:
# When both rankings and records are enabled, rankings replace records completely # When both rankings and records are enabled, rankings replace records completely
rankings = self._fetch_team_rankings() away_rank = self._team_rankings_cache.get(away_abbr, 0)
away_rank = rankings.get(away_abbr, 0)
if away_rank > 0: if away_rank > 0:
away_text = f"#{away_rank}" away_text = f"#{away_rank}"
else: else:
@@ -296,8 +287,7 @@ class HockeyLive(Hockey):
away_text = '' away_text = ''
elif self.show_ranking: elif self.show_ranking:
# Show ranking only if available # Show ranking only if available
rankings = self._fetch_team_rankings() away_rank = self._team_rankings_cache.get(away_abbr, 0)
away_rank = rankings.get(away_abbr, 0)
if away_rank > 0: if away_rank > 0:
away_text = f"#{away_rank}" away_text = f"#{away_rank}"
else: else:
@@ -317,8 +307,7 @@ class HockeyLive(Hockey):
if home_abbr: if home_abbr:
if self.show_ranking and self.show_records: if self.show_ranking and self.show_records:
# When both rankings and records are enabled, rankings replace records completely # When both rankings and records are enabled, rankings replace records completely
rankings = self._fetch_team_rankings() home_rank = self._team_rankings_cache.get(home_abbr, 0)
home_rank = rankings.get(home_abbr, 0)
if home_rank > 0: if home_rank > 0:
home_text = f"#{home_rank}" home_text = f"#{home_rank}"
else: else:
@@ -326,8 +315,7 @@ class HockeyLive(Hockey):
home_text = '' home_text = ''
elif self.show_ranking: elif self.show_ranking:
# Show ranking only if available # Show ranking only if available
rankings = self._fetch_team_rankings() home_rank = self._team_rankings_cache.get(home_abbr, 0)
home_rank = rankings.get(home_abbr, 0)
if home_rank > 0: if home_rank > 0:
home_text = f"#{home_rank}" home_text = f"#{home_rank}"
else: else:

View File

@@ -16,8 +16,8 @@ from src.logo_downloader import download_missing_logo, LogoDownloader
from pathlib import Path from pathlib import Path
# Import new architecture components (individual classes will import what they need) # Import new architecture components (individual classes will import what they need)
from .api_extractors import ESPNFootballExtractor, ESPNBaseballExtractor, ESPNHockeyExtractor from src.base_classes.api_extractors import APIDataExtractor
from .data_sources import ESPNDataSource, MLBAPIDataSource from src.base_classes.data_sources import DataSource
from src.dynamic_team_resolver import DynamicTeamResolver from src.dynamic_team_resolver import DynamicTeamResolver
class SportsCore: class SportsCore:
@@ -33,11 +33,13 @@ class SportsCore:
self.display_height = self.display_manager.matrix.height self.display_height = self.display_manager.matrix.height
self.sport_key = sport_key self.sport_key = sport_key
self.sport = None
self.league = None
# Initialize new architecture components (will be overridden by sport-specific classes) # Initialize new architecture components (will be overridden by sport-specific classes)
self.sport_config = None self.sport_config = None
self.api_extractor = None self.api_extractor: APIDataExtractor
self.data_source = None self.data_source: DataSource
self.mode_config = config.get(f"{sport_key}_scoreboard", {}) # Changed config key self.mode_config = config.get(f"{sport_key}_scoreboard", {}) # Changed config key
self.is_enabled = self.mode_config.get("enabled", False) self.is_enabled = self.mode_config.get("enabled", False)
self.show_odds = self.mode_config.get("show_odds", 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 "recent_games_to_show", 5) # Show last 5 games
self.upcoming_games_to_show = self.mode_config.get( self.upcoming_games_to_show = self.mode_config.get(
"upcoming_games_to_show", 10) # Show next 10 games "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() self.session = requests.Session()
retry_strategy = Retry( 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]: 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.""" """Load and resize a team logo, with caching and automatic download if missing."""
self.logger.debug(f"Logo path: {logo_path}") 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:
# Try different filename variations first (for cases like TA&M vs TAANDM) # Try different filename variations first (for cases like TA&M vs TAANDM)
@@ -303,117 +308,14 @@ class SportsCore:
self.logger.error(f"Error loading logo for {team_abbrev}: {e}", exc_info=True) self.logger.error(f"Error loading logo for {team_abbrev}: {e}", exc_info=True)
return None return None
def _fetch_data(self) -> Optional[Dict]: def _fetch_odds(self, game: Dict) -> None:
"""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:
"""Fetch odds for a specific game using the new architecture.""" """Fetch odds for a specific game using the new architecture."""
try: try:
if not self.show_odds: if not self.show_odds:
return return
# Check if we should only fetch for favorite teams # Check if we should only fetch for favorite teams
is_favorites_only = self.mode_config.get("show_favorite_teams_only", False) if self.show_favorite_teams_only:
if is_favorites_only:
home_abbr = game.get('home_abbr') home_abbr = game.get('home_abbr')
away_abbr = game.get('away_abbr') away_abbr = game.get('away_abbr')
if not (home_abbr in self.favorite_teams or away_abbr in self.favorite_teams): 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 # Fetch odds using OddsManager
odds_data = self.odds_manager.get_odds( odds_data = self.odds_manager.get_odds(
sport=self.sport_key, sport=self.sport,
league=self.sport_key, league=self.league,
event_id=game['id'], event_id=game['id'],
update_interval_seconds=update_interval update_interval_seconds=update_interval
) )
@@ -442,47 +344,6 @@ class SportsCore:
except Exception as e: except Exception as e:
self.logger.error(f"Error fetching odds for game {game.get('id', 'N/A')}: {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): def _get_timezone(self):
try: try:
timezone_str = self.config.get('timezone', 'UTC') timezone_str = self.config.get('timezone', 'UTC')
@@ -500,24 +361,41 @@ class SportsCore:
def _fetch_team_rankings(self) -> Dict[str, int]: def _fetch_team_rankings(self) -> Dict[str, int]:
"""Fetch team rankings using the new architecture components.""" """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: try:
# Use the data source to fetch standings data = self.data_source.fetch_standings(self.sport, self.league)
standings_data = self.data_source.fetch_standings(self.sport_key, self.sport_key)
if not standings_data:
self.logger.debug(f"No standings data found for {self.sport_key}")
return {}
# Extract rankings from standings data
rankings = {} rankings = {}
# This would need to be implemented based on the specific data structure rankings_data = data.get('rankings', [])
# returned by each data source
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 return rankings
except Exception as e: 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 {} return {}
def _extract_game_details_common(self, game_event: Dict) -> tuple[Dict | None, Dict | None, Dict | None, Dict | None, Dict | None]: 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): # def display(self, force_clear=False):
# pass # 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).""" """Fetch only today's games for live updates (not entire season)."""
try: try:
now = datetime.now() now = datetime.now()
formatted_date = now.strftime("%Y%m%d") formatted_date = now.strftime("%Y%m%d")
# Fetch todays games only # 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 = self.session.get(url, params={"dates": formatted_date, "limit": 1000}, headers=self.headers, timeout=10)
response.raise_for_status() response.raise_for_status()
data = response.json() data = response.json()
events = data.get('events', []) 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} return {'events': events}
except requests.exceptions.RequestException as e: 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 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. Get partial data for immediate display while background fetch is in progress.
This fetches current/recent games only for quick response. This fetches current/recent games only for quick response.
@@ -648,7 +529,7 @@ class SportsCore:
start_date = now + timedelta(weeks=-2) start_date = now + timedelta(weeks=-2)
end_date = now + timedelta(weeks=1) end_date = now + timedelta(weeks=1)
date_str = f"{start_date.strftime('%Y%m%d')}-{end_date.strftime('%Y%m%d')}" 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 = self.session.get(url, params={"dates": date_str, "limit": 1000},headers=self.headers, timeout=10)
response.raise_for_status() response.raise_for_status()
data = response.json() data = response.json()
@@ -659,7 +540,7 @@ class SportsCore:
return {'events': immediate_events} return {'events': immediate_events}
except requests.exceptions.RequestException as e: 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 return None
class SportsUpcoming(SportsCore): class SportsUpcoming(SportsCore):
@@ -713,7 +594,7 @@ class SportsUpcoming(SportsCore):
# Filter criteria: must be upcoming ('pre' state) # Filter criteria: must be upcoming ('pre' state)
if game and game['is_upcoming']: if game and game['is_upcoming']:
# Only fetch odds for games that will be displayed # 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: if not self.favorite_teams:
continue continue
if game['home_abbr'] not in self.favorite_teams and game['away_abbr'] not in self.favorite_teams: 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): game['away_abbr'] in self.favorite_teams):
favorite_games_found += 1 favorite_games_found += 1
if self.show_odds: if self.show_odds:
self._fetch_game_odds(game) self._fetch_odds(game)
# Enhanced logging for debugging # Enhanced logging for debugging
self.logger.info(f"Found {all_upcoming_games} total upcoming games in data") 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") self.logger.info(f"Found {favorite_games_found} favorite team upcoming games")
# Filter for favorite teams only if the config is set # 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 # Get all games involving favorite teams
favorite_team_games = [game for game in processed_games favorite_team_games = [game for game in processed_games
if game['home_abbr'] in self.favorite_teams or if game['home_abbr'] in self.favorite_teams or
@@ -926,8 +807,7 @@ class SportsUpcoming(SportsCore):
if away_abbr: if away_abbr:
if self.show_ranking and self.show_records: if self.show_ranking and self.show_records:
# When both rankings and records are enabled, rankings replace records completely # When both rankings and records are enabled, rankings replace records completely
rankings = self._fetch_team_rankings() away_rank = self._team_rankings_cache.get(away_abbr, 0)
away_rank = rankings.get(away_abbr, 0)
if away_rank > 0: if away_rank > 0:
away_text = f"#{away_rank}" away_text = f"#{away_rank}"
else: else:
@@ -935,8 +815,7 @@ class SportsUpcoming(SportsCore):
away_text = '' away_text = ''
elif self.show_ranking: elif self.show_ranking:
# Show ranking only if available # Show ranking only if available
rankings = self._fetch_team_rankings() away_rank = rankself._team_rankings_cacheings.get(away_abbr, 0)
away_rank = rankings.get(away_abbr, 0)
if away_rank > 0: if away_rank > 0:
away_text = f"#{away_rank}" away_text = f"#{away_rank}"
else: else:
@@ -956,8 +835,7 @@ class SportsUpcoming(SportsCore):
if home_abbr: if home_abbr:
if self.show_ranking and self.show_records: if self.show_ranking and self.show_records:
# When both rankings and records are enabled, rankings replace records completely # When both rankings and records are enabled, rankings replace records completely
rankings = self._fetch_team_rankings() home_rank = self._team_rankings_cache.get(home_abbr, 0)
home_rank = rankings.get(home_abbr, 0)
if home_rank > 0: if home_rank > 0:
home_text = f"#{home_rank}" home_text = f"#{home_rank}"
else: else:
@@ -965,8 +843,7 @@ class SportsUpcoming(SportsCore):
home_text = '' home_text = ''
elif self.show_ranking: elif self.show_ranking:
# Show ranking only if available # Show ranking only if available
rankings = self._fetch_team_rankings() home_rank = self._team_rankings_cache.get(home_abbr, 0)
home_rank = rankings.get(home_abbr, 0)
if home_rank > 0: if home_rank > 0:
home_text = f"#{home_rank}" home_text = f"#{home_rank}"
else: else:
@@ -1227,8 +1104,7 @@ class SportsRecent(SportsCore):
if away_abbr: if away_abbr:
if self.show_ranking and self.show_records: if self.show_ranking and self.show_records:
# When both rankings and records are enabled, rankings replace records completely # When both rankings and records are enabled, rankings replace records completely
rankings = self._fetch_team_rankings() away_rank = self._team_rankings_cache.get(away_abbr, 0)
away_rank = rankings.get(away_abbr, 0)
if away_rank > 0: if away_rank > 0:
away_text = f"#{away_rank}" away_text = f"#{away_rank}"
else: else:
@@ -1236,8 +1112,7 @@ class SportsRecent(SportsCore):
away_text = '' away_text = ''
elif self.show_ranking: elif self.show_ranking:
# Show ranking only if available # Show ranking only if available
rankings = self._fetch_team_rankings() away_rank = self._team_rankings_cache.get(away_abbr, 0)
away_rank = rankings.get(away_abbr, 0)
if away_rank > 0: if away_rank > 0:
away_text = f"#{away_rank}" away_text = f"#{away_rank}"
else: else:
@@ -1257,8 +1132,7 @@ class SportsRecent(SportsCore):
if home_abbr: if home_abbr:
if self.show_ranking and self.show_records: if self.show_ranking and self.show_records:
# When both rankings and records are enabled, rankings replace records completely # When both rankings and records are enabled, rankings replace records completely
rankings = self._fetch_team_rankings() home_rank = self._team_rankings_cache.get(home_abbr, 0)
home_rank = rankings.get(home_abbr, 0)
if home_rank > 0: if home_rank > 0:
home_text = f"#{home_rank}" home_text = f"#{home_rank}"
else: else:
@@ -1266,8 +1140,7 @@ class SportsRecent(SportsCore):
home_text = '' home_text = ''
elif self.show_ranking: elif self.show_ranking:
# Show ranking only if available # Show ranking only if available
rankings = self._fetch_team_rankings() home_rank = self._team_rankings_cache.get(home_abbr, 0)
home_rank = rankings.get(home_abbr, 0)
if home_rank > 0: if home_rank > 0:
home_text = f"#{home_rank}" home_text = f"#{home_rank}"
else: else:

View File

@@ -15,8 +15,8 @@ from src.odds_manager import OddsManager
from src.background_data_service import get_background_service from src.background_data_service import get_background_service
# Import baseball and standard sports classes # Import baseball and standard sports classes
from .base_classes.baseball import Baseball, BaseballLive from src.base_classes.baseball import Baseball, BaseballLive
from .base_classes.sports import SportsRecent, SportsUpcoming from src.base_classes.sports import SportsRecent, SportsUpcoming
# Import the API counter function from web interface # Import the API counter function from web interface
try: try:
@@ -40,7 +40,7 @@ class BaseMLBManager(Baseball):
self.show_odds = self.mlb_config.get("show_odds", False) self.show_odds = self.mlb_config.get("show_odds", False)
self.favorite_teams = self.mlb_config.get('favorite_teams', []) self.favorite_teams = self.mlb_config.get('favorite_teams', [])
self.show_records = self.mlb_config.get('show_records', False) self.show_records = self.mlb_config.get('show_records', False)
self.league = "mlb"
# Store reference to config instead of creating new ConfigManager # Store reference to config instead of creating new ConfigManager
self.config_manager = None # Not used in this class self.config_manager = None # Not used in this class
self.odds_manager = OddsManager(self.cache_manager, self.config_manager) self.odds_manager = OddsManager(self.cache_manager, self.config_manager)
@@ -83,58 +83,6 @@ class BaseMLBManager(Baseball):
self.background_enabled = False self.background_enabled = False
self.logger.info("[MLB] Background service disabled") 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]: def _get_team_logo(self, team_abbr: str) -> Optional[Image.Image]:
"""Get team logo from the configured directory.""" """Get team logo from the configured directory."""
try: try:

View File

@@ -7,15 +7,15 @@ from datetime import datetime, timedelta, timezone
import os import os
from PIL import Image, ImageDraw, ImageFont from PIL import Image, ImageDraw, ImageFont
import numpy as np import numpy as np
from .cache_manager import CacheManager from src.cache_manager import CacheManager
from requests.adapters import HTTPAdapter from requests.adapters import HTTPAdapter
from urllib3.util.retry import Retry from urllib3.util.retry import Retry
from src.odds_manager import OddsManager from src.odds_manager import OddsManager
import pytz import pytz
# Import baseball and standard sports classes # Import baseball and standard sports classes
from .base_classes.baseball import Baseball, BaseballLive from src.base_classes.baseball import Baseball, BaseballLive
from .base_classes.sports import SportsRecent, SportsUpcoming from src.base_classes.sports import SportsRecent, SportsUpcoming
# Get logger # Get logger
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -34,6 +34,7 @@ class BaseNCAABaseballManager(Baseball):
self.show_odds = self.ncaa_baseball_config.get('show_odds', False) self.show_odds = self.ncaa_baseball_config.get('show_odds', False)
self.show_records = self.ncaa_baseball_config.get('show_records', False) self.show_records = self.ncaa_baseball_config.get('show_records', False)
self.favorite_teams = self.ncaa_baseball_config.get('favorite_teams', []) self.favorite_teams = self.ncaa_baseball_config.get('favorite_teams', [])
self.league = "college-baseball"
# Store reference to config instead of creating new ConfigManager # Store reference to config instead of creating new ConfigManager
self.config_manager = None # Not used in this class self.config_manager = None # Not used in this class
@@ -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' '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]: def _get_team_logo(self, team_abbr: str) -> Optional[Image.Image]:
"""Get team logo from the configured directory or generate a fallback.""" """Get team logo from the configured directory or generate a fallback."""
try: try:

View File

@@ -37,54 +37,12 @@ class BaseNCAAFBManager(Football): # Renamed class
self.recent_enabled = display_modes.get("ncaa_fb_recent", False) self.recent_enabled = display_modes.get("ncaa_fb_recent", False)
self.upcoming_enabled = display_modes.get("ncaa_fb_upcoming", False) self.upcoming_enabled = display_modes.get("ncaa_fb_upcoming", False)
self.live_enabled = display_modes.get("ncaa_fb_live", 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"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"Logo directory: {self.logo_dir}")
self.logger.info(f"Display modes - Recent: {self.recent_enabled}, Upcoming: {self.upcoming_enabled}, Live: {self.live_enabled}") 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]: def _fetch_ncaa_fb_api_data(self, use_cache: bool = True) -> Optional[Dict]:
""" """
Fetches the full season schedule for NCAAFB using week-by-week approach to ensure Fetches the full season schedule for NCAAFB using week-by-week approach to ensure
@@ -159,7 +117,7 @@ class BaseNCAAFBManager(Football): # Renamed class
self.background_fetch_requests[season_year] = request_id self.background_fetch_requests[season_year] = request_id
# For immediate response, try to get partial data # 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: if partial_data:
return partial_data return partial_data
return None return None
@@ -191,14 +149,10 @@ class BaseNCAAFBManager(Football): # Renamed class
def _fetch_data(self) -> Optional[Dict]: def _fetch_data(self) -> Optional[Dict]:
"""Fetch data using shared data mechanism or direct fetch for live.""" """Fetch data using shared data mechanism or direct fetch for live."""
if isinstance(self, NCAAFBLiveManager): if isinstance(self, NCAAFBLiveManager):
return self._fetch_todays_games("college-football") return self._fetch_todays_games()
else: else:
return self._fetch_ncaa_fb_api_data(use_cache=True) 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 class NCAAFBLiveManager(BaseNCAAFBManager, FootballLive): # Renamed class
"""Manager for live NCAA FB games.""" # Updated docstring """Manager for live NCAA FB games.""" # Updated docstring
def __init__(self, config: Dict[str, Any], display_manager: DisplayManager, cache_manager: CacheManager): def __init__(self, config: Dict[str, Any], display_manager: DisplayManager, cache_manager: CacheManager):

View File

@@ -42,53 +42,12 @@ class BaseNCAAMHockeyManager(Hockey): # Renamed class
self.recent_enabled = display_modes.get("ncaam_hockey_recent", False) self.recent_enabled = display_modes.get("ncaam_hockey_recent", False)
self.upcoming_enabled = display_modes.get("ncaam_hockey_upcoming", False) self.upcoming_enabled = display_modes.get("ncaam_hockey_upcoming", False)
self.live_enabled = display_modes.get("ncaam_hockey_live", 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"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"Logo directory: {self.logo_dir}")
self.logger.info(f"Display modes - Recent: {self.recent_enabled}, Upcoming: {self.upcoming_enabled}, Live: {self.live_enabled}") 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): def _get_timezone(self):
try: try:
timezone_str = self.config.get('timezone', 'UTC') timezone_str = self.config.get('timezone', 'UTC')
@@ -104,9 +63,6 @@ class BaseNCAAMHockeyManager(Hockey): # Renamed class
return True return True
return False 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]: def _fetch_ncaa_fb_api_data(self, use_cache: bool = True) -> Optional[Dict]:
""" """
Fetches the full season schedule for NCAAMH, caches it, and then filters Fetches the full season schedule for NCAAMH, caches it, and then filters

View File

@@ -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"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"Logo directory: {self.logo_dir}")
self.logger.info(f"Display modes - Recent: {self.recent_enabled}, Upcoming: {self.upcoming_enabled}, Live: {self.live_enabled}") self.logger.info(f"Display modes - Recent: {self.recent_enabled}, Upcoming: {self.upcoming_enabled}, Live: {self.live_enabled}")
self.league = "nfl"
def _fetch_football_odds(self, game: Dict) -> None:
super()._fetch_odds(game, "nfl")
def _fetch_nfl_api_data(self, use_cache: bool = True) -> Optional[Dict]: 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 self.background_fetch_requests[season_year] = request_id
# For immediate response, try to get partial data # For immediate response, try to get partial data
partial_data = self._get_weeks_data("nfl") partial_data = self._get_weeks_data()
if partial_data: if partial_data:
return 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.""" """Fetch data using shared data mechanism or direct fetch for live."""
if isinstance(self, NFLLiveManager): if isinstance(self, NFLLiveManager):
# Live games should fetch only current games, not entire season # Live games should fetch only current games, not entire season
return self._fetch_todays_games("nfl") return self._fetch_todays_games()
else: else:
# Recent and Upcoming managers should use cached season data # Recent and Upcoming managers should use cached season data
return self._fetch_nfl_api_data(use_cache=True) return self._fetch_nfl_api_data(use_cache=True)

View File

@@ -22,7 +22,9 @@ class OddsManager:
self.logger = logging.getLogger(__name__) self.logger = logging.getLogger(__name__)
self.base_url = "https://sports.core.api.espn.com/v2/sports" 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}" cache_key = f"odds_espn_{sport}_{league}_{event_id}"
# Check cache first # Check cache first