gambling updates

This commit is contained in:
Chuck
2025-07-18 22:12:33 -05:00
parent 88d6f577ef
commit aa379e8369
12 changed files with 423 additions and 6 deletions

View File

@@ -116,6 +116,7 @@
}, },
"nhl_scoreboard": { "nhl_scoreboard": {
"enabled": false, "enabled": false,
"show_odds": false,
"test_mode": false, "test_mode": false,
"update_interval_seconds": 3600, "update_interval_seconds": 3600,
"live_update_interval": 30, "live_update_interval": 30,
@@ -132,6 +133,7 @@
}, },
"nba_scoreboard": { "nba_scoreboard": {
"enabled": false, "enabled": false,
"show_odds": false,
"test_mode": false, "test_mode": false,
"update_interval_seconds": 3600, "update_interval_seconds": 3600,
"live_update_interval": 30, "live_update_interval": 30,
@@ -148,6 +150,7 @@
}, },
"nfl_scoreboard": { "nfl_scoreboard": {
"enabled": false, "enabled": false,
"show_odds": false,
"test_mode": false, "test_mode": false,
"update_interval_seconds": 3600, "update_interval_seconds": 3600,
"live_update_interval": 30, "live_update_interval": 30,
@@ -163,6 +166,7 @@
}, },
"ncaa_fb_scoreboard": { "ncaa_fb_scoreboard": {
"enabled": false, "enabled": false,
"show_odds": false,
"test_mode": false, "test_mode": false,
"update_interval_seconds": 3600, "update_interval_seconds": 3600,
"live_update_interval": 30, "live_update_interval": 30,
@@ -178,6 +182,7 @@
}, },
"ncaam_basketball_scoreboard": { "ncaam_basketball_scoreboard": {
"enabled": false, "enabled": false,
"show_odds": false,
"test_mode": false, "test_mode": false,
"update_interval_seconds": 3600, "update_interval_seconds": 3600,
"live_update_interval": 30, "live_update_interval": 30,
@@ -192,6 +197,7 @@
}, },
"ncaa_baseball_scoreboard": { "ncaa_baseball_scoreboard": {
"enabled": false, "enabled": false,
"show_odds": false,
"test_mode": false, "test_mode": false,
"update_interval_seconds": 3600, "update_interval_seconds": 3600,
"live_update_interval": 30, "live_update_interval": 30,
@@ -212,6 +218,7 @@
}, },
"mlb": { "mlb": {
"enabled": true, "enabled": true,
"show_odds": false,
"test_mode": false, "test_mode": false,
"update_interval_seconds": 3600, "update_interval_seconds": 3600,
"live_update_interval": 30, "live_update_interval": 30,
@@ -228,6 +235,7 @@
}, },
"milb": { "milb": {
"enabled": true, "enabled": true,
"show_odds": false,
"test_mode": false, "test_mode": false,
"update_interval_seconds": 3600, "update_interval_seconds": 3600,
"live_update_interval": 30, "live_update_interval": 30,

View File

@@ -122,8 +122,10 @@ class CacheManager:
self._memory_cache[key] = data self._memory_cache[key] = data
self._memory_cache_timestamps[key] = time.time() self._memory_cache_timestamps[key] = time.time()
except Exception: except (IOError, OSError) as e:
pass # Silently fail if cache save fails self.logger.error(f"Failed to save cache for key '{key}': {e}")
except Exception as e:
self.logger.error(f"An unexpected error occurred while saving cache for key '{key}': {e}")
def load_cache(self, key: str) -> Optional[Dict[str, Any]]: def load_cache(self, key: str) -> Optional[Dict[str, Any]]:
"""Load data from cache with memory caching.""" """Load data from cache with memory caching."""

View File

@@ -1,6 +1,6 @@
import json import json
import os import os
from typing import Dict, Any from typing import Dict, Any, Optional
class ConfigManager: class ConfigManager:
def __init__(self, config_path: str = None, secrets_path: str = None): def __init__(self, config_path: str = None, secrets_path: str = None):
@@ -93,6 +93,18 @@ class ConfigManager:
print(f"An unexpected error occurred while saving configuration: {str(e)}") print(f"An unexpected error occurred while saving configuration: {str(e)}")
raise raise
def get_secret(self, key: str) -> Optional[Any]:
"""Get a secret value by key."""
try:
if not os.path.exists(self.secrets_path):
return None
with open(self.secrets_path, 'r') as f:
secrets = json.load(f)
return secrets.get(key)
except (json.JSONDecodeError, IOError) as e:
print(f"Error reading secrets file: {e}")
return None
def _deep_merge(self, target: Dict, source: Dict) -> None: def _deep_merge(self, target: Dict, source: Dict) -> None:
"""Deep merge source dict into target dict.""" """Deep merge source dict into target dict."""
for key, value in source.items(): for key, value in source.items():

View File

@@ -11,6 +11,7 @@ from .cache_manager import CacheManager
from requests.adapters import HTTPAdapter from requests.adapters import HTTPAdapter
from urllib3.util.retry import Retry from urllib3.util.retry import Retry
import pytz import pytz
from src.odds_manager import OddsManager
# Get logger # Get logger
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -21,8 +22,10 @@ class BaseMLBManager:
self.config = config self.config = config
self.display_manager = display_manager self.display_manager = display_manager
self.mlb_config = config.get('mlb', {}) self.mlb_config = config.get('mlb', {})
self.show_odds = self.mlb_config.get("show_odds", False)
self.favorite_teams = self.mlb_config.get('favorite_teams', []) self.favorite_teams = self.mlb_config.get('favorite_teams', [])
self.cache_manager = CacheManager() self.cache_manager = CacheManager()
self.odds_manager = OddsManager(self.cache_manager, self.config)
self.logger = logging.getLogger(__name__) self.logger = logging.getLogger(__name__)
self.logger.setLevel(logging.INFO) # Set logger level to INFO self.logger.setLevel(logging.INFO) # Set logger level to INFO
@@ -50,6 +53,22 @@ class BaseMLBManager:
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36' 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36'
} }
def _fetch_odds(self, game: Dict) -> None:
"""Fetch odds for a game and attach it to the game dictionary."""
if not self.show_odds:
return
try:
odds_data = self.odds_manager.get_odds(
sport="baseball",
league="mlb",
event_id=game["id"]
)
if odds_data:
game['odds'] = odds_data
except Exception as e:
self.logger.error(f"Error fetching odds for game {game.get('id', 'N/A')}: {e}")
def _get_team_logo(self, team_abbr: str) -> Optional[Image.Image]: def _get_team_logo(self, team_abbr: str) -> Optional[Image.Image]:
"""Get team logo from the configured directory.""" """Get team logo from the configured directory."""
try: try:
@@ -241,6 +260,40 @@ class BaseMLBManager:
# draw.text((score_x, score_y), score_text, font=score_font, fill=(255, 255, 255)) # draw.text((score_x, score_y), score_text, font=score_font, fill=(255, 255, 255))
self._draw_text_with_outline(draw, score_text, (score_x, score_y), score_font) self._draw_text_with_outline(draw, score_text, (score_x, score_y), score_font)
# Draw betting odds if available and enabled
if self.show_odds and 'odds' in game_data:
odds_details = game_data['odds'].get('details', 'N/A')
home_team_odds = game_data['odds'].get('home_team_odds', {})
away_team_odds = game_data['odds'].get('away_team_odds', {})
# Extract spread and format it
home_spread = home_team_odds.get('point_spread', 'N/A')
away_spread = away_team_odds.get('point_spread', 'N/A')
# Add a plus sign to positive spreads
if isinstance(home_spread, (int, float)) and home_spread > 0:
home_spread = f"+{home_spread}"
if isinstance(away_spread, (int, float)) and away_spread > 0:
away_spread = f"+{away_spread}"
# Define colors for odds text
odds_font = self.display_manager.status_font
odds_color = (255, 0, 0) # Red text
outline_color = (0, 0, 0) # Black outline
# Draw away team odds
if away_spread != 'N/A':
away_odds_x = 5 # Adjust as needed
away_odds_y = height - 10 # Adjust as needed
self._draw_text_with_outline(draw, str(away_spread), (away_odds_x, away_odds_y), odds_font, odds_color, outline_color)
# Draw home team odds
if home_spread != 'N/A':
home_odds_x = width - 30 # Adjust as needed
home_odds_y = height - 10 # Adjust as needed
self._draw_text_with_outline(draw, str(home_spread), (home_odds_x, home_odds_y), odds_font, odds_color, outline_color)
return image return image
def _format_game_time(self, game_time: str) -> str: def _format_game_time(self, game_time: str) -> str:
@@ -568,6 +621,7 @@ class MLBLiveManager(BaseMLBManager):
game['home_team'] in self.favorite_teams or game['home_team'] in self.favorite_teams or
game['away_team'] in self.favorite_teams game['away_team'] in self.favorite_teams
): ):
self._fetch_odds(game)
# Ensure scores are valid numbers # Ensure scores are valid numbers
try: try:
game['home_score'] = int(game['home_score']) game['home_score'] = int(game['home_score'])
@@ -942,6 +996,7 @@ class MLBRecentManager(BaseMLBManager):
# Only add favorite team games that are final and within time window # Only add favorite team games that are final and within time window
if is_favorite_game and is_final and is_within_time: if is_favorite_game and is_final and is_within_time:
self._fetch_odds(game)
new_recent_games.append(game) new_recent_games.append(game)
logger.info(f"[MLB] Added favorite team game to recent list: {game['away_team']} @ {game['home_team']}") logger.info(f"[MLB] Added favorite team game to recent list: {game['away_team']} @ {game['home_team']}")
@@ -1056,6 +1111,7 @@ class MLBUpcomingManager(BaseMLBManager):
logger.info(f"Status state not final: {game['status_state'] not in ['post', 'final', 'completed']}") logger.info(f"Status state not final: {game['status_state'] not in ['post', 'final', 'completed']}")
if is_upcoming: if is_upcoming:
self._fetch_odds(game)
new_upcoming_games.append(game) new_upcoming_games.append(game)
logger.info(f"Added favorite team game to upcoming list: {game['away_team']} @ {game['home_team']}") logger.info(f"Added favorite team game to upcoming list: {game['away_team']} @ {game['home_team']}")

View File

@@ -10,6 +10,7 @@ from datetime import datetime, timedelta, timezone
from src.display_manager import DisplayManager 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
import pytz import pytz
# Constants # Constants
@@ -32,6 +33,7 @@ class BaseNBAManager:
_shared_data = None _shared_data = None
_last_shared_update = 0 _last_shared_update = 0
cache_manager = CacheManager() # Make cache_manager a class attribute cache_manager = CacheManager() # Make cache_manager a class attribute
odds_manager = OddsManager(cache_manager)
logger = logging.getLogger('NBA') # Make logger a class attribute logger = logging.getLogger('NBA') # Make logger a class attribute
def __init__(self, config: Dict[str, Any], display_manager: DisplayManager): def __init__(self, config: Dict[str, Any], display_manager: DisplayManager):
@@ -40,6 +42,7 @@ class BaseNBAManager:
self.config = config self.config = config
self.nba_config = config.get("nba_scoreboard", {}) self.nba_config = config.get("nba_scoreboard", {})
self.is_enabled = self.nba_config.get("enabled", False) self.is_enabled = self.nba_config.get("enabled", False)
self.show_odds = self.nba_config.get("show_odds", False)
self.test_mode = self.nba_config.get("test_mode", False) self.test_mode = self.nba_config.get("test_mode", False)
self.logo_dir = self.nba_config.get("logo_dir", "assets/sports/nba_logos") self.logo_dir = self.nba_config.get("logo_dir", "assets/sports/nba_logos")
self.update_interval = self.nba_config.get("update_interval_seconds", 300) self.update_interval = self.nba_config.get("update_interval_seconds", 300)

View File

@@ -10,6 +10,7 @@ import numpy as np
from .cache_manager import CacheManager from .cache_manager import CacheManager
from requests.adapters import HTTPAdapter from requests.adapters import HTTPAdapter
from urllib3.util.retry import Retry from urllib3.util.retry import Retry
from src.odds_manager import OddsManager
import pytz import pytz
# Get logger # Get logger
@@ -24,8 +25,10 @@ class BaseNCAABaseballManager:
self.config = config self.config = config
self.display_manager = display_manager self.display_manager = display_manager
self.ncaa_baseball_config = config.get('ncaa_baseball_scoreboard', {}) self.ncaa_baseball_config = config.get('ncaa_baseball_scoreboard', {})
self.show_odds = self.ncaa_baseball_config.get('show_odds', False)
self.favorite_teams = self.ncaa_baseball_config.get('favorite_teams', []) self.favorite_teams = self.ncaa_baseball_config.get('favorite_teams', [])
self.cache_manager = CacheManager() self.cache_manager = CacheManager()
self.odds_manager = OddsManager(self.cache_manager, self.config)
self.logger = logging.getLogger(__name__) self.logger = logging.getLogger(__name__)
self.logger.setLevel(logging.DEBUG) # Set logger level to DEBUG self.logger.setLevel(logging.DEBUG) # Set logger level to DEBUG
@@ -53,6 +56,22 @@ class BaseNCAABaseballManager:
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36' 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36'
} }
def _fetch_odds(self, game: Dict) -> None:
"""Fetch odds for a game and attach it to the game dictionary."""
if not self.show_odds:
return
try:
odds_data = self.odds_manager.get_odds(
sport="baseball",
league="college-baseball",
event_id=game["id"]
)
if odds_data:
game['odds'] = odds_data
except Exception as e:
self.logger.error(f"Error fetching odds for game {game.get('id', 'N/A')}: {e}")
def _get_team_logo(self, team_abbr: str) -> Optional[Image.Image]: def _get_team_logo(self, team_abbr: str) -> Optional[Image.Image]:
"""Get team logo from the configured directory or generate a fallback.""" """Get team logo from the configured directory or generate a fallback."""
try: try:
@@ -256,6 +275,40 @@ class BaseNCAABaseballManager:
score_x = (width - score_width) // 2 score_x = (width - score_width) // 2
score_y = height - score_font.getmetrics()[0] - 2 # Adjusted for font metrics score_y = height - score_font.getmetrics()[0] - 2 # Adjusted for font metrics
self._draw_text_with_outline(draw, score_text, (score_x, score_y), score_font) self._draw_text_with_outline(draw, score_text, (score_x, score_y), score_font)
# Draw betting odds if available and enabled
if self.show_odds and 'odds' in game_data:
odds_details = game_data['odds'].get('details', 'N/A')
home_team_odds = game_data['odds'].get('home_team_odds', {})
away_team_odds = game_data['odds'].get('away_team_odds', {})
# Extract spread and format it
home_spread = home_team_odds.get('point_spread', 'N/A')
away_spread = away_team_odds.get('point_spread', 'N/A')
# Add a plus sign to positive spreads
if isinstance(home_spread, (int, float)) and home_spread > 0:
home_spread = f"+{home_spread}"
if isinstance(away_spread, (int, float)) and away_spread > 0:
away_spread = f"+{away_spread}"
# Define colors for odds text
odds_font = self.display_manager.status_font
odds_color = (255, 0, 0) # Red text
outline_color = (0, 0, 0) # Black outline
# Draw away team odds
if away_spread != 'N/A':
away_odds_x = 5
away_odds_y = height - 10
self._draw_text_with_outline(draw, str(away_spread), (away_odds_x, away_odds_y), odds_font, odds_color, outline_color)
# Draw home team odds
if home_spread != 'N/A':
home_odds_x = width - 30
home_odds_y = height - 10
self._draw_text_with_outline(draw, str(home_spread), (home_odds_x, home_odds_y), odds_font, odds_color, outline_color)
return image return image
@@ -498,6 +551,7 @@ class NCAABaseballLiveManager(BaseNCAABaseballManager):
try: try:
game['home_score'] = int(game['home_score']) game['home_score'] = int(game['home_score'])
game['away_score'] = int(game['away_score']) game['away_score'] = int(game['away_score'])
self._fetch_odds(game)
new_live_games.append(game) new_live_games.append(game)
except (ValueError, TypeError): except (ValueError, TypeError):
self.logger.warning(f"[NCAABaseball] Invalid score format for game {game['away_team']} @ {game['home_team']}") self.logger.warning(f"[NCAABaseball] Invalid score format for game {game['away_team']} @ {game['home_team']}")
@@ -777,6 +831,7 @@ class NCAABaseballRecentManager(BaseNCAABaseballManager):
logger.info(f"[NCAABaseball] Is within time window: {is_within_time}") logger.info(f"[NCAABaseball] Is within time window: {is_within_time}")
if is_final and is_within_time: if is_final and is_within_time:
self._fetch_odds(game)
new_recent_games.append(game) new_recent_games.append(game)
logger.info(f"[NCAABaseball] Added favorite team game to recent list: {game['away_team']} @ {game['home_team']}") logger.info(f"[NCAABaseball] Added favorite team game to recent list: {game['away_team']} @ {game['home_team']}")
@@ -869,6 +924,7 @@ class NCAABaseballUpcomingManager(BaseNCAABaseballManager):
logger.info(f"[NCAABaseball] Is upcoming state: {is_upcoming_state}") logger.info(f"[NCAABaseball] Is upcoming state: {is_upcoming_state}")
if is_within_time and is_upcoming_state: if is_within_time and is_upcoming_state:
self._fetch_odds(game)
new_upcoming_games.append(game) new_upcoming_games.append(game)
logger.info(f"[NCAABaseball] Added favorite team game to upcoming list: {game['away_team']} @ {game['home_team']}") logger.info(f"[NCAABaseball] Added favorite team game to upcoming list: {game['away_team']} @ {game['home_team']}")

View File

@@ -10,6 +10,7 @@ from datetime import datetime, timedelta, timezone
from src.display_manager import DisplayManager 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
import pytz import pytz
# Constants # Constants
@@ -83,6 +84,7 @@ class BaseNCAAFBManager: # Renamed class
_shared_data = None _shared_data = None
_last_shared_update = 0 _last_shared_update = 0
cache_manager = CacheManager() cache_manager = CacheManager()
odds_manager = OddsManager(cache_manager)
logger = logging.getLogger('NCAAFB') # Changed logger name logger = logging.getLogger('NCAAFB') # Changed logger name
def __init__(self, config: Dict[str, Any], display_manager: DisplayManager): def __init__(self, config: Dict[str, Any], display_manager: DisplayManager):
@@ -91,6 +93,7 @@ class BaseNCAAFBManager: # Renamed class
self.config = config self.config = config
self.ncaa_fb_config = config.get("ncaa_fb_scoreboard", {}) # Changed config key self.ncaa_fb_config = config.get("ncaa_fb_scoreboard", {}) # Changed config key
self.is_enabled = self.ncaa_fb_config.get("enabled", False) self.is_enabled = self.ncaa_fb_config.get("enabled", False)
self.show_odds = self.ncaa_fb_config.get("show_odds", False)
self.test_mode = self.ncaa_fb_config.get("test_mode", False) self.test_mode = self.ncaa_fb_config.get("test_mode", False)
self.logo_dir = self.ncaa_fb_config.get("logo_dir", "assets/sports/ncaa_fbs_logos") # Changed logo dir self.logo_dir = self.ncaa_fb_config.get("logo_dir", "assets/sports/ncaa_fbs_logos") # Changed logo dir
self.update_interval = self.ncaa_fb_config.get("update_interval_seconds", 60) self.update_interval = self.ncaa_fb_config.get("update_interval_seconds", 60)
@@ -121,6 +124,44 @@ class BaseNCAAFBManager: # Renamed class
except pytz.UnknownTimeZoneError: except pytz.UnknownTimeZoneError:
return pytz.utc return pytz.utc
def _fetch_odds(self, game: Dict) -> None:
"""Fetch odds for a specific game if conditions are met."""
self.logger.debug(f"Checking odds for game: {game.get('id', 'N/A')}")
# Ensure the API key is set in the secrets config
if not self.config_manager.get_secret("the_odds_api_key"):
if self._should_log('no_api_key', cooldown=3600): # Log once per hour
self.logger.warning("Odds API key not found. Skipping odds fetch.")
return
# Check if odds should be shown for this sport
if not self.show_odds:
self.logger.debug("Odds display is disabled for NCAAFB.")
return
# Fetch odds using OddsManager
try:
# Determine update interval based on game state
is_live = game.get('status', '').lower() == 'in'
update_interval = self.ncaa_fb_config.get("live_odds_update_interval", 60) if is_live \
else self.ncaa_fb_config.get("odds_update_interval", 3600)
odds_data = self.odds_manager.get_odds(
sport="football",
league="college-football",
event_id=game['id'],
update_interval_seconds=update_interval
)
if odds_data:
game['odds'] = odds_data
self.logger.debug(f"Successfully fetched and attached odds for game {game['id']}")
else:
self.logger.debug(f"No odds data returned for game {game['id']}")
except Exception as e:
self.logger.error(f"Error fetching odds for game {game.get('id', 'N/A')}: {e}")
@classmethod @classmethod
def _fetch_shared_data(cls, past_days: int, future_days: int, date_str: str = None) -> Optional[Dict]: def _fetch_shared_data(cls, past_days: int, future_days: int, date_str: str = None) -> Optional[Dict]:
"""Fetch and cache data for all managers to share.""" """Fetch and cache data for all managers to share."""
@@ -552,6 +593,9 @@ class NCAAFBLiveManager(BaseNCAAFBManager): # Renamed class
details["home_abbr"] in self.favorite_teams or details["home_abbr"] in self.favorite_teams or
details["away_abbr"] in self.favorite_teams details["away_abbr"] in self.favorite_teams
): ):
# Fetch odds if enabled
if self.show_odds:
self._fetch_odds(details)
new_live_games.append(details) new_live_games.append(details)
# Log changes or periodically # Log changes or periodically

View File

@@ -10,6 +10,7 @@ from datetime import datetime, timedelta, timezone
from src.display_manager import DisplayManager 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
import pytz import pytz
# Constants # Constants
@@ -32,6 +33,7 @@ class BaseNCAAMBasketballManager:
_shared_data = None _shared_data = None
_last_shared_update = 0 _last_shared_update = 0
cache_manager = CacheManager() # Make cache_manager a class attribute cache_manager = CacheManager() # Make cache_manager a class attribute
odds_manager = OddsManager(cache_manager, ConfigManager())
logger = logging.getLogger('NCAAMBasketball') # Make logger a class attribute logger = logging.getLogger('NCAAMBasketball') # Make logger a class attribute
def __init__(self, config: Dict[str, Any], display_manager: DisplayManager): def __init__(self, config: Dict[str, Any], display_manager: DisplayManager):
@@ -40,6 +42,7 @@ class BaseNCAAMBasketballManager:
self.config = config self.config = config
self.ncaam_basketball_config = config.get("ncaam_basketball_scoreboard", {}) self.ncaam_basketball_config = config.get("ncaam_basketball_scoreboard", {})
self.is_enabled = self.ncaam_basketball_config.get("enabled", False) self.is_enabled = self.ncaam_basketball_config.get("enabled", False)
self.show_odds = self.ncaam_basketball_config.get("show_odds", False)
self.test_mode = self.ncaam_basketball_config.get("test_mode", False) self.test_mode = self.ncaam_basketball_config.get("test_mode", False)
self.logo_dir = self.ncaam_basketball_config.get("logo_dir", "assets/sports/ncaam_logos") self.logo_dir = self.ncaam_basketball_config.get("logo_dir", "assets/sports/ncaam_logos")
self.update_interval = self.ncaam_basketball_config.get("update_interval_seconds", 300) self.update_interval = self.ncaam_basketball_config.get("update_interval_seconds", 300)
@@ -66,6 +69,22 @@ class BaseNCAAMBasketballManager:
self.logger.info(f"Initialized NCAAMBasketball manager with display dimensions: {self.display_width}x{self.display_height}") self.logger.info(f"Initialized NCAAMBasketball manager with display dimensions: {self.display_width}x{self.display_height}")
self.logger.info(f"Logo directory: {self.logo_dir}") self.logger.info(f"Logo directory: {self.logo_dir}")
def _fetch_odds(self, game: Dict) -> None:
"""Fetch odds for a game and attach it to the game dictionary."""
if not self.show_odds:
return
try:
odds_data = self.odds_manager.get_odds(
sport="basketball",
league="mens-college-basketball",
event_id=game["id"]
)
if odds_data:
game['odds'] = odds_data
except Exception as e:
self.logger.error(f"Error fetching odds for game {game.get('id', 'N/A')}: {e}")
def _get_timezone(self): def _get_timezone(self):
try: try:
return pytz.timezone(self.config_manager.get_timezone()) return pytz.timezone(self.config_manager.get_timezone())
@@ -436,7 +455,8 @@ class BaseNCAAMBasketballManager:
"away_score": away_team.get("score", "0"), "away_score": away_team.get("score", "0"),
"away_logo_path": os.path.join(self.logo_dir, f"{away_team['team']['abbreviation']}.png"), "away_logo_path": os.path.join(self.logo_dir, f"{away_team['team']['abbreviation']}.png"),
"game_time": game_time, "game_time": game_time,
"game_date": game_date "game_date": game_date,
"id": game_event.get("id")
} }
# Log game details for debugging # Log game details for debugging
@@ -568,6 +588,25 @@ class BaseNCAAMBasketballManager:
clock_y = period_y + 10 # Position below period clock_y = period_y + 10 # Position below period
self._draw_text_with_outline(draw, clock, (clock_x, clock_y), self.fonts['time']) self._draw_text_with_outline(draw, clock, (clock_x, clock_y), self.fonts['time'])
# Display odds if available
if 'odds' in game:
odds = game['odds']
spread = odds.get('spread', {}).get('point', None)
if spread is not None:
# Format spread text
spread_text = f"{spread:+.1f}" if spread > 0 else f"{spread:.1f}"
# Choose color and position based on which team has the spread
if odds.get('spread', {}).get('team') == game['home_abbr']:
text_color = (255, 100, 100) # Reddish
spread_x = self.display_width - draw.textlength(spread_text, font=self.fonts['status']) - 2
else:
text_color = (100, 255, 100) # Greenish
spread_x = 2
spread_y = self.display_height - 8
self._draw_text_with_outline(draw, spread_text, (spread_x, spread_y), self.fonts['status'], fill=text_color)
# Display the image # Display the image
self.display_manager.image.paste(main_img, (0, 0)) self.display_manager.image.paste(main_img, (0, 0))
self.display_manager.update_display() self.display_manager.update_display()
@@ -677,6 +716,7 @@ class NCAAMBasketballLiveManager(BaseNCAAMBasketballManager):
details["home_abbr"] in self.favorite_teams or details["home_abbr"] in self.favorite_teams or
details["away_abbr"] in self.favorite_teams details["away_abbr"] in self.favorite_teams
): ):
self._fetch_odds(details)
new_live_games.append(details) new_live_games.append(details)
if self.favorite_teams and ( if self.favorite_teams and (
details["home_abbr"] in self.favorite_teams or details["home_abbr"] in self.favorite_teams or
@@ -810,6 +850,7 @@ class NCAAMBasketballRecentManager(BaseNCAAMBasketballManager):
game = self._extract_game_details(event) game = self._extract_game_details(event)
# Filter for recent games: must be final and within the time window # Filter for recent games: must be final and within the time window
if game and game['is_final'] and game['is_within_window']: if game and game['is_final'] and game['is_within_window']:
self._fetch_odds(game)
new_recent_games.append(game) new_recent_games.append(game)
# Filter for favorite teams # Filter for favorite teams
@@ -940,6 +981,7 @@ class NCAAMBasketballUpcomingManager(BaseNCAAMBasketballManager):
for event in events: for event in events:
game = self._extract_game_details(event) game = self._extract_game_details(event)
if game and game['is_upcoming']: if game and game['is_upcoming']:
self._fetch_odds(game)
new_upcoming_games.append(game) new_upcoming_games.append(game)
self.logger.debug(f"Processing upcoming game: {game['away_abbr']} vs {game['home_abbr']}") self.logger.debug(f"Processing upcoming game: {game['away_abbr']} vs {game['home_abbr']}")

View File

@@ -10,6 +10,7 @@ from datetime import datetime, timedelta, timezone
from src.display_manager import DisplayManager 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
import pytz import pytz
# Constants # Constants
@@ -83,6 +84,7 @@ class BaseNFLManager: # Renamed class
_shared_data = None _shared_data = None
_last_shared_update = 0 _last_shared_update = 0
cache_manager = CacheManager() cache_manager = CacheManager()
odds_manager = OddsManager(cache_manager)
logger = logging.getLogger('NFL') # Changed logger name logger = logging.getLogger('NFL') # Changed logger name
def __init__(self, config: Dict[str, Any], display_manager: DisplayManager): def __init__(self, config: Dict[str, Any], display_manager: DisplayManager):
@@ -91,6 +93,7 @@ class BaseNFLManager: # Renamed class
self.config = config self.config = config
self.nfl_config = config.get("nfl_scoreboard", {}) # Changed config key self.nfl_config = config.get("nfl_scoreboard", {}) # Changed config key
self.is_enabled = self.nfl_config.get("enabled", False) self.is_enabled = self.nfl_config.get("enabled", False)
self.show_odds = self.nfl_config.get("show_odds", False)
self.test_mode = self.nfl_config.get("test_mode", False) self.test_mode = self.nfl_config.get("test_mode", False)
self.logo_dir = self.nfl_config.get("logo_dir", "assets/sports/nfl_logos") # Changed logo dir self.logo_dir = self.nfl_config.get("logo_dir", "assets/sports/nfl_logos") # Changed logo dir
self.update_interval = self.nfl_config.get("update_interval_seconds", 60) self.update_interval = self.nfl_config.get("update_interval_seconds", 60)
@@ -121,6 +124,44 @@ class BaseNFLManager: # Renamed class
except pytz.UnknownTimeZoneError: except pytz.UnknownTimeZoneError:
return pytz.utc return pytz.utc
def _fetch_odds(self, game: Dict) -> None:
"""Fetch odds for a specific game if conditions are met."""
self.logger.debug(f"Checking odds for game: {game.get('id', 'N/A')}")
# Ensure the API key is set in the secrets config
if not self.config_manager.get_secret("the_odds_api_key"):
if self._should_log('no_api_key', cooldown=3600): # Log once per hour
self.logger.warning("Odds API key not found. Skipping odds fetch.")
return
# Check if odds should be shown for this sport
if not self.show_odds:
self.logger.debug("Odds display is disabled for NFL.")
return
# Fetch odds using OddsManager
try:
# Determine update interval based on game state
is_live = game.get('status', '').lower() == 'in'
update_interval = self.nfl_config.get("live_odds_update_interval", 60) if is_live \
else self.nfl_config.get("odds_update_interval", 3600)
odds_data = self.odds_manager.get_odds(
sport="football",
league="nfl",
event_id=game['id'],
update_interval_seconds=update_interval
)
if odds_data:
game['odds'] = odds_data
self.logger.debug(f"Successfully fetched and attached odds for game {game['id']}")
else:
self.logger.debug(f"No odds data returned for game {game['id']}")
except Exception as e:
self.logger.error(f"Error fetching odds for game {game.get('id', 'N/A')}: {e}")
@classmethod @classmethod
def _fetch_shared_data(cls, past_days: int, future_days: int, date_str: str = None) -> Optional[Dict]: def _fetch_shared_data(cls, past_days: int, future_days: int, date_str: str = None) -> Optional[Dict]:
"""Fetch and cache data for all managers to share.""" """Fetch and cache data for all managers to share."""

View File

@@ -10,6 +10,7 @@ from datetime import datetime, timedelta, timezone
from src.display_manager import DisplayManager 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
import pytz import pytz
# Constants # Constants
@@ -81,6 +82,7 @@ class BaseNHLManager:
_shared_data = None _shared_data = None
_last_shared_update = 0 _last_shared_update = 0
cache_manager = CacheManager() # Make cache_manager a class attribute cache_manager = CacheManager() # Make cache_manager a class attribute
odds_manager = OddsManager(cache_manager, ConfigManager())
logger = logging.getLogger('NHL') # Make logger a class attribute logger = logging.getLogger('NHL') # Make logger a class attribute
def __init__(self, config: Dict[str, Any], display_manager: DisplayManager): def __init__(self, config: Dict[str, Any], display_manager: DisplayManager):
@@ -89,6 +91,7 @@ class BaseNHLManager:
self.config = config self.config = config
self.nhl_config = config.get("nhl_scoreboard", {}) self.nhl_config = config.get("nhl_scoreboard", {})
self.is_enabled = self.nhl_config.get("enabled", False) self.is_enabled = self.nhl_config.get("enabled", False)
self.show_odds = self.nhl_config.get("show_odds", False)
self.test_mode = self.nhl_config.get("test_mode", False) # Use test_mode from config self.test_mode = self.nhl_config.get("test_mode", False) # Use test_mode from config
self.logo_dir = self.nhl_config.get("logo_dir", "assets/sports/nhl_logos") self.logo_dir = self.nhl_config.get("logo_dir", "assets/sports/nhl_logos")
self.update_interval = self.nhl_config.get("update_interval_seconds", 60) self.update_interval = self.nhl_config.get("update_interval_seconds", 60)
@@ -115,6 +118,22 @@ class BaseNHLManager:
self.logger.info(f"Initialized NHL manager with display dimensions: {self.display_width}x{self.display_height}") self.logger.info(f"Initialized NHL manager with display dimensions: {self.display_width}x{self.display_height}")
self.logger.info(f"Logo directory: {self.logo_dir}") self.logger.info(f"Logo directory: {self.logo_dir}")
def _fetch_odds(self, game: Dict) -> None:
"""Fetch odds for a game and attach it to the game dictionary."""
if not self.show_odds:
return
try:
odds_data = self.odds_manager.get_odds(
sport="hockey",
league="nhl",
event_id=game["id"]
)
if odds_data:
game['odds'] = odds_data
except Exception as e:
self.logger.error(f"Error fetching odds for game {game.get('id', 'N/A')}: {e}")
def _get_timezone(self): def _get_timezone(self):
try: try:
return pytz.timezone(self.config_manager.get_timezone()) return pytz.timezone(self.config_manager.get_timezone())
@@ -387,7 +406,8 @@ class BaseNHLManager:
"away_score": away_team.get("score", "0"), "away_score": away_team.get("score", "0"),
"away_logo_path": os.path.join(self.logo_dir, f"{away_team['team']['abbreviation']}.png"), "away_logo_path": os.path.join(self.logo_dir, f"{away_team['team']['abbreviation']}.png"),
"game_time": game_time, "game_time": game_time,
"game_date": game_date "game_date": game_date,
"id": game_event.get("id")
} }
# Log game details for debugging # Log game details for debugging
@@ -513,6 +533,25 @@ class BaseNHLManager:
status_y = 5 status_y = 5
self._draw_text_with_outline(draw, status_text, (status_x, status_y), self.fonts['time']) self._draw_text_with_outline(draw, status_text, (status_x, status_y), self.fonts['time'])
# Display odds if available
if 'odds' in game:
odds = game['odds']
spread = odds.get('spread', {}).get('point', None)
if spread is not None:
# Format spread text
spread_text = f"{spread:+.1f}" if spread > 0 else f"{spread:.1f}"
# Choose color and position based on which team has the spread
if odds.get('spread', {}).get('team') == game['home_abbr']:
text_color = (255, 100, 100) # Reddish
spread_x = self.display_width - draw.textlength(spread_text, font=self.fonts['status']) - 2
else:
text_color = (100, 255, 100) # Greenish
spread_x = 2
spread_y = self.display_height - 8
self._draw_text_with_outline(draw, spread_text, (spread_x, spread_y), self.fonts['status'], fill=text_color)
# Display the image # Display the image
self.display_manager.image.paste(main_img, (0, 0)) self.display_manager.image.paste(main_img, (0, 0))
self.display_manager.update_display() self.display_manager.update_display()
@@ -608,6 +647,7 @@ class NHLLiveManager(BaseNHLManager):
details["home_abbr"] in self.favorite_teams or details["home_abbr"] in self.favorite_teams or
details["away_abbr"] in self.favorite_teams details["away_abbr"] in self.favorite_teams
): ):
self._fetch_odds(details)
new_live_games.append(details) new_live_games.append(details)
# Only log if there's a change in games or enough time has passed # Only log if there's a change in games or enough time has passed
@@ -707,6 +747,9 @@ class NHLRecentManager(BaseNHLManager):
for event in events: for event in events:
game = self._extract_game_details(event) game = self._extract_game_details(event)
if game: if game:
# Fetch odds if enabled
if self.show_odds:
self._fetch_odds(game)
self.recent_games.append(game) self.recent_games.append(game)
self.logger.debug(f"Processing game: {game['away_abbr']} vs {game['home_abbr']} - Final: {game['is_final']}, Within window: {game['is_within_window']}") self.logger.debug(f"Processing game: {game['away_abbr']} vs {game['home_abbr']} - Final: {game['is_final']}, Within window: {game['is_within_window']}")
@@ -796,6 +839,9 @@ class NHLUpcomingManager(BaseNHLManager):
self.logger.debug(f"[NHL] Game time: {game['start_time_utc']}") self.logger.debug(f"[NHL] Game time: {game['start_time_utc']}")
if not game['is_final'] and game['is_within_window']: if not game['is_final'] and game['is_within_window']:
# Fetch odds if enabled
if self.show_odds:
self._fetch_odds(game)
new_upcoming_games.append(game) new_upcoming_games.append(game)
self.logger.debug(f"[NHL] Added to upcoming games: {game['away_abbr']} vs {game['home_abbr']}") self.logger.debug(f"[NHL] Added to upcoming games: {game['away_abbr']} vs {game['home_abbr']}")

65
src/odds_manager.py Normal file
View File

@@ -0,0 +1,65 @@
import requests
import logging
import json
from datetime import datetime, timedelta
from src.cache_manager import CacheManager
from src.config_manager import ConfigManager
from typing import Optional, List, Dict, Any
class OddsManager:
def __init__(self, cache_manager: CacheManager, config_manager: ConfigManager):
self.cache_manager = cache_manager
self.config_manager = config_manager
self.logger = logging.getLogger(__name__)
self.base_url = "https://sports.core.api.espn.com/v2/sports"
def get_odds(self, sport: str, league: str, event_id: str, update_interval_seconds=3600):
cache_key = f"odds_espn_{sport}_{league}_{event_id}"
cached_data = self.cache_manager.get_cached_data(cache_key, max_age=update_interval_seconds)
if cached_data:
self.logger.info(f"Using cached odds from ESPN for {cache_key}")
return cached_data
self.logger.info(f"Fetching fresh odds from ESPN for {cache_key}")
try:
url = f"{self.base_url}/{sport}/leagues/{league}/events/{event_id}/competitions/{event_id}/odds"
response = requests.get(url)
response.raise_for_status()
raw_data = response.json()
odds_data = self._extract_espn_data(raw_data)
if odds_data:
self.cache_manager.save_cache(cache_key, odds_data)
return odds_data
except requests.exceptions.RequestException as e:
self.logger.error(f"Error fetching odds from ESPN API for {cache_key}: {e}")
except json.JSONDecodeError:
self.logger.error(f"Error decoding JSON response from ESPN API for {cache_key}.")
return self.cache_manager.load_cache(cache_key)
def _extract_espn_data(self, data: Dict[str, Any]) -> Optional[Dict[str, Any]]:
if "items" in data and data["items"]:
item = data["items"][0]
# Find the desired bookmaker, e.g., 'fanduel'
provider = next((p for p in item.get('providers', []) if p.get('name', '').lower() == 'fanduel'), item['providers'][0] if item.get('providers') else {})
return {
"details": provider.get("details"),
"over_under": provider.get("overUnder"),
"spread": provider.get("spread"),
"home_team_odds": {
"money_line": provider.get("homeTeamOdds", {}).get("moneyLine"),
"spread_odds": provider.get("homeTeamOdds", {}).get("spreadOdds")
},
"away_team_odds": {
"money_line": provider.get("awayTeamOdds", {}).get("moneyLine"),
"spread_odds": provider.get("awayTeamOdds", {}).get("spreadOdds")
}
}
return None

View File

@@ -11,6 +11,7 @@ from datetime import datetime, timedelta, timezone
from src.display_manager import DisplayManager 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
import pytz import pytz
# Constants # Constants
@@ -95,6 +96,7 @@ class BaseSoccerManager:
_shared_data = {} # Dictionary to hold shared data per league/date _shared_data = {} # Dictionary to hold shared data per league/date
_last_shared_update = {} # Dictionary for update times per league/date _last_shared_update = {} # Dictionary for update times per league/date
cache_manager = CacheManager() cache_manager = CacheManager()
odds_manager = OddsManager(cache_manager, ConfigManager())
logger = logging.getLogger('Soccer') # Use 'Soccer' logger logger = logging.getLogger('Soccer') # Use 'Soccer' logger
# Class attribute to store soccer_config for shared access # Class attribute to store soccer_config for shared access
@@ -109,6 +111,7 @@ class BaseSoccerManager:
BaseSoccerManager._soccer_config_shared = self.soccer_config # Store for class methods BaseSoccerManager._soccer_config_shared = self.soccer_config # Store for class methods
self.is_enabled = self.soccer_config.get("enabled", False) self.is_enabled = self.soccer_config.get("enabled", False)
self.show_odds = self.soccer_config.get("show_odds", False)
self.test_mode = self.soccer_config.get("test_mode", False) self.test_mode = self.soccer_config.get("test_mode", False)
self.logo_dir = self.soccer_config.get("logo_dir", "assets/sports/soccer_logos") # Soccer logos self.logo_dir = self.soccer_config.get("logo_dir", "assets/sports/soccer_logos") # Soccer logos
self.update_interval = self.soccer_config.get("update_interval_seconds", 60) # General fallback self.update_interval = self.soccer_config.get("update_interval_seconds", 60) # General fallback
@@ -147,6 +150,22 @@ class BaseSoccerManager:
self.config_manager = ConfigManager(config) self.config_manager = ConfigManager(config)
def _fetch_odds(self, game: Dict) -> None:
"""Fetch odds for a game and attach it to the game dictionary."""
if not self.show_odds:
return
try:
odds_data = self.odds_manager.get_odds(
sport="soccer",
league=game["league_slug"],
event_id=game["id"]
)
if odds_data:
game['odds'] = odds_data
except Exception as e:
self.logger.error(f"Error fetching odds for game {game.get('id', 'N/A')}: {e}")
# --- Team League Map Management --- # --- Team League Map Management ---
@classmethod @classmethod
def _load_team_league_map(cls) -> None: def _load_team_league_map(cls) -> None:
@@ -576,7 +595,8 @@ class BaseSoccerManager:
"away_logo": self._load_and_resize_logo(away_team["team"]["abbreviation"]), "away_logo": self._load_and_resize_logo(away_team["team"]["abbreviation"]),
"game_time": game_time, # Formatted local time (e.g., 2:30pm) "game_time": game_time, # Formatted local time (e.g., 2:30pm)
"game_date": game_date, # Formatted local date (e.g., 7/21) "game_date": game_date, # Formatted local date (e.g., 7/21)
"league": league_name "league": league_name,
"league_slug": league_slug
} }
self.logger.debug(f"[Soccer] Extracted game: {details['away_abbr']} {details['away_score']} @ {details['home_abbr']} {details['home_score']} ({details['game_clock_display']}) - League: {details['league']} - Final: {details['is_final']}, Upcoming: {details['is_upcoming']}, Live: {details['is_live']}, Within Window: {details['is_within_window']}") self.logger.debug(f"[Soccer] Extracted game: {details['away_abbr']} {details['away_score']} @ {details['home_abbr']} {details['home_score']} ({details['game_clock_display']}) - League: {details['league']} - Final: {details['is_final']}, Upcoming: {details['is_upcoming']}, Live: {details['is_live']}, Within Window: {details['is_within_window']}")
@@ -693,6 +713,25 @@ class BaseSoccerManager:
status_y_top = 1 # Original Y position for live/final status status_y_top = 1 # Original Y position for live/final status
self._draw_text_with_outline(draw, status_text, (status_x, status_y_top), status_font_top) self._draw_text_with_outline(draw, status_text, (status_x, status_y_top), status_font_top)
# Display odds if available
if 'odds' in game:
odds = game['odds']
spread = odds.get('spread', {}).get('point', None)
if spread is not None:
# Format spread text
spread_text = f"{spread:+.1f}" if spread > 0 else f"{spread:.1f}"
# Choose color and position based on which team has the spread
if odds.get('spread', {}).get('team') == game['home_abbr']:
text_color = (255, 100, 100) # Reddish
spread_x = self.display_width - draw.textlength(spread_text, font=self.fonts['status']) - 2
else:
text_color = (100, 255, 100) # Greenish
spread_x = 2
spread_y = self.display_height - 8
self._draw_text_with_outline(draw, spread_text, (spread_x, spread_y), self.fonts['status'], fill=text_color)
# --- Display Image --- # --- Display Image ---
self.display_manager.image.paste(main_img, (0, 0)) self.display_manager.image.paste(main_img, (0, 0))
self.display_manager.update_display() self.display_manager.update_display()
@@ -786,6 +825,7 @@ class SoccerLiveManager(BaseSoccerManager):
details["home_abbr"] in self.favorite_teams or details["home_abbr"] in self.favorite_teams or
details["away_abbr"] in self.favorite_teams details["away_abbr"] in self.favorite_teams
): ):
self._fetch_odds(details)
new_live_games.append(details) new_live_games.append(details)
# Logging # Logging
@@ -914,6 +954,7 @@ class SoccerRecentManager(BaseSoccerManager):
if game and game['is_final'] and game['start_time_utc'] and game['start_time_utc'] >= cutoff_time: if game and game['is_final'] and game['start_time_utc'] and game['start_time_utc'] >= cutoff_time:
# Check favorite teams if list is provided # Check favorite teams if list is provided
if not self.favorite_teams or (game['home_abbr'] in self.favorite_teams or game['away_abbr'] in self.favorite_teams): if not self.favorite_teams or (game['home_abbr'] in self.favorite_teams or game['away_abbr'] in self.favorite_teams):
self._fetch_odds(game)
new_recent_games.append(game) new_recent_games.append(game)
# Sort games by start time, most recent first # Sort games by start time, most recent first
@@ -1016,6 +1057,7 @@ class SoccerUpcomingManager(BaseSoccerManager):
game['start_time_utc'] >= now_utc and game['start_time_utc'] <= cutoff_time: game['start_time_utc'] >= now_utc and game['start_time_utc'] <= cutoff_time:
# Check favorite teams if list is provided # Check favorite teams if list is provided
if not self.favorite_teams or (game['home_abbr'] in self.favorite_teams or game['away_abbr'] in self.favorite_teams): if not self.favorite_teams or (game['home_abbr'] in self.favorite_teams or game['away_abbr'] in self.favorite_teams):
self._fetch_odds(game)
new_upcoming_games.append(game) new_upcoming_games.append(game)
# Sort games by start time, soonest first # Sort games by start time, soonest first