diff --git a/modules/nus.py b/modules/nus.py index 44b7822..8a83aa6 100644 --- a/modules/nus.py +++ b/modules/nus.py @@ -1,22 +1,21 @@ # "nus.py" from WiiPy by NinjaCheetah # https://github.com/NinjaCheetah/WiiPy +import os import pathlib import libWiiPy -def handle_nus(args): +def handle_nus_title(args): title_version = None - file_path = None + wad_file = None + output_dir = None + can_decrypt = False tid = args.tid if args.wii: - use_wiiu_servers = False + wiiu_nus_enabled = False else: - use_wiiu_servers = True - if args.verbose: - verbose = True - else: - verbose = False + wiiu_nus_enabled = True # Check if --version was passed, because it'll be None if it wasn't. if args.version is not None: @@ -26,70 +25,112 @@ def handle_nus(args): print("Enter a valid integer for the Title Version.") return - # If --output was passed, then save the file to the specified path (as long as it's valid). + # If --wad was passed, check to make sure the path is okay. + if args.wad is not None: + wad_file = pathlib.Path(args.wad) + if wad_file.suffix != ".wad": + wad_file = wad_file.with_suffix(".wad") + + # If --output was passed, make sure the directory either doesn't exist or is empty. if args.output is not None: - file_path = pathlib.Path(args.output) - if not file_path.parent.exists() or not file_path.parent.is_dir(): - print("The specified output path does not exist!") - return - if file_path.suffix != ".wad": - file_path = file_path.with_suffix(".wad") + output_dir = pathlib.Path(args.output) + if output_dir.exists(): + if output_dir.is_dir() and next(os.scandir(output_dir), None): + raise ValueError("Output folder is not empty!") + elif output_dir.is_file(): + raise ValueError("A file already exists with the provided directory name!") + else: + os.mkdir(output_dir) # Download the title from the NUS. This is done "manually" (as opposed to using download_title()) so that we can - # provide verbose output if desired. + # provide verbose output. title = libWiiPy.title.Title() # Announce the title being downloaded, and the version if applicable. - if verbose: - if title_version is not None: - print("Downloading title " + tid + " v" + str(title_version) + ", please wait...") - else: - print("Downloading title " + tid + " vLatest, please wait...") - - # Download a specific TMD version if a version was specified, otherwise just download the latest TMD. - if verbose: - print(" - Downloading and parsing TMD...") if title_version is not None: - title.load_tmd(libWiiPy.title.download_tmd(tid, title_version, wiiu_endpoint=use_wiiu_servers)) + print("Downloading title " + tid + " v" + str(title_version) + ", please wait...") else: - title.load_tmd(libWiiPy.title.download_tmd(tid, wiiu_endpoint=use_wiiu_servers)) + print("Downloading title " + tid + " vLatest, please wait...") + print(" - Downloading and parsing TMD...") + # Download a specific TMD version if a version was specified, otherwise just download the latest TMD. + if title_version is not None: + title.load_tmd(libWiiPy.title.download_tmd(tid, title_version, wiiu_endpoint=wiiu_nus_enabled)) + else: + title.load_tmd(libWiiPy.title.download_tmd(tid, wiiu_endpoint=wiiu_nus_enabled)) + title_version = title.tmd.title_version + # Write out the TMD to a file. + if output_dir is not None: + tmd_out = open(output_dir.joinpath("tmd." + str(title_version)), "wb") + tmd_out.write(title.tmd.dump()) + tmd_out.close() - # Download and parse the Ticket. - if verbose: - print(" - Downloading and parsing Ticket...") + # Download the ticket, if we can. + print(" - Downloading and parsing Ticket...") try: - title.load_ticket(libWiiPy.title.download_ticket(tid, wiiu_endpoint=use_wiiu_servers)) + title.load_ticket(libWiiPy.title.download_ticket(tid, wiiu_endpoint=wiiu_nus_enabled)) + can_decrypt = True + if output_dir is not None: + ticket_out = open(output_dir.joinpath("tik"), "wb") + ticket_out.write(title.ticket.dump()) + ticket_out.close() except ValueError: - # If libWiiPy returns an error, then no ticket is available, so we can't continue. - print("No Ticket is available for this title! Exiting...") - return + # If libWiiPy returns an error, then no ticket is available. Log this, and disable options requiring a + # ticket so that they aren't attempted later. + print(" - No Ticket is available!") + if wad_file is not None and output_dir is None: + print("--wad was passed, but this title cannot be packed into a WAD!") + return # Load the content records from the TMD, and begin iterating over the records. title.load_content_records() content_list = [] for content in range(len(title.tmd.content_records)): - if verbose: - print(" - Downloading content " + str(content + 1) + " of " + - str(len(title.tmd.content_records)) + " (Content ID: " + - str(title.tmd.content_records[content].content_id) + ", Size: " + - str(title.tmd.content_records[content].content_size) + " bytes)...") + # Generate the content file name by converting the Content ID to hex and then removing the 0x. + content_file_name = hex(title.tmd.content_records[content].content_id)[2:] + while len(content_file_name) < 8: + content_file_name = "0" + content_file_name + print(" - Downloading content " + str(content + 1) + " of " + + str(len(title.tmd.content_records)) + " (Content ID: " + + str(title.tmd.content_records[content].content_id) + ", Size: " + + str(title.tmd.content_records[content].content_size) + " bytes)...") content_list.append(libWiiPy.title.download_content(tid, title.tmd.content_records[content].content_id, - wiiu_endpoint=use_wiiu_servers)) - if verbose: - print(" - Done!") + wiiu_endpoint=wiiu_nus_enabled)) + print(" - Done!") + # If we're supposed to be outputting to a folder, then write these files out. + if output_dir is not None: + enc_content_out = open(output_dir.joinpath(content_file_name), "wb") + enc_content_out.write(content_list[content]) + enc_content_out.close() title.content.content_list = content_list - # Get the WAD certificate chain. - if verbose: + # Try to decrypt the contents for this title if a ticket was available. + if can_decrypt is True and output_dir is not None: + for content in range(len(title.tmd.content_records)): + print(" - Decrypting content " + str(content + 1) + " of " + str(len(title.tmd.content_records)) + + " (Content ID: " + str(title.tmd.content_records[content].content_id) + ")...") + dec_content = title.get_content_by_index(content) + content_file_name = hex(title.tmd.content_records[content].content_id)[2:] + while len(content_file_name) < 8: + content_file_name = "0" + content_file_name + content_file_name = content_file_name + ".app" + dec_content_out = open(output_dir.joinpath(content_file_name), "wb") + dec_content_out.write(dec_content) + dec_content_out.close() + else: + print("Title has no Ticket, so content will not be decrypted!") + + # If --wad was passed, pack a WAD and output that. + if wad_file is not None: + # Get the WAD certificate chain. print(" - Building certificate...") - title.wad.set_cert_data(libWiiPy.title.download_cert(wiiu_endpoint=use_wiiu_servers)) - - # If we haven't gotten a name yet, make one from the TID and version. - if file_path is None: - file_path = pathlib.Path(args.tid + "-v" + str(title.tmd.title_version) + ".wad") - - wad_file = open(file_path, "wb") - wad_file.write(title.dump_wad()) - wad_file.close() + title.wad.set_cert_data(libWiiPy.title.download_cert(wiiu_endpoint=wiiu_nus_enabled)) + # Ensure that the path ends in .wad, and add that if it doesn't. + print("Packing WAD...") + if wad_file.suffix != ".wad": + wad_file = wad_file.with_suffix(".wad") + # Have libWiiPy dump the WAD, and write that data out. + file = open(wad_file, "wb") + file.write(title.dump_wad()) + file.close() print("Downloaded title with Title ID \"" + args.tid + "\"!") diff --git a/modules/wad.py b/modules/wad.py index f98d3b5..7318076 100644 --- a/modules/wad.py +++ b/modules/wad.py @@ -100,7 +100,7 @@ def handle_wad(args): if output_path.is_dir() and next(os.scandir(output_path), None): raise ValueError("Output folder is not empty!") elif output_path.is_file(): - raise ValueError("A file already exists with the provided name!") + raise ValueError("A file already exists with the provided directory name!") else: os.mkdir(output_path) diff --git a/wiipy.py b/wiipy.py index b5fd271..f5840a1 100644 --- a/wiipy.py +++ b/wiipy.py @@ -14,7 +14,7 @@ if __name__ == "__main__": parser = argparse.ArgumentParser( description="WiiPy is a simple command line tool to manage file formats used by the Wii.") parser.add_argument("--version", action="version", - version=f"WiiPy v1.1.0, based on libWiiPy v{version('libWiiPy')} (from branch \'main\')") + version=f"WiiPy v1.2.0, based on libWiiPy v{version('libWiiPy')} (from branch \'main\')") subparsers = parser.add_subparsers(dest="subcommand", required=True) # Argument parser for the WAD subcommand. @@ -30,17 +30,24 @@ if __name__ == "__main__": action="store_true") # Argument parser for the NUS subcommand. - nus_parser = subparsers.add_parser("nus", help="download a title from the NUS", - description="download a title from the NUS") - nus_parser.set_defaults(func=handle_nus) - nus_parser.add_argument("tid", metavar="TID", type=str, help="Title ID to download") - nus_parser.add_argument("-v", "--version", metavar="VERSION", type=int, - help="version to download (optional)") - nus_parser.add_argument("-o", "--output", metavar="OUT", type=str, help="output file (optional)") - nus_parser.add_argument("--verbose", help="output more information about the current download", - action="store_true") - nus_parser.add_argument("-w", "--wii", help="use original Wii NUS instead of the Wii U servers", - action="store_true") + nus_parser = subparsers.add_parser("nus", help="download data from the NUS", + description="download from the NUS") + nus_subparsers = nus_parser.add_subparsers(dest="subcommand", required=True) + # Title NUS subcommand. + nus_title_parser = nus_subparsers.add_parser("title", help="download a title from the NUS", + description="download a title from the NUS") + nus_title_parser.set_defaults(func=handle_nus_title) + nus_title_parser.add_argument("tid", metavar="TID", type=str, help="Title ID to download") + nus_title_parser.add_argument("-v", "--version", metavar="VERSION", type=int, + help="version to download (optional)") + nus_title_out_group_label = nus_title_parser.add_argument_group(title="output types (required)") + nus_title_out_group = nus_title_out_group_label.add_mutually_exclusive_group(required=True) + nus_title_out_group.add_argument("-o", "--output", metavar="OUT", type=str, + help="download the title to a folder") + nus_title_out_group.add_argument("-w", "--wad", metavar="WAD", type=str, + help="pack a wad with the provided name") + nus_title_parser.add_argument("--wii", help="use original Wii NUS instead of the Wii U servers", + action="store_true") # Argument parser for the U8 subcommand. u8_parser = subparsers.add_parser("u8", help="pack/unpack a U8 archive",