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

@@ -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: