refactor(nhl): Collect and cycle through all relevant favorite game statuses (live, upcoming, final)

This commit is contained in:
ChuckBuilds
2025-04-17 13:08:56 -05:00
parent 218d4af5e9
commit e0660dbb6a

View File

@@ -5,6 +5,7 @@ from pathlib import Path
from PIL import Image, ImageDraw, ImageFont from PIL import Image, ImageDraw, ImageFont
import logging import logging
from datetime import datetime, timedelta, timezone from datetime import datetime, timedelta, timezone
from typing import List, Dict, Optional
try: try:
from zoneinfo import ZoneInfo, ZoneInfoNotFoundError from zoneinfo import ZoneInfo, ZoneInfoNotFoundError
except ImportError: except ImportError:
@@ -325,7 +326,7 @@ def create_scorebug_image(game_details):
try: try:
away_logo = Image.open(game_details["away_logo_path"]).convert("RGBA") away_logo = Image.open(game_details["away_logo_path"]).convert("RGBA")
away_logo.thumbnail(logo_size, Image.Resampling.LANCZOS) away_logo.thumbnail(logo_size, Image.Resampling.LANCZOS)
img.paste(away_logo, away_logo_pos, away_logo) # Use logo as mask for transparency img.paste(away_logo, away_logo_pos, mask=away_logo)
except Exception as e: except Exception as e:
logging.error(f"Error loading/pasting away logo {game_details['away_logo_path']}: {e}") logging.error(f"Error loading/pasting away logo {game_details['away_logo_path']}: {e}")
# Draw placeholder text if logo fails # Draw placeholder text if logo fails
@@ -341,7 +342,7 @@ def create_scorebug_image(game_details):
try: try:
home_logo = Image.open(game_details["home_logo_path"]).convert("RGBA") home_logo = Image.open(game_details["home_logo_path"]).convert("RGBA")
home_logo.thumbnail(logo_size, Image.Resampling.LANCZOS) home_logo.thumbnail(logo_size, Image.Resampling.LANCZOS)
img.paste(home_logo, home_logo_pos, home_logo) img.paste(home_logo, home_logo_pos, mask=home_logo)
except Exception as e: except Exception as e:
logging.error(f"Error loading/pasting home logo {game_details['home_logo_path']}: {e}") logging.error(f"Error loading/pasting home logo {game_details['home_logo_path']}: {e}")
draw.text(home_logo_pos, game_details["home_abbr"], font=team_font, fill="white") draw.text(home_logo_pos, game_details["home_abbr"], font=team_font, fill="white")
@@ -579,14 +580,13 @@ class NHLScoreboardManager:
self.local_timezone = ZoneInfo(DEFAULT_TIMEZONE) self.local_timezone = ZoneInfo(DEFAULT_TIMEZONE)
# State variables # State variables
self.last_data_fetch_time = 0 # When API was last called self.last_data_fetch_time = 0
self.last_logic_update_time = 0 # When selection logic was last run self.last_logic_update_time = 0
self.relevant_events = [] # Events matching current display criteria self.relevant_events: List[Dict[str, Any]] = [] # ALL relevant events (live, upcoming today, recent final)
self.current_event_index = 0 # Index for cycling through relevant_events self.current_event_index: int = 0
self.last_cycle_time = 0 # Timestamp for cycling live games self.last_cycle_time: float = 0
self.display_mode = 'none' # 'live', 'upcoming', 'recent_final', 'none', 'error' self.current_display_details: Optional[Dict[str, Any]] = None
self.current_display_details = None # Details for the currently shown game in cycle self.needs_redraw: bool = True
self.needs_redraw = True # Flag to force redraw in display()
# Get display dimensions # Get display dimensions
if hasattr(display_manager, 'width') and hasattr(display_manager, 'height'): if hasattr(display_manager, 'width') and hasattr(display_manager, 'height'):
@@ -709,63 +709,9 @@ class NHLScoreboardManager:
logging.error(f"[NHL] Failed to load test data: {load_e}") logging.error(f"[NHL] Failed to load test data: {load_e}")
return None # Return None if fetch fails return None # Return None if fetch fails
def _extract_game_details(self, game_event): def _find_events_by_criteria(self, all_events: List[Dict[str, Any]],
"""Extracts relevant details for the score bug display from raw event data.""" is_live: bool = False, is_upcoming_today: bool = False,
if not game_event: is_recent_final: bool = False) -> List[Dict[str, Any]]:
return None
details = {}
try:
competition = game_event["competitions"][0]
status = competition["status"]
competitors = competition["competitors"]
game_date_str = game_event["date"]
try:
details["start_time_utc"] = datetime.fromisoformat(game_date_str.replace("Z", "+00:00"))
except (ValueError, KeyError):
logging.warning(f"[NHL] Could not parse game date: {game_date_str}")
details["start_time_utc"] = None
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:
logging.warning(f"[NHL] Missing home or away team data in event: {game_event.get('id')}")
return None # Cannot proceed without both teams
details["status_text"] = status.get("type", {}).get("shortDetail", "N/A")
details["status_type_name"] = status.get("type", {}).get("name")
details["period"] = status.get("period", 0)
details["clock"] = status.get("displayClock", "0:00")
state = status.get("type", {}).get("state")
details["is_live"] = state in ("in", "halftime")
details["is_final"] = state == "post"
details["is_upcoming"] = state == "pre"
details["home_abbr"] = home_team.get("team", {}).get("abbreviation", "???")
details["home_score"] = home_team.get("score", "0")
details["home_logo_path"] = self.logo_dir / f"{details['home_abbr']}.png" if details['home_abbr'] != "???" else None
details["away_abbr"] = away_team.get("team", {}).get("abbreviation", "???")
details["away_score"] = away_team.get("score", "0")
details["away_logo_path"] = self.logo_dir / f"{details['away_abbr']}.png" if details['away_abbr'] != "???" else None
# Check logo files
if details["home_logo_path"] and not details["home_logo_path"].is_file():
logging.debug(f"[NHL] Home logo not found: {details['home_logo_path']}")
details["home_logo_path"] = None
if details["away_logo_path"] and not details["away_logo_path"].is_file():
logging.debug(f"[NHL] Away logo not found: {details['away_logo_path']}")
details["away_logo_path"] = None
return details
except (KeyError, IndexError, StopIteration, TypeError) as e:
logging.error(f"[NHL] Error parsing game details: {e} - Data snippet: {str(game_event)[:200]}...")
return None
def _find_events_by_criteria(self, all_events, is_live=False, is_upcoming_today=False, is_recent_final=False):
"""Helper to find favorite team events matching specific criteria.""" """Helper to find favorite team events matching specific criteria."""
matches = [] matches = []
now_utc = datetime.now(timezone.utc) now_utc = datetime.now(timezone.utc)
@@ -773,25 +719,25 @@ class NHLScoreboardManager:
cutoff_time_utc = now_utc - timedelta(hours=RECENT_GAME_HOURS) cutoff_time_utc = now_utc - timedelta(hours=RECENT_GAME_HOURS)
for event in all_events: for event in all_events:
# Check if it's a favorite team's game first
competitors = event.get("competitions", [{}])[0].get("competitors", []) competitors = event.get("competitions", [{}])[0].get("competitors", [])
if len(competitors) == 2: if len(competitors) == 2:
team1_abbr = competitors[0].get("team", {}).get("abbreviation") team1_abbr = competitors[0].get("team", {}).get("abbreviation")
team2_abbr = competitors[1].get("team", {}).get("abbreviation") team2_abbr = competitors[1].get("team", {}).get("abbreviation")
is_favorite = team1_abbr in self.favorite_teams or team2_abbr in self.favorite_teams is_favorite = team1_abbr in self.favorite_teams or team2_abbr in self.favorite_teams
# Skip non-favorites ONLY if show_only_favorites is true
if not is_favorite and self.show_only_favorites: if not is_favorite and self.show_only_favorites:
continue # Skip if not favorite and only showing favorites continue
# Now check criteria based on flags # Apply criteria ONLY if the event involves a favorite team
if is_favorite:
details = self._extract_game_details(event) details = self._extract_game_details(event)
if not details: continue # Skip if parsing failed if not details: continue
if is_live and details["is_live"]: if is_live and details["is_live"]:
matches.append(event) matches.append(event)
continue # Found live, no need for further checks for this event continue
if is_upcoming_today and details["is_upcoming"] and details["start_time_utc"]: if is_upcoming_today and details["is_upcoming"] and details["start_time_utc"]:
# Check if start time is today in local timezone
start_local_date = details["start_time_utc"].astimezone(self.local_timezone).date() start_local_date = details["start_time_utc"].astimezone(self.local_timezone).date()
if start_local_date == today_local and details["start_time_utc"] > now_utc: if start_local_date == today_local and details["start_time_utc"] > now_utc:
matches.append(event) matches.append(event)
@@ -801,174 +747,183 @@ class NHLScoreboardManager:
if details["start_time_utc"] > cutoff_time_utc: if details["start_time_utc"] > cutoff_time_utc:
matches.append(event) matches.append(event)
continue continue
# Add fallback logic if not showing only favorites? Or handle in update()? # --- NOTE: No fallback logic here yet for non-favorites if show_only_favorites is false ---
# For now, this focuses on finding FAVORITE events based on criteria.
# Sort results appropriately # Sort results appropriately
if is_live: if is_live:
# Maybe sort live games by start time or some other metric? pass # Keep API order for now
pass # Keep order for now
elif is_upcoming_today: elif is_upcoming_today:
matches.sort(key=lambda x: self._extract_game_details(x).get("start_time_utc") or datetime.max.replace(tzinfo=timezone.utc)) # Sort by soonest start time matches.sort(key=lambda x: self._extract_game_details(x).get("start_time_utc") or datetime.max.replace(tzinfo=timezone.utc)) # Sort by soonest
elif is_recent_final: elif is_recent_final:
matches.sort(key=lambda x: self._extract_game_details(x).get("start_time_utc") or datetime.min.replace(tzinfo=timezone.utc), reverse=True) # Sort by most recent start time matches.sort(key=lambda x: self._extract_game_details(x).get("start_time_utc") or datetime.min.replace(tzinfo=timezone.utc), reverse=True) # Sort by most recent
return matches return matches
def update(self): def update(self):
"""Determines the correct state and events to display.""" """Determines the list of events to cycle through based on priority."""
if not self.is_enabled: if not self.is_enabled:
if self.display_mode != 'none': if self.relevant_events: # Clear if disabled
self.display_mode = 'none'
self.relevant_events = [] self.relevant_events = []
self.current_event_index = 0 self.current_event_index = 0
self.needs_redraw = True self.needs_redraw = True
return return
now = time.time() now = time.time()
# Determine required data fetch interval # Use active interval for logic checks if we *might* be showing something, else idle
# Always use idle interval check; fetch logic inside handles actual API call timing check_interval = self.update_interval if self.relevant_events else self.idle_update_interval
check_interval = self.idle_update_interval
# If currently showing live game, force more frequent logic checks
# This ensures we notice quickly if the game ends
if self.display_mode == 'live':
check_interval = self.update_interval
if now - self.last_logic_update_time < check_interval: if now - self.last_logic_update_time < check_interval:
# Ensure live game cycling still happens even if logic doesn't run # Live game cycling still needs to happen within display() based on its own timer
if self.display_mode == 'live' and len(self.relevant_events) > 1: return
if now - self.last_cycle_time > self.cycle_duration:
self.current_event_index = (self.current_event_index + 1) % len(self.relevant_events)
self.current_display_details = self._extract_game_details(self.relevant_events[self.current_event_index])
self.last_cycle_time = now
self.needs_redraw = True
return # Interval not elapsed for full logic update
logging.info(f"[NHL] Running update logic (Check Interval: {check_interval}s)") logging.info(f"[NHL] Running update logic (Check Interval: {check_interval}s)")
self.last_logic_update_time = now self.last_logic_update_time = now
# Decide which dates to fetch based on current time and lookback/lookahead needs # Fetch data if interval passed
all_events: List[Dict] = []
if now - self.last_data_fetch_time > self.update_interval:
today_local = datetime.now(self.local_timezone) today_local = datetime.now(self.local_timezone)
dates_to_fetch = { dates_to_fetch = {
(today_local - timedelta(days=2)).strftime('%Y%m%d'), # Two days ago (for 48h lookback) (today_local - timedelta(days=2)).strftime('%Y%m%d'),
(today_local - timedelta(days=1)).strftime('%Y%m%d'), # Yesterday (today_local - timedelta(days=1)).strftime('%Y%m%d'),
today_local.strftime('%Y%m%d'), # Today today_local.strftime('%Y%m%d'),
(today_local + timedelta(days=1)).strftime('%Y%m%d') # Tomorrow (for upcoming today) (today_local + timedelta(days=1)).strftime('%Y%m%d')
} }
# Only fetch if data is stale based on *active* interval (ensures fresh data for live checks)
if now - self.last_data_fetch_time > self.update_interval:
all_events = self._fetch_data_for_dates(sorted(list(dates_to_fetch))) all_events = self._fetch_data_for_dates(sorted(list(dates_to_fetch)))
else:
# Use potentially stale data if logic check is frequent but data fetch isn't needed yet
# Or should we force fetch here? Let's assume stale data is ok for now if fetch interval not met.
# This part needs refinement - how to handle stale data vs frequent logic checks?
# For simplicity now, let's assume we fetch every logic check for testing.
all_events = self._fetch_data_for_dates(sorted(list(dates_to_fetch)))
if not all_events: if not all_events:
logging.warning("[NHL] No events found after fetching. Setting mode to error/none.") logging.warning("[NHL] No events found after fetching.")
if self.display_mode != 'error': # Decide how to handle fetch failure - clear existing or keep stale?
self.display_mode = 'error' # Or 'none'? # Let's clear for now if fetch fails entirely
if self.relevant_events: # If we previously had events
self.relevant_events = [] self.relevant_events = []
self.current_event_index = 0 self.current_event_index = 0
self.current_display_details = None
self.needs_redraw = True
return # Stop update if fetch failed
else:
# Data not stale enough for API call, but logic check proceeds.
# How do we get all_events? Need to cache it?
# --> Problem: Can't re-evaluate criteria without fetching.
# --> Solution: Always fetch data when logic runs, but use interval for logic run itself.
# --> Let's revert: Fetch data based on fetch interval, run logic based on logic interval.
# --> Requires caching the fetched `all_events`. Let's add a cache.
# --- Let's stick to the previous version's combined logic/fetch interval for now ---
# --- and focus on combining the event lists ---
today_local = datetime.now(self.local_timezone)
dates_to_fetch = {
(today_local - timedelta(days=2)).strftime('%Y%m%d'),
(today_local - timedelta(days=1)).strftime('%Y%m%d'),
today_local.strftime('%Y%m%d'),
(today_local + timedelta(days=1)).strftime('%Y%m%d')
}
all_events = self._fetch_data_for_dates(sorted(list(dates_to_fetch)))
if not all_events:
logging.warning("[NHL] No events found after fetching.")
if self.relevant_events:
self.relevant_events = []
self.current_event_index = 0
self.current_display_details = None
self.needs_redraw = True self.needs_redraw = True
return return
# --- Determine State and Relevant Events ---
new_mode = 'none'
new_relevant_events = []
# 1. Check for Live Favorite Games # --- Determine Combined List of Relevant Events ---
live_favorite_games = self._find_events_by_criteria(all_events, is_live=True) live_events = self._find_events_by_criteria(all_events, is_live=True)
if live_favorite_games: upcoming_events = self._find_events_by_criteria(all_events, is_upcoming_today=True)
new_mode = 'live' recent_final_events = self._find_events_by_criteria(all_events, is_recent_final=True)
new_relevant_events = live_favorite_games
logging.info(f"[NHL] Found {len(live_favorite_games)} live favorite game(s).")
# 2. Check for Upcoming Favorite Games Today (if no live ones) new_relevant_events_combined = []
if new_mode == 'none': added_ids = set()
upcoming_today_games = self._find_events_by_criteria(all_events, is_upcoming_today=True)
if upcoming_today_games:
new_mode = 'upcoming'
new_relevant_events = [upcoming_today_games[0]] # Show only the soonest one
logging.info(f"[NHL] No live games. Found upcoming favorite game today.")
# 3. Check for Recent Favorite Finals (if no live or upcoming today) # Add in order of priority, avoiding duplicates
if new_mode == 'none': for event in live_events + upcoming_events + recent_final_events:
recent_finals = self._find_events_by_criteria(all_events, is_recent_final=True) event_id = event.get("id")
if recent_finals: if event_id and event_id not in added_ids:
new_mode = 'recent_final' new_relevant_events_combined.append(event)
new_relevant_events = [recent_finals[0]] # Show only the most recent one added_ids.add(event_id)
logging.info(f"[NHL] No live or upcoming games. Found recent favorite final.")
# 4. Fallback (if not show_only_favorites) - Currently handled by find criteria, maybe refine? # --- TODO: Implement Fallback Logic if show_only_favorites is False ---
# For now, if show_only_favorites is true, and none of above, mode remains 'none'. if not new_relevant_events_combined and not self.show_only_favorites:
# If show_only_favorites is false, need to implement fallback search here if desired. logging.debug("[NHL] No relevant favorite games, show_only_favorites=false. Fallback needed.")
if new_mode == 'none' and not self.show_only_favorites: # Add logic here to find non-favorite games based on priority if desired
logging.debug("[NHL] No relevant favorite games, show_only_favorites=false, applying fallback.") pass # No fallback implemented yet
# Basic fallback: Show first live non-fav, or first upcoming non-fav today, or most recent non-fav final?
# Keeping it simple for now: if no favs, mode is 'none'. Refine fallback later if needed.
pass
# --- Compare and Update State ---
old_event_ids = {e.get("id") for e in self.relevant_events if e}
new_event_ids = {e.get("id") for e in new_relevant_events_combined if e}
# --- Update State if Changed --- if old_event_ids != new_event_ids:
if new_mode != self.display_mode or new_relevant_events != self.relevant_events: logging.info(f"[NHL] Relevant events changed. New count: {len(new_relevant_events_combined)}")
logging.info(f"[NHL] Mode change: {self.display_mode} -> {new_mode}") self.relevant_events = new_relevant_events_combined
logging.debug(f"[NHL] Relevant Events Change: {len(self.relevant_events)} -> {len(new_relevant_events)}") self.current_event_index = 0
self.display_mode = new_mode
self.relevant_events = new_relevant_events
self.current_event_index = 0 # Reset index on mode/list change
self.last_cycle_time = time.time() # Reset cycle timer self.last_cycle_time = time.time() # Reset cycle timer
# Load details for the first item immediately
self.current_display_details = self._extract_game_details(self.relevant_events[0] if self.relevant_events else None) self.current_display_details = self._extract_game_details(self.relevant_events[0] if self.relevant_events else None)
self.needs_redraw = True self.needs_redraw = True
elif self.display_mode == 'live': elif self.relevant_events: # List content is same, check if details of *current* item changed
# If mode is still live, check if *details* of current game changed
current_event_in_list = self.relevant_events[self.current_event_index] current_event_in_list = self.relevant_events[self.current_event_index]
new_details_for_current = self._extract_game_details(current_event_in_list) new_details_for_current = self._extract_game_details(current_event_in_list)
if new_details_for_current != self.current_display_details: # Compare specifically relevant fields (score, clock, period, status)
logging.debug("[NHL] Live game details updated.") if self._details_changed_significantly(self.current_display_details, new_details_for_current):
logging.debug(f"[NHL] Details updated for current event index {self.current_event_index}")
self.current_display_details = new_details_for_current self.current_display_details = new_details_for_current
self.needs_redraw = True self.needs_redraw = True
# else: No change in mode, events, or live details else:
logging.debug("[NHL] No significant change in details for current event.")
# else: No relevant events before or after
def _details_changed_significantly(self, old_details, new_details) -> bool:
"""Compare specific fields to see if a redraw is needed."""
if old_details is None and new_details is None: return False
if old_details is None or new_details is None: return True # Changed from something to nothing or vice-versa
fields_to_check = ['home_score', 'away_score', 'clock', 'period', 'is_live', 'is_final', 'is_upcoming']
for field in fields_to_check:
if old_details.get(field) != new_details.get(field):
return True
return False
def display(self, force_clear: bool = False): def display(self, force_clear: bool = False):
"""Generates and displays the current frame based on the determined state.""" """Generates and displays the current frame, handling cycling."""
if not self.is_enabled: if not self.is_enabled:
return return
now = time.time() now = time.time()
redraw_this_frame = force_clear or self.needs_redraw redraw_this_frame = force_clear or self.needs_redraw
# --- Handle Live Game Cycling --- # --- Handle Cycling ---
if self.display_mode == 'live' and len(self.relevant_events) > 1: if len(self.relevant_events) > 1: # Cycle if more than one relevant event exists
if now - self.last_cycle_time > self.cycle_duration: if now - self.last_cycle_time > self.cycle_duration:
self.current_event_index = (self.current_event_index + 1) % len(self.relevant_events) self.current_event_index = (self.current_event_index + 1) % len(self.relevant_events)
# Get details for the *new* index
self.current_display_details = self._extract_game_details(self.relevant_events[self.current_event_index]) self.current_display_details = self._extract_game_details(self.relevant_events[self.current_event_index])
self.last_cycle_time = now self.last_cycle_time = now
redraw_this_frame = True # Force redraw on cycle redraw_this_frame = True # Force redraw on cycle
logging.debug(f"[NHL] Cycling live game to index {self.current_event_index}") logging.debug(f"[NHL] Cycling to event index {self.current_event_index}")
elif self.current_display_details is None: # Ensure details are loaded initially elif self.current_display_details is None: # Ensure details loaded for index 0 initially
self.current_display_details = self._extract_game_details(self.relevant_events[self.current_event_index]) self.current_display_details = self._extract_game_details(self.relevant_events[0])
redraw_this_frame = True redraw_this_frame = True # Force redraw if details were missing
elif self.display_mode != 'live': elif len(self.relevant_events) == 1:
# Ensure details are loaded if mode is not live (handles initial state or mode change) # If only one event, make sure its details are loaded
if self.current_display_details is None and self.relevant_events: if self.current_display_details is None:
self.current_display_details = self._extract_game_details(self.relevant_events[0]) self.current_display_details = self._extract_game_details(self.relevant_events[0])
redraw_this_frame = True redraw_this_frame = True
elif not self.relevant_events: # No relevant events found else: # No relevant events
self.current_display_details = None # Ensure details are cleared if self.current_display_details is not None: # Clear details if list is empty now
self.current_display_details = None
redraw_this_frame = True
# --- Generate and Display Frame --- # --- Generate and Display Frame ---
if not redraw_this_frame: if not redraw_this_frame:
# logging.debug("[NHL] display() called but no redraw needed.")
return return
logging.debug(f"[NHL] Generating frame for mode: {self.display_mode} (Index: {self.current_event_index})") logging.debug(f"[NHL] Generating frame (Index: {self.current_event_index})")
# Use self.current_display_details which is updated by cycle logic or initial load frame = self._create_frame(self.current_display_details) # Pass the specific details
frame = self._create_frame(self.current_display_details)
try: try:
if hasattr(self.display_manager, 'display_image'): if hasattr(self.display_manager, 'display_image'):
@@ -978,48 +933,44 @@ class NHLScoreboardManager:
else: else:
logging.error("[NHL] DisplayManager missing display_image or matrix.SetImage method.") logging.error("[NHL] DisplayManager missing display_image or matrix.SetImage method.")
self.needs_redraw = False # Reset flag after successful display attempt self.needs_redraw = False
except Exception as e: except Exception as e:
logging.error(f"[NHL] Error displaying frame via DisplayManager: {e}") logging.error(f"[NHL] Error displaying frame via DisplayManager: {e}")
def _create_frame(self, game_details): def _create_frame(self, game_details: Optional[Dict[str, Any]]) -> Image.Image:
"""Creates a Pillow image frame based on game details and current display mode.""" """Creates a Pillow image frame based on game details."""
# Determine layout based on self.display_mode and game_details # This method now simply renders the layout based on the details passed in
# Note: game_details will be None if mode is 'none' or 'error'
img = Image.new('RGB', (self.display_width, self.display_height), color='black') img = Image.new('RGB', (self.display_width, self.display_height), color='black')
draw = ImageDraw.Draw(img) draw = ImageDraw.Draw(img)
# --- Choose Layout --- if not game_details:
if self.display_mode == 'live' and game_details:
self._draw_scorebug_layout(draw, game_details)
elif self.display_mode == 'upcoming' and game_details:
self._draw_upcoming_layout(draw, game_details)
elif self.display_mode == 'recent_final' and game_details:
self._draw_scorebug_layout(draw, game_details) # Use scorebug for final
else: # 'none' or 'error'
self._draw_placeholder_layout(draw) self._draw_placeholder_layout(draw)
elif game_details.get("is_upcoming"):
self._draw_upcoming_layout(draw, game_details)
elif game_details.get("is_live") or game_details.get("is_final"):
self._draw_scorebug_layout(draw, game_details)
else: # Fallback/Other states
self._draw_placeholder_layout(draw, msg=game_details.get("status_text", "NHL Status")) # Show status text
return img return img
def _draw_placeholder_layout(self, draw): def _draw_placeholder_layout(self, draw: ImageDraw.ImageDraw, msg: str = "No NHL Games"):
"""Draws the 'No NHL Games' message.""" """Draws the 'No NHL Games' or other placeholder message."""
font = self.fonts.get('placeholder', ImageFont.load_default()) font = self.fonts.get('placeholder', ImageFont.load_default())
msg = "No NHL Games"
bbox = draw.textbbox((0,0), msg, font=font) bbox = draw.textbbox((0,0), msg, font=font)
text_width = bbox[2] - bbox[0] text_width = bbox[2] - bbox[0]
text_height = bbox[3] - bbox[1] text_height = bbox[3] - bbox[1]
draw.text(((self.display_width - text_width) // 2, (self.display_height - text_height) // 2), draw.text(((self.display_width - text_width) // 2, (self.display_height - text_height) // 2),
msg, font=font, fill='grey') msg, font=font, fill='grey')
def _draw_upcoming_layout(self, draw, game_details): def _draw_upcoming_layout(self, draw: ImageDraw.ImageDraw, game_details: Dict[str, Any]):
"""Draws the layout for an upcoming game.""" """Draws the layout for an upcoming game."""
font_team = self.fonts.get('team', ImageFont.load_default()) font_team = self.fonts.get('team', ImageFont.load_default())
font_main = self.fonts.get('upcoming_main', ImageFont.load_default()) font_main = self.fonts.get('upcoming_main', ImageFont.load_default())
font_vs = self.fonts.get('upcoming_vs', ImageFont.load_default()) font_vs = self.fonts.get('upcoming_vs', ImageFont.load_default())
img = draw.im # Get the image object associated with the draw object img = draw.im
logging.debug("[NHL] Drawing upcoming game layout.") logging.debug("[NHL] Drawing upcoming game layout.")
@@ -1037,7 +988,7 @@ class NHLScoreboardManager:
try: try:
away_logo = Image.open(game_details["away_logo_path"]).convert("RGBA") away_logo = Image.open(game_details["away_logo_path"]).convert("RGBA")
away_logo.thumbnail(logo_size, Image.Resampling.LANCZOS) away_logo.thumbnail(logo_size, Image.Resampling.LANCZOS)
img.paste(away_logo, (away_logo_x, (self.display_height - away_logo.height) // 2), away_logo) img.paste(away_logo, (away_logo_x, (self.display_height - away_logo.height) // 2), mask=away_logo)
except Exception as e: except Exception as e:
logging.error(f"[NHL] Error rendering upcoming away logo {game_details['away_logo_path']}: {e}") logging.error(f"[NHL] Error rendering upcoming away logo {game_details['away_logo_path']}: {e}")
draw.text((away_logo_x, 5), game_details.get("away_abbr", "?"), font=font_team, fill="white") draw.text((away_logo_x, 5), game_details.get("away_abbr", "?"), font=font_team, fill="white")
@@ -1049,7 +1000,7 @@ class NHLScoreboardManager:
try: try:
home_logo = Image.open(game_details["home_logo_path"]).convert("RGBA") home_logo = Image.open(game_details["home_logo_path"]).convert("RGBA")
home_logo.thumbnail(logo_size, Image.Resampling.LANCZOS) home_logo.thumbnail(logo_size, Image.Resampling.LANCZOS)
img.paste(home_logo, (home_logo_x, (self.display_height - home_logo.height) // 2), home_logo) img.paste(home_logo, (home_logo_x, (self.display_height - home_logo.height) // 2), mask=home_logo)
except Exception as e: except Exception as e:
logging.error(f"[NHL] Error rendering upcoming home logo {game_details['home_logo_path']}: {e}") logging.error(f"[NHL] Error rendering upcoming home logo {game_details['home_logo_path']}: {e}")
draw.text((home_logo_x, 5), game_details.get("home_abbr", "?"), font=font_team, fill="white") draw.text((home_logo_x, 5), game_details.get("home_abbr", "?"), font=font_team, fill="white")
@@ -1092,7 +1043,7 @@ class NHLScoreboardManager:
draw.text((center_x, vs_y), vs_str, font=font_vs, fill='white', anchor="mt") draw.text((center_x, vs_y), vs_str, font=font_vs, fill='white', anchor="mt")
def _draw_scorebug_layout(self, draw, game_details): def _draw_scorebug_layout(self, draw: ImageDraw.ImageDraw, game_details: Dict[str, Any]):
"""Draws the standard score bug layout for live or final games.""" """Draws the standard score bug layout for live or final games."""
font_score = self.fonts.get('score', ImageFont.load_default()) font_score = self.fonts.get('score', ImageFont.load_default())
font_time = self.fonts.get('time', ImageFont.load_default()) font_time = self.fonts.get('time', ImageFont.load_default())
@@ -1121,7 +1072,7 @@ class NHLScoreboardManager:
try: try:
away_logo = Image.open(game_details["away_logo_path"]).convert("RGBA") away_logo = Image.open(game_details["away_logo_path"]).convert("RGBA")
away_logo.thumbnail(logo_size, Image.Resampling.LANCZOS) away_logo.thumbnail(logo_size, Image.Resampling.LANCZOS)
img.paste(away_logo, (away_logo_x, (self.display_height - away_logo.height) // 2), away_logo) img.paste(away_logo, (away_logo_x, (self.display_height - away_logo.height) // 2), mask=away_logo)
away_logo_drawn_size = away_logo.size away_logo_drawn_size = away_logo.size
except Exception as e: except Exception as e:
logging.error(f"[NHL] Error rendering away logo {game_details['away_logo_path']}: {e}") logging.error(f"[NHL] Error rendering away logo {game_details['away_logo_path']}: {e}")
@@ -1138,7 +1089,7 @@ class NHLScoreboardManager:
try: try:
home_logo = Image.open(game_details["home_logo_path"]).convert("RGBA") home_logo = Image.open(game_details["home_logo_path"]).convert("RGBA")
home_logo.thumbnail(logo_size, Image.Resampling.LANCZOS) home_logo.thumbnail(logo_size, Image.Resampling.LANCZOS)
img.paste(home_logo, (home_logo_x, (self.display_height - home_logo.height) // 2), home_logo) img.paste(home_logo, (home_logo_x, (self.display_height - home_logo.height) // 2), mask=home_logo)
home_logo_drawn_size = home_logo.size home_logo_drawn_size = home_logo.size
except Exception as e: except Exception as e:
logging.error(f"[NHL] Error rendering home logo {game_details['home_logo_path']}: {e}") logging.error(f"[NHL] Error rendering home logo {game_details['home_logo_path']}: {e}")