wotbreplay_uploader.py optimization

This commit is contained in:
Jylpah committed 2018-11-05 14:48:07 +02:00
1 parent 30655b2af0
commit 4acf83ce1a
3 files changed
+500 -218

No files matched your search

+46 -38
View File
@@ -5,12 +5,13 @@
import sys, argparse, json, os, inspect, aiohttp, asyncio, aiofiles, aioconsole, re, logging, time, xmltodict, collections import sys, argparse, json, os, inspect, aiohttp, asyncio, aiofiles, aioconsole, re, logging, time, xmltodict, collections
import blitzutils as bu import blitzutils as bu
from blitzutils import WG from blitzutils import WG
from blitzutils import WoTinspector
logging.getLogger("asyncio").setLevel(logging.DEBUG) logging.getLogger("asyncio").setLevel(logging.DEBUG)
WG_appID = '81381d3f45fa4aa75b78a7198eb216ad' WG_appID = '81381d3f45fa4aa75b78a7198eb216ad'
wg = None wg = None
wi = None
REPLAY_N = 0 REPLAY_N = 0
STAT_TANK_BATTLE_MIN = 100 STAT_TANK_BATTLE_MIN = 100
@@ -19,7 +20,6 @@ replay_summary_flds = [
'battle_type', 'battle_type',
'map_name', 'map_name',
'battle_duration', 'battle_duration',
'battle_start_timestamp',
'protagonist', 'protagonist',
'title' 'title'
] ]
@@ -245,13 +245,13 @@ class BattleRecordCategory():
## main() ------------------------------------------------------------- ## main() -------------------------------------------------------------
async def main(argv): async def main(argv):
global wg global wg, wi
TASK_N = 7 TASK_N = 7
WG_ID = 2008897346
parser = argparse.ArgumentParser(description='ANalyze Blitz replay JSONs from WoTinspector.com') parser = argparse.ArgumentParser(description='ANalyze Blitz replay JSONs from WoTinspector.com')
parser.add_argument('--output', default='plain', choices=['json', 'plain', 'db'], help='Select output mode: JSON, plain text or database') parser.add_argument('--output', default='plain', choices=['json', 'plain', 'db'], help='Select output mode: JSON, plain text or database')
parser.add_argument('-id', dest='accountID', type=int, default=WG_ID, help='WG account_id to analyze') parser.add_argument('-id', dest='accountID', type=int, default=None, help='WG account_id to analyze')
parser.add_argument('-a', '--account', dest='account', type=str, default=None, help='WG account nameto analyze. Format: ACCOUNT_NAME@SERVER') parser.add_argument('-a', '--account', dest='account', type=str, default=None, help='WG account nameto analyze. Format: ACCOUNT_NAME@SERVER')
parser.add_argument('-u', '--url', dest= 'url', action='store_true', default=False, help='Print replay URLs') parser.add_argument('-u', '--url', dest= 'url', action='store_true', default=False, help='Print replay URLs')
parser.add_argument('--tankfile', type=str, default='tanks.json', help='JSON file to read Tankopedia from. Default is "tanks.json"') parser.add_argument('--tankfile', type=str, default='tanks.json', help='JSON file to read Tankopedia from. Default is "tanks.json"')
@@ -259,12 +259,13 @@ async def main(argv):
parser.add_argument('-o','--outfile', type=str, default='-', metavar="OUTPUT", help='File to write results. Default STDOUT') parser.add_argument('-o','--outfile', type=str, default='-', metavar="OUTPUT", help='File to write results. Default STDOUT')
parser.add_argument('-d', '--debug', action='store_true', default=False, help='Debug mode') parser.add_argument('-d', '--debug', action='store_true', default=False, help='Debug mode')
parser.add_argument('-v', '--verbose', action='store_true', default=False, help='Verbose mode') parser.add_argument('-v', '--verbose', action='store_true', default=False, help='Verbose mode')
parser.add_argument('files', metavar='FILE1 [FILE2 ...]', type=str, nargs='+', help='Files to read. Use \'-\' for STDIN') parser.add_argument('files', metavar='FILE1 [FILE2 ...]', type=str, nargs='+', help='Files to read. Use \'-\' for STDIN"')
args = parser.parse_args() args = parser.parse_args()
bu.setVerbose(args.verbose) bu.setVerbose(args.verbose)
bu.setDebug(args.debug) bu.setDebug(args.debug)
wg = WG(WG_appID, args.tankfile, args.mapfile) wg = WG(WG_appID, args.tankfile, args.mapfile)
wi = WoTinspector()
if args.account != None: if args.account != None:
args.accountID = await wg.getAccountID(args.account) args.accountID = await wg.getAccountID(args.account)
@@ -272,8 +273,6 @@ async def main(argv):
try: try:
replayQ = asyncio.Queue() replayQ = asyncio.Queue()
#resultsQ = asyncio.Queue()
reader_tasks = [] reader_tasks = []
# Make replay Queue # Make replay Queue
@@ -304,7 +303,8 @@ async def main(argv):
finally: finally:
## Need to close the aiohttp.session since Python destructors do not support async methods... ## Need to close the aiohttp.session since Python destructors do not support async methods...
await wg.session.close() await wg.close()
await wi.close()
return None return None
def processStats(results: dict, args : argparse.Namespace): def processStats(results: dict, args : argparse.Namespace):
@@ -407,9 +407,10 @@ async def statWorker(queue : asyncio.Queue, workerID: int) -> list:
# bu.debug('[' +str(workerID) + '] AccountID: ' + acc + ' TankID: ' + tank) # bu.debug('[' +str(workerID) + '] AccountID: ' + acc + ' TankID: ' + tank)
playerTankStat = await wg.getPlayerTankStats(account_id, tank_id, ['all.battles', 'all.wins']) playerTankStat = await wg.getPlayerTankStats(account_id, tank_id, ['all.battles', 'all.wins'])
# bu.debug('[' +str(workerID) + '] ' + str(playerTankStat)) # bu.debug('[' +str(workerID) + '] ' + str(playerTankStat))
playerStat = await wg.getPlayerStats(account_id, ['statistics.all.battles', 'statistics.all.wins']) playerStat = await wg.getPlayerStats(account_id, ['statistics.all.battles', 'statistics.all.wins'])
# bu.debug('[' +str(workerID) + '] ' + str(playerStat)) # bu.debug('[' +str(workerID) + '] ' + str(playerStat))
stats[item] = {} stats[item] = {}
if (playerTankStat == None) or (playerStat == None): if (playerTankStat == None) or (playerStat == None):
stats[item]['win_rate'] = None stats[item]['win_rate'] = None
@@ -417,10 +418,10 @@ async def statWorker(queue : asyncio.Queue, workerID: int) -> list:
else: else:
playerTankStat = playerTankStat['all'] playerTankStat = playerTankStat['all']
playerStat = playerStat['statistics']['all'] playerStat = playerStat['statistics']['all']
battles = playerStat['battles'] battles = playerStat['battles']
stats[item]['battles'] = battles stats[item]['battles'] = battles
battles_in_tank = playerTankStat['battles'] battles_in_tank = playerTankStat['battles']
if battles_in_tank >= STAT_TANK_BATTLE_MIN: if battles_in_tank >= STAT_TANK_BATTLE_MIN:
stats[item]['win_rate'] = min(playerTankStat['wins'] / battles_in_tank,1) # To cope with broken stats in WG DB stats[item]['win_rate'] = min(playerTankStat['wins'] / battles_in_tank,1) # To cope with broken stats in WG DB
@@ -430,7 +431,7 @@ async def statWorker(queue : asyncio.Queue, workerID: int) -> list:
except KeyError as err: except KeyError as err:
bu.error('[' +str(workerID) + '] Key :' + str(err) + ' not found') bu.error('[' +str(workerID) + '] Key :' + str(err) + ' not found')
except Exception as err: except Exception as err:
bu.error('[' +str(workerID) + '] ' + str(err)) bu.error('[' +str(workerID) + '] ' + str(type(err)) + ' : '+ str(err))
queue.task_done() queue.task_done()
except asyncio.CancelledError: except asyncio.CancelledError:
@@ -454,6 +455,8 @@ async def mkReplayQ(queue : asyncio.Queue, files : list):
await queue.put(await mkReaderQitem(line)) await queue.put(await mkReaderQitem(line))
else: else:
for fn in files: for fn in files:
if fn.endswith(os.sep):
fn = fn[:-1]
if os.path.isfile(fn) and (p_replayfile.match(fn) != None): if os.path.isfile(fn) and (p_replayfile.match(fn) != None):
await queue.put(await mkReaderQitem(fn)) await queue.put(await mkReaderQitem(fn))
elif os.path.isdir(fn): elif os.path.isdir(fn):
@@ -485,29 +488,27 @@ async def replayReader(queue: asyncio.Queue, readerID: int, args : argparse.Name
replayID = item[1] replayID = item[1]
try: try:
if os.path.exists(filename) and os.path.isfile(filename): replay_json = await bu.readJSON(filename, wi.chkJSONreplay)
async with aiofiles.open(filename) as fp: if replay_json == None:
replay_json = json.loads(await fp.read()) bu.verbose('Replay[' + str(replayID) + ']: ' + filename + ' is invalid. Skipping.' )
if replay_json['status'] != 'ok': #SKIPPED_N += 1
bu.verbose('Replay[' + str(replayID) + ']: ' + filename + ' is invalid. Skipping.' ) queue.task_done()
#SKIPPED_N += 1 continue
queue.task_done()
continue
## Read the replay JSON ## Read the replay JSON
bu.debug('[' + str(readerID) + '] reading replay: ' + filename) bu.debug('[' + str(readerID) + '] reading replay: ' + filename)
result = await readReplayJSON(replay_json, args) result = await readReplayJSON(replay_json, args)
if result == None: if result == None:
bu.error('[' + str(readerID) + '] Error reading replay: ' + filename) bu.error('[' + str(readerID) + '] Error reading replay: ' + filename)
queue.task_done() queue.task_done()
continue continue
if (account_id != None): if (account_id != None):
playerstanks.update(set(result['allies'])) playerstanks.update(set(result['allies']))
playerstanks.update(set(result['enemies'])) playerstanks.update(set(result['enemies']))
results.append(result) results.append(result)
bu.debug('Marking task ' + str(replayID) + ' done') bu.debug('Marking task ' + str(replayID) + ' done')
except Exception as err: except Exception as err:
bu.error(str(err)) bu.error(str(err))
@@ -542,19 +543,22 @@ async def readReplayJSON(replay_json: dict, args : argparse.Namespace) -> dict:
if url: if url:
result['url'] = replay_json['data']['view_url'] result['url'] = replay_json['data']['view_url']
#bu.debug('1')
for key in replay_summary_flds: for key in replay_summary_flds:
result[key] = replay_json['data']['summary'][key] result[key] = replay_json['data']['summary'][key]
result['battle_start_timestamp'] = int(replay_json['data']['summary']['battle_start_timestamp'])
#bu.debug('2')
result['allies'] = set() result['allies'] = set()
result['enemies'] = set() result['enemies'] = set()
btl_duration = 0 btl_duration = 0
btl_tier = 0 btl_tier = 0
for player in replay_json['data']['summary']['details']: for player in replay_json['data']['summary']['details']:
#bu.debug('3')
btl_duration = max(btl_duration, player['time_alive']) btl_duration = max(btl_duration, player['time_alive'])
player_tank_tier = wg.getTankData(player['vehicle_descr'], 'tier') player_tank_tier = wg.getTankData(player['vehicle_descr'], 'tier')
btl_tier = max(btl_tier, player_tank_tier) btl_tier = max(btl_tier, player_tank_tier)
#bu.debug('4')
if player['dbid'] == account_id: if player['dbid'] == account_id:
tmp = {} tmp = {}
for key in replay_details_flds: for key in replay_details_flds:
@@ -565,6 +569,7 @@ async def readReplayJSON(replay_json: dict, args : argparse.Namespace) -> dict:
else: else:
tmp['survived'] = 1 tmp['survived'] = 1
tmp['destroyed'] = 0 tmp['destroyed'] = 0
tmp['account_id'] = account_id tmp['account_id'] = account_id
tmp['tank_id'] = player['vehicle_descr'] tmp['tank_id'] = player['vehicle_descr']
tmp['tank_tier'] = player_tank_tier tmp['tank_tier'] = player_tank_tier
@@ -575,8 +580,11 @@ async def readReplayJSON(replay_json: dict, args : argparse.Namespace) -> dict:
elif player['dbid'] in replay_json['data']['summary']['allies']: elif player['dbid'] in replay_json['data']['summary']['allies']:
result['allies'].add(':'.join([ str(player['dbid']), str(player['vehicle_descr'])])) result['allies'].add(':'.join([ str(player['dbid']), str(player['vehicle_descr'])]))
#result['allies'].add([ player['dbid'], player['vehicle_descr'], int(replay_json['data']['summary']['battle_start_timestamp']) ])
else: else:
result['enemies'].add(':'.join([ str(player['dbid']), str(player['vehicle_descr'])])) result['enemies'].add(':'.join([ str(player['dbid']), str(player['vehicle_descr'])]))
#result['enemies'].add([ player['dbid'], player['vehicle_descr'], int(replay_json['data']['summary']['battle_start_timestamp']) ])
#bu.debug('6')
result['time_alive%'] = result['time_alive'] / btl_duration result['time_alive%'] = result['time_alive'] / btl_duration
result['battle_tier'] = btl_tier result['battle_tier'] = btl_tier
result['top_tier'] = 1 if (result['tank_tier'] == btl_tier) else 0 result['top_tier'] = 1 if (result['tank_tier'] == btl_tier) else 0
@@ -587,7 +595,7 @@ async def readReplayJSON(replay_json: dict, args : argparse.Namespace) -> dict:
except KeyError as err: except KeyError as err:
bu.error('Key :' + str(err) + ' not found') bu.error('Key :' + str(err) + ' not found')
except Exception as err: except Exception as err:
bu.error(err) bu.error(str(type(err)) + ' : ' + str(err))
return None return None
### main() ### main()
+406 -114
View File
@@ -1,11 +1,14 @@
#!/usr/bin/python3.7 #!/usr/bin/python3.7
import sys, os, json, asyncio, aiofiles, aiohttp, base64, urllib, inspect import sys, os, json, time, base64, urllib, inspect, hashlib
import asyncio, aiofiles, aiohttp, aiosqlite, lxml
from bs4 import BeautifulSoup
MAX_RETRIES= 3 MAX_RETRIES= 3
SLEEP = 2 SLEEP = 2
DEBUG = False DEBUG = False
VERBOSE = False VERBOSE = False
SILENT = False
UMASK= os.umask(0) UMASK= os.umask(0)
os.umask(UMASK) os.umask(UMASK)
@@ -19,16 +22,28 @@ os.umask(UMASK)
# return False # return False
def setDebug(debug: bool): def setDebug(debug: bool):
global DEBUG, VERBOSE global DEBUG, VERBOSE, SILENT
if debug != None: if debug != None:
DEBUG = debug DEBUG = debug
if DEBUG: VERBOSE = True if DEBUG:
VERBOSE = True
SILENT = False
def setVerbose(verbose: bool): def setVerbose(verbose: bool):
global VERBOSE global VERBOSE, SILENT
if verbose != None: if verbose != None:
VERBOSE = verbose VERBOSE = verbose
if VERBOSE:
SILENT = False
def setSilent(silent: bool):
global DEBUG, VERBOSE, SILENT
if silent != None:
SILENT = silent
if SILENT:
VERBOSE = False
DEBUG = False
def verbose(msg = ""): def verbose(msg = ""):
"""Print a message""" """Print a message"""
if VERBOSE: if VERBOSE:
@@ -36,8 +51,12 @@ def verbose(msg = ""):
return None return None
def printWaiter(force = False): def printWaiter(force = False):
if VERBOSE or force: if not SILENT or force:
print('.', end='', flush=True) print('.', end='', flush=True)
def printNewline(force = False):
if not SILENT or force:
print('', flush=True)
def debug(msg = ""): def debug(msg = ""):
"""print a conditional debug message""" """print a conditional debug message"""
@@ -55,12 +74,15 @@ def error(msg = ""):
caller = calframe[1][3] caller = calframe[1][3]
print('ERROR: ' + caller + '(): ' + msg) print('ERROR: ' + caller + '(): ' + msg)
return None return None
def NOW() -> int:
return int(time.time())
async def saveJSON(filename: str, json_data: dict, sort_keys = False) -> bool: async def saveJSON(filename: str, json_data: dict, sort_keys = False) -> bool:
"""Save JSON data into file""" """Save JSON data into file"""
try: try:
dirname = os.path.dirname(filename) dirname = os.path.dirname(filename)
if not os.path.isdir(dirname): if (dirname != '') and not os.path.isdir(dirname):
os.makedirs(dirname, 0o770-UMASK) os.makedirs(dirname, 0o770-UMASK)
async with aiofiles.open(filename,'w', encoding="utf8") as outfile: async with aiofiles.open(filename,'w', encoding="utf8") as outfile:
await outfile.write(json.dumps(json_data, ensure_ascii=False, indent=4, sort_keys=sort_keys)) await outfile.write(json.dumps(json_data, ensure_ascii=False, indent=4, sort_keys=sort_keys))
@@ -69,24 +91,40 @@ async def saveJSON(filename: str, json_data: dict, sort_keys = False) -> bool:
error(str(err)) error(str(err))
return False return False
async def getUrlJSON(session: aiohttp.ClientSession, url: str, chkJSONfunc = None) -> dict: async def readJSON(filename: str, chkJSONfunc = None):
try:
async with aiofiles.open(filename) as fp:
json_data = json.loads(await fp.read())
if (chkJSONfunc == None):
debug("JSON file content not checked: " + filename)
return json_data
elif chkJSONfunc(json_data):
debug("JSON File is valid: " + filename)
return json_data
else:
debug('JSON File has invalid content: ' + filename)
except Exception as err:
error('Unexpected error when reading file: ' + filename + ' : ' + str(type(err)) + ' : '+ str(err))
return None
async def getUrlJSON(session: aiohttp.ClientSession, url: str, chkJSONfunc = None, max_retries = MAX_RETRIES) -> dict:
"""Retrieve (GET) an URL and return JSON object""" """Retrieve (GET) an URL and return JSON object"""
try: try:
debug(url) debug(url)
## To avoid excessive use of servers ## To avoid excessive use of servers
for retry in range(1,MAX_RETRIES+1): for retry in range(1,max_retries+1):
async with session.get(url) as resp: async with session.get(url) as resp:
if resp.status == 200: if resp.status == 200:
debug('HTTP request OK') debug('HTTP request OK')
json_resp = await resp.json() json_resp = await resp.json()
if (chkJSONfunc == None) or chkJSONfunc(json_resp): if (chkJSONfunc == None) or chkJSONfunc(json_resp):
debug("Received valid JSON: " + str(json_resp)) #debug("Received valid JSON: " + str(json_resp))
return json_resp return json_resp
else: else:
debug('Received JSON error.') debug('Received JSON error. URL: ' + url)
if retry == MAX_RETRIES: if retry == max_retries:
raise aiohttp.ClientError('Request failed: ' + str(resp.status) + ' JSON Response: ' + str(json_resp) ) raise aiohttp.ClientError('Request failed: ' + str(resp.status) + ' JSON Response: ' + str(json_resp) )
verbose('Retrying URL [' + str(retry) + ']: ' + url ) verbose('Retrying URL [' + str(retry) + '/' + str(max_retries) + ']: ' + url )
await asyncio.sleep(SLEEP) await asyncio.sleep(SLEEP)
except aiohttp.ClientError as err: except aiohttp.ClientError as err:
@@ -99,14 +137,57 @@ async def getUrlJSON(session: aiohttp.ClientSession, url: str, chkJSONfunc = Non
error('Unexpected Exception: ' + str(err)) error('Unexpected Exception: ' + str(err))
return None return None
def bldDictHierarcy(d : dict, key : str, value) -> dict:
"""Build hierarcical dict based on multi-level key separated with """
try:
key_hier = key.split('.')
sub_key = key_hier.pop(0)
if len(key_hier) == 0:
d[sub_key] = value
elif sub_key not in d:
d[sub_key] = bldDictHierarcy({}, key_hier, value)
else:
d[sub_key] = bldDictHierarcy(d[sub_key], key_hier, value)
return d
except KeyError as err:
error('Key not found: ' + str(err))
except Exception as err:
error(str(err))
return None
class WG: class WG:
URL_WG_clanInfo = 'clans/info/?application_id=' URL_WG_clanInfo = 'clans/info/?application_id='
URL_WG_playerTankList = 'tanks/stats/?fields=tank_id%2Clast_battle_time&application_id=' #URL_WG_playerTankList = 'tanks/stats/?fields=tank_id%2Clast_battle_time&application_id='
URL_WG_playerTankList = 'tanks/stats/?fields=account_id%2Ctank_id%2Clast_battle_time%2Cbattle_life_time%2Call&application_id='
URL_WG_playerTankStats = 'tanks/stats/?application_id=' URL_WG_playerTankStats = 'tanks/stats/?application_id='
URL_WG_accountID = 'account/list/?fields=account_id%2Cnickname&application_id=' URL_WG_accountID = 'account/list/?fields=account_id%2Cnickname&application_id='
URL_WG_playerStats = 'account/info/?application_id=' URL_WG_playerStats = 'account/info/?application_id='
SQLITE_STATS_CACHE = '.stats_cache.db'
sql_create_player_stats_tbl = """CREATE TABLE IF NOT EXISTS player_stats (
account_id INTEGER NOT NULL,
date INTEGER NOT NULL,
stat TEXT,
value FLOAT
); """
sql_create_player_tank_stats_tbl = """CREATE TABLE IF NOT EXISTS player_tank_stats (
account_id INTEGER NOT NULL,
tank_id INTEGER DEFAULT NULL,
date INTEGER NOT NULL,
stat TEXT,
value FLOAT
); """
sql_select_player_stats = """SELECT value FROM player_stats ORDERBY date ASC
WHERE account_id = {} AND stat = {}
AND date >= {} LIMIT 1;"""
maps = { maps = {
"Random": "Random map", "Random": "Random map",
"amigosville": "Falls Creek", "amigosville": "Falls Creek",
@@ -177,7 +258,8 @@ class WG:
def __init__(self, WG_appID = None, tankopedia_fn = None, maps_fn = None): def __init__(self, WG_appID = None, tankopedia_fn = None, maps_fn = None):
self.WG_appID = WG_appID self.WG_appID = WG_appID
self.tanks = None self.tanks = None
if (tankopedia_fn != None): if (tankopedia_fn != None):
if os.path.exists(tankopedia_fn) and os.path.isfile(tankopedia_fn): if os.path.exists(tankopedia_fn) and os.path.isfile(tankopedia_fn):
try: try:
@@ -203,20 +285,37 @@ class WG:
self.session = None self.session = None
debug('WG aiohttp session NOT initiated') debug('WG aiohttp session NOT initiated')
self.cache = None
self.statsQ = None
self.statCacheTask = None
#if os.path.isfile(self.SQLITE_STATS_CACHE):
if False:
try:
self.cache = aiosqlite.connect('SQLITE_STATS_CACHE')
self.cache.execute(self.sql_create_player_stats_tbl)
self.cache.execute(self.sql_create_player_tank_stats_tbl)
self.cache.commit()
self.statsQ = asyncio.Queue()
self.statCacheTask = asyncio.create_task(self.statsSaver())
except Exception as err:
error(str(err))
async def close(self):
if self.statsQ != None:
await self.statsQ.join()
self.statCacheTask.cancel()
await asyncio.gather(*self.statCacheTask)
if self.session != None:
await self.session.close()
## Class methods ------------------------------ ## Class methods ------------------------------
@classmethod @classmethod
def getServer(cls, accountID: int) -> str: def getServer(cls, accountID: int) -> str:
"""Get Realm/server of an account based on account ID""" """Get Realm/server of an account based on account ID"""
# for server in cls.accountID_server.keys():
# if accountID in cls.accountID_server[server]:
# return server
# print('ERROR: AccountID not in range: ' + str(accountID))
# faster, but can fail for negatives
if accountID > 1e9: if accountID > 1e9:
if accountID > 2e9: if accountID > 2e9:
return 'asia' return 'asia'
@@ -240,35 +339,10 @@ class WG:
def getMapUserStrs(cls) -> str: def getMapUserStrs(cls) -> str:
return cls.maps.keys() return cls.maps.keys()
@classmethod
def chkJSONresponse(cls, json_resp: dict) -> bool:
try:
if ('status' in json_resp) and (json_resp['status'] == 'error'):
debug(str(json_resp['error']['code']) + ' : ' + json_resp['error']['message'] )
return False
return True
except:
error("JSON format error")
return False
@classmethod
def chkJSONgetAccountID(cls, json_resp: dict) -> bool:
try:
if (json_resp['status'] == 'ok'):
return True
else:
error_msg = str(json_resp['error']['code']) + ' : ' + json_resp['error']['message']
raise ValueError('Received JSON error: ' + error_msg)
except KeyError as err:
error('Key :' + str(err) + ' not found')
except Exception as err:
error(str(err))
return False
@classmethod @classmethod
def chkJSONcontent(cls, json_obj, check = None) -> bool: def chkJSONcontent(cls, json_obj, check = None) -> bool:
try: try:
if (check == 'player') and (not cls.chkJSONplayer(json_obj)): if (check == 'player') and (not cls.chkJSONplayerStats(json_obj)):
debug('Checking player JSON failed.') debug('Checking player JSON failed.')
return False return False
elif (check == 'tank') and (not cls.chkJSONtank(json_obj)): elif (check == 'tank') and (not cls.chkJSONtank(json_obj)):
@@ -289,56 +363,69 @@ class WG:
return True return True
@classmethod @classmethod
def chkJSONplayer(cls, playerInfo: dict) -> bool: def chkJSONstatus(cls, json_resp: dict) -> bool:
""""Check String for being a valid Player JSON file"""
try: try:
accountID = playerInfo[0]['account_id'] if json_resp['status'] == 'ok':
if int(accountID) > 0:
return True return True
except KeyError as err: elif json_resp['status'] == 'error':
error('Key :' + str(err) + ' not found') error(str(json_resp['error']['code']) + ' : ' + json_resp['error']['message'] )
except:
debug("JSON check failed")
return False
@classmethod
def chkJSONtank(cls, tankInfo: dict) -> bool:
""""Check String for being a valid Tank JSON file"""
try:
if int(tankInfo[0]['tank_id']) > 0:
return True
except KeyError as err:
error('Key :' + str(err) + ' not found')
except:
debug("JSON check failed")
return False
@classmethod
def chkJSONtankStats(cls, playerTankStat: dict) -> bool:
""""Check String for being a valid Tank JSON file"""
try:
if playerTankStat['status'] != 'ok':
return False return False
for acc in playerTankStat['data']: else:
if playerTankStat['data'][acc] == None: debug('Unknown status-code: ' + json_resp['status'])
return False return False
return True except KeyError as err:
error('No field found in JSON data: ' + str(err))
except:
error("JSON format error")
return False
@classmethod
def chkJSONgetAccountID(cls, json_resp: dict) -> bool:
try:
if cls.chkJSONstatus(json_resp):
if (json_resp['meta']['count'] > 0):
return True
except KeyError as err:
error('Key :' + str(err) + ' not found')
except Exception as err:
error(str(err))
return False
# @classmethod
# def chkJSONplayer(cls, json_resp: dict) -> bool:
# """"Check String for being a valid Player JSON file"""
# try:
# if cls.chkJSONstatus(json_resp):
# if int(json_resp[0]['account_id']) > 0:
# return True
# except KeyError as err:
# error('Key :' + str(err) + ' not found')
# except:
# debug("JSON check failed")
# return False
@classmethod
def chkJSONtank(cls, json_resp: dict) -> bool:
""""Check String for being a valid Tank JSON file"""
try:
if cls.chkJSONstatus(json_resp):
if int(json_resp[0]['tank_id']) > 0:
return True
except KeyError as err: except KeyError as err:
error('Key :' + str(err) + ' not found') error('Key :' + str(err) + ' not found')
except: except:
debug("JSON check failed") debug("JSON check failed")
return False return False
@classmethod @classmethod
def chkJSONplayerStats(cls, playerStat: dict) -> bool: def chkJSONplayerStats(cls, json_resp: dict) -> bool:
""""Check String for being a valid Tank JSON file""" """"Check String for being a valid Tank JSON file"""
try: try:
if playerStat['status'] != 'ok': if cls.chkJSONstatus(json_resp):
return False for acc in json_resp['data']:
for acc in playerStat['data']: if json_resp['data'][acc] != None:
if playerStat['data'][acc] == None: return True
return False
return True
except KeyError as err: except KeyError as err:
error('Key :' + str(err) + ' not found') error('Key :' + str(err) + ' not found')
except: except:
@@ -346,16 +433,16 @@ class WG:
return False return False
@classmethod @classmethod
def chkJSONtankList(cls, tankList: dict) -> bool: def chkJSONtankList(cls, json_resp: dict) -> bool:
""""Check String for being a valid Tank list JSON file""" """"Check String for being a valid Tank list JSON file"""
try: try:
accountID = next(iter(tankList['data'])) if cls.chkJSONstatus(json_resp):
tankID = tankList['data'][accountID][0]['tank_id'] accountID = next(iter(json_resp['data']))
if int(tankID) > 0: if int(json_resp['data'][accountID][0]['tank_id']) > 0:
debug('JSON tank list check OK') debug('JSON tank list check OK')
return True return True
except Exception as err: except Exception as err:
error('JSON check FAILED: ' + str(err)) error('JSON check FAILED: ' + str(type(err)) + ' : '+ str(err) + ' : ' + str(json_resp) )
return False return False
## Methods -------------------------------------------------- ## Methods --------------------------------------------------
@@ -380,7 +467,11 @@ class WG:
def getUrlPlayerTankStats(self, accountID, tankID, fields) -> str: def getUrlPlayerTankStats(self, accountID, tankID, fields) -> str:
server = self.getServer(accountID) server = self.getServer(accountID)
url = self.URL_WG_server[server] + self.URL_WG_playerTankStats + self.WG_appID + '&account_id=' + str(accountID) + '&tank_id=' + str(tankID) url = self.URL_WG_server[server] + self.URL_WG_playerTankStats + self.WG_appID + '&account_id=' + str(accountID) + '&tank_id=' + str(tankID)
return url + '&fields=' + '%2C'.join(fields) if (fields != None) and (len(fields) > 0):
field_str = '%2C' + '%2C'.join(fields)
else:
field_str = ''
return url + '&fields=tank_id' + field_str
def getUrlPlayerStats(self, accountID, fields) -> str: def getUrlPlayerStats(self, accountID, fields) -> str:
server = self.getServer(accountID) server = self.getServer(accountID)
@@ -421,7 +512,7 @@ class WG:
try: try:
debug('AccountID: ' + str(accountID) + ' TankID: ' + str(tankID)) debug('AccountID: ' + str(accountID) + ' TankID: ' + str(tankID))
url = self.getUrlPlayerTankStats(accountID, tankID, fields) url = self.getUrlPlayerTankStats(accountID, tankID, fields)
json_data = await getUrlJSON(self.session, url, self.chkJSONtankStats) json_data = await getUrlJSON(self.session, url, self.chkJSONtankList)
if json_data != None: if json_data != None:
debug('JSON Response received: ' + str(json_data)) debug('JSON Response received: ' + str(json_data))
return json_data['data'][str(accountID)][0] return json_data['data'][str(accountID)][0]
@@ -429,23 +520,80 @@ class WG:
error(err) error(err)
return None return None
async def getPlayerStats(self, accountID: int, fields: list) -> dict: async def getPlayerStats(self, accountID: int, fields: list, battle_time = None) -> dict:
"""Get player's global stats """ """Get player's global stats """
# debug('Started') # debug('Started')
if self.session == None: fields_missing = fields
error('Session must be initialized first') cached_stats = None
sys.exit(1) if self.cache != None:
try: cached_stats, fields_missing = await self.getCachedPlayerStats(accountID, fields)
debug('AccountID: ' + str(accountID) + ' Fields: ' + ', '.join(fields))
url = self.getUrlPlayerStats(accountID, fields) if fields_missing != None:
json_data = await getUrlJSON(self.session, url, self.chkJSONplayerStats) if self.session == None:
if json_data != None: error('Session must be initialized first')
debug('JSON Response received: ' + str(json_data)) sys.exit(1)
return json_data['data'][str(accountID)] try:
except Exception as err: debug('AccountID: ' + str(accountID) + ' Fields: ' + ', '.join(fields_missing))
error(err) url = self.getUrlPlayerStats(accountID, fields_missing)
json_data = await getUrlJSON(self.session, url, self.chkJSONplayerStats)
if json_data != None:
debug('JSON Response received: ' + str(json_data))
stats = json_data['data'][str(accountID)]
if self.statsQ != None:
self.statsQ.put([accountID, 'player', stats])
return self.mergePlayerStats(stats, cached_stats)
except Exception as err:
error('Unexpected Exception: ' + str(type(err)) + ' : ' + str(err))
else:
return cached_stats
return None return None
async def getCachedPlayerStats(self, accountID, fields):
if self.cache == None:
error('No stats cache available')
return None, fields
try:
res_tmp = {}
missing = []
for field in fields:
curr = self.cache.execute(self.sql_select_player_stats.format(accountID, field, NOW() ))
res_tmp[field] = curr.fetchone()
if res_tmp[field] == None:
res_tmp.pop(field, None)
missing.append(field)
else:
res_tmp[field] = res_tmp[field][0]
res = {}
for field in res_tmp.keys():
res = bldDictHierarcy(res, field, res_tmp[field])
if len(missing) == 0:
missing = None
if len(res.keys()) == 0:
res = None
return res, missing
except KeyError as err:
error('Key not found: ' + str(err))
except Exception as err:
error(str(err))
return None, fields
def mergePlayerStats(self, stats1: dict, stats2: dict) -> dict:
try:
if stats2 == None: return stats1
for keyA in stats2:
if keyA not in stats1:
stats1[keyA] = stats2[keyA]
else:
for keyB in stats2[keyA]:
stats1[keyA][keyB] = stats2[keyA][keyB]
return stats1
except KeyError as err:
error('Key not found: ' + str(err))
return None
def getTankData(self, tank_id: int, field: str): def getTankData(self, tank_id: int, field: str):
if self.tanks == None: if self.tanks == None:
return None return None
@@ -454,17 +602,147 @@ class WG:
except KeyError as err: except KeyError as err:
error('Key not found: ' + str(err)) error('Key not found: ' + str(err))
return None return None
async def statsSaver(self):
while True:
stats = await self.statsQ.get()
try:
return stats ## FIX
except Exception as err:
error(str(err))
return None
class WoTinspector:
URL_WI = 'https://wotinspector.com'
URL_REPLAYS = URL_WI + '/en/replays/sort/ut/page/'
URL_REPLAY_DL = '/en/replays/download/'
URL_REPLAY_UL = '/api/replay/upload?'
URL_REPLAY_INFO = '/api/replay/upload?details=full&key='
REPLAY_N = 1
def __init__(self):
self.session = aiohttp.ClientSession()
async def close(self):
if self.session != None:
await self.session.close()
async def getReplayInfo(self, replay_id: str):
return await getUrlJSON(self.session, self.URL_WI + self.URL_REPLAY_INFO + replay_id, self.chkJSONreplay, 0)
async def postReplay(self, data, filename = 'Replay', account_id = 0, title = 'Replay', priv = True, N = None):
try:
N = N if N != None else self.REPLAY_N
msg_str = 'Replay[' + str(N) + ']: '
self.REPLAY_N += 1
hash = hashlib.md5()
hash.update(data)
replay_id = hash.hexdigest()
## Testing if the replay has already been posted
json_resp = await self.getReplayInfo(replay_id)
if json_resp != None:
debug(msg_str + 'Already uploaded: ' + title)
return json_resp
params = {
'title' : title,
'private' : (1 if priv else 0),
'uploaded_by' : account_id,
'details' : 'full',
'key' : replay_id
}
url = self.URL_WI + self.URL_REPLAY_UL + urllib.parse.urlencode(params, quote_via=urllib.parse.quote)
#debug('URL: ' + url)
headers ={'Content-type': 'application/x-www-form-urlencoded'}
payload = { 'file' : (filename, base64.b64encode(data)) }
except Exception as err:
error(msg_str + 'Unexpected Exception: ' + str(type(err)) + ' : ' + str(err) )
return None
for retry in range(MAX_RETRIES):
debug(msg_str + 'Posting: ' + title + ' Try #: ' + str(retry + 1) + '/' + str(MAX_RETRIES) )
try:
async with self.session.post(url, headers=headers, data=payload) as resp:
debug(msg_str + 'HTTP response: '+ str(resp.status))
if resp.status == 200:
debug('HTTP POST 200 = Success. Reading response data')
json_resp = await resp.json()
if json_resp.get('status', None) == None:
error(msg_str +' : ' + title + ' : Received invalid JSON')
elif (json_resp['status'] == 'error'):
error(msg_str + json_resp['error']['message'] + ' : ' + title)
else:
debug('Response data read')
verbose(msg_str + title + ' posted')
return json_resp
else:
debug(msg_str + 'Got HTTP/' + str(resp.status) + ' Retrying... ' + str(retry))
await asyncio.sleep(SLEEP)
except Exception as err:
error(str(err))
await asyncio.sleep(SLEEP)
return None
@classmethod
def getUrlReplayListing(cls, page : int):
return cls.URL_REPLAYS + str(page) + '?vt=#filters'
@classmethod
def getReplayLinks(cls, doc: str):
"""Get replay download links from WoTinspector.com replay listing page"""
try:
soup = BeautifulSoup(doc, 'lxml')
links = soup.find_all('a')
replay_links = set()
for tag in links:
link = tag.get('href',None)
if (link is not None) and (link.find(cls.URL_REPLAY_DL) == 0 ):
replay_links.add(link)
debug('Adding replay link:' + link)
except Exception as err:
debug(str(err))
return replay_links
@classmethod
def getReplayID(cls, url):
return url.rsplit('/', 1)[-1]
@classmethod
def chkJSONreplay(cls, json_resp):
""""Check String for being a valid JSON file"""
try:
if ('status' in json_resp) and json_resp['status'] == 'ok' and ('data' in json_resp) and json_resp['data'] != None:
debug("JSON check OK")
return True
except KeyError as err:
error('Key :' + str(err) + ' not found')
except:
debug("JSON check failed: " + str(json_resp))
return False
class BlitzStars: class BlitzStars:
URL_BlitzStars = 'https://www.blitzstars.com' URL_BlitzStars = 'https://www.blitzstars.com'
URL_playerStats = URL_BlitzStars + '/api/playerstats' URL_playerStats = URL_BlitzStars + '/api/playerstats'
URL_playerTankStats = URL_BlitzStars + '/api/tanks' URL_playerTankStats = URL_BlitzStars + '/api/tanks'
URL_tankAverages = URL_BlitzStars + '/tankaverages.json' URL_tankAverages = URL_BlitzStars + '/tankaverages.json'
URL_activeplayers = URL_BlitzStars + '/api/playerstats/activeinlast30days' URL_activeplayers = URL_BlitzStars + '/api/playerstats/activeinlast30days'
URL_playersTankStats= URL_BlitzStars + '/api/top/forjylpah?' URL_playersTankStats= URL_BlitzStars + '/api/top/forjylpah?'
def __init__(self):
self.session = aiohttp.ClientSession()
async def close(self):
if self.session != None:
await self.session.close()
@classmethod @classmethod
def getUrlTankAvgs(cls): def getUrlTankAvgs(cls):
return cls.URL_tankAverages return cls.URL_tankAverages
@@ -478,9 +756,23 @@ class BlitzStars:
return cls.URL_playerTankStats + '/' + str(accountID) + '/' + str(tankID) return cls.URL_playerTankStats + '/' + str(accountID) + '/' + str(tankID)
@classmethod @classmethod
def getUrlPlayersTankStats(cls, tankID: int, account_list: list): def getUrlPlayersTankStats(cls, accountID: int):
return cls.URL_playersTankStats + 'tankId=' + str(tankID) + '&accountIds=' + ','.join(str(x) for x in account_list) return cls.URL_playersTankStats + '&accountId=' + str(accountID)
@classmethod @classmethod
def getUrlActivePlayers(cls): def getUrlActivePlayers(cls):
return cls.URL_activeplayers return cls.URL_activeplayers
@classmethod
def chkJSONtankStats(cls, json_resp):
""""Check String for being a valid JSON file"""
try:
# print(str(json_resp['data']))
if ('data' in json_resp):
debug("JSON check OK")
return True
except KeyError as err:
error('Key :' + str(err) + ' not found')
except:
debug("JSON check failed")
return False
+48 -66
View File
@@ -1,8 +1,10 @@
#!/usr/bin/python3.7 #!/usr/bin/python3.7
import json, argparse, inspect, sys, os, base64, aiohttp, urllib, asyncio, aiofiles, aioconsole, logging, re import json, argparse, inspect, sys, os, base64, aiohttp, urllib, asyncio, aiofiles, aioconsole
import logging, re, concurrent.futures
import blitzutils as bu import blitzutils as bu
from blitzutils import WG from blitzutils import WG
from blitzutils import WoTinspector
logging.getLogger("asyncio").setLevel(logging.DEBUG) logging.getLogger("asyncio").setLevel(logging.DEBUG)
@@ -19,10 +21,11 @@ SKIPPED_N = 0
WIurl='https://wotinspector.com/api/replay/upload?' WIurl='https://wotinspector.com/api/replay/upload?'
WG_appID = '81381d3f45fa4aa75b78a7198eb216ad' WG_appID = '81381d3f45fa4aa75b78a7198eb216ad'
wg = None wg = None
wi = None
async def main(argv): async def main(argv):
global wg global wg, wi
parser = argparse.ArgumentParser(description='Post replays(s) to WoTinspector.com and retrieve battle data') parser = argparse.ArgumentParser(description='Post replays(s) to WoTinspector.com and retrieve battle data')
parser.add_argument('--output', default='single', choices=['file', 'files', 'db'] , help='Select output mode: single/multiple files or database') parser.add_argument('--output', default='single', choices=['file', 'files', 'db'] , help='Select output mode: single/multiple files or database')
@@ -45,6 +48,7 @@ async def main(argv):
bu.setVerbose(False) bu.setVerbose(False)
wg = WG(WG_appID, args.tankopedia, args.mapfile) wg = WG(WG_appID, args.tankopedia, args.mapfile)
wi = WoTinspector()
if args.account != None: if args.account != None:
args.accountID = await wg.getAccountID(args.account) args.accountID = await wg.getAccountID(args.account)
@@ -81,6 +85,7 @@ async def main(argv):
finally: finally:
## Need to close the aiohttp.session since Python destructors do not support async methods... ## Need to close the aiohttp.session since Python destructors do not support async methods...
await wg.session.close() await wg.session.close()
await wi.close()
return None return None
@@ -104,6 +109,7 @@ async def mkReplayQ(queue : asyncio.Queue, files : list, title : str):
fn = fn[:-1] fn = fn[:-1]
if os.path.isfile(fn) and (p_replayfile.match(fn) != None): if os.path.isfile(fn) and (p_replayfile.match(fn) != None):
await queue.put(await mkQueueItem(fn, title)) await queue.put(await mkQueueItem(fn, title))
bu.debug('File added to queue: ' + fn)
elif os.path.isdir(fn): elif os.path.isdir(fn):
with os.scandir(fn) as dirEntry: with os.scandir(fn) as dirEntry:
for entry in dirEntry: for entry in dirEntry:
@@ -112,9 +118,12 @@ async def mkReplayQ(queue : asyncio.Queue, files : list, title : str):
if entry.is_file() and (p_replayfile.match(entry.name) != None): if entry.is_file() and (p_replayfile.match(entry.name) != None):
bu.debug(entry.name) bu.debug(entry.name)
await queue.put(await mkQueueItem(entry.path, title)) await queue.put(await mkQueueItem(entry.path, title))
bu.debug('File added to queue: ' + entry.path)
except Exception as err: except Exception as err:
bu.error(str(err)) bu.error(str(err))
bu.debug('File added to queue: ' + fn) else:
bu.error('File not found: ' + fn)
bu.debug('Finished') bu.debug('Finished')
return None return None
@@ -128,76 +137,49 @@ async def replayWorker(queue: asyncio.Queue, workerID: int, account_id: int, pri
"""Async Worker to process the replay queue""" """Async Worker to process the replay queue"""
global SKIPPED_N global SKIPPED_N
async with aiohttp.ClientSession() as session: while True:
while True: item = await queue.get()
item = await queue.get() filename = item[0]
filename = item[0] N = item[1]
N = item[1] title = item[2]
title = item[2]
replay_json_fn = filename + '.json'
try: replay_json_fn = filename + '.json'
if os.path.exists(replay_json_fn) and os.path.isfile(replay_json_fn): msg_str = 'Replay[' + str(N) + ']: '
async with aiofiles.open(replay_json_fn) as fp:
replay_json = json.loads(await fp.read())
if replay_json['status'] == 'ok':
bu.verbose('Replay[' + str(N) + ']: ' + title + ' has already been posted. Skipping.' )
queue.task_done()
SKIPPED_N += 1
continue
except Exception as err:
bu.error(err)
sys.exit(1)
# debug('Opening file [' + str(N) +']: ' + filename) #bu.debug(msg_str + replay_json_fn)
try:
#if os.path.exists(replay_json_fn) and os.path.isfile(replay_json_fn):
if os.path.isfile(replay_json_fn):
async with aiofiles.open(replay_json_fn) as fp:
replay_json = json.loads(await fp.read())
#if replay_json['status'] == 'ok':
if wi.chkJSONreplay(replay_json):
bu.verbose(msg_str + title + ' has already been posted. Skipping.' )
SKIPPED_N += 1
queue.task_done()
continue
except concurrent.futures.CancelledError as err:
raise err
except Exception as err:
bu.error(msg_str + 'Unexpected error: ' + str(type(err)) + ' : '+ str(err))
try:
#bu.debug('Opening file [' + str(N) +']: ' + filename)
async with aiofiles.open(filename,'rb') as fp: async with aiofiles.open(filename,'rb') as fp:
# debug('File opened: ' + filename)
filename = os.path.basename(filename) filename = os.path.basename(filename)
params = { bu.debug(msg_str + 'File: ' + filename)
'title' : title, json_resp = await wi.postReplay(await fp.read(), filename, account_id, title, priv, N)
'private' : (1 if priv else 0), if json_resp != None:
'uploaded_by' : account_id, if (await bu.saveJSON(replay_json_fn,json_resp)):
'details' : 'full' bu.debug(msg_str + 'Replay saved OK: ' + filename )
} else:
url = WIurl + urllib.parse.urlencode(params, quote_via=urllib.parse.quote) bu.error(msg_str + 'Error saving replay: ' + filename)
headers ={'Content-type': 'application/x-www-form-urlencoded'} except Exception as err:
payload = { 'file' : (filename, base64.b64encode(await fp.read())) } bu.error(msg_str + 'Unexpected Exception: ' + str(type(err)) + ' : ' + str(err) )
for retry in range(MAX_RETRIES): bu.debug(msg_str + 'Marking task done')
bu.verbose('Posting replay[' + str(N) +']: ' + title + ' Try #: ' + str(retry+ 1) + '/' + str(MAX_RETRIES) ) queue.task_done()
try:
async with session.post(url, headers=headers, data=payload) as resp:
bu.debug('Replay #' + str(N) + ' HTTP response: '+ str(resp.status))
if resp.status == 200:
bu.debug('HTTP POST 200 = Success. Reading response data')
json_resp = await resp.json()
if json_resp['status'] == 'error':
bu.error(json_resp['error']['message'] + ' : Replay[' + str(N) + ']' + title)
else:
bu.debug('Response data read')
bu.verbose('Replay[' + str(N) + ']: ' + title + ' posted')
await storeReplayJSON(replay_json_fn,json_resp)
break
else:
bu.debug('Replay[' + str(N)+ ']: Got HTTP/' + str(resp.status) + ' Retrying... ' + str(retry))
await asyncio.sleep(SLEEP)
except Exception as err:
bu.error(str(err))
await asyncio.sleep(SLEEP)
bu.debug('Marking task ' + str(N) + ' done')
queue.task_done()
return None return None
async def storeReplayJSON(filename: str, json_data : dict):
try:
async with aiofiles.open(filename, 'w', encoding='utf8') as outfile:
await outfile.write(json.dumps(json_data, ensure_ascii=False, indent=4))
return True
except Exception as err:
bu.error(str(err))
return False
return False
def getTitle(replayfile: str, title: str, i : int) -> str: def getTitle(replayfile: str, title: str, i : int) -> str:
global wg global wg