diff --git a/plugins.json b/plugins.json index 926231b4..b3f71ec5 100644 --- a/plugins.json +++ b/plugins.json @@ -630,10 +630,10 @@ "plugin_path": "plugins/odds-ticker", "stars": 0, "downloads": 0, - "last_updated": "2026-07-31", + "last_updated": "2026-08-05", "verified": true, "screenshot": "", - "latest_version": "1.1.9" + "latest_version": "1.1.10" }, { "id": "of-the-day", diff --git a/plugins/odds-ticker/manifest.json b/plugins/odds-ticker/manifest.json index 28077992..f2a41056 100644 --- a/plugins/odds-ticker/manifest.json +++ b/plugins/odds-ticker/manifest.json @@ -1,7 +1,7 @@ { "id": "odds-ticker", "name": "Odds Ticker", - "version": "1.1.9", + "version": "1.1.10", "description": "Displays scrolling odds and betting lines for upcoming games across multiple sports leagues including NFL, NBA, MLB, NCAA Football, and more", "author": "ChuckBuilds", "category": "sports", @@ -20,6 +20,12 @@ "branch": "main", "plugin_path": "plugins/odds-ticker", "versions": [ + { + "version": "1.1.10", + "released": "2026-08-05", + "notes": "Fix missing team logos for NCAA men's basketball and NCAA baseball. The renderer discarded the logo directory the data fetcher had already resolved and used a hardcoded league map instead, which had no entries for ncaam_basketball or ncaa_baseball -- those leagues resolved to no directory and rendered every game with blank gaps where the logos belong, because the layout reserves the logo width whether or not a logo loads. The renderer now trusts the directory it is passed and falls back to the map, so a league added to the fetcher no longer needs a second table updated. A missing logo also warns once per team naming the paths searched, instead of logging at debug where nothing could see it.", + "ledmatrix_min_version": "2.0.0" + }, { "version": "1.1.9", "released": "2026-07-31", @@ -34,7 +40,7 @@ { "released": "2026-07-18", "version": "1.1.7", - "notes": "Fix config save failing with a 400 validation error. The per-league favorite-team pickers hardcoded several wrong team abbreviations that didn't match ESPN's (Rams LA→LAR, White Sox CWS→CHW, A's OAK→ATH, Warriors GSW→GS, Pelicans NOP→NO, Knicks NYK→NY, Spurs SAS→SA, Jazz UTA→UTAH, Wizards WAS→WSH, NHL Kings LAK→LA, Capitals WAS→WSH), so valid favorites were rejected on save and never matched games. Also removed the overly strict regex on the NCAA/MiLB free-text team fields so documented values like AP_TOP_25 are accepted.", + "notes": "Fix config save failing with a 400 validation error. The per-league favorite-team pickers hardcoded several wrong team abbreviations that didn't match ESPN's (Rams LA\u2192LAR, White Sox CWS\u2192CHW, A's OAK\u2192ATH, Warriors GSW\u2192GS, Pelicans NOP\u2192NO, Knicks NYK\u2192NY, Spurs SAS\u2192SA, Jazz UTA\u2192UTAH, Wizards WAS\u2192WSH, NHL Kings LAK\u2192LA, Capitals WAS\u2192WSH), so valid favorites were rejected on save and never matched games. Also removed the overly strict regex on the NCAA/MiLB free-text team fields so documented values like AP_TOP_25 are accepted.", "ledmatrix_min": "2.0.0" }, { @@ -87,7 +93,7 @@ ], "stars": 0, "downloads": 0, - "last_updated": "2026-07-31", + "last_updated": "2026-08-05", "verified": true, "screenshot": "", "display_modes": [ diff --git a/plugins/odds-ticker/odds_renderer.py b/plugins/odds-ticker/odds_renderer.py index faba1511..f54fb2f2 100644 --- a/plugins/odds-ticker/odds_renderer.py +++ b/plugins/odds-ticker/odds_renderer.py @@ -81,6 +81,9 @@ def __init__(self, display_manager, config: Dict[str, Any]): # Resolve project root path (plugin_dir -> plugins -> project_root) self.project_root = Path(__file__).resolve().parent.parent.parent + # Teams already reported as having no logo, so the warning fires once + # per team rather than on every ticker rebuild. + self._missing_logo_warned: set = set() # Display settings self.scroll_speed = config.get('scroll_speed', 2) @@ -446,38 +449,71 @@ def _create_game_display(self, game: Dict) -> Image.Image: return image - def _get_team_logo(self, league: str, team_id: str, team_abbr: str, logo_dir: str) -> Optional[Image.Image]: - """Get team logo from assets directory.""" + # Fallback only. The data fetcher already resolves a logo_dir per league + # and passes it in; this map exists for callers that don't. It had drifted + # out of sync with the fetcher — no entries for ncaam_basketball or + # ncaa_baseball — so those leagues resolved to '' and every game rendered + # with blank gaps where the logos belong, because the layout reserves the + # width whether or not a logo loads. + LEAGUE_LOGO_DIRS = { + 'nfl': 'nfl_logos', + 'mlb': 'mlb_logos', + 'nba': 'nba_logos', + 'nhl': 'nhl_logos', + 'milb': 'milb_logos', + 'ncaa_fb': 'ncaa_logos', + 'ncaam_basketball': 'ncaa_logos', + 'ncaa_baseball': 'ncaa_logos', + } + + def _get_team_logo(self, league: str, team_id: str, team_abbr: str, + logo_dir: str) -> Optional[Image.Image]: + """Get a team logo, preferring the directory the caller resolved. + + Trusting logo_dir first is what stops this drifting again: a league + added to the fetcher works here without touching a second table. + """ + _ = team_id # kept for signature compatibility try: - # Suppress unused parameter warnings - _ = team_id - _ = logo_dir - - # Map league names to logo directories - league_logo_map = { - 'nfl': 'nfl_logos', - 'mlb': 'mlb_logos', - 'nba': 'nba_logos', - 'nhl': 'nhl_logos', - 'ncaa_fb': 'ncaa_logos', - 'milb': 'milb_logos' - } - - logo_dir_name = league_logo_map.get(league, '') - if not logo_dir_name or not team_abbr: - return None - - # Resolve path relative to project root - logo_path = self.project_root / "assets" / "sports" / logo_dir_name / f"{team_abbr}.png" - if logo_path.exists(): - return Image.open(logo_path) - else: - logger.debug("Team logo not found: %s", logo_path) + if not team_abbr: return None - + + candidates = [] + if logo_dir: + candidates.append(self.project_root / logo_dir) + mapped = self.LEAGUE_LOGO_DIRS.get(league) + if mapped: + candidates.append(self.project_root / "assets" / "sports" / mapped) + + for base in candidates: + logo_path = base / f"{team_abbr}.png" + if logo_path.exists(): + return Image.open(logo_path) + + self._warn_missing_logo(league, team_abbr, candidates) + return None + except Exception as e: - logger.debug("Error loading team logo for %s in %s: %s", team_abbr, league, e) + logger.warning("Error loading team logo for %s in %s: %s", + team_abbr, league, e) return None + + def _warn_missing_logo(self, league: str, team_abbr: str, candidates) -> None: + """Report a missing logo once per team, at a level that is actually seen. + + This used to log at debug. The failure is silent on screen too — the + layout still reserves the logo's width — so a missing logo showed up as + an unexplained gap with nothing in the logs to explain it. + """ + key = (league, team_abbr) + if key in self._missing_logo_warned: + return + self._missing_logo_warned.add(key) + looked_in = ', '.join(str(c) for c in candidates) or '(no directory resolved)' + logger.warning( + "Team logo not found for %s in league %r; looked in: %s. " + "The game will render with a blank gap where the logo belongs.", + team_abbr, league, looked_in) def _load_broadcast_logo(self, logo_name: str) -> Optional[Image.Image]: """Load broadcast logo from assets."""