Files
LEDMatrix/src/ncaa_fb_managers.py
Alex Resnick 1ee805901f Fix odds and more (#81)
* fix(odds): Resolve incorrect sport and league parameters in _fetch_odds calls

- Fixed SportsCore._fetch_data() to call _fetch_odds(game) instead of _fetch_odds(game, sport_key, sport_key)
- Updated _fetch_odds method signature to accept only game parameter
- Added _fetch_odds_with_params helper method for sport-specific implementations
- Updated sport-specific managers to use correct sport and league parameters:
  - NFL: football/nfl
  - NCAA Football: football/college-football
  - NCAA Hockey: hockey/mens-college-hockey
- Ensures odds are fetched with correct ESPN API endpoints

Fixes #79

* Fix odds and other things

* update hockey

* fix rankings

* update imports

* Fix Logo Cache

* Add show_favorite_team_only attribute

---------

Co-authored-by: ChuckBuilds <33324927+ChuckBuilds@users.noreply.github.com>
Co-authored-by: Alex Resnick <adr8282@gmail.com>
2025-09-28 16:34:21 -04:00

197 lines
9.6 KiB
Python

import os
import time
import logging
import requests
import json
from typing import Dict, Any, Optional, List
from datetime import datetime, timedelta
from src.display_manager import DisplayManager
from src.cache_manager import CacheManager # Keep CacheManager import
import pytz
from src.base_classes.sports import SportsRecent, SportsUpcoming
from src.base_classes.football import Football, FootballLive
from pathlib import Path
# Constants
ESPN_NCAAFB_SCOREBOARD_URL = "https://site.api.espn.com/apis/site/v2/sports/football/college-football/scoreboard" # Changed URL for NCAA FB
class BaseNCAAFBManager(Football): # Renamed class
"""Base class for NCAA FB managers with common functionality.""" # Updated docstring
# Class variables for warning tracking
_no_data_warning_logged = False
_last_warning_time = 0
_warning_cooldown = 60 # Only log warnings once per minute
_shared_data = None
_last_shared_update = 0
_processed_games_cache = {} # Cache for processed game data
_processed_games_timestamp = 0
def __init__(self, config: Dict[str, Any], display_manager: DisplayManager, cache_manager: CacheManager):
self.logger = logging.getLogger('NCAAFB') # Changed logger name
super().__init__(config=config, display_manager=display_manager, cache_manager=cache_manager, logger=self.logger, sport_key="ncaa_fb")
# Configuration is already set in base class
# self.logo_dir and self.update_interval are already configured
# Check display modes to determine what data to fetch
display_modes = self.mode_config.get("display_modes", {})
self.recent_enabled = display_modes.get("ncaa_fb_recent", False)
self.upcoming_enabled = display_modes.get("ncaa_fb_upcoming", False)
self.live_enabled = display_modes.get("ncaa_fb_live", False)
self.league = "college-football"
self.logger.info(f"Initialized NCAAFB manager with display dimensions: {self.display_width}x{self.display_height}")
self.logger.info(f"Logo directory: {self.logo_dir}")
self.logger.info(f"Display modes - Recent: {self.recent_enabled}, Upcoming: {self.upcoming_enabled}, Live: {self.live_enabled}")
def _fetch_ncaa_fb_api_data(self, use_cache: bool = True) -> Optional[Dict]:
"""
Fetches the full season schedule for NCAAFB using week-by-week approach to ensure
we get all games, then caches the complete dataset.
This method now uses background threading to prevent blocking the display.
"""
now = datetime.now(pytz.utc)
season_year = now.year
if now.month < 8:
season_year = now.year - 1
datestring = f"{season_year}0801-{season_year+1}0201"
cache_key = f"ncaafb_schedule_{season_year}"
if use_cache:
cached_data = self.cache_manager.get(cache_key)
if cached_data:
# Validate cached data structure
if isinstance(cached_data, dict) and 'events' in cached_data:
self.logger.info(f"Using cached schedule for {season_year}")
return cached_data
elif isinstance(cached_data, list):
# Handle old cache format (list of events)
self.logger.info(f"Using cached schedule for {season_year} (legacy format)")
return {'events': cached_data}
else:
self.logger.warning(f"Invalid cached data format for {season_year}: {type(cached_data)}")
# Clear invalid cache
self.cache_manager.clear_cache(cache_key)
# If background service is disabled, fall back to synchronous fetch
if not self.background_enabled or not self.background_service:
return self._fetch_ncaa_api_data_sync(use_cache)
self.logger.info(f"Fetching full {season_year} season schedule from ESPN API...")
# Start background fetch
self.logger.info(f"Starting background fetch for {season_year} season schedule...")
def fetch_callback(result):
"""Callback when background fetch completes."""
if result.success:
self.logger.info(f"Background fetch completed for {season_year}: {len(result.data.get('events'))} events")
else:
self.logger.error(f"Background fetch failed for {season_year}: {result.error}")
# Clean up request tracking
if season_year in self.background_fetch_requests:
del self.background_fetch_requests[season_year]
# Get background service configuration
background_config = self.mode_config.get("background_service", {})
timeout = background_config.get("request_timeout", 30)
max_retries = background_config.get("max_retries", 3)
priority = background_config.get("priority", 2)
# Submit background fetch request
request_id = self.background_service.submit_fetch_request(
sport="ncaa_fb",
year=season_year,
url=ESPN_NCAAFB_SCOREBOARD_URL,
cache_key=cache_key,
params={"dates": datestring, "limit": 1000},
headers=self.headers,
timeout=timeout,
max_retries=max_retries,
priority=priority,
callback=fetch_callback
)
# Track the request
self.background_fetch_requests[season_year] = request_id
# For immediate response, try to get partial data
partial_data = self._get_weeks_data()
if partial_data:
return partial_data
return None
def _fetch_ncaa_api_data_sync(self, use_cache: bool = True) -> Optional[Dict]:
"""
Synchronous fallback for fetching NFL data when background service is disabled.
"""
now = datetime.now(pytz.utc)
current_year = now.year
cache_key = f"ncaafb_schedule_{current_year}"
self.logger.info(f"Fetching full {current_year} season schedule from ESPN API (sync mode)...")
try:
response = self.session.get(ESPN_NCAAFB_SCOREBOARD_URL, params={"dates": current_year, "limit":1000}, headers=self.headers, timeout=15)
response.raise_for_status()
data = response.json()
events = data.get('events', [])
if use_cache:
self.cache_manager.set(cache_key, events)
self.logger.info(f"Successfully fetched {len(events)} events for the {current_year} season.")
return {'events': events}
except requests.exceptions.RequestException as e:
self.logger.error(f"[API error fetching full schedule: {e}")
return None
def _fetch_data(self) -> Optional[Dict]:
"""Fetch data using shared data mechanism or direct fetch for live."""
if isinstance(self, NCAAFBLiveManager):
return self._fetch_todays_games()
else:
return self._fetch_ncaa_fb_api_data(use_cache=True)
class NCAAFBLiveManager(BaseNCAAFBManager, FootballLive): # Renamed class
"""Manager for live NCAA FB games.""" # Updated docstring
def __init__(self, config: Dict[str, Any], display_manager: DisplayManager, cache_manager: CacheManager):
super().__init__(config=config, display_manager=display_manager, cache_manager=cache_manager)
self.logger = logging.getLogger('NCAAFBLiveManager') # Changed logger name
if self.test_mode:
# More detailed test game for NCAA FB
self.current_game = {
"id": "testNCAAFB001",
"home_id": "343", "away_id": "567",
"home_abbr": "UGA", "away_abbr": "AUB", # NCAA Examples
"home_score": "28", "away_score": "21",
"period": 4, "period_text": "Q4", "clock": "01:15",
"down_distance_text": "2nd & 5",
"possession": "UGA", # Placeholder ID for home team
"possession_indicator": "home", # Explicitly set for test
"home_timeouts": 1, "away_timeouts": 2,
"home_logo_path": Path(self.logo_dir, "UGA.png"),
"away_logo_path": Path(self.logo_dir, "AUB.png"),
"is_live": True, "is_final": False, "is_upcoming": False, "is_halftime": False,
"status_text": "Q4 01:15"
}
self.live_games = [self.current_game]
logging.info("Initialized NCAAFBLiveManager with test game: AUB vs UGA") # Updated log message
else:
logging.info("Initialized NCAAFBLiveManager in live mode") # Updated log message
class NCAAFBRecentManager(BaseNCAAFBManager, SportsRecent): # Renamed class
"""Manager for recently completed NCAA FB games.""" # Updated docstring
def __init__(self, config: Dict[str, Any], display_manager: DisplayManager, cache_manager: CacheManager):
super().__init__(config, display_manager, cache_manager)
self.logger = logging.getLogger('NCAAFBRecentManager') # Changed logger name
self.logger.info(f"Initialized NCAAFBRecentManager with {len(self.favorite_teams)} favorite teams") # Changed log prefix
class NCAAFBUpcomingManager(BaseNCAAFBManager, SportsUpcoming): # Renamed class
"""Manager for upcoming NCAA FB games.""" # Updated docstring
def __init__(self, config: Dict[str, Any], display_manager: DisplayManager, cache_manager: CacheManager):
super().__init__(config, display_manager, cache_manager)
self.logger = logging.getLogger('NCAAFBUpcomingManager') # Changed logger name
self.logger.info(f"Initialized NCAAFBUpcomingManager with {len(self.favorite_teams)} favorite teams") # Changed log prefix