From 548bc00e004b18117208380ba18243ad3f51a370 Mon Sep 17 00:00:00 2001 From: Chuck <33324927+ChuckBuilds@users.noreply.github.com> Date: Fri, 12 Sep 2025 11:50:51 -0400 Subject: [PATCH] use correct AP Poll top 25 for NCAAFB --- src/leaderboard_manager.py | 109 +++++++++ test/download_ncaa_fb_logos.py | 128 ++++++++++ test/test_ncaa_fb_leaderboard.py | 288 +++++++++++++++++++++++ test/test_updated_leaderboard_manager.py | 145 ++++++++++++ 4 files changed, 670 insertions(+) create mode 100644 test/download_ncaa_fb_logos.py create mode 100644 test/test_ncaa_fb_leaderboard.py create mode 100644 test/test_updated_leaderboard_manager.py diff --git a/src/leaderboard_manager.py b/src/leaderboard_manager.py index 89950c67..5b21de4e 100644 --- a/src/leaderboard_manager.py +++ b/src/leaderboard_manager.py @@ -208,6 +208,10 @@ class LeaderboardManager: logger.info(f"Using cached leaderboard data for {league_key}") return cached_data.get('standings', []) + # Special handling for college football - use rankings endpoint + if league_key == 'college-football': + return self._fetch_ncaa_fb_rankings(league_config) + try: logger.info(f"Fetching fresh leaderboard data for {league_key}") @@ -281,6 +285,111 @@ class LeaderboardManager: logger.error(f"Error fetching standings for {league_config['league']}: {e}") return [] + def _fetch_ncaa_fb_rankings(self, league_config: Dict[str, Any]) -> List[Dict[str, Any]]: + """Fetch NCAA Football rankings from ESPN API using the rankings endpoint.""" + league_key = league_config['league'] + cache_key = f"leaderboard_{league_key}_rankings" + + # 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 rankings data for {league_key}") + return cached_data.get('standings', []) + + try: + logger.info(f"Fetching fresh rankings data for {league_key}") + rankings_url = "https://site.api.espn.com/apis/site/v2/sports/football/college-football/rankings" + + # Get rankings data + response = requests.get(rankings_url, timeout=self.request_timeout) + response.raise_for_status() + data = response.json() + + # Increment API counter for sports data + increment_api_counter('sports', 1) + + logger.info(f"Available rankings: {[rank['name'] for rank in data.get('availableRankings', [])]}") + logger.info(f"Latest season: {data.get('latestSeason', {})}") + logger.info(f"Latest week: {data.get('latestWeek', {})}") + + rankings_data = data.get('rankings', []) + if not rankings_data: + logger.warning("No rankings data found") + return [] + + # Use the first ranking (usually AP Top 25) + first_ranking = rankings_data[0] + ranking_name = first_ranking.get('name', 'Unknown') + ranking_type = first_ranking.get('type', 'Unknown') + teams = first_ranking.get('ranks', []) + + logger.info(f"Using ranking: {ranking_name} ({ranking_type})") + logger.info(f"Found {len(teams)} teams in ranking") + + standings = [] + + # Process each team in the ranking + for team_data in teams: + team_info = team_data.get('team', {}) + team_name = team_info.get('name', 'Unknown') + team_abbr = team_info.get('abbreviation', 'Unknown') + current_rank = team_data.get('current', 0) + record_summary = team_data.get('recordSummary', '0-0') + + logger.debug(f" {current_rank}. {team_name} ({team_abbr}): {record_summary}") + + # Parse the record string (e.g., "12-1", "8-4", "10-2-1") + wins = 0 + losses = 0 + ties = 0 + win_percentage = 0 + + try: + parts = record_summary.split('-') + if len(parts) >= 2: + wins = int(parts[0]) + losses = int(parts[1]) + if len(parts) == 3: + ties = int(parts[2]) + + # Calculate win percentage + total_games = wins + losses + ties + win_percentage = wins / total_games if total_games > 0 else 0 + except (ValueError, IndexError): + logger.warning(f"Could not parse record for {team_name}: {record_summary}") + continue + + standings.append({ + 'name': team_name, + 'abbreviation': team_abbr, + 'rank': current_rank, + 'wins': wins, + 'losses': losses, + 'ties': ties, + 'win_percentage': win_percentage, + 'record_summary': record_summary, + 'ranking_name': ranking_name + }) + + # Limit to top teams (they're already ranked) + top_teams = standings[:league_config['top_teams']] + + # Cache the results + cache_data = { + 'standings': top_teams, + 'timestamp': time.time(), + 'league': league_key, + 'ranking_name': ranking_name + } + self.cache_manager.save_cache(cache_key, cache_data) + + logger.info(f"Fetched and cached {len(top_teams)} teams for {league_key} using {ranking_name}") + return top_teams + + except Exception as e: + logger.error(f"Error fetching rankings for {league_key}: {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 with caching.""" league = league_config['league'] diff --git a/test/download_ncaa_fb_logos.py b/test/download_ncaa_fb_logos.py new file mode 100644 index 00000000..f3cf4915 --- /dev/null +++ b/test/download_ncaa_fb_logos.py @@ -0,0 +1,128 @@ +#!/usr/bin/env python3 +""" +Script to download all NCAA Football team logos from ESPN API +and save them with team abbreviations as filenames. +""" + +import os +import requests +import json +from pathlib import Path +import time + +def create_logo_directory(): + """Create the ncaaFBlogos directory if it doesn't exist.""" + logo_dir = Path("test/ncaaFBlogos") + logo_dir.mkdir(parents=True, exist_ok=True) + return logo_dir + +def fetch_teams_data(): + """Fetch team data from ESPN API.""" + url = "https://site.api.espn.com/apis/site/v2/sports/football/college-football/teams" + + try: + response = requests.get(url, timeout=30) + response.raise_for_status() + return response.json() + except requests.exceptions.RequestException as e: + print(f"Error fetching teams data: {e}") + return None + +def download_logo(url, filepath, team_name): + """Download a logo from URL and save to filepath.""" + try: + response = requests.get(url, timeout=30) + response.raise_for_status() + + with open(filepath, 'wb') as f: + f.write(response.content) + + print(f"✓ Downloaded: {team_name} -> {filepath.name}") + return True + + except requests.exceptions.RequestException as e: + print(f"✗ Failed to download {team_name}: {e}") + return False + +def normalize_abbreviation(abbreviation): + """Normalize team abbreviation to lowercase for filename.""" + return abbreviation.lower() + +def main(): + """Main function to download all NCAA FB team logos.""" + print("Starting NCAA Football logo download...") + + # Create directory + logo_dir = create_logo_directory() + print(f"Created/verified directory: {logo_dir}") + + # Fetch teams data + print("Fetching teams data from ESPN API...") + data = fetch_teams_data() + + if not data: + print("Failed to fetch teams data. Exiting.") + return + + # Extract teams + teams = [] + try: + sports = data.get('sports', []) + for sport in sports: + leagues = sport.get('leagues', []) + for league in leagues: + teams = league.get('teams', []) + break + except (KeyError, IndexError) as e: + print(f"Error parsing teams data: {e}") + return + + print(f"Found {len(teams)} teams") + + # Download logos + downloaded_count = 0 + failed_count = 0 + + for team_data in teams: + team = team_data.get('team', {}) + + # Extract team information + abbreviation = team.get('abbreviation', '') + display_name = team.get('displayName', 'Unknown') + logos = team.get('logos', []) + + if not abbreviation or not logos: + print(f"⚠ Skipping {display_name}: missing abbreviation or logos") + continue + + # Get the default logo (first one is usually default) + logo_url = logos[0].get('href', '') + if not logo_url: + print(f"⚠ Skipping {display_name}: no logo URL") + continue + + # Create filename + filename = f"{normalize_abbreviation(abbreviation)}.png" + filepath = logo_dir / filename + + # Skip if already exists + if filepath.exists(): + print(f"⏭ Skipping {display_name}: {filename} already exists") + continue + + # Download logo + if download_logo(logo_url, filepath, display_name): + downloaded_count += 1 + else: + failed_count += 1 + + # Small delay to be respectful to the API + time.sleep(0.1) + + print(f"\nDownload complete!") + print(f"✓ Successfully downloaded: {downloaded_count} logos") + print(f"✗ Failed downloads: {failed_count}") + print(f"📁 Logos saved in: {logo_dir}") + +if __name__ == "__main__": + main() diff --git a/test/test_ncaa_fb_leaderboard.py b/test/test_ncaa_fb_leaderboard.py new file mode 100644 index 00000000..36a1f8ee --- /dev/null +++ b/test/test_ncaa_fb_leaderboard.py @@ -0,0 +1,288 @@ +#!/usr/bin/env python3 +""" +Test script to demonstrate NCAA Football leaderboard data gathering. +Shows the top 10 NCAA Football teams ranked by win percentage. +This script examines the actual ESPN API response structure to understand +how team records are provided in the teams endpoint. +""" + +import sys +import os +import json +import time +import requests +from typing import Dict, Any, List, Optional + +# Add the src directory to the path so we can import our modules +sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', 'src')) + +from cache_manager import CacheManager +from config_manager import ConfigManager + +class NCAAFBLeaderboardTester: + """Test class to demonstrate NCAA Football leaderboard data gathering.""" + + def __init__(self): + self.cache_manager = CacheManager() + self.config_manager = ConfigManager() + self.request_timeout = 30 + + # NCAA Football configuration (matching the leaderboard manager) + self.ncaa_fb_config = { + 'sport': 'football', + 'league': 'college-football', + 'teams_url': 'https://site.api.espn.com/apis/site/v2/sports/football/college-football/teams', + 'top_teams': 10 # Show top 10 for this test + } + + def examine_api_structure(self) -> None: + """Examine the ESPN API response structure to understand available data.""" + print("Examining ESPN API response structure...") + print("=" * 60) + + try: + response = requests.get(self.ncaa_fb_config['teams_url'], timeout=self.request_timeout) + response.raise_for_status() + data = response.json() + + print(f"API Response Status: {response.status_code}") + print(f"Response Keys: {list(data.keys())}") + + sports = data.get('sports', []) + if sports: + print(f"Sports found: {len(sports)}") + sport = sports[0] + print(f"Sport keys: {list(sport.keys())}") + print(f"Sport name: {sport.get('name', 'Unknown')}") + + leagues = sport.get('leagues', []) + if leagues: + print(f"Leagues found: {len(leagues)}") + league = leagues[0] + print(f"League keys: {list(league.keys())}") + print(f"League name: {league.get('name', 'Unknown')}") + + teams = league.get('teams', []) + if teams: + print(f"Teams found: {len(teams)}") + + # Examine first team structure + first_team = teams[0] + print(f"\nFirst team structure:") + print(f"Team keys: {list(first_team.keys())}") + + team_info = first_team.get('team', {}) + print(f"Team info keys: {list(team_info.keys())}") + print(f"Team name: {team_info.get('name', 'Unknown')}") + print(f"Team abbreviation: {team_info.get('abbreviation', 'Unknown')}") + + # Check for record data + record = team_info.get('record', {}) + print(f"Record keys: {list(record.keys())}") + + if record: + items = record.get('items', []) + print(f"Record items: {len(items)}") + if items: + print(f"First record item: {items[0]}") + + # Check for stats data + stats = team_info.get('stats', []) + print(f"Stats found: {len(stats)}") + if stats: + print("Available stats:") + for stat in stats[:5]: # Show first 5 stats + print(f" {stat.get('name', 'Unknown')}: {stat.get('value', 'Unknown')}") + + # Check for standings data + standings = first_team.get('standings', {}) + print(f"Standings keys: {list(standings.keys())}") + + print(f"\nSample team data structure:") + print(json.dumps(first_team, indent=2)[:1000] + "...") + + except Exception as e: + print(f"Error examining API structure: {e}") + + def fetch_ncaa_fb_rankings_correct(self) -> List[Dict[str, Any]]: + """Fetch NCAA Football rankings from ESPN API using the correct approach.""" + cache_key = "leaderboard_college-football-rankings" + + # Try to get cached data first + cached_data = self.cache_manager.get_cached_data_with_strategy(cache_key, 'leaderboard') + if cached_data: + print("Using cached rankings data for NCAA Football") + return cached_data.get('rankings', []) + + try: + print("Fetching fresh rankings data for NCAA Football") + rankings_url = "https://site.api.espn.com/apis/site/v2/sports/football/college-football/rankings" + print(f"Rankings URL: {rankings_url}") + + # Get rankings data + response = requests.get(rankings_url, timeout=self.request_timeout) + response.raise_for_status() + data = response.json() + + print(f"Available rankings: {[rank['name'] for rank in data.get('availableRankings', [])]}") + print(f"Latest season: {data.get('latestSeason', {})}") + print(f"Latest week: {data.get('latestWeek', {})}") + + rankings_data = data.get('rankings', []) + if not rankings_data: + print("No rankings data found") + return [] + + # Use the first ranking (usually AP Top 25) + first_ranking = rankings_data[0] + ranking_name = first_ranking.get('name', 'Unknown') + ranking_type = first_ranking.get('type', 'Unknown') + teams = first_ranking.get('ranks', []) + + print(f"Using ranking: {ranking_name} ({ranking_type})") + print(f"Found {len(teams)} teams in ranking") + + standings = [] + + # Process each team in the ranking + for i, team_data in enumerate(teams): + team_info = team_data.get('team', {}) + team_name = team_info.get('name', 'Unknown') + team_abbr = team_info.get('abbreviation', 'Unknown') + current_rank = team_data.get('current', 0) + record_summary = team_data.get('recordSummary', '0-0') + + print(f" {current_rank}. {team_name} ({team_abbr}): {record_summary}") + + # Parse the record string (e.g., "12-1", "8-4", "10-2-1") + wins = 0 + losses = 0 + ties = 0 + win_percentage = 0 + + try: + parts = record_summary.split('-') + if len(parts) >= 2: + wins = int(parts[0]) + losses = int(parts[1]) + if len(parts) == 3: + ties = int(parts[2]) + + # Calculate win percentage + total_games = wins + losses + ties + win_percentage = wins / total_games if total_games > 0 else 0 + except (ValueError, IndexError): + print(f" Could not parse record: {record_summary}") + continue + + standings.append({ + 'name': team_name, + 'abbreviation': team_abbr, + 'rank': current_rank, + 'wins': wins, + 'losses': losses, + 'ties': ties, + 'win_percentage': win_percentage, + 'record_summary': record_summary, + 'ranking_name': ranking_name + }) + + # Limit to top teams (they're already ranked) + top_teams = standings[:self.ncaa_fb_config['top_teams']] + + # Cache the results + cache_data = { + 'rankings': top_teams, + 'timestamp': time.time(), + 'league': 'college-football', + 'ranking_name': ranking_name + } + self.cache_manager.save_cache(cache_key, cache_data) + + print(f"Fetched and cached {len(top_teams)} teams for college-football") + return top_teams + + except Exception as e: + print(f"Error fetching rankings for college-football: {e}") + return [] + + def display_standings(self, standings: List[Dict[str, Any]]) -> None: + """Display the standings in a formatted way.""" + if not standings: + print("No standings data available") + return + + ranking_name = standings[0].get('ranking_name', 'Unknown Ranking') if standings else 'Unknown' + + print("\n" + "="*80) + print(f"NCAA FOOTBALL LEADERBOARD - TOP 10 TEAMS ({ranking_name})") + print("="*80) + print(f"{'Rank':<4} {'Team':<25} {'Abbr':<6} {'Record':<12} {'Win %':<8}") + print("-"*80) + + for team in standings: + record_str = f"{team['wins']}-{team['losses']}" + if team['ties'] > 0: + record_str += f"-{team['ties']}" + + win_pct = team['win_percentage'] + win_pct_str = f"{win_pct:.3f}" if win_pct > 0 else "0.000" + + print(f"{team['rank']:<4} {team['name']:<25} {team['abbreviation']:<6} {record_str:<12} {win_pct_str:<8}") + + print("="*80) + print(f"Total teams processed: {len(standings)}") + print(f"Data fetched at: {time.strftime('%Y-%m-%d %H:%M:%S')}") + + def run_test(self) -> None: + """Run the complete test.""" + print("NCAA Football Leaderboard Data Gathering Test") + print("=" * 50) + print("This test demonstrates how the leaderboard manager should gather data:") + print("1. Fetches rankings from ESPN API rankings endpoint") + print("2. Uses poll-based rankings (AP, Coaches, etc.) not win percentage") + print("3. Gets team records from the ranking data") + print("4. Displays top 10 teams with their poll rankings") + print() + + print("\n" + "="*60) + print("FETCHING RANKINGS DATA") + print("="*60) + + # Fetch the rankings using the correct approach + standings = self.fetch_ncaa_fb_rankings_correct() + + # Display the results + self.display_standings(standings) + + # Show some additional info + if standings: + ranking_name = standings[0].get('ranking_name', 'Unknown') + print(f"\nAdditional Information:") + print(f"- API Endpoint: https://site.api.espn.com/apis/site/v2/sports/football/college-football/rankings") + print(f"- Single API call fetches poll-based rankings") + print(f"- Rankings are based on polls, not just win percentage") + print(f"- Data is cached to avoid excessive API calls") + print(f"- Using ranking: {ranking_name}") + + # Show the best team + best_team = standings[0] + print(f"\nCurrent #1 Team: {best_team['name']} ({best_team['abbreviation']})") + print(f"Record: {best_team['wins']}-{best_team['losses']}{f'-{best_team['ties']}' if best_team['ties'] > 0 else ''}") + print(f"Win Percentage: {best_team['win_percentage']:.3f}") + print(f"Poll Ranking: #{best_team['rank']}") + +def main(): + """Main function to run the test.""" + try: + tester = NCAAFBLeaderboardTester() + tester.run_test() + except KeyboardInterrupt: + print("\nTest interrupted by user") + except Exception as e: + print(f"Error running test: {e}") + import traceback + traceback.print_exc() + +if __name__ == "__main__": + main() diff --git a/test/test_updated_leaderboard_manager.py b/test/test_updated_leaderboard_manager.py new file mode 100644 index 00000000..b09a9e39 --- /dev/null +++ b/test/test_updated_leaderboard_manager.py @@ -0,0 +1,145 @@ +#!/usr/bin/env python3 +""" +Test script to verify the updated leaderboard manager works correctly +with the new NCAA Football rankings endpoint. +""" + +import sys +import os +import json +import time +from typing import Dict, Any + +# Add the src directory to the path so we can import our modules +sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', 'src')) + +from leaderboard_manager import LeaderboardManager +from cache_manager import CacheManager +from config_manager import ConfigManager + +def test_updated_leaderboard_manager(): + """Test the updated leaderboard manager with NCAA Football rankings.""" + + print("Testing Updated Leaderboard Manager") + print("=" * 50) + + # Create a mock display manager (we don't need the actual hardware for this test) + class MockDisplayManager: + def __init__(self): + self.matrix = type('Matrix', (), {'width': 64, 'height': 32})() + self.image = None + self.draw = None + + def update_display(self): + pass + + def set_scrolling_state(self, scrolling): + pass + + def process_deferred_updates(self): + pass + + # Create test configuration + test_config = { + 'leaderboard': { + 'enabled': True, + 'enabled_sports': { + 'ncaa_fb': { + 'enabled': True, + 'top_teams': 10 + } + }, + '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, + 'time_per_team': 2.0, + 'time_per_league': 3.0 + } + } + + try: + # Initialize the leaderboard manager + print("Initializing LeaderboardManager...") + display_manager = MockDisplayManager() + leaderboard_manager = LeaderboardManager(test_config, display_manager) + + print(f"Leaderboard enabled: {leaderboard_manager.is_enabled}") + print(f"Enabled sports: {[k for k, v in leaderboard_manager.enabled_sports.items() if v.get('enabled', False)]}") + + # Test the NCAA Football rankings fetch + print("\nTesting NCAA Football rankings fetch...") + ncaa_fb_config = leaderboard_manager.league_configs['ncaa_fb'] + print(f"NCAA FB config: {ncaa_fb_config}") + + # Fetch standings using the new method + standings = leaderboard_manager._fetch_standings(ncaa_fb_config) + + if standings: + print(f"\nSuccessfully fetched {len(standings)} teams") + print("\nTop 10 NCAA Football Teams (from rankings):") + print("-" * 60) + print(f"{'Rank':<4} {'Team':<25} {'Abbr':<6} {'Record':<12} {'Win %':<8}") + print("-" * 60) + + for team in standings: + record_str = f"{team['wins']}-{team['losses']}" + if team['ties'] > 0: + record_str += f"-{team['ties']}" + + win_pct = team['win_percentage'] + win_pct_str = f"{win_pct:.3f}" if win_pct > 0 else "0.000" + + print(f"{team.get('rank', 'N/A'):<4} {team['name']:<25} {team['abbreviation']:<6} {record_str:<12} {win_pct_str:<8}") + + print("-" * 60) + + # Show additional info + ranking_name = standings[0].get('ranking_name', 'Unknown') if standings else 'Unknown' + print(f"Ranking system used: {ranking_name}") + print(f"Data fetched at: {time.strftime('%Y-%m-%d %H:%M:%S')}") + + # Test caching + print(f"\nTesting caching...") + cached_standings = leaderboard_manager._fetch_standings(ncaa_fb_config) + if cached_standings: + print("✓ Caching works correctly - data retrieved from cache") + else: + print("✗ Caching issue - no data retrieved from cache") + + else: + print("✗ No standings data retrieved") + return False + + print("\n✓ Leaderboard manager test completed successfully!") + return True + + except Exception as e: + print(f"✗ Error testing leaderboard manager: {e}") + import traceback + traceback.print_exc() + return False + +def main(): + """Main function to run the test.""" + try: + success = test_updated_leaderboard_manager() + if success: + print("\n🎉 All tests passed! The updated leaderboard manager is working correctly.") + else: + print("\n❌ Tests failed. Please check the errors above.") + except KeyboardInterrupt: + print("\nTest interrupted by user") + except Exception as e: + print(f"Error running test: {e}") + import traceback + traceback.print_exc() + +if __name__ == "__main__": + main()