Skip to content

fix(youtube-stats): stop retrying a failing API once per frame - #323

Merged
ChuckBuilds merged 2 commits into
mainfrom
fix/youtube-stats-per-frame-api-storm
Aug 23, 2026
Merged

fix(youtube-stats): stop retrying a failing API once per frame#323
ChuckBuilds merged 2 commits into
mainfrom
fix/youtube-stats-per-frame-api-storm

Conversation

@ChuckBuilds

Copy link
Copy Markdown
Owner

Found by sweeping for network calls reachable from display() — the render thread — after the thread-safety work on #322. Most hits were guarded or false positives; this one is real.

The bug

def update(self):
    self.channel_stats = self._get_channel_stats()   # None on ANY failure

def display(self, force_clear=False):
    if not self.channel_stats:
        self.update()                                 # ...so: every frame

update() assigns the result unconditionally, so a missing key, an HTTP error, an exhausted quota or a dropped network all leave channel_stats empty — and display() gates on that emptiness. The next frame tries again.

That is one requests.get(..., timeout=10) per rendered frame, on the render thread.

Two consequences, both self-sustaining:

  • The panel stalls. Each call can block for the full 10s timeout, on the thread that draws frames.
  • The quota burns down. The YouTube Data API allows 10,000 units/day by default and channels.list costs 1. A broken key exhausts it in minutes — and quota exhaustion is itself an error, which keeps the result empty, which keeps the loop fed.

The cache inside _get_channel_stats doesn't help: a failed fetch caches nothing, so there's never an entry to serve.

Note the no-key path also logger.error(...)s on every frame before returning.

The fix

  • display() gates on whether a fetch has been attempted (_has_fetched), not on whether it produced data. This is the pattern incoming-packages and jellyfin-now-playing already use for their first-paint call, and it's why neither has this bug.
  • update() is throttled to update_interval. The core already calls it at roughly that cadence so this is a no-op for the core; it exists to bound the display-driven path so a failing API costs one request per interval rather than one per frame.
  • time.monotonic(), not time.time() — these Pis have no RTC, so a wall-clock jump at NTP sync must not make the next attempt look due (or centuries away).

Verification

New test_no_per_frame_api_storm.py (this plugin had no tests at all):

  • 200 frames against a permanently failing API cost one call
  • the retry lands once the interval has elapsed
  • a working API still populates stats, at exactly one call
  • display() no longer gates on the result; update() uses monotonic

6 of its 9 checks fail against the pre-fix manager, so it pins the behaviour rather than describing it.

Not verified on hardware — both rigs have been unreachable all session.

plugins.json deliberately omitted; update-registry.yml regenerates it on merge and carrying it conflicts with the other open PRs.

🤖 Generated with Claude Code

https://claude.ai/code/session_01STMbQE4YctTacQXfbYqKuW

display() re-entered update() whenever channel_stats was empty:

    if not self.channel_stats:
        self.update()

and update() assigns the fetch result unconditionally, so any failure --
missing key, HTTP error, exhausted quota, network down -- left it empty and
the next frame tried again. That is one requests.get(timeout=10) per rendered
frame, on the render thread.

Two consequences, both self-sustaining. The render thread can block for the
full 10s timeout per frame. And the YouTube Data API's default quota is 10,000
units/day, so a broken key burns it down in minutes -- at which point quota
exhaustion becomes the error that keeps the result empty and the loop fed.
The cache in _get_channel_stats never absorbs this: a failed fetch caches
nothing.

display() now gates on whether a fetch has been ATTEMPTED rather than on
whether it produced data, and update() is throttled to update_interval. The
core already calls update() at about that cadence, so the throttle is a no-op
for it; it exists to bound the display-driven path.

monotonic(), not time(): these Pis have no RTC, so a wall-clock jump at NTP
sync must not make the next attempt look due or centuries away.

Tests: new test_no_per_frame_api_storm.py -- 200 frames against a permanently
failing API cost one call, the retry lands once the interval elapses, a
working API is unaffected. 6 of its 9 checks fail against the pre-fix manager.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01STMbQE4YctTacQXfbYqKuW
@coderabbitai

coderabbitai Bot commented Aug 22, 2026

Copy link
Copy Markdown
Contributor

Important

  • 🔍 Trigger review

This repository does not receive automatic reviews because it has fewer than 10 stars.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: c6213e27-59f7-4732-ac00-bfc488f13654


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.

@codacy-production

codacy-production Bot commented Aug 22, 2026

Copy link
Copy Markdown

Up to standards ✅

🟢 Issues 0 issues

Results:
0 new issues

View in Codacy

🟢 Metrics 5 complexity

Metric Results
Complexity 5

View in Codacy

NEW Get contextual insights on your PRs based on Codacy's metrics, along with PR and Jira context, without leaving GitHub. Enable AI reviewer
TIP This summary will be updated as you push new changes.

@ChuckBuilds

Copy link
Copy Markdown
Owner Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Aug 22, 2026

Copy link
Copy Markdown
Contributor
⚠️ Action not completed

Review rate limited.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Codacy flagged line 59 as "No value for argument 'cls' in classmethod call".
It is a false positive -- YouTubeStatsPlugin.__new__(YouTubeStatsPlugin) is
valid -- but only this test triggers it, because its stub makes BasePlugin
`object`, so __new__ resolves to object.__new__ and the explicit class
argument reads as a bound call missing its cls.

object.__new__(YouTubeStatsPlugin) says the same thing unambiguously. The
sibling tests in hockey and soccer keep Cls.__new__(Cls); their stubs do not
create the ambiguity.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01STMbQE4YctTacQXfbYqKuW
@ChuckBuilds
ChuckBuilds merged commit 59b7d18 into main Aug 23, 2026
4 checks passed
@ChuckBuilds
ChuckBuilds deleted the fix/youtube-stats-per-frame-api-storm branch August 23, 2026 16:21
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.

2 participants