Fully functional as a basic NUS downloader application, only lacking titles database

This commit is contained in:
Campbell 2024-04-07 15:36:36 -04:00
parent 4f76259ccf
commit f513afc39a
Signed by: NinjaCheetah
GPG Key ID: B547958AF96ED344
4 changed files with 311 additions and 25 deletions

1
.gitignore vendored
View File

@ -164,6 +164,7 @@ cython_debug/
# Allows me to keep TMD files in my repository folder for testing without accidentally publishing them # Allows me to keep TMD files in my repository folder for testing without accidentally publishing them
*.tmd *.tmd
*.wad *.wad
titles/
out_prod/ out_prod/
remakewad.pl remakewad.pl

161
main.py
View File

@ -1,37 +1,180 @@
import sys import sys
import os import os
import pathlib
import traceback
import libWiiPy import libWiiPy
from PySide6.QtWidgets import QApplication, QMainWindow, QFileDialog, QMessageBox 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 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): class MainWindow(QMainWindow, Ui_MainWindow):
def __init__(self): def __init__(self):
super(MainWindow, self).__init__() super(MainWindow, self).__init__()
self.ui = Ui_MainWindow() self.ui = Ui_MainWindow()
self.ui.setupUi(self) self.ui.setupUi(self)
self.log_text = ""
self.threadpool = QThreadPool()
self.ui.download_btn.clicked.connect(self.download_btn_pressed) 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): 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() title = libWiiPy.Title()
tid = self.ui.tid_entry.text() title_dir = pathlib.Path(os.path.join(out_folder, tid))
version = int(self.ui.version_entry.text()) 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") progress_callback.emit(" - Downloading and parsing TMD...")
file.write(title.dump_wad()) if version is not None:
file.close() title.load_tmd(libWiiPy.download_tmd(tid, version))
self.ui.textBrowser.setMarkdown("## Done!") 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__": if __name__ == "__main__":
app = QApplication(sys.argv) app = QApplication(sys.argv)
app.setStyle('breeze')
out_folder = pathlib.Path("titles")
if not out_folder.is_dir():
out_folder.mkdir()
window = MainWindow() window = MainWindow()
window.setWindowTitle("NUSD-Py") window.setWindowTitle("NUSD-Py")

143
qt/py/ui_MainMenu.py Normal file
View File

@ -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

View File

@ -30,23 +30,16 @@
<item> <item>
<layout class="QHBoxLayout" name="horizontalLayout"> <layout class="QHBoxLayout" name="horizontalLayout">
<item> <item>
<widget class="QPushButton" name="pushButton_3"> <widget class="QPushButton" name="show_titles_btn">
<property name="text"> <property name="text">
<string>Titles</string> <string>Titles</string>
</property> </property>
</widget> </widget>
</item> </item>
<item> <item>
<widget class="QPushButton" name="pushButton"> <widget class="QPushButton" name="show_more_btn">
<property name="text"> <property name="text">
<string>Scripts</string> <string>More</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="pushButton_2">
<property name="text">
<string>Extras</string>
</property> </property>
</widget> </widget>
</item> </item>
@ -94,7 +87,7 @@
</widget> </widget>
</item> </item>
<item> <item>
<widget class="QTextBrowser" name="textBrowser"> <widget class="QTextBrowser" name="log_text_browser">
<property name="markdown"> <property name="markdown">
<string/> <string/>
</property> </property>
@ -103,14 +96,14 @@
<item> <item>
<layout class="QHBoxLayout" name="horizontalLayout_4"> <layout class="QHBoxLayout" name="horizontalLayout_4">
<item> <item>
<widget class="QCheckBox" name="checkBox"> <widget class="QCheckBox" name="pack_wad_chkbox">
<property name="text"> <property name="text">
<string>Pack WAD</string> <string>Pack WAD</string>
</property> </property>
</widget> </widget>
</item> </item>
<item> <item>
<widget class="QLineEdit" name="lineEdit_3"> <widget class="QLineEdit" name="wad_file_entry">
<property name="enabled"> <property name="enabled">
<bool>false</bool> <bool>false</bool>
</property> </property>
@ -122,21 +115,27 @@
</layout> </layout>
</item> </item>
<item> <item>
<widget class="QCheckBox" name="checkBox_2"> <widget class="QCheckBox" name="keep_enc_chkbox">
<property name="text"> <property name="text">
<string>Keep Enc. Contents</string> <string>Keep Enc. Contents</string>
</property> </property>
<property name="checked">
<bool>true</bool>
</property>
</widget> </widget>
</item> </item>
<item> <item>
<widget class="QCheckBox" name="checkBox_3"> <widget class="QCheckBox" name="create_dec_chkbox">
<property name="text"> <property name="text">
<string>Create Decrypted Contents (*.app)</string> <string>Create Decrypted Contents (*.app)</string>
</property> </property>
</widget> </widget>
</item> </item>
<item> <item>
<widget class="QCheckBox" name="checkBox_4"> <widget class="QCheckBox" name="use_local_chkbox">
<property name="enabled">
<bool>false</bool>
</property>
<property name="text"> <property name="text">
<string>Use Local Files If They Exist</string> <string>Use Local Files If They Exist</string>
</property> </property>