Created Base Sports Classes (#39)

* rebase

* Update NFL and NCAA FB fetch

* update FB updates

* kinda working, kinda broken

* Fixed and update loggers

* move to individual files

* timeout updates

* seems to work well

* Leaderboard overestimates time

* ignore that

* minor syntax updates

* More consolidation but i broke something

* fixed

* Hockey seems to work

* Fix my changes to logo downloader

* even more consolidation

* fixes

* more cleanup

* inheritance stuff

* Change football to ESPN down text, it does what ur already doing. Change color to red on Red ZOne

* Fix leaderboard

* Update football.py

Signed-off-by: Alex Resnick <adr8292@gmail.com>

* Minor fixes

* don't want that

* background fetch

* whoops

---------

Signed-off-by: Alex Resnick <adr8292@gmail.com>
Co-authored-by: Alex Resnick <adr8282@gmail.com>
Co-authored-by: ChuckBuilds <33324927+ChuckBuilds@users.noreply.github.com>
This commit is contained in:
Alex Resnick
2025-09-24 16:10:41 -05:00
committed by GitHub
parent 42e14f99b0
commit 76a9e98ba7
14 changed files with 2553 additions and 4322 deletions

View File

@@ -51,9 +51,10 @@ class LogoDownloader:
'nba': 'assets/sports/nba_logos',
'mlb': 'assets/sports/mlb_logos',
'nhl': 'assets/sports/nhl_logos',
# NCAA sports use same directory
'ncaa_fb': 'assets/sports/ncaa_logos',
'ncaa_fb_all': 'assets/sports/ncaa_logos', # FCS teams go in same directory
'fcs': 'assets/sports/ncaa_logos', # FCS teams go in same directory
'ncaa_fb_all': 'assets/sports/ncaa_logos',
'fcs': 'assets/sports/ncaa_logos',
'ncaam_basketball': 'assets/sports/ncaa_logos',
'ncaa_baseball': 'assets/sports/ncaa_logos',
'ncaam_hockey': 'assets/sports/ncaa_logos',
@@ -95,7 +96,8 @@ class LogoDownloader:
'Connection': 'keep-alive'
}
def normalize_abbreviation(self, abbreviation: str) -> str:
@staticmethod
def normalize_abbreviation(abbreviation: str) -> str:
"""Normalize team abbreviation for consistent filename usage."""
# Handle special characters that can cause filesystem issues
normalized = abbreviation.upper()
@@ -125,7 +127,7 @@ class LogoDownloader:
logger.error(f"Failed to create logo directory {logo_dir}: {e}")
return False
def download_logo(self, logo_url: str, filepath: Path, team_name: str) -> bool:
def download_logo(self, logo_url: str, filepath: Path, team_abbreviation: str) -> bool:
"""Download a single logo from URL and save to filepath."""
try:
response = self.session.get(logo_url, headers=self.headers, timeout=self.request_timeout)
@@ -134,7 +136,7 @@ class LogoDownloader:
# Verify it's actually an image
content_type = response.headers.get('content-type', '').lower()
if not any(img_type in content_type for img_type in ['image/png', 'image/jpeg', 'image/jpg', 'image/gif']):
logger.warning(f"Downloaded content for {team_name} is not an image: {content_type}")
logger.warning(f"Downloaded content for {team_abbreviation} is not an image: {content_type}")
return False
with open(filepath, 'wb') as f:
@@ -157,10 +159,10 @@ class LogoDownloader:
# Save the converted image
img.save(filepath, 'PNG')
logger.info(f"Successfully downloaded and converted logo for {team_name} -> {filepath.name}")
logger.info(f"Successfully downloaded and converted logo for {team_abbreviation} -> {filepath.name}")
return True
except Exception as e:
logger.error(f"Downloaded file for {team_name} is not a valid image or conversion failed: {e}")
logger.error(f"Downloaded file for {team_abbreviation} is not a valid image or conversion failed: {e}")
try:
os.remove(filepath) # Remove invalid file
except:
@@ -168,10 +170,10 @@ class LogoDownloader:
return False
except requests.exceptions.RequestException as e:
logger.error(f"Failed to download logo for {team_name}: {e}")
logger.error(f"Failed to download logo for {team_abbreviation}: {e}")
return False
except Exception as e:
logger.error(f"Unexpected error downloading logo for {team_name}: {e}")
logger.error(f"Unexpected error downloading logo for {team_abbreviation}: {e}")
return False
def fetch_teams_data(self, league: str) -> Optional[Dict]:
@@ -197,6 +199,29 @@ class LogoDownloader:
logger.error(f"Error parsing JSON response for {league}: {e}")
return None
def fetch_single_team(self, league: str, team_id: str) -> Optional[Dict]:
"""Fetch team data from ESPN API for a specific league."""
api_url = self.API_ENDPOINTS.get(league)
if not api_url:
logger.error(f"No API endpoint configured for league: {league}")
return None
try:
logger.info(f"Fetching team data for team {team_id} in {league} from ESPN API...")
response = self.session.get(f"{api_url}/{team_id}", headers=self.headers, timeout=self.request_timeout)
response.raise_for_status()
data = response.json()
logger.info(f"Successfully fetched team data for {team_id} in {league}")
return data
except requests.exceptions.RequestException as e:
logger.error(f"Error fetching team data for {team_id} in {league}: {e}")
return None
except json.JSONDecodeError as e:
logger.error(f"Error parsing JSON response for{team_id} in {league}: {e}")
return None
def extract_teams_from_data(self, data: Dict, league: str) -> List[Dict[str, str]]:
"""Extract team information from ESPN API response."""
teams = []
@@ -450,66 +475,24 @@ class LogoDownloader:
logger.info(f"Comprehensive NCAA football logo download complete: {downloaded_count} downloaded, {failed_count} failed")
return downloaded_count, failed_count
def download_missing_logo_for_team(self, team_abbreviation: str, league: str, team_name: str = None) -> bool:
def download_missing_logo_for_team(self, league: str, team_id: str, team_abbreviation: str, logo_path: Path) -> bool:
"""Download a specific team's logo if it's missing."""
logo_dir = self.get_logo_directory(league)
if not self.ensure_logo_directory(logo_dir):
return False
filename = f"{self.normalize_abbreviation(team_abbreviation)}.png"
filepath = Path(logo_dir) / filename
# Return True if logo already exists
if filepath.exists():
logger.debug(f"Logo already exists for {team_abbreviation}")
return True
# Fetch team data to find the logo URL
data = self.fetch_teams_data(league)
data = self.fetch_single_team(league, team_id)
if not data:
return False
teams = self.extract_teams_from_data(data, league)
# Find the specific team with improved matching
target_team = None
normalized_search = self.normalize_abbreviation(team_abbreviation)
# First try exact match
for team in teams:
if team['abbreviation'].upper() == team_abbreviation.upper():
target_team = team
break
# If not found, try normalized match
if not target_team:
for team in teams:
normalized_team_abbr = self.normalize_abbreviation(team['abbreviation'])
if normalized_team_abbr == normalized_search:
target_team = team
break
# If still not found, try partial matching for common variations
if not target_team:
search_variations = self._get_team_name_variations(team_abbreviation)
for team in teams:
team_variations = self._get_team_name_variations(team['abbreviation'])
if any(var in team_variations for var in search_variations):
target_team = team
logger.info(f"Found team {team_abbreviation} as {team['abbreviation']} ({team['display_name']})")
break
if not target_team:
logger.warning(f"Team {team_abbreviation} not found in {league} data")
try:
logo_url = data["team"]["logos"][0]["href"]
except KeyError:
return False
# Download the logo
success = self.download_logo(target_team['logo_url'], filepath, target_team['display_name'])
success = self.download_logo(logo_url, logo_path, team_abbreviation)
if success:
time.sleep(0.1) # Small delay
return success
def download_all_missing_logos(self, leagues: List[str] = None, force_download: bool = False) -> Dict[str, Tuple[int, int]]:
def download_all_missing_logos(self, leagues: List[str] | None = None, force_download: bool = False) -> Dict[str, Tuple[int, int]]:
"""Download missing logos for all specified leagues."""
if leagues is None:
leagues = list(self.API_ENDPOINTS.keys())
@@ -531,7 +514,7 @@ class LogoDownloader:
logger.info(f"Overall logo download results: {total_downloaded} downloaded, {total_failed} failed")
return results
def create_placeholder_logo(self, team_abbreviation: str, logo_dir: str, team_name: str = None) -> bool:
def create_placeholder_logo(self, team_abbreviation: str, logo_dir: str) -> bool:
"""Create a placeholder logo when real logo cannot be downloaded."""
try:
# Ensure the logo directory exists
@@ -642,7 +625,7 @@ def get_soccer_league_key(league_code: str) -> str:
# Convenience function for easy integration
def download_missing_logo(team_abbreviation: str, league: str, team_name: str = None, create_placeholder: bool = True) -> bool:
def download_missing_logo(league: str, team_id: str, team_abbreviation: str, logo_path: Path, logo_url: str | None = None, create_placeholder: bool = True) -> bool:
"""
Convenience function to download a missing team logo.
@@ -659,6 +642,7 @@ def download_missing_logo(team_abbreviation: str, league: str, team_name: str =
# Check if logo already exists
logo_dir = downloader.get_logo_directory(league)
downloader.ensure_logo_directory(logo_dir)
filename = f"{downloader.normalize_abbreviation(team_abbreviation)}.png"
filepath = Path(logo_dir) / filename
@@ -667,18 +651,24 @@ def download_missing_logo(team_abbreviation: str, league: str, team_name: str =
return True
# Try to download the real logo first
logger.info(f"Attempting to download logo for {team_abbreviation} ({team_name or 'Unknown'}) from {league}")
success = downloader.download_missing_logo_for_team(team_abbreviation, league, team_name)
logger.info(f"Attempting to download logo for {team_abbreviation} from {league}")
if logo_url:
success = downloader.download_logo(logo_url, filepath, team_abbreviation)
if success:
time.sleep(0.1) # Small delay
return success
success = downloader.download_missing_logo_for_team(league, team_id, team_abbreviation, logo_path)
if not success and create_placeholder:
logger.info(f"Creating placeholder logo for {team_abbreviation} ({team_name or 'Unknown'})")
logger.info(f"Creating placeholder logo for {team_abbreviation}")
# Create placeholder as fallback
success = downloader.create_placeholder_logo(team_abbreviation, logo_dir, team_name)
success = downloader.create_placeholder_logo(team_abbreviation, logo_dir)
if success:
logger.info(f"Successfully handled logo for {team_abbreviation} ({team_name or 'Unknown'})")
logger.info(f"Successfully handled logo for {team_abbreviation}")
else:
logger.warning(f"Failed to download or create logo for {team_abbreviation} ({team_name or 'Unknown'})")
logger.warning(f"Failed to download or create logo for {team_abbreviation}")
return success