Skip to content
Closed
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
30 changes: 15 additions & 15 deletions plugins.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"version": "1.0.0",
"last_updated": "2026-02-23",
"last_updated": "2026-02-25",
"plugins": [
{
"id": "hello-world",
Expand Down Expand Up @@ -196,10 +196,10 @@
"plugin_path": "plugins/hockey-scoreboard",
"stars": 0,
"downloads": 0,
"last_updated": "2026-02-15",
"last_updated": "2026-02-25",
"verified": true,
"screenshot": "",
"latest_version": "1.1.1"
"latest_version": "1.2.4"
},
{
"id": "football-scoreboard",
Expand All @@ -221,10 +221,10 @@
"plugin_path": "plugins/football-scoreboard",
"stars": 0,
"downloads": 0,
"last_updated": "2026-02-20",
"last_updated": "2026-02-25",
"verified": true,
"screenshot": "",
"latest_version": "2.2.0"
"latest_version": "2.3.4"
},
{
"id": "ufc-scoreboard",
Expand All @@ -245,10 +245,10 @@
"plugin_path": "plugins/ufc-scoreboard",
"stars": 0,
"downloads": 0,
"last_updated": "2026-02-15",
"last_updated": "2026-02-25",
"verified": true,
"screenshot": "",
"latest_version": "1.1.1"
"latest_version": "1.2.3"
},
{
"id": "basketball-scoreboard",
Expand All @@ -270,10 +270,10 @@
"plugin_path": "plugins/basketball-scoreboard",
"stars": 0,
"downloads": 0,
"last_updated": "2026-02-20",
"last_updated": "2026-02-25",
"verified": true,
"screenshot": "",
"latest_version": "1.4.0"
"latest_version": "1.5.4"
},
{
"id": "baseball-scoreboard",
Expand All @@ -296,10 +296,10 @@
"plugin_path": "plugins/baseball-scoreboard",
"stars": 0,
"downloads": 0,
"last_updated": "2026-02-20",
"last_updated": "2026-02-25",
"verified": true,
"screenshot": "",
"latest_version": "1.4.0"
"latest_version": "1.5.4"
},
{
"id": "soccer-scoreboard",
Expand All @@ -325,10 +325,10 @@
"plugin_path": "plugins/soccer-scoreboard",
"stars": 0,
"downloads": 0,
"last_updated": "2026-02-15",
"last_updated": "2026-02-25",
"verified": true,
"screenshot": "",
"latest_version": "1.3.1"
"latest_version": "1.4.4"
},
{
"id": "odds-ticker",
Expand Down Expand Up @@ -644,10 +644,10 @@
"plugin_path": "plugins/f1-scoreboard",
"stars": 0,
"downloads": 0,
"last_updated": "2026-02-18",
"last_updated": "2026-02-25",
"verified": true,
"screenshot": "",
"latest_version": "1.1.0"
"latest_version": "1.2.2"
},
{
"id": "web-ui-info",
Expand Down
21 changes: 21 additions & 0 deletions plugins/baseball-scoreboard/config_schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,13 @@
"type": "boolean",
"default": true,
"description": "Automatically calculate display duration based on content width"
},
"game_card_width": {
"type": "integer",
"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."
}
Comment on lines +129 to 135
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

python - <<'PY'
import json
from pathlib import Path

p = Path("plugins/baseball-scoreboard/manifest.json")
if not p.exists():
    raise SystemExit("Missing file: plugins/baseball-scoreboard/manifest.json")

data = json.loads(p.read_text())
versions = data.get("versions", [])
top = versions[0]["version"] if versions else data.get("version")
prev = versions[1]["version"] if len(versions) > 1 else None

print(f"manifest.version: {data.get('version')}")
print(f"top versions[0].version: {top}")
print(f"previous versions[1].version: {prev}")

def parse(v):
    a, b, c = v.split(".")
    return int(a), int(b), int(c)

if prev:
    t = parse(top)
    pr = parse(prev)
    minor_bump = (t[0] == pr[0] and t[1] == pr[1] + 1 and t[2] == 0)
    print(f"minor_bump_from_previous: {minor_bump}")

print(f"version_field_matches_top: {data.get('version') == top}")
PY

Repository: ChuckBuilds/ledmatrix-plugins

Length of output: 227


Bump manifest version to 1.6.0 for MINOR release.

New backward-compatible config options (game_card_width across multiple league blocks at lines 129–135, 467–473, 805–811) require a MINOR version bump, not PATCH. Update plugins/baseball-scoreboard/manifest.json to version 1.6.0 with the entry placed first in the versions array per semantic versioning guidelines for new features.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@plugins/baseball-scoreboard/config_schema.json` around lines 129 - 135,
Update the plugin manifest to reflect a MINOR release: add a new entry with
version "1.6.0" to plugins/baseball-scoreboard/manifest.json (because new
backward-compatible config options like game_card_width were introduced across
league blocks) and ensure the "1.6.0" entry is placed first at the top of the
versions array per semantic versioning for new features.

}
},
Expand Down Expand Up @@ -456,6 +463,13 @@
"type": "boolean",
"default": true,
"description": "Automatically calculate display duration based on content width"
},
"game_card_width": {
"type": "integer",
"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."
}
}
},
Expand Down Expand Up @@ -787,6 +801,13 @@
"type": "boolean",
"default": true,
"description": "Automatically calculate display duration based on content width"
},
"game_card_width": {
"type": "integer",
"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."
}
}
},
Expand Down
34 changes: 23 additions & 11 deletions plugins/baseball-scoreboard/game_renderer.py
Original file line number Diff line number Diff line change
Expand Up @@ -105,10 +105,13 @@ def _load_and_resize_logo(self, league: str, team_abbrev: str) -> Optional[Image
if logo.mode != 'RGBA':
logo = logo.convert('RGBA')

# Resize logo to fit display
max_width = int(self.display_width * 1.5)
max_height = int(self.display_height * 1.5)
logo.thumbnail((max_width, max_height), RESAMPLE_FILTER)
# Crop transparent padding then scale so ink fills display_height.
# thumbnail into a display_height square box preserves aspect ratio
# and prevents wide logos from exceeding their half-card slot.
bbox = logo.getbbox()
if bbox:
logo = logo.crop(bbox)
logo.thumbnail((self.display_height, self.display_height), RESAMPLE_FILTER)

# Copy before exiting context manager
cached_logo = logo.copy()
Expand Down Expand Up @@ -170,8 +173,11 @@ def _render_live_game(self, game: Dict) -> Image.Image:
center_y = self.display_height // 2

# Logos
main_img.paste(home_logo, (self.display_width - home_logo.width, center_y - home_logo.height // 2), home_logo)
main_img.paste(away_logo, (0, center_y - away_logo.height // 2), away_logo)
logo_slot = min(self.display_height, self.display_width // 2)
away_x = (logo_slot - away_logo.width) // 2
main_img.paste(away_logo, (away_x, center_y - away_logo.height // 2), away_logo)
home_x = (self.display_width - logo_slot) + (logo_slot - home_logo.width) // 2
main_img.paste(home_logo, (home_x, center_y - home_logo.height // 2), home_logo)

# Inning indicator (top center)
inning_half = game.get('inning_half', 'top')
Expand Down Expand Up @@ -311,8 +317,11 @@ def _render_recent_game(self, game: Dict) -> Image.Image:
center_y = self.display_height // 2

# Logos (tighter fit for recent)
main_img.paste(home_logo, (self.display_width - home_logo.width, center_y - home_logo.height // 2), home_logo)
main_img.paste(away_logo, (0, center_y - away_logo.height // 2), away_logo)
logo_slot = min(self.display_height, self.display_width // 2)
away_x = (logo_slot - away_logo.width) // 2
main_img.paste(away_logo, (away_x, center_y - away_logo.height // 2), away_logo)
home_x = (self.display_width - logo_slot) + (logo_slot - home_logo.width) // 2
main_img.paste(home_logo, (home_x, center_y - home_logo.height // 2), home_logo)

# "Final" (top center)
status_text = "Final"
Expand Down Expand Up @@ -357,8 +366,11 @@ def _render_upcoming_game(self, game: Dict) -> Image.Image:
center_y = self.display_height // 2

# Logos (tighter fit)
main_img.paste(home_logo, (self.display_width - home_logo.width, center_y - home_logo.height // 2), home_logo)
main_img.paste(away_logo, (0, center_y - away_logo.height // 2), away_logo)
logo_slot = min(self.display_height, self.display_width // 2)
away_x = (logo_slot - away_logo.width) // 2
main_img.paste(away_logo, (away_x, center_y - away_logo.height // 2), away_logo)
home_x = (self.display_width - logo_slot) + (logo_slot - home_logo.width) // 2
main_img.paste(home_logo, (home_x, center_y - home_logo.height // 2), home_logo)

# "Next Game" (top center)
status_font = self.fonts['status'] if self.display_width <= 128 else self.fonts['time']
Expand All @@ -373,7 +385,7 @@ def _render_upcoming_game(self, game: Dict) -> Image.Image:
if start_time:
try:
dt = datetime.fromisoformat(start_time.replace('Z', '+00:00'))
local_tz = pytz.timezone(self.config.get('timezone', 'US/Eastern'))
local_tz = pytz.timezone(self.config.get('timezone') or 'UTC')
dt_local = dt.astimezone(local_tz)
game_date = dt_local.strftime('%b %d')
game_time = dt_local.strftime('%-I:%M %p')
Expand Down
10 changes: 10 additions & 0 deletions plugins/baseball-scoreboard/manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,16 @@ def __init__(

self.logger = logger

# Resolve timezone: plugin config → global config → UTC.
# Inject into self.config so all sub-components (scroll display, game
# renderer, etc.) can read it via config.get('timezone').
if not self.config.get("timezone"):
try:
global_tz = cache_manager.config_manager.get_timezone()
except Exception:
global_tz = "UTC"
Comment on lines +100 to +103
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

rg -n -C5 'except Exception' plugins/baseball-scoreboard/manager.py

Repository: ChuckBuilds/ledmatrix-plugins

Length of output: 9060


🏁 Script executed:

cat -n plugins/baseball-scoreboard/manager.py | sed -n '95,110p'

Repository: ChuckBuilds/ledmatrix-plugins

Length of output: 946


🏁 Script executed:

# Check if self.logger is initialized before line 100
rg -n 'self.logger' plugins/baseball-scoreboard/manager.py | head -20

Repository: ChuckBuilds/ledmatrix-plugins

Length of output: 1494


Add logging to timezone resolution fallback to match error handling patterns elsewhere in this file.

Line 102 silently swallows exceptions during timezone resolution, making configuration failures invisible. This is inconsistent with all other exception handlers in this file, which log the error and exception. Catch the exception and log it before falling back to UTC.

🧰 Tools
🪛 Ruff (0.15.2)

[warning] 102-102: Do not catch blind exception: Exception

(BLE001)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@plugins/baseball-scoreboard/manager.py` around lines 100 - 103, The timezone
resolution currently swallows exceptions; update the try/except around
cache_manager.config_manager.get_timezone() to catch the exception as a variable
(e.g., "except Exception as e") and log it before falling back to "UTC" so it
matches other handlers in this file — use the same logger and error message
style used elsewhere (include the exception object) and then set global_tz =
"UTC" as the fallback.

self.config["timezone"] = global_tz or "UTC"

# Basic configuration
self.is_enabled = config.get("enabled", True)
# Get display dimensions from display_manager properties
Expand Down
29 changes: 27 additions & 2 deletions 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.4.0",
"version": "1.5.4",
"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,31 @@
"branch": "main",
"plugin_path": "plugins/baseball-scoreboard",
"versions": [
{
"released": "2026-02-25",
"version": "1.5.4",
"ledmatrix_min": "2.0.0"
},
{
"released": "2026-02-24",
"version": "1.5.3",
"ledmatrix_min": "2.0.0"
},
{
"released": "2026-02-24",
"version": "1.5.2",
"ledmatrix_min": "2.0.0"
},
{
"released": "2026-02-24",
"version": "1.5.1",
"ledmatrix_min": "2.0.0"
},
{
"released": "2026-02-24",
"version": "1.5.0",
"ledmatrix_min": "2.0.0"
},
{
"released": "2026-02-20",
"version": "1.4.0",
Expand Down Expand Up @@ -76,7 +101,7 @@
"ledmatrix_min": "2.0.0"
}
],
"last_updated": "2026-02-20",
"last_updated": "2026-02-24",
"stars": 0,
"downloads": 0,
"verified": true,
Expand Down
23 changes: 16 additions & 7 deletions plugins/baseball-scoreboard/scroll_display.py
Original file line number Diff line number Diff line change
Expand Up @@ -185,7 +185,8 @@ def _get_scroll_settings(self, league: Optional[str] = None) -> Dict[str, Any]:
"scroll_delay": 0.01,
"gap_between_games": 48,
"show_league_separators": True,
"dynamic_duration": True
"dynamic_duration": True,
"game_card_width": 128,
}

# Try to get league-specific settings first
Expand Down Expand Up @@ -215,15 +216,21 @@ def _get_scroll_settings(self, league: Optional[str] = None) -> Dict[str, Any]:

return defaults

def _get_game_renderer(self) -> Optional[GameRenderer]:
"""Get or create the cached GameRenderer instance."""
def _get_game_renderer(self, game_card_width: int = 128) -> Optional[GameRenderer]:
"""Get or create the cached GameRenderer instance.

Args:
game_card_width: Width for each game card. Cached renderer is recreated
if this differs from the current renderer's width.
"""
if GameRenderer is None:
self.logger.error("GameRenderer not available")
return None

if self._game_renderer is None:
# Recreate renderer if card width changed (e.g. config update)
if self._game_renderer is None or getattr(self._game_renderer, "display_width", None) != game_card_width:
self._game_renderer = GameRenderer(
self.display_width,
game_card_width,
self.display_height,
self.config,
logo_cache=self._logo_cache,
Expand Down Expand Up @@ -329,9 +336,11 @@ def prepare_scroll_content(
scroll_settings = self._get_scroll_settings()
gap_between_games = scroll_settings.get("gap_between_games", 24)
show_separators = scroll_settings.get("show_league_separators", True)
game_card_width = scroll_settings.get("game_card_width", 128)

# Get or create cached game renderer
renderer = self._get_game_renderer()
# Get or create cached game renderer using game_card_width so cards are a fixed
# size regardless of the full chain width (display_width may span multiple panels)
renderer = self._get_game_renderer(game_card_width)

Comment on lines +341 to 344
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Fail fast if GameRenderer could not be created.

On Line 343, renderer may be None. Then Line 376 raises per-game exceptions and can leave partial separator-only scroll content. Return early when renderer creation fails.

🛠️ Suggested fix
         renderer = self._get_game_renderer(game_card_width)
+        if renderer is None:
+            self._vegas_content_items = []
+            self._is_scrolling = False
+            return False
 
         # Pass rankings cache to renderer if available
         if renderer and rankings_cache:
             renderer.set_rankings_cache(rankings_cache)

Also applies to: 374-389

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@plugins/baseball-scoreboard/scroll_display.py` around lines 341 - 344, After
calling self._get_game_renderer(game_card_width) assign to renderer and
immediately check for None; if renderer is None, log or raise and return early
(e.g., return an empty/complete-safe scroll result) to avoid per-game exceptions
and producing partial separator-only content. Update the code paths that iterate
games (the block using renderer between where renderer is obtained and the
per-game rendering loop) so each failing _get_game_renderer returns early rather
than continuing; reference _get_game_renderer and GameRenderer/renderer to
locate and fix the checks and early-return behavior.

# Pass rankings cache to renderer if available
if renderer and rankings_cache:
Expand Down
10 changes: 8 additions & 2 deletions plugins/baseball-scoreboard/sports.py
Original file line number Diff line number Diff line change
Expand Up @@ -848,14 +848,20 @@ def _fetch_data(self) -> Optional[Dict]:
def _fetch_todays_games(self) -> Optional[Dict]:
"""Fetch only today's games for live updates (not entire season)."""
try:
now = datetime.now()
# ESPN API anchors its schedule calendar to Eastern US time.
# Always query using the Eastern date + 1-day lookback to catch
# late-night games still in progress from the previous Eastern day.
tz = pytz.timezone("America/New_York")
now = datetime.now(tz)
yesterday = now - timedelta(days=1)
formatted_date = now.strftime("%Y%m%d")
formatted_date_yesterday = yesterday.strftime("%Y%m%d")
# Fetch todays games only
url = f"https://site.api.espn.com/apis/site/v2/sports/{self.sport}/{self.league}/scoreboard"
self.logger.debug(f"Fetching today's games for {self.sport}/{self.league} on date {formatted_date}")
response = self.session.get(
url,
params={"dates": formatted_date, "limit": 1000},
params={"dates": f"{formatted_date_yesterday}-{formatted_date}", "limit": 1000},
headers=self.headers,
timeout=10,
)
Expand Down
Loading