def get_api(): """ Returns an Authorized session of the Tweepy API. Input: None Output: tweepy_session - authorized twitter session that can send a tweet. """ args = arguments.get_arguments() twitterenv = "debug" if args.debugsocial else "prod" twitter_config = utils.load_config()["twitter"][twitterenv] consumer_key = twitter_config["consumer_key"] consumer_secret = twitter_config["consumer_secret"] access_token = twitter_config["access_token"] access_secret = twitter_config["access_secret"] auth = tweepy.OAuthHandler(consumer_key, consumer_secret) auth.set_access_token(access_token, access_secret) tweepy_session = tweepy.API(auth) return tweepy_session
def get_twython_api(): """ Returns an Authorized session of the Twython API. This is only used until Tweepy supports Chunked Video uploads. Input: None Output: twython_session - authorized twitter session that can send a tweet. """ args = arguments.get_arguments() twitterenv = "debug" if args.debugsocial else "prod" twitter_config = utils.load_config()["twitter"][twitterenv] consumer_key = twitter_config["consumer_key"] consumer_secret = twitter_config["consumer_secret"] access_token = twitter_config["access_token"] access_secret = twitter_config["access_secret"] twython_session = Twython(consumer_key, consumer_secret, access_token, access_secret) return twython_session
def test_schedule_one_game(): """Performs a test to verify a schedule with 1 game is parsed correctly.""" config = utils.load_config() date = utils.date_parser("2019-10-04") resp_file = os.path.join(TESTS_RESOURCES_PATH, "schedule_one_game.json") with open(resp_file, encoding="utf-8") as json_file: json_response = json.load(json_file) url = "https://statsapi.web.nhl.com/api/v1/schedule" responses.add( responses.GET, url, json=json_response, match_querystring=False, content_type="application/json", ) game_today, game_info = schedule.is_game_today(1, date) assert game_today assert game_info == json_response["dates"][0]["games"][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
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
def live_loop(livefeed: dict, game: Game): """The master live-game loop. All logic spawns from here. Args: livefeed: Live Feed API response game: Game Object Returns: None """ config = utils.load_config() # Load all plays, the next event ID & new plays into lists # current_event_idx = livefeed.get("liveData").get("currentPlay").get("about").get("eventIdx") all_plays = livefeed.get("liveData").get("plays").get("allPlays") # Subset all_plays by last_event_idx to shorten the loop next_event_idx = game.last_event_idx + 1 new_plays_list = all_plays[next_event_idx:] if not new_plays_list: new_plays = bool(new_plays_list) logging.info( "No new plays detected. This game event loop will catch any missed events & " "and also check for any scoring changes on existing goals.") elif len(new_plays_list) < 10: new_plays = bool(new_plays_list) new_plays_shortlist = list() for play in new_plays_list: event_type = play["result"]["eventTypeId"] event_idx = play["about"]["eventIdx"] event_kv = f"{event_idx}: {event_type}" new_plays_shortlist.append(event_kv) logging.info( "%s new event(s) detected - looping through them now: %s", len(new_plays_list), new_plays_shortlist, ) else: new_plays = bool(new_plays_list) logging.info("%s new event(s) detected - looping through them now.", len(new_plays_list)) # We pass in the entire all_plays list into our event_factory in case we missed an event # it will be created because it doesn't exist in the Cache. for play in all_plays: gameevent.event_factory(game=game, play=play, livefeed=livefeed, new_plays=new_plays) # Check if any goals were removed try: for goal in game.all_goals[:]: was_goal_removed = goal.was_goal_removed(all_plays) if was_goal_removed: pref_team = game.preferred_team.team_name goals_list = game.pref_goals if goal.event_team == pref_team else game.other_goals # Remove the Goal from all lists, caches & then finallydelete the object game.all_goals.remove(goal) goals_list.remove(goal) goal.cache.remove(goal) del goal except Exception as e: logging.error( "Encounted an exception trying to detect if a goal is no longer in the livefeed." ) logging.error(e)
def pregame_image(game: Game): """Generates the pre-game image that is sent to social media platforms at the first run of the hockeygamebot script per day. Args: game: Current Game object Returns: bg: Finished Image instance """ # Fonts used within the pregame image FONT_TITLE = ImageFont.truetype(FontFiles.BITTER_BOLD, FontSizes.TITLE) FONT_RECORD = ImageFont.truetype(FontFiles.BITTER_BOLD, FontSizes.RECORD) FONT_STREAK = ImageFont.truetype(FontFiles.BITTER_BOLD, FontSizes.STREAK) FONT_DETAIL_LARGE = ImageFont.truetype(FontFiles.BITTER_BOLD, FontSizes.DETAIL_LARGE) FONT_DETAIL_SMALL = ImageFont.truetype(FontFiles.BITTER_BOLD, FontSizes.DETAIL_SMALL) FONT_GAMENUMBER = ImageFont.truetype(FontFiles.BITTER_BOLD, FontSizes.GAMENUMBER * 3) FONT_BOT_TAG = ImageFont.truetype(FontFiles.BITTER_BOLD, FontSizes.GAMENUMBER * 2) # Pre-game specific constant values (incl coordinates) HEADER_TEXT = "PRE-GAME MATCHUP" LOGO_Y = 150 COORDS_HOME_X = 245 COORDS_AWAY_X = 650 COORDS_HOME_LOGO = (COORDS_HOME_X, LOGO_Y) COORDS_AWAY_LOGO = (COORDS_AWAY_X, LOGO_Y) COORDS_GAME_NUM = (-90, 80) COORDS_BOT_TAG = (910, 330) TEAM_RECORD_Y = LOGO_Y + 200 TEAM_STREAK_Y = TEAM_RECORD_Y + FontSizes.RECORD + 10 # Generate records, venue & other strings # If this is the first team, game then no streak home_pts = game.home_team.points home_record_str = f"{home_pts} PTS • {game.home_team.current_record}" home_streak_last10 = ( f"{game.home_team.streak} • LAST 10: {game.home_team.last_ten}" if game.home_team.games > 0 else "") away_pts = game.away_team.points away_record_str = f"{away_pts} PTS • {game.away_team.current_record}" away_streak_last10 = ( f"{game.away_team.streak} • LAST 10: {game.away_team.last_ten}" if game.away_team.games > 0 else "") num_games = schedule.get_number_games(season=game.season, team_id=game.preferred_team.team_id, game_type_code=game.game_type) text_gamenumber = ("PRESEASON" if game.game_type == "PR" else f"{game.preferred_team.games + 1} OF {num_games}") text_datetime = f"{game.game_date_short} • {game.game_time_local}" text_hashtags = ( f"{utils.team_hashtag(game.preferred_team.team_name, game.game_type)} • {game.game_hashtag}" ) bg = Image.open(Backgrounds.PREGAME) bg_w, bg_h = bg.size away_team = game.away_team.team_name.replace(" ", "") home_team = game.home_team.team_name.replace(" ", "") away_logo = Image.open( os.path.join(PROJECT_ROOT, f"resources/logos/{away_team}.png")) home_logo = Image.open( os.path.join(PROJECT_ROOT, f"resources/logos/{home_team}.png")) # Paste the home / away logos with the mask the same as the image bg.paste(away_logo, COORDS_AWAY_LOGO, away_logo) bg.paste(home_logo, COORDS_HOME_LOGO, home_logo) # Generates a 'draw' object that we use to draw on top of the image draw = ImageDraw.Draw(bg) # Draw text items on the background now # fmt: off center_text(draw=draw, left=0, top=0, width=bg_w, text=HEADER_TEXT, color=Colors.WHITE, font=FONT_TITLE) center_text(draw=draw, left=COORDS_HOME_X, top=TEAM_RECORD_Y, width=300, text=home_record_str, color=Colors.WHITE, font=FONT_RECORD) center_text(draw=draw, left=COORDS_HOME_X, top=TEAM_STREAK_Y, width=300, text=home_streak_last10, color=Colors.WHITE, font=FONT_STREAK) center_text(draw=draw, left=COORDS_AWAY_X, top=TEAM_RECORD_Y, width=300, text=away_record_str, color=Colors.WHITE, font=FONT_RECORD) center_text(draw=draw, left=COORDS_AWAY_X, top=TEAM_STREAK_Y, width=300, text=away_streak_last10, color=Colors.WHITE, font=FONT_STREAK) center_text( draw=draw, left=0, top=480, width=bg_w, text=text_datetime, color=Colors.WHITE, font=FONT_DETAIL_LARGE, ) center_text( draw=draw, left=0, top=540, width=bg_w, text=game.venue.upper(), color=Colors.WHITE, font=FONT_DETAIL_LARGE, ) center_text( draw=draw, left=0, top=600, width=bg_w, text=text_hashtags, color=Colors.WHITE, font=FONT_DETAIL_SMALL, ) # fmt: on # Create a new image to put the game number & cleanly rotate it txt = Image.new("L", (900, 900)) d = ImageDraw.Draw(txt) center_text( draw=d, left=0, top=0, width=900, text=text_gamenumber, color=255, font=FONT_GAMENUMBER, ) w = txt.rotate(315, expand=True, resample=Image.BICUBIC) w_resize = w.resize((300, 300), Image.ANTIALIAS) bg.paste(w_resize, COORDS_GAME_NUM, w_resize) # Create a new image to put the game bot handle & cleanly rotate it args = arguments.get_arguments() twitterenv = "debug" if args.debugsocial else "prod" twitter_config = utils.load_config()["twitter"][twitterenv] twitter_handle = twitter_config["handle"] txt = Image.new("L", (900, 900)) d = ImageDraw.Draw(txt) center_text( draw=d, left=0, top=0, width=900, text=f"@{twitter_handle}", color=255, font=FONT_BOT_TAG, ) w = txt.rotate(315, expand=True, resample=Image.BICUBIC) w_resize = w.resize((300, 300), Image.ANTIALIAS) bg.paste(w_resize, COORDS_BOT_TAG, w_resize) return bg
def send_tweet(tweet_text, video=None, media=None, reply=None, hashtags=None, team_hashtag=None, game_hashtag=None): """Generic tweet function that uses logic to call other specific functions. Args: tweet_text: The text to send as a tweet (may contain URL at end to qote tweet). media: Any media we want to upload to Twitter (images, videos, GIFs) reply: Are we replying to a specific tweet (for threading purposes) Returns: last_tweet - A link to the last tweet sent (or search result if duplicate) If duplicate cannot be found, returns base URL (also raises error) """ logging.info("[TWITTER] %s (Media: %s, Reply: %s)", tweet_text, media, reply) args = arguments.get_arguments() twitterenv = "debug" if args.debugsocial else "prod" twitter_config = utils.load_config()["twitter"][twitterenv] twitter_handle = twitter_config["handle"] if args.notweets: logging.info("%s", tweet_text) return "https://twitter.com/{handle}/status".format( handle=twitter_handle) # Get the API session & send a tweet depending on the parameters sent api = get_api() # Add any hashtags that need to be added # Start with team hashtag (most required) # if hashtags: # tweet_text = f'{tweet_text}\n\n{hashtags}' if game_hashtag: tweet_text = f"{tweet_text}\n\n{Hashtag.game_hashtag}" # Only use this function for upload highlight videos. # Single use case & we know the exact path that triggers this if video is not None: try: logging.info( "Video was detected - using chunked video upload to download & send." ) twython_api = get_twython_api() logging.info( "Uploading the video file using chunked upload to Twitter.") video_file = open(video, "rb") upload_response = twython_api.upload_video( media=video_file, media_type="video/mp4", media_category="tweet_video", check_progress=True) processing_info = upload_response["processing_info"] state = processing_info["state"] wait = processing_info.get("check_after_secs", 1) if state == "pending" or state == "in_progress": logging.info(f"Upload not done - waiting %s seconds.", wait) time.sleep(wait) logging.info("Upload completed - sending tweet now.") # If we have gotten this far, remove the URL from the tweet text. tweet_text = tweet_text.split("\n")[0] # tweet_text = f"@{twitter_handle} {tweet_text}" status = twython_api.update_status( status=tweet_text, in_reply_to_status_id=reply, auto_populate_reply_metadata=True, media_ids=[upload_response["media_id"]], ) return status.get("id_str") except Exception as e: logging.error( "There was an error uploading and sending the embedded video - send with a link." ) logging.error(e) try: if not reply and not media: status = api.update_status(status=tweet_text) elif not reply and media: if isinstance(media, list): media_ids = [ api.media_upload(i).media_id_string for i in media ] status = api.update_status(status=tweet_text, media_ids=media_ids) else: status = api.update_with_media(status=tweet_text, filename=media) elif reply and not media: # tweet_text = f"@{twitter_handle} {tweet_text}" status = api.update_status(status=tweet_text, in_reply_to_status_id=reply, auto_populate_reply_metadata=True) elif reply and media: # tweet_text = f"@{twitter_handle} {tweet_text}" if isinstance(media, list): media_ids = [ api.media_upload(i).media_id_string for i in media ] status = api.update_status( status=tweet_text, media_ids=media_ids, in_reply_to_status_id=reply, auto_populate_reply_metadata=True, ) else: status = api.update_with_media(status=tweet_text, filename=media, in_reply_to_status_id=reply) return status.id except Exception as e: logging.error("Failed to send tweet : %s", tweet_text) logging.error(e) return None
def test_load_config(): assert utils.load_config( )["endpoints"]["nhl_base"] == "https://statsapi.web.nhl.com"
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
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"])