diff --git a/plugins/baseball-scoreboard/manager.py b/plugins/baseball-scoreboard/manager.py index 6df9b04..9298b47 100644 --- a/plugins/baseball-scoreboard/manager.py +++ b/plugins/baseball-scoreboard/manager.py @@ -955,31 +955,57 @@ def _ensure_manager_updated(self, manager) -> None: self.logger.debug(f"Auto-refresh failed for manager {manager}: {exc}") def update(self) -> None: - """Update baseball game data.""" + """Update baseball game data using parallel manager updates.""" if not self.is_enabled: return - try: - # Update MLB managers if enabled - if self.mlb_enabled: - self.mlb_live.update() - self.mlb_recent.update() - self.mlb_upcoming.update() + from concurrent.futures import ThreadPoolExecutor, as_completed, TimeoutError - # Update MiLB managers if enabled - if self.milb_enabled: - self.milb_live.update() - self.milb_recent.update() - self.milb_upcoming.update() + # Collect all enabled managers (use getattr to guard against partial init) + managers_to_update = [] + if self.mlb_enabled: + for name, attr in [("MLB Live", "mlb_live"), ("MLB Recent", "mlb_recent"), ("MLB Upcoming", "mlb_upcoming")]: + mgr = getattr(self, attr, None) + if mgr is not None: + managers_to_update.append((name, mgr)) + if self.milb_enabled: + for name, attr in [("MiLB Live", "milb_live"), ("MiLB Recent", "milb_recent"), ("MiLB Upcoming", "milb_upcoming")]: + mgr = getattr(self, attr, None) + if mgr is not None: + managers_to_update.append((name, mgr)) + if self.ncaa_baseball_enabled: + for name, attr in [("NCAA Live", "ncaa_baseball_live"), ("NCAA Recent", "ncaa_baseball_recent"), ("NCAA Upcoming", "ncaa_baseball_upcoming")]: + mgr = getattr(self, attr, None) + if mgr is not None: + managers_to_update.append((name, mgr)) - # Update NCAA Baseball managers if enabled - if self.ncaa_baseball_enabled: - self.ncaa_baseball_live.update() - self.ncaa_baseball_recent.update() - self.ncaa_baseball_upcoming.update() + if not managers_to_update: + return + def _safe_update(name_and_manager): + name, manager = name_and_manager + try: + manager.update() + except Exception as e: + self.logger.error(f"Error updating {name} manager: {e}") + + # All managers run in parallel — they're I/O-bound (ESPN API calls) + # so more threads than cores is fine on Pi + executor = ThreadPoolExecutor(max_workers=len(managers_to_update), thread_name_prefix="baseball-update") + try: + futures = { + executor.submit(_safe_update, item): item[0] + for item in managers_to_update + } + for future in as_completed(futures, timeout=25): + future.result() # propagate unexpected executor errors + except TimeoutError: + still_running = [name for f, name in futures.items() if not f.done()] + self.logger.warning(f"Manager update timed out after 25s, still running: {still_running}") except Exception as e: - self.logger.error(f"Error updating managers: {e}") + self.logger.error(f"Error in parallel manager updates: {e}") + finally: + executor.shutdown(wait=False, cancel_futures=True) def _get_managers_in_priority_order(self, mode_type: str) -> list: """ @@ -2566,7 +2592,7 @@ def get_info(self) -> Dict[str, Any]: info = { "plugin_id": self.plugin_id, "name": "Baseball Scoreboard", - "version": "1.3.0", + "version": "1.6.0", "enabled": self.is_enabled, "display_size": f"{self.display_width}x{self.display_height}", "mlb_enabled": self.mlb_enabled, diff --git a/plugins/baseball-scoreboard/manifest.json b/plugins/baseball-scoreboard/manifest.json index 69fc693..ac85b54 100644 --- a/plugins/baseball-scoreboard/manifest.json +++ b/plugins/baseball-scoreboard/manifest.json @@ -1,7 +1,7 @@ { "id": "baseball-scoreboard", "name": "Baseball Scoreboard", - "version": "1.5.6", + "version": "1.6.0", "author": "ChuckBuilds", "description": "Live, recent, and upcoming baseball games across MLB, MiLB, and NCAA Baseball with real-time scores and schedules", "category": "sports", @@ -30,6 +30,11 @@ "branch": "main", "plugin_path": "plugins/baseball-scoreboard", "versions": [ + { + "released": "2026-03-30", + "version": "1.6.0", + "ledmatrix_min": "2.0.0" + }, { "released": "2026-03-29", "version": "1.5.6", @@ -111,7 +116,7 @@ "ledmatrix_min": "2.0.0" } ], - "last_updated": "2026-03-29", + "last_updated": "2026-03-30", "stars": 0, "downloads": 0, "verified": true, diff --git a/plugins/baseball-scoreboard/milb_managers.py b/plugins/baseball-scoreboard/milb_managers.py index 44b2004..16087a5 100644 --- a/plugins/baseball-scoreboard/milb_managers.py +++ b/plugins/baseball-scoreboard/milb_managers.py @@ -1,4 +1,6 @@ import logging +import threading +import time from datetime import datetime, timedelta from pathlib import Path from typing import Any, ClassVar, Dict, List, Optional @@ -30,6 +32,9 @@ class BaseMiLBManager(Baseball): _warning_cooldown: ClassVar[int] = 60 # Only log warnings once per minute _shared_data: ClassVar[Optional[Dict]] = None _last_shared_update: ClassVar[float] = 0 + _shared_rankings_cache: ClassVar[Dict] = {} + _shared_rankings_timestamp: ClassVar[float] = 0 + _shared_rankings_lock: ClassVar[threading.Lock] = threading.Lock() def __init__(self, config: Dict[str, Any], display_manager, cache_manager): self.logger = logging.getLogger("MiLB") @@ -61,6 +66,33 @@ def __init__(self, config: Dict[str, Any], display_manager, cache_manager): ) self.league = "minor-league-baseball" + def _fetch_team_rankings(self) -> Dict[str, int]: + """Share rankings cache across all MiLB manager instances (thread-safe).""" + current_time = time.time() + if ( + BaseMiLBManager._shared_rankings_cache + and current_time - BaseMiLBManager._shared_rankings_timestamp + < self._rankings_cache_duration + ): + self._team_rankings_cache = BaseMiLBManager._shared_rankings_cache + return self._team_rankings_cache + + with BaseMiLBManager._shared_rankings_lock: + # Double-check after acquiring lock + current_time = time.time() + if ( + BaseMiLBManager._shared_rankings_cache + and current_time - BaseMiLBManager._shared_rankings_timestamp + < self._rankings_cache_duration + ): + self._team_rankings_cache = BaseMiLBManager._shared_rankings_cache + return self._team_rankings_cache + + result = super()._fetch_team_rankings() + BaseMiLBManager._shared_rankings_cache = result + BaseMiLBManager._shared_rankings_timestamp = current_time + return result + @staticmethod def _convert_stats_game_to_espn_event(game: Dict) -> Dict: """Convert a single MLB Stats API game to ESPN event format. diff --git a/plugins/baseball-scoreboard/mlb_managers.py b/plugins/baseball-scoreboard/mlb_managers.py index a1d5814..dbb252b 100644 --- a/plugins/baseball-scoreboard/mlb_managers.py +++ b/plugins/baseball-scoreboard/mlb_managers.py @@ -1,4 +1,6 @@ import logging +import threading +import time from datetime import datetime from pathlib import Path from typing import Any, ClassVar, Dict, Optional @@ -24,6 +26,9 @@ class BaseMLBManager(Baseball): _warning_cooldown: ClassVar[int] = 60 # Only log warnings once per minute _shared_data: ClassVar[Optional[Dict]] = None _last_shared_update: ClassVar[float] = 0 + _shared_rankings_cache: ClassVar[Dict] = {} + _shared_rankings_timestamp: ClassVar[float] = 0 + _shared_rankings_lock: ClassVar[threading.Lock] = threading.Lock() def __init__(self, config: Dict[str, Any], display_manager, cache_manager): self.logger = logging.getLogger("MLB") @@ -50,6 +55,33 @@ def __init__(self, config: Dict[str, Any], display_manager, cache_manager): ) self.league = "mlb" + def _fetch_team_rankings(self) -> Dict[str, int]: + """Share rankings cache across all MLB manager instances (thread-safe).""" + current_time = time.time() + if ( + BaseMLBManager._shared_rankings_cache + and current_time - BaseMLBManager._shared_rankings_timestamp + < self._rankings_cache_duration + ): + self._team_rankings_cache = BaseMLBManager._shared_rankings_cache + return self._team_rankings_cache + + with BaseMLBManager._shared_rankings_lock: + # Double-check after acquiring lock + current_time = time.time() + if ( + BaseMLBManager._shared_rankings_cache + and current_time - BaseMLBManager._shared_rankings_timestamp + < self._rankings_cache_duration + ): + self._team_rankings_cache = BaseMLBManager._shared_rankings_cache + return self._team_rankings_cache + + result = super()._fetch_team_rankings() + BaseMLBManager._shared_rankings_cache = result + BaseMLBManager._shared_rankings_timestamp = current_time + return result + def _fetch_mlb_api_data(self, use_cache: bool = True) -> Optional[Dict]: """ Fetches the full season schedule for MLB using background threading. diff --git a/plugins/baseball-scoreboard/ncaa_baseball_managers.py b/plugins/baseball-scoreboard/ncaa_baseball_managers.py index bc6733a..79c8298 100644 --- a/plugins/baseball-scoreboard/ncaa_baseball_managers.py +++ b/plugins/baseball-scoreboard/ncaa_baseball_managers.py @@ -1,4 +1,6 @@ import logging +import threading +import time from typing import ClassVar, Dict, Any, Optional from datetime import datetime import pytz @@ -22,6 +24,9 @@ class BaseNCAABaseballManager(Baseball): _last_shared_update: ClassVar[float] = 0 _processed_games_cache: ClassVar[Dict] = {} # Cache for processed game data _processed_games_timestamp: ClassVar[float] = 0 + _shared_rankings_cache: ClassVar[Dict] = {} + _shared_rankings_timestamp: ClassVar[float] = 0 + _shared_rankings_lock: ClassVar[threading.Lock] = threading.Lock() def __init__(self, config: Dict[str, Any], display_manager, cache_manager): self.logger = logging.getLogger("NCAABaseball") @@ -51,6 +56,33 @@ def __init__(self, config: Dict[str, Any], display_manager, cache_manager): f"Display modes - Recent: {self.recent_enabled}, Upcoming: {self.upcoming_enabled}, Live: {self.live_enabled}" ) + def _fetch_team_rankings(self) -> Dict[str, int]: + """Share rankings cache across all NCAA Baseball manager instances (thread-safe).""" + current_time = time.time() + if ( + BaseNCAABaseballManager._shared_rankings_cache + and current_time - BaseNCAABaseballManager._shared_rankings_timestamp + < self._rankings_cache_duration + ): + self._team_rankings_cache = BaseNCAABaseballManager._shared_rankings_cache + return self._team_rankings_cache + + with BaseNCAABaseballManager._shared_rankings_lock: + # Double-check after acquiring lock + current_time = time.time() + if ( + BaseNCAABaseballManager._shared_rankings_cache + and current_time - BaseNCAABaseballManager._shared_rankings_timestamp + < self._rankings_cache_duration + ): + self._team_rankings_cache = BaseNCAABaseballManager._shared_rankings_cache + return self._team_rankings_cache + + result = super()._fetch_team_rankings() + BaseNCAABaseballManager._shared_rankings_cache = result + BaseNCAABaseballManager._shared_rankings_timestamp = current_time + return result + def _fetch_ncaa_baseball_api_data(self, use_cache: bool = True) -> Optional[Dict]: """ Fetches the full season schedule for NCAA Baseball using date range approach to ensure diff --git a/plugins/baseball-scoreboard/sports.py b/plugins/baseball-scoreboard/sports.py index 606a302..ff91fc9 100644 --- a/plugins/baseball-scoreboard/sports.py +++ b/plugins/baseball-scoreboard/sports.py @@ -85,9 +85,8 @@ def __init__( self.session = requests.Session() retry_strategy = Retry( - total=5, # increased number of retries - backoff_factor=1, # increased backoff factor - # added 429 to retry list + total=3, + backoff_factor=0.5, # retries at 0s, 0.5s, 1s (1.5s total vs 15s before) status_forcelist=[429, 500, 502, 503, 504], allowed_methods=["GET", "HEAD", "OPTIONS"], )