mirror of
https://github.com/ChuckBuilds/LEDMatrix.git
synced 2026-10-04 06:15:09 +00:00
fix(common): favourite check no longer calls the Europa League finished between matchdays (#670)
On 2026-09-29 ESPN's uefa.europa scoreboard still showed the 17 September matchday, so every event was past. Its calendar is a "list" of rounds (League Phase to 30 Jan 2027, then the knockout rounds to the final), not a match-day whitelist, and the league's season type is a soccer id rather than 2/3, so neither 3.6.1 rule applied and the check said the season had finished. When every event is past, a round in a list calendar that has not started yet now draws no conclusion. Only a round's start date counts: end dates are padded past the last game (AFL's Grand Final round still had a day to run three days after the Grand Final), and rounds in an offseason phase (college football's All-Star week) are skipped. Season end dates are still ignored, so PLL (season to 2027-01-01) stays "finished", as do the World Cup and AFL. Of 28 live ESPN scoreboards only uefa.europa's message changes. Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -19,6 +19,16 @@ accepts both, but the store flags the old spelling as deprecated
|
||||
|
||||
## Unreleased
|
||||
|
||||
### Fixes
|
||||
|
||||
- The favourite-team check no longer says the Europa League season has
|
||||
finished between matchdays. Its scoreboard keeps showing the last matchday,
|
||||
and its calendar is a "list" of rounds rather than match days, so neither
|
||||
3.6.1 rule applied. When every event is past, a round in a list calendar
|
||||
that has not started yet (outside an offseason phase) now draws no
|
||||
conclusion. PLL, the World Cup and AFL, whose seasons are over, are still
|
||||
reported as finished: no round of theirs is still to start.
|
||||
|
||||
## 3.6.1
|
||||
|
||||
A fix to `src.common.favorite_team_check` (#667). Plugins that drop their
|
||||
|
||||
@@ -218,6 +218,8 @@ class FavoriteTeamCheck:
|
||||
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.
|
||||
if cls._later_round_scheduled(payload, now):
|
||||
return None # e.g. Europa League between matchdays.
|
||||
return ("the season has finished and the next one's fixtures are "
|
||||
"not published yet")
|
||||
|
||||
@@ -259,6 +261,44 @@ class FavoriteTeamCheck:
|
||||
known = [t for t in event_types if isinstance(t, int)]
|
||||
return bool(known) and all(t < league_type for t in known)
|
||||
|
||||
@classmethod
|
||||
def _later_round_scheduled(cls, payload, now: datetime) -> bool:
|
||||
"""
|
||||
Whether a "list" calendar has a round that has not started yet.
|
||||
|
||||
Competitions with a list calendar (the UEFA club competitions, the
|
||||
World Cup, AFL, NFL) give each phase its rounds as ``entries`` with
|
||||
start and end dates. Between matchdays the Europa League scoreboard
|
||||
keeps showing the last one: on 2026-09-29 every event was from 17
|
||||
September, the next matchday was only days away, and the rounds from
|
||||
the knockout play-offs to the final were all still to come. A round
|
||||
that starts later means the season is not over, even though the
|
||||
date of the next fixture is not known.
|
||||
|
||||
Only a round's *start* counts. End dates are padded well past the
|
||||
last game -- the World Cup's final round ran to 1 August for a 19 July
|
||||
final -- so a future end date is also true of a finished season.
|
||||
Rounds in an offseason phase (the college football All-Star week)
|
||||
are not games for the favourites and do not count either.
|
||||
"""
|
||||
league = (payload.get('leagues') or [{}])[0] or {}
|
||||
for phase in league.get('calendar') or []:
|
||||
if not isinstance(phase, dict) or cls._is_offseason(phase.get('label')):
|
||||
continue
|
||||
for entry in phase.get('entries') or []:
|
||||
if not isinstance(entry, dict) or cls._is_offseason(entry.get('label')):
|
||||
continue
|
||||
start = cls._parse_date(entry.get('startDate'))
|
||||
if start and start > now:
|
||||
return True
|
||||
return False
|
||||
|
||||
@staticmethod
|
||||
def _is_offseason(label) -> bool:
|
||||
"""'Off Season', 'Offseason', 'Off-season' ..."""
|
||||
return isinstance(label, str) and 'offseason' in re.sub(
|
||||
r'[^a-z]', '', label.lower())
|
||||
|
||||
@staticmethod
|
||||
def _parse_date(raw) -> Optional[datetime]:
|
||||
if not raw or not isinstance(raw, str):
|
||||
|
||||
@@ -439,3 +439,101 @@ class ScheduleNoteMatchdayTests(unittest.TestCase):
|
||||
# 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)
|
||||
|
||||
|
||||
class ScheduleNoteListCalendarTests(unittest.TestCase):
|
||||
"""A round still to start in a "list" calendar is not a finished season.
|
||||
|
||||
Shapes captured from ESPN on 2026-09-29, with dates kept relative to that
|
||||
day. The Europa League scoreboard still showed the 17 September matchday
|
||||
and its calendar is a ``"list"`` of rounds, not match days, so the check
|
||||
said the season had finished -- with the knockout rounds, and the next
|
||||
league-phase matchday, still to come. PLL, the World Cup and AFL really had
|
||||
finished and must still say so, although each has a season or round
|
||||
``endDate`` in the future.
|
||||
"""
|
||||
|
||||
note = ScheduleNoteTests.note
|
||||
|
||||
@staticmethod
|
||||
def iso(days):
|
||||
return (datetime.now(timezone.utc) + timedelta(days=days)).strftime(
|
||||
"%Y-%m-%dT%H:%MZ")
|
||||
|
||||
@classmethod
|
||||
def list_league(cls, event_days, rounds, league_type=14540,
|
||||
event_type=14540, phase_label="UEFA Europa League",
|
||||
extra_phases=()):
|
||||
"""``rounds`` is ``[(label, start_day, end_day), ...]`` for one phase."""
|
||||
return {
|
||||
"events": [{"date": cls.iso(d), "season": {"type": event_type}}
|
||||
for d in event_days],
|
||||
"leagues": [{
|
||||
"season": {"type": {"type": league_type}},
|
||||
"calendarType": "list",
|
||||
"calendarIsWhitelist": True,
|
||||
"calendar": [{
|
||||
"label": phase_label,
|
||||
"startDate": cls.iso(-90), "endDate": cls.iso(275),
|
||||
"entries": [{"label": label, "startDate": cls.iso(start),
|
||||
"endDate": cls.iso(end)}
|
||||
for label, start, end in rounds],
|
||||
}] + list(extra_phases),
|
||||
}],
|
||||
}
|
||||
|
||||
def test_europa_between_matchdays_is_not_finished(self):
|
||||
note = self.note(self.list_league([-12], [
|
||||
("League Phase", -31, 123),
|
||||
("Knockout Round Playoffs", 123, 151),
|
||||
("Rd of 16", 151, 172),
|
||||
("Quarterfinals", 172, 200),
|
||||
("Semifinals", 200, 221),
|
||||
("Final", 222, 275),
|
||||
]))
|
||||
self.assertIsNone(note)
|
||||
|
||||
def test_world_cup_after_the_final_is_still_finished(self):
|
||||
# The competition runs to 31 December, and the last round ended 12
|
||||
# days after the final; no round is still to start.
|
||||
note = self.note(self.list_league([-72], [
|
||||
("Group", -110, -93),
|
||||
("Semifinals", -77, -72),
|
||||
("Final", -72, -59),
|
||||
], league_type=13803, event_type=13803, phase_label="FIFA World Cup"))
|
||||
self.assertIn("season has finished", note)
|
||||
|
||||
def test_afl_after_the_grand_final_is_still_finished(self):
|
||||
# The Grand Final round had started but had not ended yet.
|
||||
note = self.note(self.list_league([-3], [
|
||||
("Preliminary Finals", -13, -6),
|
||||
("Grand Final", -6, 1),
|
||||
], league_type=3, event_type=3, phase_label="Postseason"))
|
||||
self.assertIn("season has finished", note)
|
||||
|
||||
def test_an_offseason_round_does_not_count(self):
|
||||
# College football's "Off Season" phase holds the All-Star week.
|
||||
offseason = {"label": "Off Season", "startDate": self.iso(2),
|
||||
"endDate": self.iso(6),
|
||||
"entries": [{"label": "All-Star", "startDate": self.iso(2),
|
||||
"endDate": self.iso(6)}]}
|
||||
note = self.note(self.list_league(
|
||||
[-3], [("CFP", -40, 1)], league_type=3, event_type=3,
|
||||
phase_label="Postseason", extra_phases=[offseason]))
|
||||
self.assertIn("season has finished", note)
|
||||
|
||||
def test_pll_with_a_season_end_date_in_the_future_is_still_finished(self):
|
||||
# A "day" whitelist whose last match day is past; the season's own
|
||||
# endDate (1 January) is ignored.
|
||||
note = self.note({
|
||||
"events": [{"date": self.iso(-9), "season": {"type": 2}}],
|
||||
"leagues": [{
|
||||
"season": {"type": {"type": 2}, "startDate": self.iso(-271),
|
||||
"endDate": self.iso(94)},
|
||||
"calendarType": "day",
|
||||
"calendarIsWhitelist": True,
|
||||
"calendarEndDate": self.iso(94),
|
||||
"calendar": [self.iso(-30), self.iso(-22), self.iso(-9)],
|
||||
}],
|
||||
})
|
||||
self.assertIn("season has finished", note)
|
||||
|
||||
Reference in New Issue
Block a user