mirror of
https://github.com/ChuckBuilds/LEDMatrix.git
synced 2026-04-10 21:03:01 +00:00
update UTC timezone logic to check config settings for all managers
This commit is contained in:
BIN
assets/sports/mlb_logos/mlb.png
Normal file
BIN
assets/sports/mlb_logos/mlb.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 273 B |
BIN
assets/sports/nba_logos/nba.png
Normal file
BIN
assets/sports/nba_logos/nba.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 278 B |
BIN
assets/sports/ncaa_fbs_logos/ncaa_fb.png
Normal file
BIN
assets/sports/ncaa_fbs_logos/ncaa_fb.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 418 B |
BIN
assets/sports/ncaa_fbs_logos/ncaam.png
Normal file
BIN
assets/sports/ncaa_fbs_logos/ncaam.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 434 B |
BIN
assets/sports/nfl_logos/nfl.png
Normal file
BIN
assets/sports/nfl_logos/nfl.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 253 B |
BIN
assets/sports/nhl_logos/nhl.png
Normal file
BIN
assets/sports/nhl_logos/nhl.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 236 B |
@@ -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
1
create_league_logos.py
Normal file
@@ -0,0 +1 @@
|
|||||||
|
|
||||||
1
create_ncaa_logos.py
Normal file
1
create_ncaa_logos.py
Normal file
@@ -0,0 +1 @@
|
|||||||
|
|
||||||
@@ -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)
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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
589
src/leaderboard_manager.py
Normal 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()
|
||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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
85
test/debug_espn_api.py
Normal 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
99
test/test_leaderboard.py
Normal 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()
|
||||||
205
test/test_leaderboard_simple.py
Normal file
205
test/test_leaderboard_simple.py
Normal 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!")
|
||||||
Reference in New Issue
Block a user