update UTC timezone logic to check config settings for all managers

This commit is contained in:
ChuckBuilds
2025-09-04 22:18:01 -04:00
parent 92071237c1
commit 8a0fdb005d
21 changed files with 1037 additions and 8 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 273 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 278 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 418 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 434 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 253 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 236 B

View File

@@ -39,6 +39,7 @@
"daily_forecast": 30, "daily_forecast": 30,
"stock_news": 20, "stock_news": 20,
"odds_ticker": 60, "odds_ticker": 60,
"leaderboard": 60,
"nhl_live": 30, "nhl_live": 30,
"nhl_recent": 30, "nhl_recent": 30,
"nhl_upcoming": 30, "nhl_upcoming": 30,
@@ -152,6 +153,45 @@
"max_duration": 300, "max_duration": 300,
"duration_buffer": 0.1 "duration_buffer": 0.1
}, },
"leaderboard": {
"enabled": false,
"enabled_sports": {
"nfl": {
"enabled": false,
"top_teams": 10
},
"nba": {
"enabled": false,
"top_teams": 10
},
"mlb": {
"enabled": false,
"top_teams": 10
},
"ncaa_fb": {
"enabled": false,
"top_teams": 25
},
"nhl": {
"enabled": false,
"top_teams": 10
},
"ncaam_basketball": {
"enabled": false,
"top_teams": 25
}
},
"update_interval": 3600,
"scroll_speed": 2,
"scroll_delay": 0.05,
"display_duration": 60,
"loop": true,
"request_timeout": 30,
"dynamic_duration": true,
"min_duration": 30,
"max_duration": 300,
"duration_buffer": 0.1
},
"calendar": { "calendar": {
"enabled": false, "enabled": false,
"credentials_file": "credentials.json", "credentials_file": "credentials.json",

1
create_league_logos.py Normal file
View File

@@ -0,0 +1 @@

1
create_ncaa_logos.py Normal file
View File

@@ -0,0 +1 @@

View File

@@ -42,8 +42,7 @@ class CalendarManager:
logger.info(f"Calendar configuration: enabled={self.enabled}, update_interval={self.update_interval}, max_events={self.max_events}, calendars={self.calendars}") logger.info(f"Calendar configuration: enabled={self.enabled}, update_interval={self.update_interval}, max_events={self.max_events}, calendars={self.calendars}")
# Get timezone from config # Get timezone from config
self.config_manager = ConfigManager() timezone_str = self.config.get('timezone', 'UTC')
timezone_str = self.config_manager.get_timezone()
logger.info(f"Loading timezone from config: {timezone_str}") logger.info(f"Loading timezone from config: {timezone_str}")
try: try:
self.timezone = pytz.timezone(timezone_str) self.timezone = pytz.timezone(timezone_str)

View File

@@ -31,7 +31,7 @@ class Clock:
def _get_timezone(self) -> pytz.timezone: def _get_timezone(self) -> pytz.timezone:
"""Get timezone from the config file.""" """Get timezone from the config file."""
config_timezone = self.config_manager.get_timezone() config_timezone = self.config.get('timezone', 'UTC')
try: try:
return pytz.timezone(config_timezone) return pytz.timezone(config_timezone)
except pytz.exceptions.UnknownTimeZoneError: except pytz.exceptions.UnknownTimeZoneError:

View File

@@ -20,6 +20,7 @@ from src.cache_manager import CacheManager
from src.stock_manager import StockManager from src.stock_manager import StockManager
from src.stock_news_manager import StockNewsManager from src.stock_news_manager import StockNewsManager
from src.odds_ticker_manager import OddsTickerManager from src.odds_ticker_manager import OddsTickerManager
from src.leaderboard_manager import LeaderboardManager
from src.nhl_managers import NHLLiveManager, NHLRecentManager, NHLUpcomingManager from src.nhl_managers import NHLLiveManager, NHLRecentManager, NHLUpcomingManager
from src.nba_managers import NBALiveManager, NBARecentManager, NBAUpcomingManager from src.nba_managers import NBALiveManager, NBARecentManager, NBAUpcomingManager
from src.mlb_manager import MLBLiveManager, MLBRecentManager, MLBUpcomingManager from src.mlb_manager import MLBLiveManager, MLBRecentManager, MLBUpcomingManager
@@ -60,6 +61,7 @@ class DisplayController:
self.stocks = StockManager(self.config, self.display_manager) if self.config.get('stocks', {}).get('enabled', False) else None self.stocks = StockManager(self.config, self.display_manager) if self.config.get('stocks', {}).get('enabled', False) else None
self.news = StockNewsManager(self.config, self.display_manager) if self.config.get('stock_news', {}).get('enabled', False) else None self.news = StockNewsManager(self.config, self.display_manager) if self.config.get('stock_news', {}).get('enabled', False) else None
self.odds_ticker = OddsTickerManager(self.config, self.display_manager) if self.config.get('odds_ticker', {}).get('enabled', False) else None self.odds_ticker = OddsTickerManager(self.config, self.display_manager) if self.config.get('odds_ticker', {}).get('enabled', False) else None
self.leaderboard = LeaderboardManager(self.config, self.display_manager) if self.config.get('leaderboard', {}).get('enabled', False) else None
self.calendar = CalendarManager(self.display_manager, self.config) if self.config.get('calendar', {}).get('enabled', False) else None self.calendar = CalendarManager(self.display_manager, self.config) if self.config.get('calendar', {}).get('enabled', False) else None
self.youtube = YouTubeDisplay(self.display_manager, self.config) if self.config.get('youtube', {}).get('enabled', False) else None self.youtube = YouTubeDisplay(self.display_manager, self.config) if self.config.get('youtube', {}).get('enabled', False) else None
self.text_display = TextDisplay(self.display_manager, self.config) if self.config.get('text_display', {}).get('enabled', False) else None self.text_display = TextDisplay(self.display_manager, self.config) if self.config.get('text_display', {}).get('enabled', False) else None
@@ -258,6 +260,7 @@ class DisplayController:
if self.stocks: self.available_modes.append('stocks') if self.stocks: self.available_modes.append('stocks')
if self.news: self.available_modes.append('stock_news') if self.news: self.available_modes.append('stock_news')
if self.odds_ticker: self.available_modes.append('odds_ticker') if self.odds_ticker: self.available_modes.append('odds_ticker')
if self.leaderboard: self.available_modes.append('leaderboard')
if self.calendar: self.available_modes.append('calendar') if self.calendar: self.available_modes.append('calendar')
if self.youtube: self.available_modes.append('youtube') if self.youtube: self.available_modes.append('youtube')
if self.text_display: self.available_modes.append('text_display') if self.text_display: self.available_modes.append('text_display')
@@ -1119,6 +1122,8 @@ class DisplayController:
manager_to_display = self.news manager_to_display = self.news
elif self.current_display_mode == 'odds_ticker' and self.odds_ticker: elif self.current_display_mode == 'odds_ticker' and self.odds_ticker:
manager_to_display = self.odds_ticker manager_to_display = self.odds_ticker
elif self.current_display_mode == 'leaderboard' and self.leaderboard:
manager_to_display = self.leaderboard
elif self.current_display_mode == 'calendar' and self.calendar: elif self.current_display_mode == 'calendar' and self.calendar:
manager_to_display = self.calendar manager_to_display = self.calendar
elif self.current_display_mode == 'youtube' and self.youtube: elif self.current_display_mode == 'youtube' and self.youtube:

589
src/leaderboard_manager.py Normal file
View File

@@ -0,0 +1,589 @@
import time
import logging
import requests
import json
from typing import Dict, Any, List, Optional
from datetime import datetime, timedelta, timezone
import os
from PIL import Image, ImageDraw, ImageFont
import pytz
from display_manager import DisplayManager
from cache_manager import CacheManager
from config_manager import ConfigManager
# Import the API counter function from web interface
try:
from web_interface_v2 import increment_api_counter
except ImportError:
# Fallback if web interface is not available
def increment_api_counter(kind: str, count: int = 1):
pass
# Get logger
logger = logging.getLogger(__name__)
class LeaderboardManager:
"""Manager for displaying scrolling leaderboards for multiple sports leagues."""
def __init__(self, config: Dict[str, Any], display_manager: DisplayManager):
self.config = config
self.display_manager = display_manager
self.leaderboard_config = config.get('leaderboard', {})
self.is_enabled = self.leaderboard_config.get('enabled', False)
self.enabled_sports = self.leaderboard_config.get('enabled_sports', {})
self.update_interval = self.leaderboard_config.get('update_interval', 3600)
self.scroll_speed = self.leaderboard_config.get('scroll_speed', 2)
self.scroll_delay = self.leaderboard_config.get('scroll_delay', 0.05)
self.display_duration = self.leaderboard_config.get('display_duration', 30)
self.loop = self.leaderboard_config.get('loop', True)
self.request_timeout = self.leaderboard_config.get('request_timeout', 30)
# Dynamic duration settings
self.dynamic_duration_enabled = self.leaderboard_config.get('dynamic_duration', True)
self.min_duration = self.leaderboard_config.get('min_duration', 30)
self.max_duration = self.leaderboard_config.get('max_duration', 300)
self.duration_buffer = self.leaderboard_config.get('duration_buffer', 0.1)
self.dynamic_duration = 60 # Default duration in seconds
self.total_scroll_width = 0 # Track total width for dynamic duration calculation
# Initialize managers
self.cache_manager = CacheManager()
self.config_manager = ConfigManager()
# State variables
self.last_update = 0
self.scroll_position = 0
self.last_scroll_time = 0
self.leaderboard_data = []
self.current_sport_index = 0
self.leaderboard_image = None # This will hold the single, wide image
self.last_display_time = 0
# Font setup
self.fonts = self._load_fonts()
# League configurations with ESPN API endpoints
self.league_configs = {
'nfl': {
'sport': 'football',
'league': 'nfl',
'logo_dir': 'assets/sports/nfl_logos',
'league_logo': 'assets/sports/nfl_logos/nfl.png',
'teams_url': 'https://site.api.espn.com/apis/site/v2/sports/football/nfl/teams',
'enabled': self.enabled_sports.get('nfl', {}).get('enabled', False),
'top_teams': self.enabled_sports.get('nfl', {}).get('top_teams', 10)
},
'nba': {
'sport': 'basketball',
'league': 'nba',
'logo_dir': 'assets/sports/nba_logos',
'league_logo': 'assets/sports/nba_logos/nba.png',
'teams_url': 'https://site.api.espn.com/apis/site/v2/sports/basketball/nba/teams',
'enabled': self.enabled_sports.get('nba', {}).get('enabled', False),
'top_teams': self.enabled_sports.get('nba', {}).get('top_teams', 10)
},
'mlb': {
'sport': 'baseball',
'league': 'mlb',
'logo_dir': 'assets/sports/mlb_logos',
'league_logo': 'assets/sports/mlb_logos/mlb.png',
'teams_url': 'https://site.api.espn.com/apis/site/v2/sports/baseball/mlb/teams',
'enabled': self.enabled_sports.get('mlb', {}).get('enabled', False),
'top_teams': self.enabled_sports.get('mlb', {}).get('top_teams', 10)
},
'ncaa_fb': {
'sport': 'football',
'league': 'college-football',
'logo_dir': 'assets/sports/ncaa_fbs_logos',
'league_logo': 'assets/sports/ncaa_fbs_logos/ncaa_fb.png',
'teams_url': 'https://site.api.espn.com/apis/site/v2/sports/football/college-football/teams',
'enabled': self.enabled_sports.get('ncaa_fb', {}).get('enabled', False),
'top_teams': self.enabled_sports.get('ncaa_fb', {}).get('top_teams', 25)
},
'nhl': {
'sport': 'hockey',
'league': 'nhl',
'logo_dir': 'assets/sports/nhl_logos',
'league_logo': 'assets/sports/nhl_logos/nhl.png',
'teams_url': 'https://site.api.espn.com/apis/site/v2/sports/hockey/nhl/teams',
'enabled': self.enabled_sports.get('nhl', {}).get('enabled', False),
'top_teams': self.enabled_sports.get('nhl', {}).get('top_teams', 10)
},
'ncaam_basketball': {
'sport': 'basketball',
'league': 'mens-college-basketball',
'logo_dir': 'assets/sports/ncaa_fbs_logos',
'league_logo': 'assets/sports/ncaa_fbs_logos/ncaam.png',
'teams_url': 'https://site.api.espn.com/apis/site/v2/sports/basketball/mens-college-basketball/teams',
'enabled': self.enabled_sports.get('ncaam_basketball', {}).get('enabled', False),
'top_teams': self.enabled_sports.get('ncaam_basketball', {}).get('top_teams', 25)
}
}
logger.info(f"LeaderboardManager initialized with enabled sports: {[k for k, v in self.league_configs.items() if v['enabled']]}")
def _load_fonts(self) -> Dict[str, ImageFont.FreeTypeFont]:
"""Load fonts for the leaderboard display."""
try:
return {
'small': ImageFont.truetype("assets/fonts/PressStart2P-Regular.ttf", 6),
'medium': ImageFont.truetype("assets/fonts/PressStart2P-Regular.ttf", 8),
'large': ImageFont.truetype("assets/fonts/PressStart2P-Regular.ttf", 10)
}
except Exception as e:
logger.error(f"Error loading fonts: {e}")
return {
'small': ImageFont.load_default(),
'medium': ImageFont.load_default(),
'large': ImageFont.load_default()
}
def _get_team_logo(self, team_abbr: str, logo_dir: str) -> Optional[Image.Image]:
"""Get team logo from the configured directory."""
if not team_abbr or not logo_dir:
logger.debug("Cannot get team logo with missing team_abbr or logo_dir")
return None
try:
logo_path = os.path.join(logo_dir, f"{team_abbr}.png")
logger.debug(f"Attempting to load logo from path: {logo_path}")
if os.path.exists(logo_path):
logo = Image.open(logo_path)
logger.debug(f"Successfully loaded logo for {team_abbr} from {logo_path}")
return logo
else:
logger.warning(f"Logo not found at path: {logo_path}")
return None
except Exception as e:
logger.error(f"Error loading logo for {team_abbr} from {logo_dir}: {e}")
return None
def _get_league_logo(self, league_logo_path: str) -> Optional[Image.Image]:
"""Get league logo from the configured path."""
if not league_logo_path:
return None
try:
if os.path.exists(league_logo_path):
logo = Image.open(league_logo_path)
logger.debug(f"Successfully loaded league logo from {league_logo_path}")
return logo
else:
logger.warning(f"League logo not found at path: {league_logo_path}")
return None
except Exception as e:
logger.error(f"Error loading league logo from {league_logo_path}: {e}")
return None
def _fetch_standings(self, league_config: Dict[str, Any]) -> List[Dict[str, Any]]:
"""Fetch standings for a specific league from ESPN API."""
try:
# First, get all teams for the league
teams_url = league_config['teams_url']
response = requests.get(teams_url, timeout=self.request_timeout)
response.raise_for_status()
data = response.json()
# Increment API counter for sports data
increment_api_counter('sports', 1)
standings = []
sports = data.get('sports', [])
if not sports:
logger.warning(f"No sports data found for {league_config['league']}")
return []
leagues = sports[0].get('leagues', [])
if not leagues:
logger.warning(f"No leagues data found for {league_config['league']}")
return []
teams = leagues[0].get('teams', [])
if not teams:
logger.warning(f"No teams data found for {league_config['league']}")
return []
logger.info(f"Found {len(teams)} teams for {league_config['league']}")
# For each team, fetch their individual record
for team_data in teams:
team = team_data.get('team', {})
team_abbr = team.get('abbreviation')
team_name = team.get('name', 'Unknown')
if not team_abbr:
logger.warning(f"No abbreviation found for team {team_name}")
continue
# Fetch individual team record
team_record = self._fetch_team_record(team_abbr, league_config)
if team_record:
standings.append({
'name': team_name,
'abbreviation': team_abbr,
'wins': team_record.get('wins', 0),
'losses': team_record.get('losses', 0),
'ties': team_record.get('ties', 0),
'win_percentage': team_record.get('win_percentage', 0)
})
# Sort by win percentage (descending) and limit to top teams
standings.sort(key=lambda x: x['win_percentage'], reverse=True)
top_teams = standings[:league_config['top_teams']]
logger.info(f"Fetched {len(top_teams)} teams for {league_config['league']}")
return top_teams
except Exception as e:
logger.error(f"Error fetching standings for {league_config['league']}: {e}")
return []
def _fetch_team_record(self, team_abbr: str, league_config: Dict[str, Any]) -> Optional[Dict[str, Any]]:
"""Fetch individual team record from ESPN API."""
try:
sport = league_config['sport']
league = league_config['league']
# Use a more specific endpoint for college sports
if league == 'college-football':
url = f"https://site.api.espn.com/apis/site/v2/sports/football/college-football/teams/{team_abbr}"
else:
url = f"https://site.api.espn.com/apis/site/v2/sports/{sport}/{league}/teams/{team_abbr}"
response = requests.get(url, timeout=self.request_timeout)
response.raise_for_status()
data = response.json()
# Increment API counter for sports data
increment_api_counter('sports', 1)
team_data = data.get('team', {})
stats = team_data.get('stats', [])
# Find wins and losses
wins = 0
losses = 0
ties = 0
for stat in stats:
if stat.get('name') == 'wins':
wins = stat.get('value', 0)
elif stat.get('name') == 'losses':
losses = stat.get('value', 0)
elif stat.get('name') == 'ties':
ties = stat.get('value', 0)
# Calculate win percentage
total_games = wins + losses + ties
win_percentage = wins / total_games if total_games > 0 else 0
return {
'wins': wins,
'losses': losses,
'ties': ties,
'win_percentage': win_percentage
}
except Exception as e:
logger.error(f"Error fetching record for {team_abbr} in league {league_config['league']}: {e}")
return None
def _fetch_all_standings(self) -> List[Dict[str, Any]]:
"""Fetch standings for all enabled leagues."""
all_standings = []
for league_key, league_config in self.league_configs.items():
if not league_config['enabled']:
continue
logger.debug(f"Fetching standings for {league_key}")
standings = self._fetch_standings(league_config)
if standings:
all_standings.append({
'league': league_key,
'league_config': league_config,
'teams': standings
})
return all_standings
def _create_leaderboard_image(self) -> None:
"""Create the scrolling leaderboard image."""
if not self.leaderboard_data:
logger.warning("No leaderboard data available")
return
try:
# Calculate total width needed
total_width = 0
team_height = 16 # Height for each team entry
league_header_height = 20 # Height for league logo and name
spacing = 10 # Spacing between leagues
# Calculate width for each league section
for league_data in self.leaderboard_data:
league_config = league_data['league_config']
teams = league_data['teams']
# Width for league header (logo + name)
league_width = 200 # Base width for league section
# Width for team entries (number + logo + name + record)
max_team_width = 0
for i, team in enumerate(teams):
team_text = f"{i+1}. {team['abbreviation']} {team['wins']}-{team['losses']}"
if 'ties' in team:
team_text += f"-{team['ties']}"
# Estimate text width (rough calculation)
text_width = len(team_text) * 6 # Approximate character width
team_width = 30 + text_width + 50 # Number + text + logo space
max_team_width = max(max_team_width, team_width)
league_width = max(league_width, max_team_width)
total_width += league_width + spacing
# Create the main image
height = self.display_manager.matrix.height
self.leaderboard_image = Image.new('RGB', (total_width, height), (0, 0, 0))
draw = ImageDraw.Draw(self.leaderboard_image)
current_x = 0
for league_data in self.leaderboard_data:
league_key = league_data['league']
league_config = league_data['league_config']
teams = league_data['teams']
# Draw league header
league_logo = self._get_league_logo(league_config['league_logo'])
if league_logo:
# Resize league logo to fit
logo_height = int(height * 0.4)
logo_width = int(logo_height * league_logo.width / league_logo.height)
league_logo = league_logo.resize((logo_width, logo_height), Image.Resampling.LANCZOS)
# Paste league logo
logo_y = (height - logo_height) // 2
self.leaderboard_image.paste(league_logo, (current_x + 5, logo_y), league_logo if league_logo.mode == 'RGBA' else None)
current_x += logo_width + 10
# Draw league name
league_name = league_key.upper().replace('_', ' ')
draw.text((current_x, 5), league_name, font=self.fonts['medium'], fill=(255, 255, 255))
current_x += 150
# Draw team standings
team_y = league_header_height
for i, team in enumerate(teams):
if team_y + team_height > height:
break
# Draw team number
number_text = f"{i+1}."
draw.text((current_x, team_y), number_text, font=self.fonts['small'], fill=(255, 255, 0))
# Draw team logo
team_logo = self._get_team_logo(team['abbreviation'], league_config['logo_dir'])
if team_logo:
# Resize team logo
logo_size = 12
team_logo = team_logo.resize((logo_size, logo_size), Image.Resampling.LANCZOS)
# Paste team logo
logo_x = current_x + 20
logo_y_pos = team_y + 2
self.leaderboard_image.paste(team_logo, (logo_x, logo_y_pos), team_logo if team_logo.mode == 'RGBA' else None)
# Draw team name and record
team_text = f"{team['abbreviation']} {team['wins']}-{team['losses']}"
if 'ties' in team:
team_text += f"-{team['ties']}"
draw.text((logo_x + logo_size + 5, team_y), team_text, font=self.fonts['small'], fill=(255, 255, 255))
else:
# Fallback if no logo
team_text = f"{team['abbreviation']} {team['wins']}-{team['losses']}"
if 'ties' in team:
team_text += f"-{team['ties']}"
draw.text((current_x + 20, team_y), team_text, font=self.fonts['small'], fill=(255, 255, 255))
team_y += team_height
current_x += 200 # Width for team section
current_x += spacing # Add spacing between leagues
# Calculate dynamic duration based on total width
if self.dynamic_duration_enabled:
scroll_time = (total_width / self.scroll_speed) * self.scroll_delay
self.dynamic_duration = max(self.min_duration, min(self.max_duration, scroll_time + self.duration_buffer))
logger.info(f"Calculated dynamic duration: {self.dynamic_duration:.1f}s for width {total_width}")
self.total_scroll_width = total_width
logger.info(f"Created leaderboard image with width {total_width}")
except Exception as e:
logger.error(f"Error creating leaderboard image: {e}")
self.leaderboard_image = None
def update(self) -> None:
"""Update leaderboard data."""
current_time = time.time()
if current_time - self.last_update < self.update_interval:
return
logger.info("Updating leaderboard data")
try:
self.leaderboard_data = self._fetch_all_standings()
self.last_update = current_time
if self.leaderboard_data:
self._create_leaderboard_image()
else:
logger.warning("No leaderboard data fetched")
except Exception as e:
logger.error(f"Error updating leaderboard: {e}")
def _display_fallback_message(self) -> None:
"""Display a fallback message when no data is available."""
try:
width = self.display_manager.matrix.width
height = self.display_manager.matrix.height
# Create a simple text image
image = Image.new('RGB', (width, height), (0, 0, 0))
draw = ImageDraw.Draw(image)
text = "No Leaderboard Data"
text_bbox = draw.textbbox((0, 0), text, font=self.fonts['medium'])
text_width = text_bbox[2] - text_bbox[0]
text_height = text_bbox[3] - text_bbox[1]
x = (width - text_width) // 2
y = (height - text_height) // 2
draw.text((x, y), text, font=self.fonts['medium'], fill=(255, 255, 255))
self.display_manager.set_image(image)
except Exception as e:
logger.error(f"Error displaying fallback message: {e}")
def display(self, force_clear: bool = False) -> None:
"""Display the leaderboard."""
logger.debug("Entering leaderboard display method")
logger.debug(f"Leaderboard enabled: {self.is_enabled}")
logger.debug(f"Current scroll position: {self.scroll_position}")
logger.debug(f"Leaderboard image width: {self.leaderboard_image.width if self.leaderboard_image else 'None'}")
logger.debug(f"Dynamic duration: {self.dynamic_duration}s")
if not self.is_enabled:
logger.debug("Leaderboard is disabled, exiting display method.")
return
# Reset display start time when force_clear is True or when starting fresh
if force_clear or not hasattr(self, '_display_start_time'):
self._display_start_time = time.time()
logger.debug(f"Reset/initialized display start time: {self._display_start_time}")
# Also reset scroll position for clean start
self.scroll_position = 0
else:
# Check if the display start time is too old (more than 2x the dynamic duration)
current_time = time.time()
elapsed_time = current_time - self._display_start_time
if elapsed_time > (self.dynamic_duration * 2):
logger.debug(f"Display start time is too old ({elapsed_time:.1f}s), resetting")
self._display_start_time = current_time
self.scroll_position = 0
logger.debug(f"Number of leagues in data at start of display method: {len(self.leaderboard_data)}")
if not self.leaderboard_data:
logger.warning("Leaderboard has no data. Attempting to update...")
self.update()
if not self.leaderboard_data:
logger.warning("Still no data after update. Displaying fallback message.")
self._display_fallback_message()
return
if self.leaderboard_image is None:
logger.warning("Leaderboard image is not available. Attempting to create it.")
self._create_leaderboard_image()
if self.leaderboard_image is None:
logger.error("Failed to create leaderboard image.")
self._display_fallback_message()
return
try:
current_time = time.time()
# Check if we should be scrolling
should_scroll = current_time - self.last_scroll_time >= self.scroll_delay
# Signal scrolling state to display manager
if should_scroll:
self.display_manager.set_scrolling_state(True)
else:
# If we're not scrolling, check if we should process deferred updates
self.display_manager.process_deferred_updates()
# Scroll the image
if should_scroll:
self.scroll_position += self.scroll_speed
self.last_scroll_time = current_time
# Calculate crop region
width = self.display_manager.matrix.width
height = self.display_manager.matrix.height
# Handle looping based on configuration
if self.loop:
# Reset position when we've scrolled past the end for a continuous loop
if self.scroll_position >= self.leaderboard_image.width:
logger.debug(f"Leaderboard loop reset: scroll_position {self.scroll_position} >= image width {self.leaderboard_image.width}")
self.scroll_position = 0
else:
# Stop scrolling when we reach the end
if self.scroll_position >= self.leaderboard_image.width - width:
logger.debug(f"Leaderboard reached end: scroll_position {self.scroll_position} >= {self.leaderboard_image.width - width}")
self.scroll_position = self.leaderboard_image.width - width
# Signal that scrolling has stopped
self.display_manager.set_scrolling_state(False)
# Check if we're at a natural break point for mode switching
elapsed_time = current_time - self._display_start_time
remaining_time = self.dynamic_duration - elapsed_time
# If we have less than 2 seconds remaining and we're not at a clean break point,
# try to complete the current league display
if remaining_time < 2.0 and self.scroll_position > 0:
# Calculate how much time we need to complete the current scroll position
frames_to_complete = (self.leaderboard_image.width - self.scroll_position) / self.scroll_speed
time_to_complete = frames_to_complete * self.scroll_delay
if time_to_complete <= remaining_time:
# We have enough time to complete the scroll, continue normally
pass
else:
# Not enough time, reset to beginning for clean transition
logger.debug(f"Display ending soon, resetting scroll position for clean transition")
self.scroll_position = 0
# Create the visible part of the image by cropping from the leaderboard_image
visible_image = self.leaderboard_image.crop((
self.scroll_position,
0,
self.scroll_position + width,
height
))
# Display the visible portion
self.display_manager.set_image(visible_image)
except Exception as e:
logger.error(f"Error in leaderboard display: {e}")
self._display_fallback_message()

View File

@@ -79,7 +79,8 @@ class BaseNBAManager:
def _get_timezone(self): def _get_timezone(self):
try: try:
return pytz.timezone(self.config_manager.get_timezone()) timezone_str = self.config.get('timezone', 'UTC')
return pytz.timezone(timezone_str)
except pytz.UnknownTimeZoneError: except pytz.UnknownTimeZoneError:
return pytz.utc return pytz.utc

View File

@@ -106,7 +106,8 @@ class BaseNCAAFBManager: # Renamed class
def _get_timezone(self): def _get_timezone(self):
try: try:
return pytz.timezone(self.config_manager.get_timezone()) timezone_str = self.config.get('timezone', 'UTC')
return pytz.timezone(timezone_str)
except pytz.UnknownTimeZoneError: except pytz.UnknownTimeZoneError:
return pytz.utc return pytz.utc

View File

@@ -107,7 +107,8 @@ class BaseNCAAMBasketballManager:
def _get_timezone(self): def _get_timezone(self):
try: try:
return pytz.timezone(self.config_manager.get_timezone()) timezone_str = self.config.get('timezone', 'UTC')
return pytz.timezone(timezone_str)
except pytz.UnknownTimeZoneError: except pytz.UnknownTimeZoneError:
return pytz.utc return pytz.utc

View File

@@ -87,7 +87,8 @@ class BaseNFLManager: # Renamed class
def _get_timezone(self): def _get_timezone(self):
try: try:
return pytz.timezone(self.config_manager.get_timezone()) timezone_str = self.config.get('timezone', 'UTC')
return pytz.timezone(timezone_str)
except pytz.UnknownTimeZoneError: except pytz.UnknownTimeZoneError:
return pytz.utc return pytz.utc

View File

@@ -107,7 +107,8 @@ class BaseNHLManager:
def _get_timezone(self): def _get_timezone(self):
try: try:
return pytz.timezone(self.config_manager.get_timezone()) timezone_str = self.config.get('timezone', 'UTC')
return pytz.timezone(timezone_str)
except pytz.UnknownTimeZoneError: except pytz.UnknownTimeZoneError:
return pytz.utc return pytz.utc

85
test/debug_espn_api.py Normal file
View File

@@ -0,0 +1,85 @@
#!/usr/bin/env python3
"""
Debug script to examine ESPN API response structure
"""
import requests
import json
def debug_espn_api():
"""Debug ESPN API responses."""
# Test different endpoints
test_endpoints = [
{
'name': 'NFL Standings',
'url': 'https://site.api.espn.com/apis/site/v2/sports/football/nfl/standings'
},
{
'name': 'NFL Teams',
'url': 'https://site.api.espn.com/apis/site/v2/sports/football/nfl/teams'
},
{
'name': 'NFL Scoreboard',
'url': 'https://site.api.espn.com/apis/site/v2/sports/football/nfl/scoreboard'
},
{
'name': 'NBA Teams',
'url': 'https://site.api.espn.com/apis/site/v2/sports/basketball/nba/teams'
},
{
'name': 'MLB Teams',
'url': 'https://site.api.espn.com/apis/site/v2/sports/baseball/mlb/teams'
}
]
for endpoint in test_endpoints:
print(f"\n{'='*50}")
print(f"Testing {endpoint['name']}")
print(f"URL: {endpoint['url']}")
print('='*50)
try:
response = requests.get(endpoint['url'], timeout=30)
response.raise_for_status()
data = response.json()
print(f"Response status: {response.status_code}")
print(f"Response keys: {list(data.keys())}")
# Print a sample of the response
if 'sports' in data:
sports = data['sports']
print(f"Sports found: {len(sports)}")
if sports:
leagues = sports[0].get('leagues', [])
print(f"Leagues found: {len(leagues)}")
if leagues:
teams = leagues[0].get('teams', [])
print(f"Teams found: {len(teams)}")
if teams:
print("Sample team data:")
sample_team = teams[0]
print(f" Team: {sample_team.get('team', {}).get('name', 'Unknown')}")
print(f" Abbreviation: {sample_team.get('team', {}).get('abbreviation', 'Unknown')}")
stats = sample_team.get('stats', [])
print(f" Stats found: {len(stats)}")
for stat in stats[:3]: # Show first 3 stats
print(f" {stat.get('name', 'Unknown')}: {stat.get('value', 'Unknown')}")
elif 'groups' in data:
groups = data['groups']
print(f"Groups found: {len(groups)}")
if groups:
print("Sample group data:")
print(json.dumps(groups[0], indent=2)[:500] + "...")
else:
print("Sample response data:")
print(json.dumps(data, indent=2)[:500] + "...")
except Exception as e:
print(f"Error: {e}")
if __name__ == "__main__":
debug_espn_api()

99
test/test_leaderboard.py Normal file
View File

@@ -0,0 +1,99 @@
#!/usr/bin/env python3
"""
Test script for the LeaderboardManager
"""
import sys
import os
import json
import logging
# Add the src directory to the path
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', 'src'))
from leaderboard_manager import LeaderboardManager
from display_manager import DisplayManager
from config_manager import ConfigManager
# Configure logging
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(levelname)s - %(message)s'
)
def test_leaderboard_manager():
"""Test the leaderboard manager functionality."""
# Load configuration
config_manager = ConfigManager()
config = config_manager.load_config()
# Enable leaderboard and some sports for testing
config['leaderboard'] = {
'enabled': True,
'enabled_sports': {
'nfl': {
'enabled': True,
'top_teams': 5
},
'nba': {
'enabled': True,
'top_teams': 5
},
'mlb': {
'enabled': True,
'top_teams': 5
}
},
'update_interval': 3600,
'scroll_speed': 2,
'scroll_delay': 0.05,
'display_duration': 60,
'loop': True,
'request_timeout': 30,
'dynamic_duration': True,
'min_duration': 30,
'max_duration': 300,
'duration_buffer': 0.1
}
# Initialize display manager (this will be a mock for testing)
display_manager = DisplayManager(config)
# Initialize leaderboard manager
leaderboard_manager = LeaderboardManager(config, display_manager)
print("Testing LeaderboardManager...")
print(f"Enabled: {leaderboard_manager.is_enabled}")
print(f"Enabled sports: {[k for k, v in leaderboard_manager.league_configs.items() if v['enabled']]}")
# Test fetching standings
print("\nFetching standings...")
leaderboard_manager.update()
print(f"Number of leagues with data: {len(leaderboard_manager.leaderboard_data)}")
for league_data in leaderboard_manager.leaderboard_data:
league = league_data['league']
teams = league_data['teams']
print(f"\n{league.upper()}:")
for i, team in enumerate(teams[:5]): # Show top 5
record = f"{team['wins']}-{team['losses']}"
if 'ties' in team:
record += f"-{team['ties']}"
print(f" {i+1}. {team['abbreviation']} {record}")
# Test image creation
print("\nCreating leaderboard image...")
if leaderboard_manager.leaderboard_data:
leaderboard_manager._create_leaderboard_image()
if leaderboard_manager.leaderboard_image:
print(f"Image created successfully: {leaderboard_manager.leaderboard_image.size}")
print(f"Dynamic duration: {leaderboard_manager.dynamic_duration:.1f}s")
else:
print("Failed to create image")
else:
print("No data available to create image")
if __name__ == "__main__":
test_leaderboard_manager()

View File

@@ -0,0 +1,205 @@
#!/usr/bin/env python3
"""
Simple test script for the LeaderboardManager (without display dependencies)
"""
import sys
import os
import json
import logging
import requests
from typing import Dict, Any, List, Optional
from datetime import datetime, timedelta, timezone
from PIL import Image, ImageDraw, ImageFont
# Configure logging
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(levelname)s - %(message)s'
)
logger = logging.getLogger(__name__)
def test_espn_api():
"""Test ESPN API endpoints for standings."""
# Test different league endpoints
test_leagues = [
{
'name': 'NFL',
'url': 'https://site.api.espn.com/apis/site/v2/sports/football/nfl/standings'
},
{
'name': 'NBA',
'url': 'https://site.api.espn.com/apis/site/v2/sports/basketball/nba/standings'
},
{
'name': 'MLB',
'url': 'https://site.api.espn.com/apis/site/v2/sports/baseball/mlb/standings'
}
]
for league in test_leagues:
print(f"\nTesting {league['name']} API...")
try:
response = requests.get(league['url'], timeout=30)
response.raise_for_status()
data = response.json()
print(f"{league['name']} API response successful")
# Check if we have groups data
groups = data.get('groups', [])
print(f" Groups found: {len(groups)}")
# Try to extract some team data
total_teams = 0
for group in groups:
if 'standings' in group:
total_teams += len(group['standings'])
elif 'groups' in group:
# Handle nested groups (like NFL conferences/divisions)
for sub_group in group['groups']:
if 'standings' in sub_group:
total_teams += len(sub_group['standings'])
elif 'groups' in sub_group:
for sub_sub_group in sub_group['groups']:
if 'standings' in sub_sub_group:
total_teams += len(sub_sub_group['standings'])
print(f" Total teams found: {total_teams}")
except Exception as e:
print(f"{league['name']} API failed: {e}")
def test_standings_parsing():
"""Test parsing standings data."""
# Test NFL standings parsing using teams endpoint
print("\nTesting NFL standings parsing...")
try:
# First get all teams
teams_url = 'https://site.api.espn.com/apis/site/v2/sports/football/nfl/teams'
response = requests.get(teams_url, timeout=30)
response.raise_for_status()
data = response.json()
sports = data.get('sports', [])
if not sports:
print("✗ No sports data found")
return
leagues = sports[0].get('leagues', [])
if not leagues:
print("✗ No leagues data found")
return
teams = leagues[0].get('teams', [])
if not teams:
print("✗ No teams data found")
return
print(f"Found {len(teams)} NFL teams")
# Test fetching individual team records
standings = []
test_teams = teams[:5] # Test first 5 teams to avoid too many API calls
for team_data in test_teams:
team = team_data.get('team', {})
team_abbr = team.get('abbreviation')
team_name = team.get('name', 'Unknown')
if not team_abbr:
continue
print(f" Fetching record for {team_abbr}...")
# Fetch individual team record
team_url = f"https://site.api.espn.com/apis/site/v2/sports/football/nfl/teams/{team_abbr}"
team_response = requests.get(team_url, timeout=30)
team_response.raise_for_status()
team_data = team_response.json()
team_info = team_data.get('team', {})
stats = team_info.get('stats', [])
# Find wins and losses
wins = 0
losses = 0
ties = 0
for stat in stats:
if stat.get('name') == 'wins':
wins = stat.get('value', 0)
elif stat.get('name') == 'losses':
losses = stat.get('value', 0)
elif stat.get('name') == 'ties':
ties = stat.get('value', 0)
# Calculate win percentage
total_games = wins + losses + ties
win_percentage = wins / total_games if total_games > 0 else 0
standings.append({
'name': team_name,
'abbreviation': team_abbr,
'wins': wins,
'losses': losses,
'ties': ties,
'win_percentage': win_percentage
})
# Sort by win percentage and show results
standings.sort(key=lambda x: x['win_percentage'], reverse=True)
print("NFL team records:")
for i, team in enumerate(standings):
record = f"{team['wins']}-{team['losses']}"
if team['ties'] > 0:
record += f"-{team['ties']}"
print(f" {i+1}. {team['abbreviation']} {record} ({team['win_percentage']:.3f})")
except Exception as e:
print(f"✗ NFL standings parsing failed: {e}")
def test_logo_loading():
"""Test logo loading functionality."""
print("\nTesting logo loading...")
# Test team logo loading
logo_dir = "assets/sports/nfl_logos"
test_teams = ["TB", "DAL", "NE"]
for team in test_teams:
logo_path = os.path.join(logo_dir, f"{team}.png")
if os.path.exists(logo_path):
print(f"{team} logo found: {logo_path}")
else:
print(f"{team} logo not found: {logo_path}")
# Test league logo loading
league_logos = [
"assets/sports/nfl_logos/nfl.png",
"assets/sports/nba_logos/nba.png",
"assets/sports/mlb_logos/mlb.png",
"assets/sports/nhl_logos/nhl.png",
"assets/sports/ncaa_fbs_logos/ncaa_fb.png",
"assets/sports/ncaa_fbs_logos/ncaam.png"
]
for logo_path in league_logos:
if os.path.exists(logo_path):
print(f"✓ League logo found: {logo_path}")
else:
print(f"✗ League logo not found: {logo_path}")
if __name__ == "__main__":
print("Testing LeaderboardManager components...")
test_espn_api()
test_standings_parsing()
test_logo_loading()
print("\nTest completed!")