Example #1
0
def bl_parse_stats(parsed, mode="quickplay", status=None):
    # Just a quick FYI
    # If future me or future anyone else is looking at this, I do not how this code works.
    # I'm really really hoping it doesn't break.
    # Good luck!

    try:
        # XPath for the `u-align-center` h6 which signifies there's no data.
        no_data = parsed.xpath(
            ".//div[@id='{}']//ul/h6[@class='u-align-center']".format(mode))[0]
    except IndexError:
        pass
    else:
        if no_data.text.strip(
        ) == "We don't have any data for this account in this mode yet.":
            return None

    # Start the dict.
    built_dict = {"game_stats": [], "overall_stats": {}, "average_stats": []}

    # Shortcut location for player level etc
    if not status or status.lower() != "public profile":
        hasrank = parsed.xpath(
            '//*[@id="overview-section"]/div/div/div/div/div[2]/div/div[3]/div'
        )
        if hasrank:
            comprank = int(hasrank[0].text)
        else:
            comprank = None
        built_dict["overall_stats"]["comprank"] = comprank
        return built_dict

    mast_head = parsed.xpath(".//div[@class='masthead-player']")[0]

    # Rank images are now based on 2 separate images. Prestige now also relies on 'player-rank' element
    prestige_stars = 0
    try:
        prestige_rank = mast_head.xpath(".//div[@class='player-rank']")[0]
        bg_image = [
            x for x in prestige_rank.values() if "background-image" in x
        ][0]
    except IndexError:
        # No stars
        prestige_stars = 0
    else:
        for key, val in PRESTIGE_STARS.items():
            if key in bg_image:
                prestige_stars = val
                # Adds a new dict key called "prestige_image". Left the old name below of "rank_image" for compatibility
                built_dict["overall_stats"]["prestige_image"] = bg_image.split(
                    "(")[1][:-1]
                break
            else:
                # Unknown prestige image
                prestige_stars = None

    # Extract the background-image from the styles.
    prestige_num = 0
    try:
        # Get the player-level base (border).
        prestige = mast_head.xpath(".//div[@class='player-level']")[0]
        bg_image = [x for x in prestige.values() if "background-image" in x][0]
    except IndexError:
        # Cannot find background-image.
        # Yikes!
        # Don't set a prestige.
        built_dict["overall_stats"]["prestige"] = 0
    else:
        for key, val in PRESTIGE_BORDERS.items():
            if key in bg_image:
                prestige_num = val
                built_dict["overall_stats"]["rank_image"] = bg_image.split(
                    "(")[1][:-1]
                break
        else:
            # Unknown rank image
            prestige_num = None

    # If we have prestige values, return them. Otherwise, return None
    if prestige_num is not None and prestige_stars is not None:
        built_dict["overall_stats"]["prestige"] = prestige_num + prestige_stars
    else:
        built_dict["overall_stats"]["prestige"] = None

    # Parse out the HTML.
    level = int(prestige.findall(".//div")[0].text)
    built_dict["overall_stats"]["level"] = level

    # Get and parse out endorsement level.
    endorsement_level = int(
        mast_head.xpath(
            ".//div[@class='EndorsementIcon-tooltip']/div[@class='u-center']")
        [0].text)
    built_dict["overall_stats"]["endorsement_level"] = endorsement_level

    # Get endorsement circle.
    endorsement_icon_inner = mast_head.xpath(
        ".//div[@class='endorsement-level']/div[@class='EndorsementIcon']/div["
        "@class='EndorsementIcon-inner']")[0]

    # Get individual endorsement segments.
    names = ("shotcaller", "teammate", "sportsmanship")
    for name in names:
        try:
            endorsement_value = endorsement_icon_inner.findall(
                f".//svg[@class='EndorsementIcon-border EndorsementIcon-border--{name}']"
            )[0].get("data-value")
        except:  # TODO: don't do this...
            endorsement_value = 0

        val = float(endorsement_value)
        built_dict["overall_stats"][f"endorsement_{name}"] = val

    # Get comp rank.
    try:
        for role in mast_head.xpath(".//div[@class='competitive-rank']")[0]:
            role_img = role.findall(
                ".//img[@class='competitive-rank-role-icon']")[0]
            role_img_src = role_img.values()[1]
            role_str = ""
            for key, val in role_data_img_src.items():
                if key in role_img_src:
                    role_str = val
                    break
            built_dict["overall_stats"][role_str +
                                        "_role_image"] = role_img_src

            tier_img = role.findall(
                ".//img[@class='competitive-rank-tier-icon']")[0]
            tier_img_src = tier_img.values()[1]
            tier_str = ""
            for key, val in tier_data_img_src.items():
                if key in tier_img_src:
                    tier_str = val
                    break
            built_dict["overall_stats"][role_str +
                                        "_tier_image"] = tier_img_src

            built_dict["overall_stats"][role_str + "_tier"] = tier_str

            hasrank = role.findall(".//div[@class='competitive-rank-level']")
            if hasrank:
                comprank = int(hasrank[0].text)
            else:
                comprank = None
            built_dict["overall_stats"][role_str + "_comprank"] = comprank

    except IndexError as exc:
        print(str(exc))
    finally:
        for role in ["tank", "damage", "support"]:
            if role + "_role_image" not in built_dict["overall_stats"]:
                built_dict["overall_stats"][role + "_role_image"] = None
            if role + "_tier_image" not in built_dict["overall_stats"]:
                built_dict["overall_stats"][role + "_tier_image"] = None
            if role + "_tier" not in built_dict["overall_stats"]:
                built_dict["overall_stats"][role + "_tier"] = None
            if role + "_comprank" not in built_dict["overall_stats"]:
                built_dict["overall_stats"][role + "_comprank"] = None

    # Fetch Avatar
    avatar_root = mast_head.find(".//img[@class='player-portrait']")
    # some profiles don't have an avatar url?
    # it's just a <img class="player-portrait"> ???
    if avatar_root is not None:
        try:
            avatar_url = avatar_root.attrib["src"]
        except KeyError:
            avatar_url = None
    else:
        avatar_url = None

    built_dict["overall_stats"]["avatar"] = avatar_url

    if mode == "competitive":
        # the competitive overview is under a div with id='competitive'
        # and the right category
        try:
            stat_groups = parsed.xpath(
                ".//div[@id='competitive']"
                "//div[@data-group-id='stats' and @data-category-id='0x02E00000FFFFFFFF']"
            )[0]
        except IndexError:
            # No stats
            return None
    elif mode == "quickplay":
        try:
            stat_groups = parsed.xpath(
                ".//div[@id='quickplay']"
                "//div[@data-group-id='stats' and @data-category-id='0x02E00000FFFFFFFF']"
            )[0]
        except IndexError:
            # User has no stats...
            return None
    else:
        # how else to handle fallthrough case?
        stat_groups = parsed.xpath(
            ".//div[@data-group-id='stats' and @data-category-id='0x02E00000FFFFFFFF']"
        )[0]

    # Highlight specific stat groups.
    try:
        game_box = stat_groups[3]
    except IndexError:
        try:
            game_box = stat_groups[2]  # I guess use 2?
        except IndexError:
            # edge cases...
            # we can't really extract any more stats
            # so we do an early return
            return {}

    # Calculate the wins, losses, and win rate.
    try:
        wins = int(
            game_box.xpath(".//text()[. = 'Games Won']/../..")[0]
            [1].text.replace(",", ""))
    except IndexError:
        # weird edge case
        wins = 0
    g = game_box.xpath(".//text()[. = 'Games Played']/../..")
    if len(g) < 1:
        # Blizzard f****d up, temporary quick fix for #70
        games, losses = None, None
    else:
        games = int(g[0][1].text.replace(",", ""))

    if mode == "competitive":
        try:
            losses = int(
                game_box.xpath(".//text()[. = 'Games Lost']/../..")[0]
                [1].text.replace(",", ""))
            ties = int(
                game_box.xpath(".//text()[. = 'Games Tied']/../..")[0]
                [1].text.replace(",", ""))
        except IndexError:
            # Sometimes the losses and ties don't exist.
            # I'm not 100% as to what causes this, but it might be because there are no ties.
            # In this case, just set ties to 0, and calculate losses manually.
            ties = 0
            # Quickplay shit.
            # Goddamnit blizzard.
            if games is None:
                losses = 0
                games = 0
                wins = 0
            else:
                # Competitive stats do have these values (for now...)
                losses = games - wins

        if games == 0 or games == ties:
            wr = 0
        else:
            wr = round((wins / (games - ties)) * 100, 2)

        built_dict["overall_stats"]["ties"] = ties
        built_dict["overall_stats"]["games"] = games
        built_dict["overall_stats"]["losses"] = losses
        built_dict["overall_stats"]["win_rate"] = wr

    # Update the dictionary.
    built_dict["overall_stats"]["wins"] = wins

    # Build a dict using the stats.
    game_stats = {}
    average_stats = {}
    rolling_average_stats = {}

    for subbox in stat_groups:
        trs = subbox.findall(".//tbody/tr")
        # Update the dict with [0]: [1]
        for subval in trs:
            name, value = util.sanitize_string(subval[0].text), subval[1].text
            # Try and parse out the value. It might be a time!
            # If so, try and extract the time.
            nvl = util.try_extract(value)

            if "average" in name.lower():
                name = name.replace("_average", "_avg")
                into = average_stats
            elif "_avg_per_10_min" in name.lower():
                # 2017-08-03 - calculate rolling averages.
                name = name.lower().replace("_avg_per_10_min", "")
                into = rolling_average_stats
            else:
                into = game_stats

            # Correct Blizzard Singular Plural Bug
            if "_plural_" in name:
                name = util.correct_plural_name(name, nvl)

            into[name] = nvl

    # Manually add the KPD.
    try:
        game_stats["kpd"] = round(
            game_stats["eliminations"] / game_stats["deaths"], 2)
    except KeyError:
        # They don't have any eliminations/deaths.
        # Set the KPD to 0.0.
        # See: #106
        game_stats["kpd"] = 0

    built_dict["game_stats"] = game_stats
    built_dict["average_stats"] = average_stats
    built_dict["rolling_average_stats"] = rolling_average_stats
    built_dict["competitive"] = mode == "competitive"

    if "games" not in built_dict["overall_stats"]:
        # manually calculate it
        # 2017-07-04 - changed to use eliminations
        # since damage done gave a bit of a stupid amount
        # 2017-07-11 - changed to cycle some averages
        average_keys = ("eliminations", "healing_done", "final_blows",
                        "objective_kills")
        for key in average_keys:
            try:
                total = built_dict["game_stats"][key]
                avg = built_dict["average_stats"][key + "_avg"]
            except KeyError:
                continue
            else:
                got = True
                break
        else:
            got = False

        if got:
            games = int(total // avg)

            losses = games - built_dict["overall_stats"]["wins"]
            built_dict["overall_stats"]["games"] = games
            built_dict["overall_stats"]["losses"] = losses
            built_dict["overall_stats"]["win_rate"] = round(
                (built_dict["overall_stats"]["wins"] / games) * 100, 2)
        else:
            # lol make them up
            built_dict["overall_stats"]["games"] = 0
            built_dict["overall_stats"]["losses"] = 0
            built_dict["overall_stats"]["win_rate"] = 0

    return built_dict
Example #2
0
def bl_parse_hero_data(parsed: etree._Element, mode="quickplay"):
    # Start the dict.
    built_dict = {}

    _root = parsed.xpath(".//div[@id='{}']".format(
        "competitive" if mode == "competitive" else "quickplay"))
    if not _root:
        return None

    try:
        # XPath for the `u-align-center` h6 which signifies there's no data.
        no_data = _root[0].xpath(".//ul/h6[@class='u-align-center']")[0]
    except IndexError:
        pass
    else:
        if no_data.text.strip(
        ) == "We don't have any data for this account in this mode yet.":
            return None

    for hero_name, requested_hero_div_id in hero_data_div_ids.items():
        n_dict = {}
        _stat_groups = _root[0].xpath(
            ".//div[@data-group-id='stats' and @data-category-id='{0}']".
            format(requested_hero_div_id))

        if not _stat_groups:
            continue

        stat_groups = _stat_groups[0]
        _average_stats = {}
        _t_d = {}
        _rolling_avgs = {}
        # offset for subboxes
        # if there IS a hero-specific box, we need to scan all boxes from offset to end
        # because the hero-specific box is first.
        # if there is NOT, we scan all boxes later.
        # this is determined by the xpath to find the Hero Specific page.
        subbox_offset = 0

        # .find on the assumption hero box is the *first* item
        # hbtitle = None
        # try:
        # hbtitle = stat_groups.find(".//span[@class='stat-title']").text
        # except AttributeError:
        # try:
        # hbtitle = stat_groups.find(".//h5[@class='stat-title']").text
        # except AttributeError:
        # Unable to parse stat boxes. This is likely due to 0 playtime on a hero, so there are no stats
        # pass

        for idx, sg in enumerate(stat_groups):
            stat_group_hero_specific = stat_groups[idx].find(
                './/*[@class="stat-title"]').text

            if stat_group_hero_specific.lower() == "hero specific":
                try:
                    hero_specific_box = stat_groups[idx]
                    trs = hero_specific_box.findall(".//tbody/tr")

                    # Update the dict with [0]: [1]
                    for subval in trs:
                        name, value = util.sanitize_string(
                            subval[0].text), subval[1].text

                        # Put averages into average_stats
                        if "_avg_per_10_min" in name.lower():
                            into = _rolling_avgs
                            name = name.lower().replace("_avg_per_10_min", "")
                        else:
                            into = _t_d
                        nvl = util.try_extract(value)
                        into[name] = nvl
                    break
                except IndexError:
                    pass

        n_dict["hero_stats"] = _t_d
        _t_d = {}

        for subbox in stat_groups[subbox_offset:]:
            trs = subbox.findall(".//tbody/tr")
            # Update the dict with [0]: [1]
            for subval in trs:
                name, value = util.sanitize_string(
                    subval[0].text), subval[1].text

                if "_avg_per_10_min" in name:
                    into = _rolling_avgs
                    name = name.replace("_avg_per_10_min", "")
                elif name in n_dict["hero_stats"]:
                    into = None
                else:
                    into = _t_d

                nvl = util.try_extract(value)

                # Correct Blizzard Singular Plural Bug
                if "_plural_" in name:
                    name = util.correct_plural_name(name, nvl)

                if into != None:
                    into[name] = nvl

        n_dict["general_stats"] = _t_d
        n_dict["average_stats"] = _average_stats
        n_dict["rolling_average_stats"] = _rolling_avgs

        built_dict[hero_name] = n_dict

    return built_dict