Commit Graph
3 Commits
Author SHA1 Message Date
ChuckandClaude Opus 5.5 3f8edf5113 fix(logos): one hardened logo download path; shared, real HTTP headers (#612)
* fix(logos): harden the plugin logo download and share core HTTP headers

download_missing_logo / LogoDownloader.download_logo, the path the
scoreboard plugins use, read response.content with no size cap and wrote
straight to the final path, so a failed or corrupt download could be left
in place and cached as the logo. It now goes through fetch_logo: streamed
with a 10 MB cap, image/* only, decoded by Pillow, converted to RGBA once,
and moved into place atomically. A failure leaves no partial or temp file
and keeps any logo already on disk. LogoHelper._download_logo delegates to
the same code. Public signatures and return values are unchanged; saved
files are pixel-identical to before (RGBA, palette+tRNS, L+tRNS, LA, JPEG).

download_missing_logo reuses one downloader per thread instead of a new
Session per logo. Per thread rather than behind a lock: Session is not
documented thread-safe, and a lock would serialise every plugin's
downloads behind the slowest one.

Placeholders are written atomically, without the test_write.tmp probe.

The logo downloader and background data service now send the real
ChuckBuilds User-Agent from src.common.api_helper (USER_AGENT,
DEFAULT_HTTP_HEADERS) instead of a yourusername/contact@example.com
placeholder, and no longer hand-set Accept-Encoding: br (brotli is not
installed).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(http): drop APIHelper's hand-set brotli encoding; LogoHelper sends the real UA

APIHelper advertised `br` though brotli isn't installed, so a server that
honoured it would send a body requests can't decode. LogoHelper sent a bare
`LEDMatrix-Common/1.0`, the kind of User-Agent ESPN has been rejecting.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-23 12:58:11 -04:00
ChuckandClaude Opus 5 92f9d06af9 fix(logos): stop a failed download pinning a team to a grey box forever (#512)
* fix(logos): stop a failed download pinning a team to a grey box forever

When a logo download fails, create_placeholder_logo writes a 64x64 grey PNG
under the *real* logo's filename. Every later call then hits
`if filepath.exists(): return True` and reports success, so the real logo is
never attempted again. One transient failure -- no network at boot, ESPN
blipping -- permanently costs that team its logo.

This is not hypothetical. Five of the eleven cached AFL logos in my checkout
were 384-byte stubs written in a single bad minute, and they had stayed that
way ever since; the scoreboard rendered COLL, FRE, NMFC, PORT and SYD as grey
text boxes on every card.

Placeholders are now stamped with a `ledmatrix_placeholder` PNG text chunk
carrying their creation time, and `is_placeholder_logo` recognises them. It
also matches on the placeholder's exact geometry and background colour, so the
stubs already sitting on users' disks are picked up too -- without that, this
fix would only help teams whose logos break in future. Verified against the
real stubs: all five detected, all six real logos untouched.

`download_missing_logo` now treats an existing placeholder as the failed
download it is and retries, rather than as a satisfied request. The retry is
rate-limited to PLACEHOLDER_RETRY_SECONDS (6h) so this does not trade a
permanent grey box for an ESPN request every frame; a failed retry rewrites the
placeholder, restarting the clock. The age comes from the stamp rather than
mtime, so a backup restore, an rsync, or a permissions script cannot silently
reset it.

`download_missing_logos_for_league` gets the same treatment -- a bulk pass is
exactly where a previously failed logo should get another chance -- and
`LogoHelper.load_logo_with_download` no longer accepts a stale placeholder as a
cache hit. That import is lazy and guarded so the module still works against a
core build predating the marker.

`LogoHelper._create_placeholder_logo` needs no change: it returns an in-memory
image and never writes it to disk, which is the behaviour this bug argues for.

Tests cover marked and legacy-unmarked detection, the two false-positive cases
(a real 500x500 logo, and a 64x64 image that is merely the same size), the
retry, the rate limit, and that the age survives an mtime touch.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(logos): address review — unify eligibility, invalidate cache, restart back-off

Three findings from the review on #512, all confirmed against the code:

1. The three download sites each had their own idea of "already have it".
   download_missing_logos_for_league() retried *any* placeholder, ignoring the
   back-off entirely, while download_all_ncaa_football_logos() was never
   updated and still skipped placeholders forever. They now share one
   should_attempt_download(), which also covers force_download, so the sites
   cannot drift apart again. download_missing_logo() reads through the same
   helper.

2. LogoHelper.load_logo_with_download() answered from the in-memory cache
   before touching the disk, so after a stale placeholder was successfully
   replaced the *cached placeholder image* was still returned -- the real logo
   would not have appeared until the process restarted. The cache entry for
   that file (every size of it) is now dropped after a successful download.

3. A failed retry left the stale placeholder on disk with its old timestamp,
   so the next call saw it as stale again and retried immediately: a download
   attempt per call, which is precisely what the back-off exists to prevent.
   refresh_placeholder_timestamp() restamps it, and the helper calls that on
   the failure path. It refuses to touch anything that is not a placeholder.

Tests cover both bulk loops in both directions (fresh placeholder skipped,
stale one retried), the eligibility rule including force_download, the
timestamp refresh, and the two LogoHelper paths -- including that a
freshly-downloaded logo is actually what comes back rather than the cached
placeholder.

Two of the new bulk-loop tests initially passed for the wrong reason: the
fetch_teams_data stub returned {}, which is falsy, so the loops bailed before
reaching the eligibility check at all. Fixed to return a truthy payload.

Re-verified end to end: with both halves in place, rendering the AFL scoreboard
took FRE.png from a 362-byte stub to a 12,928-byte logo.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-09-02 13:21:07 -04:00
9930bd33b1 test: add 306 new tests covering previously untested modules (#347)
* test: add 306 new tests covering previously untested modules

Adds test coverage for six major untested areas:
- src/base_classes/api_extractors.py — ESPN football, baseball, hockey, soccer extractors
- src/base_classes/data_sources.py — ESPN, MLB, and soccer API data sources (HTTP mocked)
- src/common/game_helper.py — game extraction, filtering, sorting, and summaries
- src/common/utils.py — all utility functions (normalise, format, validate, parse)
- src/common/scroll_helper.py — ScrollHelper init, create, update, visible portion, duration
- src/background_data_service.py — cache hit/miss paths, retry, cancel, cleanup, singleton
- src/vegas_mode/config.py — VegasModeConfig from_config, validate, update, ordering
- src/logo_downloader.py — normalize_abbreviation, filename variations, directory helpers
- src/plugin_system/health_monitor.py — HealthStatus determination, metrics, suggestions, lifecycle

https://claude.ai/code/session_015792DiGo27JbgH5mk3KBjk

* fix(tests): thread cleanup on assertion failure, reduce oversized image

- test_health_monitor.py: wrap start_monitoring calls in try/finally so
  the background thread is always stopped even when an assertion fails
- test_scroll_helper.py: reduce 50,000px test image to 5,000px to avoid
  unnecessary memory pressure on Raspberry Pi

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Chuck <chuck@example.com>
2026-05-24 09:38:15 -04:00