Skip to content

Commit 00cf8e7

Browse files
committed
WIP: readd old giantbomb logic and start adapting for DekuDeals
1 parent c2818aa commit 00cf8e7

File tree

3 files changed

+164
-7
lines changed

3 files changed

+164
-7
lines changed

config.example.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,8 @@
55
# Sentry DSN
66
DSN = ''
77

8-
# Giantbomb Api Credentials
9-
giantbomb = 'key'
8+
# Api Credentials
9+
dekudeals = 'key'
1010

1111
# Mongo Credentials
1212
mongoURI = 'MongoDB URI'

modules/core.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -810,6 +810,13 @@ async def _update_cache(self, interaction: discord.Interaction):
810810
return await interaction.channel.send(
811811
f'<@{interaction.user.id}> Syncronization completed. Took {timeToComplete}'
812812
)
813+
814+
@update_group.command(name='gamedb', description='Sync the games database with GiantBomb')
815+
@app_commands.describe(full='Determines if it should be a full sync, or a partial')
816+
@app_commands.default_permissions(view_audit_log=True)
817+
async def _update_game_db(self, interaction: discord.Interaction, full: bool):
818+
gamesCog = self.bot.get_cog('Games')
819+
await gamesCog.games_sync(interaction, full)
813820

814821
@app_commands.command(name='shutdown', description='Shutdown the bot and all modules')
815822
@app_commands.guilds(discord.Object(id=config.nintendoswitch))

modules/games.py

Lines changed: 155 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
import logging
55
import re
66
from datetime import datetime, timedelta, timezone
7-
from typing import Generator, Literal, Optional, Tuple, Union
7+
from typing import AsyncGenerator, Literal, Optional, Tuple, Union
88

99
import aiohttp
1010
import config # type: ignore
@@ -21,20 +21,52 @@
2121

2222
mclient = pymongo.MongoClient(config.mongoURI)
2323

24-
GIANTBOMB_NSW_ID = 157
2524
AUTO_SYNC = False
2625
SEARCH_RATIO_THRESHOLD = 50
2726

2827

28+
class DekuDeals:
29+
def __init__(self, api_key):
30+
self.ENDPOINT = 'https://www.dekudeals.com/api/rNS/games'
31+
self.api_key = api_key
32+
33+
async def fetch_games(self, platform: str):
34+
offset = 0
35+
36+
for _ in range(1, 3): # change this to 1000 later or something
37+
async with aiohttp.ClientSession() as session:
38+
headers = {'User-Agent': 'MechaBowser (+https://github.com/rNintendoSwitch/MechaBowser)'}
39+
params = {'api_key': self.api_key, 'offset': offset}
40+
41+
if platform:
42+
params['platform'] = platform
43+
44+
async with session.get(self.ENDPOINT, params=params, headers=headers) as resp:
45+
resp_json = await resp.json()
46+
47+
for item in resp_json['games']:
48+
yield item
49+
50+
offset += len(resp_json['games'])
51+
52+
if len(resp_json['games']) < 100:
53+
break # no more results expected
54+
55+
2956
class Games(commands.Cog, name='Games'):
3057
def __init__(self, bot):
3158
self.bot = bot
59+
self.DekuDeals = DekuDeals(config.dekudeals)
3260
self.db = mclient.bowser.games
3361

62+
self.last_sync = {
63+
'part': {'at': None, 'count': {'games': 0, 'releases': 0}, 'running': False},
64+
'full': {'at': None, 'count': {'games': 0, 'releases': 0}, 'running': False},
65+
}
66+
67+
# TODO uncomment
3468
# Ensure indices exist
35-
self.db.create_index([("date_last_updated", pymongo.DESCENDING)])
36-
self.db.create_index([("guid", pymongo.ASCENDING)], unique=True)
37-
self.db.create_index([("game.id", pymongo.ASCENDING)])
69+
# self.db.create_index([("deku_id", pymongo.ASCENDING)], unique=True)
3870

3971
# Generate the pipeline
4072
self.pipeline = [
@@ -62,6 +94,90 @@ def __init__(self, bot):
6294
]
6395
self.aggregatePipeline = list(self.db.aggregate(self.pipeline))
6496

97+
if AUTO_SYNC:
98+
self.sync_db.start()
99+
100+
async def cog_unload(self):
101+
if AUTO_SYNC:
102+
self.sync_db.cancel()
103+
104+
@tasks.loop(hours=1)
105+
async def sync_db(self, force_full: bool = False) -> Tuple[int, str]:
106+
# If last full sync was more then a day ago (or on restart/forced), preform a new full sync
107+
day_ago = datetime.now(tz=timezone.utc) - timedelta(days=1)
108+
full = force_full or ((self.last_sync['full']['at'] < day_ago) if self.last_sync['full']['at'] else True)
109+
110+
if not full:
111+
try:
112+
latest_doc = self.db.find().sort("date_last_updated", pymongo.DESCENDING).limit(1).next()
113+
after = latest_doc['date_last_updated']
114+
except StopIteration:
115+
full = True # Do full sync if we're having issues getting latest updated
116+
117+
detail_str = '(full)' if full else f'(partial after {after})'
118+
logging.info(f'[Games] Syncing games database {detail_str}...')
119+
self.last_sync['full' if full else 'part']['running'] = True
120+
121+
if full:
122+
# Flag items so we can detect if they are not updated.
123+
self.db.update_many({}, {'$set': {'_full_sync_updated': False}})
124+
125+
count = {}
126+
for type, path in [('game', 'games'), ('release', 'releases')]:
127+
count[path] = 0
128+
try:
129+
async for game in self.GiantBomb.fetch_items(path, None if full else after):
130+
if full:
131+
game['_full_sync_updated'] = True
132+
133+
self.update_item_in_db(type, game)
134+
count[path] += 1
135+
136+
except aiohttp.ClientResponseError as e:
137+
if e.status in [429, 420]: # Giantbomb uses 420 as ratelimiting
138+
logging.error('[Games] Ratelimited with GiantBomb, attempting retry at next loop')
139+
return
140+
141+
except Exception as e:
142+
logging.error(f'[Games] Exception while syncing games: {e}')
143+
raise
144+
145+
if full:
146+
self.db.delete_many({'_full_sync_updated': False}) # If items were not updated, delete them
147+
148+
logging.info(f'[Games] Finished syncing {count["games"]} games and {count["releases"]} releases {detail_str}')
149+
self.last_sync['full' if full else 'part'] = {
150+
'at': datetime.now(tz=timezone.utc),
151+
'count': count,
152+
'running': False,
153+
}
154+
self.aggregatePipeline = list(self.db.aggregate(self.pipeline))
155+
156+
return count, detail_str
157+
158+
def update_item_in_db(self, type: Literal['game', 'release'], game: dict):
159+
if type not in ['game', 'release']:
160+
raise ValueError(f'invalid type: {type}')
161+
162+
date_keys = {
163+
'game': ['date_added', 'date_last_updated', 'original_release_date'],
164+
'release': ['date_added', 'date_last_updated', 'release_date'],
165+
}
166+
167+
for key in date_keys[type]: # Parse dates
168+
if game[key]:
169+
game[key] = parser.parse(game[key])
170+
171+
if type == 'game' and game['aliases']:
172+
game['aliases'] = game['aliases'].splitlines()
173+
174+
if type == 'release':
175+
game['_gameid'] = game['game']['id']
176+
177+
game['_type'] = type
178+
179+
return self.db.replace_one({'guid': game['guid']}, game, upsert=True)
180+
65181
def search(self, query: str) -> Optional[dict]:
66182
match = {'guid': None, 'score': None, 'name': None}
67183
for game in self.aggregatePipeline:
@@ -175,6 +291,26 @@ class GamesCommand(app_commands.Group):
175291

176292
games_group = GamesCommand(name='game', description='Find out information about games for the Nintendo Switch!')
177293

294+
async def _games_search_autocomplete(self, interaction: discord.Interaction, current: str):
295+
if current:
296+
game = self.search(current)
297+
298+
else:
299+
# Current textbox is empty
300+
return []
301+
302+
if game:
303+
return [app_commands.Choice(name=game['name'], value=game['guid'])]
304+
305+
else:
306+
return []
307+
308+
@app_commands.guilds(discord.Object(id=config.nintendoswitch))
309+
class GamesCommand(app_commands.Group):
310+
pass
311+
312+
games_group = GamesCommand(name='game', description='Find out information about games for the Nintendo Switch!')
313+
178314
@games_group.command(name='search')
179315
@app_commands.describe(query='The term you want to search for a game')
180316
@app_commands.checks.cooldown(2, 60, key=lambda i: (i.guild_id, i.user.id))
@@ -219,6 +355,8 @@ async def _games_search(self, interaction: discord.Interaction, query: str):
219355

220356
embed.set_footer(text=f'{result["score"] }% confident{alias_str} ⯁ Entry last updated')
221357

358+
# TODO: publishers/developers
359+
222360
# Build release date line
223361
if self.parse_expected_release_date(game):
224362
game_desc = f'\n**Expected Release Date:** {self.parse_expected_release_date(game, True)}'
@@ -311,12 +449,24 @@ async def _games_info(self, interaction: discord.Interaction):
311449
embed.add_field(name='Games Stored', value=game_count, inline=True)
312450
embed.add_field(name='Releases Stored', value=release_count, inline=True)
313451

452+
# TODO: Replace with new DekuDeals logic
453+
# https://github.com/rNintendoSwitch/MechaBowser/blob/47ea5ba33bd2345356d7c0bd49c6b0ad7599f01c/modules/games.py#L558
314454
newest_update_game = self.db.find_one(sort=[("date_last_updated", -1)])
315455
last_sync = int(newest_update_game['date_last_updated'].timestamp())
316456

317457
embed.add_field(name=f'Last Sync', value=f'<t:{last_sync}:R>', inline=False)
318458

319459
return await interaction.followup.send(embed=embed)
460+
461+
# called by core.py
462+
async def games_sync(self, interaction: discord.Interaction, full: bool):
463+
'''Force a database sync'''
464+
await interaction.response.send_message('Running sync...')
465+
466+
c, detail = await self.sync_db(full)
467+
message = f'{config.greenTick} Finished syncing {c["games"]} games and {c["releases"]} releases {detail}'
468+
return await interaction.edit_original_response(content=message)
469+
320470

321471

322472
async def setup(bot):

0 commit comments

Comments
 (0)