mirror of
https://github.com/ChuckBuilds/LEDMatrix.git
synced 2026-04-10 21:03:01 +00:00
update leaderboard data sources for other sports and font sizes
This commit is contained in:
@@ -7,9 +7,15 @@ from datetime import datetime, timedelta, timezone
|
|||||||
import os
|
import os
|
||||||
from PIL import Image, ImageDraw, ImageFont
|
from PIL import Image, ImageDraw, ImageFont
|
||||||
import pytz
|
import pytz
|
||||||
from .display_manager import DisplayManager
|
try:
|
||||||
from .cache_manager import CacheManager
|
from .display_manager import DisplayManager
|
||||||
from .config_manager import ConfigManager
|
from .cache_manager import CacheManager
|
||||||
|
from .config_manager import ConfigManager
|
||||||
|
except ImportError:
|
||||||
|
# Fallback for direct imports
|
||||||
|
from display_manager import DisplayManager
|
||||||
|
from cache_manager import CacheManager
|
||||||
|
from config_manager import ConfigManager
|
||||||
|
|
||||||
# Import the API counter function from web interface
|
# Import the API counter function from web interface
|
||||||
try:
|
try:
|
||||||
@@ -71,9 +77,12 @@ class LeaderboardManager:
|
|||||||
'league': 'nfl',
|
'league': 'nfl',
|
||||||
'logo_dir': 'assets/sports/nfl_logos',
|
'logo_dir': 'assets/sports/nfl_logos',
|
||||||
'league_logo': 'assets/sports/nfl_logos/nfl.png',
|
'league_logo': 'assets/sports/nfl_logos/nfl.png',
|
||||||
'teams_url': 'https://site.api.espn.com/apis/site/v2/sports/football/nfl/teams',
|
'standings_url': 'https://site.api.espn.com/apis/v2/sports/football/nfl/standings',
|
||||||
'enabled': self.enabled_sports.get('nfl', {}).get('enabled', False),
|
'enabled': self.enabled_sports.get('nfl', {}).get('enabled', False),
|
||||||
'top_teams': self.enabled_sports.get('nfl', {}).get('top_teams', 10)
|
'top_teams': self.enabled_sports.get('nfl', {}).get('top_teams', 10),
|
||||||
|
'season': self.enabled_sports.get('nfl', {}).get('season', 2025),
|
||||||
|
'level': self.enabled_sports.get('nfl', {}).get('level', 1),
|
||||||
|
'sort': self.enabled_sports.get('nfl', {}).get('sort', 'winpercent:desc,gamesbehind:asc')
|
||||||
},
|
},
|
||||||
'nba': {
|
'nba': {
|
||||||
'sport': 'basketball',
|
'sport': 'basketball',
|
||||||
@@ -89,9 +98,12 @@ class LeaderboardManager:
|
|||||||
'league': 'mlb',
|
'league': 'mlb',
|
||||||
'logo_dir': 'assets/sports/mlb_logos',
|
'logo_dir': 'assets/sports/mlb_logos',
|
||||||
'league_logo': 'assets/sports/mlb_logos/mlb.png',
|
'league_logo': 'assets/sports/mlb_logos/mlb.png',
|
||||||
'teams_url': 'https://site.api.espn.com/apis/site/v2/sports/baseball/mlb/teams',
|
'standings_url': 'https://site.api.espn.com/apis/v2/sports/baseball/mlb/standings',
|
||||||
'enabled': self.enabled_sports.get('mlb', {}).get('enabled', False),
|
'enabled': self.enabled_sports.get('mlb', {}).get('enabled', False),
|
||||||
'top_teams': self.enabled_sports.get('mlb', {}).get('top_teams', 10)
|
'top_teams': self.enabled_sports.get('mlb', {}).get('top_teams', 10),
|
||||||
|
'season': self.enabled_sports.get('mlb', {}).get('season', 2025),
|
||||||
|
'level': self.enabled_sports.get('mlb', {}).get('level', 1),
|
||||||
|
'sort': self.enabled_sports.get('mlb', {}).get('sort', 'winpercent:desc,gamesbehind:asc')
|
||||||
},
|
},
|
||||||
'ncaa_fb': {
|
'ncaa_fb': {
|
||||||
'sport': 'football',
|
'sport': 'football',
|
||||||
@@ -108,9 +120,12 @@ class LeaderboardManager:
|
|||||||
'league': 'nhl',
|
'league': 'nhl',
|
||||||
'logo_dir': 'assets/sports/nhl_logos',
|
'logo_dir': 'assets/sports/nhl_logos',
|
||||||
'league_logo': 'assets/sports/nhl_logos/nhl.png',
|
'league_logo': 'assets/sports/nhl_logos/nhl.png',
|
||||||
'teams_url': 'https://site.api.espn.com/apis/site/v2/sports/hockey/nhl/teams',
|
'standings_url': 'https://site.api.espn.com/apis/v2/sports/hockey/nhl/standings',
|
||||||
'enabled': self.enabled_sports.get('nhl', {}).get('enabled', False),
|
'enabled': self.enabled_sports.get('nhl', {}).get('enabled', False),
|
||||||
'top_teams': self.enabled_sports.get('nhl', {}).get('top_teams', 10)
|
'top_teams': self.enabled_sports.get('nhl', {}).get('top_teams', 10),
|
||||||
|
'season': self.enabled_sports.get('nhl', {}).get('season', 2025),
|
||||||
|
'level': self.enabled_sports.get('nhl', {}).get('level', 1),
|
||||||
|
'sort': self.enabled_sports.get('nhl', {}).get('sort', 'winpercent:desc,gamesbehind:asc')
|
||||||
},
|
},
|
||||||
'ncaam_basketball': {
|
'ncaam_basketball': {
|
||||||
'sport': 'basketball',
|
'sport': 'basketball',
|
||||||
@@ -120,6 +135,19 @@ class LeaderboardManager:
|
|||||||
'teams_url': 'https://site.api.espn.com/apis/site/v2/sports/basketball/mens-college-basketball/teams',
|
'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),
|
'enabled': self.enabled_sports.get('ncaam_basketball', {}).get('enabled', False),
|
||||||
'top_teams': self.enabled_sports.get('ncaam_basketball', {}).get('top_teams', 25)
|
'top_teams': self.enabled_sports.get('ncaam_basketball', {}).get('top_teams', 25)
|
||||||
|
},
|
||||||
|
'ncaa_baseball': {
|
||||||
|
'sport': 'baseball',
|
||||||
|
'league': 'college-baseball',
|
||||||
|
'logo_dir': 'assets/sports/ncaa_fbs_logos',
|
||||||
|
'league_logo': 'assets/sports/ncaa_fbs_logos/ncaa_baseball.png',
|
||||||
|
'standings_url': 'https://site.api.espn.com/apis/v2/sports/baseball/college-baseball/standings',
|
||||||
|
'scoreboard_url': 'https://site.api.espn.com/apis/site/v2/sports/baseball/college-baseball/scoreboard',
|
||||||
|
'enabled': self.enabled_sports.get('ncaa_baseball', {}).get('enabled', False),
|
||||||
|
'top_teams': self.enabled_sports.get('ncaa_baseball', {}).get('top_teams', 25),
|
||||||
|
'season': self.enabled_sports.get('ncaa_baseball', {}).get('season', 2025),
|
||||||
|
'level': self.enabled_sports.get('ncaa_baseball', {}).get('level', 1),
|
||||||
|
'sort': self.enabled_sports.get('ncaa_baseball', {}).get('sort', 'winpercent:desc,gamesbehind:asc')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -152,15 +180,17 @@ class LeaderboardManager:
|
|||||||
try:
|
try:
|
||||||
return {
|
return {
|
||||||
'small': ImageFont.truetype("assets/fonts/PressStart2P-Regular.ttf", 6),
|
'small': ImageFont.truetype("assets/fonts/PressStart2P-Regular.ttf", 6),
|
||||||
'medium': ImageFont.truetype("assets/fonts/PressStart2P-Regular.ttf", 8),
|
'medium': ImageFont.truetype("assets/fonts/PressStart2P-Regular.ttf", 10), # Increased from 8 to 10
|
||||||
'large': ImageFont.truetype("assets/fonts/PressStart2P-Regular.ttf", 10)
|
'large': ImageFont.truetype("assets/fonts/PressStart2P-Regular.ttf", 12), # Increased from 10 to 12
|
||||||
|
'xlarge': ImageFont.truetype("assets/fonts/PressStart2P-Regular.ttf", 14) # New extra-large font
|
||||||
}
|
}
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Error loading fonts: {e}")
|
logger.error(f"Error loading fonts: {e}")
|
||||||
return {
|
return {
|
||||||
'small': ImageFont.load_default(),
|
'small': ImageFont.load_default(),
|
||||||
'medium': ImageFont.load_default(),
|
'medium': ImageFont.load_default(),
|
||||||
'large': ImageFont.load_default()
|
'large': ImageFont.load_default(),
|
||||||
|
'xlarge': ImageFont.load_default()
|
||||||
}
|
}
|
||||||
|
|
||||||
def _get_team_logo(self, team_abbr: str, logo_dir: str) -> Optional[Image.Image]:
|
def _get_team_logo(self, team_abbr: str, logo_dir: str) -> Optional[Image.Image]:
|
||||||
@@ -213,6 +243,10 @@ class LeaderboardManager:
|
|||||||
if league_key == 'college-football':
|
if league_key == 'college-football':
|
||||||
return self._fetch_ncaa_fb_rankings(league_config)
|
return self._fetch_ncaa_fb_rankings(league_config)
|
||||||
|
|
||||||
|
# Use standings endpoint for NFL, MLB, NHL, and NCAA Baseball
|
||||||
|
if league_key in ['nfl', 'mlb', 'nhl', 'college-baseball']:
|
||||||
|
return self._fetch_standings_data(league_config)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
logger.info(f"Fetching fresh leaderboard data for {league_key}")
|
logger.info(f"Fetching fresh leaderboard data for {league_key}")
|
||||||
|
|
||||||
@@ -391,6 +425,189 @@ class LeaderboardManager:
|
|||||||
logger.error(f"Error fetching rankings for {league_key}: {e}")
|
logger.error(f"Error fetching rankings for {league_key}: {e}")
|
||||||
return []
|
return []
|
||||||
|
|
||||||
|
def _fetch_standings_data(self, league_config: Dict[str, Any]) -> List[Dict[str, Any]]:
|
||||||
|
"""Fetch standings data from ESPN API using the standings endpoint."""
|
||||||
|
league_key = league_config['league']
|
||||||
|
cache_key = f"leaderboard_{league_key}_standings"
|
||||||
|
|
||||||
|
# Try to get cached data first
|
||||||
|
cached_data = self.cache_manager.get_cached_data_with_strategy(cache_key, 'leaderboard')
|
||||||
|
if cached_data:
|
||||||
|
logger.info(f"Using cached standings data for {league_key}")
|
||||||
|
return cached_data.get('standings', [])
|
||||||
|
|
||||||
|
try:
|
||||||
|
logger.info(f"Fetching fresh standings data for {league_key}")
|
||||||
|
|
||||||
|
# Build the standings URL with query parameters
|
||||||
|
standings_url = league_config['standings_url']
|
||||||
|
params = {
|
||||||
|
'season': league_config.get('season', 2025),
|
||||||
|
'level': league_config.get('level', 1),
|
||||||
|
'sort': league_config.get('sort', 'winpercent:desc,gamesbehind:asc')
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info(f"Fetching standings from: {standings_url} with params: {params}")
|
||||||
|
|
||||||
|
response = requests.get(standings_url, params=params, timeout=self.request_timeout)
|
||||||
|
response.raise_for_status()
|
||||||
|
data = response.json()
|
||||||
|
|
||||||
|
# Increment API counter for sports data
|
||||||
|
increment_api_counter('sports', 1)
|
||||||
|
|
||||||
|
standings = []
|
||||||
|
|
||||||
|
# Parse the standings data structure
|
||||||
|
# Check if we have direct standings data or children (divisions/conferences)
|
||||||
|
if 'standings' in data and 'entries' in data['standings']:
|
||||||
|
# Direct standings data (e.g., NFL overall standings)
|
||||||
|
standings_data = data['standings']['entries']
|
||||||
|
logger.info(f"Processing direct standings data with {len(standings_data)} teams")
|
||||||
|
|
||||||
|
for entry in standings_data:
|
||||||
|
team_data = entry.get('team', {})
|
||||||
|
stats = entry.get('stats', [])
|
||||||
|
|
||||||
|
team_name = team_data.get('displayName', 'Unknown')
|
||||||
|
team_abbr = team_data.get('abbreviation', 'Unknown')
|
||||||
|
|
||||||
|
# Extract record from stats
|
||||||
|
wins = 0
|
||||||
|
losses = 0
|
||||||
|
ties = 0
|
||||||
|
win_percentage = 0.0
|
||||||
|
|
||||||
|
# First pass: collect all stat values
|
||||||
|
games_played = 0
|
||||||
|
for stat in stats:
|
||||||
|
stat_type = stat.get('type', '')
|
||||||
|
stat_value = stat.get('value', 0)
|
||||||
|
|
||||||
|
if stat_type == 'wins':
|
||||||
|
wins = int(stat_value)
|
||||||
|
elif stat_type == 'losses':
|
||||||
|
losses = int(stat_value)
|
||||||
|
elif stat_type == 'ties':
|
||||||
|
ties = int(stat_value)
|
||||||
|
elif stat_type == 'winpercent':
|
||||||
|
win_percentage = float(stat_value)
|
||||||
|
# NHL specific stats
|
||||||
|
elif stat_type == 'overtimelosses' and league_key == 'nhl':
|
||||||
|
ties = int(stat_value) # NHL uses overtime losses as ties
|
||||||
|
elif stat_type == 'gamesplayed' and league_key == 'nhl':
|
||||||
|
games_played = float(stat_value)
|
||||||
|
|
||||||
|
# Second pass: calculate win percentage for NHL if not already set
|
||||||
|
if league_key == 'nhl' and win_percentage == 0.0 and games_played > 0:
|
||||||
|
win_percentage = wins / games_played
|
||||||
|
|
||||||
|
# Create record summary
|
||||||
|
if ties > 0:
|
||||||
|
record_summary = f"{wins}-{losses}-{ties}"
|
||||||
|
else:
|
||||||
|
record_summary = f"{wins}-{losses}"
|
||||||
|
|
||||||
|
standings.append({
|
||||||
|
'name': team_name,
|
||||||
|
'abbreviation': team_abbr,
|
||||||
|
'wins': wins,
|
||||||
|
'losses': losses,
|
||||||
|
'ties': ties,
|
||||||
|
'win_percentage': win_percentage,
|
||||||
|
'record_summary': record_summary,
|
||||||
|
'division': 'Overall'
|
||||||
|
})
|
||||||
|
|
||||||
|
elif 'children' in data:
|
||||||
|
# Children structure (divisions/conferences)
|
||||||
|
children = data.get('children', [])
|
||||||
|
logger.info(f"Processing {len(children)} divisions/conferences")
|
||||||
|
|
||||||
|
for child in children:
|
||||||
|
child_name = child.get('displayName', 'Unknown')
|
||||||
|
logger.info(f"Processing {child_name}")
|
||||||
|
|
||||||
|
standings_data = child.get('standings', {}).get('entries', [])
|
||||||
|
|
||||||
|
for entry in standings_data:
|
||||||
|
team_data = entry.get('team', {})
|
||||||
|
stats = entry.get('stats', [])
|
||||||
|
|
||||||
|
team_name = team_data.get('displayName', 'Unknown')
|
||||||
|
team_abbr = team_data.get('abbreviation', 'Unknown')
|
||||||
|
|
||||||
|
# Extract record from stats
|
||||||
|
wins = 0
|
||||||
|
losses = 0
|
||||||
|
ties = 0
|
||||||
|
win_percentage = 0.0
|
||||||
|
|
||||||
|
# First pass: collect all stat values
|
||||||
|
games_played = 0
|
||||||
|
for stat in stats:
|
||||||
|
stat_type = stat.get('type', '')
|
||||||
|
stat_value = stat.get('value', 0)
|
||||||
|
|
||||||
|
if stat_type == 'wins':
|
||||||
|
wins = int(stat_value)
|
||||||
|
elif stat_type == 'losses':
|
||||||
|
losses = int(stat_value)
|
||||||
|
elif stat_type == 'ties':
|
||||||
|
ties = int(stat_value)
|
||||||
|
elif stat_type == 'winpercent':
|
||||||
|
win_percentage = float(stat_value)
|
||||||
|
# NHL specific stats
|
||||||
|
elif stat_type == 'overtimelosses' and league_key == 'nhl':
|
||||||
|
ties = int(stat_value) # NHL uses overtime losses as ties
|
||||||
|
elif stat_type == 'gamesplayed' and league_key == 'nhl':
|
||||||
|
games_played = float(stat_value)
|
||||||
|
|
||||||
|
# Second pass: calculate win percentage for NHL if not already set
|
||||||
|
if league_key == 'nhl' and win_percentage == 0.0 and games_played > 0:
|
||||||
|
win_percentage = wins / games_played
|
||||||
|
|
||||||
|
# Create record summary
|
||||||
|
if ties > 0:
|
||||||
|
record_summary = f"{wins}-{losses}-{ties}"
|
||||||
|
else:
|
||||||
|
record_summary = f"{wins}-{losses}"
|
||||||
|
|
||||||
|
standings.append({
|
||||||
|
'name': team_name,
|
||||||
|
'abbreviation': team_abbr,
|
||||||
|
'wins': wins,
|
||||||
|
'losses': losses,
|
||||||
|
'ties': ties,
|
||||||
|
'win_percentage': win_percentage,
|
||||||
|
'record_summary': record_summary,
|
||||||
|
'division': child_name
|
||||||
|
})
|
||||||
|
else:
|
||||||
|
logger.warning(f"No standings or children data found for {league_key}")
|
||||||
|
return []
|
||||||
|
|
||||||
|
# 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']]
|
||||||
|
|
||||||
|
# Cache the results
|
||||||
|
cache_data = {
|
||||||
|
'standings': top_teams,
|
||||||
|
'timestamp': time.time(),
|
||||||
|
'league': league_key,
|
||||||
|
'season': params['season'],
|
||||||
|
'level': params['level']
|
||||||
|
}
|
||||||
|
self.cache_manager.save_cache(cache_key, cache_data)
|
||||||
|
|
||||||
|
logger.info(f"Fetched and cached {len(top_teams)} teams for {league_key} standings")
|
||||||
|
return top_teams
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error fetching standings for {league_key}: {e}")
|
||||||
|
return []
|
||||||
|
|
||||||
def _fetch_team_record(self, team_abbr: str, league_config: Dict[str, Any]) -> Optional[Dict[str, Any]]:
|
def _fetch_team_record(self, team_abbr: str, league_config: Dict[str, Any]) -> Optional[Dict[str, Any]]:
|
||||||
"""Fetch individual team record from ESPN API with caching."""
|
"""Fetch individual team record from ESPN API with caching."""
|
||||||
league = league_config['league']
|
league = league_config['league']
|
||||||
@@ -592,11 +809,11 @@ class LeaderboardManager:
|
|||||||
# For other leagues, show position
|
# For other leagues, show position
|
||||||
number_text = f"{i+1}."
|
number_text = f"{i+1}."
|
||||||
|
|
||||||
number_bbox = self.fonts['large'].getbbox(number_text)
|
number_bbox = self.fonts['xlarge'].getbbox(number_text)
|
||||||
number_width = number_bbox[2] - number_bbox[0]
|
number_width = number_bbox[2] - number_bbox[0]
|
||||||
number_height = number_bbox[3] - number_bbox[1]
|
number_height = number_bbox[3] - number_bbox[1]
|
||||||
number_y = (height - number_height) // 2
|
number_y = (height - number_height) // 2
|
||||||
draw.text((team_x, number_y), number_text, font=self.fonts['large'], fill=(255, 255, 0))
|
draw.text((team_x, number_y), number_text, font=self.fonts['xlarge'], fill=(255, 255, 0))
|
||||||
|
|
||||||
# Draw team logo (95% of display height, centered vertically)
|
# Draw team logo (95% of display height, centered vertically)
|
||||||
team_logo = self._get_team_logo(team['abbreviation'], league_config['logo_dir'])
|
team_logo = self._get_team_logo(team['abbreviation'], league_config['logo_dir'])
|
||||||
@@ -611,24 +828,24 @@ class LeaderboardManager:
|
|||||||
|
|
||||||
# Draw team abbreviation after the logo (centered vertically)
|
# Draw team abbreviation after the logo (centered vertically)
|
||||||
team_text = team['abbreviation']
|
team_text = team['abbreviation']
|
||||||
text_bbox = self.fonts['medium'].getbbox(team_text)
|
text_bbox = self.fonts['large'].getbbox(team_text)
|
||||||
text_width = text_bbox[2] - text_bbox[0]
|
text_width = text_bbox[2] - text_bbox[0]
|
||||||
text_height = text_bbox[3] - text_bbox[1]
|
text_height = text_bbox[3] - text_bbox[1]
|
||||||
text_x = logo_x + logo_size + 4
|
text_x = logo_x + logo_size + 4
|
||||||
text_y = (height - text_height) // 2
|
text_y = (height - text_height) // 2
|
||||||
draw.text((text_x, text_y), team_text, font=self.fonts['medium'], fill=(255, 255, 255))
|
draw.text((text_x, text_y), team_text, font=self.fonts['large'], fill=(255, 255, 255))
|
||||||
|
|
||||||
# Calculate total width used by this team
|
# Calculate total width used by this team
|
||||||
team_width = number_width + 4 + logo_size + 4 + text_width + 12 # 12px spacing to next team
|
team_width = number_width + 4 + logo_size + 4 + text_width + 12 # 12px spacing to next team
|
||||||
else:
|
else:
|
||||||
# Fallback if no logo - draw team abbreviation after bold number (centered vertically)
|
# Fallback if no logo - draw team abbreviation after bold number (centered vertically)
|
||||||
team_text = team['abbreviation']
|
team_text = team['abbreviation']
|
||||||
text_bbox = self.fonts['medium'].getbbox(team_text)
|
text_bbox = self.fonts['large'].getbbox(team_text)
|
||||||
text_width = text_bbox[2] - text_bbox[0]
|
text_width = text_bbox[2] - text_bbox[0]
|
||||||
text_height = text_bbox[3] - text_bbox[1]
|
text_height = text_bbox[3] - text_bbox[1]
|
||||||
text_x = team_x + number_width + 4
|
text_x = team_x + number_width + 4
|
||||||
text_y = (height - text_height) // 2
|
text_y = (height - text_height) // 2
|
||||||
draw.text((text_x, text_y), team_text, font=self.fonts['medium'], fill=(255, 255, 255))
|
draw.text((text_x, text_y), team_text, font=self.fonts['large'], fill=(255, 255, 255))
|
||||||
|
|
||||||
# Calculate total width used by this team
|
# Calculate total width used by this team
|
||||||
team_width = number_width + 4 + text_width + 12 # 12px spacing to next team
|
team_width = number_width + 4 + text_width + 12 # 12px spacing to next team
|
||||||
@@ -640,37 +857,119 @@ class LeaderboardManager:
|
|||||||
current_x += teams_width + 20 # Teams width + spacing
|
current_x += teams_width + 20 # Teams width + spacing
|
||||||
current_x += spacing # Add spacing between leagues
|
current_x += spacing # Add spacing between leagues
|
||||||
|
|
||||||
# Calculate dynamic duration based on number of teams and content
|
# Set total scroll width for dynamic duration calculation
|
||||||
if self.dynamic_duration_enabled:
|
|
||||||
# Count total teams across all leagues
|
|
||||||
total_teams = sum(len(league_data['teams']) for league_data in self.leaderboard_data)
|
|
||||||
total_leagues = len(self.leaderboard_data)
|
|
||||||
|
|
||||||
# Use configurable time per team and league
|
|
||||||
time_per_team = self.time_per_team
|
|
||||||
time_per_league = self.time_per_league
|
|
||||||
|
|
||||||
# Calculate base duration from teams and leagues
|
|
||||||
base_duration = (total_teams * time_per_team) + (total_leagues * time_per_league)
|
|
||||||
|
|
||||||
# Add scroll time based on content width
|
|
||||||
scroll_time = (total_width / self.scroll_speed) * self.scroll_delay
|
|
||||||
|
|
||||||
# Use the maximum of base duration and scroll time, with buffer
|
|
||||||
calculated_duration = max(base_duration, scroll_time) + self.duration_buffer
|
|
||||||
|
|
||||||
# Apply min/max constraints
|
|
||||||
self.dynamic_duration = max(self.min_duration, min(self.max_duration, calculated_duration))
|
|
||||||
|
|
||||||
logger.info(f"Calculated dynamic duration: {self.dynamic_duration:.1f}s for {total_teams} teams across {total_leagues} leagues (width: {total_width})")
|
|
||||||
|
|
||||||
self.total_scroll_width = total_width
|
self.total_scroll_width = total_width
|
||||||
|
|
||||||
|
# Calculate dynamic duration using proper scroll-based calculation
|
||||||
|
if self.dynamic_duration_enabled:
|
||||||
|
self.calculate_dynamic_duration()
|
||||||
logger.info(f"Created leaderboard image with width {total_width}")
|
logger.info(f"Created leaderboard image with width {total_width}")
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Error creating leaderboard image: {e}")
|
logger.error(f"Error creating leaderboard image: {e}")
|
||||||
self.leaderboard_image = None
|
self.leaderboard_image = None
|
||||||
|
|
||||||
|
def calculate_dynamic_duration(self):
|
||||||
|
"""Calculate the exact time needed to display all leaderboard content"""
|
||||||
|
logger.debug(f"calculate_dynamic_duration called - dynamic_duration_enabled: {self.dynamic_duration_enabled}, total_scroll_width: {self.total_scroll_width}")
|
||||||
|
|
||||||
|
# If dynamic duration is disabled, use fixed duration from config
|
||||||
|
if not self.dynamic_duration_enabled:
|
||||||
|
self.dynamic_duration = self.leaderboard_config.get('display_duration', 60)
|
||||||
|
logger.debug(f"Dynamic duration disabled, using fixed duration: {self.dynamic_duration}s")
|
||||||
|
return
|
||||||
|
|
||||||
|
if not self.total_scroll_width:
|
||||||
|
self.dynamic_duration = self.min_duration # Use configured minimum
|
||||||
|
logger.debug(f"total_scroll_width is 0, using minimum duration: {self.min_duration}s")
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Get display width (assume full width of display)
|
||||||
|
display_width = getattr(self.display_manager, 'matrix', None)
|
||||||
|
if display_width:
|
||||||
|
display_width = display_width.width
|
||||||
|
else:
|
||||||
|
display_width = 128 # Default to 128 if not available
|
||||||
|
|
||||||
|
# Calculate total scroll distance needed
|
||||||
|
# For leaderboard, we need to scroll the entire content width plus display width
|
||||||
|
# to ensure all content is visible from start to finish
|
||||||
|
total_scroll_distance = display_width + self.total_scroll_width
|
||||||
|
|
||||||
|
# Calculate time based on scroll speed and delay
|
||||||
|
# scroll_speed = pixels per frame, scroll_delay = seconds per frame
|
||||||
|
frames_needed = total_scroll_distance / self.scroll_speed
|
||||||
|
total_time = frames_needed * self.scroll_delay
|
||||||
|
|
||||||
|
# Add buffer time for smooth cycling (configurable %)
|
||||||
|
buffer_time = total_time * self.duration_buffer
|
||||||
|
|
||||||
|
# If looping is enabled, ensure we complete at least one full cycle
|
||||||
|
# and add extra time to ensure we don't cut off mid-scroll
|
||||||
|
if self.loop:
|
||||||
|
# Add extra buffer for looping to ensure smooth transition
|
||||||
|
# Use a more reasonable buffer to prevent excessive looping
|
||||||
|
loop_buffer = total_time * 0.15 # 15% extra for looping (reduced from 30%)
|
||||||
|
calculated_duration = int(total_time + buffer_time + loop_buffer)
|
||||||
|
logger.debug(f"Looping enabled, added {loop_buffer:.2f}s loop buffer")
|
||||||
|
else:
|
||||||
|
# Even without looping, add extra buffer to ensure complete display
|
||||||
|
extra_buffer = total_time * 0.1 # 10% extra to ensure complete content display (reduced from 15%)
|
||||||
|
calculated_duration = int(total_time + buffer_time + extra_buffer)
|
||||||
|
logger.debug(f"No looping, added {extra_buffer:.2f}s extra buffer for complete display")
|
||||||
|
|
||||||
|
# Apply configured min/max limits
|
||||||
|
if calculated_duration < self.min_duration:
|
||||||
|
self.dynamic_duration = self.min_duration
|
||||||
|
logger.debug(f"Duration capped to minimum: {self.min_duration}s")
|
||||||
|
elif calculated_duration > self.max_duration:
|
||||||
|
self.dynamic_duration = self.max_duration
|
||||||
|
logger.debug(f"Duration capped to maximum: {self.max_duration}s")
|
||||||
|
else:
|
||||||
|
self.dynamic_duration = calculated_duration
|
||||||
|
|
||||||
|
# Additional safety check: if the calculated duration seems too short for the content,
|
||||||
|
# ensure we have enough time to display all content properly
|
||||||
|
if self.dynamic_duration < 30 and self.total_scroll_width > 800:
|
||||||
|
# If we have a lot of content but short duration, increase it
|
||||||
|
self.dynamic_duration = max(30, int(self.total_scroll_width / 30)) # At least 30s or 1s per 30px
|
||||||
|
logger.debug(f"Adjusted duration for large content: {self.dynamic_duration}s")
|
||||||
|
|
||||||
|
logger.debug(f"Leaderboard dynamic duration calculation:")
|
||||||
|
logger.debug(f" Display width: {display_width}px")
|
||||||
|
logger.debug(f" Content width: {self.total_scroll_width}px")
|
||||||
|
logger.debug(f" Total scroll distance: {total_scroll_distance}px")
|
||||||
|
logger.debug(f" Scroll speed: {self.scroll_speed}px/frame")
|
||||||
|
logger.debug(f" Scroll delay: {self.scroll_delay}s/frame")
|
||||||
|
logger.debug(f" Frames needed: {frames_needed:.1f}")
|
||||||
|
logger.debug(f" Base time: {total_time:.2f}s")
|
||||||
|
logger.debug(f" Buffer time: {buffer_time:.2f}s ({self.duration_buffer*100}%)")
|
||||||
|
logger.debug(f" Looping enabled: {self.loop}")
|
||||||
|
logger.debug(f" Calculated duration: {calculated_duration}s")
|
||||||
|
logger.debug(f" Final duration: {self.dynamic_duration}s")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error calculating dynamic duration: {e}")
|
||||||
|
self.dynamic_duration = self.min_duration # Use configured minimum as fallback
|
||||||
|
|
||||||
|
def get_dynamic_duration(self) -> int:
|
||||||
|
"""Get the calculated dynamic duration for display"""
|
||||||
|
# If we don't have a valid dynamic duration yet (total_scroll_width is 0),
|
||||||
|
# try to update the data first
|
||||||
|
if self.total_scroll_width == 0 and self.is_enabled:
|
||||||
|
logger.debug("get_dynamic_duration called but total_scroll_width is 0, attempting update...")
|
||||||
|
try:
|
||||||
|
# Force an update to get the data and calculate proper duration
|
||||||
|
# Bypass the update interval check for duration calculation
|
||||||
|
self.update()
|
||||||
|
logger.debug(f"Force update completed, total_scroll_width: {self.total_scroll_width}px")
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error updating leaderboard for dynamic duration: {e}")
|
||||||
|
|
||||||
|
logger.debug(f"get_dynamic_duration called, returning: {self.dynamic_duration}s")
|
||||||
|
return self.dynamic_duration
|
||||||
|
|
||||||
def update(self) -> None:
|
def update(self) -> None:
|
||||||
"""Update leaderboard data."""
|
"""Update leaderboard data."""
|
||||||
current_time = time.time()
|
current_time = time.time()
|
||||||
|
|||||||
@@ -1349,13 +1349,13 @@ class OddsTickerManager:
|
|||||||
# and add extra time to ensure we don't cut off mid-scroll
|
# and add extra time to ensure we don't cut off mid-scroll
|
||||||
if self.loop:
|
if self.loop:
|
||||||
# Add extra buffer for looping to ensure smooth transition
|
# Add extra buffer for looping to ensure smooth transition
|
||||||
# Use a larger buffer to ensure complete content display
|
# Use a more reasonable buffer to prevent excessive looping
|
||||||
loop_buffer = total_time * 0.3 # 30% extra for looping (increased from 20%)
|
loop_buffer = total_time * 0.15 # 15% extra for looping (reduced from 30%)
|
||||||
calculated_duration = int(total_time + buffer_time + loop_buffer)
|
calculated_duration = int(total_time + buffer_time + loop_buffer)
|
||||||
logger.debug(f"Looping enabled, added {loop_buffer:.2f}s loop buffer")
|
logger.debug(f"Looping enabled, added {loop_buffer:.2f}s loop buffer")
|
||||||
else:
|
else:
|
||||||
# Even without looping, add extra buffer to ensure complete display
|
# Even without looping, add extra buffer to ensure complete display
|
||||||
extra_buffer = total_time * 0.15 # 15% extra to ensure complete content display
|
extra_buffer = total_time * 0.1 # 10% extra to ensure complete content display (reduced from 15%)
|
||||||
calculated_duration = int(total_time + buffer_time + extra_buffer)
|
calculated_duration = int(total_time + buffer_time + extra_buffer)
|
||||||
logger.debug(f"No looping, added {extra_buffer:.2f}s extra buffer for complete display")
|
logger.debug(f"No looping, added {extra_buffer:.2f}s extra buffer for complete display")
|
||||||
|
|
||||||
@@ -1371,9 +1371,9 @@ class OddsTickerManager:
|
|||||||
|
|
||||||
# Additional safety check: if the calculated duration seems too short for the content,
|
# Additional safety check: if the calculated duration seems too short for the content,
|
||||||
# ensure we have enough time to display all content properly
|
# ensure we have enough time to display all content properly
|
||||||
if self.dynamic_duration < 45 and self.total_scroll_width > 500:
|
if self.dynamic_duration < 30 and self.total_scroll_width > 800:
|
||||||
# If we have a lot of content but short duration, increase it
|
# If we have a lot of content but short duration, increase it
|
||||||
self.dynamic_duration = max(45, int(self.total_scroll_width / 20)) # At least 45s or 1s per 20px
|
self.dynamic_duration = max(30, int(self.total_scroll_width / 30)) # At least 30s or 1s per 30px
|
||||||
logger.debug(f"Adjusted duration for large content: {self.dynamic_duration}s")
|
logger.debug(f"Adjusted duration for large content: {self.dynamic_duration}s")
|
||||||
|
|
||||||
logger.debug(f"Odds ticker dynamic duration calculation:")
|
logger.debug(f"Odds ticker dynamic duration calculation:")
|
||||||
|
|||||||
239
test/test_standings_fetch.py
Normal file
239
test/test_standings_fetch.py
Normal file
@@ -0,0 +1,239 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Test script to verify the standings fetching logic works correctly.
|
||||||
|
This tests the core functionality without requiring the full LED matrix setup.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import requests
|
||||||
|
import json
|
||||||
|
import time
|
||||||
|
from typing import Dict, Any, List
|
||||||
|
|
||||||
|
def fetch_standings_data(league_config: Dict[str, Any]) -> List[Dict[str, Any]]:
|
||||||
|
"""Fetch standings data from ESPN API using the standings endpoint."""
|
||||||
|
league_key = league_config['league']
|
||||||
|
|
||||||
|
try:
|
||||||
|
print(f"Fetching fresh standings data for {league_key}")
|
||||||
|
|
||||||
|
# Build the standings URL with query parameters
|
||||||
|
standings_url = league_config['standings_url']
|
||||||
|
params = {
|
||||||
|
'season': league_config.get('season', 2024),
|
||||||
|
'level': league_config.get('level', 1),
|
||||||
|
'sort': league_config.get('sort', 'winpercent:desc,gamesbehind:asc')
|
||||||
|
}
|
||||||
|
|
||||||
|
print(f"Fetching standings from: {standings_url} with params: {params}")
|
||||||
|
|
||||||
|
response = requests.get(standings_url, params=params, timeout=30)
|
||||||
|
response.raise_for_status()
|
||||||
|
data = response.json()
|
||||||
|
|
||||||
|
standings = []
|
||||||
|
|
||||||
|
# Parse the standings data structure
|
||||||
|
# Check if we have direct standings data or children (divisions/conferences)
|
||||||
|
if 'standings' in data and 'entries' in data['standings']:
|
||||||
|
# Direct standings data (e.g., NFL overall standings)
|
||||||
|
standings_data = data['standings']['entries']
|
||||||
|
print(f"Processing direct standings data with {len(standings_data)} teams")
|
||||||
|
|
||||||
|
for entry in standings_data:
|
||||||
|
team_data = entry.get('team', {})
|
||||||
|
stats = entry.get('stats', [])
|
||||||
|
|
||||||
|
team_name = team_data.get('displayName', 'Unknown')
|
||||||
|
team_abbr = team_data.get('abbreviation', 'Unknown')
|
||||||
|
|
||||||
|
# Extract record from stats
|
||||||
|
wins = 0
|
||||||
|
losses = 0
|
||||||
|
ties = 0
|
||||||
|
win_percentage = 0.0
|
||||||
|
|
||||||
|
for stat in stats:
|
||||||
|
stat_type = stat.get('type', '')
|
||||||
|
stat_value = stat.get('value', 0)
|
||||||
|
|
||||||
|
if stat_type == 'wins':
|
||||||
|
wins = int(stat_value)
|
||||||
|
elif stat_type == 'losses':
|
||||||
|
losses = int(stat_value)
|
||||||
|
elif stat_type == 'ties':
|
||||||
|
ties = int(stat_value)
|
||||||
|
elif stat_type == 'winpercent':
|
||||||
|
win_percentage = float(stat_value)
|
||||||
|
|
||||||
|
# Create record summary
|
||||||
|
if ties > 0:
|
||||||
|
record_summary = f"{wins}-{losses}-{ties}"
|
||||||
|
else:
|
||||||
|
record_summary = f"{wins}-{losses}"
|
||||||
|
|
||||||
|
standings.append({
|
||||||
|
'name': team_name,
|
||||||
|
'abbreviation': team_abbr,
|
||||||
|
'wins': wins,
|
||||||
|
'losses': losses,
|
||||||
|
'ties': ties,
|
||||||
|
'win_percentage': win_percentage,
|
||||||
|
'record_summary': record_summary,
|
||||||
|
'division': 'Overall'
|
||||||
|
})
|
||||||
|
|
||||||
|
elif 'children' in data:
|
||||||
|
# Children structure (divisions/conferences)
|
||||||
|
children = data.get('children', [])
|
||||||
|
print(f"Processing {len(children)} divisions/conferences")
|
||||||
|
|
||||||
|
for child in children:
|
||||||
|
child_name = child.get('displayName', 'Unknown')
|
||||||
|
print(f"Processing {child_name}")
|
||||||
|
|
||||||
|
standings_data = child.get('standings', {}).get('entries', [])
|
||||||
|
|
||||||
|
for entry in standings_data:
|
||||||
|
team_data = entry.get('team', {})
|
||||||
|
stats = entry.get('stats', [])
|
||||||
|
|
||||||
|
team_name = team_data.get('displayName', 'Unknown')
|
||||||
|
team_abbr = team_data.get('abbreviation', 'Unknown')
|
||||||
|
|
||||||
|
# Extract record from stats
|
||||||
|
wins = 0
|
||||||
|
losses = 0
|
||||||
|
ties = 0
|
||||||
|
win_percentage = 0.0
|
||||||
|
|
||||||
|
for stat in stats:
|
||||||
|
stat_type = stat.get('type', '')
|
||||||
|
stat_value = stat.get('value', 0)
|
||||||
|
|
||||||
|
if stat_type == 'wins':
|
||||||
|
wins = int(stat_value)
|
||||||
|
elif stat_type == 'losses':
|
||||||
|
losses = int(stat_value)
|
||||||
|
elif stat_type == 'ties':
|
||||||
|
ties = int(stat_value)
|
||||||
|
elif stat_type == 'winpercent':
|
||||||
|
win_percentage = float(stat_value)
|
||||||
|
|
||||||
|
# Create record summary
|
||||||
|
if ties > 0:
|
||||||
|
record_summary = f"{wins}-{losses}-{ties}"
|
||||||
|
else:
|
||||||
|
record_summary = f"{wins}-{losses}"
|
||||||
|
|
||||||
|
standings.append({
|
||||||
|
'name': team_name,
|
||||||
|
'abbreviation': team_abbr,
|
||||||
|
'wins': wins,
|
||||||
|
'losses': losses,
|
||||||
|
'ties': ties,
|
||||||
|
'win_percentage': win_percentage,
|
||||||
|
'record_summary': record_summary,
|
||||||
|
'division': child_name
|
||||||
|
})
|
||||||
|
else:
|
||||||
|
print(f"No standings or children data found for {league_key}")
|
||||||
|
return []
|
||||||
|
|
||||||
|
# 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']]
|
||||||
|
|
||||||
|
print(f"Fetched and processed {len(top_teams)} teams for {league_key} standings")
|
||||||
|
return top_teams
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error fetching standings for {league_key}: {e}")
|
||||||
|
return []
|
||||||
|
|
||||||
|
def test_standings_fetch():
|
||||||
|
"""Test the standings fetching functionality."""
|
||||||
|
print("Testing Standings Fetching Logic")
|
||||||
|
print("=" * 50)
|
||||||
|
|
||||||
|
# Test configurations
|
||||||
|
test_configs = [
|
||||||
|
{
|
||||||
|
'name': 'NFL',
|
||||||
|
'config': {
|
||||||
|
'league': 'nfl',
|
||||||
|
'standings_url': 'https://site.api.espn.com/apis/v2/sports/football/nfl/standings',
|
||||||
|
'top_teams': 5,
|
||||||
|
'season': 2025,
|
||||||
|
'level': 1,
|
||||||
|
'sort': 'winpercent:desc,gamesbehind:asc'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'MLB',
|
||||||
|
'config': {
|
||||||
|
'league': 'mlb',
|
||||||
|
'standings_url': 'https://site.api.espn.com/apis/v2/sports/baseball/mlb/standings',
|
||||||
|
'top_teams': 5,
|
||||||
|
'season': 2025,
|
||||||
|
'level': 1,
|
||||||
|
'sort': 'winpercent:desc,gamesbehind:asc'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'NHL',
|
||||||
|
'config': {
|
||||||
|
'league': 'nhl',
|
||||||
|
'standings_url': 'https://site.api.espn.com/apis/v2/sports/hockey/nhl/standings',
|
||||||
|
'top_teams': 5,
|
||||||
|
'season': 2025,
|
||||||
|
'level': 1,
|
||||||
|
'sort': 'winpercent:desc,gamesbehind:asc'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'NCAA Baseball',
|
||||||
|
'config': {
|
||||||
|
'league': 'college-baseball',
|
||||||
|
'standings_url': 'https://site.api.espn.com/apis/v2/sports/baseball/college-baseball/standings',
|
||||||
|
'top_teams': 5,
|
||||||
|
'season': 2025,
|
||||||
|
'level': 1,
|
||||||
|
'sort': 'winpercent:desc,gamesbehind:asc'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
results = []
|
||||||
|
|
||||||
|
for test_config in test_configs:
|
||||||
|
print(f"\n--- Testing {test_config['name']} ---")
|
||||||
|
|
||||||
|
standings = fetch_standings_data(test_config['config'])
|
||||||
|
|
||||||
|
if standings:
|
||||||
|
print(f"✓ Successfully fetched {len(standings)} teams")
|
||||||
|
print(f"Top {len(standings)} teams:")
|
||||||
|
for i, team in enumerate(standings):
|
||||||
|
print(f" {i+1}. {team['name']} ({team['abbreviation']}): {team['record_summary']} ({team['win_percentage']:.3f})")
|
||||||
|
results.append(True)
|
||||||
|
else:
|
||||||
|
print(f"✗ Failed to fetch standings for {test_config['name']}")
|
||||||
|
results.append(False)
|
||||||
|
|
||||||
|
# Summary
|
||||||
|
passed = sum(results)
|
||||||
|
total = len(results)
|
||||||
|
|
||||||
|
print(f"\n=== Test Results ===")
|
||||||
|
print(f"Passed: {passed}/{total}")
|
||||||
|
|
||||||
|
if passed == total:
|
||||||
|
print("✓ All standings fetch tests passed!")
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
print("✗ Some tests failed!")
|
||||||
|
return False
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
success = test_standings_fetch()
|
||||||
|
exit(0 if success else 1)
|
||||||
293
test/test_standings_simple.py
Normal file
293
test/test_standings_simple.py
Normal file
@@ -0,0 +1,293 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Simple test script to verify the ESPN standings endpoints work correctly.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import requests
|
||||||
|
import json
|
||||||
|
|
||||||
|
def test_nfl_standings():
|
||||||
|
"""Test NFL standings endpoint with corrected parsing."""
|
||||||
|
print("\n=== Testing NFL Standings ===")
|
||||||
|
|
||||||
|
url = "https://site.api.espn.com/apis/v2/sports/football/nfl/standings"
|
||||||
|
params = {
|
||||||
|
'season': 2025,
|
||||||
|
'level': 1,
|
||||||
|
'sort': 'winpercent:desc,gamesbehind:asc'
|
||||||
|
}
|
||||||
|
|
||||||
|
try:
|
||||||
|
response = requests.get(url, params=params, timeout=30)
|
||||||
|
response.raise_for_status()
|
||||||
|
data = response.json()
|
||||||
|
|
||||||
|
print(f"✓ Successfully fetched NFL standings")
|
||||||
|
|
||||||
|
# Check for direct standings data
|
||||||
|
if 'standings' in data and 'entries' in data['standings']:
|
||||||
|
standings_data = data['standings']['entries']
|
||||||
|
print(f" Found {len(standings_data)} teams in direct standings")
|
||||||
|
|
||||||
|
# Show top 5 teams
|
||||||
|
print(f" Top 5 teams:")
|
||||||
|
for i, entry in enumerate(standings_data[:5]):
|
||||||
|
team_data = entry.get('team', {})
|
||||||
|
team_name = team_data.get('displayName', 'Unknown')
|
||||||
|
team_abbr = team_data.get('abbreviation', 'Unknown')
|
||||||
|
|
||||||
|
# Get record
|
||||||
|
wins = 0
|
||||||
|
losses = 0
|
||||||
|
ties = 0
|
||||||
|
win_percentage = 0.0
|
||||||
|
|
||||||
|
for stat in entry.get('stats', []):
|
||||||
|
stat_type = stat.get('type', '')
|
||||||
|
stat_value = stat.get('value', 0)
|
||||||
|
|
||||||
|
if stat_type == 'wins':
|
||||||
|
wins = int(stat_value)
|
||||||
|
elif stat_type == 'losses':
|
||||||
|
losses = int(stat_value)
|
||||||
|
elif stat_type == 'ties':
|
||||||
|
ties = int(stat_value)
|
||||||
|
elif stat_type == 'winpercent':
|
||||||
|
win_percentage = float(stat_value)
|
||||||
|
|
||||||
|
record = f"{wins}-{losses}" if ties == 0 else f"{wins}-{losses}-{ties}"
|
||||||
|
print(f" {i+1}. {team_name} ({team_abbr}): {record} ({win_percentage:.3f})")
|
||||||
|
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
print(" ✗ No direct standings data found")
|
||||||
|
return False
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"✗ Error testing NFL standings: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def test_mlb_standings():
|
||||||
|
"""Test MLB standings endpoint with corrected parsing."""
|
||||||
|
print("\n=== Testing MLB Standings ===")
|
||||||
|
|
||||||
|
url = "https://site.api.espn.com/apis/v2/sports/baseball/mlb/standings"
|
||||||
|
params = {
|
||||||
|
'season': 2025,
|
||||||
|
'level': 1,
|
||||||
|
'sort': 'winpercent:desc,gamesbehind:asc'
|
||||||
|
}
|
||||||
|
|
||||||
|
try:
|
||||||
|
response = requests.get(url, params=params, timeout=30)
|
||||||
|
response.raise_for_status()
|
||||||
|
data = response.json()
|
||||||
|
|
||||||
|
print(f"✓ Successfully fetched MLB standings")
|
||||||
|
|
||||||
|
# Check for direct standings data
|
||||||
|
if 'standings' in data and 'entries' in data['standings']:
|
||||||
|
standings_data = data['standings']['entries']
|
||||||
|
print(f" Found {len(standings_data)} teams in direct standings")
|
||||||
|
|
||||||
|
# Show top 5 teams
|
||||||
|
print(f" Top 5 teams:")
|
||||||
|
for i, entry in enumerate(standings_data[:5]):
|
||||||
|
team_data = entry.get('team', {})
|
||||||
|
team_name = team_data.get('displayName', 'Unknown')
|
||||||
|
team_abbr = team_data.get('abbreviation', 'Unknown')
|
||||||
|
|
||||||
|
# Get record
|
||||||
|
wins = 0
|
||||||
|
losses = 0
|
||||||
|
ties = 0
|
||||||
|
win_percentage = 0.0
|
||||||
|
|
||||||
|
for stat in entry.get('stats', []):
|
||||||
|
stat_type = stat.get('type', '')
|
||||||
|
stat_value = stat.get('value', 0)
|
||||||
|
|
||||||
|
if stat_type == 'wins':
|
||||||
|
wins = int(stat_value)
|
||||||
|
elif stat_type == 'losses':
|
||||||
|
losses = int(stat_value)
|
||||||
|
elif stat_type == 'ties':
|
||||||
|
ties = int(stat_value)
|
||||||
|
elif stat_type == 'winpercent':
|
||||||
|
win_percentage = float(stat_value)
|
||||||
|
|
||||||
|
record = f"{wins}-{losses}" if ties == 0 else f"{wins}-{losses}-{ties}"
|
||||||
|
print(f" {i+1}. {team_name} ({team_abbr}): {record} ({win_percentage:.3f})")
|
||||||
|
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
print(" ✗ No direct standings data found")
|
||||||
|
return False
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"✗ Error testing MLB standings: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def test_nhl_standings():
|
||||||
|
"""Test NHL standings endpoint with corrected parsing."""
|
||||||
|
print("\n=== Testing NHL Standings ===")
|
||||||
|
|
||||||
|
url = "https://site.api.espn.com/apis/v2/sports/hockey/nhl/standings"
|
||||||
|
params = {
|
||||||
|
'season': 2025,
|
||||||
|
'level': 1,
|
||||||
|
'sort': 'winpercent:desc,gamesbehind:asc'
|
||||||
|
}
|
||||||
|
|
||||||
|
try:
|
||||||
|
response = requests.get(url, params=params, timeout=30)
|
||||||
|
response.raise_for_status()
|
||||||
|
data = response.json()
|
||||||
|
|
||||||
|
print(f"✓ Successfully fetched NHL standings")
|
||||||
|
|
||||||
|
# Check for direct standings data
|
||||||
|
if 'standings' in data and 'entries' in data['standings']:
|
||||||
|
standings_data = data['standings']['entries']
|
||||||
|
print(f" Found {len(standings_data)} teams in direct standings")
|
||||||
|
|
||||||
|
# Show top 5 teams
|
||||||
|
print(f" Top 5 teams:")
|
||||||
|
for i, entry in enumerate(standings_data[:5]):
|
||||||
|
team_data = entry.get('team', {})
|
||||||
|
team_name = team_data.get('displayName', 'Unknown')
|
||||||
|
team_abbr = team_data.get('abbreviation', 'Unknown')
|
||||||
|
|
||||||
|
# Get record with NHL-specific parsing
|
||||||
|
wins = 0
|
||||||
|
losses = 0
|
||||||
|
ties = 0
|
||||||
|
win_percentage = 0.0
|
||||||
|
games_played = 0
|
||||||
|
|
||||||
|
# First pass: collect all stat values
|
||||||
|
for stat in entry.get('stats', []):
|
||||||
|
stat_type = stat.get('type', '')
|
||||||
|
stat_value = stat.get('value', 0)
|
||||||
|
|
||||||
|
if stat_type == 'wins':
|
||||||
|
wins = int(stat_value)
|
||||||
|
elif stat_type == 'losses':
|
||||||
|
losses = int(stat_value)
|
||||||
|
elif stat_type == 'ties':
|
||||||
|
ties = int(stat_value)
|
||||||
|
elif stat_type == 'winpercent':
|
||||||
|
win_percentage = float(stat_value)
|
||||||
|
# NHL specific stats
|
||||||
|
elif stat_type == 'overtimelosses':
|
||||||
|
ties = int(stat_value) # NHL uses overtime losses as ties
|
||||||
|
elif stat_type == 'gamesplayed':
|
||||||
|
games_played = float(stat_value)
|
||||||
|
|
||||||
|
# Second pass: calculate win percentage for NHL if not already set
|
||||||
|
if win_percentage == 0.0 and games_played > 0:
|
||||||
|
win_percentage = wins / games_played
|
||||||
|
|
||||||
|
record = f"{wins}-{losses}" if ties == 0 else f"{wins}-{losses}-{ties}"
|
||||||
|
print(f" {i+1}. {team_name} ({team_abbr}): {record} ({win_percentage:.3f})")
|
||||||
|
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
print(" ✗ No direct standings data found")
|
||||||
|
return False
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"✗ Error testing NHL standings: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def test_ncaa_baseball_standings():
|
||||||
|
"""Test NCAA Baseball standings endpoint with corrected parsing."""
|
||||||
|
print("\n=== Testing NCAA Baseball Standings ===")
|
||||||
|
|
||||||
|
url = "https://site.api.espn.com/apis/v2/sports/baseball/college-baseball/standings"
|
||||||
|
params = {
|
||||||
|
'season': 2025,
|
||||||
|
'level': 1,
|
||||||
|
'sort': 'winpercent:desc,gamesbehind:asc'
|
||||||
|
}
|
||||||
|
|
||||||
|
try:
|
||||||
|
response = requests.get(url, params=params, timeout=30)
|
||||||
|
response.raise_for_status()
|
||||||
|
data = response.json()
|
||||||
|
|
||||||
|
print(f"✓ Successfully fetched NCAA Baseball standings")
|
||||||
|
|
||||||
|
# Check for direct standings data
|
||||||
|
if 'standings' in data and 'entries' in data['standings']:
|
||||||
|
standings_data = data['standings']['entries']
|
||||||
|
print(f" Found {len(standings_data)} teams in direct standings")
|
||||||
|
|
||||||
|
# Show top 5 teams
|
||||||
|
print(f" Top 5 teams:")
|
||||||
|
for i, entry in enumerate(standings_data[:5]):
|
||||||
|
team_data = entry.get('team', {})
|
||||||
|
team_name = team_data.get('displayName', 'Unknown')
|
||||||
|
team_abbr = team_data.get('abbreviation', 'Unknown')
|
||||||
|
|
||||||
|
# Get record
|
||||||
|
wins = 0
|
||||||
|
losses = 0
|
||||||
|
ties = 0
|
||||||
|
win_percentage = 0.0
|
||||||
|
|
||||||
|
for stat in entry.get('stats', []):
|
||||||
|
stat_type = stat.get('type', '')
|
||||||
|
stat_value = stat.get('value', 0)
|
||||||
|
|
||||||
|
if stat_type == 'wins':
|
||||||
|
wins = int(stat_value)
|
||||||
|
elif stat_type == 'losses':
|
||||||
|
losses = int(stat_value)
|
||||||
|
elif stat_type == 'ties':
|
||||||
|
ties = int(stat_value)
|
||||||
|
elif stat_type == 'winpercent':
|
||||||
|
win_percentage = float(stat_value)
|
||||||
|
|
||||||
|
record = f"{wins}-{losses}" if ties == 0 else f"{wins}-{losses}-{ties}"
|
||||||
|
print(f" {i+1}. {team_name} ({team_abbr}): {record} ({win_percentage:.3f})")
|
||||||
|
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
print(" ✗ No direct standings data found")
|
||||||
|
return False
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"✗ Error testing NCAA Baseball standings: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def main():
|
||||||
|
"""Main function to run all tests."""
|
||||||
|
print("ESPN Standings Endpoints Test (Corrected)")
|
||||||
|
print("=" * 50)
|
||||||
|
|
||||||
|
results = []
|
||||||
|
|
||||||
|
# Test individual endpoints
|
||||||
|
results.append(test_nfl_standings())
|
||||||
|
results.append(test_mlb_standings())
|
||||||
|
results.append(test_nhl_standings())
|
||||||
|
results.append(test_ncaa_baseball_standings())
|
||||||
|
|
||||||
|
# Summary
|
||||||
|
passed = sum(results)
|
||||||
|
total = len(results)
|
||||||
|
|
||||||
|
print(f"\n=== Test Results ===")
|
||||||
|
print(f"Passed: {passed}/{total}")
|
||||||
|
|
||||||
|
if passed == total:
|
||||||
|
print("✓ All tests passed!")
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
print("✗ Some tests failed!")
|
||||||
|
return False
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
success = main()
|
||||||
|
exit(0 if success else 1)
|
||||||
Reference in New Issue
Block a user