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
36 changes: 18 additions & 18 deletions plugins.json
Original file line number Diff line number Diff line change
Expand Up @@ -73,10 +73,10 @@
"plugin_path": "plugins/baseball-scoreboard",
"stars": 0,
"downloads": 0,
"last_updated": "2026-08-13",
"last_updated": "2026-09-02",
"verified": true,
"screenshot": "",
"latest_version": "1.37.0"
"latest_version": "1.37.1"
},
{
"id": "basketball-scoreboard",
Expand All @@ -98,10 +98,10 @@
"plugin_path": "plugins/basketball-scoreboard",
"stars": 0,
"downloads": 0,
"last_updated": "2026-08-13",
"last_updated": "2026-09-02",
"verified": true,
"screenshot": "",
"latest_version": "1.26.0"
"latest_version": "1.26.1"
},
{
"id": "calendar",
Expand Down Expand Up @@ -237,10 +237,10 @@
"plugin_path": "plugins/football-scoreboard",
"stars": 0,
"downloads": 0,
"last_updated": "2026-08-13",
"last_updated": "2026-09-02",
"verified": true,
"screenshot": "",
"latest_version": "3.1.0"
"latest_version": "3.1.1"
},
{
"id": "geochron",
Expand Down Expand Up @@ -332,10 +332,10 @@
"plugin_path": "plugins/hockey-scoreboard",
"stars": 0,
"downloads": 0,
"last_updated": "2026-08-13",
"last_updated": "2026-09-02",
"verified": true,
"screenshot": "",
"latest_version": "1.22.0",
"latest_version": "1.22.1",
"icon": "fas fa-hockey-puck"
},
{
Expand All @@ -356,10 +356,10 @@
"plugin_path": "plugins/lacrosse-scoreboard",
"stars": 0,
"downloads": 0,
"last_updated": "2026-08-13",
"last_updated": "2026-09-02",
"verified": true,
"screenshot": "",
"latest_version": "1.21.0",
"latest_version": "1.21.1",
"icon": "fas fa-baseball-ball"
},
{
Expand Down Expand Up @@ -757,10 +757,10 @@
"plugin_path": "plugins/soccer-scoreboard",
"stars": 0,
"downloads": 0,
"last_updated": "2026-08-13",
"last_updated": "2026-09-02",
"verified": true,
"screenshot": "",
"latest_version": "2.21.0"
"latest_version": "2.21.1"
},
{
"id": "static-image",
Expand Down Expand Up @@ -902,10 +902,10 @@
"plugin_path": "plugins/ufc-scoreboard",
"stars": 0,
"downloads": 0,
"last_updated": "2026-08-05",
"last_updated": "2026-09-02",
"verified": true,
"screenshot": "",
"latest_version": "1.8.0",
"latest_version": "1.8.1",
"icon": "fas fa-fist-raised"
},
{
Expand Down Expand Up @@ -1048,8 +1048,8 @@
"downloads": 0,
"verified": true,
"screenshot": "",
"latest_version": "1.19.0",
"last_updated": "2026-08-13"
"latest_version": "1.19.1",
"last_updated": "2026-09-02"
},
{
"id": "tidbyt-baseball-scoreboard",
Expand Down Expand Up @@ -1092,10 +1092,10 @@
"plugin_path": "plugins/nrl-scoreboard",
"stars": 0,
"downloads": 0,
"last_updated": "2026-08-13",
"last_updated": "2026-09-02",
"verified": true,
"screenshot": "",
"latest_version": "1.18.0"
"latest_version": "1.18.1"
},
{
"id": "jellyfin-now-playing",
Expand Down
11 changes: 9 additions & 2 deletions 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.0",
"version": "1.19.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,13 @@
"afl_upcoming"
],
"versions": [
{
"version": "1.19.1",
"released": "2026-09-02",
"ledmatrix_min_version": "3.2.0",
"notes": "Retry a team logo whose previous download failed, instead of showing a grey box forever. A failed download is cached by the core as a placeholder wearing the real logo's filename; the logo loader scans filename variations, found that stub, and so never called the downloader again. The loader now skips a placeholder that is stale enough to be worth retrying and lets the download run, which also picks up stubs already on disk. The retry is rate-limited by the core (6h), so this does not trade a permanent grey box for a request every frame. Needs a core carrying src.logo_downloader.is_placeholder_logo; against an older core the check is skipped and behaviour is unchanged. Ported byte-identically across every sports lineage.",
"changelog": "Retry a team logo whose previous download failed, instead of showing a grey box forever. A failed download is cached by the core as a placeholder wearing the real logo's filename; the logo loader scans filename variations, found that stub, and so never called the downloader again. The loader now skips a placeholder that is stale enough to be worth retrying and lets the download run, which also picks up stubs already on disk. The retry is rate-limited by the core (6h), so this does not trade a permanent grey box for a request every frame. Needs a core carrying src.logo_downloader.is_placeholder_logo; against an older core the check is skipped and behaviour is unchanged. Ported byte-identically across every sports lineage."
},
{
"version": "1.19.0",
"released": "2026-09-02",
Expand Down Expand Up @@ -273,7 +280,7 @@
"ledmatrix_min": "2.0.0"
}
],
"last_updated": "2026-08-13",
"last_updated": "2026-09-02",
"stars": 0,
"downloads": 0,
"verified": true,
Expand Down
35 changes: 33 additions & 2 deletions plugins/afl-scoreboard/sports.py
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,37 @@ def _clamp_seconds(value: Any, fallback: int, low: int = 5,
return max(low, min(high, seconds))


def _logo_needs_refresh(logo_file) -> bool:
"""True if this file is a placeholder stale enough to retry the real logo.

A failed logo download is cached as a placeholder wearing the real logo's
filename, so "the file exists" is not proof the logo was ever fetched.
Without this check one transient failure leaves a team a grey box forever.

Returns False on a core that predates placeholder marking, which keeps the
previous behaviour rather than breaking the load.
"""
# Imported from the core by its full path, never as a bare name: a
# deferred bare-name import can bind another plugin's vendored
# logo_downloader once the core isolates top-level plugin modules.
try:
from src.logo_downloader import (
PLACEHOLDER_RETRY_SECONDS,
is_placeholder_logo,
placeholder_age_seconds,
)
except ImportError:
return False

try:
if not is_placeholder_logo(logo_file):
return False
age = placeholder_age_seconds(logo_file)
return age is None or age >= PLACEHOLDER_RETRY_SECONDS
except Exception:
return False


class SportsCore(ABC):
def __init__(
self,
Expand Down Expand Up @@ -1614,13 +1645,13 @@ def _load_and_resize_logo(

for filename in filename_variations:
test_path = logo_path.parent / filename
if test_path.exists():
if test_path.exists() and not _logo_needs_refresh(test_path):
actual_logo_path = test_path
self.logger.debug(f"Found logo at alternative path: {actual_logo_path}")
break

# If no variation found, try to download missing logo
if not actual_logo_path and not logo_path.exists():
if not actual_logo_path:
self.logger.info(f"Logo not found for {team_abbrev} at {logo_path}. Attempting to download.")

# AFL is a single league, so logos live in one shared directory.
Expand Down
37 changes: 34 additions & 3 deletions plugins/baseball-scoreboard/logo_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,37 @@
download_missing_logo = None


def _logo_needs_refresh(logo_file) -> bool:
"""True if this file is a placeholder stale enough to retry the real logo.

A failed logo download is cached as a placeholder wearing the real logo's
filename, so "the file exists" is not proof the logo was ever fetched.
Without this check one transient failure leaves a team a grey box forever.

Returns False on a core that predates placeholder marking, which keeps the
previous behaviour rather than breaking the load.
"""
# Imported from the core by its full path, never as a bare name: a
# deferred bare-name import can bind another plugin's vendored
# logo_downloader once the core isolates top-level plugin modules.
try:
from src.logo_downloader import (
PLACEHOLDER_RETRY_SECONDS,
is_placeholder_logo,
placeholder_age_seconds,
)
except ImportError:
return False

try:
if not is_placeholder_logo(logo_file):
return False
age = placeholder_age_seconds(logo_file)
return age is None or age >= PLACEHOLDER_RETRY_SECONDS
except Exception:
return False


class BaseballLogoManager:
"""Manages logo loading, caching, and downloading for baseball teams."""

Expand Down Expand Up @@ -87,17 +118,17 @@ def load_logo(self, team_id: str, team_abbr: str, logo_path: Path,

for filename in filename_variations:
test_path = logo_path.parent / filename
if test_path.exists():
if test_path.exists() and not _logo_needs_refresh(test_path):
actual_logo_path = test_path
self.logger.debug(f"Found logo at alternative path: {actual_logo_path}")
break
else:
# Fallback: just try the original path
if logo_path.exists():
if logo_path.exists() and not _logo_needs_refresh(logo_path):
actual_logo_path = logo_path

# If no variation found, try to download missing logo
if not actual_logo_path and not logo_path.exists():
if not actual_logo_path:
self.logger.info(f"Logo not found for {team_abbr} at {logo_path}. Attempting to download.")

# Try to download the logo from ESPN API (this will create placeholder if download fails)
Expand Down
17 changes: 12 additions & 5 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.37.0",
"version": "1.37.1",
"update_interval": 60,
"author": "ChuckBuilds",
"description": "Live, recent, and upcoming baseball games across MLB, MiLB, and NCAA Baseball with real-time scores and schedules",
Expand Down Expand Up @@ -31,6 +31,13 @@
"branch": "main",
"plugin_path": "plugins/baseball-scoreboard",
"versions": [
{
"version": "1.37.1",
"released": "2026-09-02",
"ledmatrix_min_version": "3.2.0",
"notes": "Retry a team logo whose previous download failed, instead of showing a grey box forever. A failed download is cached by the core as a placeholder wearing the real logo's filename; the logo loader scans filename variations, found that stub, and so never called the downloader again. The loader now skips a placeholder that is stale enough to be worth retrying and lets the download run, which also picks up stubs already on disk. The retry is rate-limited by the core (6h), so this does not trade a permanent grey box for a request every frame. Needs a core carrying src.logo_downloader.is_placeholder_logo; against an older core the check is skipped and behaviour is unchanged. Ported byte-identically across every sports lineage.",
"changelog": "Retry a team logo whose previous download failed, instead of showing a grey box forever. A failed download is cached by the core as a placeholder wearing the real logo's filename; the logo loader scans filename variations, found that stub, and so never called the downloader again. The loader now skips a placeholder that is stale enough to be worth retrying and lets the download run, which also picks up stubs already on disk. The retry is rate-limited by the core (6h), so this does not trade a permanent grey box for a request every frame. Needs a core carrying src.logo_downloader.is_placeholder_logo; against an older core the check is skipped and behaviour is unchanged. Ported byte-identically across every sports lineage."
},
{
"version": "1.37.0",
"released": "2026-09-02",
Expand Down Expand Up @@ -347,7 +354,7 @@
{
"released": "2026-07-08",
"version": "1.14.2",
"notes": "Shrink the Traditional Scoreboard's ball/strike/out circle indicators further (they were still a bit overpowering) and move the batting-team ▲/▼ indicator out of the At Bat column into the header row's empty team-column cell, right next to the inning numbers.",
"notes": "Shrink the Traditional Scoreboard's ball/strike/out circle indicators further (they were still a bit overpowering) and move the batting-team \u25b2/\u25bc indicator out of the At Bat column into the header row's empty team-column cell, right next to the inning numbers.",
"ledmatrix_min": "2.0.0"
},
{
Expand All @@ -365,7 +372,7 @@
{
"released": "2026-07-07",
"version": "1.13.1",
"notes": "Fix the Traditional Scoreboard's At Bat side panel (added in 1.13.0) clipping its ball/strike/out dots off the right edge of the display -- the fit check compared leftover space against a flush-left grid, but the grid is actually centered, so it was eating into the panel's reserved space from the left too. Also account for the Outs row's extra batting-team ▲/▼ arrow, which wasn't factored into the width check at all.",
"notes": "Fix the Traditional Scoreboard's At Bat side panel (added in 1.13.0) clipping its ball/strike/out dots off the right edge of the display -- the fit check compared leftover space against a flush-left grid, but the grid is actually centered, so it was eating into the panel's reserved space from the left too. Also account for the Outs row's extra batting-team \u25b2/\u25bc arrow, which wasn't factored into the width check at all.",
"ledmatrix_min": "2.0.0"
},
{
Expand Down Expand Up @@ -407,7 +414,7 @@
{
"released": "2026-07-02",
"version": "1.7.0",
"notes": "Add exclude_teams (hide specific teams from live rotation and recent/final scores spoiler protection) and filtering.favorite_live_boost (tune how much more often your favorite's live game appears in rotation vs other live games) per league.",
"notes": "Add exclude_teams (hide specific teams from live rotation and recent/final scores \u2014 spoiler protection) and filtering.favorite_live_boost (tune how much more often your favorite's live game appears in rotation vs other live games) per league.",
"ledmatrix_min": "2.0.0"
},
{
Expand Down Expand Up @@ -525,7 +532,7 @@
"ledmatrix_min_version": "2.0.0"
}
],
"last_updated": "2026-08-13",
"last_updated": "2026-09-02",
"stars": 0,
"downloads": 0,
"verified": true,
Expand Down
35 changes: 33 additions & 2 deletions plugins/baseball-scoreboard/sports.py
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,37 @@ def _clamp_seconds(value: Any, fallback: int, low: int = 5,
return max(low, min(high, seconds))


def _logo_needs_refresh(logo_file) -> bool:
"""True if this file is a placeholder stale enough to retry the real logo.

A failed logo download is cached as a placeholder wearing the real logo's
filename, so "the file exists" is not proof the logo was ever fetched.
Without this check one transient failure leaves a team a grey box forever.

Returns False on a core that predates placeholder marking, which keeps the
previous behaviour rather than breaking the load.
"""
# Imported from the core by its full path, never as a bare name: a
# deferred bare-name import can bind another plugin's vendored
# logo_downloader once the core isolates top-level plugin modules.
try:
from src.logo_downloader import (
PLACEHOLDER_RETRY_SECONDS,
is_placeholder_logo,
placeholder_age_seconds,
)
except ImportError:
return False

try:
if not is_placeholder_logo(logo_file):
return False
age = placeholder_age_seconds(logo_file)
return age is None or age >= PLACEHOLDER_RETRY_SECONDS
except Exception:
return False


class SportsCore(ABC):
def __init__(
self,
Expand Down Expand Up @@ -1562,15 +1593,15 @@ def _load_and_resize_logo(

for filename in filename_variations:
test_path = logo_path.parent / filename
if test_path.exists():
if test_path.exists() and not _logo_needs_refresh(test_path):
actual_logo_path = test_path
self.logger.debug(
f"Found logo at alternative path: {actual_logo_path}"
)
break

# If no variation found, try to download missing logo
if not actual_logo_path and not logo_path.exists():
if not actual_logo_path:
self.logger.info(
f"Logo not found for {team_abbrev} at {logo_path}. Attempting to download."
)
Expand Down
Loading
Loading