Created
April 21, 2016 16:57
-
-
Save blaxter/58ec728eab328bc52b60482b3e0e9443 to your computer and use it in GitHub Desktop.
Warcraftlogs tool
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/python | |
import requests | |
import lxml.html | |
import ConfigParser | |
import os | |
import sys | |
from collections import defaultdict | |
from itertools import groupby | |
class WarcraftLogs(object): | |
BASE_URL = 'https://www.warcraftlogs.com:443/v1/' | |
ENCOUNTERS_PER_PAGE = 5000 | |
def __init__(self, api_key): | |
self.api_key = api_key | |
def boss_id_by_name(self, boss_name): | |
for zone in self._get('zones'): | |
for encounter in zone['encounters']: | |
if encounter['name'] == boss_name: | |
return encounter['id'] | |
raise Exception("Not found %s" % boss_name) | |
def get_zones_and_bosses(self): | |
zones = self._get('zones') | |
ret = {} | |
for zone in zones: | |
ret[zone['name']] = [e['name'] for e in zone['encounters']] | |
return ret | |
def all_encounters_by_boss_and_difficulty(self, boss, difficulty, search=""): | |
boss_id = self.boss_id_by_name(boss) | |
total_pages, current_page = 1, 1 | |
ret, seen = list(), set() | |
while current_page <= total_pages: | |
res = self._get('rankings/encounter/%d' % boss_id, | |
difficulty=difficulty, page=current_page, filter=search, | |
limit=self.ENCOUNTERS_PER_PAGE) | |
if current_page == 1: | |
total_pages = 1 + res['total'] / self.ENCOUNTERS_PER_PAGE | |
for r in res['rankings']: | |
if r['reportID'] in seen: | |
continue | |
seen.add(r['reportID']) | |
ret.append({'size': r.get('size', 20), | |
'id': r['reportID'], | |
'fight': r['fightID']}) | |
current_page += 1 | |
return ret | |
def healers_in(self, encounter): | |
return self._composition_parse_count(encounter, "healer") | |
def tanks_in(self, encounter): | |
return self._composition_parse_count(encounter, "tank") | |
def _composition_parse_count(self, encounter, role): | |
start_time, end_time = 0, 0 | |
try: | |
res = self._get('report/fights/%s' % encounter['id']) | |
except: | |
print("Error on %r" % encounter) | |
return 0 | |
if 'fights' not in res: | |
print("Error on %r (%r)" % (encounter, res)) | |
return 0 | |
for fight in res['fights']: | |
if fight['id'] == encounter['fight']: | |
start_time = fight['start_time'] | |
end_time = fight['end_time'] | |
break | |
if start_time == 0: | |
raise Exception("Not found in report/fights") | |
url = ('https://www.warcraftlogs.com/reports/summary/%s/%d/%d/%d/0/0/Any/0/-1/0/0' % | |
(encounter['id'], encounter['fight'], start_time, end_time)) | |
body = requests.get(url) | |
dom = lxml.html.fromstring(body.text) | |
roles = ['tank', 'dps', 'healer'] | |
if role not in roles: | |
raise Exception("Unknown role %r" % role) | |
ret = len(dom.xpath('//tr[@class="composition-row"]')[roles.index(role)].getchildren()[1].getchildren()) | |
if ret == 0: | |
raise Exception(url) | |
return ret | |
def _get(self, resource, **kwargs): | |
kwargs.update({'api_key': self.api_key}) | |
return requests.get(self.BASE_URL + resource, params=kwargs).json() | |
if __name__ == "__main__": | |
import argparse | |
class ModeAction(argparse.Action): | |
NORMAL = 3 | |
HEROIC = 4 | |
MYTHIC = 5 | |
def __call__(self, parser, namespace, value, option_string=None): | |
if value == 'normal': | |
mode = self.NORMAL | |
elif value == "heroic": | |
mode = self.HEROIC | |
else: | |
mode = self.MYTHIC | |
setattr(namespace, self.dest, mode) | |
parser = argparse.ArgumentParser(description='Warcraft logs tool') | |
parser.add_argument('--config', default='wcl.config', nargs='?', | |
help='Config file with warcraftlogs\'s api key') | |
parser.add_argument('--list', action='store_true', | |
help='List zones and bosses availables') | |
parser.add_argument('--count', action='store_true', | |
help='Count number of logs per boss') | |
parser.add_argument('boss', nargs='?', help='Boss name') | |
parser.add_argument('--mode', choices=['normal', 'heroic', 'mythic'], | |
action=ModeAction, default=ModeAction.MYTHIC, | |
help='Either normal, heroic or mythic') | |
parser.add_argument('--tanks', action='store_true', | |
help='Count number of tanks') | |
parser.add_argument('--healers', action='store_true', | |
help='Count number of healers') | |
args = parser.parse_args() | |
config = ConfigParser.ConfigParser() | |
config.readfp(open(args.config)) | |
api_key = config.get('wcl', 'api_key') | |
if not api_key: | |
raise Exception('Api key not found under wcl section') | |
sys.exit(1) | |
def parse_all_encounters(args, target): | |
""" | |
Count encounters getting each one individually | |
""" | |
encounters = w.all_encounters_by_boss_and_difficulty(args.boss, args.mode) | |
data = defaultdict(list) | |
print "Total encounters: %d" % len(encounters) | |
current = 1 | |
for encounter in encounters: | |
try: | |
print "Fetching info %d" % current | |
if target == 'healers': | |
n_targets = w.healers_in(encounter) | |
else: | |
n_targets = w.tanks_in(encounter) | |
print(n_targets) | |
if n_targets > 0: | |
data[str(encounter['size'])].append(n_targets) | |
current += 1 | |
except Exception as e: | |
print(repr(e)) | |
for size in sorted(data.keys()): | |
total = len(data[size]) | |
print "Hay %d combates con %s jugadores" % (total, size) | |
grouped = [[i[0], len(list(i[1]))] for i in groupby(sorted(data[size]))] | |
grouped = sorted(grouped, key=lambda e: e[0]) | |
for el in grouped: | |
print("\tCon %d %s son %.2f%%" % | |
(el[0], target, 100.0 * float(el[1]) / float(total))) | |
def smart_count(args, target): | |
""" | |
Count encounters using the 'filter' parameter and then counting | |
the reports page by page discarting same encounters | |
""" | |
total = len(w.all_encounters_by_boss_and_difficulty(args.boss, args.mode)) | |
print "Total encounters: %d\n" % total | |
data = {} | |
still_data = True | |
import Queue, threading | |
q = Queue.Queue() | |
def count_logs(): | |
while still_data: | |
try: | |
n = q.get(False, 1) | |
if target == 'healers': | |
search = 'heal.' | |
else: | |
search = 'tank.' | |
search += str(n) | |
count = len(w.all_encounters_by_boss_and_difficulty(args.boss, args.mode, search)) | |
data[n] = count | |
q.task_done() | |
except Queue.Empty: | |
pass | |
n_workers = 10 | |
[threading.Thread(target=count_logs).start() for n in xrange(0, n_workers)] | |
max_n = 7 if target == "healers" else 4 | |
start = 0 | |
while still_data: | |
for n in xrange(start, start + max_n + 1): | |
q.put(n) | |
q.join() | |
start += max_n + 1 | |
still_data = data[sorted(data.keys())[-1]] != 0 | |
for n in sorted(data.keys()): | |
count = data[n] | |
if count == 0: | |
continue | |
print("Logs with %d %s: %d (%.2f%%)" % | |
(n, target, count, 100.0 * float(count) / float(total))) | |
w = WarcraftLogs(api_key) | |
if args.list: | |
raids = w.get_zones_and_bosses() | |
for raid, bosses in raids.iteritems(): | |
print(raid) | |
for boss in bosses: | |
print "\t%s" % boss | |
sys.exit(0) | |
elif args.count: | |
print len(w.all_encounters_by_boss_and_difficulty(args.boss, args.mode)) | |
sys.exit(0) | |
if args.tanks: | |
target = 'tanks' | |
elif args.healers: | |
target = 'healers' | |
else: | |
parser.print_help() | |
sys.exit(1) | |
if args.mode != ModeAction.MYTHIC: | |
parse_all_encounters(args, target) | |
else: | |
smart_count(args, target) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment