mirror of
https://github.com/ChuckBuilds/LEDMatrix.git
synced 2026-04-10 13:02:59 +00:00
Multiple Sports Fixes (#93)
This commit is contained in:
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,15 +257,16 @@ class HockeyLive(Hockey, SportsLive):
|
||||
)
|
||||
|
||||
# Shots on Goal
|
||||
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
|
||||
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
|
||||
)
|
||||
@@ -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}"
|
||||
)
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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."""
|
||||
|
||||
|
||||
@@ -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."""
|
||||
@@ -132,6 +125,30 @@ class BaseNHLManager(Hockey):
|
||||
|
||||
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."""
|
||||
if isinstance(self, NHLLiveManager):
|
||||
@@ -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)
|
||||
Reference in New Issue
Block a user