logo downloader for FCS teams is more robust

This commit is contained in:
Chuck
2025-09-12 14:50:19 -04:00
parent 9298eff554
commit 6d0632acee
38 changed files with 584 additions and 38 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 65 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 109 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 53 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 105 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 84 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 73 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 66 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 81 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 96 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 78 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

View File

@@ -189,9 +189,9 @@
"loop": true, "loop": true,
"request_timeout": 30, "request_timeout": 30,
"dynamic_duration": true, "dynamic_duration": true,
"min_duration": 30, "min_duration": 45,
"max_duration": 300, "max_duration": 300,
"duration_buffer": 0.1, "duration_buffer": 0.2,
"time_per_team": 2.0, "time_per_team": 2.0,
"time_per_league": 3.0 "time_per_league": 3.0
}, },

View File

@@ -11,11 +11,13 @@ try:
from .display_manager import DisplayManager from .display_manager import DisplayManager
from .cache_manager import CacheManager from .cache_manager import CacheManager
from .config_manager import ConfigManager from .config_manager import ConfigManager
from .logo_downloader import download_missing_logo
except ImportError: except ImportError:
# Fallback for direct imports # Fallback for direct imports
from display_manager import DisplayManager from display_manager import DisplayManager
from cache_manager import CacheManager from cache_manager import CacheManager
from config_manager import ConfigManager from config_manager import ConfigManager
from logo_downloader import download_missing_logo
# Import the API counter function from web interface # Import the API counter function from web interface
try: try:
@@ -193,8 +195,8 @@ class LeaderboardManager:
'xlarge': 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, league: str = None, team_name: str = None) -> Optional[Image.Image]:
"""Get team logo from the configured directory.""" """Get team logo from the configured directory, downloading if missing."""
if not team_abbr or not logo_dir: if not team_abbr or not logo_dir:
logger.debug("Cannot get team logo with missing team_abbr or logo_dir") logger.debug("Cannot get team logo with missing team_abbr or logo_dir")
return None return None
@@ -207,6 +209,18 @@ class LeaderboardManager:
return logo return logo
else: else:
logger.warning(f"Logo not found at path: {logo_path}") 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 return None
except Exception as e: except Exception as e:
logger.error(f"Error loading logo for {team_abbr} from {logo_dir}: {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) 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) 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 removed - only show league logo
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))
else: else:
# Fallback if no league logo - just show league name # No league logo available - skip league name display
league_name = league_key.upper().replace('_', ' ') pass
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))
# Move to team section # Move to team section
current_x += 64 + 10 # League logo width + spacing 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.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'],
league=league_key, team_name=team.get('name'))
if team_logo: if team_logo:
# Resize team logo to dynamic size (95% of display height) # Resize team logo to dynamic size (95% of display height)
team_logo = team_logo.resize((logo_size, logo_size), Image.Resampling.LANCZOS) team_logo = team_logo.resize((logo_size, logo_size), Image.Resampling.LANCZOS)

507
src/logo_downloader.py Normal file
View 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)

View File

@@ -11,6 +11,7 @@ from src.display_manager import DisplayManager
from src.cache_manager import CacheManager # Keep CacheManager import from src.cache_manager import CacheManager # Keep CacheManager import
from src.config_manager import ConfigManager from src.config_manager import ConfigManager
from src.odds_manager import OddsManager from src.odds_manager import OddsManager
from src.logo_downloader import download_missing_logo
import pytz import pytz
from requests.adapters import HTTPAdapter from requests.adapters import HTTPAdapter
from urllib3.util.retry import Retry 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 + dx, y + dy), text, font=font, fill=outline_color)
draw.text((x, y), text, font=font, fill=fill) draw.text((x, y), text, font=font, fill=fill)
def _load_and_resize_logo(self, team_abbrev: str) -> Optional[Image.Image]: def _load_and_resize_logo(self, team_abbrev: str, team_name: str = None) -> Optional[Image.Image]:
"""Load and resize a team logo, with caching.""" """Load and resize a team logo, with caching and automatic download if missing."""
if team_abbrev in self._logo_cache: if team_abbrev in self._logo_cache:
return self._logo_cache[team_abbrev] return self._logo_cache[team_abbrev]
@@ -367,9 +368,16 @@ class BaseNCAAFBManager: # Renamed class
self.logger.debug(f"Logo path: {logo_path}") self.logger.debug(f"Logo path: {logo_path}")
try: try:
# Create placeholder if logo doesn't exist (useful for testing) # Try to download missing logo first
if not os.path.exists(logo_path): if not os.path.exists(logo_path):
self.logger.warning(f"Logo not found for {team_abbrev} at {logo_path}. Creating placeholder.") 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) os.makedirs(os.path.dirname(logo_path), exist_ok=True)
logo = Image.new('RGBA', (32, 32), (200, 200, 200, 255)) # Gray placeholder logo = Image.new('RGBA', (32, 32), (200, 200, 200, 255)) # Gray placeholder
draw = ImageDraw.Draw(logo) draw = ImageDraw.Draw(logo)
@@ -784,8 +792,8 @@ class NCAAFBLiveManager(BaseNCAAFBManager): # Renamed class
overlay = Image.new('RGBA', (self.display_width, self.display_height), (0, 0, 0, 0)) 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 draw_overlay = ImageDraw.Draw(overlay) # Draw text elements on overlay first
home_logo = self._load_and_resize_logo(game["home_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"]) away_logo = self._load_and_resize_logo(game["away_abbr"], game.get("away_team_name"))
if not home_logo or not away_logo: 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 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)) overlay = Image.new('RGBA', (self.display_width, self.display_height), (0, 0, 0, 0))
draw_overlay = ImageDraw.Draw(overlay) draw_overlay = ImageDraw.Draw(overlay)
home_logo = self._load_and_resize_logo(game["home_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"]) away_logo = self._load_and_resize_logo(game["away_abbr"], game.get("away_team_name"))
if not home_logo or not away_logo: 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 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)) overlay = Image.new('RGBA', (self.display_width, self.display_height), (0, 0, 0, 0))
draw_overlay = ImageDraw.Draw(overlay) draw_overlay = ImageDraw.Draw(overlay)
home_logo = self._load_and_resize_logo(game["home_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"]) away_logo = self._load_and_resize_logo(game["away_abbr"], game.get("away_team_name"))
if not home_logo or not away_logo: 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 self.logger.error(f"[NCAAFB Upcoming] Failed to load logos for game: {game.get('id')}") # Changed log prefix

View File

@@ -11,6 +11,7 @@ from src.display_manager import DisplayManager
from src.cache_manager import CacheManager from src.cache_manager import CacheManager
from src.config_manager import ConfigManager from src.config_manager import ConfigManager
from src.odds_manager import OddsManager from src.odds_manager import OddsManager
from src.logo_downloader import download_missing_logo
# Import the API counter function from web interface # Import the API counter function from web interface
try: try:
@@ -125,6 +126,7 @@ class OddsTickerManager:
'nfl': { 'nfl': {
'sport': 'football', 'sport': 'football',
'league': 'nfl', 'league': 'nfl',
'logo_league': 'nfl', # ESPN API league identifier for logo downloading
'logo_dir': 'assets/sports/nfl_logos', 'logo_dir': 'assets/sports/nfl_logos',
'favorite_teams': config.get('nfl_scoreboard', {}).get('favorite_teams', []), 'favorite_teams': config.get('nfl_scoreboard', {}).get('favorite_teams', []),
'enabled': config.get('nfl_scoreboard', {}).get('enabled', False) 'enabled': config.get('nfl_scoreboard', {}).get('enabled', False)
@@ -132,6 +134,7 @@ class OddsTickerManager:
'nba': { 'nba': {
'sport': 'basketball', 'sport': 'basketball',
'league': 'nba', 'league': 'nba',
'logo_league': 'nba', # ESPN API league identifier for logo downloading
'logo_dir': 'assets/sports/nba_logos', 'logo_dir': 'assets/sports/nba_logos',
'favorite_teams': config.get('nba_scoreboard', {}).get('favorite_teams', []), 'favorite_teams': config.get('nba_scoreboard', {}).get('favorite_teams', []),
'enabled': config.get('nba_scoreboard', {}).get('enabled', False) 'enabled': config.get('nba_scoreboard', {}).get('enabled', False)
@@ -139,6 +142,7 @@ class OddsTickerManager:
'mlb': { 'mlb': {
'sport': 'baseball', 'sport': 'baseball',
'league': 'mlb', 'league': 'mlb',
'logo_league': 'mlb', # ESPN API league identifier for logo downloading
'logo_dir': 'assets/sports/mlb_logos', 'logo_dir': 'assets/sports/mlb_logos',
'favorite_teams': config.get('mlb', {}).get('favorite_teams', []), 'favorite_teams': config.get('mlb', {}).get('favorite_teams', []),
'enabled': config.get('mlb', {}).get('enabled', False) 'enabled': config.get('mlb', {}).get('enabled', False)
@@ -146,6 +150,7 @@ class OddsTickerManager:
'ncaa_fb': { 'ncaa_fb': {
'sport': 'football', 'sport': 'football',
'league': 'college-football', 'league': 'college-football',
'logo_league': 'ncaa_fb', # ESPN API league identifier for logo downloading
'logo_dir': 'assets/sports/ncaa_fbs_logos', 'logo_dir': 'assets/sports/ncaa_fbs_logos',
'favorite_teams': config.get('ncaa_fb_scoreboard', {}).get('favorite_teams', []), 'favorite_teams': config.get('ncaa_fb_scoreboard', {}).get('favorite_teams', []),
'enabled': config.get('ncaa_fb_scoreboard', {}).get('enabled', False) 'enabled': config.get('ncaa_fb_scoreboard', {}).get('enabled', False)
@@ -153,6 +158,7 @@ class OddsTickerManager:
'milb': { 'milb': {
'sport': 'baseball', 'sport': 'baseball',
'league': 'milb', 'league': 'milb',
'logo_league': 'milb', # ESPN API league identifier for logo downloading (if supported)
'logo_dir': 'assets/sports/milb_logos', 'logo_dir': 'assets/sports/milb_logos',
'favorite_teams': config.get('milb', {}).get('favorite_teams', []), 'favorite_teams': config.get('milb', {}).get('favorite_teams', []),
'enabled': config.get('milb', {}).get('enabled', False) 'enabled': config.get('milb', {}).get('enabled', False)
@@ -160,6 +166,7 @@ class OddsTickerManager:
'nhl': { 'nhl': {
'sport': 'hockey', 'sport': 'hockey',
'league': 'nhl', 'league': 'nhl',
'logo_league': 'nhl', # ESPN API league identifier for logo downloading
'logo_dir': 'assets/sports/nhl_logos', 'logo_dir': 'assets/sports/nhl_logos',
'favorite_teams': config.get('nhl_scoreboard', {}).get('favorite_teams', []), 'favorite_teams': config.get('nhl_scoreboard', {}).get('favorite_teams', []),
'enabled': config.get('nhl_scoreboard', {}).get('enabled', False) 'enabled': config.get('nhl_scoreboard', {}).get('enabled', False)
@@ -167,6 +174,7 @@ class OddsTickerManager:
'ncaam_basketball': { 'ncaam_basketball': {
'sport': 'basketball', 'sport': 'basketball',
'league': 'mens-college-basketball', 'league': 'mens-college-basketball',
'logo_league': 'ncaam_basketball', # ESPN API league identifier for logo downloading
'logo_dir': 'assets/sports/ncaa_fbs_logos', 'logo_dir': 'assets/sports/ncaa_fbs_logos',
'favorite_teams': config.get('ncaam_basketball_scoreboard', {}).get('favorite_teams', []), 'favorite_teams': config.get('ncaam_basketball_scoreboard', {}).get('favorite_teams', []),
'enabled': config.get('ncaam_basketball_scoreboard', {}).get('enabled', False) 'enabled': config.get('ncaam_basketball_scoreboard', {}).get('enabled', False)
@@ -174,6 +182,7 @@ class OddsTickerManager:
'ncaa_baseball': { 'ncaa_baseball': {
'sport': 'baseball', 'sport': 'baseball',
'league': 'college-baseball', 'league': 'college-baseball',
'logo_league': 'ncaa_baseball', # ESPN API league identifier for logo downloading
'logo_dir': 'assets/sports/ncaa_fbs_logos', 'logo_dir': 'assets/sports/ncaa_fbs_logos',
'favorite_teams': config.get('ncaa_baseball_scoreboard', {}).get('favorite_teams', []), 'favorite_teams': config.get('ncaa_baseball_scoreboard', {}).get('favorite_teams', []),
'enabled': config.get('ncaa_baseball_scoreboard', {}).get('enabled', False) 'enabled': config.get('ncaa_baseball_scoreboard', {}).get('enabled', False)
@@ -181,6 +190,7 @@ class OddsTickerManager:
'soccer': { 'soccer': {
'sport': 'soccer', 'sport': 'soccer',
'leagues': config.get('soccer_scoreboard', {}).get('leagues', []), 'leagues': config.get('soccer_scoreboard', {}).get('leagues', []),
'logo_league': None, # Soccer logos not supported by ESPN API
'logo_dir': 'assets/sports/soccer_logos', 'logo_dir': 'assets/sports/soccer_logos',
'favorite_teams': config.get('soccer_scoreboard', {}).get('favorite_teams', []), 'favorite_teams': config.get('soccer_scoreboard', {}).get('favorite_teams', []),
'enabled': config.get('soccer_scoreboard', {}).get('enabled', False) '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}") logger.error(f"Error fetching record for {team_abbr} in league {league}: {e}")
return "N/A" return "N/A"
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, league: str = None, team_name: str = None) -> Optional[Image.Image]:
"""Get team logo from the configured directory.""" """Get team logo from the configured directory, downloading if missing."""
if not team_abbr or not logo_dir: if not team_abbr or not logo_dir:
logger.debug("Cannot get team logo with missing team_abbr or logo_dir") logger.debug("Cannot get team logo with missing team_abbr or logo_dir")
return None return None
@@ -254,6 +264,18 @@ class OddsTickerManager:
return logo return logo
else: else:
logger.warning(f"Logo not found at path: {logo_path}") 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 return None
except Exception as e: except Exception as e:
logger.error(f"Error loading logo for {team_abbr} from {logo_dir}: {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, 'odds': odds_data if has_odds else None,
'broadcast_info': broadcast_info, 'broadcast_info': broadcast_info,
'logo_dir': league_config.get('logo_dir', f'assets/sports/{league.lower()}_logos'), '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': status,
'status_state': status_state, 'status_state': status_state,
'live_info': live_info 'live_info': live_info
@@ -804,9 +827,11 @@ class OddsTickerManager:
vs_font = self.fonts['medium'] vs_font = self.fonts['medium']
datetime_font = self.fonts['medium'] # Use large font for date/time datetime_font = self.fonts['medium'] # Use large font for date/time
# Get team logos # Get team logos (with automatic download if missing)
home_logo = self._get_team_logo(game['home_team'], game['logo_dir']) home_logo = self._get_team_logo(game['home_team'], game['logo_dir'],
away_logo = self._get_team_logo(game['away_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 broadcast_logo = None
# Enhanced broadcast logo debugging # Enhanced broadcast logo debugging