Skip to content

feat(baseball): full scorebug rendering with baseball-specific elements#27

Merged
ChuckBuilds merged 6 commits intomainfrom
feat/baseball-scorebug-rendering
Feb 14, 2026
Merged

feat(baseball): full scorebug rendering with baseball-specific elements#27
ChuckBuilds merged 6 commits intomainfrom
feat/baseball-scorebug-rendering

Conversation

@ChuckBuilds
Copy link
Copy Markdown
Owner

@ChuckBuilds ChuckBuilds commented Feb 14, 2026

Summary

  • Consolidate v2.5 baseball scorebug rendering into manager.py with dedicated display methods for live, recent, and upcoming games
  • Live games now render inning indicator (▲/▼), base diamonds (filled when occupied), outs circles, balls-strikes count, and team:score layout at bottom corners
  • Standardize data extraction (_extract_game_info()) to flat dict format matching football, basketball, hockey, and soccer plugins — includes baseball-specific fields from ESPN situation data (inning, inning_half, balls, strikes, outs, bases_occupied)
  • Update scroll mode game_renderer.py with matching baseball elements for scroll display cards
  • Remove unused scorebug_renderer.py (was never imported — dead code)
  • Version bump to 1.1.0

Test plan

  • Run plugin with MLB league enabled and verify live game renders with base diamonds, outs, count, and inning indicator
  • Verify recent/final games show "Final" header with centered score and records
  • Verify upcoming games show "Next Game" header with date/time and records
  • Test scroll mode renders baseball elements on game cards
  • Test with multiple leagues (MLB + NCAA) enabled simultaneously
  • Verify text-only fallback still works when logos fail to load

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Baseball Scoreboard plugin v1.3.0 (also adds v1.2.0 & v1.1.0) with richer score visuals: logos, inning/status indicators, bases, outs, balls/strikes, optional odds, and team rankings.
  • Improvements

    • Localized/timezone-aware game times, tighter logo/layout positioning, flattened live/recent/upcoming state handling, rankings caching, and more robust logo fallbacks.
  • Refactor

    • Rendering consolidated and legacy scorebug renderer removed for a streamlined pipeline.

…ements

Consolidate v2.5 baseball scorebug rendering into manager.py with dedicated
display methods for live, recent, and upcoming games. Live games now show
inning indicator (▲/▼), base diamonds, outs circles, balls-strikes count,
and team:score layout. Standardize data extraction to flat dict format
matching other sports plugins. Update scroll mode game_renderer.py with
matching baseball elements. Remove unused scorebug_renderer.py.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Feb 14, 2026

Warning

Rate limit exceeded

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

⌛ 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.

📝 Walkthrough

Walkthrough

Flattened game model and new rendering pipeline: legacy scorebug renderer removed, game rendering moved to game_renderer.py with timezone-aware rendering, logo/odds/rankings integration, and manager updated to normalize data and supply enriched game dictionaries; manifest and plugin metadata bumped.

Changes

Cohort / File(s) Summary
Plugin Index
plugins.json
Updated Baseball Scoreboard plugin metadata: last_updated and latest_version bumped to reflect new release.
Manifest & Versions
plugins/baseball-scoreboard/manifest.json
Plugin root version advanced to 1.3.0 and versions array expanded with 1.1.0–1.3.0 entries; last_updated updated.
Rendering Engine
plugins/baseball-scoreboard/game_renderer.py
New, richer image renderer: timezone-aware start times, logo overrides, logo loading/resizing changes, team rankings cache and set_rankings_cache, detailed live/recent/upcoming drawing (innings, bases, outs, balls/strikes, odds, records/rankings), helper methods, and standardized error states.
Core Orchestration & Data
plugins/baseball-scoreboard/manager.py
Large refactor: optional odds/logo/rankings managers initialized, flattened game dict fields added (is_live/is_final/is_upcoming, scores, inning, counts, bases_occupied, etc.), ESPN/MiLB extraction normalized, new rendering routing and helper methods, and _load_team_logo signature extended to accept game.
Removed Renderer
plugins/baseball-scoreboard/scorebug_renderer.py
Deleted legacy BaseballScorebugRenderer module and its rendering helpers; responsibilities moved into game_renderer.py/manager.py.
Display Routing
plugins/baseball-scoreboard/scroll_display.py
Scroll/display routing updated to use flattened booleans and now passes rankings_cache into the renderer via set_rankings_cache.

Sequence Diagram

sequenceDiagram
    participant Manager as Manager
    participant Normalizer as Data Normalization
    participant GameRenderer as GameRenderer
    participant LogoMgr as Logo Manager
    participant OddsMgr as Odds Manager
    participant Rankings as Rankings Manager
    participant Display as Display System

    rect rgba(100,150,255,0.5)
    Manager->>Normalizer: Fetch & normalize raw event (ESPN/MiLB)
    Normalizer->>Manager: Return flattened game dict (is_live/is_final/is_upcoming, inning, counts, etc.)
    Manager->>LogoMgr: Request team logos (home/away) [optional]
    LogoMgr-->>Manager: Return logo images/paths
    Manager->>OddsMgr: Request odds for game [optional]
    OddsMgr-->>Manager: Return odds data or nil
    Manager->>Rankings: Request rankings cache [optional]
    Rankings-->>Manager: Return rankings map or nil
    Manager->>GameRenderer: set_rankings_cache(rankings)
    Manager->>GameRenderer: render_game_card(game, game_type)
    GameRenderer->>GameRenderer: Compose image (logos, inning, bases, counts, scores, odds, records)
    GameRenderer-->>Manager: Return rendered Image
    Manager->>Display: Update display with final image
    end
Loading

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

Possibly related PRs

🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title clearly describes the main change: consolidating full scorebug rendering with baseball-specific elements (inning indicators, bases, outs, counts) into the manager with matching scroll mode support.
Docstring Coverage ✅ Passed Docstring coverage is 98.00% which is sufficient. The required threshold is 80.00%.
Merge Conflict Detection ✅ Passed ✅ No merge conflicts detected when merging into main

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

✨ Finishing touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/baseball-scorebug-rendering

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.

Actionable comments posted: 3

🤖 Fix all issues with AI agents
In `@plugins/baseball-scoreboard/game_renderer.py`:
- Around line 380-397: The _draw_records method currently draws records
unconditionally; update it to respect the league/config flags by reading
show_records and show_ranking from the passed game dict (e.g.,
game.get('league_config', {}) or game.get('show_records') and
game.get('show_ranking')). Only proceed to draw when show_records is truthy and,
if applicable, show_ranking is allowed (match the logic used in manager.py).
Keep the existing rendering logic in _draw_records but gate the early return
with these config checks so scroll mode obeys the user's settings.

In `@plugins/baseball-scoreboard/manager.py`:
- Around line 566-576: The current logic in manager.py that handles
status_detail/status_short sets inning_half='top' and increments inning when
'end' is present, causing "End 5th" to render as top of 6th; change the handling
in the block that reads status_detail/status_short and status.get('period') so
that when 'end' is detected you do NOT increment the period (keep inning =
status.get('period', 1)) and set a distinct inning_half value (e.g., 'end' or
'mid-end') instead of mapping it to 'top'; update downstream rendering code that
consumes inning_half to recognize the new 'end' value (or render an "E5"/"M5"
marker) rather than showing a top-arrow for finished innings.
- Around line 584-593: The current guard "if balls == 0 and strikes == 0 and
situation:" treats a valid 0-0 count as "no data" and triggers the fallback;
instead check whether the original count structure was absent/empty before
falling back to parsing situation. Change the condition to test the
presence/population of the count source (e.g., "if not count and situation:" or
"if count is None or not count and situation:") and only then attempt to parse
situation['summary'] or use situation.get('balls')/get('strikes'); keep the
existing try/except around map(int, ...) and the existing fallback assignments
to balls and strikes.
🧹 Nitpick comments (3)
plugins/baseball-scoreboard/manager.py (1)

884-1031: Substantial rendering logic duplication with game_renderer.py.

The live/recent/upcoming rendering methods here (_display_live_game, _display_recent_game, _display_upcoming_game) duplicate nearly all of the layout, geometry, font, and drawing logic from game_renderer.py's _render_live_game, _render_recent_game, _render_upcoming_game. This includes the bases diamond geometry, outs circles, count display, score corners, records, and text-with-outline drawing.

Consider having manager.py delegate to GameRenderer for image generation, then simply display the returned image:

def _display_live_game(self, game: Dict):
    renderer = GameRenderer(matrix_width, matrix_height, self.config, ...)
    img = renderer.render_game_card(game, 'live')
    self.display_manager.image = img.copy()
    self.display_manager.update_display()

This would eliminate ~300 lines of duplicated rendering code, ensuring any visual fix or change only needs to happen in one place.

plugins/baseball-scoreboard/game_renderer.py (2)

349-358: Move datetime and pytz imports to module level.

Importing inside the method body on every call is non-idiomatic and incurs repeated import overhead. datetime is always available, and pytz is already a dependency of the plugin (used in manager.py).

Proposed fix

Add at the top of the file alongside other imports:

from datetime import datetime
import pytz

Then simplify the exception handling:

             if start_time:
                 try:
-                    from datetime import datetime
-                    import pytz
                     dt = datetime.fromisoformat(start_time.replace('Z', '+00:00'))
                     local_tz = pytz.timezone(self.config.get('timezone', 'US/Eastern'))
                     dt_local = dt.astimezone(local_tz)
                     game_date = dt_local.strftime('%b %d')
                     game_time = dt_local.strftime('%-I:%M %p')
-                except (ValueError, AttributeError, ImportError):
+                except (ValueError, AttributeError):
                     game_time = start_time[:10] if len(start_time) > 10 else start_time

75-84: _get_logo_path hardcodes logo directories, ignoring per-league logo_dir config.

manager.py respects league_config.get('logo_dir', ...) for MiLB and NCAA, but this renderer uses hardcoded paths. If a user configures a custom logo_dir, scroll-mode logos will still load from the default paths.

Consider reading from the game's league_config or the renderer's config:

Proposed fix
     def _get_logo_path(self, league: str, team_abbrev: str) -> Path:
         """Get the logo path for a team based on league."""
-        if league == 'mlb':
-            return Path("assets/sports/mlb_logos") / f"{team_abbrev}.png"
-        elif league == 'milb':
-            return Path("assets/sports/milb_logos") / f"{team_abbrev}.png"
-        elif league == 'ncaa_baseball':
-            return Path("assets/sports/ncaa_logos") / f"{team_abbrev}.png"
-        else:
-            return Path("assets/sports/mlb_logos") / f"{team_abbrev}.png"
+        logo_dirs = {
+            'mlb': 'assets/sports/mlb_logos',
+            'milb': self.config.get('milb_logo_dir', 'assets/sports/milb_logos'),
+            'ncaa_baseball': self.config.get('ncaa_baseball_logo_dir', 'assets/sports/ncaa_logos'),
+        }
+        logo_dir = logo_dirs.get(league, 'assets/sports/mlb_logos')
+        return Path(logo_dir) / f"{team_abbrev}.png"

Chuck and others added 2 commits February 13, 2026 21:20
- Fix 'end of inning' rendering: don't increment period when 'end' is
  detected in status text, use distinct 'end'/'mid' inning_half values
  and render as E5/M5 markers instead of misleading ▲ arrows
- Fix count fallback: check whether count dict is present/populated
  rather than testing for 0-0 values, which treated valid 0-0 counts
  as missing data
- Fix game_renderer.py _draw_records: gate on show_records/show_ranking
  config flags from league_config to match manager.py behavior, so
  scroll mode respects user settings

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Wire up BaseballOddsManager in manager.py: import, initialize, and
  call fetch_odds/render_odds in all three display methods (live,
  recent, upcoming) when show_odds is enabled
- Add _draw_dynamic_odds() to game_renderer.py for scroll mode cards,
  matching the pattern used by football/basketball plugins
- Fix game_renderer.py _get_logo_path to read logo_dir from
  league_config instead of hardcoding paths, so custom logo directories
  (MiLB, NCAA) are respected in scroll mode
- Move datetime/pytz imports to module level in game_renderer.py

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
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.

Actionable comments posted: 4

🤖 Fix all issues with AI agents
In `@plugins.json`:
- Line 299: The registry entry's "last_updated" value is out of sync: update the
"last_updated" key in the plugins.json entry for the baseball-scoreboard plugin
from "2026-02-13" to "2026-02-14" so it matches the plugin manifest's
last_updated value; ensure the registry's latest_version and last_updated fields
are kept in sync with the plugin manifest going forward.

In `@plugins/baseball-scoreboard/game_renderer.py`:
- Around line 428-483: _draw_dynamic_odds places both spread and O/U at y=0
which overlaps the status/inning row; change the drawing y to a non-conflicting
row by introducing a single vertical offset (e.g., odds_y) and use it for both
calls to _draw_text_with_outline instead of hardcoded 0; update
_draw_dynamic_odds (use symbols: _draw_dynamic_odds, _draw_text_with_outline,
favored_spread, over_under, self.display_width, self.fonts['detail']) to compute
odds_y (for example odds_y = 2 or derive from a status_row constant) and pass
(spread_x, odds_y) and (ou_x, odds_y) so spread and O/U render on their own row
and no longer collide with the status/inning text.
- Around line 440-445: The code in game_renderer (around the spread handling:
variables top_level_spread, home_spread, away_spread) treats home_spread == 0.0
as "missing" which overwrites a valid pick'em line; change the condition so only
a missing value (None) triggers assignment from top_level_spread (i.e., replace
the check "home_spread is None or home_spread == 0.0" with just "home_spread is
None"), leaving the away_spread logic (setting away_spread = -top_level_spread
when away_spread is None) unchanged.

In `@plugins/baseball-scoreboard/manager.py`:
- Around line 901-915: The _fetch_and_render_odds method currently performs
blocking network I/O via self._odds_manager.fetch_odds during rendering; move
all odds retrieval into the update() cycle and make _fetch_and_render_odds only
render already-populated odds data. Specifically, remove the fetch call from
_fetch_and_render_odds (leave only lookup and call to
self._odds_manager.render_odds using game.get('odds')), and in update() (where
games are fetched) iterate games with show_odds true (as determined by
league_config/get('show_odds', ...)) and call self._odds_manager.fetch_odds for
each game so odds are populated/cached before display; ensure you still respect
BaseOddsManager cache and timeout behavior and avoid adding blocking fetch calls
in _display_live_game, _display_recent_game, or _display_upcoming_game paths.
🧹 Nitpick comments (4)
plugins/baseball-scoreboard/game_renderer.py (2)

77-77: Use explicit Optional[Dict] for nullable parameters.

Optional is already imported; the type hints should match PEP 484.

Proposed fix
-    def _get_logo_path(self, league: str, team_abbrev: str, game: Dict = None) -> Path:
+    def _get_logo_path(self, league: str, team_abbrev: str, game: Optional[Dict] = None) -> Path:
-    def _load_and_resize_logo(self, league: str, team_abbrev: str, game: Dict = None) -> Optional[Image.Image]:
+    def _load_and_resize_logo(self, league: str, team_abbrev: str, game: Optional[Dict] = None) -> Optional[Image.Image]:

Also applies to: 94-94


485-486: Use self.logger.exception instead of self.logger.error to capture the traceback.

Per the static analysis hint (TRY400), logging.exception automatically includes the traceback, which is more useful for debugging rendering failures.

Proposed fix
         except Exception as e:
-            self.logger.error(f"Error drawing odds: {e}")
+            self.logger.exception(f"Error drawing odds: {e}")
plugins/baseball-scoreboard/manager.py (2)

917-1070: Substantial rendering duplication between manager.py and game_renderer.py.

The live game rendering logic (bases diamond, outs circles, count, inning indicator, scores) is implemented nearly identically here and in game_renderer.py's _render_live_game. The same applies to _render_recent_game and _render_upcoming_game. If a rendering bug is found or the layout is adjusted, both files need synchronized changes.

Consider extracting the shared rendering primitives (bases drawing, outs circles, count rendering, score layout) into a common module that both manager.py and game_renderer.py can import. This would be a meaningful maintainability improvement for future updates.


721-739: Mixing status_state string checks with is_* boolean flags for the same filter logic.

Lines 724, 727, 734 filter on status_state == 'in'/'post'/'pre', while lines 729, 736 count using g.get('is_final') / g.get('is_upcoming'). Both are derived from the same source so it's not a bug, but using the boolean flags consistently would be clearer:

Proposed fix
-            if mode == 'baseball_live' and status_state == 'in':
+            if mode == 'baseball_live' and game.get('is_live'):
                 filtered.append(game)

-            elif mode == 'baseball_recent' and status_state == 'post':
+            elif mode == 'baseball_recent' and game.get('is_final'):
                 recent_limit = league_config.get('recent_games_to_show', 5)
                 recent_count = len([g for g in filtered if g.get('league') == league_key and g.get('is_final')])
                 if recent_count >= recent_limit:
                     continue
                 filtered.append(game)

-            elif mode == 'baseball_upcoming' and status_state == 'pre':
+            elif mode == 'baseball_upcoming' and game.get('is_upcoming'):
                 upcoming_limit = league_config.get('upcoming_games_to_show', 10)
                 upcoming_count = len([g for g in filtered if g.get('league') == league_key and g.get('is_upcoming')])
                 if upcoming_count >= upcoming_limit:
                     continue
                 filtered.append(game)

- Wire up BaseballLogoManager for auto-download of missing logos via
  ESPN API, with fallback to inline logo loading when unavailable
- Wire up BaseballRankingsManager to fetch real team rankings (AP Top 25
  etc.) from ESPN standings API, cached for 1 hour
- Update _draw_records in both manager.py and game_renderer.py to show
  "#rank" (e.g., "#5") when show_ranking is enabled, matching the
  football/basketball pattern
- Add _get_team_display_text helper for consistent ranking/record
  display logic across switch and scroll modes
- Pass rankings cache through scroll_display.py to GameRenderer via
  set_rankings_cache() for scroll mode support
- Version bump to 1.3.0

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
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.

Actionable comments posted: 3

Caution

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

⚠️ Outside diff range comments (1)
plugins/baseball-scoreboard/game_renderer.py (1)

97-127: ⚠️ Potential issue | 🟡 Minor

Logo cache key ignores league_config.logo_dir, risking stale/wrong logos.

cache_key is f"{league}_{team_abbrev}" (line 99), but _get_logo_path can resolve to different directories depending on game['league_config']['logo_dir']. If two games for the same league/team have different logo_dir overrides (e.g., a custom config vs. default), the first-cached logo wins regardless.

Consider incorporating the resolved logo_dir into the cache key:

Proposed fix
     def _load_and_resize_logo(self, league: str, team_abbrev: str, game: Dict = None) -> Optional[Image.Image]:
         """Load and resize a team logo, with caching."""
-        cache_key = f"{league}_{team_abbrev}"
+        logo_dir = ""
+        if game and game.get('league_config'):
+            logo_dir = game['league_config'].get('logo_dir', '')
+        cache_key = f"{league}_{team_abbrev}_{logo_dir}"
         if cache_key in self._logo_cache:
             return self._logo_cache[cache_key]
🤖 Fix all issues with AI agents
In `@plugins/baseball-scoreboard/manager.py`:
- Around line 416-436: The _fetch_all_rankings method mutates
self._team_rankings_cache without synchronization while readers like
_get_team_display_text and _draw_records may access it concurrently; protect
this write by either acquiring the existing _games_lock around the update (wrap
the call to self._team_rankings_cache.update(...) in a with self._games_lock:
block) or perform an atomic swap: build a new dict locally (e.g., new_cache),
populate it from self._rankings_manager.fetch_rankings(...), then under
self._games_lock replace self._team_rankings_cache = new_cache; update the
_fetch_all_rankings and any callers (e.g., update) accordingly to use the
locked/atomic-swap approach.
- Around line 889-895: The Image.open(found_path) call in the logo fallback path
leaves the source file open; change it to use a context manager (with
Image.open(found_path) as src:) and inside the block call src.convert('RGBA') to
produce the logo image, then proceed with logo.thumbnail(...) and further
processing so the original file handle is closed immediately; update the code
around the logo creation in the same function that contains the found_path check
to mirror the pattern used in game_renderer.py.
- Around line 892-895: The code uses Image.Resampling.LANCZOS directly in the
logo loading path (logo.thumbnail call) which raises AttributeError on Pillow <
9.1; add a compatibility shim near the top of the file (after imports) to set a
module-level RESAMPLE_FILTER by trying Image.Resampling.LANCZOS and falling back
to Image.LANCZOS on AttributeError, then replace Image.Resampling.LANCZOS in the
logo.thumbnail call with RESAMPLE_FILTER; apply the same shim and replacement in
logo_manager.py where the same pattern occurs.
🧹 Nitpick comments (4)
plugins/baseball-scoreboard/game_renderer.py (3)

162-297: Large amount of duplicated rendering logic between game_renderer.py and manager.py.

The live game rendering in this file (lines 162–297) is nearly identical to _display_live_game in manager.py (lines 1011–1164) — same inning indicator logic, same bases diamond geometry, same outs circles, same score layout. The same duplication applies to recent and upcoming renderers. This means any bug fix or layout tweak must be applied in two places.

Consider extracting the shared rendering logic (e.g., bases drawing, inning indicator, score layout) into common helper functions that both the manager's display path and the scroll renderer can call.


80-80: Use explicit Optional type annotation per PEP 484.

Static analysis (RUF013) flags implicit Optional on lines 80 and 97. Use Optional[Dict] instead of Dict = None.

Proposed fix
-    def _get_logo_path(self, league: str, team_abbrev: str, game: Dict = None) -> Path:
+    def _get_logo_path(self, league: str, team_abbrev: str, game: Optional[Dict] = None) -> Path:
-    def _load_and_resize_logo(self, league: str, team_abbrev: str, game: Dict = None) -> Optional[Image.Image]:
+    def _load_and_resize_logo(self, league: str, team_abbrev: str, game: Optional[Dict] = None) -> Optional[Image.Image]:

509-511: Use self.logger.exception instead of self.logger.error in the exception handler.

Per static analysis (TRY400), logging.exception automatically includes the traceback, which is more appropriate inside an except block.

Proposed fix
         except Exception as e:
-            self.logger.error(f"Error drawing odds: {e}")
+            self.logger.exception(f"Error drawing odds: {e}")
plugins/baseball-scoreboard/manager.py (1)

1011-1164: Massive code duplication with game_renderer.py.

The entire live game rendering (inning indicator, bases diamond, outs circles, balls/strikes count, team scores) is duplicated between _display_live_game here and _render_live_game in game_renderer.py. Same for recent and upcoming renderers. This violates DRY — any layout fix or visual change must be applied in both places.

Consider having the manager delegate to the same rendering helpers (or to the GameRenderer class itself), rather than maintaining parallel implementations.

Comment on lines 892 to 895
logo = Image.open(found_path).convert('RGBA')
max_width = int(self.display_manager.matrix.width * 1.5)
max_height = int(self.display_manager.matrix.height * 1.5)
logo.thumbnail((max_width, max_height), Image.Resampling.LANCZOS)
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🌐 Web query:

Pillow Image.Resampling.LANCZOS minimum version requirement

💡 Result:

Image.Resampling.LANCZOS requires Pillow >= 9.1.0 (released 2022-04-01), which introduced the Image.Resampling enum and mapped legacy constants like Image.LANCZOS / Image.ANTIALIAS to Image.Resampling.LANCZOS. [1]

Source: Pillow 9.1.0 release notes (Constants deprecations table). [1]

References
[1] Pillow 9.1.0 release notes (2022-04-01), “Constants” deprecations: Image.LANCZOSImage.Resampling.LANCZOS and Image.ANTIALIASImage.Resampling.LANCZOS.


🏁 Script executed:

# Find project dependency files to check Pillow version requirement
find . -type f \( -name "requirements*.txt" -o -name "setup.py" -o -name "pyproject.toml" -o -name "setup.cfg" -o -name "Pipfile" \) | head -20

Repository: ChuckBuilds/ledmatrix-plugins

Length of output: 919


🏁 Script executed:

# Inspect game_renderer.py lines 16-21 for the compatibility shim
fd "game_renderer.py" -x cat -n {} | head -50

Repository: ChuckBuilds/ledmatrix-plugins

Length of output: 1952


🏁 Script executed:

# Inspect manager.py around lines 892-895
fd "manager.py" plugins/baseball-scoreboard/ -x wc -l {} && fd "manager.py" plugins/baseball-scoreboard/ -x sed -n '885,905p' {}

Repository: ChuckBuilds/ledmatrix-plugins

Length of output: 1119


🏁 Script executed:

# Check for Image.Resampling usage across codebase
rg "Image\.Resampling" --type py

Repository: ChuckBuilds/ledmatrix-plugins

Length of output: 7104


🏁 Script executed:

# Check the baseball-scoreboard requirements to see Pillow version
cat plugins/baseball-scoreboard/requirements.txt

Repository: ChuckBuilds/ledmatrix-plugins

Length of output: 403


🏁 Script executed:

# Check if logo_manager.py has the compatibility shim
fd "logo_manager.py" plugins/baseball-scoreboard/ -x head -30 {}

Repository: ChuckBuilds/ledmatrix-plugins

Length of output: 805


🏁 Script executed:

# Verify if manager.py has a compatibility shim at the top
fd "manager.py" plugins/baseball-scoreboard/ -x head -100 {}

Repository: ChuckBuilds/ledmatrix-plugins

Length of output: 17449


Image.Resampling.LANCZOS will raise AttributeError on Pillow < 9.1.

The project requires Pillow >= 8.0.0, which does not have Image.Resampling.LANCZOS (added in 9.1.0). Line 895 uses it directly without the compatibility fallback that game_renderer.py already implements. This will crash the logo-loading path on Pillow 8.x.

Proposed fix

Add a compatibility shim near the top of the file (after imports):

try:
    RESAMPLE_FILTER = Image.Resampling.LANCZOS
except AttributeError:
    RESAMPLE_FILTER = Image.LANCZOS

Then on line 895:

-            logo.thumbnail((max_width, max_height), Image.Resampling.LANCZOS)
+            logo.thumbnail((max_width, max_height), RESAMPLE_FILTER)

Note: logo_manager.py has the same issue in two places and needs the same fix.

🤖 Prompt for AI Agents
In `@plugins/baseball-scoreboard/manager.py` around lines 892 - 895, The code uses
Image.Resampling.LANCZOS directly in the logo loading path (logo.thumbnail call)
which raises AttributeError on Pillow < 9.1; add a compatibility shim near the
top of the file (after imports) to set a module-level RESAMPLE_FILTER by trying
Image.Resampling.LANCZOS and falling back to Image.LANCZOS on AttributeError,
then replace Image.Resampling.LANCZOS in the logo.thumbnail call with
RESAMPLE_FILTER; apply the same shim and replacement in logo_manager.py where
the same pattern occurs.

Chuck and others added 2 commits February 13, 2026 22:17
…odds overlap

Move blocking odds fetch from render path (_fetch_and_render_odds) to
update() cycle so network I/O doesn't occur during display rendering.
Fix pick'em line bug where home_spread == 0.0 was treated as missing
data in both game_renderer.py and odds_manager.py. Fix odds y-position
in game_renderer.py to render below the status row instead of at y=0.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…compat

Use atomic swap under _games_lock for _team_rankings_cache so display
threads always see a consistent snapshot. Close Image.open file handles
in logo fallback path and logo_manager.py by using context managers.
Add RESAMPLE_FILTER compatibility shim for Pillow < 9.1 in both
manager.py and logo_manager.py.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
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