Skip to content

Conversation

@ChuckBuilds
Copy link
Owner

@ChuckBuilds ChuckBuilds commented Jan 19, 2026

Summary

  • Fixes NCAA basketball logo resolution issue where logos weren't being found
  • Basketball plugin uses sport_key="ncaam" and "ncaaw", but LogoDownloader only had "ncaam_basketball" mapped
  • This caused get_logo_directory() to fall back to assets/sports/ncaam_logos (non-existent) instead of assets/sports/ncaa_logos

Changes

Added aliases to both LOGO_DIRECTORIES and API_ENDPOINTS in src/logo_downloader.py:

  • ncaam -> assets/sports/ncaa_logos
  • ncaaw -> assets/sports/ncaa_logos
  • ncaaw_basketball -> assets/sports/ncaa_logos (for consistency)

Test plan

  • Run basketball scoreboard plugin with NCAA games
  • Verify logs show correct logo directory: assets/sports/ncaa_logos
  • Confirm NCAA team logos display correctly
  • Verify football scoreboard still works correctly

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features
    • Added support for NCAA basketball leagues, enabling logo downloads for men's college basketball and women's college basketball teams.

✏️ Tip: You can customize this high-level summary in your review settings.

@coderabbitai
Copy link

coderabbitai bot commented Jan 19, 2026

📝 Walkthrough

Walkthrough

Added NCAA basketball API endpoints for men's and women's college basketball (ncaam, ncaaw_basketball, ncaaw) with corresponding ESPN API URLs and expanded logo directory mappings to support these new NCAA basketball league keys without modifying existing logic.

Changes

Cohort / File(s) Summary
NCAA Basketball Support
src/logo_downloader.py
Added 3 new NCAA basketball API endpoint keys (ncaam, ncaaw_basketball, ncaaw) pointing to ESPN's men's and women's college basketball endpoints; added 4 corresponding directory mappings to LOGO_DIRECTORIES for NCAA logo asset resolution

Estimated code review effort

🎯 1 (Trivial) | ⏱️ ~3 minutes

Poem

🐰 Brackets and basketballs now find their way,
NCAA logos bright, in assets they stay,
Endpoints expanded, from men to women so keen,
Six little lines make the dream complete, I've seen! 🏀

🚥 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 accurately describes the main change: adding NCAA basketball sport key aliases (ncaam/ncaaw) to fix logo resolution in the basketball plugin.
Docstring Coverage ✅ Passed Docstring coverage is 96.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 docstrings

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

@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 `@docs/MULTI_ROOT_WORKSPACE_SETUP.md`:
- Around line 56-59: Replace the reference to the removed script
clone_plugin_repos.py with clear guidance: remove the call to python3
scripts/clone_plugin_repos.py and instead instruct readers to manually clone
required plugin repositories into the parent directory of LEDMatrix (or provide
their preferred clone commands), then run the existing setup script python3
scripts/setup_plugin_repos.py (refer to setup_plugin_repos.py) under the
LEDMatrix directory; update the section header (e.g., "Initial Setup") and the
example to show cd /home/chuck/Github/LEDMatrix followed by running
scripts/setup_plugin_repos.py so readers have a concrete, working alternative.

In `@scripts/setup_plugin_repos.py`:
- Around line 87-89: The except block that prints the error for plugin['name']
then calls link_path.unlink() can itself raise and abort processing; modify the
error-handling so unlink is performed safely—either call
link_path.unlink(missing_ok=True) if supported or wrap link_path.unlink() in its
own try/except that catches and logs any unlink-specific exception (referencing
link_path.unlink() and the surrounding except handling for plugin['name']) so
failures to remove the link do not propagate and stop processing other plugins.

In `@src/cache_manager.py`:
- Around line 587-593: The cleanup_disk_cache method currently declares a force:
bool parameter but never uses it; update cleanup_disk_cache to honor force by
adding a time-based skip that returns early when recent cleanup ran (e.g.,
consult self._last_cleanup_time or similar) unless force is True, and update the
docstring accordingly; if you prefer to remove the behavior instead, remove the
force parameter from the method signature and all callers (and adjust the
docstring) to eliminate the unused parameter. Ensure the change targets the
cleanup_disk_cache method and any references to self._last_cleanup_time or
callers that expect the force argument.
🧹 Nitpick comments (10)
docs/MULTI_ROOT_WORKSPACE_SETUP.md (2)

16-29: Add language specifier for markdown lint compliance.

The code block is missing a language identifier.

Suggested fix
-```
+```text
 /home/chuck/Github/

17-18: Consider using generic paths for broader applicability.

The documentation uses hardcoded paths like /home/chuck/Github/ throughout. Consider using a placeholder like ~/Github/ or <your-github-directory>/ to make the documentation applicable to all developers.

Also applies to: 35-35, 57-57, 64-65, 78-79, 114-114, 123-123, 130-131, 149-150, 159-159, 167-167, 174-174

scripts/setup_plugin_repos.py (4)

61-61: Remove extraneous f-string prefixes.

These strings have no placeholders, so the f prefix is unnecessary. Based on Ruff F541.

Suggested fix
-    print(f"Setting up plugin repository links...")
+    print("Setting up plugin repository links...")
-                print(f"      Skipping (manual cleanup required)")
+                print("      Skipping (manual cleanup required)")

Also applies to: 93-93


90-95: Non-symlink conflict counted as "skipped" rather than "error".

When an existing directory/file blocks symlink creation, the script increments skipped but the message indicates "manual cleanup required." This is arguably an error condition that should increment errors instead, as the plugin won't be accessible.

Suggested fix
             else:
                 # It's a directory/file, not a symlink
                 print(f"  ⚠️  {plugin['name']} - {link_path.name} exists but is not a symlink")
-                print(f"      Skipping (manual cleanup required)")
-                skipped += 1
+                print("      Skipping (manual cleanup required)")
+                errors += 1
                 continue

104-106: Consider catching specific exceptions.

Catching OSError (which includes PermissionError, FileExistsError) would be more precise than bare Exception. As per coding guidelines, catch specific exceptions, not bare except: statements.

Suggested fix
-        except Exception as e:
+        except OSError as e:
             print(f"  ✗  {plugin['name']} - failed to create link: {e}")
             errors += 1

114-118: Placeholder function with no implementation.

update_config_path() is documented as an alternative approach but has no implementation. Consider removing it or adding a TODO comment to track if it should be implemented.

src/cache/disk_cache.py (2)

301-340: Consider reducing lock contention for large cache directories.

The lock is held for the entire directory scan and file deletion loop. On a Raspberry Pi with many cache files, this could block other cache operations (reads/writes) for an extended period.

Consider collecting the list of files to delete while holding the lock, then releasing it before performing deletions, or processing in batches.

♻️ Suggested approach
         try:
-            with self._lock:
-                for filename in os.listdir(self.cache_dir):
-                    if not filename.endswith('.json'):
-                        continue
-                    
-                    stats['files_scanned'] += 1
-                    file_path = os.path.join(self.cache_dir, filename)
-                    
-                    try:
-                        # Get file age
-                        file_mtime = os.path.getmtime(file_path)
-                        file_age_days = (current_time - file_mtime) / 86400  # Convert to days
+            # Collect files to process under lock
+            with self._lock:
+                files_to_check = [f for f in os.listdir(self.cache_dir) if f.endswith('.json')]
+            
+            for filename in files_to_check:
+                stats['files_scanned'] += 1
+                file_path = os.path.join(self.cache_dir, filename)
+                
+                try:
+                    # Get file age
+                    file_mtime = os.path.getmtime(file_path)
+                    file_age_days = (current_time - file_mtime) / 86400  # Convert to days

273-273: Consider validating cache_strategy parameter.

The cache_strategy parameter is typed as Any and used without validation. If passed incorrectly (e.g., None), line 319 will raise AttributeError. Adding a guard or typing it more specifically would improve robustness.

-    def cleanup_expired_files(self, cache_strategy: Any, retention_policies: Dict[str, int]) -> Dict[str, Any]:
+    def cleanup_expired_files(self, cache_strategy: 'CacheStrategy', retention_policies: Dict[str, int]) -> Dict[str, Any]:

Or add a defensive check:

if cache_strategy is None or not hasattr(cache_strategy, 'get_data_type_from_key'):
    self.logger.warning("Invalid cache_strategy provided for cleanup")
    return {'files_scanned': 0, 'files_deleted': 0, 'space_freed_bytes': 0, 'errors': 0}
src/cache/cache_metrics.py (1)

77-91: Move import time to module level.

The import time on line 86 is inside the method. While this works, it's non-idiomatic Python and incurs a small overhead on each call. Move it to the top of the file with other imports.

♻️ Suggested fix

Add at the top of the file:

import time

Then remove the import from the method:

     def record_disk_cleanup(self, files_cleaned: int, space_freed_mb: float, duration_sec: float) -> None:
         """
         Record disk cleanup operation results.
         
         Args:
             files_cleaned: Number of files deleted
             space_freed_mb: Space freed in megabytes
             duration_sec: Duration of cleanup operation in seconds
         """
-        import time
         with self._lock:
             self._metrics['last_disk_cleanup'] = time.time()
src/cache_manager.py (1)

647-675: Consider adding graceful shutdown support for the cleanup thread.

The cleanup thread runs an infinite loop with a 24-hour sleep. While daemon=True ensures it won't block process exit, there's no way to stop the thread gracefully for testing or controlled shutdown scenarios.

Using a threading.Event would allow cleaner lifecycle management and make the code more testable.

♻️ Suggested improvement
+        self._cleanup_stop_event = threading.Event()
         
         def cleanup_loop():
             """Background loop that runs cleanup periodically."""
             self.logger.info("Disk cache cleanup thread started (interval: %d hours)", 
                            self._disk_cleanup_interval_hours)
             
-            while True:
+            while not self._cleanup_stop_event.is_set():
                 try:
                     # Sleep for the configured interval
                     sleep_seconds = self._disk_cleanup_interval_hours * 3600
-                    time.sleep(sleep_seconds)
+                    # Use wait() instead of sleep() for interruptible sleep
+                    if self._cleanup_stop_event.wait(timeout=sleep_seconds):
+                        break  # Stop event was set
                     
                     # Run cleanup
                     self.logger.debug("Running scheduled disk cache cleanup")
                     self.cleanup_disk_cache()

Then add a stop method:

def stop_cleanup_thread(self) -> None:
    """Stop the background cleanup thread."""
    if self._cleanup_stop_event:
        self._cleanup_stop_event.set()

The basketball scoreboard plugin uses sport_key="ncaam" and "ncaaw",
but LogoDownloader only had "ncaam_basketball" mapped. This caused
get_logo_directory() to fall back to "assets/sports/ncaam_logos"
(non-existent) instead of "assets/sports/ncaa_logos".

Added aliases to both LOGO_DIRECTORIES and API_ENDPOINTS:
- ncaam -> assets/sports/ncaa_logos
- ncaaw -> assets/sports/ncaa_logos
- ncaaw_basketball -> assets/sports/ncaa_logos

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
@ChuckBuilds ChuckBuilds force-pushed the fix/basketball-ncaa-logo-resolution branch from 625fd8d to 7a33767 Compare January 19, 2026 13:52
@ChuckBuilds ChuckBuilds merged commit bc23b7c into main Jan 19, 2026
1 check passed
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