From f6e2881f2fde4120e6c58b143670d85cb75d5f5b Mon Sep 17 00:00:00 2001 From: Alex Resnick Date: Thu, 2 Oct 2025 16:35:17 -0500 Subject: [PATCH] Multiple Sports Fixes (#93) --- config/config.template.json | 10 ++++ src/base_classes/baseball.py | 67 +++++++++++++++++++++++---- src/base_classes/hockey.py | 40 ++++++++-------- src/base_classes/sports.py | 19 ++++++-- src/mlb_manager.py | 8 ++-- src/nhl_managers.py | 89 +++++++++++++++--------------------- 6 files changed, 143 insertions(+), 90 deletions(-) diff --git a/config/config.template.json b/config/config.template.json index eaca232c..7bba122e 100644 --- a/config/config.template.json +++ b/config/config.template.json @@ -233,6 +233,8 @@ "recent_games_to_show": 1, "upcoming_games_to_show": 1, "show_favorite_teams_only": true, + "show_all_live": false, + "show_shots_on_goal": false, "favorite_teams": [ "TB" ], @@ -299,6 +301,7 @@ "recent_games_to_show": 1, "upcoming_games_to_show": 1, "show_favorite_teams_only": true, + "show_all_live": false, "favorite_teams": [ "TB", "DAL" @@ -331,6 +334,7 @@ "recent_games_to_show": 1, "upcoming_games_to_show": 1, "show_favorite_teams_only": true, + "show_all_live": false, "favorite_teams": [ "UGA", "AUB", @@ -365,6 +369,8 @@ "recent_games_to_show": 1, "upcoming_games_to_show": 1, "show_favorite_teams_only": true, + "show_all_live": false, + "show_series_summary": false, "favorite_teams": [ "UGA", "AUB" @@ -413,6 +419,8 @@ "recent_games_to_show": 1, "upcoming_games_to_show": 1, "show_favorite_teams_only": true, + "show_all_live": false, + "show_shots_on_goal": false, "favorite_teams": [ "RIT" ], @@ -444,6 +452,8 @@ "recent_games_to_show": 1, "upcoming_games_to_show": 1, "show_favorite_teams_only": true, + "show_all_live": false, + "show_series_summary": false, "favorite_teams": [ "TB", "TEX" diff --git a/src/base_classes/baseball.py b/src/base_classes/baseball.py index 749786a2..9dd6a7b5 100644 --- a/src/base_classes/baseball.py +++ b/src/base_classes/baseball.py @@ -6,14 +6,13 @@ with baseball-specific logic for innings, outs, bases, strikes, balls, etc. """ import logging -import random import time from typing import Any, Dict, Optional -from PIL import Image, ImageDraw +from PIL import Image, ImageDraw, ImageFont from src.base_classes.data_sources import ESPNDataSource -from src.base_classes.sports import SportsCore, SportsLive +from src.base_classes.sports import SportsCore, SportsLive, SportsRecent class Baseball(SportsCore): @@ -34,6 +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) + self.show_series_summary = self.mode_config.get("show_series_summary", False) self.data_source = ESPNDataSource(logger) self.sport = "baseball" @@ -157,7 +157,10 @@ class Baseball(SportsCore): self.logger.debug( f"Status shortDetail: {status['type'].get('shortDetail', '')}" ) - + series = game_event["competitions"][0].get("series", None) + series_summary = "" + if series: + series_summary = series.get("summary", "") # Get game state information if status_state == "in": # For live games, get detailed state @@ -297,6 +300,7 @@ class Baseball(SportsCore): "outs": outs, "bases_occupied": bases_occupied, "start_time": game_event["date"], + "series_summary": series_summary, } ) @@ -320,6 +324,38 @@ class Baseball(SportsCore): ) return None + def display_series_summary(self, game: dict, draw_overlay: ImageDraw.ImageDraw): + if not self.show_series_summary: + return + + series_summary = game.get("series_summary", "") + font = ImageFont.truetype("assets/fonts/4x6-font.ttf", 6) + bbox = draw_overlay.textbbox((0, 0), series_summary, font=self.fonts['time']) + height = bbox[3] - bbox[1] + shots_y = (self.display_height - height) // 2 + shots_width = draw_overlay.textlength(series_summary, font=self.fonts['time']) + shots_x = (self.display_width - shots_width) // 2 + self._draw_text_with_outline( + draw_overlay, series_summary, (shots_x, shots_y), self.fonts['time'] + ) + +class BaseballRecent(Baseball, SportsRecent): + """Base class for recent 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) + + + def _custom_scorebug_layout(self, game: dict, draw_overlay: ImageDraw.ImageDraw): + self.display_series_summary(game, draw_overlay) + class BaseballLive(Baseball, SportsLive): """Base class for live baseball games.""" @@ -342,14 +378,25 @@ class BaseballLive(Baseball, SportsLive): # self.current_game["balls"] = random.choice([1, 2, 3]) # self.current_game["strikes"] = random.choice([1, 2]) # self.current_game["outs"] = random.choice([1, 2]) - if self.current_game["inning_half"] == "top": self.current_game["inning_half"] = "bottom" - else: self.current_game["inning_half"] = "top"; self.current_game["inning"] += 1 + if self.current_game["inning_half"] == "top": + self.current_game["inning_half"] = "bottom" + else: + self.current_game["inning_half"] = "top" + self.current_game["inning"] += 1 self.current_game["balls"] = (self.current_game["balls"] + 1) % 4 self.current_game["strikes"] = (self.current_game["strikes"] + 1) % 3 self.current_game["outs"] = (self.current_game["outs"] + 1) % 3 - self.current_game["bases_occupied"] = [not b for b in self.current_game["bases_occupied"]] - if self.current_game["inning"] % 2 == 0: self.current_game["home_score"] = str(int(self.current_game["home_score"]) + 1) - else: self.current_game["away_score"] = str(int(self.current_game["away_score"]) + 1) + self.current_game["bases_occupied"] = [ + not b for b in self.current_game["bases_occupied"] + ] + if self.current_game["inning"] % 2 == 0: + self.current_game["home_score"] = str( + int(self.current_game["home_score"]) + 1 + ) + else: + self.current_game["away_score"] = str( + int(self.current_game["away_score"]) + 1 + ) def _draw_scorebug_layout(self, game: Dict, force_clear: bool = False) -> None: """Draw the detailed scorebug layout for a live NCAA FB game.""" # Updated docstring @@ -676,5 +723,5 @@ class BaseballLive(Baseball, SportsLive): except Exception as e: self.logger.error( - f"Error displaying live Football game: {e}", exc_info=True + f"Error displaying live Baseball game: {e}", exc_info=True ) # Changed log prefix diff --git a/src/base_classes/hockey.py b/src/base_classes/hockey.py index 203d2566..7cfd2770 100644 --- a/src/base_classes/hockey.py +++ b/src/base_classes/hockey.py @@ -25,6 +25,7 @@ class Hockey(SportsCore): super().__init__(config, display_manager, cache_manager, logger, sport_key) self.data_source = ESPNDataSource(logger) self.sport = "hockey" + self.show_shots_on_goal = self.mode_config.get("show_shots_on_goal", False) def _extract_game_details(self, game_event: Dict) -> Optional[Dict]: """Extract relevant game details from ESPN NCAA FB API response.""" @@ -74,7 +75,7 @@ class Hockey(SportsCore): home_shots = 0 away_shots = 0 - + status_short = "" if home_team_saves_per > 0: away_shots = round(home_team_saves / home_team_saves_per) if away_team_saves_per > 0: @@ -82,8 +83,8 @@ class Hockey(SportsCore): 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() + # status_detail = status["type"].get("detail", "") + status_short = status["type"].get("shortDetail", "") powerplay = situation.get("isPowerPlay", False) penalties = situation.get("penalties", "") @@ -114,6 +115,8 @@ class Hockey(SportsCore): "penalties": penalties, "home_shots": home_shots, "away_shots": away_shots, + "is_period_break": status["type"]["name"] == "STATUS_END_PERIOD", + "status_short": status_short, } ) @@ -225,8 +228,8 @@ class HockeyLive(Hockey, SportsLive): period_clock_text = ( f"{game.get('period_text', '')} {game.get('clock', '')}".strip() ) - if game.get("is_halftime"): - period_clock_text = "Halftime" # Override for halftime + if game.get("is_period_break"): + period_clock_text = game.get("status_short", "Period Break") status_width = draw_overlay.textlength( period_clock_text, font=self.fonts["time"] @@ -254,18 +257,19 @@ class HockeyLive(Hockey, SportsLive): ) # Shots on Goal - shots_font = ImageFont.truetype("assets/fonts/4x6-font.ttf", 6) - home_shots = str(game.get("home_shots", "0")) - away_shots = str(game.get("away_shots", "0")) - shots_text = f"{away_shots} SHOTS {home_shots}" - shots_width = draw_overlay.textlength(shots_text, font=shots_font) - shots_x = (self.display_width - shots_width) // 2 - shots_y = ( - self.display_height - 10 - ) # centered #from 14 # Position score higher - self._draw_text_with_outline( - draw_overlay, shots_text, (shots_x, shots_y), shots_font - ) + if self.show_shots_on_goal: + shots_font = ImageFont.truetype("assets/fonts/4x6-font.ttf", 6) + home_shots = str(game.get("home_shots", "0")) + away_shots = str(game.get("away_shots", "0")) + shots_text = f"{away_shots} SHOTS {home_shots}" + shots_bbox = draw_overlay.textbbox((0, 0), shots_text, font=shots_font) + shots_height = shots_bbox[3] - shots_bbox[1] + shots_y = self.display_height - shots_height - 1 + shots_width = draw_overlay.textlength(shots_text, font=shots_font) + shots_x = (self.display_width - shots_width) // 2 + self._draw_text_with_outline( + draw_overlay, shots_text, (shots_x, shots_y), shots_font + ) # Draw odds if available if "odds" in game and game["odds"]: @@ -290,7 +294,7 @@ class HockeyLive(Hockey, SportsLive): record_bbox = draw_overlay.textbbox((0, 0), "0-0", font=record_font) record_height = record_bbox[3] - record_bbox[1] - record_y = self.display_height - record_height - 4 + record_y = self.display_height - record_height - 1 self.logger.debug( f"Record positioning: height={record_height}, record_y={record_y}, display_height={self.display_height}" ) diff --git a/src/base_classes/sports.py b/src/base_classes/sports.py index 78ac5164..0dd2484f 100644 --- a/src/base_classes/sports.py +++ b/src/base_classes/sports.py @@ -1,16 +1,16 @@ import logging import os import time +from abc import ABC, abstractmethod from datetime import datetime, timedelta, timezone from pathlib import Path -from typing import Any, Dict, List, Optional -from typing import Callable +from typing import Any, Callable, Dict, List, Optional + import pytz import requests from PIL import Image, ImageDraw, ImageFont from requests.adapters import HTTPAdapter from urllib3.util.retry import Retry -from abc import ABC, abstractmethod from src.background_data_service import get_background_service @@ -539,6 +539,9 @@ class SportsCore(ABC): self.logger.warning(f"Error fetching this weeks games for {self.sport} - {self.league} - {date_str}: {e}") return None + def _custom_scorebug_layout(self, game: dict, draw_overlay: ImageDraw.ImageDraw): + pass + class SportsUpcoming(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) @@ -718,11 +721,14 @@ class SportsUpcoming(SportsCore): # Note: Rankings are now handled in the records/rankings section below # "Next Game" at the top (use smaller status font) + status_font = self.fonts['status'] + if self.display_width > 128: + status_font = self.fonts['time'] status_text = "Next Game" - status_width = draw_overlay.textlength(status_text, font=self.fonts['status']) + status_width = draw_overlay.textlength(status_text, font=status_font) status_x = (self.display_width - status_width) // 2 status_y = 1 # Changed from 2 - self._draw_text_with_outline(draw_overlay, status_text, (status_x, status_y), self.fonts['status']) + self._draw_text_with_outline(draw_overlay, status_text, (status_x, status_y), status_font) # Date text (centered, below "Next Game") date_width = draw_overlay.textlength(game_date, font=self.fonts['time']) @@ -1114,6 +1120,7 @@ class SportsRecent(SportsCore): self.logger.debug(f"Drawing home ranking '{home_text}' at ({home_record_x}, {record_y}) with font size {record_font.size if hasattr(record_font, 'size') else 'unknown'}") self._draw_text_with_outline(draw_overlay, home_text, (home_record_x, record_y), record_font) + self._custom_scorebug_layout(game, draw_overlay) # Composite and display main_img = Image.alpha_composite(main_img, overlay) main_img = main_img.convert('RGB') @@ -1172,6 +1179,8 @@ class SportsLive(SportsCore): self.last_display_update = 0 self.last_log_time = 0 self.log_interval = 300 + self.last_count_log_time = 0 # Track when we last logged count data + self.count_log_interval = 5 # Only log count data every 5 seconds @abstractmethod def _test_mode_update(self) -> None: diff --git a/src/mlb_manager.py b/src/mlb_manager.py index 6b7bb313..25a79628 100644 --- a/src/mlb_manager.py +++ b/src/mlb_manager.py @@ -6,10 +6,11 @@ from pathlib import Path from typing import Any, Dict, Optional import pytz +from PIL import ImageDraw # Import baseball and standard sports classes -from src.base_classes.baseball import Baseball, BaseballLive -from src.base_classes.sports import SportsRecent, SportsUpcoming +from src.base_classes.baseball import Baseball, BaseballLive, BaseballRecent +from src.base_classes.sports import SportsUpcoming from src.cache_manager import CacheManager from src.display_manager import DisplayManager @@ -180,7 +181,7 @@ class MLBLiveManager(BaseMLBManager, BaseballLive): self.logger.info("Initialized MLBLiveManager in live mode") -class MLBRecentManager(BaseMLBManager, SportsRecent): +class MLBRecentManager(BaseMLBManager, BaseballRecent): """Manager for displaying recent MLB games.""" def __init__( @@ -195,7 +196,6 @@ class MLBRecentManager(BaseMLBManager, SportsRecent): f"Initialized MLBRecentManager with {len(self.favorite_teams)} favorite teams" ) # Changed log prefix - class MLBUpcomingManager(BaseMLBManager, SportsUpcoming): """Manager for displaying upcoming MLB games.""" diff --git a/src/nhl_managers.py b/src/nhl_managers.py index 73033eaf..887c0e9f 100644 --- a/src/nhl_managers.py +++ b/src/nhl_managers.py @@ -1,22 +1,15 @@ -import os -import time import logging -import requests -import json -from typing import Dict, Any, Optional, List -from PIL import Image, ImageDraw, ImageFont +from datetime import datetime from pathlib import Path -from datetime import datetime, timedelta, timezone -from src.display_manager import DisplayManager -from src.cache_manager import CacheManager -from src.config_manager import ConfigManager -from src.odds_manager import OddsManager -from src.background_data_service import get_background_service -from requests.adapters import HTTPAdapter -from urllib3.util.retry import Retry +from typing import Any, Dict, Optional + import pytz -from src.base_classes.sports import SportsRecent, SportsUpcoming +import requests + from src.base_classes.hockey import Hockey, HockeyLive +from src.base_classes.sports import SportsRecent, SportsUpcoming +from src.cache_manager import CacheManager +from src.display_manager import DisplayManager # Import the API counter function from web interface try: @@ -73,14 +66,14 @@ class BaseNHLManager(Hockey): if cached_data: # Validate cached data structure if isinstance(cached_data, dict) and 'events' in cached_data: - self.logger.info(f"[NHL] Using cached data for {season_year}") + self.logger.info(f"Using cached data for {season_year}") return cached_data elif isinstance(cached_data, list): # Handle old cache format (list of events) - self.logger.info(f"[NHL] Using cached data for {season_year} (legacy format)") + self.logger.info(f"Using cached data for {season_year} (legacy format)") return {'events': cached_data} else: - self.logger.warning(f"[NHL] Invalid cached data format for {season_year}: {type(cached_data)}") + self.logger.warning(f"Invalid cached data format for {season_year}: {type(cached_data)}") # Clear invalid cache self.cache_manager.clear_cache(cache_key) @@ -89,7 +82,7 @@ class BaseNHLManager(Hockey): return self._fetch_nhl_api_data_sync(use_cache) # Start background fetch - self.logger.info(f"[NHL] Starting background fetch for {season_year} season schedule...") + self.logger.info(f"Starting background fetch for {season_year} season schedule...") def fetch_callback(result): """Callback when background fetch completes.""" @@ -131,6 +124,30 @@ class BaseNHLManager(Hockey): return partial_data return None + + def _fetch_nhl_api_data_sync(self, use_cache: bool = True) -> Optional[Dict]: + """ + Synchronous fallback for fetching NFL data when background service is disabled. + """ + now = datetime.now(pytz.utc) + current_year = now.year + cache_key = f"nhl_schedule_{current_year}" + + self.logger.info(f"Fetching full {current_year} season schedule from ESPN API (sync mode)...") + try: + response = self.session.get(ESPN_NHL_SCOREBOARD_URL, params={"dates": current_year, "limit":1000}, headers=self.headers, timeout=15) + response.raise_for_status() + data = response.json() + events = data.get('events', []) + + if use_cache: + self.cache_manager.set(cache_key, events) + + self.logger.info(f"Successfully fetched {len(events)} events for the {current_year} season.") + return {'events': events} + except requests.exceptions.RequestException as e: + self.logger.error(f"API error fetching full schedule: {e}") + return None def _fetch_data(self, date_str: str = None) -> Optional[Dict]: """Fetch data using shared data mechanism or direct fetch for live.""" @@ -184,37 +201,3 @@ class NHLUpcomingManager(BaseNHLManager, SportsUpcoming): super().__init__(config, display_manager, cache_manager) self.logger = logging.getLogger('NHLUpcomingManager') # Changed logger name self.logger.info(f"Initialized NHLUpcomingManager with {len(self.favorite_teams)} favorite teams") - - """Display upcoming games.""" - if not self.upcoming_games: - current_time = time.time() - if current_time - self.last_warning_time > self.warning_cooldown: - self.logger.info("[NHL] No upcoming games to display") - self.last_warning_time = current_time - return # Skip display update entirely - - try: - current_time = time.time() - - # Check if it's time to switch games - if len(self.upcoming_games) > 1 and current_time - self.last_game_switch >= self.game_display_duration: - # Move to next game - self.current_game_index = (self.current_game_index + 1) % len(self.upcoming_games) - self.current_game = self.upcoming_games[self.current_game_index] - self.last_game_switch = current_time - force_clear = True # Force clear when switching games - - # Log team switching - if self.current_game: - away_abbr = self.current_game.get('away_abbr', 'UNK') - home_abbr = self.current_game.get('home_abbr', 'UNK') - self.logger.info(f"[NHL Upcoming] Showing {away_abbr} vs {home_abbr}") - - # Draw the scorebug layout - self._draw_scorebug_layout(self.current_game, force_clear) - - # Update display - self.display_manager.update_display() - - except Exception as e: - self.logger.error(f"[NHL] Error displaying upcoming game: {e}", exc_info=True) \ No newline at end of file