Merge changes from upstream

This commit is contained in:
Campbell 2024-12-18 16:43:35 -05:00
commit 398654609b
Signed by: NinjaCheetah
GPG Key ID: 670C282B3291D63D
7 changed files with 134 additions and 104 deletions

129
NUSGet.py
View File

@ -34,8 +34,9 @@ from qt.py.ui_MainMenu import Ui_MainWindow
from modules.core import * from modules.core import *
from modules.tree import NUSGetTreeModel from modules.tree import NUSGetTreeModel
from modules.download_wii import run_nus_download_wii, run_nus_download_wii_batch from modules.download_batch import run_nus_download_batch
from modules.download_dsi import run_nus_download_dsi, run_nus_download_dsi_batch from modules.download_wii import run_nus_download_wii
from modules.download_dsi import run_nus_download_dsi
nusget_version = "1.3.0" nusget_version = "1.3.0"
@ -333,86 +334,74 @@ class MainWindow(QMainWindow, Ui_MainWindow):
msg_box.setStandardButtons(QMessageBox.StandardButton.Ok) msg_box.setStandardButtons(QMessageBox.StandardButton.Ok)
msg_box.setDefaultButton(QMessageBox.StandardButton.Ok) msg_box.setDefaultButton(QMessageBox.StandardButton.Ok)
msg_box.setWindowTitle(app.translate("MainWindow", "Script Download Failed")) msg_box.setWindowTitle(app.translate("MainWindow", "Script Download Failed"))
file_name = QFileDialog.getOpenFileName(self, caption=app.translate("MainWindow", "Open NUS script"), file_name = QFileDialog.getOpenFileName(self, caption=app.translate("MainWindow", "Open NUS Script"),
filter=app.translate("MainWindow", "NUS Scripts (*.nus *.txt)"), filter=app.translate("MainWindow", "NUS Scripts (*.nus *.json)"),
options=QFileDialog.Option.ReadOnly) options=QFileDialog.Option.ReadOnly)
# The old plaintext script format is no longer supported in NUSGet v1.3.0 and later. This script parsing code
# is for the new JSON script format, which is much easier to use and is cleaner.
if len(file_name[0]) == 0: if len(file_name[0]) == 0:
return return
try: try:
content = open(file_name[0], "r").readlines() with open(file_name[0]) as script_file:
except os.error: script_data = json.load(script_file)
msg_box.setText(app.translate("MainWindow", "The script could not be opened.")) except json.JSONDecodeError as e:
msg_box.setText(app.translate("MainWindow", "An error occurred while parsing the script file!"))
msg_box.setInformativeText(app.translate("MainWindow", f"Error encountered at line {e.lineno}, column {e.colno}. Please double-check the script and try again."))
msg_box.exec() msg_box.exec()
return return
# Build a list of the titles we need to download.
# NUS Scripts are plaintext UTF-8 files that list a title per line, terminated with newlines.
# Every title is its u64 TID, a space and its u16 version, *both* written in hexadecimal.
# NUS itself expects versions as decimal notation, so they need to be decoded first, but TIDs are always written
# in hexadecimal notation.
titles = [] titles = []
for index, title in enumerate(content): for title in script_data:
decoded = title.replace("\n", "").split(" ", 1)
if len(decoded[0]) != 16:
msg_box.setText(app.translate("MainWindow", "The TID for title #%n is not valid.", "", index + 1))
msg_box.exec()
return
elif len(decoded[1]) != 4:
msg_box.setText(app.translate("MainWindow", "The version for title #%n is not valid.", "", index + 1))
msg_box.exec()
return
tid = decoded[0]
try: try:
target_version = int(decoded[1], 16) tid = title["Title ID"]
except ValueError: except KeyError:
msg_box.setText(app.translate("MainWindow", "The version for title #%n is not valid.", "", index + 1)) msg_box.setText(app.translate("MainWindow", "An error occurred while parsing Title IDs!"))
msg_box.setInformativeText(app.translate("MainWindow", f"The title at index {script_data.index(title)} does not have a Title ID!"))
msg_box.exec() msg_box.exec()
return return
# No version key is acceptable, just treat it as latest.
title = None try:
for category in self.trees[self.ui.platform_tabs.currentIndex()][1]: title_version = int(title["Version"])
for title_ in self.trees[self.ui.platform_tabs.currentIndex()][1][category]: except KeyError:
# The last two digits are either identifying the title type (IOS slot, BC type, etc.) or a region code; in case of the latter, skip the region here to match it title_version = -1
if not ((title_["TID"][-2:] == "XX" and title_["TID"][:-2] == tid[:-2]) or title_["TID"] == tid): # If no console was specified, assume Wii.
continue try:
console = title["Console"]
found_ver = False except KeyError:
for region in title_["Versions"]: console = "Wii"
for db_version in title_["Versions"][region]: # Look up the title, and load the archive name for it if one can be found.
if db_version == target_version: archive_name = ""
found_ver = True if console == "vWii":
target_database = vwii_database
elif console == "DSi":
target_database = dsi_database
else:
target_database = wii_database
for category in target_database:
for t in target_database[category]:
if t["TID"][-2:] == "XX":
for r in regions:
if f"{t['TID'][:-2]}{regions[r][0]}" == tid:
try:
archive_name = t["Archive Name"]
break
except KeyError:
archive_name = ""
break
else:
if t["TID"] == tid:
try:
archive_name = t["Archive Name"]
break break
except KeyError:
if not found_ver: archive_name = ""
msg_box.setText(app.translate("MainWindow", "The version for title #%n could not be discovered in the database.", "", index + 1)) break
msg_box.exec() titles.append(BatchTitleData(tid, title_version, console, archive_name))
return
title = title_
break
if title is None:
msg_box.setText(app.translate("MainWindow", "Title #%n could not be discovered in the database.", "", index + 1))
msg_box.exec()
return
titles.append((title["TID"], str(target_version), title["Archive Name"]))
self.lock_ui_for_download() self.lock_ui_for_download()
worker = Worker(run_nus_download_batch, out_folder, titles, self.ui.pack_archive_chkbox.isChecked(),
self.update_log_text(f"Found {len(titles)} titles, starting batch download.") self.ui.keep_enc_chkbox.isChecked(), self.ui.create_dec_chkbox.isChecked(),
self.ui.use_wiiu_nus_chkbox.isChecked(), self.ui.use_local_chkbox.isChecked(),
if self.ui.console_select_dropdown.currentText() == "DSi": self.ui.pack_vwii_mode_chkbox.isChecked(), self.ui.patch_ios_chkbox.isChecked())
worker = Worker(run_nus_download_dsi_batch, out_folder, titles, self.ui.pack_archive_chkbox.isChecked(),
self.ui.keep_enc_chkbox.isChecked(), self.ui.create_dec_chkbox.isChecked(),
self.ui.use_local_chkbox.isChecked(), self.ui.archive_file_entry.text())
else:
worker = Worker(run_nus_download_wii_batch, out_folder, titles, self.ui.pack_archive_chkbox.isChecked(),
self.ui.keep_enc_chkbox.isChecked(), self.ui.create_dec_chkbox.isChecked(),
self.ui.use_wiiu_nus_chkbox.isChecked(), self.ui.use_local_chkbox.isChecked(),
self.ui.pack_vwii_mode_chkbox.isChecked(), self.ui.patch_ios_chkbox.isChecked())
worker.signals.result.connect(self.check_download_result) worker.signals.result.connect(self.check_download_result)
worker.signals.progress.connect(self.update_log_text) worker.signals.progress.connect(self.update_log_text)
self.threadpool.start(worker) self.threadpool.start(worker)

View File

@ -859,5 +859,17 @@
}, },
"Ticket": false "Ticket": false
} }
],
"Virtual Console - NES": [
{
"Name": "Super Mario Bros.",
"TID": "00010001464147XX",
"Versions": {
"USA/NTSC": [2],
"Europe/PAL": [2],
"Japan": [2]
},
"Ticket": false
}
] ]
} }

View File

@ -18,6 +18,15 @@ class TitleData:
danger: str danger: str
@dataclass
class BatchTitleData:
# Class to store all data for a Title in a batch operation.
tid: str
version: int
console: str
archive_name: str
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.
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)

40
modules/download_batch.py Normal file
View File

@ -0,0 +1,40 @@
# "modules/download_batch.py", licensed under the MIT license
# Copyright 2024 NinjaCheetah
import pathlib
from typing import List
from modules.core import BatchTitleData
from modules.download_dsi import run_nus_download_dsi
from modules.download_wii import run_nus_download_wii
def run_nus_download_batch(out_folder: pathlib.Path, titles: List[BatchTitleData], pack_wad_chkbox: bool,
keep_enc_chkbox: bool, decrypt_contents_chkbox: bool, wiiu_nus_chkbox: bool,
use_local_chkbox: bool, repack_vwii_chkbox: bool, patch_ios: bool,
progress_callback=None):
for title in titles:
if title.version == -1:
version_str = "Latest"
else:
version_str = str(title.version)
if title.console == "Wii" or title.console == "vWii":
if title.archive_name != "":
archive_name = f"{title.archive_name}-v{version_str}-{title.console}.wad"
else:
archive_name = f"{title.tid}-v{version_str}-{title.console}.wad"
result = run_nus_download_wii(out_folder, title.tid, version_str, pack_wad_chkbox, keep_enc_chkbox,
decrypt_contents_chkbox, wiiu_nus_chkbox, use_local_chkbox, repack_vwii_chkbox,
patch_ios, archive_name, progress_callback)
if result != 0:
return result
elif title.console == "DSi":
if title.archive_name != "":
archive_name = f"{title.archive_name}-v{version_str}-{title.console}.tad"
else:
archive_name = f"{title.tid}-v{version_str}-{title.console}.tad"
result = run_nus_download_dsi(out_folder, title.tid, version_str, pack_wad_chkbox, keep_enc_chkbox,
decrypt_contents_chkbox, use_local_chkbox, archive_name, progress_callback)
if result != 0:
return result
progress_callback.emit(f"Batch download finished.")
return 0

View File

@ -99,6 +99,8 @@ def run_nus_download_dsi(out_folder: pathlib.Path, tid: str, version: str, pack_
# Use a typed TAD name if there is one, and auto generate one based on the TID and version if there isn't. # Use a typed TAD name if there is one, and auto generate one based on the TID and version if there isn't.
progress_callback.emit("Packing TAD...") progress_callback.emit("Packing TAD...")
if tad_file_name != "" and tad_file_name is not None: if tad_file_name != "" and tad_file_name is not None:
# Batch downloads may insert -vLatest, so if it did we can fill in the real number now.
tad_file_name = tad_file_name.replace("-vLatest", f"-v{title_version}")
if tad_file_name[-4:].lower() != ".tad": if tad_file_name[-4:].lower() != ".tad":
tad_file_name += ".tad" tad_file_name += ".tad"
else: else:
@ -112,15 +114,3 @@ def run_nus_download_dsi(out_folder: pathlib.Path, tid: str, version: str, pack_
if (not pack_tad_enabled and pack_tad_chkbox) or (not decrypt_contents_enabled and decrypt_contents_chkbox): if (not pack_tad_enabled and pack_tad_chkbox) or (not decrypt_contents_enabled and decrypt_contents_chkbox):
return 1 return 1
return 0 return 0
def run_nus_download_dsi_batch(out_folder: pathlib.Path, titles: List[Tuple[str, str, str]], pack_tad_chkbox: bool,
keep_enc_chkbox: bool, decrypt_contents_chkbox: bool, use_local_chkbox: bool,
progress_callback=None):
for title in titles:
result = run_nus_download_dsi(out_folder, title[0], title[1], pack_tad_chkbox, keep_enc_chkbox,
decrypt_contents_chkbox, use_local_chkbox, f"{title[2]}-{title[1]}.tad",
progress_callback)
if result != 0:
return result
progress_callback.emit(f"Batch download finished.")
return 0

View File

@ -150,6 +150,8 @@ def run_nus_download_wii(out_folder: pathlib.Path, tid: str, version: str, pack_
# Use a typed WAD name if there is one, and auto generate one based on the TID and version if there isn't. # 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...") progress_callback.emit(" - Packing WAD...")
if wad_file_name != "" and wad_file_name is not None: if wad_file_name != "" and wad_file_name is not None:
# Batch downloads may insert -vLatest, so if it did we can fill in the real number now.
wad_file_name = wad_file_name.replace("-vLatest", f"-v{title_version}")
if wad_file_name[-4:].lower() != ".wad": if wad_file_name[-4:].lower() != ".wad":
wad_file_name += ".wad" wad_file_name += ".wad"
else: else:
@ -174,16 +176,3 @@ def run_nus_download_wii(out_folder: pathlib.Path, tid: str, version: str, pack_
if (not pack_wad_enabled and pack_wad_chkbox) or (not decrypt_contents_enabled and decrypt_contents_chkbox): if (not pack_wad_enabled and pack_wad_chkbox) or (not decrypt_contents_enabled and decrypt_contents_chkbox):
return 1 return 1
return 0 return 0
def run_nus_download_wii_batch(out_folder: pathlib.Path, titles: List[Tuple[str, str, str]], pack_wad_chkbox: bool,
keep_enc_chkbox: bool, decrypt_contents_chkbox: bool, wiiu_nus_chkbox: bool,
use_local_chkbox: bool, repack_vwii_chkbox: bool, patch_ios: bool,
progress_callback=None):
for title in titles:
result = run_nus_download_wii(out_folder, title[0], title[1], pack_wad_chkbox, keep_enc_chkbox,
decrypt_contents_chkbox, wiiu_nus_chkbox, use_local_chkbox, repack_vwii_chkbox,
patch_ios, f"{title[2]}-{title[1]}.wad", progress_callback)
if result != 0:
return result
progress_callback.emit(f"Batch download finished.")
return 0

View File

@ -11,7 +11,7 @@ class TreeItem:
self.data = data self.data = data
self.parent = parent self.parent = parent
self.children = [] self.children = []
self.metadata = metadata # Store hidden metadata self.metadata = metadata
def add_child(self, item): def add_child(self, item):
self.children.append(item) self.children.append(item)
@ -53,14 +53,16 @@ class NUSGetTreeModel(QAbstractItemModel):
name = entry.get("Name") name = entry.get("Name")
versions = entry.get("Versions", {}) versions = entry.get("Versions", {})
if tid: if tid:
tid_item = TreeItem([f"{tid} - {name}", ""], key_item) tid_item = TreeItem([f"{tid} - {name}", ""], key_item, entry.get("Ticket"))
key_item.add_child(tid_item) key_item.add_child(tid_item)
for region, version_list in versions.items(): for region, version_list in versions.items():
region_item = TreeItem([region, ""], tid_item) region_item = TreeItem([region, ""], tid_item)
tid_item.add_child(region_item) tid_item.add_child(region_item)
for version in version_list: for version in version_list:
danger = entry.get("Danger") if entry.get("Danger") is not None else "" danger = entry.get("Danger") if entry.get("Danger") is not None else ""
metadata = TitleData(entry.get("TID"), entry.get("Name"), entry.get("Archive Name"), archive_name = (entry.get("Archive Name") if entry.get("Archive Name") is not None
else entry.get("Name").replace(" ", "-"))
metadata = TitleData(entry.get("TID"), entry.get("Name"), archive_name,
version, entry.get("Ticket"), region, key, danger) version, entry.get("Ticket"), region, key, danger)
public_versions = entry.get("Public Versions") public_versions = entry.get("Public Versions")
if public_versions is not None: if public_versions is not None:
@ -96,12 +98,11 @@ class NUSGetTreeModel(QAbstractItemModel):
if role == Qt.DecorationRole and index.column() == 0: if role == Qt.DecorationRole and index.column() == 0:
# Check for icons based on the "Ticket" metadata only at the TID level # Check for icons based on the "Ticket" metadata only at the TID level
if item.parent and item.parent.data_at(0) == "System": if item.metadata is not None and isinstance(item.metadata, bool):
if item.metadata and "Ticket" in item.metadata: if item.metadata is True:
if item.metadata["Ticket"]: return QIcon.fromTheme("dialog-ok")
return QIcon.fromTheme("dialog-ok") # Checkmark icon else:
else: return QIcon.fromTheme("dialog-cancel")
return QIcon.fromTheme("dialog-cancel") # X icon
return None return None
def headerData(self, section, orientation, role=Qt.DisplayRole): def headerData(self, section, orientation, role=Qt.DisplayRole):