Skip to content
Closed
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
6 changes: 3 additions & 3 deletions plugins.json
Original file line number Diff line number Diff line change
Expand Up @@ -299,7 +299,7 @@
"last_updated": "2026-03-29",
"verified": true,
"screenshot": "",
"latest_version": "1.5.5"
"latest_version": "1.5.6"
},
{
"id": "soccer-scoreboard",
Expand Down Expand Up @@ -352,10 +352,10 @@
"plugin_path": "plugins/odds-ticker",
"stars": 0,
"downloads": 0,
"last_updated": "2026-02-23",
"last_updated": "2026-03-29",
"verified": true,
"screenshot": "",
"latest_version": "1.1.1"
"latest_version": "1.1.3"
},
{
"id": "leaderboard",
Expand Down
6 changes: 3 additions & 3 deletions plugins/baseball-scoreboard/baseball.py
Original file line number Diff line number Diff line change
Expand Up @@ -136,9 +136,9 @@ def _extract_game_details(self, game_event: Dict) -> Optional[Dict]:
try:
game_status = status["type"]["name"].lower()
status_state = status["type"]["state"].lower()
# Get team abbreviations
home_abbr = home_team["team"]["abbreviation"]
away_abbr = away_team["team"]["abbreviation"]
# Get team abbreviations (some NCAA teams lack 'abbreviation')
home_abbr = home_team["team"].get("abbreviation") or (home_team["team"].get("name") or "?")[:3]
away_abbr = away_team["team"].get("abbreviation") or (away_team["team"].get("name") or "?")[:3]

# Check if this is a favorite team game
is_favorite_game = (
Expand Down
98 changes: 79 additions & 19 deletions plugins/baseball-scoreboard/game_renderer.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@
import logging
from datetime import datetime
from pathlib import Path
from typing import Dict, Optional
import os
from typing import Any, Dict, Optional

import pytz
from PIL import Image, ImageDraw, ImageFont
Expand Down Expand Up @@ -56,27 +57,86 @@ def __init__(
# Load fonts
self.fonts = self._load_fonts()

def _load_fonts(self):
"""Load fonts used by the renderer."""
def _load_fonts(self) -> Dict[str, ImageFont.FreeTypeFont]:
"""Load fonts used by the scoreboard from config or use defaults."""
fonts = {}

# Get customization config
customization = self.config.get('customization', {})

# Load fonts from config with defaults for backward compatibility
score_config = customization.get('score_text', {})
period_config = customization.get('period_text', {})
team_config = customization.get('team_name', {})
status_config = customization.get('status_text', {})
detail_config = customization.get('detail_text', {})
rank_config = customization.get('rank_text', {})

try:
fonts['score'] = ImageFont.truetype("assets/fonts/PressStart2P-Regular.ttf", 10)
fonts['time'] = ImageFont.truetype("assets/fonts/PressStart2P-Regular.ttf", 8)
fonts['team'] = ImageFont.truetype("assets/fonts/PressStart2P-Regular.ttf", 8)
fonts['status'] = ImageFont.truetype("assets/fonts/4x6-font.ttf", 6)
fonts['detail'] = ImageFont.truetype("assets/fonts/4x6-font.ttf", 6)
fonts['rank'] = ImageFont.truetype("assets/fonts/PressStart2P-Regular.ttf", 10)
self.logger.debug("Successfully loaded fonts")
except IOError:
self.logger.warning("Fonts not found, using default PIL font.")
fonts['score'] = ImageFont.load_default()
fonts['time'] = ImageFont.load_default()
fonts['team'] = ImageFont.load_default()
fonts['status'] = ImageFont.load_default()
fonts['detail'] = ImageFont.load_default()
fonts['rank'] = ImageFont.load_default()
fonts["score"] = self._load_custom_font(score_config, default_size=10)
fonts["time"] = self._load_custom_font(period_config, default_size=8)
fonts["team"] = self._load_custom_font(team_config, default_size=8)
fonts["status"] = self._load_custom_font(status_config, default_size=6)
fonts["detail"] = self._load_custom_font(detail_config, default_size=6, default_font='4x6-font.ttf')
fonts["rank"] = self._load_custom_font(rank_config, default_size=10)
self.logger.debug("Successfully loaded fonts from config")
except Exception as e:
self.logger.exception("Error loading fonts, using defaults")
# Fallback to hardcoded defaults
try:
fonts["score"] = ImageFont.truetype("assets/fonts/PressStart2P-Regular.ttf", 10)
fonts["time"] = ImageFont.truetype("assets/fonts/PressStart2P-Regular.ttf", 8)
fonts["team"] = ImageFont.truetype("assets/fonts/PressStart2P-Regular.ttf", 8)
fonts["status"] = ImageFont.truetype("assets/fonts/4x6-font.ttf", 6)
fonts["detail"] = ImageFont.truetype("assets/fonts/4x6-font.ttf", 6)
fonts["rank"] = ImageFont.truetype("assets/fonts/PressStart2P-Regular.ttf", 10)
except IOError:
self.logger.warning("Fonts not found, using default PIL font.")
default_font = ImageFont.load_default()
fonts = {k: default_font for k in ["score", "time", "team", "status", "detail", "rank"]}

return fonts

def _load_custom_font(self, element_config: Dict[str, Any], default_size: int = 8, default_font: str = 'PressStart2P-Regular.ttf') -> ImageFont.FreeTypeFont:
"""Load a custom font from an element configuration dictionary."""
font_name = element_config.get('font', default_font)
font_size = int(element_config.get('font_size', default_size))
font_path = os.path.join('assets', 'fonts', font_name)

try:
if os.path.exists(font_path):
if font_path.lower().endswith('.ttf') or font_path.lower().endswith('.otf'):
return ImageFont.truetype(font_path, font_size)
elif font_path.lower().endswith('.bdf'):
# BDF fonts require pre-conversion: pilfont.py font.bdf -> font.pil + font.pbm
pil_font_path = font_path.rsplit('.', 1)[0] + '.pil'
if os.path.exists(pil_font_path):
try:
return ImageFont.load(pil_font_path)
except Exception as e:
self.logger.warning(f"Failed to load pre-converted BDF font {pil_font_path}: {e}")
else:
self.logger.warning(
f"BDF font {font_name} requires conversion. "
f"Run: pilfont.py {font_path}"
)
else:
self.logger.warning(f"Unknown font file type: {font_name}")
else:
self.logger.warning(f"Font file not found: {font_path}")
except Exception as e:
self.logger.warning(f"Error loading font {font_name}: {e}")

# Fallback to default font
default_font_path = os.path.join('assets', 'fonts', 'PressStart2P-Regular.ttf')
try:
if os.path.exists(default_font_path):
return ImageFont.truetype(default_font_path, font_size)
except Exception as e:
self.logger.warning(f"Error loading default font {default_font_path}: {e}")

return ImageFont.load_default()

def _get_logo_path(self, league: str, team_abbrev: str) -> Path:
"""Get the logo path for a team based on league."""
if league == 'mlb':
Expand Down Expand Up @@ -501,7 +561,7 @@ def _draw_dynamic_odds(self, draw, odds: Dict) -> None:
favored_side = 'away'

# Odds row below the status/inning text row
status_bbox = draw.textbbox((0, 0), "A", font=self.fonts['time'])
status_bbox = draw.textbbox((0, 0), "A", font=self.fonts['detail'])
odds_y = status_bbox[3] + 2 # just below the status row

# Show the negative spread on the appropriate side
Expand Down
9 changes: 7 additions & 2 deletions plugins/baseball-scoreboard/manifest.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"id": "baseball-scoreboard",
"name": "Baseball Scoreboard",
"version": "1.5.5",
"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",
Expand Down Expand Up @@ -30,6 +30,11 @@
"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",
Expand Down Expand Up @@ -106,7 +111,7 @@
"ledmatrix_min": "2.0.0"
}
],
"last_updated": "2026-02-24",
"last_updated": "2026-03-29",
"stars": 0,
"downloads": 0,
"verified": true,
Expand Down
4 changes: 2 additions & 2 deletions plugins/odds-ticker/manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -1148,8 +1148,8 @@ def _fetch_league_games(self, league_config: Dict[str, Any], now: datetime, cano
away_team = next(c for c in competitors if c['homeAway'] == 'away')
home_id = home_team['team']['id']
away_id = away_team['team']['id']
home_abbr = home_team['team']['abbreviation']
away_abbr = away_team['team']['abbreviation']
home_abbr = home_team['team'].get('abbreviation') or (home_team['team'].get('name') or '?')[:3]
away_abbr = away_team['team'].get('abbreviation') or (away_team['team'].get('name') or '?')[:3]
home_name = home_team['team'].get('name', home_abbr)
away_name = away_team['team'].get('name', away_abbr)

Expand Down
14 changes: 12 additions & 2 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.1",
"version": "1.1.3",
"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 @@ -10,6 +10,16 @@
"branch": "main",
"plugin_path": "plugins/odds-ticker",
"versions": [
{
"version": "1.1.3",
"ledmatrix_min": "2.0.0",
"released": "2026-03-29"
},
{
"version": "1.1.2",
"ledmatrix_min": "2.0.0",
"released": "2026-03-29"
},
{
"version": "1.1.1",
"ledmatrix_min": "2.0.0",
Expand All @@ -33,7 +43,7 @@
],
"stars": 0,
"downloads": 0,
"last_updated": "2026-02-23",
"last_updated": "2026-03-29",
"verified": true,
"screenshot": "",
"display_modes": [
Expand Down