Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
64 changes: 45 additions & 19 deletions plugins/baseball-scoreboard/manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -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}")
Comment on lines +958 to +1006
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Bump the plugin patch version for this perf change.

This hunk is a performance optimization, but get_info() later in the file still reports 1.3.0. Please increment the patch version so downstream consumers can distinguish this build.

🔖 Suggested follow-up outside this hunk
-                "version": "1.3.0",
+                "version": "1.3.1",
As per coding guidelines, `**/plugins/**/*.py`: Bump PATCH version (1.2.x) for bug fixes, performance improvements, documentation updates, or minor tweaks.
🧰 Tools
🪛 Ruff (0.15.7)

[warning] 992-992: Do not catch blind exception: Exception

(BLE001)


[warning] 1004-1004: Do not catch blind exception: Exception

(BLE001)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@plugins/baseball-scoreboard/manager.py` around lines 958 - 1005, Update the
plugin patch version reported by get_info() to reflect this performance change:
locate the get_info() function (and any plugin version consts like __version__
if present) that currently returns "1.3.0" and increment the patch number to
"1.3.1" so downstream consumers can distinguish this build; ensure any other
in-file version references are updated consistently.

finally:
executor.shutdown(wait=False, cancel_futures=True)

def _get_managers_in_priority_order(self, mode_type: str) -> list:
"""
Expand Down Expand Up @@ -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,
Expand Down
9 changes: 7 additions & 2 deletions plugins/baseball-scoreboard/manifest.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -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,
Expand Down
32 changes: 32 additions & 0 deletions plugins/baseball-scoreboard/milb_managers.py
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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")
Expand Down Expand Up @@ -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.
Expand Down
32 changes: 32 additions & 0 deletions plugins/baseball-scoreboard/mlb_managers.py
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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")
Expand All @@ -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.
Expand Down
32 changes: 32 additions & 0 deletions plugins/baseball-scoreboard/ncaa_baseball_managers.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import logging
import threading
import time
from typing import ClassVar, Dict, Any, Optional
from datetime import datetime
import pytz
Expand All @@ -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")
Expand Down Expand Up @@ -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
Expand Down
5 changes: 2 additions & 3 deletions plugins/baseball-scoreboard/sports.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"],
)
Expand Down