forked from NinjaCheetah/NUSGet
Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2b46edf401 | ||
|
92ee96fb1b
|
|||
|
8bd72c5899
|
|||
|
e4b5f184c6
|
2
.github/FUNDING.yml
vendored
Normal file
2
.github/FUNDING.yml
vendored
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
github: ninjacheetah
|
||||||
|
ko_fi: ninjacheetah
|
||||||
6
Makefile
6
Makefile
@@ -6,13 +6,17 @@ all:
|
|||||||
$(CC) --show-progress --assume-yes-for-downloads NUSGet.py $(ARCH_FLAGS) -o NUSGet
|
$(CC) --show-progress --assume-yes-for-downloads NUSGet.py $(ARCH_FLAGS) -o NUSGet
|
||||||
|
|
||||||
install:
|
install:
|
||||||
rm -rd /opt/NUSGet/
|
rm -rf /opt/NUSGet/
|
||||||
install -d /opt/NUSGet
|
install -d /opt/NUSGet
|
||||||
cp -r ./NUSGet.dist/* /opt/NUSGet/
|
cp -r ./NUSGet.dist/* /opt/NUSGet/
|
||||||
chmod 755 /opt/NUSGet/
|
chmod 755 /opt/NUSGet/
|
||||||
install ./packaging/icon.png /opt/NUSGet/NUSGet.png
|
install ./packaging/icon.png /opt/NUSGet/NUSGet.png
|
||||||
install ./packaging/NUSGet.desktop /usr/share/applications
|
install ./packaging/NUSGet.desktop /usr/share/applications
|
||||||
|
|
||||||
|
uninstall:
|
||||||
|
rm -rf /opt/NUSGet
|
||||||
|
rm /usr/share/applications/NUSGet.desktop
|
||||||
|
|
||||||
clean:
|
clean:
|
||||||
rm NUSGet
|
rm NUSGet
|
||||||
rm -rd NUSGet.build/
|
rm -rd NUSGet.build/
|
||||||
|
|||||||
30
NUSGet.py
30
NUSGet.py
@@ -1,5 +1,5 @@
|
|||||||
# "NUSGet.py", licensed under the MIT license
|
# "NUSGet.py", licensed under the MIT license
|
||||||
# Copyright 2024-2025 NinjaCheetah and Contributors
|
# Copyright 2024-2026 NinjaCheetah and Contributors
|
||||||
|
|
||||||
# Nuitka options. These determine compilation settings based on the current OS.
|
# Nuitka options. These determine compilation settings based on the current OS.
|
||||||
# nuitka-project-if: {OS} == "Darwin":
|
# nuitka-project-if: {OS} == "Darwin":
|
||||||
@@ -39,9 +39,9 @@ from modules.download_batch import run_nus_download_batch
|
|||||||
from modules.download_wii import run_nus_download_wii
|
from modules.download_wii import run_nus_download_wii
|
||||||
from modules.download_dsi import run_nus_download_dsi
|
from modules.download_dsi import run_nus_download_dsi
|
||||||
|
|
||||||
nusget_version = "1.5.0"
|
NUSGET_VERSION = "1.5.1"
|
||||||
|
|
||||||
regions = {"World": ["41"], "USA/NTSC": ["45"], "Europe/PAL": ["50"], "Japan": ["4A"], "Korea": ["4B"], "China": ["43"],
|
REGIONS = {"World": ["41"], "USA/NTSC": ["45"], "Europe/PAL": ["50"], "Japan": ["4A"], "Korea": ["4B"], "China": ["43"],
|
||||||
"Australia/NZ": ["55"]}
|
"Australia/NZ": ["55"]}
|
||||||
|
|
||||||
|
|
||||||
@@ -247,9 +247,9 @@ class MainWindow(QMainWindow, Ui_MainWindow):
|
|||||||
save_config(config_data)
|
save_config(config_data)
|
||||||
if auto_update:
|
if auto_update:
|
||||||
# Do a quick check to see if there's a newer release available if auto-updates are enabled.
|
# Do a quick check to see if there's a newer release available if auto-updates are enabled.
|
||||||
worker = Worker(check_nusget_updates, app, nusget_version)
|
worker = Worker(check_nusget_updates, app, NUSGET_VERSION)
|
||||||
worker.signals.result.connect(self.prompt_for_update)
|
worker.signals.result.connect(self.prompt_for_update)
|
||||||
worker.signals.progress.connect(self.update_log_text)
|
worker.signals.progress.connect(self.progress_update)
|
||||||
self.threadpool.start(worker)
|
self.threadpool.start(worker)
|
||||||
|
|
||||||
def title_double_clicked(self, index):
|
def title_double_clicked(self, index):
|
||||||
@@ -287,7 +287,7 @@ class MainWindow(QMainWindow, Ui_MainWindow):
|
|||||||
return
|
return
|
||||||
self.ui.patch_ios_checkbox.setEnabled(False)
|
self.ui.patch_ios_checkbox.setEnabled(False)
|
||||||
|
|
||||||
def download_progress_update(self, done, total, log_text):
|
def progress_update(self, done, total, log_text):
|
||||||
if done == 0 and total == 0:
|
if done == 0 and total == 0:
|
||||||
self.ui.progress_bar.setRange(0, 0)
|
self.ui.progress_bar.setRange(0, 0)
|
||||||
elif done == -1 and total == -1:
|
elif done == -1 and total == -1:
|
||||||
@@ -316,10 +316,10 @@ class MainWindow(QMainWindow, Ui_MainWindow):
|
|||||||
msg_box.setDefaultButton(QMessageBox.StandardButton.Yes)
|
msg_box.setDefaultButton(QMessageBox.StandardButton.Yes)
|
||||||
msg_box.setWindowTitle(app.translate("MainWindow", "NUSGet Update Available"))
|
msg_box.setWindowTitle(app.translate("MainWindow", "NUSGet Update Available"))
|
||||||
msg_box.setText(app.translate("MainWindow", "<b>There's a newer version of NUSGet available!</b>"))
|
msg_box.setText(app.translate("MainWindow", "<b>There's a newer version of NUSGet available!</b>"))
|
||||||
msg_box.setInformativeText(app.translate("MainWindow", "You're currently running v{nusget_version}, "
|
msg_box.setInformativeText(app.translate("MainWindow", "You're currently running v{NUSGET_VERSION}, "
|
||||||
"but v{new_version} is available on GitHub. Would you like to view"
|
"but v{new_version} is available on GitHub. Would you like to view"
|
||||||
" the latest version?"
|
" the latest version?"
|
||||||
.format(nusget_version=nusget_version, new_version=new_version)))
|
.format(NUSGET_VERSION=NUSGET_VERSION, new_version=new_version)))
|
||||||
ret = msg_box.exec()
|
ret = msg_box.exec()
|
||||||
if ret == QMessageBox.StandardButton.Yes:
|
if ret == QMessageBox.StandardButton.Yes:
|
||||||
webbrowser.open("https://github.com/NinjaCheetah/NUSGet/releases/latest")
|
webbrowser.open("https://github.com/NinjaCheetah/NUSGet/releases/latest")
|
||||||
@@ -329,8 +329,8 @@ class MainWindow(QMainWindow, Ui_MainWindow):
|
|||||||
# If the last two characters are "XX", then this title has multiple regions, and each region uses its own
|
# If the last two characters are "XX", then this title has multiple regions, and each region uses its own
|
||||||
# two-digit code. Use the region info passed to load the correct code.
|
# two-digit code. Use the region info passed to load the correct code.
|
||||||
if selected_title.tid[-2:] == "XX":
|
if selected_title.tid[-2:] == "XX":
|
||||||
global regions
|
global REGIONS
|
||||||
region_code = regions[selected_title.region][0]
|
region_code = REGIONS[selected_title.region][0]
|
||||||
tid = selected_title.tid[:-2] + region_code
|
tid = selected_title.tid[:-2] + region_code
|
||||||
else:
|
else:
|
||||||
tid = selected_title.tid
|
tid = selected_title.tid
|
||||||
@@ -440,7 +440,7 @@ class MainWindow(QMainWindow, Ui_MainWindow):
|
|||||||
self.ui.pack_vwii_mode_checkbox.isChecked(), self.ui.patch_ios_checkbox.isChecked(),
|
self.ui.pack_vwii_mode_checkbox.isChecked(), self.ui.patch_ios_checkbox.isChecked(),
|
||||||
self.ui.archive_file_entry.text())
|
self.ui.archive_file_entry.text())
|
||||||
worker.signals.result.connect(self.check_download_result)
|
worker.signals.result.connect(self.check_download_result)
|
||||||
worker.signals.progress.connect(self.download_progress_update)
|
worker.signals.progress.connect(self.progress_update)
|
||||||
self.threadpool.start(worker)
|
self.threadpool.start(worker)
|
||||||
|
|
||||||
def check_download_result(self, result):
|
def check_download_result(self, result):
|
||||||
@@ -584,8 +584,8 @@ class MainWindow(QMainWindow, Ui_MainWindow):
|
|||||||
for category in target_database:
|
for category in target_database:
|
||||||
for t in target_database[category]:
|
for t in target_database[category]:
|
||||||
if t["TID"][-2:] == "XX":
|
if t["TID"][-2:] == "XX":
|
||||||
for r in regions:
|
for r in REGIONS:
|
||||||
if f"{t['TID'][:-2]}{regions[r][0]}" == tid:
|
if f"{t['TID'][:-2]}{REGIONS[r][0]}" == tid:
|
||||||
try:
|
try:
|
||||||
archive_name = t["Name"].replace(" ", "-")
|
archive_name = t["Name"].replace(" ", "-")
|
||||||
break
|
break
|
||||||
@@ -610,7 +610,7 @@ class MainWindow(QMainWindow, Ui_MainWindow):
|
|||||||
self.ui.use_wiiu_nus_checkbox.isChecked(), self.ui.use_local_checkbox.isChecked(),
|
self.ui.use_wiiu_nus_checkbox.isChecked(), self.ui.use_local_checkbox.isChecked(),
|
||||||
self.ui.pack_vwii_mode_checkbox.isChecked(), self.ui.patch_ios_checkbox.isChecked())
|
self.ui.pack_vwii_mode_checkbox.isChecked(), self.ui.patch_ios_checkbox.isChecked())
|
||||||
worker.signals.result.connect(self.check_batch_result)
|
worker.signals.result.connect(self.check_batch_result)
|
||||||
worker.signals.progress.connect(self.download_progress_update)
|
worker.signals.progress.connect(self.progress_update)
|
||||||
self.threadpool.start(worker)
|
self.threadpool.start(worker)
|
||||||
|
|
||||||
def open_output_dir(self):
|
def open_output_dir(self):
|
||||||
@@ -692,7 +692,7 @@ class MainWindow(QMainWindow, Ui_MainWindow):
|
|||||||
return out_folder
|
return out_folder
|
||||||
|
|
||||||
def about_nusget(self):
|
def about_nusget(self):
|
||||||
about_box = AboutNUSGet([nusget_version, version("libWiiPy"), version("libTWLPy")])
|
about_box = AboutNUSGet([NUSGET_VERSION, version("libWiiPy"), version("libTWLPy")])
|
||||||
about_box.exec()
|
about_box.exec()
|
||||||
|
|
||||||
def change_language(self, new_lang):
|
def change_language(self, new_lang):
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
# 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.
|
|
||||||
<div align="center">
|
<div align="center">
|
||||||
<img src="https://github.com/user-attachments/assets/156eb949-93aa-4453-b7a0-99b784ec0c8c" alt="The icon for NUSGet" width=256 height=256>
|
<img src="https://github.com/user-attachments/assets/156eb949-93aa-4453-b7a0-99b784ec0c8c" alt="The icon for NUSGet" width=256 height=256>
|
||||||
<h1>NUSGet</h1>
|
<h1>NUSGet</h1>
|
||||||
@@ -19,12 +17,11 @@ NUSGet also offers the ability to create vWii WADs that can be installed from wi
|
|||||||
|
|
||||||
The following features are available for all supported consoles:
|
The following features are available for all supported consoles:
|
||||||
- Downloading encrypted contents (files like `00000000`, `00000001`, etc.) directly from the update servers for any title.
|
- Downloading encrypted contents (files like `00000000`, `00000001`, etc.) directly from the update servers for any title.
|
||||||
- Creating decrypted contents (*.app files) from the encrypted contents on the servers.
|
- Creating decrypted contents (*.app files) from the encrypted contents on the servers. Only supported for free titles.
|
||||||
- Scripting support, allowing you to perform batch downloads of any titles for the Wii, vWii, or DSi in one script. (See `example-script.json` for an example of the scripting format.)
|
- Scripting support, allowing you to perform batch downloads of any titles for the Wii, vWii, or DSi in one script. (See `example-script.json` for an example of the scripting format.)
|
||||||
|
|
||||||
**For Wii and vWii titles only:**
|
**For Wii and vWii titles only:**
|
||||||
- "Pack installable archive (WAD/TAD)": Pack the encrypted contents, TMD, and Ticket into a WAD file that can be installed on a Wii or in Dolphin Emulator.
|
- "Pack installable archive (WAD/TAD)": Pack the encrypted contents, TMD, and Ticket into a WAD file that can be installed on a Wii or in Dolphin Emulator. Only supported for free titles.
|
||||||
- Forging Tickets for titles without a common Ticket available on the NUS by using the Title Key algorithm to derive the key needed to decrypt the title.
|
|
||||||
|
|
||||||
**For vWii titles only:**
|
**For vWii titles only:**
|
||||||
- "Re-encrypt title using the Wii Common Key": Re-encrypt the Title Key in a vWii title's Ticket before packing the WAD, so that the WAD can be installed via a normal WAD manager on the vWii, and can be extracted with legacy tools. **This also creates WADs that can be installed directly in Dolphin, allowing for running the vWii System Menu in Dolphin without a vWii NAND dump!**
|
- "Re-encrypt title using the Wii Common Key": Re-encrypt the Title Key in a vWii title's Ticket before packing the WAD, so that the WAD can be installed via a normal WAD manager on the vWii, and can be extracted with legacy tools. **This also creates WADs that can be installed directly in Dolphin, allowing for running the vWii System Menu in Dolphin without a vWii NAND dump!**
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
# "build_translations.py", licensed under the MIT license
|
# "build_translations.py", licensed under the MIT license
|
||||||
# Copyright 2024-2025 NinjaCheetah
|
# Copyright 2024-2026 NinjaCheetah & Contributors
|
||||||
# This script exists to work around an issue in PySide6 where the "pyside6-project build" command incorrectly places
|
# 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.
|
# translation files in the root of the project directory while building.
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
# "modules/config.py", licensed under the MIT license
|
# "modules/config.py", licensed under the MIT license
|
||||||
# Copyright 2024-2025 NinjaCheetah & Contributors
|
# Copyright 2024-2026 NinjaCheetah & Contributors
|
||||||
|
|
||||||
import os
|
import os
|
||||||
import json
|
import json
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
# "modules/core.py", licensed under the MIT license
|
# "modules/core.py", licensed under the MIT license
|
||||||
# Copyright 2024-2025 NinjaCheetah & Contributors
|
# Copyright 2024-2026 NinjaCheetah & Contributors
|
||||||
|
|
||||||
import requests
|
import requests
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
@@ -69,9 +69,11 @@ def fixup_qmenu_background(menu):
|
|||||||
|
|
||||||
def check_nusget_updates(app, current_version: str, progress_callback=None) -> str | None:
|
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.
|
# Simple function to make a request to the GitHub API and then check if the latest available version is newer.
|
||||||
|
print("checking for updates...")
|
||||||
gh_api_request = requests.get(url="https://api.github.com/repos/NinjaCheetah/NUSGet/releases/latest", stream=True)
|
gh_api_request = requests.get(url="https://api.github.com/repos/NinjaCheetah/NUSGet/releases/latest", stream=True)
|
||||||
if gh_api_request.status_code != 200:
|
if gh_api_request.status_code != 200:
|
||||||
progress_callback.emit(app.translate("MainWindow", "\n\nCould not check for updates."))
|
progress_callback.emit(-1, -1, app.translate("MainWindow", "\n\nCould not check for updates."))
|
||||||
|
print(f"update check failed, status code: {gh_api_request.status_code}")
|
||||||
else:
|
else:
|
||||||
api_response = gh_api_request.json()
|
api_response = gh_api_request.json()
|
||||||
new_version: str = api_response["tag_name"].replace('v', '')
|
new_version: str = api_response["tag_name"].replace('v', '')
|
||||||
@@ -79,9 +81,13 @@ def check_nusget_updates(app, current_version: str, progress_callback=None) -> s
|
|||||||
current_version_split = current_version.split('.')
|
current_version_split = current_version.split('.')
|
||||||
for place in range(len(new_version_split)):
|
for place in range(len(new_version_split)):
|
||||||
if new_version_split[place] < current_version_split[place]:
|
if new_version_split[place] < current_version_split[place]:
|
||||||
|
progress_callback.emit(-1, -1, "\n\nYou're running a development version of NUSGet.")
|
||||||
|
print("no update available, this is a development version")
|
||||||
return None
|
return None
|
||||||
elif new_version_split[place] > current_version_split[place]:
|
elif new_version_split[place] > current_version_split[place]:
|
||||||
progress_callback.emit(app.translate("MainWindow", "\n\nThere's a newer version of NUSGet available!"))
|
progress_callback.emit(-1, -1, app.translate("MainWindow", "\n\nThere's a newer version of NUSGet available!"))
|
||||||
|
print("update available")
|
||||||
return new_version
|
return new_version
|
||||||
progress_callback.emit(app.translate("MainWindow", "\n\nYou're running the latest release of NUSGet."))
|
progress_callback.emit(-1, -1, app.translate("MainWindow", "\n\nYou're running the latest release of NUSGet."))
|
||||||
|
print("no update available")
|
||||||
return None
|
return None
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
# "modules/download_batch.py", licensed under the MIT license
|
# "modules/download_batch.py", licensed under the MIT license
|
||||||
# Copyright 2024-2025 NinjaCheetah
|
# Copyright 2024-2026 NinjaCheetah & Contributors
|
||||||
|
|
||||||
import pathlib
|
import pathlib
|
||||||
from typing import List
|
from typing import List
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
# "modules/download_dsi.py", licensed under the MIT license
|
# "modules/download_dsi.py", licensed under the MIT license
|
||||||
# Copyright 2024-2025 NinjaCheetah
|
# Copyright 2024-2026 NinjaCheetah & Contributors
|
||||||
|
|
||||||
import pathlib
|
import pathlib
|
||||||
|
|
||||||
@@ -76,11 +76,11 @@ def run_nus_download_dsi(out_folder: pathlib.Path, tid: str, version: str, pack_
|
|||||||
content = libTWLPy.download_content(tid, title.tmd.content_record.content_id)
|
content = libTWLPy.download_content(tid, title.tmd.content_record.content_id)
|
||||||
progress_callback.emit(-1, -1, " - Done!")
|
progress_callback.emit(-1, -1, " - Done!")
|
||||||
# If keep encrypted contents is on, write out the content after its downloaded.
|
# If keep encrypted contents is on, write out the content after its downloaded.
|
||||||
if keep_enc_chkbox is True:
|
if keep_enc_chkbox:
|
||||||
version_dir.joinpath(content_file_name).write_bytes(content)
|
version_dir.joinpath(content_file_name).write_bytes(content)
|
||||||
title.content.content = content
|
title.content.content = content
|
||||||
# If decrypt local contents is still true, decrypt the content and write out the decrypted file.
|
# If decrypt local contents is still true, decrypt the content and write out the decrypted file.
|
||||||
if decrypt_contents_enabled is True:
|
if decrypt_contents_enabled:
|
||||||
try:
|
try:
|
||||||
progress_callback.emit(-1, -1, f" - Decrypting content (Content ID: {title.tmd.content_record.content_id})...")
|
progress_callback.emit(-1, -1, f" - Decrypting content (Content ID: {title.tmd.content_record.content_id})...")
|
||||||
dec_content = title.get_content()
|
dec_content = title.get_content()
|
||||||
@@ -91,7 +91,7 @@ def run_nus_download_dsi(out_folder: pathlib.Path, tid: str, version: str, pack_
|
|||||||
# local encrypted contents that have been altered at present.
|
# local encrypted contents that have been altered at present.
|
||||||
return -3
|
return -3
|
||||||
# If pack TAD is still true, pack the TMD, ticket, and content into a TAD.
|
# If pack TAD is still true, pack the TMD, ticket, and content into a TAD.
|
||||||
if pack_tad_enabled is True:
|
if pack_tad_enabled:
|
||||||
# Get the TAD certificate chain, courtesy of libTWLPy.
|
# Get the TAD certificate chain, courtesy of libTWLPy.
|
||||||
progress_callback.emit(-1, -1, " - Building certificate...")
|
progress_callback.emit(-1, -1, " - Building certificate...")
|
||||||
title.tad.set_cert_data(libTWLPy.download_cert())
|
title.tad.set_cert_data(libTWLPy.download_cert())
|
||||||
|
|||||||
@@ -1,11 +1,9 @@
|
|||||||
# "modules/download_wii.py", licensed under the MIT license
|
# "modules/download_wii.py", licensed under the MIT license
|
||||||
# Copyright 2024-2025 NinjaCheetah & Contributors
|
# Copyright 2024-2026 NinjaCheetah & Contributors
|
||||||
|
|
||||||
import pathlib
|
import pathlib
|
||||||
from typing import List, Tuple
|
|
||||||
from .tkey import find_tkey
|
|
||||||
import libWiiPy
|
import libWiiPy
|
||||||
from libWiiPy.title.ticket import _TitleLimit
|
|
||||||
|
|
||||||
|
|
||||||
def run_nus_download_wii(out_folder: pathlib.Path, tid: str, version: str, pack_wad_chkbox: bool, keep_enc_chkbox: bool,
|
def run_nus_download_wii(out_folder: pathlib.Path, tid: str, version: str, pack_wad_chkbox: bool, keep_enc_chkbox: bool,
|
||||||
@@ -55,7 +53,6 @@ def run_nus_download_wii(out_folder: pathlib.Path, tid: str, version: str, pack_
|
|||||||
# Write out the TMD to a file.
|
# Write out the TMD to a file.
|
||||||
version_dir.joinpath(f"tmd.{title_version}").write_bytes(title.tmd.dump())
|
version_dir.joinpath(f"tmd.{title_version}").write_bytes(title.tmd.dump())
|
||||||
# Use a local ticket, if one exists and "use local files" is enabled.
|
# Use a local ticket, if one exists and "use local files" is enabled.
|
||||||
forge_ticket = False
|
|
||||||
if use_local_chkbox and version_dir.joinpath("tik").exists():
|
if use_local_chkbox and version_dir.joinpath("tik").exists():
|
||||||
progress_callback.emit(-1, -1, " - Parsing local copy of Ticket...")
|
progress_callback.emit(-1, -1, " - Parsing local copy of Ticket...")
|
||||||
title.load_ticket(version_dir.joinpath("tik").read_bytes())
|
title.load_ticket(version_dir.joinpath("tik").read_bytes())
|
||||||
@@ -65,10 +62,11 @@ def run_nus_download_wii(out_folder: pathlib.Path, tid: str, version: str, pack_
|
|||||||
title.load_ticket(libWiiPy.title.download_ticket(tid, wiiu_endpoint=wiiu_nus_enabled, progress=progress_update))
|
title.load_ticket(libWiiPy.title.download_ticket(tid, wiiu_endpoint=wiiu_nus_enabled, progress=progress_update))
|
||||||
version_dir.joinpath("tik").write_bytes(title.ticket.dump())
|
version_dir.joinpath("tik").write_bytes(title.ticket.dump())
|
||||||
except ValueError:
|
except ValueError:
|
||||||
# If libWiiPy returns an error, then no ticket is available. Try to forge a ticket after we download the
|
# If libWiiPy returns an error, then no ticket is available. Log this, and disable options requiring a
|
||||||
# content.
|
# ticket so that they aren't attempted later.
|
||||||
progress_callback.emit(0, 0, " - No Ticket is available! Will try forging a Ticket.")
|
progress_callback.emit(0, 0, " - No Ticket is available!")
|
||||||
forge_ticket = True
|
pack_wad_enabled = False
|
||||||
|
decrypt_contents_enabled = False
|
||||||
# Load the content records from the TMD, and begin iterating over the records.
|
# Load the content records from the TMD, and begin iterating over the records.
|
||||||
title.load_content_records()
|
title.load_content_records()
|
||||||
content_list = []
|
content_list = []
|
||||||
@@ -87,44 +85,11 @@ def run_nus_download_wii(out_folder: pathlib.Path, tid: str, version: str, pack_
|
|||||||
wiiu_endpoint=wiiu_nus_enabled, progress=progress_update))
|
wiiu_endpoint=wiiu_nus_enabled, progress=progress_update))
|
||||||
progress_callback.emit(-1, -1, " - Done!")
|
progress_callback.emit(-1, -1, " - Done!")
|
||||||
# If keep encrypted contents is on, write out each content after its downloaded.
|
# If keep encrypted contents is on, write out each content after its downloaded.
|
||||||
if keep_enc_chkbox is True:
|
if keep_enc_chkbox:
|
||||||
version_dir.joinpath(content_file_name).write_bytes(content_list[content])
|
version_dir.joinpath(content_file_name).write_bytes(content_list[content])
|
||||||
title.content.content_list = content_list
|
title.content.content_list = content_list
|
||||||
# Try to forge a Ticket, if a common one wasn't available.
|
|
||||||
if forge_ticket is True:
|
|
||||||
progress_callback.emit(0, 0, " - Attempting to forge Ticket...")
|
|
||||||
try:
|
|
||||||
title_key = find_tkey(tid, title.content.content_list[0], title.tmd.content_records[0])
|
|
||||||
title_key_enc = libWiiPy.title.encrypt_title_key(title_key, 0, tid)
|
|
||||||
ticket = libWiiPy.title.Ticket()
|
|
||||||
ticket.common_key_index = 0
|
|
||||||
ticket.console_id = 0
|
|
||||||
ticket.content_access_permissions = b'\xff' * 64
|
|
||||||
ticket.ecdh_data = b'\x00' * 60
|
|
||||||
ticket.permit_mask = b'\x00' * 4
|
|
||||||
ticket.permitted_titles = b'\x00' * 4
|
|
||||||
ticket.signature = b'\x00' * 256
|
|
||||||
ticket.signature_issuer = "Root-CA00000001-XS00000003" + ("\x00" * 38)
|
|
||||||
ticket.signature_type = b'\x00\x01' * 2
|
|
||||||
ticket.ticket_id = b'\x00' * 8
|
|
||||||
ticket.ticket_version = 0
|
|
||||||
ticket.title_export_allowed = 0
|
|
||||||
ticket.title_id = tid.encode()
|
|
||||||
ticket.title_key_enc = title_key_enc
|
|
||||||
ticket.title_limits_list = [_TitleLimit(0, 0) for _ in range(0, 8)]
|
|
||||||
ticket.title_version = 0
|
|
||||||
ticket.unknown1 = b'\xff' * 2
|
|
||||||
ticket.unknown2 = b'\x00' * 48
|
|
||||||
ticket.fakesign()
|
|
||||||
title.ticket = ticket
|
|
||||||
version_dir.joinpath("tik").write_bytes(title.ticket.dump())
|
|
||||||
progress_callback.emit(-1, -1, " - Successfully forged Ticket!")
|
|
||||||
except Exception:
|
|
||||||
progress_callback.emit(-1, -1, " - Ticket could not be forged!")
|
|
||||||
pack_wad_enabled = False
|
|
||||||
decrypt_contents_enabled = False
|
|
||||||
# If decrypt local contents is still true, decrypt each content and write out the decrypted file.
|
# If decrypt local contents is still true, decrypt each content and write out the decrypted file.
|
||||||
if decrypt_contents_enabled is True:
|
if decrypt_contents_enabled:
|
||||||
try:
|
try:
|
||||||
for content in range(len(title.tmd.content_records)):
|
for content in range(len(title.tmd.content_records)):
|
||||||
progress_callback.emit(-1, -1, f" - Decrypting content {content + 1} of {len(title.tmd.content_records)} "
|
progress_callback.emit(-1, -1, f" - Decrypting content {content + 1} of {len(title.tmd.content_records)} "
|
||||||
@@ -137,7 +102,7 @@ def run_nus_download_wii(out_folder: pathlib.Path, tid: str, version: str, pack_
|
|||||||
# local encrypted contents that have been altered at present.
|
# local encrypted contents that have been altered at present.
|
||||||
return -3
|
return -3
|
||||||
# If pack WAD is still true, pack the TMD, ticket, and contents all into a WAD.
|
# If pack WAD is still true, pack the TMD, ticket, and contents all into a WAD.
|
||||||
if pack_wad_enabled is True:
|
if pack_wad_enabled:
|
||||||
# If the option to pack for vWii mode instead of Wii U mode is enabled, then the Title Key needs to be
|
# If the option to pack for vWii mode instead of Wii U mode is enabled, then the Title Key needs to be
|
||||||
# re-encrypted with the common key instead of the vWii key, so that the title can be installed from within
|
# re-encrypted with the common key instead of the vWii key, so that the title can be installed from within
|
||||||
# vWii mode. (vWii mode does not have access to the vWii key, only Wii U mode has that.)
|
# vWii mode. (vWii mode does not have access to the vWii key, only Wii U mode has that.)
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
# "modules/language.py", licensed under the MIT license
|
# "modules/language.py", licensed under the MIT license
|
||||||
# Copyright 2024-2025 NinjaCheetah & Contributors
|
# Copyright 2024-2026 NinjaCheetah & Contributors
|
||||||
|
|
||||||
from modules.config import update_setting
|
from modules.config import update_setting
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
# "modules/theme.py", licensed under the MIT license
|
# "modules/theme.py", licensed under the MIT license
|
||||||
# Copyright 2024-2025 NinjaCheetah & Contributors
|
# Copyright 2024-2026 NinjaCheetah & Contributors
|
||||||
|
|
||||||
import os
|
import os
|
||||||
import platform
|
import platform
|
||||||
|
|||||||
@@ -1,53 +0,0 @@
|
|||||||
# "tkey-gen.py", licensed under the MIT license
|
|
||||||
# Copyright 2024 NinjaCheetah
|
|
||||||
|
|
||||||
import binascii
|
|
||||||
import hashlib
|
|
||||||
import libWiiPy
|
|
||||||
from libWiiPy.title.types import ContentRecord
|
|
||||||
|
|
||||||
|
|
||||||
def _secret(start, length):
|
|
||||||
ret = b''
|
|
||||||
add = start + length
|
|
||||||
for _ in range(length):
|
|
||||||
unsigned_start = start & 0xFF # Compensates for how Python handles negative values vs PHP.
|
|
||||||
ret += bytes.fromhex(f"{unsigned_start:02x}"[-2:])
|
|
||||||
nxt = start + add
|
|
||||||
add = start
|
|
||||||
start = nxt
|
|
||||||
return ret
|
|
||||||
|
|
||||||
|
|
||||||
def _mungetid(tid):
|
|
||||||
# Remove leading zeroes from the TID.
|
|
||||||
while tid.startswith("00"):
|
|
||||||
tid = tid[2:]
|
|
||||||
if tid == "":
|
|
||||||
tid = "00"
|
|
||||||
# In PHP, the last character just gets dropped if you make a hex string from an odd-length input, so this
|
|
||||||
# replicates that functionality.
|
|
||||||
if len(tid) % 2 != 0:
|
|
||||||
tid = tid[:-1]
|
|
||||||
return bytes.fromhex(tid)
|
|
||||||
|
|
||||||
|
|
||||||
def _derive_key(tid, passwd):
|
|
||||||
key_secret = _secret(-3, 10)
|
|
||||||
salt = hashlib.md5(key_secret + _mungetid(tid)).digest()
|
|
||||||
# Had to reduce the length here from 32 to 16 when converting to get the same length keys.
|
|
||||||
return hashlib.pbkdf2_hmac("sha1", passwd.encode(), salt, 20, 16).hex()
|
|
||||||
|
|
||||||
|
|
||||||
def find_tkey(tid: str, banner_enc: bytes, content_record: ContentRecord) -> bytes:
|
|
||||||
# Find a working Title Key by generating a key with a password, then decrypting content 0 and comparing it to the
|
|
||||||
# expected hash. If the hash matches, then we generated the correct key.
|
|
||||||
passwds = ["nintendo", "mypass"]
|
|
||||||
for passwd in passwds:
|
|
||||||
key = binascii.unhexlify(_derive_key(tid, passwd).encode())
|
|
||||||
banner_dec = libWiiPy.title.decrypt_content(banner_enc, key, content_record.index, content_record.content_size)
|
|
||||||
banner_dec_hash = hashlib.sha1(banner_dec).hexdigest()
|
|
||||||
content_record_hash = content_record.content_hash.decode()
|
|
||||||
if banner_dec_hash == content_record_hash:
|
|
||||||
return key
|
|
||||||
raise Exception("Valid Title Key could not be generated")
|
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
# "modules/tree.py", licensed under the MIT license
|
# "modules/tree.py", licensed under the MIT license
|
||||||
# Copyright 2024-2025 NinjaCheetah
|
# Copyright 2024-2026 NinjaCheetah & Contributors
|
||||||
|
|
||||||
from modules.core import TitleData
|
from modules.core import TitleData
|
||||||
from PySide6.QtCore import QAbstractItemModel, QModelIndex, Qt, QSortFilterProxyModel
|
from PySide6.QtCore import QAbstractItemModel, QModelIndex, Qt, QSortFilterProxyModel
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 41 KiB After Width: | Height: | Size: 40 KiB |
@@ -1,5 +1,5 @@
|
|||||||
# "qt/py/ui_AboutDialog.py", licensed under the MIT license
|
# "qt/py/ui_AboutDialog.py", licensed under the MIT license
|
||||||
# Copyright 2024-2025 NinjaCheetah and Contributors
|
# Copyright 2024-2026 NinjaCheetah and Contributors
|
||||||
# Thanks Isla and Alex for making such a nice about dialog that I could then "borrow" :p
|
# Thanks Isla and Alex for making such a nice about dialog that I could then "borrow" :p
|
||||||
|
|
||||||
import os
|
import os
|
||||||
@@ -46,7 +46,7 @@ class AboutNUSGet(QDialog):
|
|||||||
libraries_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
libraries_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||||
|
|
||||||
# Copyright
|
# Copyright
|
||||||
copyright_label = QLabel(self.tr("© 2024-2025 NinjaCheetah & Contributors"))
|
copyright_label = QLabel(self.tr("© 2024-2026 NinjaCheetah & Contributors"))
|
||||||
copyright_label.setProperty("class", "copyright")
|
copyright_label.setProperty("class", "copyright")
|
||||||
copyright_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
copyright_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||||
|
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 41 KiB After Width: | Height: | Size: 40 KiB |
@@ -47,7 +47,7 @@ QMenuBar::item:selected {
|
|||||||
}
|
}
|
||||||
|
|
||||||
QMenuBar::item:pressed {
|
QMenuBar::item:pressed {
|
||||||
background-color: #6c1ae8;
|
background-color: #1a73e8;
|
||||||
color: white;
|
color: white;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -68,7 +68,7 @@ QMenu::item {
|
|||||||
}
|
}
|
||||||
|
|
||||||
QMenu::item:selected {
|
QMenu::item:selected {
|
||||||
background-color: #6c1ae8;
|
background-color: #1a73e8;
|
||||||
color: white;
|
color: white;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -88,13 +88,13 @@ QRadioButton {
|
|||||||
|
|
||||||
QRadioButton:hover {
|
QRadioButton:hover {
|
||||||
background-color: rgba(60, 60, 60, 1);
|
background-color: rgba(60, 60, 60, 1);
|
||||||
border-color: #9c4ae8;
|
border-color: #4a86e8;
|
||||||
}
|
}
|
||||||
|
|
||||||
QRadioButton:checked {
|
QRadioButton:checked {
|
||||||
background-color: rgba(26, 115, 232, 0.08);
|
background-color: rgba(26, 115, 232, 0.08);
|
||||||
border: 1px solid #6c1ae8;
|
border: 1px solid #1a73e8;
|
||||||
color: #6c1ae8;
|
color: #1a73e8;
|
||||||
}
|
}
|
||||||
|
|
||||||
QRadioButton::indicator {
|
QRadioButton::indicator {
|
||||||
@@ -107,13 +107,13 @@ QRadioButton::indicator {
|
|||||||
}
|
}
|
||||||
|
|
||||||
QRadioButton::indicator:checked {
|
QRadioButton::indicator:checked {
|
||||||
background-color: #6c1ae8;
|
background-color: #1a73e8;
|
||||||
border: 1px solid #6c1ae8;
|
border: 1px solid #1a73e8;
|
||||||
image: url("{IMAGE_PREFIX}/rounded_square.svg");
|
image: url("{IMAGE_PREFIX}/rounded_square.svg");
|
||||||
}
|
}
|
||||||
|
|
||||||
QRadioButton::indicator:hover {
|
QRadioButton::indicator:hover {
|
||||||
border-color: #6c1ae8;
|
border-color: #1a73e8;
|
||||||
}
|
}
|
||||||
|
|
||||||
QLineEdit {
|
QLineEdit {
|
||||||
@@ -124,11 +124,11 @@ QLineEdit {
|
|||||||
margin: 4px 0px;
|
margin: 4px 0px;
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
color: #ffffff;
|
color: #ffffff;
|
||||||
selection-background-color: #6c1ae8;
|
selection-background-color: #1a73e8;
|
||||||
}
|
}
|
||||||
|
|
||||||
QLineEdit:focus {
|
QLineEdit:focus {
|
||||||
border-color: #6c1ae8;
|
border-color: #1a73e8;
|
||||||
}
|
}
|
||||||
|
|
||||||
QLineEdit:disabled {
|
QLineEdit:disabled {
|
||||||
@@ -187,11 +187,11 @@ QTreeView::item:hover {
|
|||||||
}
|
}
|
||||||
|
|
||||||
QTreeView::item:focus {
|
QTreeView::item:focus {
|
||||||
background-color: rgba(64, 26, 232, 0.15);
|
background-color: rgba(26, 115, 232, 0.08);
|
||||||
}
|
}
|
||||||
|
|
||||||
QTreeView::item:selected {
|
QTreeView::item:selected {
|
||||||
background-color: #6c1ae8;
|
background-color: #1a73e8;
|
||||||
}
|
}
|
||||||
|
|
||||||
QTreeView QScrollBar:vertical {
|
QTreeView QScrollBar:vertical {
|
||||||
@@ -211,7 +211,7 @@ QTreeView::branch:open:has-children:has-siblings {
|
|||||||
QTextBrowser {
|
QTextBrowser {
|
||||||
color: white;
|
color: white;
|
||||||
background-color: #1a1a1a;
|
background-color: #1a1a1a;
|
||||||
selection-background-color: #6c1ae8;
|
selection-background-color: #1a73e8;
|
||||||
}
|
}
|
||||||
|
|
||||||
QPushButton {
|
QPushButton {
|
||||||
@@ -229,17 +229,17 @@ QPushButton {
|
|||||||
|
|
||||||
QPushButton:hover {
|
QPushButton:hover {
|
||||||
background-color: rgba(60, 60, 60, 1);
|
background-color: rgba(60, 60, 60, 1);
|
||||||
border-color: #9c4ae8;
|
border-color: #4a86e8;
|
||||||
}
|
}
|
||||||
|
|
||||||
QPushButton:focus {
|
QPushButton:focus {
|
||||||
background-color: rgba(60, 60, 60, 1);
|
background-color: rgba(60, 60, 60, 1);
|
||||||
border-color: #9c4ae8;
|
border-color: #4a86e8;
|
||||||
}
|
}
|
||||||
|
|
||||||
QPushButton:pressed {
|
QPushButton:pressed {
|
||||||
background-color: rgba(64, 26, 232, 0.15);
|
background-color: rgba(26, 115, 232, 0.15);
|
||||||
border: 1px solid #6c1ae8;
|
border: 1px solid #1a73e8;
|
||||||
}
|
}
|
||||||
|
|
||||||
QPushButton:disabled {
|
QPushButton:disabled {
|
||||||
@@ -261,18 +261,18 @@ QComboBox {
|
|||||||
}
|
}
|
||||||
|
|
||||||
QComboBox:on {
|
QComboBox:on {
|
||||||
background-color: rgba(64, 26, 232, 0.15);
|
background-color: rgba(26, 115, 232, 0.15);
|
||||||
border: 1px solid #6c1ae8;
|
border: 1px solid #1a73e8;
|
||||||
}
|
}
|
||||||
|
|
||||||
QComboBox:hover {
|
QComboBox:hover {
|
||||||
background-color: rgba(60, 60, 60, 1);
|
background-color: rgba(60, 60, 60, 1);
|
||||||
border-color: #9c4ae8;
|
border-color: #4a86e8;
|
||||||
}
|
}
|
||||||
|
|
||||||
QComboBox:focus {
|
QComboBox:focus {
|
||||||
background-color: rgba(60, 60, 60, 1);
|
background-color: rgba(60, 60, 60, 1);
|
||||||
border-color: #9c4ae8;
|
border-color: #4a86e8;
|
||||||
}
|
}
|
||||||
|
|
||||||
QComboBox::drop-down {
|
QComboBox::drop-down {
|
||||||
@@ -301,7 +301,7 @@ QComboBox QAbstractItemView::item {
|
|||||||
}
|
}
|
||||||
|
|
||||||
QComboBox QAbstractItemView::item:hover {
|
QComboBox QAbstractItemView::item:hover {
|
||||||
background-color: #6c1ae8;
|
background-color: #1a73e8;
|
||||||
}
|
}
|
||||||
|
|
||||||
QScrollBar:vertical {
|
QScrollBar:vertical {
|
||||||
@@ -320,7 +320,7 @@ QScrollBar::handle:vertical {
|
|||||||
}
|
}
|
||||||
|
|
||||||
QScrollBar::handle:vertical:hover {
|
QScrollBar::handle:vertical:hover {
|
||||||
background-color: rgba(71, 26, 232, 0.4);
|
background-color: rgba(26, 115, 232, 0.4);
|
||||||
}
|
}
|
||||||
|
|
||||||
QScrollBar::add-line:vertical {
|
QScrollBar::add-line:vertical {
|
||||||
@@ -350,7 +350,7 @@ QScrollBar::handle:horizontal {
|
|||||||
}
|
}
|
||||||
|
|
||||||
QScrollBar::handle:horizontal:hover {
|
QScrollBar::handle:horizontal:hover {
|
||||||
background-color: rgba(71, 26, 232, 0.4);
|
background-color: rgba(26, 115, 232, 0.4);
|
||||||
}
|
}
|
||||||
|
|
||||||
QScrollBar::add-line:horizontal {
|
QScrollBar::add-line:horizontal {
|
||||||
@@ -381,7 +381,7 @@ QProgressBar {
|
|||||||
QProgressBar::chunk {
|
QProgressBar::chunk {
|
||||||
background-color: qlineargradient(
|
background-color: qlineargradient(
|
||||||
x1: 0, y1: 0, x2: 1, y2: 0,
|
x1: 0, y1: 0, x2: 1, y2: 0,
|
||||||
stop: 0 #6c1ae8, stop: 1 #8941ec
|
stop: 0 #1a73e8, stop: 1 #5596f4
|
||||||
);
|
);
|
||||||
border-radius: 5px;
|
border-radius: 5px;
|
||||||
margin: 0.5px;
|
margin: 0.5px;
|
||||||
@@ -401,7 +401,7 @@ WrapCheckboxWidget {
|
|||||||
|
|
||||||
WrapCheckboxWidget:hover {
|
WrapCheckboxWidget:hover {
|
||||||
background-color: rgba(60, 60, 60, 1);
|
background-color: rgba(60, 60, 60, 1);
|
||||||
border-color: #9c4ae8;
|
border-color: #4a86e8;
|
||||||
}
|
}
|
||||||
|
|
||||||
WrapCheckboxWidget:disabled {
|
WrapCheckboxWidget:disabled {
|
||||||
@@ -421,20 +421,16 @@ WrapCheckboxWidget QCheckBox::indicator {
|
|||||||
border: 1px solid #5f6368;
|
border: 1px solid #5f6368;
|
||||||
}
|
}
|
||||||
|
|
||||||
WrapCheckboxWidget QCheckBox::indicator::focus {
|
|
||||||
background-color: rgba(64, 26, 232, 0.15);
|
|
||||||
}
|
|
||||||
|
|
||||||
WrapCheckboxWidget QCheckBox::indicator:checked {
|
WrapCheckboxWidget QCheckBox::indicator:checked {
|
||||||
background-color: #6c1ae8;
|
background-color: #1a73e8;
|
||||||
border: 1px solid #6c1ae8;
|
border: 1px solid #1a73e8;
|
||||||
image: url("{IMAGE_PREFIX}/check.svg");
|
image: url("{IMAGE_PREFIX}/check.svg");
|
||||||
}
|
}
|
||||||
|
|
||||||
WrapCheckboxWidget QCheckBox::indicator:hover {
|
WrapCheckboxWidget QCheckBox::indicator:hover {
|
||||||
border-color: #6c1ae8;
|
border-color: #1a73e8;
|
||||||
}
|
}
|
||||||
|
|
||||||
WrapCheckboxWidget QCheckBox:checked {
|
WrapCheckboxWidget QCheckBox:checked {
|
||||||
color: #6c1ae8;
|
color: #1a73e8;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
# "update_translations.py", licensed under the MIT license
|
# "update_translations.py", licensed under the MIT license
|
||||||
# Copyright 2024-2025 NinjaCheetah
|
# Copyright 2024-2026 NinjaCheetah & Contributors
|
||||||
# This script exists to work around an issue in PySide6 where the "pyside6-project lupdate" command doesn't work as
|
# This script exists to work around an issue in PySide6 where the "pyside6-project lupdate" command doesn't work as
|
||||||
# expected, as it struggles to parse the paths in the .pyproject file. This does what it's meant to do for it.
|
# expected, as it struggles to parse the paths in the .pyproject file. This does what it's meant to do for it.
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user