Files
LEDMatrix/test
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
..