Skip to content

fix(soccer): eliminate flicker from redundant redraws in switch mode (v1.5.2)#58

Merged
ChuckBuilds merged 4 commits intomainfrom
fix/soccer-redundant-redraw
Mar 27, 2026
Merged

fix(soccer): eliminate flicker from redundant redraws in switch mode (v1.5.2)#58
ChuckBuilds merged 4 commits intomainfrom
fix/soccer-redundant-redraw

Conversation

@ChuckBuilds
Copy link
Copy Markdown
Owner

@ChuckBuilds ChuckBuilds commented Mar 27, 2026

Summary

  • Root cause: The display controller calls display() every 1 second in the standard loop. Each call to _draw_scorebug_layout was re-creating full PIL images from scratch (new Image, logo loading/resizing, text drawing, update_display()) even when the displayed game hadn't changed. This caused visible flicker on the LED matrix.
  • Fix: SportsRecent.display() and SportsUpcoming.display() now track the last rendered game ID (_last_rendered_game_id). When force_clear=False and the same game is still showing, the full redraw is skipped entirely — the existing frame stays on the matrix.
  • Cache is invalidated when: game list changes from update, game switches to next in rotation, or force_clear=True is passed.

Test plan

  • Deployed to devpi — plugin loads without errors
  • Watch soccer recent/upcoming modes — display should be rock-solid with no flicker
  • Game switching still works (every game_display_duration seconds, display updates)
  • After data update (hourly), new game data is reflected

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • YouTube Stats plugin now detects and displays API key errors directly on the LED matrix, clearly distinguishing API authentication failures from general data unavailability.
  • Improvements

    • Soccer Scoreboard plugin optimized to eliminate redundant scoreboard rendering when displaying the same game consecutively.
    • Version updates: Soccer Scoreboard (1.5.2), YouTube Stats (1.0.1).

ChuckBuilds and others added 2 commits March 27, 2026 16:55
…v1.0.1)

When the YouTube API returns 400/401/403, the plugin now logs a clear
message: "YouTube API key is invalid or expired. Update your API key
in Settings > Secrets." instead of dumping the raw HTTP error.

Also renders "YT: Update API Key" on the LED display so the user
sees the problem without checking logs.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…1.5.2)

The display controller calls display() every 1 second in the standard
loop. Each call was re-creating full PIL images, loading logos, drawing
text, and pushing to the matrix — even when the displayed game hadn't
changed. This caused visible flicker on the LED panel.

Now Recent and Upcoming display() methods track the last rendered game
ID and skip _draw_scorebug_layout entirely when force_clear=False and
the same game is still showing. The cache is invalidated when the game
list changes from an update or when the game switches.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Mar 27, 2026

Warning

Rate limit exceeded

@ChuckBuilds has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 5 minutes and 0 seconds before requesting another review.

Your organization is not enrolled in usage-based pricing. Contact your admin to enable usage-based pricing to continue reviews beyond the rate limit, or try again in 5 minutes and 0 seconds.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: bbafa624-1123-4f85-b52e-aa2704300173

📥 Commits

Reviewing files that changed from the base of the PR and between 60f25ea and 3b7a488.

📒 Files selected for processing (1)
  • plugins/soccer-scoreboard/sports.py
📝 Walkthrough

Walkthrough

Two plugins receive enhancements: soccer-scoreboard adds render-state tracking to skip redundant redraws, and youtube-stats adds API key error detection with on-matrix error display. Both plugins have version bumps.

Changes

Cohort / File(s) Summary
Soccer Scoreboard Manifest
plugins/soccer-scoreboard/manifest.json
Version bumped from 1.5.1 to 1.5.2.
Soccer Scoreboard Render Caching
plugins/soccer-scoreboard/sports.py
Added _last_rendered_game_id field to track rendered game state. Reset on games_list changes; display() short-circuits if current game ID matches cached ID, avoiding redundant _draw_scorebug_layout() calls. Removed trailing comment markers from error logging.
YouTube Stats Error Handling
plugins/youtube-stats/manager.py
Added _api_key_error field to track API key invalidity. _get_channel_stats() now clears error on success and sets it for HTTP 400/401/403. New _show_error_on_display() method renders error messages on LED matrix. display() differentiates between "no stats available" and "API key invalid/expired" scenarios.
YouTube Stats Manifest
plugins/youtube-stats/manifest.json
Version bumped from 1.0.0 to 1.0.1.

Sequence Diagram(s)

sequenceDiagram
    participant D as display()
    participant G as _get_channel_stats()
    participant A as YouTube API
    participant E as _show_error_on_display()
    participant M as LED Matrix

    D->>G: Request channel stats
    G->>A: Fetch stats (API call)
    alt API Success
        A-->>G: Return stats
        G->>G: Clear _api_key_error
        G-->>D: Return stats
        D->>D: Render stats normally
    else API Error (400/401/403)
        A-->>G: Error response
        G->>G: Set _api_key_error
        G-->>D: Return None
        D->>D: Check _api_key_error
        D->>E: Log & show error
        E->>M: Render centered error text
        M-->>E: (success/failure)
        E->>E: Swallow render errors
    end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and specifically describes the main change: eliminating flicker from redundant redraws in soccer switch mode, with version reference.
Docstring Coverage ✅ Passed Docstring coverage is 90.00% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/soccer-redundant-redraw

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
plugins/soccer-scoreboard/sports.py (1)

1290-1299: ⚠️ Potential issue | 🟠 Major

Cache invalidation misses same-ID data updates.

Invalidation only runs when the set of IDs changes. If game payload changes (e.g., time/status/odds/order) while IDs stay the same, stale data can persist because the fast-path at Line 1620 and Line 2152 keeps skipping redraws.

💡 Proposed fix (refresh list and invalidate once when IDs are unchanged)
@@
-                elif self.games_list:
-                    self.current_game = self.games_list[
-                        self.current_game_index
-                    ]  # Update data
+                elif self.games_list:
+                    # IDs unchanged, but payload/order may have changed.
+                    # Refresh backing list and force one redraw.
+                    self.games_list = team_games
+                    self.current_game_index = min(
+                        self.current_game_index, len(self.games_list) - 1
+                    )
+                    self.current_game = self.games_list[self.current_game_index]
+                    self._last_rendered_game_id = None
@@
-                elif self.games_list:
-                    # List content is same, just update data for current game
-                    self.current_game = self.games_list[self.current_game_index]
+                elif self.games_list:
+                    # IDs unchanged, but payload/order may have changed.
+                    # Refresh backing list and force one redraw.
+                    self.games_list = team_games
+                    self.current_game_index = min(
+                        self.current_game_index, len(self.games_list) - 1
+                    )
+                    self.current_game = self.games_list[self.current_game_index]
+                    self._last_rendered_game_id = None

Also applies to: 1819-1828

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@plugins/soccer-scoreboard/sports.py` around lines 1290 - 1299, The code only
resets self.games_list and self._last_rendered_game_id when the set of IDs
changes, which misses payload-only updates; change the logic inside the with
self._games_lock block to also detect when the payload changed even if IDs are
equal (e.g., compare team_games != self.games_list or compute a stable checksum
of game entries) and in that case assign self.games_list = team_games and set
self._last_rendered_game_id = None to force a redraw; update the same pattern
wherever the ID-only comparison is used (references: new_game_ids,
current_game_ids, self.games_list, self._last_rendered_game_id).
🧹 Nitpick comments (3)
plugins/youtube-stats/manager.py (2)

300-318: Consider adding exc_info for debugging broad exception catches.

The defensive exception handling is appropriate for hardware rendering, but logging exc_info=True would help diagnose unexpected failures during development without cluttering production logs (since it's at debug level).

Suggested improvement
         except Exception as e:
-            self.logger.debug(f"Could not show error on display: {e}")
+            self.logger.debug(f"Could not show error on display: {e}", exc_info=True)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@plugins/youtube-stats/manager.py` around lines 300 - 318, In
_show_error_on_display, update the broad exception logging to include exception
info so failures during rendering are easier to diagnose: when catching
Exception in _show_error_on_display, call self.logger.debug with the same
message but pass exc_info=True (or include the exception object) so the
stacktrace is captured; keep the handler behavior (swallowing the exception) and
debug log level unchanged.

161-167: Simplify dictionary access.

The key existence check can be simplified using dict.get().

Suggested simplification
-            if 'items' in data and data['items']:
+            if data.get('items'):
                 channel = data['items'][0]
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@plugins/youtube-stats/manager.py` around lines 161 - 167, Replace explicit
key existence checks for 'items' with dict.get to simplify and make access
safer: use data.get('items') and get the first item with something like items =
data.get('items') and channel = items[0] if items else None; then build stats
using channel.get('snippet', {}).get('title') and channel.get('statistics',
{}).get('subscriberCount') / 'viewCount' with safe int conversion (handle
None/defaults) so that the block around variables data, channel, and stats is
concise and resilient to missing keys.
plugins/youtube-stats/manifest.json (1)

1-35: Missing versions array in manifest.

The manifest lacks the versions array required by coding guidelines. This array should track version history with released, version, and ledmatrix_min fields.

Consider whether 1.0.1 is the correct version bump—the new _show_error_on_display() feature could arguably warrant a MINOR bump (1.1.0) per the guideline for "new display modes/functionality."

As per coding guidelines: "Add the new version FIRST (most recent at top) to the versions array in manifest.json, with fields: released (date), version (semver), and ledmatrix_min (minimum LEDMatrix version)."

Suggested manifest structure
 {
   "id": "youtube-stats",
   "name": "YouTube Stats",
-  "version": "1.0.1",
+  "version": "1.1.0",
   "author": "ChuckBuilds",
+  "versions": [
+    {
+      "released": "2026-03-27",
+      "version": "1.1.0",
+      "ledmatrix_min": "1.0.0"
+    },
+    {
+      "released": "2026-01-01",
+      "version": "1.0.0",
+      "ledmatrix_min": "1.0.0"
+    }
+  ],
   "description": "Display YouTube channel statistics including subscriber count, total views, and channel name on your LED matrix",
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@plugins/youtube-stats/manifest.json` around lines 1 - 35, The manifest is
missing the required "versions" array; add a top-level "versions" array to
manifest.json with the newest entry first containing the fields "released" (ISO
date string), "version" (semver) and "ledmatrix_min" (minimum LEDMatrix
version), and include the new feature bump—either keep "version": "1.0.1" or
bump to "1.1.0" to reflect the new _show_error_on_display() functionality—then
update the top-level "version" field to match the newest entry in the "versions"
array.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Outside diff comments:
In `@plugins/soccer-scoreboard/sports.py`:
- Around line 1290-1299: The code only resets self.games_list and
self._last_rendered_game_id when the set of IDs changes, which misses
payload-only updates; change the logic inside the with self._games_lock block to
also detect when the payload changed even if IDs are equal (e.g., compare
team_games != self.games_list or compute a stable checksum of game entries) and
in that case assign self.games_list = team_games and set
self._last_rendered_game_id = None to force a redraw; update the same pattern
wherever the ID-only comparison is used (references: new_game_ids,
current_game_ids, self.games_list, self._last_rendered_game_id).

---

Nitpick comments:
In `@plugins/youtube-stats/manager.py`:
- Around line 300-318: In _show_error_on_display, update the broad exception
logging to include exception info so failures during rendering are easier to
diagnose: when catching Exception in _show_error_on_display, call
self.logger.debug with the same message but pass exc_info=True (or include the
exception object) so the stacktrace is captured; keep the handler behavior
(swallowing the exception) and debug log level unchanged.
- Around line 161-167: Replace explicit key existence checks for 'items' with
dict.get to simplify and make access safer: use data.get('items') and get the
first item with something like items = data.get('items') and channel = items[0]
if items else None; then build stats using channel.get('snippet',
{}).get('title') and channel.get('statistics', {}).get('subscriberCount') /
'viewCount' with safe int conversion (handle None/defaults) so that the block
around variables data, channel, and stats is concise and resilient to missing
keys.

In `@plugins/youtube-stats/manifest.json`:
- Around line 1-35: The manifest is missing the required "versions" array; add a
top-level "versions" array to manifest.json with the newest entry first
containing the fields "released" (ISO date string), "version" (semver) and
"ledmatrix_min" (minimum LEDMatrix version), and include the new feature
bump—either keep "version": "1.0.1" or bump to "1.1.0" to reflect the new
_show_error_on_display() functionality—then update the top-level "version" field
to match the newest entry in the "versions" array.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 53dbbb42-ca69-481c-96cb-a72763f14a6c

📥 Commits

Reviewing files that changed from the base of the PR and between 8014e98 and 60f25ea.

📒 Files selected for processing (4)
  • plugins/soccer-scoreboard/manifest.json
  • plugins/soccer-scoreboard/sports.py
  • plugins/youtube-stats/manager.py
  • plugins/youtube-stats/manifest.json

ChuckBuilds and others added 2 commits March 27, 2026 17:16
The elif branches that handle same-IDs-but-refreshed-data were updating
self.current_game but not clearing _last_rendered_game_id, so payload
changes (e.g. corrected final scores) wouldn't trigger a redraw until
the next game switch or force_clear.

Now both Upcoming and Recent update paths clear _last_rendered_game_id
when the game list is refreshed, even if the set of IDs is unchanged.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…dling

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@ChuckBuilds ChuckBuilds merged commit c8c5251 into main Mar 27, 2026
@ChuckBuilds ChuckBuilds deleted the fix/soccer-redundant-redraw branch March 30, 2026 13:35
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant