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,202 +719,211 @@ 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
details = self._extract_game_details(event) if is_favorite:
if not details: continue # Skip if parsing failed details = self._extract_game_details(event)
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) continue
continue
if is_recent_final and details["is_final"] and details["start_time_utc"]: if is_recent_final and details["is_final"] and details["start_time_utc"]:
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
today_local = datetime.now(self.local_timezone) all_events: List[Dict] = []
dates_to_fetch = {
(today_local - timedelta(days=2)).strftime('%Y%m%d'), # Two days ago (for 48h lookback)
(today_local - timedelta(days=1)).strftime('%Y%m%d'), # Yesterday
today_local.strftime('%Y%m%d'), # Today
(today_local + timedelta(days=1)).strftime('%Y%m%d') # Tomorrow (for upcoming today)
}
# 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: if now - self.last_data_fetch_time > self.update_interval:
all_events = self._fetch_data_for_dates(sorted(list(dates_to_fetch))) 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.")
# Decide how to handle fetch failure - clear existing or keep stale?
# Let's clear for now if fetch fails entirely
if self.relevant_events: # If we previously had events
self.relevant_events = []
self.current_event_index = 0
self.current_display_details = None
self.needs_redraw = True
return # Stop update if fetch failed
else: else:
# Use potentially stale data if logic check is frequent but data fetch isn't needed yet # Data not stale enough for API call, but logic check proceeds.
# Or should we force fetch here? Let's assume stale data is ok for now if fetch interval not met. # How do we get all_events? Need to cache it?
# This part needs refinement - how to handle stale data vs frequent logic checks? # --> Problem: Can't re-evaluate criteria without fetching.
# For simplicity now, let's assume we fetch every logic check for testing. # --> Solution: Always fetch data when logic runs, but use interval for logic run itself.
all_events = self._fetch_data_for_dates(sorted(list(dates_to_fetch))) # --> 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
return
if not all_events: # --- Determine Combined List of Relevant Events ---
logging.warning("[NHL] No events found after fetching. Setting mode to error/none.") live_events = self._find_events_by_criteria(all_events, is_live=True)
if self.display_mode != 'error': upcoming_events = self._find_events_by_criteria(all_events, is_upcoming_today=True)
self.display_mode = 'error' # Or 'none'? recent_final_events = self._find_events_by_criteria(all_events, is_recent_final=True)
self.relevant_events = []
self.current_event_index = 0 new_relevant_events_combined = []
added_ids = set()
# Add in order of priority, avoiding duplicates
for event in live_events + upcoming_events + recent_final_events:
event_id = event.get("id")
if event_id and event_id not in added_ids:
new_relevant_events_combined.append(event)
added_ids.add(event_id)
# --- TODO: Implement Fallback Logic if show_only_favorites is False ---
if not new_relevant_events_combined and not self.show_only_favorites:
logging.debug("[NHL] No relevant favorite games, show_only_favorites=false. Fallback needed.")
# Add logic here to find non-favorite games based on priority if desired
pass # No fallback implemented yet
# --- 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}
if old_event_ids != new_event_ids:
logging.info(f"[NHL] Relevant events changed. New count: {len(new_relevant_events_combined)}")
self.relevant_events = new_relevant_events_combined
self.current_event_index = 0
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.needs_redraw = True
elif self.relevant_events: # List content is same, check if details of *current* item changed
current_event_in_list = self.relevant_events[self.current_event_index]
new_details_for_current = self._extract_game_details(current_event_in_list)
# Compare specifically relevant fields (score, clock, period, status)
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.needs_redraw = True self.needs_redraw = True
return else:
logging.debug("[NHL] No significant change in details for current event.")
# --- Determine State and Relevant Events --- # else: No relevant events before or after
new_mode = 'none'
new_relevant_events = []
# 1. Check for Live Favorite Games
live_favorite_games = self._find_events_by_criteria(all_events, is_live=True)
if live_favorite_games:
new_mode = 'live'
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)
if new_mode == 'none':
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)
if new_mode == 'none':
recent_finals = self._find_events_by_criteria(all_events, is_recent_final=True)
if recent_finals:
new_mode = 'recent_final'
new_relevant_events = [recent_finals[0]] # Show only the most recent one
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?
# For now, if show_only_favorites is true, and none of above, mode remains 'none'.
# If show_only_favorites is false, need to implement fallback search here if desired.
if new_mode == 'none' and not self.show_only_favorites:
logging.debug("[NHL] No relevant favorite games, show_only_favorites=false, applying fallback.")
# 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
# --- Update State if Changed --- def _details_changed_significantly(self, old_details, new_details) -> bool:
if new_mode != self.display_mode or new_relevant_events != self.relevant_events: """Compare specific fields to see if a redraw is needed."""
logging.info(f"[NHL] Mode change: {self.display_mode} -> {new_mode}") if old_details is None and new_details is None: return False
logging.debug(f"[NHL] Relevant Events Change: {len(self.relevant_events)} -> {len(new_relevant_events)}") if old_details is None or new_details is None: return True # Changed from something to nothing or vice-versa
self.display_mode = new_mode
self.relevant_events = new_relevant_events fields_to_check = ['home_score', 'away_score', 'clock', 'period', 'is_live', 'is_final', 'is_upcoming']
self.current_event_index = 0 # Reset index on mode/list change for field in fields_to_check:
self.last_cycle_time = time.time() # Reset cycle timer if old_details.get(field) != new_details.get(field):
self.current_display_details = self._extract_game_details(self.relevant_events[0] if self.relevant_events else None) return True
self.needs_redraw = True return False
elif self.display_mode == 'live':
# If mode is still live, check if *details* of current game changed
current_event_in_list = self.relevant_events[self.current_event_index]
new_details_for_current = self._extract_game_details(current_event_in_list)
if new_details_for_current != self.current_display_details:
logging.debug("[NHL] Live game details updated.")
self.current_display_details = new_details_for_current
self.needs_redraw = True
# else: No change in mode, events, or live details
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 # Force redraw if details were missing
elif len(self.relevant_events) == 1:
# If only one event, make sure its details are loaded
if self.current_display_details is None:
self.current_display_details = self._extract_game_details(self.relevant_events[0])
redraw_this_frame = True
else: # No relevant events
if self.current_display_details is not None: # Clear details if list is empty now
self.current_display_details = None
redraw_this_frame = True redraw_this_frame = True
elif self.display_mode != 'live':
# Ensure details are loaded if mode is not live (handles initial state or mode change)
if self.current_display_details is None and self.relevant_events:
self.current_display_details = self._extract_game_details(self.relevant_events[0])
redraw_this_frame = True
elif not self.relevant_events: # No relevant events found
self.current_display_details = None # Ensure details are cleared
# --- 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_placeholder_layout(draw)
self._draw_scorebug_layout(draw, game_details) elif game_details.get("is_upcoming"):
elif self.display_mode == 'upcoming' and game_details: self._draw_upcoming_layout(draw, game_details)
self._draw_upcoming_layout(draw, game_details) elif game_details.get("is_live") or game_details.get("is_final"):
elif self.display_mode == 'recent_final' and game_details: self._draw_scorebug_layout(draw, game_details)
self._draw_scorebug_layout(draw, game_details) # Use scorebug for final else: # Fallback/Other states
else: # 'none' or 'error' self._draw_placeholder_layout(draw, msg=game_details.get("status_text", "NHL Status")) # Show status text
self._draw_placeholder_layout(draw)
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}")