Skip to content
Open
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
@@ -1,6 +1,6 @@
{
"version": "1.0.0",
"last_updated": "2026-07-08",
"last_updated": "2026-07-12",
"plugins": [
{
"id": "7-segment-clock",
Expand Down Expand Up @@ -507,7 +507,7 @@
{
"id": "on-air",
"name": "On Air Light",
"description": "Retro broadcast ON AIR tally light. Activate remotely via MQTT or Home Assistant to signal you're on a call, recording, or live \u2014 stays on until you turn it off.",
"description": "Retro broadcast ON AIR tally light. Activate remotely via MQTT or Home Assistant to signal you're on a call, recording, or live stays on until you turn it off.",
"author": "ChuckBuilds",
"category": "utility",
"tags": [
Expand Down Expand Up @@ -654,7 +654,7 @@
{
"id": "plex-marquee",
"name": "Plex Marquee",
"description": "Cinema marquee display powered by Fanart.tv \u2014 shows banner art for whatever is currently playing on Plex, with a web portal to browse and pick banners for your library.",
"description": "Cinema marquee display powered by Fanart.tv shows banner art for whatever is currently playing on Plex, with a web portal to browse and pick banners for your library.",
"author": "ant456",
"category": "media",
"tags": [
Expand Down Expand Up @@ -991,10 +991,10 @@
"plugin_path": "plugins/nrl-scoreboard",
"stars": 0,
"downloads": 0,
"last_updated": "2026-07-10",
"last_updated": "2026-07-12",
"verified": true,
"screenshot": "",
"latest_version": "1.0.0"
"latest_version": "1.0.1"
}
]
}
}
6 changes: 3 additions & 3 deletions plugins/nrl-scoreboard/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,8 +64,8 @@ options (see `config_schema.json` for the full list, types, and defaults):
| Key | Description |
|---|---|
| `enabled` | Master on/off switch |
| `favorite_teams` | List of team names/abbreviations to prioritize |
| `exclude_teams` | Teams to hide (spoiler protection) |
| `favorite_teams` | List of favorite NRL teams to prioritize. Use the full team name (e.g. `"Newcastle Knights"`) or ESPN team ID, **not** the 3-letter abbreviation — some abbreviations are shared by two teams (`NEW` is both Newcastle Knights and New Zealand Warriors; `CAN` is both Canberra Raiders and Canterbury Bulldogs) and would match both |
| `exclude_teams` | Teams to hide (spoiler protection). Same name/ID guidance as `favorite_teams` |
| `display_modes` | Toggle `live`/`recent`/`upcoming` and set `*_display_mode` to `switch` or `scroll` |
| `live_priority` | Interrupt the rotation to show live games immediately |
| `live_game_duration` / `recent_game_duration` / `upcoming_game_duration` | Per-game on-screen time (seconds) |
Expand All @@ -85,7 +85,7 @@ options (see `config_schema.json` for the full list, types, and defaults):
{
"nrl-scoreboard": {
"enabled": true,
"favorite_teams": ["PEN", "BRI"],
"favorite_teams": ["Penrith Panthers", "Brisbane Broncos"],
"display_modes": {
"live": true,
"live_display_mode": "switch",
Expand Down
8 changes: 4 additions & 4 deletions plugins/nrl-scoreboard/config_schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,22 +13,22 @@
"type": "array",
"items": {
"type": "string",
"description": "NRL team name or abbreviation"
"description": "NRL full team name (e.g. 'Newcastle Knights') or ESPN team ID. Avoid 3-letter abbreviations - some are shared by two teams (e.g. 'NEW' is both Newcastle Knights and New Zealand Warriors, 'CAN' is both Canberra Raiders and Canterbury Bulldogs) and will match both."
},
"uniqueItems": true,
"maxItems": 20,
"description": "List of favorite NRL teams to prioritize"
"description": "List of favorite NRL teams to prioritize. Use the full team name or ESPN team ID - some abbreviations (e.g. NEW, CAN) are shared by two different NRL teams."
},
"exclude_teams": {
"type": "array",
"items": {
"type": "string",
"description": "NRL team name or abbreviation"
"description": "NRL full team name (e.g. 'Newcastle Knights') or ESPN team ID. Avoid 3-letter abbreviations - some are shared by two teams (e.g. 'NEW' is both Newcastle Knights and New Zealand Warriors, 'CAN' is both Canberra Raiders and Canterbury Bulldogs) and will match both."
},
"uniqueItems": true,
"maxItems": 20,
"default": [],
"description": "Team abbreviations to always hide from live rotation and recent/final scores for this league (spoiler protection). Takes precedence over Favorite Teams and Show All Live."
"description": "Teams to always hide from live rotation and recent/final scores for this league (spoiler protection). Takes precedence over Favorite Teams and Show All Live. Use the full team name or ESPN team ID - some abbreviations (e.g. NEW, CAN) are shared by two different NRL teams."
},
"display_modes": {
"type": "object",
Expand Down
197 changes: 165 additions & 32 deletions plugins/nrl-scoreboard/dynamic_team_resolver.py
Original file line number Diff line number Diff line change
@@ -1,63 +1,196 @@
"""
Simplified DynamicTeamResolver for NRL plugin use
DynamicTeamResolver for the NRL plugin

Resolves user-configured team strings (favorite_teams / exclude_teams) to
ESPN team IDs.

NRL's ESPN abbreviations are NOT unique: Newcastle Knights and New Zealand
Warriors both use "NEW", and Canberra Raiders and Canterbury Bulldogs both
use "CAN". Matching favorite/excluded teams by abbreviation therefore
silently conflates unrelated teams (a user configuring "NEW" for the
Newcastle Knights would also match every New Zealand Warriors game).

To avoid that, this resolver converts each configured entry into a unique
ESPN team ID by fetching the NRL team list once (cached in-process) and
matching against team names first, then abbreviations - but only when an
abbreviation maps to exactly one team. Every downstream membership check in
sports.py compares team IDs (home_id/away_id) against the output of
resolve_teams(), so resolution here must produce team IDs.
"""

import logging
import time
from typing import Dict, List
from typing import Dict, List, Optional, Tuple

import requests

logger = logging.getLogger(__name__)

NRL_TEAMS_URL = "https://site.api.espn.com/apis/site/v2/sports/rugby-league/3/teams?limit=50"


class DynamicTeamResolver:
"""
Simplified resolver for dynamic team names to actual team abbreviations.

This class handles special team names that represent dynamic groups
for NRL.
Resolves NRL team names/abbreviations/IDs to canonical ESPN team IDs.
"""

# Cache for rankings data
_rankings_cache: Dict[str, List[str]] = {}
_cache_timestamp: float = 0
_cache_duration: int = 3600 # 1 hour cache

def __init__(self, request_timeout: int = 30):

# Class-level cache shared across instances/managers so the teams list
# is fetched at most once per process (per cache_duration window).
_teams_index_cache: Dict[str, dict] = {}
_teams_index_timestamp: Dict[str, float] = {}
_cache_duration: int = 24 * 3600 # team ids/abbreviations don't change intra-season

def __init__(self, request_timeout: int = 15):
"""Initialize the dynamic team resolver."""
self.request_timeout = request_timeout
self.logger = logger

def resolve_teams(self, team_list: List[str], sport: str = 'rugby-league') -> List[str]:
"""
Resolve a list of team names, expanding dynamic team names.
Resolve a list of team names/abbreviations/IDs to ESPN team IDs.

Args:
team_list: List of team names
team_list: List of team names, abbreviations, or ESPN team IDs
sport: Sport type for context (default: 'rugby-league')

Returns:
List of resolved team abbreviations
List of resolved ESPN team IDs (as strings). Entries that could
not be resolved (fetch failed, no match, ambiguous abbreviation)
are passed through unchanged and logged so the underlying issue
is visible.
"""
if not team_list:
return []

resolved_teams = []

for team in team_list:
# For NRL, we just pass through team names as-is
# No dynamic patterns like AP_TOP_25 for NRL
resolved_teams.append(team)


index = self._get_teams_index(sport)

resolved_teams = [self._resolve_one(team, index) for team in team_list]

# Remove duplicates while preserving order
seen = set()
unique_teams = []
for team in resolved_teams:
if team not in seen:
seen.add(team)
unique_teams.append(team)

return unique_teams

def _is_cache_valid(self) -> bool:
"""Check if the rankings cache is still valid."""
return time.time() - self._cache_timestamp < self._cache_duration

def _resolve_one(self, team: str, index: Optional[dict]) -> str:
raw = (team or "").strip()
if not raw:
return raw

if raw.isdigit():
# Already an ESPN team ID.
return raw

if not index:
self.logger.warning(
f"NRL team list unavailable; using '{raw}' as-is for "
"favorite/exclude matching. If this is an abbreviation it "
"may not be unique (e.g. 'NEW' matches both Newcastle "
"Knights and New Zealand Warriors)."
)
return raw

key = raw.lower()

by_name = index["by_name"]
if key in by_name:
return by_name[key]

by_abbr = index["by_abbr"]
candidates = by_abbr.get(key)
if not candidates:
self.logger.warning(
f"Could not resolve NRL team '{raw}' to a known team; using "
"it as-is. Use the full team name (e.g. 'Newcastle Knights') "
"or the ESPN team ID instead of an abbreviation."
)
return raw

if len(candidates) > 1:
names = ", ".join(f"{name} (id {tid})" for tid, name in candidates)
self.logger.error(
f"NRL abbreviation '{raw}' is ambiguous - it matches multiple "
f"teams: {names}. Configure the full team name or the ESPN "
"team ID in favorite_teams/exclude_teams instead."
)
return raw

return candidates[0][0]

def _get_teams_index(self, sport: str) -> Optional[dict]:
if sport not in ('rugby-league', 'nrl'):
return None

now = time.time()
cached = DynamicTeamResolver._teams_index_cache.get(sport)
cached_at = DynamicTeamResolver._teams_index_timestamp.get(sport, 0)
if cached and (now - cached_at) < self._cache_duration:
return cached

fetched = self._fetch_teams_index()
if fetched:
DynamicTeamResolver._teams_index_cache[sport] = fetched
DynamicTeamResolver._teams_index_timestamp[sport] = now
return fetched

# Fetch failed: fall back to a stale cache rather than nothing.
return cached

def _fetch_teams_index(self) -> Optional[dict]:
try:
response = requests.get(NRL_TEAMS_URL, timeout=self.request_timeout)
response.raise_for_status()
data = response.json()
except Exception as e:
self.logger.warning(
f"Failed to fetch NRL team list for favorite/exclude team "
f"resolution: {e}"
)
return None

return self._build_index(data)

@staticmethod
def _build_index(data: dict) -> Optional[dict]:
try:
teams = data["sports"][0]["leagues"][0]["teams"]
except (KeyError, IndexError, TypeError):
logger.warning(
"Unexpected NRL teams response shape; cannot resolve "
"favorite/exclude teams"
)
return None

by_name: Dict[str, str] = {}
by_abbr: Dict[str, List[Tuple[str, str]]] = {}

for entry in teams:
t = entry.get("team", {})
team_id = t.get("id")
if not team_id:
continue
display_name = t.get("displayName") or t.get("name") or ""

for name in (
t.get("displayName"),
t.get("shortDisplayName"),
t.get("name"),
t.get("location"),
t.get("nickname"),
):
if name:
by_name[name.strip().lower()] = team_id

abbr = t.get("abbreviation")
if abbr:
by_abbr.setdefault(abbr.strip().lower(), []).append(
(team_id, display_name or abbr)
)

if not by_name and not by_abbr:
return None
return {"by_name": by_name, "by_abbr": by_abbr}
4 changes: 2 additions & 2 deletions plugins/nrl-scoreboard/manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -669,8 +669,8 @@ def _has_favorite_or_all_live(self, live_manager) -> bool:
favorite_teams = getattr(live_manager, "favorite_teams", [])
if favorite_teams:
return any(
game.get("home_abbr") in favorite_teams
or game.get("away_abbr") in favorite_teams
str(game.get("home_id")) in favorite_teams
or str(game.get("away_id")) in favorite_teams
for game in live_games
)
return False
Expand Down
10 changes: 8 additions & 2 deletions plugins/nrl-scoreboard/manifest.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"id": "nrl-scoreboard",
"name": "NRL Scoreboard",
"version": "1.0.0",
"version": "1.0.1",
"author": "ChuckBuilds",
"description": "Live, recent, and upcoming NRL (National Rugby League) games with real-time scores and game status.",
"category": "sports",
Expand All @@ -18,14 +18,20 @@
"nrl_upcoming"
],
"versions": [
{
"released": "2026-07-12",
"version": "1.0.1",
"notes": "Fix favorite_teams/exclude_teams matching: NRL abbreviations aren't unique (\"NEW\" is both Newcastle Knights and New Zealand Warriors, \"CAN\" is both Canberra Raiders and Canterbury Bulldogs). Team matching now resolves to unique ESPN team IDs instead of relying on the ambiguous abbreviation.",
"ledmatrix_min": "2.0.0"
},
{
"released": "2026-07-10",
"version": "1.0.0",
"notes": "Initial release: live, recent, and upcoming NRL games from ESPN's rugby-league API with switch/scroll display, live priority, goal/win celebrations, dynamic duration, and Vegas scroll support.",
"ledmatrix_min": "2.0.0"
}
],
"last_updated": "2026-07-10",
"last_updated": "2026-07-12",
"stars": 0,
"downloads": 0,
"verified": true,
Expand Down
Loading
Loading