mirror of
https://github.com/ChuckBuilds/LEDMatrix.git
synced 2026-10-04 06:15:09 +00:00
fix(common): favourite check no longer calls a started postseason a finished season (#667)
* fix(common): favourite check no longer calls a started postseason a finished season The day after a regular season ends, ESPN's default scoreboard still returns that last regular-season day, while leagues[0].season has moved to Postseason. All events were in the past, so the check logged "the season has finished" for MLB on 2026-09-29 while the upcoming manager in the same process was showing TB's wild-card games. When every event is past and the league is in a later in-season phase (regular season or postseason) than all of the returned events, draw no conclusion. The offseason is excluded, so a genuinely finished season is still reported as finished. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(common): favourite check reports the next matchday between soccer rounds Between matchdays ESPN's soccer scoreboard keeps showing the last one, so every event is in the past and in the league's current phase, which the postseason rule does not cover; the check said the Premier League season had finished on 2026-09-29 (last games 20 September, next 10 October). When the league calendar is a "day" whitelist, its entries are days with games, so a future one is used as the next fixture. MLB's day calendar is a blacklist and is not read that way; PLL's whitelist has no future days and is still reported as finished. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -199,13 +199,25 @@ class FavoriteTeamCheck:
|
||||
# has no events at all to roll forward to: events that exist but are
|
||||
# all in the past mean the season is over, and an offseason calendar
|
||||
# phase must not be dressed up as its next game.
|
||||
#
|
||||
# The exception is a calendar of match days. With calendarType "day"
|
||||
# and calendarIsWhitelist true, every entry is a day that has games,
|
||||
# so a future entry is a real next fixture. Soccer needs it: between
|
||||
# matchdays the scoreboard keeps showing the last one, so on
|
||||
# 2026-09-29 every Premier League event was from 20 September and the
|
||||
# next games (10 October) were only in the calendar. A day calendar
|
||||
# that is not a whitelist (MLB's) lists days *without* games.
|
||||
if any(event_dates):
|
||||
upcoming = future(event_dates)
|
||||
if not upcoming and cls._calendar_is_match_days(payload):
|
||||
upcoming = future(calendar_dates)
|
||||
else:
|
||||
upcoming = future(calendar_dates)
|
||||
if not upcoming:
|
||||
if not any(event_dates) and not any(calendar_dates):
|
||||
return None # Nothing published either way; draw no conclusion.
|
||||
if cls._moved_to_later_phase(payload):
|
||||
return None # e.g. postseason under way; see the method.
|
||||
return ("the season has finished and the next one's fixtures are "
|
||||
"not published yet")
|
||||
|
||||
@@ -215,6 +227,38 @@ class FavoriteTeamCheck:
|
||||
return "the league has nothing on until {}".format(
|
||||
upcoming[0].strftime('%d %B %Y'))
|
||||
|
||||
@staticmethod
|
||||
def _calendar_is_match_days(payload) -> bool:
|
||||
"""Whether the league calendar lists the days that have games."""
|
||||
league = (payload.get('leagues') or [{}])[0] or {}
|
||||
return (league.get('calendarType') == 'day'
|
||||
and league.get('calendarIsWhitelist') is True)
|
||||
|
||||
@staticmethod
|
||||
def _moved_to_later_phase(payload) -> bool:
|
||||
"""
|
||||
Whether the league is in a later in-season phase than its events.
|
||||
|
||||
ESPN does not roll the scoreboard forward into a postseason. The day
|
||||
after MLB's regular season ended, the default scoreboard still returned
|
||||
that last regular-season day, while ``leagues[0].season`` already said
|
||||
Postseason and the wild-card games were two days out. Past events alone
|
||||
then read as a finished season while the same process's upcoming
|
||||
manager was showing the favourite's playoff games.
|
||||
|
||||
Only regular season (2) and postseason (3) count as "later". The
|
||||
offseason (4) follows the postseason too, and there past events really
|
||||
do mean the season is over.
|
||||
"""
|
||||
season = ((payload.get('leagues') or [{}])[0] or {}).get('season') or {}
|
||||
league_type = (season.get('type') or {}).get('type')
|
||||
if league_type not in (2, 3):
|
||||
return False
|
||||
event_types = [(e.get('season') or {}).get('type')
|
||||
for e in payload.get('events') or []]
|
||||
known = [t for t in event_types if isinstance(t, int)]
|
||||
return bool(known) and all(t < league_type for t in known)
|
||||
|
||||
@staticmethod
|
||||
def _parse_date(raw) -> Optional[datetime]:
|
||||
if not raw or not isinstance(raw, str):
|
||||
|
||||
@@ -353,3 +353,89 @@ class ScheduleNoteUsesGameDatesTests(unittest.TestCase):
|
||||
"events": [{"date": self.iso(base, 1)}],
|
||||
"leagues": [{"calendar": [{"startDate": self.iso(base, 6)}]}],
|
||||
}))
|
||||
|
||||
|
||||
class ScheduleNotePostseasonTests(unittest.TestCase):
|
||||
"""Past regular-season events are not a finished season once the postseason starts.
|
||||
|
||||
Captured from ESPN's MLB scoreboard on 2026-09-29, two days after the
|
||||
regular season ended: the default scoreboard still returned the last
|
||||
regular-season day (event ``season.type`` 2), while ``leagues[0].season``
|
||||
had already moved to Postseason (type 3). The check told a Rays fan the
|
||||
season was over while their upcoming manager listed TB's wild-card games.
|
||||
"""
|
||||
|
||||
note = ScheduleNoteTests.note
|
||||
|
||||
@staticmethod
|
||||
def payload(league_type, event_type, days=-2):
|
||||
date = (datetime.now(timezone.utc) + timedelta(days=days)).isoformat()
|
||||
return {
|
||||
"events": [{"date": date, "season": {"year": 2026, "type": event_type}}],
|
||||
"leagues": [{"season": {"year": 2026, "type": {"type": league_type}},
|
||||
"calendar": [date]}],
|
||||
}
|
||||
|
||||
def test_postseason_after_regular_season_events_is_not_finished(self):
|
||||
self.assertIsNone(self.note(self.payload(league_type=3, event_type=2)))
|
||||
|
||||
def test_a_finished_postseason_is_still_reported_as_finished(self):
|
||||
# AFL the week after its grand final: league and events both postseason.
|
||||
note = self.note(self.payload(league_type=3, event_type=3, days=-4))
|
||||
self.assertIn("season has finished", note)
|
||||
|
||||
def test_offseason_after_the_postseason_is_still_finished(self):
|
||||
note = self.note(self.payload(league_type=4, event_type=3, days=-40))
|
||||
self.assertIn("season has finished", note)
|
||||
|
||||
|
||||
class ScheduleNoteMatchdayTests(unittest.TestCase):
|
||||
"""Between soccer matchdays the next fixture is only in the calendar.
|
||||
|
||||
Captured from ESPN's Premier League scoreboard on 2026-09-29: the
|
||||
scoreboard still showed the 20 September matchday, the league was in the
|
||||
same season phase as those events, and the next games (10 October) were
|
||||
only in ``leagues[0].calendar`` -- a ``"day"`` calendar with
|
||||
``calendarIsWhitelist`` true, i.e. the days that have games. The check
|
||||
said the season had finished.
|
||||
"""
|
||||
|
||||
note = ScheduleNoteTests.note
|
||||
|
||||
@staticmethod
|
||||
def payload(event_days, calendar_days, whitelist=True):
|
||||
from datetime import datetime, timedelta, timezone
|
||||
now = datetime.now(timezone.utc)
|
||||
|
||||
def iso(days):
|
||||
return (now + timedelta(days=days)).strftime("%Y-%m-%dT%H:%MZ")
|
||||
|
||||
return {
|
||||
"events": [{"date": iso(d), "season": {"type": 14308}}
|
||||
for d in event_days],
|
||||
"leagues": [{"season": {"type": {"type": 14308}},
|
||||
"calendarType": "day",
|
||||
"calendarIsWhitelist": whitelist,
|
||||
"calendar": [iso(d) for d in calendar_days]}],
|
||||
}
|
||||
|
||||
def test_gap_between_matchdays_reports_the_next_matchday(self):
|
||||
from datetime import datetime, timedelta, timezone
|
||||
note = self.note(self.payload([-9], [-9, 11, 12]))
|
||||
self.assertIn("nothing on until", note)
|
||||
self.assertIn((datetime.now(timezone.utc) + timedelta(days=11))
|
||||
.strftime("%d %B %Y"), note)
|
||||
|
||||
def test_next_matchday_within_a_couple_of_days_says_nothing(self):
|
||||
self.assertIsNone(self.note(self.payload([-2], [-2, 1])))
|
||||
|
||||
def test_no_matchdays_left_is_still_finished(self):
|
||||
# PLL on the same day: a match-day calendar whose last day is past.
|
||||
note = self.note(self.payload([-9], [-30, -9]))
|
||||
self.assertIn("season has finished", note)
|
||||
|
||||
def test_a_blacklist_day_calendar_is_not_read_as_fixtures(self):
|
||||
# MLB's day calendar has calendarIsWhitelist false: it lists days
|
||||
# without games, so a future entry there is not a next fixture.
|
||||
note = self.note(self.payload([-9], [11], whitelist=False))
|
||||
self.assertIn("season has finished", note)
|
||||
|
||||
Reference in New Issue
Block a user