コード例 #1
0
    def goalie_pull_social(self, team_name, trailing_score):
        """Sends a message to social media about the goalie for a team being pulled.

        Args:
            self: current game instance
            team_name: team short name (from the team's object)
            trailing_score: the amount of goals the pulled team is trailing by

        Returns:
            None
        """

        goalie_pull_text = (
            f"The {team_name} have pulled their goalie trailing by {trailing_score} with "
            f"{self.period.time_remaining} left in the {self.period.current_ordinal} period."
        )

        if goalie_pull_text == self.last_goalie_pull_text:
            logging.info(
                "Goalie pull detected, but the text is same - skipping this social send."
            )
            return

        socialhandler.send(msg=goalie_pull_text, force_send=True)

        # Keep track of goalie pull text within the Game object
        self.last_goalie_pull_text = goalie_pull_text
コード例 #2
0
ファイル: common.py プロジェクト: mattdonders/nhl-twitter-bot
def search_send_shotmap(game):
    search_account = "shotmaps"
    search_hashtag = game.game_hashtag
    search_term = f"from:{search_account} {search_hashtag}"

    tweets = twitter.search_twitter(search_term, 1)
    try:
        tweet = next(tweets)
        tweet_user = tweet.user.screen_name
        tweet_text = tweet.text
        tweet_id = tweet.id

        # If the period from the tweet doesn't match the game bot period, skip this loop
        game_final = True if game.game_state == GameState.FINAL.value else False
        current_period_check = "end of the game" if game_final else game.period.current_ordinal
        if current_period_check not in tweet_text:
            logging.info("Current Period: %s | Tweet Text: %s",
                         current_period_check, tweet_text)
            raise ValueError("Period ordinal does not match tweet.")

        url = f"https://twitter.com/{tweet_user}/status/{tweet_id}"
        rt_text = f"⬇️ 5v5 & all situation shotmaps for {search_hashtag}.\n{url}"
        socialhandler.send(rt_text)
        return True

    except StopIteration:
        logging.warning("No tweets match the following - '%s'", search_term)
        return False
    except ValueError:
        logging.info(
            "The tweet found does not match the current period - try again next loop."
        )
        return False
コード例 #3
0
def hockeystatcards(game: Game):
    """Uses the Hockey Stat Cards API to retrieve gamescores for the current game.
        Generates an image based on those values & sends the socials.

    Args:
        game (Game): Current Game object.

    Returns:
        None
    """

    game_scores = thirdparty.hockeystatcard_gamescores(game=game)
    if not game_scores:
        logging.warning("Could not get game scores, exiting.")
        return False

    home_gs = game_scores[0]
    away_gs = game_scores[1]

    hsc_charts = images.hockeystatcards_charts(game=game,
                                               home_gs=home_gs,
                                               away_gs=away_gs)

    hsc_social_text = (
        f"{game.preferred_team.short_name} & {game.other_team.short_name} Game Score leaderboard."
        f"\n\n(via @hockeystatcards @NatStatTrick @domluszczyszyn)")

    socialhandler.send(msg=hsc_social_text, media=hsc_charts)

    # Set the end of game social attributes
    game.final_socials.hsc_msg = hsc_social_text
    game.final_socials.hsc_sent = True
コード例 #4
0
def three_stars(livefeed: dict, game: Game):
    """Takes the livefeed response from the NHL API and sets the status & key of the three-stars
        attribute in the EndOfGame socials class.

    Args:
        livefeed: Live Feed API response
        game: Game Object

    Returns:
        None: Sets a status & message in EndOfGame object
        # bool: if the function runs & sends to social succesfully
    """
    if game.final_socials.three_stars_sent:
        logging.debug("Three stars social already sent - skip this loop!")
        return

    logging.info("Checking for the 3-stars of the game.")

    all_players = livefeed["gameData"]["players"]
    decisions = livefeed["liveData"]["decisions"]

    try:
        first_star_id = f"ID{decisions['firstStar']['id']}"
        first_star_name = decisions["firstStar"]["fullName"]
        first_star_tricode = all_players[first_star_id]["currentTeam"][
            "triCode"]
        first_star_full = f"{first_star_name} ({first_star_tricode})"

        second_star_id = f"ID{decisions['secondStar']['id']}"
        second_star_name = decisions["secondStar"]["fullName"]
        second_star_tricode = all_players[second_star_id]["currentTeam"][
            "triCode"]
        second_star_full = f"{second_star_name} ({second_star_tricode})"

        third_star_id = f"ID{decisions['thirdStar']['id']}"
        third_star_name = decisions["thirdStar"]["fullName"]
        third_star_tricode = all_players[third_star_id]["currentTeam"][
            "triCode"]
        third_star_full = f"{third_star_name} ({third_star_tricode})"

        stars_text = f"⭐️: {first_star_full}\n⭐️⭐️: {second_star_full}\n⭐️⭐️⭐️: {third_star_full}"
        three_stars_msg = f"The three stars for the game are - \n{stars_text}"

    except KeyError:
        logging.info(
            "3-stars have not yet posted - try again in next iteration.")
        return

    discord_color = images.discord_color(game.preferred_team.team_name)
    socialhandler.send(three_stars_msg,
                       discord_title="FINAL: Three Stars",
                       discord_color=discord_color)

    # Set the final score message & status in the EndOfGame Social object
    game.final_socials.three_stars_msg = three_stars_msg
    game.final_socials.three_stars_sent = True
コード例 #5
0
ファイル: live.py プロジェクト: Eric-DuBose/nhl-twitter-bot
def minute_remaining_check(game: Game):
    """ A function to check if there is approximately a minute remaining in the period. """

    if game.period.time_remaining == "END":
        game.period.current_oneminute_sent = True
        return

    period_remain_ss = utils.from_mmss(game.period.time_remaining)
    if 50 <= period_remain_ss <= 65:
        msg = f"One minute remaining in the {game.period.current_ordinal} period."
        socialhandler.send(msg=msg, game_hashtag=True)
        game.period.current_oneminute_sent = True
    elif period_remain_ss < 50:
        # Force the property to true if the period is below 50s
        game.period.current_oneminute_sent = True
コード例 #6
0
ファイル: game.py プロジェクト: Eric-DuBose/nhl-twitter-bot
    def goalie_pull_social(self, team_name, trailing_score):
        """ Sends a message to social media about the goalie for a team being pulled.

        Args:
            self: current game instance
            team_name: team short name (from the team's object)
            trailing_score: the amount of goals the pulled team is trailing by

        Returns:
            None
        """

        goalie_pull_text = (
            f"The {team_name} have pulled their goalie trailing by {trailing_score} with "
            f"{self.period.time_remaining} left in the {self.period.current_ordinal} period."
        )

        socialhandler.send(msg=goalie_pull_text, force_send=True)
コード例 #7
0
def get_starters(game: Game):
    """ Uses the NHL Roster Report to get the starting lineup. """
    def get_players_name(score_rpt_row):
        """ Very specific function to only return the player's last name from the roster report. """
        player_name = score_rpt_row.find_all("td")[2].text
        return " ".join(
            player_name.replace(" (A)", "").replace(" (C)",
                                                    "").title().split()[1:])

    while not game.preview_socials.starters_sent:
        livefeed_resp = livefeed.get_livefeed(game.game_id)
        game.update_game(livefeed_resp)
        if game.game_state == GameState.LIVE.value:
            logging.info(
                "Game state switched to live - forget about the starters.")
            return

        roster_endpoint = f"/{game.season}/RO{game.game_id_html}.HTM"
        r = api.nhl_score_rpt(roster_endpoint)

        if not r:
            logging.warning(
                "Roster report is not available, something is wrong.")
            return

        try:
            soup = thirdparty.bs4_parse(r.content)
            team_headings = soup.find(
                "td", class_="teamHeading + border").find_parent("tr")
            data = team_headings.find_next("tr")

            rosters = data.find("tr").find_all("td", recursive=False)
            roster = rosters[
                0] if game.preferred_team.home_away == "away" else rosters[1]

            players = [x for x in roster.find_all("tr")]

            starters = list()
            for pos in [("L", "R", "C"), "D", "G"]:
                pos_all = [
                    x for x in players if x.find_all("td")[1].text in pos
                ]
                pos_start = [
                    get_players_name(x) for x in pos_all
                    if "bold" in x.find_all("td")[0]["class"]
                ]
                pos_start_str = " - ".join(pos_start)
                starters.append(pos_start_str)
        except Exception as e:
            logging.error(
                "Something happened while trying to get the starters - sleep for 20s & try again. %s",
                e,
            )
            time.sleep(20)
            continue

        if not starters:
            logging.info(
                "Starters not yet avialble from the roster report - sleep & try again."
            )
            time.sleep(20)
            continue

        starters_string = "\n".join(starters)
        starters_msg = (
            f"{utils.team_hashtag(game.preferred_team.team_name)} Starters:"
            f"\n\n{starters_string}")
        socialhandler.send(starters_msg, force_send=True, game_hashtag=True)
        game.preview_socials.starters_msg = starters_msg
        game.preview_socials.starters_sent = True
コード例 #8
0
def generate_game_preview(game: Game):
    """Generates and sends the game preview to social media.

    This function runs when the game is in Preview State and it is not yet
    game time. Should only run once at the morning scheduled time.

    Args:
        game: Game Object

    Returns:
        sleep_time: Seconds to sleep until next action
    """

    logging.info("Generating Game Preview images & social media posts.")
    logging.info("Game Date: Local - %s, UTC - %s", game.game_time_local,
                 game.date_time)

    # Load our Config
    # config = utils.load_config()
    # preview_sleep_time = config["script"]["preview_sleep_time"]
    # preview_sleep_mins = preview_sleep_time / 60

    # Get the preferred team, other teams & homeaway from the Game object
    pref_team, other_team = game.get_preferred_team()
    # pref_team_homeaway = game.preferred_team.home_away

    # Get Team Hashtags
    pref_hashtag = utils.team_hashtag(pref_team.team_name, game.game_type)
    other_hashtag = utils.team_hashtag(other_team.team_name, game.game_type)

    # Generate the propert clock emoji for the game time
    clock_emoji = utils.clock_emoji(game.game_time_local)

    # If the game is a playoff game, our preview text changes slightly
    if GameType(game.game_type) == GameType.PLAYOFFS:
        preview_text_teams = (
            f"Tune in {game.game_time_of_day} for Game #{game.game_id_playoff_game} when the "
            f"{pref_team.team_name} take on the {other_team.team_name} at {game.venue}."
        )
    else:
        preview_text_teams = (
            f"Tune in {game.game_time_of_day} when the {pref_team.team_name} "
            f"take on the {other_team.team_name} at {game.venue}.")

    # Generate clock, channel & hashtag emoji text preview
    preview_text_emojis = (
        f"{clock_emoji} {game.game_time_local}\n"
        f"\U0001F4FA {pref_team.tv_channel}\n"
        f"\U00000023\U0000FE0F\U000020E3 {game.game_hashtag}")

    # Generate final preview tweet text
    preview_tweet_text = f"{preview_text_teams}\n\n{preview_text_emojis}"
    game.preview_socials.core_msg = preview_tweet_text

    # Generate pre-game image
    pregame_image = images.pregame_image(game)
    img_filename = os.path.join(IMAGES_PATH, "temp",
                                f"Pregame-{game.game_id}.png")
    pregame_image.save(img_filename)

    # Send preview tweet w/ pre-game image to social media handler
    social_dict = socialhandler.send(msg=preview_tweet_text,
                                     media=img_filename,
                                     force_send=True)
    game.pregame_lasttweet = social_dict["twitter"]

    # Generate Season Series Data
    season_series = schedule.season_series(game.game_id, pref_team, other_team)
    season_series_string = season_series[0]

    if season_series_string is None:
        # If this is the first game of the season, we can set the 'last_season' flag to enable the
        # season series function to check last year's season series between the two teams.
        logging.info(
            "First game of the season - re-run the season series function with the last_season flag."
        )

        season_series = schedule.season_series(game.game_id,
                                               pref_team,
                                               other_team,
                                               last_season=True)

        season_series_string = season_series[0]
        season_series_string = (
            f"This is the first meeting of the season between the "
            f"{pref_team.short_name} & the {other_team.short_name}.\n\n"
            f"LAST SEASON STATS\n{season_series_string}")

        # season_series_tweet_text = (
        #     f"This is the first meeting of the season between the "
        #     f"{pref_team.short_name} & the {other_team.short_name}. "
        #     f"Last season's stats -"
        #     f"\n\n{season_series_string}\n{points_leader_str}\n{toi_leader_str}"
        #     f"\n\n{pref_hashtag} {other_hashtag} {game.game_hashtag}"
        # )

    # Extract strings from returned list / tuple
    points_leader_str = season_series[1]
    toi_leader_str = season_series[2]

    if game.game_type == "P":
        # season_series_str = season_series_str.replace("season series", "regular season series")
        season_series_string = f"Regular Season Stats -\n\n{season_series_string}"

    season_series_tweet_text = (
        f"{season_series_string}\n{points_leader_str}\n{toi_leader_str}"
        f"\n\n{pref_hashtag} {other_hashtag} {game.game_hashtag}")

    game.preview_socials.season_series_msg = season_series_tweet_text

    # logging.info(preview_tweet_text)
    # logging.info(season_series_tweet_text)
    discord_color = images.discord_color(game.preferred_team.team_name)
    social_dict = socialhandler.send(
        msg=season_series_tweet_text,
        reply=game.pregame_lasttweet,
        force_send=True,
        discord_title="PREVIEW: Season Series",
        discord_color=discord_color,
    )

    game.pregame_lasttweet = social_dict["twitter"]

    game.preview_socials.core_sent = True
    game.preview_socials.season_series_sent = True
コード例 #9
0
def game_preview_others(game: Game):
    """Other game preview information (excluding our core game preview).
        This includes things like goalies, lines, referees, etc.

    This function runs when the game is in Preview State and it is not yet
    game time. Runs every xxx minutes (configured in config.yaml file).

    Args:
        game: Game Object

    Returns:
        sleep_time: Seconds to sleep until next action
    """
    # All of the below functions containg information from non-NHL API sites
    # Each one is wrapped in a try / except just in case.

    # Load our Config
    config = utils.load_config()
    preview_sleep_time = config["script"]["preview_sleep_time"]
    preview_sleep_mins = preview_sleep_time / 60

    # Get the preferred team, other teams & homeaway from the Game object
    pref_team, other_team = game.get_preferred_team()
    pref_team_homeaway = game.preferred_team.home_away

    # Get Team Hashtags
    pref_hashtag = utils.team_hashtag(pref_team.team_name, game.game_type)
    other_hashtag = utils.team_hashtag(other_team.team_name, game.game_type)

    # Process the pre-game information for the starting goalies
    if not game.preview_socials.goalies_pref_sent or not game.preview_socials.goalies_other_sent:
        logging.info(
            "One of the two goalies is not yet confirmed - getting their info now."
        )
        # goalies_confirmed_values = ("Confirmed", "Likely", "Unconfirmed")
        goalies_confirmed_values = ("Confirmed", "Likely")
        try:
            df_date = game.custom_game_date("%m-%d-%Y")
            goalies_df = thirdparty.dailyfaceoff_goalies(
                pref_team, other_team, pref_team_homeaway, df_date)
            logging.info(goalies_df)

            goalie_confirm_pref = bool(
                goalies_df.get("pref").get("confirm") in
                goalies_confirmed_values)
            goalie_confirm_other = bool(
                goalies_df.get("other").get("confirm") in
                goalies_confirmed_values)

            logging.info("Goalie Confirmed PREF : %s", goalie_confirm_pref)
            logging.info("Goalie Confirmed OTHER : %s", goalie_confirm_other)

            if goalie_confirm_pref and not game.preview_socials.goalies_pref_sent:
                try:
                    goalie_pref = goalies_df.get("pref")
                    goalie_pref_name = goalie_pref.get("name")
                    goalie_pref_confirm = goalie_pref.get("confirm")
                    goalie_pref_season = goalie_pref.get("season")
                    if goalie_pref_season == "-- W-L | GAA | SV% | SO":
                        goalie_pref_season = "None (Season Debut)"
                    goalie_hr_pref = thirdparty.hockeyref_goalie_against_team(
                        goalie_pref_name, game.other_team.team_name)
                    logging.info("Hockey Reference Goalie PREF : %s",
                                 goalie_hr_pref)

                    pref_goalie_tweet_text = (
                        f"{goalie_pref_confirm} goalie for the {pref_team.short_name} -\n"
                        f"(via @DailyFaceoff)\n\n{goalie_pref_name}\n"
                        f"Season Stats: {goalie_pref_season}\n"
                        f"Career (vs. {other_team.short_name}): {goalie_hr_pref}\n\n"
                        f"{pref_hashtag} {game.game_hashtag}")
                    discord_color = images.discord_color(
                        game.preferred_team.team_name)
                    social_dict = socialhandler.send(
                        msg=pref_goalie_tweet_text,
                        reply=game.pregame_lasttweet,
                        force_send=True,
                        discord_title="PREVIEW: Goalie Start",
                        discord_color=discord_color,
                    )
                    game.pregame_lasttweet = social_dict["twitter"]
                    game.preview_socials.goalies_pref_sent = True

                except Exception as e:
                    logging.error(
                        "Exception getting PREFERRED Hockey Reference splits - try again next loop."
                    )
                    logging.error(e)
            else:
                logging.info(
                    "Preferred goalie not yet confirmed - try again next loop."
                )

            if goalie_confirm_other and not game.preview_socials.goalies_other_sent:
                try:
                    goalie_other = goalies_df.get("other")
                    goalie_other_name = goalie_other.get("name")
                    goalie_other_confirm = goalie_other.get("confirm")
                    goalie_other_season = goalie_other.get("season")
                    if goalie_other_season == "-- W-L | GAA | SV% | SO":
                        goalie_other_season = "None (Season Debut)"
                    goalie_hr_other = thirdparty.hockeyref_goalie_against_team(
                        goalie_other_name, game.preferred_team.team_name)
                    logging.info("Hockey Reference Goalie OTHER : %s",
                                 goalie_hr_other)

                    other_goalie_tweet_text = (
                        f"{goalie_other_confirm} goalie for the {other_team.short_name} -\n"
                        f"(via @DailyFaceoff)\n\n{goalie_other_name}\n"
                        f"Season Stats: {goalie_other_season}\n"
                        f"Career (vs. {pref_team.short_name}): {goalie_hr_other}\n\n"
                        f"{other_hashtag} {game.game_hashtag}")

                    discord_color = images.discord_color(
                        game.preferred_team.team_name)
                    social_dict = socialhandler.send(
                        msg=other_goalie_tweet_text,
                        reply=game.pregame_lasttweet,
                        force_send=True,
                        discord_title="PREVIEW: Goalie Start",
                        discord_color=discord_color,
                    )

                    game.pregame_lasttweet = social_dict["twitter"]
                    game.preview_socials.goalies_other_sent = True

                except Exception as e:
                    logging.error(
                        "Exception getting OTHER Hockey Reference splits - try again next loop."
                    )
                    logging.error(e)
            else:
                logging.info(
                    "Other goalie not yet confirmed - try again next loop.")

        except Exception as e:
            logging.error(
                "Exception getting Daily Faceoff goalies - try again next loop."
            )
            logging.error(e)

    # Process the pre-game information for the game officials
    if not game.preview_socials.officials_sent:
        try:
            officials = thirdparty.scouting_the_refs(game, pref_team)
            logging.info(officials)

            officials_confirmed = officials.get("confirmed")

            if officials_confirmed:
                officials_tweet_text = f"The officials (via @ScoutingTheRefs) for {game.game_hashtag} are -\n"
                for key, attrs in officials.items():
                    if key == "confirmed":
                        continue
                    officials_tweet_text = f"{officials_tweet_text}\n\n{key.title()}:"
                    for official in attrs:
                        official_name = official.get("name")
                        official_season = official.get("seasongames")
                        official_career = official.get("careergames")
                        official_games = official.get("totalgames", 0)
                        official_penalty_game = official.get("penaltygame")
                        if official_penalty_game:
                            official_detail = (
                                f"{official_name} (Games: {official_games} | P/GM: {official_penalty_game})"
                            )
                            # official_detail = f"{official_name} (Gms: {official_season} / {official_career} | P/GM: {official_penalty_game})"
                        else:
                            official_detail = f"{official_name} (Games: {official_games})"
                            # official_detail = f"{official_name} (Games: {official_season} / {official_career})"
                        officials_tweet_text = f"{officials_tweet_text}\n- {official_detail}"

                social_dict = socialhandler.send(msg=officials_tweet_text,
                                                 reply=game.pregame_lasttweet,
                                                 force_send=True)

                game.pregame_lasttweet = social_dict["twitter"]
                game.preview_socials.officials_sent = True
            else:
                logging.info(
                    "Officials not yet confirmed - try again next loop.")

        except Exception as e:
            logging.error(
                "Exception getting Scouting the Refs information - try again next loop."
            )
            logging.error(e)

    # Process the pre-game information for the preferred team lines
    if not game.preview_socials.pref_lines_sent or game.preview_socials.check_for_changed_lines(
            "preferred"):
        try:
            pref_lines = thirdparty.dailyfaceoff_lines(game, pref_team)
            if not pref_lines.get("confirmed"):
                raise AttributeError(
                    "Preferred team lines are not yet confirmed yet - try again next loop."
                )

            fwd_string = pref_lines.get("fwd_string")
            def_string = pref_lines.get("def_string")

            lines_tweet_text = (f"Lines for the {pref_hashtag} -\n"
                                f"(via @DailyFaceoff)\n\n"
                                f"Forwards:\n{fwd_string}\n\n"
                                f"Defense:\n{def_string}")

            # If we have not sent the lines out at all, force send them
            if not game.preview_socials.pref_lines_sent:
                social_dict = socialhandler.send(msg=lines_tweet_text,
                                                 reply=game.pregame_lasttweet,
                                                 force_send=True)
                game.pregame_lasttweet = social_dict["twitter"]
                game.preview_socials.pref_lines_msg = lines_tweet_text
                game.preview_socials.pref_lines_sent = True
            else:
                lines_changed, lines_tweet_text = game.preview_socials.did_lines_change(
                    "preferred", lines_tweet_text)
                if lines_changed:
                    social_dict = socialhandler.send(
                        msg=lines_tweet_text,
                        reply=game.pregame_lasttweet,
                        force_send=True)
                    game.pregame_lasttweet = social_dict["twitter"]
                    game.preview_socials.pref_lines_msg = lines_tweet_text
                    game.preview_socials.pref_lines_resent = True
                else:
                    logging.info(
                        "The preferred team lines have not changed - check again in an hour."
                    )

        except AttributeError as e:
            logging.info(e)
        except Exception as e:
            logging.error(
                "Exception getting Daily Faceoff lines information - try again next loop."
            )
            logging.error(e)

    # Process the pre-game information for the preferred team lines
    if not game.preview_socials.other_lines_sent or game.preview_socials.check_for_changed_lines(
            "other"):
        try:
            other_lines = thirdparty.dailyfaceoff_lines(game, other_team)
            if not other_lines.get("confirmed"):
                raise AttributeError(
                    "Other team lines are not yet confirmed yet - try again next loop."
                )

            fwd_string = other_lines.get("fwd_string")
            def_string = other_lines.get("def_string")

            lines_tweet_text = (f"Lines for the {other_hashtag} -\n"
                                f"(via @DailyFaceoff)\n\n"
                                f"Forwards:\n{fwd_string}\n\n"
                                f"Defense:\n{def_string}")

            # If we have not sent the lines out at all, force send them
            if not game.preview_socials.other_lines_sent:
                social_dict = socialhandler.send(msg=lines_tweet_text,
                                                 reply=game.pregame_lasttweet,
                                                 force_send=True)
                game.pregame_lasttweet = social_dict["twitter"]
                game.preview_socials.other_lines_msg = lines_tweet_text
                game.preview_socials.other_lines_sent = True
            else:
                lines_changed, lines_tweet_text = game.preview_socials.did_lines_change(
                    "other", lines_tweet_text)
                if lines_changed:
                    social_dict = socialhandler.send(
                        msg=lines_tweet_text,
                        reply=game.pregame_lasttweet,
                        force_send=True)
                    game.pregame_lasttweet = social_dict["twitter"]
                    game.preview_socials.other_lines_msg = lines_tweet_text
                    game.preview_socials.other_lines_resent = True
                else:
                    logging.info(
                        "The preferred team lines have not changed - check again in an hour."
                    )

        except AttributeError as e:
            logging.info(e)
        except Exception as e:
            logging.error(
                "Exception getting Daily Faceoff lines information - try again next loop."
            )
            logging.error(e)

    # Check if all pre-game tweets are sent
    # And return time to sleep
    all_pregametweets = all(value is True
                            for value in game.pregametweets.values())

    if not all_pregametweets and game.game_time_countdown > preview_sleep_time:
        logging.info(
            "Game State is Preview & all pre-game tweets are not sent. "
            "Sleep for 30 minutes & check again.")
        return preview_sleep_time, False
    elif not all_pregametweets and game.game_time_countdown < preview_sleep_time:
        logging.warning(
            "Game State is Preview & all pre-game tweets are not sent. "
            "Less than %s minutes until game time so we skip these today."
            "If needed, we try to get lines at the end of the game for advanced stats.",
            preview_sleep_mins,
        )
        return game.game_time_countdown, True
    else:
        logging.info(
            "Game State is Preview & all tweets are sent. Sleep for %s seconds until game time.",
            game.game_time_countdown,
        )

        # We need to subtract 5-minutes from this
        return game.game_time_countdown, True
コード例 #10
0
ファイル: live.py プロジェクト: Eric-DuBose/nhl-twitter-bot
def intermission_loop(game: Game):
    """The live-game intermission loop. Things to do during an intermission

    Args:
        game: Game Object

    Returns:
        live_sleep_time: The amount to sleep until our next check.
    """

    args = arguments.get_arguments()
    config = utils.load_config()

    # If we are in intermission, check if NST is ready for charts.
    # Incorporating the check into this loop will be sure we obey the 60s sleep rule.
    # We use the currentPeriod as the key to lookup if the charts
    # have been sent for the current period's intermission
    nst_chart_period_sent = game.nst_charts.charts_by_period.get(
        game.period.current)
    if not nst_chart_period_sent:
        logging.info(
            "NST Charts not yet sent - check if it's ready for us to scrape.")
        nst_ready = nst.is_nst_ready(
            game.preferred_team.short_name) if not args.date else True
        if nst_ready:
            try:
                list_of_charts = nst.generate_all_charts(game=game)
                # Chart at Position 0 is the Overview Chart & 1-4 are the existing charts
                overview_chart = list_of_charts["overview"]
                team_charts = list_of_charts["barcharts"]
                shift_chart = list_of_charts["shift"]

                overview_chart_msg = (
                    f"Team Overview stat percentages - 5v5 (SVA) after the "
                    f"{game.period.current_ordinal} period (via @NatStatTrick)."
                )

                ov_social_ids = socialhandler.send(overview_chart_msg,
                                                   media=overview_chart,
                                                   game_hashtag=True)

                charts_msg = (
                    f"Individual, on-ice, forward lines & defensive pairs after the "
                    f"{game.period.current_ordinal} period (via @NatStatTrick)."
                )
                ind_social_ids = socialhandler.send(
                    charts_msg,
                    media=team_charts,
                    game_hashtag=True,
                    reply=ov_social_ids["twitter"])

                charts_msg = (
                    f"Shift length breakdown after the "
                    f"{game.period.current_ordinal} period (via @NatStatTrick)."
                )
                social_ids = socialhandler.send(
                    charts_msg,
                    media=shift_chart,
                    game_hashtag=True,
                    reply=ind_social_ids["twitter"])

                game.nst_charts.charts_by_period[game.period.current] = True

            except Exception as e:
                logging.error(
                    "Error creating Natural Stat Trick charts (%s) - sleep for a bit longer.",
                    e)

    # Check if our shotmap was RT'd & if not try to search for it and send it out
    shotmap_retweet_sent = game.period.shotmap_retweet
    if not shotmap_retweet_sent and config["socials"]["twitter"]:
        game.period.shotmap_retweet = common.search_send_shotmap(game=game)

    # Calculate proper sleep time based on intermission status
    if game.period.intermission_remaining > config["script"][
            "intermission_sleep_time"]:
        live_sleep_time = config["script"]["intermission_sleep_time"]
        logging.info(
            "Sleeping for configured intermission time (%ss).",
            config["script"]["intermission_sleep_time"],
        )
    else:
        live_sleep_time = game.period.intermission_remaining
        logging.info("Sleeping for remaining intermission time (%ss).",
                     game.period.intermission_remaining)

    return live_sleep_time
コード例 #11
0
def final_score(livefeed: dict, game: Game):
    """Takes the livefeed response from the NHL API and sets the status & key of this attribute
        in the EndOfGame socials class.

    Args:
        livefeed: Live Feed API response
        game: Game Object

    Returns:
        None: Sets a status & message in EndOfGame object
        # bool: if the function runs & sends to social succesfully
    """
    logging.info("Starting the core Final Score work now.")
    if game.final_socials.final_score_sent:
        logging.debug("Final score social already sent - skip this iteration!")
        return

    # Get all nested dictionaries frmo the livefeed response
    all_plays = livefeed["liveData"]["plays"]["allPlays"]
    boxscore = livefeed["liveData"]["boxscore"]
    linescore = livefeed["liveData"]["linescore"]

    linescore_period = linescore["currentPeriod"]
    linescore_teams = linescore["teams"]
    linescore_pref = linescore_teams[game.preferred_team.home_away]
    linescore_other = linescore_teams[game.other_team.home_away]

    pref_home_text = "on the road" if game.preferred_team.home_away == "away" else "at home"
    score_pref = linescore_pref["goals"]
    score_other = linescore_other["goals"]

    # Everytime there is a shootout, when the GAME_END event is posted,
    # the score of the game is still tied - fix this by checking the SO scores.
    if linescore_period == 5 and (score_pref == score_other):
        logging.info(
            "A shootout caused the final score to be tied - checke the shootoutInfo key"
        )
        shootout_info = linescore["shootoutInfo"]
        logging.info("Shootout Info: %s", shootout_info)
        pref_so_goals = shootout_info[game.preferred_team.home_away]["scores"]
        other_so_goals = shootout_info[game.other_team.home_away]["scores"]
        if pref_so_goals > other_so_goals:
            logging.info(
                "Preferred Team scored more shootout goals, increment score by 1."
            )
            score_pref = score_pref + 1
        else:
            logging.info(
                "Other Team scored more shootout goals, increment score by 1.")
            score_other = score_other + 1

        # Set Team Objects New Score
        game.preferred_team.score = score_pref
        game.other_team.score = score_other

    if score_pref > score_other:
        final_score_text = (
            f"{game.preferred_team.short_name} win {pref_home_text} over the "
            f"{game.other_team.short_name} by a score of {score_pref} to "
            f"{score_other}! 🚨🚨🚨")
    else:
        final_score_text = (
            f"{game.preferred_team.short_name} lose {pref_home_text} to the "
            f"{game.other_team.short_name} by a score of {score_pref} to "
            f"{score_other}! 👎🏻👎🏻👎🏻")

    # Using the NHL Schedule API, get the next game which goes at the bottom of this core tweet
    try:
        next_game = schedule.get_next_game(game.local_datetime,
                                           game.preferred_team.team_id)

        # Caclulate the game in the team's local time zone
        tz_id = dateutil.tz.gettz(game.preferred_team.tz_id)
        tz_offset = tz_id.utcoffset(datetime.now(tz_id))
        next_game_date = next_game["gameDate"]
        next_game_dt = parse(next_game_date)
        next_game_dt_local = next_game_dt + tz_offset
        next_game_string = datetime.strftime(next_game_dt_local,
                                             "%A %B %d @ %I:%M%p")

        # Get next game's opponent
        next_game_teams = next_game["teams"]

        next_game_home = next_game_teams["home"]
        next_game_home_id = next_game_home["team"]["id"]
        next_game_home_name = next_game_home["team"]["name"]

        next_game_away = next_game_teams["away"]
        next_game_away_name = next_game_away["team"]["name"]

        if next_game_home_id == game.preferred_team.team_id:
            next_opponent = next_game_away_name
        else:
            next_opponent = next_game_home_name

        next_game_venue = next_game["venue"]["name"]
        next_game_text = f"Next Game: {next_game_string} vs. {next_opponent} (at {next_game_venue})!"
    except Exception as e:
        logging.warning("Error getting next game via the schedule endpoint.")
        logging.error(e)
        next_game_text = ""

    final_image = images.stats_image(game=game,
                                     game_end=True,
                                     boxscore=boxscore)
    img_filename = os.path.join(IMAGES_PATH, "temp",
                                f"Final-{game.game_id}.png")
    final_image.save(img_filename)

    final_score_msg = f"{final_score_text}\n\n{next_game_text}"
    # socialhandler.send(final_score_msg)
    socialhandler.send(msg=final_score_msg, media=img_filename)

    # Set the final score message & status in the EndOfGame Social object
    game.final_socials.final_score_msg = final_score_msg
    game.final_socials.final_score_sent = True
コード例 #12
0
def run():
    """ The main script runner - everything starts here! """
    config = utils.load_config()
    args = arguments.get_arguments()

    # Setup the logging for this script run (console, file, etc)
    utils.setup_logging()

    # Get the team name the bot is running as
    team_name = args.team if args.team else config["default"]["team_name"]

    # ------------------------------------------------------------------------------
    # PRE-SCRIPT STARTS PROCESSING BELOW
    # ------------------------------------------------------------------------------

    # Log script start lines
    logging.info("#" * 80)
    logging.info("New instance of the Hockey Twitter Bot (V%s) started.",
                 VERSION)
    if args.docker:
        logging.info(
            "Running in a Docker container - environment variables parsed.")
    logging.info("TIME: %s", datetime.now())
    logging.info("ARGS - notweets: %s, console: %s, teamoverride: %s",
                 args.notweets, args.console, args.team)
    logging.info(
        "ARGS - debug: %s, debugsocial: %s, overridelines: %s",
        args.debug,
        args.debugsocial,
        args.overridelines,
    )
    logging.info("ARGS - date: %s, split: %s, localdata: %s", args.date,
                 args.split, args.localdata)
    logging.info(
        "SOCIAL - twitter: %s, discord: %s, slack: %s",
        config["socials"]["twitter"],
        config["socials"]["discord"],
        config["socials"]["slack"],
    )
    logging.info("%s\n", "#" * 80)

    # Check if there is a game scheduled for today -
    # If there is no game, exit the script.
    date = utils.date_parser(args.date) if args.date else datetime.now()
    team_id = schedule.get_team_id(team_name)
    game_today, game_info = schedule.is_game_today(team_id, date)
    if not game_today:
        game_yesterday, prev_game = schedule.was_game_yesterday(team_id, date)
        if game_yesterday:
            logging.info(
                "There was a game yesterday - send recap, condensed game "
                "and generate new season overview stats chart, tweet it & exit."
            )

            # Get Team Information
            home_team = prev_game["teams"]["home"]
            home_team_name = home_team["team"]["name"]
            away_team = prev_game["teams"]["away"]
            away_team_name = away_team["team"]["name"]

            pref_team = home_team if home_team["team"][
                "name"] == team_name else away_team
            other_team = away_team if home_team["team"][
                "name"] == team_name else home_team

            pref_team_name = pref_team["team"]["name"]
            pref_score = pref_team["score"]
            pref_hashtag = utils.team_hashtag(pref_team_name)
            other_team_name = other_team["team"]["name"]
            other_score = other_team["score"]

            # Get the Recap & Condensed Game
            game_id = prev_game["gamePk"]
            content_feed = contentfeed.get_content_feed(game_id)

            # Send Recap Tweet
            try:
                recap, recap_video_url = contentfeed.get_game_recap(
                    content_feed)
                recap_description = recap["items"][0]["description"]
                recap_msg = f"📺 {recap_description}.\n\n{recap_video_url}"
                socialhandler.send(recap_msg)
            except Exception as e:
                logging.error("Error getting Game Recap. %s", e)

            # Send Condensed Game / Extended Highlights Tweet
            try:
                condensed_game, condensed_video_url = contentfeed.get_condensed_game(
                    content_feed)
                condensed_blurb = condensed_game["items"][0]["blurb"]
                condensed_msg = f"📺 {condensed_blurb}.\n\n{condensed_video_url}"
                socialhandler.send(condensed_msg)
            except Exception as e:
                logging.error(
                    "Error getting Condensed Game from NHL - trying YouTube. %s",
                    e)
                try:
                    condensed_game = youtube.youtube_condensed(
                        away_team_name, home_team_name)
                    condensed_blurb = condensed_game["title"]
                    condensed_video_url = condensed_game["yt_link"]
                    condensed_msg = f"📺 {condensed_blurb}.\n\n{condensed_video_url}"
                    socialhandler.send(condensed_msg)
                except Exception as e:
                    logging.error(
                        "Error getting Condensed Game from NHL & YouTube - skip this today. %s",
                        e)

            # Generate the Season Overview charts
            game_result_str = "defeat" if pref_score > other_score else "lose to"

            team_season_msg = (
                f"Updated season overview & last 10 game stats after the {pref_team_name} "
                f"{game_result_str} the {other_team_name} by a score of {pref_score} to {other_score}."
                f"\n\n{pref_hashtag}")

            team_season_fig = nst.generate_team_season_charts(team_name, "sva")
            team_season_fig_last10 = nst.generate_team_season_charts(
                team_name, "sva", lastgames=10)
            team_season_fig_all = nst.generate_team_season_charts(
                team_name, "all")
            team_season_fig_last10_all = nst.generate_team_season_charts(
                team_name, "all", lastgames=10)
            team_season_charts = [
                team_season_fig,
                team_season_fig_last10,
                team_season_fig_all,
                team_season_fig_last10_all,
            ]
            socialhandler.send(team_season_msg, media=team_season_charts)
        else:
            logging.info("There was no game yesterday - exiting!")

        sys.exit()

    # For debugging purposes, print all game_info
    logging.debug("%s", game_info)

    # Create the Home & Away Team objects
    away_team = Team.from_json(game_info, "away")
    home_team = Team.from_json(game_info, "home")

    # If lines are being overriden by a local lineup file,
    # set the overrlide lines property to True
    if args.overridelines:
        home_team.overridelines = True
        away_team.overridelines = True

    # The preferred team is the team the bot is running as
    # This allows us to track if the preferred team is home / away
    home_team.preferred = bool(home_team.team_name == team_name)
    away_team.preferred = bool(away_team.team_name == team_name)

    # Create the Game Object!
    game = Game.from_json_and_teams(game_info, home_team, away_team)
    GlobalGame.game = game

    # Override Game State for localdata testing
    game.game_state = "Live" if args.localdata else game.game_state

    # Update the Team Objects with the gameday rosters
    roster.gameday_roster_update(game)

    # print(vars(game))
    # print(vars(away_team))
    # print(vars(home_team))

    # If the codedGameState is set to 9 originally, game is postponed (exit immediately)
    if game.game_state_code == GameStateCode.POSTPONED.value:
        logging.warning(
            "This game is marked as postponed during our first run - exit silently."
        )
        end_game_loop(game)

    # Return the game object to use in the game loop function
    return game
コード例 #13
0
def start_game_loop(game: Game):
    """The main game loop - tracks game state & calls all relevant functions.

    Args:
        game: Current Game object

    Returns:
        None
    """

    args = arguments.get_arguments()
    config = utils.load_config()

    # ------------------------------------------------------------------------------
    # START THE MAIN LOOP
    # ------------------------------------------------------------------------------

    while True:
        if game.game_state == GameState.PREVIEW.value:
            livefeed_resp = livefeed.get_livefeed(game.game_id)
            game.update_game(livefeed_resp)

            # If after the update_game() function runs, we have a Postponed Game
            # We should tweet it - this means it happened after the game was scheduled
            if game.game_state_code == GameStateCode.POSTPONED.value:
                logging.warning(
                    "This game was originally scheduled, but is now postponed."
                )
                social_msg = (
                    f"⚠️ The {game.preferred_team.team_name} game scheduled for today has been postponed."
                )
                socialhandler.send(social_msg)
                end_game_loop(game)

            if game.game_time_countdown > 0:
                logging.info(
                    "Game is in Preview state - send out all pregame information."
                )
                # The core game preview function should run only once
                if not game.preview_socials.core_sent:
                    preview.generate_game_preview(game)

                # The other game preview function should run every xxx minutes
                # until all pregame tweets are sent or its too close to game time
                sleep_time, last_sleep_before_live = preview.game_preview_others(
                    game)
                game.preview_socials.increment_counter()

                # If this is the last sleep before the game goes live, cut it by 5 minutes for starters function.
                if last_sleep_before_live:
                    logging.info(
                        "This is the last sleep before the game goes live - 5 minutes less & starters."
                    )
                    sleep_time = 0 if (sleep_time - 300) < 0 else sleep_time
                    time.sleep(sleep_time)
                    preview.get_starters(game)
                else:
                    time.sleep(sleep_time)

            else:
                logging.info(
                    "Game is in Preview state, but past game start time - sleep for a bit "
                    "& update game attributes so we detect when game goes live."
                )

                # Somehow we got here without the starting lineup - try again
                if not game.preview_socials.starters_sent:
                    preview.get_starters(game)

                sleep_time = config["script"]["pregame_sleep_time"]
                time.sleep(sleep_time)

        elif game.game_state == GameState.LIVE.value:
            try:
                logging.info("-" * 80)
                logging.info(
                    "Game is LIVE (loop #%s) - checking events after event Idx %s.",
                    game.live_loop_counter,
                    game.last_event_idx,
                )

                # On my development machine, this command starts the files for this game
                # python -m hockeygamebot --console --notweets --team 'Vancouver Canucks' --date '2019-09-17' --localdata
                if args.localdata:
                    logging.info(
                        "SIMULATION DETECTED - running a live game replay for Game %s (%s vs. %s).",
                        game.game_id,
                        game.home_team.team_name,
                        game.away_team.team_name,
                    )
                    directory = "/Users/mattdonders/Development/python/devils-goal-twitter-bitbucket/scratchpad/feed-samples"
                    for file in sorted(os.listdir(directory)):
                        filename = os.fsdecode(file)
                        if filename.endswith(".json"):
                            feed_json = os.path.join(directory, filename)
                            with open(feed_json) as json_file:
                                data = json.load(json_file)

                            # Logging (Temporarily) for Penalty Killed Tweets
                            logging.info(
                                "Current Period Info: %s - %s",
                                game.period.current_ordinal,
                                game.period.time_remaining,
                            )
                            logging.info(
                                "Pref On Ice: %s - %s",
                                len(game.preferred_team.onice),
                                game.preferred_team.onice,
                            )
                            logging.info(
                                "Other On Ice: %s - %s",
                                len(game.other_team.onice),
                                game.other_team.onice,
                            )

                            # Penalty Killed Status
                            penalty_situation = game.penalty_situation
                            if penalty_situation.penalty_killed:
                                logging.info(
                                    "***** PENALTY KILLED NOTIFICATION *****")
                                shots_taken = (
                                    penalty_situation.pp_team.shots -
                                    penalty_situation.pp_team_shots_start)
                                logging.info("PP Shots Taken: %s", shots_taken)
                                game.penalty_situation = PenaltySituation()

                            if game.penalty_situation.in_situation:
                                logging.info(
                                    "Current Penalty (In Situation): %s",
                                    vars(game.penalty_situation),
                                )

                            if not game.period.current_oneminute_sent:
                                live.minute_remaining_check(game)

                            live.live_loop(livefeed=data, game=game)
                            game.update_game(data)

                            time.sleep(0.1)

                # Non-Local Data
                livefeed_resp = livefeed.get_livefeed(game.game_id)
                # all_events = live.live_loop(livefeed=livefeed_resp, game=game)

                # Update all game attributes & check for goalie pulls
                game.update_game(livefeed_resp)
                game.goalie_pull_updater(livefeed_resp)

                # Logging (Temporarily) for Penalty Killed Tweets
                logging.info(
                    "Current Period Info: %s - %s",
                    game.period.current_ordinal,
                    game.period.time_remaining,
                )
                logging.info(
                    "Pref On Ice: %s - %s",
                    len(game.preferred_team.onice),
                    game.preferred_team.onice,
                )
                logging.info("Other On Ice: %s - %s",
                             len(game.other_team.onice), game.other_team.onice)

                # Penalty Killed Status
                penalty_situation = game.penalty_situation
                if penalty_situation.penalty_killed:
                    logging.info("***** PENALTY KILLED NOTIFICATION *****")
                    shots_taken = penalty_situation.pp_team.shots - penalty_situation.pp_team_shots_start
                    logging.info("PP Shots Taken: %s", shots_taken)
                    game.penalty_situation = PenaltySituation()

                if game.penalty_situation.in_situation:
                    logging.info("Current Penalty (In Situation): %s",
                                 vars(game.penalty_situation))

                if not game.period.current_oneminute_sent:
                    live.minute_remaining_check(game)

                # Pass the live feed response to the live loop (to parse events)
                live.live_loop(livefeed=livefeed_resp, game=game)
                # game_events = get_game_events(game_obj)
                # loop_game_events(game_events, game_obj)

            except Exception as error:
                logging.error(
                    "Uncaught exception in live game loop - see below error.")
                logging.error(error)

            # Perform any intermission score changes, charts & sleep
            if game.period.intermission:
                # Uncomment this tomorrow to test the function relocation
                live_sleep_time = live.intermission_loop(game)

            else:
                live_sleep_time = config["script"]["live_sleep_time"]
                logging.info(
                    "Sleeping for configured live game time (%ss).",
                    config["script"]["live_sleep_time"],
                )

            # Now increment the counter sleep for the calculated time above
            game.live_loop_counter += 1
            time.sleep(live_sleep_time)

        elif game.game_state == GameState.FINAL.value:
            logging.info(
                "Game is now over & 'Final' - run end of game functions with increased sleep time."
            )

            livefeed_resp = livefeed.get_livefeed(game.game_id)
            game.update_game(livefeed_resp)

            # If (for some reason) the bot was started after the end of the game
            # We need to re-run the live loop once to parse all of the events
            if not game.events:
                logging.info(
                    "Bot started after game ended, pass livefeed into event factory to fill events."
                )
                live.live_loop(livefeed=livefeed_resp, game=game)

            # shotmaps.generate_shotmaps(game=game)

            # Run all end of game / final functions
            if not game.final_socials.final_score_sent:
                final.final_score(livefeed=livefeed_resp, game=game)

            if not game.final_socials.three_stars_sent:
                final.three_stars(livefeed=livefeed_resp, game=game)

            if not game.final_socials.nst_linetool_sent:
                # thirdparty.nst_linetool(game=game, team=game.preferred_team)
                game.final_socials.nst_linetool_sent = True

            if not game.final_socials.shotmap_retweet:
                game.final_socials.shotmap_retweet = common.search_send_shotmap(
                    game=game)

            if not game.final_socials.hsc_sent:
                final.hockeystatcards(game=game)

            if not game.nst_charts.final_charts:
                logging.info(
                    "NST Charts not yet sent - check if it's ready for us to scrape."
                )
                nst_ready = nst.is_nst_ready(
                    game.preferred_team.short_name) if not args.date else True
                if nst_ready:
                    all_charts = nst.generate_all_charts(game=game)
                    # Chart at Position 0 is the Overview Chart & 1-4 are the existing charts
                    overview_chart = all_charts["overview"]
                    team_charts = all_charts["barcharts"]
                    scatter_charts = all_charts["scatters"]
                    shift_chart = all_charts["shift"]

                    overview_chart_msg = (
                        f"Team Overview stat percentages - 5v5 (SVA) at the "
                        f"end of the game (via @NatStatTrick).")

                    ov_social_ids = socialhandler.send(overview_chart_msg,
                                                       media=overview_chart,
                                                       game_hashtag=True)

                    charts_msg = (
                        f"Individual, on-ice, forward lines & defensive pairs at the "
                        f"end of the game (via @NatStatTrick).")
                    ind_social_ids = socialhandler.send(
                        charts_msg,
                        media=team_charts,
                        game_hashtag=True,
                        reply=ov_social_ids["twitter"],
                    )

                    charts_msg = f"Shift length breakdown at the end of the game (via @NatStatTrick)."
                    shift_social_ids = socialhandler.send(
                        charts_msg,
                        media=shift_chart,
                        game_hashtag=True,
                        reply=ind_social_ids["twitter"],
                    )

                    charts_msg = (
                        f"Quality vs. Quantity & Expected Goals Rate / 60 at the"
                        " end of the game (via @NatStatTrick).")
                    xg60_social_ids = socialhandler.send(
                        charts_msg,
                        media=scatter_charts,
                        game_hashtag=True,
                        reply=shift_social_ids["twitter"],
                    )
                    game.nst_charts.final_charts = True

            # If we have exceeded the number of retries, stop pinging NST
            if game.final_socials.retries_exeeded:
                game.final_socials.nst_linetool_sent = True

            if game.final_socials.all_social_sent:
                logging.info(
                    "All end of game socials sent or retries were exceeded - ending game!"
                )
                end_game_loop(game=game)

            # If all socials aren't sent or retry limit is not exceeded, sleep & check again.
            logging.info(
                "Final loop #%s done - sleep for %s seconds and check again.",
                game.final_socials.retry_count,
                config["script"]["final_sleep_time"],
            )

            game.final_socials.retry_count += 1
            time.sleep(config["script"]["final_sleep_time"])

        else:
            logging.warning(
                "Game State %s is unknown - sleep for 5 seconds and check again.",
                game.game_state)
            time.sleep(config["script"]["live_sleep_time"])