Last active
          February 5, 2025 13:05 
        
      - 
      
 - 
        
Save garoxas/c99cf2d85f4bcbad0ad1d56b2284a310 to your computer and use it in GitHub Desktop.  
    add Required FW Version check
  
        
  
    
      This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
      Learn more about bidirectional Unicode characters
    
  
  
    
  | #!/usr/bin/env python3 | |
| # -*- coding: utf-8 -*- | |
| # Credit: | |
| # Thanks to Zotan (DB and script), Big Poppa Panda, AnalogMan, F!rsT-S0uL | |
| # Design inspiration: Lucas Rey's GUI (https://darkumbra.net/forums/topic/174470-app-cdnsp-gui-v105-download-nsp-gamez-using-a-gui/) | |
| # Thanks to the developer(s) that worked on CDNSP_Next for the cert fix! | |
| # Thanks to the help of devloper NighTime, kvn1351, gizmomelb, theLorknessMonster | |
| # CDNSP - GUI - Bob - v4 | |
| import sys | |
| import time | |
| import random | |
| # Check that user is using Python 3 | |
| if (sys.version_info > (3, 0)): | |
| # Python 3 code in this block | |
| pass | |
| else: | |
| # Python 2 code in this block | |
| print("\n\nError - Application launched with Python 2, please install Python 3 and delete Python 2\n") | |
| time.sleep(1000) | |
| sys.exit() | |
| from tkinter import * | |
| import os | |
| from tkinter import messagebox | |
| import tkinter.ttk as ttk | |
| from importlib import util | |
| import subprocess | |
| import urllib.request | |
| import pip | |
| from pathlib import Path | |
| def check_req_file(file): | |
| if not os.path.exists(file): | |
| url = 'https://raw.githubusercontent.com/bob7689/CDNSP-GUI/master/{}'.format(file) | |
| urllib.request.urlretrieve(url, file) | |
| def install_module(module): | |
| try: | |
| subprocess.check_output("pip3 install {}".format(module), shell=True) | |
| except: | |
| print("Error installing {0}, close the application and you can install the module manually by typing in CMD: pip3 install {0}".format(module)) | |
| print("\nChecking if all required modules are installed!\n\n") | |
| try: | |
| import requests | |
| except ImportError: | |
| install_module("requests") | |
| import requests | |
| try: | |
| from tqdm import tqdm | |
| except ImportError: | |
| install_module("tqdm") | |
| from tqdm import tqdm | |
| try: | |
| import unidecode | |
| except ImportError: | |
| install_module("unidecode") | |
| import unidecode | |
| try: | |
| from PIL import Image, ImageTk | |
| except ImportError: | |
| install_module("Pillow") | |
| from PIL import Image, ImageTk | |
| try: | |
| import pyopenssl | |
| except: | |
| pass | |
| req_file = ["CDNSPconfig.json", "keys.txt", "nx_tls_client_cert.pem", "titlekeys.txt"] | |
| try: | |
| for file in req_file: | |
| check_req_file(file) | |
| print("Everything looks good!") | |
| except: | |
| print("Unable to get required files! Check your internet connection") | |
| # CDNSP script | |
| import argparse | |
| import base64 | |
| import platform | |
| import re | |
| import shlex | |
| import xml.dom.minidom as minidom | |
| import xml.etree.ElementTree as ET | |
| from binascii import hexlify as hx, unhexlify as uhx | |
| from io import TextIOWrapper | |
| import os, sys | |
| import subprocess | |
| import urllib3 | |
| import json | |
| import shutil | |
| import argparse | |
| import configparser | |
| from hashlib import sha256 | |
| from struct import pack as pk, unpack as upk | |
| from binascii import hexlify as hx, unhexlify as uhx | |
| import xml.etree.ElementTree as ET, xml.dom.minidom as minidom | |
| import re | |
| import operator | |
| import platform | |
| import base64 | |
| import shlex | |
| from tkinter import filedialog | |
| import threading | |
| sys_name = "Win" | |
| global noaria | |
| noaria = True | |
| import webbrowser | |
| titlekey_list = [] | |
| global tqdmProgBar | |
| tqdmProgBar = True | |
| sysver0 = False | |
| warnfw = False | |
| switchfw = "Latest" | |
| import locale | |
| global sys_locale | |
| if platform.system() != 'Darwin': | |
| sys_locale = locale.getdefaultlocale() # Detect system locale to fix Window size on Chinese Windows Computer | |
| sys_locale = sys_locale[0] | |
| else: | |
| sys_locale = "Mac" | |
| #Global Vars | |
| truncateName = False | |
| tinfoil = False | |
| enxhop = False | |
| SWITCH_FIRMWARE = {'1.0.0': 450, '2.0.0': 65796, '2.1.0': 131162, '2.2.0': 196628, '2.3.0': 262164, '3.0.0': 201327002, '3.0.1': 201392178, '3.0.2': 201457684, | |
| '4.0.0': 268435656, '4.0.1': 268501002, '4.1.0': 269484082, '5.0.0': 335544750, '5.0.1': 335609886, '5.0.2': 335675432, '5.1.0': 336592976, | |
| 'Latest': sys.maxsize} | |
| import os, sys | |
| import re | |
| import shutil | |
| import subprocess | |
| import requests | |
| import urllib3 | |
| import json | |
| import argparse | |
| import unicodedata as ud | |
| import xml.etree.ElementTree as ET, xml.dom.minidom as minidom | |
| from tqdm import tqdm | |
| from hashlib import sha256 | |
| from struct import pack as pk, unpack as upk | |
| from binascii import hexlify as hx, unhexlify as uhx | |
| def read_at(f, off, len): | |
| f.seek(off) | |
| return f.read(len) | |
| def read_u8(f, off): | |
| return upk('<B', read_at(f, off, 1))[0] | |
| def read_u16(f, off): | |
| return upk('<H', read_at(f, off, 2))[0] | |
| def read_u32(f, off): | |
| return upk('<I', read_at(f, off, 4))[0] | |
| def read_u48(f, off): | |
| s = upk('<HI', read_at(f, off, 6)) | |
| return s[1] << 16 | s[0] | |
| def read_u64(f, off): | |
| return upk('<Q', read_at(f, off, 8))[0] | |
| def sha256_file(fPath): | |
| f = open(fPath, 'rb') | |
| fSize = os.path.getsize(fPath) | |
| hash = sha256() | |
| if fSize >= 10000: | |
| t = tqdm(total=fSize, unit='B', unit_scale=True, desc=os.path.basename(fPath), leave=False) | |
| while True: | |
| buf = f.read(4096) | |
| if not buf: | |
| break | |
| hash.update(buf) | |
| t.update(len(buf)) | |
| t.close() | |
| else: | |
| hash.update(f.read()) | |
| f.close() | |
| return hash.hexdigest() | |
| def bytes2human(n, f='%(value).3f %(symbol)s'): | |
| n = int(n) | |
| if n < 0: | |
| raise ValueError('n < 0') | |
| symbols = ('B', 'KB', 'MB', 'GB', 'TB') | |
| prefix = {} | |
| for i, s in enumerate(symbols[1:]): | |
| prefix[s] = 1 << (i + 1) * 10 | |
| for symbol in reversed(symbols[1:]): | |
| if n >= prefix[symbol]: | |
| value = float(n) / prefix[symbol] | |
| return f % locals() | |
| return f % dict(symbol=symbols[0], value=n) | |
| def get_name(tid): | |
| try: | |
| with open('titlekeys.txt',encoding="utf8") as f: | |
| lines = f.readlines() | |
| except Exception as e: | |
| print("Error:", e) | |
| exit() | |
| for line in lines: | |
| if line.strip() == '': | |
| return | |
| temp = line.split("|") | |
| if tid.endswith('800'): | |
| tid = '%s000' % tid[:-3] | |
| if tid.strip() == temp[0].strip()[:16]: | |
| return re.sub(r'[/\\:*?!"|™©®()]+', "", unidecode.unidecode(temp[2].strip())) | |
| return "UNKNOWN TITLE" | |
| def safe_name(name): | |
| return re.sub('[^\x00-\x7f]', '', ud.normalize('NFD', name)) | |
| def safe_filename(safe_name): | |
| return re.sub('[<>.:"/\\|?*]+', '', safe_name) | |
| def check_tid(tid): | |
| return re.match('0100[0-9a-fA-F]{12}', tid) | |
| def check_tkey(tkey): | |
| return re.match('[0-9a-fA-F]{32}', tkey) | |
| def load_config(fPath): | |
| try: | |
| f = open(fPath, 'r') | |
| except FileNotFoundError: | |
| print('Missing CDNSPconfig.json file!') | |
| raise | |
| j = json.load(f) | |
| hactoolPath = j['Paths']['hactoolPath'] | |
| keysPath = j['Paths']['keysPath'] | |
| NXclientPath = j['Paths']['NXclientPath'] | |
| ShopNPath = j['Paths']['ShopNPath'] | |
| nspDir = j['Values']['NspOut'] | |
| reg = j['Values']['Region'] | |
| fw = j['Values']['Firmware'] | |
| did = j['Values']['DeviceID'] | |
| env = j['Values']['Environment'] | |
| dbURL = j['Values']['TitleKeysURL'] | |
| return hactoolPath, keysPath, NXclientPath, ShopNPath, nspDir, reg, fw, did, env, dbURL | |
| def gen_tik(fPath, rightsID, tkey, mkeyrev): | |
| f = open(fPath, 'wb') | |
| f.write(b'\x04\x00\x01\x00') | |
| f.write(0x100 * b'\xFF') | |
| f.write(0x3C * b'\x00') | |
| f.write(b'Root-CA00000003-XS00000020') | |
| f.write(0x6 * b'\x00') | |
| f.write(0x20 * b'\x00') | |
| if tkey: | |
| f.write(uhx(tkey)) | |
| else: | |
| f.write(0x10 * b'\x00') | |
| f.write(0xF0 * b'\x00') | |
| f.write(b'\x02\x00\x00\x00\x00') | |
| f.write(pk('<B', int(mkeyrev))) | |
| f.write(0xA * b'\x00') | |
| f.write(0x10 * b'\x00') | |
| f.write(uhx(rightsID)) | |
| f.write(0x8 * b'\x00') | |
| f.write(b'\xC0\x02\x00\x00\x00\x00\x00\x00') | |
| f.close() | |
| return fPath | |
| def gen_cert(fPath): | |
| f = open(fPath, 'wb') | |
| f.write(uhx(b'''\ | |
| 00010003704138efbbbda16a987dd901 | |
| 326d1c9459484c88a2861b91a312587a | |
| e70ef6237ec50e1032dc39dde89a96a8 | |
| e859d76a98a6e7e36a0cfe352ca89305 | |
| 8234ff833fcb3b03811e9f0dc0d9a52f | |
| 8045b4b2f9411b67a51c44b5ef8ce77b | |
| d6d56ba75734a1856de6d4bed6d3a242 | |
| c7c8791b3422375e5c779abf072f7695 | |
| efa0f75bcb83789fc30e3fe4cc839220 | |
| 7840638949c7f688565f649b74d63d8d | |
| 58ffadda571e9554426b1318fc468983 | |
| d4c8a5628b06b6fc5d507c13e7a18ac1 | |
| 511eb6d62ea5448f83501447a9afb3ec | |
| c2903c9dd52f922ac9acdbef58c60218 | |
| 48d96e208732d3d1d9d9ea440d91621c | |
| 7a99db8843c59c1f2e2c7d9b577d512c | |
| 166d6f7e1aad4a774a37447e78fe2021 | |
| e14a95d112a068ada019f463c7a55685 | |
| aabb6888b9246483d18b9c806f474918 | |
| 331782344a4b8531334b26303263d9d2 | |
| eb4f4bb99602b352f6ae4046c69a5e7e | |
| 8e4a18ef9bc0a2ded61310417012fd82 | |
| 4cc116cfb7c4c1f7ec7177a17446cbde | |
| 96f3edd88fcd052f0b888a45fdaf2b63 | |
| 1354f40d16e5fa9c2c4eda98e798d15e | |
| 6046dc5363f3096b2c607a9d8dd55b15 | |
| 02a6ac7d3cc8d8c575998e7d796910c8 | |
| 04c495235057e91ecd2637c9c1845151 | |
| ac6b9a0490ae3ec6f47740a0db0ba36d | |
| 075956cee7354ea3e9a4f2720b26550c | |
| 7d394324bc0cb7e9317d8a8661f42191 | |
| ff10b08256ce3fd25b745e5194906b4d | |
| 61cb4c2e000000000000000000000000 | |
| 00000000000000000000000000000000 | |
| 00000000000000000000000000000000 | |
| 00000000000000000000000000000000 | |
| 526f6f74000000000000000000000000 | |
| 00000000000000000000000000000000 | |
| 00000000000000000000000000000000 | |
| 00000000000000000000000000000000 | |
| 00000001434130303030303030330000 | |
| 00000000000000000000000000000000 | |
| 00000000000000000000000000000000 | |
| 00000000000000000000000000000000 | |
| 000000007be8ef6cb279c9e2eee121c6 | |
| eaf44ff639f88f078b4b77ed9f9560b0 | |
| 358281b50e55ab721115a177703c7a30 | |
| fe3ae9ef1c60bc1d974676b23a68cc04 | |
| b198525bc968f11de2db50e4d9e7f071 | |
| e562dae2092233e9d363f61dd7c19ff3 | |
| a4a91e8f6553d471dd7b84b9f1b8ce73 | |
| 35f0f5540563a1eab83963e09be90101 | |
| 1f99546361287020e9cc0dab487f140d | |
| 6626a1836d27111f2068de4772149151 | |
| cf69c61ba60ef9d949a0f71f5499f2d3 | |
| 9ad28c7005348293c431ffbd33f6bca6 | |
| 0dc7195ea2bcc56d200baf6d06d09c41 | |
| db8de9c720154ca4832b69c08c69cd3b | |
| 073a0063602f462d338061a5ea6c915c | |
| d5623579c3eb64ce44ef586d14baaa88 | |
| 34019b3eebeed3790001000100000000 | |
| 00000000000000000000000000000000 | |
| 00000000000000000000000000000000 | |
| 00000000000000000000000000000000 | |
| 00010004969fe8288da6b9dd52c7bd63 | |
| 642a4a9ae5f053eccb93613fda379920 | |
| 87bd9199da5e6797618d77098133fd5b | |
| 05cd8288139e2e975cd2608003878cda | |
| f020f51a0e5b7692780845561b31c618 | |
| 08e8a47c3462224d94f736e9a14e56ac | |
| bf71b7f11bbdee38ddb846d6bd8f0ab4 | |
| e4948c5434eaf9bf26529b7eb83671d3 | |
| ce60a6d7a850dbe6801ec52a7b7a3e5a | |
| 27bc675ba3c53377cfc372ebce02062f | |
| 59f37003aa23ae35d4880e0e4b69f982 | |
| fb1bac806c2f75ba29587f2815fd7783 | |
| 998c354d52b19e3fad9fbef444c48579 | |
| 288db0978116afc82ce54dacb9ed7e1b | |
| fd50938f22f85eecf3a4f426ae5feb15 | |
| b72f022fb36ecce9314dad131429bfc9 | |
| 675f58ee000000000000000000000000 | |
| 00000000000000000000000000000000 | |
| 00000000000000000000000000000000 | |
| 00000000000000000000000000000000 | |
| 526f6f742d4341303030303030303300 | |
| 00000000000000000000000000000000 | |
| 00000000000000000000000000000000 | |
| 00000000000000000000000000000000 | |
| 00000001585330303030303032300000 | |
| 00000000000000000000000000000000 | |
| 00000000000000000000000000000000 | |
| 00000000000000000000000000000000 | |
| 0000000000000000d21d3ce67c1069da | |
| 049d5e5310e76b907e18eec80b337c47 | |
| 23e339573f4c664907db2f0832d03df5 | |
| ea5f160a4af24100d71afac2e3ae75af | |
| a1228012a9a21616597df71eafcb6594 | |
| 1470d1b40f5ef83a597e179fcb5b57c2 | |
| ee17da3bc3769864cb47856767229d67 | |
| 328141fc9ab1df149e0c5c15aeb80bc5 | |
| 8fc71be18966642d68308b506934b8ef | |
| 779f78e4ddf30a0dcf93fcafbfa131a8 | |
| 839fd641949f47ee25ceecf814d55b0b | |
| e6e5677c1effec6f29871ef29aa3ed91 | |
| 97b0d83852e050908031ef1abbb5afc8 | |
| b3dd937a076ff6761ab362405c3f7d86 | |
| a3b17a6170a659c16008950f7f5e06a5 | |
| de3e5998895efa7deea060be9575668f | |
| 78ab1907b3ba1b7d0001000100000000 | |
| 00000000000000000000000000000000 | |
| 00000000000000000000000000000000 | |
| 00000000000000000000000000000000'''.replace(b'\n', b''))) | |
| f.close() | |
| return fPath | |
| def decrypt_NCA(fPath, outDir=''): | |
| if not outDir: | |
| outDir = os.path.splitext(fPath)[0] | |
| os.makedirs(outDir, exist_ok=True) | |
| commandLine = hactoolPath + ' "' + fPath + '"' + keysArg\ | |
| + ' --exefsdir="' + outDir + '\\exefs"'\ | |
| + ' --romfsdir="' + outDir + '\\romfs"'\ | |
| + ' --section0dir="' + outDir + '\\section0"'\ | |
| + ' --section1dir="' + outDir + '\\section1"'\ | |
| + ' --section2dir="' + outDir + '\\section2"'\ | |
| + ' --section3dir="' + outDir + '\\section3"'\ | |
| + ' --header="' + outDir + '\\Header.bin"' | |
| pipes = subprocess.Popen(commandLine, stdout=subprocess.PIPE, stderr=subprocess.PIPE) | |
| _, std_err = pipes.communicate() | |
| if pipes.returncode != 0: | |
| err_msg = '%s. Code: %s' % (std_err.strip(), pipes.returncode) | |
| raise Exception(err_msg) | |
| elif len(std_err): | |
| raise Exception(std_err) | |
| return outDir | |
| def get_name_from_nacp(fPath): | |
| dir = decrypt_NCA(fPath) | |
| nacpPath = os.path.join(dir, 'romfs', 'control.nacp') | |
| try: | |
| f = open(nacpPath, 'rb') | |
| name = f.read(0x200).strip(b'\x00').decode() | |
| f.close() | |
| return safe_name(name) | |
| except FileNotFoundError: | |
| return '' | |
| def make_request(method, url, certificate='', hdArgs={}): | |
| if not certificate: # Workaround for defining errors | |
| certificate = NXclientPath | |
| reqHd = {'User-Agent': 'NintendoSDK Firmware/%s (platform:NX; did:%s; eid:%s)' % (fw, did, env), | |
| 'Accept-Encoding': 'gzip, deflate', | |
| 'Accept': '*/*', | |
| 'Connection': 'keep-alive'} | |
| reqHd.update(hdArgs) | |
| r = requests.request(method, url, cert=certificate, headers=reqHd, verify=False, stream=True) | |
| if r.status_code == 403: | |
| raise requests.exceptions.SSLError('Request rejected!') | |
| if r.status_code == 404: | |
| raise requests.exceptions.HTTPError('File doesn\'t exist!') | |
| return r | |
| def make_request_new(method, url, certificate='', hdArgs={}): | |
| if certificate == '': # Workaround for defining errors | |
| certificate = NXclientPath | |
| reqHd = {'User-Agent': 'NintendoSDK Firmware/%s (platform:NX; eid:%s)' % (fw, env), | |
| 'Accept-Encoding': 'gzip, deflate', | |
| 'Accept': '*/*', | |
| 'Connection': 'keep-alive'} | |
| reqHd.update(hdArgs) | |
| r = requests.request(method, url, cert=certificate, headers=reqHd, verify=False, stream=True) | |
| if r.status_code == 403: | |
| print('Request rejected by server! Check your cert.') | |
| return r | |
| return r | |
| def print_info(tid): | |
| print('\n%s:' % tid) | |
| if tid.endswith('000'): | |
| basetid = tid | |
| updatetid = '%016x' % (int(tid, 16) + 0x800) | |
| elif tid.endswith('800'): | |
| basetid = '%016x' % (int(tid, 16) & 0xFFFFFFFFFFFFF000) | |
| updatetid = tid | |
| else: | |
| basetid = '%016x' % (int(tid, 16) - 0x1000 & 0xFFFFFFFFFFFFF000) | |
| updatetid = basetid[:-3] + '800' | |
| try: | |
| _, name, size = get_info(tid=basetid) | |
| except requests.exceptions.SSLError: | |
| print('\tCould not get info from Shogun') | |
| name = '' | |
| size = 0 | |
| if name: | |
| print('\tName: %s' % name) | |
| if size: | |
| print('\tSize: %s' % bytes2human(size)) | |
| if tid != basetid: | |
| versions = get_versions(tid) | |
| print('\n\tAvailable versions for %s:' % tid) | |
| print('\t\tv' + ' v'.join(versions)) | |
| print('\n\tBase TID: %s' % basetid) | |
| versions = get_versions(basetid) | |
| print('\tAvailable versions for %s:' % basetid) | |
| print('\t\tv' + ' v'.join(versions)) | |
| print('\n\tUpdate TID: %s' % updatetid) | |
| versions = get_versions(updatetid) | |
| print('\tAvailable versions for %s:' % updatetid) | |
| if versions: | |
| print('\t\tv' + ' v'.join(versions)) | |
| else: | |
| print('\t\t%s has no version available' % updatetid) | |
| def get_info(tid='', freeword=''): | |
| print(tid) | |
| print("HEEELOOO") | |
| if tid: | |
| url = 'https://bugyo.hac.%s.eshop.nintendo.net/shogun/v1/contents/ids?shop_id=4&lang=en&country=%s&type=title&title_ids=%s'\ | |
| % (env, reg, tid) | |
| r = make_request('GET', url, certificate=ShopNPath) | |
| j = r.json() | |
| nsuid = j['id_pairs'][0]['id'] | |
| print(nsuid) | |
| elif freeword: | |
| url = 'https://bugyo.hac.lp1.eshop.nintendo.net/shogun/v1/titles?shop_id=4&lang=en&offset=0&country=%s&limit=25&sort=popular&freeword=%s'\ | |
| % (reg, freeword) | |
| r = make_request('GET', url, certificate=ShopNPath) | |
| j = r.json() | |
| nsuid = j['contents'][0]['id'] | |
| try: | |
| url = 'https://bugyo.hac.%s.eshop.nintendo.net/shogun/v1/titles/%s?shop_id=4&lang=en&country=%s' % (env, nsuid, reg) | |
| r = make_request('GET', url, certificate=ShopNPath) | |
| j = r.json() | |
| if freeword: | |
| try: | |
| tid = j['applications'][0]['id'] | |
| except KeyError: | |
| print('Found no result for %s on Shogun' % freeword) | |
| raise | |
| try: | |
| name = j['formal_name'] | |
| name = safe_name(name) | |
| except IndexError: | |
| name = '' | |
| try: | |
| size = j['total_rom_size'] | |
| except IndexError: | |
| size = 0 | |
| except IndexError: | |
| print('\tTitleID not found on Shogun!') | |
| return tid, name, size | |
| def get_versions(tid): | |
| #url = 'https://tagaya.hac.%s.eshop.nintendo.net/tagaya/hac_versionlist' % env | |
| url = 'https://superfly.hac.%s.d4c.nintendo.net/v1/t/%s/dv' % (env,tid) | |
| r = make_request_new('GET', url) | |
| j = r.json() | |
| try: | |
| if j['error']: | |
| return ['none'] | |
| except Exception as e: | |
| pass | |
| try: | |
| lastestVer = j['version'] | |
| if lastestVer < 65536: | |
| return ['%s' % lastestVer] | |
| else: | |
| versionList = ('%s' % "-".join(str(i) for i in range(0x10000, lastestVer+1, 0x10000))).split('-') | |
| return versionList | |
| except Exception as e: | |
| return ['none'] | |
| def check_versions(fPath): | |
| f = open(fPath, 'r') | |
| old = {} | |
| for line in f.readlines(): | |
| tid, ver = line.strip().split('-') | |
| old[tid] = ver | |
| n = 1 | |
| new = {} | |
| for tid in old: | |
| sys.stdout.write('\rChecking for updates for title %s of %s...' % (n, len(old))) | |
| sys.stdout.flush() | |
| n += 1 | |
| latestVer = get_versions(tid)[-1] | |
| if latestVer and int(latestVer) > int(old[tid]): | |
| new[tid] = latestVer | |
| if tid.endswith('000'): | |
| updatetid = '%016x'.lower() % (int(tid, 16) + 0x800) | |
| if updatetid not in old: | |
| updateVer = get_versions(updatetid) | |
| if updateVer: | |
| new[updatetid] = updateVer[-1] | |
| sys.stdout.write('\r\033[F') | |
| if new: | |
| for tid in new: | |
| print('New update available for %s: v%s' % (tid, new[tid])) | |
| dl = input('\nType anything to download the new updates: ') | |
| if dl: | |
| for tid in new: | |
| download_game(tid, new[tid], nspRepack=True, verify=True) | |
| else: | |
| print('No new update was found for any of the downloaded titles!') | |
| f.close() | |
| def download_file(url, fPath, fSize=0): | |
| fName = os.path.basename(fPath).split()[0] | |
| if os.path.exists(fPath) and fSize != 0: | |
| dlded = os.path.getsize(fPath) | |
| if dlded == fSize: | |
| print('\t\tDownload is already complete, skipping!') | |
| return fPath | |
| elif dlded < fSize: | |
| print('\t\tResuming download...') | |
| r = make_request('GET', url, hdArgs={'Range': 'bytes=%s-' % dlded}) | |
| f = open(fPath, 'ab') | |
| else: | |
| print('\t\tExisting file is bigger than expected (%s/%s), restarting download...' % (dlded, fSize)) | |
| dlded = 0 | |
| r = make_request('GET', url) | |
| f = open(fPath, "wb") | |
| else: | |
| dlded = 0 | |
| r = make_request('GET', url) | |
| fSize = int(r.headers.get('Content-Length')) | |
| f = open(fPath, 'wb') | |
| if fSize >= 10000: | |
| t = tqdm(initial=dlded, total=int(fSize), desc=fName, unit='B', unit_scale=True, leave=False, mininterval=0.5) | |
| for chunk in r.iter_content(4096): | |
| f.write(chunk) | |
| dlded += len(chunk) | |
| t.update(len(chunk)) | |
| t.close() | |
| else: | |
| f.write(r.content) | |
| dlded += len(r.content) | |
| if fSize != 0 and dlded != fSize: | |
| raise ValueError('Downloaded data is not as big as expected (%s/%s)!' % (dlded, fSize)) | |
| f.close() | |
| print('\r\t\tSaved to %s!' % os.path.basename(f.name)) | |
| return fPath | |
| def download_cetk(rightsID, fPath): | |
| url = 'https://atum.hac.%s.d4c.nintendo.net/r/t/%s?device_id=%s' % (env, rightsID, did) | |
| r = make_request('HEAD', url) | |
| id = r.headers.get('X-Nintendo-Content-ID') | |
| url = 'https://atum.hac.%s.d4c.nintendo.net/c/t/%s?device_id=%s' % (env, id, did) | |
| cetk = download_file(url, fPath, fSize=2496) | |
| return cetk | |
| def download_title(gameDir, tid, ver, tkey='', nspRepack=False, verify=False, n=''): | |
| print('\n%s v%s:' % (tid, ver)) | |
| if len(tid) != 16: | |
| tid = (16-len(tid)) * '0' + tid | |
| url = 'https://atum%s.hac.%s.d4c.nintendo.net/t/a/%s/%s?device_id=%s' % (n, env, tid, ver, did) | |
| r = make_request('HEAD', url) | |
| CNMTid = r.headers.get('X-Nintendo-Content-ID') | |
| print('\tDownloading CNMT (%s.cnmt.nca)...' % CNMTid) | |
| url = 'https://atum%s.hac.%s.d4c.nintendo.net/c/a/%s?device_id=%s' % (n, env, CNMTid, did) | |
| fPath = os.path.join(gameDir, CNMTid + '.cnmt.nca') | |
| cnmtNCA = download_file(url, fPath) | |
| cnmtDir = decrypt_NCA(cnmtNCA) | |
| CNMT = cnmt(os.path.join(cnmtDir, 'section0', os.listdir(os.path.join(cnmtDir, 'section0'))[0]), | |
| os.path.join(cnmtDir, 'Header.bin')) | |
| outf = os.path.join(gameDir, '%s.xml' % os.path.basename(cnmtNCA).strip('.nca')) | |
| cnmtXML = CNMT.gen_xml(cnmtNCA, outf) | |
| if warnfw and switchfw != "Latest": | |
| root = ET.parse(outf).getroot() | |
| content_meta = {} | |
| for child in root: | |
| if len(list(child)) == 0: # Content | |
| content_meta[child.tag] = child.text | |
| sysver = SWITCH_FIRMWARE[switchfw] | |
| required_sysver = int(content_meta['RequiredSystemVersion']) % 0x100000000 | |
| if required_sysver > sysver: | |
| if not messagebox.askyesno("", "This game requires Firmware version {}. Do you wish to continue downloading?" | |
| .format(next((fw for fw, ver in SWITCH_FIRMWARE.items() if ver >= required_sysver), "Unknown"))): | |
| os.remove(outf) | |
| return ([], None) # What should be returned? | |
| if nspRepack: | |
| rightsID = '%032x' % ((int(tid, 16) << 64) + int(CNMT.mkeyrev)) | |
| tikPath = os.path.join(gameDir, rightsID+'.tik') | |
| certPath = os.path.join(gameDir, rightsID+'.cert') | |
| if CNMT.type == 'Application' or CNMT.type == 'AddOnContent': | |
| gen_cert(certPath) | |
| gen_tik(tikPath, rightsID, tkey, CNMT.mkeyrev) | |
| print('\t\tGenerated %s and %s!' % (os.path.basename(certPath), os.path.basename(tikPath))) | |
| elif CNMT.type == 'Patch': | |
| print('\tDownloading CETK...') | |
| with open(download_cetk(rightsID, os.path.join(gameDir, rightsID+'.cetk')), 'rb') as cetk: | |
| cetk.seek(0x180) | |
| tkey = hx(cetk.read(0x10)).decode() | |
| print('\t\t\tTitlekey: %s' % tkey) | |
| with open(tikPath, 'wb') as tik: | |
| cetk.seek(0x0) | |
| tik.write(cetk.read(0x2C0)) | |
| with open(certPath, 'wb') as cert: | |
| cetk.seek(0x2C0) | |
| cert.write(cetk.read(0x700)) | |
| print('\t\tExtracted %s and %s from CETK!' % (os.path.basename(certPath), os.path.basename(tikPath))) | |
| NCAs = { | |
| 0: [], | |
| 1: [], | |
| 2: [], | |
| 3: [], | |
| 4: [], | |
| 5: [], | |
| 6: [], | |
| } | |
| name = '' | |
| for type in [0, 3, 4, 5, 1, 2, 6]: # Download smaller files first | |
| cnmtlist = CNMT.parse(CNMT.contentTypes[type]) | |
| for ncaID in cnmtlist: | |
| print('\tDownloading %s entry (%s.nca)...' % (CNMT.contentTypes[type], ncaID)) | |
| url = 'https://atum%s.hac.%s.d4c.nintendo.net/c/c/%s?device_id=%s' % (n, env, ncaID, did) | |
| fPath = os.path.join(gameDir, ncaID + '.nca') | |
| fSize = cnmtlist[ncaID][1] | |
| NCAs[type].append(download_file(url, fPath, fSize)) | |
| if verify: | |
| print('\t\tVerifying file...') | |
| if sha256_file(fPath) == cnmtlist[ncaID][2]: | |
| print('\t\t\tHashes match, file is correct!') | |
| else: | |
| print('\t\t\t%s is corrupted, hashes don\'t match!' % os.path.basename(fPath)) | |
| if type == 3: | |
| name = get_name_from_nacp(NCAs[type][-1]) | |
| if not name: | |
| name = '' | |
| if nspRepack: | |
| files = [] | |
| if tkey: | |
| files.append(certPath) | |
| files.append(tikPath) | |
| for key in [1, 5, 2, 4, 6]: | |
| if NCAs[key]: | |
| files.extend(NCAs[key]) | |
| files.append(cnmtNCA) | |
| files.append(cnmtXML) | |
| if NCAs[3]: | |
| files.extend(NCAs[3]) | |
| return files, name | |
| else: | |
| return gameDir, name | |
| def download_game(tid, ver, tkey='', nspRepack=False, verify=False, clean=False, path_Dir=""): | |
| print(path_Dir) | |
| name = get_name(tid) | |
| global titlekey_check | |
| gameType = '' | |
| basetid = '' | |
| if name == 'Unknown Title': | |
| temp = "[" + tid + "]" | |
| else: | |
| temp = name + " [" + tid + "]" | |
| if tid.endswith('000'): # Base game | |
| gameType = 'BASE' | |
| elif tid.endswith('800'): # Update | |
| basetid = '%s000' % tid[:-3] | |
| gameType = 'UPD' | |
| else: # DLC | |
| basetid = '%s%s000' % (tid[:-4], str(int(tid[-4], 16) - 1)) | |
| gameType = 'DLC' | |
| if path_Dir == "": | |
| path_Dir = os.path.join(os.path.dirname(__file__), "_NSPOUT") | |
| gameDir = os.path.join(path_Dir, tid) | |
| if not os.path.exists(gameDir): | |
| os.makedirs(gameDir, exist_ok=True) | |
| outputDir = path_Dir | |
| if not os.path.exists(outputDir): | |
| os.makedirs(outputDir, exist_ok=True) | |
| if name != "": | |
| if gameType == "DLC": | |
| outf = os.path.join(outputDir, '%s [%s][v%s]' % (name,tid,ver)) | |
| elif gameType == 'BASE': | |
| outf = os.path.join(outputDir, '%s [%s][v%s]' % (name,tid,ver)) | |
| else: | |
| outf = os.path.join(outputDir, '%s [%s][%s][v%s]' % (name,gameType,tid,ver)) | |
| else: | |
| if gameType == "DLC": | |
| outf = os.path.join(outputDir, '%s [%s][v%s]' % (name,tid,ver)) | |
| elif gameType == 'BASE': | |
| outf = os.path.join(outputDir, '%s [v%s]' % (tid,ver)) | |
| else: | |
| outf = os.path.join(outputDir, '%s [%s][v%s]' % (tid,gameType,ver)) | |
| outname = outf.split(outputDir)[1][1:] | |
| if truncateName: | |
| name = name.replace(' ','')[0:20] | |
| outf = os.path.join(outputDir, '%s%sv%s' % (name,tid,ver)) | |
| if tinfoil: | |
| outf = outf + '[tf]' | |
| outf = outf + '.nsp' | |
| for item in os.listdir(outputDir): | |
| if item.find('%s' % tid) != -1: | |
| if item.find('v%s' % ver) != -1: | |
| if not tinfoil: | |
| print('%s already exists, skipping download' % outf) | |
| shutil.rmtree(gameDir) | |
| return | |
| os.makedirs(gameDir, exist_ok=True) | |
| if tid.endswith('800'): | |
| basetid = '%016x' % (int(tid, 16) & 0xFFFFFFFFFFFFF000) | |
| elif not tid.endswith('000'): | |
| basetid = '%016x' % (int(tid, 16) - 0x1000 & 0xFFFFFFFFFFFFF000) | |
| else: | |
| basetid = tid | |
| if nspRepack: | |
| files, name = download_title(gameDir, tid, ver, tkey, nspRepack, verify) | |
| else: | |
| _, name = download_title(gameDir, tid, ver, tkey, nspRepack, verify) | |
| if name is None: | |
| shutil.rmtree(gameDir, ignore_errors=True) | |
| return gameDir | |
| try: | |
| _, name, _ = get_info(tid=basetid) | |
| except requests.exceptions.SSLError: | |
| pass | |
| finally: | |
| if ver == '0': | |
| nspName = '-'.join([tid, safe_filename(name)]) | |
| else: | |
| nspName = '-'.join([tid, ver, safe_filename(name)]) | |
| if nspRepack: | |
| os.makedirs(path_Dir, exist_ok=True) | |
| outf = os.path.join(path_Dir, outname+'.nsp') | |
| NSP = nsp(outf, files) | |
| NSP.repack() | |
| shutil.rmtree(gameDir, ignore_errors=True) | |
| return gameDir | |
| def download_sysupdate(ver): | |
| if ver == 'LTST': | |
| url = 'https://sun.hac.%s.d4c.nintendo.net/v1/system_update_meta?device_id=%s' % (env, did) | |
| r = make_request('GET', url) | |
| j = r.json() | |
| ver = str(j['system_update_metas'][0]['title_version']) | |
| sysupdateDir = os.path.join(os.path.dirname(__file__), '0100000000000816-SysUpdate', ver) | |
| os.makedirs(sysupdateDir, exist_ok=True) | |
| url = 'https://atumn.hac.%s.d4c.nintendo.net/t/s/0100000000000816/%s?device_id=%s' % (env, ver, did) | |
| r = make_request('HEAD', url) | |
| cnmtID = r.headers.get('X-Nintendo-Content-ID') | |
| print('\nDownloading CNMT (%s)...' % cnmtID) | |
| url = 'https://atumn.hac.%s.d4c.nintendo.net/c/s/%s?device_id=%s' % (env, cnmtID, did) | |
| fPath = os.path.join(sysupdateDir, '%s.cnmt.nca' % cnmtID) | |
| cnmtNCA = download_file(url, fPath) | |
| cnmtDir = decrypt_NCA(cnmtNCA) | |
| CNMT = cnmt(os.path.join(cnmtDir, 'section0', os.listdir(os.path.join(cnmtDir, 'section0'))[0]), | |
| os.path.join(cnmtDir, 'Header.bin')) | |
| titles = CNMT.parse() | |
| for title in titles: | |
| dir = os.path.join(sysupdateDir, title) | |
| os.makedirs(dir, exist_ok=True) | |
| download_title(dir, title, titles[title][0], n='n') | |
| return sysupdateDir | |
| class cnmt: | |
| titleTypes = { | |
| 0x1: 'SystemProgram', | |
| 0x2: 'SystemData', | |
| 0x3: 'SystemUpdate', | |
| 0x4: 'BootImagePackage', | |
| 0x5: 'BootImagePackageSafe', | |
| 0x80:'Application', | |
| 0x81:'Patch', | |
| 0x82:'AddOnContent', | |
| 0x83:'Delta' | |
| } | |
| contentTypes = { | |
| 0:'Meta', | |
| 1:'Program', | |
| 2:'Data', | |
| 3:'Control', | |
| 4:'HtmlDocument', | |
| 5:'LegalInformation', | |
| 6:'DeltaFragment' | |
| } | |
| def __init__(self, fPath, hdPath): | |
| f = open(fPath, 'rb') | |
| self.path = fPath | |
| self.type = self.titleTypes[read_u8(f, 0xC)] | |
| self.id = '%016x' % read_u64(f, 0x0) | |
| self.ver = str(read_u32(f, 0x8)) | |
| self.sysver = str(read_u64(f, 0x28)) | |
| self.dlsysver = str(read_u64(f, 0x18)) | |
| self.digest = hx(read_at(f, f.seek(0, 2)-0x20, f.seek(0, 2))).decode() | |
| with open(hdPath, 'rb') as ncaHd: | |
| self.mkeyrev = str(read_u8(ncaHd, 0x220)) | |
| f.close() | |
| def parse(self, contentType=''): | |
| f = open(self.path, 'rb') | |
| data = {} | |
| if self.type == 'SystemUpdate': | |
| metaEntriesNB = read_u16(f, 0x12) | |
| for n in range(metaEntriesNB): | |
| offset = 0x20 + 0x10*n | |
| tid = '%016x' % read_u64(f, offset) | |
| ver = str(read_u32(f, offset+0x8)) | |
| titleType = self.titleTypes[read_u8(f, offset+0xC)] | |
| data[tid] = ver, titleType | |
| else: | |
| tableOffset = read_u16(f,0xE) | |
| contentEntriesNB = read_u16(f, 0x10) | |
| for n in range(contentEntriesNB): | |
| offset = 0x20 + tableOffset + 0x38*n | |
| hash = hx(read_at(f, offset, 0x20)).decode() | |
| tid = hx(read_at(f, offset+0x20, 0x10)).decode() | |
| size = read_u48(f, offset+0x30) | |
| type = self.contentTypes[read_u16(f, offset+0x36)] | |
| if type == contentType or contentType == '': | |
| data[tid] = type, size, hash | |
| f.close() | |
| return data | |
| def gen_xml(self, ncaPath, outf): | |
| data = self.parse() | |
| ContentMeta = ET.Element('ContentMeta') | |
| ET.SubElement(ContentMeta, 'Type').text = self.type | |
| ET.SubElement(ContentMeta, 'Id').text = '0x' + self.id | |
| ET.SubElement(ContentMeta, 'Version').text = self.ver | |
| ET.SubElement(ContentMeta, 'RequiredDownloadSystemVersion').text = self.dlsysver | |
| n = 1 | |
| for tid in data: | |
| locals()["Content"+str(n)] = ET.SubElement(ContentMeta, 'Content') | |
| ET.SubElement(locals()["Content"+str(n)], 'Type').text = data[tid][0] | |
| ET.SubElement(locals()["Content"+str(n)], 'Id').text = tid | |
| ET.SubElement(locals()["Content"+str(n)], 'Size').text = str(data[tid][1]) | |
| ET.SubElement(locals()["Content"+str(n)], 'Hash').text = data[tid][2] | |
| ET.SubElement(locals()["Content"+str(n)], 'KeyGeneration').text = self.mkeyrev | |
| n += 1 | |
| # cnmt.nca itself | |
| cnmt = ET.SubElement(ContentMeta, 'Content') | |
| ET.SubElement(cnmt, 'Type').text = 'Meta' | |
| ET.SubElement(cnmt, 'Id').text = os.path.basename(ncaPath).split('.')[0] | |
| ET.SubElement(cnmt, 'Size').text = str(os.path.getsize(ncaPath)) | |
| ET.SubElement(cnmt, 'Hash').text = sha256_file(ncaPath) | |
| ET.SubElement(cnmt, 'KeyGeneration').text = self.mkeyrev | |
| ET.SubElement(ContentMeta, 'Digest').text = self.digest | |
| ET.SubElement(ContentMeta, 'KeyGenerationMin').text = self.mkeyrev | |
| ET.SubElement(ContentMeta, 'RequiredSystemVersion').text = self.sysver | |
| if self.type == 'Application': | |
| ET.SubElement(ContentMeta, 'PatchId').text = '0x%016x' % (int(self.id, 16) + 0x800) | |
| elif self.type == 'Patch': | |
| ET.SubElement(ContentMeta, 'OriginalId').text = '0x%016x' % (int(self.id, 16) & 0xFFFFFFFFFFFFF000) | |
| elif self.type == 'AddOnContent': | |
| ET.SubElement(ContentMeta, 'ApplicationId').text = '0x%016x' % (int(self.id, 16) - 0x1000 & 0xFFFFFFFFFFFFF000) | |
| string = ET.tostring(ContentMeta, encoding='utf-8') | |
| reparsed = minidom.parseString(string) | |
| with open(outf, 'wb') as f: | |
| f.write(reparsed.toprettyxml(encoding='utf-8', indent=' ')[:-1]) | |
| print('\t\tGenerated %s!' % os.path.basename(outf)) | |
| return outf | |
| class nsp: | |
| def __init__(self, outf, files): | |
| self.path = outf | |
| self.files = files | |
| def repack(self): | |
| print('\tRepacking to NSP...') | |
| hd = self._gen_header() | |
| totSize = len(hd) + sum(os.path.getsize(file) for file in self.files) | |
| if os.path.exists(self.path) and os.path.getsize(self.path) == totSize: | |
| print('\t\tRepack %s is already complete!' % self.path) | |
| return | |
| t = tqdm(total=totSize, unit='B', unit_scale=True, desc=os.path.basename(self.path), leave=False) | |
| t.write('\t\tWriting header...') | |
| outf = open(self.path, 'wb') | |
| outf.write(hd) | |
| t.update(len(hd)) | |
| for file in self.files: | |
| t.write('\t\tAppending %s...' % os.path.basename(file)) | |
| with open(file, 'rb') as inf: | |
| while True: | |
| buf = inf.read(4096) | |
| if not buf: | |
| break | |
| outf.write(buf) | |
| t.update(len(buf)) | |
| t.close() | |
| outf.close() | |
| print('\t\tRepacked to %s!' % outf.name) | |
| def _gen_header(self): | |
| filesNb = len(self.files) | |
| stringTable = '\x00'.join(os.path.basename(file) for file in self.files) | |
| headerSize = 0x10 + filesNb*0x18 + len(stringTable) | |
| remainder = 0x10 - headerSize%0x10 | |
| headerSize += remainder | |
| fileSizes = [os.path.getsize(file) for file in self.files] | |
| fileOffsets = [sum(fileSizes[:n]) for n in range(filesNb)] | |
| fileNamesLengths = [len(os.path.basename(file))+1 for file in self.files] # +1 for the \x00 | |
| stringTableOffsets = [sum(fileNamesLengths[:n]) for n in range(filesNb)] | |
| header = b'' | |
| header += b'PFS0' | |
| header += pk('<I', filesNb) | |
| header += pk('<I', len(stringTable)+remainder) | |
| header += b'\x00\x00\x00\x00' | |
| for n in range(filesNb): | |
| header += pk('<Q', fileOffsets[n]) | |
| header += pk('<Q', fileSizes[n]) | |
| header += pk('<I', stringTableOffsets[n]) | |
| header += b'\x00\x00\x00\x00' | |
| header += stringTable.encode() | |
| header += remainder * b'\x00' | |
| return header | |
| def game_image(tid, ver, tkey="", nspRepack=False, n='',verify=False): | |
| import glob | |
| if not os.path.isdir("Images"): | |
| os.mkdir("Images") | |
| if not os.path.isdir("Images/{}".format(tid)): | |
| os.mkdir("Images/{}".format(tid)) | |
| gameDir = "Images/{}".format(tid) | |
| if os.path.isdir(os.path.dirname(os.path.abspath(__file__))+'/Images/{}/section0/'.format(tid)): | |
| for fname in os.listdir(os.path.dirname(os.path.abspath(__file__))+'/Images/{}/section0/'.format(tid)): | |
| if fname.endswith('.jpg'): | |
| return (gameDir, "Exist") | |
| tid = tid.lower(); | |
| tkey = tkey.lower(); | |
| if len(tid) != 16: | |
| tid = (16-len(tid)) * '0' + tid | |
| url = 'https://atum%s.hac.%s.d4c.nintendo.net/t/a/%s/%s?device_id=%s' % (n, env, tid, ver, did) | |
| try: | |
| r = make_request('HEAD', url) | |
| except Exception as e: | |
| pass | |
| CNMTid = r.headers.get('X-Nintendo-Content-ID') | |
| if CNMTid is None: | |
| print("not a valid title") | |
| fPath = os.path.join(gameDir, CNMTid + '.cnmt.nca') | |
| cnmtNCA = download_file(url, fPath) | |
| cnmtDir = decrypt_NCA(cnmtNCA) | |
| CNMT = cnmt(os.path.join(cnmtDir, 'section0', os.listdir(os.path.join(cnmtDir, 'section0'))[0]), | |
| os.path.join(cnmtDir, 'Header.bin')) | |
| NCAs = { | |
| 0: [], | |
| 1: [], | |
| 2: [], | |
| 3: [], | |
| 4: [], | |
| 5: [], | |
| 6: [], | |
| } | |
| for type in [3]: # Download smaller files first | |
| list = CNMT.parse(CNMT.contentTypes[type]) | |
| for ncaID in list: | |
| print('\tDownloading %s entry (%s.nca)...' % (CNMT.contentTypes[type], ncaID)) | |
| url = 'https://atum%s.hac.%s.d4c.nintendo.net/c/c/%s?device_id=%s' % (n, env, ncaID, did) | |
| fPath = os.path.join(gameDir, "control" + '.nca') | |
| fSize = list[ncaID][1] | |
| NCAs[type].append(download_file(url, fPath, fSize)) | |
| if verify: | |
| print('\t\tVerifying file...') | |
| if sha256_file(fPath) == list[ncaID][2]: | |
| print('\t\t\tHashes match, file is correct!') | |
| else: | |
| print('\t\t\t%s is corrupted, hashes don\'t match!' % os.path.basename(fPath)) | |
| return (gameDir, "N") | |
| # End of CDNSP script | |
| # -------------------------- | |
| # GUI code begins | |
| f = open("titlekeys.txt", "r", encoding="utf8") | |
| content = f.readlines() | |
| global titleID_list | |
| global titleKey_list | |
| global title_list | |
| titleID_list = [] | |
| titleKey_list = [] | |
| title_list = [] | |
| for i in range(len(content)): | |
| titleID = "" | |
| try: | |
| titleID, titleKey, title = content[i].split("|") | |
| except: | |
| print("Check if there's extra spaces at the bottom of your titlekeys.txt file! Delete if you do!") | |
| if titleID != "": | |
| titleID_list.append(titleID[:16]) | |
| titleKey_list.append(titleKey) | |
| if title[:-1] == "\n": | |
| title_list.append(title[:-1]) | |
| else: | |
| title_list.append(title) | |
| f.close() | |
| def updateJsonFile(key, value): | |
| if os.path.isfile("CDNSP-GUI-config.json"): | |
| with open("CDNSP-GUI-config.json", "r") as jsonFile: | |
| data = json.load(jsonFile) | |
| data["Options"]["{}".format(key)] = value | |
| with open("CDNSP-GUI-config.json", "w") as jsonFile: | |
| json.dump(data, jsonFile, indent=4) | |
| else: | |
| print("Error!, Missing CDNSP-GUI-config.json file") | |
| def GUI_config(fPath): | |
| if sys_locale != "zh_CN": | |
| main_win = "650x530+100+100" | |
| queue_win = "620x300+770+100" | |
| else: | |
| main_win = "750x550+100+100" | |
| queue_win = "720x300+770+100" | |
| config = {"Options": { | |
| "Download_location": "", | |
| "Game_location": "", | |
| "NSP_repack": "True", | |
| "Mute": "False", | |
| "Titlekey_check": "True", | |
| "noaria": "True", | |
| "Disable_game_image": "False", | |
| "Shorten": "False", | |
| "Tinfoil": "False", | |
| "SysVerZero": "False", | |
| "WarnFirmware": "False", | |
| "SwitchFirmware": "Latest", | |
| "Main_win": main_win, | |
| "Queue_win": queue_win, | |
| "Update_win": "600x400+120+200"}} | |
| try: | |
| f = open(fPath, 'r') | |
| except FileNotFoundError: | |
| f = open(fPath, 'w') | |
| json.dump(config, f, indent=4) | |
| f.close() | |
| f = open(fPath, 'r') | |
| j = json.load(f) | |
| def str2bool(v): | |
| return v.lower() == "true" | |
| download_location = j['Options']['Download_location'] | |
| game_location = j['Options']['Game_location'] | |
| repack = str2bool(j['Options']['NSP_repack']) | |
| mute = str2bool(j['Options']['Mute']) | |
| titlekey_check = str2bool(j['Options']['Titlekey_check']) | |
| noaria = str2bool(j['Options']['noaria']) | |
| disable_game_image = str2bool(j['Options']['Disable_game_image']) | |
| shorten = str2bool(j['Options']['Shorten']) | |
| tinfoil = str2bool(j['Options']['Tinfoil']) | |
| sysver0 = str2bool(j['Options']['SysVerZero']) | |
| try: | |
| warnfw = str2bool(j['Options']['WarnFirmware']) | |
| except KeyError: | |
| warnfw = False | |
| try: | |
| switchfw = j['Options']['SwitchFirmware'] | |
| except KeyError: | |
| switchfw = "Latest" | |
| main_win = j['Options']['Main_win'] | |
| queue_win = j['Options']['Queue_win'] | |
| update_win = j['Options']['Update_win'] | |
| if not os.path.exists(download_location): # If the custom download directory doesn't exist then use default path | |
| download_location = "" | |
| updateJsonFile("Download_location", download_location) | |
| return download_location, game_location, repack, mute, titlekey_check, noaria, \ | |
| disable_game_image, shorten, tinfoil, sysver0, warnfw, switchfw, main_win, queue_win, update_win | |
| class Application(): | |
| def __init__(self, root, titleID, titleKey, title, dbURL): | |
| global main_win | |
| global queue_win | |
| global update_win_size | |
| configGUIPath = os.path.join(os.path.dirname(__file__), 'CDNSP-GUI-config.json') # Load config file | |
| self.path, self.game_location, self.repack, self.mute, self.titlekey_check, noaria_temp, \ | |
| self.game_image_disable, shorten_temp, tinfoil_temp, sysver0_temp, warnfw_temp, switchfw_temp, \ | |
| main_win, queue_win, update_win = GUI_config(configGUIPath) # Get config values | |
| update_win_size = update_win | |
| self.root = root | |
| self.root.geometry(main_win) | |
| self.titleID = titleID | |
| self.titleKey = titleKey | |
| self.title = title | |
| self.first_queue = True | |
| self.queue_list = [] | |
| self.persistent_queue = [] | |
| self.db_URL = dbURL | |
| self.current_status = [] | |
| self.installed = [] # List of TID already installed | |
| self.installed_ver = [] | |
| if os.path.exists(r"Config/installed.txt"): | |
| f = open(r"Config/installed.txt", "r") | |
| for line in f.readlines(): | |
| tid = line.split(",")[0].strip() | |
| ver = line.split(",")[1].strip() | |
| if ver != "0": | |
| tid = "{}800".format(tid[:13]) | |
| self.installed.append(tid) | |
| self.installed_ver.append(ver) | |
| self.listWidth = 67 | |
| global sys_name | |
| ## global save_game_folder | |
| ## save_game_folder = False -- To be worked on in the future | |
| global titlekey_check | |
| titlekey_check = self.titlekey_check | |
| if platform.system() == 'Linux': | |
| sys_name = "Linux" | |
| if platform.system() == 'Darwin': | |
| sys_name = "Mac" | |
| self.listWidth = 39 | |
| self.sys_name = sys_name | |
| global noaria | |
| noaria = True | |
| global truncateName | |
| truncateName = shorten_temp | |
| global tinfoil | |
| tinfoil = tinfoil_temp | |
| global sysver0 | |
| sysver0 = sysver0_temp | |
| global warnfw | |
| warnfw = warnfw_temp | |
| global switchfw | |
| switchfw = switchfw_temp | |
| # Top Menu bar | |
| self.menubar = Menu(self.root) | |
| # Download Menu Tab | |
| self.downloadMenu = Menu(self.menubar, tearoff=0) | |
| self.downloadMenu.add_command(label="Select Download Location", command=self.change_dl_path) | |
| self.downloadMenu.add_command(label="Preload Game Images", command=self.preload_images) | |
| self.downloadMenu.add_separator() # Add separator to the menu dropdown | |
| self.downloadMenu.add_command(label="Load Saved Queue", command=self.import_persistent_queue) | |
| self.downloadMenu.add_command(label="Save Queue", command=self.export_persistent_queue) | |
| # Options Menu Tab | |
| self.optionMenu = Menu(self.menubar, tearoff=0) | |
| self.optionMenu.add_command(label="Aria2c will be missed", command=self.disable_aria2c) | |
| self.optionMenu.add_command(label="DISABLE GAME IMAGE", command=self.disable_game_image) | |
| self.optionMenu.add_separator() # Add separator to the menu dropdown | |
| self.optionMenu.add_command(label="Mute All Pop-ups", command=self.mute_all) | |
| self.optionMenu.add_command(label="Disable NSP Repack", command=self.nsp_repack_option) | |
| self.optionMenu.add_command(label="Disable Titlekey check", command=self.titlekey_check_option) | |
| self.optionMenu.add_separator() # Add separator to the menu dropdown | |
| self.optionMenu.add_command(label="Enable Shorten Name", command=self.shorten) | |
| self.optionMenu.add_command(label="Enable Tinfoil Download", command=self.tinfoil_change) | |
| self.optionMenu.add_command(label="Enable SysVer 0 Patch", command=self.sysver_zero) | |
| self.optionMenu.add_separator() # Add separator to the menu dropdown | |
| self.optionMenu.add_command(label="Warn Required Firmware", command=self.warn_firmware) | |
| # Firmware Menu Tab | |
| self.firmwareMenu = Menu(self.optionMenu, tearoff=0) | |
| self.optionMenu.add_cascade(label="Switch System Firmware", menu=self.firmwareMenu) | |
| #for fw in SWITCH_FIRMWARE: | |
| # self.firmwareMenu.add_command(label=fw, command=lambda: self.switch_firmware(fw)) | |
| self.firmwareMenu.add_command(label="1.0.0", command=lambda: self.switch_firmware("1.0.0")) | |
| self.firmwareMenu.add_command(label="2.0.0", command=lambda: self.switch_firmware("2.0.0")) | |
| self.firmwareMenu.add_command(label="2.1.0", command=lambda: self.switch_firmware("2.1.0")) | |
| self.firmwareMenu.add_command(label="2.2.0", command=lambda: self.switch_firmware("2.2.0")) | |
| self.firmwareMenu.add_command(label="2.3.0", command=lambda: self.switch_firmware("2.3.0")) | |
| self.firmwareMenu.add_command(label="3.0.0", command=lambda: self.switch_firmware("3.0.0")) | |
| self.firmwareMenu.add_command(label="3.0.1", command=lambda: self.switch_firmware("3.0.1")) | |
| self.firmwareMenu.add_command(label="3.0.2", command=lambda: self.switch_firmware("3.0.2")) | |
| self.firmwareMenu.add_command(label="4.0.0", command=lambda: self.switch_firmware("4.0.0")) | |
| self.firmwareMenu.add_command(label="4.0.1", command=lambda: self.switch_firmware("4.0.1")) | |
| self.firmwareMenu.add_command(label="4.1.0", command=lambda: self.switch_firmware("4.1.0")) | |
| self.firmwareMenu.add_command(label="5.0.0", command=lambda: self.switch_firmware("5.0.0")) | |
| self.firmwareMenu.add_command(label="5.0.1", command=lambda: self.switch_firmware("5.0.1")) | |
| self.firmwareMenu.add_command(label="5.0.2", command=lambda: self.switch_firmware("5.0.2")) | |
| self.firmwareMenu.add_command(label="5.1.0", command=lambda: self.switch_firmware("5.1.0")) | |
| self.firmwareMenu.add_command(label="Latest", command=lambda: self.switch_firmware("Latest")) | |
| self.optionMenu.add_separator() # Add separator to the menu dropdown | |
| self.optionMenu.add_command(label="Save Windows Location and Size", command=self.window_save) | |
| # Tool Menu Tab | |
| self.toolMenu = Menu(self.menubar, tearoff=0) | |
| self.toolMenu.add_command(label="Scan for existing games", command=self.my_game_GUI) | |
| self.toolMenu.add_command(label="Base64 Decoder", command=self.base_64_GUI) | |
| # Menubar config | |
| self.menubar.add_cascade(label="Download", menu=self.downloadMenu) | |
| self.menubar.add_cascade(label="Options", menu=self.optionMenu) | |
| self.menubar.add_cascade(label="Tools", menu=self.toolMenu) | |
| self.root.config(menu=self.menubar) | |
| # Change Menu Label Based on loaded values | |
| if self.repack == True: | |
| self.optionMenu.entryconfig(4, label= "Disable NSP Repack") | |
| else: | |
| self.optionMenu.entryconfig(4, label= "Enable NSP Repack") | |
| if self.mute == True: | |
| self.optionMenu.entryconfig(3, label= "Unmute All Pop-ups") | |
| else: | |
| self.optionMenu.entryconfig(3, label= "Mute All Pop-ups") | |
| if self.titlekey_check == True: | |
| self.optionMenu.entryconfig(5, label= "Disable Titlekey check") | |
| else: | |
| self.optionMenu.entryconfig(5, label= "Enable Titlekey check") | |
| if self.game_image_disable == True: | |
| self.optionMenu.entryconfig(1, label= "ENABLE GAME IMAGE") | |
| else: | |
| self.optionMenu.entryconfig(1, label= "DISABLE GAME IMAGE") | |
| if truncateName == True: | |
| self.optionMenu.entryconfig(7, label= "Disable Shorten Name") | |
| else: | |
| self.optionMenu.entryconfig(7, label= "Enable Shorten Name") | |
| if tinfoil == True: | |
| self.optionMenu.entryconfig(8, label= "Disable Tinfoil Download") | |
| else: | |
| self.optionMenu.entryconfig(8, label= "Enable Tinfoil Download") | |
| if sysver0 == True: | |
| self.optionMenu.entryconfig(9, label= "Disable SysVer 0 Patch") | |
| else: | |
| self.optionMenu.entryconfig(9, label= "Enable SysVer 0 Patch") | |
| if warnfw == True: | |
| self.optionMenu.entryconfig(11, label= "Ignore Required Firmware") | |
| else: | |
| self.optionMenu.entryconfig(11, label= "Warn Required Firmware") | |
| try: | |
| index = list(SWITCH_FIRMWARE).index(switchfw) | |
| self.firmwareMenu.entryconfig(index, label= switchfw + " *") | |
| except: | |
| pass | |
| # Status Label | |
| self.status_label = Label(self.root, text="Status:") | |
| self.status_label.grid(row=0, column=0, columnspan=2, sticky=NS) | |
| # Game selection section | |
| self.search_var = StringVar() | |
| self.search_var.trace("w", lambda name, index, mode: self.update_list(True)) | |
| game_selection_frame = Frame(self.root) | |
| game_selection_frame.grid(row=1, column=0, padx=20, pady=20, sticky=N) | |
| # Filter entrybox | |
| if sys_name != "Mac": | |
| entryWidth = self.listWidth + 3 | |
| else: | |
| entryWidth = self.listWidth | |
| self.entry = Entry(game_selection_frame, textvariable=self.search_var, width=entryWidth) | |
| self.entry.grid(row=0, column=0, columnspan=2, sticky=N) | |
| # Setup Listbox and scrollbar | |
| self.scrollbar = Scrollbar(game_selection_frame) | |
| ## self.scrollbar.grid(row=1, column=1, sticky=N+S+W) | |
| self.title_list = Listbox(game_selection_frame, exportselection = False,\ | |
| yscrollcommand = self.scrollbar.set, width=self.listWidth, selectmode=EXTENDED) | |
| self.title_list.bind('<<ListboxSelect>>', self.game_info) | |
| ## self.title_list.grid(row=1, column=0, sticky=W) | |
| self.scrollbar.config(command = self.title_list.yview) | |
| # Setup Treeview and Two Scrollbars | |
| container = ttk.Frame(game_selection_frame) | |
| container.grid(row=1, column=0, columnspan=2) | |
| self.tree = ttk.Treeview(columns=("num", "G", "S"), show="headings", selectmode=EXTENDED) | |
| self.tree.bind('<<TreeviewSelect>>', self.game_info) | |
| self.tree.heading("num", text="#", command=lambda c="num": self.sortby(self.tree, c, 0)) | |
| self.tree.column("num", width=40) | |
| self.tree.heading("G", text="Game", command=lambda c="G": self.sortby(self.tree, c, 0)) | |
| self.tree.column("G", width=590) | |
| self.tree.heading("S", text="State", command=lambda c="S": self.sortby(self.tree, c, 0)) | |
| self.tree.column("S", width=60) | |
| vsb = ttk.Scrollbar(orient="vertical", command=self.tree.yview) | |
| hsb = ttk.Scrollbar(orient="horizontal", command=self.tree.xview) | |
| self.tree.configure(yscrollcommand=vsb.set, xscrollcommand=hsb.set) | |
| self.tree.grid(column=0, row=0, sticky='nsew', in_=container) | |
| vsb.grid(column=1, row=0, sticky='ns', in_=container) | |
| hsb.grid(column=0, row=1, sticky='ew', in_=container) | |
| container.grid_columnconfigure(0, weight=1) | |
| container.grid_rowconfigure(0, weight=1) | |
| # Info labels | |
| self.sizeLabel = Label(game_selection_frame, text="Game Image:") | |
| self.sizeLabel.grid(row=2, column=0, pady=(20, 0), columnspan=2) | |
| self.update_list() | |
| self.image = Image.open("blank.jpg") | |
| self.photo = ImageTk.PhotoImage(self.image) | |
| self.imageLabel = Label(game_selection_frame, image=self.photo, borderwidth=0, highlightthickness=0, cursor="hand2") | |
| self.imageLabel.bind("<Button-1>", self.eShop_link) | |
| self.imageLabel.image = self.photo # keep a reference! | |
| self.imageLabel.grid(row=3, column=0, sticky=N, pady=0, columnspan=2) | |
| Label(game_selection_frame, text="Click the game image above \nto open the game in the eShop!").grid(row=4, column=0, pady=10, columnspan=2) | |
| #------------------------------------------- | |
| # Game title info section | |
| game_info_frame = Frame(self.root) | |
| game_info_frame.grid(row=1, column=1, sticky=N) | |
| # Demo filter | |
| self.demo = IntVar() | |
| Checkbutton(game_info_frame, text="No Demo", \ | |
| variable=self.demo, command=self.filter_demo)\ | |
| .grid(row=0, column=0, columnspan=2, pady=(20,0), sticky=NS) | |
| # Title ID info | |
| self.titleID_label = Label(game_info_frame, text="Title ID:") | |
| self.titleID_label.grid(row=1, column=0, pady=(20,0), columnspan=2) | |
| self.game_titleID = StringVar() | |
| self.gameID_entry = Entry(game_info_frame, textvariable=self.game_titleID) | |
| self.gameID_entry.grid(row=2, column=0, columnspan=2) | |
| # Title Key info | |
| self.titleID_label = Label(game_info_frame, text="Title Key:") | |
| self.titleID_label.grid(row=3, column=0, pady=(20,0), columnspan=2) | |
| self.game_titleKey = StringVar() | |
| self.gameKey_entry = Entry(game_info_frame, textvariable=self.game_titleKey) | |
| self.gameKey_entry.grid(row=4, column=0, columnspan=2) | |
| # Select update versions | |
| self.version_label = Label(game_info_frame, text="Select update version:") | |
| self.version_label.grid(row=5, column=0, pady=(20,0), columnspan=2) | |
| self.version_option = StringVar() | |
| self.version_select = ttk.Combobox(game_info_frame, textvariable=self.version_option, state="readonly", postcommand=self.get_update_ver) | |
| self.version_select["values"] = ("Latest") | |
| self.version_select.set("Latest") | |
| self.version_select.grid(row=6, column=0, columnspan=2) | |
| # Download options | |
| self.download_label = Label(game_info_frame, text="Download options:") | |
| self.download_label.grid(row=7, column=0, pady=(20,0), columnspan=2) | |
| MODES = [ | |
| ("Base game + Update + DLC", "B+U+D"), | |
| ("Base game + Update", "B+U"), | |
| ("Update + DLC", "U+D"), | |
| ("Base game only", "B"), | |
| ("Update only", "U"), | |
| ("All DLC", "D") | |
| ] | |
| self.updateOptions = StringVar() | |
| self.updateOptions.set("B+U+D") | |
| self.radio_btn_collection = [] | |
| row_count = 8 | |
| for index in range(len(MODES)): | |
| a = Radiobutton(game_info_frame, text=MODES[index][0], | |
| variable=self.updateOptions, value=MODES[index][1]) | |
| a.grid(row=row_count, column=0, sticky=W, columnspan=2) | |
| row_count += 1 | |
| self.radio_btn_collection.append(a) | |
| # Queue and Download button | |
| queue_btn = Button(game_info_frame, text="Add to queue", command=self.add_selected_items_to_queue) | |
| queue_btn.grid(row=50, column=0, pady=(20,0)) | |
| dl_btn = Button(game_info_frame, text="Download", command=self.download) | |
| dl_btn.grid(row=50, column=1, pady=(20,0), padx=(5,0)) | |
| update_btn = Button(game_info_frame, text="Update Titlekeys", command=self.update_titlekeys) | |
| update_btn.grid(row=51, column=0, pady=(20, 0), columnspan=2) | |
| #----------------------------------------- | |
| self.queue_menu_setup() | |
| #----------------------------------------- | |
| self.load_persistent_queue() # only load the queue once the UI is initialized | |
| def queue_menu_setup(self): | |
| # Queue Menu | |
| global queue_win | |
| self.queue_win = Toplevel(self.root) | |
| self.queue_win.title("Queue Menu") | |
| self.queue_win.geometry(queue_win) | |
| self.queue_win.withdraw() # Hide queue window, will show later | |
| # Top Menu bar | |
| menubar = Menu(self.queue_win) | |
| # Download Menu Tab | |
| downloadMenu = Menu(menubar, tearoff=0) | |
| downloadMenu.add_command(label="Load Saved Queue", command=self.import_persistent_queue) | |
| downloadMenu.add_command(label="Save Queue", command=self.export_persistent_queue) | |
| # Menubar config | |
| menubar.add_cascade(label="Download", menu=downloadMenu) | |
| self.queue_win.config(menu=menubar) | |
| # Queue GUI | |
| self.queue_scrollbar = Scrollbar(self.queue_win) | |
| self.queue_scrollbar.grid(row=0, column=3, sticky=N+S+W) | |
| if self.sys_name == "Mac": | |
| self.queue_width = self.listWidth+28 | |
| else: | |
| self.queue_width = 100 # Windows | |
| self.queue_title_list = Listbox(self.queue_win, yscrollcommand = self.queue_scrollbar.set, width=self.queue_width, selectmode=EXTENDED) | |
| self.queue_title_list.grid(row=0, column=0, sticky=W, columnspan=3) | |
| self.queue_scrollbar.config(command = self.queue_title_list.yview) | |
| Button(self.queue_win, text="Remove selected game", command=self.remove_selected_items).grid(row=1, column=0, pady=(30,0)) | |
| Button(self.queue_win, text="Remove all", command=self.remove_all_and_dump).grid(row=1, column=1, pady=(30,0)) | |
| Button(self.queue_win, text="Download all", command=self.download_all).grid(row=1, column=2, pady=(30,0)) | |
| self.stateLabel = Label(self.queue_win, text="Click download all to download all games in queue!") | |
| self.stateLabel.grid(row=2, column=0, columnspan=3, pady=(20, 0)) | |
| # Sorting function for the treeview widget | |
| def sortby(self, tree, col, descending): | |
| """sort tree contents when a column header is clicked on""" | |
| # grab values to sort | |
| data = [(self.tree.set(child, col), child) for child in self.tree.get_children('')] | |
| # if the data to be sorted is numeric change to float | |
| #data = change_numeric(data) | |
| # now sort the data in place | |
| data.sort(reverse=descending) | |
| for ix, item in enumerate(data): | |
| self.tree.move(item[1], '', ix) | |
| # switch the heading so it will sort in the opposite direction | |
| self.tree.heading(col, command=lambda col=col: self.sortby(tree, col, \ | |
| int(not descending))) | |
| def remove_all(self, dump_queue = False): | |
| self.queue_list = [] | |
| self.persistent_queue = [] | |
| self.queue_title_list.delete(0, "end") | |
| if dump_queue: self.dump_persistent_queue() | |
| def remove_all_and_dump(self): | |
| self.remove_all(True) | |
| def threaded_eShop_link(self, evt): | |
| tid = self.game_titleID.get() | |
| isDLC = False | |
| if not tid.endswith("00"): | |
| isDLC = True | |
| tid = "{}".format(tid[0:12]) | |
| indices = [i for i, s in enumerate(self.titleID) if tid in s] | |
| if len(indices) >= 2: | |
| for t in indices: | |
| if self.titleID[t].endswith("000"): | |
| tid = self.titleID[t] | |
| break | |
| if tid.endswith("000"): | |
| isDLC = False | |
| if not isDLC: | |
| region = ["US", "EU", "GB", "FR", "JP"] | |
| ## https://ec.nintendo.com/apps/01000320000cc000/FR | |
| url = "" | |
| for c in region: | |
| r = requests.get("https://ec.nintendo.com/apps/{}/{}".format(tid, c)) | |
| if r.status_code == 200: | |
| url = "https://ec.nintendo.com/apps/{}/{}".format(tid, c) | |
| break | |
| if url != "": | |
| webbrowser.open(url, new=0, autoraise=True) | |
| self.root.config(cursor="") | |
| self.imageLabel.config(cursor="hand2") | |
| def eShop_link(self, evt): | |
| if self.game_titleID.get() != "": | |
| thread = threading.Thread(target = lambda: self.threaded_eShop_link(evt)) | |
| self.root.config(cursor="wait") | |
| self.imageLabel.config(cursor="wait") | |
| thread.start() | |
| def update_list(self, search=False, rebuild=False): | |
| self.root.config(cursor="wait") | |
| self.status_label.config(text="Status: Getting game status... Please wait") | |
| if not os.path.isdir("Config"): | |
| os.mkdir("Config") | |
| if not os.path.isfile(r"Config/Current_status.txt"): | |
| rebuild = True | |
| if rebuild: # Rebuild current_status.txt file | |
| print("\nBuilding the current state file... Please wait, this may take some time \ | |
| depending on how many games you have.") | |
| try: | |
| self.status_label.config(text="Status: Building the current state file... Please wait, this may take some time \ | |
| depending on how many games you have.") | |
| except: | |
| pass | |
| updates_tid = [] | |
| installed = [] | |
| new_tid = [] | |
| file_path = r"Config/installed.txt" | |
| if os.path.exists(file_path): | |
| file = open(file_path, "r") | |
| for line in file.readlines(): | |
| if line[-1] == "\n": | |
| line = line[:-1] | |
| installed.append(line) | |
| file.close() | |
| for info in installed: | |
| tid = info.split(",")[0].strip() | |
| ver = info.split(",")[1].strip() | |
| if any(tid_text in tid for tid_text in self.titleID): | |
| if tid.endswith("00"): | |
| tid = "{}800".format(tid[0:13]) | |
| latest_ver = get_versions(tid)[-1] | |
| if latest_ver == "none": | |
| latest_ver = 0 | |
| else: | |
| latest_ver = int(latest_ver) | |
| if latest_ver > int(ver): | |
| if tid.endswith("00"): | |
| tid = "{}000".format(tid[0:13]) | |
| updates_tid.append(tid) | |
| else: | |
| file = open(file_path, "w") | |
| file.close() | |
| new_path = r"Config/new.txt" | |
| if os.path.isfile(new_path): | |
| file = open(new_path, "r") | |
| for line in file.readlines(): | |
| if line[-1] == "\n": | |
| line = line[:-1] | |
| new_tid.append(line[:16]) | |
| file.close() | |
| installed = [install.split(",")[0] for install in installed] | |
| status_file = open(r"Config/Current_status.txt", "w", encoding="utf-8") | |
| for tid in self.titleID: | |
| number = int(self.titleID.index(tid))+1 | |
| game_name = self.title[number-1] | |
| if game_name[-1] == "\n": | |
| game_name = game_name[:-1] | |
| state = "" | |
| if any(list_tid in tid for list_tid in new_tid): | |
| state = "New" | |
| if any(list_tid in tid for list_tid in installed): | |
| state = "Own" | |
| if any(list_tid in tid for list_tid in updates_tid): | |
| state = "Update" | |
| tree_row = (str(number), game_name, state) | |
| status_file.write(str(tree_row)+"\n") | |
| status_file.close() | |
| self.update_list() | |
| elif search: | |
| search_term = self.search_var.get() | |
| self.tree.delete(*self.tree.get_children()) | |
| for game_status in self.current_status: | |
| number = game_status[0].strip() | |
| game_name = game_status[1].strip() | |
| state = game_status[2].strip() | |
| tree_row = (number, game_name, state) | |
| if search_term.lower() in game_name.lower(): | |
| self.tree.insert('', 'end', values=tree_row) | |
| else: | |
| if os.path.isfile(r"Config/Current_status.txt"): | |
| self.current_status = [] | |
| file = open(r"Config/Current_status.txt", "r", encoding="utf-8") | |
| for line in file.readlines(): | |
| if line[-1] == "\n": | |
| line = line[:-1] | |
| status_list = eval(line) | |
| self.current_status.append(status_list) | |
| self.update_list(search=True) | |
| file.close() | |
| else: | |
| print("Error, Current_status.txt doesn't exist") | |
| self.tree.yview_moveto(0) | |
| # Reset the sorting back to default (descending) | |
| self.tree.heading("num", text="#", command=lambda c="num": self.sortby(self.tree, c, 1)) | |
| self.tree.heading("G", text="Game", command=lambda c="G": self.sortby(self.tree, c, 1)) | |
| self.tree.heading("S", text="State", command=lambda c="S": self.sortby(self.tree, c, 1)) | |
| # Reset cursor status | |
| self.root.config(cursor="") | |
| try: | |
| self.imageLabel.config(cursor="hand2") | |
| except: | |
| pass | |
| self.status_label.config(text="Status: Done!") | |
| ## def update_list(self, search=False): | |
| ## # Set cursor status to waiting | |
| ## thread = threading.Thread(target = lambda: self.threaded_update_list(search)) | |
| ## thread.start() | |
| def threaded_game_info(self, evt): | |
| selection=self.tree.selection()[0] | |
| selected = self.tree.item(selection,"value") # Returns the selected value as a dictionary | |
| if selection: | |
| w = evt.widget | |
| self.is_DLC = False | |
| ## try: | |
| value = selected[1] | |
| if "[DLC]" in value: | |
| for radio in self.radio_btn_collection: | |
| radio.configure(state = DISABLED) | |
| self.is_DLC=True | |
| else: | |
| for radio in self.radio_btn_collection: | |
| radio.configure(state = "normal") | |
| value = int(selected[0]) | |
| value -= 1 | |
| self.game_titleID.set(self.titleID[value]) | |
| self.game_titleKey.set(self.titleKey[value]) | |
| ## except: | |
| ## pass | |
| tid = self.titleID[value] | |
| ## isDLC = False | |
| ## if not tid.endswith('00'): | |
| ## isDLC = True | |
| isDLC = False | |
| if not tid.endswith("00"): | |
| isDLC = True | |
| tid = "{}".format(tid[0:12]) | |
| indices = [i for i, s in enumerate(self.titleID) if tid in s] | |
| if len(indices) >= 2: | |
| for t in indices: | |
| if self.titleID[t].endswith("000"): | |
| tid = self.titleID[t] | |
| break | |
| if tid.endswith("000"): | |
| isDLC = False | |
| ## try: | |
| if not self.game_image_disable and not isDLC: | |
| start = time.time() | |
| if not os.path.isfile("Images/{}.jpg".format(tid)): | |
| global noaria | |
| noaria = True | |
| base_ver = get_versions(tid)[-1] | |
| result = game_image(tid, base_ver, self.titleKey[self.titleID.index(tid)]) | |
| noaria = False | |
| if result[1] != "Exist": | |
| if self.sys_name == "Win": | |
| subprocess.check_output("{0} -k keys.txt {1}\\control.nca --section0dir={1}\\section0".format(hactoolPath, result[0].replace("/", "\\")), shell=True) | |
| else: | |
| subprocess.check_output("{0} -k keys.txt '{1}/control.nca' --section0dir='{1}/section0'".format(hactoolPath, result[0]), shell=True) | |
| icon_list = ["icon_AmericanEnglish.dat", "icon_BritishEnglish.dat", "icon_CanadianFrench.dat", "icon_German.dat", "icon_Italian.dat", "icon_Japanese.dat", "icon_LatinAmericanSpanish.dat", "icon_Spanish.dat"] | |
| file_name = "" | |
| dir_content = os.listdir(os.path.dirname(os.path.abspath(__file__))+'/Images/{}/section0/'.format(tid)) | |
| for i in icon_list: | |
| if i in dir_content: | |
| file_name = i.split(".")[0] | |
| break | |
| os.rename('{}/section0/{}.dat'.format(result[0], file_name), '{}/section0/{}.jpg'.format(result[0], file_name)) | |
| shutil.copyfile('{}/section0/{}.jpg'.format(result[0], file_name), 'Images/{}.jpg'.format(tid)) | |
| shutil.rmtree(os.path.dirname(os.path.abspath(__file__))+'/Images/{}'.format(tid)) | |
| img2 = ImageTk.PhotoImage(Image.open(os.path.dirname(os.path.abspath(__file__))+'/Images/{}.jpg'.format(tid))) | |
| self.imageLabel.configure(image=img2, text="") | |
| self.imageLabel.image = img2 | |
| end = time.time() | |
| # print("\nIt took {} seconds to get the image\n".format(end - start)) | |
| else: | |
| img2 = ImageTk.PhotoImage(Image.open('blank.jpg')) | |
| self.imageLabel.configure(image=img2, text="") | |
| self.imageLabel.image = img2 | |
| ## except: | |
| ## pass | |
| def game_info(self, evt): | |
| self.imageLabel.config(image="", text="\n\n\nDownloading game image...") | |
| thread = threading.Thread(target = lambda: self.threaded_game_info(evt)) | |
| thread.start() | |
| def threaded_download(self): | |
| option = self.updateOptions.get() | |
| ## try: | |
| tid = self.game_titleID.get() | |
| updateTid = tid | |
| tkey = self.game_titleKey.get() | |
| ver = self.version_option.get() | |
| if len(tkey) != 32 and self.titlekey_check: | |
| self.messages('Error', 'Titlekey %s is not a 32-digits hexadecimal number!' % tkey) | |
| elif len(tid) != 16: | |
| self.messages('Error', 'TitleID %s is not a 16-digits hexadecimal number!' % tid) | |
| else: | |
| if self.titlekey_check == False: | |
| tkey = "" | |
| if "Latest" in ver: | |
| updateTid = tid | |
| if tid.endswith('000'): | |
| updateTid = '%s800' % tid[:-3] | |
| elif tid.endswith('800'): | |
| baseTid = '%s000' % tid[:-3] | |
| updateTid = tid | |
| ver = get_versions(updateTid)[-1] | |
| elif "none" in ver: | |
| ver == "none" | |
| if tid.endswith('000'): | |
| updateTid = '%s800' % tid[:-3] | |
| elif tid.endswith('800'): | |
| baseTid = '%s000' % tid[:-3] | |
| updateTid = tid | |
| if option == "U" or self.is_DLC == True: | |
| if ver != "none": | |
| self.messages("", "Starting to download! It will take some time, please be patient. You can check the CMD (command prompt) at the back to see your download progress.") | |
| download_game(updateTid, ver, tkey, nspRepack=self.repack, path_Dir=self.path) | |
| self.messages("", "Download finished!") | |
| else: | |
| self.messages("", "No updates available for the game") | |
| elif option == "B+U+D": | |
| base_tid = "{}000".format(tid[0:13]) | |
| self.messages("", "Starting to download! It will take some time, please be patient. You can check the CMD (command prompt) at the back to see your download progress.") | |
| base_ver = get_versions(base_tid)[-1] | |
| download_game(base_tid, base_ver, tkey, nspRepack=self.repack, path_Dir=self.path) | |
| if ver != 'none': | |
| updateTid = "{}800".format(tid[0:13]) | |
| download_game(updateTid, ver, tkey, nspRepack=self.repack, path_Dir=self.path) | |
| DLC_titleID = [] | |
| tid = "{}".format(tid[0:12]) | |
| indices = [i for i, s in enumerate(self.titleID) if tid in s] | |
| for index in indices: | |
| if not self.titleID[index].endswith("00"): | |
| DLC_titleID.append(self.titleID[index]) | |
| for DLC_ID in DLC_titleID: | |
| DLC_ver = get_versions(DLC_ID)[-1] | |
| download_game(DLC_ID, DLC_ver, self.titleKey[self.titleID.index(DLC_ID)], nspRepack=self.repack, path_Dir=self.path) | |
| self.messages("", "Download finished!") | |
| elif option == "U+D": | |
| if ver != "none": | |
| updateTid = "{}800".format(tid[0:13]) | |
| self.messages("", "Starting to download! It will take some time, please be patient. You can check the CMD (command prompt) at the back to see your download progress.") | |
| download_game(updateTid, ver, tkey, nspRepack=self.repack, path_Dir=self.path) | |
| DLC_titleID = [] | |
| tid = "{}".format(tid[0:12]) | |
| indices = [i for i, s in enumerate(self.titleID) if tid in s] | |
| for index in indices: | |
| if not self.titleID[index].endswith("00"): | |
| DLC_titleID.append(self.titleID[index]) | |
| for DLC_ID in DLC_titleID: | |
| DLC_ver = get_versions(DLC_ID)[-1] | |
| download_game(DLC_ID, DLC_ver, self.titleKey[self.titleID.index(DLC_ID)], nspRepack=self.repack, path_Dir=self.path) | |
| self.messages("", "Download finished!") | |
| elif option == "D": | |
| self.messages("", "Starting to download! It will take some time, please be patient. You can check the CMD (command prompt) at the back to see your download progress.") | |
| DLC_titleID = [] | |
| tid = "{}".format(tid[0:12]) | |
| indices = [i for i, s in enumerate(self.titleID) if tid in s] | |
| for index in indices: | |
| if not self.titleID[index].endswith("00"): | |
| DLC_titleID.append(self.titleID[index]) | |
| for DLC_ID in DLC_titleID: | |
| DLC_ver = get_versions(DLC_ID)[-1] | |
| download_game(DLC_ID, DLC_ver, self.titleKey[self.titleID.index(DLC_ID)], nspRepack=self.repack, path_Dir=self.path) | |
| self.messages("", "Download finished!") | |
| elif option == "B": | |
| base_tid = "{}000".format(tid[0:13]) | |
| self.messages("", "Starting to download! It will take some time, please be patient. You can check the CMD (command prompt) at the back to see your download progress.") | |
| base_ver = get_versions(base_tid)[-1] | |
| download_game(base_tid, base_ver, tkey, nspRepack=self.repack, path_Dir=self.path) | |
| self.messages("", "Download finished!") | |
| elif option == "B+U": | |
| base_tid = "{}000".format(tid[0:13]) | |
| self.messages("", "Starting to download! It will take some time, please be patient. You can check the CMD (command prompt) at the back to see your download progress.") | |
| base_ver = get_versions(base_tid)[-1] | |
| download_game(base_tid, base_ver, tkey, nspRepack=self.repack, path_Dir=self.path) | |
| if ver != 'none': | |
| updateTid = "{}800".format(tid[0:13]) | |
| download_game(updateTid, ver, tkey, nspRepack=self.repack, path_Dir=self.path) | |
| self.messages("", "Download finished!") | |
| else: | |
| self.messages("", "No updates available for the game, base game downloaded!") | |
| ## except: | |
| ## print("Error downloading {}, note: if you're downloading a DLC then different versions of DLC may have different titlekeys".format(tid)) | |
| return | |
| def download(self): | |
| thread = threading.Thread(target = self.threaded_download) | |
| thread.start() | |
| def export_persistent_queue(self): | |
| self.dump_persistent_queue(self.normalize_file_path(filedialog.asksaveasfilename(initialdir = self.path, title = "Select file", filetypes = (("JSON files","*.json"),("all files","*.*"))))) | |
| def import_persistent_queue(self): | |
| self.load_persistent_queue(self.normalize_file_path(filedialog.askopenfilename(initialdir = self.path, title = "Select file", filetypes = (("JSON files","*.json"),("all files","*.*"))))) | |
| def dump_persistent_queue(self, path = r'Config/CDNSP_queue.json'): | |
| if path == "": | |
| print("\nYou didn't choose a location to save the file!") | |
| return | |
| elif not path.endswith(".json"): | |
| path += ".json" | |
| # if self.persist_queue # check for user option here | |
| f = open(path, 'w') | |
| json.dump(self.persistent_queue, f) | |
| f.close() | |
| def load_persistent_queue(self, path = r'Config/CDNSP_queue.json'): | |
| # if self.persist_queue # check for user option here | |
| try: | |
| f = open(path, 'r') | |
| self.remove_all() | |
| for c, tid, ver, key, option in json.load(f): | |
| self.add_item_to_queue((tid, ver, key, option), True) | |
| f.close() | |
| except: | |
| print("Persistent queue not found, skipping...") | |
| def get_index_in_queue(self, item): | |
| try: | |
| return self.queue_list.index(item) | |
| except: | |
| print("Item not found in queue", item) | |
| def add_selected_items_to_queue(self): | |
| self.add_items_to_queue(self.tree.selection()) | |
| def add_items_to_queue(self, indices): | |
| ## try: | |
| for index in indices: | |
| index = int(self.tree.item(index,"value")[0])-1 | |
| ## index = int(self.temp_list[index].split(",")[0])-1 | |
| tid = self.titleID[index] | |
| key = self.titleKey[index] | |
| ver = self.version_option.get() | |
| option = self.updateOptions.get() | |
| if len(key) != 32 and self.titlekey_check: | |
| self.messages('Error', 'Titlekey %s is not a 32-digits hexadecimal number!' % key) | |
| elif len(tid) != 16: | |
| self.messages('Error', 'TitleID %s is not a 16-digits hexadecimal number!' % tid) | |
| else: | |
| self.add_item_to_queue((tid, ver, key, option)) | |
| self.dump_persistent_queue() | |
| ## except: | |
| ## messagebox.showerror("Error", "No game selected/entered to add to queue") | |
| def process_item_versions(self, tid, ver): | |
| if "Latest" in ver: | |
| if tid.endswith('000'): | |
| tid = '%s800' % tid[:-3] | |
| ver = get_versions(tid)[-1] | |
| elif "none" in ver: | |
| ver = "none" | |
| return (tid, ver) | |
| # takes an item with unformatted tid and ver | |
| def add_item_to_queue(self, item, dump_queue = False): | |
| tid, ver, key, option = item | |
| if len(tid) == 16: | |
| if self.first_queue: | |
| if not Toplevel.winfo_exists(self.queue_win): | |
| self.queue_menu_setup() #Fix for app crashing when close the queue menu and re-open | |
| self.queue_win.update() | |
| self.queue_win.deiconify() | |
| try: | |
| c = self.titleID.index(tid) | |
| c = self.title[c] # Name of the game | |
| except: | |
| print("Name for titleID not found in the list", tid) | |
| c = "UNKNOWN NAME" | |
| formatted_tid, formatted_ver = self.process_item_versions(tid, ver) | |
| if "[DLC]" in c: | |
| option = "DLC" | |
| self.queue_title_list.insert("end", "{}---{}---{}".format(c, ver, option)) | |
| self.queue_list.append((formatted_tid, formatted_ver, key, option)) | |
| self.persistent_queue.append((c, tid, ver, key, option)) | |
| if dump_queue: self.dump_persistent_queue() | |
| self.queue_title_list.yview(END) # Auto scroll the queue menu as requested | |
| def remove_selected_items(self): | |
| self.remove_items(self.queue_title_list.curselection()) | |
| def remove_items(self, indices): | |
| counter = 0 | |
| for index in indices: | |
| index = index - counter | |
| try: | |
| self.remove_item(index) | |
| counter += 1 | |
| except: | |
| print("No game selected to remove!") | |
| self.dump_persistent_queue() | |
| def remove_item(self, index, dump_queue = False): | |
| del self.queue_list[index] | |
| del self.persistent_queue[index] | |
| self.queue_title_list.delete(index) | |
| if dump_queue: self.dump_persistent_queue() | |
| def threaded_download_all(self): | |
| self.messages("", "Download for all your queued games will now begin! You will be informed once all the download has completed, please wait and be patient!") | |
| self.stateLabel.configure(text = "Downloading games...") | |
| download_list = self.queue_list.copy() | |
| for item in download_list: | |
| tid, ver, tkey, option = item | |
| ## try: | |
| if option == "U" or option == "DLC": | |
| if ver != "none": | |
| if tid.endswith("00"): | |
| tid = "{}800".format(tid[0:13]) | |
| download_game(tid, ver, tkey, nspRepack=self.repack, path_Dir=self.path) | |
| else: | |
| print("No updates available for titleID: {}".format(tid)) | |
| elif option == "B+U+D": | |
| base_tid = "{}000".format(tid[0:13]) | |
| base_ver = get_versions(base_tid)[-1] | |
| download_game(base_tid, base_ver, tkey, nspRepack=self.repack, path_Dir=self.path) | |
| if ver != 'none': | |
| updateTid = "{}800".format(tid[0:13]) | |
| download_game(updateTid, ver, tkey, nspRepack=self.repack, path_Dir=self.path) | |
| DLC_titleID = [] | |
| tid = "{}".format(tid[0:12]) | |
| indices = [i for i, s in enumerate(self.titleID) if tid in s] | |
| for index in indices: | |
| if not self.titleID[index].endswith("00"): | |
| DLC_titleID.append(self.titleID[index]) | |
| for DLC_ID in DLC_titleID: | |
| DLC_ver = get_versions(DLC_ID)[-1] | |
| download_game(DLC_ID, DLC_ver, self.titleKey[self.titleID.index(DLC_ID)], nspRepack=self.repack, path_Dir=self.path) | |
| elif option == "U+D": | |
| if ver != "none": | |
| updateTid = "{}800".format(tid[0:13]) | |
| download_game(updateTid, ver, tkey, nspRepack=self.repack, path_Dir=self.path) | |
| DLC_titleID = [] | |
| tid = "{}".format(tid[0:12]) | |
| indices = [i for i, s in enumerate(self.titleID) if tid in s] | |
| for index in indices: | |
| if not self.titleID[index].endswith("00"): | |
| DLC_titleID.append(self.titleID[index]) | |
| for DLC_ID in DLC_titleID: | |
| DLC_ver = get_versions(DLC_ID)[-1] | |
| download_game(DLC_ID, DLC_ver, self.titleKey[self.titleID.index(DLC_ID)], nspRepack=self.repack, path_Dir=self.path) | |
| elif option == "D": | |
| DLC_titleID = [] | |
| tid = "{}".format(tid[0:12]) | |
| indices = [i for i, s in enumerate(self.titleID) if tid in s] | |
| for index in indices: | |
| if not self.titleID[index].endswith("00"): | |
| DLC_titleID.append(self.titleID[index]) | |
| for DLC_ID in DLC_titleID: | |
| DLC_ver = get_versions(DLC_ID)[-1] | |
| download_game(DLC_ID, DLC_ver, self.titleKey[self.titleID.index(DLC_ID)], nspRepack=self.repack, path_Dir=self.path) | |
| elif option == "B": | |
| base_tid = "{}000".format(tid[0:13]) | |
| base_ver = get_versions(base_tid)[-1] | |
| download_game(base_tid, base_ver, tkey, nspRepack=self.repack, path_Dir=self.path) | |
| elif option == "B+U": | |
| base_tid = "{}000".format(tid[0:13]) | |
| base_ver = get_versions(base_tid)[-1] | |
| download_game(base_tid, base_ver, tkey, nspRepack=self.repack, path_Dir=self.path) | |
| if ver != 'none': | |
| updateTid = "{}800".format(tid[0:13]) | |
| download_game(updateTid, ver, tkey, nspRepack=self.repack, path_Dir=self.path) | |
| else: | |
| print("No updates available for titleID: {}, base game downloaded!".format(tid)) | |
| index = self.get_index_in_queue(item) | |
| self.remove_item(index, True) | |
| ## except: | |
| ## print("Error downloading {}, note: if you're downloading a DLC then different versions of DLC may have different titlekeys".format(tid)) | |
| self.messages("", "Download complete!") | |
| self.stateLabel["text"] = "Download Complete!" | |
| # self.remove_all(dump_queue = True) | |
| def download_all(self): | |
| thread = threading.Thread(target = self.threaded_download_all) | |
| thread.start() | |
| ## def download_check(self, tid, ver): | |
| ## if ver != "0" or ver != "1": # Is an update game | |
| ## if any(tid_list in tid for tid_list in self.installed): | |
| ## print(tid, self.installed.index(tid)) | |
| ## else: | |
| ## if any(tid_list in tid for tid_list in self.installed): | |
| def normalize_file_path(self, file_path): | |
| if self.sys_name == "Win": | |
| return file_path.replace("/", "\\") | |
| else: | |
| return file_path | |
| def change_dl_path(self): | |
| self.path = self.normalize_file_path(filedialog.askdirectory()) | |
| updateJsonFile("Download_location", self.path) | |
| print("\n{}".format(self.path)) | |
| def nsp_repack_option(self): | |
| if self.repack == True: | |
| self.optionMenu.entryconfig(4, label= "Enable NSP Repack") | |
| self.repack = False | |
| elif self.repack == False: | |
| self.optionMenu.entryconfig(4, label= "Disable NSP Repack") | |
| self.repack = True | |
| updateJsonFile("NSP_repack", str(self.repack)) | |
| def threaded_update_titlekeys(self): | |
| self.status_label.config(text="Status: Updating titlekeys") | |
| print(self.db_URL) | |
| ## try: | |
| r = requests.get(self.db_URL, allow_redirects=True, verify=False) | |
| if str(r.history) == "[<Response [302]>]" and str(r.status_code) == "200": | |
| r.encoding = "utf-8" | |
| newdb = r.text.split('\n') | |
| if newdb[-1] == "": | |
| newdb = newdb[:-1] | |
| if os.path.isfile('titlekeys.txt'): | |
| with open('titlekeys.txt',encoding="utf8") as f: | |
| currdb = f.read().split('\n') | |
| if currdb[-1] == "": | |
| currdb = currdb[:-1] | |
| currdb = [x.strip() for x in currdb] | |
| # print('There are currently %d titles in titlekeys.txt.\n' % len(currdb)) | |
| counter = 0 | |
| info = '' | |
| new_tid = [] | |
| for line in newdb: | |
| if line.strip() not in currdb: | |
| if line.strip() != newdb[0].strip(): | |
| new_tid.append(line.strip().split('|')[0]) | |
| info += (line.strip()).rsplit('|',1)[1] + '\n' | |
| counter += 1 | |
| if len(new_tid) != 0: | |
| file_path = open(r"Config/new.txt", "w") | |
| text = "" | |
| for new in new_tid: | |
| text += "{}\n".format(new[:16]) | |
| file_path.write(text) | |
| file_path.close() | |
| if counter: | |
| update_win = Toplevel(self.root) #https://stackoverflow.com/questions/13832720/how-to-attach-a-scrollbar-to-a-text-widget | |
| self.update_window = update_win | |
| global update_win_size | |
| update_win.title("Finished update!") | |
| if update_win_size != "": | |
| update_win.geometry(update_win_size) | |
| else: | |
| update_win.geometry("600x400+120+200") | |
| # create a Frame for the Text and Scrollbar | |
| txt_frm = Frame(update_win, width=600, height=400) | |
| txt_frm.pack(fill="both", expand=True) | |
| # ensure a consistent GUI size | |
| txt_frm.grid_propagate(False) | |
| # implement stretchability | |
| txt_frm.grid_rowconfigure(0, weight=1) | |
| txt_frm.grid_columnconfigure(0, weight=3) | |
| # create a Text widget | |
| txt = Text(txt_frm, borderwidth=3, relief="sunken") | |
| txt.config(font=("Open Sans", 12), undo=True, wrap='word') | |
| txt.grid(row=0, column=0, sticky="nsew", padx=2, pady=2, columnspan=2) | |
| txt.insert("1.0", info) | |
| # create a Scrollbar and associate it with txt | |
| scrollb = Scrollbar(txt_frm, command=txt.yview, width=20) | |
| scrollb.grid(row=0, column=3, sticky='nsew') | |
| txt['yscrollcommand'] = scrollb.set | |
| # info on total games added and an exit button | |
| Label(txt_frm, text="Total of new games added: {}".format(counter)).grid(row=1, column=0) | |
| Button(txt_frm, text="Close", width=5, height=2, command=lambda: update_win.destroy()).grid(row=1, column=1) | |
| ## Label(txt_frm, text=" ").grid(row=1, column=3) | |
| try: | |
| # print('\nSaving new database...') | |
| f = open('titlekeys.txt','w',encoding="utf-8") | |
| self.title = [] | |
| self.titleID = [] | |
| self.titleKey = [] | |
| for line in newdb: | |
| if line.strip(): | |
| self.title.append(line.strip().split("|")[2]) | |
| self.titleID.append(line.strip().split("|")[0][:16]) | |
| self.titleKey.append(line.strip().split("|")[1]) | |
| f.write(line.strip() + '\n') | |
| f.close() | |
| self.current_status = [] | |
| self.update_list(rebuild=True) | |
| except Exception as e: | |
| print(e) | |
| else: | |
| self.status_label.config(text='Status: Finished update, There were no new games to update!') | |
| print('\nStatus: Finished update, There were no new games to update!') | |
| else: | |
| try: | |
| # print('\nSaving new database...') | |
| f = open('titlekeys.txt','w',encoding="utf-8") | |
| self.title = [] | |
| self.titleID = [] | |
| self.titleKey = [] | |
| for line in newdb: | |
| if line.strip(): | |
| self.title.append(line.strip().split("|")[2]) | |
| self.titleID.append(line.strip().split("|")[0][:16]) | |
| self.titleKey.append(line.strip().split("|")[1]) | |
| f.write(line.strip() + '\n') | |
| f.close() | |
| self.status_label.config(text='Status: Finished update, Database rebuilt from scratch') | |
| self.current_status = [] | |
| self.update_list(rebuild=True) | |
| except Exception as e: | |
| print(e) | |
| else: | |
| self.messages("Error", "The database server {} might be down or unavailable".format(self.db_URL)) | |
| game_list = [] | |
| self.title_list.delete(0, END) | |
| f = open("titlekeys.txt", "r", encoding="utf8") | |
| content = f.readlines() | |
| self.titleID = [] | |
| self.titleKey = [] | |
| self.title = [] | |
| for i in range(len(content)): | |
| titleID = "" | |
| try: | |
| titleID, titleKey, title = content[i].split("|") | |
| except: | |
| print("Check if there's extra spaces at the bottom of your titlekeys.txt file! Delete if you do!") | |
| if titleID != "": | |
| self.titleID.append(titleID[:16]) | |
| self.titleKey.append(titleKey) | |
| self.title.append(title[:-1]) | |
| global titleID_list | |
| global titleKey_list | |
| global title_list | |
| titleID_list = [] | |
| titleKey_list = [] | |
| title_list = [] | |
| f.close() | |
| self.update_list(True) | |
| # self.messages("", "Updated!") | |
| ## except: | |
| ## self.messages("Error", "Too many people accessing the database, or then link is died, or the link is incorrect!") | |
| def update_titlekeys(self): | |
| self.root.config(cursor="wait") | |
| self.imageLabel.config(cursor="wait") | |
| thread = threading.Thread(target = self.threaded_update_titlekeys) | |
| thread.start() | |
| def mute_all(self): | |
| if self.mute == False: | |
| self.optionMenu.entryconfig(3, label= "Unmute All Pop-ups") | |
| self.mute = True | |
| elif self.mute == True: | |
| self.optionMenu.entryconfig(3, label= "Mute All Pop-ups") | |
| self.mute = False | |
| updateJsonFile("Mute", str(self.mute)) | |
| def messages(self, title, text): | |
| if self.mute != True: | |
| messagebox.showinfo(title, text) | |
| else: | |
| print("\n{}\n".format(text)) | |
| def titlekey_check_option(self): | |
| global titlekey_check | |
| if self.titlekey_check == True: | |
| self.titlekey_check = False | |
| titlekey_check = self.titlekey_check | |
| self.optionMenu.entryconfig(5, label= "Enable Titlekey Check") # Automatically disable repacking as well | |
| self.repack = False | |
| self.optionMenu.entryconfig(4, label= "Enable NSP Repack") | |
| elif self.titlekey_check == False: | |
| self.titlekey_check = True | |
| titlekey_check = self.titlekey_check | |
| self.optionMenu.entryconfig(5, label= "Disable Titlekey Check") | |
| self.repack = True | |
| self.optionMenu.entryconfig(4, label= "Disable NSP Repack") | |
| updateJsonFile("Titlekey_check", str(self.titlekey_check)) | |
| def save_game_in_folder(self): | |
| ## global save_game_folder | |
| ## if save_game_folder == False: | |
| ## save_game_folder = True | |
| ## self.fileMenuItem.entryconfig(5, label= "Disable Saving in Game Name Folder") | |
| ## elif save_game_folder == True: | |
| ## save_game_folder = False | |
| ## self.fileMenuItem.entryconfig(5, label= "Enable Saving in Game Name Folder") | |
| pass | |
| def disable_aria2c(self): | |
| pass | |
| def disable_game_image(self): | |
| if self.game_image_disable == False: | |
| self.game_image_disable = True | |
| self.optionMenu.entryconfig(1, label= "ENABLE GAME IMAGE") | |
| elif self.game_image_disable == True: | |
| self.game_image_disable = False | |
| self.optionMenu.entryconfig(1, label= "DISABLE GAME IMAGE") | |
| updateJsonFile("Disable_game_image", str(self.game_image_disable)) | |
| def threaded_preload_images(self): | |
| ## try: | |
| for k in self.titleID: | |
| if k != "": | |
| start = time.time() | |
| tid = k | |
| isDLC = False | |
| if not tid.endswith("00"): | |
| isDLC = True | |
| tid = "{}".format(tid[0:12]) | |
| indices = [i for i, s in enumerate(self.titleID) if tid in s] | |
| if len(indices) >= 2: | |
| for t in indices: | |
| if self.titleID[t].endswith("000"): | |
| tid = self.titleID[t] | |
| break | |
| if tid.endswith("000"): | |
| isDLC = False | |
| if not os.path.isfile("Images/{}.jpg".format(tid)): | |
| if not isDLC: | |
| global noaria | |
| noaria = True | |
| start = time.time() | |
| base_ver = get_versions(tid)[-1] | |
| result = game_image(tid, base_ver, self.titleKey[self.titleID.index(tid)]) | |
| print(result) | |
| noaria = False | |
| if result[1] != "Exist": | |
| if self.sys_name == "Win": | |
| try: | |
| subprocess.check_output("{0} -k keys.txt {1}\\control.nca --section0dir={1}\\section0".format(hactoolPath, result[0].replace("/", "\\")), shell=True) | |
| except subprocess.CalledProcessError as e: | |
| raise RuntimeError("command '{}' return with error (code {}): {}".format(e.cmd, e.returncode, e.output)) | |
| else: | |
| subprocess.check_output("{0} -k keys.txt '{1}/control.nca' --section0dir='{1}/section0'".format(hactoolPath, result[0]), shell=True) | |
| icon_list = ["icon_AmericanEnglish.dat", "icon_BritishEnglish.dat", "icon_CanadianFrench.dat", "icon_German.dat", "icon_Italian.dat", "icon_Japanese.dat", "icon_LatinAmericanSpanish.dat", "icon_Spanish.dat"] | |
| file_name = "" | |
| dir_content = os.listdir(os.path.dirname(os.path.abspath(__file__))+'/Images/{}/section0/'.format(tid)) | |
| for i in icon_list: | |
| if i in dir_content: | |
| file_name = i.split(".")[0] | |
| break | |
| os.rename('{}/section0/{}.dat'.format(result[0], file_name), '{}/section0/{}.jpg'.format(result[0], file_name)) | |
| shutil.copyfile('{}/section0/{}.jpg'.format(result[0], file_name), 'Images/{}.jpg'.format(tid)) | |
| shutil.rmtree(os.path.dirname(os.path.abspath(__file__))+'/Images/{}'.format(tid)) | |
| end = time.time() | |
| print("\nIt took {} seconds for you to get all images!\n".format(end - start)) | |
| self.messages("", "Done getting all game images!") | |
| ## except: | |
| ## print("Error getting game images") | |
| def preload_images(self): | |
| thread = threading.Thread(target = self.threaded_preload_images) | |
| thread.start() | |
| def get_update_ver(self): | |
| tid = self.game_titleID.get() | |
| if tid != "" and len(tid) == 16: | |
| value = self.titleID.index(tid) | |
| print(tid) | |
| try: | |
| isDLC = False | |
| tid = self.titleID[value] | |
| updateTid = tid | |
| if tid.endswith('000'): | |
| updateTid = '%s800' % tid[:-3] | |
| elif tid.endswith('800'): | |
| baseTid = '%s000' % tid[:-3] | |
| updateTid = tid | |
| elif not tid.endswith('00'): | |
| isDLC = True | |
| update_list = [] | |
| for i in get_versions(updateTid): | |
| update_list.append(i) | |
| if isDLC: | |
| if update_list[0] != "0": | |
| update_list.insert(0, "0") | |
| if update_list[0] == 'none': | |
| update_list[0] = "0" | |
| print(update_list) | |
| update_list.insert(0, "Latest") | |
| self.version_select["values"] = update_list | |
| self.version_select.set("Latest") | |
| except: | |
| print("Failed to get version") | |
| else: | |
| print("No TitleID or TitleID not 16 characters!") | |
| def shorten(self): | |
| global truncateName | |
| if truncateName == False: | |
| truncateName = True | |
| self.optionMenu.entryconfig(7, label= "Disable Shorten Name") | |
| elif truncateName == True: | |
| truncateName = False | |
| self.optionMenu.entryconfig(7, label= "Enable Shorten Name") | |
| updateJsonFile("Shorten", str(truncateName)) | |
| def tinfoil_change(self): | |
| global tinfoil | |
| if tinfoil == False: | |
| tinfoil = True | |
| self.optionMenu.entryconfig(8, label= "Disable Tinfoil Download") | |
| elif tinfoil == True: | |
| tinfoil = False | |
| self.optionMenu.entryconfig(8, label= "Enable Tinfoil Download") | |
| updateJsonFile("Tinfoil", str(tinfoil)) | |
| def window_info(self, window): | |
| x = window.winfo_x() | |
| y = window.winfo_y() | |
| w = window.winfo_width() | |
| h = window.winfo_height() | |
| size_pos = "{}x{}+{}+{}".format(w, h, x, y) | |
| return size_pos | |
| def window_save(self): | |
| if Toplevel.winfo_exists(self.root): | |
| updateJsonFile("Main_win", self.window_info(self.root)) | |
| try: | |
| if Toplevel.winfo_exists(self.queue_win): | |
| updateJsonFile("Queue_win", self.window_info(self.queue_win)) | |
| except: | |
| pass | |
| try: | |
| if Toplevel.winfo_exists(self.update_window): | |
| updateJsonFile("Update_win", self.window_info(self.update_window)) | |
| except: | |
| pass | |
| self.messages("", "Windows size and position saved!") | |
| def my_game_GUI(self): | |
| my_game = Toplevel(self.root) | |
| self.my_game = my_game | |
| my_game.title("Search for existing games") | |
| my_game.geometry("400x100+100+100") | |
| entry_w = 50 | |
| if sys_name == "Mac": | |
| entry_w = 32 | |
| dir_text = "" | |
| dir_entry = Entry(my_game, width=entry_w, text=dir_text) | |
| if self.game_location != "": | |
| dir_entry.delete(0, END) | |
| dir_entry.insert(0, self.game_location) | |
| self.dir_entry = dir_entry | |
| dir_entry.grid(row=0, column=0, padx=(10,0), pady=10) | |
| browse_btn = Button(my_game, text="Browse", command=self.my_game_directory) | |
| browse_btn.grid(row=0, column=1, padx=10, pady=10) | |
| scan_btn = Button(my_game, text="Scan", command=self.my_game_scan) | |
| scan_btn.grid(row=1, column=0, columnspan=2, sticky=N, padx=10) | |
| def my_game_directory(self): | |
| path = self.normalize_file_path(filedialog.askdirectory()) | |
| if path != "": | |
| self.game_location = path | |
| updateJsonFile("Game_location", str(self.game_location)) | |
| self.my_game.lift() | |
| self.my_game.update() | |
| self.dir_entry.delete(0, END) | |
| self.dir_entry.insert(0, path) | |
| def my_game_scan(self): | |
| import re | |
| a_dir = self.dir_entry.get() | |
| if a_dir == "": | |
| self.messages("Error", "You didn't choose a directory!") | |
| self.my_game.lift() | |
| elif not os.path.isdir(a_dir): | |
| self.messages("Error", "The chosen directory doesn't exist!") | |
| self.my_game.lift() | |
| else: | |
| game_list = [] | |
| ## print([name for name in os.listdir(a_dir) if os.path.isdir(os.path.join(a_dir, name))]) | |
| ## print(glob.glob(a_dir+"\*.nsp")) | |
| ## for game in glob.iglob(r"{}\**\*.nsp".format(a_dir), recursive=True): | |
| ## game_list.append(game) | |
| ## print(os.scandir(a_dir)) | |
| for root, dirs, files in os.walk(a_dir): | |
| for basename in files: | |
| if basename.endswith(".nsp"): | |
| game_list.append(basename) | |
| ## game_list = os.listdir(self.dir_entry.get()) | |
| if not os.path.isdir("Config"): | |
| os.mkdir("Config") | |
| tid_exist = [] # Tid that's already in the installed.txt | |
| ver_exist = [] | |
| if os.path.isfile(r"Config/installed.txt"): | |
| file = open(r"Config/installed.txt", "r") | |
| for game in file.readlines(): | |
| tid_exist.append("{}".format(game.split(",")[0].strip())) | |
| ver_exist.append("{}".format(game.split(",")[1].strip())) | |
| file.close() | |
| file = open(r"Config/installed.txt", "w") | |
| for game in game_list: | |
| title = re.search(r".*[0-9a-zA-Z]{16}.*[.nsp]", game) | |
| if title: | |
| try: | |
| tid_check = re.compile(r"[\[][0-9a-zA-Z]{16}[\]]") | |
| tid_result = tid_check.findall(game)[0] | |
| tid_result = tid_result[1:-1] | |
| if tid_result.endswith("800"): | |
| tid_result = "{}000".format(tid_result[:13]) | |
| except: | |
| tid_result = "0" | |
| try: | |
| ver_check = re.compile(r"[\[][v][0-9]+[\]]") | |
| ver_result = ver_check.findall(game)[0] | |
| ver_result = ver_result[1:-1].split("v")[1] | |
| except: | |
| ver_result = "0" | |
| if tid_result != "0": | |
| if any(list_tid in tid_result for list_tid in tid_exist): # Check if the tid is already in installed.txt | |
| if int(ver_result) > int(ver_exist[tid_exist.index(tid_result)]): | |
| ver_exist[tid_exist.index(tid_result)] = ver_result | |
| else: | |
| tid_exist.append(tid_result) | |
| ver_exist.append(ver_result) | |
| for i in range(len(tid_exist)): | |
| file.write("{}, {}\n".format(tid_exist[i], ver_exist[i])) | |
| file.close() | |
| self.messages("", "Finished scanning your games!") | |
| self.status_label.config(text="Status: Building the current state file... Please wait, this may take some time depending on how many games you have.") | |
| self.my_game.lift() | |
| self.my_game.destroy() | |
| self.update_list(rebuild=True) | |
| def base_64_GUI(self): | |
| base_64 = Toplevel(self.root) | |
| self.base_64 = base_64 | |
| base_64.title("Base64 Decoder") | |
| base_64.geometry("470x100+100+100") | |
| entry_w = 50 | |
| if sys_name == "Mac": | |
| entry_w = 28 | |
| base_64_label = Label(base_64, text="Base64 text:") | |
| base_64_label.grid(row=0, column=0) | |
| base_64_entry = Entry(base_64, width=entry_w) | |
| self.base_64_entry = base_64_entry | |
| base_64_entry.grid(row=0, column=1, padx=(10,0), pady=10) | |
| browse_btn = Button(base_64, text="Decode", command=self.decode_64) | |
| browse_btn.grid(row=0, column=2, padx=10, pady=10) | |
| decoded_label = Label(base_64, text="Decoded text:") | |
| decoded_label.grid(row=1, column=0) | |
| decoded_entry = Entry(base_64, width=entry_w) | |
| self.decoded_entry = decoded_entry | |
| decoded_entry.grid(row=1, column=1, padx=(10,0), pady=10) | |
| scan_btn = Button(base_64, text="Open", command=self.base64_open) | |
| scan_btn.grid(row=1, column=2, sticky=N, padx=10, pady=10) | |
| def decode_64(self): | |
| base64_text = self.base_64_entry.get() | |
| self.decoded_entry.delete(0, END) | |
| self.decoded_entry.insert(0, base64.b64decode(base64_text)) | |
| def base64_open(self): | |
| url = self.decoded_entry.get() | |
| webbrowser.open(url, new=0, autoraise=True) | |
| def filter_demo(self): | |
| demo_off = self.demo.get() | |
| if demo_off: | |
| self.full_list = self.current_status | |
| no_demo_list = [] | |
| for game in self.current_status: | |
| if not "demo" in game[1].strip().lower(): | |
| no_demo_list.append(game) | |
| self.current_status = no_demo_list | |
| search_term = "" | |
| self.tree.delete(*self.tree.get_children()) | |
| for game_status in self.current_status: | |
| number = game_status[0].strip() | |
| game_name = game_status[1].strip() | |
| state = game_status[2].strip() | |
| tree_row = (number, game_name, state) | |
| if search_term.lower() in game_name.lower(): | |
| self.tree.insert('', 'end', values=tree_row) | |
| else: | |
| self.current_status = self.full_list | |
| search_term = "" | |
| self.tree.delete(*self.tree.get_children()) | |
| for game_status in self.current_status: | |
| number = game_status[0].strip() | |
| game_name = game_status[1].strip() | |
| state = game_status[2].strip() | |
| tree_row = (number, game_name, state) | |
| if search_term.lower() in game_name.lower(): | |
| self.tree.insert('', 'end', values=tree_row) | |
| def sysver_zero(self): | |
| global sysver0 | |
| if sysver0 == False: | |
| sysver0 = True | |
| self.optionMenu.entryconfig(9, label= "Disable SysVer 0 Patch") | |
| elif sysver0 == True: | |
| sysver0 = False | |
| self.optionMenu.entryconfig(9, label= "Enable SysVer 0 Patch") | |
| updateJsonFile("SysVerZero", str(sysver0)) | |
| def warn_firmware(self): | |
| global warnfw | |
| if warnfw == False: | |
| warnfw = True | |
| self.optionMenu.entryconfig(11, label= "Ignore Required Firmware") | |
| elif warnfw == True: | |
| warnfw = False | |
| self.optionMenu.entryconfig(11, label= "Warn Required Firmware") | |
| updateJsonFile("WarnFirmware", str(warnfw)) | |
| def switch_firmware(self, firmware): | |
| global switchfw | |
| try: | |
| index = list(SWITCH_FIRMWARE).index(switchfw) | |
| self.firmwareMenu.entryconfig(index, label= switchfw) | |
| except: | |
| pass | |
| switchfw = firmware | |
| try: | |
| index = list(SWITCH_FIRMWARE).index(switchfw) | |
| self.firmwareMenu.entryconfig(index, label= switchfw + " *") | |
| except: | |
| pass | |
| updateJsonFile("SwitchFirmware", switchfw) | |
| # ------------------------ | |
| # Main Section | |
| if __name__ == '__main__': | |
| urllib3.disable_warnings() | |
| configPath = os.path.join(os.path.dirname(__file__), 'CDNSPconfig.json') | |
| hactoolPath, keysPath, NXclientPath, ShopNPath, nspDir, reg, fw, did, env, dbURL = load_config(configPath) | |
| spam_spec = util.find_spec("tqdm") | |
| found = spam_spec is not None | |
| if found: | |
| tqdmProgBar = True | |
| else: | |
| tqdmProgBar = False | |
| print('Install the tqdm library for better-looking progress bars! (pip install tqdm)') | |
| if keysPath != '': | |
| keysArg = ' -k "%s"' % keysPath | |
| else: | |
| keysArg = '' | |
| root = Tk() | |
| root.title("CDNSP GUI - Bobv4.0.1") | |
| Application(root, titleID_list, titleKey_list, title_list, dbURL) | |
| root.mainloop() | 
  
    Sign up for free
    to join this conversation on GitHub.
    Already have an account?
    Sign in to comment