logo downloader for FCS teams is more robust
BIN
assets/sports/ncaa_fbs_logos/AMH.png
Normal file
|
After Width: | Height: | Size: 27 KiB |
BIN
assets/sports/ncaa_fbs_logos/ANN.png
Normal file
|
After Width: | Height: | Size: 65 KiB |
BIN
assets/sports/ncaa_fbs_logos/ASU.png
Normal file
|
After Width: | Height: | Size: 27 KiB |
BIN
assets/sports/ncaa_fbs_logos/BOIS.png
Normal file
|
After Width: | Height: | Size: 54 KiB |
BIN
assets/sports/ncaa_fbs_logos/BRST.png
Normal file
|
After Width: | Height: | Size: 109 KiB |
BIN
assets/sports/ncaa_fbs_logos/BUENA.png
Normal file
|
After Width: | Height: | Size: 42 KiB |
BIN
assets/sports/ncaa_fbs_logos/CAR.png
Normal file
|
After Width: | Height: | Size: 34 KiB |
BIN
assets/sports/ncaa_fbs_logos/CLA.png
Normal file
|
After Width: | Height: | Size: 53 KiB |
BIN
assets/sports/ncaa_fbs_logos/COLBY.png
Normal file
|
After Width: | Height: | Size: 105 KiB |
BIN
assets/sports/ncaa_fbs_logos/CP.png
Normal file
|
After Width: | Height: | Size: 49 KiB |
BIN
assets/sports/ncaa_fbs_logos/CSU.png
Normal file
|
After Width: | Height: | Size: 84 KiB |
BIN
assets/sports/ncaa_fbs_logos/CUR.png
Normal file
|
After Width: | Height: | Size: 26 KiB |
BIN
assets/sports/ncaa_fbs_logos/DEL.png
Normal file
|
After Width: | Height: | Size: 47 KiB |
BIN
assets/sports/ncaa_fbs_logos/DUB.png
Normal file
|
After Width: | Height: | Size: 12 KiB |
BIN
assets/sports/ncaa_fbs_logos/ELM.png
Normal file
|
After Width: | Height: | Size: 42 KiB |
BIN
assets/sports/ncaa_fbs_logos/FAMU.png
Normal file
|
After Width: | Height: | Size: 44 KiB |
BIN
assets/sports/ncaa_fbs_logos/FLA.png
Normal file
|
After Width: | Height: | Size: 73 KiB |
BIN
assets/sports/ncaa_fbs_logos/GRI.png
Normal file
|
After Width: | Height: | Size: 34 KiB |
BIN
assets/sports/ncaa_fbs_logos/GTWN.png
Normal file
|
After Width: | Height: | Size: 26 KiB |
BIN
assets/sports/ncaa_fbs_logos/HAW.png
Normal file
|
After Width: | Height: | Size: 44 KiB |
BIN
assets/sports/ncaa_fbs_logos/HOW.png
Normal file
|
After Width: | Height: | Size: 25 KiB |
BIN
assets/sports/ncaa_fbs_logos/IDHO.png
Normal file
|
After Width: | Height: | Size: 66 KiB |
BIN
assets/sports/ncaa_fbs_logos/JXST.png
Normal file
|
After Width: | Height: | Size: 40 KiB |
BIN
assets/sports/ncaa_fbs_logos/LUT.png
Normal file
|
After Width: | Height: | Size: 81 KiB |
BIN
assets/sports/ncaa_fbs_logos/MESA.png
Normal file
|
After Width: | Height: | Size: 96 KiB |
BIN
assets/sports/ncaa_fbs_logos/MIL.png
Normal file
|
After Width: | Height: | Size: 6.3 KiB |
BIN
assets/sports/ncaa_fbs_logos/MOR.png
Normal file
|
After Width: | Height: | Size: 22 KiB |
BIN
assets/sports/ncaa_fbs_logos/NOR.png
Normal file
|
After Width: | Height: | Size: 45 KiB |
BIN
assets/sports/ncaa_fbs_logos/RED.png
Normal file
|
After Width: | Height: | Size: 78 KiB |
BIN
assets/sports/ncaa_fbs_logos/SAC.png
Normal file
|
After Width: | Height: | Size: 47 KiB |
BIN
assets/sports/ncaa_fbs_logos/STET.png
Normal file
|
After Width: | Height: | Size: 47 KiB |
BIN
assets/sports/ncaa_fbs_logos/USA.png
Normal file
|
After Width: | Height: | Size: 38 KiB |
BIN
assets/sports/ncaa_fbs_logos/YALE.png
Normal file
|
After Width: | Height: | Size: 23 KiB |
@@ -189,9 +189,9 @@
|
||||
"loop": true,
|
||||
"request_timeout": 30,
|
||||
"dynamic_duration": true,
|
||||
"min_duration": 30,
|
||||
"min_duration": 45,
|
||||
"max_duration": 300,
|
||||
"duration_buffer": 0.1,
|
||||
"duration_buffer": 0.2,
|
||||
"time_per_team": 2.0,
|
||||
"time_per_league": 3.0
|
||||
},
|
||||
|
||||
@@ -11,11 +11,13 @@ try:
|
||||
from .display_manager import DisplayManager
|
||||
from .cache_manager import CacheManager
|
||||
from .config_manager import ConfigManager
|
||||
from .logo_downloader import download_missing_logo
|
||||
except ImportError:
|
||||
# Fallback for direct imports
|
||||
from display_manager import DisplayManager
|
||||
from cache_manager import CacheManager
|
||||
from config_manager import ConfigManager
|
||||
from logo_downloader import download_missing_logo
|
||||
|
||||
# Import the API counter function from web interface
|
||||
try:
|
||||
@@ -193,8 +195,8 @@ class LeaderboardManager:
|
||||
'xlarge': ImageFont.load_default()
|
||||
}
|
||||
|
||||
def _get_team_logo(self, team_abbr: str, logo_dir: str) -> Optional[Image.Image]:
|
||||
"""Get team logo from the configured directory."""
|
||||
def _get_team_logo(self, team_abbr: str, logo_dir: str, league: str = None, team_name: str = None) -> Optional[Image.Image]:
|
||||
"""Get team logo from the configured directory, downloading if missing."""
|
||||
if not team_abbr or not logo_dir:
|
||||
logger.debug("Cannot get team logo with missing team_abbr or logo_dir")
|
||||
return None
|
||||
@@ -207,6 +209,18 @@ class LeaderboardManager:
|
||||
return logo
|
||||
else:
|
||||
logger.warning(f"Logo not found at path: {logo_path}")
|
||||
|
||||
# Try to download the missing logo if we have league information
|
||||
if league:
|
||||
logger.info(f"Attempting to download missing logo for {team_abbr} in league {league}")
|
||||
success = download_missing_logo(team_abbr, league, team_name)
|
||||
if success:
|
||||
# Try to load the downloaded logo
|
||||
if os.path.exists(logo_path):
|
||||
logo = Image.open(logo_path)
|
||||
logger.info(f"Successfully downloaded and loaded logo for {team_abbr}")
|
||||
return logo
|
||||
|
||||
return None
|
||||
except Exception as e:
|
||||
logger.error(f"Error loading logo for {team_abbr} from {logo_dir}: {e}")
|
||||
@@ -767,19 +781,10 @@ class LeaderboardManager:
|
||||
league_logo = league_logo.resize((logo_width, logo_height), Image.Resampling.LANCZOS)
|
||||
self.leaderboard_image.paste(league_logo, (logo_x, logo_y), league_logo if league_logo.mode == 'RGBA' else None)
|
||||
|
||||
# Draw league name at the bottom
|
||||
league_name = league_key.upper().replace('_', ' ')
|
||||
league_name_bbox = self.fonts['small'].getbbox(league_name)
|
||||
league_name_width = league_name_bbox[2] - league_name_bbox[0]
|
||||
league_name_x = current_x + (64 - league_name_width) // 2
|
||||
draw.text((league_name_x, height - 8), league_name, font=self.fonts['small'], fill=(255, 255, 255))
|
||||
# League name removed - only show league logo
|
||||
else:
|
||||
# Fallback if no league logo - just show league name
|
||||
league_name = league_key.upper().replace('_', ' ')
|
||||
league_name_bbox = self.fonts['medium'].getbbox(league_name)
|
||||
league_name_width = league_name_bbox[2] - league_name_bbox[0]
|
||||
league_name_x = current_x + (64 - league_name_width) // 2
|
||||
draw.text((league_name_x, height // 2), league_name, font=self.fonts['medium'], fill=(255, 255, 255))
|
||||
# No league logo available - skip league name display
|
||||
pass
|
||||
|
||||
# Move to team section
|
||||
current_x += 64 + 10 # League logo width + spacing
|
||||
@@ -816,7 +821,8 @@ class LeaderboardManager:
|
||||
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)
|
||||
team_logo = self._get_team_logo(team['abbreviation'], league_config['logo_dir'])
|
||||
team_logo = self._get_team_logo(team['abbreviation'], league_config['logo_dir'],
|
||||
league=league_key, team_name=team.get('name'))
|
||||
if team_logo:
|
||||
# Resize team logo to dynamic size (95% of display height)
|
||||
team_logo = team_logo.resize((logo_size, logo_size), Image.Resampling.LANCZOS)
|
||||
|
||||
507
src/logo_downloader.py
Normal file
@@ -0,0 +1,507 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Centralized logo downloader utility for automatically fetching team logos from ESPN API.
|
||||
This module provides functionality to download missing team logos for various sports leagues,
|
||||
with special support for FCS teams and other NCAA divisions.
|
||||
"""
|
||||
|
||||
import os
|
||||
import time
|
||||
import logging
|
||||
import requests
|
||||
import json
|
||||
from typing import Dict, Any, List, Optional, Tuple
|
||||
from pathlib import Path
|
||||
from PIL import Image, ImageDraw, ImageFont
|
||||
from requests.adapters import HTTPAdapter
|
||||
from urllib3.util.retry import Retry
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class LogoDownloader:
|
||||
"""Centralized logo downloader for team logos from ESPN API."""
|
||||
|
||||
# ESPN API endpoints for different sports/leagues
|
||||
API_ENDPOINTS = {
|
||||
'nfl': 'https://site.api.espn.com/apis/site/v2/sports/football/nfl/teams',
|
||||
'nba': 'https://site.api.espn.com/apis/site/v2/sports/basketball/nba/teams',
|
||||
'mlb': 'https://site.api.espn.com/apis/site/v2/sports/baseball/mlb/teams',
|
||||
'nhl': 'https://site.api.espn.com/apis/site/v2/sports/hockey/nhl/teams',
|
||||
'ncaa_fb': 'https://site.api.espn.com/apis/site/v2/sports/football/college-football/teams',
|
||||
'ncaa_fb_all': 'https://site.api.espn.com/apis/site/v2/sports/football/college-football/teams', # Includes FCS
|
||||
'fcs': 'https://site.api.espn.com/apis/site/v2/sports/football/college-football/teams', # FCS teams from same endpoint
|
||||
'ncaam_basketball': 'https://site.api.espn.com/apis/site/v2/sports/basketball/mens-college-basketball/teams',
|
||||
'ncaa_baseball': 'https://site.api.espn.com/apis/site/v2/sports/baseball/college-baseball/teams'
|
||||
}
|
||||
|
||||
# Directory mappings for different leagues
|
||||
LOGO_DIRECTORIES = {
|
||||
'nfl': 'assets/sports/nfl_logos',
|
||||
'nba': 'assets/sports/nba_logos',
|
||||
'mlb': 'assets/sports/mlb_logos',
|
||||
'nhl': 'assets/sports/nhl_logos',
|
||||
'ncaa_fb': 'assets/sports/ncaa_fbs_logos',
|
||||
'ncaa_fb_all': 'assets/sports/ncaa_fbs_logos', # FCS teams go in same directory
|
||||
'fcs': 'assets/sports/ncaa_fbs_logos', # FCS teams go in same directory
|
||||
'ncaam_basketball': 'assets/sports/ncaa_fbs_logos',
|
||||
'ncaa_baseball': 'assets/sports/ncaa_fbs_logos'
|
||||
}
|
||||
|
||||
def __init__(self, request_timeout: int = 30, retry_attempts: int = 3):
|
||||
"""Initialize the logo downloader with HTTP session and retry logic."""
|
||||
self.request_timeout = request_timeout
|
||||
self.retry_attempts = retry_attempts
|
||||
|
||||
# Set up session with retry logic
|
||||
self.session = requests.Session()
|
||||
retry_strategy = Retry(
|
||||
total=retry_attempts,
|
||||
backoff_factor=1,
|
||||
status_forcelist=[429, 500, 502, 503, 504],
|
||||
allowed_methods=["GET", "HEAD", "OPTIONS"]
|
||||
)
|
||||
adapter = HTTPAdapter(max_retries=retry_strategy)
|
||||
self.session.mount("https://", adapter)
|
||||
self.session.mount("http://", adapter)
|
||||
|
||||
# Set up headers
|
||||
self.headers = {
|
||||
'User-Agent': 'LEDMatrix/1.0 (https://github.com/yourusername/LEDMatrix; contact@example.com)',
|
||||
'Accept': 'application/json',
|
||||
'Accept-Language': 'en-US,en;q=0.9',
|
||||
'Accept-Encoding': 'gzip, deflate, br',
|
||||
'Connection': 'keep-alive'
|
||||
}
|
||||
|
||||
def normalize_abbreviation(self, abbreviation: str) -> str:
|
||||
"""Normalize team abbreviation for consistent filename usage."""
|
||||
return abbreviation.upper()
|
||||
|
||||
def get_logo_directory(self, league: str) -> str:
|
||||
"""Get the logo directory for a given league."""
|
||||
return self.LOGO_DIRECTORIES.get(league, f'assets/sports/{league}_logos')
|
||||
|
||||
def ensure_logo_directory(self, logo_dir: str) -> bool:
|
||||
"""Ensure the logo directory exists, create if necessary."""
|
||||
try:
|
||||
os.makedirs(logo_dir, exist_ok=True)
|
||||
return True
|
||||
except Exception as e:
|
||||
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:
|
||||
"""Download a single logo from URL and save to filepath."""
|
||||
try:
|
||||
response = self.session.get(logo_url, headers=self.headers, timeout=self.request_timeout)
|
||||
response.raise_for_status()
|
||||
|
||||
# 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}")
|
||||
return False
|
||||
|
||||
with open(filepath, 'wb') as f:
|
||||
f.write(response.content)
|
||||
|
||||
# Verify the downloaded file is a valid image
|
||||
try:
|
||||
with Image.open(filepath) as img:
|
||||
img.verify()
|
||||
logger.info(f"Successfully downloaded logo for {team_name} -> {filepath.name}")
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.error(f"Downloaded file for {team_name} is not a valid image: {e}")
|
||||
os.remove(filepath) # Remove invalid file
|
||||
return False
|
||||
|
||||
except requests.exceptions.RequestException as e:
|
||||
logger.error(f"Failed to download logo for {team_name}: {e}")
|
||||
return False
|
||||
except Exception as e:
|
||||
logger.error(f"Unexpected error downloading logo for {team_name}: {e}")
|
||||
return False
|
||||
|
||||
def fetch_teams_data(self, league: 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 {league} from ESPN API...")
|
||||
response = self.session.get(api_url, headers=self.headers, timeout=self.request_timeout)
|
||||
response.raise_for_status()
|
||||
data = response.json()
|
||||
|
||||
logger.info(f"Successfully fetched team data for {league}")
|
||||
return data
|
||||
|
||||
except requests.exceptions.RequestException as e:
|
||||
logger.error(f"Error fetching team data for {league}: {e}")
|
||||
return None
|
||||
except json.JSONDecodeError as e:
|
||||
logger.error(f"Error parsing JSON response for {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 = []
|
||||
|
||||
try:
|
||||
sports = data.get('sports', [])
|
||||
for sport in sports:
|
||||
leagues_data = sport.get('leagues', [])
|
||||
for league_data in leagues_data:
|
||||
teams_data = league_data.get('teams', [])
|
||||
|
||||
for team_data in teams_data:
|
||||
team_info = team_data.get('team', {})
|
||||
|
||||
abbreviation = team_info.get('abbreviation', '')
|
||||
display_name = team_info.get('displayName', 'Unknown')
|
||||
logos = team_info.get('logos', [])
|
||||
|
||||
if not abbreviation or not logos:
|
||||
continue
|
||||
|
||||
# Get the default logo (first one is usually default)
|
||||
logo_url = logos[0].get('href', '')
|
||||
if not logo_url:
|
||||
continue
|
||||
|
||||
# For NCAA football, try to determine if it's FCS or FBS
|
||||
team_category = 'FBS' # Default
|
||||
if league in ['ncaa_fb', 'ncaa_fb_all', 'fcs']:
|
||||
# Check if this is an FCS team by looking at conference or other indicators
|
||||
# ESPN API includes both FBS and FCS teams in the same endpoint
|
||||
# We'll include all teams and let the user decide which ones to use
|
||||
team_category = self._determine_ncaa_football_division(team_info, league_data)
|
||||
|
||||
teams.append({
|
||||
'abbreviation': abbreviation,
|
||||
'display_name': display_name,
|
||||
'logo_url': logo_url,
|
||||
'league': league,
|
||||
'category': team_category,
|
||||
'conference': league_data.get('name', 'Unknown')
|
||||
})
|
||||
|
||||
logger.info(f"Extracted {len(teams)} teams for {league}")
|
||||
return teams
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error extracting teams for {league}: {e}")
|
||||
return []
|
||||
|
||||
def _determine_ncaa_football_division(self, team_info: Dict, league_data: Dict) -> str:
|
||||
"""Determine if an NCAA football team is FBS or FCS based on conference and other indicators."""
|
||||
conference_name = league_data.get('name', '').lower()
|
||||
|
||||
# FBS Conferences (more comprehensive list)
|
||||
fbs_conferences = {
|
||||
'acc', 'american athletic', 'big 12', 'big ten', 'conference usa', 'c-usa',
|
||||
'mid-american', 'mac', 'mountain west', 'pac-12', 'pac-10', 'sec',
|
||||
'sun belt', 'independents', 'big east'
|
||||
}
|
||||
|
||||
# FCS Conferences (more comprehensive list)
|
||||
fcs_conferences = {
|
||||
'big sky', 'big south', 'colonial athletic', 'caa', 'ivy league',
|
||||
'meac', 'missouri valley', 'mvfc', 'northeast', 'nec',
|
||||
'ohio valley', 'ovc', 'patriot league', 'pioneer football',
|
||||
'southland', 'southern', 'southwestern athletic', 'swac',
|
||||
'western athletic', 'wac', 'ncaa division i-aa'
|
||||
}
|
||||
|
||||
# Also check for specific team indicators
|
||||
team_abbreviation = team_info.get('abbreviation', '').upper()
|
||||
|
||||
# Known FBS teams that might be misclassified
|
||||
known_fbs_teams = {
|
||||
'ASU', 'ARIZ', 'ARK', 'AUB', 'BOIS', 'CSU', 'FLA', 'HAW', 'IDHO', 'USA'
|
||||
}
|
||||
|
||||
# Check if it's a known FBS team first
|
||||
if team_abbreviation in known_fbs_teams:
|
||||
return 'FBS'
|
||||
|
||||
# Check conference names
|
||||
if any(fbs_conf in conference_name for fbs_conf in fbs_conferences):
|
||||
return 'FBS'
|
||||
elif any(fcs_conf in conference_name for fcs_conf in fcs_conferences):
|
||||
return 'FCS'
|
||||
|
||||
# If conference is just "NCAA - Football", we need to use other indicators
|
||||
if conference_name == 'ncaa - football':
|
||||
# Check team name for indicators of FCS (smaller schools, Division II/III)
|
||||
team_name = team_info.get('displayName', '').lower()
|
||||
fcs_indicators = ['college', 'university', 'state', 'tech', 'community']
|
||||
|
||||
# If it has typical FCS naming patterns and isn't a known FBS team
|
||||
if any(indicator in team_name for indicator in fcs_indicators):
|
||||
return 'FCS'
|
||||
else:
|
||||
return 'FBS'
|
||||
|
||||
# Default to FBS for unknown conferences
|
||||
return 'FBS'
|
||||
|
||||
def download_missing_logos_for_league(self, league: str, force_download: bool = False) -> Tuple[int, int]:
|
||||
"""Download missing logos for a specific league."""
|
||||
logger.info(f"Starting logo download for league: {league}")
|
||||
|
||||
# Get logo directory
|
||||
logo_dir = self.get_logo_directory(league)
|
||||
if not self.ensure_logo_directory(logo_dir):
|
||||
logger.error(f"Failed to create logo directory for {league}")
|
||||
return 0, 0
|
||||
|
||||
# Fetch team data
|
||||
data = self.fetch_teams_data(league)
|
||||
if not data:
|
||||
logger.error(f"Failed to fetch team data for {league}")
|
||||
return 0, 0
|
||||
|
||||
# Extract teams
|
||||
teams = self.extract_teams_from_data(data, league)
|
||||
if not teams:
|
||||
logger.warning(f"No teams found for {league}")
|
||||
return 0, 0
|
||||
|
||||
# Download missing logos
|
||||
downloaded_count = 0
|
||||
failed_count = 0
|
||||
|
||||
for team in teams:
|
||||
abbreviation = team['abbreviation']
|
||||
display_name = team['display_name']
|
||||
logo_url = team['logo_url']
|
||||
|
||||
# Create filename
|
||||
filename = f"{self.normalize_abbreviation(abbreviation)}.png"
|
||||
filepath = Path(logo_dir) / filename
|
||||
|
||||
# Skip if already exists and not forcing download
|
||||
if filepath.exists() and not force_download:
|
||||
logger.debug(f"Skipping {display_name}: {filename} already exists")
|
||||
continue
|
||||
|
||||
# Download logo
|
||||
if self.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)
|
||||
|
||||
logger.info(f"Logo download complete for {league}: {downloaded_count} downloaded, {failed_count} failed")
|
||||
return downloaded_count, failed_count
|
||||
|
||||
def download_all_ncaa_football_logos(self, include_fcs: bool = True, force_download: bool = False) -> Tuple[int, int]:
|
||||
"""Download all NCAA football team logos including FCS teams."""
|
||||
logger.info(f"Starting comprehensive NCAA football logo download (FCS: {include_fcs})")
|
||||
|
||||
# Use the comprehensive NCAA football endpoint
|
||||
league = 'ncaa_fb_all'
|
||||
logo_dir = self.get_logo_directory(league)
|
||||
if not self.ensure_logo_directory(logo_dir):
|
||||
logger.error(f"Failed to create logo directory for {league}")
|
||||
return 0, 0
|
||||
|
||||
# Fetch team data
|
||||
data = self.fetch_teams_data(league)
|
||||
if not data:
|
||||
logger.error(f"Failed to fetch team data for {league}")
|
||||
return 0, 0
|
||||
|
||||
# Extract teams
|
||||
teams = self.extract_teams_from_data(data, league)
|
||||
if not teams:
|
||||
logger.warning(f"No teams found for {league}")
|
||||
return 0, 0
|
||||
|
||||
# Filter teams based on FCS inclusion
|
||||
if not include_fcs:
|
||||
teams = [team for team in teams if team.get('category') == 'FBS']
|
||||
logger.info(f"Filtered to FBS teams only: {len(teams)} teams")
|
||||
|
||||
# Download missing logos
|
||||
downloaded_count = 0
|
||||
failed_count = 0
|
||||
|
||||
for team in teams:
|
||||
abbreviation = team['abbreviation']
|
||||
display_name = team['display_name']
|
||||
logo_url = team['logo_url']
|
||||
category = team.get('category', 'Unknown')
|
||||
conference = team.get('conference', 'Unknown')
|
||||
|
||||
# Create filename
|
||||
filename = f"{self.normalize_abbreviation(abbreviation)}.png"
|
||||
filepath = Path(logo_dir) / filename
|
||||
|
||||
# Skip if already exists and not forcing download
|
||||
if filepath.exists() and not force_download:
|
||||
logger.debug(f"Skipping {display_name} ({category}, {conference}): {filename} already exists")
|
||||
continue
|
||||
|
||||
# Download logo
|
||||
if self.download_logo(logo_url, filepath, display_name):
|
||||
downloaded_count += 1
|
||||
logger.info(f"Downloaded {display_name} ({category}, {conference}) -> {filename}")
|
||||
else:
|
||||
failed_count += 1
|
||||
logger.warning(f"Failed to download {display_name} ({category}, {conference})")
|
||||
|
||||
# Small delay to be respectful to the API
|
||||
time.sleep(0.1)
|
||||
|
||||
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:
|
||||
"""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)
|
||||
if not data:
|
||||
return False
|
||||
|
||||
teams = self.extract_teams_from_data(data, league)
|
||||
|
||||
# Find the specific team
|
||||
target_team = None
|
||||
for team in teams:
|
||||
if team['abbreviation'].upper() == team_abbreviation.upper():
|
||||
target_team = team
|
||||
break
|
||||
|
||||
if not target_team:
|
||||
logger.warning(f"Team {team_abbreviation} not found in {league} data")
|
||||
return False
|
||||
|
||||
# Download the logo
|
||||
success = self.download_logo(target_team['logo_url'], filepath, target_team['display_name'])
|
||||
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]]:
|
||||
"""Download missing logos for all specified leagues."""
|
||||
if leagues is None:
|
||||
leagues = list(self.API_ENDPOINTS.keys())
|
||||
|
||||
results = {}
|
||||
total_downloaded = 0
|
||||
total_failed = 0
|
||||
|
||||
for league in leagues:
|
||||
if league not in self.API_ENDPOINTS:
|
||||
logger.warning(f"Skipping unknown league: {league}")
|
||||
continue
|
||||
|
||||
downloaded, failed = self.download_missing_logos_for_league(league, force_download)
|
||||
results[league] = (downloaded, failed)
|
||||
total_downloaded += downloaded
|
||||
total_failed += failed
|
||||
|
||||
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:
|
||||
"""Create a placeholder logo when real logo cannot be downloaded."""
|
||||
try:
|
||||
filename = f"{self.normalize_abbreviation(team_abbreviation)}.png"
|
||||
filepath = Path(logo_dir) / filename
|
||||
|
||||
# Create a simple placeholder logo
|
||||
logo = Image.new('RGBA', (64, 64), (100, 100, 100, 255)) # Gray background
|
||||
draw = ImageDraw.Draw(logo)
|
||||
|
||||
# Try to load a font, fallback to default
|
||||
try:
|
||||
font = ImageFont.truetype("assets/fonts/PressStart2P-Regular.ttf", 12)
|
||||
except:
|
||||
try:
|
||||
font = ImageFont.load_default()
|
||||
except:
|
||||
font = None
|
||||
|
||||
# Draw team abbreviation
|
||||
text = team_abbreviation
|
||||
if font:
|
||||
# Center the text
|
||||
bbox = draw.textbbox((0, 0), text, font=font)
|
||||
text_width = bbox[2] - bbox[0]
|
||||
text_height = bbox[3] - bbox[1]
|
||||
x = (64 - text_width) // 2
|
||||
y = (64 - text_height) // 2
|
||||
draw.text((x, y), text, font=font, fill=(255, 255, 255, 255))
|
||||
else:
|
||||
# Fallback without font
|
||||
draw.text((16, 24), text, fill=(255, 255, 255, 255))
|
||||
|
||||
logo.save(filepath)
|
||||
logger.info(f"Created placeholder logo for {team_abbreviation} at {filepath}")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to create placeholder logo for {team_abbreviation}: {e}")
|
||||
return False
|
||||
|
||||
|
||||
# Convenience function for easy integration
|
||||
def download_missing_logo(team_abbreviation: str, league: str, team_name: str = None, create_placeholder: bool = True) -> bool:
|
||||
"""
|
||||
Convenience function to download a missing team logo.
|
||||
|
||||
Args:
|
||||
team_abbreviation: Team abbreviation (e.g., 'UGA', 'BAMA')
|
||||
league: League identifier (e.g., 'ncaa_fb', 'nfl')
|
||||
team_name: Optional team name for logging
|
||||
create_placeholder: Whether to create a placeholder if download fails
|
||||
|
||||
Returns:
|
||||
True if logo exists or was successfully downloaded, False otherwise
|
||||
"""
|
||||
downloader = LogoDownloader()
|
||||
|
||||
# Try to download the real logo first
|
||||
success = downloader.download_missing_logo_for_team(team_abbreviation, league, team_name)
|
||||
|
||||
if not success and create_placeholder:
|
||||
# Create placeholder as fallback
|
||||
logo_dir = downloader.get_logo_directory(league)
|
||||
success = downloader.create_placeholder_logo(team_abbreviation, logo_dir, team_name)
|
||||
|
||||
return success
|
||||
|
||||
|
||||
def download_all_logos_for_league(league: str, force_download: bool = False) -> Tuple[int, int]:
|
||||
"""
|
||||
Convenience function to download all missing logos for a league.
|
||||
|
||||
Args:
|
||||
league: League identifier (e.g., 'ncaa_fb', 'nfl')
|
||||
force_download: Whether to re-download existing logos
|
||||
|
||||
Returns:
|
||||
Tuple of (downloaded_count, failed_count)
|
||||
"""
|
||||
downloader = LogoDownloader()
|
||||
return downloader.download_missing_logos_for_league(league, force_download)
|
||||
@@ -11,6 +11,7 @@ from src.display_manager import DisplayManager
|
||||
from src.cache_manager import CacheManager # Keep CacheManager import
|
||||
from src.config_manager import ConfigManager
|
||||
from src.odds_manager import OddsManager
|
||||
from src.logo_downloader import download_missing_logo
|
||||
import pytz
|
||||
from requests.adapters import HTTPAdapter
|
||||
from urllib3.util.retry import Retry
|
||||
@@ -358,8 +359,8 @@ class BaseNCAAFBManager: # Renamed class
|
||||
draw.text((x + dx, y + dy), text, font=font, fill=outline_color)
|
||||
draw.text((x, y), text, font=font, fill=fill)
|
||||
|
||||
def _load_and_resize_logo(self, team_abbrev: str) -> Optional[Image.Image]:
|
||||
"""Load and resize a team logo, with caching."""
|
||||
def _load_and_resize_logo(self, team_abbrev: str, team_name: str = None) -> Optional[Image.Image]:
|
||||
"""Load and resize a team logo, with caching and automatic download if missing."""
|
||||
if team_abbrev in self._logo_cache:
|
||||
return self._logo_cache[team_abbrev]
|
||||
|
||||
@@ -367,15 +368,22 @@ class BaseNCAAFBManager: # Renamed class
|
||||
self.logger.debug(f"Logo path: {logo_path}")
|
||||
|
||||
try:
|
||||
# Create placeholder if logo doesn't exist (useful for testing)
|
||||
# Try to download missing logo first
|
||||
if not os.path.exists(logo_path):
|
||||
self.logger.warning(f"Logo not found for {team_abbrev} at {logo_path}. Creating placeholder.")
|
||||
os.makedirs(os.path.dirname(logo_path), exist_ok=True)
|
||||
logo = Image.new('RGBA', (32, 32), (200, 200, 200, 255)) # Gray placeholder
|
||||
draw = ImageDraw.Draw(logo)
|
||||
draw.text((2, 10), team_abbrev, fill=(0, 0, 0, 255))
|
||||
logo.save(logo_path)
|
||||
self.logger.info(f"Created placeholder logo at {logo_path}")
|
||||
self.logger.info(f"Logo not found for {team_abbrev} at {logo_path}. Attempting to download.")
|
||||
|
||||
# Try to download the logo from ESPN API
|
||||
success = download_missing_logo(team_abbrev, 'ncaa_fb', team_name)
|
||||
|
||||
if not success:
|
||||
# Create placeholder if download fails
|
||||
self.logger.warning(f"Failed to download logo for {team_abbrev}. Creating placeholder.")
|
||||
os.makedirs(os.path.dirname(logo_path), exist_ok=True)
|
||||
logo = Image.new('RGBA', (32, 32), (200, 200, 200, 255)) # Gray placeholder
|
||||
draw = ImageDraw.Draw(logo)
|
||||
draw.text((2, 10), team_abbrev, fill=(0, 0, 0, 255))
|
||||
logo.save(logo_path)
|
||||
self.logger.info(f"Created placeholder logo at {logo_path}")
|
||||
|
||||
logo = Image.open(logo_path)
|
||||
if logo.mode != 'RGBA':
|
||||
@@ -784,8 +792,8 @@ class NCAAFBLiveManager(BaseNCAAFBManager): # Renamed class
|
||||
overlay = Image.new('RGBA', (self.display_width, self.display_height), (0, 0, 0, 0))
|
||||
draw_overlay = ImageDraw.Draw(overlay) # Draw text elements on overlay first
|
||||
|
||||
home_logo = self._load_and_resize_logo(game["home_abbr"])
|
||||
away_logo = self._load_and_resize_logo(game["away_abbr"])
|
||||
home_logo = self._load_and_resize_logo(game["home_abbr"], game.get("home_team_name"))
|
||||
away_logo = self._load_and_resize_logo(game["away_abbr"], game.get("away_team_name"))
|
||||
|
||||
if not home_logo or not away_logo:
|
||||
self.logger.error(f"[NCAAFB] Failed to load logos for live game: {game.get('id')}") # Changed log prefix
|
||||
@@ -1011,8 +1019,8 @@ class NCAAFBRecentManager(BaseNCAAFBManager): # Renamed class
|
||||
overlay = Image.new('RGBA', (self.display_width, self.display_height), (0, 0, 0, 0))
|
||||
draw_overlay = ImageDraw.Draw(overlay)
|
||||
|
||||
home_logo = self._load_and_resize_logo(game["home_abbr"])
|
||||
away_logo = self._load_and_resize_logo(game["away_abbr"])
|
||||
home_logo = self._load_and_resize_logo(game["home_abbr"], game.get("home_team_name"))
|
||||
away_logo = self._load_and_resize_logo(game["away_abbr"], game.get("away_team_name"))
|
||||
|
||||
if not home_logo or not away_logo:
|
||||
self.logger.error(f"[NCAAFB Recent] Failed to load logos for game: {game.get('id')}") # Changed log prefix
|
||||
@@ -1286,8 +1294,8 @@ class NCAAFBUpcomingManager(BaseNCAAFBManager): # Renamed class
|
||||
overlay = Image.new('RGBA', (self.display_width, self.display_height), (0, 0, 0, 0))
|
||||
draw_overlay = ImageDraw.Draw(overlay)
|
||||
|
||||
home_logo = self._load_and_resize_logo(game["home_abbr"])
|
||||
away_logo = self._load_and_resize_logo(game["away_abbr"])
|
||||
home_logo = self._load_and_resize_logo(game["home_abbr"], game.get("home_team_name"))
|
||||
away_logo = self._load_and_resize_logo(game["away_abbr"], game.get("away_team_name"))
|
||||
|
||||
if not home_logo or not away_logo:
|
||||
self.logger.error(f"[NCAAFB Upcoming] Failed to load logos for game: {game.get('id')}") # Changed log prefix
|
||||
|
||||
@@ -11,6 +11,7 @@ from src.display_manager import DisplayManager
|
||||
from src.cache_manager import CacheManager
|
||||
from src.config_manager import ConfigManager
|
||||
from src.odds_manager import OddsManager
|
||||
from src.logo_downloader import download_missing_logo
|
||||
|
||||
# Import the API counter function from web interface
|
||||
try:
|
||||
@@ -125,6 +126,7 @@ class OddsTickerManager:
|
||||
'nfl': {
|
||||
'sport': 'football',
|
||||
'league': 'nfl',
|
||||
'logo_league': 'nfl', # ESPN API league identifier for logo downloading
|
||||
'logo_dir': 'assets/sports/nfl_logos',
|
||||
'favorite_teams': config.get('nfl_scoreboard', {}).get('favorite_teams', []),
|
||||
'enabled': config.get('nfl_scoreboard', {}).get('enabled', False)
|
||||
@@ -132,6 +134,7 @@ class OddsTickerManager:
|
||||
'nba': {
|
||||
'sport': 'basketball',
|
||||
'league': 'nba',
|
||||
'logo_league': 'nba', # ESPN API league identifier for logo downloading
|
||||
'logo_dir': 'assets/sports/nba_logos',
|
||||
'favorite_teams': config.get('nba_scoreboard', {}).get('favorite_teams', []),
|
||||
'enabled': config.get('nba_scoreboard', {}).get('enabled', False)
|
||||
@@ -139,6 +142,7 @@ class OddsTickerManager:
|
||||
'mlb': {
|
||||
'sport': 'baseball',
|
||||
'league': 'mlb',
|
||||
'logo_league': 'mlb', # ESPN API league identifier for logo downloading
|
||||
'logo_dir': 'assets/sports/mlb_logos',
|
||||
'favorite_teams': config.get('mlb', {}).get('favorite_teams', []),
|
||||
'enabled': config.get('mlb', {}).get('enabled', False)
|
||||
@@ -146,6 +150,7 @@ class OddsTickerManager:
|
||||
'ncaa_fb': {
|
||||
'sport': 'football',
|
||||
'league': 'college-football',
|
||||
'logo_league': 'ncaa_fb', # ESPN API league identifier for logo downloading
|
||||
'logo_dir': 'assets/sports/ncaa_fbs_logos',
|
||||
'favorite_teams': config.get('ncaa_fb_scoreboard', {}).get('favorite_teams', []),
|
||||
'enabled': config.get('ncaa_fb_scoreboard', {}).get('enabled', False)
|
||||
@@ -153,6 +158,7 @@ class OddsTickerManager:
|
||||
'milb': {
|
||||
'sport': 'baseball',
|
||||
'league': 'milb',
|
||||
'logo_league': 'milb', # ESPN API league identifier for logo downloading (if supported)
|
||||
'logo_dir': 'assets/sports/milb_logos',
|
||||
'favorite_teams': config.get('milb', {}).get('favorite_teams', []),
|
||||
'enabled': config.get('milb', {}).get('enabled', False)
|
||||
@@ -160,6 +166,7 @@ class OddsTickerManager:
|
||||
'nhl': {
|
||||
'sport': 'hockey',
|
||||
'league': 'nhl',
|
||||
'logo_league': 'nhl', # ESPN API league identifier for logo downloading
|
||||
'logo_dir': 'assets/sports/nhl_logos',
|
||||
'favorite_teams': config.get('nhl_scoreboard', {}).get('favorite_teams', []),
|
||||
'enabled': config.get('nhl_scoreboard', {}).get('enabled', False)
|
||||
@@ -167,6 +174,7 @@ class OddsTickerManager:
|
||||
'ncaam_basketball': {
|
||||
'sport': 'basketball',
|
||||
'league': 'mens-college-basketball',
|
||||
'logo_league': 'ncaam_basketball', # ESPN API league identifier for logo downloading
|
||||
'logo_dir': 'assets/sports/ncaa_fbs_logos',
|
||||
'favorite_teams': config.get('ncaam_basketball_scoreboard', {}).get('favorite_teams', []),
|
||||
'enabled': config.get('ncaam_basketball_scoreboard', {}).get('enabled', False)
|
||||
@@ -174,6 +182,7 @@ class OddsTickerManager:
|
||||
'ncaa_baseball': {
|
||||
'sport': 'baseball',
|
||||
'league': 'college-baseball',
|
||||
'logo_league': 'ncaa_baseball', # ESPN API league identifier for logo downloading
|
||||
'logo_dir': 'assets/sports/ncaa_fbs_logos',
|
||||
'favorite_teams': config.get('ncaa_baseball_scoreboard', {}).get('favorite_teams', []),
|
||||
'enabled': config.get('ncaa_baseball_scoreboard', {}).get('enabled', False)
|
||||
@@ -181,6 +190,7 @@ class OddsTickerManager:
|
||||
'soccer': {
|
||||
'sport': 'soccer',
|
||||
'leagues': config.get('soccer_scoreboard', {}).get('leagues', []),
|
||||
'logo_league': None, # Soccer logos not supported by ESPN API
|
||||
'logo_dir': 'assets/sports/soccer_logos',
|
||||
'favorite_teams': config.get('soccer_scoreboard', {}).get('favorite_teams', []),
|
||||
'enabled': config.get('soccer_scoreboard', {}).get('enabled', False)
|
||||
@@ -240,8 +250,8 @@ class OddsTickerManager:
|
||||
logger.error(f"Error fetching record for {team_abbr} in league {league}: {e}")
|
||||
return "N/A"
|
||||
|
||||
def _get_team_logo(self, team_abbr: str, logo_dir: str) -> Optional[Image.Image]:
|
||||
"""Get team logo from the configured directory."""
|
||||
def _get_team_logo(self, team_abbr: str, logo_dir: str, league: str = None, team_name: str = None) -> Optional[Image.Image]:
|
||||
"""Get team logo from the configured directory, downloading if missing."""
|
||||
if not team_abbr or not logo_dir:
|
||||
logger.debug("Cannot get team logo with missing team_abbr or logo_dir")
|
||||
return None
|
||||
@@ -254,6 +264,18 @@ class OddsTickerManager:
|
||||
return logo
|
||||
else:
|
||||
logger.warning(f"Logo not found at path: {logo_path}")
|
||||
|
||||
# Try to download the missing logo if we have league information
|
||||
if league:
|
||||
logger.info(f"Attempting to download missing logo for {team_abbr} in league {league}")
|
||||
success = download_missing_logo(team_abbr, league, team_name)
|
||||
if success:
|
||||
# Try to load the downloaded logo
|
||||
if os.path.exists(logo_path):
|
||||
logo = Image.open(logo_path)
|
||||
logger.info(f"Successfully downloaded and loaded logo for {team_abbr}")
|
||||
return logo
|
||||
|
||||
return None
|
||||
except Exception as e:
|
||||
logger.error(f"Error loading logo for {team_abbr} from {logo_dir}: {e}")
|
||||
@@ -512,6 +534,7 @@ class OddsTickerManager:
|
||||
'odds': odds_data if has_odds else None,
|
||||
'broadcast_info': broadcast_info,
|
||||
'logo_dir': league_config.get('logo_dir', f'assets/sports/{league.lower()}_logos'),
|
||||
'league': league_config.get('logo_league', league), # Use logo_league for downloading
|
||||
'status': status,
|
||||
'status_state': status_state,
|
||||
'live_info': live_info
|
||||
@@ -804,9 +827,11 @@ class OddsTickerManager:
|
||||
vs_font = self.fonts['medium']
|
||||
datetime_font = self.fonts['medium'] # Use large font for date/time
|
||||
|
||||
# Get team logos
|
||||
home_logo = self._get_team_logo(game['home_team'], game['logo_dir'])
|
||||
away_logo = self._get_team_logo(game['away_team'], game['logo_dir'])
|
||||
# Get team logos (with automatic download if missing)
|
||||
home_logo = self._get_team_logo(game['home_team'], game['logo_dir'],
|
||||
league=game.get('league'), team_name=game.get('home_team_name'))
|
||||
away_logo = self._get_team_logo(game['away_team'], game['logo_dir'],
|
||||
league=game.get('league'), team_name=game.get('away_team_name'))
|
||||
broadcast_logo = None
|
||||
|
||||
# Enhanced broadcast logo debugging
|
||||
|
||||