43 Commits
Author SHA1 Message Date
Jylpah d59d677ee1 Update README.md 2024-01-28 21:54:25 +02:00
Jylpah 5082eaa6cf Update 10.4 added 2023-11-19 11:06:51 +02:00
Jylpah 8a6d6176e9 Merge pull request #39 from Jylpah/dev-fix-tankopedia
FIX: Make analyze_replays.py to work on Windows
2023-09-11 21:00:06 +03:00
jylpah d29f092bff remove unused code 2023-09-11 20:53:33 +03:00
jylpah 87d8b9a19d REFACTOR: FileQueue(), use Path for Win compat 2023-09-11 20:53:27 +03:00
Jylpah 06b81d79cb Merge pull request #38 from Jylpah/dev-fix-tankopedia
FIX: JSON search, REAFACTOR: blitzutils.Maps
2023-09-10 23:35:24 +03:00
jylpah de85e95f96 Fix replay search with deep dir trees 2023-09-10 23:33:55 +03:00
jylpah f1a876ea4b update 10.2 2023-09-10 23:33:24 +03:00
jylpah 5628cd4f7e WG(): Use blitzutils.Maps() 2023-09-10 23:33:12 +03:00
Jylpah 76bda7e4e7 Merge pull request #37 from Jylpah/dev-fix-tankopedia
Use new tankopedia format
2023-09-10 20:08:29 +03:00
jylpah b001bb44be remove legacy scripts, use Jylpah/blitz-replays 2023-09-10 20:02:59 +03:00
jylpah 555aa1ae3e Update 10.2, new tankopedia format 2023-09-10 20:01:07 +03:00
jylpah 2a6ee87673 Use blitzutils.WGApiTankopedia() 2023-09-10 20:00:23 +03:00
jylpah c27d7ce4fc rename legacy blitzutils 2023-09-10 19:58:39 +03:00
jylpah 82fa381720 add: blitz-utils 2023-09-10 18:03:46 +03:00
Jylpah da8efce128 Merge pull request #36 from Jylpah/master
Mix fixes
2023-09-10 17:48:11 +03:00
jylpah b5efc4bc9d commit mix fixes 2023-09-10 17:47:13 +03:00
jylpah 859c7aa293 update tanks.json 2023-09-10 17:47:13 +03:00
jylpah b5621991c1 ignore .venv 2023-09-10 17:47:13 +03:00
jylpah b273fcd9f3 sort_dict(): force input to str 2023-09-10 17:47:13 +03:00
Jylpah d4a35e6e8d Whitespace 2023-09-10 17:47:13 +03:00
Jylpah 3f4e9c42d1 FIX Alpenstadt 2023-09-10 17:47:13 +03:00
Jylpah e4f746ca38 Update 9.7 2023-09-10 17:47:13 +03:00
Jylpah ffc006ea05 update 9.6 2023-09-10 17:47:13 +03:00
Jylpah 2b28319271 fix typo 2023-07-09 20:46:11 +03:00
Jylpah 926ef711e7 Updates 10.0 and 10.1 2023-07-09 20:45:55 +03:00
Jylpah 55a1eb65c5 Update 4.10: replays were added 2023-05-01 21:16:44 +03:00
Jylpah 4ca2ad5b69 Update 9.8 2023-05-01 21:14:29 +03:00
Jylpah becc608f0c release 9.7 2023-02-15 14:18:25 +02:00
Jylpah 97e9cf5a54 Add releases 9.4 - 9.6 2023-01-11 00:41:44 +02:00
Jylpah f5e67798b4 get_tank_type(), get_tank_id(): remove async 2022-10-30 15:46:33 +02:00
Jylpah 2eced2eda0 Update 9.3 2022-10-30 15:13:28 +02:00
Jylpah 025a786bdd Merge pull request #35 from Jylpah/dev-fix-wi-rate-limit
Dev fix replays.wotinspector.com rate limit
2022-10-29 22:22:30 +03:00
Jylpah bca474218e WoTinspector.read_replay_id(): added back 2022-10-29 22:21:14 +03:00
Jylpah e48759ce79 ThrottledClientSession(), WoTinspector(): updated 2022-10-29 22:14:33 +03:00
Jylpah 5c694fd721 Typing + remove unsed imports 2022-10-29 22:13:52 +03:00
Jylpah 1f41901042 update 9.2 2022-10-09 00:18:30 +03:00
Jylpah d275914b32 Update .gitignore 2022-08-18 09:44:39 +03:00
Jylpah 9f8c82f434 Stream client support 2022-08-18 09:42:55 +03:00
Jylpah 3a8f065f35 Update 9.2 2022-08-18 09:42:32 +03:00
Jylpah 0cac200f06 Update 9.1 2022-07-14 11:25:38 +03:00
Jylpah 342b90bcf9 SSL -> TLS 2022-06-09 20:48:50 +03:00
Jylpah a9845b6fbb Update 9.0 2022-06-08 17:25:06 +03:00
12 changed files with 6359 additions and 12034 deletions

No files matched your search

Executable
+16
View File
@@ -0,0 +1,16 @@
__pycache__/
BlitzApp
tmp/
ssl/
*.ini
*.swp
*.code-workspace
*.sqlite3
.vscode/
*.sqlite3-journal
.gitattributes
export*
*.csv
!/stats/*.csv
!*.py
/.venv
+4
View File
@@ -1,3 +1,7 @@
## Unmaintained
Please use [blitz-replays](https://github.com/Jylpah/blitz-replays) instead. It is faster and more feature-rich.
# blitz-tools # blitz-tools
Python scripts to analyze WoT Blitz replays and stats. All the scripts require [Python 3.7](https://www.python.org/downloads/) or later. Python scripts to analyze WoT Blitz replays and stats. All the scripts require [Python 3.7](https://www.python.org/downloads/) or later.
+3101 -2512
View File
File diff suppressed because it is too large. Load diff
-2111
View File
File diff suppressed because it is too large. Load diff
Executable
+2715
View File
File diff suppressed because it is too large. Load diff
-225
View File
@@ -1,225 +0,0 @@
#!/usr/bin/env python3
## PYTHON VERSION MUST BE 3.7 OR HIGHER
# Extract tankopedia data in WG API JSON format from Blitz app files (Android APK unzipped)
import sys, argparse, json, os, inspect, aiohttp, asyncio, aiofiles, re, logging, time, xmltodict, collections, configparser
import blitzutils as bu
from blitzutils import WG
logging.getLogger("asyncio").setLevel(logging.DEBUG)
FILE_CONFIG = 'blitzstats.ini'
BLITZAPP_STRINGS='/assets/Data/Strings/en.yaml'
BLITZAPP_VEHICLES_DIR='/assets/Data/XML/item_defs/vehicles/'
BLITZAPP_VEHICLE_FILE='/list.xml'
wg = None
## main() -------------------------------------------------------------
async def main(argv):
global wg
# set the directory for the script
os.chdir(os.path.dirname(sys.argv[0]))
## Read config
BLITZAPP_FOLDER = '.'
try:
if os.path.isfile(FILE_CONFIG):
config = configparser.ConfigParser()
config.read(FILE_CONFIG)
configOptions = config['EXTRACT_TANKOPEDIA']
BLITZAPP_FOLDER = configOptions.get('blitz_app_dir', BLITZAPP_FOLDER)
except:
pass
parser = argparse.ArgumentParser(description='Extract Tankopedia data from Blitz game files')
parser.add_argument('blitz_app_base', type=str, nargs='?', metavar="BLITZAPP_FOLDER", default=BLITZAPP_FOLDER, help='Base dir of the Blitz App files')
parser.add_argument('tanks', type=str, default='tanks.json', nargs='?', metavar="TANKS_FILE", help='File to write Tankopedia')
parser.add_argument('maps', type=str, default='maps.json', nargs='?', metavar='MAPS_FILE', help='File to write map names')
arggroup = parser.add_mutually_exclusive_group()
arggroup.add_argument('-d', '--debug', action='store_true', default=False, help='Debug mode')
arggroup.add_argument('-v', '--verbose', action='store_true', default=False, help='Verbose mode')
arggroup.add_argument('-s', '--silent', action='store_true', default=False, help='Silent mode')
args = parser.parse_args()
bu.set_log_level(args.silent, args.verbose, args.debug)
wg = WG()
tasks = []
for nation in wg.NATION:
tasks.append(asyncio.create_task(extract_tanks(args.blitz_app_base, nation)))
tanklist = []
for tanklist_tmp in await asyncio.gather(*tasks):
tanklist.extend(tanklist_tmp)
tank_strs, map_strs = await read_user_strs(args.blitz_app_base)
json_data = None
userStrs = {}
tanks = {}
if os.path.exists(args.tanks):
try:
async with aiofiles.open(args.tanks) as infile:
json_data = json.loads(await infile.read())
userStrs = json_data['userStr']
tanks = json_data['data']
except Exception as err:
bu.error('Unexpected error when reading file: ' + args.tanks + ' : ' + str(err))
async with aiofiles.open(args.tanks, 'w', encoding="utf8") as outfile:
new_tanks, new_userStrs = await convert_tank_names(tanklist, tank_strs)
# merge old and new tankopedia
tanks.update(new_tanks)
userStrs.update(new_userStrs)
tankopedia = collections.OrderedDict()
tankopedia['status'] = 'ok'
tankopedia['meta'] = { "count": len(tanks) }
tankopedia['data'] = bu.sort_dict(tanks, number=True)
tankopedia['userStr'] = bu.sort_dict(userStrs)
bu.verbose_std('New tankopedia \'' + args.tanks + '\' contains ' + str(len(tanks)) + ' tanks')
bu.verbose_std('New tankopedia \'' + args.tanks + '\' contains ' + str(len(userStrs)) + ' tank strings')
await outfile.write(json.dumps(tankopedia, ensure_ascii=False, indent=4, sort_keys=False))
if args.maps != None:
maps = {}
if os.path.exists(args.maps):
try:
async with aiofiles.open(args.maps) as infile:
maps = json.loads(await infile.read())
except Exception as err:
bu.error('Unexpected error when reading file: ' + args.maps + ' : ' + str(err))
# merge old and new map data
maps.update(map_strs)
async with aiofiles.open(args.maps, 'w', encoding="utf8") as outfile:
bu.verbose_std('New maps file \'' + args.maps + '\' contains ' + str(len(maps)) + ' maps')
await outfile.write(json.dumps(maps, ensure_ascii=False, indent=4, sort_keys=True))
return None
async def extract_tanks(blitz_app_base : str, nation: str):
tanks = []
list_xml_file = blitz_app_base + BLITZAPP_VEHICLES_DIR + nation + BLITZAPP_VEHICLE_FILE
if not os.path.isfile(list_xml_file):
print('ERROR: cannot open ' + list_xml_file)
return None
bu.debug('Opening file: ' + list_xml_file + ' (Nation: ' + nation + ')')
async with aiofiles.open(list_xml_file, 'r', encoding="utf8") as f:
try:
tankList = xmltodict.parse(await f.read())
for data in tankList['root'].keys():
tank_xml = tankList['root'][data]
tank = dict()
tank['is_premium'] = issubclass(type(tank_xml['price']), dict)
tank['nation'] = nation
tank['tank_id'] = await get_tank_id(nation, int(tank_xml['id']))
tank['tier'] = int(tank_xml['level'])
tank['type'] = await get_tank_type(tank_xml['tags'])
tank['userStr'] = tank_xml['userString']
#bu.debug('Reading tank string: ' + tank['userStr'], force=True)
tanks.append(tank)
except Exception as err:
bu.error(err)
sys.exit(2)
return tanks
async def read_user_strs(blitz_app_base : str) -> dict:
"""Read user strings to convert map and tank names"""
tank_strs = {}
map_strs = {}
filename = blitz_app_base + BLITZAPP_STRINGS
bu.debug('Opening file: ' + filename + ' for reading UserStrings')
try:
async with aiofiles.open(filename, 'r', encoding="utf8") as f:
p_tank = re.compile('^"(#\\w+?_vehicles:.+?)": "(.+)"$')
p_map = re.compile('^"#maps:(.+?):.+?: "(.+?)"$')
async for l in f:
m = p_tank.match(l)
if m != None:
tank_strs[m.group(1)] = m.group(2)
m = p_map.match(l)
if m != None and m.group(2) != 'Macragge':
map_strs[m.group(1)] = m.group(2)
except Exception as err:
bu.error(err)
sys.exit(1)
return tank_strs, map_strs
async def convert_tank_names(tanklist : list, tank_strs: dict) -> dict:
"""Convert tank names for Tankopedia"""
tankopedia = {}
userStrs = {}
try:
for tank in tanklist:
tank['name'] = tank_strs[tank['userStr']]
#userStrs[tank['userStr'].split(':')[1]] = tank['name']
tank.pop('userStr', None)
tank_tmp = collections.OrderedDict()
for key in sorted(tank.keys()):
tank_tmp[key] = tank[key]
tankopedia[str(tank['tank_id'])] = tank_tmp
for tank_str in tank_strs:
skip = False
key = tank_str.split(':')[1]
bu.debug('Tank string: ' + key + ' = ' + tank_strs[tank_str])
re_strs = [r'^Chassis_', r'^Turret_', r'^_', r'_short$' ]
for re_str in re_strs:
p = re.compile(re_str)
if p.match(key):
skip = True
break
if skip:
continue
userStrs[key] = tank_strs[tank_str]
# sorting
tankopedia_sorted = collections.OrderedDict()
for tank_id in sorted(tankopedia.keys(), key=int):
tankopedia_sorted[str(tank_id)] = tankopedia[str(tank_id)]
userStrs_sorted = collections.OrderedDict()
for userStr in sorted(userStrs.keys()):
userStrs_sorted[userStr] = userStrs[userStr]
# bu.debug('Tank strings: ' + str(len(userStrs_sorted)))
except Exception as err:
bu.error(err)
sys.exit(1)
return tankopedia_sorted, userStrs_sorted
async def get_tank_id(nation: str, tank_id : int) -> int:
return (tank_id << 8) + (wg.NATION_ID[nation] << 4) + 1
async def get_tank_type(tagstr : str):
tags = tagstr.split(' ')
for t_type in wg.TANK_TYPE:
if tags[0] == t_type:
return t_type
return None
### main()
if __name__ == "__main__":
# 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=True)
asyncio.run(main(sys.argv[1:]))
-91
View File
@@ -1,91 +0,0 @@
#!/usr/bin/env python3
# Get Tankpedia from WoTinspector.com
import sys
import argparse
import json
import os
import inspect
import aiohttp
import asyncio
import re
import logging
from pathlib import Path
import blitzutils as bu
from blitzutils import WG
from blitzutils import WoTinspector
logging.getLogger("asyncio").setLevel(logging.DEBUG)
VERBOSE = False
DEBUG = False
tank_type = ['lightTank', 'mediumTank', 'heavyTank', 'AT-SPG']
# @asyncio.coroutine
async def get_tankopedia(filename: str):
url = "https://wotinspector.com/static/armorinspector/tank_db_blitz.js"
async with aiohttp.ClientSession() as client:
async with client.get(url) as r:
if r.status == 200:
WI_tank_db = await r.text()
WI_tank_db = WI_tank_db.split("\n")
else:
print('Error: Could not get valid HTTPS response. HTTP: ' + str(r.status) )
sys.exit(1)
tanks = {}
n = 0
p = re.compile('\\s*(\\d+):\\s{"en":"([^"]+)",.*?"tier":(\\d+), "type":(\\d), "premium":(\\d).*')
for line in WI_tank_db[1:-1]:
m = p.match(line)
tank = {}
tank['tank_id'] = int(m.group(1))
tank['name'] = m.group(2)
tank['tier'] = int(m.group(3))
tank['type'] = tank_type[int(m.group(4))]
tank['is_premium'] = (int(m.group(5)) == 1)
tanks[str(m.group(1))] = tank
n += 1
tankopedia = {}
tankopedia['status'] = "ok"
tankopedia['meta'] = {"count" : n}
tankopedia['data'] = tanks
print("Tankopedia has " + str(n) + " tanks in: " + filename)
with open(filename,'w') as outfile:
outfile.write(json.dumps(tankopedia, ensure_ascii=False, indent=4, sort_keys=True))
return None
async def main(argv):
# set the directory for the script
os.chdir(os.path.dirname(sys.argv[0]))
parser = argparse.ArgumentParser(description='Retrieve Tankopedia from WoTinspector.com')
parser.add_argument('--file', dest="outfile", help='Write Tankopedia to file')
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('-s', '--silent', action='store_true', default=False, help='Silent mode')
args = parser.parse_args()
bu.set_log_level(args.silent, args.verbose, args.debug)
wi = WoTinspector()
await wi.get_tankopedia(args.outfile)
await wi.close()
# main()
if __name__ == "__main__":
# 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)
+1 -60
View File
@@ -1,60 +1 @@
{ {"Random": {"key": "Random", "name": "Random map", "id": -1, "mode": 1}, "amigosville": {"key": "amigosville", "name": "Falls Creek", "id": -1, "mode": 1}, "amigosville_02": {"key": "amigosville_02", "name": "Falls Creek - Bridge", "id": -1, "mode": 3}, "amigosville_03": {"key": "amigosville_03", "name": "Falls Creek - Factory", "id": -1, "mode": 3}, "asia": {"key": "asia", "name": "Lost Temple", "id": -1, "mode": 1}, "canal": {"key": "canal", "name": "Canal", "id": -1, "mode": 1}, "canal_01": {"key": "canal_01", "name": "Canal - Factory", "id": -1, "mode": 3}, "canyon": {"key": "canyon", "name": "Canyon", "id": -1, "mode": 1}, "desert_train": {"key": "desert_train", "name": "Desert Sands", "id": -1, "mode": 1}, "desert_train_02": {"key": "desert_train_02", "name": "Desert Sands - Town", "id": -1, "mode": 3}, "desert_train_03": {"key": "desert_train_03", "name": "Desert Sands - Dunes", "id": -1, "mode": 3}, "erlenberg": {"key": "erlenberg", "name": "Middleburg", "id": -1, "mode": 1}, "erlenberg_01": {"key": "erlenberg_01", "name": "Middleburg - Town", "id": -1, "mode": 3}, "erlenberg_02": {"key": "erlenberg_02", "name": "Middleburg - Hill", "id": -1, "mode": 3}, "faust": {"key": "faust", "name": "Faust", "id": -1, "mode": 1}, "forgecity": {"key": "forgecity", "name": "New Bay", "id": -1, "mode": 1}, "fort": {"key": "fort", "name": "Fort Despair", "id": -1, "mode": 1}, "grossberg": {"key": "grossberg", "name": "Dynasty's Pearl", "id": -1, "mode": 1}, "himmelsdorf": {"key": "himmelsdorf", "name": "Himmelsdorf", "id": -1, "mode": 1}, "holland": {"key": "holland", "name": "Molendijk", "id": -1, "mode": 1}, "holmeisk": {"key": "holmeisk", "name": "Wasteland", "id": -1, "mode": 1}, "iceworld": {"key": "iceworld", "name": "Everfrost", "id": -1, "mode": 2}, "idle": {"key": "idle", "name": "Yukon", "id": -1, "mode": 1}, "italy": {"key": "italy", "name": "Vineyards", "id": -1, "mode": 1}, "karelia": {"key": "karelia", "name": "Rockfield", "id": -1, "mode": 1}, "karieri": {"key": "karieri", "name": "Copperfield", "id": -1, "mode": 1}, "lake": {"key": "lake", "name": "Mirage", "id": -1, "mode": 1}, "lumber": {"key": "lumber", "name": "Horrorstadt", "id": -1, "mode": 1}, "lumber_01": {"key": "lumber_01", "name": "Alpenstadt - Town", "id": -1, "mode": 3}, "malinovka": {"key": "malinovka", "name": "Winter Malinovka", "id": -1, "mode": 1}, "medvedkovo": {"key": "medvedkovo", "name": "Dead Rail", "id": -1, "mode": 1}, "medvedkovo_02": {"key": "medvedkovo_02", "name": "Dead Rail - Valley", "id": -1, "mode": 3}, "medvedkovo_03": {"key": "medvedkovo_03", "name": "Dead Rail - Railroad", "id": -1, "mode": 3}, "milbase": {"key": "milbase", "name": "Yamato Harbor", "id": -1, "mode": 1}, "milbase_02": {"key": "milbase_02", "name": "Yamato Harbor - Battleship", "id": -1, "mode": 3}, "milbase_03": {"key": "milbase_03", "name": "Yamato Harbor - Hills", "id": -1, "mode": 3}, "milbase_04": {"key": "milbase_04", "name": "Yamato Harbor - Center", "id": -1, "mode": 3}, "milbase_05": {"key": "milbase_05", "name": "Yamato Harbor - Dock", "id": -1, "mode": 3}, "moon": {"key": "moon", "name": "Sea of Tranquility", "id": -1, "mode": 2}, "mountain": {"key": "mountain", "name": "Black Goldville", "id": -1, "mode": 1}, "neptune": {"key": "neptune", "name": "Normandy", "id": -1, "mode": 1}, "neptune_01": {"key": "neptune_01", "name": "Normandy - Hills", "id": -1, "mode": 3}, "neptune_02": {"key": "neptune_02", "name": "Normandy - Beach", "id": -1, "mode": 3}, "north": {"key": "north", "name": "North", "id": -1, "mode": 1}, "ordeal": {"key": "ordeal", "name": "Trial by Fire", "id": -1, "mode": 1}, "plant": {"key": "plant", "name": "Ghost Factory", "id": -1, "mode": 1}, "pliego": {"key": "pliego", "name": "Castilla", "id": -1, "mode": 1}, "port": {"key": "port", "name": "Port Bay", "id": -1, "mode": 1}, "rift": {"key": "rift", "name": "Hellas", "id": -1, "mode": 1}, "rock": {"key": "rock", "name": "Mayan Ruins", "id": -1, "mode": 1}, "rudniki": {"key": "rudniki", "name": "Mines", "id": -1, "mode": 1}, "rudniki_01": {"key": "rudniki_01", "name": "Mines - Hill", "id": -1, "mode": 3}, "rudniki_02": {"key": "rudniki_02", "name": "Mines - Village", "id": -1, "mode": 3}, "rudniki_03": {"key": "rudniki_03", "name": "Mines - Center", "id": -1, "mode": 3}, "savanna": {"key": "savanna", "name": "Oasis Palms", "id": -1, "mode": 1}, "skit": {"key": "skit", "name": "Naval Frontier", "id": -1, "mode": 1}, "test": {"key": "test", "name": "World of Ducks", "id": -1, "mode": 2}, "tutorial": {"key": "tutorial", "name": "Proving Grounds", "id": -1, "mode": 0}}
"Random": "Random map",
"amigosville": "Falls Creek",
"amigosville_02": "Falls Creek - Bridge",
"amigosville_03": "Falls Creek - Factory",
"asia": "Lost Temple",
"canal": "Canal",
"canal_01": "Canal - Factory",
"canyon": "Canyon",
"desert_train": "Desert Sands",
"desert_train_02": "Desert Sands - Town",
"desert_train_03": "Desert Sands - Dunes",
"erlenberg": "Middleburg",
"erlenberg_01": "Middleburg - Town",
"erlenberg_02": "Middleburg - Hill",
"faust": "Faust",
"forgecity": "New Bay",
"fort": "Fort Despair",
"grossberg": "Dynasty's Pearl",
"himmelsdorf": "Himmelsdorf",
"holland": "Molendijk",
"holmeisk": "Wasteland",
"iceworld": "Everfrost",
"idle": "Yukon",
"italy": "Vineyards",
"karelia": "Rockfield",
"karieri": "Copperfield",
"lake": "Mirage",
"lumber": "Alpenstadt",
"lumber_01": "Alpenstadt - Town",
"malinovka": "Winter Malinovka",
"medvedkovo": "Dead Rail",
"medvedkovo_02": "Dead Rail - Valley",
"medvedkovo_03": "Dead Rail - Railroad",
"milbase": "Yamato Harbor",
"milbase_02": "Yamato Harbor - Battleship",
"milbase_03": "Yamato Harbor - Hills",
"milbase_04": "Yamato Harbor - Center",
"milbase_05": "Yamato Harbor - Dock",
"moon": "Sea of Tranquility",
"mountain": "Black Goldville",
"neptune": "Normandy",
"neptune_01": "Normandy - Hills",
"neptune_02": "Normandy - Beach",
"north": "North",
"ordeal": "Trial by Fire",
"plant": "Ghost Factory",
"pliego": "Castilla",
"port": "Port Bay",
"rift": "Hellas",
"rock": "Mayan Ruins",
"rudniki": "Mines",
"rudniki_01": "Mines - Hill",
"rudniki_02": "Mines - Village",
"rudniki_03": "Mines - Center",
"savanna": "Oasis Palms",
"skit": "Naval Frontier",
"test": "World of Ducks",
"tutorial": "Proving Grounds"
}
+106 -91
View File
@@ -1,93 +1,108 @@
# WoT Blitz releases # WoT Blitz releases
| Release | Date | Notes | Inaccurate? | | Release | Date | Content updates |
|:-------:|:--------:|-------------|-----| |:-------:|:--------:|-------------|
| 8.9 | 2022-03-23 | "Free" camo resupply, VK 72.01 supplies | | | 10.4 | 2023-11-15 | Threesome-platoons for fun modes, camos |
| 8.8 | 2022-02-16 | Molendijk map | | | 10.3 | 2023-09-27 | Polish Medium Tanks, Tier X rebalance, map updates: Ghost Factory, Desert Sands, Super-cool legenday camos|
| 8.7 | 2022-01-19 | Soviet Light Tanks | | | 10.2 | 2023-08-16 | PBR Camos and bug fixes ... |
| 8.6 | 2021-12-15 | Joining Light and Medium Tank classes in MM | | | 10.1 | 2023-07-05 | PBR Camos and ? |
| 8.5 | 2021-11-17 | New national crew voices, economy update | | | 10.0 | 2023-05-24 | Lagoon map, Czechoslovakian heavies, tanks start shells loaded |
| 8.4 | 2021-10-20 | Polish heavy tanks | | | 9.8 | 2023-03-22 | F\*cked up The Quick Commands Wheel, camos |
| 8.3 | 2021-09-16 | FV4005, FV4022, VK 91.01 (P) and WZ-121 legendary camos, -5 deg gun depression to WZ-120 | | | 9.7 | 2023-02-15 | Chinese light tanks |
| 8.2 | 2021-08-18 | Ghost Factory map, XP/credits nerf for inactive players, new store UI | | | 9.6 | 2023-01-11 | Finally fixing game sounds for players with Genelec studio monitors, more gold camos |
| 8.1 | 2021-07-15 | Armor highlighting updated | | | 9.5 | 2022-12-14 | Camos and ?|
| 8.0 | 2021-06-08 | American experimental heavies (Yoh-line), PBR camos | | | 9.4 | 2022-11-09 | Italian TD line |
| 7.9 | 2021-04-27 | Mines rework, new tutorial, colorblind mode, JgPz E100 legendary camo | | | 9.3 | 2022-10-04 | Nothing, except legandary camos and utterly incomprehensible shell icons |
| 7.8 | 2021-03-24 | Mid-tier balance changes, Grille line new consumables | | | 9.2 | 2022-08-17 | New shot sounds and shell hit marks, vehicle restoration from the game client |
| 7.7 | 2021-02-24 | Czechoslovakian medium tanks | | | 9.1 | 2022-07-14 | Releases updated via DLC, graphics updates for Object 140, Patton 46 |
| 7.6 | 2021-01-27 | AMC 13 90, TOG II buffs, free replay camera | | | 9.0 | 2022-06-08 | Japanies heavies, Castilla graphics update, new garage |
| 7.5 | 2020-12-09 | ATGM removal, Batchat, Kpz70, T-2020 buff | | | 8.10 | 2022-04-28 | New battle results screen, new "Operation" mechanics |
| 7.4 | 2020-11-12 | TD buffs, Clan missions | | | 8.9 | 2022-03-23 | "Free" camo resupply, VK 72.01 supplies |
| 7.3 | 2020-10-08 | Swedish HT line | | | 8.8 | 2022-02-16 | Molendijk map |
| 7.2 | 2020-08-19 | Balance changes, added tier X ammo capa, new aiming reticles | | | 8.7 | 2022-01-19 | Soviet Light Tanks |
| 7.1 | 2020-07-15 | New Yukon map, IS-6, KV-4, Patton 48 armor buff, Grille DPM buff, SP IC, AMZ 13 75, M41 Bulldog mobility buff | | | 8.6 | 2021-12-15 | Joining Light and Medium Tank classes in MM |
| 7.0 | 2020-06-17 | British LTs, graphics improvements, ATGM nerf, Sheridan nerf, map fixes | | | 8.5 | 2021-11-17 | New national crew voices, economy update |
| 6.10 | 2020-04-29 | T92E1 nerf, Tier V-IX heavy HP buff, Battle Pass, map fixes | | | 8.4 | 2021-10-20 | Polish heavy tanks |
| 6.9 | 2020-03-25 | Japanese TDs, Tier X heavy HP buff | | | 8.3 | 2021-09-16 | FV4005, FV4022, VK 91.01 (P) and WZ-121 legendary camos, -5 deg gun depression to WZ-120 |
| 6.8 | 2020-02-19 | ATGM nerf, Hellas map | | | 8.2 | 2021-08-18 | Ghost Factory map, XP/credits nerf for inactive players, new store UI |
| 6.7 | 2020-01-15 | ATGM US Lights | | | 8.1 | 2021-07-15 | Armor highlighting updated |
| 6.6 | 2019-12-18 | T-62A + Leo buffs | | | 8.0 | 2021-06-08 | American experimental heavies (Yoh-line), PBR camos |
| 6.5 | 2019-11-21 | New UI | | | 7.9 | 2021-04-27 | Mines rework, new tutorial, colorblind mode, JgPz E100 legendary camo |
| 6.4 | 2019-10-16 | Halloween map, mid-tier HT buff... | | | 7.8 | 2021-03-24 | Mid-tier balance changes, Grille line new consumables |
| 6.3 | 2019-09-18 | European line | | | 7.7 | 2021-02-24 | Czechoslovakian medium tanks |
| 6.2 | 2019-08-14 | New MM | | | 7.6 | 2021-01-27 | AMC 13 90, TOG II buffs, free replay camera |
| 6.1 | 2019-07-10 | Autoloader buff | | | 7.5 | 2020-12-09 | ATGM removal, Batchat, Kpz70, T-2020 buff |
| 6.0 | 2019-06-05 | UK TD line #2 | | | 7.4 | 2020-11-12 | TD buffs, Clan missions |
| 5.10 | 2019-04-24 | new missions | | | 7.3 | 2020-10-08 | Swedish HT line |
| 5.9 | 2019-03-27 | New shell tracers, minor fixes | | | 7.2 | 2020-08-19 | Balance changes, added tier X ammo capa, new aiming reticles |
| 5.8 | 2019-02-21 | Chinese TD line | | | 7.1 | 2020-07-15 | New Yukon map, IS-6, KV-4, Patton 48 armor buff, Grille DPM buff, SP IC, AMZ 13 75, M41 Bulldog mobility buff |
| 5.7 | 2019-01-16 | Avatars | | | 7.0 | 2020-06-17 | British LTs, graphics improvements, ATGM nerf, Sheridan nerf, map fixes |
| 5.6 | 2018-12-05 | Normandy map | | | 6.10 | 2020-04-29 | T92E1 nerf, Tier V-IX heavy HP buff, Battle Pass, map fixes |
| 5.5 | 2018-11-13 | Low-tier clean-up | | | 6.9 | 2020-03-25 | Japanese TDs, Tier X heavy HP buff |
| 5.4 | 2018-10-10 | MM "rigging" adjusted | | | 6.8 | 2020-02-19 | ATGM nerf, Hellas map |
| 5.3 | 2018-09-12 | MM "rigging" started, Newbie queue added | | | 6.7 | 2020-01-15 | ATGM US Lights |
| 5.2 | 2018-08-22 | Faust map | | | 6.6 | 2019-12-18 | T-62A + Leo buffs |
| 5.1 | 2018-07-31 | New MM: vehicle class matching | | | 6.5 | 2019-11-21 | New UI |
| 5.0 | 2018-06-30 | Vineyeards map update, Spare Parts killed | | | 6.4 | 2019-10-16 | Halloween map, mid-tier HT buff... |
| 4.10 | 2018-05-24 | Alpenstadt map | | | 6.3 | 2019-09-18 | European line |
| 4.9 | 2018-04-25 | Chinese heavy tanks | | | 6.2 | 2019-08-14 | New MM |
| 4.8 | 2018-03-28 | Choice of battle modes, crew skills accelerator | | | 6.1 | 2019-07-10 | Autoloader buff |
| 4.7 | 2018-02-28 | New minimap | | | 6.0 | 2019-06-05 | UK TD line #2 |
| 4.6 | 2018-01-31 | Chinese nation | | | 5.10 | 2019-04-24 | new missions |
| 4.5 | 2017-12-19 | Dynasty's Pearl map | | | 5.9 | 2019-03-27 | New shell tracers, minor fixes |
| 4.4 | 2017-11-09 | New British tanks | | | 5.8 | 2019-02-21 | Chinese TD line |
| 4.3 | 2017-10-10 | Level 7 screw skills | | | 5.7 | 2019-01-16 | Avatars |
| 4.2 | 2017-09-12 | French LTs, Naval Frontier map | | | 5.6 | 2018-12-05 | Normandy map |
| 4.1 | 2017-08-16 | New markers | | | 5.5 | 2018-11-13 | Low-tier clean-up :-( |
| 4.0 | 2017-07-13 | Rating battles | Y | | 5.4 | 2018-10-10 | MM "rigging" adjusted |
| 3.10 | 2017-06-15 | Fixes | Y | | 5.3 | 2018-09-12 | MM "rigging" started, Newbie queue added |
| 3.9 | 2017-05-19 | Military Honor, Mayan Ruins | Y | | 5.2 | 2018-08-22 | Faust map |
| 3.8 | 2017-04-24 | Spare Parts, New 9-slot Equipment system | Y | | 5.1 | 2018-07-31 | New MM: vehicle class matching |
| 3.7 | 2017-03-22 | Balance changes, Tournaments | Y | | 5.0 | 2018-06-30 | Vineyeards map update, Spare Parts removed ! |
| 3.6 | 2017-02-16 | Vehicle Enrichment | Y | | 4.10 | 2018-05-24 | Replays, Alpenstadt map |
| 3.5 | 2017-01-13 | Boosters, Dead rail update | Y | | 4.9 | 2018-04-25 | Chinese heavy tanks |
| 3.4 | 2016-11-28 | Canyon map, USA T57 line | | | 4.8 | 2018-03-28 | Choice of battle modes, crew skills accelerator |
| 3.3 | 2016-11-01 | Updated Mines map | | | 4.7 | 2018-02-28 | New minimap |
| 3.2 | 2016-10-04 | Copperfield update | | | 4.6 | 2018-01-31 | Chinese nation |
| 3.1 | 2016-09-03 | Clan tournaments, Yamato Harbor map | Y | | 4.5 | 2017-12-19 | Dynasty's Pearl map |
| 3.0 | 2016-07-20 | Supremacy mode, Falls Creek update | Y | | 4.4 | 2017-11-09 | New British tanks |
| 2.11 | 2016-06-28 | Vineyeards map, Oasis Palms update, German paper TDs | | | 4.3 | 2017-10-10 | Level 7 screw skills |
| 2.10 | 2016-06-07 | Rockfield map update, Tank birthdays, high-pen gun for IS-6 | | | 4.2 | 2017-09-12 | French LTs, Naval Frontier map |
| 2.9 | 2016-05-03 | Training rooms, +/-1 Matchmaker | Y | | 4.1 | 2017-08-16 | New markers |
| 2.8 | 2016-04-04 | New Russian TD line, new consumables system | Y | | 4.0 | 2017-07-13 | Rating battles (date uncertain) |
| 2.7 | 2016-03-07 | Mirage map | Y | | 3.10 | 2017-06-15 | Fixes (date uncertain) |
| 2.6 | 2016-02-10 | German LTs, shooting through obstacles | Y | | 3.9 | 2017-05-19 | Military Honor, Mayan Ruins (date uncertain) |
| 2.5 | 2016-01-11 | Camouflages, Clan supply | Y | | 3.8 | 2017-04-24 | Spare Parts, New 9-slot Equipment system (date uncertain) |
| 2.4 | 2015-12-09 | Additional Japanese tanks, windows version | Y | | 3.7 | 2017-03-22 | Balance changes, Tournaments (date uncertain) |
| 2.3 | 2015-11-12 | Canal map | Y | | 3.6 | 2017-02-16 | Vehicle Enrichment (date uncertain) |
| 2.2 | 2015-10-05 | Desert Sands map update | Y | | 3.5 | 2017-01-13 | Boosters, Dead rail update (date uncertain) |
| 2.1 | 2015-09-07 | UK TDs, Clan Chat | Y | | 3.4 | 2016-11-28 | Canyon map, USA T57 line |
| 2.0 | 2015-08-05 | Clans, Oasis Palms updated | | | 3.3 | 2016-11-01 | Updated Mines map |
| 1.11 | 2015-07-13 | KV-2, KV-13, Sherman Jumbo | Y | | 3.2 | 2016-10-04 | Copperfield update |
| 1.10 | 2015-06-11 | Port Bay map | Y | | 3.1 | 2016-09-03 | Clan tournaments, Yamato Harbor map (date uncertain) |
| 1.9 | 2015-05-13 | New German medium tanks (Leo) | Y | | 3.0 | 2016-07-20 | Supremacy mode, Falls Creek update (date uncertain) |
| 1.8 | 2015-04-14 | Game tutorial, Bot map for new players | Y | | 2.11 | 2016-06-28 | Vineyeards map, Oasis Palms update, German paper TDs |
| 1.7 | 2015-02-16 | US Turreted TDs | Y | | 2.10 | 2016-06-07 | Rockfield map update, Tank birthdays, high-pen gun for IS-6 |
| 1.6 | 2015-01-19 | Castilla map, UK HT line | Y | | 2.9 | 2016-05-03 | Training rooms, +/-1 Matchmaker (date uncertain) |
| 1.5 | 2014-12-01 | UK Nation | Y | | 2.8 | 2016-04-04 | New Russian TD line, new consumables system (date uncertain) |
| 1.4 | 2014-10-29 | German HT line (Maus) | Y | | 2.7 | 2016-03-07 | Mirage map (date uncertain) |
| 1.3 | 2014-10-02 | USSR IS-4 line, Lost Temple, Black Goldville maps | Y | | 2.6 | 2016-02-10 | German LTs, shooting through obstacles (date uncertain) |
| 1.2 | 2014-08-20 | Oasis Palms map | Y | | 2.5 | 2016-01-11 | Camouflages, Clan supply (date uncertain) |
| 1.1.1 | 2014-07-23 | Fixes :-) | | | 2.4 | 2015-12-09 | Additional Japanese tanks, windows version (date uncertain) |
| 1.0 | 2014-06-26 | Worldwide launch | | | 2.3 | 2015-11-12 | Canal map (date uncertain) |
| 0.9 | 2014-05-05 | Nordic soft launch | | | 2.2 | 2015-10-05 | Desert Sands map update (date uncertain) |
| 2.1 | 2015-09-07 | UK TDs, Clan Chat (date uncertain) |
| 2.0 | 2015-08-05 | Clans, Oasis Palms updated |
| 1.11 | 2015-07-13 | KV-2, KV-13, Sherman Jumbo (date uncertain) |
| 1.10 | 2015-06-11 | Port Bay map (date uncertain) |
| 1.9 | 2015-05-13 | New German medium tanks (Leo) (date uncertain) |
| 1.8 | 2015-04-14 | Game tutorial, Bot map for new players (date uncertain) |
| 1.7 | 2015-02-16 | US Turreted TDs (date uncertain) |
| 1.6 | 2015-01-19 | Castilla map, UK HT line (date uncertain) |
| 1.5 | 2014-12-01 | UK Nation (date uncertain) |
| 1.4 | 2014-10-29 | German HT line (Maus) (date uncertain) |
| 1.3 | 2014-10-02 | USSR IS-4 line, Lost Temple, Black Goldville maps (date uncertain) |
| 1.2 | 2014-08-20 | Oasis Palms map (date uncertain) |
| 1.1.1 | 2014-07-23 | Fixes :-) |
| 1.0 | 2014-06-26 | Worldwide launch |
| 0.9 | 2014-05-05 | Nordic soft launch |
+2 -1
View File
@@ -7,4 +7,5 @@ beautifulsoup4
lxml lxml
xmltodict xmltodict
progress progress
pytest-asyncio pytest-asyncio
blitz-utils @ git+https://github.com/Jylpah/blitz-utils
+1 -6632
View File
File diff suppressed because it is too large. Load diff
+413 -311
View File
@@ -1,343 +1,445 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
## PYTHON VERSION MUST BE 3.7 OR HIGHER ## PYTHON VERSION MUST BE 3.7 OR HIGHER
import json, argparse, inspect, sys, os, io, base64, aiohttp, urllib, asyncio, aiofiles, aioconsole import json, argparse, sys, os, io, asyncio, aiofiles, aioconsole
import logging, re, concurrent.futures, configparser, motor.motor_asyncio, ssl, zipfile import logging, re, configparser, zipfile
import blitzutils as bu import blitzutils1 as bu
from blitzutils import WG from blitzutils1 import WG
from blitzutils import WoTinspector from blitzutils1 import WoTinspector
logging.getLogger("asyncio").setLevel(logging.DEBUG) logging.getLogger("asyncio").setLevel(logging.DEBUG)
FILE_CONFIG = 'blitzstats.ini' FILE_CONFIG = "blitzstats.ini"
# DB_C_REPLAYS = 'Replays' # DB_C_REPLAYS = 'Replays'
DEBUG = False DEBUG = False
VERBOSE = False VERBOSE = False
SLEEP = 2 SLEEP = 2
MAX_RETRIES = 3 MAX_RETRIES = 3
REPLAY_N = 0 REPLAY_N = 0
SKIPPED_N = 0 SKIPPED_N = 0
ERROR_N = 0 ERROR_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: WG | None = None
wi = None wi: WoTinspector | None = None
async def main(argv):
global wg, wi
current_dir = os.getcwd()
# set the directory for the script
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
if os.path.isfile(FILE_CONFIG):
bu.debug('Reading config file: ' + FILE_CONFIG)
config = configparser.ConfigParser()
config.read(FILE_CONFIG)
try:
if 'UPLOADER' in config.sections():
configUploader = config['UPLOADER']
OPT_WORKERS_N = configUploader.getint('opt_workers', OPT_WORKERS_N)
except (KeyError, configparser.NoSectionError) as err:
bu.error(exception=err)
try:
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)
except (KeyError, configparser.NoSectionError) as err:
bu.error(exception=err)
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('-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 automatic naming')
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('--mapfile', type=str, default='maps.json', help='JSON file to read Blitz map names from. Default: "maps.json"')
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('-s', '--silent', action='store_true', default=False, help='Silent mode')
parser.add_argument('files', metavar='FILE1 [FILE2 ...]', type=str, nargs='+', help='Files to read. Use \'-\' for STDIN"')
args = parser.parse_args()
bu.set_verbose(args.verbose)
bu.set_log_level(args.silent, args.verbose, args.debug)
wg = WG(WG_appID, args.tankopedia, args.mapfile)
wi = WoTinspector(rate_limit=WG_RATE_LIMIT)
if args.account != None:
args.accountID = await wg.get_account_id(args.account)
bu.debug('WG account_id: ' + str(args.accountID))
if args.accountID == None:
args.accountID = 0
try:
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 = []
# Make replay Queue
tasks.append(asyncio.create_task(mkReplayQ(queue, args.files, args.title)))
# Start tasks to process the Queue
for i in range(OPT_WORKERS_N):
tasks.append(asyncio.create_task(replayWorker(queue, i, args.accountID, args.private)))
bu.debug('Task ' + str(i) + ' started')
bu.debug('Waiting for the replay scanner to finish')
await asyncio.wait([tasks[0]])
bu.debug('Scanner finished. Waiting for workers to finish queue')
await queue.join()
bu.debug('Cancelling workers')
for task in tasks:
task.cancel()
bu.debug('Waiting for workers to cancel')
await asyncio.gather(*tasks, return_exceptions=True)
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:
print('Ctrl-c pressed ...')
sys.exit(1)
finally:
## Need to close the aiohttp.session since Python destructors do not support async methods...
await wg.session.close()
await wi.close()
return None
async def mkReplayQ(queue : asyncio.Queue, files : list, title : str): async def main(argv) -> int:
"""Create queue of replays to post""" global wg, wi
p_replayfile = re.compile('.*\\.wotbreplay$') current_dir = os.getcwd()
if files[0] == '-': # set the directory for the script
bu.debug('reading replay file list from STDIN') os.chdir(os.path.dirname(sys.argv[0]))
stdin, _ = await aioconsole.get_standard_streams()
while True: # options defaults
line = (await stdin.readline()).decode('utf-8').rstrip() OPT_WORKERS_N = 2
if not line: WG_ID = None
break WG_ACCOUNT = None # format: nick@server, where server is either 'eu', 'ru', 'na', 'asia' or 'china'.
else: # China is not supported since WG API stats are not available there
if (p_replayfile.match(line) != None): WG_RATE_LIMIT = 10
await queue.put(await mkQueueItem(line, title))
else: try:
for fn in files: ## Read config
if fn.endswith('"'): if os.path.isfile(FILE_CONFIG):
fn = fn[:-1] bu.debug("Reading config file: " + FILE_CONFIG)
if os.path.isfile(fn) and (p_replayfile.match(fn) != None): config = configparser.ConfigParser()
await queue.put(await mkQueueItem(fn, title)) config.read(FILE_CONFIG)
bu.debug('File added to queue: ' + fn)
elif os.path.isdir(fn): try:
with os.scandir(fn) as dirEntry: if "WG" in config.sections():
for entry in dirEntry: configWG = config["WG"]
try: # WG account id of the uploader:
bu.debug('Found: ' + entry.name) # # Find it here: https://developers.wargaming.net/reference/all/wotb/account/list/
if entry.is_file() and (p_replayfile.match(entry.name) != None): WG_ID = configWG.getint("wg_id", WG_ID)
bu.debug(entry.name) WG_ACCOUNT = configWG.get("wg_account", WG_ACCOUNT)
await queue.put(await mkQueueItem(entry.path, title)) ## WG API Rules limit 10 request / sec. Higher rate of requests will return errors ==> extra delay
bu.debug('File added to queue: ' + entry.path) WG_RATE_LIMIT = configWG.getint("wg_rate_limit", WG_RATE_LIMIT)
except Exception as err: except (KeyError, configparser.NoSectionError) as err:
bu.error(str(err)) bu.error(exception=err)
else:
bu.error('File not found: ' + fn) except Exception as err:
bu.error(exception=err)
bu.debug('Finished')
return None 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(
"-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 automatic naming',
)
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(
"--mapfile",
type=str,
default="maps.json",
help='JSON file to read Blitz map names from. Default: "maps.json"',
)
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(
"-s", "--silent", action="store_true", default=False, help="Silent mode"
)
parser.add_argument(
"files",
metavar="FILE1 [FILE2 ...]",
type=str,
nargs="+",
help="Files to read. Use '-' for STDIN\"",
)
args = parser.parse_args()
bu.set_verbose(args.verbose)
bu.set_log_level(args.silent, args.verbose, args.debug)
wg = WG(WG_appID, args.tankopedia, args.mapfile)
wi = WoTinspector()
if args.account is not None:
args.accountID = await wg.get_account_id(args.account)
bu.debug("WG account_id: " + str(args.accountID))
if args.accountID is None:
args.accountID = 0
try:
queue: asyncio.Queue = asyncio.Queue()
# rebase file arguments due to moving the working directory to the script location
bu.verbose_std("replays.wotinspector.com rate limits replays to 6 / minute")
args.files = bu.rebase_file_args(current_dir, args.files)
tasks = []
# Make replay Queue
tasks.append(asyncio.create_task(mkReplayQ(queue, args.files, args.title)))
# Start tasks to process the Queue
for i in range(OPT_WORKERS_N):
tasks.append(
asyncio.create_task(
replayWorker(queue, i, args.accountID, args.private)
)
)
bu.debug("Task " + str(i) + " started")
bu.debug("Waiting for the replay scanner to finish")
await asyncio.wait([tasks[0]])
bu.debug("Scanner finished. Waiting for workers to finish queue")
await queue.join()
bu.debug("Cancelling workers")
for task in tasks:
task.cancel()
bu.debug("Waiting for workers to cancel")
await asyncio.gather(*tasks, return_exceptions=True)
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:
print("Ctrl-c pressed ...")
sys.exit(1)
finally:
## Need to close the aiohttp.session since Python destructors do not support async methods...
await wg.session.close()
await wi.close()
return 0
async def mkQueueItem(filename : str, title : str) -> list: async def mkReplayQ(queue: asyncio.Queue, files: list, title: str):
"""Make an item to replay queue""" """Create queue of replays to post"""
global REPLAY_N p_replayfile = re.compile(".*\\.wotbreplay$")
REPLAY_N +=1 if files[0] == "-":
return [filename, REPLAY_N, getTitle(filename, title, REPLAY_N)] bu.debug("reading replay file list from STDIN")
stdin, _ = await aioconsole.get_standard_streams()
while True:
line = (await stdin.readline()).decode("utf-8").rstrip()
if not line:
break
else:
if p_replayfile.match(line) is not None:
await queue.put(await mkQueueItem(line, title))
else:
for fn in files:
if fn.endswith('"'):
fn = fn[:-1]
if os.path.isfile(fn) and (p_replayfile.match(fn) is not None):
await queue.put(await mkQueueItem(fn, title))
bu.debug("File added to queue: " + fn)
elif os.path.isdir(fn):
with os.scandir(fn) as dirEntry:
for entry in dirEntry:
try:
bu.debug("Found: " + entry.name)
if entry.is_file() and (
p_replayfile.match(entry.name) is not None
):
bu.debug(entry.name)
await queue.put(await mkQueueItem(entry.path, title))
bu.debug("File added to queue: " + entry.path)
except Exception as err:
bu.error(str(err))
else:
bu.error("File not found: " + fn)
bu.debug("Finished")
return None
async def replayWorker(queue: asyncio.Queue, workerID: int, account_id: int, priv = False): async def mkQueueItem(filename: str, title: str) -> list:
"""Async Worker to process the replay queue""" """Make an item to replay queue"""
global SKIPPED_N global REPLAY_N
global ERROR_N REPLAY_N += 1
return [filename, REPLAY_N, getTitle(filename, title, REPLAY_N)]
while True:
item = await queue.get()
filename = item[0]
N = item[1]
title = item[2]
replay_json_fn = filename + '.json'
msg_str = 'Replay[' + str(N) + ']: '
#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':
if wi.chk_JSON_replay(replay_json):
bu.verbose_std(msg_str + title + ' has already been posted. Skipping.' )
else:
os.remove(replay_json_fn)
bu.debug(msg_str + "Replay JSON not valid/complete: Deleting " + replay_json_fn, id=workerID)
SKIPPED_N += 1
queue.task_done()
continue
except asyncio.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:
filename = os.path.basename(filename)
bu.debug(msg_str + 'File: ' + filename)
json_resp = await wi.post_replay(await fp.read(), filename, account_id, title, priv, N)
if json_resp != None:
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):
bu.verbose_std(msg_str + title + ' posted')
else:
bu.warning(msg_str +'Replay file is not valid/complete: ' + filename)
ERROR_N += 1
else:
bu.error(msg_str + 'Error saving replay: ' + filename)
ERROR_N += 1
else:
bu.error(msg_str + 'Replay file is not valid/complete: ' + filename)
ERROR_N += 1
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()
return None
def getTitle_old(replayfile: str, title: str, i : int) -> str: async def replayWorker(
global wg queue: asyncio.Queue, workerID: int, account_id: int, priv=False
):
"""Async Worker to process the replay queue"""
global SKIPPED_N
global ERROR_N
global wi
if title == None: assert wi is not None, "wi cannot be None"
try:
filename = os.path.basename(replayfile) while True:
bu.debug(filename) item = await queue.get()
map_usrStrs = wg.get_map_user_strs() filename = item[0]
tank_userStrs = wg.get_tank_user_strs() N = item[1]
title = item[2]
#p = re.compile('\\d{8}_\\d{4}_(.+)_(' + '|'.join(map_usrStrs) + ')(?:-\\d)?\\.wotbreplay$')
# update 6.2 changed the file name format. Bug fixed 2019-09-09 Jylpah replay_json_fn = filename + ".json"
p = re.compile('\\d{8}_\\d{4}_.*?(' + '|'.join(tank_userStrs) + ')_(' + '|'.join(map_usrStrs) + ')(?:-\\d)?\\.wotbreplay$') msg_str = "Replay[" + str(N) + "]: "
m = p.match(filename) # bu.debug(msg_str + replay_json_fn)
if (m != None): try:
if wg.tanks != None: if os.path.isfile(replay_json_fn):
tank = m.group(1) async with aiofiles.open(replay_json_fn) as fp:
if tank in wg.tanks['userStr']: replay_json = json.loads(await fp.read())
tank = wg.tanks['userStr'][tank] # if replay_json['status'] == 'ok':
else: if wi.chk_JSON_replay(replay_json):
bu.error('Tank code: "' + tank + '" not found from Tankopedia (tanks.json)') bu.verbose_std(
else: msg_str + title + " has already been posted. Skipping."
tank = m.group(1) )
blitz_map = m.group(2) else:
if blitz_map in wg.maps: os.remove(replay_json_fn)
blitz_map = wg.maps[blitz_map] bu.debug(
else: msg_str
bu.error('Mapcode: "' + blitz_map + '" not found from map database (maps.json)') + "Replay JSON not valid/complete: Deleting "
title = tank + ' @ ' + blitz_map + replay_json_fn,
else: id=workerID,
title = re.sub('\\.wotbreplay$', '', filename) )
except Exception as err: SKIPPED_N += 1
bu.error(err) queue.task_done()
else: continue
title.replace('NN', str(i))
return title except asyncio.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:
filename = os.path.basename(filename)
bu.debug(msg_str + "File: " + filename)
json_resp = await wi.post_replay(
await fp.read(), filename, account_id, title, priv, N
)
if json_resp is not None:
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):
bu.verbose_std(msg_str + title + " posted")
else:
bu.warning(
msg_str
+ "Replay file is not valid/complete: "
+ filename
)
ERROR_N += 1
else:
bu.error(msg_str + "Error saving replay: " + filename)
ERROR_N += 1
else:
bu.error(msg_str + "Replay file is not valid/complete: " + filename)
ERROR_N += 1
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()
return None
def getTitle(replayfile: str, title: str = None, i : int = 0) -> str: # def getTitle_old(replayfile: str, title: str, i: int) -> str:
global wg # global wg
if title == None:
try: # if title is None:
filename = os.path.basename(replayfile) # try:
bu.debug(filename) # filename = os.path.basename(replayfile)
tank = None # bu.debug(filename)
map_name = None # map_usrStrs = wg.get_map_user_strs()
# tank_userStrs = wg.get_tank_user_strs()
# # p = re.compile('\\d{8}_\\d{4}_(.+)_(' + '|'.join(map_usrStrs) + ')(?:-\\d)?\\.wotbreplay$')
# # 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$"
# )
# m = p.match(filename)
# if m is not None:
# if wg.tanks is not None:
# 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)'
# )
# else:
# tank = m.group(1)
# 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)'
# )
# title = tank + " @ " + blitz_map
# else:
# title = re.sub("\\.wotbreplay$", "", filename)
# except Exception as err:
# bu.error(err)
# else:
# title.replace("NN", str(i))
# return title
def getTitle(replayfile: str, title: str | None = None, i: int = 0) -> str:
global wg
assert wg is not None, "wg must not be None"
if title is None:
try:
filename = os.path.basename(replayfile)
bu.debug(filename)
tank = None
map_name = None
with zipfile.ZipFile(replayfile, "r") as archive:
# bu.debug('Replay file: ' + replayfile + ' opened')
with io.TextIOWrapper(archive.open("meta.json")) as meta:
# bu.debug('Replay file\'s metadata: opened')
try:
metadata_json = json.load(meta)
# player = metadata_json['playerName']
tank_name = wg.get_tank_name(
metadata_json["vehicleCompDescriptor"]
)
map_name = wg.get_map(metadata_json["mapName"])
bu.debug(
"Tank: " + tank_name
if tank_name is not None
else "NOT FOUND IN tanks.json" + " Map: " + map_name
)
except Exception as err:
bu.error(exception=err)
if (tank_name is not None) and (map_name is not None):
title = tank_name + " @ " + map_name
else:
title = re.sub("\\.wotbreplay$", "", filename)
except Exception as err:
bu.error(err)
else:
title.replace("NN", str(i))
bu.debug("Returning: " + title)
return title
with zipfile.ZipFile(replayfile, 'r') as archive:
# bu.debug('Replay file: ' + replayfile + ' opened')
with io.TextIOWrapper(archive.open('meta.json')) as meta:
# bu.debug('Replay file\'s metadata: opened')
try:
metadata_json = json.load(meta)
#player = metadata_json['playerName']
tank_name = wg.get_tank_name(metadata_json['vehicleCompDescriptor'])
map_name = wg.get_map(metadata_json['mapName'])
bu.debug('Tank: ' + tank_name if tank_name != None else "NOT FOUND IN tanks.json" + ' Map: ' + map_name)
except Exception as err:
bu.error(exception = err)
if (tank_name != None) and (map_name != None):
title = tank_name + ' @ ' + map_name
else:
title = re.sub('\\.wotbreplay$', '', filename)
except Exception as err:
bu.error(err)
else:
title.replace('NN', str(i))
bu.debug('Returning: ' + title)
return title
### UNIT TESTS - pytest ### UNIT TESTS - pytest
# class TestClass: # class TestClass:
# def test_getTitle(self): # def test_getTitle(self):
# #player = "jylpah" # #player = "jylpah"
# WG_appID = '81381d3f45fa4aa75b78a7198eb216ad' # WG_appID = '81381d3f45fa4aa75b78a7198eb216ad'
# wg = WG(WG_appID, 'tanks.json', 'maps.json') # wg = WG(WG_appID, 'tanks.json', 'maps.json')
# tank_name = "T28 Defender" # "A68_T28D" # tank_name = "T28 Defender" # "A68_T28D"
# map_name = "Yukon" # "idle" # map_name = "Yukon" # "idle"
# replay = './test/' + '20201030_2358__jylpah_A_1157485188135816346.wotbreplay' # replay = './test/' + '20201030_2358__jylpah_A_1157485188135816346.wotbreplay'
# res = tank_name + ' @ ' + map_name # res = tank_name + ' @ ' + map_name
# assert getTitle(replay) == res # assert getTitle(replay) == res
# @pytest.mark.asyncio # @pytest.mark.asyncio
# async def test_getTitle(self): # async def test_getTitle(self):
# #player = "jylpah" # #player = "jylpah"
# WG_appID = '81381d3f45fa4aa75b78a7198eb216ad' # WG_appID = '81381d3f45fa4aa75b78a7198eb216ad'
# wg = WG(WG_appID, 'tanks.json', 'maps.json') # wg = WG(WG_appID, 'tanks.json', 'maps.json')
# tank_name = "T28 Defender" # "A68_T28D" # tank_name = "T28 Defender" # "A68_T28D"
# map_name = "Yukon" # "idle" # map_name = "Yukon" # "idle"
# replay = './test/' + '20201030_2358__jylpah_A_1157485188135816346.wotbreplay' # replay = './test/' + '20201030_2358__jylpah_A_1157485188135816346.wotbreplay'
# res = tank_name + ' @ ' + map_name # res = tank_name + ' @ ' + map_name
# assert getTitle(replay) == res # assert getTitle(replay) == res
### main() ------------------------------------------- ### main() -------------------------------------------
if __name__ == "__main__": if __name__ == "__main__":
# To avoid 'Event loop is closed' RuntimeError due to compatibility issue with aiohttp # To avoid 'Event loop is closed' RuntimeError due to compatibility issue with aiohttp
if sys.platform.startswith("win") and sys.version_info >= (3, 8): if sys.platform.startswith("win") and sys.version_info >= (3, 8):
try: try:
from asyncio import WindowsSelectorEventLoopPolicy from asyncio import WindowsSelectorEventLoopPolicy
except ImportError: except ImportError:
pass pass
else: else:
if not isinstance(asyncio.get_event_loop_policy(), WindowsSelectorEventLoopPolicy): if not isinstance(
asyncio.set_event_loop_policy(WindowsSelectorEventLoopPolicy()) asyncio.get_event_loop_policy(), WindowsSelectorEventLoopPolicy
asyncio.run(main(sys.argv[1:]), debug=False) ):
asyncio.set_event_loop_policy(WindowsSelectorEventLoopPolicy())
asyncio.run(main(sys.argv[1:]), debug=False)