mirror of
https://github.com/ChuckBuilds/LEDMatrix.git
synced 2026-10-04 06:15:09 +00:00
fix(sports): recover from ESPN rejecting scoreboard date ranges (#591)
* fix(sports): recover from ESPN rejecting scoreboard date ranges Since 2026-09-15 ESPN's site API answers `dates=YYYYMMDD-YYYYMMDD` with 400 "Failed to get events endpoint." for every sport. Single days, months (`YYYYMM`) and season years still work. Every season and weeks-window fetch in core failed, including the background service the scoreboards submit their season schedules to. src/common/espn_dates.py re-asks a rejected range as whole-month chunks plus the leftover edge days, which tile the window exactly (a season is 8 requests, not 213). A month that comes back with exactly 500 events is truncated (college baseball's March) and is re-asked day by day. It also clamps `limit` to 500: above that ESPN truncates silently, e.g. college football returns 25 of 68 games for one Saturday at limit=1000. BackgroundDataService recovers rejected ranges on the worker thread and advertises `handles_espn_date_ranges` so plugins can tell whether to hand it a range. SportsCore, sports_shared, ESPNDataSource and APIHelper route through the helper or the clamped limit. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * docs(changelog): ESPN date-range fallback and limit clamp Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(sports): stop re-sending ESPN date ranges once one is rejected Live scoreboards refresh every 30 seconds, and each refresh sent the range first, got the 400, then fetched the chunks: three requests where one used to do. After a rejection, ranges now go straight to chunks for six hours, then the range is tried again so the workaround retires itself if ESPN reverts. A single-day 400 does not set the memo, and when every chunk fails the range request supplies the error without the chunks being fetched a second time. Per-fetch chunk logging drops to debug; the rejection itself stays a warning. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(sports): clamp limit only on ESPN scoreboard submissions The background service is generic, and limit above 500 only truncates scoreboards. /teams needs limit=1000 (college football has 762 teams and limit=500 returns 500), so a teams submission must keep its limit. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -25,6 +25,7 @@ from enum import Enum
|
||||
import queue
|
||||
from concurrent.futures import ThreadPoolExecutor
|
||||
from src.cache_manager import CacheManager
|
||||
from src.common.espn_dates import clamp_espn_limit, fetch_espn_date_chunks
|
||||
# Configure logging
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -98,7 +99,12 @@ class BackgroundDataService:
|
||||
This service manages a pool of background threads to fetch data asynchronously,
|
||||
with intelligent caching, retry logic, and progress tracking.
|
||||
"""
|
||||
|
||||
|
||||
# Plugins feature-detect this. A core without it sends season ranges to
|
||||
# ESPN as-is and gets 400s since 2026-09-15, so plugins fetch those
|
||||
# ranges themselves instead of submitting them here.
|
||||
handles_espn_date_ranges = True
|
||||
|
||||
def __init__(self, cache_manager: CacheManager, max_workers: int = 3, request_timeout: int = 30):
|
||||
"""
|
||||
Initialize the background data service.
|
||||
@@ -247,6 +253,12 @@ class BackgroundDataService:
|
||||
logger.debug(f"Cache hit for {sport} {year} data")
|
||||
return request_id
|
||||
|
||||
# limit above 500 makes an ESPN *scoreboard* return a truncated list
|
||||
# (src/common/espn_dates.py). Other endpoints need more: /teams has 762
|
||||
# college-football teams, so only scoreboards are clamped.
|
||||
if url.split('?', 1)[0].rstrip('/').endswith('/scoreboard'):
|
||||
params = clamp_espn_limit(params)
|
||||
|
||||
# Create fetch request
|
||||
request = FetchRequest(
|
||||
id=request_id,
|
||||
@@ -254,7 +266,7 @@ class BackgroundDataService:
|
||||
year=year,
|
||||
cache_key=cache_key,
|
||||
url=url,
|
||||
params=params or {},
|
||||
params=dict(params or {}),
|
||||
headers={**self.default_headers, **(headers or {})},
|
||||
timeout=timeout or self.request_timeout,
|
||||
max_retries=max_retries,
|
||||
@@ -340,10 +352,17 @@ class BackgroundDataService:
|
||||
|
||||
# Perform HTTP request with retry logic
|
||||
response = self._make_request_with_retry(request)
|
||||
response.raise_for_status()
|
||||
|
||||
# Parse response
|
||||
data = response.json()
|
||||
|
||||
# ESPN stopped accepting dates=YYYYMMDD-YYYYMMDD on 2026-09-15 and
|
||||
# answers 400 for every sport. Re-ask in months and days rather
|
||||
# than let a whole season fail. See src/common/espn_dates.py.
|
||||
if response.status_code == 400:
|
||||
data = self._fetch_in_date_chunks(request)
|
||||
if data is None:
|
||||
response.raise_for_status()
|
||||
else:
|
||||
response.raise_for_status()
|
||||
data = response.json()
|
||||
|
||||
# Validate data structure
|
||||
if not isinstance(data, dict):
|
||||
@@ -519,6 +538,23 @@ class BackgroundDataService:
|
||||
"""
|
||||
result.data = None
|
||||
|
||||
def _fetch_in_date_chunks(self, request: FetchRequest) -> Optional[Dict[str, Any]]:
|
||||
"""Re-fetch a rejected ``YYYYMMDD-YYYYMMDD`` range as month/day chunks.
|
||||
|
||||
None means the request was not a day range, or every chunk failed; the
|
||||
caller then re-raises the original 400 instead of caching an empty
|
||||
season. See src/common/espn_dates.py.
|
||||
"""
|
||||
logger.info("Recovering %s %s from a rejected date range", request.sport, request.year)
|
||||
return fetch_espn_date_chunks(
|
||||
self.session,
|
||||
request.url,
|
||||
params=request.params,
|
||||
headers=request.headers,
|
||||
timeout=request.timeout,
|
||||
logger=logger,
|
||||
)
|
||||
|
||||
def _make_request_with_retry(self, request: FetchRequest) -> requests.Response:
|
||||
"""
|
||||
Make HTTP request with retry logic and exponential backoff.
|
||||
|
||||
Reference in New Issue
Block a user