mirror of
https://github.com/ChuckBuilds/LEDMatrix.git
synced 2026-04-10 13:02:59 +00:00
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:
@@ -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."""
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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]:
|
||||
"""
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user