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 Basketball(SportsCore): """Base class for basketball 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 = "basketball" 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: # 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 <= 4: period_text = f"Q{period}" # OT starts after Q4 elif period > 4: period_text = f"OT{period - 4}" # OT starts after Q4 elif status["type"]["state"] == "halftime" or status["type"]["name"] == "STATUS_HALFTIME": # Check explicit halftime state period_text = "HALF" elif status["type"]["state"] == "post": if period > 4 : 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"), }) # 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 BasketballLive(Basketball, 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 Basketball 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() ) 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"] ) # 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: record_font = self.fonts.get('detail', ImageFont.load_default()) # 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