import logging import time from datetime import datetime, timezone from typing import Any, Dict, Optional from PIL import Image, ImageDraw, ImageFont from src.base_classes.data_sources import ESPNDataSource from src.base_classes.sports import SportsCore, SportsLive from src.cache_manager import CacheManager from src.display_manager import DisplayManager class Hockey(SportsCore): """Base class for hockey sports with common functionality.""" 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.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.""" # --- THIS METHOD MAY NEED ADJUSTMENTS FOR NCAA FB API DIFFERENCES --- details, home_team, away_team, status, situation = ( self._extract_game_details_common(game_event) ) if details is None or home_team is None or away_team is None or status is None: return try: competition = game_event["competitions"][0] status = competition["status"] powerplay = False penalties = "" home_team_saves = next( ( int(c["displayValue"]) for c in home_team["statistics"] if c.get("name") == "saves" ), 0, ) home_team_saves_per = next( ( float(c["displayValue"]) for c in home_team["statistics"] if c.get("name") == "savePct" ), 0.0, ) away_team_saves = next( ( int(c["displayValue"]) for c in away_team["statistics"] if c.get("name") == "saves" ), 0, ) away_team_saves_per = next( ( float(c["displayValue"]) for c in away_team["statistics"] if c.get("name") == "savePct" ), 0.0, ) home_shots = 0 away_shots = 0 if home_team_saves_per > 0: away_shots = round(home_team_saves / home_team_saves_per) if away_team_saves_per > 0: home_shots = round(away_team_saves / away_team_saves_per) status_short = status["type"].get("shortDetail", "") if situation and status["type"]["state"] == "in": # Detect scoring events from status detail # status_detail = status["type"].get("detail", "") powerplay = situation.get("isPowerPlay", False) penalties = situation.get("penalties", "") # Format period/quarter period = status.get("period", 0) period_text = "" if status["type"]["state"] == "in": if period == 0: period_text = "Start" # Before kickoff elif period >= 1 and period <= 3: period_text = f"P{period}" # OT starts after Q4 elif period > 3: period_text = f"OT{period - 3}" # OT starts after Q4 elif status["type"]["state"] == "post": if period > 3: period_text = "Final/OT" else: period_text = "Final" elif status["type"]["state"] == "pre": period_text = details.get("game_time", "") # Show time for upcoming details.update( { "period": period, "period_text": period_text, # Formatted quarter/status "clock": status.get("displayClock", "0:00"), "power_play": powerplay, "penalties": penalties, "home_shots": home_shots, "away_shots": away_shots, } ) # Basic validation (can be expanded) if not details["home_abbr"] or not details["away_abbr"]: self.logger.warning( f"Missing team abbreviation in event: {details['id']}" ) return None self.logger.debug( f"Extracted: {details['away_abbr']}@{details['home_abbr']}, Status: {status['type']['name']}, Live: {details['is_live']}, Final: {details['is_final']}, Upcoming: {details['is_upcoming']}" ) return details except Exception as e: # Log the problematic event structure if possible self.logger.error( f"Error extracting game details: {e} from event: {game_event.get('id')}", exc_info=True, ) return None class HockeyLive(Hockey, SportsLive): 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) def _test_mode_update(self): if self.current_game and self.current_game["is_live"]: # For testing, we'll just update the clock to show it's working minutes = int(self.current_game["clock"].split(":")[0]) seconds = int(self.current_game["clock"].split(":")[1]) seconds -= 1 if seconds < 0: seconds = 59 minutes -= 1 if minutes < 0: minutes = 19 if self.current_game["period"] < 3: self.current_game["period"] += 1 else: self.current_game["period"] = 1 self.current_game["clock"] = f"{minutes:02d}:{seconds:02d}" # Always update display in test mode 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 try: main_img = Image.new( "RGBA", (self.display_width, self.display_height), (0, 0, 0, 255) ) overlay = Image.new( "RGBA", (self.display_width, self.display_height), (0, 0, 0, 0) ) draw_overlay = ImageDraw.Draw( overlay ) # Draw text elements on overlay first home_logo = self._load_and_resize_logo( game["home_id"], game["home_abbr"], game["home_logo_path"], game.get("home_logo_url"), ) away_logo = self._load_and_resize_logo( game["away_id"], game["away_abbr"], game["away_logo_path"], game.get("away_logo_url"), ) if not home_logo or not away_logo: self.logger.error( f"Failed to load logos for live game: {game.get('id')}" ) # Changed log prefix # Draw placeholder text if logos fail draw_final = ImageDraw.Draw(main_img.convert("RGB")) self._draw_text_with_outline( draw_final, "Logo Error", (5, 5), self.fonts["status"] ) self.display_manager.image.paste(main_img.convert("RGB"), (0, 0)) self.display_manager.update_display() return center_y = self.display_height // 2 # Draw logos (shifted slightly more inward than NHL perhaps) home_x = ( self.display_width - home_logo.width + 10 ) # adjusted from 18 # Adjust position as needed home_y = center_y - (home_logo.height // 2) main_img.paste(home_logo, (home_x, home_y), home_logo) away_x = -10 # adjusted from 18 # Adjust position as needed away_y = center_y - (away_logo.height // 2) main_img.paste(away_logo, (away_x, away_y), away_logo) # --- Draw Text Elements on Overlay --- # Note: Rankings are now handled in the records/rankings section below # Period/Quarter and Clock (Top center) period_clock_text = ( f"{game.get('period_text', '')} {game.get('clock', '')}".strip() ) if game.get("is_period_break"): period_clock_text = game.get("status_text", "Period Break") status_width = draw_overlay.textlength( period_clock_text, font=self.fonts["time"] ) status_x = (self.display_width - status_width) // 2 status_y = 1 # Position at top self._draw_text_with_outline( draw_overlay, period_clock_text, (status_x, status_y), self.fonts["time"], ) # Scores (centered, slightly above bottom) home_score = str(game.get("home_score", "0")) away_score = str(game.get("away_score", "0")) score_text = f"{away_score}-{home_score}" score_width = draw_overlay.textlength(score_text, font=self.fonts["score"]) score_x = (self.display_width - score_width) // 2 score_y = ( self.display_height // 2 ) - 3 # centered #from 14 # Position score higher self._draw_text_with_outline( draw_overlay, score_text, (score_x, score_y), self.fonts["score"] ) # Shots on Goal 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"]: self._draw_dynamic_odds( draw_overlay, game["odds"], self.display_width, self.display_height ) # Draw records or rankings if enabled if self.show_records or self.show_ranking: try: record_font = ImageFont.truetype("assets/fonts/4x6-font.ttf", 6) self.logger.debug(f"Loaded 6px record font successfully") except IOError: record_font = ImageFont.load_default() self.logger.warning( f"Failed to load 6px font, using default font (size: {record_font.size})" ) # Get team abbreviations away_abbr = game.get("away_abbr", "") home_abbr = game.get("home_abbr", "") 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 - 1 self.logger.debug( f"Record positioning: height={record_height}, record_y={record_y}, display_height={self.display_height}" ) # Display away team info if away_abbr: if self.show_ranking and self.show_records: # When both rankings and records are enabled, rankings replace records completely away_rank = self._team_rankings_cache.get(away_abbr, 0) if away_rank > 0: away_text = f"#{away_rank}" else: # Show nothing for unranked teams when rankings are prioritized away_text = "" elif self.show_ranking: # Show ranking only if available away_rank = self._team_rankings_cache.get(away_abbr, 0) if away_rank > 0: away_text = f"#{away_rank}" else: away_text = "" elif self.show_records: # Show record only when rankings are disabled away_text = game.get("away_record", "") else: away_text = "" if away_text: away_record_x = 3 self.logger.debug( f"Drawing away ranking '{away_text}' at ({away_record_x}, {record_y}) with font size {record_font.size if hasattr(record_font, 'size') else 'unknown'}" ) self._draw_text_with_outline( draw_overlay, away_text, (away_record_x, record_y), record_font, ) # Display home team info if home_abbr: if self.show_ranking and self.show_records: # When both rankings and records are enabled, rankings replace records completely home_rank = self._team_rankings_cache.get(home_abbr, 0) if home_rank > 0: home_text = f"#{home_rank}" else: # Show nothing for unranked teams when rankings are prioritized home_text = "" elif self.show_ranking: # Show ranking only if available home_rank = self._team_rankings_cache.get(home_abbr, 0) if home_rank > 0: home_text = f"#{home_rank}" else: home_text = "" elif self.show_records: # Show record only when rankings are disabled home_text = game.get("home_record", "") else: home_text = "" if home_text: home_record_bbox = draw_overlay.textbbox( (0, 0), home_text, font=record_font ) home_record_width = home_record_bbox[2] - home_record_bbox[0] home_record_x = self.display_width - home_record_width - 3 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, ) # Composite the text overlay onto the main image main_img = Image.alpha_composite(main_img, overlay) main_img = main_img.convert("RGB") # Convert for display # Display the final image self.display_manager.image.paste(main_img, (0, 0)) self.display_manager.update_display() # Update display here for live except Exception as e: self.logger.error( f"Error displaying live Hockey game: {e}", exc_info=True ) # Changed log prefix