missing files
This commit is contained in:
parent
ae1f72b032
commit
1e3c2750e5
|
@ -1 +1,7 @@
|
||||||
Create a `.credentials` file with `client_id:secret` from https://develop.battle.net/access
|
Create a `.credentials` file with `client_id:secret` from https://develop.battle.net/access
|
||||||
|
|
||||||
|
Crontab, every day at 12:30h:
|
||||||
|
|
||||||
|
```
|
||||||
|
30 12 * * * time python3 updater.py
|
||||||
|
```
|
||||||
|
|
123
spec.py
123
spec.py
|
@ -12,51 +12,88 @@ regions = [
|
||||||
#{'code': 'cn', 'locales': ['zh_CN']}
|
#{'code': 'cn', 'locales': ['zh_CN']}
|
||||||
]
|
]
|
||||||
|
|
||||||
|
groups = [
|
||||||
|
'Achievement',
|
||||||
|
'Auction House',
|
||||||
|
'Azerite Essence',
|
||||||
|
'Connected Realm',
|
||||||
|
'Covenant',
|
||||||
|
'Creature',
|
||||||
|
'Guild Crest',
|
||||||
|
'Item',
|
||||||
|
'Journal',
|
||||||
|
'Media Search',
|
||||||
|
'Modified Crafting',
|
||||||
|
'Mount',
|
||||||
|
'Mythic Keystone Affix',
|
||||||
|
'Mythic Keystone Dungeon',
|
||||||
|
'Mythic Keystone Leaderboard',
|
||||||
|
'Mythic Raid Leaderboard',
|
||||||
|
'Pet',
|
||||||
|
'Playable Class',
|
||||||
|
'Playable Race',
|
||||||
|
'Playable Specialization',
|
||||||
|
'Power Type',
|
||||||
|
'Profession',
|
||||||
|
'PvP Season',
|
||||||
|
'PvP Tier',
|
||||||
|
'Quest',
|
||||||
|
'Realm',
|
||||||
|
'Region',
|
||||||
|
'Reputations',
|
||||||
|
'Spell',
|
||||||
|
'Talent',
|
||||||
|
'Tech Talent',
|
||||||
|
'Title',
|
||||||
|
'WoW Token',
|
||||||
|
]
|
||||||
|
|
||||||
"""
|
"""
|
||||||
https://develop.battle.net/documentation/world-of-warcraft/game-data-apis
|
https://develop.battle.net/documentation/world-of-warcraft/game-data-apis
|
||||||
"""
|
"""
|
||||||
apis = [
|
apis = [
|
||||||
{'path': '/data/wow/achievement-category/index', 'namespaces': ['static'], 'index': True},
|
{'group': 'Achievement', 'path': '/data/wow/achievement-category/index', 'namespaces': ['static'], 'index': True},
|
||||||
{'path': '/data/wow/achievement/index', 'namespaces': ['static'], 'index': True},
|
{'group': 'Achievement', 'path': '/data/wow/achievement/index', 'namespaces': ['static'], 'index': True},
|
||||||
{'path': '/data/wow/connected-realm/index', 'namespaces': ['dynamic', 'dynamic-classic'], 'index': True},
|
{'group': 'Connected Realm', 'path': '/data/wow/connected-realm/index', 'namespaces': ['dynamic', 'dynamic-classic'], 'index': True},
|
||||||
{'path': '/data/wow/covenant/index', 'namespaces': ['static'], 'index': True},
|
{'group': 'Covenant', 'path': '/data/wow/covenant/index', 'namespaces': ['static'], 'index': True},
|
||||||
{'path': '/data/wow/covenant/soulbind/index', 'namespaces': ['static'], 'index': True},
|
{'group': 'Covenant', 'path': '/data/wow/covenant/soulbind/index', 'namespaces': ['static'], 'index': True},
|
||||||
{'path': '/data/wow/covenant/conduit/index', 'namespaces': ['static'], 'index': True},
|
{'group': 'Covenant', 'path': '/data/wow/covenant/conduit/index', 'namespaces': ['static'], 'index': True},
|
||||||
{'path': '/data/wow/creature-family/index', 'namespaces': ['static'], 'index': True},
|
{'group': 'Creature', 'path': '/data/wow/creature-family/index', 'namespaces': ['static'], 'index': True},
|
||||||
{'path': '/data/wow/creature-type/index', 'namespaces': ['static'], 'index': True},
|
{'group': 'Creature', 'path': '/data/wow/creature-type/index', 'namespaces': ['static'], 'index': True},
|
||||||
{'path': '/data/wow/item-class/index', 'namespaces': ['static'], 'index': True},
|
{'group': 'Guild Crest', 'path': '/data/wow/guild-crest/index', 'namespaces': ['static'], 'index': True},
|
||||||
{'path': '/data/wow/item-set/index', 'namespaces': ['static'], 'index': True},
|
{'group': '', 'path': '/data/wow/item-class/index', 'namespaces': ['static'], 'index': True},
|
||||||
{'path': '/data/wow/journal-expansion/index', 'namespaces': ['static'], 'index': True},
|
{'group': '', 'path': '/data/wow/item-set/index', 'namespaces': ['static'], 'index': True},
|
||||||
{'path': '/data/wow/journal-encounter/index', 'namespaces': ['static'], 'index': True},
|
{'group': '', 'path': '/data/wow/journal-expansion/index', 'namespaces': ['static'], 'index': True},
|
||||||
{'path': '/data/wow/journal-instance/index', 'namespaces': ['static'], 'index': True},
|
{'group': '', 'path': '/data/wow/journal-encounter/index', 'namespaces': ['static'], 'index': True},
|
||||||
{'path': '/data/wow/modified-crafting/index', 'namespaces': ['static'], 'index': True},
|
{'group': '', 'path': '/data/wow/journal-instance/index', 'namespaces': ['static'], 'index': True},
|
||||||
{'path': '/data/wow/modified-crafting/category/index', 'namespaces': ['static'], 'index': True},
|
{'group': '', 'path': '/data/wow/modified-crafting/index', 'namespaces': ['static'], 'index': True},
|
||||||
{'path': '/data/wow/modified-crafting/reagent-slot-type/index', 'namespaces': ['static'], 'index': True},
|
{'group': '', 'path': '/data/wow/modified-crafting/category/index', 'namespaces': ['static'], 'index': True},
|
||||||
{'path': '/data/wow/mount/index', 'namespaces': ['static'], 'index': True},
|
{'group': '', 'path': '/data/wow/modified-crafting/reagent-slot-type/index', 'namespaces': ['static'], 'index': True},
|
||||||
{'path': '/data/wow/keystone-affix/index', 'namespaces': ['static'], 'index': True},
|
{'group': '', 'path': '/data/wow/mount/index', 'namespaces': ['static'], 'index': True},
|
||||||
{'path': '/data/wow/mythic-keystone/dungeon/index', 'namespaces': ['dynamic'], 'index': True},
|
{'group': '', 'path': '/data/wow/keystone-affix/index', 'namespaces': ['static'], 'index': True},
|
||||||
{'path': '/data/wow/mythic-keystone/index', 'namespaces': ['dynamic'], 'index': True},
|
{'group': '', 'path': '/data/wow/mythic-keystone/dungeon/index', 'namespaces': ['dynamic'], 'index': True},
|
||||||
{'path': '/data/wow/mythic-keystone/period/index', 'namespaces': ['dynamic'], 'index': True},
|
{'group': '', 'path': '/data/wow/mythic-keystone/index', 'namespaces': ['dynamic'], 'index': True},
|
||||||
{'path': '/data/wow/pet/index', 'namespaces': ['static'], 'index': True},
|
{'group': '', 'path': '/data/wow/mythic-keystone/period/index', 'namespaces': ['dynamic'], 'index': True},
|
||||||
{'path': '/data/wow/pet-ability/index', 'namespaces': ['static'], 'index': True},
|
{'group': '', 'path': '/data/wow/pet/index', 'namespaces': ['static'], 'index': True},
|
||||||
{'path': '/data/wow/playable-class/index', 'namespaces': ['static'], 'index': True},
|
{'group': '', 'path': '/data/wow/pet-ability/index', 'namespaces': ['static'], 'index': True},
|
||||||
{'path': '/data/wow/playable-race/index', 'namespaces': ['static'], 'index': True},
|
{'group': '', 'path': '/data/wow/playable-class/index', 'namespaces': ['static'], 'index': True},
|
||||||
{'path': '/data/wow/playable-specialization/index', 'namespaces': ['static'], 'index': True},
|
{'group': '', 'path': '/data/wow/playable-race/index', 'namespaces': ['static'], 'index': True},
|
||||||
{'path': '/data/wow/power-type/index', 'namespaces': ['static'], 'index': True},
|
{'group': '', 'path': '/data/wow/playable-specialization/index', 'namespaces': ['static'], 'index': True},
|
||||||
{'path': '/data/wow/profession/index', 'namespaces': ['static'], 'index': True},
|
{'group': '', 'path': '/data/wow/power-type/index', 'namespaces': ['static'], 'index': True},
|
||||||
{'path': '/data/wow/pvp-season/index', 'namespaces': ['dynamic'], 'index': True},
|
{'group': '', 'path': '/data/wow/profession/index', 'namespaces': ['static'], 'index': True},
|
||||||
{'path': '/data/wow/pvp-tier/index', 'namespaces': ['static'], 'index': True},
|
{'group': '', 'path': '/data/wow/pvp-season/index', 'namespaces': ['dynamic'], 'index': True},
|
||||||
{'path': '/data/wow/quest/index', 'namespaces': ['static'], 'index': True},
|
{'group': '', 'path': '/data/wow/pvp-tier/index', 'namespaces': ['static'], 'index': True},
|
||||||
{'path': '/data/wow/quest/category/index', 'namespaces': ['static'], 'index': True},
|
{'group': '', 'path': '/data/wow/quest/index', 'namespaces': ['static'], 'index': True},
|
||||||
{'path': '/data/wow/quest/area/index', 'namespaces': ['static'], 'index': True},
|
{'group': '', 'path': '/data/wow/quest/category/index', 'namespaces': ['static'], 'index': True},
|
||||||
{'path': '/data/wow/quest/type/index', 'namespaces': ['static'], 'index': True},
|
{'group': '', 'path': '/data/wow/quest/area/index', 'namespaces': ['static'], 'index': True},
|
||||||
{'path': '/data/wow/realm/index', 'namespaces': ['dynamic'], 'index': True},
|
{'group': '', 'path': '/data/wow/quest/type/index', 'namespaces': ['static'], 'index': True},
|
||||||
{'path': '/data/wow/region/index', 'namespaces': ['dynamic'], 'index': True},
|
{'group': '', 'path': '/data/wow/realm/index', 'namespaces': ['dynamic'], 'index': True},
|
||||||
{'path': '/data/wow/reputation-faction/index', 'namespaces': ['static'], 'index': True},
|
{'group': '', 'path': '/data/wow/region/index', 'namespaces': ['dynamic'], 'index': True},
|
||||||
{'path': '/data/wow/reputation-tiers/index', 'namespaces': ['static'], 'index': True},
|
{'group': '', 'path': '/data/wow/reputation-faction/index', 'namespaces': ['static'], 'index': True},
|
||||||
{'path': '/data/wow/talent/index', 'namespaces': ['static'], 'index': True},
|
{'group': '', 'path': '/data/wow/reputation-tiers/index', 'namespaces': ['static'], 'index': True},
|
||||||
{'path': '/data/wow/pvp-talent/index', 'namespaces': ['static'], 'index': True},
|
{'group': '', 'path': '/data/wow/talent/index', 'namespaces': ['static'], 'index': True},
|
||||||
{'path': '/data/wow/tech-talent-tree/index', 'namespaces': ['static'], 'index': True},
|
{'group': '', 'path': '/data/wow/pvp-talent/index', 'namespaces': ['static'], 'index': True},
|
||||||
{'path': '/data/wow/tech-talent/index', 'namespaces': ['static'], 'index': True},
|
{'group': '', 'path': '/data/wow/tech-talent-tree/index', 'namespaces': ['static'], 'index': True},
|
||||||
{'path': '/data/wow/title/index', 'namespaces': ['static'], 'index': True},
|
{'group': '', 'path': '/data/wow/tech-talent/index', 'namespaces': ['static'], 'index': True},
|
||||||
|
{'group': '', 'path': '/data/wow/title/index', 'namespaces': ['static'], 'index': True},
|
||||||
]
|
]
|
||||||
|
|
79
updater.py
79
updater.py
|
@ -4,18 +4,24 @@ import errno
|
||||||
import requests
|
import requests
|
||||||
import json
|
import json
|
||||||
import datetime
|
import datetime
|
||||||
|
import tempfile
|
||||||
|
|
||||||
import config
|
import config
|
||||||
import blizzard
|
import blizzard
|
||||||
import spec
|
import spec
|
||||||
|
|
||||||
|
|
||||||
def log(*data):
|
def log(*data):
|
||||||
print(datetime.datetime.now(), '|', *data)
|
print(datetime.datetime.now(), '|', *data)
|
||||||
|
|
||||||
|
|
||||||
class Updater(object):
|
class Updater(object):
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.http = requests.Session()
|
self.http = requests.Session()
|
||||||
self.credentials = blizzard.get_credentials(config.pwd)
|
self.credentials = blizzard.get_credentials(config.pwd)
|
||||||
|
self.last_modified = None
|
||||||
|
|
||||||
|
|
||||||
def region_oauth(self, region: str) -> dict:
|
def region_oauth(self, region: str) -> dict:
|
||||||
api = self.http.post(f"{blizzard.get_bnet_host(region)}/oauth/token", data={'grant_type': 'client_credentials'}, auth=self.credentials)
|
api = self.http.post(f"{blizzard.get_bnet_host(region)}/oauth/token", data={'grant_type': 'client_credentials'}, auth=self.credentials)
|
||||||
|
@ -24,11 +30,16 @@ class Updater(object):
|
||||||
api.raise_for_status()
|
api.raise_for_status()
|
||||||
return oauth
|
return oauth
|
||||||
|
|
||||||
def api_call(self, url: str) -> requests.Response:
|
|
||||||
api = self.http.get(url) #, headers={'Authorization': f"Authorization: Token {access_token}"})
|
def api_call(self, region: str, path: str, namespace: str, locale: str, access_token: str, headers=None) -> requests.Response:
|
||||||
|
url = f"{blizzard.get_api_host(region)}{path}"
|
||||||
|
qs = {'namespace': f"{namespace}-{region}", 'local': locale, 'access_token': access_token}
|
||||||
|
|
||||||
|
api = self.http.get(url, params=qs, headers=headers) #, headers={'Authorization': f"Authorization: Token {access_token}"})
|
||||||
api.raise_for_status()
|
api.raise_for_status()
|
||||||
return api
|
return api
|
||||||
|
|
||||||
|
|
||||||
def create_dst(self, dst: str):
|
def create_dst(self, dst: str):
|
||||||
try:
|
try:
|
||||||
os.makedirs(os.path.dirname(dst))
|
os.makedirs(os.path.dirname(dst))
|
||||||
|
@ -36,9 +47,56 @@ class Updater(object):
|
||||||
if e.errno != errno.EEXIST:
|
if e.errno != errno.EEXIST:
|
||||||
raise
|
raise
|
||||||
|
|
||||||
def save(self, dst, content):
|
|
||||||
|
def save_raw(self, region: str, path: str, namespace: str, locale: str, raw: requests.Response):
|
||||||
|
dst = f"{config.raw}/{region}/{locale}/{path.replace('/', '_')}.{namespace}.json"
|
||||||
|
|
||||||
|
self.create_dst(dst)
|
||||||
|
|
||||||
with open(dst, 'w+') as f:
|
with open(dst, 'w+') as f:
|
||||||
f.write(json.dumps(content.json(), indent=4))
|
f.write(json.dumps(raw.json(), indent=2))
|
||||||
|
|
||||||
|
|
||||||
|
def get_last_modified(self, region: str, path: str, namespace: str, locale: str):
|
||||||
|
key = f"{region}.{locale}.{path}.{namespace}"
|
||||||
|
|
||||||
|
# cached!
|
||||||
|
if self.last_modified is not None and key in self.last_modified:
|
||||||
|
return self.last_modified[key]
|
||||||
|
|
||||||
|
# not cached, check db again
|
||||||
|
db = os.path.join(config.pwd, 'last-modified.db')
|
||||||
|
|
||||||
|
with open(db, 'r') as f:
|
||||||
|
data = json.load(f)
|
||||||
|
self.last_modified = data
|
||||||
|
|
||||||
|
try:
|
||||||
|
return data[key]
|
||||||
|
except KeyError:
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def save_last_modified(self, region: str, path: str, namespace: str, locale: str, modified: str):
|
||||||
|
key = f"{region}.{locale}.{path}.{namespace}"
|
||||||
|
db = os.path.join(config.pwd, 'last-modified.db')
|
||||||
|
|
||||||
|
# never trust the cache, open, modify and save updated data for safety
|
||||||
|
with open(db, 'r') as f:
|
||||||
|
data = json.load(f)
|
||||||
|
|
||||||
|
data[key] = modified
|
||||||
|
# update cache with new data
|
||||||
|
self.last_modified = data
|
||||||
|
|
||||||
|
# safe write: tmp write and then mv
|
||||||
|
fd, tmp_path = tempfile.mkstemp(dir=config.pwd)
|
||||||
|
try:
|
||||||
|
with os.fdopen(fd, 'w') as tmp:
|
||||||
|
json.dump(data, tmp, indent=2)
|
||||||
|
finally:
|
||||||
|
os.replace(tmp_path, db)
|
||||||
|
|
||||||
|
|
||||||
def iterate_index(self):
|
def iterate_index(self):
|
||||||
for region in spec.regions:
|
for region in spec.regions:
|
||||||
|
@ -59,19 +117,20 @@ class Updater(object):
|
||||||
# retail or classic
|
# retail or classic
|
||||||
for namespace in api['namespaces']:
|
for namespace in api['namespaces']:
|
||||||
try:
|
try:
|
||||||
dst = f"{config.raw}/{region['code']}/{locale}/{api['path'].replace('/', '_')}.{namespace}.json"
|
last_modified = self.get_last_modified(region['code'], api['path'], namespace, locale)
|
||||||
|
headers = {'If-Modified-Since': last_modified} if last_modified is not None else None
|
||||||
|
|
||||||
url = f"{blizzard.get_api_host(region['code'])}{api['path']}"
|
response = self.api_call(region['code'], api['path'], namespace, locale, access_token, headers=headers)
|
||||||
url += f"?namespace={namespace}-{region['code']}&locale={locale}&access_token={access_token}"
|
|
||||||
response = self.api_call(url)
|
|
||||||
|
|
||||||
self.create_dst(dst)
|
if response.status_code == 200:
|
||||||
self.save(dst, response)
|
self.save_last_modified(region['code'], api['path'], namespace, locale, response.headers['Last-Modified'])
|
||||||
|
self.save_raw(region['code'], api['path'], namespace, locale, response)
|
||||||
|
|
||||||
except requests.exceptions.HTTPError as e:
|
except requests.exceptions.HTTPError as e:
|
||||||
log(e)
|
log(e)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
updater = Updater()
|
updater = Updater()
|
||||||
updater.iterate_index()
|
updater.iterate_index()
|
||||||
|
|
Loading…
Reference in New Issue