Multiple Sports Fixes (#93)

This commit is contained in:
Alex Resnick
2025-10-02 16:35:17 -05:00
committed by GitHub
parent 6c493e8329
commit f6e2881f2f
6 changed files with 143 additions and 90 deletions

View File

@@ -233,6 +233,8 @@
"recent_games_to_show": 1,
"upcoming_games_to_show": 1,
"show_favorite_teams_only": true,
"show_all_live": false,
"show_shots_on_goal": false,
"favorite_teams": [
"TB"
],
@@ -299,6 +301,7 @@
"recent_games_to_show": 1,
"upcoming_games_to_show": 1,
"show_favorite_teams_only": true,
"show_all_live": false,
"favorite_teams": [
"TB",
"DAL"
@@ -331,6 +334,7 @@
"recent_games_to_show": 1,
"upcoming_games_to_show": 1,
"show_favorite_teams_only": true,
"show_all_live": false,
"favorite_teams": [
"UGA",
"AUB",
@@ -365,6 +369,8 @@
"recent_games_to_show": 1,
"upcoming_games_to_show": 1,
"show_favorite_teams_only": true,
"show_all_live": false,
"show_series_summary": false,
"favorite_teams": [
"UGA",
"AUB"
@@ -413,6 +419,8 @@
"recent_games_to_show": 1,
"upcoming_games_to_show": 1,
"show_favorite_teams_only": true,
"show_all_live": false,
"show_shots_on_goal": false,
"favorite_teams": [
"RIT"
],
@@ -444,6 +452,8 @@
"recent_games_to_show": 1,
"upcoming_games_to_show": 1,
"show_favorite_teams_only": true,
"show_all_live": false,
"show_series_summary": false,
"favorite_teams": [
"TB",
"TEX"

View File

@@ -6,14 +6,13 @@ with baseball-specific logic for innings, outs, bases, strikes, balls, etc.
"""
import logging
import random
import time
from typing import Any, Dict, Optional
from PIL import Image, ImageDraw
from PIL import Image, ImageDraw, ImageFont
from src.base_classes.data_sources import ESPNDataSource
from src.base_classes.sports import SportsCore, SportsLive
from src.base_classes.sports import SportsCore, SportsLive, SportsRecent
class Baseball(SportsCore):
@@ -34,6 +33,7 @@ class Baseball(SportsCore):
self.show_bases = self.mode_config.get("show_bases", True)
self.show_count = self.mode_config.get("show_count", True)
self.show_pitcher_batter = self.mode_config.get("show_pitcher_batter", False)
self.show_series_summary = self.mode_config.get("show_series_summary", False)
self.data_source = ESPNDataSource(logger)
self.sport = "baseball"
@@ -157,7 +157,10 @@ class Baseball(SportsCore):
self.logger.debug(
f"Status shortDetail: {status['type'].get('shortDetail', '')}"
)
series = game_event["competitions"][0].get("series", None)
series_summary = ""
if series:
series_summary = series.get("summary", "")
# Get game state information
if status_state == "in":
# For live games, get detailed state
@@ -297,6 +300,7 @@ class Baseball(SportsCore):
"outs": outs,
"bases_occupied": bases_occupied,
"start_time": game_event["date"],
"series_summary": series_summary,
}
)
@@ -320,6 +324,38 @@ class Baseball(SportsCore):
)
return None
def display_series_summary(self, game: dict, draw_overlay: ImageDraw.ImageDraw):
if not self.show_series_summary:
return
series_summary = game.get("series_summary", "")
font = ImageFont.truetype("assets/fonts/4x6-font.ttf", 6)
bbox = draw_overlay.textbbox((0, 0), series_summary, font=self.fonts['time'])
height = bbox[3] - bbox[1]
shots_y = (self.display_height - height) // 2
shots_width = draw_overlay.textlength(series_summary, font=self.fonts['time'])
shots_x = (self.display_width - shots_width) // 2
self._draw_text_with_outline(
draw_overlay, series_summary, (shots_x, shots_y), self.fonts['time']
)
class BaseballRecent(Baseball, SportsRecent):
"""Base class for recent baseball games."""
def __init__(
self,
config: Dict[str, Any],
display_manager,
cache_manager,
logger: logging.Logger,
sport_key: str,
):
super().__init__(config, display_manager, cache_manager, logger, sport_key)
def _custom_scorebug_layout(self, game: dict, draw_overlay: ImageDraw.ImageDraw):
self.display_series_summary(game, draw_overlay)
class BaseballLive(Baseball, SportsLive):
"""Base class for live baseball games."""
@@ -342,14 +378,25 @@ class BaseballLive(Baseball, SportsLive):
# self.current_game["balls"] = random.choice([1, 2, 3])
# self.current_game["strikes"] = random.choice([1, 2])
# self.current_game["outs"] = random.choice([1, 2])
if self.current_game["inning_half"] == "top": self.current_game["inning_half"] = "bottom"
else: self.current_game["inning_half"] = "top"; self.current_game["inning"] += 1
if self.current_game["inning_half"] == "top":
self.current_game["inning_half"] = "bottom"
else:
self.current_game["inning_half"] = "top"
self.current_game["inning"] += 1
self.current_game["balls"] = (self.current_game["balls"] + 1) % 4
self.current_game["strikes"] = (self.current_game["strikes"] + 1) % 3
self.current_game["outs"] = (self.current_game["outs"] + 1) % 3
self.current_game["bases_occupied"] = [not b for b in self.current_game["bases_occupied"]]
if self.current_game["inning"] % 2 == 0: self.current_game["home_score"] = str(int(self.current_game["home_score"]) + 1)
else: self.current_game["away_score"] = str(int(self.current_game["away_score"]) + 1)
self.current_game["bases_occupied"] = [
not b for b in self.current_game["bases_occupied"]
]
if self.current_game["inning"] % 2 == 0:
self.current_game["home_score"] = str(
int(self.current_game["home_score"]) + 1
)
else:
self.current_game["away_score"] = str(
int(self.current_game["away_score"]) + 1
)
def _draw_scorebug_layout(self, game: Dict, force_clear: bool = False) -> None:
"""Draw the detailed scorebug layout for a live NCAA FB game.""" # Updated docstring
@@ -676,5 +723,5 @@ class BaseballLive(Baseball, SportsLive):
except Exception as e:
self.logger.error(
f"Error displaying live Football game: {e}", exc_info=True
f"Error displaying live Baseball game: {e}", exc_info=True
) # Changed log prefix

View File

@@ -25,6 +25,7 @@ class Hockey(SportsCore):
super().__init__(config, display_manager, cache_manager, logger, sport_key)
self.data_source = ESPNDataSource(logger)
self.sport = "hockey"
self.show_shots_on_goal = self.mode_config.get("show_shots_on_goal", False)
def _extract_game_details(self, game_event: Dict) -> Optional[Dict]:
"""Extract relevant game details from ESPN NCAA FB API response."""
@@ -74,7 +75,7 @@ class Hockey(SportsCore):
home_shots = 0
away_shots = 0
status_short = ""
if home_team_saves_per > 0:
away_shots = round(home_team_saves / home_team_saves_per)
if away_team_saves_per > 0:
@@ -82,8 +83,8 @@ class Hockey(SportsCore):
if situation and status["type"]["state"] == "in":
# Detect scoring events from status detail
status_detail = status["type"].get("detail", "").lower()
status_short = status["type"].get("shortDetail", "").lower()
# status_detail = status["type"].get("detail", "")
status_short = status["type"].get("shortDetail", "")
powerplay = situation.get("isPowerPlay", False)
penalties = situation.get("penalties", "")
@@ -114,6 +115,8 @@ class Hockey(SportsCore):
"penalties": penalties,
"home_shots": home_shots,
"away_shots": away_shots,
"is_period_break": status["type"]["name"] == "STATUS_END_PERIOD",
"status_short": status_short,
}
)
@@ -225,8 +228,8 @@ class HockeyLive(Hockey, SportsLive):
period_clock_text = (
f"{game.get('period_text', '')} {game.get('clock', '')}".strip()
)
if game.get("is_halftime"):
period_clock_text = "Halftime" # Override for halftime
if game.get("is_period_break"):
period_clock_text = game.get("status_short", "Period Break")
status_width = draw_overlay.textlength(
period_clock_text, font=self.fonts["time"]
@@ -254,18 +257,19 @@ class HockeyLive(Hockey, SportsLive):
)
# Shots on Goal
shots_font = ImageFont.truetype("assets/fonts/4x6-font.ttf", 6)
home_shots = str(game.get("home_shots", "0"))
away_shots = str(game.get("away_shots", "0"))
shots_text = f"{away_shots} SHOTS {home_shots}"
shots_width = draw_overlay.textlength(shots_text, font=shots_font)
shots_x = (self.display_width - shots_width) // 2
shots_y = (
self.display_height - 10
) # centered #from 14 # Position score higher
self._draw_text_with_outline(
draw_overlay, shots_text, (shots_x, shots_y), shots_font
)
if self.show_shots_on_goal:
shots_font = ImageFont.truetype("assets/fonts/4x6-font.ttf", 6)
home_shots = str(game.get("home_shots", "0"))
away_shots = str(game.get("away_shots", "0"))
shots_text = f"{away_shots} SHOTS {home_shots}"
shots_bbox = draw_overlay.textbbox((0, 0), shots_text, font=shots_font)
shots_height = shots_bbox[3] - shots_bbox[1]
shots_y = self.display_height - shots_height - 1
shots_width = draw_overlay.textlength(shots_text, font=shots_font)
shots_x = (self.display_width - shots_width) // 2
self._draw_text_with_outline(
draw_overlay, shots_text, (shots_x, shots_y), shots_font
)
# Draw odds if available
if "odds" in game and game["odds"]:
@@ -290,7 +294,7 @@ class HockeyLive(Hockey, SportsLive):
record_bbox = draw_overlay.textbbox((0, 0), "0-0", font=record_font)
record_height = record_bbox[3] - record_bbox[1]
record_y = self.display_height - record_height - 4
record_y = self.display_height - record_height - 1
self.logger.debug(
f"Record positioning: height={record_height}, record_y={record_y}, display_height={self.display_height}"
)

View File

@@ -1,16 +1,16 @@
import logging
import os
import time
from abc import ABC, abstractmethod
from datetime import datetime, timedelta, timezone
from pathlib import Path
from typing import Any, Dict, List, Optional
from typing import Callable
from typing import Any, Callable, Dict, List, Optional
import pytz
import requests
from PIL import Image, ImageDraw, ImageFont
from requests.adapters import HTTPAdapter
from urllib3.util.retry import Retry
from abc import ABC, abstractmethod
from src.background_data_service import get_background_service
@@ -539,6 +539,9 @@ class SportsCore(ABC):
self.logger.warning(f"Error fetching this weeks games for {self.sport} - {self.league} - {date_str}: {e}")
return None
def _custom_scorebug_layout(self, game: dict, draw_overlay: ImageDraw.ImageDraw):
pass
class SportsUpcoming(SportsCore):
def __init__(self, config: Dict[str, Any], display_manager: DisplayManager, cache_manager: CacheManager, logger: logging.Logger, sport_key: str):
super().__init__(config, display_manager, cache_manager, logger, sport_key)
@@ -718,11 +721,14 @@ class SportsUpcoming(SportsCore):
# Note: Rankings are now handled in the records/rankings section below
# "Next Game" at the top (use smaller status font)
status_font = self.fonts['status']
if self.display_width > 128:
status_font = self.fonts['time']
status_text = "Next Game"
status_width = draw_overlay.textlength(status_text, font=self.fonts['status'])
status_width = draw_overlay.textlength(status_text, font=status_font)
status_x = (self.display_width - status_width) // 2
status_y = 1 # Changed from 2
self._draw_text_with_outline(draw_overlay, status_text, (status_x, status_y), self.fonts['status'])
self._draw_text_with_outline(draw_overlay, status_text, (status_x, status_y), status_font)
# Date text (centered, below "Next Game")
date_width = draw_overlay.textlength(game_date, font=self.fonts['time'])
@@ -1114,6 +1120,7 @@ class SportsRecent(SportsCore):
self.logger.debug(f"Drawing home ranking '{home_text}' at ({home_record_x}, {record_y}) with font size {record_font.size if hasattr(record_font, 'size') else 'unknown'}")
self._draw_text_with_outline(draw_overlay, home_text, (home_record_x, record_y), record_font)
self._custom_scorebug_layout(game, draw_overlay)
# Composite and display
main_img = Image.alpha_composite(main_img, overlay)
main_img = main_img.convert('RGB')
@@ -1172,6 +1179,8 @@ class SportsLive(SportsCore):
self.last_display_update = 0
self.last_log_time = 0
self.log_interval = 300
self.last_count_log_time = 0 # Track when we last logged count data
self.count_log_interval = 5 # Only log count data every 5 seconds
@abstractmethod
def _test_mode_update(self) -> None:

View File

@@ -6,10 +6,11 @@ from pathlib import Path
from typing import Any, Dict, Optional
import pytz
from PIL import ImageDraw
# Import baseball and standard sports classes
from src.base_classes.baseball import Baseball, BaseballLive
from src.base_classes.sports import SportsRecent, SportsUpcoming
from src.base_classes.baseball import Baseball, BaseballLive, BaseballRecent
from src.base_classes.sports import SportsUpcoming
from src.cache_manager import CacheManager
from src.display_manager import DisplayManager
@@ -180,7 +181,7 @@ class MLBLiveManager(BaseMLBManager, BaseballLive):
self.logger.info("Initialized MLBLiveManager in live mode")
class MLBRecentManager(BaseMLBManager, SportsRecent):
class MLBRecentManager(BaseMLBManager, BaseballRecent):
"""Manager for displaying recent MLB games."""
def __init__(
@@ -195,7 +196,6 @@ class MLBRecentManager(BaseMLBManager, SportsRecent):
f"Initialized MLBRecentManager with {len(self.favorite_teams)} favorite teams"
) # Changed log prefix
class MLBUpcomingManager(BaseMLBManager, SportsUpcoming):
"""Manager for displaying upcoming MLB games."""

View File

@@ -1,22 +1,15 @@
import os
import time
import logging
import requests
import json
from typing import Dict, Any, Optional, List
from PIL import Image, ImageDraw, ImageFont
from datetime import datetime
from pathlib import Path
from datetime import datetime, timedelta, timezone
from src.display_manager import DisplayManager
from src.cache_manager import CacheManager
from src.config_manager import ConfigManager
from src.odds_manager import OddsManager
from src.background_data_service import get_background_service
from requests.adapters import HTTPAdapter
from urllib3.util.retry import Retry
from typing import Any, Dict, Optional
import pytz
from src.base_classes.sports import SportsRecent, SportsUpcoming
import requests
from src.base_classes.hockey import Hockey, HockeyLive
from src.base_classes.sports import SportsRecent, SportsUpcoming
from src.cache_manager import CacheManager
from src.display_manager import DisplayManager
# Import the API counter function from web interface
try:
@@ -73,14 +66,14 @@ class BaseNHLManager(Hockey):
if cached_data:
# Validate cached data structure
if isinstance(cached_data, dict) and 'events' in cached_data:
self.logger.info(f"[NHL] Using cached data for {season_year}")
self.logger.info(f"Using cached data for {season_year}")
return cached_data
elif isinstance(cached_data, list):
# Handle old cache format (list of events)
self.logger.info(f"[NHL] Using cached data for {season_year} (legacy format)")
self.logger.info(f"Using cached data for {season_year} (legacy format)")
return {'events': cached_data}
else:
self.logger.warning(f"[NHL] Invalid cached data format for {season_year}: {type(cached_data)}")
self.logger.warning(f"Invalid cached data format for {season_year}: {type(cached_data)}")
# Clear invalid cache
self.cache_manager.clear_cache(cache_key)
@@ -89,7 +82,7 @@ class BaseNHLManager(Hockey):
return self._fetch_nhl_api_data_sync(use_cache)
# Start background fetch
self.logger.info(f"[NHL] Starting background fetch for {season_year} season schedule...")
self.logger.info(f"Starting background fetch for {season_year} season schedule...")
def fetch_callback(result):
"""Callback when background fetch completes."""
@@ -131,6 +124,30 @@ class BaseNHLManager(Hockey):
return partial_data
return None
def _fetch_nhl_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"nhl_schedule_{current_year}"
self.logger.info(f"Fetching full {current_year} season schedule from ESPN API (sync mode)...")
try:
response = self.session.get(ESPN_NHL_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, date_str: str = None) -> Optional[Dict]:
"""Fetch data using shared data mechanism or direct fetch for live."""
@@ -184,37 +201,3 @@ class NHLUpcomingManager(BaseNHLManager, SportsUpcoming):
super().__init__(config, display_manager, cache_manager)
self.logger = logging.getLogger('NHLUpcomingManager') # Changed logger name
self.logger.info(f"Initialized NHLUpcomingManager with {len(self.favorite_teams)} favorite teams")
"""Display upcoming games."""
if not self.upcoming_games:
current_time = time.time()
if current_time - self.last_warning_time > self.warning_cooldown:
self.logger.info("[NHL] No upcoming games to display")
self.last_warning_time = current_time
return # Skip display update entirely
try:
current_time = time.time()
# Check if it's time to switch games
if len(self.upcoming_games) > 1 and current_time - self.last_game_switch >= self.game_display_duration:
# Move to next game
self.current_game_index = (self.current_game_index + 1) % len(self.upcoming_games)
self.current_game = self.upcoming_games[self.current_game_index]
self.last_game_switch = current_time
force_clear = True # Force clear when switching games
# Log team switching
if self.current_game:
away_abbr = self.current_game.get('away_abbr', 'UNK')
home_abbr = self.current_game.get('home_abbr', 'UNK')
self.logger.info(f"[NHL Upcoming] Showing {away_abbr} vs {home_abbr}")
# Draw the scorebug layout
self._draw_scorebug_layout(self.current_game, force_clear)
# Update display
self.display_manager.update_display()
except Exception as e:
self.logger.error(f"[NHL] Error displaying upcoming game: {e}", exc_info=True)