From d155046cc3c275614511272a6849a85835d78e89 Mon Sep 17 00:00:00 2001 From: TrustyJAID Date: Tue, 14 Nov 2023 11:07:50 -0700 Subject: [PATCH] [Hockey] Fix detection of game finals, periodrecaps, and period starts. --- hockey/api.py | 49 +++++++++++++++++++-------------- hockey/game.py | 69 +++++++++++++++++++++++++++-------------------- hockey/helper.py | 10 +++++-- hockey/hockey.py | 8 +++--- hockey/pickems.py | 2 +- 5 files changed, 82 insertions(+), 56 deletions(-) diff --git a/hockey/api.py b/hockey/api.py index ed2b6bf93b..d1552c4ab0 100644 --- a/hockey/api.py +++ b/hockey/api.py @@ -1,8 +1,10 @@ from __future__ import annotations +import json from dataclasses import dataclass from datetime import datetime, timezone from enum import Enum +from pathlib import Path from typing import Dict, List, Optional, Tuple, TypedDict import aiohttp @@ -231,7 +233,6 @@ def description(self, data: dict) -> str: return description def get_highlight(self, content: Optional[dict]) -> Optional[str]: - log.debug("Looking for highlight for goal") if content is None: return None clip_id = None @@ -241,14 +242,8 @@ def get_highlight(self, content: Optional[dict]) -> Optional[str]: log.debug("ignoring period because it doesn't match.") continue for goal in period.get("goals", []): - log.debug( - "Checking if goal time in period matches. landing: %s - current %s", - goal.get("timeInPeriod", ""), - self.time_in_period, - ) if goal.get("timeInPeriod", "") == self.time_in_period: clip_id = goal.get("highlightClip", None) - log.debug("Found highlight clip") if clip_id is not None: return VIDEO_URL.format(clip_id=clip_id) return None @@ -358,11 +353,12 @@ def from_nhle(cls, data: dict) -> Schedule: class HockeyAPI: - def __init__(self): + def __init__(self, testing: bool = False): self.session = aiohttp.ClientSession( headers={"User-Agent": "Red-DiscordBot Trusty-cogs Hockey"} ) self.base_url = None + self.testing = testing async def close(self): await self.session.close() @@ -375,7 +371,7 @@ async def get_schedule( team: Optional[str] = None, start_date: Optional[datetime] = None, end_date: Optional[datetime] = None, - ) -> dict: + ) -> Schedule: raise NotImplementedError async def get_games_list( @@ -383,7 +379,7 @@ async def get_games_list( team: Optional[str] = None, start_date: Optional[datetime] = None, end_date: Optional[datetime] = None, - ) -> List[dict]: + ) -> List[Game]: raise NotImplementedError async def get_games( @@ -391,19 +387,19 @@ async def get_games( team: Optional[str] = None, start_date: Optional[datetime] = None, end_date: Optional[datetime] = None, - ) -> List[dict]: + ) -> List[Game]: raise NotImplementedError - async def get_game_from_id(self, game_id: int) -> dict: + async def get_game_from_id(self, game_id: int) -> Game: raise NotImplementedError - async def get_game_from_url(self, game_url: str) -> dict: + async def get_game_from_url(self, game_url: str) -> Game: raise NotImplementedError class StatsAPI(HockeyAPI): - def __init__(self): - super().__init__() + def __init__(self, testing: bool = False): + super().__init__(testing) self.base_url = "https://statsapi.web.nhl.com" async def get_game_content(self, game_id: int): @@ -495,7 +491,7 @@ async def get_games( continue return return_games_list - async def get_game_from_id(self, game_id: int) -> dict: + async def get_game_from_id(self, game_id: int) -> Game: url = f"{self.base_url}/api/v1/game/{game_id}/feed/live" async with self.session.get(url) as resp: data = await resp.json() @@ -694,8 +690,8 @@ async def to_game(self, data: dict, content: Optional[dict]) -> Game: class NewAPI(HockeyAPI): - def __init__(self): - super().__init__() + def __init__(self, testing: bool = False): + super().__init__(testing) self.base_url = "https://api-web.nhle.com/v1" async def get_game_content(self, game_id: int): @@ -711,6 +707,9 @@ def team_to_abbrev(self, team: str) -> Optional[str]: return TEAMS.get(team_name, {}).get("tri_code", None) async def schedule_now(self) -> Schedule: + if self.testing: + data = await self.load_testing_data("testschedule.json") + return Schedule.from_nhle(data) async with self.session.get(f"{self.base_url}/schedule/now") as resp: if resp.status != 200: log.error("Error accessing the Schedule for now. %s", resp.status) @@ -853,7 +852,16 @@ async def get_games( return [await self.get_game_from_id(g.id) for g in schedule.days[0]] return [] + async def load_testing_data(self, file_name: str) -> dict: + path = Path(__file__).parent.resolve() / file_name + with path.open("r") as infile: + data = json.loads(infile.read()) + return data + async def get_game_from_id(self, game_id: int) -> Game: + if self.testing: + data = await self.load_testing_data("testgame.json") + return await self.to_game(data) data = await self.gamecenter_pbp(game_id) try: landing = await self.gamecenter_landing(game_id) @@ -898,7 +906,8 @@ async def get_game_recap(self, game_id: int) -> Optional[str]: async def to_game(self, data: dict, content: Optional[dict] = None) -> Game: game_id = data["id"] period = data.get("period", -1) - game_state = GameState.from_nhle(data["gameState"], period) + period_time_left = data.get("clock", {}).get("timeRemaining") + game_state = GameState.from_nhle(data["gameState"], period, period_time_left) home_id = data.get("homeTeam", {}).get("id", -1) home_team = TEAM_IDS.get(home_id, "Unknown Team") away_id = data.get("awayTeam", {}).get("id", -1) @@ -918,7 +927,7 @@ async def to_game(self, data: dict, content: Optional[dict] = None) -> Game: first_star = None second_star = None third_star = None - period_time_left = data.get("clock", {}).get("timeRemaining") + recap_url = None if content: recap = content.get("summary", {}).get("gameVideo", {}).get("condensedGame") diff --git a/hockey/game.py b/hockey/game.py index b88f37cb57..e2fcfcf733 100644 --- a/hockey/game.py +++ b/hockey/game.py @@ -36,7 +36,12 @@ class GameState(Enum): live_end_first = 6 live_end_second = 7 live_end_third = 8 - final = 9 + over = 9 + final = 10 + official_final = 11 + + def __str__(self): + return self.name.replace("_", " ").title() def is_preview(self): return self in ( @@ -66,13 +71,14 @@ def from_statsapi(cls, game_state: str) -> GameState: }.get(game_state, GameState.unknown) @classmethod - def from_nhle(cls, game_state: str, period: int) -> GameState: - if period == 2: - return GameState.live_end_first - elif period == 3 and game_state == "LIVE": - return GameState.live_end_second - if period > 3 and game_state in ["LIVE", "CRIT"]: - return GameState.live_end_third + def from_nhle(cls, game_state: str, period: int, remaining: Optional[str] = None) -> GameState: + if remaining and game_state not in ["CRIT", "OVER", "FINAL", "OFF"]: + if period == 1 and remaining == "00:00": + return GameState.live_end_first + elif period == 2 and remaining == "00:00": + return GameState.live_end_second + elif period == 3 and remaining == "00:00": + return GameState.live_end_third return { "FUT": GameState.preview, "PRE": GameState.preview, @@ -83,9 +89,9 @@ def from_nhle(cls, game_state: str, period: int) -> GameState: # These previews are only my internal code, not sure if they'll be used "LIVE": GameState.live, "CRIT": GameState.live, - "OVER": GameState.final, + "OVER": GameState.over, "FINAL": GameState.final, - "OFF": GameState.final, + "OFF": GameState.official_final, }.get(game_state, GameState.unknown) @@ -248,8 +254,6 @@ class Game: away_score: int game_start: datetime goals: List[Goal] - home_goals: list - away_goals: list home_abr: str away_abr: str period_ord: str @@ -324,11 +328,11 @@ def __repr__(self): ) @property - def home_goals(self): + def home_goals(self) -> List[Goal]: return [g for g in self.goals if g.team_name == self.home_team] @property - def away_goals(self): + def away_goals(self) -> List[Goal]: return [g for g in self.goals if g.team_name == self.away_team] @property @@ -417,7 +421,7 @@ async def make_game_embed( ) # timestamp = datetime.strptime(self.game_start, "%Y-%m-%dT%H:%M:%SZ") title = "{away} @ {home} {state}".format( - away=self.away_team, home=self.home_team, state=self.game_state.name + away=self.away_team, home=self.home_team, state=str(self.game_state) ) colour = ( int(TEAMS[self.home_team]["home"].replace("#", ""), 16) @@ -565,7 +569,7 @@ async def game_state_embed(self) -> discord.Embed: """ # post_state = ["all", self.home_team, self.away_team] # timestamp = datetime.strptime(self.game_start, "%Y-%m-%dT%H:%M:%SZ") - title = f"{self.away_team} @ {self.home_team} {self.game_state.name}" + title = f"{self.away_team} @ {self.home_team} {str(self.game_state)}" em = discord.Embed(timestamp=self.game_start) home_field = "{0} {1} {0}".format(self.home_emoji, self.home_team) away_field = "{0} {1} {0}".format(self.away_emoji, self.away_team) @@ -608,7 +612,7 @@ async def game_state_text(self) -> str: time_string = f"" em = ( f"{self.away_emoji}{self.away_team} @ {self.home_emoji}{self.home_team} " - f"{self.game_state.name}\n({time_string})" + f"{str(self.game_state)}\n({time_string})" ) if not self.game_state.is_preview(): em = ( @@ -691,6 +695,9 @@ async def check_game_state(self, bot: Red, count: int = 0) -> bool: home = await get_team(bot, self.home_team, self.game_start_str, self.game_id) try: old_game_state = GameState(home["game_state"]) + log.trace( + "Old Game State for %s @ %s is %r", self.away_team, self.home_team, old_game_state + ) except ValueError: old_game_state = GameState.unknown # away = await get_team(self.away_team) @@ -749,34 +756,38 @@ async def check_game_state(self, bot: Red, count: int = 0) -> bool: # Check if there's goals only if there are goals await self.check_team_goals(bot) if end_first and old_game_state is not GameState.live_end_first: - log.debug("End of the first period") + log.debug("End of the first period %s @ %s", self.away_team, self.home_team) await self.period_recap(bot, "1st") await self.save_game_state(bot, "END1st") if end_second and old_game_state is not GameState.live_end_second: - log.debug("End of the second period") + log.debug("End of the second period %s @ %s", self.away_team, self.home_team) await self.period_recap(bot, "2nd") await self.save_game_state(bot, "END2nd") if end_third and old_game_state is not GameState.live_end_third: - log.debug("End of the third period") + log.debug("End of the third period %s @ %s", self.away_team, self.home_team) await self.period_recap(bot, "3rd") await self.save_game_state(bot, "END3rd") - if self.game_state is GameState.final: + if self.game_state.value > GameState.over.value: if (self.home_score + self.away_score) != 0: # Check if there's goals only if there are goals await self.check_team_goals(bot) - if end_third and home["game_state"] not in ["LiveEND3rd", "FinalEND3rd"]: - log.debug("End of the third period") + if end_third and old_game_state not in [GameState.final]: + log.debug("End of the third period %s @ %s", self.away_team, self.home_team) await self.period_recap(bot, "3rd") await self.save_game_state(bot, "END3rd") if ( - self.first_star is not None - and self.second_star is not None - and self.third_star is not None - and len(self.home_goals) == self.home_score - and len(self.away_goals) == self.away_score - ) or count >= 20: + ( + self.first_star is not None + and self.second_star is not None + and self.third_star is not None + and len(self.home_goals) == self.home_score + and len(self.away_goals) == self.away_score + ) + or count >= 20 + or self.game_state is GameState.official_final + ): """Final game state checks""" if old_game_state is not self.game_state: # Post game final data and check for next game diff --git a/hockey/helper.py b/hockey/helper.py index c34a3e3bd5..bd148077f4 100644 --- a/hockey/helper.py +++ b/hockey/helper.py @@ -539,7 +539,13 @@ async def autocomplete( def game_states_to_int(states: List[str]) -> List[int]: ret = [] - options = {"Preview": [1, 2, 3, 4], "Live": [5], "Final": [9], "Goal": [], "Recap": [6, 7, 8]} + options = { + "Preview": [1, 2, 3, 4], + "Live": [5], + "Final": [9, 10, 11], + "Goal": [], + "Periodrecap": [6, 7, 8], + } for state in states: ret += options.get(state, []) return ret @@ -549,7 +555,7 @@ async def check_to_post( bot: Red, channel: discord.TextChannel, channel_data: dict, - post_state: str, + post_state: List[str], game_state: str, is_goal: bool = False, ) -> bool: diff --git a/hockey/hockey.py b/hockey/hockey.py index dd05004fc2..05d8e44ee4 100644 --- a/hockey/hockey.py +++ b/hockey/hockey.py @@ -338,7 +338,7 @@ async def game_check_loop(self) -> None: continue if schedule.days != []: for game in schedule.days[0]: - if game.game_state is GameState.final: + if game.game_state.value >= GameState.final.value: continue if game.schedule_state != "OK": continue @@ -393,7 +393,7 @@ async def game_check_loop(self) -> None: log.exception("Error checking game state: ") posted_final = False if ( - game.game_state in [GameState.live] + game.game_state.is_live() and not self.current_games[game_id]["disabled_buttons"] ): log.verbose("Disabling buttons for %r", game) @@ -409,7 +409,7 @@ async def game_check_loop(self) -> None: game.home_score, ) - if game.game_state is GameState.final: + if game.game_state.value > GameState.over.value: self.current_games[game_id]["count"] += 1 if posted_final: try: @@ -424,7 +424,7 @@ async def game_check_loop(self) -> None: to_delete.append(link) for link in to_delete: del self.current_games[link] - if not self.TEST_LOOP: + if not self.api.testing: await asyncio.sleep(60) else: await asyncio.sleep(10) diff --git a/hockey/pickems.py b/hockey/pickems.py index a2c2f85dad..60366456ed 100644 --- a/hockey/pickems.py +++ b/hockey/pickems.py @@ -133,7 +133,7 @@ def __init__( self.link = link self._should_save: bool = True # Start true so we save instantiated pickems - self.game_type: str = game_type + self.game_type: GameType = game_type super().__init__(timeout=None) disabled_buttons = datetime.now(tz=timezone.utc) > self.game_start self.home_button = PickemsButton(