mirror of
https://github.com/Jylpah/blitz-tools.git
synced 2026-10-23 19:19:53 +02:00
Merge pull request #23 from Jylpah/master
Fixes: config file reading, DB, warning messages
This commit is contained in:
5 files changed
+206
-62
No files matched your search
@@ -3,7 +3,7 @@ Python scripts to analyze WoT Blitz replays and stats. All the scripts require [
|
|||||||
|
|
||||||
### Replays
|
### Replays
|
||||||
* [upload_wotb_replays.py](upload_wotb_replays.py): An asynchronous replay uploader.
|
* [upload_wotb_replays.py](upload_wotb_replays.py): An asynchronous replay uploader.
|
||||||
* [analyze_wotb_replays.py](analyze_wotb_replays.py): A tool to calculate statistics of a set of replays. Upload the replays first with [upload_wotb_replays.py](upload_wotb_replays.py) and then calculate the statistics.
|
* [analyze_wotb_replays.py](analyze_wotb_replays.py): A tool to calculate statistics of a set of replays. **Upload the replays first** with [upload_wotb_replays.py](upload_wotb_replays.py) and then calculate the statistics with analyze_wotb_replays.py based on the resulting **.json** files.
|
||||||
|
|
||||||
### Tank data
|
### Tank data
|
||||||
* [get_tankopedia.py](get_tankopedia.py): Get the lastest tankopedia data from wotinspector.com. The script fetches only a limited set of data. You may edit to your needs.
|
* [get_tankopedia.py](get_tankopedia.py): Get the lastest tankopedia data from wotinspector.com. The script fetches only a limited set of data. You may edit to your needs.
|
||||||
@@ -19,23 +19,51 @@ Python scripts to analyze WoT Blitz replays and stats. All the scripts require [
|
|||||||
### Quick instructions
|
### Quick instructions
|
||||||
|
|
||||||
1. Install [Python 3.8](https://www.python.org/downloads/) on to your computer/laptop
|
1. Install [Python 3.8](https://www.python.org/downloads/) on to your computer/laptop
|
||||||
1. Download all the files including the [maps.json](maps.json) and [tanks.json](tanks.json) files. The recommended way to do this is to use [GitHub Dekstop](https://desktop.github.com/) or do _git pull_
|
2. Install the required Python modules: motor, aiohttp, aioconsole, aiofiles, aiosqlite, beautifulsoup4, lxml, xmltodict, progress
|
||||||
1. Install the required Python modules: motor, aiohttp, aioconsole, aiofiles, aiosqlite, beautifulsoup4, lxml, xmltodict, progress
|
|
||||||
|
|
||||||
```
|
```
|
||||||
# Remember to run the command with a Python interpreter >= 3.7
|
# Remember to run the command with a Python interpreter >= 3.7
|
||||||
python -m pip install motor aiohttp aioconsole aiofiles aiosqlite beautifulsoup4 lxml xmltodict progress
|
python -m pip install motor aiohttp aioconsole aiofiles aiosqlite beautifulsoup4 lxml xmltodict progress
|
||||||
```
|
```
|
||||||
|
3. Download all the files including the [maps.json](maps.json) and [tanks.json](tanks.json) files. A recommened way is to install [GitHub Desktop](https://desktop.github.com/) and clone the repository via _git_. This makes it easy to receive the updates.
|
||||||
|
|
||||||
4. Upload your replays. Run _python.exe .\upload_wotb_replays.py --help_ (Windows) or _./upload_wotb_replays.py --help_ (Linux)
|
4. Upload your replays.
|
||||||
5. Analyze replays. Run _python.exe .\analyze_wotb_replays.py --help_ (Windows) or _./analyze_wotb_replays.py --help_ (Linux)
|
```
|
||||||
|
# Windows, run directly from Command Prompt or Powershell
|
||||||
|
# Check the options
|
||||||
|
python.exe .\upload_wotb_replays.py --help
|
||||||
|
|
||||||
|
# Syntax
|
||||||
|
python.exe .\upload_wotb_replays.py REPLAY_FILE.wotbreplay
|
||||||
|
|
||||||
|
# example
|
||||||
|
python.exe .\upload_wotb_replays.py 20200223_0122__jylpah_T-54_grossberg.wotbreplay
|
||||||
|
|
||||||
|
# Linux
|
||||||
|
./upload_wotb_replays.py --help
|
||||||
|
```
|
||||||
|
5. Analyze replays.
|
||||||
|
```
|
||||||
|
# Windows, run directly from Command Prompt or Powershell
|
||||||
|
python.exe .\analyze_wotb_replays.py --help
|
||||||
|
|
||||||
|
# Syntax
|
||||||
|
python.exe .\analyze_wotb_replays.py REPLAY_FILE.wotbreplay.json
|
||||||
|
|
||||||
|
# Example
|
||||||
|
python.exe .\analyze_wotb_replays.py 20200223_0122__jylpah_T-54_grossberg.wotbreplay.json
|
||||||
|
|
||||||
|
# Linux
|
||||||
|
./analyze_wotb_replays.py --help
|
||||||
|
```
|
||||||
|
|
||||||
### Blitz releases
|
### Blitz releases
|
||||||
|
|
||||||
[WoT Blitz releases](releases.md): launch dates and key content.
|
[WoT Blitz releases](releases.md): launch dates and key content.
|
||||||
|
|
||||||
### Notes
|
### Other
|
||||||
|
|
||||||
Even though the code has an option to use MongoDB, you should not bother with that. The MongoDB support is solely for my own purposes and you do not need MongoDB to run the scripts.
|
* Please remember to run the scripts from command line, not from the Python interpreter. On Windows open Command Prompt or Powershell.
|
||||||
|
* [GitHub Desktop](https://desktop.github.com/) is the recommended way of downloading the scripts you can update the scripts easily via git/GitHub Deskop. To prevent GitHub Desktop from uploading your other files to github.com, you can "ignore" the files by right-clicking a file on GitHub Desktop or by manually editing the _.gitignore_ file.
|
||||||
|
* Even though the code has an option to use MongoDB, you should not bother with that. The MongoDB support is solely for my own purposes and you do not need MongoDB to run the scripts.
|
||||||
|
|
||||||
You can reach me via email: [[email protected]](mailto:[email protected]) or [Discord](https://discordapp.com/): Jylpah#4662. Happy tanking and stats analysis!
|
You can reach me via email: [[email protected]](mailto:[email protected]) or [Discord](https://discordapp.com/): Jylpah#4662. Happy tanking and stats analysis!
|
||||||
+46
-26
@@ -27,6 +27,8 @@ REPLAY_I = 0
|
|||||||
STAT_TANK_BATTLE_MIN = 100
|
STAT_TANK_BATTLE_MIN = 100
|
||||||
BATTLE_TIME_BUCKET = 3600*24*14
|
BATTLE_TIME_BUCKET = 3600*24*14
|
||||||
|
|
||||||
|
RE_SRC_IS_DB = re.compile(r'^DB:')
|
||||||
|
|
||||||
## For different player stat functions (global stats, tank tier stats, etc)
|
## For different player stat functions (global stats, tank tier stats, etc)
|
||||||
# 1st function = for forming stat_id, 2nd for DB stats query, 3rd for WG API stats query
|
# 1st function = for forming stat_id, 2nd for DB stats query, 3rd for WG API stats query
|
||||||
STAT_FUNC = {
|
STAT_FUNC = {
|
||||||
@@ -74,9 +76,9 @@ replay_details_flds = [
|
|||||||
## Syntax: key == stat field in https://api.wotblitz.eu/wotb/tanks/stats/ (all.KEY)
|
## Syntax: key == stat field in https://api.wotblitz.eu/wotb/tanks/stats/ (all.KEY)
|
||||||
## Value array [ 'Stat Title', [ 0, data buckets ....], scaling_factor_for_bucket_values, 'print_format' ]
|
## Value array [ 'Stat Title', [ 0, data buckets ....], scaling_factor_for_bucket_values, 'print_format' ]
|
||||||
histogram_fields = {
|
histogram_fields = {
|
||||||
'wins' : [ 'Win rate', [0, .35, .40, .45, .5, .55, .60, .65, 1], 100, '{:.0f}%' ],
|
'wins' : [ 'Win rate', [0, .35, .40, .45, .48, .5, .52, .55, .60, .65, .70, 1], 100, '{:.0f}%' ],
|
||||||
'damage_dealt' : [ 'Avg. Dmg.', [0, .25e3, .5e3, 1e3, 1.5e3, 2e3,2.5e3,3e3,100e3], 1, '{:.0f}' ],
|
'damage_dealt' : [ 'Avg. Dmg.', [0, 200, 300, 400, 500, 600, 700, 800, 900, 1000, 1100, 1200, 1300, 1400, 1500, 1750, 2000, 2250, 2500, 2750, 3000, 100e3], 1, '{:.0f}' ],
|
||||||
'battles' : [ 'Battles', [0, 1e3, 3e3, 5e3, 10e3, 15e3, 25e3, 50e3, 5e7], .001, '{:.0f}k'] # battles is a mandatory stat to include
|
'battles' : [ 'Battles', [0, 1000, 2500, 5000, 7000, 10e3, 15e3, 25e3, 50e3, 5e7], .001, '{:.0f}k'] # battles is a mandatory stat to include
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -499,6 +501,7 @@ class ErrorCatchingArgumentParser(argparse.ArgumentParser):
|
|||||||
async def main(argv):
|
async def main(argv):
|
||||||
global wg, wi, WG_APP_ID
|
global wg, wi, WG_APP_ID
|
||||||
# set the directory for the script
|
# set the directory for the script
|
||||||
|
current_dir = os.getcwd()
|
||||||
os.chdir(os.path.dirname(sys.argv[0]))
|
os.chdir(os.path.dirname(sys.argv[0]))
|
||||||
|
|
||||||
## Default options:
|
## Default options:
|
||||||
@@ -508,8 +511,11 @@ async def main(argv):
|
|||||||
OPT_STAT_FUNC = 'player'
|
OPT_STAT_FUNC = 'player'
|
||||||
OPT_WORKERS_N = 10
|
OPT_WORKERS_N = 10
|
||||||
|
|
||||||
WG_ID = None
|
WG_ACCOUNT = None # format: nick@server, where server is either 'eu', 'ru', 'na', 'asia' or 'china'.
|
||||||
#WG_APP_ID = WG_APP_ID
|
# China is not supported since WG API stats are not available there
|
||||||
|
WG_ID = None # WG account_id in integer format.
|
||||||
|
# WG_ACCOUNT will be used to retrieve the account_id, but it can be set directly too
|
||||||
|
# WG_APP_ID = WG_APP_ID
|
||||||
WG_RATE_LIMIT = 10 ## WG standard. Do not edit unless you have your
|
WG_RATE_LIMIT = 10 ## WG standard. Do not edit unless you have your
|
||||||
## own server app ID, it will REDUCE the performance
|
## own server app ID, it will REDUCE the performance
|
||||||
|
|
||||||
@@ -521,7 +527,7 @@ async def main(argv):
|
|||||||
DB_AUTH = 'admin'
|
DB_AUTH = 'admin'
|
||||||
DB_NAME = 'BlitzStats'
|
DB_NAME = 'BlitzStats'
|
||||||
DB_USER = 'mongouser'
|
DB_USER = 'mongouser'
|
||||||
DB_PASSWD = "PASSWORD"
|
DB_PASSWD = 'PASSWORD'
|
||||||
DB_CERT = None
|
DB_CERT = None
|
||||||
DB_CA = None
|
DB_CA = None
|
||||||
|
|
||||||
@@ -532,6 +538,7 @@ async def main(argv):
|
|||||||
config.read(FILE_CONFIG)
|
config.read(FILE_CONFIG)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
if 'OPTIONS' in config.sections():
|
||||||
configOptions = config['OPTIONS']
|
configOptions = config['OPTIONS']
|
||||||
# WG account id of the uploader:
|
# WG account id of the uploader:
|
||||||
# # Find it here: https://developers.wargaming.net/reference/all/wotb/account/list/
|
# # Find it here: https://developers.wargaming.net/reference/all/wotb/account/list/
|
||||||
@@ -540,18 +547,21 @@ async def main(argv):
|
|||||||
OPT_HIST = configOptions.getboolean('opt_analyzer_hist', OPT_HIST)
|
OPT_HIST = configOptions.getboolean('opt_analyzer_hist', OPT_HIST)
|
||||||
OPT_STAT_FUNC = configOptions.get('opt_analyzer_stat_func', fallback=OPT_STAT_FUNC)
|
OPT_STAT_FUNC = configOptions.get('opt_analyzer_stat_func', fallback=OPT_STAT_FUNC)
|
||||||
OPT_WORKERS_N = configOptions.getint('opt_analyzer_workers', OPT_WORKERS_N)
|
OPT_WORKERS_N = configOptions.getint('opt_analyzer_workers', OPT_WORKERS_N)
|
||||||
except configparser.NoSectionError as err:
|
except (KeyError, configparser.NoSectionError) as err:
|
||||||
bu.error(exception=err)
|
bu.error(exception=err)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
if 'WG' in config.sections():
|
||||||
configWG = config['WG']
|
configWG = config['WG']
|
||||||
WG_ID = configWG.getint('wg_id', WG_ID)
|
WG_ID = configWG.getint('wg_id', WG_ID)
|
||||||
|
WG_ACCOUNT = configWG.get('wg_account', WG_ACCOUNT)
|
||||||
WG_APP_ID = configWG.get('wg_app_id', WG_APP_ID)
|
WG_APP_ID = configWG.get('wg_app_id', WG_APP_ID)
|
||||||
WG_RATE_LIMIT = configWG.getint('wg_rate_limit', WG_RATE_LIMIT)
|
WG_RATE_LIMIT = configWG.getint('wg_rate_limit', WG_RATE_LIMIT)
|
||||||
except configparser.NoSectionError as err:
|
except (KeyError, configparser.NoSectionError) as err:
|
||||||
bu.error(exception=err)
|
bu.error(exception=err)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
if 'DATABASE' in config.sections():
|
||||||
configDB = config['DATABASE']
|
configDB = config['DATABASE']
|
||||||
DB_SERVER = configDB.get('db_server', DB_SERVER)
|
DB_SERVER = configDB.get('db_server', DB_SERVER)
|
||||||
DB_PORT = configDB.getint('db_port', DB_PORT)
|
DB_PORT = configDB.getint('db_port', DB_PORT)
|
||||||
@@ -563,14 +573,14 @@ async def main(argv):
|
|||||||
DB_PASSWD = configDB.get('db_password', DB_PASSWD)
|
DB_PASSWD = configDB.get('db_password', DB_PASSWD)
|
||||||
DB_CERT = configDB.get('db_ssl_cert_file', DB_CERT)
|
DB_CERT = configDB.get('db_ssl_cert_file', DB_CERT)
|
||||||
DB_CA = configDB.get('db_ssl_ca_file', DB_CA)
|
DB_CA = configDB.get('db_ssl_ca_file', DB_CA)
|
||||||
except configparser.NoSectionError as err:
|
except (KeyError, configparser.NoSectionError) as err:
|
||||||
bu.error(exception=err)
|
bu.error(exception=err)
|
||||||
|
|
||||||
parser = ErrorCatchingArgumentParser(description='Analyze Blitz replay JSONs from WoTinspector.com')
|
parser = ErrorCatchingArgumentParser(description='Analyze Blitz replay JSON files from WoTinspector.com. Use \'upload_wotb_replays.py\' to upload the replay files first.')
|
||||||
|
|
||||||
parser.add_argument('--output', default='plain', choices=['plain', 'db'], help='Select output mode: plain text or database')
|
parser.add_argument('--output', default='plain', choices=['plain', 'db'], help='Select output mode: plain text or database')
|
||||||
parser.add_argument('-id', dest='account_id', type=int, default=WG_ID, help='WG account_id to analyze')
|
parser.add_argument('-id', dest='account_id', type=int, default=WG_ID, help='WG account_id to analyze')
|
||||||
parser.add_argument('-a', '--account', type=str, default=None, help='WG account nameto analyze. Format: ACCOUNT_NAME@SERVER')
|
parser.add_argument('-a', '--account', type=str, default=WG_ACCOUNT, help='WG account nameto analyze. Format: ACCOUNT_NAME@SERVER')
|
||||||
parser.add_argument('-x', '--extended', action='store_true', default=OPT_EXTENDED, help='Print Extended stats')
|
parser.add_argument('-x', '--extended', action='store_true', default=OPT_EXTENDED, help='Print Extended stats')
|
||||||
parser.add_argument('-X', '--extra_categories', choices=BattleRecordCategory.get_extra_categories(), default=None, nargs='*', help='Print Extended categories')
|
parser.add_argument('-X', '--extra_categories', choices=BattleRecordCategory.get_extra_categories(), default=None, nargs='*', help='Print Extended categories')
|
||||||
parser.add_argument('--hist', action='store_true', default=OPT_HIST, help='Print player histograms (WR/battles)')
|
parser.add_argument('--hist', action='store_true', default=OPT_HIST, help='Print player histograms (WR/battles)')
|
||||||
@@ -622,10 +632,12 @@ async def main(argv):
|
|||||||
bu.error("Could no initiate DB connection: Disabling DB", err)
|
bu.error("Could no initiate DB connection: Disabling DB", err)
|
||||||
args.db = False
|
args.db = False
|
||||||
pass
|
pass
|
||||||
|
else:
|
||||||
if not(args.db):
|
|
||||||
bu.debug('No DB in use')
|
bu.debug('No DB in use')
|
||||||
|
|
||||||
|
# rebase file arguments due to moving the working directory to the script location
|
||||||
|
args.files = bu.rebase_file_args(current_dir, args.files)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
replayQ = asyncio.Queue(maxsize=1000)
|
replayQ = asyncio.Queue(maxsize=1000)
|
||||||
reader_tasks = []
|
reader_tasks = []
|
||||||
@@ -1126,14 +1138,15 @@ async def mk_replayQ(queue : asyncio.Queue, args : argparse.Namespace, db : moto
|
|||||||
bu.debug(str(args.filters))
|
bu.debug(str(args.filters))
|
||||||
filters = json.loads(args.filters)
|
filters = json.loads(args.filters)
|
||||||
bu.debug(json.dumps(filters, indent=2))
|
bu.debug(json.dumps(filters, indent=2))
|
||||||
cursor = dbc.find(filters, {'_id': 0 })
|
cursor = dbc.find(filters)
|
||||||
else:
|
else:
|
||||||
# select all
|
# select all
|
||||||
cursor = dbc.find({}, {'_id': 0 })
|
cursor = dbc.find({})
|
||||||
bu.debug('Reading replays...')
|
bu.debug('Reading replays...')
|
||||||
async for replay_json in cursor:
|
async for replay_json in cursor:
|
||||||
#bu.debug(json.dumps(replay_json, indent=2))
|
_id = replay_json['_id']
|
||||||
await queue.put(await mk_readerQ_item(replay_json))
|
del(replay_json['_id'])
|
||||||
|
await queue.put(await mk_readerQ_item(replay_json, 'DB: _id = ' + _id))
|
||||||
Nreplays += 1
|
Nreplays += 1
|
||||||
bu.debug('All the matching replays have been read from the DB')
|
bu.debug('All the matching replays have been read from the DB')
|
||||||
except Exception as err:
|
except Exception as err:
|
||||||
@@ -1149,7 +1162,7 @@ async def mk_replayQ(queue : asyncio.Queue, args : argparse.Namespace, db : moto
|
|||||||
|
|
||||||
if (p_replayfile.match(line) != None):
|
if (p_replayfile.match(line) != None):
|
||||||
replay_json = await bu.open_JSON(line, wi.chk_JSON_replay)
|
replay_json = await bu.open_JSON(line, wi.chk_JSON_replay)
|
||||||
await queue.put(await mk_readerQ_item(replay_json))
|
await queue.put(await mk_readerQ_item(replay_json, line))
|
||||||
except Exception as err:
|
except Exception as err:
|
||||||
bu.error(exception=err)
|
bu.error(exception=err)
|
||||||
else:
|
else:
|
||||||
@@ -1160,7 +1173,7 @@ async def mk_replayQ(queue : asyncio.Queue, args : argparse.Namespace, db : moto
|
|||||||
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):
|
||||||
replay_json = await bu.open_JSON(fn, wi.chk_JSON_replay)
|
replay_json = await bu.open_JSON(fn, wi.chk_JSON_replay)
|
||||||
await queue.put(await mk_readerQ_item(replay_json))
|
await queue.put(await mk_readerQ_item(replay_json, fn))
|
||||||
bu.debug('File added to queue: ' + fn)
|
bu.debug('File added to queue: ' + fn)
|
||||||
Nreplays += 1
|
Nreplays += 1
|
||||||
elif os.path.isdir(fn):
|
elif os.path.isdir(fn):
|
||||||
@@ -1169,7 +1182,7 @@ async def mk_replayQ(queue : asyncio.Queue, args : argparse.Namespace, db : moto
|
|||||||
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)
|
||||||
replay_json = await bu.open_JSON(entry.path, wi.chk_JSON_replay)
|
replay_json = await bu.open_JSON(entry.path, wi.chk_JSON_replay)
|
||||||
await queue.put(await mk_readerQ_item(replay_json))
|
await queue.put(await mk_readerQ_item(replay_json, entry.name))
|
||||||
bu.debug('File added to queue: ' + entry.path)
|
bu.debug('File added to queue: ' + entry.path)
|
||||||
Nreplays += 1
|
Nreplays += 1
|
||||||
except Exception as err:
|
except Exception as err:
|
||||||
@@ -1178,17 +1191,21 @@ async def mk_replayQ(queue : asyncio.Queue, args : argparse.Namespace, db : moto
|
|||||||
return Nreplays
|
return Nreplays
|
||||||
|
|
||||||
|
|
||||||
async def mk_readerQ_item(replay_json) -> list:
|
async def mk_readerQ_item(replay_json, filename : str = None) -> list:
|
||||||
"""Make an item to replay queue"""
|
"""Make an item to replay queue"""
|
||||||
global REPLAY_N
|
global REPLAY_N
|
||||||
REPLAY_N +=1
|
REPLAY_N +=1
|
||||||
return [replay_json, REPLAY_N]
|
if filename == None:
|
||||||
|
return [replay_json, REPLAY_N, '' ]
|
||||||
|
elif RE_SRC_IS_DB.match(filename) != None:
|
||||||
|
return [replay_json, REPLAY_N, filename ]
|
||||||
|
else:
|
||||||
|
return [replay_json, REPLAY_N, os.path.basename(filename) ]
|
||||||
|
|
||||||
|
|
||||||
async def replay_reader(queue: asyncio.Queue, readerID: int, args : argparse.Namespace):
|
async def replay_reader(queue: asyncio.Queue, readerID: int, args : argparse.Namespace):
|
||||||
"""Async Worker to process the replay queue"""
|
"""Async Worker to process the replay queue"""
|
||||||
#global SKIPPED_N
|
#global SKIPPED_N
|
||||||
# account_id = args.account_id
|
|
||||||
results = []
|
results = []
|
||||||
playerstanks = set()
|
playerstanks = set()
|
||||||
try:
|
try:
|
||||||
@@ -1196,10 +1213,12 @@ async def replay_reader(queue: asyncio.Queue, readerID: int, args : argparse.Nam
|
|||||||
item = await queue.get()
|
item = await queue.get()
|
||||||
replay_json = item[0]
|
replay_json = item[0]
|
||||||
replayID = item[1]
|
replayID = item[1]
|
||||||
|
replay_file = item[2]
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
msg_str = 'Replay[' + str(replayID) + ']: '
|
||||||
if replay_json == None:
|
if replay_json == None:
|
||||||
bu.verbose('Replay[' + str(replayID) + ']: is empty. Skipping.' )
|
bu.warning(msg_str + 'Invalid replay. Skipping: ' + (replay_file if replay_file != None else '') )
|
||||||
#SKIPPED_N += 1
|
#SKIPPED_N += 1
|
||||||
queue.task_done()
|
queue.task_done()
|
||||||
continue
|
continue
|
||||||
@@ -1209,7 +1228,7 @@ async def replay_reader(queue: asyncio.Queue, readerID: int, args : argparse.Nam
|
|||||||
result = await read_replay_JSON(replay_json, args)
|
result = await read_replay_JSON(replay_json, args)
|
||||||
bu.print_progress()
|
bu.print_progress()
|
||||||
if result == None:
|
if result == None:
|
||||||
bu.debug('Replay[' + str(replayID) + ']: Invalid replay', id=readerID)
|
bu.warning(msg_str + 'Invalid replay' + (replay_file if replay_file != None else '') )
|
||||||
queue.task_done()
|
queue.task_done()
|
||||||
continue
|
continue
|
||||||
|
|
||||||
@@ -1238,7 +1257,8 @@ async def read_replay_JSON(replay_json: dict, args : argparse.Namespace) -> dict
|
|||||||
#db = args.db
|
#db = args.db
|
||||||
result = {}
|
result = {}
|
||||||
try:
|
try:
|
||||||
if (replay_json['data']['summary']['mastery_badge'] == None) or (replay_json['data']['summary']['battle_result'] < 0):
|
# bu.debug(str(replay_json))
|
||||||
|
if not wi.chk_JSON_replay(replay_json):
|
||||||
bu.debug('Invalid replay')
|
bu.debug('Invalid replay')
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|||||||
+11
-3
@@ -5,9 +5,17 @@
|
|||||||
#opt_analyzer_stat_func = player
|
#opt_analyzer_stat_func = player
|
||||||
|
|
||||||
[WG]
|
[WG]
|
||||||
|
|
||||||
## Find your WG account_id : https://developers.wargaming.net/reference/all/wotb/account/list/
|
## Find your WG account_id : https://developers.wargaming.net/reference/all/wotb/account/list/
|
||||||
#wg_id = 0
|
# wg_id = 0
|
||||||
## WG API limits requests / sec to 10 for client apps
|
|
||||||
|
## wg_account 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_account = your_nick@eu
|
||||||
|
|
||||||
|
## WG API limits requests / sec to 10 for mobile apps.
|
||||||
|
## You can register your "own" server app and
|
||||||
|
## upgrade the rate limit to 20 requests / sec.
|
||||||
## Check https://developers.wargaming.net/documentation/guide/getting-started/
|
## Check https://developers.wargaming.net/documentation/guide/getting-started/
|
||||||
#wg_app_id = XXXXXXXXXXX
|
# wg_app_id = XXXXXXXXXXX
|
||||||
|
|
||||||
+74
-17
@@ -9,7 +9,7 @@ from progress.counter import Counter
|
|||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
|
|
||||||
MAX_RETRIES= 3
|
MAX_RETRIES= 3
|
||||||
SLEEP = 3
|
SLEEP = 1.5
|
||||||
|
|
||||||
LOG_LEVELS = { 'silent': 0, 'normal': 1, 'verbose': 2, 'debug': 3 }
|
LOG_LEVELS = { 'silent': 0, 'normal': 1, 'verbose': 2, 'debug': 3 }
|
||||||
SILENT = 0
|
SILENT = 0
|
||||||
@@ -210,6 +210,13 @@ def verbose_std(msg = "", id = None) -> bool:
|
|||||||
return True
|
return True
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
def warning(msg = "", id = None) -> bool:
|
||||||
|
"""Print a warning message"""
|
||||||
|
if _log_level >= NORMAL:
|
||||||
|
_print_log_msg('', 'Warning: ' + msg, None, id)
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
def debug(msg = "", id = None, exception = None, force: bool = False) -> bool:
|
def debug(msg = "", id = None, exception = None, force: bool = False) -> bool:
|
||||||
"""print a conditional debug message"""
|
"""print a conditional debug message"""
|
||||||
if (_log_level >= DEBUG) or force:
|
if (_log_level >= DEBUG) or force:
|
||||||
@@ -341,6 +348,13 @@ def NOW() -> int:
|
|||||||
return int(time.time())
|
return int(time.time())
|
||||||
|
|
||||||
|
|
||||||
|
def rebase_file_args(current_dir, files):
|
||||||
|
"""REbase file command line params after moving working dir to the script's dir"""
|
||||||
|
if (files[0] == '-') or (files[0] == 'db:'):
|
||||||
|
return files
|
||||||
|
else:
|
||||||
|
return [ os.path.join(current_dir, fn) for fn in files ]
|
||||||
|
|
||||||
async def read_int_list(filename: str) -> list():
|
async def read_int_list(filename: str) -> list():
|
||||||
"""Read file to a list and return list of integers in the input file"""
|
"""Read file to a list and return list of integers in the input file"""
|
||||||
|
|
||||||
@@ -445,6 +459,40 @@ def bld_dict_hierarcy(d : dict, key : str, value) -> dict:
|
|||||||
error('Unexpected Exception', err)
|
error('Unexpected Exception', err)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
def get_JSON_keypath(keypath: str, key: str):
|
||||||
|
if keypath == None:
|
||||||
|
return key
|
||||||
|
else:
|
||||||
|
return '.'.join([keypath, key])
|
||||||
|
|
||||||
|
def get_JSON_value(json, key : str = None, keys : list = None, keypath = None):
|
||||||
|
if keys == None:
|
||||||
|
keys = key.split('.')
|
||||||
|
|
||||||
|
if len(keys) == 0:
|
||||||
|
return json
|
||||||
|
|
||||||
|
key = keys.pop(0)
|
||||||
|
if type(json) == dict:
|
||||||
|
if key in json:
|
||||||
|
return get_JSON_value(json[key], keys=keys, keypath=get_JSON_keypath(keypath, key))
|
||||||
|
else:
|
||||||
|
raise KeyError('Key: '+ get_JSON_keypath(keypath, key) + ' not found')
|
||||||
|
|
||||||
|
if type(json) == list:
|
||||||
|
p = re.compile(r'^\[(\d+)\]$')
|
||||||
|
m = p.match(key)
|
||||||
|
if len(m.groups()) != 1:
|
||||||
|
raise KeyError('Invalid key given: ' + get_JSON_keypath(keypath, key))
|
||||||
|
ndx = m.group(1)
|
||||||
|
try:
|
||||||
|
return get_JSON_value(json[ndx], keys=keys, keypath=get_JSON_keypath(keypath, key))
|
||||||
|
except IndexError:
|
||||||
|
raise KeyError('JSON array index out of range: ' + get_JSON_keypath(keypath, key))
|
||||||
|
|
||||||
|
raise KeyError('Key not found: ' + get_JSON_keypath(keypath, keys[0]))
|
||||||
|
|
||||||
|
|
||||||
## -----------------------------------------------------------
|
## -----------------------------------------------------------
|
||||||
#### Class SlowBar
|
#### Class SlowBar
|
||||||
## -----------------------------------------------------------
|
## -----------------------------------------------------------
|
||||||
@@ -1365,9 +1413,11 @@ class WoTinspector:
|
|||||||
URL_WI = 'https://replays.wotinspector.com'
|
URL_WI = 'https://replays.wotinspector.com'
|
||||||
URL_REPLAY_LIST = URL_WI + '/en/sort/ut/page/'
|
URL_REPLAY_LIST = URL_WI + '/en/sort/ut/page/'
|
||||||
URL_REPLAY_DL = URL_WI + '/en/download/'
|
URL_REPLAY_DL = URL_WI + '/en/download/'
|
||||||
|
URL_REPLAY_VIEW = URL_WI +'/en/view/'
|
||||||
URL_REPLAY_UL = 'https://api.wotinspector.com/replay/upload?'
|
URL_REPLAY_UL = 'https://api.wotinspector.com/replay/upload?'
|
||||||
URL_REPLAY_INFO = 'https://api.wotinspector.com/replay/upload?details=full&key='
|
URL_REPLAY_INFO = 'https://api.wotinspector.com/replay/upload?details=full&key='
|
||||||
URL_TANK_DB ="https://wotinspector.com/static/armorinspector/tank_db_blitz.js"
|
|
||||||
|
URL_TANK_DB = "https://wotinspector.com/static/armorinspector/tank_db_blitz.js"
|
||||||
|
|
||||||
REPLAY_N = 1
|
REPLAY_N = 1
|
||||||
|
|
||||||
@@ -1462,41 +1512,44 @@ class WoTinspector:
|
|||||||
error(msg_str + 'Unexpected Exception', err)
|
error(msg_str + 'Unexpected Exception', err)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
json_resp = None
|
||||||
for retry in range(MAX_RETRIES):
|
for retry in range(MAX_RETRIES):
|
||||||
debug(msg_str + 'Posting: ' + title + ' Try #: ' + str(retry + 1) + '/' + str(MAX_RETRIES) )
|
debug(msg_str + 'Posting: ' + title + ' Try #: ' + str(retry + 1) + '/' + str(MAX_RETRIES) )
|
||||||
try:
|
try:
|
||||||
async with self.session.post(url, headers=headers, data=payload) as resp:
|
async with self.session.post(url, headers=headers, data=payload) as resp:
|
||||||
debug(msg_str + 'HTTP response: '+ str(resp.status))
|
debug(msg_str + 'HTTP response: '+ str(resp.status))
|
||||||
if resp.status == 200:
|
if resp.status == 200:
|
||||||
debug('HTTP POST 200 = Success. Reading response data')
|
debug(msg_str + 'HTTP POST 200 = Success. Reading response data')
|
||||||
json_resp = await resp.json()
|
json_resp = await resp.json()
|
||||||
if json_resp.get('status', None) == None:
|
if self.chk_JSON_replay(json_resp):
|
||||||
error(msg_str +' : ' + title + ' : Received invalid JSON')
|
debug(msg_str + 'Response data read. Status OK')
|
||||||
elif (json_resp['status'] == 'ok'):
|
|
||||||
debug('Response data read. Status OK')
|
|
||||||
return json_resp
|
return json_resp
|
||||||
elif (json_resp['status'] == 'error'):
|
debug(msg_str +' : ' + title + ' : Receive invalid JSON')
|
||||||
error(msg_str + json_resp['error']['message'] + ' : ' + title)
|
|
||||||
else:
|
|
||||||
error(msg_str + ' Unspecified error: ' + title)
|
|
||||||
else:
|
else:
|
||||||
debug(msg_str + 'Got HTTP/' + str(resp.status))
|
debug(msg_str + 'Got HTTP/' + str(resp.status))
|
||||||
except Exception as err:
|
except Exception as err:
|
||||||
error(exception=err)
|
debug(msg_str, exception=err)
|
||||||
await asyncio.sleep(SLEEP)
|
await asyncio.sleep(SLEEP)
|
||||||
|
|
||||||
error(msg_str + ' Could not post replay: ' + title)
|
debug(msg_str + ' Could not post replay: ' + title)
|
||||||
return None
|
return json_resp
|
||||||
|
|
||||||
|
|
||||||
async def get_replay_listing(self, page: int = 0) -> aiohttp.ClientResponse:
|
async def get_replay_listing(self, page: int = 0) -> aiohttp.ClientResponse:
|
||||||
url = self.get_url_replay_listing(page)
|
url = self.get_url_replay_listing(page)
|
||||||
return await self.session.get(url)
|
return await self.session.get(url)
|
||||||
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_url_replay_listing(cls, page : int):
|
def get_url_replay_listing(cls, page : int):
|
||||||
return cls.URL_REPLAY_LIST + str(page) + '?vt=#filters'
|
return cls.URL_REPLAY_LIST + str(page) + '?vt=#filters'
|
||||||
|
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_url_replay_view(cls, replay_id):
|
||||||
|
return cls.URL_REPLAY_VIEW + replay_id
|
||||||
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_replay_links(cls, doc: str):
|
def get_replay_links(cls, doc: str):
|
||||||
"""Get replay download links from WoTinspector.com replay listing page"""
|
"""Get replay download links from WoTinspector.com replay listing page"""
|
||||||
@@ -1513,23 +1566,27 @@ class WoTinspector:
|
|||||||
error(exception=err)
|
error(exception=err)
|
||||||
return replay_links
|
return replay_links
|
||||||
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_replay_id(cls, url):
|
def get_replay_id(cls, url):
|
||||||
return url.rsplit('/', 1)[-1]
|
return url.rsplit('/', 1)[-1]
|
||||||
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def chk_JSON_replay(cls, json_resp):
|
def chk_JSON_replay(cls, json_resp):
|
||||||
""""Check String for being a valid JSON file"""
|
""""Check String for being a valid JSON file"""
|
||||||
try:
|
try:
|
||||||
if ('status' in json_resp) and json_resp['status'] == 'ok' and ('data' in json_resp) and json_resp['data'] != None:
|
if ('status' in json_resp) and json_resp['status'] == 'ok' and \
|
||||||
|
(get_JSON_value(json_resp, key='data.summary.exp_base') != None) :
|
||||||
debug("JSON check OK")
|
debug("JSON check OK")
|
||||||
return True
|
return True
|
||||||
except KeyError as err:
|
except KeyError as err:
|
||||||
error('Key not found', err)
|
debug('Replay JSON check failed', exception=err)
|
||||||
except:
|
except:
|
||||||
debug("JSON check failed: " + str(json_resp))
|
debug("Replay JSON check failed: " + str(json_resp))
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
class BlitzStars:
|
class BlitzStars:
|
||||||
|
|
||||||
URL_BLITZSTARS = 'https://www.blitzstars.com'
|
URL_BLITZSTARS = 'https://www.blitzstars.com'
|
||||||
|
|||||||
+39
-8
@@ -25,26 +25,48 @@ wi = None
|
|||||||
|
|
||||||
async def main(argv):
|
async def main(argv):
|
||||||
global wg, wi
|
global wg, wi
|
||||||
|
current_dir = os.getcwd()
|
||||||
# set the directory for the script
|
# set the directory for the script
|
||||||
os.chdir(os.path.dirname(sys.argv[0]))
|
os.chdir(os.path.dirname(sys.argv[0]))
|
||||||
|
|
||||||
|
# options defaults
|
||||||
|
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
|
||||||
|
|
||||||
|
try:
|
||||||
## Read config
|
## Read config
|
||||||
|
if os.path.isfile(FILE_CONFIG):
|
||||||
|
bu.debug('Reading config file: ' + FILE_CONFIG)
|
||||||
config = configparser.ConfigParser()
|
config = configparser.ConfigParser()
|
||||||
config.read(FILE_CONFIG)
|
config.read(FILE_CONFIG)
|
||||||
|
try:
|
||||||
|
if 'OPTIONS' in config.sections():
|
||||||
configOptions = config['OPTIONS']
|
configOptions = config['OPTIONS']
|
||||||
OPT_WORKERS_N = configOptions.getint('opt_uploader_workers', 5)
|
OPT_WORKERS_N = configOptions.getint('opt_uploader_workers', OPT_WORKERS_N)
|
||||||
|
except (KeyError, configparser.NoSectionError) as err:
|
||||||
|
bu.error(exception=err)
|
||||||
|
|
||||||
|
try:
|
||||||
|
if 'WG' in config.sections():
|
||||||
configWG = config['WG']
|
configWG = config['WG']
|
||||||
# WG account id of the uploader:
|
# WG account id of the uploader:
|
||||||
# # Find it here: https://developers.wargaming.net/reference/all/wotb/account/list/
|
# # Find it here: https://developers.wargaming.net/reference/all/wotb/account/list/
|
||||||
WG_ID = configWG.getint('wg_id', None)
|
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 API Rules limit 10 request / sec. Higher rate of requests will return errors ==> extra delay
|
||||||
WG_RATE_LIMIT = configWG.getint('wg_rate_limit', 10)
|
WG_RATE_LIMIT = configWG.getint('wg_rate_limit', WG_RATE_LIMIT)
|
||||||
|
except (KeyError, configparser.NoSectionError) as err:
|
||||||
|
bu.error(exception=err)
|
||||||
|
|
||||||
parser = argparse.ArgumentParser(description='Post replays(s) to WoTinspector.com and retrieve battle data')
|
except Exception as err:
|
||||||
|
bu.error(exception=err)
|
||||||
|
|
||||||
|
parser = argparse.ArgumentParser(description='Post replays(s) to WoTinspector.com and retrieve replay data as JSON')
|
||||||
parser.add_argument('-id', dest='accountID', type=int, default=WG_ID, help='WG account_id')
|
parser.add_argument('-id', dest='accountID', type=int, default=WG_ID, help='WG account_id')
|
||||||
parser.add_argument('-a', '--account', dest='account', type=str, default=None, help='Uploader\'s WG account name. Format: ACCOUNT_NAME@SERVER')
|
parser.add_argument('-a', '--account', dest='account', type=str, default=WG_ACCOUNT, help='Uploader\'s WG account name. Format: ACCOUNT_NAME@SERVER')
|
||||||
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('-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')
|
parser.add_argument('-p', '--private', dest="private", action='store_true', default=False, help='Set replays private on WoTinspector.com')
|
||||||
parser.add_argument('--tankopedia', type=str, default='tanks.json', help='JSON file to read Tankopedia from. Default: "tanks.json"')
|
parser.add_argument('--tankopedia', type=str, default='tanks.json', help='JSON file to read Tankopedia from. Default: "tanks.json"')
|
||||||
@@ -70,6 +92,8 @@ async def main(argv):
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
queue = asyncio.Queue()
|
queue = asyncio.Queue()
|
||||||
|
# rebase file arguments due to moving the working directory to the script location
|
||||||
|
args.files = bu.rebase_file_args(current_dir, args.files)
|
||||||
|
|
||||||
tasks = []
|
tasks = []
|
||||||
# Make replay Queue
|
# Make replay Queue
|
||||||
@@ -88,7 +112,7 @@ async def main(argv):
|
|||||||
task.cancel()
|
task.cancel()
|
||||||
bu.debug('Waiting for workers to cancel')
|
bu.debug('Waiting for workers to cancel')
|
||||||
await asyncio.gather(*tasks, return_exceptions=True)
|
await asyncio.gather(*tasks, return_exceptions=True)
|
||||||
bu.verbose(str(REPLAY_N) + ' replays: ' + str(REPLAY_N - SKIPPED_N - ERROR_N) + ' uploaded, ' + str(SKIPPED_N) + ' skipped, ' + str(ERROR_N) + ' errors')
|
bu.verbose_std(str(REPLAY_N) + ' replays: ' + str(REPLAY_N - SKIPPED_N - ERROR_N) + ' uploaded, ' + str(SKIPPED_N) + ' skipped, ' + str(ERROR_N) + ' errors')
|
||||||
|
|
||||||
except KeyboardInterrupt:
|
except KeyboardInterrupt:
|
||||||
print('Ctrl-c pressed ...')
|
print('Ctrl-c pressed ...')
|
||||||
@@ -170,7 +194,7 @@ async def replayWorker(queue: asyncio.Queue, workerID: int, account_id: int, pri
|
|||||||
bu.verbose_std(msg_str + title + ' has already been posted. Skipping.' )
|
bu.verbose_std(msg_str + title + ' has already been posted. Skipping.' )
|
||||||
else:
|
else:
|
||||||
os.remove(replay_json_fn)
|
os.remove(replay_json_fn)
|
||||||
bu.debug("Replay JSON not valid: Deleting " + replay_json_fn)
|
bu.debug(msg_str + "Replay JSON not valid: Deleting " + replay_json_fn, id=workerID)
|
||||||
SKIPPED_N += 1
|
SKIPPED_N += 1
|
||||||
queue.task_done()
|
queue.task_done()
|
||||||
continue
|
continue
|
||||||
@@ -187,11 +211,18 @@ async def replayWorker(queue: asyncio.Queue, workerID: int, account_id: int, pri
|
|||||||
json_resp = await wi.post_replay(await fp.read(), filename, account_id, title, priv, N)
|
json_resp = await wi.post_replay(await fp.read(), filename, account_id, title, priv, N)
|
||||||
if json_resp != None:
|
if json_resp != None:
|
||||||
if (await bu.save_JSON(replay_json_fn,json_resp)):
|
if (await bu.save_JSON(replay_json_fn,json_resp)):
|
||||||
|
if wi.chk_JSON_replay(json_resp):
|
||||||
if not bu.debug(msg_str + 'Replay saved OK: ' + filename):
|
if not bu.debug(msg_str + 'Replay saved OK: ' + filename):
|
||||||
bu.verbose_std(msg_str + title + ' posted')
|
bu.verbose_std(msg_str + title + ' posted')
|
||||||
|
else:
|
||||||
|
bu.warning(msg_str +'Replay file is not valid: ' + filename)
|
||||||
|
ERROR_N += 1
|
||||||
else:
|
else:
|
||||||
bu.error(msg_str + 'Error saving replay: ' + filename)
|
bu.error(msg_str + 'Error saving replay: ' + filename)
|
||||||
ERROR_N += 1
|
ERROR_N += 1
|
||||||
|
else:
|
||||||
|
bu.error(msg_str + 'Replay file is not valid: ' + filename)
|
||||||
|
ERROR_N += 1
|
||||||
except Exception as err:
|
except Exception as err:
|
||||||
bu.error(msg_str + 'Unexpected Exception: ' + str(type(err)) + ' : ' + str(err) )
|
bu.error(msg_str + 'Unexpected Exception: ' + str(type(err)) + ' : ' + str(err) )
|
||||||
bu.debug(msg_str + 'Marking task done')
|
bu.debug(msg_str + 'Marking task done')
|
||||||
|
|||||||
Reference in new issue
Block a user