From e74fc076351c888ab8b995b9ad5d7b6b8eb7106d Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 3 Sep 2026 18:04:50 -0400 Subject: [PATCH] fix(scoreboards): announce the last-ditch font fallback _load_custom_font's final fallback caught every exception, passed, and returned PIL's built-in face. That face does not match the panel's pixel grid, so a missing or unreadable font file rendered as fuzzy text with nothing in the log to explain it -- it reads as a rendering bug rather than a missing file. Both paths now warn: the one where the fallback font itself fails to load (naming the path and the exception), and the one that reaches PIL's built-in face regardless. Nothing renders differently. This only makes an existing failure visible. Scope: an audit of the font and odds paths across all eight scoreboards found 77 broad handlers, of which 74 already log. These two were the only ones that were both broad and silent. football/game_renderer.py keeps its silent BDF handler deliberately -- its comment states the warning is deferred to the downstream fallback, and one is emitted there. --- plugins.json | 4 ++-- plugins/basketball-scoreboard/game_renderer.py | 12 ++++++++++-- plugins/basketball-scoreboard/manifest.json | 9 ++++++++- plugins/soccer-scoreboard/game_renderer.py | 14 +++++++++++--- plugins/soccer-scoreboard/manifest.json | 9 ++++++++- 5 files changed, 39 insertions(+), 9 deletions(-) diff --git a/plugins.json b/plugins.json index f818927f..3dcfc78e 100644 --- a/plugins.json +++ b/plugins.json @@ -101,7 +101,7 @@ "last_updated": "2026-09-02", "verified": true, "screenshot": "", - "latest_version": "1.29.1" + "latest_version": "1.29.2" }, { "id": "calendar", @@ -760,7 +760,7 @@ "last_updated": "2026-09-02", "verified": true, "screenshot": "", - "latest_version": "2.24.1" + "latest_version": "2.24.2" }, { "id": "static-image", diff --git a/plugins/basketball-scoreboard/game_renderer.py b/plugins/basketball-scoreboard/game_renderer.py index 8dc0dbef..ae1095b2 100644 --- a/plugins/basketball-scoreboard/game_renderer.py +++ b/plugins/basketball-scoreboard/game_renderer.py @@ -259,9 +259,17 @@ 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: + # Say so. Reaching PIL's built-in face means this element will + # not match the panel's pixel grid, which reads as a rendering + # bug rather than a missing font file. + self.logger.warning( + "Fallback font %s failed to load (%s: %s)", + default_font_path, type(e).__name__, e) + self.logger.warning( + "No usable font found; using PIL's built-in face, which will " + "not be pixel-crisp") return ImageFont.load_default() def preload_logos(self, games: list, logo_dir: Path) -> None: diff --git a/plugins/basketball-scoreboard/manifest.json b/plugins/basketball-scoreboard/manifest.json index 151dec6d..e9e2f4bc 100644 --- a/plugins/basketball-scoreboard/manifest.json +++ b/plugins/basketball-scoreboard/manifest.json @@ -1,7 +1,7 @@ { "id": "basketball-scoreboard", "name": "Basketball Scoreboard", - "version": "1.29.1", + "version": "1.29.2", "update_interval": 60, "description": "Live, recent, and upcoming basketball games across NBA, NCAA Men's, NCAA Women's, and WNBA with real-time scores, schedules, and March Madness tournament support", "author": "ChuckBuilds", @@ -19,6 +19,13 @@ "branch": "main", "plugin_path": "plugins/basketball-scoreboard", "versions": [ + { + "version": "1.29.2", + "released": "2026-09-02", + "ledmatrix_min_version": "3.3.0", + "notes": "Say so when no usable font can be found. The last-ditch fallback silently dropped to PIL's built-in face, which does not match the panel's pixel grid -- so a missing or unreadable font file looked like a rendering bug with nothing in the log to explain it. It now warns, naming the font it failed to load. Nothing renders differently; this only makes an existing failure visible.", + "changelog": "Retry a team logo whose previous download failed, instead of showing a grey box forever. A failed download is cached by the core as a placeholder wearing the real logo's filename; the logo loader scans filename variations, found that stub, and so never called the downloader again. The loader now skips a placeholder that is stale enough to be worth retrying and lets the download run, which also picks up stubs already on disk. The retry is rate-limited by the core (6h), so this does not trade a permanent grey box for a request every frame. Needs a core carrying src.logo_downloader.is_placeholder_logo; against an older core the check is skipped and behaviour is unchanged. Ported byte-identically across every sports lineage." + }, { "version": "1.29.1", "released": "2026-09-02", diff --git a/plugins/soccer-scoreboard/game_renderer.py b/plugins/soccer-scoreboard/game_renderer.py index b9e17205..1247598a 100644 --- a/plugins/soccer-scoreboard/game_renderer.py +++ b/plugins/soccer-scoreboard/game_renderer.py @@ -223,9 +223,17 @@ 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: + # Say so. Reaching PIL's built-in face means this element will + # not match the panel's pixel grid, which reads as a rendering + # bug rather than a missing font file. + self.logger.warning( + "Fallback font %s failed to load (%s: %s)", + default_font_path, type(e).__name__, e) + + self.logger.warning( + "No usable font found; using PIL's built-in face, which will " + "not be pixel-crisp") return ImageFont.load_default() def preload_logos(self, games: list, logo_dir: Path) -> None: diff --git a/plugins/soccer-scoreboard/manifest.json b/plugins/soccer-scoreboard/manifest.json index 6977cc39..268e4f78 100644 --- a/plugins/soccer-scoreboard/manifest.json +++ b/plugins/soccer-scoreboard/manifest.json @@ -1,7 +1,7 @@ { "id": "soccer-scoreboard", "name": "Soccer Scoreboard", - "version": "2.24.1", + "version": "2.24.2", "author": "ChuckBuilds", "description": "Live, recent, and upcoming soccer games across multiple leagues including Premier League, La Liga, Bundesliga, Serie A, Ligue 1, MLS, Liga Portugal, Champions League, Europa League, and FIFA World Cup", "category": "sports", @@ -26,6 +26,13 @@ "soccer_upcoming" ], "versions": [ + { + "version": "2.24.2", + "released": "2026-09-02", + "ledmatrix_min_version": "3.3.0", + "notes": "Say so when no usable font can be found. The last-ditch fallback silently dropped to PIL's built-in face, which does not match the panel's pixel grid -- so a missing or unreadable font file looked like a rendering bug with nothing in the log to explain it. It now warns, naming the font it failed to load. Nothing renders differently; this only makes an existing failure visible.", + "changelog": "Retry a team logo whose previous download failed, instead of showing a grey box forever. A failed download is cached by the core as a placeholder wearing the real logo's filename; the logo loader scans filename variations, found that stub, and so never called the downloader again. The loader now skips a placeholder that is stale enough to be worth retrying and lets the download run, which also picks up stubs already on disk. The retry is rate-limited by the core (6h), so this does not trade a permanent grey box for a request every frame. Needs a core carrying src.logo_downloader.is_placeholder_logo; against an older core the check is skipped and behaviour is unchanged. Ported byte-identically across every sports lineage." + }, { "version": "2.24.1", "released": "2026-09-02",