diff --git a/.github/workflows/python-build.yml b/.github/workflows/python-build.yml
index 4a073f6..e291a71 100644
--- a/.github/workflows/python-build.yml
+++ b/.github/workflows/python-build.yml
@@ -19,7 +19,9 @@ jobs:
steps:
- uses: actions/checkout@v4
- name: Install ccache for Nuitka
- run: sudo apt update && sudo apt install -y ccache libicu70
+ run: |
+ sudo apt update && \
+ sudo apt install -y ccache patchelf
- name: Set up Python 3.12
uses: actions/setup-python@v5
with:
diff --git a/NUSGet.py b/NUSGet.py
index 33dcc45..d1e6e2a 100644
--- a/NUSGet.py
+++ b/NUSGet.py
@@ -1,5 +1,5 @@
# "NUSGet.py", licensed under the MIT license
-# Copyright 2024 NinjaCheetah
+# Copyright 2024-2025 NinjaCheetah
# Nuitka options. These determine compilation settings based on the current OS.
# nuitka-project-if: {OS} == "Darwin":
@@ -82,7 +82,8 @@ class MainWindow(QMainWindow, Ui_MainWindow):
self.threadpool = QThreadPool()
self.ui.download_btn.clicked.connect(self.download_btn_pressed)
self.ui.script_btn.clicked.connect(self.script_btn_pressed)
- self.ui.pack_archive_chkbox.clicked.connect(self.pack_wad_chkbox_toggled)
+ self.ui.pack_archive_chkbox.toggled.connect(
+ lambda: self.ui.archive_file_entry.setEnabled(self.ui.pack_archive_chkbox.isChecked()))
self.ui.tid_entry.textChanged.connect(self.tid_updated)
# Basic intro text set to automatically show when the app loads. This may be changed in the future.
libwiipy_version = "v" + version("libWiiPy")
@@ -93,7 +94,7 @@ class MainWindow(QMainWindow, Ui_MainWindow):
"Titles marked with a checkmark are free and have a ticket available, and can"
" be decrypted and/or packed into a WAD or TAD. Titles with an X do not have "
"a ticket, and only their encrypted contents can be saved.\n\nTitles will be "
- "downloaded to a folder named \"NUSGet\" inside your downloads folder.")
+ "downloaded to a folder named \"NUSGet Downloads\" inside your downloads folder.")
.format(nusget_version=nusget_version, libwiipy_version=libwiipy_version,
libtwlpy_version=libtwlpy_version))
self.ui.log_text_browser.setText(self.log_text)
@@ -123,6 +124,15 @@ class MainWindow(QMainWindow, Ui_MainWindow):
self.trees[tree].collapsed.connect(lambda: self.resize_tree(self.ui.platform_tabs.currentIndex()))
# Prevent resizing.
self.setFixedSize(self.size())
+ # These connections allow for clicking the checkbox labels to toggle the checkboxes, if they're enabled. This is
+ # required because checkboxes can't word wrap, so regular labels must be used in their place.
+ connect_label_to_checkbox(self.ui.pack_archive_chkbox_lbl, self.ui.pack_archive_chkbox)
+ connect_label_to_checkbox(self.ui.keep_enc_chkbox_lbl, self.ui.keep_enc_chkbox)
+ connect_label_to_checkbox(self.ui.create_dec_chkbox_lbl, self.ui.create_dec_chkbox)
+ connect_label_to_checkbox(self.ui.use_local_chkbox_lbl, self.ui.use_local_chkbox)
+ connect_label_to_checkbox(self.ui.use_wiiu_nus_chkbox_lbl, self.ui.use_wiiu_nus_chkbox)
+ connect_label_to_checkbox(self.ui.patch_ios_chkbox_lbl, self.ui.patch_ios_chkbox)
+ connect_label_to_checkbox(self.ui.pack_vwii_mode_chkbox_lbl, self.ui.pack_vwii_mode_chkbox)
# Do a quick check to see if there's a newer release available, and inform the user if there is.
worker = Worker(check_nusget_updates, app, nusget_version)
worker.signals.result.connect(self.prompt_for_update)
@@ -241,6 +251,7 @@ class MainWindow(QMainWindow, Ui_MainWindow):
self.ui.keep_enc_chkbox.setEnabled(False)
self.ui.create_dec_chkbox.setEnabled(False)
self.ui.use_local_chkbox.setEnabled(False)
+ self.ui.patch_ios_chkbox.setEnabled(False)
self.ui.use_wiiu_nus_chkbox.setEnabled(False)
self.ui.pack_vwii_mode_chkbox.setEnabled(False)
self.ui.archive_file_entry.setEnabled(False)
@@ -258,6 +269,7 @@ class MainWindow(QMainWindow, Ui_MainWindow):
self.ui.keep_enc_chkbox.setEnabled(True)
self.ui.create_dec_chkbox.setEnabled(True)
self.ui.use_local_chkbox.setEnabled(True)
+ self.ui.patch_ios_chkbox.setEnabled(True)
self.ui.use_wiiu_nus_chkbox.setEnabled(True)
self.ui.console_select_dropdown.setEnabled(True)
if self.ui.pack_archive_chkbox.isChecked() is True:
@@ -373,14 +385,6 @@ class MainWindow(QMainWindow, Ui_MainWindow):
self.update_log_text(f" - {title}")
self.unlock_ui()
- def pack_wad_chkbox_toggled(self):
- # Simple function to catch when the WAD/TAD checkbox is toggled and enable/disable the file name entry box
- # accordingly.
- if self.ui.pack_archive_chkbox.isChecked() is True:
- self.ui.archive_file_entry.setEnabled(True)
- else:
- self.ui.archive_file_entry.setEnabled(False)
-
def selected_console_changed(self):
# Callback function to enable or disable console-specific settings based on the selected console.
if self.ui.console_select_dropdown.currentText() == "vWii":
@@ -488,9 +492,9 @@ if __name__ == "__main__":
else:
location = pathlib.Path(os.path.expanduser('~')).joinpath('Downloads')
# Build the path by combining the path to the Downloads photo with "NUSGet".
- out_folder = location.joinpath("NUSGet")
- # Create the "NUSGet" directory if it doesn't exist. In the future, this will be user-customizable, but this works
- # for now, and avoids the issues from when it used to use a directory next to the binary (mostly on macOS).
+ out_folder = location.joinpath("NUSGet Downloads")
+ # Create the "NUSGet Downloads" directory if it doesn't exist. In the future, this will be user-customizable, but
+ # this works for now, and avoids using a directory next to the binary (mostly an issue on macOS/Linux).
if not out_folder.is_dir():
out_folder.mkdir()
@@ -498,11 +502,21 @@ if __name__ == "__main__":
# it looks nice, but fallback on kvantum if it isn't, since kvantum is likely to exist. If all else fails, fusion.
if platform.system() == "Linux":
if os.path.isdir("/usr/lib/qt6/plugins"):
- app.addLibraryPath("/usr/lib/qt6/plugins")
- if "Breeze" in QStyleFactory.keys():
- app.setStyle("Breeze")
- elif "kvantum" in QStyleFactory.keys():
- app.setStyle("kvantum")
+ import subprocess
+ try:
+ # This CANNOT be the best way to get the system Qt version, but it's what I came up with for now.
+ result = subprocess.run(['/usr/lib/qt6/bin/qtdiag'], stdout=subprocess.PIPE)
+ result_str = result.stdout.decode("utf-8").split("\n")[0]
+ sys_qt_ver = result_str.split(" ")[1].split(".")
+ pyside_qt_ver = version("PySide6").split(".")
+ if sys_qt_ver[0:2] == pyside_qt_ver[0:2]:
+ app.addLibraryPath("/usr/lib/qt6/plugins")
+ if "Breeze" in QStyleFactory.keys():
+ app.setStyle("Breeze")
+ elif "kvantum" in QStyleFactory.keys():
+ app.setStyle("kvantum")
+ except Exception as e:
+ print(e)
# Load qtbase translations, and then apps-specific translations.
path = QLibraryInfo.path(QLibraryInfo.LibraryPath.TranslationsPath)
diff --git a/README.md b/README.md
index 3a89093..365c0f5 100644
--- a/README.md
+++ b/README.md
@@ -1,10 +1,17 @@
# NUSGet After Dark
A modern and supercharged NUS downloader built with Python and Qt6. Powered by libWiiPy and libTWLPy. Fork with features not acceptable for prod.
+
+

+
NUSGet
+
A modern and supercharged NUS downloader built with Python and Qt6.
+
Powered by libWiiPy and libTWLPy.
+
+
+
+
-[](https://github.com/NinjaCheetah/NUSGet/actions/workflows/python-build.yml)
-
-The name is a play on NuGet, the .NET package manager. Thank you [@Janni9009](https://github.com/Janni9009) for the name idea!
-
+
+
## Features
NUSGet allows you to download any content from the Nintendo Update Servers. Free content (content with a Ticket freely available on the servers) can be decrypted or packed directly into an installable archive (WAD/TAD).
@@ -74,7 +81,7 @@ A huge thanks to all the wonderful translators who've helped make NUSGet availab
If your language isn't present or is out of date, and you'd like to contribute, you can check out [TRANSLATING.md](https://github.com/NinjaCheetah/NUSGet/blob/main/TRANSLATING.md) for directions on how to translate NUSGet.
-## Why this and not NUSD?
-NUS Downloader (Nintendo Update Server Downloader), is an old tool for downloading titles from the Nintendo Update Servers for the Wii and DSi. Originally released in 2009, and effectively last updated in 2011, it stills works today, however it definitely shows its age, and is in need of a refresh. One of the major shortcomings of NUSD is that it only supports Windows, as most of the tools for the Wii from that era are written in C# and use the .NET Framework, especially since they tend to rely on the C# library libWiiSharp. NUSD also has far more limited support for DSi titles, and no support whatsoever for vWii titles.
+## Additional Thanks
+The name is a play on NuGet, the .NET package manager. Thank you [@Janni9009](https://github.com/Janni9009) for the name idea!
-With my introduction of [libWiiPy](https://github.com/NinjaCheetah/libWiiPy), there's now a work-in-progress Python library designed to eventually have feature parity with libWiiSharp. At this point in time, the library is featured enough that every piece of libWiiSharp that NUSD relied on is now available in libWiiPy, so I decided to put that to use and create a replacement for it. NUSGet offers nearly all the same features as NUSD (currently there is no support for the DSi servers or for scripting), but is built on top of a modern library with a modern graphical framework, that being Qt6. A major benefit of this rewrite is that its fully cross-platform, and is natively compiled for Windows, Linux, and macOS.
+Thanks to all those who contributed to libWiiSharp and NUSD, without which this project would not exist.
diff --git a/build_translations.py b/build_translations.py
index d4d0f55..bc515e8 100644
--- a/build_translations.py
+++ b/build_translations.py
@@ -1,5 +1,5 @@
# "build_translations.py", licensed under the MIT license
-# Copyright 2024 NinjaCheetah
+# Copyright 2024-2025 NinjaCheetah
# This script exists to work around an issue in PySide6 where the "pyside6-project build" command incorrectly places
# translation files in the root of the project directory while building.
diff --git a/modules/core.py b/modules/core.py
index 6c0033a..de79849 100644
--- a/modules/core.py
+++ b/modules/core.py
@@ -1,10 +1,12 @@
# "modules/core.py", licensed under the MIT license
-# Copyright 2024 NinjaCheetah
+# Copyright 2024-2025 NinjaCheetah
import requests
from dataclasses import dataclass
from typing import List
+from PySide6.QtCore import Qt as _Qt
+
@dataclass
class TitleData:
@@ -36,6 +38,13 @@ class BatchResults:
failed_titles: List[str]
+def connect_label_to_checkbox(label, checkbox):
+ def toggle_checkbox(event):
+ if checkbox.isEnabled() and event.button() == _Qt.LeftButton:
+ checkbox.toggle()
+ label.mousePressEvent = toggle_checkbox
+
+
def check_nusget_updates(app, current_version: str, progress_callback=None) -> str | None:
# Simple function to make a request to the GitHub API and then check if the latest available version is newer.
gh_api_request = requests.get(url="https://api.github.com/repos/NinjaCheetah/NUSGet/releases/latest", stream=True)
diff --git a/modules/download_batch.py b/modules/download_batch.py
index d2c3cb9..bc86208 100644
--- a/modules/download_batch.py
+++ b/modules/download_batch.py
@@ -1,5 +1,5 @@
# "modules/download_batch.py", licensed under the MIT license
-# Copyright 2024 NinjaCheetah
+# Copyright 2024-2025 NinjaCheetah
import pathlib
from typing import List
diff --git a/modules/download_dsi.py b/modules/download_dsi.py
index 285acc4..c0d8c98 100644
--- a/modules/download_dsi.py
+++ b/modules/download_dsi.py
@@ -1,8 +1,7 @@
# "modules/download_dsi.py", licensed under the MIT license
-# Copyright 2024 NinjaCheetah
+# Copyright 2024-2025 NinjaCheetah
import pathlib
-from typing import List, Tuple
import libTWLPy
diff --git a/modules/download_wii.py b/modules/download_wii.py
index f42af6a..2547659 100644
--- a/modules/download_wii.py
+++ b/modules/download_wii.py
@@ -1,5 +1,5 @@
# "modules/download_wii.py", licensed under the MIT license
-# Copyright 2024 NinjaCheetah
+# Copyright 2024-2025 NinjaCheetah
import pathlib
from typing import List, Tuple
@@ -146,7 +146,7 @@ def run_nus_download_wii(out_folder: pathlib.Path, tid: str, version: str, pack_
title.ticket.title_key_enc = title_key_common
# Get the WAD certificate chain, courtesy of libWiiPy.
progress_callback.emit(" - Building certificate...")
- title.wad.set_cert_data(libWiiPy.title.download_cert(wiiu_endpoint=wiiu_nus_enabled))
+ title.load_cert_chain(libWiiPy.title.download_cert_chain(wiiu_endpoint=wiiu_nus_enabled))
# Use a typed WAD name if there is one, and auto generate one based on the TID and version if there isn't.
progress_callback.emit(" - Packing WAD...")
if wad_file_name != "" and wad_file_name is not None:
diff --git a/modules/tree.py b/modules/tree.py
index 3b407d8..10fb3c9 100644
--- a/modules/tree.py
+++ b/modules/tree.py
@@ -1,5 +1,5 @@
# "modules/tree.py", licensed under the MIT license
-# Copyright 2024 NinjaCheetah
+# Copyright 2024-2025 NinjaCheetah
from modules.core import TitleData
from PySide6.QtCore import QAbstractItemModel, QModelIndex, Qt, QSortFilterProxyModel
diff --git a/qt/py/ui_MainMenu.py b/qt/py/ui_MainMenu.py
index b5334c7..62329a0 100644
--- a/qt/py/ui_MainMenu.py
+++ b/qt/py/ui_MainMenu.py
@@ -189,16 +189,16 @@ class Ui_MainWindow(object):
self.pack_archive_row.addWidget(self.pack_archive_chkbox)
- self.label_7 = QLabel(self.centralwidget)
- self.label_7.setObjectName(u"label_7")
+ self.pack_archive_chkbox_lbl = QLabel(self.centralwidget)
+ self.pack_archive_chkbox_lbl.setObjectName(u"pack_archive_chkbox_lbl")
sizePolicy4 = QSizePolicy(QSizePolicy.Policy.Preferred, QSizePolicy.Policy.MinimumExpanding)
sizePolicy4.setHorizontalStretch(0)
sizePolicy4.setVerticalStretch(0)
- sizePolicy4.setHeightForWidth(self.label_7.sizePolicy().hasHeightForWidth())
- self.label_7.setSizePolicy(sizePolicy4)
- self.label_7.setWordWrap(True)
+ sizePolicy4.setHeightForWidth(self.pack_archive_chkbox_lbl.sizePolicy().hasHeightForWidth())
+ self.pack_archive_chkbox_lbl.setSizePolicy(sizePolicy4)
+ self.pack_archive_chkbox_lbl.setWordWrap(True)
- self.pack_archive_row.addWidget(self.label_7)
+ self.pack_archive_row.addWidget(self.pack_archive_chkbox_lbl)
self.verticalLayout_7.addLayout(self.pack_archive_row)
@@ -221,13 +221,13 @@ class Ui_MainWindow(object):
self.keep_enc_row.addWidget(self.keep_enc_chkbox)
- self.label_6 = QLabel(self.centralwidget)
- self.label_6.setObjectName(u"label_6")
- sizePolicy4.setHeightForWidth(self.label_6.sizePolicy().hasHeightForWidth())
- self.label_6.setSizePolicy(sizePolicy4)
- self.label_6.setWordWrap(True)
+ self.keep_enc_chkbox_lbl = QLabel(self.centralwidget)
+ self.keep_enc_chkbox_lbl.setObjectName(u"keep_enc_chkbox_lbl")
+ sizePolicy4.setHeightForWidth(self.keep_enc_chkbox_lbl.sizePolicy().hasHeightForWidth())
+ self.keep_enc_chkbox_lbl.setSizePolicy(sizePolicy4)
+ self.keep_enc_chkbox_lbl.setWordWrap(True)
- self.keep_enc_row.addWidget(self.label_6)
+ self.keep_enc_row.addWidget(self.keep_enc_chkbox_lbl)
self.verticalLayout_7.addLayout(self.keep_enc_row)
@@ -314,14 +314,14 @@ class Ui_MainWindow(object):
self.patch_ios_row.addWidget(self.patch_ios_chkbox)
- self.patch_ios_lbl = QLabel(self.centralwidget)
- self.patch_ios_lbl.setObjectName(u"patch_ios_lbl")
- self.patch_ios_lbl.setEnabled(True)
- sizePolicy4.setHeightForWidth(self.patch_ios_lbl.sizePolicy().hasHeightForWidth())
- self.patch_ios_lbl.setSizePolicy(sizePolicy4)
- self.patch_ios_lbl.setWordWrap(True)
+ self.patch_ios_chkbox_lbl = QLabel(self.centralwidget)
+ self.patch_ios_chkbox_lbl.setObjectName(u"patch_ios_chkbox_lbl")
+ self.patch_ios_chkbox_lbl.setEnabled(True)
+ sizePolicy4.setHeightForWidth(self.patch_ios_chkbox_lbl.sizePolicy().hasHeightForWidth())
+ self.patch_ios_chkbox_lbl.setSizePolicy(sizePolicy4)
+ self.patch_ios_chkbox_lbl.setWordWrap(True)
- self.patch_ios_row.addWidget(self.patch_ios_lbl)
+ self.patch_ios_row.addWidget(self.patch_ios_chkbox_lbl)
self.verticalLayout_7.addLayout(self.patch_ios_row)
@@ -357,17 +357,17 @@ class Ui_MainWindow(object):
self.pack_vwii_mode_row.addWidget(self.pack_vwii_mode_chkbox)
- self.pack_vwii_mode_lbl = QLabel(self.centralwidget)
- self.pack_vwii_mode_lbl.setObjectName(u"pack_vwii_mode_lbl")
- self.pack_vwii_mode_lbl.setEnabled(True)
+ self.pack_vwii_mode_chkbox_lbl = QLabel(self.centralwidget)
+ self.pack_vwii_mode_chkbox_lbl.setObjectName(u"pack_vwii_mode_chkbox_lbl")
+ self.pack_vwii_mode_chkbox_lbl.setEnabled(True)
sizePolicy5 = QSizePolicy(QSizePolicy.Policy.Preferred, QSizePolicy.Policy.Preferred)
sizePolicy5.setHorizontalStretch(0)
sizePolicy5.setVerticalStretch(0)
- sizePolicy5.setHeightForWidth(self.pack_vwii_mode_lbl.sizePolicy().hasHeightForWidth())
- self.pack_vwii_mode_lbl.setSizePolicy(sizePolicy5)
- self.pack_vwii_mode_lbl.setWordWrap(True)
+ sizePolicy5.setHeightForWidth(self.pack_vwii_mode_chkbox_lbl.sizePolicy().hasHeightForWidth())
+ self.pack_vwii_mode_chkbox_lbl.setSizePolicy(sizePolicy5)
+ self.pack_vwii_mode_chkbox_lbl.setWordWrap(True)
- self.pack_vwii_mode_row.addWidget(self.pack_vwii_mode_lbl)
+ self.pack_vwii_mode_row.addWidget(self.pack_vwii_mode_chkbox_lbl)
self.verticalLayout_8.addLayout(self.pack_vwii_mode_row)
@@ -426,15 +426,15 @@ class Ui_MainWindow(object):
self.download_btn.setText(QCoreApplication.translate("MainWindow", u"Start Download", None))
self.script_btn.setText(QCoreApplication.translate("MainWindow", u"Run Script", None))
self.label_3.setText(QCoreApplication.translate("MainWindow", u"General Settings", None))
- self.label_7.setText(QCoreApplication.translate("MainWindow", u"Pack installable archive (WAD/TAD)", None))
+ self.pack_archive_chkbox_lbl.setText(QCoreApplication.translate("MainWindow", u"Pack installable archive (WAD/TAD)", None))
self.archive_file_entry.setPlaceholderText(QCoreApplication.translate("MainWindow", u"File Name", None))
- self.label_6.setText(QCoreApplication.translate("MainWindow", u"Keep encrypted contents", None))
+ self.keep_enc_chkbox_lbl.setText(QCoreApplication.translate("MainWindow", u"Keep encrypted contents", None))
self.create_dec_chkbox_lbl.setText(QCoreApplication.translate("MainWindow", u"Create decrypted contents (*.app)", None))
self.use_local_chkbox_lbl.setText(QCoreApplication.translate("MainWindow", u"Use local files, if they exist", None))
self.use_wiiu_nus_chkbox_lbl.setText(QCoreApplication.translate("MainWindow", u"Use the Wii U NUS (faster, only effects Wii/vWii)", None))
- self.patch_ios_lbl.setText(QCoreApplication.translate("MainWindow", u"Apply patches to IOS (Applies to WADs only)", None))
+ self.patch_ios_chkbox_lbl.setText(QCoreApplication.translate("MainWindow", u"Apply patches to IOS (Applies to WADs only)", None))
self.label_4.setText(QCoreApplication.translate("MainWindow", u"vWii Title Settings", None))
- self.pack_vwii_mode_lbl.setText(QCoreApplication.translate("MainWindow", u"Re-encrypt title using the Wii Common Key", None))
+ self.pack_vwii_mode_chkbox_lbl.setText(QCoreApplication.translate("MainWindow", u"Re-encrypt title using the Wii Common Key", None))
self.log_text_browser.setMarkdown("")
self.log_text_browser.setHtml(QCoreApplication.translate("MainWindow", u"\n"
"