NUSGet/NUSD-Py.py

366 lines
16 KiB
Python

import sys
import os
import json
import pathlib
import libWiiPy
from PySide6.QtWidgets import QApplication, QMainWindow, QMessageBox, QTreeWidgetItem, QHeaderView
from PySide6.QtCore import QRunnable, Slot, QThreadPool, Signal, QObject
from qt.py.ui_MainMenu import Ui_MainWindow
regions = [["World", "World", "41"], ["USA", "USA/NTSC", "45"], ["JAP", "Japan", "4A"], ["EUR", "Europe/PAL", "50"],
["KOR", "Korea", "4B"]]
class WorkerSignals(QObject):
result = Signal(int)
progress = Signal(str)
class Worker(QRunnable):
def __init__(self, fn, **kwargs):
super(Worker, self).__init__()
self.fn = fn
self.kwargs = kwargs
self.signals = WorkerSignals()
self.kwargs['progress_callback'] = self.signals.progress
@Slot()
def run(self):
try:
result = self.fn(**self.kwargs)
except ValueError:
self.signals.result.emit(1)
else:
self.signals.result.emit(result)
class MainWindow(QMainWindow, Ui_MainWindow):
def __init__(self):
super(MainWindow, self).__init__()
self.ui = Ui_MainWindow()
self.ui.setupUi(self)
self.log_text = ""
self.threadpool = QThreadPool()
self.ui.download_btn.clicked.connect(self.download_btn_pressed)
self.ui.pack_wad_chkbox.clicked.connect(self.pack_wad_chkbox_toggled)
# noinspection PyUnresolvedReferences
self.ui.title_tree.header().setSectionResizeMode(QHeaderView.ResizeToContents)
self.ui.log_text_browser.setText("NUSD-Py v1.0\nDeveloped by NinjaCheetah\nPowered by libWiiPy\n\n"
"Select a title from the list on the left, or enter a Title ID to begin.")
tree = self.ui.title_tree
self.tree_categories = []
global regions
for key in wii_database:
new_category = QTreeWidgetItem()
new_category.setText(0, key)
for title in wii_database[key]:
new_title = QTreeWidgetItem()
new_title.setText(0, title["TID"] + " - " + title["Name"])
for region in title["Versions"]:
new_region = QTreeWidgetItem()
region_title = ""
if region == "World":
region_title = "World"
else:
for entry in regions:
if entry[0] == region:
region_title = entry[1]
new_region.setText(0, region_title)
for version in title["Versions"][region]:
new_version = QTreeWidgetItem()
new_version.setText(0, "v" + str(version))
new_region.addChild(new_version)
new_title.addChild(new_region)
new_category.addChild(new_title)
self.tree_categories.append(new_category)
tree.insertTopLevelItems(0, self.tree_categories)
tree.itemDoubleClicked.connect(self.onItemClicked)
@Slot(QTreeWidgetItem, int)
def onItemClicked(self, item, col):
if self.ui.download_btn.isEnabled() is True:
global regions
region_names = []
for region in regions:
region_names.append(region[1])
if ((item.parent() is not None) and item.parent() not in self.tree_categories
and item.parent().parent() not in self.tree_categories):
category = item.parent().parent().parent().text(0)
for title in wii_database[category]:
if item.parent().parent().text(0) == (title["TID"] + " - " + title["Name"]):
selected_title = title
selected_version = item.text(0)
selected_region = item.parent().text(0)
self.load_title_data(selected_title, selected_version, selected_region)
def update_log_text(self, new_text):
self.log_text += new_text + "\n"
self.ui.log_text_browser.setText(self.log_text)
# Always auto-scroll to the bottom of the log.
scrollBar = self.ui.log_text_browser.verticalScrollBar()
scrollBar.setValue(scrollBar.maximum())
def load_title_data(self, selected_title, selected_version, selected_region=None):
selected_version = selected_version[1:]
if selected_title["TID"][-2:] == "XX":
global regions
region_code = ""
for region in regions:
if region[1] == selected_region:
region_code = region[2]
tid = selected_title["TID"][:-2] + region_code
else:
tid = selected_title["TID"]
self.ui.tid_entry.setText(tid)
self.ui.version_entry.setText(selected_version)
try:
wad_name = selected_title["WAD Name"] + "-v" + selected_version + ".wad"
self.ui.wad_file_entry.setText(wad_name)
except KeyError:
pass
danger_text = ""
try:
danger_text = selected_title["Danger"]
except KeyError:
pass
if selected_title["Ticket"] is False:
danger_text = danger_text + ("Note: This Title does not have a Ticket available, so it cannot be "
"packed into a WAD or decrypted.")
self.log_text = (tid + " - " + selected_title["Name"] + "\n" + "Version: " + selected_version + "\n\n" +
danger_text + "\n")
self.ui.log_text_browser.setText(self.log_text)
def download_btn_pressed(self):
if (self.ui.pack_wad_chkbox.isChecked() is False and self.ui.keep_enc_chkbox.isChecked() is False and
self.ui.create_dec_chkbox.isChecked() is False):
msgBox = QMessageBox()
msgBox.setIcon(QMessageBox.Icon.Critical)
msgBox.setStandardButtons(QMessageBox.StandardButton.Ok)
msgBox.setDefaultButton(QMessageBox.StandardButton.Ok)
msgBox.setWindowTitle("No Output Selected")
msgBox.setText("You have not selected any format to output the data in!")
msgBox.setInformativeText("Please select at least one option for how you would like the download to be "
"saved.")
msgBox.exec()
return
self.ui.tid_entry.setEnabled(False)
self.ui.version_entry.setEnabled(False)
self.ui.download_btn.setEnabled(False)
self.ui.pack_wad_chkbox.setEnabled(False)
self.ui.keep_enc_chkbox.setEnabled(False)
self.ui.create_dec_chkbox.setEnabled(False)
self.ui.use_local_chkbox.setEnabled(False)
self.ui.wad_file_entry.setEnabled(False)
self.log_text = ""
self.ui.log_text_browser.setText(self.log_text)
worker = Worker(self.run_nus_download)
worker.signals.result.connect(self.check_download_result)
worker.signals.progress.connect(self.update_log_text)
self.threadpool.start(worker)
def check_download_result(self, result):
msgBox = QMessageBox()
msgBox.setIcon(QMessageBox.Icon.Critical)
msgBox.setStandardButtons(QMessageBox.StandardButton.Ok)
msgBox.setDefaultButton(QMessageBox.StandardButton.Ok)
if result == -1:
msgBox.setWindowTitle("Invalid Title ID")
msgBox.setText("The Title ID you have entered is not in a valid format!")
msgBox.setInformativeText("Title IDs must be 16 digit strings of numbers and letters. Please enter a "
"correctly formatted Title ID, or select one from the menu on the left.")
msgBox.exec()
elif result == -2:
msgBox.setWindowTitle("Title ID/Version Not Found")
msgBox.setText("No title with the provided Title ID or version could be found!")
msgBox.setInformativeText("Please make sure that you have entered a valid Title ID, or selected one from "
" the title database, and that the provided version exists for the title you are"
" attempting to download.")
msgBox.exec()
elif result == -3:
msgBox.setWindowTitle("Content Decryption Failed")
msgBox.setText("Content decryption was not successful! Decrypted contents could not be created.")
msgBox.setInformativeText("Your TMD or Ticket may be damaged, or they may not correspond with the content "
"being decrypted. If you have checked \"Use local files, if they exist\", try "
"disabling that option before trying the download again to fix potential issues "
"with local data.")
msgBox.exec()
elif result == 1:
msgBox.setIcon(QMessageBox.Icon.Warning)
msgBox.setWindowTitle("Ticket Not Available")
msgBox.setText("No Ticket is Available for the Requested Title!")
msgBox.setInformativeText("A ticket could not be downloaded for the requested title, but you have selected "
"\"Pack WAD\" or \"Create Decrypted Contents\". These options are not available "
"for titles without a ticket. Only encrypted contents have been saved.")
msgBox.exec()
self.ui.tid_entry.setEnabled(True)
self.ui.version_entry.setEnabled(True)
self.ui.download_btn.setEnabled(True)
self.ui.pack_wad_chkbox.setEnabled(True)
self.ui.keep_enc_chkbox.setEnabled(True)
self.ui.create_dec_chkbox.setEnabled(True)
self.ui.use_local_chkbox.setEnabled(True)
if self.ui.pack_wad_chkbox.isChecked() is True:
self.ui.wad_file_entry.setEnabled(True)
def run_nus_download(self, progress_callback):
tid = self.ui.tid_entry.text()
if len(tid) != 16:
return -1
try:
version = int(self.ui.version_entry.text())
except ValueError:
version = None
pack_wad_enabled = self.ui.pack_wad_chkbox.isChecked()
decrypt_contents_enabled = self.ui.create_dec_chkbox.isChecked()
title = libWiiPy.Title()
title_dir = pathlib.Path(os.path.join(out_folder, tid))
if not title_dir.is_dir():
title_dir.mkdir()
if version is not None:
progress_callback.emit("Downloading title " + tid + " v" + str(version) + ", please wait...")
else:
progress_callback.emit("Downloading title " + tid + " vLatest, please wait...")
progress_callback.emit(" - Downloading and parsing TMD...")
try:
if version is not None:
title.load_tmd(libWiiPy.download_tmd(tid, version))
else:
title.load_tmd(libWiiPy.download_tmd(tid))
version = title.tmd.title_version
except ValueError:
return -2
version_dir = pathlib.Path(os.path.join(title_dir, str(version)))
if not version_dir.is_dir():
version_dir.mkdir()
tmd_out = open(os.path.join(version_dir, "tmd." + str(version)), "wb")
tmd_out.write(title.tmd.dump())
tmd_out.close()
if self.ui.use_local_chkbox.isChecked() is True and os.path.exists(os.path.join(version_dir, "tik")):
progress_callback.emit(" - Parsing local copy of Ticket...")
local_ticket = open(os.path.join(version_dir, "tik"), "rb")
title.load_ticket(local_ticket.read())
else:
progress_callback.emit(" - Downloading and parsing Ticket...")
try:
title.load_ticket(libWiiPy.download_ticket(tid))
ticket_out = open(os.path.join(version_dir, "tik"), "wb")
ticket_out.write(title.ticket.dump())
ticket_out.close()
except ValueError:
progress_callback.emit(" - No Ticket is available!")
pack_wad_enabled = False
decrypt_contents_enabled = False
title.load_content_records()
content_list = []
for content in range(len(title.tmd.content_records)):
content_id_hex = hex(title.tmd.content_records[content].content_id)[2:]
if len(content_id_hex) < 2:
content_id_hex = "0" + content_id_hex
content_file_name = "000000" + content_id_hex
if self.ui.use_local_chkbox.isChecked() is True and os.path.exists(os.path.join(version_dir,
content_file_name)):
progress_callback.emit(" - Using local copy of content " + str(content + 1) + " of " +
str(len(title.tmd.content_records)))
local_file = open(os.path.join(version_dir, content_file_name), "rb")
content_list.append(local_file.read())
else:
progress_callback.emit(" - Downloading content " + str(content + 1) + " of " +
str(len(title.tmd.content_records)) + " (" +
str(title.tmd.content_records[content].content_size) + " bytes)...")
content_list.append(libWiiPy.download_content(tid, title.tmd.content_records[content].content_id))
progress_callback.emit(" - Done!")
if self.ui.keep_enc_chkbox.isChecked() is True:
content_id_hex = hex(title.tmd.content_records[content].content_id)[2:]
if len(content_id_hex) < 2:
content_id_hex = "0" + content_id_hex
content_file_name = "000000" + content_id_hex
enc_content_out = open(os.path.join(version_dir, content_file_name), "wb")
enc_content_out.write(content_list[content])
enc_content_out.close()
title.content.content_list = content_list
if decrypt_contents_enabled is True:
try:
for content in range(len(title.tmd.content_records)):
progress_callback.emit(" - Decrypting content " + str(content + 1) + " of " +
str(len(title.tmd.content_records)) + "...")
dec_content = title.get_content_by_index(content)
content_id_hex = hex(title.tmd.content_records[content].content_id)[2:]
if len(content_id_hex) < 2:
content_id_hex = "0" + content_id_hex
content_file_name = "000000" + content_id_hex + ".app"
dec_content_out = open(os.path.join(version_dir, content_file_name), "wb")
dec_content_out.write(dec_content)
dec_content_out.close()
except ValueError:
return -3
if pack_wad_enabled is True:
progress_callback.emit(" - Building certificate...")
title.wad.set_cert_data(libWiiPy.download_cert())
progress_callback.emit("Packing WAD...")
if self.ui.wad_file_entry.text() != "":
wad_file_name = self.ui.wad_file_entry.text()
if wad_file_name[-4:] != ".wad":
wad_file_name = wad_file_name + ".wad"
else:
wad_file_name = tid + "-v" + str(version) + ".wad"
file = open(os.path.join(version_dir, wad_file_name), "wb")
file.write(title.dump_wad())
file.close()
progress_callback.emit("Download complete!")
if ((not pack_wad_enabled and self.ui.pack_wad_chkbox.isChecked()) or
(not decrypt_contents_enabled and self.ui.create_dec_chkbox.isChecked())):
return 1
return 0
def pack_wad_chkbox_toggled(self):
if self.ui.pack_wad_chkbox.isChecked() is True:
self.ui.wad_file_entry.setEnabled(True)
else:
self.ui.wad_file_entry.setEnabled(False)
if __name__ == "__main__":
app = QApplication(sys.argv)
database_file = open(os.path.join(os.path.dirname(__file__), "data/wii-database.json"))
wii_database = json.load(database_file)
try:
# noinspection PyUnresolvedReferences
out_folder = os.path.join(__compiled__.containing_dir, "titles")
except NameError:
out_folder = os.path.join(os.path.dirname(sys.argv[0]), "titles")
if not os.path.isdir(out_folder):
os.mkdir(out_folder)
window = MainWindow()
window.setWindowTitle("NUSD-Py")
window.show()
sys.exit(app.exec())