From 09a1aaa41a0ed8647010097b46b48ed3cb66ab27 Mon Sep 17 00:00:00 2001 From: ChuckBuilds Date: Sun, 29 Mar 2026 21:33:41 -0400 Subject: [PATCH 1/4] fix(baseball,odds): handle missing abbreviation in ESPN NCAA data Some NCAA baseball teams lack the 'abbreviation' field in ESPN API responses, causing KeyError crashes. Use .get() with fallback to team name truncated to 3 chars, matching the pattern already used in sports.py and data_manager.py. Co-Authored-By: Claude Opus 4.6 (1M context) --- plugins/baseball-scoreboard/baseball.py | 6 +++--- plugins/odds-ticker/manager.py | 4 ++-- plugins/odds-ticker/manifest.json | 7 ++++++- 3 files changed, 11 insertions(+), 6 deletions(-) diff --git a/plugins/baseball-scoreboard/baseball.py b/plugins/baseball-scoreboard/baseball.py index 054c8a2..5ea468f 100644 --- a/plugins/baseball-scoreboard/baseball.py +++ b/plugins/baseball-scoreboard/baseball.py @@ -136,9 +136,9 @@ def _extract_game_details(self, game_event: Dict) -> Optional[Dict]: try: game_status = status["type"]["name"].lower() status_state = status["type"]["state"].lower() - # Get team abbreviations - home_abbr = home_team["team"]["abbreviation"] - away_abbr = away_team["team"]["abbreviation"] + # Get team abbreviations (some NCAA teams lack 'abbreviation') + home_abbr = home_team["team"].get("abbreviation", home_team["team"].get("name", "?")[:3]) + away_abbr = away_team["team"].get("abbreviation", away_team["team"].get("name", "?")[:3]) # Check if this is a favorite team game is_favorite_game = ( diff --git a/plugins/odds-ticker/manager.py b/plugins/odds-ticker/manager.py index fa19a3e..02222d8 100644 --- a/plugins/odds-ticker/manager.py +++ b/plugins/odds-ticker/manager.py @@ -1148,8 +1148,8 @@ def _fetch_league_games(self, league_config: Dict[str, Any], now: datetime, cano away_team = next(c for c in competitors if c['homeAway'] == 'away') home_id = home_team['team']['id'] away_id = away_team['team']['id'] - home_abbr = home_team['team']['abbreviation'] - away_abbr = away_team['team']['abbreviation'] + home_abbr = home_team['team'].get('abbreviation', home_team['team'].get('name', '?')[:3]) + away_abbr = away_team['team'].get('abbreviation', away_team['team'].get('name', '?')[:3]) home_name = home_team['team'].get('name', home_abbr) away_name = away_team['team'].get('name', away_abbr) diff --git a/plugins/odds-ticker/manifest.json b/plugins/odds-ticker/manifest.json index 6918baf..aa8648e 100644 --- a/plugins/odds-ticker/manifest.json +++ b/plugins/odds-ticker/manifest.json @@ -1,7 +1,7 @@ { "id": "odds-ticker", "name": "Odds Ticker", - "version": "1.1.1", + "version": "1.1.2", "description": "Displays scrolling odds and betting lines for upcoming games across multiple sports leagues including NFL, NBA, MLB, NCAA Football, and more", "author": "ChuckBuilds", "category": "sports", @@ -10,6 +10,11 @@ "branch": "main", "plugin_path": "plugins/odds-ticker", "versions": [ + { + "version": "1.1.2", + "ledmatrix_min": "2.0.0", + "released": "2026-03-29" + }, { "version": "1.1.1", "ledmatrix_min": "2.0.0", From c68025f19ae8247f3a2f4461b5a0923e17e6ac8f Mon Sep 17 00:00:00 2001 From: ChuckBuilds Date: Sun, 29 Mar 2026 22:04:56 -0400 Subject: [PATCH 2/4] fix(baseball): add custom font support and fix odds y-position font mismatch Baseball game_renderer was hardcoding fonts instead of reading from customization config like basketball and football do. Also fixes textbbox using 'time' font instead of 'detail' font for odds y-position calculation. Co-Authored-By: Claude Opus 4.6 (1M context) --- plugins/baseball-scoreboard/game_renderer.py | 86 +++++++++++++++----- plugins/baseball-scoreboard/manifest.json | 7 +- 2 files changed, 73 insertions(+), 20 deletions(-) diff --git a/plugins/baseball-scoreboard/game_renderer.py b/plugins/baseball-scoreboard/game_renderer.py index 6a6441d..40cabd3 100644 --- a/plugins/baseball-scoreboard/game_renderer.py +++ b/plugins/baseball-scoreboard/game_renderer.py @@ -8,7 +8,8 @@ import logging from datetime import datetime from pathlib import Path -from typing import Dict, Optional +import os +from typing import Any, Dict, Optional import pytz from PIL import Image, ImageDraw, ImageFont @@ -56,27 +57,74 @@ def __init__( # Load fonts self.fonts = self._load_fonts() - def _load_fonts(self): - """Load fonts used by the renderer.""" + def _load_fonts(self) -> Dict[str, ImageFont.FreeTypeFont]: + """Load fonts used by the scoreboard from config or use defaults.""" fonts = {} + + # Get customization config + customization = self.config.get('customization', {}) + + # Load fonts from config with defaults for backward compatibility + score_config = customization.get('score_text', {}) + period_config = customization.get('period_text', {}) + team_config = customization.get('team_name', {}) + status_config = customization.get('status_text', {}) + detail_config = customization.get('detail_text', {}) + rank_config = customization.get('rank_text', {}) + try: - fonts['score'] = ImageFont.truetype("assets/fonts/PressStart2P-Regular.ttf", 10) - fonts['time'] = ImageFont.truetype("assets/fonts/PressStart2P-Regular.ttf", 8) - fonts['team'] = ImageFont.truetype("assets/fonts/PressStart2P-Regular.ttf", 8) - fonts['status'] = ImageFont.truetype("assets/fonts/4x6-font.ttf", 6) - fonts['detail'] = ImageFont.truetype("assets/fonts/4x6-font.ttf", 6) - fonts['rank'] = ImageFont.truetype("assets/fonts/PressStart2P-Regular.ttf", 10) - self.logger.debug("Successfully loaded fonts") - except IOError: - self.logger.warning("Fonts not found, using default PIL font.") - fonts['score'] = ImageFont.load_default() - fonts['time'] = ImageFont.load_default() - fonts['team'] = ImageFont.load_default() - fonts['status'] = ImageFont.load_default() - fonts['detail'] = ImageFont.load_default() - fonts['rank'] = ImageFont.load_default() + fonts["score"] = self._load_custom_font(score_config, default_size=10) + fonts["time"] = self._load_custom_font(period_config, default_size=8) + fonts["team"] = self._load_custom_font(team_config, default_size=8) + fonts["status"] = self._load_custom_font(status_config, default_size=6) + fonts["detail"] = self._load_custom_font(detail_config, default_size=6, default_font='4x6-font.ttf') + fonts["rank"] = self._load_custom_font(rank_config, default_size=10) + self.logger.debug("Successfully loaded fonts from config") + except Exception as e: + self.logger.exception("Error loading fonts, using defaults") + # Fallback to hardcoded defaults + try: + fonts["score"] = ImageFont.truetype("assets/fonts/PressStart2P-Regular.ttf", 10) + fonts["time"] = ImageFont.truetype("assets/fonts/PressStart2P-Regular.ttf", 8) + fonts["team"] = ImageFont.truetype("assets/fonts/PressStart2P-Regular.ttf", 8) + fonts["status"] = ImageFont.truetype("assets/fonts/4x6-font.ttf", 6) + fonts["detail"] = ImageFont.truetype("assets/fonts/4x6-font.ttf", 6) + fonts["rank"] = ImageFont.truetype("assets/fonts/PressStart2P-Regular.ttf", 10) + except IOError: + self.logger.warning("Fonts not found, using default PIL font.") + default_font = ImageFont.load_default() + fonts = {k: default_font for k in ["score", "time", "team", "status", "detail", "rank"]} + return fonts + def _load_custom_font(self, element_config: Dict[str, Any], default_size: int = 8, default_font: str = 'PressStart2P-Regular.ttf') -> ImageFont.FreeTypeFont: + """Load a custom font from an element configuration dictionary.""" + font_name = element_config.get('font', default_font) + font_size = int(element_config.get('font_size', default_size)) + font_path = os.path.join('assets', 'fonts', font_name) + + try: + if os.path.exists(font_path): + if font_path.lower().endswith('.ttf'): + return ImageFont.truetype(font_path, font_size) + elif font_path.lower().endswith('.bdf'): + try: + return ImageFont.truetype(font_path, font_size) + except Exception: + self.logger.warning(f"Could not load BDF font {font_name}, using default") + except Exception as e: + self.logger.exception(f"Error loading font {font_name}") + + # Fallback to default font + default_font_path = os.path.join('assets', 'fonts', 'PressStart2P-Regular.ttf') + try: + if os.path.exists(default_font_path): + return ImageFont.truetype(default_font_path, font_size) + except Exception: + pass + + return ImageFont.load_default() + def _get_logo_path(self, league: str, team_abbrev: str) -> Path: """Get the logo path for a team based on league.""" if league == 'mlb': @@ -501,7 +549,7 @@ def _draw_dynamic_odds(self, draw, odds: Dict) -> None: favored_side = 'away' # Odds row below the status/inning text row - status_bbox = draw.textbbox((0, 0), "A", font=self.fonts['time']) + status_bbox = draw.textbbox((0, 0), "A", font=self.fonts['detail']) odds_y = status_bbox[3] + 2 # just below the status row # Show the negative spread on the appropriate side diff --git a/plugins/baseball-scoreboard/manifest.json b/plugins/baseball-scoreboard/manifest.json index 43fab42..4ddf766 100644 --- a/plugins/baseball-scoreboard/manifest.json +++ b/plugins/baseball-scoreboard/manifest.json @@ -1,7 +1,7 @@ { "id": "baseball-scoreboard", "name": "Baseball Scoreboard", - "version": "1.5.5", + "version": "1.5.6", "author": "ChuckBuilds", "description": "Live, recent, and upcoming baseball games across MLB, MiLB, and NCAA Baseball with real-time scores and schedules", "category": "sports", @@ -30,6 +30,11 @@ "branch": "main", "plugin_path": "plugins/baseball-scoreboard", "versions": [ + { + "released": "2026-03-29", + "version": "1.5.6", + "ledmatrix_min": "2.0.0" + }, { "released": "2026-03-29", "version": "1.5.5", From 56f451dae62daa1612b784863554f1c29a0f1a80 Mon Sep 17 00:00:00 2001 From: ChuckBuilds Date: Sun, 29 Mar 2026 22:12:10 -0400 Subject: [PATCH 3/4] fix(baseball,odds-ticker): safe abbreviation fallback and metadata updates Fix TypeError when team name is None by using `or` coalescing instead of passing potentially-None values to [:3] slice. Log default font fallback failures instead of silently swallowing them. Update last_updated and latest_version in manifests and registry. Co-Authored-By: Claude Opus 4.6 (1M context) --- plugins.json | 6 +++--- plugins/baseball-scoreboard/baseball.py | 4 ++-- plugins/baseball-scoreboard/game_renderer.py | 4 ++-- plugins/baseball-scoreboard/manifest.json | 2 +- plugins/odds-ticker/manager.py | 4 ++-- plugins/odds-ticker/manifest.json | 9 +++++++-- 6 files changed, 17 insertions(+), 12 deletions(-) diff --git a/plugins.json b/plugins.json index 759485c..3045da2 100644 --- a/plugins.json +++ b/plugins.json @@ -299,7 +299,7 @@ "last_updated": "2026-03-29", "verified": true, "screenshot": "", - "latest_version": "1.5.5" + "latest_version": "1.5.6" }, { "id": "soccer-scoreboard", @@ -352,10 +352,10 @@ "plugin_path": "plugins/odds-ticker", "stars": 0, "downloads": 0, - "last_updated": "2026-02-23", + "last_updated": "2026-03-29", "verified": true, "screenshot": "", - "latest_version": "1.1.1" + "latest_version": "1.1.3" }, { "id": "leaderboard", diff --git a/plugins/baseball-scoreboard/baseball.py b/plugins/baseball-scoreboard/baseball.py index 5ea468f..c0e7832 100644 --- a/plugins/baseball-scoreboard/baseball.py +++ b/plugins/baseball-scoreboard/baseball.py @@ -137,8 +137,8 @@ def _extract_game_details(self, game_event: Dict) -> Optional[Dict]: game_status = status["type"]["name"].lower() status_state = status["type"]["state"].lower() # Get team abbreviations (some NCAA teams lack 'abbreviation') - home_abbr = home_team["team"].get("abbreviation", home_team["team"].get("name", "?")[:3]) - away_abbr = away_team["team"].get("abbreviation", away_team["team"].get("name", "?")[:3]) + home_abbr = home_team["team"].get("abbreviation") or (home_team["team"].get("name") or "?")[:3] + away_abbr = away_team["team"].get("abbreviation") or (away_team["team"].get("name") or "?")[:3] # Check if this is a favorite team game is_favorite_game = ( diff --git a/plugins/baseball-scoreboard/game_renderer.py b/plugins/baseball-scoreboard/game_renderer.py index 40cabd3..6237884 100644 --- a/plugins/baseball-scoreboard/game_renderer.py +++ b/plugins/baseball-scoreboard/game_renderer.py @@ -120,8 +120,8 @@ def _load_custom_font(self, element_config: Dict[str, Any], default_size: int = try: if os.path.exists(default_font_path): return ImageFont.truetype(default_font_path, font_size) - except Exception: - pass + except Exception as e: + self.logger.warning(f"Error loading default font {default_font_path}: {e}") return ImageFont.load_default() diff --git a/plugins/baseball-scoreboard/manifest.json b/plugins/baseball-scoreboard/manifest.json index 4ddf766..69fc693 100644 --- a/plugins/baseball-scoreboard/manifest.json +++ b/plugins/baseball-scoreboard/manifest.json @@ -111,7 +111,7 @@ "ledmatrix_min": "2.0.0" } ], - "last_updated": "2026-02-24", + "last_updated": "2026-03-29", "stars": 0, "downloads": 0, "verified": true, diff --git a/plugins/odds-ticker/manager.py b/plugins/odds-ticker/manager.py index 02222d8..e2d6797 100644 --- a/plugins/odds-ticker/manager.py +++ b/plugins/odds-ticker/manager.py @@ -1148,8 +1148,8 @@ def _fetch_league_games(self, league_config: Dict[str, Any], now: datetime, cano away_team = next(c for c in competitors if c['homeAway'] == 'away') home_id = home_team['team']['id'] away_id = away_team['team']['id'] - home_abbr = home_team['team'].get('abbreviation', home_team['team'].get('name', '?')[:3]) - away_abbr = away_team['team'].get('abbreviation', away_team['team'].get('name', '?')[:3]) + home_abbr = home_team['team'].get('abbreviation') or (home_team['team'].get('name') or '?')[:3] + away_abbr = away_team['team'].get('abbreviation') or (away_team['team'].get('name') or '?')[:3] home_name = home_team['team'].get('name', home_abbr) away_name = away_team['team'].get('name', away_abbr) diff --git a/plugins/odds-ticker/manifest.json b/plugins/odds-ticker/manifest.json index aa8648e..2657d8a 100644 --- a/plugins/odds-ticker/manifest.json +++ b/plugins/odds-ticker/manifest.json @@ -1,7 +1,7 @@ { "id": "odds-ticker", "name": "Odds Ticker", - "version": "1.1.2", + "version": "1.1.3", "description": "Displays scrolling odds and betting lines for upcoming games across multiple sports leagues including NFL, NBA, MLB, NCAA Football, and more", "author": "ChuckBuilds", "category": "sports", @@ -10,6 +10,11 @@ "branch": "main", "plugin_path": "plugins/odds-ticker", "versions": [ + { + "version": "1.1.3", + "ledmatrix_min": "2.0.0", + "released": "2026-03-29" + }, { "version": "1.1.2", "ledmatrix_min": "2.0.0", @@ -38,7 +43,7 @@ ], "stars": 0, "downloads": 0, - "last_updated": "2026-02-23", + "last_updated": "2026-03-29", "verified": true, "screenshot": "", "display_modes": [ From 0a3d9bbc63bccf26de2863bd420e8f57f615f55f Mon Sep 17 00:00:00 2001 From: ChuckBuilds Date: Sun, 29 Mar 2026 22:19:18 -0400 Subject: [PATCH 4/4] fix(baseball): use ImageFont.load for BDF fonts instead of truetype BDF fonts can't be loaded via ImageFont.truetype(). Look for pre-converted .pil/.pbm files and use ImageFont.load(), matching the football renderer pattern. Also adds OTF support and better logging for missing/unknown fonts. Co-Authored-By: Claude Opus 4.6 (1M context) --- plugins/baseball-scoreboard/game_renderer.py | 24 +++++++++++++++----- 1 file changed, 18 insertions(+), 6 deletions(-) diff --git a/plugins/baseball-scoreboard/game_renderer.py b/plugins/baseball-scoreboard/game_renderer.py index 6237884..8f17615 100644 --- a/plugins/baseball-scoreboard/game_renderer.py +++ b/plugins/baseball-scoreboard/game_renderer.py @@ -105,15 +105,27 @@ def _load_custom_font(self, element_config: Dict[str, Any], default_size: int = try: if os.path.exists(font_path): - if font_path.lower().endswith('.ttf'): + if font_path.lower().endswith('.ttf') or font_path.lower().endswith('.otf'): return ImageFont.truetype(font_path, font_size) elif font_path.lower().endswith('.bdf'): - try: - return ImageFont.truetype(font_path, font_size) - except Exception: - self.logger.warning(f"Could not load BDF font {font_name}, using default") + # BDF fonts require pre-conversion: pilfont.py font.bdf -> font.pil + font.pbm + pil_font_path = font_path.rsplit('.', 1)[0] + '.pil' + if os.path.exists(pil_font_path): + try: + return ImageFont.load(pil_font_path) + except Exception as e: + self.logger.warning(f"Failed to load pre-converted BDF font {pil_font_path}: {e}") + else: + self.logger.warning( + f"BDF font {font_name} requires conversion. " + f"Run: pilfont.py {font_path}" + ) + else: + self.logger.warning(f"Unknown font file type: {font_name}") + else: + self.logger.warning(f"Font file not found: {font_path}") except Exception as e: - self.logger.exception(f"Error loading font {font_name}") + self.logger.warning(f"Error loading font {font_name}: {e}") # Fallback to default font default_font_path = os.path.join('assets', 'fonts', 'PressStart2P-Regular.ttf')