cache enhancements

This commit is contained in:
Chuck
2025-07-21 18:20:01 -05:00
parent e611715d54
commit 8d4736c91b
6 changed files with 365 additions and 9 deletions

View File

@@ -101,6 +101,38 @@ You can test the odds ticker functionality using:
python test_odds_ticker.py 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 ### Financial Information
- Near real-time stock & crypto price updates - Near real-time stock & crypto price updates
- Stock news headlines - Stock news headlines

34
setup_cache.sh Normal file
View File

@@ -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."

View File

@@ -58,12 +58,32 @@ class CacheManager:
except Exception as e: except Exception as e:
self.logger.warning(f"Could not use user-specific cache directory: {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: 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) os.makedirs(system_cache_dir, exist_ok=True)
if os.access(system_cache_dir, os.W_OK): if os.access(system_cache_dir, os.W_OK):
return system_cache_dir 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: except Exception as e:
self.logger.warning(f"Could not use system-wide temporary cache directory: {e}") self.logger.warning(f"Could not use system-wide temporary cache directory: {e}")
@@ -295,3 +315,43 @@ class CacheManager:
'timestamp': time.time() 'timestamp': time.time()
} }
return self.save_cache(data_type, cache_data) 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

View File

@@ -220,10 +220,14 @@ class BaseNCAAFBManager: # Renamed class
# Track games found for each favorite team # Track games found for each favorite team
favorite_team_games = {team: [] for team in self.favorite_teams} if self.favorite_teams else {} 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}" 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 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: if cached_ranges:
past_days_needed = cached_ranges.get('past_days', 0) past_days_needed = cached_ranges.get('past_days', 0)
future_days_needed = cached_ranges.get('future_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) days_to_check = max(past_days_needed, future_days_needed)
max_days_to_check = 365 # Limit to 1 year to prevent infinite loops 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 while (len(past_events) < actual_past_games or
(need_future_games and self.favorite_teams and (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: not all(len(games) >= actual_future_games for games in favorite_team_games.values()))) and days_to_check <= max_days_to_check:
@@ -345,6 +363,29 @@ class BaseNCAAFBManager: # Renamed class
BaseNCAAFBManager.cache_manager.set(range_cache_key, range_data) 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") 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 # Take the specified number of games
selected_past_events = past_events[-actual_past_games:] if past_events else [] selected_past_events = past_events[-actual_past_games:] if past_events else []

View File

@@ -220,10 +220,14 @@ class BaseNFLManager: # Renamed class
# Track games found for each favorite team # Track games found for each favorite team
favorite_team_games = {team: [] for team in self.favorite_teams} if self.favorite_teams else {} 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}" 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 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: if cached_ranges:
past_days_needed = cached_ranges.get('past_days', 0) past_days_needed = cached_ranges.get('past_days', 0)
future_days_needed = cached_ranges.get('future_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) days_to_check = max(past_days_needed, future_days_needed)
max_days_to_check = 365 # Limit to 1 year to prevent infinite loops 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 while (len(past_events) < actual_past_games or
(need_future_games and self.favorite_teams and (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: not all(len(games) >= actual_future_games for games in favorite_team_games.values()))) and days_to_check <= max_days_to_check:
@@ -345,6 +363,29 @@ class BaseNFLManager: # Renamed class
BaseNFLManager.cache_manager.set(range_cache_key, range_data) 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") 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 # Take the specified number of games
selected_past_events = past_events[-actual_past_games:] if past_events else [] selected_past_events = past_events[-actual_past_games:] if past_events else []

View File

@@ -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"OddsTickerManager initialized with enabled leagues: {self.enabled_leagues}")
logger.info(f"Show favorite teams only: {self.show_favorite_teams_only}") logger.info(f"Show favorite teams only: {self.show_favorite_teams_only}")
@@ -103,6 +237,13 @@ class OddsTickerManager:
'large': ImageFont.load_default() '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: def _fetch_team_record(self, team_abbr: str, league: str) -> str:
"""Fetch team record from ESPN API.""" """Fetch team record from ESPN API."""
# This is a simplified implementation; a more robust solution would cache team data # This is a simplified implementation; a more robust solution would cache team data
@@ -285,10 +426,17 @@ class OddsTickerManager:
has_odds = True has_odds = True
if odds_data.get('over_under') is not None: if odds_data.get('over_under') is not None:
has_odds = True 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 = { game = {
'id': game_id, 'id': game_id,
'home_team': home_abbr, 'home_team': home_team_name,
'away_team': away_abbr, '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, 'start_time': game_time,
'home_record': home_record, 'home_record': home_record,
'away_record': away_record, 'away_record': away_record,
@@ -399,8 +547,8 @@ class OddsTickerManager:
datetime_font = self.fonts['medium'] # Use large font for date/time datetime_font = self.fonts['medium'] # Use large font for date/time
# Get team logos # Get team logos
home_logo = self._get_team_logo(game['home_team'], game['logo_dir']) home_logo = self._get_team_logo(game['home_abbr'], game['logo_dir'])
away_logo = self._get_team_logo(game['away_team'], game['logo_dir']) away_logo = self._get_team_logo(game['away_abbr'], game['logo_dir'])
if home_logo: if home_logo:
home_logo = home_logo.resize((logo_size, logo_size), Image.Resampling.LANCZOS) home_logo = home_logo.resize((logo_size, logo_size), Image.Resampling.LANCZOS)