From 125bf952d384172bfa1f7edc367fbcc80124ec41 Mon Sep 17 00:00:00 2001 From: ChuckBuilds Date: Mon, 30 Mar 2026 12:28:13 -0400 Subject: [PATCH 1/2] fix(basketball,baseball): add odds layout offsets and align customization Basketball: - Add odds rendering to scroll mode (render_game_card now calls _draw_dynamic_odds, matching baseball's behavior) - Apply user-configurable layout offsets to odds positioning in both switch mode (sports.py) and scroll mode (game_renderer.py) Baseball: - Add missing font customization entries: period_text, team_name, rank_text (aligns with football/basketball) - Add odds layout offset to config schema - Apply user-configurable layout offsets to odds positioning - Update x-propertyOrder to include new entries Co-Authored-By: Claude Opus 4.6 (1M context) --- .../baseball-scoreboard/config_schema.json | 2525 +++++++++-------- plugins/baseball-scoreboard/game_renderer.py | 26 +- plugins/baseball-scoreboard/manifest.json | 238 +- plugins/baseball-scoreboard/sports.py | 24 +- .../basketball-scoreboard/game_renderer.py | 59 +- plugins/basketball-scoreboard/manifest.json | 184 +- plugins/basketball-scoreboard/sports.py | 24 +- 7 files changed, 1631 insertions(+), 1449 deletions(-) diff --git a/plugins/baseball-scoreboard/config_schema.json b/plugins/baseball-scoreboard/config_schema.json index 53f867b..8383ab6 100644 --- a/plugins/baseball-scoreboard/config_schema.json +++ b/plugins/baseball-scoreboard/config_schema.json @@ -1,1234 +1,1377 @@ { - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "Baseball Scoreboard Plugin Configuration", - "description": "Configuration schema for the Baseball Scoreboard plugin - displays live, recent, and upcoming MLB, MiLB, and NCAA Baseball games", - "type": "object", - "properties": { - "enabled": { - "type": "boolean", - "default": true, - "description": "Enable or disable the baseball scoreboard plugin" - }, - "display_duration": { - "type": "number", - "default": 30, - "minimum": 5, - "maximum": 300, - "description": "Duration in seconds for the display controller to show this plugin mode before rotating to next plugin" - }, - "update_interval": { - "type": "integer", - "default": 3600, - "minimum": 30, - "maximum": 86400, - "description": "How often to fetch new data in seconds" - }, - "game_display_duration": { - "type": "number", - "default": 15, - "minimum": 3, - "maximum": 60, - "description": "Duration in seconds to show each individual game before rotating to the next game within the same mode" - }, - "mlb": { - "type": "object", - "title": "MLB Settings", - "description": "Configuration for MLB games", - "properties": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Baseball Scoreboard Plugin Configuration", + "description": "Configuration schema for the Baseball Scoreboard plugin - displays live, recent, and upcoming MLB, MiLB, and NCAA Baseball games", + "type": "object", + "properties": { "enabled": { - "type": "boolean", - "default": true, - "description": "Enable MLB games" - }, - "favorite_teams": { - "type": "array", - "items": { - "type": "string" - }, - "default": [], - "uniqueItems": true, - "maxItems": 30, - "description": "List of favorite MLB team abbreviations (e.g., NYY, BOS, LAD). Use 2-3 letter codes." - }, - "display_modes": { - "type": "object", - "title": "Display Modes", - "description": "Control which game types to show", - "properties": { - "show_live": { - "type": "boolean", - "default": true, - "description": "Show live MLB games" - }, - "show_recent": { - "type": "boolean", - "default": true, - "description": "Show recently completed MLB games" - }, - "show_upcoming": { - "type": "boolean", - "default": true, - "description": "Show upcoming MLB games" - }, - "live_display_mode": { - "type": "string", - "enum": ["switch", "scroll"], - "default": "switch", - "description": "Display mode for live games: 'switch' shows one game at a time, 'scroll' scrolls all games horizontally" - }, - "recent_display_mode": { - "type": "string", - "enum": ["switch", "scroll"], - "default": "switch", - "description": "Display mode for recent games: 'switch' shows one game at a time, 'scroll' scrolls all games horizontally" - }, - "upcoming_display_mode": { - "type": "string", - "enum": ["switch", "scroll"], - "default": "switch", - "description": "Display mode for upcoming games: 'switch' shows one game at a time, 'scroll' scrolls all games horizontally" - } - } - }, - "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 game cards when scrolling" - }, - "show_league_separators": { - "type": "boolean", - "default": true, - "description": "Show league icons (MLB shield, NCAA logos) 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 game card in scroll mode (pixels). Default 128. Set lower on multi-panel chains to show more games simultaneously." - } - } - }, - "live_priority": { - "type": "boolean", - "default": false, - "description": "Give live games priority over other modes. When enabled, live games will interrupt the normal mode rotation and be displayed immediately when available." - }, - "live_game_duration": { - "type": "integer", - "default": 30, - "minimum": 10, - "maximum": 120, - "description": "Duration in seconds to display each live game before rotating to the next live game" + "type": "boolean", + "default": true, + "description": "Enable or disable the baseball scoreboard plugin" }, - "recent_game_duration": { - "type": "number", - "default": 15, - "minimum": 10, - "maximum": 120, - "description": "Duration in seconds to show each recent game before rotating to the next game. If not set, uses the top-level game_display_duration setting (default: 15 seconds)." + "display_duration": { + "type": "number", + "default": 30, + "minimum": 5, + "maximum": 300, + "description": "Duration in seconds for the display controller to show this plugin mode before rotating to next plugin" }, - "upcoming_game_duration": { - "type": "number", - "default": 15, - "minimum": 10, - "maximum": 120, - "description": "Duration in seconds to show each upcoming game before rotating to the next game. If not set, uses the top-level game_display_duration setting (default: 15 seconds)." + "update_interval": { + "type": "integer", + "default": 3600, + "minimum": 30, + "maximum": 86400, + "description": "How often to fetch new data in seconds" }, - "live_update_interval": { - "type": "integer", - "default": 30, - "minimum": 5, - "maximum": 300, - "description": "How often to update live game data (seconds)" + "game_display_duration": { + "type": "number", + "default": 15, + "minimum": 3, + "maximum": 60, + "description": "Duration in seconds to show each individual game before rotating to the next game within the same mode" }, - "update_interval_seconds": { - "type": "integer", - "default": 3600, - "minimum": 30, - "maximum": 86400, - "description": "How often to fetch new data for this league (seconds)" - }, - "game_limits": { - "type": "object", - "title": "Game Limits", - "description": "Control how many games to show", - "properties": { - "recent_games_to_show": { - "type": "integer", - "default": 5, - "minimum": 1, - "maximum": 20, - "description": "With favorites: N games per favorite team. Without favorites: N total games sorted by time." - }, - "upcoming_games_to_show": { - "type": "integer", - "default": 1, - "minimum": 1, - "maximum": 20, - "description": "With favorites: N games per favorite team. Without favorites: N total games sorted by time." - } - } - }, - "display_options": { - "type": "object", - "title": "Display Options", - "description": "Additional information to show", - "properties": { - "show_records": { - "type": "boolean", - "default": false, - "description": "Show team records (wins-losses)" - }, - "show_ranking": { - "type": "boolean", - "default": false, - "description": "Show team rankings (when available)" - }, - "show_odds": { - "type": "boolean", - "default": true, - "description": "Show betting odds" - }, - "show_series_summary": { - "type": "boolean", - "default": false, - "description": "Show series summary information" - } - } - }, - "filtering": { - "type": "object", - "title": "Filtering Options", - "description": "Control which teams are shown", - "properties": { - "show_favorite_teams_only": { - "type": "boolean", - "default": true, - "description": "Only show games from favorite teams" - }, - "show_all_live": { - "type": "boolean", - "default": false, - "description": "Show all live games, not just favorites" - } - } - }, - "mode_durations": { - "type": "object", - "title": "Mode Duration Overrides", - "description": "Override how long each mode displays before rotating", - "properties": { - "recent_mode_duration": { - "type": ["number", "null"], - "default": null, - "minimum": 10, - "maximum": 600, - "description": "Override display duration for recent games mode (seconds). Null uses default." - }, - "upcoming_mode_duration": { - "type": ["number", "null"], - "default": null, - "minimum": 10, - "maximum": 600, - "description": "Override display duration for upcoming games mode (seconds). Null uses default." - }, - "live_mode_duration": { - "type": ["number", "null"], - "default": null, - "minimum": 10, - "maximum": 600, - "description": "Override display duration for live games mode (seconds). Null uses default." - } - } - }, - "dynamic_duration": { - "type": "object", - "title": "MLB Dynamic Duration Settings", - "description": "Configure dynamic duration settings for MLB games.", - "properties": { - "enabled": { - "type": "boolean", - "default": false, - "description": "Enable dynamic duration for MLB games" - }, - "min_duration_seconds": { - "type": "number", - "minimum": 10, - "maximum": 300, - "default": 30, - "description": "Minimum total duration in seconds for this mode, even if few games are available. Ensures the mode stays visible long enough." - }, - "max_duration_seconds": { - "type": "number", - "minimum": 60, - "maximum": 600, - "description": "Maximum total duration in seconds for this mode, even if many games are available." - }, - "modes": { - "type": "object", - "title": "Per-Mode Settings for MLB", - "description": "Configure dynamic duration for specific MLB modes", - "properties": { - "live": { - "type": "object", - "properties": { - "enabled": { - "type": "boolean", - "default": false, - "description": "Enable dynamic duration for MLB live games" - }, - "min_duration_seconds": { - "type": "number", - "minimum": 10, - "maximum": 300, - "description": "Minimum duration for MLB live mode" + "mlb": { + "type": "object", + "title": "MLB Settings", + "description": "Configuration for MLB games", + "properties": { + "enabled": { + "type": "boolean", + "default": true, + "description": "Enable MLB games" + }, + "favorite_teams": { + "type": "array", + "items": { + "type": "string" }, - "max_duration_seconds": { - "type": "number", - "minimum": 60, - "maximum": 600, - "description": "Max duration for MLB live games" + "default": [], + "uniqueItems": true, + "maxItems": 30, + "description": "List of favorite MLB team abbreviations (e.g., NYY, BOS, LAD). Use 2-3 letter codes." + }, + "display_modes": { + "type": "object", + "title": "Display Modes", + "description": "Control which game types to show", + "properties": { + "show_live": { + "type": "boolean", + "default": true, + "description": "Show live MLB games" + }, + "show_recent": { + "type": "boolean", + "default": true, + "description": "Show recently completed MLB games" + }, + "show_upcoming": { + "type": "boolean", + "default": true, + "description": "Show upcoming MLB games" + }, + "live_display_mode": { + "type": "string", + "enum": [ + "switch", + "scroll" + ], + "default": "switch", + "description": "Display mode for live games: 'switch' shows one game at a time, 'scroll' scrolls all games horizontally" + }, + "recent_display_mode": { + "type": "string", + "enum": [ + "switch", + "scroll" + ], + "default": "switch", + "description": "Display mode for recent games: 'switch' shows one game at a time, 'scroll' scrolls all games horizontally" + }, + "upcoming_display_mode": { + "type": "string", + "enum": [ + "switch", + "scroll" + ], + "default": "switch", + "description": "Display mode for upcoming games: 'switch' shows one game at a time, 'scroll' scrolls all games horizontally" + } } - } }, - "recent": { - "type": "object", - "properties": { - "enabled": { - "type": "boolean", - "default": false, - "description": "Enable dynamic duration for MLB recent games" - }, - "min_duration_seconds": { - "type": "number", - "minimum": 10, - "maximum": 300, - "description": "Minimum duration for MLB recent mode" - }, - "max_duration_seconds": { - "type": "number", - "minimum": 60, - "maximum": 600, - "description": "Max duration for MLB recent games" + "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 game cards when scrolling" + }, + "show_league_separators": { + "type": "boolean", + "default": true, + "description": "Show league icons (MLB shield, NCAA logos) 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 game card in scroll mode (pixels). Default 128. Set lower on multi-panel chains to show more games simultaneously." + } } - } }, - "upcoming": { - "type": "object", - "properties": { - "enabled": { - "type": "boolean", - "default": false, - "description": "Enable dynamic duration for MLB upcoming games" - }, - "min_duration_seconds": { - "type": "number", - "minimum": 10, - "maximum": 300, - "description": "Minimum duration for MLB upcoming mode" - }, - "max_duration_seconds": { - "type": "number", - "minimum": 60, - "maximum": 600, - "description": "Max duration for MLB upcoming games" + "live_priority": { + "type": "boolean", + "default": false, + "description": "Give live games priority over other modes. When enabled, live games will interrupt the normal mode rotation and be displayed immediately when available." + }, + "live_game_duration": { + "type": "integer", + "default": 30, + "minimum": 10, + "maximum": 120, + "description": "Duration in seconds to display each live game before rotating to the next live game" + }, + "recent_game_duration": { + "type": "number", + "default": 15, + "minimum": 10, + "maximum": 120, + "description": "Duration in seconds to show each recent game before rotating to the next game. If not set, uses the top-level game_display_duration setting (default: 15 seconds)." + }, + "upcoming_game_duration": { + "type": "number", + "default": 15, + "minimum": 10, + "maximum": 120, + "description": "Duration in seconds to show each upcoming game before rotating to the next game. If not set, uses the top-level game_display_duration setting (default: 15 seconds)." + }, + "live_update_interval": { + "type": "integer", + "default": 30, + "minimum": 5, + "maximum": 300, + "description": "How often to update live game data (seconds)" + }, + "update_interval_seconds": { + "type": "integer", + "default": 3600, + "minimum": 30, + "maximum": 86400, + "description": "How often to fetch new data for this league (seconds)" + }, + "game_limits": { + "type": "object", + "title": "Game Limits", + "description": "Control how many games to show", + "properties": { + "recent_games_to_show": { + "type": "integer", + "default": 5, + "minimum": 1, + "maximum": 20, + "description": "With favorites: N games per favorite team. Without favorites: N total games sorted by time." + }, + "upcoming_games_to_show": { + "type": "integer", + "default": 1, + "minimum": 1, + "maximum": 20, + "description": "With favorites: N games per favorite team. Without favorites: N total games sorted by time." + } + } + }, + "display_options": { + "type": "object", + "title": "Display Options", + "description": "Additional information to show", + "properties": { + "show_records": { + "type": "boolean", + "default": false, + "description": "Show team records (wins-losses)" + }, + "show_ranking": { + "type": "boolean", + "default": false, + "description": "Show team rankings (when available)" + }, + "show_odds": { + "type": "boolean", + "default": true, + "description": "Show betting odds" + }, + "show_series_summary": { + "type": "boolean", + "default": false, + "description": "Show series summary information" + } + } + }, + "filtering": { + "type": "object", + "title": "Filtering Options", + "description": "Control which teams are shown", + "properties": { + "show_favorite_teams_only": { + "type": "boolean", + "default": true, + "description": "Only show games from favorite teams" + }, + "show_all_live": { + "type": "boolean", + "default": false, + "description": "Show all live games, not just favorites" + } + } + }, + "mode_durations": { + "type": "object", + "title": "Mode Duration Overrides", + "description": "Override how long each mode displays before rotating", + "properties": { + "recent_mode_duration": { + "type": [ + "number", + "null" + ], + "default": null, + "minimum": 10, + "maximum": 600, + "description": "Override display duration for recent games mode (seconds). Null uses default." + }, + "upcoming_mode_duration": { + "type": [ + "number", + "null" + ], + "default": null, + "minimum": 10, + "maximum": 600, + "description": "Override display duration for upcoming games mode (seconds). Null uses default." + }, + "live_mode_duration": { + "type": [ + "number", + "null" + ], + "default": null, + "minimum": 10, + "maximum": 600, + "description": "Override display duration for live games mode (seconds). Null uses default." + } + } + }, + "dynamic_duration": { + "type": "object", + "title": "MLB Dynamic Duration Settings", + "description": "Configure dynamic duration settings for MLB games.", + "properties": { + "enabled": { + "type": "boolean", + "default": false, + "description": "Enable dynamic duration for MLB games" + }, + "min_duration_seconds": { + "type": "number", + "minimum": 10, + "maximum": 300, + "default": 30, + "description": "Minimum total duration in seconds for this mode, even if few games are available. Ensures the mode stays visible long enough." + }, + "max_duration_seconds": { + "type": "number", + "minimum": 60, + "maximum": 600, + "description": "Maximum total duration in seconds for this mode, even if many games are available." + }, + "modes": { + "type": "object", + "title": "Per-Mode Settings for MLB", + "description": "Configure dynamic duration for specific MLB modes", + "properties": { + "live": { + "type": "object", + "properties": { + "enabled": { + "type": "boolean", + "default": false, + "description": "Enable dynamic duration for MLB live games" + }, + "min_duration_seconds": { + "type": "number", + "minimum": 10, + "maximum": 300, + "description": "Minimum duration for MLB live mode" + }, + "max_duration_seconds": { + "type": "number", + "minimum": 60, + "maximum": 600, + "description": "Max duration for MLB live games" + } + } + }, + "recent": { + "type": "object", + "properties": { + "enabled": { + "type": "boolean", + "default": false, + "description": "Enable dynamic duration for MLB recent games" + }, + "min_duration_seconds": { + "type": "number", + "minimum": 10, + "maximum": 300, + "description": "Minimum duration for MLB recent mode" + }, + "max_duration_seconds": { + "type": "number", + "minimum": 60, + "maximum": 600, + "description": "Max duration for MLB recent games" + } + } + }, + "upcoming": { + "type": "object", + "properties": { + "enabled": { + "type": "boolean", + "default": false, + "description": "Enable dynamic duration for MLB upcoming games" + }, + "min_duration_seconds": { + "type": "number", + "minimum": 10, + "maximum": 300, + "description": "Minimum duration for MLB upcoming mode" + }, + "max_duration_seconds": { + "type": "number", + "minimum": 60, + "maximum": 600, + "description": "Max duration for MLB upcoming games" + } + } + } + } + } } - } } - } - } - } - } - } - }, - "milb": { - "type": "object", - "title": "MiLB Settings", - "description": "Configuration for MiLB (Minor League Baseball) games", - "properties": { - "enabled": { - "type": "boolean", - "default": false, - "description": "Enable MiLB games" - }, - "favorite_teams": { - "type": "array", - "items": { - "type": "string" - }, - "default": [], - "uniqueItems": true, - "maxItems": 120, - "description": "List of favorite MiLB team abbreviations (e.g., DUR, SWB). Use 2-4 letter codes." - }, - "display_modes": { - "type": "object", - "title": "Display Modes", - "description": "Control which game types to show", - "properties": { - "show_live": { - "type": "boolean", - "default": true, - "description": "Show live MiLB games" - }, - "show_recent": { - "type": "boolean", - "default": true, - "description": "Show recently completed MiLB games" - }, - "show_upcoming": { - "type": "boolean", - "default": true, - "description": "Show upcoming MiLB games" - }, - "live_display_mode": { - "type": "string", - "enum": ["switch", "scroll"], - "default": "switch", - "description": "Display mode for live games: 'switch' shows one game at a time, 'scroll' scrolls all games horizontally" - }, - "recent_display_mode": { - "type": "string", - "enum": ["switch", "scroll"], - "default": "switch", - "description": "Display mode for recent games: 'switch' shows one game at a time, 'scroll' scrolls all games horizontally" - }, - "upcoming_display_mode": { - "type": "string", - "enum": ["switch", "scroll"], - "default": "switch", - "description": "Display mode for upcoming games: 'switch' shows one game at a time, 'scroll' scrolls all games horizontally" - } - } - }, - "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 game 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 game card in scroll mode (pixels). Default 128. Set lower on multi-panel chains to show more games simultaneously." - } - } - }, - "live_priority": { - "type": "boolean", - "default": false, - "description": "Give live games priority over other modes. When enabled, live games will interrupt the normal mode rotation and be displayed immediately when available." - }, - "live_game_duration": { - "type": "integer", - "default": 30, - "minimum": 10, - "maximum": 120, - "description": "Duration in seconds to display each live game before rotating to the next live game" - }, - "recent_game_duration": { - "type": "number", - "default": 15, - "minimum": 10, - "maximum": 120, - "description": "Duration in seconds to show each recent game before rotating to the next game. If not set, uses the top-level game_display_duration setting (default: 15 seconds)." - }, - "upcoming_game_duration": { - "type": "number", - "default": 15, - "minimum": 10, - "maximum": 120, - "description": "Duration in seconds to show each upcoming game before rotating to the next game. If not set, uses the top-level game_display_duration setting (default: 15 seconds)." - }, - "live_update_interval": { - "type": "integer", - "default": 30, - "minimum": 5, - "maximum": 300, - "description": "How often to update live game data (seconds)" - }, - "update_interval_seconds": { - "type": "integer", - "default": 3600, - "minimum": 30, - "maximum": 86400, - "description": "How often to fetch new data for this league (seconds)" - }, - "game_limits": { - "type": "object", - "title": "Game Limits", - "description": "Control how many games to show", - "properties": { - "recent_games_to_show": { - "type": "integer", - "default": 1, - "minimum": 1, - "maximum": 20, - "description": "With favorites: N games per favorite team. Without favorites: N total games sorted by time." - }, - "upcoming_games_to_show": { - "type": "integer", - "default": 1, - "minimum": 1, - "maximum": 20, - "description": "With favorites: N games per favorite team. Without favorites: N total games sorted by time." - } - } - }, - "display_options": { - "type": "object", - "title": "Display Options", - "description": "Additional information to show", - "properties": { - "show_records": { - "type": "boolean", - "default": false, - "description": "Show team records (wins-losses)" - }, - "show_ranking": { - "type": "boolean", - "default": false, - "description": "Show team rankings (when available)" - }, - "show_odds": { - "type": "boolean", - "default": false, - "description": "Show betting odds" - }, - "show_series_summary": { - "type": "boolean", - "default": false, - "description": "Show series summary information" - } - } - }, - "filtering": { - "type": "object", - "title": "Filtering Options", - "description": "Control which teams are shown", - "properties": { - "show_favorite_teams_only": { - "type": "boolean", - "default": true, - "description": "Only show games from favorite teams" - }, - "show_all_live": { - "type": "boolean", - "default": false, - "description": "Show all live games, not just favorites" } - } }, - "mode_durations": { - "type": "object", - "title": "Mode Duration Overrides", - "description": "Override how long each mode displays before rotating", - "properties": { - "recent_mode_duration": { - "type": ["number", "null"], - "default": null, - "minimum": 10, - "maximum": 600, - "description": "Override display duration for recent games mode (seconds). Null uses default." - }, - "upcoming_mode_duration": { - "type": ["number", "null"], - "default": null, - "minimum": 10, - "maximum": 600, - "description": "Override display duration for upcoming games mode (seconds). Null uses default." - }, - "live_mode_duration": { - "type": ["number", "null"], - "default": null, - "minimum": 10, - "maximum": 600, - "description": "Override display duration for live games mode (seconds). Null uses default." - } - } - }, - "dynamic_duration": { - "type": "object", - "title": "MiLB Dynamic Duration Settings", - "description": "Configure dynamic duration settings for MiLB games.", - "properties": { - "enabled": { - "type": "boolean", - "default": false, - "description": "Enable dynamic duration for MiLB games" - }, - "min_duration_seconds": { - "type": "number", - "minimum": 10, - "maximum": 300, - "default": 30, - "description": "Minimum total duration in seconds for this mode, even if few games are available. Ensures the mode stays visible long enough." - }, - "max_duration_seconds": { - "type": "number", - "minimum": 60, - "maximum": 600, - "description": "Maximum total duration in seconds for this mode, even if many games are available." - }, - "modes": { - "type": "object", - "title": "Per-Mode Settings for MiLB", - "description": "Configure dynamic duration for specific MiLB modes", - "properties": { - "live": { - "type": "object", - "properties": { - "enabled": { - "type": "boolean", - "default": false, - "description": "Enable dynamic duration for MiLB live games" - }, - "min_duration_seconds": { - "type": "number", - "minimum": 10, - "maximum": 300, - "description": "Minimum duration for MiLB live mode" + "milb": { + "type": "object", + "title": "MiLB Settings", + "description": "Configuration for MiLB (Minor League Baseball) games", + "properties": { + "enabled": { + "type": "boolean", + "default": false, + "description": "Enable MiLB games" + }, + "favorite_teams": { + "type": "array", + "items": { + "type": "string" }, - "max_duration_seconds": { - "type": "number", - "minimum": 60, - "maximum": 600, - "description": "Max duration for MiLB live games" + "default": [], + "uniqueItems": true, + "maxItems": 120, + "description": "List of favorite MiLB team abbreviations (e.g., DUR, SWB). Use 2-4 letter codes." + }, + "display_modes": { + "type": "object", + "title": "Display Modes", + "description": "Control which game types to show", + "properties": { + "show_live": { + "type": "boolean", + "default": true, + "description": "Show live MiLB games" + }, + "show_recent": { + "type": "boolean", + "default": true, + "description": "Show recently completed MiLB games" + }, + "show_upcoming": { + "type": "boolean", + "default": true, + "description": "Show upcoming MiLB games" + }, + "live_display_mode": { + "type": "string", + "enum": [ + "switch", + "scroll" + ], + "default": "switch", + "description": "Display mode for live games: 'switch' shows one game at a time, 'scroll' scrolls all games horizontally" + }, + "recent_display_mode": { + "type": "string", + "enum": [ + "switch", + "scroll" + ], + "default": "switch", + "description": "Display mode for recent games: 'switch' shows one game at a time, 'scroll' scrolls all games horizontally" + }, + "upcoming_display_mode": { + "type": "string", + "enum": [ + "switch", + "scroll" + ], + "default": "switch", + "description": "Display mode for upcoming games: 'switch' shows one game at a time, 'scroll' scrolls all games horizontally" + } } - } }, - "recent": { - "type": "object", - "properties": { - "enabled": { - "type": "boolean", - "default": false, - "description": "Enable dynamic duration for MiLB recent games" - }, - "min_duration_seconds": { - "type": "number", - "minimum": 10, - "maximum": 300, - "description": "Minimum duration for MiLB recent mode" - }, - "max_duration_seconds": { - "type": "number", - "minimum": 60, - "maximum": 600, - "description": "Max duration for MiLB recent games" + "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 game 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 game card in scroll mode (pixels). Default 128. Set lower on multi-panel chains to show more games simultaneously." + } } - } }, - "upcoming": { - "type": "object", - "properties": { - "enabled": { - "type": "boolean", - "default": false, - "description": "Enable dynamic duration for MiLB upcoming games" - }, - "min_duration_seconds": { - "type": "number", - "minimum": 10, - "maximum": 300, - "description": "Minimum duration for MiLB upcoming mode" - }, - "max_duration_seconds": { - "type": "number", - "minimum": 60, - "maximum": 600, - "description": "Max duration for MiLB upcoming games" + "live_priority": { + "type": "boolean", + "default": false, + "description": "Give live games priority over other modes. When enabled, live games will interrupt the normal mode rotation and be displayed immediately when available." + }, + "live_game_duration": { + "type": "integer", + "default": 30, + "minimum": 10, + "maximum": 120, + "description": "Duration in seconds to display each live game before rotating to the next live game" + }, + "recent_game_duration": { + "type": "number", + "default": 15, + "minimum": 10, + "maximum": 120, + "description": "Duration in seconds to show each recent game before rotating to the next game. If not set, uses the top-level game_display_duration setting (default: 15 seconds)." + }, + "upcoming_game_duration": { + "type": "number", + "default": 15, + "minimum": 10, + "maximum": 120, + "description": "Duration in seconds to show each upcoming game before rotating to the next game. If not set, uses the top-level game_display_duration setting (default: 15 seconds)." + }, + "live_update_interval": { + "type": "integer", + "default": 30, + "minimum": 5, + "maximum": 300, + "description": "How often to update live game data (seconds)" + }, + "update_interval_seconds": { + "type": "integer", + "default": 3600, + "minimum": 30, + "maximum": 86400, + "description": "How often to fetch new data for this league (seconds)" + }, + "game_limits": { + "type": "object", + "title": "Game Limits", + "description": "Control how many games to show", + "properties": { + "recent_games_to_show": { + "type": "integer", + "default": 1, + "minimum": 1, + "maximum": 20, + "description": "With favorites: N games per favorite team. Without favorites: N total games sorted by time." + }, + "upcoming_games_to_show": { + "type": "integer", + "default": 1, + "minimum": 1, + "maximum": 20, + "description": "With favorites: N games per favorite team. Without favorites: N total games sorted by time." + } + } + }, + "display_options": { + "type": "object", + "title": "Display Options", + "description": "Additional information to show", + "properties": { + "show_records": { + "type": "boolean", + "default": false, + "description": "Show team records (wins-losses)" + }, + "show_ranking": { + "type": "boolean", + "default": false, + "description": "Show team rankings (when available)" + }, + "show_odds": { + "type": "boolean", + "default": false, + "description": "Show betting odds" + }, + "show_series_summary": { + "type": "boolean", + "default": false, + "description": "Show series summary information" + } + } + }, + "filtering": { + "type": "object", + "title": "Filtering Options", + "description": "Control which teams are shown", + "properties": { + "show_favorite_teams_only": { + "type": "boolean", + "default": true, + "description": "Only show games from favorite teams" + }, + "show_all_live": { + "type": "boolean", + "default": false, + "description": "Show all live games, not just favorites" + } + } + }, + "mode_durations": { + "type": "object", + "title": "Mode Duration Overrides", + "description": "Override how long each mode displays before rotating", + "properties": { + "recent_mode_duration": { + "type": [ + "number", + "null" + ], + "default": null, + "minimum": 10, + "maximum": 600, + "description": "Override display duration for recent games mode (seconds). Null uses default." + }, + "upcoming_mode_duration": { + "type": [ + "number", + "null" + ], + "default": null, + "minimum": 10, + "maximum": 600, + "description": "Override display duration for upcoming games mode (seconds). Null uses default." + }, + "live_mode_duration": { + "type": [ + "number", + "null" + ], + "default": null, + "minimum": 10, + "maximum": 600, + "description": "Override display duration for live games mode (seconds). Null uses default." + } + } + }, + "dynamic_duration": { + "type": "object", + "title": "MiLB Dynamic Duration Settings", + "description": "Configure dynamic duration settings for MiLB games.", + "properties": { + "enabled": { + "type": "boolean", + "default": false, + "description": "Enable dynamic duration for MiLB games" + }, + "min_duration_seconds": { + "type": "number", + "minimum": 10, + "maximum": 300, + "default": 30, + "description": "Minimum total duration in seconds for this mode, even if few games are available. Ensures the mode stays visible long enough." + }, + "max_duration_seconds": { + "type": "number", + "minimum": 60, + "maximum": 600, + "description": "Maximum total duration in seconds for this mode, even if many games are available." + }, + "modes": { + "type": "object", + "title": "Per-Mode Settings for MiLB", + "description": "Configure dynamic duration for specific MiLB modes", + "properties": { + "live": { + "type": "object", + "properties": { + "enabled": { + "type": "boolean", + "default": false, + "description": "Enable dynamic duration for MiLB live games" + }, + "min_duration_seconds": { + "type": "number", + "minimum": 10, + "maximum": 300, + "description": "Minimum duration for MiLB live mode" + }, + "max_duration_seconds": { + "type": "number", + "minimum": 60, + "maximum": 600, + "description": "Max duration for MiLB live games" + } + } + }, + "recent": { + "type": "object", + "properties": { + "enabled": { + "type": "boolean", + "default": false, + "description": "Enable dynamic duration for MiLB recent games" + }, + "min_duration_seconds": { + "type": "number", + "minimum": 10, + "maximum": 300, + "description": "Minimum duration for MiLB recent mode" + }, + "max_duration_seconds": { + "type": "number", + "minimum": 60, + "maximum": 600, + "description": "Max duration for MiLB recent games" + } + } + }, + "upcoming": { + "type": "object", + "properties": { + "enabled": { + "type": "boolean", + "default": false, + "description": "Enable dynamic duration for MiLB upcoming games" + }, + "min_duration_seconds": { + "type": "number", + "minimum": 10, + "maximum": 300, + "description": "Minimum duration for MiLB upcoming mode" + }, + "max_duration_seconds": { + "type": "number", + "minimum": 60, + "maximum": 600, + "description": "Max duration for MiLB upcoming games" + } + } + } + } + } } - } } - } - } - } - } - } - }, - "ncaa_baseball": { - "type": "object", - "title": "NCAA Baseball Settings", - "description": "Configuration for NCAA Baseball games", - "properties": { - "enabled": { - "type": "boolean", - "default": false, - "description": "Enable NCAA Baseball games" - }, - "favorite_teams": { - "type": "array", - "items": { - "type": "string" - }, - "default": [], - "uniqueItems": true, - "maxItems": 300, - "description": "List of favorite NCAA Baseball team abbreviations (e.g., LSU, FLA). Use 2-4 letter codes." - }, - "display_modes": { - "type": "object", - "title": "Display Modes", - "description": "Control which game types to show", - "properties": { - "show_live": { - "type": "boolean", - "default": true, - "description": "Show live NCAA Baseball games" - }, - "show_recent": { - "type": "boolean", - "default": true, - "description": "Show recently completed NCAA Baseball games" - }, - "show_upcoming": { - "type": "boolean", - "default": true, - "description": "Show upcoming NCAA Baseball games" - }, - "live_display_mode": { - "type": "string", - "enum": ["switch", "scroll"], - "default": "switch", - "description": "Display mode for live games: 'switch' shows one game at a time, 'scroll' scrolls all games horizontally" - }, - "recent_display_mode": { - "type": "string", - "enum": ["switch", "scroll"], - "default": "switch", - "description": "Display mode for recent games: 'switch' shows one game at a time, 'scroll' scrolls all games horizontally" - }, - "upcoming_display_mode": { - "type": "string", - "enum": ["switch", "scroll"], - "default": "switch", - "description": "Display mode for upcoming games: 'switch' shows one game at a time, 'scroll' scrolls all games horizontally" - } - } - }, - "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 game 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 game card in scroll mode (pixels). Default 128. Set lower on multi-panel chains to show more games simultaneously." - } - } - }, - "live_priority": { - "type": "boolean", - "default": true, - "description": "Give live games priority over other modes. When enabled, live games will interrupt the normal mode rotation and be displayed immediately when available." - }, - "live_game_duration": { - "type": "integer", - "default": 30, - "minimum": 10, - "maximum": 120, - "description": "Duration in seconds to display each live game before rotating to the next live game" - }, - "recent_game_duration": { - "type": "number", - "default": 15, - "minimum": 10, - "maximum": 120, - "description": "Duration in seconds to show each recent game before rotating to the next game. If not set, uses the top-level game_display_duration setting (default: 15 seconds)." - }, - "upcoming_game_duration": { - "type": "number", - "default": 15, - "minimum": 10, - "maximum": 120, - "description": "Duration in seconds to show each upcoming game before rotating to the next game. If not set, uses the top-level game_display_duration setting (default: 15 seconds)." - }, - "live_update_interval": { - "type": "integer", - "default": 30, - "minimum": 5, - "maximum": 300, - "description": "How often to update live game data (seconds)" - }, - "update_interval_seconds": { - "type": "integer", - "default": 3600, - "minimum": 30, - "maximum": 86400, - "description": "How often to fetch new data for this league (seconds)" - }, - "game_limits": { - "type": "object", - "title": "Game Limits", - "description": "Control how many games to show", - "properties": { - "recent_games_to_show": { - "type": "integer", - "default": 1, - "minimum": 1, - "maximum": 20, - "description": "With favorites: N games per favorite team. Without favorites: N total games sorted by time." - }, - "upcoming_games_to_show": { - "type": "integer", - "default": 1, - "minimum": 1, - "maximum": 20, - "description": "With favorites: N games per favorite team. Without favorites: N total games sorted by time." - } - } - }, - "display_options": { - "type": "object", - "title": "Display Options", - "description": "Additional information to show", - "properties": { - "show_records": { - "type": "boolean", - "default": false, - "description": "Show team records (wins-losses)" - }, - "show_ranking": { - "type": "boolean", - "default": false, - "description": "Show team rankings (rankings can be important in college baseball)" - }, - "show_odds": { - "type": "boolean", - "default": true, - "description": "Show betting odds" - }, - "show_series_summary": { - "type": "boolean", - "default": false, - "description": "Show series summary information" - } - } - }, - "filtering": { - "type": "object", - "title": "Filtering Options", - "description": "Control which teams are shown", - "properties": { - "show_favorite_teams_only": { - "type": "boolean", - "default": true, - "description": "Only show games from favorite teams" - }, - "show_all_live": { - "type": "boolean", - "default": false, - "description": "Show all live games, not just favorites" - } - } - }, - "mode_durations": { - "type": "object", - "title": "Mode Duration Overrides", - "description": "Override how long each mode displays before rotating", - "properties": { - "recent_mode_duration": { - "type": ["number", "null"], - "default": null, - "minimum": 10, - "maximum": 600, - "description": "Override display duration for recent games mode (seconds). Null uses default." - }, - "upcoming_mode_duration": { - "type": ["number", "null"], - "default": null, - "minimum": 10, - "maximum": 600, - "description": "Override display duration for upcoming games mode (seconds). Null uses default." - }, - "live_mode_duration": { - "type": ["number", "null"], - "default": null, - "minimum": 10, - "maximum": 600, - "description": "Override display duration for live games mode (seconds). Null uses default." } - } }, - "dynamic_duration": { - "type": "object", - "title": "NCAA Baseball Dynamic Duration Settings", - "description": "Configure dynamic duration settings for NCAA Baseball games.", - "properties": { - "enabled": { - "type": "boolean", - "default": false, - "description": "Enable dynamic duration for NCAA Baseball games" - }, - "min_duration_seconds": { - "type": "number", - "minimum": 10, - "maximum": 300, - "default": 30, - "description": "Minimum total duration in seconds for this mode, even if few games are available. Ensures the mode stays visible long enough." - }, - "max_duration_seconds": { - "type": "number", - "minimum": 60, - "maximum": 600, - "description": "Maximum total duration in seconds for this mode, even if many games are available." - }, - "modes": { - "type": "object", - "title": "Per-Mode Settings for NCAA Baseball", - "description": "Configure dynamic duration for specific NCAA Baseball modes", - "properties": { - "live": { - "type": "object", - "properties": { - "enabled": { - "type": "boolean", - "default": false, - "description": "Enable dynamic duration for NCAA Baseball live games" - }, - "min_duration_seconds": { - "type": "number", - "minimum": 10, - "maximum": 300, - "description": "Minimum duration for NCAA Baseball live mode" + "ncaa_baseball": { + "type": "object", + "title": "NCAA Baseball Settings", + "description": "Configuration for NCAA Baseball games", + "properties": { + "enabled": { + "type": "boolean", + "default": false, + "description": "Enable NCAA Baseball games" + }, + "favorite_teams": { + "type": "array", + "items": { + "type": "string" }, - "max_duration_seconds": { - "type": "number", - "minimum": 60, - "maximum": 600, - "description": "Max duration for NCAA Baseball live games" + "default": [], + "uniqueItems": true, + "maxItems": 300, + "description": "List of favorite NCAA Baseball team abbreviations (e.g., LSU, FLA). Use 2-4 letter codes." + }, + "display_modes": { + "type": "object", + "title": "Display Modes", + "description": "Control which game types to show", + "properties": { + "show_live": { + "type": "boolean", + "default": true, + "description": "Show live NCAA Baseball games" + }, + "show_recent": { + "type": "boolean", + "default": true, + "description": "Show recently completed NCAA Baseball games" + }, + "show_upcoming": { + "type": "boolean", + "default": true, + "description": "Show upcoming NCAA Baseball games" + }, + "live_display_mode": { + "type": "string", + "enum": [ + "switch", + "scroll" + ], + "default": "switch", + "description": "Display mode for live games: 'switch' shows one game at a time, 'scroll' scrolls all games horizontally" + }, + "recent_display_mode": { + "type": "string", + "enum": [ + "switch", + "scroll" + ], + "default": "switch", + "description": "Display mode for recent games: 'switch' shows one game at a time, 'scroll' scrolls all games horizontally" + }, + "upcoming_display_mode": { + "type": "string", + "enum": [ + "switch", + "scroll" + ], + "default": "switch", + "description": "Display mode for upcoming games: 'switch' shows one game at a time, 'scroll' scrolls all games horizontally" + } } - } }, - "recent": { - "type": "object", - "properties": { - "enabled": { - "type": "boolean", - "default": false, - "description": "Enable dynamic duration for NCAA Baseball recent games" - }, - "min_duration_seconds": { - "type": "number", - "minimum": 10, - "maximum": 300, - "description": "Minimum duration for NCAA Baseball recent mode" - }, - "max_duration_seconds": { - "type": "number", - "minimum": 60, - "maximum": 600, - "description": "Max duration for NCAA Baseball recent games" + "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 game 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 game card in scroll mode (pixels). Default 128. Set lower on multi-panel chains to show more games simultaneously." + } } - } }, - "upcoming": { - "type": "object", - "properties": { - "enabled": { - "type": "boolean", - "default": false, - "description": "Enable dynamic duration for NCAA Baseball upcoming games" - }, - "min_duration_seconds": { - "type": "number", - "minimum": 10, - "maximum": 300, - "description": "Minimum duration for NCAA Baseball upcoming mode" - }, - "max_duration_seconds": { - "type": "number", - "minimum": 60, - "maximum": 600, - "description": "Max duration for NCAA Baseball upcoming games" + "live_priority": { + "type": "boolean", + "default": true, + "description": "Give live games priority over other modes. When enabled, live games will interrupt the normal mode rotation and be displayed immediately when available." + }, + "live_game_duration": { + "type": "integer", + "default": 30, + "minimum": 10, + "maximum": 120, + "description": "Duration in seconds to display each live game before rotating to the next live game" + }, + "recent_game_duration": { + "type": "number", + "default": 15, + "minimum": 10, + "maximum": 120, + "description": "Duration in seconds to show each recent game before rotating to the next game. If not set, uses the top-level game_display_duration setting (default: 15 seconds)." + }, + "upcoming_game_duration": { + "type": "number", + "default": 15, + "minimum": 10, + "maximum": 120, + "description": "Duration in seconds to show each upcoming game before rotating to the next game. If not set, uses the top-level game_display_duration setting (default: 15 seconds)." + }, + "live_update_interval": { + "type": "integer", + "default": 30, + "minimum": 5, + "maximum": 300, + "description": "How often to update live game data (seconds)" + }, + "update_interval_seconds": { + "type": "integer", + "default": 3600, + "minimum": 30, + "maximum": 86400, + "description": "How often to fetch new data for this league (seconds)" + }, + "game_limits": { + "type": "object", + "title": "Game Limits", + "description": "Control how many games to show", + "properties": { + "recent_games_to_show": { + "type": "integer", + "default": 1, + "minimum": 1, + "maximum": 20, + "description": "With favorites: N games per favorite team. Without favorites: N total games sorted by time." + }, + "upcoming_games_to_show": { + "type": "integer", + "default": 1, + "minimum": 1, + "maximum": 20, + "description": "With favorites: N games per favorite team. Without favorites: N total games sorted by time." + } + } + }, + "display_options": { + "type": "object", + "title": "Display Options", + "description": "Additional information to show", + "properties": { + "show_records": { + "type": "boolean", + "default": false, + "description": "Show team records (wins-losses)" + }, + "show_ranking": { + "type": "boolean", + "default": false, + "description": "Show team rankings (rankings can be important in college baseball)" + }, + "show_odds": { + "type": "boolean", + "default": true, + "description": "Show betting odds" + }, + "show_series_summary": { + "type": "boolean", + "default": false, + "description": "Show series summary information" + } + } + }, + "filtering": { + "type": "object", + "title": "Filtering Options", + "description": "Control which teams are shown", + "properties": { + "show_favorite_teams_only": { + "type": "boolean", + "default": true, + "description": "Only show games from favorite teams" + }, + "show_all_live": { + "type": "boolean", + "default": false, + "description": "Show all live games, not just favorites" + } + } + }, + "mode_durations": { + "type": "object", + "title": "Mode Duration Overrides", + "description": "Override how long each mode displays before rotating", + "properties": { + "recent_mode_duration": { + "type": [ + "number", + "null" + ], + "default": null, + "minimum": 10, + "maximum": 600, + "description": "Override display duration for recent games mode (seconds). Null uses default." + }, + "upcoming_mode_duration": { + "type": [ + "number", + "null" + ], + "default": null, + "minimum": 10, + "maximum": 600, + "description": "Override display duration for upcoming games mode (seconds). Null uses default." + }, + "live_mode_duration": { + "type": [ + "number", + "null" + ], + "default": null, + "minimum": 10, + "maximum": 600, + "description": "Override display duration for live games mode (seconds). Null uses default." + } + } + }, + "dynamic_duration": { + "type": "object", + "title": "NCAA Baseball Dynamic Duration Settings", + "description": "Configure dynamic duration settings for NCAA Baseball games.", + "properties": { + "enabled": { + "type": "boolean", + "default": false, + "description": "Enable dynamic duration for NCAA Baseball games" + }, + "min_duration_seconds": { + "type": "number", + "minimum": 10, + "maximum": 300, + "default": 30, + "description": "Minimum total duration in seconds for this mode, even if few games are available. Ensures the mode stays visible long enough." + }, + "max_duration_seconds": { + "type": "number", + "minimum": 60, + "maximum": 600, + "description": "Maximum total duration in seconds for this mode, even if many games are available." + }, + "modes": { + "type": "object", + "title": "Per-Mode Settings for NCAA Baseball", + "description": "Configure dynamic duration for specific NCAA Baseball modes", + "properties": { + "live": { + "type": "object", + "properties": { + "enabled": { + "type": "boolean", + "default": false, + "description": "Enable dynamic duration for NCAA Baseball live games" + }, + "min_duration_seconds": { + "type": "number", + "minimum": 10, + "maximum": 300, + "description": "Minimum duration for NCAA Baseball live mode" + }, + "max_duration_seconds": { + "type": "number", + "minimum": 60, + "maximum": 600, + "description": "Max duration for NCAA Baseball live games" + } + } + }, + "recent": { + "type": "object", + "properties": { + "enabled": { + "type": "boolean", + "default": false, + "description": "Enable dynamic duration for NCAA Baseball recent games" + }, + "min_duration_seconds": { + "type": "number", + "minimum": 10, + "maximum": 300, + "description": "Minimum duration for NCAA Baseball recent mode" + }, + "max_duration_seconds": { + "type": "number", + "minimum": 60, + "maximum": 600, + "description": "Max duration for NCAA Baseball recent games" + } + } + }, + "upcoming": { + "type": "object", + "properties": { + "enabled": { + "type": "boolean", + "default": false, + "description": "Enable dynamic duration for NCAA Baseball upcoming games" + }, + "min_duration_seconds": { + "type": "number", + "minimum": 10, + "maximum": 300, + "description": "Minimum duration for NCAA Baseball upcoming mode" + }, + "max_duration_seconds": { + "type": "number", + "minimum": 60, + "maximum": 600, + "description": "Max duration for NCAA Baseball upcoming games" + } + } + } + } + } } - } } - } - } - } - } - } - }, - "customization": { - "type": "object", - "title": "Display Customization", - "description": "Customize fonts, colors, and layout positioning for scoreboard elements", - "properties": { - "score_text": { - "type": "object", - "title": "Score Text", - "description": "Customize the score display", - "properties": { - "font": { - "type": "string", - "default": "press_start", - "description": "Font family for score text" - }, - "font_size": { - "type": "integer", - "default": 10, - "description": "Font size for score text" - } - }, - "additionalProperties": false - }, - "status_text": { - "type": "object", - "title": "Status Text", - "description": "Customize the status/inning display", - "properties": { - "font": { - "type": "string", - "default": "press_start", - "description": "Font family for status text" - }, - "font_size": { - "type": "integer", - "default": 8, - "description": "Font size for status text" - } - }, - "additionalProperties": false - }, - "detail_text": { - "type": "object", - "title": "Detail Text", - "description": "Customize detail text (records, etc.)", - "properties": { - "font": { - "type": "string", - "default": "four_by_six", - "description": "Font family for detail text" - }, - "font_size": { - "type": "integer", - "default": 6, - "description": "Font size for detail text" } - }, - "additionalProperties": false }, - "layout": { - "type": "object", - "title": "Layout Positioning", - "description": "Adjust X,Y coordinate offsets for elements. Values are relative to default positions. Use negative values to move left/up, positive to move right/down.", - "properties": { - "home_logo": { - "type": "object", - "title": "Home Team Logo", - "properties": { - "x_offset": { - "type": "integer", - "default": 0, - "description": "Horizontal offset from default position (default: 0)" - }, - "y_offset": { - "type": "integer", - "default": 0, - "description": "Vertical offset from default position (default: 0)" - } - }, - "additionalProperties": false - }, - "away_logo": { - "type": "object", - "title": "Away Team Logo", - "properties": { - "x_offset": { - "type": "integer", - "default": 0, - "description": "Horizontal offset from default position (default: 0)" + "customization": { + "type": "object", + "title": "Display Customization", + "description": "Customize fonts, colors, and layout positioning for scoreboard elements", + "properties": { + "score_text": { + "type": "object", + "title": "Score Text", + "description": "Customize the score display", + "properties": { + "font": { + "type": "string", + "default": "press_start", + "description": "Font family for score text" + }, + "font_size": { + "type": "integer", + "default": 10, + "description": "Font size for score text" + } + }, + "additionalProperties": false }, - "y_offset": { - "type": "integer", - "default": 0, - "description": "Vertical offset from default position (default: 0)" - } - }, - "additionalProperties": false - }, - "score": { - "type": "object", - "title": "Game Score", - "properties": { - "x_offset": { - "type": "integer", - "default": 0, - "description": "Horizontal offset from center (default: 0)" + "status_text": { + "type": "object", + "title": "Status Text", + "description": "Customize the status/inning display", + "properties": { + "font": { + "type": "string", + "default": "press_start", + "description": "Font family for status text" + }, + "font_size": { + "type": "integer", + "default": 8, + "description": "Font size for status text" + } + }, + "additionalProperties": false }, - "y_offset": { - "type": "integer", - "default": 0, - "description": "Vertical offset from center (default: 0)" - } - }, - "additionalProperties": false - }, - "status": { - "type": "object", - "title": "Game Status/Inning", - "properties": { - "x_offset": { - "type": "integer", - "default": 0, - "description": "Horizontal offset from default position (default: 0)" + "detail_text": { + "type": "object", + "title": "Detail Text", + "description": "Customize detail text (records, etc.)", + "properties": { + "font": { + "type": "string", + "default": "four_by_six", + "description": "Font family for detail text" + }, + "font_size": { + "type": "integer", + "default": 6, + "description": "Font size for detail text" + } + }, + "additionalProperties": false }, - "y_offset": { - "type": "integer", - "default": 0, - "description": "Vertical offset from default position (default: 0)" - } - }, - "additionalProperties": false - }, - "record": { - "type": "object", - "title": "Team Records", - "properties": { - "x_offset": { - "type": "integer", - "default": 0, - "description": "Horizontal offset from default position (default: 0)" + "layout": { + "type": "object", + "title": "Layout Positioning", + "description": "Adjust X,Y coordinate offsets for elements. Values are relative to default positions. Use negative values to move left/up, positive to move right/down.", + "properties": { + "home_logo": { + "type": "object", + "title": "Home Team Logo", + "properties": { + "x_offset": { + "type": "integer", + "default": 0, + "description": "Horizontal offset from default position (default: 0)" + }, + "y_offset": { + "type": "integer", + "default": 0, + "description": "Vertical offset from default position (default: 0)" + } + }, + "additionalProperties": false + }, + "away_logo": { + "type": "object", + "title": "Away Team Logo", + "properties": { + "x_offset": { + "type": "integer", + "default": 0, + "description": "Horizontal offset from default position (default: 0)" + }, + "y_offset": { + "type": "integer", + "default": 0, + "description": "Vertical offset from default position (default: 0)" + } + }, + "additionalProperties": false + }, + "score": { + "type": "object", + "title": "Game Score", + "properties": { + "x_offset": { + "type": "integer", + "default": 0, + "description": "Horizontal offset from center (default: 0)" + }, + "y_offset": { + "type": "integer", + "default": 0, + "description": "Vertical offset from center (default: 0)" + } + }, + "additionalProperties": false + }, + "status": { + "type": "object", + "title": "Game Status/Inning", + "properties": { + "x_offset": { + "type": "integer", + "default": 0, + "description": "Horizontal offset from default position (default: 0)" + }, + "y_offset": { + "type": "integer", + "default": 0, + "description": "Vertical offset from default position (default: 0)" + } + }, + "additionalProperties": false + }, + "record": { + "type": "object", + "title": "Team Records", + "properties": { + "x_offset": { + "type": "integer", + "default": 0, + "description": "Horizontal offset from default position (default: 0)" + }, + "y_offset": { + "type": "integer", + "default": 0, + "description": "Vertical offset from default position (default: 0)" + }, + "away_x_offset": { + "type": "integer", + "default": 0, + "description": "Additional horizontal offset for away team record (default: 0)" + }, + "home_x_offset": { + "type": "integer", + "default": 0, + "description": "Additional horizontal offset for home team record (default: 0)" + } + }, + "additionalProperties": false + }, + "ranking": { + "type": "object", + "title": "Team Rankings", + "properties": { + "x_offset": { + "type": "integer", + "default": 0, + "description": "Horizontal offset from default position (default: 0)" + }, + "y_offset": { + "type": "integer", + "default": 0, + "description": "Vertical offset from default position (default: 0)" + } + }, + "additionalProperties": false + }, + "odds": { + "type": "object", + "title": "Betting Odds", + "properties": { + "x_offset": { + "type": "integer", + "default": 0, + "description": "Horizontal offset from default position (default: 0)" + }, + "y_offset": { + "type": "integer", + "default": 0, + "description": "Vertical offset from default position (default: 0)" + } + }, + "additionalProperties": false + } + }, + "x-propertyOrder": [ + "home_logo", + "away_logo", + "score", + "status", + "record", + "ranking", + "odds" + ], + "additionalProperties": false }, - "y_offset": { - "type": "integer", - "default": 0, - "description": "Vertical offset from default position (default: 0)" + "period_text": { + "type": "object", + "title": "Period/Inning Text", + "description": "Customize the period/inning display font", + "properties": { + "font": { + "type": "string", + "default": "press_start", + "description": "Font family for period/inning text" + }, + "font_size": { + "type": "integer", + "default": 8, + "description": "Font size for period/inning text" + } + }, + "additionalProperties": false }, - "away_x_offset": { - "type": "integer", - "default": 0, - "description": "Additional horizontal offset for away team record (default: 0)" + "team_name": { + "type": "object", + "title": "Team Name Text", + "description": "Customize the team name/abbreviation display", + "properties": { + "font": { + "type": "string", + "default": "press_start", + "description": "Font family for team name text" + }, + "font_size": { + "type": "integer", + "default": 8, + "description": "Font size for team name text" + } + }, + "additionalProperties": false }, - "home_x_offset": { - "type": "integer", - "default": 0, - "description": "Additional horizontal offset for home team record (default: 0)" + "rank_text": { + "type": "object", + "title": "Ranking Text", + "description": "Customize the ranking display font", + "properties": { + "font": { + "type": "string", + "default": "press_start", + "description": "Font family for ranking text" + }, + "font_size": { + "type": "integer", + "default": 10, + "description": "Font size for ranking text" + } + }, + "additionalProperties": false } - }, - "additionalProperties": false }, - "ranking": { - "type": "object", - "title": "Team Rankings", - "properties": { - "x_offset": { - "type": "integer", - "default": 0, - "description": "Horizontal offset from default position (default: 0)" - }, - "y_offset": { - "type": "integer", - "default": 0, - "description": "Vertical offset from default position (default: 0)" - } - }, - "additionalProperties": false - } - }, - "x-propertyOrder": ["home_logo", "away_logo", "score", "status", "record", "ranking"], - "additionalProperties": false + "x-propertyOrder": [ + "score_text", + "period_text", + "team_name", + "status_text", + "detail_text", + "rank_text", + "layout" + ], + "additionalProperties": false } - }, - "x-propertyOrder": ["score_text", "status_text", "detail_text", "layout"], - "additionalProperties": false - } - }, - "additionalProperties": false, - "required": ["enabled"] + }, + "additionalProperties": false, + "required": [ + "enabled" + ] } diff --git a/plugins/baseball-scoreboard/game_renderer.py b/plugins/baseball-scoreboard/game_renderer.py index 8f17615..6e4863f 100644 --- a/plugins/baseball-scoreboard/game_renderer.py +++ b/plugins/baseball-scoreboard/game_renderer.py @@ -527,6 +527,16 @@ def _draw_records(self, draw, game: Dict): home_w = home_bbox[2] - home_bbox[0] self._draw_text_with_outline(draw, home_text, (self.display_width - home_w, record_y), record_font) + def _get_layout_offset(self, element: str, axis: str, default: int = 0) -> int: + """Get layout offset for a specific element and axis from config.""" + try: + layout_config = self.config.get('customization', {}).get('layout', {}) + element_config = layout_config.get(element, {}) + offset_value = element_config.get(axis, default) + return int(offset_value) if offset_value is not None else default + except (TypeError, ValueError): + return default + def _draw_dynamic_odds(self, draw, odds: Dict) -> None: """Draw odds with dynamic positioning based on favored team.""" try: @@ -560,9 +570,13 @@ def _draw_dynamic_odds(self, draw, odds: Dict) -> None: favored_spread = away_spread favored_side = 'away' + # Get user-configurable layout offsets for odds + odds_x_offset = self._get_layout_offset('odds', 'x_offset') + odds_y_offset = self._get_layout_offset('odds', 'y_offset') + # Odds row below the status/inning text row status_bbox = draw.textbbox((0, 0), "A", font=self.fonts['detail']) - odds_y = status_bbox[3] + 2 # just below the status row + odds_y = status_bbox[3] + 2 + odds_y_offset # Show the negative spread on the appropriate side font = self.fonts['detail'] @@ -570,9 +584,9 @@ def _draw_dynamic_odds(self, draw, odds: Dict) -> None: spread_text = str(favored_spread) spread_width = draw.textlength(spread_text, font=font) if favored_side == 'home': - spread_x = self.display_width - spread_width + spread_x = self.display_width - spread_width + odds_x_offset else: - spread_x = 0 + spread_x = 0 + odds_x_offset self._draw_text_with_outline(draw, spread_text, (spread_x, odds_y), font, fill=(0, 255, 0)) # Show over/under on opposite side @@ -581,11 +595,11 @@ def _draw_dynamic_odds(self, draw, odds: Dict) -> None: ou_text = f"O/U: {over_under}" ou_width = draw.textlength(ou_text, font=font) if favored_side == 'home': - ou_x = 0 + ou_x = 0 + odds_x_offset elif favored_side == 'away': - ou_x = self.display_width - ou_width + ou_x = self.display_width - ou_width + odds_x_offset else: - ou_x = (self.display_width - ou_width) // 2 + ou_x = (self.display_width - ou_width) // 2 + odds_x_offset self._draw_text_with_outline(draw, ou_text, (ou_x, odds_y), font, fill=(0, 255, 0)) except Exception: diff --git a/plugins/baseball-scoreboard/manifest.json b/plugins/baseball-scoreboard/manifest.json index 69fc693..2fb7ffb 100644 --- a/plugins/baseball-scoreboard/manifest.json +++ b/plugins/baseball-scoreboard/manifest.json @@ -1,121 +1,121 @@ { - "id": "baseball-scoreboard", - "name": "Baseball Scoreboard", - "version": "1.5.6", - "author": "ChuckBuilds", - "description": "Live, recent, and upcoming baseball games across MLB, MiLB, and NCAA Baseball with real-time scores and schedules", - "category": "sports", - "tags": [ - "baseball", - "mlb", - "milb", - "ncaa", - "college-baseball", - "sports", - "scoreboard", - "live-scores" - ], - "display_modes": [ - "mlb_live", - "mlb_recent", - "mlb_upcoming", - "milb_live", - "milb_recent", - "milb_upcoming", - "ncaa_baseball_live", - "ncaa_baseball_recent", - "ncaa_baseball_upcoming" - ], - "repo": "https://github.com/ChuckBuilds/ledmatrix-plugins", - "branch": "main", - "plugin_path": "plugins/baseball-scoreboard", - "versions": [ - { - "released": "2026-03-29", - "version": "1.5.6", - "ledmatrix_min": "2.0.0" - }, - { - "released": "2026-03-29", - "version": "1.5.5", - "ledmatrix_min": "2.0.0" - }, - { - "released": "2026-03-02", - "version": "1.5.4", - "ledmatrix_min": "2.0.0" - }, - { - "released": "2026-02-24", - "version": "1.5.3", - "ledmatrix_min": "2.0.0" - }, - { - "released": "2026-02-24", - "version": "1.5.2", - "ledmatrix_min": "2.0.0" - }, - { - "released": "2026-02-24", - "version": "1.5.1", - "ledmatrix_min": "2.0.0" - }, - { - "released": "2026-02-24", - "version": "1.5.0", - "ledmatrix_min": "2.0.0" - }, - { - "released": "2026-02-20", - "version": "1.4.0", - "ledmatrix_min": "2.0.0" - }, - { - "released": "2026-02-17", - "version": "1.3.1", - "ledmatrix_min": "2.0.0" - }, - { - "released": "2026-02-15", - "version": "1.3.0", - "ledmatrix_min": "2.0.0" - }, - { - "released": "2026-02-14", - "version": "1.2.0", - "ledmatrix_min": "2.0.0" - }, - { - "released": "2026-02-13", - "version": "1.1.0", - "ledmatrix_min": "2.0.0" - }, - { - "released": "2026-02-12", - "version": "1.0.5", - "ledmatrix_min": "2.0.0" - }, - { - "released": "2025-10-20", - "version": "1.0.4", - "ledmatrix_min": "2.0.0" - }, - { - "released": "2025-10-20", - "version": "1.0.3", - "ledmatrix_min": "2.0.0" - }, - { - "released": "2025-10-19", - "version": "1.0.2", - "ledmatrix_min": "2.0.0" - } - ], - "last_updated": "2026-03-29", - "stars": 0, - "downloads": 0, - "verified": true, - "screenshot": "", - "class_name": "BaseballScoreboardPlugin", - "entry_point": "manager.py" + "id": "baseball-scoreboard", + "name": "Baseball Scoreboard", + "version": "1.5.7", + "author": "ChuckBuilds", + "description": "Live, recent, and upcoming baseball games across MLB, MiLB, and NCAA Baseball with real-time scores and schedules", + "category": "sports", + "tags": [ + "baseball", + "mlb", + "milb", + "ncaa", + "college-baseball", + "sports", + "scoreboard", + "live-scores" + ], + "display_modes": [ + "mlb_live", + "mlb_recent", + "mlb_upcoming", + "milb_live", + "milb_recent", + "milb_upcoming", + "ncaa_baseball_live", + "ncaa_baseball_recent", + "ncaa_baseball_upcoming" + ], + "repo": "https://github.com/ChuckBuilds/ledmatrix-plugins", + "branch": "main", + "plugin_path": "plugins/baseball-scoreboard", + "versions": [ + { + "released": "2026-03-29", + "version": "1.5.6", + "ledmatrix_min": "2.0.0" + }, + { + "released": "2026-03-29", + "version": "1.5.5", + "ledmatrix_min": "2.0.0" + }, + { + "released": "2026-03-02", + "version": "1.5.4", + "ledmatrix_min": "2.0.0" + }, + { + "released": "2026-02-24", + "version": "1.5.3", + "ledmatrix_min": "2.0.0" + }, + { + "released": "2026-02-24", + "version": "1.5.2", + "ledmatrix_min": "2.0.0" + }, + { + "released": "2026-02-24", + "version": "1.5.1", + "ledmatrix_min": "2.0.0" + }, + { + "released": "2026-02-24", + "version": "1.5.0", + "ledmatrix_min": "2.0.0" + }, + { + "released": "2026-02-20", + "version": "1.4.0", + "ledmatrix_min": "2.0.0" + }, + { + "released": "2026-02-17", + "version": "1.3.1", + "ledmatrix_min": "2.0.0" + }, + { + "released": "2026-02-15", + "version": "1.3.0", + "ledmatrix_min": "2.0.0" + }, + { + "released": "2026-02-14", + "version": "1.2.0", + "ledmatrix_min": "2.0.0" + }, + { + "released": "2026-02-13", + "version": "1.1.0", + "ledmatrix_min": "2.0.0" + }, + { + "released": "2026-02-12", + "version": "1.0.5", + "ledmatrix_min": "2.0.0" + }, + { + "released": "2025-10-20", + "version": "1.0.4", + "ledmatrix_min": "2.0.0" + }, + { + "released": "2025-10-20", + "version": "1.0.3", + "ledmatrix_min": "2.0.0" + }, + { + "released": "2025-10-19", + "version": "1.0.2", + "ledmatrix_min": "2.0.0" + } + ], + "last_updated": "2026-03-29", + "stars": 0, + "downloads": 0, + "verified": true, + "screenshot": "", + "class_name": "BaseballScoreboardPlugin", + "entry_point": "manager.py" } diff --git a/plugins/baseball-scoreboard/sports.py b/plugins/baseball-scoreboard/sports.py index 606a302..0cdba3c 100644 --- a/plugins/baseball-scoreboard/sports.py +++ b/plugins/baseball-scoreboard/sports.py @@ -400,6 +400,10 @@ def _draw_dynamic_odds( "No clear favorite - spreads: home={home_spread}, away={away_spread}" ) + # Get user-configurable layout offsets for odds + odds_x_offset = self._get_layout_offset('odds', 'x_offset') + odds_y_offset = self._get_layout_offset('odds', 'y_offset') + # Show the negative spread on the appropriate side if favored_spread is not None: spread_text = str(favored_spread) @@ -408,8 +412,8 @@ def _draw_dynamic_odds( if favored_side == "home": # Home team is favored, show spread on right side spread_width = draw.textlength(spread_text, font=font) - spread_x = width - spread_width # Top right - spread_y = 0 + spread_x = width - spread_width + odds_x_offset + spread_y = 0 + odds_y_offset self._draw_text_with_outline( draw, spread_text, (spread_x, spread_y), font, fill=(0, 255, 0) ) @@ -418,8 +422,8 @@ def _draw_dynamic_odds( ) else: # Away team is favored, show spread on left side - spread_x = 0 # Top left - spread_y = 0 + spread_x = 0 + odds_x_offset + spread_y = 0 + odds_y_offset self._draw_text_with_outline( draw, spread_text, (spread_x, spread_y), font, fill=(0, 255, 0) ) @@ -436,22 +440,22 @@ def _draw_dynamic_odds( if favored_side == "home": # Home favored, show O/U on left side (opposite of spread) - ou_x = 0 # Top left - ou_y = 0 + ou_x = 0 + odds_x_offset + ou_y = 0 + odds_y_offset self.logger.debug( f"Showing O/U '{ou_text}' on left side (home favored)" ) elif favored_side == "away": # Away favored, show O/U on right side (opposite of spread) - ou_x = width - ou_width # Top right - ou_y = 0 + ou_x = width - ou_width + odds_x_offset + ou_y = 0 + odds_y_offset self.logger.debug( f"Showing O/U '{ou_text}' on right side (away favored)" ) else: # No clear favorite, show O/U in center - ou_x = (width - ou_width) // 2 - ou_y = 0 + ou_x = (width - ou_width) // 2 + odds_x_offset + ou_y = 0 + odds_y_offset self.logger.debug( f"Showing O/U '{ou_text}' in center (no clear favorite)" ) diff --git a/plugins/basketball-scoreboard/game_renderer.py b/plugins/basketball-scoreboard/game_renderer.py index 3820940..88c8112 100644 --- a/plugins/basketball-scoreboard/game_renderer.py +++ b/plugins/basketball-scoreboard/game_renderer.py @@ -371,7 +371,11 @@ def render_game_card( show_tourney_seeds = game.get("is_tournament", False) and self._get_mm_setting(game, 'show_seeds') if self.show_records or self.show_ranking or show_tourney_seeds: self._draw_records_or_rankings(draw_overlay, game) - + + # Draw odds if available + if game.get('odds'): + self._draw_dynamic_odds(draw_overlay, game['odds']) + # Composite the overlay onto main image main_img = Image.alpha_composite(main_img, overlay) return main_img.convert('RGB') @@ -455,17 +459,27 @@ def _draw_upcoming_game_status(self, draw: ImageDraw.Draw, game: Dict) -> None: time_y = (self.display_height // 2) - 7 + 9 self._draw_text_with_outline(draw, game_time, (time_x, time_y), self.fonts['time']) + def _get_layout_offset(self, element: str, axis: str, default: int = 0) -> int: + """Get layout offset for a specific element and axis from config.""" + try: + layout_config = self.config.get('customization', {}).get('layout', {}) + element_config = layout_config.get(element, {}) + offset_value = element_config.get(axis, default) + return int(offset_value) if offset_value is not None else default + except (TypeError, ValueError): + return default + def _draw_dynamic_odds(self, draw: ImageDraw.Draw, odds: Dict[str, Any]) -> None: - """Draw odds with dynamic positioning.""" + """Draw odds with dynamic positioning and user-configurable offsets.""" try: if not odds: return - + home_team_odds = odds.get("home_team_odds", {}) away_team_odds = odds.get("away_team_odds", {}) home_spread = home_team_odds.get("spread_odds") away_spread = away_team_odds.get("spread_odds") - + # Get top-level spread as fallback top_level_spread = odds.get("spread") if top_level_spread is not None: @@ -473,51 +487,54 @@ def _draw_dynamic_odds(self, draw: ImageDraw.Draw, odds: Dict[str, Any]) -> None home_spread = top_level_spread if away_spread is None: away_spread = -top_level_spread - + # Determine favored team home_favored = home_spread is not None and isinstance(home_spread, (int, float)) and home_spread < 0 away_favored = away_spread is not None and isinstance(away_spread, (int, float)) and away_spread < 0 - + favored_spread = None favored_side = None - + if home_favored: favored_spread = home_spread favored_side = "home" elif away_favored: favored_spread = away_spread favored_side = "away" - + + odds_x_offset = self._get_layout_offset('odds', 'x_offset') + odds_y_offset = self._get_layout_offset('odds', 'y_offset') + # Show the negative spread if favored_spread is not None: spread_text = str(favored_spread) font = self.fonts["detail"] - + if favored_side == "home": spread_width = draw.textlength(spread_text, font=font) - spread_x = self.display_width - spread_width - spread_y = 0 + spread_x = self.display_width - spread_width + odds_x_offset + spread_y = 0 + odds_y_offset else: - spread_x = 0 - spread_y = 0 - + spread_x = 0 + odds_x_offset + spread_y = 0 + odds_y_offset + self._draw_text_with_outline(draw, spread_text, (spread_x, spread_y), font, fill=(0, 255, 0)) - + # Show over/under on opposite side over_under = odds.get("over_under") if over_under is not None and isinstance(over_under, (int, float)): ou_text = f"O/U: {over_under}" font = self.fonts["detail"] ou_width = draw.textlength(ou_text, font=font) - + if favored_side == "home": - ou_x = 0 + ou_x = 0 + odds_x_offset elif favored_side == "away": - ou_x = self.display_width - ou_width + ou_x = self.display_width - ou_width + odds_x_offset else: - ou_x = (self.display_width - ou_width) // 2 - ou_y = 0 - + ou_x = (self.display_width - ou_width) // 2 + odds_x_offset + ou_y = 0 + odds_y_offset + self._draw_text_with_outline(draw, ou_text, (ou_x, ou_y), font, fill=(0, 255, 0)) except Exception as e: diff --git a/plugins/basketball-scoreboard/manifest.json b/plugins/basketball-scoreboard/manifest.json index 77d5535..26800b9 100644 --- a/plugins/basketball-scoreboard/manifest.json +++ b/plugins/basketball-scoreboard/manifest.json @@ -1,94 +1,94 @@ { - "id": "basketball-scoreboard", - "name": "Basketball Scoreboard", - "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", - "tags": [ - "basketball", - "nba", - "ncaa", - "wnba", - "sports", - "scoreboard", - "live-scores" - ], - "repo": "https://github.com/ChuckBuilds/ledmatrix-plugins", - "branch": "main", - "plugin_path": "plugins/basketball-scoreboard", - "versions": [ - { - "released": "2026-03-02", - "version": "1.5.4", - "ledmatrix_min": "2.0.0" - }, - { - "released": "2026-02-24", - "version": "1.5.3", - "ledmatrix_min": "2.0.0" - }, - { - "version": "1.5.2", - "ledmatrix_min": "2.0.0", - "released": "2026-02-24" - }, - { - "version": "1.5.1", - "ledmatrix_min": "2.0.0", - "released": "2026-02-24" - }, - { - "version": "1.5.0", - "ledmatrix_min": "2.0.0", - "released": "2026-02-24" - }, - { - "version": "1.4.0", - "ledmatrix_min": "2.0.0", - "released": "2026-02-20" - }, - { - "version": "1.3.0", - "ledmatrix_min": "2.0.0", - "released": "2026-02-17" - }, - { - "version": "1.1.1", - "ledmatrix_min": "2.0.0", - "released": "2026-02-15" - }, - { - "version": "1.1.0", - "ledmatrix_min": "2.0.0", - "released": "2026-02-14" - }, - { - "version": "1.0.6", - "ledmatrix_min": "2.0.0", - "released": "2026-02-13" - } - ], - "stars": 0, - "downloads": 0, - "last_updated": "2026-03-02", - "verified": true, - "screenshot": "", - "display_modes": [ - "nba_recent", - "nba_upcoming", - "nba_live", - "wnba_recent", - "wnba_upcoming", - "wnba_live", - "ncaam_recent", - "ncaam_upcoming", - "ncaam_live", - "ncaaw_recent", - "ncaaw_upcoming", - "ncaaw_live" - ], - "dependencies": {}, - "entry_point": "manager.py", - "class_name": "BasketballScoreboardPlugin" + "id": "basketball-scoreboard", + "name": "Basketball Scoreboard", + "version": "1.5.5", + "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", + "tags": [ + "basketball", + "nba", + "ncaa", + "wnba", + "sports", + "scoreboard", + "live-scores" + ], + "repo": "https://github.com/ChuckBuilds/ledmatrix-plugins", + "branch": "main", + "plugin_path": "plugins/basketball-scoreboard", + "versions": [ + { + "released": "2026-03-02", + "version": "1.5.4", + "ledmatrix_min": "2.0.0" + }, + { + "released": "2026-02-24", + "version": "1.5.3", + "ledmatrix_min": "2.0.0" + }, + { + "version": "1.5.2", + "ledmatrix_min": "2.0.0", + "released": "2026-02-24" + }, + { + "version": "1.5.1", + "ledmatrix_min": "2.0.0", + "released": "2026-02-24" + }, + { + "version": "1.5.0", + "ledmatrix_min": "2.0.0", + "released": "2026-02-24" + }, + { + "version": "1.4.0", + "ledmatrix_min": "2.0.0", + "released": "2026-02-20" + }, + { + "version": "1.3.0", + "ledmatrix_min": "2.0.0", + "released": "2026-02-17" + }, + { + "version": "1.1.1", + "ledmatrix_min": "2.0.0", + "released": "2026-02-15" + }, + { + "version": "1.1.0", + "ledmatrix_min": "2.0.0", + "released": "2026-02-14" + }, + { + "version": "1.0.6", + "ledmatrix_min": "2.0.0", + "released": "2026-02-13" + } + ], + "stars": 0, + "downloads": 0, + "last_updated": "2026-03-02", + "verified": true, + "screenshot": "", + "display_modes": [ + "nba_recent", + "nba_upcoming", + "nba_live", + "wnba_recent", + "wnba_upcoming", + "wnba_live", + "ncaam_recent", + "ncaam_upcoming", + "ncaam_live", + "ncaaw_recent", + "ncaaw_upcoming", + "ncaaw_live" + ], + "dependencies": {}, + "entry_point": "manager.py", + "class_name": "BasketballScoreboardPlugin" } diff --git a/plugins/basketball-scoreboard/sports.py b/plugins/basketball-scoreboard/sports.py index 7e618c2..bf25f6b 100644 --- a/plugins/basketball-scoreboard/sports.py +++ b/plugins/basketball-scoreboard/sports.py @@ -424,6 +424,10 @@ def _draw_dynamic_odds( "No clear favorite - spreads: home={home_spread}, away={away_spread}" ) + # Get user-configurable layout offsets for odds + odds_x_offset = self._get_layout_offset('odds', 'x_offset') + odds_y_offset = self._get_layout_offset('odds', 'y_offset') + # Show the negative spread on the appropriate side if favored_spread is not None: spread_text = str(favored_spread) @@ -432,8 +436,8 @@ def _draw_dynamic_odds( if favored_side == "home": # Home team is favored, show spread on right side spread_width = draw.textlength(spread_text, font=font) - spread_x = width - spread_width # Top right - spread_y = 0 + spread_x = width - spread_width + odds_x_offset + spread_y = 0 + odds_y_offset self._draw_text_with_outline( draw, spread_text, (spread_x, spread_y), font, fill=(0, 255, 0) ) @@ -442,8 +446,8 @@ def _draw_dynamic_odds( ) else: # Away team is favored, show spread on left side - spread_x = 0 # Top left - spread_y = 0 + spread_x = 0 + odds_x_offset + spread_y = 0 + odds_y_offset self._draw_text_with_outline( draw, spread_text, (spread_x, spread_y), font, fill=(0, 255, 0) ) @@ -460,22 +464,22 @@ def _draw_dynamic_odds( if favored_side == "home": # Home favored, show O/U on left side (opposite of spread) - ou_x = 0 # Top left - ou_y = 0 + ou_x = 0 + odds_x_offset + ou_y = 0 + odds_y_offset self.logger.debug( f"Showing O/U '{ou_text}' on left side (home favored)" ) elif favored_side == "away": # Away favored, show O/U on right side (opposite of spread) - ou_x = width - ou_width # Top right - ou_y = 0 + ou_x = width - ou_width + odds_x_offset + ou_y = 0 + odds_y_offset self.logger.debug( f"Showing O/U '{ou_text}' on right side (away favored)" ) else: # No clear favorite, show O/U in center - ou_x = (width - ou_width) // 2 - ou_y = 0 + ou_x = (width - ou_width) // 2 + odds_x_offset + ou_y = 0 + odds_y_offset self.logger.debug( f"Showing O/U '{ou_text}' in center (no clear favorite)" ) From 31c0badde329b550f02f4d5977f905aca882db61 Mon Sep 17 00:00:00 2001 From: ChuckBuilds Date: Mon, 30 Mar 2026 12:43:41 -0400 Subject: [PATCH 2/2] fix: address PR review findings for manifest and layout offsets MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Baseball manifest: bump to 1.6.0 (schema additions warrant minor), add versions array entry, update last_updated to 2026-03-30 - Basketball manifest: add missing 1.5.5 entry to versions array so versions[0] matches the version field - Both game_renderer.py _get_layout_offset: align with SportsCore pattern — handle string values via int(float(value)) and log warnings on invalid values instead of silently returning default Co-Authored-By: Claude Opus 4.6 (1M context) --- plugins/baseball-scoreboard/game_renderer.py | 11 ++++++++++- plugins/baseball-scoreboard/manifest.json | 9 +++++++-- plugins/basketball-scoreboard/game_renderer.py | 11 ++++++++++- plugins/basketball-scoreboard/manifest.json | 5 +++++ 4 files changed, 32 insertions(+), 4 deletions(-) diff --git a/plugins/baseball-scoreboard/game_renderer.py b/plugins/baseball-scoreboard/game_renderer.py index 6e4863f..17a19f6 100644 --- a/plugins/baseball-scoreboard/game_renderer.py +++ b/plugins/baseball-scoreboard/game_renderer.py @@ -533,7 +533,16 @@ def _get_layout_offset(self, element: str, axis: str, default: int = 0) -> int: layout_config = self.config.get('customization', {}).get('layout', {}) element_config = layout_config.get(element, {}) offset_value = element_config.get(axis, default) - return int(offset_value) if offset_value is not None else default + if offset_value is None: + return default + if isinstance(offset_value, (int, float)): + return int(offset_value) + # Handle string values (e.g. "2.0" from config) + try: + return int(float(offset_value)) + except (ValueError, TypeError): + self.logger.warning(f"Invalid layout offset for {element}.{axis}: '{offset_value}', using default {default}") + return default except (TypeError, ValueError): return default diff --git a/plugins/baseball-scoreboard/manifest.json b/plugins/baseball-scoreboard/manifest.json index 2fb7ffb..c20f8f2 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.7", + "version": "1.6.0", "author": "ChuckBuilds", "description": "Live, recent, and upcoming baseball games across MLB, MiLB, and NCAA Baseball with real-time scores and schedules", "category": "sports", @@ -30,6 +30,11 @@ "branch": "main", "plugin_path": "plugins/baseball-scoreboard", "versions": [ + { + "released": "2026-03-30", + "version": "1.6.0", + "ledmatrix_min": "2.0.0" + }, { "released": "2026-03-29", "version": "1.5.6", @@ -111,7 +116,7 @@ "ledmatrix_min": "2.0.0" } ], - "last_updated": "2026-03-29", + "last_updated": "2026-03-30", "stars": 0, "downloads": 0, "verified": true, diff --git a/plugins/basketball-scoreboard/game_renderer.py b/plugins/basketball-scoreboard/game_renderer.py index 88c8112..fab5871 100644 --- a/plugins/basketball-scoreboard/game_renderer.py +++ b/plugins/basketball-scoreboard/game_renderer.py @@ -465,7 +465,16 @@ def _get_layout_offset(self, element: str, axis: str, default: int = 0) -> int: layout_config = self.config.get('customization', {}).get('layout', {}) element_config = layout_config.get(element, {}) offset_value = element_config.get(axis, default) - return int(offset_value) if offset_value is not None else default + if offset_value is None: + return default + if isinstance(offset_value, (int, float)): + return int(offset_value) + # Handle string values (e.g. "2.0" from config) + try: + return int(float(offset_value)) + except (ValueError, TypeError): + self.logger.warning(f"Invalid layout offset for {element}.{axis}: '{offset_value}', using default {default}") + return default except (TypeError, ValueError): return default diff --git a/plugins/basketball-scoreboard/manifest.json b/plugins/basketball-scoreboard/manifest.json index 26800b9..60b5cb9 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": [ + { + "released": "2026-03-30", + "version": "1.5.5", + "ledmatrix_min": "2.0.0" + }, { "released": "2026-03-02", "version": "1.5.4",