def _MatchWinner(self, match): winner = 'TIE' if match.get('isComplete') else None if util_lib.Access(match, 'top.winner'): winner = match['top']['teamID'] elif util_lib.Access(match, 'bottom.winner'): winner = match['bottom']['teamID'] return winner
def _LoadStage(self, stage_id): """Loads a single stage (bracket).""" with self._lock: stage = self._proxy.FetchJson( '/'.join([self._BASE_URL, 'stages', stage_id]), force_lookup=True) bracket = esports_pb2.Bracket( bracket_id=self._MakeBracketId(stage_id), name=stage['name'], league_id=self.league_id, is_playoffs='playoff' in stage['name'].lower()) self._brackets[bracket.bracket_id] = bracket matches = self._proxy.FetchJson( '/'.join([self._BASE_URL, 'stages', stage_id, 'matches']), force_lookup=True) # Battlefy doesn't provide actual match start times. We assume that # matches are only provided for the current week. And then replace with # completed time if it exists. default_match_time = util_lib.ArrowTime( weekday=5, hour=12, tz='America/Los_Angeles') for match in matches: match_time = default_match_time if 'completedAt' in match: match_time = arrow.get(match['completedAt']) m = bracket.schedule.add( match_id=match['_id'], bracket_id=bracket.bracket_id, red=util_lib.Access(match, 'top.teamID', 'BYE'), blue=util_lib.Access(match, 'bottom.teamID', 'BYE'), timestamp=match_time.timestamp, winner=self._MatchWinner(match)) self._matches[m.match_id] = m stats = None if self.stats_enabled and m.winner: stats = self._proxy.FetchJson( '/'.join([self._BASE_URL, 'matches', m.match_id]), params={'extend[stats]': 'true'}, force_lookup=True) for stat_idx, game_id in enumerate(match.get('appliedRiotGameIDs', [])): game = m.games.add( game_id=game_id, realm=self._realm, hash=match['lolHookUrl']) game_stats = util_lib.Access(stats, '0.stats.%d.stats' % stat_idx) if game_stats: self._ParseGameStats(game, game_stats) self._UpdateStandings(bracket)
def _LoadSchedule(self, bracket): """Loads schedule for given bracket. This is a lie. It loads the schedule for the bracket's tournament and pretends like all matches belong to this bracket since Rito no longer provides indication which bracket a match belongs to. Args: bracket: The bracket for which the schedule should be loaded. Returns: List of matches that were updated i.e., completed since the last time that _LoadSchedule was called. """ updated_matches = [] schedule_data = self._FetchEsportsData( 'getSchedule', {'leagueId': bracket.league_id}) schedule_data = util_lib.Access(schedule_data, 'schedule.events') if not schedule_data: logging.warning('Failed to load schedule for %s', bracket.name) return [] for event in schedule_data: winner = 'TIE' if event['state'] == 'completed' else None if util_lib.Access(event, 'match.teams.0.result.outcome') == 'win': winner = self._FindTeamId(event['match']['teams'][0]['code']) elif util_lib.Access(event, 'match.teams.1.result.outcome') == 'win': winner = self._FindTeamId(event['match']['teams'][1]['code']) match_id = event['match']['id'] if match_id in self._matches: match = self._matches[match_id] if winner and not match.winner: match.winner = winner updated_matches.append(match) else: match = bracket.schedule.add( match_id=event['match']['id'], bracket_id=bracket.bracket_id, red=self._FindTeamId(event['match']['teams'][0]['code']), blue=self._FindTeamId(event['match']['teams'][1]['code']), timestamp=arrow.get(event['startTime']).timestamp, winner=winner) self._matches[match.match_id] = match return updated_matches
def _LoadStandings(self, tournament_id): """(Re)loads standings for a given tournament_id. Note: If no bracket exists for the given `stage` of the tournament, this will create one, otherwise it will simply clear the existing standings for the bracket and update in place. Args: tournament_id: ID of tournament. """ standings_data = self._FetchEsportsData( 'getStandings', {'tournamentId': tournament_id}) if not standings_data or 'standings' not in standings_data: logging.error('Failed to get standings.') return for stage in standings_data['standings'][0]['stages']: for section in stage['sections']: # The section name is more of a slug and not a pretty name. Lolesports # uses JavaScript and a locale file to convert to human name, but we do # the best we can given that it's non-trivial (and even more brittle) to # try to parse the JS to figure out exactly what to display. section_name = section['name'].replace('_', ' ').title() full_name = '%s: %s' % (stage['name'], section_name) bracket_id = self._MakeBracketId(full_name) if bracket_id in self._brackets: b = self._brackets[bracket_id] del b.standings[:] else: b = esports_pb2.Bracket( bracket_id=bracket_id, name=full_name, league_id=self._league_id, is_playoffs=stage['slug'] == 'playoffs') self._brackets[b.bracket_id] = b rankings = util_lib.Access(section, 'rankings') if not rankings: continue for group in rankings: for team in group['teams']: if team['id'] not in self._teams: self._LoadTeam(team['slug']) if team['id'] not in self._teams: # New for 2020, we have TBD teams which don't exist. continue b.standings.add( rank=group['ordinal'], wins=team['record']['wins'], losses=team['record']['losses'], team=self._teams[team['id']])
def _LoadData(self): """Helper method to load data.""" updated_matches = [] tournament_data = self._FetchEsportsData('getTournamentsForLeague', {'leagueId': self._league_id}) if not tournament_data or 'leagues' not in tournament_data: logging.error('Could not get tournaments for league: %s', self._region) return [] for tournament in self._FindActiveTournaments( # We requested a single league so we try to take the first. util_lib.Access(tournament_data, 'leagues.0.tournaments', [])): self._LoadStandings(tournament['id']) updated_matches.extend( self._LoadSchedule(list(self._brackets.values())[0])) return updated_matches
def GetAQI(self, location: str): """Get air quality index for the location. Args: location: Location in human readable form. Returns: AQI response from airnow. """ location = self._LocationToGPS(location) if not location: return None zip_code = util_lib.Access(location, 'address_components.zip') if not zip_code: return None return self._CallAQI(zip_code)
def _LoadTeam(self, slug): """Loads information about a team from Rito.""" team_data = self._FetchEsportsData('getTeams', {'id': slug}) team_data = util_lib.Access(team_data, 'teams.0') if not team_data: logging.warning('Failed to load team: %s', slug) return team = esports_pb2.Team( team_id=team_data['id'], name=team_data['name'], abbreviation=team_data['code']) observed_positions = set() for player_data in team_data['players']: position = player_data['role'].title() team.players.add( summoner_name=player_data['summonerName'], position=position, is_substitute=position in observed_positions, team_id=team.team_id) observed_positions.add(position) self._teams[team.team_id] = team
def _UpdateSchedule(self, schedule, bracket): """Update existing matches if they are now wonnered.""" updated_matches = [] match_count = 0 with self._lock: for week in schedule: for match in week['matches']: match_count += 1 match_id = '%s-%s-%s' % (self.league_id, bracket.bracket_id, match_count) old_match = self._matches.get(match_id) if not old_match or old_match.winner: continue for team in [match['team1'], match['team2']]: team_id = util_lib.Access(team, 'ref.id') if not team_id or not team['outcome']: continue if team['outcome'] == 'VICTORY': old_match.winner = team_id elif team['outcome'] == 'TIE': old_match.winner = 'TIE' if old_match.winner: updated_matches.append(old_match) return updated_matches
def GetHeadlines(self, query, max_results=5): endpoint_url = self._params.base_url + 'search/v2/articlesearch.json' r = self._proxy.FetchJson( endpoint_url, params={ 'api-key': self._params.api_key, 'q': query, 'sort': 'relevance', 'fl': 'headline,web_url,source,pub_date', 'begin_date': arrow.now().shift(years=-1).strftime('%Y%m%d') }) articles = [] docs = util_lib.Access(r, 'response.docs') for doc in docs: if 'source' not in doc: continue articles.append({ 'title': doc['headline'].get('main'), 'url': doc['web_url'], 'source': doc['source'], 'pub_date': doc['pub_date'] }) return articles[:max_results]
def _ParseSchedule(self, schedule, bracket): """Parse schedule into bracket.""" match_count = 0 standings = {} with self._lock: for week in schedule: for match in week['matches']: match_count += 1 m = bracket.schedule.add( match_id='%s-%s-%s' % (self.league_id, bracket.bracket_id, match_count), bracket_id=bracket.bracket_id, blue=util_lib.Access(match, 'team1.ref.id', 'TBD'), red=util_lib.Access(match, 'team2.ref.id', 'TBD'), timestamp=match['timestampSec']) self._matches[m.match_id] = m for game in match['games']: game_proto = m.games.add( game_id=str(util_lib.Access(game, 'ref.gameId')), realm=self._realm, hash=util_lib.Access(game, 'ref.tournamentCode')) if self.stats_enabled and util_lib.Access(game, 'winner'): response = self._FetchJson( 'game', [game_proto.game_id, game_proto.hash], use_year=False, use_storage=True) if response: json_format.ParseDict( response, game_proto.stats, ignore_unknown_fields=True) for team in [match['team1'], match['team2']]: team_id = util_lib.Access(team, 'ref.id') if not team_id: continue if team_id not in self._teams: self._teams[team_id] = esports_pb2.Team( team_id=team_id, abbreviation=team_id, name=team['ref']['displayName'], league_id=self.league_id) if team_id not in standings: standings[team_id] = esports_pb2.TeamStanding( team=self._teams[team_id]) if not team['outcome']: continue if team['outcome'] == 'VICTORY': m.winner = team_id standings[team_id].wins += 1 standings[team_id].points += 3 elif team['outcome'] == 'TIE': m.winner = 'TIE' standings[team_id].ties += 1 standings[team_id].points += 1 else: standings[team_id].losses += 1 standings = sorted( standings.values(), key=lambda x: x.points, reverse=True) rank = 1 cur_points = -1 for i, team in enumerate(standings): if team.points != cur_points: rank = i + 1 cur_points = team.points team.rank = rank bracket.standings.extend(standings)
def _ScrapePickBanData(self, tournament_id, region, match): """For each game in match, fetches and tallies pick/ban data from Riot.""" game_id_mappings = self.FetchEsportsData( 'matchDetails', (tournament_id, match['id']), use_storage=True).get('gameIdMappings') if not game_id_mappings: # We won't be able to find the gameHash, so just log logging.info('Not retrieving game stats for match %s', match['id']) return # Skip games that weren't actually played for guid, game in (m for m in match['games'].items() if 'gameId' in m[1]): self.num_games[region] += 1 game_hash = [ i['gameHash'] for i in game_id_mappings if i['id'] == guid ] if len(game_hash) != 1: logging.warning( 'Couldn\'t find exactly one hash for match/game %s/%s:', match['id'], guid) logging.warning('\tActual: %s', game_hash) continue game_hash = game_hash[0] game_stats = self._proxy.FetchJson( 'https://acs.leagueoflegends.com/v1/stats/game/%s/%s?gameHash=%s' % (game['gameRealm'], game['gameId'], game_hash), use_storage=True) winning_team = None participant_to_player = { p['participantId']: util_lib.Access(p, 'player.summonerName') for p in game_stats.get('participantIdentities', []) } # Collect ban data for team in game_stats.get('teams', []): if team.get('win') == 'Win': winning_team = team.get('teamId') for ban in team.get('bans', []): self.champ_to_stats[ban['championId']][region]['bans'] += 1 # Collect pick and W/L data for player in game_stats.get('participants', []): champ_id = player['championId'] champ_name = self._game.GetChampNameFromId(champ_id) # We need to use separate player_name and player_key here because Rito # doesn't like to keep things like capitalization consistent with player # names so they aren't useful as keys, but we still want to display the # "canonical" player name back to the user eventually, so we save it as # a value in player_to_stats instead. player_name = participant_to_player[player['participantId']] player_key = util_lib.CanonicalizeName(player_name) self.player_to_stats[player_key]['name'] = player_name if player.get('teamId') == winning_team: self.champ_to_stats[champ_id][region]['wins'] += 1 self.player_to_stats[player_key][champ_name]['wins'] += 1 self.player_to_stats[player_key]['num_games']['wins'] += 1 self.champ_to_stats[champ_id][region]['picks'] += 1 self.player_to_stats[player_key][champ_name]['picks'] += 1 self.player_to_stats[player_key]['num_games']['picks'] += 1
def GetRoster(self, team, region, include_subs): """Gets roster for specified team.""" roster = [] query = util_lib.CanonicalizeName(team) slug_to_role = { 'toplane': 'Top', 'jungle': 'Jungle', 'midlane': 'Mid', 'adcarry': 'ADC', 'support': 'Support' } role_ordering = { 'toplane': 0, 'jungle': 1, 'midlane': 2, 'adcarry': 3, 'support': 4 } query_params = {} if region: query_params['region'] = region if include_subs: query_params['full'] = True roster_data = self._proxy.FetchJson( ESPORTS_API_BASE_URL + 'roster/%s' % query, query_params) if 'error' in roster_data: if roster_data.get('ready'): logging.warning('Roster service returned an error: %s', roster_data['error']) return roster return ['Roster data is still loading, try again soon.'] elif not roster_data: logging.error('Could not retrieve roster.') return roster roster.append('%s %sRoster:' % (util_lib.Access(roster_data, 'team.0.name', query), 'Full ' if include_subs else '')) # A map of actual player names to what their name should be displayed as. # You know, for memes. player_name_substitutions = { 'Revolta': 'Travolta', 'MikeYeung': 'Mike "Mike Yeung" Yeung' } players = [] sorted_roster_data = sorted(roster_data.items(), key=lambda x: role_ordering.get(x[0], -1)) for position, player_list in sorted_roster_data: if position not in slug_to_role: continue # Make sure we add the starter before any possible subs for player in sorted(player_list, key=lambda x: not x['starter']): real_name = player['name'] display_name = player_name_substitutions.get( real_name, real_name) player_pair = [display_name, slug_to_role[position]] if not player['starter']: player_pair[1] += ' (Sub)' players.append(player_pair) roster.extend([' - '.join(p) for p in players]) return roster