Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions plugins.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
12 changes: 9 additions & 3 deletions plugins/odds-ticker/manifest.json
Original file line number Diff line number Diff line change
@@ -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",
Expand All @@ -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",
Expand All @@ -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"
},
{
Expand Down Expand Up @@ -87,7 +93,7 @@
],
"stars": 0,
"downloads": 0,
"last_updated": "2026-07-31",
"last_updated": "2026-08-05",
"verified": true,
"screenshot": "",
"display_modes": [
Expand Down
92 changes: 64 additions & 28 deletions plugins/odds-ticker/odds_renderer.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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."""
Expand Down
Loading