mirror of
https://github.com/ChuckBuilds/LEDMatrix.git
synced 2026-04-10 21:03:01 +00:00
* Fix leaderboard scrolling performance after PR #39 merge - Restore leaderboard background updates that were accidentally removed - Fix duration method call from get_dynamic_duration() back to get_duration() - Restore proper fallback duration (600s instead of 60s) for leaderboard - Add back sports manager updates that feed data to leaderboard - Fix leaderboard defer_update priority to prevent scrolling lag These changes restore the leaderboard's dynamic duration calculation and ensure it gets proper background updates for smooth scrolling. * Apply PR #60 leaderboard performance optimizations - Change scroll_delay from 0.05s to 0.01s (100fps instead of 20fps) - Remove conditional scrolling logic - scroll every frame for smooth animation - Add FPS tracking and logging for performance monitoring - Restore high-framerate scrolling that was working before PR #39 merge These changes restore the smooth leaderboard scrolling performance that was achieved in PR #60 but was lost during the PR #39 merge. * Fix critical bugs identified in PR #39 review - Fix record filtering logic bug: change away_record == set to away_record in set - Fix incorrect sport specification: change 'nfl' to 'ncaa_fb' for NCAA Football data requests - These bugs were causing incorrect data display and wrong sport data fetching Addresses issues found by cursor bot in PR #39 review: - Record filtering was always evaluating to False - NCAA Football was fetching NFL data instead of college football data * Enhance cache clearing implementation from PR #39 - Add detailed logging to cache clearing process for better visibility - Log cache clearing statistics (memory entries and file count) - Improve startup logging to show cache clearing and data refetch process - Addresses legoguy1000's comment about preventing stale data issues This enhances the cache clearing implementation that was added in PR #39 to help prevent legacy cache issues and stale data problems. * continuing on base_classes - added baseball and api extractor since we don't use ESPN api for all sports * tests * fix missing duration * ensure milb, mlb, ncaa bb are all using new baseball base class properly * cursor rule to help with PR creation * fix image call * fix _scoreboard suffix on milb, MLB
This commit is contained in:
363
src/base_classes/api_extractors.py
Normal file
363
src/base_classes/api_extractors.py
Normal file
@@ -0,0 +1,363 @@
|
||||
"""
|
||||
Abstract API Data Extraction Layer
|
||||
|
||||
This module provides a pluggable system for extracting game data from different
|
||||
sports APIs. Each sport can have its own extractor that handles sport-specific
|
||||
fields and data structures.
|
||||
"""
|
||||
|
||||
from abc import ABC, abstractmethod
|
||||
from typing import Dict, Any, Optional, List
|
||||
import logging
|
||||
from datetime import datetime
|
||||
import pytz
|
||||
|
||||
class APIDataExtractor(ABC):
|
||||
"""Abstract base class for API data extraction."""
|
||||
|
||||
def __init__(self, logger: logging.Logger):
|
||||
self.logger = logger
|
||||
|
||||
@abstractmethod
|
||||
def extract_game_details(self, game_event: Dict) -> Optional[Dict]:
|
||||
"""Extract common game details from raw API data."""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def get_sport_specific_fields(self, game_event: Dict) -> Dict:
|
||||
"""Extract sport-specific fields (downs, innings, periods, etc.)."""
|
||||
pass
|
||||
|
||||
def _extract_common_details(self, game_event: Dict) -> tuple[Dict | None, Dict | None, Dict | None, Dict | None, Dict | None]:
|
||||
"""Extract common game details that work across all sports."""
|
||||
if not game_event:
|
||||
return None, None, None, None, None
|
||||
|
||||
try:
|
||||
competition = game_event["competitions"][0]
|
||||
status = competition["status"]
|
||||
competitors = competition["competitors"]
|
||||
game_date_str = game_event["date"]
|
||||
situation = competition.get("situation")
|
||||
|
||||
# Parse game time
|
||||
start_time_utc = None
|
||||
try:
|
||||
start_time_utc = datetime.fromisoformat(game_date_str.replace("Z", "+00:00"))
|
||||
except ValueError:
|
||||
self.logger.warning(f"Could not parse game date: {game_date_str}")
|
||||
|
||||
# Extract teams
|
||||
home_team = next((c for c in competitors if c.get("homeAway") == "home"), None)
|
||||
away_team = next((c for c in competitors if c.get("homeAway") == "away"), None)
|
||||
|
||||
if not home_team or not away_team:
|
||||
self.logger.warning(f"Could not find home or away team in event: {game_event.get('id')}")
|
||||
return None, None, None, None, None
|
||||
|
||||
return {
|
||||
"game_event": game_event,
|
||||
"competition": competition,
|
||||
"status": status,
|
||||
"situation": situation,
|
||||
"start_time_utc": start_time_utc,
|
||||
"home_team": home_team,
|
||||
"away_team": away_team
|
||||
}, home_team, away_team, status, situation
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error extracting common details: {e}")
|
||||
return None, None, None, None, None
|
||||
|
||||
|
||||
class ESPNFootballExtractor(APIDataExtractor):
|
||||
"""ESPN API extractor for football (NFL/NCAA)."""
|
||||
|
||||
def extract_game_details(self, game_event: Dict) -> Optional[Dict]:
|
||||
"""Extract football game details from ESPN API."""
|
||||
common_data, home_team, away_team, status, situation = self._extract_common_details(game_event)
|
||||
if not common_data:
|
||||
return None
|
||||
|
||||
try:
|
||||
# Extract basic team info
|
||||
home_abbr = home_team["team"]["abbreviation"]
|
||||
away_abbr = away_team["team"]["abbreviation"]
|
||||
home_score = home_team.get("score", "0")
|
||||
away_score = away_team.get("score", "0")
|
||||
|
||||
# Extract sport-specific fields
|
||||
sport_fields = self.get_sport_specific_fields(game_event)
|
||||
|
||||
# Build game details
|
||||
details = {
|
||||
"id": game_event.get("id"),
|
||||
"home_abbr": home_abbr,
|
||||
"away_abbr": away_abbr,
|
||||
"home_score": str(home_score),
|
||||
"away_score": str(away_score),
|
||||
"home_team_name": home_team["team"].get("displayName", ""),
|
||||
"away_team_name": away_team["team"].get("displayName", ""),
|
||||
"status_text": status["type"].get("shortDetail", ""),
|
||||
"is_live": status["type"]["state"] == "in",
|
||||
"is_final": status["type"]["state"] == "post",
|
||||
"is_upcoming": status["type"]["state"] == "pre",
|
||||
**sport_fields # Add sport-specific fields
|
||||
}
|
||||
|
||||
return details
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error extracting football game details: {e}")
|
||||
return None
|
||||
|
||||
def get_sport_specific_fields(self, game_event: Dict) -> Dict:
|
||||
"""Extract football-specific fields."""
|
||||
try:
|
||||
competition = game_event["competitions"][0]
|
||||
status = competition["status"]
|
||||
situation = competition.get("situation", {})
|
||||
|
||||
sport_fields = {
|
||||
"down": "",
|
||||
"distance": "",
|
||||
"possession": "",
|
||||
"is_redzone": False,
|
||||
"home_timeouts": 0,
|
||||
"away_timeouts": 0,
|
||||
"scoring_event": ""
|
||||
}
|
||||
|
||||
if situation and status["type"]["state"] == "in":
|
||||
sport_fields.update({
|
||||
"down": situation.get("down", ""),
|
||||
"distance": situation.get("distance", ""),
|
||||
"possession": situation.get("possession", ""),
|
||||
"is_redzone": situation.get("isRedZone", False),
|
||||
"home_timeouts": situation.get("homeTimeouts", 0),
|
||||
"away_timeouts": situation.get("awayTimeouts", 0)
|
||||
})
|
||||
|
||||
# Detect scoring events
|
||||
status_detail = status["type"].get("detail", "").lower()
|
||||
if "touchdown" in status_detail or "field goal" in status_detail:
|
||||
sport_fields["scoring_event"] = status_detail
|
||||
|
||||
return sport_fields
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error extracting football-specific fields: {e}")
|
||||
return {}
|
||||
|
||||
|
||||
class ESPNBaseballExtractor(APIDataExtractor):
|
||||
"""ESPN API extractor for baseball (MLB)."""
|
||||
|
||||
def extract_game_details(self, game_event: Dict) -> Optional[Dict]:
|
||||
"""Extract baseball game details from ESPN API."""
|
||||
common_data, home_team, away_team, status, situation = self._extract_common_details(game_event)
|
||||
if not common_data:
|
||||
return None
|
||||
|
||||
try:
|
||||
# Extract basic team info
|
||||
home_abbr = home_team["team"]["abbreviation"]
|
||||
away_abbr = away_team["team"]["abbreviation"]
|
||||
home_score = home_team.get("score", "0")
|
||||
away_score = away_team.get("score", "0")
|
||||
|
||||
# Extract sport-specific fields
|
||||
sport_fields = self.get_sport_specific_fields(game_event)
|
||||
|
||||
# Build game details
|
||||
details = {
|
||||
"id": game_event.get("id"),
|
||||
"home_abbr": home_abbr,
|
||||
"away_abbr": away_abbr,
|
||||
"home_score": str(home_score),
|
||||
"away_score": str(away_score),
|
||||
"home_team_name": home_team["team"].get("displayName", ""),
|
||||
"away_team_name": away_team["team"].get("displayName", ""),
|
||||
"status_text": status["type"].get("shortDetail", ""),
|
||||
"is_live": status["type"]["state"] == "in",
|
||||
"is_final": status["type"]["state"] == "post",
|
||||
"is_upcoming": status["type"]["state"] == "pre",
|
||||
**sport_fields # Add sport-specific fields
|
||||
}
|
||||
|
||||
return details
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error extracting baseball game details: {e}")
|
||||
return None
|
||||
|
||||
def get_sport_specific_fields(self, game_event: Dict) -> Dict:
|
||||
"""Extract baseball-specific fields."""
|
||||
try:
|
||||
competition = game_event["competitions"][0]
|
||||
status = competition["status"]
|
||||
situation = competition.get("situation", {})
|
||||
|
||||
sport_fields = {
|
||||
"inning": "",
|
||||
"outs": 0,
|
||||
"bases": "",
|
||||
"strikes": 0,
|
||||
"balls": 0,
|
||||
"pitcher": "",
|
||||
"batter": ""
|
||||
}
|
||||
|
||||
if situation and status["type"]["state"] == "in":
|
||||
sport_fields.update({
|
||||
"inning": situation.get("inning", ""),
|
||||
"outs": situation.get("outs", 0),
|
||||
"bases": situation.get("bases", ""),
|
||||
"strikes": situation.get("strikes", 0),
|
||||
"balls": situation.get("balls", 0),
|
||||
"pitcher": situation.get("pitcher", ""),
|
||||
"batter": situation.get("batter", "")
|
||||
})
|
||||
|
||||
return sport_fields
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error extracting baseball-specific fields: {e}")
|
||||
return {}
|
||||
|
||||
|
||||
class ESPNHockeyExtractor(APIDataExtractor):
|
||||
"""ESPN API extractor for hockey (NHL/NCAA)."""
|
||||
|
||||
def extract_game_details(self, game_event: Dict) -> Optional[Dict]:
|
||||
"""Extract hockey game details from ESPN API."""
|
||||
common_data, home_team, away_team, status, situation = self._extract_common_details(game_event)
|
||||
if not common_data:
|
||||
return None
|
||||
|
||||
try:
|
||||
# Extract basic team info
|
||||
home_abbr = home_team["team"]["abbreviation"]
|
||||
away_abbr = away_team["team"]["abbreviation"]
|
||||
home_score = home_team.get("score", "0")
|
||||
away_score = away_team.get("score", "0")
|
||||
|
||||
# Extract sport-specific fields
|
||||
sport_fields = self.get_sport_specific_fields(game_event)
|
||||
|
||||
# Build game details
|
||||
details = {
|
||||
"id": game_event.get("id"),
|
||||
"home_abbr": home_abbr,
|
||||
"away_abbr": away_abbr,
|
||||
"home_score": str(home_score),
|
||||
"away_score": str(away_score),
|
||||
"home_team_name": home_team["team"].get("displayName", ""),
|
||||
"away_team_name": away_team["team"].get("displayName", ""),
|
||||
"status_text": status["type"].get("shortDetail", ""),
|
||||
"is_live": status["type"]["state"] == "in",
|
||||
"is_final": status["type"]["state"] == "post",
|
||||
"is_upcoming": status["type"]["state"] == "pre",
|
||||
**sport_fields # Add sport-specific fields
|
||||
}
|
||||
|
||||
return details
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error extracting hockey game details: {e}")
|
||||
return None
|
||||
|
||||
def get_sport_specific_fields(self, game_event: Dict) -> Dict:
|
||||
"""Extract hockey-specific fields."""
|
||||
try:
|
||||
competition = game_event["competitions"][0]
|
||||
status = competition["status"]
|
||||
situation = competition.get("situation", {})
|
||||
|
||||
sport_fields = {
|
||||
"period": "",
|
||||
"period_text": "",
|
||||
"power_play": False,
|
||||
"penalties": "",
|
||||
"shots_on_goal": {"home": 0, "away": 0}
|
||||
}
|
||||
|
||||
if situation and status["type"]["state"] == "in":
|
||||
period = status.get("period", 0)
|
||||
period_text = ""
|
||||
if period == 1:
|
||||
period_text = "P1"
|
||||
elif period == 2:
|
||||
period_text = "P2"
|
||||
elif period == 3:
|
||||
period_text = "P3"
|
||||
elif period > 3:
|
||||
period_text = f"OT{period-3}"
|
||||
|
||||
sport_fields.update({
|
||||
"period": str(period),
|
||||
"period_text": period_text,
|
||||
"power_play": situation.get("isPowerPlay", False),
|
||||
"penalties": situation.get("penalties", ""),
|
||||
"shots_on_goal": {
|
||||
"home": situation.get("homeShots", 0),
|
||||
"away": situation.get("awayShots", 0)
|
||||
}
|
||||
})
|
||||
|
||||
return sport_fields
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error extracting hockey-specific fields: {e}")
|
||||
return {}
|
||||
|
||||
|
||||
class SoccerAPIExtractor(APIDataExtractor):
|
||||
"""Generic extractor for soccer APIs (different structure than ESPN)."""
|
||||
|
||||
def extract_game_details(self, game_event: Dict) -> Optional[Dict]:
|
||||
"""Extract soccer game details from various soccer APIs."""
|
||||
# This would need to be adapted based on the specific soccer API being used
|
||||
# For now, return a basic structure
|
||||
try:
|
||||
return {
|
||||
"id": game_event.get("id"),
|
||||
"home_abbr": game_event.get("home_team", {}).get("abbreviation", ""),
|
||||
"away_abbr": game_event.get("away_team", {}).get("abbreviation", ""),
|
||||
"home_score": str(game_event.get("home_score", "0")),
|
||||
"away_score": str(game_event.get("away_score", "0")),
|
||||
"home_team_name": game_event.get("home_team", {}).get("name", ""),
|
||||
"away_team_name": game_event.get("away_team", {}).get("name", ""),
|
||||
"status_text": game_event.get("status", ""),
|
||||
"is_live": game_event.get("is_live", False),
|
||||
"is_final": game_event.get("is_final", False),
|
||||
"is_upcoming": game_event.get("is_upcoming", False),
|
||||
**self.get_sport_specific_fields(game_event)
|
||||
}
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error extracting soccer game details: {e}")
|
||||
return None
|
||||
|
||||
def get_sport_specific_fields(self, game_event: Dict) -> Dict:
|
||||
"""Extract soccer-specific fields."""
|
||||
try:
|
||||
return {
|
||||
"half": game_event.get("half", ""),
|
||||
"stoppage_time": game_event.get("stoppage_time", ""),
|
||||
"cards": {
|
||||
"home_yellow": game_event.get("home_yellow_cards", 0),
|
||||
"away_yellow": game_event.get("away_yellow_cards", 0),
|
||||
"home_red": game_event.get("home_red_cards", 0),
|
||||
"away_red": game_event.get("away_red_cards", 0)
|
||||
},
|
||||
"possession": {
|
||||
"home": game_event.get("home_possession", 0),
|
||||
"away": game_event.get("away_possession", 0)
|
||||
}
|
||||
}
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error extracting soccer-specific fields: {e}")
|
||||
return {}
|
||||
|
||||
|
||||
# Factory function removed - sport classes now instantiate extractors directly
|
||||
165
src/base_classes/baseball.py
Normal file
165
src/base_classes/baseball.py
Normal file
@@ -0,0 +1,165 @@
|
||||
"""
|
||||
Baseball Base Classes
|
||||
|
||||
This module provides baseball-specific base classes that extend the core sports functionality
|
||||
with baseball-specific logic for innings, outs, bases, strikes, balls, etc.
|
||||
"""
|
||||
|
||||
from typing import Dict, Any, Optional, List
|
||||
from src.base_classes.sports import SportsCore
|
||||
from src.base_classes.api_extractors import ESPNBaseballExtractor
|
||||
from src.base_classes.data_sources import ESPNDataSource, MLBAPIDataSource
|
||||
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)
|
||||
if sport_key == 'mlb':
|
||||
self.data_source = MLBAPIDataSource(logger)
|
||||
else:
|
||||
self.data_source = ESPNDataSource(logger)
|
||||
|
||||
# Baseball-specific configuration
|
||||
self.show_innings = self.mode_config.get("show_innings", True)
|
||||
self.show_outs = self.mode_config.get("show_outs", True)
|
||||
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()
|
||||
|
||||
def _get_baseball_display_text(self, game: Dict) -> str:
|
||||
"""Get baseball-specific display text."""
|
||||
try:
|
||||
display_parts = []
|
||||
|
||||
# Inning information
|
||||
if self.show_innings:
|
||||
inning = game.get('inning', '')
|
||||
if inning:
|
||||
display_parts.append(f"Inning: {inning}")
|
||||
|
||||
# Outs information
|
||||
if self.show_outs:
|
||||
outs = game.get('outs', 0)
|
||||
if outs is not None:
|
||||
display_parts.append(f"Outs: {outs}")
|
||||
|
||||
# Bases information
|
||||
if self.show_bases:
|
||||
bases = game.get('bases', '')
|
||||
if bases:
|
||||
display_parts.append(f"Bases: {bases}")
|
||||
|
||||
# Count information
|
||||
if self.show_count:
|
||||
strikes = game.get('strikes', 0)
|
||||
balls = game.get('balls', 0)
|
||||
if strikes is not None and balls is not None:
|
||||
display_parts.append(f"Count: {balls}-{strikes}")
|
||||
|
||||
# Pitcher/Batter information
|
||||
if self.show_pitcher_batter:
|
||||
pitcher = game.get('pitcher', '')
|
||||
batter = game.get('batter', '')
|
||||
if pitcher:
|
||||
display_parts.append(f"Pitcher: {pitcher}")
|
||||
if batter:
|
||||
display_parts.append(f"Batter: {batter}")
|
||||
|
||||
return " | ".join(display_parts) if display_parts else ""
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error getting baseball display text: {e}")
|
||||
return ""
|
||||
|
||||
def _is_baseball_game_live(self, game: Dict) -> bool:
|
||||
"""Check if a baseball game is currently live."""
|
||||
try:
|
||||
# Check if game is marked as live
|
||||
is_live = game.get('is_live', False)
|
||||
if is_live:
|
||||
return True
|
||||
|
||||
# Check inning to determine if game is active
|
||||
inning = game.get('inning', '')
|
||||
if inning and inning != 'Final':
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error checking if baseball game is live: {e}")
|
||||
return False
|
||||
|
||||
def _get_baseball_game_status(self, game: Dict) -> str:
|
||||
"""Get baseball-specific game status."""
|
||||
try:
|
||||
status = game.get('status_text', '')
|
||||
inning = game.get('inning', '')
|
||||
|
||||
if self._is_baseball_game_live(game):
|
||||
if inning:
|
||||
return f"Live - {inning}"
|
||||
else:
|
||||
return "Live"
|
||||
elif game.get('is_final', False):
|
||||
return "Final"
|
||||
elif game.get('is_upcoming', False):
|
||||
return "Upcoming"
|
||||
else:
|
||||
return status
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error getting baseball game status: {e}")
|
||||
return ""
|
||||
|
||||
|
||||
class BaseballLive(Baseball):
|
||||
"""Base class for live baseball games."""
|
||||
|
||||
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)
|
||||
self.logger.info(f"{sport_key.upper()} Live Manager initialized")
|
||||
|
||||
def _should_show_baseball_game(self, game: Dict) -> bool:
|
||||
"""Determine if a baseball game should be shown."""
|
||||
try:
|
||||
# Only show live games
|
||||
if not self._is_baseball_game_live(game):
|
||||
return False
|
||||
|
||||
# Check if game meets display criteria
|
||||
return self._should_show_game(game)
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error checking if baseball game should be shown: {e}")
|
||||
return False
|
||||
|
||||
|
||||
288
src/base_classes/data_sources.py
Normal file
288
src/base_classes/data_sources.py
Normal file
@@ -0,0 +1,288 @@
|
||||
"""
|
||||
Pluggable Data Source Architecture
|
||||
|
||||
This module provides abstract data sources that can be plugged into the sports system
|
||||
to support different APIs and data providers.
|
||||
"""
|
||||
|
||||
from abc import ABC, abstractmethod
|
||||
from typing import Dict, Any, Optional, List
|
||||
import requests
|
||||
import logging
|
||||
from datetime import datetime, timedelta
|
||||
import time
|
||||
|
||||
class DataSource(ABC):
|
||||
"""Abstract base class for data sources."""
|
||||
|
||||
def __init__(self, logger: logging.Logger):
|
||||
self.logger = logger
|
||||
self.session = requests.Session()
|
||||
|
||||
# Configure retry strategy
|
||||
from requests.adapters import HTTPAdapter
|
||||
from urllib3.util.retry import Retry
|
||||
|
||||
retry_strategy = Retry(
|
||||
total=5,
|
||||
backoff_factor=1,
|
||||
status_forcelist=[429, 500, 502, 503, 504],
|
||||
)
|
||||
adapter = HTTPAdapter(max_retries=retry_strategy)
|
||||
self.session.mount("http://", adapter)
|
||||
self.session.mount("https://", adapter)
|
||||
|
||||
@abstractmethod
|
||||
def fetch_live_games(self, sport: str, league: str) -> List[Dict]:
|
||||
"""Fetch live games for a sport/league."""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def fetch_schedule(self, sport: str, league: str, date_range: tuple) -> List[Dict]:
|
||||
"""Fetch schedule for a sport/league within date range."""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def fetch_standings(self, sport: str, league: str) -> Dict:
|
||||
"""Fetch standings for a sport/league."""
|
||||
pass
|
||||
|
||||
def get_headers(self) -> Dict[str, str]:
|
||||
"""Get headers for API requests."""
|
||||
return {
|
||||
'User-Agent': 'LEDMatrix/1.0',
|
||||
'Accept': 'application/json'
|
||||
}
|
||||
|
||||
|
||||
class ESPNDataSource(DataSource):
|
||||
"""ESPN API data source."""
|
||||
|
||||
def __init__(self, logger: logging.Logger):
|
||||
super().__init__(logger)
|
||||
self.base_url = "https://site.api.espn.com/apis/site/v2/sports"
|
||||
|
||||
def fetch_live_games(self, sport: str, league: str) -> List[Dict]:
|
||||
"""Fetch live games from ESPN API."""
|
||||
try:
|
||||
url = f"{self.base_url}/{sport}/{league}/scoreboard"
|
||||
response = self.session.get(url, headers=self.get_headers(), timeout=15)
|
||||
response.raise_for_status()
|
||||
|
||||
data = response.json()
|
||||
events = data.get('events', [])
|
||||
|
||||
# Filter for live games
|
||||
live_events = [event for event in events
|
||||
if event.get('competitions', [{}])[0].get('status', {}).get('type', {}).get('state') == 'in']
|
||||
|
||||
self.logger.debug(f"Fetched {len(live_events)} live games for {sport}/{league}")
|
||||
return live_events
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error fetching live games from ESPN: {e}")
|
||||
return []
|
||||
|
||||
def fetch_schedule(self, sport: str, league: str, date_range: tuple) -> List[Dict]:
|
||||
"""Fetch schedule from ESPN API."""
|
||||
try:
|
||||
start_date, end_date = date_range
|
||||
url = f"{self.base_url}/{sport}/{league}/scoreboard"
|
||||
|
||||
params = {
|
||||
'dates': f"{start_date.strftime('%Y%m%d')}-{end_date.strftime('%Y%m%d')}"
|
||||
}
|
||||
|
||||
response = self.session.get(url, headers=self.get_headers(), params=params, timeout=15)
|
||||
response.raise_for_status()
|
||||
|
||||
data = response.json()
|
||||
events = data.get('events', [])
|
||||
|
||||
self.logger.debug(f"Fetched {len(events)} scheduled games for {sport}/{league}")
|
||||
return events
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error fetching schedule from ESPN: {e}")
|
||||
return []
|
||||
|
||||
def fetch_standings(self, sport: str, league: str) -> Dict:
|
||||
"""Fetch standings from ESPN API."""
|
||||
try:
|
||||
url = f"{self.base_url}/{sport}/{league}/standings"
|
||||
response = self.session.get(url, headers=self.get_headers(), timeout=15)
|
||||
response.raise_for_status()
|
||||
|
||||
data = response.json()
|
||||
self.logger.debug(f"Fetched standings for {sport}/{league}")
|
||||
return data
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error fetching standings from ESPN: {e}")
|
||||
return {}
|
||||
|
||||
|
||||
class MLBAPIDataSource(DataSource):
|
||||
"""MLB API data source."""
|
||||
|
||||
def __init__(self, logger: logging.Logger):
|
||||
super().__init__(logger)
|
||||
self.base_url = "https://statsapi.mlb.com/api/v1"
|
||||
|
||||
def fetch_live_games(self, sport: str, league: str) -> List[Dict]:
|
||||
"""Fetch live games from MLB API."""
|
||||
try:
|
||||
url = f"{self.base_url}/schedule"
|
||||
params = {
|
||||
'sportId': 1, # MLB
|
||||
'date': datetime.now().strftime('%Y-%m-%d'),
|
||||
'hydrate': 'game,team,venue,weather'
|
||||
}
|
||||
|
||||
response = self.session.get(url, headers=self.get_headers(), params=params, timeout=15)
|
||||
response.raise_for_status()
|
||||
|
||||
data = response.json()
|
||||
games = data.get('dates', [{}])[0].get('games', [])
|
||||
|
||||
# Filter for live games
|
||||
live_games = [game for game in games
|
||||
if game.get('status', {}).get('abstractGameState') == 'Live']
|
||||
|
||||
self.logger.debug(f"Fetched {len(live_games)} live games from MLB API")
|
||||
return live_games
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error fetching live games from MLB API: {e}")
|
||||
return []
|
||||
|
||||
def fetch_schedule(self, sport: str, league: str, date_range: tuple) -> List[Dict]:
|
||||
"""Fetch schedule from MLB API."""
|
||||
try:
|
||||
start_date, end_date = date_range
|
||||
url = f"{self.base_url}/schedule"
|
||||
|
||||
params = {
|
||||
'sportId': 1, # MLB
|
||||
'startDate': start_date.strftime('%Y-%m-%d'),
|
||||
'endDate': end_date.strftime('%Y-%m-%d'),
|
||||
'hydrate': 'game,team,venue'
|
||||
}
|
||||
|
||||
response = self.session.get(url, headers=self.get_headers(), params=params, timeout=15)
|
||||
response.raise_for_status()
|
||||
|
||||
data = response.json()
|
||||
all_games = []
|
||||
for date_data in data.get('dates', []):
|
||||
all_games.extend(date_data.get('games', []))
|
||||
|
||||
self.logger.debug(f"Fetched {len(all_games)} scheduled games from MLB API")
|
||||
return all_games
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error fetching schedule from MLB API: {e}")
|
||||
return []
|
||||
|
||||
def fetch_standings(self, sport: str, league: str) -> Dict:
|
||||
"""Fetch standings from MLB API."""
|
||||
try:
|
||||
url = f"{self.base_url}/standings"
|
||||
params = {
|
||||
'leagueId': 103, # American League
|
||||
'season': datetime.now().year,
|
||||
'standingsType': 'regularSeason'
|
||||
}
|
||||
|
||||
response = self.session.get(url, headers=self.get_headers(), params=params, timeout=15)
|
||||
response.raise_for_status()
|
||||
|
||||
data = response.json()
|
||||
self.logger.debug(f"Fetched standings from MLB API")
|
||||
return data
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error fetching standings from MLB API: {e}")
|
||||
return {}
|
||||
|
||||
|
||||
class SoccerAPIDataSource(DataSource):
|
||||
"""Soccer API data source (generic structure)."""
|
||||
|
||||
def __init__(self, logger: logging.Logger, api_key: str = None):
|
||||
super().__init__(logger)
|
||||
self.api_key = api_key
|
||||
self.base_url = "https://api.football-data.org/v4" # Example API
|
||||
|
||||
def get_headers(self) -> Dict[str, str]:
|
||||
"""Get headers with API key for soccer API."""
|
||||
headers = super().get_headers()
|
||||
if self.api_key:
|
||||
headers['X-Auth-Token'] = self.api_key
|
||||
return headers
|
||||
|
||||
def fetch_live_games(self, sport: str, league: str) -> List[Dict]:
|
||||
"""Fetch live games from soccer API."""
|
||||
try:
|
||||
# This would need to be adapted based on the specific soccer API
|
||||
url = f"{self.base_url}/matches"
|
||||
params = {
|
||||
'status': 'LIVE',
|
||||
'competition': league
|
||||
}
|
||||
|
||||
response = self.session.get(url, headers=self.get_headers(), params=params, timeout=15)
|
||||
response.raise_for_status()
|
||||
|
||||
data = response.json()
|
||||
matches = data.get('matches', [])
|
||||
|
||||
self.logger.debug(f"Fetched {len(matches)} live games from soccer API")
|
||||
return matches
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error fetching live games from soccer API: {e}")
|
||||
return []
|
||||
|
||||
def fetch_schedule(self, sport: str, league: str, date_range: tuple) -> List[Dict]:
|
||||
"""Fetch schedule from soccer API."""
|
||||
try:
|
||||
start_date, end_date = date_range
|
||||
url = f"{self.base_url}/matches"
|
||||
|
||||
params = {
|
||||
'competition': league,
|
||||
'dateFrom': start_date.strftime('%Y-%m-%d'),
|
||||
'dateTo': end_date.strftime('%Y-%m-%d')
|
||||
}
|
||||
|
||||
response = self.session.get(url, headers=self.get_headers(), params=params, timeout=15)
|
||||
response.raise_for_status()
|
||||
|
||||
data = response.json()
|
||||
matches = data.get('matches', [])
|
||||
|
||||
self.logger.debug(f"Fetched {len(matches)} scheduled games from soccer API")
|
||||
return matches
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error fetching schedule from soccer API: {e}")
|
||||
return []
|
||||
|
||||
def fetch_standings(self, sport: str, league: str) -> Dict:
|
||||
"""Fetch standings from soccer API."""
|
||||
try:
|
||||
url = f"{self.base_url}/competitions/{league}/standings"
|
||||
response = self.session.get(url, headers=self.get_headers(), timeout=15)
|
||||
response.raise_for_status()
|
||||
|
||||
data = response.json()
|
||||
self.logger.debug(f"Fetched standings from soccer API")
|
||||
return data
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error fetching standings from soccer API: {e}")
|
||||
return {}
|
||||
|
||||
|
||||
# Factory function removed - sport classes now instantiate data sources directly
|
||||
@@ -7,11 +7,40 @@ from PIL import Image, ImageDraw, ImageFont
|
||||
import time
|
||||
import pytz
|
||||
from src.base_classes.sports import SportsCore
|
||||
from src.base_classes.api_extractors import ESPNFootballExtractor
|
||||
from src.base_classes.data_sources import ESPNDataSource
|
||||
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
|
||||
|
||||
@@ -6,10 +6,39 @@ import logging
|
||||
from PIL import Image, ImageDraw, ImageFont
|
||||
import time
|
||||
from src.base_classes.sports import SportsCore
|
||||
from src.base_classes.api_extractors import ESPNHockeyExtractor
|
||||
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)
|
||||
|
||||
@@ -15,6 +15,10 @@ from src.background_data_service import get_background_service
|
||||
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
|
||||
|
||||
class SportsCore:
|
||||
def __init__(self, config: Dict[str, Any], display_manager: DisplayManager, cache_manager: CacheManager, logger: logging.Logger, sport_key: str):
|
||||
self.logger = logger
|
||||
@@ -28,6 +32,11 @@ class SportsCore:
|
||||
self.display_height = self.display_manager.matrix.height
|
||||
|
||||
self.sport_key = sport_key
|
||||
|
||||
# Initialize new architecture components (will be overridden by sport-specific classes)
|
||||
self.sport_config = None
|
||||
self.api_extractor = None
|
||||
self.data_source = None
|
||||
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)
|
||||
@@ -267,20 +276,143 @@ class SportsCore:
|
||||
return None
|
||||
|
||||
def _fetch_data(self) -> Optional[Dict]:
|
||||
"""Override this from the sports class"""
|
||||
pass
|
||||
"""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]:
|
||||
"""Override this from the sports class"""
|
||||
return []
|
||||
"""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]:
|
||||
"""Override this from the sports class"""
|
||||
return []
|
||||
"""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, _: Dict) -> None:
|
||||
"""Override this from the sports class"""
|
||||
pass
|
||||
def _fetch_game_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:
|
||||
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
|
||||
|
||||
# Determine update interval based on game state
|
||||
is_live = game.get('is_live', False)
|
||||
update_interval = self.mode_config.get("live_odds_update_interval", 60) if is_live \
|
||||
else self.mode_config.get("odds_update_interval", 3600)
|
||||
|
||||
# Fetch odds using OddsManager
|
||||
odds_data = self.odds_manager.get_odds(
|
||||
sport=self.sport_key,
|
||||
league=self.sport_key,
|
||||
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 _fetch_odds(self, game: Dict, sport: str, league: str) -> None:
|
||||
"""Fetch odds for a specific game if conditions are met."""
|
||||
@@ -339,7 +471,26 @@ class SportsCore:
|
||||
return False
|
||||
|
||||
def _fetch_team_rankings(self) -> Dict[str, int]:
|
||||
return {}
|
||||
"""Fetch team rankings using the new architecture components."""
|
||||
try:
|
||||
# Use the data source to fetch standings
|
||||
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 = {}
|
||||
# This would need to be implemented based on the specific data structure
|
||||
# returned by each data source
|
||||
|
||||
self.logger.debug(f"Successfully fetched rankings for {self.sport_key}")
|
||||
return rankings
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error fetching team rankings for {self.sport_key}: {e}")
|
||||
return {}
|
||||
|
||||
def _extract_game_details_common(self, game_event: Dict) -> tuple[Dict | None, Dict | None, Dict | None, Dict | None, Dict | None]:
|
||||
if not game_event:
|
||||
@@ -393,7 +544,7 @@ class SportsCore:
|
||||
# Don't show "0-0" records - set to blank instead
|
||||
if home_record in {"0-0", "0-0-0"}:
|
||||
home_record = ''
|
||||
if away_record == {"0-0", "0-0-0"}:
|
||||
if away_record in {"0-0", "0-0-0"}:
|
||||
away_record = ''
|
||||
|
||||
details = {
|
||||
|
||||
@@ -308,14 +308,19 @@ class CacheManager:
|
||||
cache_path = self._get_cache_path(key)
|
||||
if cache_path and os.path.exists(cache_path):
|
||||
os.remove(cache_path)
|
||||
self.logger.info(f"Cleared cache for key: {key}")
|
||||
else:
|
||||
# Clear all keys
|
||||
memory_count = len(self._memory_cache)
|
||||
self._memory_cache.clear()
|
||||
self._memory_cache_timestamps.clear()
|
||||
file_count = 0
|
||||
if self.cache_dir:
|
||||
for file in os.listdir(self.cache_dir):
|
||||
if file.endswith('.json'):
|
||||
os.remove(os.path.join(self.cache_dir, file))
|
||||
file_count += 1
|
||||
self.logger.info(f"Cleared all cache: {memory_count} memory entries, {file_count} cache files")
|
||||
|
||||
def has_data_changed(self, data_type: str, new_data: Dict[str, Any]) -> bool:
|
||||
"""Check if data has changed from cached version."""
|
||||
@@ -511,11 +516,8 @@ class CacheManager:
|
||||
|
||||
try:
|
||||
config = self.config_manager.config
|
||||
# For MiLB, look for "milb" config instead of "milb_scoreboard"
|
||||
if sport_key == 'milb':
|
||||
sport_config = config.get("milb", {})
|
||||
else:
|
||||
sport_config = config.get(f"{sport_key}_scoreboard", {})
|
||||
# All sports now use _scoreboard suffix
|
||||
sport_config = config.get(f"{sport_key}_scoreboard", {})
|
||||
return sport_config.get("live_update_interval", 60) # Default to 60 seconds
|
||||
except Exception as e:
|
||||
self.logger.warning(f"Could not get live_update_interval for {sport_key}: {e}")
|
||||
@@ -536,10 +538,8 @@ class CacheManager:
|
||||
upcoming_interval = None
|
||||
if self.config_manager and sport_key:
|
||||
try:
|
||||
if sport_key == 'milb':
|
||||
sport_cfg = self.config_manager.config.get('milb', {})
|
||||
else:
|
||||
sport_cfg = self.config_manager.config.get(f"{sport_key}_scoreboard", {})
|
||||
# All sports now use _scoreboard suffix
|
||||
sport_cfg = self.config_manager.config.get(f"{sport_key}_scoreboard", {})
|
||||
recent_interval = sport_cfg.get('recent_update_interval')
|
||||
upcoming_interval = sport_cfg.get('upcoming_update_interval')
|
||||
except Exception as e:
|
||||
|
||||
@@ -133,8 +133,8 @@ class DisplayController:
|
||||
|
||||
# Initialize MLB managers if enabled
|
||||
mlb_time = time.time()
|
||||
mlb_enabled = self.config.get('mlb', {}).get('enabled', False)
|
||||
mlb_display_modes = self.config.get('mlb', {}).get('display_modes', {})
|
||||
mlb_enabled = self.config.get('mlb_scoreboard', {}).get('enabled', False)
|
||||
mlb_display_modes = self.config.get('mlb_scoreboard', {}).get('display_modes', {})
|
||||
|
||||
if mlb_enabled:
|
||||
self.mlb_live = MLBLiveManager(self.config, self.display_manager, self.cache_manager) if mlb_display_modes.get('mlb_live', True) else None
|
||||
@@ -148,8 +148,8 @@ class DisplayController:
|
||||
|
||||
# Initialize MiLB managers if enabled
|
||||
milb_time = time.time()
|
||||
milb_enabled = self.config.get('milb', {}).get('enabled', False)
|
||||
milb_display_modes = self.config.get('milb', {}).get('display_modes', {})
|
||||
milb_enabled = self.config.get('milb_scoreboard', {}).get('enabled', False)
|
||||
milb_display_modes = self.config.get('milb_scoreboard', {}).get('display_modes', {})
|
||||
|
||||
if milb_enabled:
|
||||
self.milb_live = MiLBLiveManager(self.config, self.display_manager, self.cache_manager) if milb_display_modes.get('milb_live', True) else None
|
||||
@@ -256,14 +256,14 @@ class DisplayController:
|
||||
# Track MLB rotation state
|
||||
self.mlb_current_team_index = 0
|
||||
self.mlb_showing_recent = True
|
||||
self.mlb_favorite_teams = self.config.get('mlb', {}).get('favorite_teams', [])
|
||||
self.mlb_favorite_teams = self.config.get('mlb_scoreboard', {}).get('favorite_teams', [])
|
||||
self.in_mlb_rotation = False
|
||||
|
||||
# Read live_priority flags for all sports
|
||||
self.nhl_live_priority = self.config.get('nhl_scoreboard', {}).get('live_priority', True)
|
||||
self.nba_live_priority = self.config.get('nba_scoreboard', {}).get('live_priority', True)
|
||||
self.mlb_live_priority = self.config.get('mlb', {}).get('live_priority', True)
|
||||
self.milb_live_priority = self.config.get('milb', {}).get('live_priority', True)
|
||||
self.mlb_live_priority = self.config.get('mlb_scoreboard', {}).get('live_priority', True)
|
||||
self.milb_live_priority = self.config.get('milb_scoreboard', {}).get('live_priority', True)
|
||||
self.soccer_live_priority = self.config.get('soccer_scoreboard', {}).get('live_priority', True)
|
||||
self.nfl_live_priority = self.config.get('nfl_scoreboard', {}).get('live_priority', True)
|
||||
self.ncaa_fb_live_priority = self.config.get('ncaa_fb_scoreboard', {}).get('live_priority', True)
|
||||
@@ -438,7 +438,7 @@ class DisplayController:
|
||||
if mlb_enabled:
|
||||
logger.info(f"MLB Favorite teams: {self.mlb_favorite_teams}")
|
||||
if milb_enabled:
|
||||
logger.info(f"MiLB Favorite teams: {self.config.get('milb', {}).get('favorite_teams', [])}")
|
||||
logger.info(f"MiLB Favorite teams: {self.config.get('milb_scoreboard', {}).get('favorite_teams', [])}")
|
||||
if soccer_enabled: # Check if soccer is enabled
|
||||
logger.info(f"Soccer Favorite teams: {self.soccer_favorite_teams}")
|
||||
if nfl_enabled: # Check if NFL is enabled
|
||||
@@ -541,19 +541,20 @@ class DisplayController:
|
||||
# Fall back to configured duration
|
||||
return self.display_durations.get(mode_key, 60)
|
||||
|
||||
# Handle dynamic duration for leaderboard
|
||||
# Handle leaderboard duration (user choice between fixed or dynamic)
|
||||
elif mode_key == 'leaderboard' and self.leaderboard:
|
||||
try:
|
||||
dynamic_duration = self.leaderboard.get_dynamic_duration()
|
||||
duration = self.leaderboard.get_duration()
|
||||
mode_type = "dynamic" if self.leaderboard.dynamic_duration else "fixed"
|
||||
# Only log if duration has changed or we haven't logged this duration yet
|
||||
if not hasattr(self, '_last_logged_leaderboard_duration') or self._last_logged_leaderboard_duration != dynamic_duration:
|
||||
logger.info(f"Using dynamic duration for leaderboard: {dynamic_duration} seconds")
|
||||
self._last_logged_leaderboard_duration = dynamic_duration
|
||||
return dynamic_duration
|
||||
if not hasattr(self, '_last_logged_leaderboard_duration') or self._last_logged_leaderboard_duration != duration:
|
||||
logger.info(f"Using leaderboard {mode_type} duration: {duration} seconds")
|
||||
self._last_logged_leaderboard_duration = duration
|
||||
return duration
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting dynamic duration for leaderboard: {e}")
|
||||
logger.error(f"Error getting duration for leaderboard: {e}")
|
||||
# Fall back to configured duration
|
||||
return self.display_durations.get(mode_key, 60)
|
||||
return self.display_durations.get(mode_key, 600)
|
||||
|
||||
# Simplify weather key handling
|
||||
elif mode_key.startswith('weather_'):
|
||||
@@ -575,6 +576,8 @@ class DisplayController:
|
||||
# Defer updates for modules that might cause lag during scrolling
|
||||
if self.odds_ticker:
|
||||
self.display_manager.defer_update(self.odds_ticker.update, priority=1)
|
||||
if self.leaderboard:
|
||||
self.display_manager.defer_update(self.leaderboard.update, priority=1)
|
||||
if self.stocks:
|
||||
self.display_manager.defer_update(self.stocks.update_stock_data, priority=2)
|
||||
if self.news:
|
||||
@@ -608,6 +611,17 @@ class DisplayController:
|
||||
if self.youtube: self.youtube.update()
|
||||
if self.text_display: self.text_display.update()
|
||||
if self.of_the_day: self.of_the_day.update(time.time())
|
||||
|
||||
# Update sports managers for leaderboard data
|
||||
if self.leaderboard: self.leaderboard.update()
|
||||
|
||||
# Update key sports managers that feed the leaderboard
|
||||
if self.nfl_live: self.nfl_live.update()
|
||||
if self.nfl_recent: self.nfl_recent.update()
|
||||
if self.nfl_upcoming: self.nfl_upcoming.update()
|
||||
if self.ncaa_fb_live: self.ncaa_fb_live.update()
|
||||
if self.ncaa_fb_recent: self.ncaa_fb_recent.update()
|
||||
if self.ncaa_fb_upcoming: self.ncaa_fb_upcoming.update()
|
||||
|
||||
# News manager fetches data when displayed, not during updates
|
||||
# if self.news_manager: self.news_manager.fetch_news_data()
|
||||
@@ -824,7 +838,7 @@ class DisplayController:
|
||||
manager_recent = self.mlb_recent
|
||||
manager_upcoming = self.mlb_upcoming
|
||||
elif sport == 'milb':
|
||||
favorite_teams = self.config.get('milb', {}).get('favorite_teams', [])
|
||||
favorite_teams = self.config.get('milb_scoreboard', {}).get('favorite_teams', [])
|
||||
manager_recent = self.milb_recent
|
||||
manager_upcoming = self.milb_upcoming
|
||||
elif sport == 'soccer':
|
||||
@@ -862,8 +876,8 @@ class DisplayController:
|
||||
current_team = self.mlb_favorite_teams[self.mlb_current_team_index]
|
||||
# ... (rest of MLB rotation logic)
|
||||
elif sport == 'milb':
|
||||
if not self.config.get('milb', {}).get('favorite_teams', []): return
|
||||
current_team = self.config['milb']['favorite_teams'][self.milb_current_team_index]
|
||||
if not self.config.get('milb_scoreboard', {}).get('favorite_teams', []): return
|
||||
current_team = self.config['milb_scoreboard']['favorite_teams'][self.milb_current_team_index]
|
||||
# ... (rest of MiLB rotation logic)
|
||||
elif sport == 'soccer':
|
||||
if not self.soccer_favorite_teams: return
|
||||
@@ -978,8 +992,8 @@ class DisplayController:
|
||||
# Check if each sport is enabled before processing
|
||||
nhl_enabled = self.config.get('nhl_scoreboard', {}).get('enabled', False)
|
||||
nba_enabled = self.config.get('nba_scoreboard', {}).get('enabled', False)
|
||||
mlb_enabled = self.config.get('mlb', {}).get('enabled', False)
|
||||
milb_enabled = self.config.get('milb', {}).get('enabled', False)
|
||||
mlb_enabled = self.config.get('mlb_scoreboard', {}).get('enabled', False)
|
||||
milb_enabled = self.config.get('milb_scoreboard', {}).get('enabled', False)
|
||||
soccer_enabled = self.config.get('soccer_scoreboard', {}).get('enabled', False)
|
||||
nfl_enabled = self.config.get('nfl_scoreboard', {}).get('enabled', False)
|
||||
ncaa_fb_enabled = self.config.get('ncaa_fb_scoreboard', {}).get('enabled', False)
|
||||
@@ -1006,8 +1020,10 @@ class DisplayController:
|
||||
return
|
||||
|
||||
try:
|
||||
logger.info("Clearing cache and refetching data to prevent stale data issues...")
|
||||
self.cache_manager.clear_cache()
|
||||
self._update_modules()
|
||||
logger.info("Cache cleared, waiting 5 seconds for fresh data fetch...")
|
||||
time.sleep(5)
|
||||
self.current_display_mode = self.available_modes[self.current_mode_index] if self.available_modes else 'none'
|
||||
while True:
|
||||
|
||||
@@ -40,7 +40,7 @@ class LeaderboardManager:
|
||||
self.enabled_sports = self.leaderboard_config.get('enabled_sports', {})
|
||||
self.update_interval = self.leaderboard_config.get('update_interval', 3600)
|
||||
self.scroll_speed = self.leaderboard_config.get('scroll_speed', 2)
|
||||
self.scroll_delay = self.leaderboard_config.get('scroll_delay', 0.05)
|
||||
self.scroll_delay = self.leaderboard_config.get('scroll_delay', 0.01)
|
||||
self.display_duration = self.leaderboard_config.get('display_duration', 30)
|
||||
self.loop = self.leaderboard_config.get('loop', True)
|
||||
self.request_timeout = self.leaderboard_config.get('request_timeout', 30)
|
||||
@@ -53,6 +53,12 @@ class LeaderboardManager:
|
||||
self.dynamic_duration = 60 # Default duration in seconds
|
||||
self.total_scroll_width = 0 # Track total width for dynamic duration calculation
|
||||
|
||||
# FPS tracking variables
|
||||
self.frame_times = [] # Store last 30 frame times for averaging
|
||||
self.last_frame_time = 0
|
||||
self.fps_log_interval = 10.0 # Log FPS every 10 seconds
|
||||
self.last_fps_log_time = 0
|
||||
|
||||
# Initialize managers
|
||||
self.cache_manager = CacheManager()
|
||||
# Store reference to config instead of creating new ConfigManager
|
||||
@@ -1234,6 +1240,13 @@ class LeaderboardManager:
|
||||
logger.debug(f"get_dynamic_duration called, returning: {self.dynamic_duration}s")
|
||||
return self.dynamic_duration
|
||||
|
||||
def get_duration(self) -> int:
|
||||
"""Get the display duration for the leaderboard."""
|
||||
if self.dynamic_duration_enabled:
|
||||
return self.get_dynamic_duration()
|
||||
else:
|
||||
return self.display_duration
|
||||
|
||||
def update(self) -> None:
|
||||
"""Update leaderboard data."""
|
||||
current_time = time.time()
|
||||
@@ -1329,20 +1342,31 @@ class LeaderboardManager:
|
||||
try:
|
||||
current_time = time.time()
|
||||
|
||||
# Check if we should be scrolling
|
||||
should_scroll = current_time - self.last_scroll_time >= self.scroll_delay
|
||||
# FPS tracking
|
||||
if self.last_frame_time > 0:
|
||||
frame_time = current_time - self.last_frame_time
|
||||
self.frame_times.append(frame_time)
|
||||
if len(self.frame_times) > 30:
|
||||
self.frame_times.pop(0)
|
||||
|
||||
# Log FPS every 10 seconds
|
||||
if current_time - self.last_fps_log_time >= self.fps_log_interval:
|
||||
if self.frame_times:
|
||||
avg_frame_time = sum(self.frame_times) / len(self.frame_times)
|
||||
fps = 1.0 / avg_frame_time if avg_frame_time > 0 else 0
|
||||
logger.info(f"Leaderboard FPS: {fps:.1f} (avg frame time: {avg_frame_time*1000:.1f}ms)")
|
||||
self.last_fps_log_time = current_time
|
||||
|
||||
self.last_frame_time = current_time
|
||||
|
||||
# Signal scrolling state to display manager
|
||||
if should_scroll:
|
||||
self.display_manager.set_scrolling_state(True)
|
||||
else:
|
||||
# If we're not scrolling, check if we should process deferred updates
|
||||
self.display_manager.process_deferred_updates()
|
||||
self.display_manager.set_scrolling_state(True)
|
||||
|
||||
# Scroll the image
|
||||
if should_scroll:
|
||||
self.scroll_position += self.scroll_speed
|
||||
self.last_scroll_time = current_time
|
||||
# Scroll the image every frame for smooth animation
|
||||
self.scroll_position += self.scroll_speed
|
||||
|
||||
# Add scroll delay like other managers for consistent timing
|
||||
time.sleep(self.scroll_delay)
|
||||
|
||||
# Calculate crop region
|
||||
width = self.display_manager.matrix.width
|
||||
|
||||
@@ -14,6 +14,10 @@ from urllib3.util.retry import Retry
|
||||
import pytz
|
||||
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
|
||||
|
||||
# Import API counter function
|
||||
try:
|
||||
from web_interface_v2 import increment_api_counter
|
||||
@@ -24,17 +28,16 @@ except ImportError:
|
||||
# Get logger
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class BaseMiLBManager:
|
||||
"""Base class for MiLB managers with common functionality."""
|
||||
class BaseMiLBManager(Baseball):
|
||||
"""Base class for MiLB managers using new baseball architecture."""
|
||||
def __init__(self, config: Dict[str, Any], display_manager, cache_manager: CacheManager):
|
||||
self.config = config
|
||||
self.display_manager = display_manager
|
||||
self.milb_config = config.get('milb', {})
|
||||
# Initialize with sport_key for MiLB
|
||||
super().__init__(config, display_manager, cache_manager, logger, "milb")
|
||||
|
||||
# MiLB-specific configuration
|
||||
self.milb_config = config.get('milb_scoreboard', {})
|
||||
self.favorite_teams = self.milb_config.get('favorite_teams', [])
|
||||
self.show_records = self.milb_config.get('show_records', False)
|
||||
self.cache_manager = cache_manager
|
||||
self.logger = logging.getLogger(__name__)
|
||||
self.logger.setLevel(logging.INFO) # Set logger level to INFO
|
||||
|
||||
# Load MiLB team mapping
|
||||
self.team_mapping = {}
|
||||
@@ -896,7 +899,7 @@ class BaseMiLBManager:
|
||||
return game_data
|
||||
return {}
|
||||
|
||||
class MiLBLiveManager(BaseMiLBManager):
|
||||
class MiLBLiveManager(BaseMiLBManager, BaseballLive):
|
||||
"""Manager for displaying live MiLB games."""
|
||||
def __init__(self, config: Dict[str, Any], display_manager, cache_manager: CacheManager):
|
||||
super().__init__(config, display_manager, cache_manager)
|
||||
@@ -1424,7 +1427,7 @@ class MiLBLiveManager(BaseMiLBManager):
|
||||
except Exception as e:
|
||||
logger.error(f"[MiLB] Error displaying live game: {e}", exc_info=True)
|
||||
|
||||
class MiLBRecentManager(BaseMiLBManager):
|
||||
class MiLBRecentManager(BaseMiLBManager, SportsRecent):
|
||||
"""Manager for displaying recent MiLB games."""
|
||||
def __init__(self, config: Dict[str, Any], display_manager, cache_manager: CacheManager):
|
||||
super().__init__(config, display_manager, cache_manager)
|
||||
@@ -1615,7 +1618,7 @@ class MiLBRecentManager(BaseMiLBManager):
|
||||
except Exception as e:
|
||||
logger.error(f"[MiLB] Error displaying recent game: {e}", exc_info=True)
|
||||
|
||||
class MiLBUpcomingManager(BaseMiLBManager):
|
||||
class MiLBUpcomingManager(BaseMiLBManager, SportsUpcoming):
|
||||
"""Manager for upcoming MiLB games."""
|
||||
def __init__(self, config: Dict[str, Any], display_manager, cache_manager: CacheManager):
|
||||
super().__init__(config, display_manager, cache_manager)
|
||||
|
||||
@@ -14,6 +14,10 @@ import pytz
|
||||
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
|
||||
|
||||
# Import the API counter function from web interface
|
||||
try:
|
||||
from web_interface_v2 import increment_api_counter
|
||||
@@ -25,20 +29,21 @@ except ImportError:
|
||||
# Get logger
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class BaseMLBManager:
|
||||
"""Base class for MLB managers with common functionality."""
|
||||
class BaseMLBManager(Baseball):
|
||||
"""Base class for MLB managers using new baseball architecture."""
|
||||
def __init__(self, config: Dict[str, Any], display_manager, cache_manager: CacheManager):
|
||||
self.config = config
|
||||
self.display_manager = display_manager
|
||||
# Store reference to config instead of creating new ConfigManager
|
||||
self.config_manager = None # Not used in this class
|
||||
self.mlb_config = config.get('mlb', {})
|
||||
# Initialize with sport_key for MLB
|
||||
super().__init__(config, display_manager, cache_manager, logger, "mlb")
|
||||
|
||||
# MLB-specific configuration
|
||||
self.mlb_config = config.get('mlb_scoreboard', {})
|
||||
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.cache_manager = cache_manager
|
||||
|
||||
# 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)
|
||||
self.logger = logging.getLogger(__name__)
|
||||
|
||||
# Logo handling
|
||||
self.logo_dir = self.mlb_config.get('logo_dir', os.path.join('assets', 'sports', 'mlb_logos'))
|
||||
@@ -744,7 +749,7 @@ class BaseMLBManager:
|
||||
|
||||
self._draw_text_with_outline(draw, ou_text, (ou_x, ou_y), font, fill=(0, 255, 0))
|
||||
|
||||
class MLBLiveManager(BaseMLBManager):
|
||||
class MLBLiveManager(BaseMLBManager, BaseballLive):
|
||||
"""Manager for displaying live MLB games."""
|
||||
def __init__(self, config: Dict[str, Any], display_manager, cache_manager: CacheManager):
|
||||
super().__init__(config, display_manager, cache_manager)
|
||||
@@ -1157,7 +1162,7 @@ class MLBLiveManager(BaseMLBManager):
|
||||
except Exception as e:
|
||||
logger.error(f"[MLB] Error displaying live game: {e}", exc_info=True)
|
||||
|
||||
class MLBRecentManager(BaseMLBManager):
|
||||
class MLBRecentManager(BaseMLBManager, SportsRecent):
|
||||
"""Manager for displaying recent MLB games."""
|
||||
def __init__(self, config: Dict[str, Any], display_manager, cache_manager: CacheManager):
|
||||
super().__init__(config, display_manager, cache_manager)
|
||||
@@ -1318,7 +1323,7 @@ class MLBRecentManager(BaseMLBManager):
|
||||
except Exception as e:
|
||||
logger.error(f"[MLB] Error displaying recent game: {e}", exc_info=True)
|
||||
|
||||
class MLBUpcomingManager(BaseMLBManager):
|
||||
class MLBUpcomingManager(BaseMLBManager, SportsUpcoming):
|
||||
"""Manager for displaying upcoming MLB games."""
|
||||
def __init__(self, config: Dict[str, Any], display_manager, cache_manager: CacheManager):
|
||||
super().__init__(config, display_manager, cache_manager)
|
||||
|
||||
@@ -13,27 +13,31 @@ 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
|
||||
|
||||
# Get logger
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Constants for NCAA Baseball API URL
|
||||
ESPN_NCAABB_SCOREBOARD_URL = "https://site.api.espn.com/apis/site/v2/sports/baseball/college-baseball/scoreboard"
|
||||
|
||||
class BaseNCAABaseballManager:
|
||||
"""Base class for NCAA Baseball managers with common functionality."""
|
||||
class BaseNCAABaseballManager(Baseball):
|
||||
"""Base class for NCAA Baseball managers using new baseball architecture."""
|
||||
def __init__(self, config: Dict[str, Any], display_manager, cache_manager: CacheManager):
|
||||
self.config = config
|
||||
self.display_manager = display_manager
|
||||
# Store reference to config instead of creating new ConfigManager
|
||||
self.config_manager = None # Not used in this class
|
||||
# Initialize with sport_key for NCAA Baseball
|
||||
super().__init__(config, display_manager, cache_manager, logger, "ncaa_baseball")
|
||||
|
||||
# NCAA Baseball-specific configuration
|
||||
self.ncaa_baseball_config = config.get('ncaa_baseball_scoreboard', {})
|
||||
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.cache_manager = cache_manager
|
||||
|
||||
# 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)
|
||||
self.logger = logging.getLogger(__name__)
|
||||
self.logger.setLevel(logging.DEBUG) # Set logger level to DEBUG
|
||||
|
||||
# Logo handling
|
||||
self.logo_dir = self.ncaa_baseball_config.get('logo_dir', os.path.join('assets', 'sports', 'ncaa_logos'))
|
||||
@@ -549,7 +553,7 @@ class BaseNCAABaseballManager:
|
||||
self.logger.error(f"[NCAABaseball] Error fetching NCAA Baseball data from ESPN API: {e}", exc_info=True)
|
||||
return {}
|
||||
|
||||
class NCAABaseballLiveManager(BaseNCAABaseballManager):
|
||||
class NCAABaseballLiveManager(BaseNCAABaseballManager, BaseballLive):
|
||||
"""Manager for displaying live NCAA Baseball games."""
|
||||
def __init__(self, config: Dict[str, Any], display_manager, cache_manager: CacheManager):
|
||||
super().__init__(config, display_manager, cache_manager)
|
||||
@@ -850,7 +854,7 @@ class NCAABaseballLiveManager(BaseNCAABaseballManager):
|
||||
except Exception as e:
|
||||
logger.error(f"[NCAABaseball] Error displaying live game: {e}", exc_info=True)
|
||||
|
||||
class NCAABaseballRecentManager(BaseNCAABaseballManager):
|
||||
class NCAABaseballRecentManager(BaseNCAABaseballManager, SportsRecent):
|
||||
"""Manager for displaying recent NCAA Baseball games."""
|
||||
def __init__(self, config: Dict[str, Any], display_manager, cache_manager: CacheManager):
|
||||
super().__init__(config, display_manager, cache_manager)
|
||||
@@ -974,7 +978,7 @@ class NCAABaseballRecentManager(BaseNCAABaseballManager):
|
||||
except Exception as e:
|
||||
logger.error(f"[NCAABaseball] Error displaying recent game: {e}", exc_info=True)
|
||||
|
||||
class NCAABaseballUpcomingManager(BaseNCAABaseballManager):
|
||||
class NCAABaseballUpcomingManager(BaseNCAABaseballManager, SportsUpcoming):
|
||||
"""Manager for displaying upcoming NCAA Baseball games."""
|
||||
def __init__(self, config: Dict[str, Any], display_manager, cache_manager: CacheManager):
|
||||
super().__init__(config, display_manager, cache_manager)
|
||||
|
||||
@@ -28,6 +28,9 @@ class BaseNCAAFBManager(Football): # Renamed class
|
||||
def __init__(self, config: Dict[str, Any], display_manager: DisplayManager, cache_manager: CacheManager):
|
||||
self.logger = logging.getLogger('NCAAFB') # Changed logger name
|
||||
super().__init__(config=config, display_manager=display_manager, cache_manager=cache_manager, logger=self.logger, sport_key="ncaa_fb")
|
||||
|
||||
# Configuration is already set in base class
|
||||
# self.logo_dir and self.update_interval are already configured
|
||||
|
||||
# Check display modes to determine what data to fetch
|
||||
display_modes = self.mode_config.get("display_modes", {})
|
||||
@@ -140,7 +143,7 @@ class BaseNCAAFBManager(Football): # Renamed class
|
||||
|
||||
# Submit background fetch request
|
||||
request_id = self.background_service.submit_fetch_request(
|
||||
sport="nfl",
|
||||
sport="ncaa_fb",
|
||||
year=season_year,
|
||||
url=ESPN_NCAAFB_SCOREBOARD_URL,
|
||||
cache_key=cache_key,
|
||||
@@ -184,7 +187,7 @@ class BaseNCAAFBManager(Football): # Renamed class
|
||||
except requests.exceptions.RequestException as e:
|
||||
self.logger.error(f"[API error fetching full schedule: {e}")
|
||||
return None
|
||||
|
||||
|
||||
def _fetch_data(self) -> Optional[Dict]:
|
||||
"""Fetch data using shared data mechanism or direct fetch for live."""
|
||||
if isinstance(self, NCAAFBLiveManager):
|
||||
|
||||
@@ -33,6 +33,9 @@ class BaseNCAAMHockeyManager(Hockey): # Renamed class
|
||||
def __init__(self, config: Dict[str, Any], display_manager: DisplayManager, cache_manager: CacheManager):
|
||||
self.logger = logging.getLogger('NCAAMH') # Changed logger name
|
||||
super().__init__(config=config, display_manager=display_manager, cache_manager=cache_manager, logger=self.logger, sport_key="ncaam_hockey")
|
||||
|
||||
# Configuration is already set in base class
|
||||
# self.logo_dir and self.update_interval are already configured
|
||||
|
||||
# Check display modes to determine what data to fetch
|
||||
display_modes = self.mode_config.get("display_modes", {})
|
||||
|
||||
@@ -25,6 +25,9 @@ class BaseNFLManager(Football): # Renamed class
|
||||
def __init__(self, config: Dict[str, Any], display_manager: DisplayManager, cache_manager: CacheManager):
|
||||
self.logger = logging.getLogger('NFL') # Changed logger name
|
||||
super().__init__(config=config, display_manager=display_manager, cache_manager=cache_manager, logger=self.logger, sport_key="nfl")
|
||||
|
||||
# Configuration is already set in base class
|
||||
# self.logo_dir and self.update_interval are already configured
|
||||
|
||||
# Check display modes to determine what data to fetch
|
||||
display_modes = self.mode_config.get("display_modes", {})
|
||||
|
||||
@@ -162,8 +162,8 @@ class OddsTickerManager:
|
||||
'league': 'mlb',
|
||||
'logo_league': 'mlb', # ESPN API league identifier for logo downloading
|
||||
'logo_dir': 'assets/sports/mlb_logos',
|
||||
'favorite_teams': config.get('mlb', {}).get('favorite_teams', []),
|
||||
'enabled': config.get('mlb', {}).get('enabled', False)
|
||||
'favorite_teams': config.get('mlb_scoreboard', {}).get('favorite_teams', []),
|
||||
'enabled': config.get('mlb_scoreboard', {}).get('enabled', False)
|
||||
},
|
||||
'ncaa_fb': {
|
||||
'sport': 'football',
|
||||
@@ -178,8 +178,8 @@ class OddsTickerManager:
|
||||
'league': 'milb',
|
||||
'logo_league': 'milb', # ESPN API league identifier for logo downloading (if supported)
|
||||
'logo_dir': 'assets/sports/milb_logos',
|
||||
'favorite_teams': config.get('milb', {}).get('favorite_teams', []),
|
||||
'enabled': config.get('milb', {}).get('enabled', False)
|
||||
'favorite_teams': config.get('milb_scoreboard', {}).get('favorite_teams', []),
|
||||
'enabled': config.get('milb_scoreboard', {}).get('enabled', False)
|
||||
},
|
||||
'nhl': {
|
||||
'sport': 'hockey',
|
||||
|
||||
Reference in New Issue
Block a user