Skip to content
Open
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
2 changes: 1 addition & 1 deletion plugins.json
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@
"last_updated": "2026-07-17",
"verified": true,
"screenshot": "",
"latest_version": "1.19.3"
"latest_version": "1.20.1"
},
{
"id": "basketball-scoreboard",
Expand Down
27 changes: 27 additions & 0 deletions plugins/baseball-scoreboard/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,32 @@
# Changelog

## [1.20.1] - 2026-07-28

### Fixed
- **Vegas scroll showed only one game**: `get_vegas_content()` returned the union
of every scroll display's cached items, so once the standalone rotation had
rendered a mode, Vegas inherited that mode's games — with a single live game in
progress the whole ticker entry collapsed to one card. It now reads a dedicated
combined slate (live + recent + upcoming, across every enabled league) that
cannot be clobbered by the standalone displays, and a game held by two displays
is no longer shown twice.
- **Vegas content stalled the scroll**: building the Vegas slate called `update()`,
putting network I/O on the render path and freezing the ticker for seconds. The
slate is now rendered from whatever data the plugin already has, and is rebuilt
only when the games actually change (fingerprinted on scores, inning, count and
game set) instead of on every fetch.

- **Vegas build hijacked the standalone scroll**: rendering the Vegas slate went
through `prepare_and_display()`, which also repoints the manager's active
scroll display, so the next standalone frame rendered the Vegas slate instead
of the game type the rotation was showing. Vegas now uses a new
`prepare_content()` that renders without switching the active display.

### Changed
- **`game_card_width` guidance**: the description advised lowering it on
multi-panel chains, which is backwards — on a wide panel cards need to be
*wider* to stay readable. It now suggests roughly display width / 3.

## [1.19.0] - 2026-07-09

### Added
Expand Down
6 changes: 3 additions & 3 deletions plugins/baseball-scoreboard/config_schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -161,7 +161,7 @@
"default": 128,
"minimum": 32,
"maximum": 512,
"description": "Width of each game card in scroll mode (pixels). Default 128. Set lower on multi-panel chains to show more games simultaneously."
"description": "Width of each game card in scroll mode (pixels). Default 128, which suits a single 128px panel. On a wider chain raise it so each card stays readable - roughly display width divided by 3 shows about three games at once. Lower it to fit more games on screen at the cost of detail."
}
}
},
Expand Down Expand Up @@ -602,7 +602,7 @@
"default": 128,
"minimum": 32,
"maximum": 512,
"description": "Width of each game card in scroll mode (pixels). Default 128. Set lower on multi-panel chains to show more games simultaneously."
"description": "Width of each game card in scroll mode (pixels). Default 128, which suits a single 128px panel. On a wider chain raise it so each card stays readable - roughly display width divided by 3 shows about three games at once. Lower it to fit more games on screen at the cost of detail."
}
}
},
Expand Down Expand Up @@ -1019,7 +1019,7 @@
"default": 128,
"minimum": 32,
"maximum": 512,
"description": "Width of each game card in scroll mode (pixels). Default 128. Set lower on multi-panel chains to show more games simultaneously."
"description": "Width of each game card in scroll mode (pixels). Default 128, which suits a single 128px panel. On a wider chain raise it so each card stays readable - roughly display width divided by 3 shows about three games at once. Lower it to fit more games on screen at the cost of detail."
}
}
},
Expand Down
208 changes: 143 additions & 65 deletions plugins/baseball-scoreboard/manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,12 @@

logger = logging.getLogger(__name__)

# Scroll-display key for the combined live/recent/upcoming slate that Vegas
# mode consumes. Kept separate from the per-mode displays ('live', 'recent',
# 'upcoming') that the standalone rotation renders, so the two cannot clobber
# each other's content.
VEGAS_SCROLL_KEY = 'mixed'


class BaseballScoreboardPlugin(BasePlugin if BasePlugin else object):
"""
Expand Down Expand Up @@ -189,6 +195,10 @@ def __init__(
self._scroll_prepared: Dict[str, bool] = {} # {game_type: is_prepared}
self._scroll_active_league: Dict[str, str] = {} # {game_type: league currently prepared}

# Fingerprint of the slate the Vegas cards were last rendered from, so
# they are only re-rendered when the games actually change.
self._vegas_signature: Optional[tuple] = None

# Enable high-FPS mode for scroll display (allows 100+ FPS scrolling)
# This signals to the display controller to use high-FPS loop (8ms = 125 FPS)
self.enable_scrolling = self._scroll_manager is not None
Expand Down Expand Up @@ -310,6 +320,9 @@ def on_config_change(self, new_config: Dict[str, Any]) -> None:
self._scroll_active = {}
self._scroll_prepared = {}
self._scroll_active_league = {}
# The scroll manager was just rebuilt, so the rendered Vegas cards are
# gone; drop the fingerprint or they would never be rebuilt.
self._vegas_signature = None

# Rebuild rotation modes and reset cycling state.
self.modes = self._get_available_modes()
Expand Down Expand Up @@ -3924,31 +3937,137 @@ def get_vegas_content(self) -> Optional[Any]:
"""
Get content for Vegas-style continuous scroll mode.

Triggers scroll content generation if cache is empty, then returns
the cached scroll image(s) for Vegas to compose into its scroll strip.
Returns one card per game across every enabled league and all three
game types (live, recent, upcoming), so the ticker shows the full
slate rather than a single game.

This deliberately reads its own dedicated scroll display rather than
the union of all of them. Previously it returned
get_all_vegas_content_items() and only built the combined set when that
came back empty — which meant that once a standalone display mode had
rendered, Vegas inherited *that* mode's games. With one live game in
progress that reduced the whole ticker entry to a single card, and it
could also show a game twice when two displays held it.

Content is rebuilt only when the underlying game data changes, so the
common case is a cheap cache read. Notably it does not call update():
refreshing data is the update cycle's job, and doing network I/O here
would stall the Vegas render loop (measured at seconds) with the panel
frozen.

Returns:
List of PIL Images from scroll displays, or None if no content
List of PIL Images, one per game, or None if there is nothing to show
"""
if not hasattr(self, '_scroll_manager') or not self._scroll_manager:
if not getattr(self, '_scroll_manager', None):
return None

images = self._scroll_manager.get_all_vegas_content_items()
try:
games, leagues = self._collect_games_for_scroll(live_priority_active=False)
except Exception:
self.logger.exception("[Baseball Vegas] Failed to collect games")
return None

if not images:
self.logger.info("[Baseball Vegas] Triggering scroll content generation")
self._ensure_scroll_content_for_vegas()
images = self._scroll_manager.get_all_vegas_content_items()
if not games:
self.logger.debug("[Baseball Vegas] No games available")
return None

if images:
total_width = sum(img.width for img in images)
signature = self._vegas_game_signature(games)
images = self._scroll_manager.get_vegas_content_items_for(VEGAS_SCROLL_KEY)

if not images or signature != self._vegas_signature:
reason = "no cached content" if not images else "game data changed"
self.logger.info(
"[Baseball Vegas] Returning %d image(s), %dpx total",
len(images), total_width
"[Baseball Vegas] Rebuilding scroll content (%s): %d game(s) from %s",
reason, len(games), ', '.join(leagues) or 'no leagues'
)
return images
if self._build_vegas_scroll_content(games, leagues):
self._vegas_signature = signature
images = self._scroll_manager.get_vegas_content_items_for(VEGAS_SCROLL_KEY)

return None
if not images:
return None

total_width = sum(img.width for img in images)
self.logger.info(
"[Baseball Vegas] Returning %d image(s), %dpx total",
len(images), total_width
)
return images

def _vegas_game_signature(self, games: List[Dict]) -> tuple:
"""
Build a cheap fingerprint of the game slate.

Rebuilding the Vegas cards means re-rendering every game, so it should
only happen when something a viewer would notice has actually changed —
the set of games, or a score/inning within them. Every field is read
with .get() because game dicts vary by league and completeness.
"""
fingerprint = []
for game in games:
status = game.get('status')
state = status.get('state') if isinstance(status, dict) else status
fingerprint.append((
game.get('id') or game.get('game_id') or game.get('start_time'),
game.get('league'),
state,
game.get('home_abbr'), game.get('away_abbr'),
game.get('home_score'), game.get('away_score'),
game.get('inning'), game.get('inning_half'),
game.get('outs'), game.get('balls'), game.get('strikes'),
game.get('is_final'),
))
return tuple(fingerprint)

def _build_vegas_scroll_content(
self, games: List[Dict], leagues: List[str]
) -> bool:
"""
Render the combined game slate into the Vegas scroll display.

Args:
games: Games collected across all leagues and game types
leagues: League IDs represented in ``games``

Returns:
True if content was rendered successfully
"""
rankings_cache = (
self._get_rankings_cache() if hasattr(self, '_get_rankings_cache') else None
)

try:
# prepare_content, not prepare_and_display: the latter also makes
# this the manager's active scroll display, which would hijack the
# standalone rotation's in-progress scroll.
success = self._scroll_manager.prepare_content(
games, VEGAS_SCROLL_KEY, leagues, rankings_cache
)
except Exception:
self.logger.exception("[Baseball Vegas] Error rendering scroll content")
return False

if not success:
self.logger.warning("[Baseball Vegas] Failed to generate scroll content")
return False

counts = {'live': 0, 'recent': 0, 'upcoming': 0}
for game in games:
status = game.get('status')
state = status.get('state') if isinstance(status, dict) else status
if state == 'in':
counts['live'] += 1
elif state == 'post':
counts['recent'] += 1
elif state == 'pre':
counts['upcoming'] += 1

summary = ', '.join(f"{n} {kind}" for kind, n in counts.items() if n)
self.logger.info(
"[Baseball Vegas] Generated scroll content: %d games (%s) from %s",
len(games), summary or 'unclassified', ', '.join(leagues)
)
return True

def get_vegas_content_type(self) -> str:
"""
Expand Down Expand Up @@ -3982,66 +4101,25 @@ def get_vegas_display_mode(self) -> 'VegasDisplayMode':

def _ensure_scroll_content_for_vegas(self) -> None:
"""
Ensure scroll content is generated for Vegas mode.
Build the combined Vegas slate if it is missing.

This method is called by get_vegas_content() when the scroll cache is empty.
It collects all game types (live, recent, upcoming) organized by league.
Retained for backward compatibility; get_vegas_content() now handles
rebuilding directly and keys off a data fingerprint. Unlike the previous
implementation this does not call update() — refreshing data is the
update cycle's job, and doing network I/O from the Vegas render path
stalled the scroll for seconds with the panel frozen.
"""
if not hasattr(self, '_scroll_manager') or not self._scroll_manager:
if not getattr(self, '_scroll_manager', None):
self.logger.debug("[Baseball Vegas] No scroll manager available")
return

# Refresh internal managers/cache so Vegas has up-to-date content
try:
if hasattr(self, 'update') and callable(self.update):
self.update()
self.logger.debug("[Baseball Vegas] Refreshed managers via update()")
elif hasattr(self, 'refresh_managers') and callable(self.refresh_managers):
self.refresh_managers()
self.logger.debug("[Baseball Vegas] Refreshed managers via refresh_managers()")
elif hasattr(self, '_update') and callable(self._update):
self._update()
self.logger.debug("[Baseball Vegas] Refreshed managers via _update()")
except Exception as e:
self.logger.debug(f"[Baseball Vegas] Manager refresh failed (non-fatal): {e}")

# Collect all games (live, recent, upcoming) organized by league
games, leagues = self._collect_games_for_scroll(live_priority_active=False)

if not games:
self.logger.debug("[Baseball Vegas] No games available")
return

# Count games by type for logging
game_type_counts = {'live': 0, 'recent': 0, 'upcoming': 0}
for game in games:
state = game.get('status', {}).get('state', '')
if state == 'in':
game_type_counts['live'] += 1
elif state == 'post':
game_type_counts['recent'] += 1
elif state == 'pre':
game_type_counts['upcoming'] += 1

# Get rankings cache if available
rankings_cache = self._get_rankings_cache() if hasattr(self, '_get_rankings_cache') else None

# Prepare scroll content with mixed game types
# Note: Using 'mixed' as game_type indicator for scroll config
success = self._scroll_manager.prepare_and_display(
games, 'mixed', leagues, rankings_cache
)

if success:
type_summary = ', '.join(
f"{count} {gtype}" for gtype, count in game_type_counts.items() if count > 0
)
self.logger.info(
f"[Baseball Vegas] Successfully generated scroll content: "
f"{len(games)} games ({type_summary}) from {', '.join(leagues)}"
)
else:
self.logger.warning("[Baseball Vegas] Failed to generate scroll content")
if self._build_vegas_scroll_content(games, leagues):
self._vegas_signature = self._vegas_game_signature(games)

def cleanup(self) -> None:
"""Clean up resources."""
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.19.3",
"version": "1.20.1",
"author": "ChuckBuilds",
"description": "Live, recent, and upcoming baseball games across MLB, MiLB, and NCAA Baseball with real-time scores and schedules",
"category": "sports",
Expand Down Expand Up @@ -30,6 +30,12 @@
"branch": "main",
"plugin_path": "plugins/baseball-scoreboard",
"versions": [
{
"released": "2026-07-28",
"version": "1.20.1",
"notes": "Fix Vegas scroll mode showing only a single game: get_vegas_content() inherited whichever standalone display mode had rendered last, so one live game collapsed the whole ticker entry to one card. It now reads a dedicated combined live/recent/upcoming slate that the standalone modes cannot clobber, rebuilt only when the games actually change. Also stops the Vegas build doing network I/O on the render path (which froze the ticker) and stops it hijacking an in-progress standalone scroll.",
"ledmatrix_min": "2.0.0"
},
{
"released": "2026-07-18",
"version": "1.19.3",
Expand Down
Loading
Loading