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
18 changes: 9 additions & 9 deletions plugins.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"version": "1.0.0",
"last_updated": "2026-09-02",
"last_updated": "2026-09-03",
"plugins": [
{
"id": "cricket-scoreboard",
Expand Down Expand Up @@ -76,7 +76,7 @@
"last_updated": "2026-09-02",
"verified": true,
"screenshot": "",
"latest_version": "1.37.2"
"latest_version": "1.38.0"
},
{
"id": "basketball-scoreboard",
Expand All @@ -101,7 +101,7 @@
"last_updated": "2026-09-02",
"verified": true,
"screenshot": "",
"latest_version": "1.26.1"
"latest_version": "1.27.0"
},
{
"id": "calendar",
Expand Down Expand Up @@ -240,7 +240,7 @@
"last_updated": "2026-09-02",
"verified": true,
"screenshot": "",
"latest_version": "3.1.1"
"latest_version": "3.2.0"
},
{
"id": "geochron",
Expand Down Expand Up @@ -335,7 +335,7 @@
"last_updated": "2026-09-02",
"verified": true,
"screenshot": "",
"latest_version": "1.22.1",
"latest_version": "1.23.0",
"icon": "fas fa-hockey-puck"
},
{
Expand All @@ -359,7 +359,7 @@
"last_updated": "2026-09-02",
"verified": true,
"screenshot": "",
"latest_version": "1.21.1",
"latest_version": "1.22.0",
"icon": "fas fa-baseball-ball"
},
{
Expand Down Expand Up @@ -760,7 +760,7 @@
"last_updated": "2026-09-02",
"verified": true,
"screenshot": "",
"latest_version": "2.21.1"
"latest_version": "2.22.0"
},
{
"id": "static-image",
Expand Down Expand Up @@ -1048,7 +1048,7 @@
"downloads": 0,
"verified": true,
"screenshot": "",
"latest_version": "1.19.3",
"latest_version": "1.20.0",
"last_updated": "2026-09-02"
},
{
Expand Down Expand Up @@ -1095,7 +1095,7 @@
"last_updated": "2026-09-02",
"verified": true,
"screenshot": "",
"latest_version": "1.18.1"
"latest_version": "1.19.0"
},
{
"id": "jellyfin-now-playing",
Expand Down
39 changes: 39 additions & 0 deletions plugins/afl-scoreboard/config_schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -1042,6 +1042,45 @@
],
"additionalProperties": false
},
"odds_text": {
"type": "object",
"title": "Odds Text",
"description": "Customize the betting line and over/under text.",
"properties": {
"font": {
"x-advanced": true,
"type": "string",
"default": "4x6-font.ttf",
"description": "Font family for odds text. Defaults to the same 4x6 font the detail text uses; press_start is chunkier and easier to read on a large panel, but roughly twice as wide."
},
"font_size": {
"x-advanced": true,
"type": "integer",
"default": 6,
"description": "Font size for odds text. Sizes snap to the font's pixel grid to stay crisp: 4x6-font.ttf snaps to 7, 14, 21; press_start to 8, 16."
},
"text_color": {
"type": "array",
"title": "Text Color",
"description": "Colour [R, G, B] for the odds text.",
"x-widget": "color-picker",
"items": {
"type": "integer",
"minimum": 0,
"maximum": 255
},
"minItems": 3,
"maxItems": 3,
"default": [
0,
255,
0
],
"x-advanced": true
}
},
"additionalProperties": false
},
"rank_text": {
"type": "object",
"title": "Rankings",
Expand Down
28 changes: 25 additions & 3 deletions plugins/afl-scoreboard/game_renderer.py
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,9 @@ def _load_fonts(self) -> Dict[str, ImageFont.FreeTypeFont]:
team_config = customization.get('team_name', {})
status_config = customization.get('status_text', {})
detail_config = customization.get('detail_text', {})
# Falls back to detail_text so a config written before this
# setting existed keeps rendering odds exactly as it did.
odds_config = customization.get('odds_text') or detail_config
rank_config = customization.get('rank_text', {})

try:
Expand All @@ -141,6 +144,7 @@ def _load_fonts(self) -> Dict[str, ImageFont.FreeTypeFont]:
fonts["team"] = self._load_custom_font(team_config, default_size=8, element_key='team_name')
fonts["status"] = self._load_custom_font(status_config, default_size=6, element_key='status_text')
fonts["detail"] = self._load_custom_font(detail_config, default_size=6, default_font='4x6-font.ttf', element_key='detail_text')
fonts["odds"] = self._load_custom_font(odds_config, default_size=6, default_font='4x6-font.ttf', element_key='odds_text')
fonts["rank"] = self._load_custom_font(rank_config, default_size=10, element_key='rank_text')
# Not user-customizable (no config_schema.json entry) -- fixed
# size, loaded once here instead of per-render in
Expand Down Expand Up @@ -425,6 +429,7 @@ def _calculate_max_logo_dimensions(
#: resolving the colour from the face keeps the two in step by
#: construction, rather than by every draw site remembering to agree.
_ELEMENT_FOR_FONT: ClassVar[Dict[str, str]] = {
"odds": "odds_text",
"score": "score_text",
"time": "period_text",
"team": "team_name",
Expand Down Expand Up @@ -1128,6 +1133,23 @@ def _draw_upcoming_game_status(self, draw: ImageDraw.Draw, game: Dict) -> None:
fill=self._element_color(bottom_color)
)

def _odds_color(self) -> Tuple[int, int, int]:
"""Colour for the odds text; the green it always drew unless configured.

Guarded with getattr because not every class that reaches
_draw_dynamic_odds carries the element-colour helper -- the plugins'
own test harnesses build minimal manager objects, and a bare
AttributeError here is swallowed by the surrounding except, which
drops the odds off the card instead of failing loudly.
"""
getter = getattr(self, "_element_color", None)
if getter is None:
return (0, 255, 0)
try:
return getter("odds_text", (0, 255, 0))
except Exception:
return (0, 255, 0)

def _draw_dynamic_odds(self, draw: ImageDraw.Draw, odds: Dict[str, Any]) -> None:
"""Draw odds with dynamic positioning."""
try:
Expand Down Expand Up @@ -1178,7 +1200,7 @@ def _draw_dynamic_odds(self, draw: ImageDraw.Draw, odds: Dict[str, Any]) -> None
# widest time string the centre can hold, measured in the font the
# renderer will actually use, so this tracks font changes instead
# of hard-coding a width.
font = self.fonts["detail"]
font = self.fonts.get("odds") or self.fonts["detail"]
time_font = self.fonts.get("time", font)
centre_reserve = draw.textlength("12:00 PM", font=time_font)
side_budget = max(0.0, (self.display_width - centre_reserve) / 2)
Expand All @@ -1197,7 +1219,7 @@ def _draw_dynamic_odds(self, draw: ImageDraw.Draw, odds: Dict[str, Any]) -> None
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))
self._draw_text_with_outline(draw, spread_text, (spread_x, spread_y), font, fill=self._odds_color())

# Show over/under on opposite side
over_under = odds.get("over_under")
Expand Down Expand Up @@ -1225,7 +1247,7 @@ def _draw_dynamic_odds(self, draw: ImageDraw.Draw, odds: Dict[str, Any]) -> None
ou_x = 0 + 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))
self._draw_text_with_outline(draw, ou_text, (ou_x, ou_y), font, fill=self._odds_color())

except Exception as e:
self.logger.error(f"Error drawing odds: {e}")
Expand Down
9 changes: 8 additions & 1 deletion plugins/afl-scoreboard/manifest.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"id": "afl-scoreboard",
"name": "AFL Scoreboard",
"version": "1.19.3",
"version": "1.20.0",
"author": "ChuckBuilds",
"description": "Live, recent, and upcoming AFL (Australian Football League) games with real-time scores and game status.",
"category": "sports",
Expand All @@ -18,6 +18,13 @@
"afl_upcoming"
],
"versions": [
{
"version": "1.20.0",
"released": "2026-09-02",
"ledmatrix_min_version": "3.2.0",
"notes": "Odds get their own font, size and colour. The betting line and over/under borrowed the detail_text settings, so making them bigger on a large panel also enlarged the team records and the date row, and their green was hard-coded in the drawing call where no setting could reach it. A new customization.odds_text block controls the odds alone, in both the full-screen scorebug and the scroll/Vegas card. Nothing changes unless you set it: the defaults are exactly what was rendered before -- the same 4x6 font at the same size, and the same green -- and a config saved before this setting existed falls back to detail_text, so all 176 safety-harness renders are byte-identical to the previous version. Sizes still snap to the font's pixel grid to stay crisp, so 4x6-font.ttf gives 7, 14 or 21 and press_start gives 8 or 16.",
"changelog": "Documentation only. Fills gaps left by the previous README rewrite. Documents other_games_min_quality and other_games_divisions, which the rewrite dropped entirely, and records that both are inert in this league: 'ranked' needs a national poll the AFL does not publish, and the divisions filter needs ESPN's FBS/FCS group rosters, which exist for college football and nothing else. Adds that both filters fail open and that favourites are never filtered by either. Corrects the framing of upcoming_games_to_show: it is a pool the panel cycles through one card at a time, so raising it makes any one game come round less often, not more. Restores the debug-logging tip for finding team abbreviations and a manual-installation section."
},
{
"version": "1.19.3",
"released": "2026-09-02",
Expand Down
32 changes: 27 additions & 5 deletions plugins/afl-scoreboard/sports.py
Original file line number Diff line number Diff line change
Expand Up @@ -1353,6 +1353,9 @@ def _load_fonts(self):
team_config = customization.get('team_name', {})
status_config = customization.get('status_text', {})
detail_config = customization.get('detail_text', {})
# Falls back to detail_text so a config written before this
# setting existed keeps rendering odds exactly as it did.
odds_config = customization.get('odds_text') or detail_config
rank_config = customization.get('rank_text', {})

try:
Expand All @@ -1361,6 +1364,7 @@ def _load_fonts(self):
fonts["team"] = self._load_custom_font_from_element_config(team_config, default_size=8, element_key='team_name')
fonts["status"] = self._load_custom_font_from_element_config(status_config, default_size=6, element_key='status_text')
fonts["detail"] = self._load_custom_font_from_element_config(detail_config, default_size=6, element_key='detail_text', default_font='4x6-font.ttf')
fonts["odds"] = self._load_custom_font_from_element_config(odds_config, default_size=6, element_key='odds_text', default_font='4x6-font.ttf')
fonts["rank"] = self._load_custom_font_from_element_config(rank_config, default_size=10, element_key='rank_text')
self.logger.info("Successfully loaded fonts from config")
except Exception as e:
Expand Down Expand Up @@ -1393,6 +1397,23 @@ def _load_fonts(self):
# narrower FACE rather than let a grown score crowd out the logos.
return self._fit_score_font(self._scale_headline_fonts(fonts))

def _odds_color(self) -> Tuple[int, int, int]:
"""Colour for the odds text; the green it always drew unless configured.

Guarded with getattr because not every class that reaches
_draw_dynamic_odds carries the element-colour helper -- the plugins'
own test harnesses build minimal manager objects, and a bare
AttributeError here is swallowed by the surrounding except, which
drops the odds off the card instead of failing loudly.
"""
getter = getattr(self, "_element_color", None)
if getter is None:
return (0, 255, 0)
try:
return getter("odds_text", (0, 255, 0))
except Exception:
return (0, 255, 0)

def _draw_dynamic_odds(
self, draw: ImageDraw.Draw, odds: Dict[str, Any], width: int, height: int
) -> None:
Expand Down Expand Up @@ -1457,15 +1478,15 @@ def _draw_dynamic_odds(
# Show the negative spread on the appropriate side
if favored_spread is not None:
spread_text = str(favored_spread)
font = self.fonts["detail"] # Use detail font for odds
font = self.fonts.get("odds") or self.fonts["detail"]

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
self._draw_text_with_outline(
draw, spread_text, (spread_x, spread_y), font, fill=(0, 255, 0)
draw, spread_text, (spread_x, spread_y), font, fill=self._odds_color()
)
self.logger.debug(
f"Showing home spread '{spread_text}' on right side"
Expand All @@ -1475,7 +1496,7 @@ def _draw_dynamic_odds(
spread_x = 0 # Top left
spread_y = 0
self._draw_text_with_outline(
draw, spread_text, (spread_x, spread_y), font, fill=(0, 255, 0)
draw, spread_text, (spread_x, spread_y), font, fill=self._odds_color()
)
self.logger.debug(
f"Showing away spread '{spread_text}' on left side"
Expand All @@ -1485,7 +1506,7 @@ def _draw_dynamic_odds(
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"] # Use detail font for odds
font = self.fonts.get("odds") or self.fonts["detail"]
ou_width = draw.textlength(ou_text, font=font)

if favored_side == "home":
Expand All @@ -1511,7 +1532,7 @@ def _draw_dynamic_odds(
)

self._draw_text_with_outline(
draw, ou_text, (ou_x, ou_y), font, fill=(0, 255, 0)
draw, ou_text, (ou_x, ou_y), font, fill=self._odds_color()
)

except Exception as e:
Expand All @@ -1522,6 +1543,7 @@ def _draw_dynamic_odds(
#: resolving the colour from the face keeps the two in step by
#: construction, rather than by every draw site remembering to agree.
_ELEMENT_FOR_FONT: ClassVar[Dict[str, str]] = {
"odds": "odds_text",
"score": "score_text",
"time": "period_text",
"team": "team_name",
Expand Down
11 changes: 10 additions & 1 deletion plugins/afl-scoreboard/test_element_text_colors.py
Original file line number Diff line number Diff line change
Expand Up @@ -126,9 +126,18 @@ def draw_with(config, font_key, text="12:00", fill=None):
if "text_color" in v.get("properties", {})}
defaults = draw_with({"customization": materialised}, "time")
check("schema defaults render identically to no config", defaults == plain)
# odds_text is the one deliberate exception. The betting line and
# over/under have always drawn green -- it was hard-coded in the draw call
# before the setting existed -- so advertising white would both lie about
# the default and change what is rendered the moment the store
# materialises it. Green here IS "unset", which is what the check above
# actually cares about.
check("every advertised text_color default is white",
all(tuple(v["text_color"]) == (255, 255, 255)
for v in materialised.values()))
for k, v in materialised.items() if k != "odds_text"))
check("odds_text advertises the green it has always drawn",
tuple(materialised.get("odds_text", {}).get("text_color", (0, 255, 0)))
== (0, 255, 0))

# -- 2. a configured colour reaches that element's face -------------------
cfg = {"customization": {"period_text": {"text_color": list(CYAN)},
Expand Down
39 changes: 39 additions & 0 deletions plugins/baseball-scoreboard/config_schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -1936,6 +1936,45 @@
},
"additionalProperties": false
},
"odds_text": {
"type": "object",
"title": "Odds Text",
"description": "Customize the betting line and over/under text.",
"properties": {
"font": {
"x-advanced": true,
"type": "string",
"default": "4x6-font.ttf",
"description": "Font family for odds text. Defaults to the same 4x6 font the detail text uses; press_start is chunkier and easier to read on a large panel, but roughly twice as wide."
},
"font_size": {
"x-advanced": true,
"type": "integer",
"default": 6,
"description": "Font size for odds text. Sizes snap to the font's pixel grid to stay crisp: 4x6-font.ttf snaps to 7, 14, 21; press_start to 8, 16."
},
"text_color": {
"type": "array",
"title": "Text Color",
"description": "Colour [R, G, B] for the odds text.",
"x-widget": "color-picker",
"items": {
"type": "integer",
"minimum": 0,
"maximum": 255
},
"minItems": 3,
"maxItems": 3,
"default": [
0,
255,
0
],
"x-advanced": true
}
},
"additionalProperties": false
},
"layout": {
"type": "object",
"title": "Layout Positioning",
Expand Down
Loading
Loading