From f513afc39aa87fc90108400ee8de34e37b324e97 Mon Sep 17 00:00:00 2001
From: NinjaCheetah <58050615+NinjaCheetah@users.noreply.github.com>
Date: Sun, 7 Apr 2024 15:36:36 -0400
Subject: [PATCH] Fully functional as a basic NUS downloader application, only
lacking titles database
---
.gitignore | 1 +
main.py | 161 ++++++++++++++++++++++++++++++++++++++++---
qt/py/ui_MainMenu.py | 143 ++++++++++++++++++++++++++++++++++++++
qt/ui/MainMenu.ui | 31 ++++-----
4 files changed, 311 insertions(+), 25 deletions(-)
create mode 100644 qt/py/ui_MainMenu.py
diff --git a/.gitignore b/.gitignore
index aef39b9..14d098e 100644
--- a/.gitignore
+++ b/.gitignore
@@ -164,6 +164,7 @@ cython_debug/
# Allows me to keep TMD files in my repository folder for testing without accidentally publishing them
*.tmd
*.wad
+titles/
out_prod/
remakewad.pl
diff --git a/main.py b/main.py
index 3bdb8ec..e82b84b 100644
--- a/main.py
+++ b/main.py
@@ -1,37 +1,180 @@
import sys
import os
+import pathlib
+import traceback
+
import libWiiPy
from PySide6.QtWidgets import QApplication, QMainWindow, QFileDialog, QMessageBox
-from PySide6.QtCore import QThread, Signal, Qt
+from PySide6.QtCore import QRunnable, Slot, QThreadPool, Signal, QObject, Qt
from qt.py.ui_MainMenu import Ui_MainWindow
+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:
+ self.fn(**self.kwargs)
+ except ValueError:
+ self.signals.result.emit(1)
+ else:
+ self.signals.result.emit(0)
+
+
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)
+
+ 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 download_btn_pressed(self):
+ 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):
+ if result == 1:
+ msgBox = QMessageBox()
+ msgBox.setWindowTitle("Invalid Title ID/Version")
+ msgBox.setIcon(QMessageBox.Icon.Critical)
+ msgBox.setTextFormat(Qt.MarkdownText)
+ msgBox.setText("### No title with the requested Title ID or version could be found!")
+ msgBox.setInformativeText("Please make sure the Title ID is entered correctly, and if a specific version is"
+ " set, that it exists for the chosen title.")
+ msgBox.setStandardButtons(QMessageBox.StandardButton.Ok)
+ msgBox.setDefaultButton(QMessageBox.StandardButton.Ok)
+ msgBox.exec()
+
+ def run_nus_download(self, progress_callback):
+ tid = self.ui.tid_entry.text()
+ try:
+ version = int(self.ui.version_entry.text())
+ except ValueError:
+ version = None
+
title = libWiiPy.Title()
- tid = self.ui.tid_entry.text()
- version = int(self.ui.version_entry.text())
+ title_dir = pathlib.Path(os.path.join(out_folder, tid))
+ if not title_dir.is_dir():
+ title_dir.mkdir()
- title = libWiiPy.download_title(tid, version)
+ 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...")
- file = open(tid + "-v" + str(version) + ".wad", "wb")
- file.write(title.dump_wad())
- file.close()
- self.ui.textBrowser.setMarkdown("## Done!")
+ progress_callback.emit(" - Downloading and parsing TMD...")
+ 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
+
+ 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()
+
+ progress_callback.emit(" - Downloading and parsing Ticket...")
+ 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()
+
+ title.load_content_records()
+ content_list = []
+ for content in range(len(title.tmd.content_records)):
+ 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 self.ui.create_dec_chkbox.isChecked() is True:
+ 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()
+
+ if self.ui.pack_wad_chkbox.isChecked() 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!")
+
+ 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)
- app.setStyle('breeze')
+
+ out_folder = pathlib.Path("titles")
+ if not out_folder.is_dir():
+ out_folder.mkdir()
window = MainWindow()
window.setWindowTitle("NUSD-Py")
diff --git a/qt/py/ui_MainMenu.py b/qt/py/ui_MainMenu.py
new file mode 100644
index 0000000..0d9ad63
--- /dev/null
+++ b/qt/py/ui_MainMenu.py
@@ -0,0 +1,143 @@
+# -*- coding: utf-8 -*-
+
+################################################################################
+## Form generated from reading UI file 'MainMenu.ui'
+##
+## Created by: Qt User Interface Compiler version 6.6.3
+##
+## WARNING! All changes made in this file will be lost when recompiling UI file!
+################################################################################
+
+from PySide6.QtCore import (QCoreApplication, QDate, QDateTime, QLocale,
+ QMetaObject, QObject, QPoint, QRect,
+ QSize, QTime, QUrl, Qt)
+from PySide6.QtGui import (QBrush, QColor, QConicalGradient, QCursor,
+ QFont, QFontDatabase, QGradient, QIcon,
+ QImage, QKeySequence, QLinearGradient, QPainter,
+ QPalette, QPixmap, QRadialGradient, QTransform)
+from PySide6.QtWidgets import (QApplication, QCheckBox, QHBoxLayout, QLabel,
+ QLineEdit, QMainWindow, QMenuBar, QPushButton,
+ QSizePolicy, QStatusBar, QTextBrowser, QVBoxLayout,
+ QWidget)
+
+class Ui_MainWindow(object):
+ def setupUi(self, MainWindow):
+ if not MainWindow.objectName():
+ MainWindow.setObjectName(u"MainWindow")
+ MainWindow.resize(305, 605)
+ MainWindow.setMinimumSize(QSize(305, 605))
+ MainWindow.setMaximumSize(QSize(305, 605))
+ self.centralwidget = QWidget(MainWindow)
+ self.centralwidget.setObjectName(u"centralwidget")
+ self.verticalLayout_2 = QVBoxLayout(self.centralwidget)
+ self.verticalLayout_2.setObjectName(u"verticalLayout_2")
+ self.horizontalLayout = QHBoxLayout()
+ self.horizontalLayout.setObjectName(u"horizontalLayout")
+ self.show_titles_btn = QPushButton(self.centralwidget)
+ self.show_titles_btn.setObjectName(u"show_titles_btn")
+
+ self.horizontalLayout.addWidget(self.show_titles_btn)
+
+ self.show_more_btn = QPushButton(self.centralwidget)
+ self.show_more_btn.setObjectName(u"show_more_btn")
+
+ self.horizontalLayout.addWidget(self.show_more_btn)
+
+
+ self.verticalLayout_2.addLayout(self.horizontalLayout)
+
+ self.horizontalLayout_2 = QHBoxLayout()
+ self.horizontalLayout_2.setObjectName(u"horizontalLayout_2")
+ self.tid_entry = QLineEdit(self.centralwidget)
+ self.tid_entry.setObjectName(u"tid_entry")
+
+ self.horizontalLayout_2.addWidget(self.tid_entry)
+
+ self.label = QLabel(self.centralwidget)
+ self.label.setObjectName(u"label")
+
+ self.horizontalLayout_2.addWidget(self.label)
+
+ self.version_entry = QLineEdit(self.centralwidget)
+ self.version_entry.setObjectName(u"version_entry")
+ self.version_entry.setMaximumSize(QSize(75, 16777215))
+
+ self.horizontalLayout_2.addWidget(self.version_entry)
+
+
+ self.verticalLayout_2.addLayout(self.horizontalLayout_2)
+
+ self.download_btn = QPushButton(self.centralwidget)
+ self.download_btn.setObjectName(u"download_btn")
+
+ self.verticalLayout_2.addWidget(self.download_btn)
+
+ self.log_text_browser = QTextBrowser(self.centralwidget)
+ self.log_text_browser.setObjectName(u"log_text_browser")
+
+ self.verticalLayout_2.addWidget(self.log_text_browser)
+
+ self.horizontalLayout_4 = QHBoxLayout()
+ self.horizontalLayout_4.setObjectName(u"horizontalLayout_4")
+ self.pack_wad_chkbox = QCheckBox(self.centralwidget)
+ self.pack_wad_chkbox.setObjectName(u"pack_wad_chkbox")
+
+ self.horizontalLayout_4.addWidget(self.pack_wad_chkbox)
+
+ self.wad_file_entry = QLineEdit(self.centralwidget)
+ self.wad_file_entry.setObjectName(u"wad_file_entry")
+ self.wad_file_entry.setEnabled(False)
+
+ self.horizontalLayout_4.addWidget(self.wad_file_entry)
+
+
+ self.verticalLayout_2.addLayout(self.horizontalLayout_4)
+
+ self.keep_enc_chkbox = QCheckBox(self.centralwidget)
+ self.keep_enc_chkbox.setObjectName(u"keep_enc_chkbox")
+ self.keep_enc_chkbox.setChecked(True)
+
+ self.verticalLayout_2.addWidget(self.keep_enc_chkbox)
+
+ self.create_dec_chkbox = QCheckBox(self.centralwidget)
+ self.create_dec_chkbox.setObjectName(u"create_dec_chkbox")
+
+ self.verticalLayout_2.addWidget(self.create_dec_chkbox)
+
+ self.use_local_chkbox = QCheckBox(self.centralwidget)
+ self.use_local_chkbox.setObjectName(u"use_local_chkbox")
+ self.use_local_chkbox.setEnabled(False)
+
+ self.verticalLayout_2.addWidget(self.use_local_chkbox)
+
+ MainWindow.setCentralWidget(self.centralwidget)
+ self.menubar = QMenuBar(MainWindow)
+ self.menubar.setObjectName(u"menubar")
+ self.menubar.setGeometry(QRect(0, 0, 305, 30))
+ MainWindow.setMenuBar(self.menubar)
+ self.statusbar = QStatusBar(MainWindow)
+ self.statusbar.setObjectName(u"statusbar")
+ MainWindow.setStatusBar(self.statusbar)
+
+ self.retranslateUi(MainWindow)
+
+ QMetaObject.connectSlotsByName(MainWindow)
+ # setupUi
+
+ def retranslateUi(self, MainWindow):
+ MainWindow.setWindowTitle(QCoreApplication.translate("MainWindow", u"MainWindow", None))
+ self.show_titles_btn.setText(QCoreApplication.translate("MainWindow", u"Titles", None))
+ self.show_more_btn.setText(QCoreApplication.translate("MainWindow", u"More", None))
+ self.tid_entry.setText("")
+ self.tid_entry.setPlaceholderText(QCoreApplication.translate("MainWindow", u"Title ID", None))
+ self.label.setText(QCoreApplication.translate("MainWindow", u"v", None))
+ self.version_entry.setPlaceholderText(QCoreApplication.translate("MainWindow", u"Version", None))
+ self.download_btn.setText(QCoreApplication.translate("MainWindow", u"Start NUS Download!", None))
+ self.log_text_browser.setMarkdown("")
+ self.pack_wad_chkbox.setText(QCoreApplication.translate("MainWindow", u"Pack WAD", None))
+ self.wad_file_entry.setPlaceholderText(QCoreApplication.translate("MainWindow", u"File Name", None))
+ self.keep_enc_chkbox.setText(QCoreApplication.translate("MainWindow", u"Keep Enc. Contents", None))
+ self.create_dec_chkbox.setText(QCoreApplication.translate("MainWindow", u"Create Decrypted Contents (*.app)", None))
+ self.use_local_chkbox.setText(QCoreApplication.translate("MainWindow", u"Use Local Files If They Exist", None))
+ # retranslateUi
+
diff --git a/qt/ui/MainMenu.ui b/qt/ui/MainMenu.ui
index 610415c..3ffda20 100644
--- a/qt/ui/MainMenu.ui
+++ b/qt/ui/MainMenu.ui
@@ -30,23 +30,16 @@
-
-
-
+
Titles
-
-
+
- Scripts
-
-
-
- -
-
-
- Extras
+ More
@@ -94,7 +87,7 @@
-
-
+
@@ -103,14 +96,14 @@
-
-
-
+
Pack WAD
-
-
+
false
@@ -122,21 +115,27 @@
-
-
+
Keep Enc. Contents
+
+ true
+
-
-
+
Create Decrypted Contents (*.app)
-
-
+
+
+ false
+
Use Local Files If They Exist