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
12 changes: 6 additions & 6 deletions plugins.json
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@
"last_updated": "2026-08-05",
"verified": true,
"screenshot": "",
"latest_version": "1.24.0"
"latest_version": "1.24.3"
},
{
"id": "basketball-scoreboard",
Expand Down Expand Up @@ -240,7 +240,7 @@
"last_updated": "2026-08-05",
"verified": true,
"screenshot": "",
"latest_version": "2.14.0"
"latest_version": "2.14.1"
},
{
"id": "geochron",
Expand Down Expand Up @@ -760,7 +760,7 @@
"last_updated": "2026-08-05",
"verified": true,
"screenshot": "",
"latest_version": "2.8.0"
"latest_version": "2.9.1"
},
{
"id": "static-image",
Expand Down Expand Up @@ -905,7 +905,7 @@
"last_updated": "2026-08-05",
"verified": true,
"screenshot": "",
"latest_version": "1.3.3",
"latest_version": "1.3.4",
"icon": "fas fa-fist-raised"
},
{
Expand Down Expand Up @@ -1048,7 +1048,7 @@
"downloads": 0,
"verified": true,
"screenshot": "",
"latest_version": "1.5.0",
"latest_version": "1.6.1",
"last_updated": "2026-08-05"
},
{
Expand Down Expand Up @@ -1095,7 +1095,7 @@
"last_updated": "2026-08-05",
"verified": true,
"screenshot": "",
"latest_version": "1.5.0"
"latest_version": "1.6.1"
},
{
"id": "jellyfin-now-playing",
Expand Down
22 changes: 21 additions & 1 deletion plugins/afl-scoreboard/config_schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -1131,6 +1131,25 @@
}
},
"additionalProperties": false
},
"odds": {
"type": "object",
"title": "Betting Odds",
"properties": {
"x_offset": {
"x-advanced": true,
"type": "integer",
"default": 0,
"description": "Horizontal offset from default position (default: 0)"
},
"y_offset": {
"x-advanced": true,
"type": "integer",
"default": 0,
"description": "Vertical offset from default position (default: 0)"
}
},
"additionalProperties": false
}
},
"x-propertyOrder": [
Expand All @@ -1140,7 +1159,8 @@
"status_text",
"date",
"time",
"records"
"records",
"odds"
],
"additionalProperties": false
},
Expand Down
24 changes: 15 additions & 9 deletions plugins/afl-scoreboard/game_renderer.py
Original file line number Diff line number Diff line change
Expand Up @@ -903,18 +903,24 @@ def _draw_dynamic_odds(self, draw: ImageDraw.Draw, odds: Dict[str, Any]) -> None
favored_spread = away_spread
favored_side = "away"

# Read once, before either branch. These used to be read inside
# the spread branch, but the over/under below uses them too -- so
# a game with a total and no spread raised UnboundLocalError, and
# the except swallowed it, drawing no odds at all.
odds_x_offset = self._layout_offset('odds', 'x_offset')
odds_y_offset = self._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
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))

Expand All @@ -926,12 +932,12 @@ def _draw_dynamic_odds(self, draw: ImageDraw.Draw, odds: Dict[str, Any]) -> None
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))

Expand Down
14 changes: 13 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.5.0",
"version": "1.6.1",
"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,18 @@
"afl_upcoming"
],
"versions": [
{
"released": "2026-08-12",
"version": "1.6.1",
"ledmatrix_min_version": "2.0.0",
"notes": "Draw the odds when a game is priced with a total and no spread. The layout offsets were read inside the spread branch while the over/under below uses them too, so such a game raised UnboundLocalError and the surrounding except swallowed it -- the card drew no odds at all."
},
{
"version": "1.6.0",
"released": "2026-08-11",
"notes": "Add layout offsets for the betting odds. The spread and over/under were pinned to the top corners with no way to nudge them, while baseball, basketball, football and ufc all expose customization.layout.odds. Same element and same card, so it now takes the same knob.",
"ledmatrix_min_version": "2.0.0"
},
{
"version": "1.5.0",
"released": "2026-08-06",
Expand Down
103 changes: 103 additions & 0 deletions plugins/afl-scoreboard/test_odds_without_spread.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
#!/usr/bin/env python3
"""Tests that odds still draw when a game has a total but no spread.

The layout offsets used to be read inside `if favored_spread is not None`,
while the over/under block below uses them unconditionally. A game priced
with a total and no spread -- ordinary enough -- therefore raised
UnboundLocalError, and the bare `except` around the whole method swallowed
it, so the card drew no odds at all rather than drawing the total.

Run: <core-venv>/bin/python plugins/afl-scoreboard/test_odds_without_spread.py
"""

import os
import sys
from pathlib import Path

PLUGIN_DIR = Path(__file__).resolve().parent
sys.path.insert(0, str(PLUGIN_DIR))

# This renderer imports src.logo_downloader at module scope, so the core tree
# has to be importable. LEDMATRIX_CORE points at a checkout.
_core = os.environ.get('LEDMATRIX_CORE', '')
for _candidate in (_core, str(PLUGIN_DIR.parents[2] / 'LEDMatrix')):
if _candidate and (Path(_candidate) / 'src').is_dir():
sys.path.insert(0, _candidate)
break
else:
print("SKIP: no LEDMatrix core checkout found (set LEDMATRIX_CORE)")
sys.exit(2)

try:
from PIL import Image, ImageDraw, ImageFont
except ImportError:
print("SKIP: Pillow not installed")
sys.exit(2)

import game_renderer as gr # noqa: E402 # pylint: disable=wrong-import-position

failures = []


def check(name, cond, detail=""):
if cond:
print(" PASS %s" % name)
else:
print(" FAIL %s%s" % (name, (": " + detail) if detail else ""))
failures.append(name)


def _renderer(drawn):
r = gr.GameRenderer.__new__(gr.GameRenderer)
r.display_width, r.display_height = 128, 64
r.config = {}
r.logger = type("L", (), {m: (lambda *a, **k: None)
for m in ("exception", "error", "warning", "debug", "info")})()
font = ImageFont.load_default()
r.fonts = {"detail": font, "time": font, "score": font, "team": font}
r._layout_offset = lambda e, a, default=0: 0
r._draw_text_with_outline = (
lambda draw, t, xy, font, fill=None: drawn.append(t))
return r


def main():
draw = ImageDraw.Draw(Image.new("RGB", (128, 64)))

print("a total with no spread still draws")
drawn = []
_renderer(drawn)._draw_dynamic_odds(draw, {"over_under": 47.5})
check("the over/under reached the panel", any("47.5" in t for t in drawn),
"drew %r" % drawn)

print("\nand the ordinary case is unchanged")
drawn = []
_renderer(drawn)._draw_dynamic_odds(draw, {
"spread": -3.5, "over_under": 47.5,
"home_team_odds": {"spread_odds": -3.5},
"away_team_odds": {"spread_odds": 3.5},
})
check("both spread and total drew", len(drawn) >= 2, "drew %r" % drawn)

print("\na spread with no total still draws")
drawn = []
_renderer(drawn)._draw_dynamic_odds(draw, {
"spread": -3.5,
"home_team_odds": {"spread_odds": -3.5},
"away_team_odds": {"spread_odds": 3.5},
})
check("the spread reached the panel", any("3.5" in t for t in drawn),
"drew %r" % drawn)

print("\nempty odds draw nothing")
drawn = []
_renderer(drawn)._draw_dynamic_odds(draw, {})
check("nothing drawn", not drawn, "drew %r" % drawn)

print("\n%s" % ("FAILED: %d" % len(failures) if failures
else "All checks passed"))
return 1 if failures else 0


if __name__ == "__main__":
sys.exit(main())
Loading
Loading