mirror of
https://github.com/Jylpah/blitz-tools.git
synced 2026-10-23 19:19:53 +02:00
wotbreplay_uploader.py optimization
This commit is contained in:
1 parent
30655b2af0
commit
4acf83ce1a
3 files changed
+500
-218
No files matched your search
+46
-38
@@ -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
@@ -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
@@ -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
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user