From 203760c314437edd2fb0c5551a547ceac28620a4 Mon Sep 17 00:00:00 2001 From: Chuck Date: Mon, 23 Feb 2026 17:03:58 -0500 Subject: [PATCH 1/9] fix(soccer): remove debug artifacts from custom-leagues widget - Remove redundant "DEBUG:" string prefix from logger.debug() in manager.py - Remove console.log from render() (fired on every config page load) - Remove console.log widget registration notification Co-Authored-By: Claude Sonnet 4.6 --- plugins/soccer-scoreboard/manager.py | 2 +- plugins/soccer-scoreboard/widgets/custom-leagues.js | 2 -- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/plugins/soccer-scoreboard/manager.py b/plugins/soccer-scoreboard/manager.py index 4958c3e..2575b7e 100644 --- a/plugins/soccer-scoreboard/manager.py +++ b/plugins/soccer-scoreboard/manager.py @@ -345,7 +345,7 @@ def _adapt_config_for_manager(self, league_key: str) -> Dict[str, Any]: leagues_config = self.config.get('leagues', {}) league_config = leagues_config.get(league_key, {}) - self.logger.debug(f"DEBUG: league_config for {league_key} = {league_config}") + self.logger.debug(f"league_config for {league_key} = {league_config}") # Extract nested configurations display_modes_config = league_config.get("display_modes", {}) diff --git a/plugins/soccer-scoreboard/widgets/custom-leagues.js b/plugins/soccer-scoreboard/widgets/custom-leagues.js index 949d5d8..d7acac6 100644 --- a/plugins/soccer-scoreboard/widgets/custom-leagues.js +++ b/plugins/soccer-scoreboard/widgets/custom-leagues.js @@ -47,7 +47,6 @@ * Render the custom leagues widget */ render: function(container, config, value, options) { - console.log('[CustomLeaguesWidget] Render called (server-side rendered)'); }, /** @@ -427,5 +426,4 @@ } }; - console.log('[CustomLeaguesWidget] Custom leagues widget registered (soccer-scoreboard plugin)'); })(); From 699ea4eee8b167ed58d32bb226b3c1c12622b884 Mon Sep 17 00:00:00 2001 From: Chuck Date: Tue, 24 Feb 2026 11:38:34 -0500 Subject: [PATCH 2/9] fix(scoreboards): fix scroll mode game cards filling full chain width on multi-panel setups MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit On multi-panel chains (e.g. 5×64px = 320px wide), each scroll mode game card was rendered at the full chain width instead of a compact fixed size. This caused one game to fill the entire display with huge blank space between logos and score text, and no other games visible simultaneously. Root cause: ScrollDisplay read display_manager.matrix.width (the full chain width) and passed it directly to GameRenderer as the card canvas width. Fix: Add a configurable game_card_width setting (default 128px) to scroll_settings for all sports plugins. The GameRenderer now receives game_card_width instead of the full chain width, so cards are always rendered at a consistent size regardless of how many panels are chained. Multiple game cards are now visible simultaneously as the scroll buffer moves across the full display width. For F1, a separate _scroll_renderer is used at card width while the main renderer keeps full display_width for static single-card display. UFC's cached FightRenderer is recreated if game_card_width changes. Baseball's cached GameRenderer invalidates similarly. Bumps versions: football 2.3.0, basketball 1.5.0, baseball 1.5.0, hockey 1.2.0, soccer 1.4.0, ufc 1.2.0, f1 1.2.0 Co-Authored-By: Claude Sonnet 4.6 --- plugins.json | 30 +++++----- .../baseball-scoreboard/config_schema.json | 21 +++++++ plugins/baseball-scoreboard/manifest.json | 2 +- plugins/baseball-scoreboard/scroll_display.py | 23 +++++--- .../basketball-scoreboard/config_schema.json | 28 ++++++++++ plugins/basketball-scoreboard/manifest.json | 2 +- .../basketball-scoreboard/scroll_display.py | 11 ++-- plugins/f1-scoreboard/config_schema.json | 32 +++++++++++ plugins/f1-scoreboard/manager.py | 46 ++++++++++----- plugins/f1-scoreboard/manifest.json | 11 +++- .../football-scoreboard/config_schema.json | 14 +++++ plugins/football-scoreboard/manifest.json | 2 +- plugins/football-scoreboard/scroll_display.py | 11 ++-- plugins/hockey-scoreboard/config_schema.json | 21 +++++++ plugins/hockey-scoreboard/manifest.json | 2 +- plugins/hockey-scoreboard/scroll_display.py | 9 ++- plugins/soccer-scoreboard/config_schema.json | 56 +++++++++++++++++++ plugins/soccer-scoreboard/manifest.json | 2 +- plugins/soccer-scoreboard/scroll_display.py | 7 ++- plugins/ufc-scoreboard/config_schema.json | 45 +++++++++++++++ plugins/ufc-scoreboard/manifest.json | 11 +++- plugins/ufc-scoreboard/scroll_display.py | 8 ++- 22 files changed, 333 insertions(+), 61 deletions(-) diff --git a/plugins.json b/plugins.json index 6f02ee2..790094d 100644 --- a/plugins.json +++ b/plugins.json @@ -1,6 +1,6 @@ { "version": "1.0.0", - "last_updated": "2026-02-20", + "last_updated": "2026-02-24", "plugins": [ { "id": "hello-world", @@ -196,10 +196,10 @@ "plugin_path": "plugins/hockey-scoreboard", "stars": 0, "downloads": 0, - "last_updated": "2026-02-15", + "last_updated": "2026-02-24", "verified": true, "screenshot": "", - "latest_version": "1.1.1" + "latest_version": "1.2.0" }, { "id": "football-scoreboard", @@ -221,10 +221,10 @@ "plugin_path": "plugins/football-scoreboard", "stars": 0, "downloads": 0, - "last_updated": "2026-02-20", + "last_updated": "2026-02-24", "verified": true, "screenshot": "", - "latest_version": "2.2.0" + "latest_version": "2.3.0" }, { "id": "ufc-scoreboard", @@ -245,10 +245,10 @@ "plugin_path": "plugins/ufc-scoreboard", "stars": 0, "downloads": 0, - "last_updated": "2026-02-15", + "last_updated": "2026-02-24", "verified": true, "screenshot": "", - "latest_version": "1.1.1" + "latest_version": "1.2.0" }, { "id": "basketball-scoreboard", @@ -270,10 +270,10 @@ "plugin_path": "plugins/basketball-scoreboard", "stars": 0, "downloads": 0, - "last_updated": "2026-02-20", + "last_updated": "2026-02-24", "verified": true, "screenshot": "", - "latest_version": "1.4.0" + "latest_version": "1.5.0" }, { "id": "baseball-scoreboard", @@ -296,10 +296,10 @@ "plugin_path": "plugins/baseball-scoreboard", "stars": 0, "downloads": 0, - "last_updated": "2026-02-20", + "last_updated": "2026-02-24", "verified": true, "screenshot": "", - "latest_version": "1.4.0" + "latest_version": "1.5.0" }, { "id": "soccer-scoreboard", @@ -325,10 +325,10 @@ "plugin_path": "plugins/soccer-scoreboard", "stars": 0, "downloads": 0, - "last_updated": "2026-02-15", + "last_updated": "2026-02-24", "verified": true, "screenshot": "", - "latest_version": "1.3.1" + "latest_version": "1.4.0" }, { "id": "odds-ticker", @@ -644,10 +644,10 @@ "plugin_path": "plugins/f1-scoreboard", "stars": 0, "downloads": 0, - "last_updated": "2026-02-18", + "last_updated": "2026-02-24", "verified": true, "screenshot": "", - "latest_version": "1.1.0" + "latest_version": "1.2.0" }, { "id": "web-ui-info", diff --git a/plugins/baseball-scoreboard/config_schema.json b/plugins/baseball-scoreboard/config_schema.json index 8f48d77..53f867b 100644 --- a/plugins/baseball-scoreboard/config_schema.json +++ b/plugins/baseball-scoreboard/config_schema.json @@ -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." } } }, @@ -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." } } }, @@ -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." } } }, diff --git a/plugins/baseball-scoreboard/manifest.json b/plugins/baseball-scoreboard/manifest.json index c043e32..ae45388 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.4.0", + "version": "1.5.0", "author": "ChuckBuilds", "description": "Live, recent, and upcoming baseball games across MLB, MiLB, and NCAA Baseball with real-time scores and schedules", "category": "sports", diff --git a/plugins/baseball-scoreboard/scroll_display.py b/plugins/baseball-scoreboard/scroll_display.py index ad3dbeb..196062a 100644 --- a/plugins/baseball-scoreboard/scroll_display.py +++ b/plugins/baseball-scoreboard/scroll_display.py @@ -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 @@ -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, @@ -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) # Pass rankings cache to renderer if available if renderer and rankings_cache: diff --git a/plugins/basketball-scoreboard/config_schema.json b/plugins/basketball-scoreboard/config_schema.json index 3dcd341..e752e39 100644 --- a/plugins/basketball-scoreboard/config_schema.json +++ b/plugins/basketball-scoreboard/config_schema.json @@ -151,6 +151,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." } } }, @@ -463,6 +470,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." } } }, @@ -775,6 +789,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." } } }, @@ -1120,6 +1141,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." } } }, diff --git a/plugins/basketball-scoreboard/manifest.json b/plugins/basketball-scoreboard/manifest.json index 62fe4f3..1209723 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.4.0", + "version": "1.5.0", "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", "category": "sports", diff --git a/plugins/basketball-scoreboard/scroll_display.py b/plugins/basketball-scoreboard/scroll_display.py index 20c3fdd..fb334fe 100644 --- a/plugins/basketball-scoreboard/scroll_display.py +++ b/plugins/basketball-scoreboard/scroll_display.py @@ -179,9 +179,10 @@ def _get_scroll_settings(self, league: 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 if league: league_config = self.config.get(league, {}) @@ -330,15 +331,17 @@ 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) # Verify GameRenderer is available if GameRenderer is None: self.logger.error("GameRenderer not available - cannot prepare scroll content") return False - # Create game renderer + # Create 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 = GameRenderer( - self.display_width, + game_card_width, self.display_height, self.config, logo_cache=self._logo_cache, diff --git a/plugins/f1-scoreboard/config_schema.json b/plugins/f1-scoreboard/config_schema.json index 4b9b2e9..f3af9fe 100644 --- a/plugins/f1-scoreboard/config_schema.json +++ b/plugins/f1-scoreboard/config_schema.json @@ -320,6 +320,38 @@ "x-propertyOrder": ["enabled", "max_duration_seconds"], "additionalProperties": false }, + "scroll": { + "type": "object", + "title": "Scroll Settings", + "description": "Settings for scroll display mode", + "properties": { + "game_card_width": { + "type": "integer", + "title": "Card Width", + "default": 128, + "minimum": 32, + "maximum": 512, + "description": "Width of each card in scroll mode (pixels). Default 128. Set lower on multi-panel chains to show more cards simultaneously." + }, + "scroll_speed": { + "type": "number", + "title": "Scroll Speed", + "default": 1, + "minimum": 0.01, + "maximum": 10, + "description": "Pixels per frame to scroll (frame-based mode)" + }, + "scroll_delay": { + "type": "number", + "title": "Scroll Delay", + "default": 0.03, + "minimum": 0.001, + "maximum": 0.1, + "description": "Delay between scroll frames in seconds" + } + }, + "additionalProperties": false + }, "customization": { "type": "object", "title": "Display Customization", diff --git a/plugins/f1-scoreboard/manager.py b/plugins/f1-scoreboard/manager.py index 95f4482..ef5f9a8 100644 --- a/plugins/f1-scoreboard/manager.py +++ b/plugins/f1-scoreboard/manager.py @@ -54,12 +54,22 @@ def __init__(self, plugin_id, config, display_manager, # Display duration self.display_duration = config.get("display_duration", 30) + # Scroll card width: use a fixed card width for scroll mode so cards are + # properly sized regardless of the full chain width (multi-panel setups) + scroll_cfg = config.get("scroll", {}) if isinstance(config.get("scroll"), dict) else {} + self._card_width = scroll_cfg.get("game_card_width", 128) + # Initialize components self.logo_loader = F1LogoLoader() self.data_source = F1DataSource(cache_manager, config) + # Full-width renderer for static single-card display self.renderer = F1Renderer( self.display_width, self.display_height, config, self.logo_loader, self.logger) + # Card-width renderer for scroll/Vegas mode + self._scroll_renderer = F1Renderer( + self._card_width, self.display_height, + config, self.logo_loader, self.logger) self._scroll_manager = ScrollDisplayManager( display_manager, config, self.logger) @@ -297,25 +307,26 @@ def _update_calendar(self): def _prepare_scroll_content(self): """Pre-render all scroll mode content.""" - separator = self.renderer.render_f1_separator() + r = self._scroll_renderer + separator = r.render_f1_separator() # Driver standings if self._driver_standings: - cards = [self.renderer.render_driver_standing(e) + cards = [r.render_driver_standing(e) for e in self._driver_standings] self._scroll_manager.prepare_and_display( "driver_standings", cards, separator) # Constructor standings if self._constructor_standings: - cards = [self.renderer.render_constructor_standing(e) + cards = [r.render_constructor_standing(e) for e in self._constructor_standings] self._scroll_manager.prepare_and_display( "constructor_standings", cards, separator) # Recent races if self._recent_races: - cards = [self.renderer.render_race_result(race) + cards = [r.render_race_result(race) for race in self._recent_races] self._scroll_manager.prepare_and_display( "recent_races", cards, separator) @@ -335,16 +346,16 @@ def _prepare_scroll_content(self): # Sprint if self._sprint and self._sprint.get("results"): - cards = [self.renderer.render_sprint_header( + cards = [r.render_sprint_header( self._sprint.get("race_name", ""))] for entry in self._sprint["results"]: - cards.append(self.renderer.render_sprint_entry(entry)) + cards.append(r.render_sprint_entry(entry)) self._scroll_manager.prepare_and_display( "sprint", cards, separator) # Calendar if self._calendar: - cards = [self.renderer.render_calendar_entry(e) + cards = [r.render_calendar_entry(e) for e in self._calendar] self._scroll_manager.prepare_and_display( "calendar", cards, separator) @@ -354,6 +365,7 @@ def _build_qualifying_cards(self) -> List[Image.Image]: if not self._qualifying: return [] + r = self._scroll_renderer cards = [] quali_config = self.config.get("qualifying", {}) results = self._qualifying.get("results", []) @@ -368,24 +380,25 @@ def _build_qualifying_cards(self) -> List[Image.Image]: continue # Add session header - cards.append(self.renderer.render_qualifying_header( + cards.append(r.render_qualifying_header( label, race_name)) # Add entries for this session for entry in results: # Only show entries that have a time for this session if entry.get(session_key): - cards.append(self.renderer.render_qualifying_entry( + cards.append(r.render_qualifying_entry( entry, label)) elif entry.get("eliminated_in") == label: # Show eliminated driver - cards.append(self.renderer.render_qualifying_entry( + cards.append(r.render_qualifying_entry( entry, label)) return cards def _build_practice_cards(self) -> List[Image.Image]: """Build practice result cards for all configured sessions.""" + r = self._scroll_renderer cards = [] for fp_key in ["FP3", "FP2", "FP1"]: # Most recent first @@ -393,11 +406,11 @@ def _build_practice_cards(self) -> List[Image.Image]: continue fp_data = self._practice_results[fp_key] - cards.append(self.renderer.render_practice_header( + cards.append(r.render_practice_header( fp_key, fp_data.get("circuit", ""))) for entry in fp_data.get("results", []): - cards.append(self.renderer.render_practice_entry(entry)) + cards.append(r.render_practice_entry(entry)) return cards @@ -513,9 +526,9 @@ def get_vegas_content(self) -> Optional[List[Image.Image]]: images.extend( self._scroll_manager.get_vegas_items_for_mode(mode_key)) - # Add upcoming race card if available + # Add upcoming race card if available (use scroll renderer for consistent card width) if self._upcoming_race: - upcoming_card = self.renderer.render_upcoming_race( + upcoming_card = self._scroll_renderer.render_upcoming_race( self._enrich_upcoming_with_countdown(self._upcoming_race)) images.insert(0, upcoming_card) @@ -607,9 +620,14 @@ def on_config_change(self, new_config): self.modes = self._build_enabled_modes() # Force re-render with new settings + scroll_cfg = new_config.get("scroll", {}) if isinstance(new_config.get("scroll"), dict) else {} + self._card_width = scroll_cfg.get("game_card_width", 128) self.renderer = F1Renderer( self.display_width, self.display_height, new_config, self.logo_loader, self.logger) + self._scroll_renderer = F1Renderer( + self._card_width, self.display_height, + new_config, self.logo_loader, self.logger) self._scroll_manager = ScrollDisplayManager( self.display_manager, new_config, self.logger) diff --git a/plugins/f1-scoreboard/manifest.json b/plugins/f1-scoreboard/manifest.json index 541ae01..628c8c6 100644 --- a/plugins/f1-scoreboard/manifest.json +++ b/plugins/f1-scoreboard/manifest.json @@ -1,7 +1,7 @@ { "id": "f1-scoreboard", "name": "F1 Scoreboard", - "version": "1.1.0", + "version": "1.2.0", "author": "ChuckBuilds", "class_name": "F1ScoreboardPlugin", "entry_point": "manager.py", @@ -10,7 +10,14 @@ "plugin_path": "plugins/f1-scoreboard", "description": "Formula 1 racing plugin showing driver/constructor standings, race results, qualifying breakdowns, practice standings, sprint results, upcoming races, and race calendar with team-colored displays and favorite driver/team support", "category": "sports", - "tags": ["f1", "formula1", "racing", "motorsport", "sports", "scoreboard"], + "tags": [ + "f1", + "formula1", + "racing", + "motorsport", + "sports", + "scoreboard" + ], "display_modes": [ "f1_driver_standings", "f1_constructor_standings", diff --git a/plugins/football-scoreboard/config_schema.json b/plugins/football-scoreboard/config_schema.json index 591909b..6c06503 100644 --- a/plugins/football-scoreboard/config_schema.json +++ b/plugins/football-scoreboard/config_schema.json @@ -123,6 +123,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." } } }, @@ -408,6 +415,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." } } }, diff --git a/plugins/football-scoreboard/manifest.json b/plugins/football-scoreboard/manifest.json index be94ac8..38bf923 100644 --- a/plugins/football-scoreboard/manifest.json +++ b/plugins/football-scoreboard/manifest.json @@ -1,7 +1,7 @@ { "id": "football-scoreboard", "name": "Football Scoreboard", - "version": "2.2.0", + "version": "2.3.0", "author": "ChuckBuilds", "class_name": "FootballScoreboardPlugin", "description": "Standalone plugin for live, recent, and upcoming football games across NFL and NCAA Football with real-time scores, down/distance, possession, and game status. Now with organized nested config!", diff --git a/plugins/football-scoreboard/scroll_display.py b/plugins/football-scoreboard/scroll_display.py index becaa9f..8b287b4 100644 --- a/plugins/football-scoreboard/scroll_display.py +++ b/plugins/football-scoreboard/scroll_display.py @@ -167,9 +167,10 @@ def _get_scroll_settings(self, league: 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 if league: league_config = self.config.get(league, {}) @@ -291,10 +292,12 @@ 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) - # Create game renderer + # Create 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 = GameRenderer( - self.display_width, + game_card_width, self.display_height, self.config, logo_cache=self._logo_cache, diff --git a/plugins/hockey-scoreboard/config_schema.json b/plugins/hockey-scoreboard/config_schema.json index a7f09a8..4fbe444 100644 --- a/plugins/hockey-scoreboard/config_schema.json +++ b/plugins/hockey-scoreboard/config_schema.json @@ -145,6 +145,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." } } }, @@ -486,6 +493,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." } } }, @@ -827,6 +841,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." } } }, diff --git a/plugins/hockey-scoreboard/manifest.json b/plugins/hockey-scoreboard/manifest.json index 1dd3b58..feb5912 100644 --- a/plugins/hockey-scoreboard/manifest.json +++ b/plugins/hockey-scoreboard/manifest.json @@ -1,7 +1,7 @@ { "id": "hockey-scoreboard", "name": "Hockey Scoreboard", - "version": "1.1.1", + "version": "1.2.0", "author": "ChuckBuilds", "description": "Live, recent, and upcoming hockey games across NHL, NCAA Men's, and NCAA Women's hockey with real-time scores and schedules", "homepage": "https://github.com/ChuckBuilds/ledmatrix-plugins/tree/main/plugins/hockey-scoreboard", diff --git a/plugins/hockey-scoreboard/scroll_display.py b/plugins/hockey-scoreboard/scroll_display.py index 5a49291..5490f39 100644 --- a/plugins/hockey-scoreboard/scroll_display.py +++ b/plugins/hockey-scoreboard/scroll_display.py @@ -172,7 +172,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 @@ -314,10 +315,12 @@ def prepare_scroll_content( scroll_settings = self._get_scroll_settings(primary_league) 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) - # Create game renderer + # Create 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 = GameRenderer( - self.display_width, + game_card_width, self.display_height, self.config, logo_cache=self._logo_cache, diff --git a/plugins/soccer-scoreboard/config_schema.json b/plugins/soccer-scoreboard/config_schema.json index be3dc8d..2144aae 100644 --- a/plugins/soccer-scoreboard/config_schema.json +++ b/plugins/soccer-scoreboard/config_schema.json @@ -237,6 +237,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." } } }, @@ -563,6 +570,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." } } }, @@ -889,6 +903,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." } } }, @@ -1215,6 +1236,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." } } }, @@ -1541,6 +1569,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." } } }, @@ -1867,6 +1902,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." } } }, @@ -2193,6 +2235,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." } } }, @@ -2519,6 +2568,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." } } }, diff --git a/plugins/soccer-scoreboard/manifest.json b/plugins/soccer-scoreboard/manifest.json index e52c631..b576384 100644 --- a/plugins/soccer-scoreboard/manifest.json +++ b/plugins/soccer-scoreboard/manifest.json @@ -1,7 +1,7 @@ { "id": "soccer-scoreboard", "name": "Soccer Scoreboard", - "version": "1.3.1", + "version": "1.4.0", "author": "ChuckBuilds", "description": "Live, recent, and upcoming soccer games across multiple leagues including Premier League, La Liga, Bundesliga, Serie A, Ligue 1, MLS, and more", "category": "sports", diff --git a/plugins/soccer-scoreboard/scroll_display.py b/plugins/soccer-scoreboard/scroll_display.py index 729e391..1f9c3de 100644 --- a/plugins/soccer-scoreboard/scroll_display.py +++ b/plugins/soccer-scoreboard/scroll_display.py @@ -143,6 +143,7 @@ def _get_scroll_settings(self) -> Dict[str, Any]: 'show_league_separators': scroll_config.get('show_league_separators', True), 'min_duration': scroll_config.get('min_duration', 30), 'max_duration': scroll_config.get('max_duration', 300), + 'game_card_width': scroll_config.get('game_card_width', 128), } def _configure_scroll_helper(self) -> None: @@ -323,10 +324,12 @@ 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) - # Create game renderer + # Create 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 = GameRenderer( - self.display_width, + game_card_width, self.display_height, self.config, self.plugin_dir diff --git a/plugins/ufc-scoreboard/config_schema.json b/plugins/ufc-scoreboard/config_schema.json index ab458c7..f860811 100644 --- a/plugins/ufc-scoreboard/config_schema.json +++ b/plugins/ufc-scoreboard/config_schema.json @@ -93,6 +93,51 @@ "enum": ["switch", "scroll"], "default": "switch", "description": "Display mode for upcoming fights" + }, + "scroll_settings": { + "type": "object", + "title": "Scroll Settings", + "description": "Settings for scroll display mode (when display mode is set to 'scroll')", + "properties": { + "scroll_speed": { + "type": "number", + "default": 50.0, + "minimum": 1.0, + "maximum": 200.0, + "description": "Scroll speed in pixels per second (default: 50). Higher values scroll faster." + }, + "scroll_delay": { + "type": "number", + "default": 0.01, + "minimum": 0.001, + "maximum": 0.1, + "description": "Delay between scroll frames in seconds (default: 0.01 = 100 FPS). Lower values = smoother scrolling." + }, + "gap_between_games": { + "type": "integer", + "default": 48, + "minimum": 8, + "maximum": 128, + "description": "Gap in pixels between fight cards when scrolling" + }, + "show_league_separators": { + "type": "boolean", + "default": true, + "description": "Show league icons between different leagues" + }, + "dynamic_duration": { + "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 fight card in scroll mode (pixels). Default 128. Set lower on multi-panel chains to show more fights simultaneously." + } + } } } }, diff --git a/plugins/ufc-scoreboard/manifest.json b/plugins/ufc-scoreboard/manifest.json index 2869d43..ed4ce92 100644 --- a/plugins/ufc-scoreboard/manifest.json +++ b/plugins/ufc-scoreboard/manifest.json @@ -1,7 +1,7 @@ { "id": "ufc-scoreboard", "name": "UFC Scoreboard", - "version": "1.1.1", + "version": "1.2.0", "author": "LegoGuy1000", "contributors": [ { @@ -14,7 +14,14 @@ "entry_point": "manager.py", "description": "Live, recent, and upcoming UFC fights with fighter headshots, records, odds, and fight results. Based on original work by Alex Resnick (legoguy1000).", "category": "sports", - "tags": ["ufc", "mma", "fighting", "sports", "scoreboard", "live-scores"], + "tags": [ + "ufc", + "mma", + "fighting", + "sports", + "scoreboard", + "live-scores" + ], "icon": "fas fa-fist-raised", "display_modes": [ "ufc_live", diff --git a/plugins/ufc-scoreboard/scroll_display.py b/plugins/ufc-scoreboard/scroll_display.py index 79aa94c..ae980ec 100644 --- a/plugins/ufc-scoreboard/scroll_display.py +++ b/plugins/ufc-scoreboard/scroll_display.py @@ -143,6 +143,7 @@ def _get_scroll_settings(self) -> Dict[str, Any]: "gap_between_games": 48, "show_league_separators": True, "dynamic_duration": True, + "game_card_width": 128, } ufc_config = self.config.get("ufc", {}) @@ -235,15 +236,16 @@ def prepare_and_display( scroll_settings = self._get_scroll_settings() gap_between_fights = scroll_settings.get("gap_between_games", 48) show_separators = scroll_settings.get("show_league_separators", True) + game_card_width = scroll_settings.get("game_card_width", 128) # Get display options from UFC config ufc_config = self.config.get("ufc", {}) display_options = ufc_config.get("display_options", {}) - # Reuse cached fight renderer - if self._renderer is None: + # Reuse cached fight renderer; recreate if game_card_width changed + if self._renderer is None or getattr(self._renderer, "display_width", None) != game_card_width: self._renderer = FightRenderer( - self.display_width, + game_card_width, self.display_height, self.config, headshot_cache=self._headshot_cache, From f175037a2a5bf852af90ca60b55b2ab0558c8cb9 Mon Sep 17 00:00:00 2001 From: Chuck Date: Tue, 24 Feb 2026 13:35:21 -0500 Subject: [PATCH 3/9] fix(scoreboards): address code review issues from scroll card width PR - All manifests: insert new version entry at top of versions array so versions[0].version matches the top-level version field - UFC config_schema: move scroll_settings from ufc.display_modes to ufc level so runtime config lookup (config["ufc"]["scroll_settings"]) matches the schema path - Soccer manager: update get_info() version string from 1.3.1 to 1.4.0 - F1 config_schema: add "scroll" to top-level x-propertyOrder Co-Authored-By: Claude Sonnet 4.6 --- plugins/baseball-scoreboard/manifest.json | 5 +++++ plugins/basketball-scoreboard/manifest.json | 5 +++++ plugins/f1-scoreboard/config_schema.json | 2 +- plugins/f1-scoreboard/manifest.json | 5 +++++ plugins/football-scoreboard/manifest.json | 5 +++++ plugins/hockey-scoreboard/manifest.json | 5 +++++ plugins/soccer-scoreboard/manager.py | 2 +- plugins/soccer-scoreboard/manifest.json | 5 +++++ plugins/ufc-scoreboard/config_schema.json | 8 ++++---- plugins/ufc-scoreboard/manifest.json | 5 +++++ 10 files changed, 41 insertions(+), 6 deletions(-) diff --git a/plugins/baseball-scoreboard/manifest.json b/plugins/baseball-scoreboard/manifest.json index ae45388..addebb9 100644 --- a/plugins/baseball-scoreboard/manifest.json +++ b/plugins/baseball-scoreboard/manifest.json @@ -30,6 +30,11 @@ "branch": "main", "plugin_path": "plugins/baseball-scoreboard", "versions": [ + { + "released": "2026-02-24", + "version": "1.5.0", + "ledmatrix_min": "2.0.0" + }, { "released": "2026-02-20", "version": "1.4.0", diff --git a/plugins/basketball-scoreboard/manifest.json b/plugins/basketball-scoreboard/manifest.json index 1209723..9087e61 100644 --- a/plugins/basketball-scoreboard/manifest.json +++ b/plugins/basketball-scoreboard/manifest.json @@ -18,6 +18,11 @@ "branch": "main", "plugin_path": "plugins/basketball-scoreboard", "versions": [ + { + "version": "1.5.0", + "ledmatrix_min": "2.0.0", + "released": "2026-02-24" + }, { "version": "1.4.0", "ledmatrix_min": "2.0.0", diff --git a/plugins/f1-scoreboard/config_schema.json b/plugins/f1-scoreboard/config_schema.json index f3af9fe..6571dbd 100644 --- a/plugins/f1-scoreboard/config_schema.json +++ b/plugins/f1-scoreboard/config_schema.json @@ -450,7 +450,7 @@ "additionalProperties": false } }, - "x-propertyOrder": ["enabled", "display_duration", "update_interval", "favorite_team", "favorite_driver", "driver_standings", "constructor_standings", "recent_races", "upcoming", "qualifying", "practice", "sprint", "calendar", "dynamic_duration", "customization"], + "x-propertyOrder": ["enabled", "display_duration", "update_interval", "favorite_team", "favorite_driver", "driver_standings", "constructor_standings", "recent_races", "upcoming", "qualifying", "practice", "sprint", "calendar", "dynamic_duration", "scroll", "customization"], "additionalProperties": false, "required": ["enabled"] } diff --git a/plugins/f1-scoreboard/manifest.json b/plugins/f1-scoreboard/manifest.json index 628c8c6..b6fed21 100644 --- a/plugins/f1-scoreboard/manifest.json +++ b/plugins/f1-scoreboard/manifest.json @@ -29,6 +29,11 @@ "f1_calendar" ], "versions": [ + { + "version": "1.2.0", + "ledmatrix_min": "2.0.0", + "released": "2026-02-24" + }, { "version": "1.1.0", "ledmatrix_min": "2.0.0", diff --git a/plugins/football-scoreboard/manifest.json b/plugins/football-scoreboard/manifest.json index 38bf923..6876d2f 100644 --- a/plugins/football-scoreboard/manifest.json +++ b/plugins/football-scoreboard/manifest.json @@ -24,6 +24,11 @@ "ncaa_fb_live" ], "versions": [ + { + "version": "2.3.0", + "ledmatrix_min": "2.0.0", + "released": "2026-02-24" + }, { "version": "2.2.0", "ledmatrix_min": "2.0.0", diff --git a/plugins/hockey-scoreboard/manifest.json b/plugins/hockey-scoreboard/manifest.json index feb5912..1b44634 100644 --- a/plugins/hockey-scoreboard/manifest.json +++ b/plugins/hockey-scoreboard/manifest.json @@ -55,6 +55,11 @@ } ], "versions": [ + { + "version": "1.2.0", + "ledmatrix_min": "2.0.0", + "released": "2026-02-24" + }, { "version": "1.1.1", "ledmatrix_min": "2.0.0", diff --git a/plugins/soccer-scoreboard/manager.py b/plugins/soccer-scoreboard/manager.py index 2575b7e..1dd8704 100644 --- a/plugins/soccer-scoreboard/manager.py +++ b/plugins/soccer-scoreboard/manager.py @@ -1684,7 +1684,7 @@ def get_info(self) -> Dict[str, Any]: info = { "plugin_id": self.plugin_id, "name": "Soccer Scoreboard", - "version": "1.3.1", + "version": "1.4.0", "enabled": self.is_enabled, "display_size": f"{self.display_width}x{self.display_height}", "leagues": league_info, diff --git a/plugins/soccer-scoreboard/manifest.json b/plugins/soccer-scoreboard/manifest.json index b576384..e285434 100644 --- a/plugins/soccer-scoreboard/manifest.json +++ b/plugins/soccer-scoreboard/manifest.json @@ -24,6 +24,11 @@ "soccer_upcoming" ], "versions": [ + { + "version": "1.4.0", + "ledmatrix_min": "2.0.0", + "released": "2026-02-24" + }, { "version": "1.3.1", "ledmatrix_min": "2.0.0", diff --git a/plugins/ufc-scoreboard/config_schema.json b/plugins/ufc-scoreboard/config_schema.json index f860811..89ca1d5 100644 --- a/plugins/ufc-scoreboard/config_schema.json +++ b/plugins/ufc-scoreboard/config_schema.json @@ -93,8 +93,10 @@ "enum": ["switch", "scroll"], "default": "switch", "description": "Display mode for upcoming fights" - }, - "scroll_settings": { + } + } + }, + "scroll_settings": { "type": "object", "title": "Scroll Settings", "description": "Settings for scroll display mode (when display mode is set to 'scroll')", @@ -138,8 +140,6 @@ "description": "Width of each fight card in scroll mode (pixels). Default 128. Set lower on multi-panel chains to show more fights simultaneously." } } - } - } }, "live_priority": { "type": "boolean", diff --git a/plugins/ufc-scoreboard/manifest.json b/plugins/ufc-scoreboard/manifest.json index ed4ce92..23b2b79 100644 --- a/plugins/ufc-scoreboard/manifest.json +++ b/plugins/ufc-scoreboard/manifest.json @@ -32,6 +32,11 @@ "default_duration": 15, "config_schema": "config_schema.json", "versions": [ + { + "version": "1.2.0", + "ledmatrix_min": "2.0.0", + "released": "2026-02-24" + }, { "version": "1.1.1", "ledmatrix_min": "2.0.0", From 97c47d8866b9706c3113db985cde4b102434a7f3 Mon Sep 17 00:00:00 2001 From: Chuck Date: Tue, 24 Feb 2026 15:24:49 -0500 Subject: [PATCH 4/9] fix(scroll): fix logo overflow and single-pass scrolling on multi-panel displays - Fix logo sizing in game_renderer.py (all sports) and fight_renderer.py (UFC): logos now size to display height with width capped at half the card width, preventing overflow on tall displays (e.g., 320x128 chain) - Bump all 7 plugin versions (patch) Co-Authored-By: Claude Sonnet 4.6 --- plugins.json | 14 +++++++------- plugins/baseball-scoreboard/game_renderer.py | 7 ++++--- plugins/baseball-scoreboard/manifest.json | 7 ++++++- plugins/basketball-scoreboard/game_renderer.py | 11 ++++++----- plugins/basketball-scoreboard/manifest.json | 7 ++++++- plugins/f1-scoreboard/manifest.json | 7 ++++++- plugins/football-scoreboard/game_renderer.py | 7 ++++--- plugins/football-scoreboard/manifest.json | 7 ++++++- plugins/hockey-scoreboard/game_renderer.py | 7 ++++--- plugins/hockey-scoreboard/manifest.json | 7 ++++++- plugins/soccer-scoreboard/game_renderer.py | 7 ++++--- plugins/soccer-scoreboard/manifest.json | 7 ++++++- plugins/ufc-scoreboard/fight_renderer.py | 7 ++++--- plugins/ufc-scoreboard/manifest.json | 7 ++++++- 14 files changed, 75 insertions(+), 34 deletions(-) diff --git a/plugins.json b/plugins.json index 0995742..ad9e612 100644 --- a/plugins.json +++ b/plugins.json @@ -199,7 +199,7 @@ "last_updated": "2026-02-24", "verified": true, "screenshot": "", - "latest_version": "1.2.0" + "latest_version": "1.2.1" }, { "id": "football-scoreboard", @@ -224,7 +224,7 @@ "last_updated": "2026-02-24", "verified": true, "screenshot": "", - "latest_version": "2.3.0" + "latest_version": "2.3.1" }, { "id": "ufc-scoreboard", @@ -248,7 +248,7 @@ "last_updated": "2026-02-24", "verified": true, "screenshot": "", - "latest_version": "1.2.0" + "latest_version": "1.2.1" }, { "id": "basketball-scoreboard", @@ -273,7 +273,7 @@ "last_updated": "2026-02-24", "verified": true, "screenshot": "", - "latest_version": "1.5.0" + "latest_version": "1.5.1" }, { "id": "baseball-scoreboard", @@ -299,7 +299,7 @@ "last_updated": "2026-02-24", "verified": true, "screenshot": "", - "latest_version": "1.5.0" + "latest_version": "1.5.1" }, { "id": "soccer-scoreboard", @@ -328,7 +328,7 @@ "last_updated": "2026-02-24", "verified": true, "screenshot": "", - "latest_version": "1.4.0" + "latest_version": "1.4.1" }, { "id": "odds-ticker", @@ -647,7 +647,7 @@ "last_updated": "2026-02-24", "verified": true, "screenshot": "", - "latest_version": "1.2.0" + "latest_version": "1.2.1" }, { "id": "web-ui-info", diff --git a/plugins/baseball-scoreboard/game_renderer.py b/plugins/baseball-scoreboard/game_renderer.py index 9064568..e54102f 100644 --- a/plugins/baseball-scoreboard/game_renderer.py +++ b/plugins/baseball-scoreboard/game_renderer.py @@ -105,9 +105,10 @@ 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) + # Resize logo to fill display height, capped at half card width + # so both logos never overlap the center score area + max_height = self.display_height + max_width = min(self.display_height, self.display_width // 2) logo.thumbnail((max_width, max_height), RESAMPLE_FILTER) # Copy before exiting context manager diff --git a/plugins/baseball-scoreboard/manifest.json b/plugins/baseball-scoreboard/manifest.json index addebb9..9305c2a 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.0", + "version": "1.5.1", "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-02-24", + "version": "1.5.1", + "ledmatrix_min": "2.0.0" + }, { "released": "2026-02-24", "version": "1.5.0", diff --git a/plugins/basketball-scoreboard/game_renderer.py b/plugins/basketball-scoreboard/game_renderer.py index 04183e5..246bc20 100644 --- a/plugins/basketball-scoreboard/game_renderer.py +++ b/plugins/basketball-scoreboard/game_renderer.py @@ -226,9 +226,10 @@ def _load_and_resize_logo( if img.mode != "RGBA": img = img.convert("RGBA") - # Resize to fit display - max_width = int(self.display_width * 1.5) - max_height = int(self.display_height * 1.5) + # Resize logo to fill display height, capped at half card width + # so both logos never overlap the center score area + max_height = self.display_height + max_width = min(self.display_height, self.display_width // 2) img.thumbnail((max_width, max_height), resample=RESAMPLE_FILTER) # Copy before context manager closes file handle @@ -245,8 +246,8 @@ def _load_and_resize_logo( if img.mode != "RGBA": img = img.convert("RGBA") - max_width = int(self.display_width * 1.5) - max_height = int(self.display_height * 1.5) + max_height = self.display_height + max_width = min(self.display_height, self.display_width // 2) img.thumbnail((max_width, max_height), resample=RESAMPLE_FILTER) # Copy before context manager closes file handle diff --git a/plugins/basketball-scoreboard/manifest.json b/plugins/basketball-scoreboard/manifest.json index 9087e61..878ce89 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.5.0", + "version": "1.5.1", "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", "category": "sports", @@ -18,6 +18,11 @@ "branch": "main", "plugin_path": "plugins/basketball-scoreboard", "versions": [ + { + "version": "1.5.1", + "ledmatrix_min": "2.0.0", + "released": "2026-02-24" + }, { "version": "1.5.0", "ledmatrix_min": "2.0.0", diff --git a/plugins/f1-scoreboard/manifest.json b/plugins/f1-scoreboard/manifest.json index b6fed21..245a9a6 100644 --- a/plugins/f1-scoreboard/manifest.json +++ b/plugins/f1-scoreboard/manifest.json @@ -1,7 +1,7 @@ { "id": "f1-scoreboard", "name": "F1 Scoreboard", - "version": "1.2.0", + "version": "1.2.1", "author": "ChuckBuilds", "class_name": "F1ScoreboardPlugin", "entry_point": "manager.py", @@ -29,6 +29,11 @@ "f1_calendar" ], "versions": [ + { + "version": "1.2.1", + "ledmatrix_min": "2.0.0", + "released": "2026-02-24" + }, { "version": "1.2.0", "ledmatrix_min": "2.0.0", diff --git a/plugins/football-scoreboard/game_renderer.py b/plugins/football-scoreboard/game_renderer.py index 0b4e2af..54cb1d1 100644 --- a/plugins/football-scoreboard/game_renderer.py +++ b/plugins/football-scoreboard/game_renderer.py @@ -236,9 +236,10 @@ def _load_and_resize_logo( if logo.mode != "RGBA": logo = logo.convert("RGBA") - # Resize to fit display - max_width = int(self.display_width * 1.5) - max_height = int(self.display_height * 1.5) + # Resize logo to fill display height, capped at half card width + # so both logos never overlap the center score area + max_height = self.display_height + max_width = min(self.display_height, self.display_width // 2) logo.thumbnail((max_width, max_height), Image.Resampling.LANCZOS) self._logo_cache[team_abbrev] = logo diff --git a/plugins/football-scoreboard/manifest.json b/plugins/football-scoreboard/manifest.json index 6876d2f..d8c7876 100644 --- a/plugins/football-scoreboard/manifest.json +++ b/plugins/football-scoreboard/manifest.json @@ -1,7 +1,7 @@ { "id": "football-scoreboard", "name": "Football Scoreboard", - "version": "2.3.0", + "version": "2.3.1", "author": "ChuckBuilds", "class_name": "FootballScoreboardPlugin", "description": "Standalone plugin for live, recent, and upcoming football games across NFL and NCAA Football with real-time scores, down/distance, possession, and game status. Now with organized nested config!", @@ -24,6 +24,11 @@ "ncaa_fb_live" ], "versions": [ + { + "version": "2.3.1", + "ledmatrix_min": "2.0.0", + "released": "2026-02-24" + }, { "version": "2.3.0", "ledmatrix_min": "2.0.0", diff --git a/plugins/hockey-scoreboard/game_renderer.py b/plugins/hockey-scoreboard/game_renderer.py index d3a7d46..3b3498c 100644 --- a/plugins/hockey-scoreboard/game_renderer.py +++ b/plugins/hockey-scoreboard/game_renderer.py @@ -209,9 +209,10 @@ def _load_and_resize_logo( else: logo = logo_file.copy() - # Resize to fit display (after context manager closes file) - max_width = int(self.display_width * 1.5) - max_height = int(self.display_height * 1.5) + # Resize logo to fill display height, capped at half card width + # so both logos never overlap the center score area + max_height = self.display_height + max_width = min(self.display_height, self.display_width // 2) logo.thumbnail((max_width, max_height), RESAMPLE_FILTER) self._logo_cache[cache_key] = logo diff --git a/plugins/hockey-scoreboard/manifest.json b/plugins/hockey-scoreboard/manifest.json index 1b44634..0a456b1 100644 --- a/plugins/hockey-scoreboard/manifest.json +++ b/plugins/hockey-scoreboard/manifest.json @@ -1,7 +1,7 @@ { "id": "hockey-scoreboard", "name": "Hockey Scoreboard", - "version": "1.2.0", + "version": "1.2.1", "author": "ChuckBuilds", "description": "Live, recent, and upcoming hockey games across NHL, NCAA Men's, and NCAA Women's hockey with real-time scores and schedules", "homepage": "https://github.com/ChuckBuilds/ledmatrix-plugins/tree/main/plugins/hockey-scoreboard", @@ -55,6 +55,11 @@ } ], "versions": [ + { + "version": "1.2.1", + "ledmatrix_min": "2.0.0", + "released": "2026-02-24" + }, { "version": "1.2.0", "ledmatrix_min": "2.0.0", diff --git a/plugins/soccer-scoreboard/game_renderer.py b/plugins/soccer-scoreboard/game_renderer.py index 4fa2fdb..09725a8 100644 --- a/plugins/soccer-scoreboard/game_renderer.py +++ b/plugins/soccer-scoreboard/game_renderer.py @@ -180,9 +180,10 @@ def _load_and_resize_logo( if logo.mode != "RGBA": logo = logo.convert("RGBA") - # Resize to fit display - max_width = int(self.display_width * 1.5) - max_height = int(self.display_height * 1.5) + # Resize logo to fill display height, capped at half card width + # so both logos never overlap the center score area + max_height = self.display_height + max_width = min(self.display_height, self.display_width // 2) logo.thumbnail((max_width, max_height), Image.Resampling.LANCZOS) self._logo_cache[team_abbrev] = logo diff --git a/plugins/soccer-scoreboard/manifest.json b/plugins/soccer-scoreboard/manifest.json index e285434..300998b 100644 --- a/plugins/soccer-scoreboard/manifest.json +++ b/plugins/soccer-scoreboard/manifest.json @@ -1,7 +1,7 @@ { "id": "soccer-scoreboard", "name": "Soccer Scoreboard", - "version": "1.4.0", + "version": "1.4.1", "author": "ChuckBuilds", "description": "Live, recent, and upcoming soccer games across multiple leagues including Premier League, La Liga, Bundesliga, Serie A, Ligue 1, MLS, and more", "category": "sports", @@ -24,6 +24,11 @@ "soccer_upcoming" ], "versions": [ + { + "version": "1.4.1", + "ledmatrix_min": "2.0.0", + "released": "2026-02-24" + }, { "version": "1.4.0", "ledmatrix_min": "2.0.0", diff --git a/plugins/ufc-scoreboard/fight_renderer.py b/plugins/ufc-scoreboard/fight_renderer.py index 711bd68..739d34c 100644 --- a/plugins/ufc-scoreboard/fight_renderer.py +++ b/plugins/ufc-scoreboard/fight_renderer.py @@ -164,9 +164,10 @@ def _load_headshot( if img.mode != "RGBA": img = img.convert("RGBA") - # Scale headshot to fit display height - max_width = int(self.display_width * 1.5) - max_height = int(self.display_height * 1.5) + # Scale headshot to fill display height, capped at half card width + # so both fighters never overlap the center text area + max_height = self.display_height + max_width = min(self.display_height, self.display_width // 2) img.thumbnail((max_width, max_height), LANCZOS) img.load() # Ensure pixel data is loaded before closing file self._headshot_cache[fighter_id] = img diff --git a/plugins/ufc-scoreboard/manifest.json b/plugins/ufc-scoreboard/manifest.json index 23b2b79..fdb335d 100644 --- a/plugins/ufc-scoreboard/manifest.json +++ b/plugins/ufc-scoreboard/manifest.json @@ -1,7 +1,7 @@ { "id": "ufc-scoreboard", "name": "UFC Scoreboard", - "version": "1.2.0", + "version": "1.2.1", "author": "LegoGuy1000", "contributors": [ { @@ -32,6 +32,11 @@ "default_duration": 15, "config_schema": "config_schema.json", "versions": [ + { + "version": "1.2.1", + "ledmatrix_min": "2.0.0", + "released": "2026-02-24" + }, { "version": "1.2.0", "ledmatrix_min": "2.0.0", From a4cebeb52dcfca0218a4aee2929abaa589927683 Mon Sep 17 00:00:00 2001 From: Chuck Date: Tue, 24 Feb 2026 15:31:01 -0500 Subject: [PATCH 5/9] fix(scroll): correct logo sizing to fill full display height Previously capped max_width at card_width // 2 which incorrectly limited logo height to half of display_height on square logos. Now capped at card_width so logos fill the full display height without overflowing the card canvas. Co-Authored-By: Claude Sonnet 4.6 --- plugins/baseball-scoreboard/game_renderer.py | 5 ++--- plugins/basketball-scoreboard/game_renderer.py | 7 +++---- plugins/football-scoreboard/game_renderer.py | 5 ++--- plugins/hockey-scoreboard/game_renderer.py | 5 ++--- plugins/soccer-scoreboard/game_renderer.py | 5 ++--- plugins/ufc-scoreboard/fight_renderer.py | 5 ++--- 6 files changed, 13 insertions(+), 19 deletions(-) diff --git a/plugins/baseball-scoreboard/game_renderer.py b/plugins/baseball-scoreboard/game_renderer.py index e54102f..ecae733 100644 --- a/plugins/baseball-scoreboard/game_renderer.py +++ b/plugins/baseball-scoreboard/game_renderer.py @@ -105,10 +105,9 @@ def _load_and_resize_logo(self, league: str, team_abbrev: str) -> Optional[Image if logo.mode != 'RGBA': logo = logo.convert('RGBA') - # Resize logo to fill display height, capped at half card width - # so both logos never overlap the center score area + # Resize logo to fill display height, capped at card width to prevent overflow max_height = self.display_height - max_width = min(self.display_height, self.display_width // 2) + max_width = self.display_width logo.thumbnail((max_width, max_height), RESAMPLE_FILTER) # Copy before exiting context manager diff --git a/plugins/basketball-scoreboard/game_renderer.py b/plugins/basketball-scoreboard/game_renderer.py index 246bc20..3e8e00b 100644 --- a/plugins/basketball-scoreboard/game_renderer.py +++ b/plugins/basketball-scoreboard/game_renderer.py @@ -226,10 +226,9 @@ def _load_and_resize_logo( if img.mode != "RGBA": img = img.convert("RGBA") - # Resize logo to fill display height, capped at half card width - # so both logos never overlap the center score area + # Resize logo to fill display height, capped at card width to prevent overflow max_height = self.display_height - max_width = min(self.display_height, self.display_width // 2) + max_width = self.display_width img.thumbnail((max_width, max_height), resample=RESAMPLE_FILTER) # Copy before context manager closes file handle @@ -247,7 +246,7 @@ def _load_and_resize_logo( img = img.convert("RGBA") max_height = self.display_height - max_width = min(self.display_height, self.display_width // 2) + max_width = self.display_width img.thumbnail((max_width, max_height), resample=RESAMPLE_FILTER) # Copy before context manager closes file handle diff --git a/plugins/football-scoreboard/game_renderer.py b/plugins/football-scoreboard/game_renderer.py index 54cb1d1..ceb0011 100644 --- a/plugins/football-scoreboard/game_renderer.py +++ b/plugins/football-scoreboard/game_renderer.py @@ -236,10 +236,9 @@ def _load_and_resize_logo( if logo.mode != "RGBA": logo = logo.convert("RGBA") - # Resize logo to fill display height, capped at half card width - # so both logos never overlap the center score area + # Resize logo to fill display height, capped at card width to prevent overflow max_height = self.display_height - max_width = min(self.display_height, self.display_width // 2) + max_width = self.display_width logo.thumbnail((max_width, max_height), Image.Resampling.LANCZOS) self._logo_cache[team_abbrev] = logo diff --git a/plugins/hockey-scoreboard/game_renderer.py b/plugins/hockey-scoreboard/game_renderer.py index 3b3498c..4bd26ff 100644 --- a/plugins/hockey-scoreboard/game_renderer.py +++ b/plugins/hockey-scoreboard/game_renderer.py @@ -209,10 +209,9 @@ def _load_and_resize_logo( else: logo = logo_file.copy() - # Resize logo to fill display height, capped at half card width - # so both logos never overlap the center score area + # Resize logo to fill display height, capped at card width to prevent overflow max_height = self.display_height - max_width = min(self.display_height, self.display_width // 2) + max_width = self.display_width logo.thumbnail((max_width, max_height), RESAMPLE_FILTER) self._logo_cache[cache_key] = logo diff --git a/plugins/soccer-scoreboard/game_renderer.py b/plugins/soccer-scoreboard/game_renderer.py index 09725a8..be1ff23 100644 --- a/plugins/soccer-scoreboard/game_renderer.py +++ b/plugins/soccer-scoreboard/game_renderer.py @@ -180,10 +180,9 @@ def _load_and_resize_logo( if logo.mode != "RGBA": logo = logo.convert("RGBA") - # Resize logo to fill display height, capped at half card width - # so both logos never overlap the center score area + # Resize logo to fill display height, capped at card width to prevent overflow max_height = self.display_height - max_width = min(self.display_height, self.display_width // 2) + max_width = self.display_width logo.thumbnail((max_width, max_height), Image.Resampling.LANCZOS) self._logo_cache[team_abbrev] = logo diff --git a/plugins/ufc-scoreboard/fight_renderer.py b/plugins/ufc-scoreboard/fight_renderer.py index 739d34c..f03c103 100644 --- a/plugins/ufc-scoreboard/fight_renderer.py +++ b/plugins/ufc-scoreboard/fight_renderer.py @@ -164,10 +164,9 @@ def _load_headshot( if img.mode != "RGBA": img = img.convert("RGBA") - # Scale headshot to fill display height, capped at half card width - # so both fighters never overlap the center text area + # Scale headshot to fill display height, capped at card width to prevent overflow max_height = self.display_height - max_width = min(self.display_height, self.display_width // 2) + max_width = self.display_width img.thumbnail((max_width, max_height), LANCZOS) img.load() # Ensure pixel data is loaded before closing file self._headshot_cache[fighter_id] = img From 70664b829069e9e128b16a6004484e49c69f3035 Mon Sep 17 00:00:00 2001 From: Chuck Date: Tue, 24 Feb 2026 15:36:18 -0500 Subject: [PATCH 6/9] =?UTF-8?q?fix(scroll):=20size=20logos=20proportionall?= =?UTF-8?q?y=20=E2=80=94=20min(height,=20card=5Fwidth//3)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Logos now scale with display height but are capped at 1/3 of card width, keeping scores readable on tall displays (128x64, 128x128) while maintaining the same appearance on standard 128x32 single-panel setups. Co-Authored-By: Claude Sonnet 4.6 --- plugins/baseball-scoreboard/game_renderer.py | 8 ++++---- plugins/basketball-scoreboard/game_renderer.py | 13 ++++++------- plugins/football-scoreboard/game_renderer.py | 8 ++++---- plugins/hockey-scoreboard/game_renderer.py | 8 ++++---- plugins/soccer-scoreboard/game_renderer.py | 8 ++++---- plugins/ufc-scoreboard/fight_renderer.py | 8 ++++---- 6 files changed, 26 insertions(+), 27 deletions(-) diff --git a/plugins/baseball-scoreboard/game_renderer.py b/plugins/baseball-scoreboard/game_renderer.py index ecae733..89830dc 100644 --- a/plugins/baseball-scoreboard/game_renderer.py +++ b/plugins/baseball-scoreboard/game_renderer.py @@ -105,10 +105,10 @@ def _load_and_resize_logo(self, league: str, team_abbrev: str) -> Optional[Image if logo.mode != 'RGBA': logo = logo.convert('RGBA') - # Resize logo to fill display height, capped at card width to prevent overflow - max_height = self.display_height - max_width = self.display_width - logo.thumbnail((max_width, max_height), RESAMPLE_FILTER) + # Size logo proportionally: scale with height but cap at 1/3 card width + # so scores remain readable on tall displays (128x64, 128x128, etc.) + logo_size = min(self.display_height, self.display_width // 3) + logo.thumbnail((logo_size, logo_size), RESAMPLE_FILTER) # Copy before exiting context manager cached_logo = logo.copy() diff --git a/plugins/basketball-scoreboard/game_renderer.py b/plugins/basketball-scoreboard/game_renderer.py index 3e8e00b..6f5e54b 100644 --- a/plugins/basketball-scoreboard/game_renderer.py +++ b/plugins/basketball-scoreboard/game_renderer.py @@ -226,10 +226,10 @@ def _load_and_resize_logo( if img.mode != "RGBA": img = img.convert("RGBA") - # Resize logo to fill display height, capped at card width to prevent overflow - max_height = self.display_height - max_width = self.display_width - img.thumbnail((max_width, max_height), resample=RESAMPLE_FILTER) + # Size logo proportionally: scale with height but cap at 1/3 card width + # so scores remain readable on tall displays (128x64, 128x128, etc.) + logo_size = min(self.display_height, self.display_width // 3) + img.thumbnail((logo_size, logo_size), resample=RESAMPLE_FILTER) # Copy before context manager closes file handle logo = img.copy() @@ -245,9 +245,8 @@ def _load_and_resize_logo( if img.mode != "RGBA": img = img.convert("RGBA") - max_height = self.display_height - max_width = self.display_width - img.thumbnail((max_width, max_height), resample=RESAMPLE_FILTER) + logo_size = min(self.display_height, self.display_width // 3) + img.thumbnail((logo_size, logo_size), resample=RESAMPLE_FILTER) # Copy before context manager closes file handle logo = img.copy() diff --git a/plugins/football-scoreboard/game_renderer.py b/plugins/football-scoreboard/game_renderer.py index ceb0011..f2005b6 100644 --- a/plugins/football-scoreboard/game_renderer.py +++ b/plugins/football-scoreboard/game_renderer.py @@ -236,10 +236,10 @@ def _load_and_resize_logo( if logo.mode != "RGBA": logo = logo.convert("RGBA") - # Resize logo to fill display height, capped at card width to prevent overflow - max_height = self.display_height - max_width = self.display_width - logo.thumbnail((max_width, max_height), Image.Resampling.LANCZOS) + # Size logo proportionally: scale with height but cap at 1/3 card width + # so scores remain readable on tall displays (128x64, 128x128, etc.) + logo_size = min(self.display_height, self.display_width // 3) + logo.thumbnail((logo_size, logo_size), Image.Resampling.LANCZOS) self._logo_cache[team_abbrev] = logo return logo diff --git a/plugins/hockey-scoreboard/game_renderer.py b/plugins/hockey-scoreboard/game_renderer.py index 4bd26ff..a8a91b5 100644 --- a/plugins/hockey-scoreboard/game_renderer.py +++ b/plugins/hockey-scoreboard/game_renderer.py @@ -209,10 +209,10 @@ def _load_and_resize_logo( else: logo = logo_file.copy() - # Resize logo to fill display height, capped at card width to prevent overflow - max_height = self.display_height - max_width = self.display_width - logo.thumbnail((max_width, max_height), RESAMPLE_FILTER) + # Size logo proportionally: scale with height but cap at 1/3 card width + # so scores remain readable on tall displays (128x64, 128x128, etc.) + logo_size = min(self.display_height, self.display_width // 3) + logo.thumbnail((logo_size, logo_size), RESAMPLE_FILTER) self._logo_cache[cache_key] = logo return logo diff --git a/plugins/soccer-scoreboard/game_renderer.py b/plugins/soccer-scoreboard/game_renderer.py index be1ff23..70b1e1f 100644 --- a/plugins/soccer-scoreboard/game_renderer.py +++ b/plugins/soccer-scoreboard/game_renderer.py @@ -180,10 +180,10 @@ def _load_and_resize_logo( if logo.mode != "RGBA": logo = logo.convert("RGBA") - # Resize logo to fill display height, capped at card width to prevent overflow - max_height = self.display_height - max_width = self.display_width - logo.thumbnail((max_width, max_height), Image.Resampling.LANCZOS) + # Size logo proportionally: scale with height but cap at 1/3 card width + # so scores remain readable on tall displays (128x64, 128x128, etc.) + logo_size = min(self.display_height, self.display_width // 3) + logo.thumbnail((logo_size, logo_size), Image.Resampling.LANCZOS) self._logo_cache[team_abbrev] = logo return logo diff --git a/plugins/ufc-scoreboard/fight_renderer.py b/plugins/ufc-scoreboard/fight_renderer.py index f03c103..93e1906 100644 --- a/plugins/ufc-scoreboard/fight_renderer.py +++ b/plugins/ufc-scoreboard/fight_renderer.py @@ -164,10 +164,10 @@ def _load_headshot( if img.mode != "RGBA": img = img.convert("RGBA") - # Scale headshot to fill display height, capped at card width to prevent overflow - max_height = self.display_height - max_width = self.display_width - img.thumbnail((max_width, max_height), LANCZOS) + # Size headshot proportionally: scale with height but cap at 1/3 card width + # so fighter info remains readable on tall displays + logo_size = min(self.display_height, self.display_width // 3) + img.thumbnail((logo_size, logo_size), LANCZOS) img.load() # Ensure pixel data is loaded before closing file self._headshot_cache[fighter_id] = img return img From afb4fbdbb164a0465b165ff9868d0c140538bac6 Mon Sep 17 00:00:00 2001 From: Chuck Date: Tue, 24 Feb 2026 16:28:09 -0500 Subject: [PATCH 7/9] fix(logos): crop transparent padding and scale to full display height Replace min(display_height, card_width//3) thumbnail with crop-to-ink then thumbnail into a display_height square box. This removes the transparent canvas padding around logos before scaling, so the actual artwork fills the full display height. Each logo is then centered within a display_height-wide slot on its side of the card, giving consistent card geometry regardless of logo aspect ratio. Affects: football, baseball, basketball, hockey, soccer, ufc scoreboards. Co-Authored-By: Claude Sonnet 4.6 --- plugins.json | 12 +++---- plugins/baseball-scoreboard/game_renderer.py | 32 +++++++++++------ plugins/baseball-scoreboard/manifest.json | 11 ++++-- .../basketball-scoreboard/game_renderer.py | 25 +++++++------ plugins/basketball-scoreboard/manifest.json | 11 ++++-- plugins/football-scoreboard/game_renderer.py | 27 ++++++++------ plugins/football-scoreboard/manifest.json | 11 ++++-- plugins/hockey-scoreboard/game_renderer.py | 25 +++++++------ plugins/hockey-scoreboard/manifest.json | 11 ++++-- plugins/soccer-scoreboard/game_renderer.py | 35 ++++++++----------- plugins/soccer-scoreboard/manifest.json | 11 ++++-- plugins/ufc-scoreboard/fight_renderer.py | 10 +++--- plugins/ufc-scoreboard/manifest.json | 11 ++++-- 13 files changed, 143 insertions(+), 89 deletions(-) diff --git a/plugins.json b/plugins.json index ad9e612..9845937 100644 --- a/plugins.json +++ b/plugins.json @@ -199,7 +199,7 @@ "last_updated": "2026-02-24", "verified": true, "screenshot": "", - "latest_version": "1.2.1" + "latest_version": "1.2.2" }, { "id": "football-scoreboard", @@ -224,7 +224,7 @@ "last_updated": "2026-02-24", "verified": true, "screenshot": "", - "latest_version": "2.3.1" + "latest_version": "2.3.2" }, { "id": "ufc-scoreboard", @@ -248,7 +248,7 @@ "last_updated": "2026-02-24", "verified": true, "screenshot": "", - "latest_version": "1.2.1" + "latest_version": "1.2.2" }, { "id": "basketball-scoreboard", @@ -273,7 +273,7 @@ "last_updated": "2026-02-24", "verified": true, "screenshot": "", - "latest_version": "1.5.1" + "latest_version": "1.5.2" }, { "id": "baseball-scoreboard", @@ -299,7 +299,7 @@ "last_updated": "2026-02-24", "verified": true, "screenshot": "", - "latest_version": "1.5.1" + "latest_version": "1.5.2" }, { "id": "soccer-scoreboard", @@ -328,7 +328,7 @@ "last_updated": "2026-02-24", "verified": true, "screenshot": "", - "latest_version": "1.4.1" + "latest_version": "1.4.2" }, { "id": "odds-ticker", diff --git a/plugins/baseball-scoreboard/game_renderer.py b/plugins/baseball-scoreboard/game_renderer.py index 89830dc..c84fca1 100644 --- a/plugins/baseball-scoreboard/game_renderer.py +++ b/plugins/baseball-scoreboard/game_renderer.py @@ -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') - # Size logo proportionally: scale with height but cap at 1/3 card width - # so scores remain readable on tall displays (128x64, 128x128, etc.) - logo_size = min(self.display_height, self.display_width // 3) - logo.thumbnail((logo_size, logo_size), 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() @@ -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 = self.display_height + 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') @@ -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 = self.display_height + 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" @@ -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 = self.display_height + 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'] diff --git a/plugins/baseball-scoreboard/manifest.json b/plugins/baseball-scoreboard/manifest.json index 9305c2a..32f4f13 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.1", + "version": "1.5.2", "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": [ + { + "version": "1.5.2", + "ledmatrix_min": "2.0.0", + "released": "2026-02-24" + }, { "released": "2026-02-24", "version": "1.5.1", @@ -86,11 +91,11 @@ "ledmatrix_min": "2.0.0" } ], - "last_updated": "2026-02-20", + "last_updated": "2026-02-24", "stars": 0, "downloads": 0, "verified": true, "screenshot": "", "class_name": "BaseballScoreboardPlugin", "entry_point": "manager.py" -} +} \ No newline at end of file diff --git a/plugins/basketball-scoreboard/game_renderer.py b/plugins/basketball-scoreboard/game_renderer.py index 6f5e54b..b3ca9ac 100644 --- a/plugins/basketball-scoreboard/game_renderer.py +++ b/plugins/basketball-scoreboard/game_renderer.py @@ -226,10 +226,13 @@ def _load_and_resize_logo( if img.mode != "RGBA": img = img.convert("RGBA") - # Size logo proportionally: scale with height but cap at 1/3 card width - # so scores remain readable on tall displays (128x64, 128x128, etc.) - logo_size = min(self.display_height, self.display_width // 3) - img.thumbnail((logo_size, logo_size), resample=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 = img.getbbox() + if bbox: + img = img.crop(bbox) + img.thumbnail((self.display_height, self.display_height), resample=RESAMPLE_FILTER) # Copy before context manager closes file handle logo = img.copy() @@ -333,14 +336,16 @@ def render_game_card( center_y = self.display_height // 2 - # Draw logos - home_x = self.display_width - home_logo.width - home_y = center_y - (home_logo.height // 2) - main_img.paste(home_logo, (home_x, home_y), home_logo) - - away_x = 0 + # Draw logos — each centered within a display_height-wide slot on its side + logo_slot = self.display_height + away_x = (logo_slot - away_logo.width) // 2 away_y = center_y - (away_logo.height // 2) main_img.paste(away_logo, (away_x, away_y), away_logo) + + home_slot_start = self.display_width - logo_slot + home_x = home_slot_start + (logo_slot - home_logo.width) // 2 + home_y = center_y - (home_logo.height // 2) + main_img.paste(home_logo, (home_x, home_y), home_logo) # Draw scores (centered) home_score = str(game.get("home_score", "0")) diff --git a/plugins/basketball-scoreboard/manifest.json b/plugins/basketball-scoreboard/manifest.json index 878ce89..0c3bd16 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.5.1", + "version": "1.5.2", "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", "category": "sports", @@ -18,6 +18,11 @@ "branch": "main", "plugin_path": "plugins/basketball-scoreboard", "versions": [ + { + "version": "1.5.2", + "ledmatrix_min": "2.0.0", + "released": "2026-02-24" + }, { "version": "1.5.1", "ledmatrix_min": "2.0.0", @@ -56,7 +61,7 @@ ], "stars": 0, "downloads": 0, - "last_updated": "2026-02-20", + "last_updated": "2026-02-24", "verified": true, "screenshot": "", "display_modes": [ @@ -76,4 +81,4 @@ "dependencies": {}, "entry_point": "manager.py", "class_name": "BasketballScoreboardPlugin" -} +} \ No newline at end of file diff --git a/plugins/football-scoreboard/game_renderer.py b/plugins/football-scoreboard/game_renderer.py index f2005b6..b137444 100644 --- a/plugins/football-scoreboard/game_renderer.py +++ b/plugins/football-scoreboard/game_renderer.py @@ -236,11 +236,14 @@ def _load_and_resize_logo( if logo.mode != "RGBA": logo = logo.convert("RGBA") - # Size logo proportionally: scale with height but cap at 1/3 card width - # so scores remain readable on tall displays (128x64, 128x128, etc.) - logo_size = min(self.display_height, self.display_width // 3) - logo.thumbnail((logo_size, logo_size), Image.Resampling.LANCZOS) - + # 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), Image.Resampling.LANCZOS) + self._logo_cache[team_abbrev] = logo return logo else: @@ -325,14 +328,16 @@ def render_game_card( center_y = self.display_height // 2 - # Draw logos - home_x = self.display_width - home_logo.width - home_y = center_y - (home_logo.height // 2) - main_img.paste(home_logo, (home_x, home_y), home_logo) - - away_x = 0 + # Draw logos — each centered within a display_height-wide slot on its side + logo_slot = self.display_height + away_x = (logo_slot - away_logo.width) // 2 away_y = center_y - (away_logo.height // 2) main_img.paste(away_logo, (away_x, away_y), away_logo) + + home_slot_start = self.display_width - logo_slot + home_x = home_slot_start + (logo_slot - home_logo.width) // 2 + home_y = center_y - (home_logo.height // 2) + main_img.paste(home_logo, (home_x, home_y), home_logo) # Draw scores (centered) home_score = str(game.get("home_score", "0")) diff --git a/plugins/football-scoreboard/manifest.json b/plugins/football-scoreboard/manifest.json index d8c7876..a4c89d8 100644 --- a/plugins/football-scoreboard/manifest.json +++ b/plugins/football-scoreboard/manifest.json @@ -1,7 +1,7 @@ { "id": "football-scoreboard", "name": "Football Scoreboard", - "version": "2.3.1", + "version": "2.3.2", "author": "ChuckBuilds", "class_name": "FootballScoreboardPlugin", "description": "Standalone plugin for live, recent, and upcoming football games across NFL and NCAA Football with real-time scores, down/distance, possession, and game status. Now with organized nested config!", @@ -24,6 +24,11 @@ "ncaa_fb_live" ], "versions": [ + { + "version": "2.3.2", + "ledmatrix_min": "2.0.0", + "released": "2026-02-24" + }, { "version": "2.3.1", "ledmatrix_min": "2.0.0", @@ -225,11 +230,11 @@ "ledmatrix_min": "2.0.0" } ], - "last_updated": "2026-02-20", + "last_updated": "2026-02-24", "stars": 0, "downloads": 0, "verified": true, "screenshot": "", "config_schema": "config_schema.json", "entry_point": "manager.py" -} +} \ No newline at end of file diff --git a/plugins/hockey-scoreboard/game_renderer.py b/plugins/hockey-scoreboard/game_renderer.py index a8a91b5..0b999d2 100644 --- a/plugins/hockey-scoreboard/game_renderer.py +++ b/plugins/hockey-scoreboard/game_renderer.py @@ -209,10 +209,13 @@ def _load_and_resize_logo( else: logo = logo_file.copy() - # Size logo proportionally: scale with height but cap at 1/3 card width - # so scores remain readable on tall displays (128x64, 128x128, etc.) - logo_size = min(self.display_height, self.display_width // 3) - logo.thumbnail((logo_size, logo_size), 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) self._logo_cache[cache_key] = logo return logo @@ -364,15 +367,17 @@ def render_game_card( center_y = self.display_height // 2 - # Draw logos (flush with card edges to avoid clipping in scroll/Vegas mode) - home_x = self.display_width - home_logo.width - home_y = center_y - (home_logo.height // 2) - main_img.paste(home_logo, (home_x, home_y), home_logo) - - away_x = 0 + # Draw logos — each centered within a display_height-wide slot on its side + logo_slot = self.display_height + away_x = (logo_slot - away_logo.width) // 2 away_y = center_y - (away_logo.height // 2) main_img.paste(away_logo, (away_x, away_y), away_logo) + home_slot_start = self.display_width - logo_slot + home_x = home_slot_start + (logo_slot - home_logo.width) // 2 + home_y = center_y - (home_logo.height // 2) + main_img.paste(home_logo, (home_x, home_y), home_logo) + # Draw scores (centered) - only for live and recent games if game_type in ("live", "recent"): home_score = str(home_team.get("score", "0")) diff --git a/plugins/hockey-scoreboard/manifest.json b/plugins/hockey-scoreboard/manifest.json index 0a456b1..0ee2b18 100644 --- a/plugins/hockey-scoreboard/manifest.json +++ b/plugins/hockey-scoreboard/manifest.json @@ -1,7 +1,7 @@ { "id": "hockey-scoreboard", "name": "Hockey Scoreboard", - "version": "1.2.1", + "version": "1.2.2", "author": "ChuckBuilds", "description": "Live, recent, and upcoming hockey games across NHL, NCAA Men's, and NCAA Women's hockey with real-time scores and schedules", "homepage": "https://github.com/ChuckBuilds/ledmatrix-plugins/tree/main/plugins/hockey-scoreboard", @@ -55,6 +55,11 @@ } ], "versions": [ + { + "version": "1.2.2", + "ledmatrix_min": "2.0.0", + "released": "2026-02-24" + }, { "version": "1.2.1", "ledmatrix_min": "2.0.0", @@ -116,9 +121,9 @@ "ledmatrix_min": "2.0.0" } ], - "last_updated": "2026-02-15", + "last_updated": "2026-02-24", "stars": 0, "downloads": 0, "verified": true, "screenshot": "" -} +} \ No newline at end of file diff --git a/plugins/soccer-scoreboard/game_renderer.py b/plugins/soccer-scoreboard/game_renderer.py index 70b1e1f..60c01fd 100644 --- a/plugins/soccer-scoreboard/game_renderer.py +++ b/plugins/soccer-scoreboard/game_renderer.py @@ -180,11 +180,14 @@ def _load_and_resize_logo( if logo.mode != "RGBA": logo = logo.convert("RGBA") - # Size logo proportionally: scale with height but cap at 1/3 card width - # so scores remain readable on tall displays (128x64, 128x128, etc.) - logo_size = min(self.display_height, self.display_width // 3) - logo.thumbnail((logo_size, logo_size), Image.Resampling.LANCZOS) - + # 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), Image.Resampling.LANCZOS) + self._logo_cache[team_abbrev] = logo return logo else: @@ -326,23 +329,15 @@ def render_game_card( ) return main_img.convert('RGB') - # Calculate maximum dimensions for each logo based on available space - away_max_width, away_max_height = self._calculate_max_logo_dimensions(score_width, 'away') - home_max_width, home_max_height = self._calculate_max_logo_dimensions(score_width, 'home') - - # Resize logos to fit within their allocated space - away_logo = self._resize_logo_to_fit(away_logo, away_max_width, away_max_height) - home_logo = self._resize_logo_to_fit(home_logo, home_max_width, home_max_height) - center_y = self.display_height // 2 - - # Calculate logo positions - ensure they stay on screen - # Away logo: positioned from left edge with padding - away_x = 10 + + # Place logos — each centered within a display_height-wide slot on its side + logo_slot = self.display_height + away_x = (logo_slot - away_logo.width) // 2 away_y = center_y - (away_logo.height // 2) - - # Home logo: positioned from right edge with padding - home_x = self.display_width - home_logo.width - 10 + + home_slot_start = self.display_width - logo_slot + home_x = home_slot_start + (logo_slot - home_logo.width) // 2 home_y = center_y - (home_logo.height // 2) # Draw logos diff --git a/plugins/soccer-scoreboard/manifest.json b/plugins/soccer-scoreboard/manifest.json index 300998b..366df1d 100644 --- a/plugins/soccer-scoreboard/manifest.json +++ b/plugins/soccer-scoreboard/manifest.json @@ -1,7 +1,7 @@ { "id": "soccer-scoreboard", "name": "Soccer Scoreboard", - "version": "1.4.1", + "version": "1.4.2", "author": "ChuckBuilds", "description": "Live, recent, and upcoming soccer games across multiple leagues including Premier League, La Liga, Bundesliga, Serie A, Ligue 1, MLS, and more", "category": "sports", @@ -24,6 +24,11 @@ "soccer_upcoming" ], "versions": [ + { + "version": "1.4.2", + "ledmatrix_min": "2.0.0", + "released": "2026-02-24" + }, { "version": "1.4.1", "ledmatrix_min": "2.0.0", @@ -50,7 +55,7 @@ "ledmatrix_min": "2.0.0" } ], - "last_updated": "2026-02-15", + "last_updated": "2026-02-24", "stars": 0, "downloads": 0, "verified": true, @@ -64,4 +69,4 @@ "description": "Custom soccer league editor with ESPN validation" } ] -} +} \ No newline at end of file diff --git a/plugins/ufc-scoreboard/fight_renderer.py b/plugins/ufc-scoreboard/fight_renderer.py index 93e1906..73794af 100644 --- a/plugins/ufc-scoreboard/fight_renderer.py +++ b/plugins/ufc-scoreboard/fight_renderer.py @@ -164,10 +164,12 @@ def _load_headshot( if img.mode != "RGBA": img = img.convert("RGBA") - # Size headshot proportionally: scale with height but cap at 1/3 card width - # so fighter info remains readable on tall displays - logo_size = min(self.display_height, self.display_width // 3) - img.thumbnail((logo_size, logo_size), LANCZOS) + # Crop transparent padding then scale so ink fills display_height. + # thumbnail into a display_height square box preserves aspect ratio. + bbox = img.getbbox() + if bbox: + img = img.crop(bbox) + img.thumbnail((self.display_height, self.display_height), LANCZOS) img.load() # Ensure pixel data is loaded before closing file self._headshot_cache[fighter_id] = img return img diff --git a/plugins/ufc-scoreboard/manifest.json b/plugins/ufc-scoreboard/manifest.json index fdb335d..f4a735e 100644 --- a/plugins/ufc-scoreboard/manifest.json +++ b/plugins/ufc-scoreboard/manifest.json @@ -1,7 +1,7 @@ { "id": "ufc-scoreboard", "name": "UFC Scoreboard", - "version": "1.2.1", + "version": "1.2.2", "author": "LegoGuy1000", "contributors": [ { @@ -32,6 +32,11 @@ "default_duration": 15, "config_schema": "config_schema.json", "versions": [ + { + "version": "1.2.2", + "ledmatrix_min": "2.0.0", + "released": "2026-02-24" + }, { "version": "1.2.1", "ledmatrix_min": "2.0.0", @@ -64,9 +69,9 @@ "released": "2026-02-12" } ], - "last_updated": "2026-02-15", + "last_updated": "2026-02-24", "stars": 0, "downloads": 0, "verified": true, "screenshot": "" -} +} \ No newline at end of file From 7e73d30a0968ee9b3ef310afad59dae717348268 Mon Sep 17 00:00:00 2001 From: Chuck Date: Tue, 24 Feb 2026 16:36:25 -0500 Subject: [PATCH 8/9] fix(logos): cap logo_slot at display_width//2 to prevent slot overlap logo_slot = self.display_height can produce a negative home_slot_start (display_width - display_height) when display_height >= display_width, causing the home logo to render off the left edge. Cap to min(display_height, display_width // 2) so the two slots always fit within the card without overlap. Also fix basketball fallback logo path to use the same crop-to-ink + thumbnail(display_height) sizing as the primary path, and reorder the baseball 1.5.2 manifest version keys to match project convention. Affects: football, baseball, basketball, hockey, soccer scoreboards. Co-Authored-By: Claude Sonnet 4.6 --- plugins.json | 10 +++++----- plugins/baseball-scoreboard/game_renderer.py | 6 +++--- plugins/baseball-scoreboard/manifest.json | 11 ++++++++--- plugins/basketball-scoreboard/game_renderer.py | 11 +++++++---- plugins/basketball-scoreboard/manifest.json | 7 ++++++- plugins/football-scoreboard/game_renderer.py | 5 +++-- plugins/football-scoreboard/manifest.json | 7 ++++++- plugins/hockey-scoreboard/game_renderer.py | 5 +++-- plugins/hockey-scoreboard/manifest.json | 7 ++++++- plugins/soccer-scoreboard/game_renderer.py | 5 +++-- plugins/soccer-scoreboard/manifest.json | 7 ++++++- 11 files changed, 56 insertions(+), 25 deletions(-) diff --git a/plugins.json b/plugins.json index 9845937..de5f6af 100644 --- a/plugins.json +++ b/plugins.json @@ -199,7 +199,7 @@ "last_updated": "2026-02-24", "verified": true, "screenshot": "", - "latest_version": "1.2.2" + "latest_version": "1.2.3" }, { "id": "football-scoreboard", @@ -224,7 +224,7 @@ "last_updated": "2026-02-24", "verified": true, "screenshot": "", - "latest_version": "2.3.2" + "latest_version": "2.3.3" }, { "id": "ufc-scoreboard", @@ -273,7 +273,7 @@ "last_updated": "2026-02-24", "verified": true, "screenshot": "", - "latest_version": "1.5.2" + "latest_version": "1.5.3" }, { "id": "baseball-scoreboard", @@ -299,7 +299,7 @@ "last_updated": "2026-02-24", "verified": true, "screenshot": "", - "latest_version": "1.5.2" + "latest_version": "1.5.3" }, { "id": "soccer-scoreboard", @@ -328,7 +328,7 @@ "last_updated": "2026-02-24", "verified": true, "screenshot": "", - "latest_version": "1.4.2" + "latest_version": "1.4.3" }, { "id": "odds-ticker", diff --git a/plugins/baseball-scoreboard/game_renderer.py b/plugins/baseball-scoreboard/game_renderer.py index c84fca1..de4acac 100644 --- a/plugins/baseball-scoreboard/game_renderer.py +++ b/plugins/baseball-scoreboard/game_renderer.py @@ -173,7 +173,7 @@ def _render_live_game(self, game: Dict) -> Image.Image: center_y = self.display_height // 2 # Logos - logo_slot = self.display_height + 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 @@ -317,7 +317,7 @@ def _render_recent_game(self, game: Dict) -> Image.Image: center_y = self.display_height // 2 # Logos (tighter fit for recent) - logo_slot = self.display_height + 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 @@ -366,7 +366,7 @@ def _render_upcoming_game(self, game: Dict) -> Image.Image: center_y = self.display_height // 2 # Logos (tighter fit) - logo_slot = self.display_height + 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 diff --git a/plugins/baseball-scoreboard/manifest.json b/plugins/baseball-scoreboard/manifest.json index 32f4f13..303108a 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.2", + "version": "1.5.3", "author": "ChuckBuilds", "description": "Live, recent, and upcoming baseball games across MLB, MiLB, and NCAA Baseball with real-time scores and schedules", "category": "sports", @@ -31,9 +31,14 @@ "plugin_path": "plugins/baseball-scoreboard", "versions": [ { + "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" + "ledmatrix_min": "2.0.0" }, { "released": "2026-02-24", diff --git a/plugins/basketball-scoreboard/game_renderer.py b/plugins/basketball-scoreboard/game_renderer.py index b3ca9ac..3820940 100644 --- a/plugins/basketball-scoreboard/game_renderer.py +++ b/plugins/basketball-scoreboard/game_renderer.py @@ -248,8 +248,10 @@ def _load_and_resize_logo( if img.mode != "RGBA": img = img.convert("RGBA") - logo_size = min(self.display_height, self.display_width // 3) - img.thumbnail((logo_size, logo_size), resample=RESAMPLE_FILTER) + bbox = img.getbbox() + if bbox: + img = img.crop(bbox) + img.thumbnail((self.display_height, self.display_height), resample=RESAMPLE_FILTER) # Copy before context manager closes file handle logo = img.copy() @@ -336,8 +338,9 @@ def render_game_card( center_y = self.display_height // 2 - # Draw logos — each centered within a display_height-wide slot on its side - logo_slot = self.display_height + # Draw logos — each centered within a slot on its side; cap at half the card + # width so home_slot_start stays non-negative on square/tall displays + logo_slot = min(self.display_height, self.display_width // 2) away_x = (logo_slot - away_logo.width) // 2 away_y = center_y - (away_logo.height // 2) main_img.paste(away_logo, (away_x, away_y), away_logo) diff --git a/plugins/basketball-scoreboard/manifest.json b/plugins/basketball-scoreboard/manifest.json index 0c3bd16..c9742e3 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.5.2", + "version": "1.5.3", "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", "category": "sports", @@ -18,6 +18,11 @@ "branch": "main", "plugin_path": "plugins/basketball-scoreboard", "versions": [ + { + "released": "2026-02-24", + "version": "1.5.3", + "ledmatrix_min": "2.0.0" + }, { "version": "1.5.2", "ledmatrix_min": "2.0.0", diff --git a/plugins/football-scoreboard/game_renderer.py b/plugins/football-scoreboard/game_renderer.py index b137444..0ca2c4f 100644 --- a/plugins/football-scoreboard/game_renderer.py +++ b/plugins/football-scoreboard/game_renderer.py @@ -328,8 +328,9 @@ def render_game_card( center_y = self.display_height // 2 - # Draw logos — each centered within a display_height-wide slot on its side - logo_slot = self.display_height + # Draw logos — each centered within a slot on its side; cap at half the card + # width so home_slot_start stays non-negative on square/tall displays + logo_slot = min(self.display_height, self.display_width // 2) away_x = (logo_slot - away_logo.width) // 2 away_y = center_y - (away_logo.height // 2) main_img.paste(away_logo, (away_x, away_y), away_logo) diff --git a/plugins/football-scoreboard/manifest.json b/plugins/football-scoreboard/manifest.json index a4c89d8..b62fb69 100644 --- a/plugins/football-scoreboard/manifest.json +++ b/plugins/football-scoreboard/manifest.json @@ -1,7 +1,7 @@ { "id": "football-scoreboard", "name": "Football Scoreboard", - "version": "2.3.2", + "version": "2.3.3", "author": "ChuckBuilds", "class_name": "FootballScoreboardPlugin", "description": "Standalone plugin for live, recent, and upcoming football games across NFL and NCAA Football with real-time scores, down/distance, possession, and game status. Now with organized nested config!", @@ -24,6 +24,11 @@ "ncaa_fb_live" ], "versions": [ + { + "released": "2026-02-24", + "version": "2.3.3", + "ledmatrix_min": "2.0.0" + }, { "version": "2.3.2", "ledmatrix_min": "2.0.0", diff --git a/plugins/hockey-scoreboard/game_renderer.py b/plugins/hockey-scoreboard/game_renderer.py index 0b999d2..e465bf1 100644 --- a/plugins/hockey-scoreboard/game_renderer.py +++ b/plugins/hockey-scoreboard/game_renderer.py @@ -367,8 +367,9 @@ def render_game_card( center_y = self.display_height // 2 - # Draw logos — each centered within a display_height-wide slot on its side - logo_slot = self.display_height + # Draw logos — each centered within a slot on its side; cap at half the card + # width so home_slot_start stays non-negative on square/tall displays + logo_slot = min(self.display_height, self.display_width // 2) away_x = (logo_slot - away_logo.width) // 2 away_y = center_y - (away_logo.height // 2) main_img.paste(away_logo, (away_x, away_y), away_logo) diff --git a/plugins/hockey-scoreboard/manifest.json b/plugins/hockey-scoreboard/manifest.json index 0ee2b18..ab5481e 100644 --- a/plugins/hockey-scoreboard/manifest.json +++ b/plugins/hockey-scoreboard/manifest.json @@ -1,7 +1,7 @@ { "id": "hockey-scoreboard", "name": "Hockey Scoreboard", - "version": "1.2.2", + "version": "1.2.3", "author": "ChuckBuilds", "description": "Live, recent, and upcoming hockey games across NHL, NCAA Men's, and NCAA Women's hockey with real-time scores and schedules", "homepage": "https://github.com/ChuckBuilds/ledmatrix-plugins/tree/main/plugins/hockey-scoreboard", @@ -55,6 +55,11 @@ } ], "versions": [ + { + "released": "2026-02-24", + "version": "1.2.3", + "ledmatrix_min": "2.0.0" + }, { "version": "1.2.2", "ledmatrix_min": "2.0.0", diff --git a/plugins/soccer-scoreboard/game_renderer.py b/plugins/soccer-scoreboard/game_renderer.py index 60c01fd..1bfe513 100644 --- a/plugins/soccer-scoreboard/game_renderer.py +++ b/plugins/soccer-scoreboard/game_renderer.py @@ -331,8 +331,9 @@ def render_game_card( center_y = self.display_height // 2 - # Place logos — each centered within a display_height-wide slot on its side - logo_slot = self.display_height + # Place logos — each centered within a slot on its side; cap at half the card + # width so home_slot_start stays non-negative on square/tall displays + logo_slot = min(self.display_height, self.display_width // 2) away_x = (logo_slot - away_logo.width) // 2 away_y = center_y - (away_logo.height // 2) diff --git a/plugins/soccer-scoreboard/manifest.json b/plugins/soccer-scoreboard/manifest.json index 366df1d..7e440a6 100644 --- a/plugins/soccer-scoreboard/manifest.json +++ b/plugins/soccer-scoreboard/manifest.json @@ -1,7 +1,7 @@ { "id": "soccer-scoreboard", "name": "Soccer Scoreboard", - "version": "1.4.2", + "version": "1.4.3", "author": "ChuckBuilds", "description": "Live, recent, and upcoming soccer games across multiple leagues including Premier League, La Liga, Bundesliga, Serie A, Ligue 1, MLS, and more", "category": "sports", @@ -24,6 +24,11 @@ "soccer_upcoming" ], "versions": [ + { + "released": "2026-02-24", + "version": "1.4.3", + "ledmatrix_min": "2.0.0" + }, { "version": "1.4.2", "ledmatrix_min": "2.0.0", From c83012d6cbd815d93c5248a50739e2dcd3542a17 Mon Sep 17 00:00:00 2001 From: Chuck Date: Wed, 25 Feb 2026 09:22:58 -0500 Subject: [PATCH 9/9] fix(timezone): use correct timezone for ESPN API dates and local time display MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - All sports plugins: use America/New_York (with DST) instead of naive datetime.now() for ESPN API date queries, plus 1-day lookback to catch late-night games still in progress — matching the fix already in LEDMatrix core (commit cd0c43fb). ESPN anchors its schedule calendar to Eastern US time regardless of where the user is located. - Baseball: inject resolved timezone (plugin config → global config → UTC) into self.config early in manager __init__ so game_renderer.py and scroll_display.py both get the correct timezone. Fix game_renderer.py fallback from 'US/Eastern' to 'UTC' (fallback is now never reached for users with a global config timezone set). - F1: inject resolved timezone into config before constructing F1Renderer. Add _to_local_dt() helper to F1Renderer and use it in render_upcoming_race() and render_calendar_entry() so session/race dates display in local time instead of UTC. Co-Authored-By: Claude Sonnet 4.6 --- plugins.json | 30 ++++++++++---------- plugins/baseball-scoreboard/game_renderer.py | 2 +- plugins/baseball-scoreboard/manager.py | 10 +++++++ plugins/baseball-scoreboard/manifest.json | 9 ++++-- plugins/baseball-scoreboard/sports.py | 10 +++++-- plugins/basketball-scoreboard/manifest.json | 9 ++++-- plugins/basketball-scoreboard/sports.py | 10 +++++-- plugins/f1-scoreboard/f1_renderer.py | 17 ++++++++--- plugins/f1-scoreboard/manager.py | 9 ++++++ plugins/f1-scoreboard/manifest.json | 7 ++++- plugins/football-scoreboard/manifest.json | 9 ++++-- plugins/football-scoreboard/sports.py | 10 +++++-- plugins/hockey-scoreboard/manifest.json | 9 ++++-- plugins/hockey-scoreboard/sports.py | 10 +++++-- plugins/soccer-scoreboard/manifest.json | 9 ++++-- plugins/soccer-scoreboard/sports.py | 11 +++++-- plugins/ufc-scoreboard/manifest.json | 9 ++++-- plugins/ufc-scoreboard/sports.py | 10 +++++-- 18 files changed, 144 insertions(+), 46 deletions(-) diff --git a/plugins.json b/plugins.json index de5f6af..c68e944 100644 --- a/plugins.json +++ b/plugins.json @@ -1,6 +1,6 @@ { "version": "1.0.0", - "last_updated": "2026-02-24", + "last_updated": "2026-02-25", "plugins": [ { "id": "hello-world", @@ -196,10 +196,10 @@ "plugin_path": "plugins/hockey-scoreboard", "stars": 0, "downloads": 0, - "last_updated": "2026-02-24", + "last_updated": "2026-02-25", "verified": true, "screenshot": "", - "latest_version": "1.2.3" + "latest_version": "1.2.4" }, { "id": "football-scoreboard", @@ -221,10 +221,10 @@ "plugin_path": "plugins/football-scoreboard", "stars": 0, "downloads": 0, - "last_updated": "2026-02-24", + "last_updated": "2026-02-25", "verified": true, "screenshot": "", - "latest_version": "2.3.3" + "latest_version": "2.3.4" }, { "id": "ufc-scoreboard", @@ -245,10 +245,10 @@ "plugin_path": "plugins/ufc-scoreboard", "stars": 0, "downloads": 0, - "last_updated": "2026-02-24", + "last_updated": "2026-02-25", "verified": true, "screenshot": "", - "latest_version": "1.2.2" + "latest_version": "1.2.3" }, { "id": "basketball-scoreboard", @@ -270,10 +270,10 @@ "plugin_path": "plugins/basketball-scoreboard", "stars": 0, "downloads": 0, - "last_updated": "2026-02-24", + "last_updated": "2026-02-25", "verified": true, "screenshot": "", - "latest_version": "1.5.3" + "latest_version": "1.5.4" }, { "id": "baseball-scoreboard", @@ -296,10 +296,10 @@ "plugin_path": "plugins/baseball-scoreboard", "stars": 0, "downloads": 0, - "last_updated": "2026-02-24", + "last_updated": "2026-02-25", "verified": true, "screenshot": "", - "latest_version": "1.5.3" + "latest_version": "1.5.4" }, { "id": "soccer-scoreboard", @@ -325,10 +325,10 @@ "plugin_path": "plugins/soccer-scoreboard", "stars": 0, "downloads": 0, - "last_updated": "2026-02-24", + "last_updated": "2026-02-25", "verified": true, "screenshot": "", - "latest_version": "1.4.3" + "latest_version": "1.4.4" }, { "id": "odds-ticker", @@ -644,10 +644,10 @@ "plugin_path": "plugins/f1-scoreboard", "stars": 0, "downloads": 0, - "last_updated": "2026-02-24", + "last_updated": "2026-02-25", "verified": true, "screenshot": "", - "latest_version": "1.2.1" + "latest_version": "1.2.2" }, { "id": "web-ui-info", diff --git a/plugins/baseball-scoreboard/game_renderer.py b/plugins/baseball-scoreboard/game_renderer.py index de4acac..15398e7 100644 --- a/plugins/baseball-scoreboard/game_renderer.py +++ b/plugins/baseball-scoreboard/game_renderer.py @@ -385,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') diff --git a/plugins/baseball-scoreboard/manager.py b/plugins/baseball-scoreboard/manager.py index af13622..3e96a75 100644 --- a/plugins/baseball-scoreboard/manager.py +++ b/plugins/baseball-scoreboard/manager.py @@ -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" + self.config["timezone"] = global_tz or "UTC" + # Basic configuration self.is_enabled = config.get("enabled", True) # Get display dimensions from display_manager properties diff --git a/plugins/baseball-scoreboard/manifest.json b/plugins/baseball-scoreboard/manifest.json index 303108a..f2118e5 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.3", + "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", @@ -30,6 +30,11 @@ "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", @@ -103,4 +108,4 @@ "screenshot": "", "class_name": "BaseballScoreboardPlugin", "entry_point": "manager.py" -} \ No newline at end of file +} diff --git a/plugins/baseball-scoreboard/sports.py b/plugins/baseball-scoreboard/sports.py index efb8907..66921b7 100644 --- a/plugins/baseball-scoreboard/sports.py +++ b/plugins/baseball-scoreboard/sports.py @@ -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, ) diff --git a/plugins/basketball-scoreboard/manifest.json b/plugins/basketball-scoreboard/manifest.json index c9742e3..ec8cf7f 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.5.3", + "version": "1.5.4", "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", "category": "sports", @@ -18,6 +18,11 @@ "branch": "main", "plugin_path": "plugins/basketball-scoreboard", "versions": [ + { + "released": "2026-02-25", + "version": "1.5.4", + "ledmatrix_min": "2.0.0" + }, { "released": "2026-02-24", "version": "1.5.3", @@ -86,4 +91,4 @@ "dependencies": {}, "entry_point": "manager.py", "class_name": "BasketballScoreboardPlugin" -} \ No newline at end of file +} diff --git a/plugins/basketball-scoreboard/sports.py b/plugins/basketball-scoreboard/sports.py index 1514131..94d9716 100644 --- a/plugins/basketball-scoreboard/sports.py +++ b/plugins/basketball-scoreboard/sports.py @@ -1162,9 +1162,15 @@ def _fetch_todays_games(self) -> Optional[Dict]: params = {"limit": 1000} # No dates parameter self.logger.debug(f"Fetching current games for {self.sport}/{self.league} (no dates)") else: - 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") - params = {"dates": formatted_date, "limit": 1000} + formatted_date_yesterday = yesterday.strftime("%Y%m%d") + params = {"dates": f"{formatted_date_yesterday}-{formatted_date}", "limit": 1000} self.logger.debug(f"Fetching today's games for {self.sport}/{self.league} on date {formatted_date}") response = self.session.get( diff --git a/plugins/f1-scoreboard/f1_renderer.py b/plugins/f1-scoreboard/f1_renderer.py index fc02934..e633abd 100644 --- a/plugins/f1-scoreboard/f1_renderer.py +++ b/plugins/f1-scoreboard/f1_renderer.py @@ -14,6 +14,7 @@ from pathlib import Path from typing import Any, Dict, List, Optional, Tuple, Union +import pytz from PIL import Image, ImageDraw, ImageFont from logo_downloader import F1LogoLoader @@ -77,6 +78,16 @@ def _load_fonts(self) -> Dict[str, Any]: return fonts + def _to_local_dt(self, utc_iso_str: str) -> datetime: + """Parse a UTC ISO datetime string and convert to configured local timezone.""" + dt = datetime.fromisoformat(utc_iso_str.replace("Z", "+00:00")) + tz_str = self.config.get("timezone", "UTC") + try: + local_tz = pytz.timezone(tz_str) + except Exception: + local_tz = pytz.UTC + return dt.astimezone(local_tz) + def _load_font(self, font_name: str, size: int) -> Union[ImageFont.FreeTypeFont, Any]: """Load a font with multiple path fallbacks.""" @@ -705,8 +716,7 @@ def render_upcoming_race(self, race: Dict) -> Image.Image: date_prefix = "" if race_date: try: - dt = datetime.fromisoformat( - race_date.replace("Z", "+00:00")) + dt = self._to_local_dt(race_date) date_prefix = dt.strftime("%b %d").upper() + " " except (ValueError, TypeError): pass @@ -739,8 +749,7 @@ def render_calendar_entry(self, entry: Dict) -> Image.Image: day_display = "" if date_str: try: - dt = datetime.fromisoformat( - date_str.replace("Z", "+00:00")) + dt = self._to_local_dt(date_str) date_display = dt.strftime("%b %d").upper() day_display = dt.strftime("%a").upper() except (ValueError, TypeError): diff --git a/plugins/f1-scoreboard/manager.py b/plugins/f1-scoreboard/manager.py index ef5f9a8..5edc96c 100644 --- a/plugins/f1-scoreboard/manager.py +++ b/plugins/f1-scoreboard/manager.py @@ -59,6 +59,15 @@ def __init__(self, plugin_id, config, display_manager, scroll_cfg = config.get("scroll", {}) if isinstance(config.get("scroll"), dict) else {} self._card_width = scroll_cfg.get("game_card_width", 128) + # Resolve timezone: plugin config → global config → UTC. + # Inject into config so the renderer can convert UTC API times to local. + if not config.get("timezone"): + try: + global_tz = cache_manager.config_manager.get_timezone() + except Exception: + global_tz = "UTC" + config["timezone"] = global_tz or "UTC" + # Initialize components self.logo_loader = F1LogoLoader() self.data_source = F1DataSource(cache_manager, config) diff --git a/plugins/f1-scoreboard/manifest.json b/plugins/f1-scoreboard/manifest.json index 245a9a6..2ec8c4d 100644 --- a/plugins/f1-scoreboard/manifest.json +++ b/plugins/f1-scoreboard/manifest.json @@ -1,7 +1,7 @@ { "id": "f1-scoreboard", "name": "F1 Scoreboard", - "version": "1.2.1", + "version": "1.2.2", "author": "ChuckBuilds", "class_name": "F1ScoreboardPlugin", "entry_point": "manager.py", @@ -29,6 +29,11 @@ "f1_calendar" ], "versions": [ + { + "released": "2026-02-25", + "version": "1.2.2", + "ledmatrix_min": "2.0.0" + }, { "version": "1.2.1", "ledmatrix_min": "2.0.0", diff --git a/plugins/football-scoreboard/manifest.json b/plugins/football-scoreboard/manifest.json index b62fb69..5906421 100644 --- a/plugins/football-scoreboard/manifest.json +++ b/plugins/football-scoreboard/manifest.json @@ -1,7 +1,7 @@ { "id": "football-scoreboard", "name": "Football Scoreboard", - "version": "2.3.3", + "version": "2.3.4", "author": "ChuckBuilds", "class_name": "FootballScoreboardPlugin", "description": "Standalone plugin for live, recent, and upcoming football games across NFL and NCAA Football with real-time scores, down/distance, possession, and game status. Now with organized nested config!", @@ -24,6 +24,11 @@ "ncaa_fb_live" ], "versions": [ + { + "released": "2026-02-25", + "version": "2.3.4", + "ledmatrix_min": "2.0.0" + }, { "released": "2026-02-24", "version": "2.3.3", @@ -242,4 +247,4 @@ "screenshot": "", "config_schema": "config_schema.json", "entry_point": "manager.py" -} \ No newline at end of file +} diff --git a/plugins/football-scoreboard/sports.py b/plugins/football-scoreboard/sports.py index bd2b301..921c868 100644 --- a/plugins/football-scoreboard/sports.py +++ b/plugins/football-scoreboard/sports.py @@ -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, ) diff --git a/plugins/hockey-scoreboard/manifest.json b/plugins/hockey-scoreboard/manifest.json index ab5481e..c8099fb 100644 --- a/plugins/hockey-scoreboard/manifest.json +++ b/plugins/hockey-scoreboard/manifest.json @@ -1,7 +1,7 @@ { "id": "hockey-scoreboard", "name": "Hockey Scoreboard", - "version": "1.2.3", + "version": "1.2.4", "author": "ChuckBuilds", "description": "Live, recent, and upcoming hockey games across NHL, NCAA Men's, and NCAA Women's hockey with real-time scores and schedules", "homepage": "https://github.com/ChuckBuilds/ledmatrix-plugins/tree/main/plugins/hockey-scoreboard", @@ -55,6 +55,11 @@ } ], "versions": [ + { + "released": "2026-02-25", + "version": "1.2.4", + "ledmatrix_min": "2.0.0" + }, { "released": "2026-02-24", "version": "1.2.3", @@ -131,4 +136,4 @@ "downloads": 0, "verified": true, "screenshot": "" -} \ No newline at end of file +} diff --git a/plugins/hockey-scoreboard/sports.py b/plugins/hockey-scoreboard/sports.py index 6f6bfdc..9dd8730 100644 --- a/plugins/hockey-scoreboard/sports.py +++ b/plugins/hockey-scoreboard/sports.py @@ -833,13 +833,19 @@ 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" 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, ) diff --git a/plugins/soccer-scoreboard/manifest.json b/plugins/soccer-scoreboard/manifest.json index 7e440a6..75623c6 100644 --- a/plugins/soccer-scoreboard/manifest.json +++ b/plugins/soccer-scoreboard/manifest.json @@ -1,7 +1,7 @@ { "id": "soccer-scoreboard", "name": "Soccer Scoreboard", - "version": "1.4.3", + "version": "1.4.4", "author": "ChuckBuilds", "description": "Live, recent, and upcoming soccer games across multiple leagues including Premier League, La Liga, Bundesliga, Serie A, Ligue 1, MLS, and more", "category": "sports", @@ -24,6 +24,11 @@ "soccer_upcoming" ], "versions": [ + { + "released": "2026-02-25", + "version": "1.4.4", + "ledmatrix_min": "2.0.0" + }, { "released": "2026-02-24", "version": "1.4.3", @@ -74,4 +79,4 @@ "description": "Custom soccer league editor with ESPN validation" } ] -} \ No newline at end of file +} diff --git a/plugins/soccer-scoreboard/sports.py b/plugins/soccer-scoreboard/sports.py index ab39f07..f481af1 100644 --- a/plugins/soccer-scoreboard/sports.py +++ b/plugins/soccer-scoreboard/sports.py @@ -998,10 +998,15 @@ def _fetch_todays_games(self) -> Optional[Dict]: self.logger.debug(f"Using cached current scoreboard for {self.sport}/{self.league}") return cached_data - # For soccer, use today's date - 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") - params = {"dates": formatted_date, "limit": 1000} + formatted_date_yesterday = yesterday.strftime("%Y%m%d") + params = {"dates": f"{formatted_date_yesterday}-{formatted_date}", "limit": 1000} self.logger.debug(f"Fetching today's games for {self.sport}/{self.league} on date {formatted_date}") response = self.session.get( diff --git a/plugins/ufc-scoreboard/manifest.json b/plugins/ufc-scoreboard/manifest.json index f4a735e..edfa4cd 100644 --- a/plugins/ufc-scoreboard/manifest.json +++ b/plugins/ufc-scoreboard/manifest.json @@ -1,7 +1,7 @@ { "id": "ufc-scoreboard", "name": "UFC Scoreboard", - "version": "1.2.2", + "version": "1.2.3", "author": "LegoGuy1000", "contributors": [ { @@ -32,6 +32,11 @@ "default_duration": 15, "config_schema": "config_schema.json", "versions": [ + { + "released": "2026-02-25", + "version": "1.2.3", + "ledmatrix_min": "2.0.0" + }, { "version": "1.2.2", "ledmatrix_min": "2.0.0", @@ -74,4 +79,4 @@ "downloads": 0, "verified": true, "screenshot": "" -} \ No newline at end of file +} diff --git a/plugins/ufc-scoreboard/sports.py b/plugins/ufc-scoreboard/sports.py index dee1a8d..7fc17bf 100644 --- a/plugins/ufc-scoreboard/sports.py +++ b/plugins/ufc-scoreboard/sports.py @@ -860,14 +860,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, )