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
10 changes: 4 additions & 6 deletions plugins.json
Original file line number Diff line number Diff line change
Expand Up @@ -80,9 +80,8 @@
"last_updated": "2026-10-05",
"verified": true,
"screenshot": "",
"latest_version": "1.57.3",
"ledmatrix_min_version": "3.8.0",
"commit": "8db9d3ebe374dc4155906ad29f6f5baa5536fe1c"
"latest_version": "1.57.4",
"ledmatrix_min_version": "3.8.0"
},
{
"id": "basketball-scoreboard",
Expand Down Expand Up @@ -385,10 +384,9 @@
"last_updated": "2026-10-05",
"verified": true,
"screenshot": "",
"latest_version": "1.42.4",
"latest_version": "1.42.5",
"icon": "fas fa-hockey-puck",
"ledmatrix_min_version": "3.8.0",
"commit": "8db9d3ebe374dc4155906ad29f6f5baa5536fe1c"
"ledmatrix_min_version": "3.8.0"
},
{
"id": "lacrosse-scoreboard",
Expand Down
11 changes: 11 additions & 0 deletions plugins/baseball-scoreboard/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,16 @@
# Changelog

## [1.57.4] - 2026-10-05

### Fixed
- A win celebration can take the live-priority slot. A win fires as the
game goes final, so the game has already left the live list:
`has_live_content()` reported the celebration but `get_live_modes()`
returned no mode for it. `get_live_modes()` now returns a celebrating
league's live mode (MLB, MiLB and NCAA baseball), once, and only for a
league that is enabled with `live_priority` on; `has_live_content()`
applies the same `live_priority` gate.

## [1.57.3] - 2026-10-05

### Changed
Expand Down
40 changes: 36 additions & 4 deletions plugins/baseball-scoreboard/manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -2296,14 +2296,21 @@ def display(self, display_mode: str = None, force_clear: bool = False) -> bool:
self.logger.error(f"Error in display method: {e}")
return False

def _get_active_celebration_manager(self):
def _get_active_celebration_manager(self, live_priority_only=False):
"""Return the (league_key, live_manager) of an enabled league whose live
manager currently has a celebration running, else None."""
manager currently has a celebration running, else None.

``live_priority_only`` also skips leagues with live priority off -- the
same gate get_live_modes() applies, so has_live_content() never reports
a celebration that no live mode can show.
"""
if not self.is_enabled:
return None
for league_key, league_data in self._league_registry.items():
if not league_data.get("enabled", False):
continue
if live_priority_only and not league_data.get("live_priority", True):
continue
live_manager = self._get_league_manager_for_mode(league_key, "live")
if live_manager is not None and getattr(
live_manager, "has_active_celebration", lambda: False)():
Expand Down Expand Up @@ -2386,7 +2393,7 @@ def has_live_content(self) -> bool:

# A running celebration (notably a win, whose game has already left
# the live list) keeps the live mode on screen.
if self._get_active_celebration_manager() is not None:
if self._get_active_celebration_manager(live_priority_only=True) is not None:
return True

# Live game counts per league, folded into the single throttled summary
Expand Down Expand Up @@ -2688,6 +2695,14 @@ def get_live_modes(self) -> list:
and self.mlb_live_priority
and hasattr(self, "mlb_live")
):
# A celebrating league must be selectable even if its live list is
# already empty (a win fires as the game goes final).
if (
hasattr(self.mlb_live, "has_active_celebration")
and self.mlb_live.has_active_celebration()
):
live_modes.append("mlb_live")

live_games = getattr(self.mlb_live, "live_games", [])
if live_games:
# Filter out any games that are final or appear over
Expand Down Expand Up @@ -2716,6 +2731,14 @@ def get_live_modes(self) -> list:
and self.milb_live_priority
and hasattr(self, "milb_live")
):
# A celebrating league must be selectable even if its live list is
# already empty (a win fires as the game goes final).
if (
hasattr(self.milb_live, "has_active_celebration")
and self.milb_live.has_active_celebration()
):
live_modes.append("milb_live")

live_games = getattr(self.milb_live, "live_games", [])
if live_games:
live_games = [g for g in live_games if not g.get("is_final", False)]
Expand All @@ -2740,6 +2763,14 @@ def get_live_modes(self) -> list:
and self.ncaa_baseball_live_priority
and hasattr(self, "ncaa_baseball_live")
):
# A celebrating league must be selectable even if its live list is
# already empty (a win fires as the game goes final).
if (
hasattr(self.ncaa_baseball_live, "has_active_celebration")
and self.ncaa_baseball_live.has_active_celebration()
):
live_modes.append("ncaa_baseball_live")

live_games = getattr(self.ncaa_baseball_live, "live_games", [])
if live_games:
live_games = [g for g in live_games if not g.get("is_final", False)]
Expand All @@ -2758,7 +2789,8 @@ def get_live_modes(self) -> list:
else:
live_modes.append("ncaa_baseball_live")

return live_modes
# A celebration and live games for the same league can both append it.
return list(dict.fromkeys(live_modes))

def _get_game_duration(self, league: str, mode_type: str, manager=None) -> float:
"""Get game duration for a league and mode type combination.
Expand Down
8 changes: 7 additions & 1 deletion 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.57.3",
"version": "1.57.4",
"update_interval": 60,
"author": "ChuckBuilds",
"description": "Live, recent, and upcoming baseball games across MLB, MiLB, and NCAA Baseball with real-time scores and schedules",
Expand Down Expand Up @@ -31,6 +31,12 @@
"branch": "main",
"plugin_path": "plugins/baseball-scoreboard",
"versions": [
{
"version": "1.57.4",
"released": "2026-10-05",
"ledmatrix_min_version": "3.8.0",
"notes": "A win celebration can take the live-priority slot. A win fires as the game goes final, so the game has already left the live list; has_live_content() reported the celebration but get_live_modes() returned no mode for it. get_live_modes() now returns a celebrating league's live mode (MLB, MiLB and NCAA baseball), once, and only for a league that is enabled with live_priority on; has_live_content() applies the same live_priority gate."
},
{
"version": "1.57.3",
"released": "2026-10-05",
Expand Down
127 changes: 127 additions & 0 deletions plugins/baseball-scoreboard/test_celebration_is_a_live_mode.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
#!/usr/bin/env python3
"""
A win celebration must be selectable as a live mode.

A win fires as the game goes final, so by the time it is on screen the game has
already left the league's live list. has_live_content() said True (a celebration
is running) while get_live_modes() -- which only looked at live games -- said [],
so the display controller saw live content with no live mode of this league to
show for it. Football carried the fix; baseball did not.

Covers, for every league:
1. A celebration with an empty live list -> get_live_modes() returns that
league's live mode, and has_live_content() agrees.
2. A celebration and a live game in the same league -> the mode appears once.
3. A disabled league, or one with live priority off -> neither method reports
the celebration.

Run: <core-venv>/bin/python plugins/baseball-scoreboard/test_celebration_is_a_live_mode.py
"""

import os
import sys

import pytest

PLUGIN_DIR = os.path.dirname(os.path.abspath(__file__))
if PLUGIN_DIR not in sys.path:
sys.path.insert(0, PLUGIN_DIR)

from manager import BaseballScoreboardPlugin # noqa: E402

LEAGUES = ("mlb", "milb", "ncaa_baseball")


class _QuietLogger:
def _drop(self, *a, **k):
pass

debug = info = warning = error = _drop


class _Live:
"""Stands in for a league's live manager. Has no _is_game_really_over, so
both methods skip that filter."""

def __init__(self, games=(), celebrating=False):
self.live_games = list(games)
self.favorite_teams = []
self.celebrating = celebrating

def has_active_celebration(self):
return self.celebrating


class _Stub:
"""Carries just the attributes the two methods read, with the real methods
bound to it -- the full constructor needs a display manager."""

get_live_modes = BaseballScoreboardPlugin.get_live_modes
has_live_content = BaseballScoreboardPlugin.has_live_content
_get_active_celebration_manager = BaseballScoreboardPlugin._get_active_celebration_manager

def __init__(self, enabled=True, live_priority=True):
self.logger = _QuietLogger()
self.is_enabled = True
self._league_registry = {}
for league in LEAGUES:
setattr(self, f"{league}_enabled", enabled)
setattr(self, f"{league}_live_priority", live_priority)
setattr(self, f"{league}_live", _Live())
self._league_registry[league] = {
"enabled": enabled, "live_priority": live_priority}
self._last_live_content_log = 0.0
self._last_live_content_state = None
self._live_content_log_interval = 60.0

def _get_league_manager_for_mode(self, league, mode):
return getattr(self, f"{league}_live") if mode == "live" else None


def _game():
return {"home_abbr": "ATL", "away_abbr": "PHI", "is_final": False, "is_live": True}


@pytest.mark.parametrize("league", LEAGUES)
def test_celebration_with_empty_live_list_is_a_live_mode(league):
stub = _Stub()
getattr(stub, f"{league}_live").celebrating = True

assert stub.get_live_modes() == [f"{league}_live"]
assert stub.has_live_content() is True


@pytest.mark.parametrize("league", LEAGUES)
def test_celebration_and_live_game_list_the_mode_once(league):
stub = _Stub()
live = getattr(stub, f"{league}_live")
live.celebrating = True
live.live_games.append(_game())

assert stub.get_live_modes() == [f"{league}_live"]
assert stub.has_live_content() is True


@pytest.mark.parametrize("league", LEAGUES)
@pytest.mark.parametrize("gate", ("enabled", "live_priority"))
def test_gated_off_league_does_not_report_its_celebration(league, gate):
stub = _Stub(**{gate: False})
getattr(stub, f"{league}_live").celebrating = True

assert stub.get_live_modes() == []
assert stub.has_live_content() is False


def test_only_the_celebrating_league_is_returned():
stub = _Stub()
stub.mlb_live.live_games.append(_game())
stub.ncaa_baseball_live.celebrating = True

assert stub.get_live_modes() == ["mlb_live", "ncaa_baseball_live"]


def test_no_celebration_and_no_games_is_not_live():
stub = _Stub()

assert stub.get_live_modes() == []
assert stub.has_live_content() is False
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ class _Stub:

has_live_content = BaseballScoreboardPlugin.has_live_content

def _get_active_celebration_manager(self):
def _get_active_celebration_manager(self, live_priority_only=False):
return None # no celebration is running in this stub

def __init__(self, mlb_games=(), milb_games=(), ncaa_games=()):
Expand Down
11 changes: 11 additions & 0 deletions plugins/hockey-scoreboard/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,16 @@
# Changelog

## [1.42.5] - 2026-10-05

### Fixed
- A win celebration can take the live-priority slot. A win fires as the
game goes final, so the game has already left the live list:
`has_live_content()` reported the celebration but `get_live_modes()`
returned no mode for it. `get_live_modes()` now returns a celebrating
league's live mode (NHL and NCAA men's and women's hockey), once, and only for a
league that is enabled with `live_priority` on; `has_live_content()`
applies the same `live_priority` gate.

## [1.42.4] - 2026-10-05

### Fixed
Expand Down
40 changes: 36 additions & 4 deletions plugins/hockey-scoreboard/manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -2147,14 +2147,21 @@ def get_dynamic_duration_cap(self) -> Optional[float]:
# No global fallback - return None
return None

def _get_active_celebration_manager(self):
def _get_active_celebration_manager(self, live_priority_only=False):
"""Return the (league_key, live_manager) of an enabled league whose live
manager currently has an active goal/win celebration, else None."""
manager currently has an active goal/win celebration, else None.

``live_priority_only`` also skips leagues with live priority off -- the
same gate get_live_modes() applies, so has_live_content() never reports
a celebration that no live mode can show.
"""
if not self.is_enabled:
return None
for league_key, league_data in self._league_registry.items():
if not league_data.get("enabled", False):
continue
if live_priority_only and not league_data.get("live_priority", True):
continue
live_manager = self._get_league_manager_for_mode(league_key, "live")
if (
live_manager
Expand Down Expand Up @@ -2240,7 +2247,7 @@ def has_live_content(self) -> bool:

# An active celebration (notably a win, whose game has already left the
# live list) must keep the live mode on screen.
if self._get_active_celebration_manager() is not None:
if self._get_active_celebration_manager(live_priority_only=True) is not None:
return True

# Check NHL live content
Expand Down Expand Up @@ -2376,6 +2383,14 @@ def get_live_modes(self) -> list:
and self.nhl_live_priority
and hasattr(self, "nhl_live")
):
# A celebrating league must be selectable even if its live list is
# already empty (a win fires as the game goes final).
if (
hasattr(self.nhl_live, "has_active_celebration")
and self.nhl_live.has_active_celebration()
):
live_modes.append("nhl_live")

live_games = getattr(self.nhl_live, "live_games", [])
if live_games:
# Filter out any games that are final or appear over
Expand Down Expand Up @@ -2405,6 +2420,14 @@ def get_live_modes(self) -> list:
and self.ncaa_mens_live_priority
and hasattr(self, "ncaa_mens_live")
):
# A celebrating league must be selectable even if its live list is
# already empty (a win fires as the game goes final).
if (
hasattr(self.ncaa_mens_live, "has_active_celebration")
and self.ncaa_mens_live.has_active_celebration()
):
live_modes.append("ncaa_mens_live")

live_games = getattr(self.ncaa_mens_live, "live_games", [])
if live_games:
# Filter out any games that are final or appear over
Expand Down Expand Up @@ -2434,6 +2457,14 @@ def get_live_modes(self) -> list:
and self.ncaa_womens_live_priority
and hasattr(self, "ncaa_womens_live")
):
# A celebrating league must be selectable even if its live list is
# already empty (a win fires as the game goes final).
if (
hasattr(self.ncaa_womens_live, "has_active_celebration")
and self.ncaa_womens_live.has_active_celebration()
):
live_modes.append("ncaa_womens_live")

live_games = getattr(self.ncaa_womens_live, "live_games", [])
if live_games:
# Filter out any games that are final or appear over
Expand All @@ -2457,7 +2488,8 @@ def get_live_modes(self) -> list:
# No favorite teams configured, include if any live games exist
live_modes.append("ncaa_womens_live")

return live_modes
# A celebration and live games for the same league can both append it.
return list(dict.fromkeys(live_modes))

def _has_any_scroll_mode(self) -> bool:
"""Return True if any enabled league uses scroll display for any mode.
Expand Down
Loading
Loading