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
import logging
from datetime import datetime, timedelta, timezone
from typing import List, Dict, Optional
try:
from zoneinfo import ZoneInfo, ZoneInfoNotFoundError
except ImportError:
@@ -325,7 +326,7 @@ def create_scorebug_image(game_details):
try:
away_logo = Image.open(game_details["away_logo_path"]).convert("RGBA")
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:
logging.error(f"Error loading/pasting away logo {game_details['away_logo_path']}: {e}")
# Draw placeholder text if logo fails
@@ -341,7 +342,7 @@ def create_scorebug_image(game_details):
try:
home_logo = Image.open(game_details["home_logo_path"]).convert("RGBA")
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:
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")
@@ -579,14 +580,13 @@ class NHLScoreboardManager:
self.local_timezone = ZoneInfo(DEFAULT_TIMEZONE)
# State variables
self.last_data_fetch_time = 0 # When API was last called
self.last_logic_update_time = 0 # When selection logic was last run
self.relevant_events = [] # Events matching current display criteria
self.current_event_index = 0 # Index for cycling through relevant_events
self.last_cycle_time = 0 # Timestamp for cycling live games
self.display_mode = 'none' # 'live', 'upcoming', 'recent_final', 'none', 'error'
self.current_display_details = None # Details for the currently shown game in cycle
self.needs_redraw = True # Flag to force redraw in display()
self.last_data_fetch_time = 0
self.last_logic_update_time = 0
self.relevant_events: List[Dict[str, Any]] = [] # ALL relevant events (live, upcoming today, recent final)
self.current_event_index: int = 0
self.last_cycle_time: float = 0
self.current_display_details: Optional[Dict[str, Any]] = None
self.needs_redraw: bool = True
# Get display dimensions
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}")
return None # Return None if fetch fails
def _extract_game_details(self, game_event):
"""Extracts relevant details for the score bug display from raw event data."""
if not game_event:
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):
def _find_events_by_criteria(self, all_events: List[Dict[str, Any]],
is_live: bool = False, is_upcoming_today: bool = False,
is_recent_final: bool = False) -> List[Dict[str, Any]]:
"""Helper to find favorite team events matching specific criteria."""
matches = []
now_utc = datetime.now(timezone.utc)
@@ -773,202 +719,211 @@ class NHLScoreboardManager:
cutoff_time_utc = now_utc - timedelta(hours=RECENT_GAME_HOURS)
for event in all_events:
# Check if it's a favorite team's game first
competitors = event.get("competitions", [{}])[0].get("competitors", [])
if len(competitors) == 2:
team1_abbr = competitors[0].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
# Skip non-favorites ONLY if show_only_favorites is true
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
details = self._extract_game_details(event)
if not details: continue # Skip if parsing failed
# Apply criteria ONLY if the event involves a favorite team
if is_favorite:
details = self._extract_game_details(event)
if not details: continue
if is_live and details["is_live"]:
matches.append(event)
continue # Found live, no need for further checks for this event
if is_live and details["is_live"]:
matches.append(event)
continue
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()
if start_local_date == today_local and details["start_time_utc"] > now_utc:
matches.append(event)
continue
if is_upcoming_today and details["is_upcoming"] and details["start_time_utc"]:
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:
matches.append(event)
continue
if is_recent_final and details["is_final"] and details["start_time_utc"]:
if details["start_time_utc"] > cutoff_time_utc:
matches.append(event)
continue
# Add fallback logic if not showing only favorites? Or handle in update()?
# For now, this focuses on finding FAVORITE events based on criteria.
if is_recent_final and details["is_final"] and details["start_time_utc"]:
if details["start_time_utc"] > cutoff_time_utc:
matches.append(event)
continue
# --- NOTE: No fallback logic here yet for non-favorites if show_only_favorites is false ---
# Sort results appropriately
if is_live:
# Maybe sort live games by start time or some other metric?
pass # Keep order for now
pass # Keep API order for now
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:
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
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 self.display_mode != 'none':
self.display_mode = 'none'
if self.relevant_events: # Clear if disabled
self.relevant_events = []
self.current_event_index = 0
self.needs_redraw = True
return
now = time.time()
# Determine required data fetch interval
# Always use idle interval check; fetch logic inside handles actual API call timing
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
# Use active interval for logic checks if we *might* be showing something, else idle
check_interval = self.update_interval if self.relevant_events else self.idle_update_interval
if now - self.last_logic_update_time < check_interval:
# Ensure live game cycling still happens even if logic doesn't run
if self.display_mode == 'live' and len(self.relevant_events) > 1:
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
# Live game cycling still needs to happen within display() based on its own timer
return
logging.info(f"[NHL] Running update logic (Check Interval: {check_interval}s)")
self.last_logic_update_time = now
# Decide which dates to fetch based on current time and lookback/lookahead needs
today_local = datetime.now(self.local_timezone)
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)
# Fetch data if interval passed
all_events: List[Dict] = []
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:
# 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)))
# 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
return
if not all_events:
logging.warning("[NHL] No events found after fetching. Setting mode to error/none.")
if self.display_mode != 'error':
self.display_mode = 'error' # Or 'none'?
self.relevant_events = []
self.current_event_index = 0
# --- Determine Combined List of Relevant Events ---
live_events = self._find_events_by_criteria(all_events, is_live=True)
upcoming_events = self._find_events_by_criteria(all_events, is_upcoming_today=True)
recent_final_events = self._find_events_by_criteria(all_events, is_recent_final=True)
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
return
# --- Determine State and Relevant Events ---
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
else:
logging.debug("[NHL] No significant change in details for current event.")
# else: No relevant events before or after
# --- Update State if Changed ---
if new_mode != self.display_mode or new_relevant_events != self.relevant_events:
logging.info(f"[NHL] Mode change: {self.display_mode} -> {new_mode}")
logging.debug(f"[NHL] Relevant Events Change: {len(self.relevant_events)} -> {len(new_relevant_events)}")
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.current_display_details = self._extract_game_details(self.relevant_events[0] if self.relevant_events else None)
self.needs_redraw = True
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 _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):
"""Generates and displays the current frame based on the determined state."""
"""Generates and displays the current frame, handling cycling."""
if not self.is_enabled:
return
now = time.time()
redraw_this_frame = force_clear or self.needs_redraw
# --- Handle Live Game Cycling ---
if self.display_mode == 'live' and len(self.relevant_events) > 1:
# --- Handle Cycling ---
if len(self.relevant_events) > 1: # Cycle if more than one relevant event exists
if now - self.last_cycle_time > self.cycle_duration:
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.last_cycle_time = now
redraw_this_frame = True # Force redraw on cycle
logging.debug(f"[NHL] Cycling live game to index {self.current_event_index}")
elif self.current_display_details is None: # Ensure details are loaded initially
self.current_display_details = self._extract_game_details(self.relevant_events[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 loaded for index 0 initially
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
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 ---
if not redraw_this_frame:
# logging.debug("[NHL] display() called but no redraw needed.")
return
logging.debug(f"[NHL] Generating frame for mode: {self.display_mode} (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)
logging.debug(f"[NHL] Generating frame (Index: {self.current_event_index})")
frame = self._create_frame(self.current_display_details) # Pass the specific details
try:
if hasattr(self.display_manager, 'display_image'):
@@ -978,48 +933,44 @@ class NHLScoreboardManager:
else:
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:
logging.error(f"[NHL] Error displaying frame via DisplayManager: {e}")
def _create_frame(self, game_details):
"""Creates a Pillow image frame based on game details and current display mode."""
# Determine layout based on self.display_mode and game_details
# Note: game_details will be None if mode is 'none' or 'error'
def _create_frame(self, game_details: Optional[Dict[str, Any]]) -> Image.Image:
"""Creates a Pillow image frame based on game details."""
# This method now simply renders the layout based on the details passed in
img = Image.new('RGB', (self.display_width, self.display_height), color='black')
draw = ImageDraw.Draw(img)
# --- Choose Layout ---
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)
if not game_details:
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
def _draw_placeholder_layout(self, draw):
"""Draws the 'No NHL Games' message."""
def _draw_placeholder_layout(self, draw: ImageDraw.ImageDraw, msg: str = "No NHL Games"):
"""Draws the 'No NHL Games' or other placeholder message."""
font = self.fonts.get('placeholder', ImageFont.load_default())
msg = "No NHL Games"
bbox = draw.textbbox((0,0), msg, font=font)
text_width = bbox[2] - bbox[0]
text_height = bbox[3] - bbox[1]
draw.text(((self.display_width - text_width) // 2, (self.display_height - text_height) // 2),
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."""
font_team = self.fonts.get('team', ImageFont.load_default())
font_main = self.fonts.get('upcoming_main', 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.")
@@ -1037,7 +988,7 @@ class NHLScoreboardManager:
try:
away_logo = Image.open(game_details["away_logo_path"]).convert("RGBA")
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:
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")
@@ -1049,7 +1000,7 @@ class NHLScoreboardManager:
try:
home_logo = Image.open(game_details["home_logo_path"]).convert("RGBA")
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:
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")
@@ -1092,7 +1043,7 @@ class NHLScoreboardManager:
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."""
font_score = self.fonts.get('score', ImageFont.load_default())
font_time = self.fonts.get('time', ImageFont.load_default())
@@ -1121,7 +1072,7 @@ class NHLScoreboardManager:
try:
away_logo = Image.open(game_details["away_logo_path"]).convert("RGBA")
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
except Exception as e:
logging.error(f"[NHL] Error rendering away logo {game_details['away_logo_path']}: {e}")
@@ -1138,7 +1089,7 @@ class NHLScoreboardManager:
try:
home_logo = Image.open(game_details["home_logo_path"]).convert("RGBA")
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
except Exception as e:
logging.error(f"[NHL] Error rendering home logo {game_details['home_logo_path']}: {e}")