def gameday_roster_update(game): """ Gets the gameday rosters from the live feed endpoint. This is needed because in some instances a player is not included on the /teams/{id}/roster page for some reason. Args: game: Current Game object Returns: None """ args = arguments.get_arguments() home_team = game.home_team away_team = game.away_team logging.info("Getting gameday rosters from Live Feed endpoint.") try: gameday_roster = livefeed.get_livefeed(game.game_id) all_players = gameday_roster.get("gameData").get("players") for player_id, player in all_players.items(): try: team = player.get("currentTeam").get("name") if team == home_team.team_name: home_team.gameday_roster[player_id] = player else: away_team.gameday_roster[player_id] = player except Exception as e: logging.error("%s doesn't have a team - skipping.", player["fullName"]) except Exception as e: logging.error("Unable to get all players.") logging.error(e)
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 spawn_another_process(): """Spawns a second process of the hockeygamebot (for split squad games).""" args = arguments.get_arguments() if args.date is not None: python_exec = sys.executable script_path = os.path.join(PROJECT_ROOT, "app.py") spawn_args = " ".join(sys.argv[1:]) full_exec = [ "{python} {script} --split {args}".format(python=python_exec, script=script_path, args=spawn_args) ] logging.debug("Spawning Process: %s", full_exec) Popen(full_exec, shell=True) else: python_exec = sys.executable script_path = os.path.join(PROJECT_ROOT, "app.py") spawn_args = " ".join(sys.argv[1:]) full_exec = [ "{python} {script} --split {args}".format(python=python_exec, script=script_path, args=spawn_args) ] logging.debug("Spawning Process: %s", full_exec) # Popen(["nohup python3 " + dirname + "/hockey_twitter_bot.py --split &"], shell=True) Popen(full_exec, shell=True)
def wrapper_social_timeout(*args, **kwargs): parsed_args = arguments.get_arguments() config = load_config() # If notweets is specified, always run the social methods if parsed_args.notweets: return func(*args, **kwargs) # Check for a force-send flag & if not False, force the message through if kwargs.get("force_send"): return func(*args, **kwargs) try: event = kwargs.get("event") event_time = dateutil.parser.parse(event.date_time) timeout = config["script"]["event_timeout"] utcnow = datetime.now(timezone.utc) time_since_event = (utcnow - event_time).total_seconds() if time_since_event < timeout: return func(*args, **kwargs) else: logging.info( "Event #%s (%s) occurred %s second(s) in the past - older than our social timeout.", event.event_idx, event.event_type, time_since_event, ) # return False return {"twitter": None, "discord": None, "slack": None} except Exception as e: logging.warning("Timeout function should contain an event key:value. %s", e) return func(*args, **kwargs)
def setup_logging(): """Configures application logging and prints the first three log lines.""" # pylint: disable=line-too-long # logger = logging.getLogger(__name__) # Create logs directory if not present if not os.path.exists(LOGS_PATH): os.makedirs(LOGS_PATH) args = arguments.get_arguments() # Reset root handler to default so BasicConfig is respected root = logging.getLogger() if root.handlers: for handler in root.handlers: root.removeHandler(handler) log_file_name = datetime.now().strftime( load_config()["script"]["log_file_name"] + "-%Y%m%d%H%M%S.log") log_file = os.path.join(LOGS_PATH, log_file_name) if args.console and args.debug: logging.basicConfig( level=logging.DEBUG, datefmt="%Y-%m-%d %H:%M:%S", format= "%(asctime)s - %(module)s.%(funcName)s (%(lineno)d) - %(levelname)s - %(message)s", ) elif args.console: logging.basicConfig( level=logging.INFO, datefmt="%Y-%m-%d %H:%M:%S", format= "%(asctime)s - %(module)s.%(funcName)s (%(lineno)d) - %(levelname)s - %(message)s", ) else: logging.basicConfig( filename=log_file, level=logging.INFO, datefmt="%Y-%m-%d %H:%M:%S", format= "%(asctime)s - %(module)s.%(funcName)s (%(lineno)d) - %(levelname)s - %(message)s", ) # Reset logging level (outside of Basic Config) logger = logging.getLogger() logger_level = logging.DEBUG if args.debug else logging.INFO logger.setLevel(logger_level)
def send_discord(msg, title=None, color=16777215, embed=None, media=None): """Sends a text-only Discord message. Args: msg: Message to send to the channel. media: Any media to be sent to the Webhook Returns: None """ args = arguments.get_arguments() discordenv = "debug" if args.debugsocial else "prod" discord_config = config.discord[discordenv] webhook_url = discord_config["webhook_url"] # Support multiple Discord Servers webhook_url = [webhook_url] if not isinstance(webhook_url, list) else webhook_url for url in webhook_url: if embed: requests.post(url, json=embed) continue linebreak_msg = f"▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬\n{msg}" payload = {"content": linebreak_msg} if title: embed_msg = {"embeds": [{"title": title, "description": msg, "color": color}]} response = requests.post(url, json=embed_msg) elif not title and not media: embed_msg = {"embeds": [{"title": "Game Bot Update", "description": msg, "color": color}]} response = requests.post(url, json=embed_msg) else: if isinstance(media, list): files = dict() for idx, image in enumerate(media): files_key = f"file{idx}" files[files_key] = open(image, "rb") else: files = {"file": open(media, "rb")} response = requests.post(url, files=files, data=payload) # If we get a non-OK code back from the Discord endpoint, log it. if not response.ok: logging.warning(response.json())
def is_game_today(team_id, date): """Queries the NHL Schedule API to determine if there is a game today. Args: team_id (int) - The unique identifier of the team (from get_team function). Returns: (bool, games_info) bool - True if game today, False if not. games_info (dict) - A dictionary from the Schedule API that describes game information. """ args = arguments.get_arguments() url = "/schedule?teamId={id}&expand=" "schedule.broadcasts,schedule.teams&date={date:%Y-%m-%d}".format( id=team_id, date=date ) response = api.nhl_api(url) if response: schedule = response.json() games_total = schedule["totalItems"] else: return False, None if games_total == 1: games_info = schedule["dates"][0]["games"][0] return True, games_info if games_total == 2: if args.split is False: logging.info("Split Squad - spawning a second process to pick up second game.") game_index = 0 process.spawn_another_process() time.sleep(10) else: game_index = 1 logging.info("Split Squad - this is the process to pick up second game (sleep 5 seconds).") time.sleep(5) games_info = schedule["dates"][0]["games"][game_index] return True, games_info date_string = date.date() if args.date else "today" logging.info("There are no games scheduled for %s, SAD!", date_string) return False, schedule
def get_number_games(season: str, team_id: int, game_type_code: str = "R") -> dict: """Queries the NHL Schedule API to how many games are in this season. This is particularly important in the 2020-2021 shortened season. Args: season (str) - The 8-digit season code (ex: 20202021). team_id (int) - The unique identifier of the team (from get_team function). Returns: num_games (int) - The number of games played in the regular season. """ args = arguments.get_arguments() endpoint = f"/schedule?teamId={team_id}&season={season}&gameType={game_type_code}" response = api.nhl_api(endpoint) if response: schedule = response.json() games_total = schedule["totalItems"] return games_total # If no valid response, just return default number of games (82) return 82
import os from hockeygamebot.helpers import arguments PROJECT_ROOT = os.path.dirname(os.path.abspath(__file__)) RESOURCES_PATH = os.path.join(PROJECT_ROOT, "resources") IMAGES_PATH = os.path.join(RESOURCES_PATH, "images") LOGS_PATH = os.path.abspath(os.path.join(PROJECT_ROOT, os.pardir, "logs")) TESTS_ROOT = os.path.abspath(os.path.join(PROJECT_ROOT, os.pardir, "tests")) TESTS_RESOURCES_PATH = os.path.join(TESTS_ROOT, "resources") # Define CONFIG_PATH separately in case of override args = arguments.get_arguments() URLS_PATH = os.path.join(PROJECT_ROOT, "config", "urls.yaml") CONFIG_FILE = "config.yaml" if not args.config else args.config CONFIG_PATH = os.path.join(PROJECT_ROOT, "config", CONFIG_FILE) VERSION = "2.0.1"
def send(msg, **kwargs): """The handler function that takes a message and a set of key-value arguments to be routed to social media functions. Args: message: The main message to be sent to all social media sites. # TODO: **kwargs Returns: None """ # If for some reason, message is None (or False), just exit the function. if not msg: return if GlobalGame.game and GlobalGame.game.other_team.team_name == "Washington Capitals": msg = msg.lower() args = arguments.get_arguments() social_config = config.socials # Initialize a return dictionary return_dict = {"twitter": None, "discord": None, "slack": None} if args.notweets: logging.info("[SOCIAL] %s", msg) if kwargs.get("media"): media = kwargs.get("media") if isinstance(media, list): for single_image in media: Image.open(single_image).show() else: Image.open(media).show() # kwargs.get("media").show() return return_dict if social_config["twitter"]: # tweet_id = twitter.send_tweet( # msg, media=kwargs.get("media"), reply=kwargs.get("reply"), hashtag=kwargs.get("hashtag") # ) team_hashtag = kwargs.get("team_hashtag") game_hashtag = kwargs.get("game_hashtag") tweet_id = twitter.send_tweet( msg, media=kwargs.get("media"), video=kwargs.get("video"), reply=kwargs.get("reply"), hashtags=kwargs.get("hashtags"), team_hashtag=kwargs.get("team_hashtag"), game_hashtag=kwargs.get("game_hashtag"), ) return_dict["twitter"] = tweet_id if social_config["discord"]: msg = kwargs.get("discord_msg", msg) discord.send_discord( msg, embed=kwargs.get("discord_embed"), title=kwargs.get("discord_title"), color=kwargs.get("discord_color"), media=kwargs.get("media"), ) if social_config["slack"]: pass return return_dict
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 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 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 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 dailyfaceoff_goalies(pref_team, other_team, pref_homeaway, game_date): """Scrapes Daily Faceoff for starting goalies for the night. Args: pref_team (Team): Preferred team object. other_team (Team): Other team object. pref_homeaway (str): Is preferred team home or away? game_date: Game date to add to DF URL Returns: Tuple: dictionary {goalie string, goalie confirmed} """ return_dict = {} urls = utils.load_urls() args = arguments.get_arguments() df_goalies_url = urls["endpoints"]["df_starting_goalies"] df_goalies_url = f"{df_goalies_url}{game_date}" df_linecombos_url = urls["endpoints"]["df_line_combos"] logging.info( "Trying to get starting goalie information via Daily Faceoff.") cache_headers = {"cache-control": "max-age=0"} resp = thirdparty_request(df_goalies_url, headers=cache_headers) # If we get a bad response from the function above, return False if resp is None: return False soup = bs4_parse(resp.content) if soup is None: return False logging.info( "Valid response received & souped - parse the Daily Faceoff page!") pref_team_name = pref_team.team_name games = soup.find_all("div", class_="starting-goalies-card stat-card") team_playing_today = any(pref_team_name in game.text for game in games) if games and team_playing_today: for game in games: teams = game.find("h4", class_="top-heading-heavy").text # If the preferred team is not in this matchup, skip this loop iteration if pref_team_name not in teams: continue teams_split = teams.split(" at ") home_team = teams_split[1] away_team = teams_split[0] goalies = game.find("div", class_="stat-card-main-contents") away_goalie_info = goalies.find("div", class_="away-goalie") away_goalie_name = away_goalie_info.find("h4").text.strip() away_goalie_confirm = away_goalie_info.find("h5", class_="news-strength") away_goalie_confirm = str(away_goalie_confirm.text.strip()) away_goalie_stats = away_goalie_info.find("p", class_="goalie-record") away_goalie_stats_str = " ".join( away_goalie_stats.text.strip().split()) away_goalie = dict() away_goalie["name"] = away_goalie_name away_goalie["confirm"] = away_goalie_confirm away_goalie["season"] = away_goalie_stats_str home_goalie_info = goalies.find("div", class_="home-goalie") home_goalie_name = home_goalie_info.find("h4").text.strip() home_goalie_confirm = home_goalie_info.find("h5", class_="news-strength") home_goalie_confirm = str(home_goalie_confirm.text.strip()) home_goalie_stats = home_goalie_info.find("p", class_="goalie-record") home_goalie_stats_str = " ".join( home_goalie_stats.text.strip().split()) home_goalie = dict() home_goalie["name"] = home_goalie_name home_goalie["confirm"] = home_goalie_confirm home_goalie["season"] = home_goalie_stats_str if pref_homeaway == "home": return_dict["pref"] = home_goalie return_dict["other"] = away_goalie return_dict["pref"]["homeaway"] = "home" return_dict["other"]["homeaway"] = "away" else: return_dict["pref"] = away_goalie return_dict["other"] = home_goalie return_dict["pref"]["homeaway"] = "away" return_dict["other"]["homeaway"] = "home" return_dict["home"] = home_goalie return_dict["away"] = away_goalie return return_dict # If there is any issue parsing the Daily Faceoff page, grab a goalie from each team else: logging.info( "There was an issue parsing the Daily Faceoff page, " "grabbing a goalie from each individual team's line combinations page." ) pref_team_encoded = pref_team.team_name.replace(" ", "-").replace( "é", "e").lower() other_team_encoded = other_team.team_name.replace(" ", "-").replace( "é", "e").lower() df_url_pref = df_linecombos_url.replace("TEAMNAME", pref_team_encoded) df_url_other = df_linecombos_url.replace("TEAMNAME", other_team_encoded) logging.info("Getting a fallback goalie for the preferred team.") resp = thirdparty_request(df_url_pref) soup = bs4_parse(resp.content) goalie_table = soup.find("table", attrs={ "summary": "Goalies" }).find("tbody").find_all("tr") pref_goalie_name = goalie_table[0].find_all("td")[0].find("a").text logging.info("Getting a fallback goalie for the other team.") resp = thirdparty_request(df_url_other) soup = bs4_parse(resp.content) goalie_table = soup.find("table", attrs={ "summary": "Goalies" }).find("tbody").find_all("tr") other_goalie_name = goalie_table[0].find_all("td")[0].find("a").text return_dict["pref_goalie"] = pref_goalie_name return_dict["pref_goalie_confirm"] = "Not Found" return_dict["other_goalie"] = other_goalie_name return_dict["other_goalie_confirm"] = "Not Found" return return_dict return True
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"])