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