Skip to content

Commit 2ed01bf

Browse files
committed
Title info update
1 parent 4d13ff9 commit 2ed01bf

6 files changed

Lines changed: 111 additions & 8 deletions

File tree

‎api/love3.py‎

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
# Created by Deltaion Lee (MCMi460) on Github
2+
# Grab icons and title information from IDBE database
3+
4+
# A solid afternoon was spent. :D
5+
6+
# Thank you to 3DBrew's IDBE page (https://www.3dbrew.org/wiki/IDBE) which was written by Cruel (https://www.3dbrew.org/w/index.php?title=User:Cruel).
7+
# And thank you to Cruel again (https://github.com/Cruel) for helping with understanding the RGB565 data type and Nintendo's anger issues
8+
9+
from Cryptodome.Cipher import AES
10+
import Cryptodome.Cipher.AES
11+
from binascii import hexlify, unhexlify
12+
import requests, io, math
13+
from PIL import Image
14+
15+
def getTitleData(titleID:hex):
16+
data = requests.get('https://idbe-ctr.cdn.nintendo.net/icondata/10/%s.idbe' % titleID.zfill(16), verify = False).content
17+
18+
IV = unhexlify('A46987AE47D82BB4FA8ABC0450285FA4')
19+
20+
K0 = unhexlify('4AB9A40E146975A84BB1B4F3ECEFC47B')
21+
K1 = unhexlify('90A0BB1E0E864AE87D13A6A03D28C9B8')
22+
K2 = unhexlify('FFBB57C14E98EC6975B384FCF40786B5')
23+
K3 = unhexlify('80923799B41F36A6A75FB8B48C95F66F')
24+
25+
AES_KEYS = [K0, K1, K2, K3]
26+
27+
key = AES_KEYS[data[1]]
28+
29+
decipher = AES.new(key,AES.MODE_CBC,IV)
30+
31+
return decipher.decrypt(data[2:])
32+
33+
def getTitleInfo(titleID:hex):
34+
int(titleID, 16) # Errors if not HEX
35+
if isinstance(titleID, str):
36+
titleID = hex(int(titleID)).replace('0x', '')
37+
38+
titleID = titleID.zfill(16).upper()
39+
try:
40+
data = getTitleData(titleID)
41+
except:
42+
return None
43+
44+
for i in range(3):
45+
offset = 0x50 + i * 0x200
46+
47+
n = data[offset:offset+0x200]
48+
short = n[0x00:0x00+0x80].decode('utf-16').replace('\x00', '')
49+
long = n[0x80:0x80+0x100].decode('utf-16').replace('\x00', '')
50+
publisher = n[0x180:0x180+0x80].decode('utf-16').replace('\x00', '')
51+
if short:
52+
break
53+
54+
icon_data = data[0x24D0:0x24D0+0x1200] # 48x48 icon data
55+
56+
# Taken from Repo3DS (https://github.com/Repo3DS/shop-cache/blob/master/TitleInfo.py) -- Thank you so much, Cruel!
57+
(w, h) = (48, 48)
58+
tiled_icon = Image.frombuffer('RGB', (w, h), icon_data, 'raw', 'BGR;16')
59+
# Untile the image
60+
tile_order = [0,1,8,9,2,3,10,11,16,17,24,25,18,19,26,27,4,5,12,13,6,7,14,15,20,21,28,29,22,23,30,31,32,33,40,41,34,35,42,43,48,49,56,57,50,51,58,59,36,37,44,45,38,39,46,47,52,53,60,61,54,55,62,63]
61+
icon = Image.new('RGB', (w, h))
62+
pos = 0
63+
for y in range(0, h, 8):
64+
for x in range(0, w, 8):
65+
for k in range(8 * 8):
66+
xoff = tile_order[k] % 8
67+
yoff = int((tile_order[k] - xoff) / 8)
68+
69+
posx = pos % w
70+
posy = math.floor(pos / w)
71+
pos += 1
72+
73+
pixel = tiled_icon.getpixel((posx, posy))
74+
icon.putpixel((x + xoff, y + yoff), pixel)
75+
76+
icon.save('cache/' + titleID + '.png', 'PNG')
77+
78+
return {
79+
'short': short,
80+
'long': long,
81+
'publisher': publisher,
82+
'imageID': titleID,
83+
}

‎api/util.py‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
except:
1414
pass
1515
import typing
16+
from .love3 import *
1617

1718
def getAppPath(): # Credit to @HotaruBlaze
1819
applicationPath = os.path.expanduser('~/Documents/3DS-RPC')
@@ -142,6 +143,14 @@ def getTitle(titleID, titlesToUID, titleDatabase):
142143
if not key in game.keys():
143144
game[key] = _template[key]
144145

146+
if game == _template:
147+
response = getTitleInfo(titleID)
148+
if response:
149+
game['name'] = response['short']
150+
game['publisher']['name'] = response['publisher']
151+
game['icon_url'] = '/cdn/l/' + response['imageID']
152+
game['banner_url'] = '/cdn/l/' + response['imageID']
153+
145154
# Support browsers' security stuff
146155
game['icon_url'] = game['icon_url'].replace('https://kanzashi-ctr.cdn.nintendo.net/i/', '/cdn/i/')
147156
game['banner_url'] = game['banner_url'].replace('https://kanzashi-ctr.cdn.nintendo.net/i/', '/cdn/i/')

‎client/client.py‎

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -145,9 +145,9 @@ def loop(self):
145145

146146
game = presence['game']
147147

148-
log = 'Update'
148+
logger = 'Update'
149149
if self.currentGame != game:
150-
log += ' [%s -> %s]' % (self.currentGame['@id'], game['@id'])
150+
logger += ' [%s -> %s]' % (self.currentGame['@id'], game['@id'])
151151
self.currentGame = game
152152
self.start = int(time.time())
153153
kwargs = {
@@ -179,11 +179,11 @@ def loop(self):
179179
if self.connected:self.rpc.update(**kwargs)
180180
if self.GUI:self.GUI.update(kwargs)
181181
else:
182-
log = 'Clear [%s -> %s]' % (self.currentGame['@id'], None)
182+
logger = 'Clear [%s -> %s]' % (self.currentGame['@id'], None)
183183
self.currentGame = {'@id': None}
184184
if self.connected:self.rpc.clear()
185185
if self.GUI:self.GUI.update(None)
186-
self.gameLog.append(log)
186+
self.gameLog.append(logger)
187187

188188
def background(self):
189189
try:

‎server/discord.py‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -191,7 +191,7 @@ def deactivateDiscordUser(self, ID:int):
191191
v2 = cursor.fetchone()
192192
cursor.execute('SELECT * FROM discord WHERE ID = ?', (r[0],))
193193
v3 = cursor.fetchone()
194-
if time.time() - v3[5] >= 60:
194+
if time.time() - v3[5] >= 60 and v2:
195195
principalId = convertFriendCodeToPrincipalId(v2[0])
196196
if not v2[1]:
197197
try:

‎server/requirements.txt‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,3 +6,5 @@ gevent
66
SQLAlchemy<1.4.46
77
Flask-SQLAlchemy # Using deprecated features, apparently
88
xmltodict
9+
Pillow
10+
pycryptodomex

‎server/server.py‎

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -369,9 +369,7 @@ def userPage(friendCode:str):
369369
raise Exception(userData['Exception'])
370370
except:
371371
return render_template('dist/404.html')
372-
if userData['User']['online'] and userData['User']['Presence']:
373-
userData['User']['Presence']['game'] = getTitle(userData['User']['Presence']['titleID'], titlesToUID, titleDatabase)
374-
else:
372+
if not userData['User']['online'] or not userData['User']['Presence']:
375373
userData['User']['Presence']['game'] = None
376374
userData['User']['favoriteGame'] = getTitle(userData['User']['favoriteGame'], titlesToUID, titleDatabase)
377375
if userData['User']['favoriteGame']['name'] == 'Home Screen':
@@ -452,6 +450,10 @@ def newAlias3(friendCode:int):
452450
@limiter.limit(userPresenceLimit)
453451
def toggler(friendCode:int):
454452
fc = str(convertPrincipalIdtoFriendCode(convertFriendCodeToPrincipalId(friendCode))).zfill(12)
453+
result = db.session.execute('SELECT * FROM friends WHERE friendCode = \'%s\'' % fc)
454+
result = result.fetchone()
455+
if not result:
456+
return 'failure!'
455457
f = request.data.decode('utf-8').split(',')
456458
token = f[0]
457459
active = bool(int(f[1]))
@@ -488,6 +490,13 @@ def cdnImage(file:str):
488490
response.headers['Content-Type'] = 'image/jpeg'
489491
return response
490492

493+
# Local image cache
494+
@app.route('/cdn/l/<string:file>/', methods=['GET'])
495+
@limiter.limit(cdnLimit)
496+
def localImageCdn(file:str):
497+
file = hex(int(file, 16)).replace('0x', '').zfill(16)
498+
return send_file('cache/' + file + '.png')
499+
491500
# Login route
492501
@app.route('/login', methods=['POST'])
493502
@limiter.limit(newUserLimit)

0 commit comments

Comments
 (0)