forked from NinjaCheetah/NUSGet
		
	Merge changes from upstream
This commit is contained in:
		
						commit
						398654609b
					
				
							
								
								
									
										129
									
								
								NUSGet.py
									
									
									
									
									
								
							
							
						
						
									
										129
									
								
								NUSGet.py
									
									
									
									
									
								
							| @ -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) | ||||||
|  | |||||||
| @ -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 | ||||||
|  |     } | ||||||
|   ] |   ] | ||||||
| } | } | ||||||
| @ -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
									
								
							
							
						
						
									
										40
									
								
								modules/download_batch.py
									
									
									
									
									
										Normal 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 | ||||||
| @ -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 |  | ||||||
|  | |||||||
| @ -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 |  | ||||||
|  | |||||||
| @ -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): | ||||||
|  | |||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user