Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line numberDiff line numberDiff line change
Expand Up@@ -22,6 +22,7 @@ ENV/
.idea/
*.swp
*.swo
emulator_config.json

# Cache directory
cache/
Binary file modifiedassets/sports/ncaa_logos/ncaam.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 0 additions & 2 deletions config/config.template.json
Original file line numberDiff line numberDiff line change
Expand Up@@ -328,7 +328,6 @@
"live_update_interval": 30,
"live_odds_update_interval": 3600,
"odds_update_interval": 3600,
"season_cache_duration_seconds": 86400,
"recent_games_to_show": 1,
"upcoming_games_to_show": 1,
"show_favorite_teams_only": true,
Expand DownExpand Up@@ -410,7 +409,6 @@
"live_update_interval": 30,
"live_odds_update_interval": 3600,
"odds_update_interval": 3600,
"season_cache_duration_seconds": 86400,
"recent_games_to_show": 1,
"upcoming_games_to_show": 1,
"show_favorite_teams_only": true,
Expand Down
4 changes: 2 additions & 2 deletions src/background_data_service.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -27,7 +27,7 @@
import queue
from concurrent.futures import ThreadPoolExecutor, Future
import weakref

from src.cache_manager import CacheManager
# Configure logging
logger = logging.getLogger(__name__)

Expand DownExpand Up@@ -78,7 +78,7 @@ class BackgroundDataService:
with intelligent caching, retry logic, and progress tracking.
"""

def __init__(self, cache_manager, max_workers: int = 3, request_timeout: int = 30):
def __init__(self, cache_manager: CacheManager, max_workers: int = 3, request_timeout: int = 30):
"""
Initialize the background data service.

Expand Down
545 changes: 545 additions & 0 deletions src/base_classes/football.py

Large diffs are not rendered by default.

328 changes: 328 additions & 0 deletions src/base_classes/hockey.py

Large diffs are not rendered by default.

1,135 changes: 1,135 additions & 0 deletions src/base_classes/sports.py

Large diffs are not rendered by default.

163 changes: 22 additions & 141 deletions src/display_controller.py

Large diffs are not rendered by default.

395 changes: 246 additions & 149 deletions src/leaderboard_manager.py

Large diffs are not rendered by default.

122 changes: 56 additions & 66 deletions src/logo_downloader.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -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',
Expand DownExpand Up@@ -95,7 +96,8 @@ def __init__(self, request_timeout: int = 30, retry_attempts: int = 3):
'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()
Expand DownExpand Up@@ -125,7 +127,7 @@ def ensure_logo_directory(self, logo_dir: str) -> bool:
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)
Expand All@@ -134,7 +136,7 @@ def download_logo(self, logo_url: str, filepath: Path, team_name: str) -> bool:
# 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:
Expand All@@ -157,21 +159,21 @@ def download_logo(self, logo_url: str, filepath: Path, team_name: str) -> bool:
# 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:
pass
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]:
Expand All@@ -197,6 +199,29 @@ def fetch_teams_data(self, league: str) -> Optional[Dict]:
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 = []
Expand DownExpand Up@@ -450,66 +475,24 @@ def download_all_ncaa_football_logos(self, include_fcs: bool = True, force_downl
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())
Expand All@@ -531,7 +514,7 @@ def download_all_missing_logos(self, leagues: List[str] = None, force_download:
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
Expand DownExpand Up@@ -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.

Expand All@@ -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

Expand All@@ -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

Expand Down
Loading