From 4c0d8bbce01c2a6042c3b5245f0d1c89da0b70af Mon Sep 17 00:00:00 2001 From: Chuck <33324927+ChuckBuilds@users.noreply.github.com> Date: Sun, 4 Oct 2026 16:58:33 -0400 Subject: [PATCH 1/2] feat(sports): cache ESPN scoreboards under core's shared key (fetch service stage 2) Needs the LEDMatrix core release that ships fetch service stage 2 (ChuckBuilds/LEDMatrix#728: espn_scoreboard_cache_key, get_espn_scoreboard, SportsFetchMixin._schedule_cache_key / _cached_schedule). That is 3.8.1. - odds-ticker: per-day scoreboards go through core's read/store helpers under espn_scoreboard_cache_key, with scoreboard_data_* read as a fallback, and are fetched with fetch_espn_scoreboard (limit=500, the identifying session, the fetch service) instead of a bare requests.get. A day holding a live game is held to the live interval by the entry's own age. - baseball, basketball, football, hockey: the nine schedule-window managers use SportsFetchMixin._schedule_cache_key / _cached_schedule, with the old {sport_key}_schedule_{window} key read as a fallback. - afl, nrl, soccer: their window keys move to the canonical key for the URL, the old key read as a fallback. Soccer keeps 2.39.3's single fetch under the per-key window lock; _cached_window now reads through _cached_schedule. - The live poll shares today's scoreboard with odds-ticker: baseball, football and hockey still ask ESPN on every live poll but leave the answer under core's shared key (get_espn_scoreboard, max_age=0); basketball and soccer move their 30 s live cache from _scoreboard_current to the shared key for the dates asked, the old key read for a release. Co-Authored-By: Claude Opus 5.5 --- plugins/afl-scoreboard/afl_managers.py | 8 ++- plugins/baseball-scoreboard/mlb_managers.py | 7 +- .../ncaa_baseball_managers.py | 7 +- plugins/baseball-scoreboard/sports.py | 21 +++--- plugins/basketball-scoreboard/nba_managers.py | 7 +- plugins/basketball-scoreboard/sports.py | 32 ++++++---- .../test_live_scoreboard_cache_is_short.py | 6 +- .../basketball-scoreboard/wnba_managers.py | 7 +- .../football-scoreboard/ncaa_fb_managers.py | 7 +- plugins/football-scoreboard/nfl_managers.py | 7 +- plugins/football-scoreboard/sports.py | 21 +++--- .../test_espn_date_ranges.py | 16 ++++- .../ncaam_hockey_managers.py | 7 +- .../ncaaw_hockey_managers.py | 7 +- plugins/hockey-scoreboard/nhl_managers.py | 7 +- plugins/hockey-scoreboard/sports.py | 21 +++--- .../test_espn_date_ranges.py | 3 +- plugins/nrl-scoreboard/nrl_managers.py | 8 ++- plugins/odds-ticker/manager.py | 64 +++++++++++++------ .../test_config_save_and_fetch_paths.py | 12 +++- plugins/soccer-scoreboard/soccer_managers.py | 15 +++-- plugins/soccer-scoreboard/sports.py | 32 ++++++---- 22 files changed, 223 insertions(+), 99 deletions(-) diff --git a/plugins/afl-scoreboard/afl_managers.py b/plugins/afl-scoreboard/afl_managers.py index e9def4e0..c41f77bd 100644 --- a/plugins/afl-scoreboard/afl_managers.py +++ b/plugins/afl-scoreboard/afl_managers.py @@ -7,6 +7,7 @@ Live/Recent/Upcoming managers. """ +from src.common.espn_dates import espn_scoreboard_cache_key_for_url import logging from datetime import datetime, timedelta from typing import Any, Dict, Optional @@ -92,12 +93,15 @@ def _fetch_afl_api_data(self, use_cache: bool = True) -> Optional[Dict]: end_date = now + timedelta(days=self.schedule_lookahead_days) date_str = f"{start_date.strftime('%Y%m%d')}-{end_date.strftime('%Y%m%d')}" - cache_key = f"afl_schedule_{date_str}" url = f"{ESPN_AFL_BASE_URL}/{self.league_key}/scoreboard" + # The canonical key (core espn_scoreboard_cache_key), shared with + # every plugin showing this league; the old one is read until it ages out. + legacy_key = f"afl_schedule_{date_str}" + cache_key = espn_scoreboard_cache_key_for_url(url, date_str) or legacy_key # Check cache first if use_cache: - cached_data = self.cache_manager.get(cache_key) + cached_data = self._cached_schedule(cache_key, (legacy_key,)) if cached_data: # Validate cached data structure if isinstance(cached_data, dict) and "events" in cached_data: diff --git a/plugins/baseball-scoreboard/mlb_managers.py b/plugins/baseball-scoreboard/mlb_managers.py index 3daa39fe..d8d3c063 100644 --- a/plugins/baseball-scoreboard/mlb_managers.py +++ b/plugins/baseball-scoreboard/mlb_managers.py @@ -96,11 +96,14 @@ def _fetch_mlb_api_data(self, use_cache: bool = True) -> Optional[Dict]: season_year = now.year - 1 # Only what Recent and Upcoming can show; see _schedule_window. datestring, window = self._schedule_window() - cache_key = f"{self.sport_key}_schedule_{window}" + # The canonical key (core espn_scoreboard_cache_key), shared with + # every plugin showing this league; the old one is read until it ages out. + cache_key = self._schedule_cache_key(datestring) + legacy_key = f"{self.sport_key}_schedule_{window}" # Check cache first if use_cache: - cached_data = self.cache_manager.get(cache_key) + cached_data = self._cached_schedule(cache_key, (legacy_key,)) if cached_data: # Validate cached data structure if isinstance(cached_data, dict) and "events" in cached_data: diff --git a/plugins/baseball-scoreboard/ncaa_baseball_managers.py b/plugins/baseball-scoreboard/ncaa_baseball_managers.py index 1a215aa0..3b490e05 100644 --- a/plugins/baseball-scoreboard/ncaa_baseball_managers.py +++ b/plugins/baseball-scoreboard/ncaa_baseball_managers.py @@ -98,10 +98,13 @@ def _fetch_ncaa_baseball_api_data(self, use_cache: bool = True) -> Optional[Dict season_year = now.year - 1 # Only what Recent and Upcoming can show; see _schedule_window. datestring, window = self._schedule_window() - cache_key = f"ncaa_baseball_schedule_{window}" + # The canonical key (core espn_scoreboard_cache_key), shared with + # every plugin showing this league; the old one is read until it ages out. + cache_key = self._schedule_cache_key(datestring) + legacy_key = f"ncaa_baseball_schedule_{window}" if use_cache: - cached_data = self.cache_manager.get(cache_key) + cached_data = self._cached_schedule(cache_key, (legacy_key,)) if cached_data: # Validate cached data structure if isinstance(cached_data, dict) and "events" in cached_data: diff --git a/plugins/baseball-scoreboard/sports.py b/plugins/baseball-scoreboard/sports.py index d090678f..b0bddede 100644 --- a/plugins/baseball-scoreboard/sports.py +++ b/plugins/baseball-scoreboard/sports.py @@ -50,7 +50,11 @@ def resolve_font_name(font_name: str) -> str: from src.base_odds_manager import BaseOddsManager from data_sources import ESPNDataSource # ESPN date-range helper: core ships it from 3.5.0, the manifest's floor. -from src.common.espn_dates import ESPN_MAX_LIMIT, fetch_espn_scoreboard +from src.common.espn_dates import ( + ESPN_MAX_LIMIT, + fetch_espn_scoreboard, + get_espn_scoreboard, +) from baseball_timezone import resolve_timezone from src.common.sports_shared import ( SportsCoreSharedMixin, SportsLiveSharedMixin, SportsRecentSharedMixin) @@ -1413,13 +1417,14 @@ def _fetch_todays_games(self) -> Optional[Dict]: f"Fetching games for {self.sport}/{self.league} over date range " f"{dates_param}" ) - data = fetch_espn_scoreboard( - self.session, - url, - params={"dates": dates_param, "limit": ESPN_MAX_LIMIT}, - headers=self.headers, - timeout=10, - logger=self.logger, + # Always asked (max_age=0: the live poll's own interval decides), + # and left under core's shared key for this scoreboard and date + # (espn_scoreboard_cache_key), so odds-ticker and anything else + # showing the same day reuse it instead of fetching it again. + data = get_espn_scoreboard( + self.session, self.sport, self.league, dates_param, + cache_manager=self.cache_manager, max_age=0, + headers=self.headers, timeout=10, logger=self.logger, ) events = data.get("events", []) diff --git a/plugins/basketball-scoreboard/nba_managers.py b/plugins/basketball-scoreboard/nba_managers.py index 0b7f3284..b79aa71d 100644 --- a/plugins/basketball-scoreboard/nba_managers.py +++ b/plugins/basketball-scoreboard/nba_managers.py @@ -60,11 +60,14 @@ def _fetch_nba_api_data(self, use_cache: bool = True) -> Optional[Dict]: season_year = now.year - 1 # Only what Recent and Upcoming can show; see _schedule_window. datestring, window = self._schedule_window() - cache_key = f"{self.sport_key}_schedule_{window}" + # The canonical key (core espn_scoreboard_cache_key), shared with + # every plugin showing this league; the old one is read until it ages out. + cache_key = self._schedule_cache_key(datestring) + legacy_key = f"{self.sport_key}_schedule_{window}" # Check cache first if use_cache: - cached_data = self.cache_manager.get(cache_key) + cached_data = self._cached_schedule(cache_key, (legacy_key,)) if cached_data: # Validate cached data structure if isinstance(cached_data, dict) and "events" in cached_data: diff --git a/plugins/basketball-scoreboard/sports.py b/plugins/basketball-scoreboard/sports.py index a9833de8..9e61bc8a 100644 --- a/plugins/basketball-scoreboard/sports.py +++ b/plugins/basketball-scoreboard/sports.py @@ -20,7 +20,13 @@ from dynamic_team_resolver import DynamicTeamResolver from data_sources import ESPNDataSource # ESPN date-range helper: core ships it from 3.5.0, the manifest's floor. -from src.common.espn_dates import ESPN_MAX_LIMIT, fetch_espn_scoreboard +from src.common.espn_dates import ( + ESPN_MAX_LIMIT, + espn_scoreboard_cache_key, + fetch_espn_scoreboard, + read_espn_scoreboard_cache, + store_espn_scoreboard_cache, +) from basketball_timezone import resolve_timezone # Import main logo downloader (same as football plugin) @@ -1652,14 +1658,6 @@ def _fetch_todays_games(self) -> Optional[Dict]: # This works around the date range limitation url = f"https://site.api.espn.com/apis/site/v2/sports/{self.sport}/{self.league}/scoreboard" - # Check cache first (short TTL for live data) - cache_key = f"{self.sport_key}_scoreboard_current" - cached_data = self.cache_manager.get(cache_key, max_age=30) # 30s cache for live data - if cached_data: - if isinstance(cached_data, dict) and "events" in cached_data: - self.logger.debug(f"Using cached current scoreboard for {self.sport}/{self.league}") - return cached_data - # For NCAA Basketball, don't use dates parameter (it causes 404) # For other sports, use today's date if self.league in ["mens-college-basketball", "womens-college-basketball"]: @@ -1681,6 +1679,18 @@ def _fetch_todays_games(self) -> Optional[Dict]: params = {"dates": dates_param, "limit": ESPN_MAX_LIMIT} self.logger.debug(f"Fetching today's games for {self.sport}/{self.league} on dates {dates_param}") + # Shared with any plugin showing the same scoreboard and dates + # (core espn_scoreboard_cache_key; this was + # "_scoreboard_current" whatever the dates). At most + # 30 s old, as before; the old key is still read for a release. + cache_key = espn_scoreboard_cache_key(self.sport, self.league, params.get("dates")) + cached_data = read_espn_scoreboard_cache( + self.cache_manager, cache_key, 30, + legacy_keys=(f"{self.sport_key}_scoreboard_current",)) + if cached_data and "events" in cached_data: + self.logger.debug(f"Using cached current scoreboard for {self.sport}/{self.league}") + return cached_data + data = fetch_espn_scoreboard( self.session, url, @@ -1707,8 +1717,8 @@ def _fetch_todays_games(self) -> Optional[Dict]: f"shortDetail={status_type.get('shortDetail', 'N/A')}" ) - # Cache the result (short TTL for live data) - self.cache_manager.set(cache_key, data) + # Cache the result for the next 30 s, for every reader + store_espn_scoreboard_cache(self.cache_manager, cache_key, data) return {"events": events} except requests.exceptions.RequestException as e: self.logger.error( diff --git a/plugins/basketball-scoreboard/test_live_scoreboard_cache_is_short.py b/plugins/basketball-scoreboard/test_live_scoreboard_cache_is_short.py index d5cf557b..5f0a35ef 100644 --- a/plugins/basketball-scoreboard/test_live_scoreboard_cache_is_short.py +++ b/plugins/basketball-scoreboard/test_live_scoreboard_cache_is_short.py @@ -59,6 +59,8 @@ def set(self, *a, **k): class _Stub: _fetch_todays_games = SportsCore._fetch_todays_games + _needs_previous_day = SportsCore._needs_previous_day + _LOOKBACK_CUTOFF_HOUR = SportsCore._LOOKBACK_CUTOFF_HOUR def __init__(self, league): self.sport = "basketball" @@ -73,7 +75,9 @@ def __init__(self, league): for league in ("nba", "wnba", "mens-college-basketball", "womens-college-basketball"): stub = _Stub(league) data = stub._fetch_todays_games() - ages = [age for key, age in stub.cache_manager.asked if key.endswith("_scoreboard_current")] + # Core's shared key (espn_scoreboard_cache_key) or the one used before it. + ages = [age for key, age in stub.cache_manager.asked + if key.startswith("espn_scoreboard_") or key.endswith("_scoreboard_current")] check(f"{league}: served from cache without a request", bool(data and data.get("events"))) check(f"{league}: the live scoreboard cache is at most {LIVE_CACHE_SECONDS}s", bool(ages) and all(age is not None and age <= LIVE_CACHE_SECONDS for age in ages), diff --git a/plugins/basketball-scoreboard/wnba_managers.py b/plugins/basketball-scoreboard/wnba_managers.py index 885ec9e0..3ce3b4f4 100644 --- a/plugins/basketball-scoreboard/wnba_managers.py +++ b/plugins/basketball-scoreboard/wnba_managers.py @@ -65,11 +65,14 @@ def _fetch_wnba_api_data(self, use_cache: bool = True) -> Optional[Dict]: season_year = now.year - 1 # Only what Recent and Upcoming can show; see _schedule_window. datestring, window = self._schedule_window() - cache_key = f"{self.sport_key}_schedule_{window}" + # The canonical key (core espn_scoreboard_cache_key), shared with + # every plugin showing this league; the old one is read until it ages out. + cache_key = self._schedule_cache_key(datestring) + legacy_key = f"{self.sport_key}_schedule_{window}" # Check cache first if use_cache: - cached_data = self.cache_manager.get(cache_key) + cached_data = self._cached_schedule(cache_key, (legacy_key,)) if cached_data: if isinstance(cached_data, dict) and "events" in cached_data: self.logger.info(f"Using cached schedule for {season_year}") diff --git a/plugins/football-scoreboard/ncaa_fb_managers.py b/plugins/football-scoreboard/ncaa_fb_managers.py index a1c4c610..832e96b3 100644 --- a/plugins/football-scoreboard/ncaa_fb_managers.py +++ b/plugins/football-scoreboard/ncaa_fb_managers.py @@ -63,10 +63,13 @@ def _fetch_ncaa_fb_api_data(self, use_cache: bool = True) -> Optional[Dict]: season_year = now.year - 1 # Only what Recent and Upcoming can show; see _schedule_window. datestring, window = self._schedule_window() - cache_key = f"ncaafb_schedule_{window}" + # The canonical key (core espn_scoreboard_cache_key), shared with + # every plugin showing this league; the old one is read until it ages out. + cache_key = self._schedule_cache_key(datestring) + legacy_key = f"ncaafb_schedule_{window}" if use_cache: - cached_data = self.cache_manager.get(cache_key) + cached_data = self._cached_schedule(cache_key, (legacy_key,)) if cached_data: # Validate cached data structure if isinstance(cached_data, dict) and "events" in cached_data: diff --git a/plugins/football-scoreboard/nfl_managers.py b/plugins/football-scoreboard/nfl_managers.py index abf5bb41..34800a4c 100644 --- a/plugins/football-scoreboard/nfl_managers.py +++ b/plugins/football-scoreboard/nfl_managers.py @@ -60,11 +60,14 @@ def _fetch_nfl_api_data(self, use_cache: bool = True) -> Optional[Dict]: season_year = now.year - 1 # Only what Recent and Upcoming can show; see _schedule_window. datestring, window = self._schedule_window() - cache_key = f"{self.sport_key}_schedule_{window}" + # The canonical key (core espn_scoreboard_cache_key), shared with + # every plugin showing this league; the old one is read until it ages out. + cache_key = self._schedule_cache_key(datestring) + legacy_key = f"{self.sport_key}_schedule_{window}" # Check cache first if use_cache: - cached_data = self.cache_manager.get(cache_key) + cached_data = self._cached_schedule(cache_key, (legacy_key,)) if cached_data: # Validate cached data structure if isinstance(cached_data, dict) and "events" in cached_data: diff --git a/plugins/football-scoreboard/sports.py b/plugins/football-scoreboard/sports.py index 7c896649..04e3c017 100644 --- a/plugins/football-scoreboard/sports.py +++ b/plugins/football-scoreboard/sports.py @@ -30,7 +30,11 @@ from src.base_odds_manager import BaseOddsManager from data_sources import ESPNDataSource # ESPN date-range helper: core ships it from 3.5.0, the manifest's floor. -from src.common.espn_dates import ESPN_MAX_LIMIT, fetch_espn_scoreboard +from src.common.espn_dates import ( + ESPN_MAX_LIMIT, + fetch_espn_scoreboard, + get_espn_scoreboard, +) from football_timezone import resolve_timezone # Imported at module load time on purpose (see the monorepo module-naming # rules): a deferred bare-name import could bind another plugin's @@ -1548,13 +1552,14 @@ def _fetch_todays_games(self) -> Optional[Dict]: f"Fetching games for {self.sport}/{self.league} over date range " f"{dates_param}" ) - data = fetch_espn_scoreboard( - self.session, - url, - params={"dates": dates_param, "limit": ESPN_MAX_LIMIT}, - headers=self.headers, - timeout=10, - logger=self.logger, + # Always asked (max_age=0: the live poll's own interval decides), + # and left under core's shared key for this scoreboard and date + # (espn_scoreboard_cache_key), so odds-ticker and anything else + # showing the same day reuse it instead of fetching it again. + data = get_espn_scoreboard( + self.session, self.sport, self.league, dates_param, + cache_manager=self.cache_manager, max_age=0, + headers=self.headers, timeout=10, logger=self.logger, ) events = data.get("events", []) diff --git a/plugins/football-scoreboard/test_espn_date_ranges.py b/plugins/football-scoreboard/test_espn_date_ranges.py index b5badfbb..e77fb439 100644 --- a/plugins/football-scoreboard/test_espn_date_ranges.py +++ b/plugins/football-scoreboard/test_espn_date_ranges.py @@ -163,8 +163,8 @@ def test_on_an_older_core_the_window_is_fetched_here_in_chunks(): assert sorted(sent[1:]) == sorted(chunks) assert all(call["limit"] <= espn_dates.ESPN_MAX_LIMIT for call in manager.session.calls) assert len(data["events"]) == len(chunks) - cached = [value for key, value in manager.cache_manager.store.items() if "schedule" in key] - assert cached and cached[0] is data + # Cached under core's canonical scoreboard key for this window. + assert manager.cache_manager.store[f"espn_scoreboard_football_nfl_{expected}"] is data def test_on_a_fixed_core_the_season_goes_to_the_background_service(): @@ -185,6 +185,18 @@ def test_with_no_background_service_the_season_is_fetched_here(): assert data is not None and data["events"] +def test_the_window_cached_under_the_old_key_is_used_after_an_upgrade(): + # The window an older version cached as nfl_schedule_window__ is + # served until it ages out, rather than every league refetching at once. + manager = make_manager(OldCoreService()) + _expected, window = manager._schedule_window() + old = {"events": [{"id": "from-the-old-key"}]} + manager.cache_manager.store[f"nfl_schedule_{window}"] = old + + assert manager._fetch_nfl_api_data(use_cache=True) is old + assert manager.session.calls == [] + + def test_the_lookback_window_recovers_from_a_rejected_range(): manager = make_manager(FixedCoreService()) data = manager._get_weeks_data() diff --git a/plugins/hockey-scoreboard/ncaam_hockey_managers.py b/plugins/hockey-scoreboard/ncaam_hockey_managers.py index b4753a64..f13929eb 100644 --- a/plugins/hockey-scoreboard/ncaam_hockey_managers.py +++ b/plugins/hockey-scoreboard/ncaam_hockey_managers.py @@ -50,10 +50,13 @@ def _fetch_ncaa_hockey_api_data(self, use_cache: bool = True) -> Optional[Dict]: season_year = now.year - 1 # Only what Recent and Upcoming can show; see _schedule_window. datestring, window = self._schedule_window() - cache_key = f"ncaa_mens_hockey_schedule_{window}" + # The canonical key (core espn_scoreboard_cache_key), shared with + # every plugin showing this league; the old one is read until it ages out. + cache_key = self._schedule_cache_key(datestring) + legacy_key = f"ncaa_mens_hockey_schedule_{window}" if use_cache: - cached_data = self.cache_manager.get(cache_key) + cached_data = self._cached_schedule(cache_key, (legacy_key,)) if cached_data: # Validate cached data structure if isinstance(cached_data, dict) and 'events' in cached_data: diff --git a/plugins/hockey-scoreboard/ncaaw_hockey_managers.py b/plugins/hockey-scoreboard/ncaaw_hockey_managers.py index cc468c39..96f2608a 100644 --- a/plugins/hockey-scoreboard/ncaaw_hockey_managers.py +++ b/plugins/hockey-scoreboard/ncaaw_hockey_managers.py @@ -68,10 +68,13 @@ def _fetch_ncaa_hockey_api_data(self, use_cache: bool = True) -> Optional[Dict]: season_year = now.year - 1 # Only what Recent and Upcoming can show; see _schedule_window. datestring, window = self._schedule_window() - cache_key = f"ncaa_womens_hockey_schedule_{window}" + # The canonical key (core espn_scoreboard_cache_key), shared with + # every plugin showing this league; the old one is read until it ages out. + cache_key = self._schedule_cache_key(datestring) + legacy_key = f"ncaa_womens_hockey_schedule_{window}" if use_cache: - cached_data = self.cache_manager.get(cache_key) + cached_data = self._cached_schedule(cache_key, (legacy_key,)) if cached_data: # Validate cached data structure if isinstance(cached_data, dict) and "events" in cached_data: diff --git a/plugins/hockey-scoreboard/nhl_managers.py b/plugins/hockey-scoreboard/nhl_managers.py index 8e2ac3d8..2b87cbe8 100644 --- a/plugins/hockey-scoreboard/nhl_managers.py +++ b/plugins/hockey-scoreboard/nhl_managers.py @@ -58,11 +58,14 @@ def _fetch_nhl_api_data(self, use_cache: bool = True) -> Optional[Dict]: season_year = now.year - 1 # Only what Recent and Upcoming can show; see _schedule_window. datestring, window = self._schedule_window() - cache_key = f"nhl_schedule_{window}" + # The canonical key (core espn_scoreboard_cache_key), shared with + # every plugin showing this league; the old one is read until it ages out. + cache_key = self._schedule_cache_key(datestring) + legacy_key = f"nhl_schedule_{window}" # Check cache first if use_cache: - cached_data = self.cache_manager.get(cache_key) + cached_data = self._cached_schedule(cache_key, (legacy_key,)) if cached_data: # Validate cached data structure if isinstance(cached_data, dict) and 'events' in cached_data: diff --git a/plugins/hockey-scoreboard/sports.py b/plugins/hockey-scoreboard/sports.py index 83a38301..23b3075e 100644 --- a/plugins/hockey-scoreboard/sports.py +++ b/plugins/hockey-scoreboard/sports.py @@ -37,7 +37,11 @@ from src.base_odds_manager import BaseOddsManager from data_sources import ESPNDataSource # ESPN date-range helper: core ships it from 3.5.0, the manifest's floor. -from src.common.espn_dates import ESPN_MAX_LIMIT, fetch_espn_scoreboard +from src.common.espn_dates import ( + ESPN_MAX_LIMIT, + fetch_espn_scoreboard, + get_espn_scoreboard, +) from hockey_timezone import resolve_timezone from src.common.sports_shared import ( SportsCoreSharedMixin, SportsLiveSharedMixin, SportsRecentSharedMixin) @@ -1362,13 +1366,14 @@ def _fetch_todays_games(self) -> Optional[Dict]: ) # Fetch todays games only url = f"https://site.api.espn.com/apis/site/v2/sports/{self.sport}/{self.league}/scoreboard" - data = fetch_espn_scoreboard( - self.session, - url, - params={"dates": dates_param, "limit": ESPN_MAX_LIMIT}, - headers=self.headers, - timeout=10, - logger=self.logger, + # Always asked (max_age=0: the live poll's own interval decides), + # and left under core's shared key for this scoreboard and date + # (espn_scoreboard_cache_key), so odds-ticker and anything else + # showing the same day reuse it instead of fetching it again. + data = get_espn_scoreboard( + self.session, self.sport, self.league, dates_param, + cache_manager=self.cache_manager, max_age=0, + headers=self.headers, timeout=10, logger=self.logger, ) events = data.get("events", []) diff --git a/plugins/hockey-scoreboard/test_espn_date_ranges.py b/plugins/hockey-scoreboard/test_espn_date_ranges.py index 8db52971..0421d62e 100644 --- a/plugins/hockey-scoreboard/test_espn_date_ranges.py +++ b/plugins/hockey-scoreboard/test_espn_date_ranges.py @@ -145,7 +145,8 @@ def test_the_window_is_fetched_here_when_the_service_cannot_fetch_ranges(service # fixed position; which chunk answers first is not significant. assert sorted(sent[1:]) == sorted(chunks) assert len(data["events"]) == len(chunks) - assert manager.cache_manager.store[f"nhl_schedule_{window}"] is data + # Cached under core's canonical scoreboard key for this window. + assert manager.cache_manager.store[f"espn_scoreboard_hockey_nhl_{expected}"] is data def test_the_window_is_the_configured_lookback_and_lookahead(): diff --git a/plugins/nrl-scoreboard/nrl_managers.py b/plugins/nrl-scoreboard/nrl_managers.py index 6aea0b5e..afc6377c 100644 --- a/plugins/nrl-scoreboard/nrl_managers.py +++ b/plugins/nrl-scoreboard/nrl_managers.py @@ -7,6 +7,7 @@ Live/Recent/Upcoming managers. """ +from src.common.espn_dates import espn_scoreboard_cache_key_for_url import logging from datetime import datetime, timedelta from typing import Any, Dict, Optional @@ -97,14 +98,17 @@ def _fetch_nrl_api_data(self, use_cache: bool = True) -> Optional[Dict]: end_date = now + timedelta(days=self.schedule_lookahead_days) date_str = f"{start_date.strftime('%Y%m%d')}-{end_date.strftime('%Y%m%d')}" - cache_key = f"nrl_schedule_{date_str}" # NOTE: NRL_LEAGUE_SLUG is "3" (ESPN's NRL slug) — the resulting URL is # .../sports/rugby-league/3/scoreboard. Do NOT replace "3" with "nrl". url = f"{ESPN_NRL_BASE_URL}/{NRL_LEAGUE_SLUG}/scoreboard" + # The canonical key (core espn_scoreboard_cache_key), shared with + # every plugin showing this league; the old one is read until it ages out. + legacy_key = f"nrl_schedule_{date_str}" + cache_key = espn_scoreboard_cache_key_for_url(url, date_str) or legacy_key # Check cache first if use_cache: - cached_data = self.cache_manager.get(cache_key) + cached_data = self._cached_schedule(cache_key, (legacy_key,)) if cached_data: if isinstance(cached_data, dict) and "events" in cached_data: self.logger.info("Using cached schedule for NRL") diff --git a/plugins/odds-ticker/manager.py b/plugins/odds-ticker/manager.py index d4eca5f5..93920133 100644 --- a/plugins/odds-ticker/manager.py +++ b/plugins/odds-ticker/manager.py @@ -118,6 +118,15 @@ def download_missing_logo(league, team_id, team_abbr, logo_path, logo_url): # Not optional: the ticker cannot draw without it, and every core this plugin # admits ships it. from src.common.scroll_helper import ScrollHelper +# Core's shared ESPN scoreboard cache (core 3.8.1, this version's floor). +from src.common.espn_dates import ( + ESPN_MAX_LIMIT, + espn_scoreboard_cache_key, + espn_scoreboard_url, + fetch_espn_scoreboard, + read_espn_scoreboard_cache, + store_espn_scoreboard_cache, +) # Get logger logger = logging.getLogger(__name__) @@ -1263,6 +1272,11 @@ def fetch(): logger.warning("Odds fetch failed for game %s: %s", game_id, payload) return None + #: The longest a fetched scoreboard may come from core's short response + #: cache: the shortest live interval _live_scoreboard_ttl can return, so + #: a payload that turns out to hold a live game is never older than that. + _LIVE_SCOREBOARD_MAX_AGE = 30 + def _live_scoreboard_ttl(self, data: Dict[str, Any], now: datetime) -> Optional[int]: """Short max_age for a scoreboard payload holding a live or imminent game. @@ -1331,7 +1345,12 @@ def _fetch_league_games(self, league_config: Dict[str, Any], now: datetime, cano and games_found >= self._collection_limit()): break # Enough for the widest candidate window; stop searching try: - cache_key = f"scoreboard_data_{sport}_{league}_{date}" + # One key for this scoreboard across plugins (core + # espn_scoreboard_cache_key): a scoreboard plugin's copy of + # the same day serves the ticker, and the other way round. + # The key this plugin used before is read until it ages out. + cache_key = espn_scoreboard_cache_key(sport, league, date) + legacy_keys = (f"scoreboard_data_{sport}_{league}_{date}",) # Dynamically set TTL for scoreboard data current_date_obj = now.date() @@ -1350,27 +1369,34 @@ def _fetch_league_games(self, league_config: Dict[str, Any], now: datetime, cano else: ttl = 43200 # 12 hours for future dates - data = self.cache_manager.get(cache_key, max_age=ttl) - if data is not None: - # The date-based TTL above is wrong for any game that - # is live or about to be: ESPN's day follows US time, - # so an evening game sits under the UTC "yesterday" - # (1h) or "tomorrow" (12h) key. Hold a payload with - # such a game to the live interval instead. - live_ttl = self._live_scoreboard_ttl(data, now) - if live_ttl is not None and live_ttl < ttl: - ttl = live_ttl - data = self.cache_manager.get(cache_key, max_age=ttl) + # The date-based TTL above is wrong for any game that is + # live or about to be: ESPN's day follows US time, so an + # evening game sits under the UTC "yesterday" (1h) or + # "tomorrow" (12h) date. A payload with such a game is + # held to the live interval instead. The read checks the + # entry's own age, whoever wrote it. + def fresh_enough(payload, age, ttl=ttl): + live_ttl = self._live_scoreboard_ttl(payload, now) + if live_ttl is None or live_ttl >= ttl: + return True + return age is not None and age <= live_ttl + + data = read_espn_scoreboard_cache( + self.cache_manager, cache_key, ttl, legacy_keys, accept=fresh_enough) if data is None: - url = f"https://site.api.espn.com/apis/site/v2/sports/{sport}/{league}/scoreboard?dates={date}" logger.debug(f"Fetching {league} games from ESPN API for date: {date}") - response = requests.get(url, timeout=self.request_timeout) - response.raise_for_status() - data = response.json() - - - self.cache_manager.set(cache_key, data) + # Through core's fetch service with the scoreboards' + # request (limit=500, the identifying session), so the + # two can share it; a 4xx/5xx still raises HTTPError. + data = fetch_espn_scoreboard( + getattr(self, 'session', None), + espn_scoreboard_url(sport, league), + params={"dates": date, "limit": ESPN_MAX_LIMIT}, + timeout=self.request_timeout, + cache_max_age=min(ttl, self._LIVE_SCOREBOARD_MAX_AGE), + ) + store_espn_scoreboard_cache(self.cache_manager, cache_key, data) logger.debug(f"Cached scoreboard for {league} on {date} with a TTL of {ttl} seconds.") else: logger.debug(f"Using cached scoreboard data for {league} on {date}.") diff --git a/plugins/odds-ticker/test_config_save_and_fetch_paths.py b/plugins/odds-ticker/test_config_save_and_fetch_paths.py index bef6c93a..41339002 100644 --- a/plugins/odds-ticker/test_config_save_and_fetch_paths.py +++ b/plugins/odds-ticker/test_config_save_and_fetch_paths.py @@ -258,14 +258,20 @@ def __getattr__(self, level): response = requests.Response() response.status_code = 404 real_logger = manager.logger +class _NotFound: + """The schedule goes through core's fetch_espn_scoreboard on the + plugin's session; ESPN answering 404 raises HTTPError from it.""" + + def get(self, url, **kwargs): + raise requests.exceptions.HTTPError("404", response=response) + + try: manager.logger = recorder - manager.requests.get = lambda *a, **k: (_ for _ in ()).throw( - requests.exceptions.HTTPError("404", response=response)) + p.session = _NotFound() p._fetch_league_games({"sport": "football", "league": "nfl"}, datetime.now(timezone.utc), "nfl") finally: - manager.requests.get = real_get manager.logger = real_logger levels = [lvl for lvl, msg in recorder.records if "on " in msg and ("404" in msg or "HTTP error" in msg)] check("an ESPN 404 takes the 404 branch (debug), not the generic HTTP error", diff --git a/plugins/soccer-scoreboard/soccer_managers.py b/plugins/soccer-scoreboard/soccer_managers.py index 47c11198..85ce65c8 100644 --- a/plugins/soccer-scoreboard/soccer_managers.py +++ b/plugins/soccer-scoreboard/soccer_managers.py @@ -5,6 +5,7 @@ Premier League, La Liga, Bundesliga, Serie A, Ligue 1, MLS, Champions League, and Europa League. """ +from src.common.espn_dates import espn_scoreboard_cache_key_for_url import logging import shutil import threading @@ -186,21 +187,25 @@ def _fetch_soccer_api_data(self, use_cache: bool = True) -> Optional[Dict]: end_date = now + timedelta(days=self.schedule_lookahead_days) date_str = f"{start_date.strftime('%Y%m%d')}-{end_date.strftime('%Y%m%d')}" - cache_key = f"soccer_{self.league_key}_schedule_{date_str}" url = f"{ESPN_SOCCER_BASE_URL}/{self.league_key}/scoreboard" + # The canonical key (core espn_scoreboard_cache_key), shared with + # every plugin showing this league; the old one is read until it ages out. + legacy_key = f"soccer_{self.league_key}_schedule_{date_str}" + cache_key = espn_scoreboard_cache_key_for_url(url, date_str) or legacy_key with self._window_lock(cache_key): if use_cache: - cached = self._cached_window(cache_key) + cached = self._cached_window(cache_key, (legacy_key,)) if cached is not None: return cached return self._fetch_season_directly( url, date_str, cache_key, f"{self.league_name}" ) - def _cached_window(self, cache_key: str) -> Optional[Dict]: - """The schedule cached under ``cache_key``, or None.""" - cached_data = self.cache_manager.get(cache_key) + def _cached_window(self, cache_key: str, legacy_keys=()) -> Optional[Dict]: + """The schedule cached under ``cache_key`` (or, until it ages out, + under one of ``legacy_keys``), or None.""" + cached_data = self._cached_schedule(cache_key, legacy_keys) if not cached_data: return None # Validate cached data structure diff --git a/plugins/soccer-scoreboard/sports.py b/plugins/soccer-scoreboard/sports.py index e85d3cc8..0d149059 100644 --- a/plugins/soccer-scoreboard/sports.py +++ b/plugins/soccer-scoreboard/sports.py @@ -20,7 +20,13 @@ from dynamic_team_resolver import DynamicTeamResolver from data_sources import ESPNDataSource # ESPN date-range helper: core ships it from 3.5.0, the manifest's floor. -from src.common.espn_dates import ESPN_MAX_LIMIT, fetch_espn_scoreboard +from src.common.espn_dates import ( + ESPN_MAX_LIMIT, + espn_scoreboard_cache_key, + fetch_espn_scoreboard, + read_espn_scoreboard_cache, + store_espn_scoreboard_cache, +) from soccer_timezone import resolve_timezone # Import main logo downloader (same as football plugin) @@ -1526,14 +1532,6 @@ def _fetch_todays_games(self) -> Optional[Dict]: # This works around the date range limitation url = f"https://site.api.espn.com/apis/site/v2/sports/{self.sport}/{self.league}/scoreboard" - # Check cache first (short TTL for live data) - cache_key = f"{self.sport_key}_scoreboard_current" - cached_data = self.cache_manager.get(cache_key, max_age=30) # 30s cache for live data - if cached_data: - if isinstance(cached_data, dict) and "events" in cached_data: - self.logger.debug(f"Using cached current scoreboard for {self.sport}/{self.league}") - return cached_data - # ESPN API anchors its schedule calendar to Eastern US time. # Always query using the Eastern date + 1-day lookback to catch # late-night games still in progress from the previous Eastern day. @@ -1549,6 +1547,18 @@ def _fetch_todays_games(self) -> Optional[Dict]: params = {"dates": dates_param, "limit": ESPN_MAX_LIMIT} self.logger.debug(f"Fetching today's games for {self.sport}/{self.league} on date {formatted_date}") + # Shared with any plugin showing the same scoreboard and dates + # (core espn_scoreboard_cache_key; this was + # "_scoreboard_current" whatever the dates). At most + # 30 s old, as before; the old key is still read for a release. + cache_key = espn_scoreboard_cache_key(self.sport, self.league, params.get("dates")) + cached_data = read_espn_scoreboard_cache( + self.cache_manager, cache_key, 30, + legacy_keys=(f"{self.sport_key}_scoreboard_current",)) + if cached_data and "events" in cached_data: + self.logger.debug(f"Using cached current scoreboard for {self.sport}/{self.league}") + return cached_data + data = fetch_espn_scoreboard( self.session, url, @@ -1575,8 +1585,8 @@ def _fetch_todays_games(self) -> Optional[Dict]: f"shortDetail={status_type.get('shortDetail', 'N/A')}" ) - # Cache the result (short TTL for live data) - self.cache_manager.set(cache_key, data) + # Cache the result for the next 30 s, for every reader + store_espn_scoreboard_cache(self.cache_manager, cache_key, data) return {"events": events} except requests.exceptions.RequestException as e: self.logger.error( From b148ccf39a5c8c3ee1166a06b542b0628e9cea0f Mon Sep 17 00:00:00 2001 From: Chuck <33324927+ChuckBuilds@users.noreply.github.com> Date: Sun, 4 Oct 2026 16:59:07 -0400 Subject: [PATCH 2/2] chore: bump the eight plugins for the shared ESPN scoreboard key (floor 3.8.1) afl 1.37.0, baseball 1.59.0, basketball 1.44.0, football 3.20.0, hockey 1.44.0, nrl 1.36.0, odds-ticker 1.8.0, soccer 2.41.0, each with a versions[] entry floored on core 3.8.1 (the first release with ChuckBuilds/LEDMatrix#728), CHANGELOG entries and plugins.json. Co-Authored-By: Claude Opus 5.5 --- plugins.json | 36 ++++++++++----------- plugins/afl-scoreboard/CHANGELOG.md | 10 ++++++ plugins/afl-scoreboard/manifest.json | 8 ++++- plugins/baseball-scoreboard/CHANGELOG.md | 13 ++++++++ plugins/baseball-scoreboard/manifest.json | 8 ++++- plugins/basketball-scoreboard/CHANGELOG.md | 14 ++++++++ plugins/basketball-scoreboard/manifest.json | 8 ++++- plugins/football-scoreboard/CHANGELOG.md | 13 ++++++++ plugins/football-scoreboard/manifest.json | 8 ++++- plugins/hockey-scoreboard/CHANGELOG.md | 13 ++++++++ plugins/hockey-scoreboard/manifest.json | 8 ++++- plugins/nrl-scoreboard/CHANGELOG.md | 10 ++++++ plugins/nrl-scoreboard/manifest.json | 8 ++++- plugins/odds-ticker/manifest.json | 10 ++++-- plugins/soccer-scoreboard/CHANGELOG.md | 16 +++++++++ plugins/soccer-scoreboard/manifest.json | 8 ++++- 16 files changed, 164 insertions(+), 27 deletions(-) diff --git a/plugins.json b/plugins.json index fb095076..fdc03ee6 100644 --- a/plugins.json +++ b/plugins.json @@ -80,9 +80,9 @@ "last_updated": "2026-10-05", "verified": true, "screenshot": "", - "latest_version": "1.58.0", + "latest_version": "1.59.0", "ledmatrix_min_version": "3.8.1", - "commit": "540fd24022abf04f66b0657d923cac8ee0453aad" + "commit": "80aba717c9cd67d91d047ae9c5eb9607b9d584ad" }, { "id": "basketball-scoreboard", @@ -107,9 +107,9 @@ "last_updated": "2026-10-05", "verified": true, "screenshot": "", - "latest_version": "1.43.0", + "latest_version": "1.44.0", "ledmatrix_min_version": "3.8.1", - "commit": "540fd24022abf04f66b0657d923cac8ee0453aad" + "commit": "80aba717c9cd67d91d047ae9c5eb9607b9d584ad" }, { "id": "calendar", @@ -283,9 +283,9 @@ "last_updated": "2026-10-05", "verified": true, "screenshot": "", - "latest_version": "3.19.0", + "latest_version": "3.20.0", "ledmatrix_min_version": "3.8.1", - "commit": "540fd24022abf04f66b0657d923cac8ee0453aad" + "commit": "80aba717c9cd67d91d047ae9c5eb9607b9d584ad" }, { "id": "geochron", @@ -385,10 +385,10 @@ "last_updated": "2026-10-05", "verified": true, "screenshot": "", - "latest_version": "1.43.0", + "latest_version": "1.44.0", "icon": "fas fa-hockey-puck", "ledmatrix_min_version": "3.8.1", - "commit": "540fd24022abf04f66b0657d923cac8ee0453aad" + "commit": "80aba717c9cd67d91d047ae9c5eb9607b9d584ad" }, { "id": "lacrosse-scoreboard", @@ -710,12 +710,12 @@ "plugin_path": "plugins/odds-ticker", "stars": 0, "downloads": 0, - "last_updated": "2026-10-02", + "last_updated": "2026-10-05", "verified": true, "screenshot": "", - "latest_version": "1.7.7", - "ledmatrix_min_version": "3.4.0", - "commit": "886d2038d22a35c962beac0ea261543339cffec9" + "latest_version": "1.8.0", + "ledmatrix_min_version": "3.8.1", + "commit": "80aba717c9cd67d91d047ae9c5eb9607b9d584ad" }, { "id": "of-the-day", @@ -847,9 +847,9 @@ "last_updated": "2026-10-05", "verified": true, "screenshot": "", - "latest_version": "2.40.0", + "latest_version": "2.41.0", "ledmatrix_min_version": "3.8.1", - "commit": "540fd24022abf04f66b0657d923cac8ee0453aad" + "commit": "80aba717c9cd67d91d047ae9c5eb9607b9d584ad" }, { "id": "static-image", @@ -1164,10 +1164,10 @@ "downloads": 0, "verified": true, "screenshot": "", - "latest_version": "1.36.0", + "latest_version": "1.37.0", "last_updated": "2026-10-05", "ledmatrix_min_version": "3.8.1", - "commit": "540fd24022abf04f66b0657d923cac8ee0453aad" + "commit": "80aba717c9cd67d91d047ae9c5eb9607b9d584ad" }, { "id": "tidbyt-baseball-scoreboard", @@ -1213,9 +1213,9 @@ "last_updated": "2026-10-05", "verified": true, "screenshot": "", - "latest_version": "1.35.0", + "latest_version": "1.36.0", "ledmatrix_min_version": "3.8.1", - "commit": "540fd24022abf04f66b0657d923cac8ee0453aad" + "commit": "80aba717c9cd67d91d047ae9c5eb9607b9d584ad" }, { "id": "jellyfin-now-playing", diff --git a/plugins/afl-scoreboard/CHANGELOG.md b/plugins/afl-scoreboard/CHANGELOG.md index cf35a1c9..4c6368cc 100644 --- a/plugins/afl-scoreboard/CHANGELOG.md +++ b/plugins/afl-scoreboard/CHANGELOG.md @@ -1,5 +1,15 @@ # Changelog +## [1.37.0] - 2026-10-05 + +### Changed +- Schedule windows are cached under core's shared ESPN scoreboard key + (espn_scoreboard_cache_key, fetch service stage 2), so every plugin showing + a league shares one fetch and one cached copy. The key this plugin used + before is still read, so an upgrade serves the copy already on disk instead + of refetching every league at once; a window that moves on a day deletes its + copy from the day before. Requires LEDMatrix core 3.8.1. + ## [1.36.0] - 2026-10-05 ### Changed diff --git a/plugins/afl-scoreboard/manifest.json b/plugins/afl-scoreboard/manifest.json index ef2d50ff..a59199af 100644 --- a/plugins/afl-scoreboard/manifest.json +++ b/plugins/afl-scoreboard/manifest.json @@ -1,7 +1,7 @@ { "id": "afl-scoreboard", "name": "AFL Scoreboard", - "version": "1.36.0", + "version": "1.37.0", "author": "ChuckBuilds", "description": "Live, recent, and upcoming AFL (Australian Football League) games with real-time scores and game status.", "category": "sports", @@ -18,6 +18,12 @@ "afl_upcoming" ], "versions": [ + { + "version": "1.37.0", + "released": "2026-10-05", + "ledmatrix_min_version": "3.8.1", + "notes": "Schedule windows are cached under core's shared ESPN scoreboard key (espn_scoreboard_cache_key, fetch service stage 2), so every plugin showing a league shares one fetch and one cached copy. The key this plugin used before is still read, so an upgrade serves the copy already on disk instead of refetching every league at once; a window that moves on a day deletes its copy from the day before. Requires LEDMatrix core 3.8.1." + }, { "version": "1.36.0", "released": "2026-10-05", diff --git a/plugins/baseball-scoreboard/CHANGELOG.md b/plugins/baseball-scoreboard/CHANGELOG.md index 9fb79a8d..16d15f12 100644 --- a/plugins/baseball-scoreboard/CHANGELOG.md +++ b/plugins/baseball-scoreboard/CHANGELOG.md @@ -1,5 +1,18 @@ # Changelog +## [1.59.0] - 2026-10-05 + +### Changed +- Schedule windows are cached under core's shared ESPN scoreboard key + (espn_scoreboard_cache_key, fetch service stage 2), so every plugin showing + a league shares one fetch and one cached copy. The key this plugin used + before is still read, so an upgrade serves the copy already on disk instead + of refetching every league at once; a window that moves on a day deletes its + copy from the day before. Requires LEDMatrix core 3.8.1. +- The live poll also leaves today's scoreboard under that key (it still asks + ESPN on every poll), so odds-ticker reuses it instead of fetching the same + day again. + ## [1.58.0] - 2026-10-05 ### Changed diff --git a/plugins/baseball-scoreboard/manifest.json b/plugins/baseball-scoreboard/manifest.json index a714ae39..ad393f16 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.58.0", + "version": "1.59.0", "update_interval": 60, "author": "ChuckBuilds", "description": "Live, recent, and upcoming baseball games across MLB, MiLB, and NCAA Baseball with real-time scores and schedules", @@ -31,6 +31,12 @@ "branch": "main", "plugin_path": "plugins/baseball-scoreboard", "versions": [ + { + "version": "1.59.0", + "released": "2026-10-05", + "ledmatrix_min_version": "3.8.1", + "notes": "Schedule windows are cached under core's shared ESPN scoreboard key (espn_scoreboard_cache_key, fetch service stage 2), so every plugin showing a league shares one fetch and one cached copy. The key this plugin used before is still read, so an upgrade serves the copy already on disk instead of refetching every league at once; a window that moves on a day deletes its copy from the day before. The live poll also leaves today's scoreboard under that key (it still asks ESPN on every poll), so odds-ticker reuses it instead of fetching the same day again. Requires LEDMatrix core 3.8.1." + }, { "version": "1.58.0", "released": "2026-10-05", diff --git a/plugins/basketball-scoreboard/CHANGELOG.md b/plugins/basketball-scoreboard/CHANGELOG.md index e0d6a94f..ba153d51 100644 --- a/plugins/basketball-scoreboard/CHANGELOG.md +++ b/plugins/basketball-scoreboard/CHANGELOG.md @@ -1,5 +1,19 @@ # Changelog +## [1.44.0] - 2026-10-05 + +### Changed +- Schedule windows are cached under core's shared ESPN scoreboard key + (espn_scoreboard_cache_key, fetch service stage 2), so every plugin showing + a league shares one fetch and one cached copy. The key this plugin used + before is still read, so an upgrade serves the copy already on disk instead + of refetching every league at once; a window that moves on a day deletes its + copy from the day before. Requires LEDMatrix core 3.8.1. +- The live poll's 30-second cache moves from `_scoreboard_current`, + whatever the dates, to the shared key for the dates it asks, so odds-ticker + and the scoreboard read each other's copy; the old key is read for a + release. + ## [1.43.0] - 2026-10-05 ### Changed diff --git a/plugins/basketball-scoreboard/manifest.json b/plugins/basketball-scoreboard/manifest.json index 9471c44b..4abc51dd 100644 --- a/plugins/basketball-scoreboard/manifest.json +++ b/plugins/basketball-scoreboard/manifest.json @@ -1,7 +1,7 @@ { "id": "basketball-scoreboard", "name": "Basketball Scoreboard", - "version": "1.43.0", + "version": "1.44.0", "update_interval": 60, "description": "Live, recent, and upcoming basketball games across NBA, NCAA Men's, NCAA Women's, and WNBA with real-time scores, schedules, and March Madness tournament support", "author": "ChuckBuilds", @@ -19,6 +19,12 @@ "branch": "main", "plugin_path": "plugins/basketball-scoreboard", "versions": [ + { + "version": "1.44.0", + "released": "2026-10-05", + "ledmatrix_min_version": "3.8.1", + "notes": "Schedule windows are cached under core's shared ESPN scoreboard key (espn_scoreboard_cache_key, fetch service stage 2), so every plugin showing a league shares one fetch and one cached copy. The key this plugin used before is still read, so an upgrade serves the copy already on disk instead of refetching every league at once; a window that moves on a day deletes its copy from the day before. The live poll's 30-second cache moves from _scoreboard_current, whatever the dates, to the shared key for the dates it asks, so odds-ticker and the scoreboard read each other's copy; the old key is read for a release. Requires LEDMatrix core 3.8.1." + }, { "version": "1.43.0", "released": "2026-10-05", diff --git a/plugins/football-scoreboard/CHANGELOG.md b/plugins/football-scoreboard/CHANGELOG.md index bd12566c..1e581d86 100644 --- a/plugins/football-scoreboard/CHANGELOG.md +++ b/plugins/football-scoreboard/CHANGELOG.md @@ -1,5 +1,18 @@ # Changelog +## [3.20.0] - 2026-10-05 + +### Changed +- Schedule windows are cached under core's shared ESPN scoreboard key + (espn_scoreboard_cache_key, fetch service stage 2), so every plugin showing + a league shares one fetch and one cached copy. The key this plugin used + before is still read, so an upgrade serves the copy already on disk instead + of refetching every league at once; a window that moves on a day deletes its + copy from the day before. Requires LEDMatrix core 3.8.1. +- The live poll also leaves today's scoreboard under that key (it still asks + ESPN on every poll), so odds-ticker reuses it instead of fetching the same + day again. + ## [3.19.0] - 2026-10-05 ### Changed diff --git a/plugins/football-scoreboard/manifest.json b/plugins/football-scoreboard/manifest.json index a3b83f7f..89923eff 100644 --- a/plugins/football-scoreboard/manifest.json +++ b/plugins/football-scoreboard/manifest.json @@ -1,7 +1,7 @@ { "id": "football-scoreboard", "name": "Football Scoreboard", - "version": "3.19.0", + "version": "3.20.0", "update_interval": 60, "author": "ChuckBuilds", "class_name": "FootballScoreboardPlugin", @@ -25,6 +25,12 @@ "ncaa_fb_live" ], "versions": [ + { + "version": "3.20.0", + "released": "2026-10-05", + "ledmatrix_min_version": "3.8.1", + "notes": "Schedule windows are cached under core's shared ESPN scoreboard key (espn_scoreboard_cache_key, fetch service stage 2), so every plugin showing a league shares one fetch and one cached copy. The key this plugin used before is still read, so an upgrade serves the copy already on disk instead of refetching every league at once; a window that moves on a day deletes its copy from the day before. The live poll also leaves today's scoreboard under that key (it still asks ESPN on every poll), so odds-ticker reuses it instead of fetching the same day again. Requires LEDMatrix core 3.8.1." + }, { "version": "3.19.0", "released": "2026-10-05", diff --git a/plugins/hockey-scoreboard/CHANGELOG.md b/plugins/hockey-scoreboard/CHANGELOG.md index dd85de5b..fe47e2c7 100644 --- a/plugins/hockey-scoreboard/CHANGELOG.md +++ b/plugins/hockey-scoreboard/CHANGELOG.md @@ -1,5 +1,18 @@ # Changelog +## [1.44.0] - 2026-10-05 + +### Changed +- Schedule windows are cached under core's shared ESPN scoreboard key + (espn_scoreboard_cache_key, fetch service stage 2), so every plugin showing + a league shares one fetch and one cached copy. The key this plugin used + before is still read, so an upgrade serves the copy already on disk instead + of refetching every league at once; a window that moves on a day deletes its + copy from the day before. Requires LEDMatrix core 3.8.1. +- The live poll also leaves today's scoreboard under that key (it still asks + ESPN on every poll), so odds-ticker reuses it instead of fetching the same + day again. + ## [1.43.0] - 2026-10-05 ### Changed diff --git a/plugins/hockey-scoreboard/manifest.json b/plugins/hockey-scoreboard/manifest.json index b3992ac0..526f1751 100644 --- a/plugins/hockey-scoreboard/manifest.json +++ b/plugins/hockey-scoreboard/manifest.json @@ -1,7 +1,7 @@ { "id": "hockey-scoreboard", "name": "Hockey Scoreboard", - "version": "1.43.0", + "version": "1.44.0", "author": "ChuckBuilds", "description": "Live, recent, and upcoming hockey games across NHL, NCAA Men's, and NCAA Women's hockey with real-time scores and schedules", "homepage": "https://github.com/ChuckBuilds/ledmatrix-plugins/tree/main/plugins/hockey-scoreboard", @@ -54,6 +54,12 @@ } ], "versions": [ + { + "version": "1.44.0", + "released": "2026-10-05", + "ledmatrix_min_version": "3.8.1", + "notes": "Schedule windows are cached under core's shared ESPN scoreboard key (espn_scoreboard_cache_key, fetch service stage 2), so every plugin showing a league shares one fetch and one cached copy. The key this plugin used before is still read, so an upgrade serves the copy already on disk instead of refetching every league at once; a window that moves on a day deletes its copy from the day before. The live poll also leaves today's scoreboard under that key (it still asks ESPN on every poll), so odds-ticker reuses it instead of fetching the same day again. Requires LEDMatrix core 3.8.1." + }, { "version": "1.43.0", "released": "2026-10-05", diff --git a/plugins/nrl-scoreboard/CHANGELOG.md b/plugins/nrl-scoreboard/CHANGELOG.md index 3642edea..d841bd5f 100644 --- a/plugins/nrl-scoreboard/CHANGELOG.md +++ b/plugins/nrl-scoreboard/CHANGELOG.md @@ -1,5 +1,15 @@ # Changelog +## [1.36.0] - 2026-10-05 + +### Changed +- Schedule windows are cached under core's shared ESPN scoreboard key + (espn_scoreboard_cache_key, fetch service stage 2), so every plugin showing + a league shares one fetch and one cached copy. The key this plugin used + before is still read, so an upgrade serves the copy already on disk instead + of refetching every league at once; a window that moves on a day deletes its + copy from the day before. Requires LEDMatrix core 3.8.1. + ## [1.35.0] - 2026-10-05 ### Changed diff --git a/plugins/nrl-scoreboard/manifest.json b/plugins/nrl-scoreboard/manifest.json index d7c1365e..939e630e 100644 --- a/plugins/nrl-scoreboard/manifest.json +++ b/plugins/nrl-scoreboard/manifest.json @@ -1,7 +1,7 @@ { "id": "nrl-scoreboard", "name": "NRL Scoreboard", - "version": "1.35.0", + "version": "1.36.0", "author": "ChuckBuilds", "description": "Live, recent, and upcoming NRL (National Rugby League) games with real-time scores and game status.", "category": "sports", @@ -18,6 +18,12 @@ "nrl_upcoming" ], "versions": [ + { + "version": "1.36.0", + "released": "2026-10-05", + "ledmatrix_min_version": "3.8.1", + "notes": "Schedule windows are cached under core's shared ESPN scoreboard key (espn_scoreboard_cache_key, fetch service stage 2), so every plugin showing a league shares one fetch and one cached copy. The key this plugin used before is still read, so an upgrade serves the copy already on disk instead of refetching every league at once; a window that moves on a day deletes its copy from the day before. Requires LEDMatrix core 3.8.1." + }, { "version": "1.35.0", "released": "2026-10-05", diff --git a/plugins/odds-ticker/manifest.json b/plugins/odds-ticker/manifest.json index 0c158ee1..721ceb6a 100644 --- a/plugins/odds-ticker/manifest.json +++ b/plugins/odds-ticker/manifest.json @@ -1,7 +1,7 @@ { "id": "odds-ticker", "name": "Odds Ticker", - "version": "1.7.7", + "version": "1.8.0", "description": "Displays scrolling odds and betting lines for upcoming games across multiple sports leagues including NFL, NBA, MLB, NCAA Football, and more", "author": "ChuckBuilds", "category": "sports", @@ -20,6 +20,12 @@ "branch": "main", "plugin_path": "plugins/odds-ticker", "versions": [ + { + "version": "1.8.0", + "released": "2026-10-05", + "ledmatrix_min_version": "3.8.1", + "notes": "The per-day ESPN scoreboards are cached under core's shared key (espn_scoreboard_cache_key, fetch service stage 2), so a scoreboard plugin's copy of the same day serves the ticker and the other way round. The old scoreboard_data_* key is still read until it ages out. The request goes through core's fetch service (counted, merged with identical requests, limit=500 like the scoreboards) on the plugin's identifying session instead of a bare requests.get. A cached day holding a live game is still held to the live interval, now judged by the entry's own age whoever wrote it. Requires LEDMatrix core 3.8.1." + }, { "version": "1.7.7", "released": "2026-10-02", @@ -229,7 +235,7 @@ ], "class_name": "OddsTickerPlugin", "compatible_versions": [ - ">=3.4.0" + ">=3.8.1" ], "entry_point": "manager.py" } diff --git a/plugins/soccer-scoreboard/CHANGELOG.md b/plugins/soccer-scoreboard/CHANGELOG.md index 35564b2d..9320382b 100644 --- a/plugins/soccer-scoreboard/CHANGELOG.md +++ b/plugins/soccer-scoreboard/CHANGELOG.md @@ -1,5 +1,21 @@ # Changelog +## [2.41.0] - 2026-10-05 + +### Changed +- Schedule windows are cached under core's shared ESPN scoreboard key + (espn_scoreboard_cache_key, fetch service stage 2), so every plugin showing + a league shares one fetch and one cached copy. The key this plugin used + before is still read, so an upgrade serves the copy already on disk instead + of refetching every league at once; a window that moves on a day deletes its + copy from the day before. Requires LEDMatrix core 3.8.1. +- The live poll's 30-second cache moves from `_scoreboard_current`, + whatever the dates, to the shared key for the dates it asks, so odds-ticker + and the scoreboard read each other's copy; the old key is read for a + release. +- 2.39.3's single fetch per cache miss and its per-window lock are unchanged; + the lock is now taken on the shared key. + ## [2.40.0] - 2026-10-05 ### Changed diff --git a/plugins/soccer-scoreboard/manifest.json b/plugins/soccer-scoreboard/manifest.json index 282010f2..19a81c2e 100644 --- a/plugins/soccer-scoreboard/manifest.json +++ b/plugins/soccer-scoreboard/manifest.json @@ -1,7 +1,7 @@ { "id": "soccer-scoreboard", "name": "Soccer Scoreboard", - "version": "2.40.0", + "version": "2.41.0", "author": "ChuckBuilds", "description": "Live, recent, and upcoming soccer games across multiple leagues including Premier League, La Liga, Bundesliga, Serie A, Ligue 1, MLS, Liga Portugal, Champions League, Europa League, and FIFA World Cup", "category": "sports", @@ -53,6 +53,12 @@ "soccer_usa.1_upcoming" ], "versions": [ + { + "version": "2.41.0", + "released": "2026-10-05", + "ledmatrix_min_version": "3.8.1", + "notes": "Schedule windows are cached under core's shared ESPN scoreboard key (espn_scoreboard_cache_key, fetch service stage 2), so every plugin showing a league shares one fetch and one cached copy. The key this plugin used before is still read, so an upgrade serves the copy already on disk instead of refetching every league at once; a window that moves on a day deletes its copy from the day before. The live poll's 30-second cache moves from _scoreboard_current, whatever the dates, to the shared key for the dates it asks, so odds-ticker and the scoreboard read each other's copy; the old key is read for a release. Requires LEDMatrix core 3.8.1." + }, { "version": "2.40.0", "released": "2026-10-05",