import hashlib import json from pathlib import Path from datetime import datetime, timedelta, timezone import requests import re import asyncio import nextcord import itertools from nextcord.ext import commands, application_checks, tasks from nextcord import TextInputStyle, IntegrationType teams_dict = {} teams_list = [] def get_all_teams(): data = requests.get("https://freecashe.ws/api/teams").json() teams_dict.clear() teams_list.clear() for index in data["items"]: teams_list.append(f"{index["location"]} {index["name"]}") teams_dict.update({f"{index["location"]} {index["name"]}": index["team_id"]}) print(teams_dict) print(teams_list) #all of this code is by evilscientist3, full credit to them HTTP_CACHE_DIR = Path("http_cache") statdict = { "BA":(0.4, 0.35, 0.3, 0.25, 0.2, 0.15), "OBP":(0.475, 0.425, 0.375, 0.325, 0.275, 0.225), "SLG":(0.55, 0.52, 0.46, 0.4, 0.36, 0.33), "OPS":(1.0, 0.9, 0.8, 0.7, 0.6, 0.5), "ERA":(2.75, 3.25, 3.75, 4.75, 5.5, 6.5), "WHIP":(1, 1.1, 1.25, 1.4, 1.5, 1.6) } # whether ascending is GOOD (True) or BAD (False) for figure of merit NGUdict = { "BA":True, "OBP":True, "SLG":True, "OPS":True, "ERA":False, "WHIP":False } # sig figs for each stat sfdict = { "BA":3, "OBP":3, "SLG":3, "OPS":3, "ERA":2, "WHIP":2 } def eval_stat(stat, val): # modifier for if descending is better. in this case logic is reversed; most easily accounted for by multiplying bounds and values by -1 # why bother keeping descending values' bounds positive, you ask? for human readability and ease of modification, I say mod = 1 if NGUdict[stat] else -1 # set of ANSI colour codes. best to worst, one more than bounds colours = [32, 72, 36, 30, 33, 31, 31] # multiply bounds to account for descending bounds = [mod*i for i in statdict[stat]] mval = mod*val # flag for colour being set, as an "else" condition to the entire loop cset = 0 # main loop - checks quality in descending order for i, j in enumerate(bounds): if mval > j: colour = colours[i] cset = 1 break # check if value is below all bounds if cset == 0: colour = colours[-1] # return stat string in correct colour return f"\033[38;5;{colour}m{val:.{sfdict[stat]}f}\033[0m" def stable_str_hash(in_val: str) -> str: return hex(int(hashlib.md5(in_val.encode("utf-8")).hexdigest(), 16))[2:] def get_json(url: str) -> dict: HTTP_CACHE_DIR.mkdir(exist_ok=True) cache = {} cache_file_path = HTTP_CACHE_DIR / f"{stable_str_hash(url)}.json" try: with open(cache_file_path) as cache_file: cache = json.load(cache_file) except FileNotFoundError: pass # Return from cache if the cache entry is less than 5 minutes old now = datetime.now(timezone.utc) if ( url in cache and "__archived_at" in cache[url] and cache[url]["__archived_at"] > (now - timedelta(minutes=5)).isoformat() ): return cache[url] data = requests.get(url).json() cache[url] = data cache[url]["__archived_at"] = now.isoformat() with open(cache_file_path, "w") as cache_file: json.dump(cache, cache_file) return data def teamstats(MY_TEAM_ID): finalstr = "" team_obj = get_json(f"https://mmolb.com/api/team/{MY_TEAM_ID}") for player in team_obj["Players"]: if player['FirstName'] == 'Empty' and player["LastName"] == "Slot": continue player_obj = get_json(f"https://mmolb.com/api/player/{player['PlayerID']}") try: # I'm pretty sure IDs are lexicographically ordered, so we want the # maximum value to get stats for the latest season stats_obj = player_obj["Stats"][max(player_obj["Stats"].keys())] except ValueError: finalstr += (f"No stats for {player["FirstName"]} {player["LastName"]} \n") continue singles = stats_obj.get("singles", 0) doubles = stats_obj.get("doubles", 0) triples = stats_obj.get("triples", 0) home_runs = stats_obj.get("home_runs", 0) hits = singles + doubles + triples + home_runs bb = stats_obj.get("walked", 0) hbp = stats_obj.get("hit_by_pitch", 0) earned_runs = stats_obj.get("earned_runs", 0) hits_allowed = stats_obj.get("hits_allowed", 0) walks = stats_obj.get("walks", 0) try: ab = stats_obj["at_bats"] except KeyError: ba_str = None else: ba = hits / ab ba_str = f"BA: {eval_stat('BA', ba)} ({ab} AB)" try: pa = stats_obj["plate_appearances"] ab = stats_obj["at_bats"] except KeyError: ops_str = None else: obp = (hits + bb + hbp) / pa slg = (singles + 2 * doubles + 3 * triples + 4 * home_runs) / ab ops = obp + slg pa_str = dot_format(pa) ops_str = f"OBP: {eval_stat('OBP', obp)}, SLG: {eval_stat('SLG', slg)}, OPS: {eval_stat('OPS', ops)} ({pa_str} PA)" try: ip = stats_obj["outs"] / 3 except KeyError: era_str = None else: era = 9 * earned_runs / ip whip = (walks + hits_allowed) / ip ip_str = dot_format(ip) era_str = f"ERA {eval_stat('ERA', era)} WHIP {eval_stat('WHIP', whip)} ({ip_str} IP)" stats_str = ", ".join(s for s in [ba_str, ops_str, era_str] if s is not None) if stats_str: finalstr += (f"{player["Position"]} {player["FirstName"]} {player["LastName"]} ") finalstr += "" * (30 - (len(player["Position"]) + len(player["FirstName"]) + len(player["LastName"]))) finalstr += f"{stats_str}\n" return (finalstr if finalstr != "" else "There are no players on this team") def dot_format(in_val: float) -> str: ip_whole = int(in_val) ip_remainder = int((in_val - ip_whole) / 3 * 10) if ip_remainder == 0: ip_str = f"{ip_whole}" else: ip_str = f"{ip_whole}.{ip_remainder}" return ip_str #------- class TeamView(nextcord.ui.View): def __init__(self): super().__init__(timeout=None) @nextcord.ui.button( label="Players", style=nextcord.ButtonStyle.green, custom_id="team:players" ) async def getplayersbutton(self, button: nextcord.ui.Button, interaction: nextcord.Interaction): await interaction.response.defer(ephemeral=True) loop = asyncio.get_event_loop() ogmsg = interaction.message.embeds embed = ogmsg[0] stats = await loop.run_in_executor(None, teamstats,embed.footer.text) splistats = [stats[i:i+1900] for i in range(0, len(stats), 1900)] #TODO make better as in don't do cutoff for i in splistats: await interaction.followup.send(f"```ansi\n{i}```",ephemeral=True) @nextcord.ui.button( label="Game History", style=nextcord.ButtonStyle.green, custom_id="team:gamehistory" ) async def gamehistorybutton(self, button: nextcord.ui.Button, interaction: nextcord.Interaction): await interaction.response.defer() ogmsg = interaction.message.embeds embed = ogmsg[0] teamid = embed.footer.text data = requests.get(f"https://mmolb.com/api/team/{teamid}").json() history = requests.get(f"https://freecashe.ws/api/games?team={teamid}&season=2").json()["items"] color = tuple(int(data["Color"][i:i+2], 16) for i in (0, 2, 4)) embed = nextcord.Embed(title=f"Last ten games for the {data["Location"]} {data["Name"]} {data["Emoji"]}", colour = nextcord.Color.from_rgb(color[0], color[1], color[2])) embed.set_footer(text=teamid) for index in reversed(list(itertools.islice(history, (len(history)-10 if len(history)-10 > 0 else 0) , len(history)))): if index["away_team_id"] != teamid: awayteamid = index["away_team_id"] otherscore = index["last_update"]["away_score"] ourscore = index["last_update"]["home_score"] else: awayteamid = index["home_team_id"] otherscore = index["last_update"]["home_score"] ourscore = index["last_update"]["away_score"] tempdata = requests.get(f"https://mmolb.com/api/team/{awayteamid}").json() embed.add_field(name=f"vs. {tempdata["Location"]} {tempdata["Name"]} {tempdata["Emoji"]} ({ourscore} - {otherscore})", value=f"[watch]()", inline=False) await interaction.followup.send(embed=embed,ephemeral=True) class team(commands.Cog): def __init__(self, bot: commands.Bot): self.bot = bot self.updateallteams.start() def cog_unload(self): self.updateallteams.cancel() @nextcord.slash_command( name="team", description="Get information about a team", integration_types=[ IntegrationType.user_install, IntegrationType.guild_install, ], contexts=[ nextcord.InteractionContextType.guild, nextcord.InteractionContextType.bot_dm, nextcord.InteractionContextType.private_channel, ], force_global=True, ) async def greaterteam(self, interaction: nextcord.Interaction, team: str = nextcord.SlashOption( name = "team", description = "The team", )): if team not in teams_dict: await interaction.response.send_message("Invalid Team!", ephemeral=True) return await interaction.response.defer() teamid = teams_dict[team] data = requests.get(f"https://mmolb.com/api/team/{teamid}").json() rankdata = requests.get(f"https://mmolb.com/api/league-top-teams/{data["League"]}").json()["teams"] ranking = 0 for i in rankdata: ranking += 1 if i["_id"] == teamid: break color = tuple(int(data["Color"][i:i+2], 16) for i in (0, 2, 4)) embed = nextcord.Embed(title=f"{data["Location"]} {data["Name"]} {data["Emoji"]}", description=f"{data["Motto"]}", colour = nextcord.Color.from_rgb(color[0], color[1], color[2])) embed.add_field(name="League", value=f"{dict((v,k) for k,v in self.bot.leagues_dict.items())[data["League"]]}", inline=True) embed.add_field(name="Wins", value=f"{data["Record"]["Regular Season"]["Wins"]}", inline=True) embed.add_field(name="Losses", value=f"{data["Record"]["Regular Season"]["Losses"]}", inline=True) embed.add_field(name="Run Differential", value=f"{data["Record"]["Regular Season"]["RunDifferential"]}", inline=True) embed.add_field(name="Rank", value=f"{ranking}", inline=True) embed.add_field(name="Augments", value=f"{data["Augments"]}", inline=True) embed.add_field(name="Championships", value=f"{data["Championships"]}", inline=True) embed.set_footer(text=teamid) await interaction.edit_original_message(embed=embed,view=TeamView()) @greaterteam.on_autocomplete("team") async def greaterteamac(self, interaction: nextcord.Interaction, team: str): if not team: thanksdiscord = teams_list[:20] await interaction.response.send_autocomplete(thanksdiscord) return closestteam = [name for name in teams_list if name.lower().startswith(team.lower())] thanksdiscord = closestteam[:20] await interaction.response.send_autocomplete(thanksdiscord) @nextcord.slash_command( name="gamehistory", description="Get the last 10 games played by a team", integration_types=[ IntegrationType.user_install, IntegrationType.guild_install, ], contexts=[ nextcord.InteractionContextType.guild, nextcord.InteractionContextType.bot_dm, nextcord.InteractionContextType.private_channel, ], force_global=True, ) async def gamehistory(self, interaction: nextcord.Interaction, team: str = nextcord.SlashOption( name = "team", description = "The team" )): if team not in teams_dict: await interaction.response.send_message("Invalid Team!", ephemeral=True) return await interaction.response.defer() teamid = teams_dict[team] data = requests.get(f"https://mmolb.com/api/team/{teamid}").json() history = requests.get(f"https://freecashe.ws/api/games?team={teamid}&season=2").json()["items"] color = tuple(int(data["Color"][i:i+2], 16) for i in (0, 2, 4)) embed = nextcord.Embed(title=f"Last ten games for the {data["Location"]} {data["Name"]} {data["Emoji"]}", colour = nextcord.Color.from_rgb(color[0], color[1], color[2])) embed.set_footer(text=teamid) for index in reversed(list(itertools.islice(history, (len(history)-10 if len(history)-10 > 0 else 0) , len(history)))): if index["away_team_id"] != teamid: awayteamid = index["away_team_id"] otherscore = index["last_update"]["away_score"] ourscore = index["last_update"]["home_score"] else: awayteamid = index["home_team_id"] otherscore = index["last_update"]["home_score"] ourscore = index["last_update"]["away_score"] tempdata = requests.get(f"https://mmolb.com/api/team/{awayteamid}").json() embed.add_field(name=f"vs. {tempdata["Location"]} {tempdata["Name"]} {tempdata["Emoji"]} ({ourscore} - {otherscore})", value=f"[watch]()", inline=False) await interaction.edit_original_message(embed=embed) @gamehistory.on_autocomplete("team") async def gamehistoryac(self, interaction: nextcord.Interaction, team: str): if not team: print("we're here") thanksdiscord = teams_list[:20] await interaction.response.send_autocomplete(thanksdiscord) return closestteam = [name for name in teams_list if name.lower().startswith(team.lower())] thanksdiscord = closestteam[:20] await interaction.response.send_autocomplete(thanksdiscord) @nextcord.slash_command( name="players", description="Get a team's player statistics", integration_types=[ IntegrationType.user_install, IntegrationType.guild_install, ], contexts=[ nextcord.InteractionContextType.guild, nextcord.InteractionContextType.bot_dm, nextcord.InteractionContextType.private_channel, ], force_global=True, ) async def teamstats(self, interaction: nextcord.Interaction, team: str): if team not in teams_dict: await interaction.response.send_message("Invalid Team!", ephemeral=True) return await interaction.response.defer() teamid = teams_dict[team] loop = asyncio.get_event_loop() stats = await loop.run_in_executor(None, teamstats,teamid) splistats = [stats[i:i+1900] for i in range(0, len(stats), 1900)] #TODO make better as in don't do cutoff await interaction.edit_original_message(content=f"```ansi\n{splistats[0]}```") splistats.pop(0) for i in splistats: await interaction.followup.send(f"```ansi\n{i}```") @teamstats.on_autocomplete("team") async def teamstatsac(self, interaction: nextcord.Interaction, team: str): if not team: print("we're here") thanksdiscord = teams_list[:20] await interaction.response.send_autocomplete(thanksdiscord) return closestteam = [name for name in teams_list if name.lower().startswith(team.lower())] thanksdiscord = closestteam[:20] await interaction.response.send_autocomplete(thanksdiscord) @tasks.loop(hours=1) async def updateallteams(self): print("Updating teams autocomplete") loop = asyncio.get_event_loop() await loop.run_in_executor(None, get_all_teams) self.bot.teams_list = teams_list self.bot.teams_dict = teams_dict def setup(bot: commands.Bot): bot.add_cog(team(bot))