Create Basketball Base Class and Consolidate Basketball Managers (#97)

* Create basketball Base class

* Migrate NBA

* Add NCAA Mens Basketball

* Add NCAA Women's Basketball

* Add WNBA

---------

Co-authored-by: Alex Resnick <adr8282@gmail.com>
This commit is contained in:
Alex Resnick
2025-10-05 14:22:00 -05:00
committed by GitHub
parent 3406234e00
commit fb38a5a814
13 changed files with 1416 additions and 1993 deletions

View File

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

View File

@@ -419,6 +419,7 @@ class SportsCore(ABC):
if not home_team or not away_team:
self.logger.warning(f"Could not find home or away team in event: {game_event.get('id')}")
return None, None, None, None, None
try:
home_abbr = home_team["team"]["abbreviation"]
except KeyError:

View File

@@ -23,6 +23,7 @@ from src.odds_ticker_manager import OddsTickerManager
from src.leaderboard_manager import LeaderboardManager
from src.nhl_managers import NHLLiveManager, NHLRecentManager, NHLUpcomingManager
from src.nba_managers import NBALiveManager, NBARecentManager, NBAUpcomingManager
from src.wnba_managers import WNBALiveManager, WNBARecentManager, WNBAUpcomingManager
from src.mlb_manager import MLBLiveManager, MLBRecentManager, MLBUpcomingManager
from src.milb_manager import MiLBLiveManager, MiLBRecentManager, MiLBUpcomingManager
from src.soccer_managers import SoccerLiveManager, SoccerRecentManager, SoccerUpcomingManager
@@ -30,6 +31,7 @@ from src.nfl_managers import NFLLiveManager, NFLRecentManager, NFLUpcomingManage
from src.ncaa_fb_managers import NCAAFBLiveManager, NCAAFBRecentManager, NCAAFBUpcomingManager
from src.ncaa_baseball_managers import NCAABaseballLiveManager, NCAABaseballRecentManager, NCAABaseballUpcomingManager
from src.ncaam_basketball_managers import NCAAMBasketballLiveManager, NCAAMBasketballRecentManager, NCAAMBasketballUpcomingManager
from src.ncaaw_basketball_managers import NCAAWBasketballLiveManager, NCAAWBasketballRecentManager, NCAAWBasketballUpcomingManager
from src.ncaam_hockey_managers import NCAAMHockeyLiveManager, NCAAMHockeyRecentManager, NCAAMHockeyUpcomingManager
from src.ncaaw_hockey_managers import NCAAWHockeyLiveManager, NCAAWHockeyRecentManager, NCAAWHockeyUpcomingManager
from src.youtube_display import YouTubeDisplay
@@ -134,6 +136,21 @@ class DisplayController:
self.nba_recent = None
self.nba_upcoming = None
logger.info("NBA managers initialized in %.3f seconds", time.time() - nba_time)
# Initialize WNBA managers if enabled
wnba_time = time.time()
wnba_enabled = self.config.get('wnba_scoreboard', {}).get('enabled', False)
wnba_display_modes = self.config.get('wnba_scoreboard', {}).get('display_modes', {})
if wnba_enabled:
self.wnba_live = WNBALiveManager(self.config, self.display_manager, self.cache_manager) if wnba_display_modes.get('wnba_live', True) else None
self.wnba_recent = WNBARecentManager(self.config, self.display_manager, self.cache_manager) if wnba_display_modes.get('wnba_recent', True) else None
self.wnba_upcoming = WNBAUpcomingManager(self.config, self.display_manager, self.cache_manager) if wnba_display_modes.get('wnba_upcoming', True) else None
else:
self.wnba_live = None
self.wnba_recent = None
self.wnba_upcoming = None
logger.info("WNBA managers initialized in %.3f seconds", time.time() - wnba_time)
# Initialize MLB managers if enabled
mlb_time = time.time()
@@ -242,6 +259,21 @@ class DisplayController:
self.ncaam_basketball_upcoming = None
logger.info("NCAA Men's Basketball managers initialized in %.3f seconds", time.time() - ncaam_basketball_time)
# Initialize NCAA Womens's Basketball managers if enabled
ncaaw_basketball_time = time.time()
ncaaw_basketball_enabled = self.config.get('ncaaw_basketball_scoreboard', {}).get('enabled', False)
ncaaw_basketball_display_modes = self.config.get('ncaaw_basketball_scoreboard', {}).get('display_modes', {})
if ncaaw_basketball_enabled:
self.ncaaw_basketball_live = NCAAWBasketballLiveManager(self.config, self.display_manager, self.cache_manager) if ncaaw_basketball_display_modes.get('ncaaw_basketball_live', True) else None
self.ncaaw_basketball_recent = NCAAWBasketballRecentManager(self.config, self.display_manager, self.cache_manager) if ncaaw_basketball_display_modes.get('ncaaw_basketball_recent', True) else None
self.ncaaw_basketball_upcoming = NCAAWBasketballUpcomingManager(self.config, self.display_manager, self.cache_manager) if ncaaw_basketball_display_modes.get('ncaaw_basketball_upcoming', True) else None
else:
self.ncaaw_basketball_live = None
self.ncaaw_basketball_recent = None
self.ncaaw_basketball_upcoming = None
logger.info("NCAA Womens's Basketball managers initialized in %.3f seconds", time.time() - ncaaw_basketball_time)
# Initialize NCAA Men's Hockey managers if enabled
ncaam_hockey_time = time.time()
ncaam_hockey_enabled = self.config.get('ncaam_hockey_scoreboard', {}).get('enabled', False)
@@ -281,6 +313,7 @@ class DisplayController:
# Read live_priority flags for all sports
self.nhl_live_priority = self.config.get('nhl_scoreboard', {}).get('live_priority', True)
self.nba_live_priority = self.config.get('nba_scoreboard', {}).get('live_priority', True)
self.wnba_live_priority = self.config.get('wnba_scoreboard', {}).get('live_priority', True)
self.mlb_live_priority = self.config.get('mlb_scoreboard', {}).get('live_priority', True)
self.milb_live_priority = self.config.get('milb_scoreboard', {}).get('live_priority', True)
self.soccer_live_priority = self.config.get('soccer_scoreboard', {}).get('live_priority', True)
@@ -288,6 +321,7 @@ class DisplayController:
self.ncaa_fb_live_priority = self.config.get('ncaa_fb_scoreboard', {}).get('live_priority', True)
self.ncaa_baseball_live_priority = self.config.get('ncaa_baseball_scoreboard', {}).get('live_priority', True)
self.ncaam_basketball_live_priority = self.config.get('ncaam_basketball_scoreboard', {}).get('live_priority', True)
self.ncaaw_basketball_live_priority = self.config.get('ncaaw_basketball_scoreboard', {}).get('live_priority', True)
self.ncaam_hockey_live_priority = self.config.get('ncaam_hockey_scoreboard', {}).get('live_priority', True)
self.ncaaw_hockey_live_priority = self.config.get('ncaaw_hockey_scoreboard', {}).get('live_priority', True)
@@ -315,6 +349,9 @@ class DisplayController:
if nba_enabled:
if self.nba_recent: self.available_modes.append('nba_recent')
if self.nba_upcoming: self.available_modes.append('nba_upcoming')
if wnba_enabled:
if self.wnba_recent: self.available_modes.append('wnba_recent')
if self.wnba_upcoming: self.available_modes.append('wnba_upcoming')
if mlb_enabled:
if self.mlb_recent: self.available_modes.append('mlb_recent')
if self.mlb_upcoming: self.available_modes.append('mlb_upcoming')
@@ -336,6 +373,9 @@ class DisplayController:
if ncaam_basketball_enabled:
if self.ncaam_basketball_recent: self.available_modes.append('ncaam_basketball_recent')
if self.ncaam_basketball_upcoming: self.available_modes.append('ncaam_basketball_upcoming')
if ncaaw_basketball_enabled:
if self.ncaaw_basketball_recent: self.available_modes.append('ncaaw_basketball_recent')
if self.ncaaw_basketball_upcoming: self.available_modes.append('ncaaw_basketball_upcoming')
if ncaam_hockey_enabled:
if self.ncaam_hockey_recent: self.available_modes.append('ncaam_hockey_recent')
if self.ncaam_hockey_upcoming: self.available_modes.append('ncaam_hockey_upcoming')
@@ -366,6 +406,11 @@ class DisplayController:
self.nba_favorite_teams = self.config.get('nba_scoreboard', {}).get('favorite_teams', [])
self.in_nba_rotation = False
self.wnba_current_team_index = 0
self.wnba_showing_recent = True
self.wnba_favorite_teams = self.config.get('wnba_scoreboard', {}).get('favorite_teams', [])
self.in_wnba_rotation = False
self.soccer_current_team_index = 0 # Soccer rotation state
self.soccer_showing_recent = True
self.soccer_favorite_teams = self.config.get('soccer_scoreboard', {}).get('favorite_teams', [])
@@ -394,6 +439,12 @@ class DisplayController:
self.ncaam_basketball_showing_recent = True
self.ncaam_basketball_favorite_teams = self.config.get('ncaam_basketball_scoreboard', {}).get('favorite_teams', [])
self.in_ncaam_basketball_rotation = False
# Add NCAA Womens's Basketball rotation state
self.ncaaw_basketball_current_team_index = 0
self.ncaaw_basketball_showing_recent = True
self.ncaaw_basketball_favorite_teams = self.config.get('ncaaw_basketball_scoreboard', {}).get('favorite_teams', [])
self.in_ncaaw_basketball_rotation = False
# Update display durations to include all modes
self.display_durations = self.config['display'].get('display_durations', {})
@@ -423,6 +474,9 @@ class DisplayController:
'nba_live': 30,
'nba_recent': 20,
'nba_upcoming': 20,
'wnba_live': 30,
'wnba_recent': 20,
'wnba_upcoming': 20,
'mlb_live': 30,
'mlb_recent': 20,
'mlb_upcoming': 20,
@@ -445,6 +499,9 @@ class DisplayController:
'ncaam_basketball_live': 30, # Added NCAA Men's Basketball durations
'ncaam_basketball_recent': 15,
'ncaam_basketball_upcoming': 15,
'ncaaw_basketball_live': 30, # Added NCAA Womens's Basketball durations
'ncaaw_basketball_recent': 15,
'ncaaw_basketball_upcoming': 15,
'ncaam_hockey_live': 30, # Added NCAA Men's Hockey durations
'ncaam_hockey_recent': 15,
'ncaam_hockey_upcoming': 15,
@@ -462,6 +519,8 @@ class DisplayController:
logger.info(f"NHL Favorite teams: {self.nhl_favorite_teams}")
if nba_enabled:
logger.info(f"NBA Favorite teams: {self.nba_favorite_teams}")
if wnba_enabled:
logger.info(f"WNBA Favorite teams: {self.wnba_favorite_teams}")
if mlb_enabled:
logger.info(f"MLB Favorite teams: {self.mlb_favorite_teams}")
if milb_enabled:
@@ -476,6 +535,8 @@ class DisplayController:
logger.info(f"NCAA Baseball Favorite teams: {self.ncaa_baseball_favorite_teams}")
if ncaam_basketball_enabled: # Check if NCAA Men's Basketball is enabled
logger.info(f"NCAA Men's Basketball Favorite teams: {self.ncaam_basketball_favorite_teams}")
if ncaaw_basketball_enabled: # Check if NCAA Womens's Basketball is enabled
logger.info(f"NCAA Womens's Basketball Favorite teams: {self.ncaaw_basketball_favorite_teams}")
logger.info(f"Available display modes: {self.available_modes}")
logger.info(f"Initial display mode: {self.current_display_mode}")
@@ -674,6 +735,10 @@ class DisplayController:
if self.nba_live: self.nba_live.update()
if self.nba_recent: self.nba_recent.update()
if self.nba_upcoming: self.nba_upcoming.update()
elif current_sport == 'wnba':
if self.wnba_live: self.wnba_live.update()
if self.wnba_recent: self.wnba_recent.update()
if self.wnba_upcoming: self.wnba_upcoming.update()
elif current_sport == 'mlb':
if self.mlb_live: self.mlb_live.update()
if self.mlb_recent: self.mlb_recent.update()
@@ -702,6 +767,10 @@ class DisplayController:
if self.ncaam_basketball_live: self.ncaam_basketball_live.update()
if self.ncaam_basketball_recent: self.ncaam_basketball_recent.update()
if self.ncaam_basketball_upcoming: self.ncaam_basketball_upcoming.update()
elif current_sport == 'ncaaw_basketball':
if self.ncaaw_basketball_live: self.ncaaw_basketball_live.update()
if self.ncaaw_basketball_recent: self.ncaaw_basketball_recent.update()
if self.ncaaw_basketball_upcoming: self.ncaaw_basketball_upcoming.update()
elif current_sport == 'ncaam_hockey':
if self.ncaam_hockey_live: self.ncaam_hockey_live.update()
if self.ncaam_hockey_recent: self.ncaam_hockey_recent.update()
@@ -721,6 +790,10 @@ class DisplayController:
if self.nba_recent: self.nba_recent.update()
if self.nba_upcoming: self.nba_upcoming.update()
if self.wnba_live: self.wnba_live.update()
if self.wnba_recent: self.wnba_recent.update()
if self.wnba_upcoming: self.wnba_upcoming.update()
if self.mlb_live: self.mlb_live.update()
if self.mlb_recent: self.mlb_recent.update()
if self.mlb_upcoming: self.mlb_upcoming.update()
@@ -749,6 +822,10 @@ class DisplayController:
if self.ncaam_basketball_recent: self.ncaam_basketball_recent.update()
if self.ncaam_basketball_upcoming: self.ncaam_basketball_upcoming.update()
if self.ncaaw_basketball_live: self.ncaaw_basketball_live.update()
if self.ncaaw_basketball_recent: self.ncaaw_basketball_recent.update()
if self.ncaaw_basketball_upcoming: self.ncaaw_basketball_upcoming.update()
if self.ncaam_hockey_live: self.ncaam_hockey_live.update()
if self.ncaam_hockey_recent: self.ncaam_hockey_recent.update()
if self.ncaam_hockey_upcoming: self.ncaam_hockey_upcoming.update()
@@ -770,6 +847,8 @@ class DisplayController:
live_checks['nhl'] = self.nhl_live and self.nhl_live.live_games
if 'nba_scoreboard' in self.config and self.config['nba_scoreboard'].get('enabled', False):
live_checks['nba'] = self.nba_live and self.nba_live.live_games
if 'wnba_scoreboard' in self.config and self.config['wnba_scoreboard'].get('enabled', False):
live_checks['wnba'] = self.wnba_live and self.wnba_live.live_games
if 'mlb' in self.config and self.config['mlb'].get('enabled', False):
live_checks['mlb'] = self.mlb_live and self.mlb_live.live_games
if 'milb' in self.config and self.config['milb'].get('enabled', False):
@@ -784,6 +863,8 @@ class DisplayController:
live_checks['ncaa_baseball'] = self.ncaa_baseball_live and self.ncaa_baseball_live.live_games
if 'ncaam_basketball_scoreboard' in self.config and self.config['ncaam_basketball_scoreboard'].get('enabled', False):
live_checks['ncaam_basketball'] = self.ncaam_basketball_live and self.ncaam_basketball_live.live_games
if 'ncaaw_basketball_scoreboard' in self.config and self.config['ncaaw_basketball_scoreboard'].get('enabled', False):
live_checks['ncaaw_basketball'] = self.ncaaw_basketball_live and self.ncaaw_basketball_live.live_games
if 'ncaam_hockey_scoreboard' in self.config and self.config['ncaam_hockey_scoreboard'].get('enabled', False):
live_checks['ncaam_hockey'] = self.ncaam_hockey_live and self.ncaam_hockey_live.live_games
if 'ncaaw_hockey_scoreboard' in self.config and self.config['ncaaw_hockey_scoreboard'].get('enabled', False):
@@ -818,6 +899,9 @@ class DisplayController:
elif sport == 'nba':
manager_recent = self.nba_recent
manager_upcoming = self.nba_upcoming
elif sport == 'wnba':
manager_recent = self.wnba_recent
manager_upcoming = self.wnba_upcoming
elif sport == 'mlb':
manager_recent = self.mlb_recent
manager_upcoming = self.mlb_upcoming
@@ -872,6 +956,10 @@ class DisplayController:
favorite_teams = self.nba_favorite_teams
manager_recent = self.nba_recent
manager_upcoming = self.nba_upcoming
elif sport == 'wnba':
favorite_teams = self.wnba_favorite_teams
manager_recent = self.wnba_recent
manager_upcoming = self.wnba_upcoming
elif sport == 'mlb':
favorite_teams = self.mlb_favorite_teams
manager_recent = self.mlb_recent
@@ -895,73 +983,6 @@ class DisplayController:
return bool(favorite_teams and (manager_recent or manager_upcoming))
def _rotate_team_games(self, sport: str = 'nhl') -> None:
"""Rotate through games for favorite teams. (No longer used directly in loop)"""
# This logic is now mostly handled within each manager's display/update
# Keeping the structure in case direct rotation is needed later.
if not self._has_team_games(sport):
return
if sport == 'nhl':
if not self.nhl_favorite_teams: return
current_team = self.nhl_favorite_teams[self.nhl_current_team_index]
# ... (rest of NHL rotation logic - now less relevant)
elif sport == 'nba':
if not self.nba_favorite_teams: return
current_team = self.nba_favorite_teams[self.nba_current_team_index]
# ... (rest of NBA rotation logic)
elif sport == 'mlb':
if not self.mlb_favorite_teams: return
current_team = self.mlb_favorite_teams[self.mlb_current_team_index]
# ... (rest of MLB rotation logic)
elif sport == 'milb':
if not self.config.get('milb_scoreboard', {}).get('favorite_teams', []): return
current_team = self.config['milb_scoreboard']['favorite_teams'][self.milb_current_team_index]
# ... (rest of MiLB rotation logic)
elif sport == 'soccer':
if not self.soccer_favorite_teams: return
current_team = self.soccer_favorite_teams[self.soccer_current_team_index]
# Try to find games for current team (recent first)
found_games = self._get_team_games(current_team, 'soccer', self.soccer_showing_recent)
if not found_games:
# Try opposite type (upcoming/recent)
self.soccer_showing_recent = not self.soccer_showing_recent
found_games = self._get_team_games(current_team, 'soccer', self.soccer_showing_recent)
if not found_games:
# Move to next team if no games found for current one
self.soccer_current_team_index = (self.soccer_current_team_index + 1) % len(self.soccer_favorite_teams)
self.soccer_showing_recent = True # Reset to recent for the new team
# Maybe try finding game for the *new* team immediately? Optional.
elif sport == 'nfl':
if not self.nfl_favorite_teams: return
current_team = self.nfl_favorite_teams[self.nfl_current_team_index]
# Try to find games for current team (recent first)
found_games = self._get_team_games(current_team, 'nfl', self.nfl_showing_recent)
if not found_games:
# Try opposite type (upcoming/recent)
self.nfl_showing_recent = not self.nfl_showing_recent
found_games = self._get_team_games(current_team, 'nfl', self.nfl_showing_recent)
if not found_games:
# Move to next team if no games found for current one
self.nfl_current_team_index = (self.nfl_current_team_index + 1) % len(self.nfl_favorite_teams)
self.nfl_showing_recent = True # Reset to recent for the new team
elif sport == 'ncaa_fb': # Add NCAA FB case
if not self.ncaa_fb_favorite_teams: return
current_team = self.ncaa_fb_favorite_teams[self.ncaa_fb_current_team_index]
# Try to find games for current team (recent first)
found_games = self._get_team_games(current_team, 'ncaa_fb', self.ncaa_fb_showing_recent)
if not found_games:
# Try opposite type (upcoming/recent)
self.ncaa_fb_showing_recent = not self.ncaa_fb_showing_recent
found_games = self._get_team_games(current_team, 'ncaa_fb', self.ncaa_fb_showing_recent)
if not found_games:
# Move to next team if no games found for current one
self.ncaa_fb_current_team_index = (self.ncaa_fb_current_team_index + 1) % len(self.ncaa_fb_favorite_teams)
self.ncaa_fb_showing_recent = True # Reset to recent for the new team
# --- SCHEDULING METHODS ---
def _load_schedule_config(self):
"""Load schedule configuration once at startup."""
@@ -1031,6 +1052,7 @@ class DisplayController:
# Check if each sport is enabled before processing
nhl_enabled = self.config.get('nhl_scoreboard', {}).get('enabled', False)
nba_enabled = self.config.get('nba_scoreboard', {}).get('enabled', False)
wnba_enabled = self.config.get('wnba_scoreboard', {}).get('enabled', False)
mlb_enabled = self.config.get('mlb_scoreboard', {}).get('enabled', False)
milb_enabled = self.config.get('milb_scoreboard', {}).get('enabled', False)
soccer_enabled = self.config.get('soccer_scoreboard', {}).get('enabled', False)
@@ -1038,11 +1060,13 @@ class DisplayController:
ncaa_fb_enabled = self.config.get('ncaa_fb_scoreboard', {}).get('enabled', False)
ncaa_baseball_enabled = self.config.get('ncaa_baseball_scoreboard', {}).get('enabled', False)
ncaam_basketball_enabled = self.config.get('ncaam_basketball_scoreboard', {}).get('enabled', False)
ncaaw_basketball_enabled = self.config.get('ncaaw_basketball_scoreboard', {}).get('enabled', False)
ncaam_hockey_enabled = self.config.get('ncaam_hockey_scoreboard', {}).get('enabled', False)
ncaaw_hockey_enabled = self.config.get('ncaaw_hockey_scoreboard', {}).get('enabled', False)
update_mode('nhl_live', getattr(self, 'nhl_live', None), self.nhl_live_priority, nhl_enabled)
update_mode('nba_live', getattr(self, 'nba_live', None), self.nba_live_priority, nba_enabled)
update_mode('wnba_live', getattr(self, 'wnba_live', None), self.wnba_live_priority, wnba_enabled)
update_mode('mlb_live', getattr(self, 'mlb_live', None), self.mlb_live_priority, mlb_enabled)
update_mode('milb_live', getattr(self, 'milb_live', None), self.milb_live_priority, milb_enabled)
update_mode('soccer_live', getattr(self, 'soccer_live', None), self.soccer_live_priority, soccer_enabled)
@@ -1050,6 +1074,7 @@ class DisplayController:
update_mode('ncaa_fb_live', getattr(self, 'ncaa_fb_live', None), self.ncaa_fb_live_priority, ncaa_fb_enabled)
update_mode('ncaa_baseball_live', getattr(self, 'ncaa_baseball_live', None), self.ncaa_baseball_live_priority, ncaa_baseball_enabled)
update_mode('ncaam_basketball_live', getattr(self, 'ncaam_basketball_live', None), self.ncaam_basketball_live_priority, ncaam_basketball_enabled)
update_mode('ncaaw_basketball_live', getattr(self, 'ncaaw_basketball_live', None), self.ncaaw_basketball_live_priority, ncaaw_basketball_enabled)
update_mode('ncaam_hockey_live', getattr(self, 'ncaam_hockey_live', None), self.ncaam_hockey_live_priority, ncaam_hockey_enabled)
update_mode('ncaaw_hockey_live', getattr(self, 'ncaaw_hockey_live', None), self.ncaaw_hockey_live_priority, ncaaw_hockey_enabled)
@@ -1094,6 +1119,7 @@ class DisplayController:
for sport, attr, priority in [
('nhl', 'nhl_live', self.nhl_live_priority),
('nba', 'nba_live', self.nba_live_priority),
('wnba', 'wnba_live', self.wnba_live_priority),
('mlb', 'mlb_live', self.mlb_live_priority),
('milb', 'milb_live', self.milb_live_priority),
('soccer', 'soccer_live', self.soccer_live_priority),
@@ -1101,6 +1127,7 @@ class DisplayController:
('ncaa_fb', 'ncaa_fb_live', self.ncaa_fb_live_priority),
('ncaa_baseball', 'ncaa_baseball_live', self.ncaa_baseball_live_priority),
('ncaam_basketball', 'ncaam_basketball_live', self.ncaam_basketball_live_priority),
('ncaaw_basketball', 'ncaaw_basketball_live', self.ncaaw_basketball_live_priority),
('ncaam_hockey', 'ncaam_hockey_live', self.ncaam_hockey_live_priority),
('ncaaw_hockey', 'ncaaw_hockey_live', self.ncaaw_hockey_live_priority)
]:
@@ -1264,6 +1291,10 @@ class DisplayController:
manager_to_display = self.nba_recent
elif self.current_display_mode == 'nba_upcoming' and self.nba_upcoming:
manager_to_display = self.nba_upcoming
elif self.current_display_mode == 'wnba_recent' and self.wnba_recent:
manager_to_display = self.wnba_recent
elif self.current_display_mode == 'wnba_upcoming' and self.wnba_upcoming:
manager_to_display = self.wnba_upcoming
elif self.current_display_mode == 'nfl_recent' and self.nfl_recent:
manager_to_display = self.nfl_recent
elif self.current_display_mode == 'nfl_upcoming' and self.nfl_upcoming:
@@ -1280,6 +1311,10 @@ class DisplayController:
manager_to_display = self.ncaam_basketball_recent
elif self.current_display_mode == 'ncaam_basketball_upcoming' and self.ncaam_basketball_upcoming:
manager_to_display = self.ncaam_basketball_upcoming
elif self.current_display_mode == 'ncaaw_basketball_recent' and self.ncaaw_basketball_recent:
manager_to_display = self.ncaaw_basketball_recent
elif self.current_display_mode == 'ncaaw_basketball_upcoming' and self.ncaaw_basketball_upcoming:
manager_to_display = self.ncaaw_basketball_upcoming
elif self.current_display_mode == 'mlb_recent' and self.mlb_recent:
manager_to_display = self.mlb_recent
elif self.current_display_mode == 'mlb_upcoming' and self.mlb_upcoming:
@@ -1298,6 +1333,8 @@ class DisplayController:
manager_to_display = self.nhl_live
elif self.current_display_mode == 'nba_live' and self.nba_live:
manager_to_display = self.nba_live
elif self.current_display_mode == 'wnba_live' and self.wnba_live:
manager_to_display = self.wnba_live
elif self.current_display_mode == 'nfl_live' and self.nfl_live:
manager_to_display = self.nfl_live
elif self.current_display_mode == 'ncaa_fb_live' and self.ncaa_fb_live:
@@ -1306,6 +1343,8 @@ class DisplayController:
manager_to_display = self.ncaa_baseball_live
elif self.current_display_mode == 'ncaam_basketball_live' and self.ncaam_basketball_live:
manager_to_display = self.ncaam_basketball_live
elif self.current_display_mode == 'ncaaw_basketball_live' and self.ncaaw_basketball_live:
manager_to_display = self.ncaaw_basketball_live
elif self.current_display_mode == 'ncaam_hockey_live' and self.ncaam_hockey_live:
manager_to_display = self.ncaam_hockey_live
elif self.current_display_mode == 'ncaam_hockey_recent' and self.ncaam_hockey_recent:
@@ -1383,6 +1422,10 @@ class DisplayController:
self.ncaam_basketball_recent.display(force_clear=self.force_clear)
elif self.current_display_mode == 'ncaam_basketball_upcoming' and self.ncaam_basketball_upcoming:
self.ncaam_basketball_upcoming.display(force_clear=self.force_clear)
elif self.current_display_mode == 'ncaaw_basketball_recent' and self.ncaaw_basketball_recent:
self.ncaaw_basketball_recent.display(force_clear=self.force_clear)
elif self.current_display_mode == 'ncaaw_basketball_upcoming' and self.ncaaw_basketball_upcoming:
self.ncaaw_basketball_upcoming.display(force_clear=self.force_clear)
elif self.current_display_mode == 'ncaa_baseball_recent' and self.ncaa_baseball_recent:
self.ncaa_baseball_recent.display(force_clear=self.force_clear)
elif self.current_display_mode == 'ncaa_baseball_upcoming' and self.ncaa_baseball_upcoming:

View File

@@ -49,6 +49,7 @@ class LogoDownloader:
LOGO_DIRECTORIES = {
'nfl': 'assets/sports/nfl_logos',
'nba': 'assets/sports/nba_logos',
'wnba': 'assets/sports/wnba_logos',
'mlb': 'assets/sports/mlb_logos',
'nhl': 'assets/sports/nhl_logos',
# NCAA sports use same directory

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,274 @@
import logging
from datetime import datetime
from pathlib import Path
from typing import Any, Dict, Optional
import pytz
import requests
from src.base_classes.basketball import Basketball, BasketballLive
from src.base_classes.sports import SportsRecent, SportsUpcoming
from src.cache_manager import CacheManager
from src.display_manager import DisplayManager
# Import the API counter function from web interface
try:
from web_interface_v2 import increment_api_counter
except ImportError:
# Fallback if web interface is not available
def increment_api_counter(kind: str, count: int = 1):
pass
# Constants
ESPN_NCAAWB_SCOREBOARD_URL = "https://site.api.espn.com/apis/site/v2/sports/basketball/womens-college-basketball/scoreboard"
class BaseNCAAWBasketballManager(Basketball):
"""Base class for NCAA WB managers with common functionality."""
# Class variables for warning tracking
_no_data_warning_logged = False
_last_warning_time = 0
_warning_cooldown = 60 # Only log warnings once per minute
_last_log_times = {}
_shared_data = None
_last_shared_update = 0
def __init__(
self,
config: Dict[str, Any],
display_manager: DisplayManager,
cache_manager: CacheManager,
):
self.logger = logging.getLogger("NCAAWB") # Changed logger name
super().__init__(
config=config,
display_manager=display_manager,
cache_manager=cache_manager,
logger=self.logger,
sport_key="ncaaw_basketball",
)
# Check display modes to determine what data to fetch
display_modes = self.mode_config.get("display_modes", {})
self.recent_enabled = display_modes.get("ncaaw_basketball_recent", False)
self.upcoming_enabled = display_modes.get("ncaaw_basketball_upcoming", False)
self.live_enabled = display_modes.get("ncaaw_basketball_live", False)
self.logger.info(
f"Initialized NCAA Womens Basketball manager with display dimensions: {self.display_width}x{self.display_height}"
)
self.logger.info(f"Logo directory: {self.logo_dir}")
self.logger.info(
f"Display modes - Recent: {self.recent_enabled}, Upcoming: {self.upcoming_enabled}, Live: {self.live_enabled}"
)
self.league = "womens-college-basketball"
def _fetch_ncaaw_basketball_api_data(
self, use_cache: bool = True
) -> Optional[Dict]:
"""
Fetches the full season schedule for NCAA Womens Basketball using background threading.
Returns cached data immediately if available, otherwise starts background fetch.
"""
now = datetime.now(pytz.utc)
season_year = now.year
cache_key = f"{self.sport_key}_schedule_{season_year}"
# Check cache first
if use_cache:
cached_data = self.cache_manager.get(cache_key)
if cached_data:
# Validate cached data structure
if isinstance(cached_data, dict) and "events" in cached_data:
self.logger.info(f"Using cached schedule for {season_year}")
return cached_data
elif isinstance(cached_data, list):
# Handle old cache format (list of events)
self.logger.info(
f"Using cached schedule for {season_year} (legacy format)"
)
return {"events": cached_data}
else:
self.logger.warning(
f"Invalid cached data format for {season_year}: {type(cached_data)}"
)
# Clear invalid cache
self.cache_manager.clear_cache(cache_key)
# If background service is disabled, fall back to synchronous fetch
if not self.background_enabled or not self.background_service:
return self._fetch_ncaaw_basketball_api_data_sync(use_cache)
# Start background fetch
self.logger.info(
f"Starting background fetch for {season_year} season schedule..."
)
def fetch_callback(result):
"""Callback when background fetch completes."""
if result.success:
self.logger.info(
f"Background fetch completed for {season_year}: {len(result.data.get('events'))} events"
)
else:
self.logger.error(
f"Background fetch failed for {season_year}: {result.error}"
)
# Clean up request tracking
if season_year in self.background_fetch_requests:
del self.background_fetch_requests[season_year]
# Get background service configuration
background_config = self.mode_config.get("background_service", {})
timeout = background_config.get("request_timeout", 30)
max_retries = background_config.get("max_retries", 3)
priority = background_config.get("priority", 2)
# Submit background fetch request
request_id = self.background_service.submit_fetch_request(
sport="ncaa_womens_basketball",
year=season_year,
url=ESPN_NCAAWB_SCOREBOARD_URL,
cache_key=cache_key,
params={"dates": season_year, "limit": 1000},
headers=self.headers,
timeout=timeout,
max_retries=max_retries,
priority=priority,
callback=fetch_callback,
)
# Track the request
self.background_fetch_requests[season_year] = request_id
# For immediate response, try to get partial data
partial_data = self._get_weeks_data()
if partial_data:
return partial_data
return None
def _fetch_ncaaw_basketball_api_data_sync(
self, use_cache: bool = True
) -> Optional[Dict]:
"""
Synchronous fallback for fetching NFL data when background service is disabled.
"""
now = datetime.now(pytz.utc)
current_year = now.year
cache_key = f"ncaa_womens_basketball_schedule_{current_year}"
self.logger.info(
f"Fetching full {current_year} season schedule from ESPN API (sync mode)..."
)
try:
response = self.session.get(
ESPN_NCAAWB_SCOREBOARD_URL,
params={"dates": current_year, "limit": 1000},
headers=self.headers,
timeout=15,
)
response.raise_for_status()
data = response.json()
events = data.get("events", [])
if use_cache:
self.cache_manager.set(cache_key, events)
self.logger.info(
f"Successfully fetched {len(events)} events for the {current_year} season."
)
return {"events": events}
except requests.exceptions.RequestException as e:
self.logger.error(f"API error fetching full schedule: {e}")
return None
def _fetch_data(self) -> Optional[Dict]:
"""Fetch data using shared data mechanism or direct fetch for live."""
if isinstance(self, NCAAWBasketballLiveManager):
# Live games should fetch only current games, not entire season
return self._fetch_todays_games()
else:
# Recent and Upcoming managers should use cached season data
return self._fetch_ncaaw_basketball_api_data(use_cache=True)
class NCAAWBasketballLiveManager(BaseNCAAWBasketballManager, BasketballLive):
"""Manager for live NCAA WB games."""
def __init__(
self,
config: Dict[str, Any],
display_manager: DisplayManager,
cache_manager: CacheManager,
):
super().__init__(config, display_manager, cache_manager)
self.logger = logging.getLogger(
"NCAAWBasketballLiveManager"
) # Changed logger name
if self.test_mode:
# More detailed test game for NCAA WB
self.current_game = {
"id": "test001",
"home_abbr": "AUB",
"home_id": "123",
"away_abbr": "GT",
"away_id": "asdf",
"home_score": "21",
"away_score": "17",
"period": 3,
"period_text": "Q3",
"clock": "5:24",
"home_logo_path": Path(self.logo_dir, "AUB.png"),
"away_logo_path": Path(self.logo_dir, "GT.png"),
"is_live": True,
"is_final": False,
"is_upcoming": False,
"is_halftime": False,
}
self.live_games = [self.current_game]
self.logger.info(
"Initialized NCAAWBasketballLiveManager with test game: GT vs AUB"
)
else:
self.logger.info(" Initialized NCAAWBasketballLiveManager in live mode")
class NCAAWBasketballRecentManager(BaseNCAAWBasketballManager, SportsRecent):
"""Manager for recently completed NCAA WB games."""
def __init__(
self,
config: Dict[str, Any],
display_manager: DisplayManager,
cache_manager: CacheManager,
):
super().__init__(config, display_manager, cache_manager)
self.logger = logging.getLogger(
"NCAAWBasketballRecentManager"
) # Changed logger name
self.logger.info(
f"Initialized NCAAWBasketballRecentManager with {len(self.favorite_teams)} favorite teams"
)
class NCAAWBasketballUpcomingManager(BaseNCAAWBasketballManager, SportsUpcoming):
"""Manager for upcoming NCAA WB games."""
def __init__(
self,
config: Dict[str, Any],
display_manager: DisplayManager,
cache_manager: CacheManager,
):
super().__init__(config, display_manager, cache_manager)
self.logger = logging.getLogger(
"NCAAWBasketballUpcomingManager"
) # Changed logger name
self.logger.info(
f"Initialized NCAAWBasketballUpcomingManager with {len(self.favorite_teams)} favorite teams"
)

View File

@@ -23,13 +23,10 @@ class BaseNFLManager(Football): # Renamed class
_warning_cooldown = 60 # Only log warnings once per minute
_shared_data = None
_last_shared_update = 0
def __init__(self, config: Dict[str, Any], display_manager: DisplayManager, cache_manager: CacheManager):
self.logger = logging.getLogger('NFL') # Changed logger name
super().__init__(config=config, display_manager=display_manager, cache_manager=cache_manager, logger=self.logger, sport_key="nfl")
# Configuration is already set in base class
# self.logo_dir and self.update_interval are already configured
# Check display modes to determine what data to fetch
display_modes = self.mode_config.get("display_modes", {})

304
src/wnba_managers.py Normal file
View File

@@ -0,0 +1,304 @@
import logging
import time
from datetime import datetime
from pathlib import Path
from typing import Any, Dict, Optional
import pytz
import requests
from src.base_classes.basketball import Basketball, BasketballLive
from src.base_classes.sports import SportsRecent, SportsUpcoming
from src.cache_manager import CacheManager
from src.display_manager import DisplayManager
# Import the API counter function from web interface
try:
from web_interface_v2 import increment_api_counter
except ImportError:
# Fallback if web interface is not available
def increment_api_counter(kind: str, count: int = 1):
pass
# Constants
ESPN_WNBA_SCOREBOARD_URL = (
"https://site.api.espn.com/apis/site/v2/sports/basketball/wnba/scoreboard"
)
class BaseWNBAManager(Basketball):
"""Base class for WNBA managers with common functionality."""
# Class variables for warning tracking
_no_data_warning_logged = False
_last_warning_time = 0
_warning_cooldown = 60 # Only log warnings once per minute
_last_log_times = {}
_shared_data = None
_last_shared_update = 0
def __init__(
self,
config: Dict[str, Any],
display_manager: DisplayManager,
cache_manager: CacheManager,
):
self.logger = logging.getLogger("WNBA") # Changed logger name
super().__init__(
config=config,
display_manager=display_manager,
cache_manager=cache_manager,
logger=self.logger,
sport_key="wnba",
)
# Check display modes to determine what data to fetch
display_modes = self.mode_config.get("display_modes", {})
self.recent_enabled = display_modes.get("wnba_recent", False)
self.upcoming_enabled = display_modes.get("wnba_upcoming", False)
self.live_enabled = display_modes.get("wnba_live", False)
self.logger.info(
f"Initialized WNBA manager with display dimensions: {self.display_width}x{self.display_height}"
)
self.logger.info(f"Logo directory: {self.logo_dir}")
self.logger.info(
f"Display modes - Recent: {self.recent_enabled}, Upcoming: {self.upcoming_enabled}, Live: {self.live_enabled}"
)
self.league = "wnba"
def _fetch_wnba_api_data(self, use_cache: bool = True) -> Optional[Dict]:
"""
Fetches the full season schedule for WNBA using background threading.
Returns cached data immediately if available, otherwise starts background fetch.
"""
now = datetime.now(pytz.utc)
season_year = now.year
if now.month < 2:
season_year = now.year - 1
datestring = f"{season_year}0401-{season_year+1}1101"
cache_key = f"{self.sport_key}_schedule_{season_year}"
# Check cache first
if use_cache:
cached_data = self.cache_manager.get(cache_key)
if cached_data:
# Validate cached data structure
if isinstance(cached_data, dict) and "events" in cached_data:
self.logger.info(f"Using cached schedule for {season_year}")
return cached_data
elif isinstance(cached_data, list):
# Handle old cache format (list of events)
self.logger.info(
f"Using cached schedule for {season_year} (legacy format)"
)
return {"events": cached_data}
else:
self.logger.warning(
f"Invalid cached data format for {season_year}: {type(cached_data)}"
)
# Clear invalid cache
self.cache_manager.clear_cache(cache_key)
# If background service is disabled, fall back to synchronous fetch
if not self.background_enabled or not self.background_service:
return self._fetch_wnba_api_data_sync(use_cache)
# Start background fetch
self.logger.info(
f"Starting background fetch for {season_year} season schedule..."
)
def fetch_callback(result):
"""Callback when background fetch completes."""
if result.success:
self.logger.info(
f"Background fetch completed for {season_year}: {len(result.data.get('events'))} events"
)
else:
self.logger.error(
f"Background fetch failed for {season_year}: {result.error}"
)
# Clean up request tracking
if season_year in self.background_fetch_requests:
del self.background_fetch_requests[season_year]
# Get background service configuration
background_config = self.mode_config.get("background_service", {})
timeout = background_config.get("request_timeout", 30)
max_retries = background_config.get("max_retries", 3)
priority = background_config.get("priority", 2)
# Submit background fetch request
request_id = self.background_service.submit_fetch_request(
sport="nba",
year=season_year,
url=ESPN_WNBA_SCOREBOARD_URL,
cache_key=cache_key,
params={"dates": datestring, "limit": 1000},
headers=self.headers,
timeout=timeout,
max_retries=max_retries,
priority=priority,
callback=fetch_callback,
)
# Track the request
self.background_fetch_requests[season_year] = request_id
# For immediate response, try to get partial data
partial_data = self._get_weeks_data()
if partial_data:
return partial_data
return None
def _fetch_wnba_api_data_sync(self, use_cache: bool = True) -> Optional[Dict]:
"""
Synchronous fallback for fetching WNBA data when background service is disabled.
"""
now = datetime.now(pytz.utc)
current_year = now.year
cache_key = f"nba_schedule_{current_year}"
self.logger.info(
f"Fetching full {current_year} season schedule from ESPN API (sync mode)..."
)
try:
response = self.session.get(
ESPN_WNBA_SCOREBOARD_URL,
params={"dates": current_year, "limit": 1000},
headers=self.headers,
timeout=15,
)
response.raise_for_status()
data = response.json()
events = data.get("events", [])
if use_cache:
self.cache_manager.set(cache_key, events)
self.logger.info(
f"Successfully fetched {len(events)} events for the {current_year} season."
)
return {"events": events}
except requests.exceptions.RequestException as e:
self.logger.error(f"API error fetching full schedule: {e}")
return None
def _fetch_data(self) -> Optional[Dict]:
"""Fetch data using shared data mechanism or direct fetch for live."""
if isinstance(self, WNBALiveManager):
# Live games should fetch only current games, not entire season
return self._fetch_todays_games()
else:
# Recent and Upcoming managers should use cached season data
return self._fetch_wnba_api_data(use_cache=True)
class WNBALiveManager(BaseWNBAManager, BasketballLive):
"""Manager for live NBA games."""
def __init__(
self,
config: Dict[str, Any],
display_manager: DisplayManager,
cache_manager: CacheManager,
):
super().__init__(config, display_manager, cache_manager)
self.logger = logging.getLogger("WNBALiveManager") # Changed logger name
if self.test_mode:
# More detailed test game for NBA
self.current_game = {
"id": "test001",
"home_abbr": "CHI",
"home_id": "123",
"away_abbr": "ATL",
"away_id": "asdf",
"home_score": "21",
"away_score": "17",
"period": 3,
"period_text": "Q3",
"clock": "5:24",
"home_logo_path": Path(self.logo_dir, "CHI.png"),
"away_logo_path": Path(self.logo_dir, "ATL.png"),
"is_live": True,
"is_final": False,
"is_upcoming": False,
"is_halftime": False,
}
self.live_games = [self.current_game]
self.logger.info("Initialized WNBALiveManager with test game: BUF vs KC")
else:
self.logger.info(" Initialized WNBALiveManager in live mode")
class WNBARecentManager(BaseWNBAManager, SportsRecent):
"""Manager for recently completed WNBA games."""
def __init__(
self,
config: Dict[str, Any],
display_manager: DisplayManager,
cache_manager: CacheManager,
):
super().__init__(config, display_manager, cache_manager)
self.logger = logging.getLogger("WNBARecentManager") # Changed logger name
self.logger.info(
f"Initialized WNBARecentManager with {len(self.favorite_teams)} favorite teams"
)
class WNBAUpcomingManager(BaseWNBAManager, SportsUpcoming):
"""Manager for upcoming WNBA games."""
def __init__(
self,
config: Dict[str, Any],
display_manager: DisplayManager,
cache_manager: CacheManager,
):
super().__init__(config, display_manager, cache_manager)
self.logger = logging.getLogger("WNBAUpcomingManager") # Changed logger name
self.logger.info(
f"Initialized WNBAUpcomingManager with {len(self.favorite_teams)} favorite teams"
)
"""Display upcoming games."""
if not self.upcoming_games:
return
try:
current_time = time.time()
# Check if it's time to switch games
if (
len(self.upcoming_games) > 1
and current_time - self.last_game_switch >= self.game_display_duration
):
# Move to next game
self.current_game_index = (self.current_game_index + 1) % len(
self.upcoming_games
)
self.current_game = self.upcoming_games[self.current_game_index]
self.last_game_switch = current_time
force_clear = True
# Log team switching
if self.current_game:
away_abbr = self.current_game.get("away_abbr", "UNK")
home_abbr = self.current_game.get("home_abbr", "UNK")
self.logger.info(
f"[NBA Upcoming] Showing {away_abbr} vs {home_abbr}"
)
# Draw the scorebug layout
self._draw_scorebug_layout(self.current_game, force_clear)
except Exception as e:
self.logger.error(
f"[NBA] Error displaying upcoming game: {e}", exc_info=True
)