2020-01-18 19:47:17 +02:00
#!/usr/bin/env python3.8
2018-10-01 00:02:08 +08:00
2018-11-05 14:48:07 +02:00
import json , argparse , inspect , sys , os , base64 , aiohttp , urllib , asyncio , aiofiles , aioconsole
2020-07-01 17:59:34 +03:00
import logging , re , concurrent.futures , configparser , motor.motor_asyncio , ssl
2018-10-04 11:24:24 +08:00
import blitzutils as bu
2018-10-03 23:17:27 +08:00
from blitzutils import WG
2018-11-05 14:48:07 +02:00
from blitzutils import WoTinspector
2018-10-01 00:02:08 +08:00
logging . getLogger ( "asyncio" ) . setLevel ( logging . DEBUG )
2019-09-20 19:38:40 +03:00
FILE_CONFIG = 'blitzstats.ini'
2020-02-11 21:22:18 +02:00
# DB_C_REPLAYS = 'Replays'
2018-10-01 00:02:08 +08:00
2020-02-24 23:58:20 +02:00
DEBUG = False
2018-10-01 00:02:08 +08:00
VERBOSE = False
2020-02-24 23:58:20 +02:00
SLEEP = 2
2018-10-01 12:41:23 +08:00
MAX_RETRIES = 3
2019-08-16 22:53:06 +03:00
REPLAY_N = 0
SKIPPED_N = 0
ERROR_N = 0
2018-10-01 12:41:23 +08:00
WIurl = 'https://wotinspector.com/api/replay/upload?'
2018-10-01 21:35:49 +08:00
WG_appID = '81381d3f45fa4aa75b78a7198eb216ad'
wg = None
2018-11-05 14:48:07 +02:00
wi = None
2018-10-01 00:02:08 +08:00
async def main ( argv ):
2018-11-05 14:48:07 +02:00
global wg , wi
2020-07-10 11:39:19 +03:00
current_dir = os . getcwd ()
2019-12-17 21:25:54 +02:00
# set the directory for the script
os . chdir ( os . path . dirname ( sys . argv [ 0 ]))
2020-07-10 11:39:19 +03:00
2020-07-08 22:53:11 +03:00
# options defaults
2020-07-10 11:39:19 +03:00
OPT_WORKERS_N = 5
WG_ID = None
WG_ACCOUNT = None # format: nick@server, where server is either 'eu', 'ru', 'na', 'asia' or 'china'.
# China is not supported since WG API stats are not available there
WG_RATE_LIMIT = 10
2019-11-23 01:44:21 +02:00
2020-07-08 22:53:11 +03:00
try :
## Read config
if os . path . isfile ( FILE_CONFIG ):
2020-07-10 11:39:19 +03:00
bu . debug ( 'Reading config file: ' + FILE_CONFIG )
2020-07-08 22:53:11 +03:00
config = configparser . ConfigParser ()
config . read ( FILE_CONFIG )
try :
2020-07-10 11:39:19 +03:00
if 'OPTIONS' in config . sections ():
configOptions = config [ 'OPTIONS' ]
OPT_WORKERS_N = configOptions . getint ( 'opt_uploader_workers' , OPT_WORKERS_N )
2020-07-08 22:53:11 +03:00
except ( KeyError , configparser . NoSectionError ) as err :
2020-07-10 11:39:19 +03:00
bu . error ( exception = err )
2020-07-08 22:53:11 +03:00
try :
2020-07-10 11:39:19 +03:00
if 'WG' in config . sections ():
configWG = config [ 'WG' ]
# WG account id of the uploader:
# # Find it here: https://developers.wargaming.net/reference/all/wotb/account/list/
WG_ID = configWG . getint ( 'wg_id' , WG_ID )
WG_ACCOUNT = configWG . get ( 'wg_account' , WG_ACCOUNT )
## WG API Rules limit 10 request / sec. Higher rate of requests will return errors ==> extra delay
WG_RATE_LIMIT = configWG . getint ( 'wg_rate_limit' , WG_RATE_LIMIT )
2020-07-08 22:53:11 +03:00
except ( KeyError , configparser . NoSectionError ) as err :
2020-07-10 11:39:19 +03:00
bu . error ( exception = err )
2020-07-08 22:53:11 +03:00
except Exception as err :
bu . error ( exception = err )
parser = argparse . ArgumentParser ( description = 'Post replays(s) to WoTinspector.com and retrieve replay data as JSON' )
2018-10-01 00:02:08 +08:00
parser . add_argument ( '-id' , dest = 'accountID' , type = int , default = WG_ID , help = 'WG account_id' )
2020-07-10 11:39:19 +03:00
parser . add_argument ( '-a' , '--account' , dest = 'account' , type = str , default = WG_ACCOUNT , help = 'Uploader \' s WG account name. Format: ACCOUNT_NAME@SERVER' )
2018-10-01 00:02:08 +08:00
parser . add_argument ( '-t' , '--title' , type = str , default = None , help = 'Title for replays. Use NN for continous numbering. Default is filename-based numbering' )
parser . add_argument ( '-p' , '--private' , dest = "private" , action = 'store_true' , default = False , help = 'Set replays private on WoTinspector.com' )
2018-10-04 16:50:14 +08:00
parser . add_argument ( '--tankopedia' , type = str , default = 'tanks.json' , help = 'JSON file to read Tankopedia from. Default: "tanks.json"' )
parser . add_argument ( '--mapfile' , type = str , default = 'maps.json' , help = 'JSON file to read Blitz map names from. Default: "maps.json"' )
2018-10-01 00:02:08 +08:00
parser . add_argument ( '-d' , '--debug' , action = 'store_true' , default = False , help = 'Debug mode' )
2019-12-24 12:30:09 +02:00
parser . add_argument ( '-v' , '--verbose' , action = 'store_true' , default = False , help = 'Verbose mode' )
parser . add_argument ( '-s' , '--silent' , action = 'store_true' , default = False , help = 'Silent mode' )
2018-10-01 00:02:08 +08:00
parser . add_argument ( 'files' , metavar = 'FILE1 [FILE2 ...]' , type = str , nargs = '+' , help = 'Files to read. Use \' - \' for STDIN"' )
args = parser . parse_args ()
2019-12-24 02:52:56 +02:00
bu . set_verbose ( args . verbose )
2019-12-24 12:30:09 +02:00
bu . set_log_level ( args . silent , args . verbose , args . debug )
2018-10-17 11:02:39 +08:00
2018-10-04 11:24:24 +08:00
wg = WG ( WG_appID , args . tankopedia , args . mapfile )
2020-02-24 23:58:20 +02:00
wi = WoTinspector ( rate_limit = WG_RATE_LIMIT )
2019-12-18 00:19:21 +02:00
2018-10-01 21:35:49 +08:00
if args . account != None :
2019-12-24 02:52:56 +02:00
args . accountID = await wg . get_account_id ( args . account )
2018-10-04 11:24:24 +08:00
bu . debug ( 'WG account_id: ' + str ( args . accountID ))
2018-10-01 00:02:08 +08:00
2018-10-03 23:17:27 +08:00
if args . accountID == None :
args . accountID = 0
2018-10-01 00:02:08 +08:00
try :
2018-10-01 12:41:23 +08:00
queue = asyncio . Queue ()
2020-07-10 12:16:15 +03:00
# rebase file arguments due to moving the working directory to the script location
args . files = bu . rebase_file_args ( current_dir , args . files )
2020-07-10 12:07:03 +03:00
2018-10-01 00:02:08 +08:00
tasks = []
2018-10-01 12:41:23 +08:00
# Make replay Queue
2020-07-10 12:07:03 +03:00
tasks . append ( asyncio . create_task ( mkReplayQ ( queue , args . files , args . title )))
2018-10-01 12:41:23 +08:00
# Start tasks to process the Queue
2020-02-25 20:22:00 +02:00
for i in range ( OPT_WORKERS_N ):
2020-02-11 21:22:18 +02:00
tasks . append ( asyncio . create_task ( replayWorker ( queue , i , args . accountID , args . private )))
2018-10-04 11:24:24 +08:00
bu . debug ( 'Task ' + str ( i ) + ' started' )
2018-10-01 12:41:23 +08:00
2018-10-04 11:24:24 +08:00
bu . debug ( 'Waiting for the replay scanner to finish' )
2018-10-01 00:02:08 +08:00
await asyncio . wait ([ tasks [ 0 ]])
2018-10-04 11:24:24 +08:00
bu . debug ( 'Scanner finished. Waiting for workers to finish queue' )
2018-10-01 00:02:08 +08:00
await queue . join ()
2018-10-04 11:24:24 +08:00
bu . debug ( 'Cancelling workers' )
2018-10-01 00:02:08 +08:00
for task in tasks :
task . cancel ()
2018-10-04 11:24:24 +08:00
bu . debug ( 'Waiting for workers to cancel' )
2018-10-01 00:02:08 +08:00
await asyncio . gather ( * tasks , return_exceptions = True )
2020-07-10 11:39:19 +03:00
bu . verbose_std ( str ( REPLAY_N ) + ' replays: ' + str ( REPLAY_N - SKIPPED_N - ERROR_N ) + ' uploaded, ' + str ( SKIPPED_N ) + ' skipped, ' + str ( ERROR_N ) + ' errors' )
2018-10-01 21:35:49 +08:00
2018-10-01 00:02:08 +08:00
except KeyboardInterrupt :
print ( 'Ctrl-c pressed ...' )
sys . exit ( 1 )
2018-10-01 21:35:49 +08:00
finally :
## Need to close the aiohttp.session since Python destructors do not support async methods...
await wg . session . close ()
2018-11-05 14:48:07 +02:00
await wi . close ()
2018-10-01 00:02:08 +08:00
return None
async def mkReplayQ ( queue : asyncio . Queue , files : list , title : str ):
"""Create queue of replays to post"""
p_replayfile = re . compile ( '.* \\ .wotbreplay$' )
if files [ 0 ] == '-' :
2018-10-04 11:24:24 +08:00
bu . debug ( 'reading replay file list from STDIN' )
2018-10-01 00:02:08 +08:00
stdin , _ = await aioconsole . get_standard_streams ()
while True :
2018-10-04 11:24:24 +08:00
line = ( await stdin . readline ()) . decode ( 'utf-8' ) . rstrip ()
2018-10-01 00:02:08 +08:00
if not line :
break
else :
if ( p_replayfile . match ( line ) != None ):
2018-10-01 12:41:23 +08:00
await queue . put ( await mkQueueItem ( line , title ))
2018-10-01 00:02:08 +08:00
else :
for fn in files :
2018-11-05 18:15:51 +02:00
if fn . endswith ( '"' ):
2018-11-04 16:46:27 +02:00
fn = fn [: - 1 ]
2018-10-01 00:02:08 +08:00
if os . path . isfile ( fn ) and ( p_replayfile . match ( fn ) != None ):
2018-10-01 12:41:23 +08:00
await queue . put ( await mkQueueItem ( fn , title ))
2018-11-05 14:48:07 +02:00
bu . debug ( 'File added to queue: ' + fn )
2018-10-01 00:02:08 +08:00
elif os . path . isdir ( fn ):
with os . scandir ( fn ) as dirEntry :
for entry in dirEntry :
2018-11-04 13:25:58 +02:00
try :
bu . debug ( 'Found: ' + entry . name )
if entry . is_file () and ( p_replayfile . match ( entry . name ) != None ):
bu . debug ( entry . name )
await queue . put ( await mkQueueItem ( entry . path , title ))
2018-11-05 14:48:07 +02:00
bu . debug ( 'File added to queue: ' + entry . path )
2018-11-04 13:25:58 +02:00
except Exception as err :
bu . error ( str ( err ))
2018-11-05 14:48:07 +02:00
else :
bu . error ( 'File not found: ' + fn )
2018-10-04 11:24:24 +08:00
bu . debug ( 'Finished' )
2018-10-01 00:02:08 +08:00
return None
2020-02-24 23:58:20 +02:00
2018-10-01 12:41:23 +08:00
async def mkQueueItem ( filename : str , title : str ) -> list :
2018-10-01 00:02:08 +08:00
"""Make an item to replay queue"""
global REPLAY_N
REPLAY_N += 1
return [ filename , REPLAY_N , getTitle ( filename , title , REPLAY_N )]
2020-02-24 23:58:20 +02:00
2020-02-11 21:22:18 +02:00
async def replayWorker ( queue : asyncio . Queue , workerID : int , account_id : int , priv = False ):
2018-10-02 14:43:06 +08:00
"""Async Worker to process the replay queue"""
global SKIPPED_N
2019-08-16 22:53:06 +03:00
global ERROR_N
2018-10-02 14:43:06 +08:00
2018-11-05 14:48:07 +02:00
while True :
item = await queue . get ()
filename = item [ 0 ]
N = item [ 1 ]
title = item [ 2 ]
2018-10-02 14:43:06 +08:00
2018-11-05 14:48:07 +02:00
replay_json_fn = filename + '.json'
msg_str = 'Replay[' + str ( N ) + ']: '
2018-10-02 14:43:06 +08:00
2018-11-05 14:48:07 +02:00
#bu.debug(msg_str + replay_json_fn)
try :
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':
2019-12-24 02:52:56 +02:00
if wi . chk_JSON_replay ( replay_json ):
2020-02-24 23:58:20 +02:00
bu . verbose_std ( msg_str + title + ' has already been posted. Skipping.' )
2019-08-16 22:53:06 +03:00
else :
os . remove ( replay_json_fn )
2020-07-10 12:07:03 +03:00
bu . debug ( msg_str + "Replay JSON not valid: Deleting " + replay_json_fn , id = workerID )
2020-02-24 23:58:20 +02:00
SKIPPED_N += 1
queue . task_done ()
continue
2018-11-05 23:09:08 +02:00
except asyncio . CancelledError as err :
2018-11-05 14:48:07 +02:00
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)
2018-10-01 00:02:08 +08:00
async with aiofiles . open ( filename , 'rb' ) as fp :
filename = os . path . basename ( filename )
2018-11-05 14:48:07 +02:00
bu . debug ( msg_str + 'File: ' + filename )
2019-12-24 02:52:56 +02:00
json_resp = await wi . post_replay ( await fp . read (), filename , account_id , title , priv , N )
2018-11-05 14:48:07 +02:00
if json_resp != None :
2020-02-24 23:58:20 +02:00
if ( await bu . save_JSON ( replay_json_fn , json_resp )):
2020-07-10 11:39:19 +03:00
if wi . chk_JSON_replay ( json_resp ):
if not bu . debug ( msg_str + 'Replay saved OK: ' + filename ):
bu . verbose_std ( msg_str + title + ' posted' )
else :
2020-07-10 12:07:03 +03:00
bu . warning ( msg_str + 'Replay file is not valid: ' + filename )
2020-07-10 11:39:19 +03:00
ERROR_N += 1
2018-11-05 14:48:07 +02:00
else :
bu . error ( msg_str + 'Error saving replay: ' + filename )
2020-07-10 11:39:19 +03:00
ERROR_N += 1
else :
2020-07-10 12:07:03 +03:00
bu . error ( msg_str + 'Replay file is not valid: ' + filename )
2020-07-10 11:39:19 +03:00
ERROR_N += 1
2018-11-05 14:48:07 +02:00
except Exception as err :
bu . error ( msg_str + 'Unexpected Exception: ' + str ( type ( err )) + ' : ' + str ( err ) )
bu . debug ( msg_str + 'Marking task done' )
queue . task_done ()
2018-10-01 00:02:08 +08:00
return None
2019-09-20 19:38:40 +03:00
2018-10-01 00:02:08 +08:00
def getTitle ( replayfile : str , title : str , i : int ) -> str :
2018-10-01 21:35:49 +08:00
global wg
2018-10-01 00:02:08 +08:00
if title == None :
try :
filename = os . path . basename ( replayfile )
2018-11-04 13:25:58 +02:00
bu . debug ( filename )
2019-12-24 02:52:56 +02:00
map_usrStrs = wg . get_map_user_strs ()
tank_userStrs = wg . get_tank_user_strs ()
2019-08-16 22:53:06 +03:00
#p = re.compile('\\d{8}_\\d{4}_(.+)_(' + '|'.join(map_usrStrs) + ')(?:-\\d)?\\.wotbreplay$')
2019-09-09 23:30:58 +03:00
# update 6.2 changed the file name format. Bug fixed 2019-09-09 Jylpah
p = re . compile ( ' \\ d {8} _ \\ d {4} _.*?(' + '|' . join ( tank_userStrs ) + ')_(' + '|' . join ( map_usrStrs ) + ')(?:- \\ d)? \\ .wotbreplay$' )
2019-08-16 22:53:06 +03:00
2018-10-01 00:02:08 +08:00
m = p . match ( filename )
if ( m != None ):
2018-10-01 21:35:49 +08:00
if wg . tanks != None :
2018-11-04 13:25:58 +02:00
tank = m . group ( 1 )
if tank in wg . tanks [ 'userStr' ]:
tank = wg . tanks [ 'userStr' ][ tank ]
else :
bu . error ( 'Tank code: "' + tank + '" not found from Tankopedia (tanks.json)' )
2018-10-01 00:02:08 +08:00
else :
tank = m . group ( 1 )
2018-11-04 13:25:58 +02:00
blitz_map = m . group ( 2 )
if blitz_map in wg . maps :
blitz_map = wg . maps [ blitz_map ]
else :
bu . error ( 'Mapcode: "' + blitz_map + '" not found from map database (maps.json)' )
2018-10-01 00:02:08 +08:00
title = tank + ' @ ' + blitz_map
else :
title = re . sub ( ' \\ .wotbreplay$' , '' , filename )
except Exception as err :
2018-10-04 11:24:24 +08:00
bu . error ( err )
2018-10-01 00:02:08 +08:00
else :
2018-10-01 12:41:23 +08:00
title . replace ( 'NN' , str ( i ))
2018-10-01 00:02:08 +08:00
return title
2018-10-04 11:24:24 +08:00
### main() -------------------------------------------
2018-10-01 00:02:08 +08:00
if __name__ == "__main__" :
2020-07-01 17:42:13 +03:00
# To avoid 'Event loop is closed' RuntimeError due to compatibility issue with aiohttp
if sys . platform . startswith ( "win" ) and sys . version_info >= ( 3 , 8 ):
try :
from asyncio import WindowsSelectorEventLoopPolicy
except ImportError :
pass
else :
if not isinstance ( asyncio . get_event_loop_policy (), WindowsSelectorEventLoopPolicy ):
asyncio . set_event_loop_policy ( WindowsSelectorEventLoopPolicy ())
asyncio . run ( main ( sys . argv [ 1 :]), debug = False )