diff --git a/README.md b/README.md index 88ca000f..6d4af27d 100644 --- a/README.md +++ b/README.md @@ -101,6 +101,38 @@ You can test the odds ticker functionality using: python test_odds_ticker.py ``` +### Persistent Caching Setup + +The LEDMatrix system uses persistent caching to improve performance and reduce API calls. When running with `sudo`, the system needs a persistent cache directory that survives restarts. + +**Automatic Setup:** +Run the setup script to create a persistent cache directory: +```bash +chmod +x setup_cache.sh +./setup_cache.sh +``` + +This will: +- Create `/var/cache/ledmatrix/` directory +- Set proper ownership to your user account +- Set appropriate permissions (755) + +**Manual Setup:** +If you prefer to set up manually: +```bash +sudo mkdir -p /var/cache/ledmatrix +sudo chown $USER:$USER /var/cache/ledmatrix +sudo chmod 755 /var/cache/ledmatrix +``` + +**Cache Locations (in order of preference):** +1. `~/.ledmatrix_cache/` (user's home directory) - **Most persistent** +2. `/var/cache/ledmatrix/` (system cache directory) - **Persistent across restarts** +3. `/opt/ledmatrix/cache/` (alternative persistent location) +4. `/tmp/ledmatrix_cache/` (temporary directory) - **NOT persistent** + +**Note:** If the system falls back to `/tmp/ledmatrix_cache/`, you'll see a warning message and the cache will not persist across restarts. + ### Financial Information - Near real-time stock & crypto price updates - Stock news headlines diff --git a/setup_cache.sh b/setup_cache.sh new file mode 100644 index 00000000..3c9e6d08 --- /dev/null +++ b/setup_cache.sh @@ -0,0 +1,34 @@ +#!/bin/bash + +# LEDMatrix Cache Setup Script +# This script sets up a persistent cache directory for LEDMatrix + +echo "Setting up LEDMatrix persistent cache directory..." + +# Create the cache directory +sudo mkdir -p /var/cache/ledmatrix + +# Get the real user (not root when running with sudo) +REAL_USER=${SUDO_USER:-$USER} + +# Set ownership to the real user +sudo chown $REAL_USER:$REAL_USER /var/cache/ledmatrix + +# Set permissions +sudo chmod 755 /var/cache/ledmatrix + +echo "Cache directory created: /var/cache/ledmatrix" +echo "Ownership set to: $REAL_USER" +echo "Permissions set to: 755" + +# Test if the directory is writable +if [ -w /var/cache/ledmatrix ]; then + echo "✓ Cache directory is writable" +else + echo "✗ Cache directory is not writable" + exit 1 +fi + +echo "" +echo "Setup complete! LEDMatrix will now use persistent caching." +echo "The cache will survive system restarts." \ No newline at end of file diff --git a/src/cache_manager.py b/src/cache_manager.py index d7390a61..d60cab7b 100644 --- a/src/cache_manager.py +++ b/src/cache_manager.py @@ -58,12 +58,32 @@ class CacheManager: except Exception as e: self.logger.warning(f"Could not use user-specific cache directory: {e}") - # Attempt 2: System-wide temporary directory + # Attempt 2: System-wide persistent cache directory (for sudo scenarios) try: - system_cache_dir = os.path.join(tempfile.gettempdir(), 'ledmatrix_cache') + # Try /var/cache/ledmatrix first (most standard) + system_cache_dir = '/var/cache/ledmatrix' os.makedirs(system_cache_dir, exist_ok=True) if os.access(system_cache_dir, os.W_OK): return system_cache_dir + except Exception as e: + self.logger.warning(f"Could not use /var/cache/ledmatrix: {e}") + + # Attempt 3: /opt/ledmatrix/cache (alternative persistent location) + try: + opt_cache_dir = '/opt/ledmatrix/cache' + os.makedirs(opt_cache_dir, exist_ok=True) + if os.access(opt_cache_dir, os.W_OK): + return opt_cache_dir + except Exception as e: + self.logger.warning(f"Could not use /opt/ledmatrix/cache: {e}") + + # Attempt 4: System-wide temporary directory (fallback, not persistent) + try: + temp_cache_dir = os.path.join(tempfile.gettempdir(), 'ledmatrix_cache') + os.makedirs(temp_cache_dir, exist_ok=True) + if os.access(temp_cache_dir, os.W_OK): + self.logger.warning("Using temporary cache directory - cache will NOT persist across restarts") + return temp_cache_dir except Exception as e: self.logger.warning(f"Could not use system-wide temporary cache directory: {e}") @@ -294,4 +314,44 @@ class CacheManager: 'data': data, 'timestamp': time.time() } - return self.save_cache(data_type, cache_data) \ No newline at end of file + return self.save_cache(data_type, cache_data) + + def get(self, key: str, max_age: int = 300) -> Optional[Dict]: + """Get data from cache if it exists and is not stale.""" + return self.get_cached_data(key, max_age) + + def set(self, key: str, data: Dict) -> None: + """Store data in cache with current timestamp.""" + self.save_cache(key, data) + + def setup_persistent_cache(self) -> bool: + """ + Set up a persistent cache directory with proper permissions. + This should be run once with sudo to create the directory. + """ + try: + # Try to create /var/cache/ledmatrix with proper permissions + cache_dir = '/var/cache/ledmatrix' + os.makedirs(cache_dir, exist_ok=True) + + # Set ownership to the real user (not root) + real_user = os.environ.get('SUDO_USER') + if real_user: + import pwd + try: + uid = pwd.getpwnam(real_user).pw_uid + gid = pwd.getpwnam(real_user).pw_gid + os.chown(cache_dir, uid, gid) + self.logger.info(f"Set ownership of {cache_dir} to {real_user}") + except Exception as e: + self.logger.warning(f"Could not set ownership: {e}") + + # Set permissions to 755 (rwxr-xr-x) + os.chmod(cache_dir, 0o755) + + self.logger.info(f"Successfully set up persistent cache directory: {cache_dir}") + return True + + except Exception as e: + self.logger.error(f"Failed to set up persistent cache directory: {e}") + return False \ No newline at end of file diff --git a/src/ncaa_fb_managers.py b/src/ncaa_fb_managers.py index 42f0eb3e..dfa2b627 100644 --- a/src/ncaa_fb_managers.py +++ b/src/ncaa_fb_managers.py @@ -220,10 +220,14 @@ class BaseNCAAFBManager: # Renamed class # Track games found for each favorite team favorite_team_games = {team: [] for team in self.favorite_teams} if self.favorite_teams else {} - # Check for cached search ranges + # Check for cached search ranges and last successful date range_cache_key = f"search_ranges_ncaafb_{actual_past_games}_{actual_future_games}" cached_ranges = BaseNCAAFBManager.cache_manager.get(range_cache_key, max_age=86400) # Cache for 24 hours + # Check for last successful date cache + last_successful_cache_key = f"last_successful_date_ncaafb_{actual_past_games}_{actual_future_games}" + last_successful_date = BaseNCAAFBManager.cache_manager.get(last_successful_cache_key, max_age=86400) + if cached_ranges: past_days_needed = cached_ranges.get('past_days', 0) future_days_needed = cached_ranges.get('future_days', 0) @@ -236,6 +240,20 @@ class BaseNCAAFBManager: # Renamed class days_to_check = max(past_days_needed, future_days_needed) max_days_to_check = 365 # Limit to 1 year to prevent infinite loops + # If we have a last successful date, start from there + if last_successful_date and need_future_games: + last_successful_str = last_successful_date.get('date') + if last_successful_str: + try: + last_successful = datetime.strptime(last_successful_str, '%Y%m%d').date() + days_since_last = (today - last_successful).days + if days_since_last > 0: + BaseNCAAFBManager.logger.info(f"[NCAAFB] Starting search from last successful date: {last_successful_str} ({days_since_last} days ago)") + # Start from the day after the last successful date + days_to_check = max(days_to_check, days_since_last + 1) + except ValueError: + BaseNCAAFBManager.logger.warning(f"[NCAAFB] Invalid last successful date format: {last_successful_str}") + while (len(past_events) < actual_past_games or (need_future_games and self.favorite_teams and not all(len(games) >= actual_future_games for games in favorite_team_games.values()))) and days_to_check <= max_days_to_check: @@ -344,6 +362,29 @@ class BaseNCAAFBManager: # Renamed class } BaseNCAAFBManager.cache_manager.set(range_cache_key, range_data) BaseNCAAFBManager.logger.info(f"[NCAAFB] Cached search ranges: {actual_past_days} days past, {actual_future_days} days future") + + # Store the last successful date for future searches + if future_events: + # Find the furthest future date where we found games + furthest_future_date = None + for event in future_events: + try: + event_time = datetime.fromisoformat(event['date'].replace('Z', '+00:00')) + if event_time.tzinfo is None: + event_time = event_time.replace(tzinfo=pytz.UTC) + event_date = event_time.date() + if furthest_future_date is None or event_date > furthest_future_date: + furthest_future_date = event_date + except Exception: + continue + + if furthest_future_date: + last_successful_data = { + 'date': furthest_future_date.strftime('%Y%m%d'), + 'last_updated': current_time + } + BaseNCAAFBManager.cache_manager.set(last_successful_cache_key, last_successful_data) + BaseNCAAFBManager.logger.info(f"[NCAAFB] Cached last successful date: {furthest_future_date.strftime('%Y%m%d')}") # Take the specified number of games selected_past_events = past_events[-actual_past_games:] if past_events else [] diff --git a/src/nfl_managers.py b/src/nfl_managers.py index fd2beb7c..18320e34 100644 --- a/src/nfl_managers.py +++ b/src/nfl_managers.py @@ -220,10 +220,14 @@ class BaseNFLManager: # Renamed class # Track games found for each favorite team favorite_team_games = {team: [] for team in self.favorite_teams} if self.favorite_teams else {} - # Check for cached search ranges + # Check for cached search ranges and last successful date range_cache_key = f"search_ranges_nfl_{actual_past_games}_{actual_future_games}" cached_ranges = BaseNFLManager.cache_manager.get(range_cache_key, max_age=86400) # Cache for 24 hours + # Check for last successful date cache + last_successful_cache_key = f"last_successful_date_nfl_{actual_past_games}_{actual_future_games}" + last_successful_date = BaseNFLManager.cache_manager.get(last_successful_cache_key, max_age=86400) + if cached_ranges: past_days_needed = cached_ranges.get('past_days', 0) future_days_needed = cached_ranges.get('future_days', 0) @@ -236,6 +240,20 @@ class BaseNFLManager: # Renamed class days_to_check = max(past_days_needed, future_days_needed) max_days_to_check = 365 # Limit to 1 year to prevent infinite loops + # If we have a last successful date, start from there + if last_successful_date and need_future_games: + last_successful_str = last_successful_date.get('date') + if last_successful_str: + try: + last_successful = datetime.strptime(last_successful_str, '%Y%m%d').date() + days_since_last = (today - last_successful).days + if days_since_last > 0: + BaseNFLManager.logger.info(f"[NFL] Starting search from last successful date: {last_successful_str} ({days_since_last} days ago)") + # Start from the day after the last successful date + days_to_check = max(days_to_check, days_since_last + 1) + except ValueError: + BaseNFLManager.logger.warning(f"[NFL] Invalid last successful date format: {last_successful_str}") + while (len(past_events) < actual_past_games or (need_future_games and self.favorite_teams and not all(len(games) >= actual_future_games for games in favorite_team_games.values()))) and days_to_check <= max_days_to_check: @@ -344,6 +362,29 @@ class BaseNFLManager: # Renamed class } BaseNFLManager.cache_manager.set(range_cache_key, range_data) BaseNFLManager.logger.info(f"[NFL] Cached search ranges: {actual_past_days} days past, {actual_future_days} days future") + + # Store the last successful date for future searches + if future_events: + # Find the furthest future date where we found games + furthest_future_date = None + for event in future_events: + try: + event_time = datetime.fromisoformat(event['date'].replace('Z', '+00:00')) + if event_time.tzinfo is None: + event_time = event_time.replace(tzinfo=pytz.UTC) + event_date = event_time.date() + if furthest_future_date is None or event_date > furthest_future_date: + furthest_future_date = event_date + except Exception: + continue + + if furthest_future_date: + last_successful_data = { + 'date': furthest_future_date.strftime('%Y%m%d'), + 'last_updated': current_time + } + BaseNFLManager.cache_manager.set(last_successful_cache_key, last_successful_data) + BaseNFLManager.logger.info(f"[NFL] Cached last successful date: {furthest_future_date.strftime('%Y%m%d')}") # Take the specified number of games selected_past_events = past_events[-actual_past_games:] if past_events else [] diff --git a/src/odds_ticker_manager.py b/src/odds_ticker_manager.py index 673eea2f..4dbb43fe 100644 --- a/src/odds_ticker_manager.py +++ b/src/odds_ticker_manager.py @@ -84,6 +84,140 @@ class OddsTickerManager: } } + # Team name mappings for better display + self.team_names = { + 'nfl': { + 'TB': 'Bucs', + 'DAL': 'Cowboys', + 'NE': 'Patriots', + 'NYG': 'Giants', + 'NYJ': 'Jets', + 'BUF': 'Bills', + 'MIA': 'Dolphins', + 'BAL': 'Ravens', + 'CIN': 'Bengals', + 'CLE': 'Browns', + 'PIT': 'Steelers', + 'HOU': 'Texans', + 'IND': 'Colts', + 'JAX': 'Jaguars', + 'TEN': 'Titans', + 'DEN': 'Broncos', + 'KC': 'Chiefs', + 'LV': 'Raiders', + 'LAC': 'Chargers', + 'ARI': 'Cardinals', + 'ATL': 'Falcons', + 'CAR': 'Panthers', + 'CHI': 'Bears', + 'DET': 'Lions', + 'GB': 'Packers', + 'MIN': 'Vikings', + 'NO': 'Saints', + 'PHI': 'Eagles', + 'SEA': 'Seahawks', + 'SF': '49ers', + 'LAR': 'Rams', + 'WAS': 'Commanders' + }, + 'ncaa_fb': { + 'UGA': 'Georgia', + 'AUB': 'Auburn', + 'ALA': 'Alabama', + 'LSU': 'LSU', + 'FLA': 'Florida', + 'TEN': 'Tennessee', + 'TEX': 'Texas', + 'OKL': 'Oklahoma', + 'ORE': 'Oregon', + 'WAS': 'Washington', + 'USC': 'USC', + 'UCLA': 'UCLA', + 'MICH': 'Michigan', + 'OSU': 'Ohio State', + 'PSU': 'Penn State', + 'ND': 'Notre Dame', + 'CLEM': 'Clemson', + 'FSU': 'Florida State', + 'MIAMI': 'Miami', + 'VT': 'Virginia Tech', + 'UNC': 'North Carolina', + 'DUKE': 'Duke', + 'NCST': 'NC State', + 'WAKE': 'Wake Forest', + 'BC': 'Boston College', + 'GT': 'Georgia Tech', + 'LOU': 'Louisville', + 'PITT': 'Pittsburgh', + 'SYR': 'Syracuse', + 'CUSE': 'Syracuse' + }, + 'mlb': { + 'TB': 'Rays', + 'TEX': 'Rangers', + 'NYY': 'Yankees', + 'BOS': 'Red Sox', + 'BAL': 'Orioles', + 'TOR': 'Blue Jays', + 'CWS': 'White Sox', + 'CLE': 'Guardians', + 'DET': 'Tigers', + 'KC': 'Royals', + 'MIN': 'Twins', + 'HOU': 'Astros', + 'LAA': 'Angels', + 'OAK': 'Athletics', + 'SEA': 'Mariners', + 'ATL': 'Braves', + 'MIA': 'Marlins', + 'NYM': 'Mets', + 'PHI': 'Phillies', + 'WSH': 'Nationals', + 'CHC': 'Cubs', + 'CIN': 'Reds', + 'MIL': 'Brewers', + 'PIT': 'Pirates', + 'STL': 'Cardinals', + 'ARI': 'Diamondbacks', + 'COL': 'Rockies', + 'LAD': 'Dodgers', + 'SD': 'Padres', + 'SF': 'Giants' + }, + 'nba': { + 'DAL': 'Mavericks', + 'BOS': 'Celtics', + 'LAL': 'Lakers', + 'LAC': 'Clippers', + 'GSW': 'Warriors', + 'PHX': 'Suns', + 'SAC': 'Kings', + 'POR': 'Trail Blazers', + 'UTA': 'Jazz', + 'DEN': 'Nuggets', + 'OKC': 'Thunder', + 'HOU': 'Rockets', + 'SAS': 'Spurs', + 'MEM': 'Grizzlies', + 'NOP': 'Pelicans', + 'MIN': 'Timberwolves', + 'MIA': 'Heat', + 'ORL': 'Magic', + 'ATL': 'Hawks', + 'CHA': 'Hornets', + 'WAS': 'Wizards', + 'DET': 'Pistons', + 'CLE': 'Cavaliers', + 'IND': 'Pacers', + 'CHI': 'Bulls', + 'MIL': 'Bucks', + 'TOR': 'Raptors', + 'PHI': '76ers', + 'BKN': 'Nets', + 'NYK': 'Knicks' + } + } + logger.info(f"OddsTickerManager initialized with enabled leagues: {self.enabled_leagues}") logger.info(f"Show favorite teams only: {self.show_favorite_teams_only}") @@ -103,6 +237,13 @@ class OddsTickerManager: 'large': ImageFont.load_default() } + def _get_team_name(self, team_abbr: str, league: str) -> str: + """Convert team abbreviation to full team name for display.""" + league_key = league.lower() + if league_key in self.team_names and team_abbr in self.team_names[league_key]: + return self.team_names[league_key][team_abbr] + return team_abbr # Return original if no mapping found + def _fetch_team_record(self, team_abbr: str, league: str) -> str: """Fetch team record from ESPN API.""" # This is a simplified implementation; a more robust solution would cache team data @@ -285,10 +426,17 @@ class OddsTickerManager: has_odds = True if odds_data.get('over_under') is not None: has_odds = True + + # Convert abbreviations to full team names + home_team_name = self._get_team_name(home_abbr, league) + away_team_name = self._get_team_name(away_abbr, league) + game = { 'id': game_id, - 'home_team': home_abbr, - 'away_team': away_abbr, + 'home_team': home_team_name, + 'away_team': away_team_name, + 'home_abbr': home_abbr, # Keep original for logo loading + 'away_abbr': away_abbr, # Keep original for logo loading 'start_time': game_time, 'home_record': home_record, 'away_record': away_record, @@ -399,8 +547,8 @@ class OddsTickerManager: datetime_font = self.fonts['medium'] # Use large font for date/time # Get team logos - home_logo = self._get_team_logo(game['home_team'], game['logo_dir']) - away_logo = self._get_team_logo(game['away_team'], game['logo_dir']) + home_logo = self._get_team_logo(game['home_abbr'], game['logo_dir']) + away_logo = self._get_team_logo(game['away_abbr'], game['logo_dir']) if home_logo: home_logo = home_logo.resize((logo_size, logo_size), Image.Resampling.LANCZOS)