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
16 changes: 8 additions & 8 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.22.2"
"latest_version": "1.23.0"
},
{
"id": "basketball-scoreboard",
Expand All @@ -101,7 +101,7 @@
"last_updated": "2026-08-05",
"verified": true,
"screenshot": "",
"latest_version": "1.10.3"
"latest_version": "1.11.0"
},
{
"id": "calendar",
Expand Down Expand Up @@ -240,7 +240,7 @@
"last_updated": "2026-08-05",
"verified": true,
"screenshot": "",
"latest_version": "2.11.2"
"latest_version": "2.12.0"
},
{
"id": "geochron",
Expand Down Expand Up @@ -335,7 +335,7 @@
"last_updated": "2026-08-05",
"verified": true,
"screenshot": "",
"latest_version": "1.7.3",
"latest_version": "1.8.0",
"icon": "fas fa-hockey-puck"
},
{
Expand All @@ -359,7 +359,7 @@
"last_updated": "2026-08-05",
"verified": true,
"screenshot": "",
"latest_version": "1.7.3",
"latest_version": "1.8.0",
"icon": "fas fa-baseball-ball"
},
{
Expand Down Expand Up @@ -760,7 +760,7 @@
"last_updated": "2026-08-05",
"verified": true,
"screenshot": "",
"latest_version": "2.6.2"
"latest_version": "2.7.0"
},
{
"id": "static-image",
Expand Down Expand Up @@ -1048,7 +1048,7 @@
"downloads": 0,
"verified": true,
"screenshot": "",
"latest_version": "1.3.2",
"latest_version": "1.4.0",
"last_updated": "2026-08-05"
},
{
Expand Down Expand Up @@ -1095,7 +1095,7 @@
"last_updated": "2026-08-05",
"verified": true,
"screenshot": "",
"latest_version": "1.3.2"
"latest_version": "1.4.0"
},
{
"id": "jellyfin-now-playing",
Expand Down
32 changes: 32 additions & 0 deletions plugins/afl-scoreboard/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ Football League)** games with real-time scores and game status.
- **Switch or Scroll**: Show one game at a time, or scroll all games horizontally
- **Dynamic Duration & Live Priority**: Spend more time on live games; let live games interrupt the rotation
- **Background Data Fetching**: Efficient API calls without blocking the display
- **Favorite Team Result Colors**: Optionally show a finished game's score in green when your favorite team won and red when it lost

## Display Modes

Expand Down Expand Up @@ -149,6 +150,37 @@ Manual install: copy this directory into your LEDMatrix `plugins_directory`
upcoming game) for the core plugin safety harness
(`LEDMatrix/scripts/check_plugin.py`).

## Favorite Team Result Colors

A run of games against the same opponent is hard to read at a glance: in scroll
and Vegas mode the same two logos go past several times and only the digits
change. Turn on **Customization -> Favorite Team Result Colors** to color a
finished game's score by how your favorite team did - green for a win, red for
a loss.

```json
{
"customization": {
"favorite_result_colors": {
"enabled": true,
"win_color": [0, 255, 0],
"loss_color": [255, 0, 0],
"tie_color": [255, 200, 0]
}
}
}
```

- Off by default. Until you enable it the score keeps exactly the color it has
today.
- Only finished games are colored. Live and upcoming cards are untouched.
- A game needs exactly one favorite team. If neither side is a favorite, or both
are, the score keeps its normal color.
- Applies to both the one-game-at-a-time switch view and the scroll/Vegas
ticker.
- The three colors are Advanced settings; leave them alone for the defaults
above.

## Troubleshooting

- **Start times look like UTC** (a 6:45pm Central start showing as 11:45PM):
Expand Down
68 changes: 67 additions & 1 deletion plugins/afl-scoreboard/config_schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -900,6 +900,71 @@
"records"
],
"additionalProperties": false
},
"favorite_result_colors": {
"type": "object",
"title": "Favorite Team Result Colors",
"description": "Color the final score of a recent game by how your favorite team did. Most useful in scroll and Vegas modes, where the same matchup can go past several times and the logos alone do not tell you who won.",
"properties": {
"enabled": {
"type": "boolean",
"title": "Color Scores by Result",
"description": "Show the final score in green when your favorite team won and red when it lost. Games without exactly one favorite team - neither side, or both - keep the normal score color.",
"default": false
},
"win_color": {
"type": "array",
"title": "Win Color",
"description": "Score color [R, G, B] when your favorite team won",
"x-widget": "color-picker",
"items": {
"type": "integer",
"minimum": 0,
"maximum": 255
},
"minItems": 3,
"maxItems": 3,
"default": [0, 255, 0],
"x-advanced": true
},
"loss_color": {
"type": "array",
"title": "Loss Color",
"description": "Score color [R, G, B] when your favorite team lost",
"x-widget": "color-picker",
"items": {
"type": "integer",
"minimum": 0,
"maximum": 255
},
"minItems": 3,
"maxItems": 3,
"default": [255, 0, 0],
"x-advanced": true
},
"tie_color": {
"type": "array",
"title": "Tie Color",
"description": "Score color [R, G, B] when the game ended level",
"x-widget": "color-picker",
"items": {
"type": "integer",
"minimum": 0,
"maximum": 255
},
"minItems": 3,
"maxItems": 3,
"default": [255, 200, 0],
"x-advanced": true
}
},
"x-propertyOrder": [
"enabled",
"win_color",
"loss_color",
"tie_color"
],
"additionalProperties": false
}
},
"x-propertyOrder": [
Expand All @@ -909,7 +974,8 @@
"status_text",
"detail_text",
"rank_text",
"layout"
"layout",
"favorite_result_colors"
],
"additionalProperties": false
}
Expand Down
144 changes: 142 additions & 2 deletions plugins/afl-scoreboard/game_renderer.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
import os
import sys
from pathlib import Path
from typing import Dict, Any, Optional, Tuple
from typing import Any, ClassVar, Dict, Optional, Tuple
from PIL import Image, ImageDraw, ImageFont

# Add project root to path to import the shared logo downloader (same
Expand Down Expand Up @@ -303,6 +303,143 @@ def _draw_text_with_outline(
draw.text((x + dx, y + dy), text, font=font, fill=outline_color)
draw.text((x, y), text, font=font, fill=fill)

# ------------------------------------------------------------------
# Favorite-team result colors for finished games.
#
# This is the scroll/Vegas path, and it is where the setting earns its
# keep: a series against the same opponent scrolls past as several
# near-identical cards, so tinting the final score green or red is the
# only quick way to tell a win from a loss. Off by default -- the score
# keeps the color it has today until the user opts in.
# ------------------------------------------------------------------

FAVORITE_RESULT_COLOR_DEFAULTS: ClassVar[Dict[str, Tuple[int, int, int]]] = {
"win": (0, 255, 0),
"loss": (255, 0, 0),
"tie": (255, 200, 0),
}

@staticmethod
def _coerce_rgb(value, fallback):
"""Turn a configured [R, G, B] list into a clamped (r, g, b) tuple."""
# Checked before unpacking: a 3-character string ("123") would otherwise
# iterate into three digits and yield a colour rather than the fallback.
if not isinstance(value, (list, tuple)) or len(value) != 3:
return fallback
try:
r, g, b = (max(0, min(255, int(channel))) for channel in value)
except (TypeError, ValueError):
return fallback
return (r, g, b)

def _favorite_teams_for(self, game: Dict[str, Any]) -> list:
"""Favorite teams that apply to this game.

Both sources are used. Games carry the league manager's *resolved*
favorites, which is the only place dynamic groups such as AP_TOP_25
appear expanded; the config is read as well so an edit takes effect on
already-fetched games, and so hand-built game dicts (tests, other
callers) still work.
"""
favorites = list(game.get("favorite_teams") or [])
league_config = self.config.get(str(game.get("league", "") or ""))
if isinstance(league_config, dict):
favorites += list(league_config.get("favorite_teams") or [])
else:
favorites += list(self.config.get("favorite_teams") or [])
return favorites

@staticmethod
def _side_is_favorite(game: Dict[str, Any], side: str, favorites: set) -> bool:
"""Is the home/away side of this game a favorite team?

Reads both the flat (``home_abbr``) and nested (``home_team.abbrev``)
payload shapes, and matches on the ESPN id too, because a couple of
leagues (NRL) key favorites by id where abbreviations collide.
"""
candidates = [game.get(f"{side}_abbr"), game.get(f"{side}_id")]
team = game.get(f"{side}_team")
if isinstance(team, dict):
candidates += [team.get("abbrev"), team.get("abbreviation"), team.get("id")]
for value in candidates:
if value is not None and str(value).strip().upper() in favorites:
return True
return False

@staticmethod
def _side_score(game: Dict[str, Any], side: str) -> Optional[int]:
"""Numeric score for one side, from either payload shape."""
raw = None
team = game.get(f"{side}_team")
if isinstance(team, dict) and team.get("score") is not None:
raw = team.get("score")
if raw is None:
raw = game.get(f"{side}_score")
try:
return int(float(str(raw).strip()))
except (TypeError, ValueError):
return None

def _favorite_result(self, game: Dict[str, Any]) -> Optional[str]:
"""Say how the favorite team did in a finished game.

Returns 'win', 'loss' or 'tie', or None when there is no single team
to root for: no favorites configured, neither side is a favorite, or
*both* are -- a favorite-vs-favorite game has no losing side worth
flagging in red. Also None when the scores are not usable numbers.
"""
favorites = {
str(team).strip().upper()
for team in self._favorite_teams_for(game)
if str(team).strip()
}
if not favorites:
return None

home_fav = self._side_is_favorite(game, "home", favorites)
away_fav = self._side_is_favorite(game, "away", favorites)
if home_fav == away_fav:
return None

home_score = self._side_score(game, "home")
away_score = self._side_score(game, "away")
if home_score is None or away_score is None:
return None

if home_score == away_score:
return "tie"
favorite_score, other_score = (
(home_score, away_score) if home_fav else (away_score, home_score)
)
return "win" if favorite_score > other_score else "loss"

def _score_color_for(self, game: Dict[str, Any], game_type: str, default=(255, 255, 255)):
"""Fill color for a game card's score. Only finished games are tinted."""
if game_type != "recent":
return default
return self._recent_score_color(game, default)

def _recent_score_color(self, game: Dict[str, Any], default):
"""Fill color for a finished game's score, per favorite_result_colors."""
try:
settings = (self.config.get("customization") or {}).get(
"favorite_result_colors"
) or {}
if not settings.get("enabled", False):
return default
result = self._favorite_result(game)
if result is None:
return default
return self._coerce_rgb(
settings.get(f"{result}_color"),
self.FAVORITE_RESULT_COLOR_DEFAULTS[result],
)
except Exception:
self.logger.debug(
"Could not resolve favorite result color", exc_info=True
)
return default

def render_game_card(
self,
game: Dict[str, Any],
Expand Down Expand Up @@ -373,7 +510,10 @@ def render_game_card(
# Draw scores (centered)
score_x = (self.display_width - score_width) // 2
score_y = (self.display_height // 2) - 3
self._draw_text_with_outline(draw_overlay, score_text, (score_x, score_y), self.fonts['score'])
self._draw_text_with_outline(
draw_overlay, score_text, (score_x, score_y), self.fonts['score'],
fill=self._score_color_for(game, game_type)
)

# Draw period/status based on game type
if game_type == "live":
Expand Down
8 changes: 7 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.3.2",
"version": "1.4.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,12 @@
"afl_upcoming"
],
"versions": [
{
"version": "1.4.0",
"released": "2026-08-05",
"notes": "Add customization.favorite_result_colors: an optional setting that colors a recent game's final score green when your favorite team won and red when it lost. Aimed at scroll and Vegas modes, where the same matchup can go past several times and the logos alone do not say who won. Off by default; games without exactly one favorite team keep the normal score color.",
"ledmatrix_min_version": "2.0.0"
},
{
"version": "1.3.2",
"released": "2026-08-05",
Expand Down
Loading
Loading