From d802adb048dbfc20fa2f1d90bac17526b892337c Mon Sep 17 00:00:00 2001 From: Chuck <33324927+ChuckBuilds@users.noreply.github.com> Date: Sat, 9 Aug 2025 13:32:22 -0500 Subject: [PATCH] updates to milb live descriptors --- src/milb_manager.py | 110 ++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 102 insertions(+), 8 deletions(-) diff --git a/src/milb_manager.py b/src/milb_manager.py index 44dcd481..563b4ae7 100644 --- a/src/milb_manager.py +++ b/src/milb_manager.py @@ -63,6 +63,79 @@ class BaseMiLBManager: '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 _probe_and_update_from_live_feed(self, game_pk: str, game_data: Dict[str, Any]) -> bool: + """Probe MLB Stats live feed for a game and update game_data in-place if live. + + Returns True if the feed indicates the game is in progress; False otherwise. + """ + try: + live_url = f"http://statsapi.mlb.com/api/v1.1/game/{game_pk}/feed/live" + self.logger.debug(f"[MiLB] Probing live feed for game {game_pk}: {live_url}") + resp = self.session.get(live_url, headers=self.headers, timeout=6) + resp.raise_for_status() + payload = resp.json() + game_data_obj = payload.get('gameData', {}) + status_obj = game_data_obj.get('status', {}) + status_code = str(status_obj.get('statusCode', '')).upper() + abstract_state = str(status_obj.get('abstractGameState', '')).lower() + + is_live = (status_code == 'I') or (abstract_state == 'live') + if not is_live: + # Some feeds report via liveData/linescore even when abstractGameState lags + live_data = payload.get('liveData', {}) + linescore = live_data.get('linescore', {}) + if linescore.get('currentInning'): + is_live = True + + if not is_live: + return False + + # Update primary fields from live feed + live_data = payload.get('liveData', {}) + linescore = live_data.get('linescore', {}) + + # Scores + away_runs = linescore.get('teams', {}).get('away', {}).get('runs') + home_runs = linescore.get('teams', {}).get('home', {}).get('runs') + if away_runs is not None: + game_data['away_score'] = away_runs + if home_runs is not None: + game_data['home_score'] = home_runs + + # Inning and half + inning = linescore.get('currentInning') + if inning is not None: + game_data['inning'] = inning + inning_state_live = str(linescore.get('inningState', '')).lower() + if inning_state_live: + game_data['inning_half'] = 'bottom' if 'bottom' in inning_state_live else 'top' + + # Count and outs + balls = linescore.get('balls') + strikes = linescore.get('strikes') + outs = linescore.get('outs') + if balls is not None: + game_data['balls'] = balls + if strikes is not None: + game_data['strikes'] = strikes + if outs is not None: + game_data['outs'] = outs + + offense = linescore.get('offense', {}) + game_data['bases_occupied'] = [ + 'first' in offense, + 'second' in offense, + 'third' in offense + ] + + # Set status to in-progress + game_data['status'] = 'status_in_progress' + game_data['status_state'] = 'in' + return True + except Exception as e: + self.logger.debug(f"[MiLB] Live feed probe failed for {game_pk}: {e}") + return False + def _get_team_logo(self, team_abbr: str) -> Optional[Image.Image]: """Get team logo from the configured directory.""" try: @@ -816,10 +889,34 @@ class MiLBLiveManager(BaseMiLBManager): detailed = str(game.get('detailed_state','')).lower() is_live_by_detail = any( token in detailed for token in [ - 'in progress', 'game in progress', 'top of the', 'bottom of the', 'warmup' + 'in progress', 'game in progress', 'top of the', 'bottom of the', 'middle of the', 'end of the', 'warmup', 'delayed start', 'delayed' ] ) - if is_live_by_flags or is_live_by_detail: + is_live = is_live_by_flags or is_live_by_detail + + # Fallback: probe live feed if not already considered live and game near today + if not is_live: + try: + game_pk = game.get('id') or game.get('game_pk') + start_time_str = game.get('start_time') + should_probe = True + if start_time_str: + try: + start_dt = datetime.fromisoformat(start_time_str.replace('Z', '+00:00')) + now_utc = datetime.now(timezone.utc) + # Probe only if within +/- 12 hours of now + if abs((now_utc - start_dt).total_seconds()) > 12 * 3600: + should_probe = False + except Exception: + pass + if game_pk and should_probe: + if self._probe_and_update_from_live_feed(str(game_pk), game): + is_live = True + self.logger.info(f"[MiLB] Live confirmed via feed: {game['away_team']} @ {game['home_team']}") + except Exception: + pass + + if is_live: # Sanity check on time game_date_str = game.get('start_time', '') if game_date_str: @@ -827,14 +924,11 @@ class MiLBLiveManager(BaseMiLBManager): game_date = datetime.fromisoformat(game_date_str.replace('Z', '+00:00')) current_utc = datetime.now(timezone.utc) hours_diff = (current_utc - game_date).total_seconds() / 3600 - # Accept slightly future-started games as live if detailed_state indicates in progress/warmup - future_grace_hours = 1.0 if is_live_by_detail else 0.0 - if hours_diff > 24: + # If a game is flagged live, do NOT exclude for future start; only guard against stale past + if hours_diff > 48: self.logger.warning(f"[MiLB] Skipping old game marked live: {game['away_team']} @ {game['home_team']}") continue - elif hours_diff < -future_grace_hours: - self.logger.warning(f"[MiLB] Skipping future game marked live: {game['away_team']} @ {game['home_team']} (starts in {abs(hours_diff):.2f}h)") - continue + # Note: We intentionally allow future games if API reports live/detailed live except Exception as e: self.logger.warning(f"[MiLB] Could not parse game date {game_date_str}: {e}")