import argparse import configparser import datetime import html import json import logging import pathlib import requests import sys import appdirs from . import export from . import resources from . import smashgg from . import version # ============================================================================= __version__ = version.__version__ __license__ = version.__license__ ROOTDIR = pathlib.Path(__file__).absolute().parent APPDIRS = appdirs.AppDirs(version.NAME, version.ENTITY) # ============================================================================= def main(): # ------------------------------------------------------------------------- parser = argparse.ArgumentParser( formatter_class = argparse.ArgumentDefaultsHelpFormatter, ) subparsers = parser.add_subparsers( dest = "command", help = "commands", ) parser.add_argument( "--proxy", "-p", default = None, help = "the proxy to use", ) # ------------------------------------------------------------------------- init_parser = subparsers.add_parser( "init", formatter_class = argparse.ArgumentDefaultsHelpFormatter, ) init_parser.add_argument( "game", default = "ssbu", help = "The game you want to initialize the resources for", ) init_parser.add_argument( "--imgdir", "-ID", type = pathlib.Path, default = pathlib.Path(APPDIRS.user_data_dir) / "res", help = "The directory we should download the resources to", ) # ------------------------------------------------------------------------- top8_parser = subparsers.add_parser( "top8", formatter_class = argparse.ArgumentDefaultsHelpFormatter, ) top8_parser.add_argument( "tournament", default = None, help = "The tournament slug or id", ) top8_parser.add_argument( "--token", "-t", default = None, help = "the authentication token to use", ) top8_parser.add_argument( "--imgdir", "-ID", type = pathlib.Path, default = pathlib.Path(APPDIRS.user_data_dir) / "res", help = "The directories containing images, be careful whether " \ "you specify an absolute path or a relative one.", ) top8_parser.add_argument( "--playerskinsdb", "-PD", type = (lambda s: s if s.startswith("http") else pathlib.Path(s)), default = ROOTDIR / "data" / "playerskinsdb.json", help = "A JSON file path or urk matching player tags, characters,"\ " sponsors, and preferred skins", ) top8_parser.add_argument( "--cachedir", "-CD", type = pathlib.Path, default = pathlib.Path(APPDIRS.user_cache_dir), help = "A directory to use for temporary files", ) top8_parser.add_argument( "--templatesdir", "-TD", type = pathlib.Path, default = ROOTDIR / "templates", help = "The local result templates directory", ) top8_parser.add_argument( "--template", "-T", default = "rebootlyon2020", help = "The local result template to use", ) top8_parser.add_argument( "--template-options", "-O", action = "append", default = [], help = "Template-specific options", ) top8_parser.add_argument( "--lkrz-file", "-f", type = pathlib.Path, default = None, help = "The lkrz file in which the results are stored ; if it " \ "does not exist, one will be created from the smashgg data", ) top8_parser.add_argument( "--outfile", "-o", type = pathlib.Path, default = None, help = "The SVG or PNG local result file to output to ; if it's " \ "not specified, it will use the tournament slug as name", ) top8_parser.add_argument( "--name-seo-delimiter", default = None, help = "A character that will delimit in a tournament name what " \ "really is its name, and what's actually here for SEO " \ "purposes (example: in 'Cornismash #42 - Ultimate Weekly " \ "Lyon', only the 'Cornismash #42' is the tournament name, "\ "the rest is here to help find the tournament).", ) top8_parser.add_argument( "--use-smashgg-prefixes", "-P", action = "store_true", help = "Use the prefixes (sponsor, team, etc) set by players on " \ "smash.gg for the tournament", ) # ------------------------------------------------------------------------- parser.add_argument( "--verbose", "-v", default = 0, action = "count", help = "increase verbosity" ) parser.add_argument( "--version", "-V", default = False, action = "store_true", help = "show version number" ) # ------------------------------------------------------------------------- args = parser.parse_args() # Set log level # ------------------------------------------------------------------------- log = logging.getLogger(version.NAME) log.setLevel(logging.DEBUG) log_handler_console = logging.StreamHandler() log_handler_console.setLevel(logging.WARNING) if(args.verbose >= 2): log_handler_console.setLevel(logging.DEBUG) elif(args.verbose >=1): log_handler_console.setLevel(logging.INFO) else: log_handler_console.setLevel(logging.WARNING) log_formatter_console = logging.Formatter("%(name)s:%(levelname)s: %(message)s") log_handler_console.setFormatter(log_formatter_console) log.addHandler(log_handler_console) # Print version if required # ------------------------------------------------------------------------- if args.version: print(version.VERSION_NAME) return 0 # ------------------------------------------------------------------------- if args.command not in [ "init", "top8" ]: parser.print_help() return 1 # ------------------------------------------------------------------------- if args.command == "init": args.imgdir.mkdir(parents=True, exist_ok=True) resources.download_res_ssbu( dstdir = args.imgdir, proxy = args.proxy, log = log, ) return 0 # ------------------------------------------------------------------------- if args.command == "top8": # Initialize PLAYERSKINS db log.debug("loading playerskins db from '{}'" \ .format(args.playerskinsdb)) try: smashgg.PLAYERSKINS = requests.get(args.playerskinsdb).json() except: with args.playerskinsdb.open("r", encoding="utf8") as f: smashgg.PLAYERSKINS = json.load(f) # tournament = None top_players = {} if args.lkrz_file is not None and args.lkrz_file.exists(): lkrz = configparser.ConfigParser() lkrz.read(str(args.lkrz_file)) log.info("Loading data from '{}'".format(args.lkrz_file)) for s in lkrz: section = lkrz[s] if s == "Tournament": tournament = smashgg.Tournament( id = 0, name = section["name"], slug = section["slug"], startAt = datetime.datetime.strptime( section["date"], "%Y-%m-%d %H:%M:%S", ), numEntrants = int(section["numEntrants"]), venueName = section["location"], ) elif s.startswith("player "): chars = {} for char in section["characters"].split(","): c = char.strip() charname = c.split("_")[0] charskin = c.split("_")[1].split(" ")[0] charscore = float(c.split("(")[1].split(")")[0]) chars[(charname,charskin)] = charscore player = smashgg.Player( id = 0, prefix = section["team"], gamerTag = section["tag"], placement = section["placement"], seeding = section["seeding"], twitterHandle = section["twitter"], chars = chars, ) top_players[player.gamerTag] = player else: # Get infos from smash.gg and write the config file tournament, top_players = getTournamentTop( id_or_slug = args.tournament, get_prefixes = args.use_smashgg_prefixes, top = 8, token = args.token, proxy = args.proxy, log = log, ) if tournament is None or top_players is None: log.error("Could not load data from smash.gg") return 1 lkrz_data = "\n".join( [ tournament.conf() ] \ + list(map( lambda p:p.conf(), top_players.values(), )) ) if args.lkrz_file is None: args.lkrz_file = pathlib.Path( "{}.lkrz".format(tournament.slug) ) with args.lkrz_file.open("w", encoding="utf8") as f: f.write(lkrz_data) # Default outfile is 'tournament-slug.svg' if args.outfile is None: args.outfile = pathlib.Path( "{}.svg".format(tournament.slug), ) # Build the context which will be passed to the template try: dir_res_ssbu = args.imgdir.as_uri() # not absolute => error except ValueError: dir_res_ssbu = args.imgdir.as_posix() context = { "tournament": tournament.clean_name(args.name_seo_delimiter), "players" : sorted( top_players.values(), key = lambda p: p.placement, ), "dir_res_ssbu": dir_res_ssbu, "dir_template": str(args.templatesdir / args.template), "options": args.template_options, } rv = export.generate_outfile( args.templatesdir, args.template, context, args.outfile, log = log, cachedir = args.cachedir, ) if rv is None: return 1 log.info("Successfully saved outfile as '{}'".format(rv)) return 0 # ----------------------------------------------------------------------------- def getTournamentTop( id_or_slug, get_prefixes = True, top=8, token = "", proxy = None, log=None): """Returns a tuple : the smashgg.Tournament object and a list of the top smashgg.Player in that tournament.""" # ------------------------------------------------------------------------- # Select the right event (the one with the most entrants or the most sets) def selectBiggestEvent(data, log=None): try: event = data["events"][0] except: log.error("No event found in data") log.debug(data) return None try: numEntrants = event["numEntrants"] except KeyError: numEntrants = event["standings"]["pageInfo"]["total"] for e in data["events"]: try: ne = e["numEntrants"] except KeyError: ne = e["standings"]["pageInfo"]["total"] if ne > numEntrants: event = e numEntrants = ne log.info("Selected Event '{}' with {} entrants" \ .format( event["name"], numEntrants, )) return event # ------------------------------------------------------------------------- data = None try: data = smashgg.run_query( query_name = "getTournamentTopById", variables = { "id" : int(id_or_slug), # If this fails, it's a slug "top": top, }, query_dir = ROOTDIR / "queries", token = token, proxy = proxy, log = log, ) except ValueError: data = smashgg.run_query( query_name = "getTournamentTopBySlug", variables = { "slug" : id_or_slug, "top": top, }, query_dir = ROOTDIR / "queries", token = token, proxy = proxy, log = log, ) try: tournament_data = data["tournament"] except: log.error("Failed to load Tournaments") return None,None if tournament_data is None: log.error("Failed to load Tournament") return None,None event = selectBiggestEvent(tournament_data, log) if event is None : return None,None # Get the tournament tournament = smashgg.Tournament( id = tournament_data["id"], slug = tournament_data["slug"], name = tournament_data["name"], startAt = \ datetime.datetime. \ fromtimestamp(tournament_data["startAt"]), numEntrants = event["standings"]["pageInfo"]["total"], venueAddress = tournament_data["venueAddress"], venueName = tournament_data["venueName"], city = tournament_data["city"], countryCode = tournament_data["countryCode"], hashtag = tournament_data["hashtag"], ) # Get the top players top_players = {} standings = event["standings"]["nodes"] for standing in standings : seeding = None for seed in standing["entrant"]["seeds"]: # Take the seeding from the phase with *all* Event entrants if seed["phase"]["numSeeds"] == tournament.numEntrants: seeding = seed["groupSeedNum"] participant_data = standing["entrant"]["participants"][0] try: twitterHandle = participant_data \ ["player"] \ ["user"] \ ["authorizations"] \ [0] \ ["externalUsername"] except TypeError: twitterHandle = None if get_prefixes: prefix = participant_data["prefix"] else: prefix = "" player = smashgg.Player( id = standing["entrant"]["id"], prefix = prefix, gamerTag = participant_data["gamerTag"], placement = standing["placement"], seeding = seeding, twitterHandle = twitterHandle, ) top_players[player.id] = player # ------------------------------------------------------------------------- # Now, we need to find which characters those top players chose data = None data = smashgg.run_query( query_name = "getCharsByTournamentIdAndEntrantIds", variables = { "tournamentId" : int(tournament.id), "entrantIds": [ id for id in top_players.keys() ], }, query_dir = ROOTDIR / "queries", token = token, proxy = proxy, log = log, ) try: tournament_data = data["tournament"] except: log.error("Failed to load Tournament") return None,None if tournament_data is None: log.error("Failed to load Tournament") return None,None event = selectBiggestEvent(tournament_data, log) if event is None : return None,None # TODO check that sets number is < to hardcoded 100 max value (cf query) sets = event["sets"]["nodes"] for s in sets: try: for g in s["games"]: winnerId = g["winnerId"] for slct in g["selections"]: if slct["selectionType"] == "CHARACTER": eid = slct["entrant"]["id"] try: top_players[eid].add_character_selection( character = slct["selectionValue"], win = (winnerId == eid), ) except KeyError: pass except TypeError: # If some games or selections are null, this can happen continue # Return the data return tournament, top_players # ============================================================================= if __name__ == '__main__': rv = main() sys.exit(rv)