mirror of
https://github.com/ChuckBuilds/LEDMatrix.git
synced 2026-04-10 13:02:59 +00:00
Consolidate Baseball leagues to completely use Baseball class (#84)
* Consolidate MLB to completely use Baseball class * typos * add OT period number * Add new live class and abstracts * NCAA BB is consolidated * MLB Working * NCAA Hockey and NHL working * didn't need wrapper function * Add hockey shots on goal * cleanup --------- Co-authored-by: Alex Resnick <adr8282@gmail.com>
This commit is contained in:
@@ -1,26 +1,30 @@
|
||||
from typing import Dict, Any, Optional, List
|
||||
from src.display_manager import DisplayManager
|
||||
from src.cache_manager import CacheManager
|
||||
from datetime import datetime, timedelta, timezone
|
||||
import logging
|
||||
import os
|
||||
from src.odds_manager import OddsManager
|
||||
import time
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, List, Optional
|
||||
from typing import Callable
|
||||
import pytz
|
||||
import requests
|
||||
from PIL import Image, ImageDraw, ImageFont
|
||||
from requests.adapters import HTTPAdapter
|
||||
from urllib3.util.retry import Retry
|
||||
from PIL import Image, ImageDraw, ImageFont
|
||||
import pytz
|
||||
import time
|
||||
from abc import ABC, abstractmethod
|
||||
|
||||
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 src.base_classes.api_extractors import APIDataExtractor
|
||||
from src.base_classes.data_sources import DataSource
|
||||
from src.cache_manager import CacheManager
|
||||
from src.display_manager import DisplayManager
|
||||
from src.dynamic_team_resolver import DynamicTeamResolver
|
||||
from src.logo_downloader import LogoDownloader, download_missing_logo
|
||||
from src.odds_manager import OddsManager
|
||||
|
||||
class SportsCore:
|
||||
|
||||
class SportsCore(ABC):
|
||||
def __init__(self, config: Dict[str, Any], display_manager: DisplayManager, cache_manager: CacheManager, logger: logging.Logger, sport_key: str):
|
||||
self.logger = logger
|
||||
self.config = config
|
||||
@@ -41,20 +45,21 @@ class SportsCore:
|
||||
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)
|
||||
self.test_mode = self.mode_config.get("test_mode", False)
|
||||
self.is_enabled: bool = self.mode_config.get("enabled", False)
|
||||
self.show_odds: bool = self.mode_config.get("show_odds", False)
|
||||
self.test_mode: bool = self.mode_config.get("test_mode", False)
|
||||
self.logo_dir = Path(self.mode_config.get("logo_dir", "assets/sports/ncaa_logos")) # Changed logo dir
|
||||
self.update_interval = self.mode_config.get(
|
||||
self.update_interval: int = self.mode_config.get(
|
||||
"update_interval_seconds", 60)
|
||||
self.show_records = self.mode_config.get('show_records', False)
|
||||
self.show_ranking = self.mode_config.get('show_ranking', False)
|
||||
self.show_records: bool = self.mode_config.get('show_records', False)
|
||||
self.show_ranking: bool = self.mode_config.get('show_ranking', False)
|
||||
# Number of games to show (instead of time-based windows)
|
||||
self.recent_games_to_show = self.mode_config.get(
|
||||
self.recent_games_to_show: int = self.mode_config.get(
|
||||
"recent_games_to_show", 5) # Show last 5 games
|
||||
self.upcoming_games_to_show = self.mode_config.get(
|
||||
self.upcoming_games_to_show: int = 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.show_favorite_teams_only: bool = self.mode_config.get("show_favorite_teams_only", False)
|
||||
self.show_all_live: bool = self.mode_config.get("show_all_live", False)
|
||||
|
||||
self.session = requests.Session()
|
||||
retry_strategy = Retry(
|
||||
@@ -114,6 +119,9 @@ class SportsCore:
|
||||
self.background_enabled = False
|
||||
self.logger.info("Background service disabled")
|
||||
|
||||
def _get_season_schedule_dates(self) -> tuple[str, str]:
|
||||
return "", ""
|
||||
|
||||
def _draw_scorebug_layout(self, game: Dict, force_clear: bool = False) -> None:
|
||||
"""Placeholder draw method - subclasses should override."""
|
||||
# This base method will be simple, subclasses provide specifics
|
||||
@@ -314,14 +322,6 @@ class SportsCore:
|
||||
if not self.show_odds:
|
||||
return
|
||||
|
||||
# Check if we should only fetch for favorite teams
|
||||
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):
|
||||
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 \
|
||||
@@ -485,16 +485,12 @@ class SportsCore:
|
||||
logging.error(f"Error extracting game details: {e} from event: {game_event.get('id')}", exc_info=True)
|
||||
return None, None, None, None, None
|
||||
|
||||
@abstractmethod
|
||||
def _extract_game_details(self, game_event: dict) -> dict | None:
|
||||
details, _, _, _, _ = self._extract_game_details_common(game_event)
|
||||
return details
|
||||
|
||||
# def _draw_scorebug_layout(self, game: Dict, force_clear: bool = False) -> None:
|
||||
# pass
|
||||
|
||||
# def display(self, force_clear=False):
|
||||
# pass
|
||||
|
||||
@abstractmethod
|
||||
def _fetch_data(self) -> Optional[Dict]:
|
||||
pass
|
||||
|
||||
@@ -610,62 +606,22 @@ class SportsUpcoming(SportsCore):
|
||||
# Enhanced logging for debugging
|
||||
self.logger.info(f"Found {all_upcoming_games} total upcoming games in data")
|
||||
self.logger.info(f"Found {len(processed_games)} upcoming games after filtering")
|
||||
|
||||
# Debug: Check what statuses we're seeing
|
||||
status_counts = {}
|
||||
status_names = {} # Track actual status names from ESPN
|
||||
favorite_team_games = []
|
||||
for event in events:
|
||||
game = self._extract_game_details(event)
|
||||
if game:
|
||||
status = "upcoming" if game['is_upcoming'] else "final" if game['is_final'] else "live" if game['is_live'] else "other"
|
||||
status_counts[status] = status_counts.get(status, 0) + 1
|
||||
|
||||
# Track actual ESPN status names
|
||||
actual_status = event.get('competitions', [{}])[0].get('status', {}).get('type', {})
|
||||
status_name = actual_status.get('name', 'Unknown')
|
||||
status_state = actual_status.get('state', 'Unknown')
|
||||
status_names[f"{status_name} ({status_state})"] = status_names.get(f"{status_name} ({status_state})", 0) + 1
|
||||
|
||||
# Check for favorite team games regardless of status
|
||||
if (game['home_abbr'] in self.favorite_teams or game['away_abbr'] in self.favorite_teams):
|
||||
favorite_team_games.append({
|
||||
'teams': f"{game['away_abbr']} @ {game['home_abbr']}",
|
||||
'status': status,
|
||||
'date': game.get('start_time_utc', 'Unknown'),
|
||||
'espn_status': f"{status_name} ({status_state})"
|
||||
})
|
||||
|
||||
# Special check for Tennessee game (Georgia @ Tennessee)
|
||||
if (game['home_abbr'] == 'TENN' and game['away_abbr'] == 'UGA') or (game['home_abbr'] == 'UGA' and game['away_abbr'] == 'TENN'):
|
||||
self.logger.info(f"Found Tennessee game: {game['away_abbr']} @ {game['home_abbr']} - {status} - {game.get('start_time_utc')} - ESPN: {status_name} ({status_state})")
|
||||
|
||||
self.logger.info(f"Status breakdown: {status_counts}")
|
||||
self.logger.info(f"ESPN status names: {status_names}")
|
||||
if favorite_team_games:
|
||||
self.logger.info(f"Favorite team games found: {len(favorite_team_games)}")
|
||||
for game in favorite_team_games[:3]: # Show first 3
|
||||
self.logger.info(f" {game['teams']} - {game['status']} - {game['date']} - ESPN: {game['espn_status']}")
|
||||
|
||||
if processed_games:
|
||||
for game in processed_games[:3]: # Show first 3
|
||||
self.logger.info(f" {game['away_abbr']}@{game['home_abbr']} - {game['start_time_utc']}")
|
||||
|
||||
if self.favorite_teams and all_upcoming_games > 0:
|
||||
self.logger.info(f"Favorite teams: {self.favorite_teams}")
|
||||
self.logger.info(f"Found {favorite_games_found} favorite team upcoming games")
|
||||
|
||||
# Filter for favorite teams only if the config is set
|
||||
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
|
||||
game['away_abbr'] in self.favorite_teams]
|
||||
|
||||
if self.show_favorite_teams_only:
|
||||
# Select one game per favorite team (earliest upcoming game for each team)
|
||||
team_games = []
|
||||
for team in self.favorite_teams:
|
||||
# Find games where this team is playing
|
||||
team_specific_games = [game for game in favorite_team_games
|
||||
if game['home_abbr'] == team or game['away_abbr'] == team]
|
||||
|
||||
if team_specific_games:
|
||||
# Find games where this team is playing
|
||||
if team_specific_games := [game for game in processed_games if game['home_abbr'] == team or game['away_abbr'] == team]:
|
||||
# Sort by game time and take the earliest
|
||||
team_specific_games.sort(key=lambda g: g.get('start_time_utc') or datetime.max.replace(tzinfo=timezone.utc))
|
||||
team_games.append(team_specific_games[0])
|
||||
@@ -1200,4 +1156,144 @@ class SportsRecent(SportsCore):
|
||||
# update_display() is called within _draw_scorebug_layout for recent
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error in display loop: {e}", exc_info=True) # Changed log prefix
|
||||
self.logger.error(f"Error in display loop: {e}", exc_info=True) # Changed log prefix
|
||||
|
||||
class SportsLive(SportsCore):
|
||||
|
||||
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)
|
||||
self.update_interval = self.mode_config.get("live_update_interval", 15)
|
||||
self.no_data_interval = 300
|
||||
self.last_update = 0
|
||||
self.live_games = []
|
||||
self.current_game_index = 0
|
||||
self.last_game_switch = 0
|
||||
self.game_display_duration = self.mode_config.get("live_game_duration", 20)
|
||||
self.last_display_update = 0
|
||||
self.last_log_time = 0
|
||||
self.log_interval = 300
|
||||
|
||||
@abstractmethod
|
||||
def _test_mode_update(self) -> None:
|
||||
return
|
||||
|
||||
def update(self):
|
||||
"""Update live game data and handle game switching."""
|
||||
if not self.is_enabled:
|
||||
return
|
||||
|
||||
# Define current_time and interval before the problematic line (originally line 455)
|
||||
# Ensure 'import time' is present at the top of the file.
|
||||
current_time = time.time()
|
||||
|
||||
# Define interval using a pattern similar to NFLLiveManager's update method.
|
||||
# Uses getattr for robustness, assuming attributes for live_games, test_mode,
|
||||
# no_data_interval, and update_interval are available on self.
|
||||
_live_games_attr = self.live_games
|
||||
_test_mode_attr = self.test_mode # test_mode is often from a base class or config
|
||||
_no_data_interval_attr = self.no_data_interval # Default similar to NFLLiveManager
|
||||
_update_interval_attr = self.update_interval # Default similar to NFLLiveManager
|
||||
|
||||
interval = _no_data_interval_attr if not _live_games_attr and not _test_mode_attr else _update_interval_attr
|
||||
|
||||
# Original line from traceback (line 455), now with variables defined:
|
||||
if current_time - self.last_update >= interval:
|
||||
self.last_update = current_time
|
||||
|
||||
# Fetch rankings if enabled
|
||||
if self.show_ranking:
|
||||
self._fetch_team_rankings()
|
||||
|
||||
if self.test_mode:
|
||||
# Simulate clock running down in test mode
|
||||
self._test_mode_update()
|
||||
else:
|
||||
# Fetch live game data
|
||||
data = self._fetch_data()
|
||||
new_live_games = []
|
||||
if data and "events" in data:
|
||||
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.show_all_live or not self.show_favorite_teams_only or (self.show_favorite_teams_only and (details["home_abbr"] in self.favorite_teams or details["away_abbr"] in self.favorite_teams)):
|
||||
if self.show_odds:
|
||||
self._fetch_odds(details)
|
||||
new_live_games.append(details)
|
||||
# Log changes or periodically
|
||||
current_time_for_log = time.time() # Use a consistent time for logging comparison
|
||||
should_log = (
|
||||
current_time_for_log - self.last_log_time >= self.log_interval or
|
||||
len(new_live_games) != len(self.live_games) or
|
||||
any(g1['id'] != g2.get('id') for g1, g2 in zip(self.live_games, new_live_games)) or # Check if game IDs changed
|
||||
(not self.live_games and new_live_games) # Log if games appeared
|
||||
)
|
||||
|
||||
if should_log:
|
||||
if new_live_games:
|
||||
filter_text = "favorite teams" if self.show_favorite_teams_only or self.show_all_live 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.show_favorite_teams_only or self.show_all_live else "criteria"
|
||||
self.logger.info(f"No live/halftime games found for {filter_text}.")
|
||||
self.last_log_time = current_time_for_log
|
||||
|
||||
|
||||
# Update game list and current game
|
||||
if new_live_games:
|
||||
# Check if the games themselves changed, not just scores/time
|
||||
new_game_ids = {g['id'] for g in new_live_games}
|
||||
current_game_ids = {g['id'] for g in self.live_games}
|
||||
|
||||
if new_game_ids != current_game_ids:
|
||||
self.live_games = sorted(new_live_games, key=lambda g: g.get('start_time_utc') or datetime.now(timezone.utc)) # Sort by start time
|
||||
# Reset index if current game is gone or list is new
|
||||
if not self.current_game or self.current_game['id'] not in new_game_ids:
|
||||
self.current_game_index = 0
|
||||
self.current_game = self.live_games[0] if self.live_games else None
|
||||
self.last_game_switch = current_time
|
||||
else:
|
||||
# Find current game's new index if it still exists
|
||||
try:
|
||||
self.current_game_index = next(i for i, g in enumerate(self.live_games) if g['id'] == self.current_game['id'])
|
||||
self.current_game = self.live_games[self.current_game_index] # Update current_game with fresh data
|
||||
except StopIteration: # Should not happen if check above passed, but safety first
|
||||
self.current_game_index = 0
|
||||
self.current_game = self.live_games[0]
|
||||
self.last_game_switch = current_time
|
||||
|
||||
else:
|
||||
# Just update the data for the existing games
|
||||
temp_game_dict = {g['id']: g for g in new_live_games}
|
||||
self.live_games = [temp_game_dict.get(g['id'], g) for g in self.live_games] # Update in place
|
||||
if self.current_game:
|
||||
self.current_game = temp_game_dict.get(self.current_game['id'], self.current_game)
|
||||
|
||||
# Display update handled by main loop based on interval
|
||||
|
||||
else:
|
||||
# No live games found
|
||||
if self.live_games: # Were there games before?
|
||||
self.logger.info("Live games previously showing have ended or are no longer live.") # Changed log prefix
|
||||
self.live_games = []
|
||||
self.current_game = None
|
||||
self.current_game_index = 0
|
||||
|
||||
else:
|
||||
# Error fetching data or no events
|
||||
if self.live_games: # Were there games before?
|
||||
self.logger.warning("Could not fetch update; keeping existing live game data for now.") # Changed log prefix
|
||||
else:
|
||||
self.logger.warning("Could not fetch data and no existing live games.") # Changed log prefix
|
||||
self.current_game = None # Clear current game if fetch fails and no games were active
|
||||
|
||||
# Handle game switching (outside test mode check)
|
||||
if not self.test_mode and len(self.live_games) > 1 and (current_time - self.last_game_switch) >= self.game_display_duration:
|
||||
self.current_game_index = (self.current_game_index + 1) % len(self.live_games)
|
||||
self.current_game = self.live_games[self.current_game_index]
|
||||
self.last_game_switch = current_time
|
||||
self.logger.info(f"Switched live view to: {self.current_game['away_abbr']}@{self.current_game['home_abbr']}") # Changed log prefix
|
||||
# Force display update via flag or direct call if needed, but usually let main loop handle
|
||||
Reference in New Issue
Block a user