forked from NinjaCheetah/NUSGet
		
	Merge changes from upstream
This commit is contained in:
		
						commit
						398654609b
					
				
							
								
								
									
										125
									
								
								NUSGet.py
									
									
									
									
									
								
							
							
						
						
									
										125
									
								
								NUSGet.py
									
									
									
									
									
								
							| @ -34,8 +34,9 @@ from qt.py.ui_MainMenu import Ui_MainWindow | ||||
| 
 | ||||
| from modules.core import * | ||||
| from modules.tree import NUSGetTreeModel | ||||
| from modules.download_wii import run_nus_download_wii, run_nus_download_wii_batch | ||||
| from modules.download_dsi import run_nus_download_dsi, run_nus_download_dsi_batch | ||||
| from modules.download_batch import run_nus_download_batch | ||||
| from modules.download_wii import run_nus_download_wii | ||||
| from modules.download_dsi import run_nus_download_dsi | ||||
| 
 | ||||
| nusget_version = "1.3.0" | ||||
| 
 | ||||
| @ -333,86 +334,74 @@ class MainWindow(QMainWindow, Ui_MainWindow): | ||||
|         msg_box.setStandardButtons(QMessageBox.StandardButton.Ok) | ||||
|         msg_box.setDefaultButton(QMessageBox.StandardButton.Ok) | ||||
|         msg_box.setWindowTitle(app.translate("MainWindow", "Script Download Failed")) | ||||
|         file_name = QFileDialog.getOpenFileName(self, caption=app.translate("MainWindow", "Open NUS script"), | ||||
|                                                 filter=app.translate("MainWindow", "NUS Scripts (*.nus *.txt)"), | ||||
|         file_name = QFileDialog.getOpenFileName(self, caption=app.translate("MainWindow", "Open NUS Script"), | ||||
|                                                 filter=app.translate("MainWindow", "NUS Scripts (*.nus *.json)"), | ||||
|                                                 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: | ||||
|             return | ||||
|         try: | ||||
|             content = open(file_name[0], "r").readlines() | ||||
|         except os.error: | ||||
|             msg_box.setText(app.translate("MainWindow", "The script could not be opened.")) | ||||
|             with open(file_name[0]) as script_file: | ||||
|                 script_data = json.load(script_file) | ||||
|         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() | ||||
|             return | ||||
| 
 | ||||
|         # 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. | ||||
|         # Build a list of the titles we need to download. | ||||
|         titles = [] | ||||
|         for index, title in enumerate(content): | ||||
|             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] | ||||
| 
 | ||||
|         for title in script_data: | ||||
|             try: | ||||
|                 target_version = int(decoded[1], 16) | ||||
|             except ValueError: | ||||
|                 msg_box.setText(app.translate("MainWindow", "The version for title #%n is not valid.", "", index + 1)) | ||||
|                 tid = title["Title ID"] | ||||
|             except KeyError: | ||||
|                 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() | ||||
|                 return | ||||
| 
 | ||||
|             title = None | ||||
|             for category in self.trees[self.ui.platform_tabs.currentIndex()][1]: | ||||
|                 for title_ in self.trees[self.ui.platform_tabs.currentIndex()][1][category]: | ||||
|                     # 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 | ||||
|                     if not ((title_["TID"][-2:] == "XX" and title_["TID"][:-2] == tid[:-2]) or title_["TID"] == tid): | ||||
|                         continue | ||||
| 
 | ||||
|                     found_ver = False | ||||
|                     for region in title_["Versions"]: | ||||
|                         for db_version in title_["Versions"][region]: | ||||
|                             if db_version == target_version: | ||||
|                                 found_ver = True | ||||
|                                 break | ||||
| 
 | ||||
|                     if not found_ver: | ||||
|                         msg_box.setText(app.translate("MainWindow", "The version for title #%n could not be discovered in the database.", "", index + 1)) | ||||
|                         msg_box.exec() | ||||
|                         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.update_log_text(f"Found {len(titles)} titles, starting batch download.") | ||||
| 
 | ||||
|         if self.ui.console_select_dropdown.currentText() == "DSi": | ||||
|             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()) | ||||
|             # No version key is acceptable, just treat it as latest. | ||||
|             try: | ||||
|                 title_version = int(title["Version"]) | ||||
|             except KeyError: | ||||
|                 title_version = -1 | ||||
|             # If no console was specified, assume Wii. | ||||
|             try: | ||||
|                 console = title["Console"] | ||||
|             except KeyError: | ||||
|                 console = "Wii" | ||||
|             # Look up the title, and load the archive name for it if one can be found. | ||||
|             archive_name = "" | ||||
|             if console == "vWii": | ||||
|                 target_database = vwii_database | ||||
|             elif console == "DSi": | ||||
|                 target_database = dsi_database | ||||
|             else: | ||||
|             worker = Worker(run_nus_download_wii_batch, out_folder, titles, self.ui.pack_archive_chkbox.isChecked(), | ||||
|                 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 | ||||
|                             except KeyError: | ||||
|                                 archive_name = "" | ||||
|                                 break | ||||
|             titles.append(BatchTitleData(tid, title_version, console, archive_name)) | ||||
|         self.lock_ui_for_download() | ||||
|         worker = Worker(run_nus_download_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.progress.connect(self.update_log_text) | ||||
|         self.threadpool.start(worker) | ||||
|  | ||||
| @ -859,5 +859,17 @@ | ||||
|       }, | ||||
|       "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 | ||||
| 
 | ||||
| 
 | ||||
| @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: | ||||
|     # 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) | ||||
|  | ||||
							
								
								
									
										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. | ||||
|         progress_callback.emit("Packing TAD...") | ||||
|         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": | ||||
|                 tad_file_name += ".tad" | ||||
|         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): | ||||
|         return 1 | ||||
|     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. | ||||
|         progress_callback.emit(" - Packing WAD...") | ||||
|         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": | ||||
|                 wad_file_name += ".wad" | ||||
|         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): | ||||
|         return 1 | ||||
|     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.parent = parent | ||||
|         self.children = [] | ||||
|         self.metadata = metadata  # Store hidden metadata | ||||
|         self.metadata = metadata | ||||
| 
 | ||||
|     def add_child(self, item): | ||||
|         self.children.append(item) | ||||
| @ -53,14 +53,16 @@ class NUSGetTreeModel(QAbstractItemModel): | ||||
|                         name = entry.get("Name") | ||||
|                         versions = entry.get("Versions", {}) | ||||
|                         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) | ||||
|                             for region, version_list in versions.items(): | ||||
|                                 region_item = TreeItem([region, ""], tid_item) | ||||
|                                 tid_item.add_child(region_item) | ||||
|                                 for version in version_list: | ||||
|                                     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) | ||||
|                                     public_versions = entry.get("Public Versions") | ||||
|                                     if public_versions is not None: | ||||
| @ -96,12 +98,11 @@ class NUSGetTreeModel(QAbstractItemModel): | ||||
| 
 | ||||
|         if role == Qt.DecorationRole and index.column() == 0: | ||||
|             # 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 and "Ticket" in item.metadata: | ||||
|                     if item.metadata["Ticket"]: | ||||
|                         return QIcon.fromTheme("dialog-ok")  # Checkmark icon | ||||
|             if item.metadata is not None and isinstance(item.metadata, bool): | ||||
|                 if item.metadata is True: | ||||
|                     return QIcon.fromTheme("dialog-ok") | ||||
|                 else: | ||||
|                         return QIcon.fromTheme("dialog-cancel")  # X icon | ||||
|                     return QIcon.fromTheme("dialog-cancel") | ||||
|         return None | ||||
| 
 | ||||
|     def headerData(self, section, orientation, role=Qt.DisplayRole): | ||||
|  | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user