Rewrote nus.py to be functions instead of an NUSDownloader class because the class was unnecessary and made things more complex

This commit is contained in:
Campbell 2024-04-05 00:09:19 -04:00
parent 60918f1a39
commit ccbc2e262b
Signed by: NinjaCheetah
GPG Key ID: B547958AF96ED344

View File

@ -12,186 +12,194 @@ from .tmd import TMD
from .ticket import Ticket from .ticket import Ticket
class NUSDownloader: def download_title(title_id: str, title_version: int = None) -> Title:
""" """
An NUS Downloader class that allows for easy downloading of a title from the NUS. Download an entire title and all of its contents, then load the downloaded components into a Title object for
further use. This method is NOT recommended for general use, as it has absolutely no verbosity. It is instead
recommended to call the individual download methods instead to provide more flexibility and output.
Parameters Parameters
---------- ----------
title_id : str title_id : str
The Title ID of the title you wish to download data for. The Title ID of the title to download.
title_version : int, default: None title_version : int, option
The version of the title you wish to download data for. Will assume latest if this is "None". The version of the title to download. Defaults to latest if not set.
Returns
-------
Title
A Title object containing all the data from the downloaded title.
""" """
def __init__(self, title_id: str, title_version: int = None): # First, create the new title.
self.title_id = title_id title = Title()
self.title_version = title_version # Download and load the TMD, Ticket, and certs.
# Data from the NUS title.load_tmd(download_tmd(title_id, title_version))
self.raw_tmd: bytes = b'' title.load_ticket(download_ticket(title_id))
self.tmd: bytes = b'' title.wad.set_cert_data(download_cert())
self.cetk: bytes = b'' # Download all contents
self.cert: bytes = b'' title.load_content_records()
self.ticket: bytes = b'' title.content.content_list = download_contents(title_id, title.tmd)
self.content_list: List[bytes] = [] # Return the completed title.
return title
def download_title(self) -> Title:
"""
Download an entire title and all of its contents, then load the downloaded components into a Title object for
further use.
Returns def download_tmd(title_id: str, title_version: int = None) -> bytes:
------- """
Title Downloads the TMD of the Title specified in the object. Will download the latest version by default, or another
A Title object containing all the data from the downloaded title. version if it was manually specified in the object.
"""
# First, create the new title.
title = Title()
# Download and load the TMD, Ticket, and certs.
title.load_tmd(self.download_tmd())
title.load_ticket(self.download_ticket())
title.wad.set_cert_data(self.download_cert())
# Download all contents
title.load_content_records()
title.content.content_list = self.download_contents(title.tmd)
# Return the completed title.
return title
def download_tmd(self) -> bytes: Parameters
""" ----------
Downloads the TMD of the Title specified in the object. Will download the latest version by default, or another title_id : str
version if it was manually specified in the object. The Title ID of the title to download the TMD for.
title_version : int, option
The version of the TMD to download. Defaults to latest if not set.
Returns Returns
------- -------
bytes bytes
The TMD file from the NUS. The TMD file from the NUS.
""" """
# Build the download URL. The structure is download/<TID>/tmd for latest and download/<TID>/tmd.<version> for # Build the download URL. The structure is download/<TID>/tmd for latest and download/<TID>/tmd.<version> for
# when a specific version is requested. # when a specific version is requested.
tmd_url = "http://ccs.shop.wii.com/ccs/download/" + self.title_id + "/tmd" tmd_url = "http://ccs.shop.wii.com/ccs/download/" + title_id + "/tmd"
# Add the version to the URL if one was specified. # Add the version to the URL if one was specified.
if self.title_version is not None: if title_version is not None:
tmd_url += "." + str(self.title_version) tmd_url += "." + str(title_version)
# Make the request. # Make the request.
tmd_response = urllib3.request(method='GET', url=tmd_url, headers={'User-Agent': 'wii libnup/1.0'}) tmd_response = urllib3.request(method='GET', url=tmd_url, headers={'User-Agent': 'wii libnup/1.0'})
# Handle a 404 if the TID/version doesn't exist. # Handle a 404 if the TID/version doesn't exist.
if tmd_response.status != 200: if tmd_response.status != 200:
raise ValueError("The requested Title ID or TMD version does not exist. Please check the Title ID and Title" raise ValueError("The requested Title ID or TMD version does not exist. Please check the Title ID and Title"
" version and then try again.") " version and then try again.")
# Save the raw TMD. # Save the raw TMD.
self.raw_tmd = tmd_response.data raw_tmd = tmd_response.data
# Use a TMD object to load the data and then return only the actual TMD. # Use a TMD object to load the data and then return only the actual TMD.
tmd_temp = TMD() tmd_temp = TMD()
tmd_temp.load(self.raw_tmd) tmd_temp.load(raw_tmd)
self.tmd = tmd_temp.dump() tmd = tmd_temp.dump()
return self.tmd return tmd
def download_ticket(self) -> bytes:
"""
Downloads the Ticket of the Title specified in the object. This will only work if the Title ID specified is for
a free title.
Returns def download_ticket(title_id: str) -> bytes:
------- """
bytes Downloads the Ticket of the Title specified in the object. This will only work if the Title ID specified is for
The Ticket file from the NUS. a free title.
"""
# Build the download URL. The structure is download/<TID>/cetk, and cetk will only exist if this is a free
# title.
ticket_url = "http://ccs.shop.wii.com/ccs/download/" + self.title_id + "/cetk"
# Make the request.
ticket_response = urllib3.request(method='GET', url=ticket_url, headers={'User-Agent': 'wii libnup/1.0'})
if ticket_response.status != 200:
raise ValueError("The requested Title ID does not exist, or refers to a non-free title. Tickets can only"
" be downloaded for titles that are free on the NUS.")
# Save the raw cetk file.
self.cetk = ticket_response.data
# Use a Ticket object to load only the Ticket data from cetk and return it.
ticket_temp = Ticket()
ticket_temp.load(self.cetk)
self.ticket = ticket_temp.dump()
return self.ticket
def download_cert(self) -> bytes: Parameters
""" ----------
Downloads the signing certificate used by all WADs. This uses System Menu 4.3U as the source. title_id : str
The Title ID of the title to download the Ticket for.
Returns Returns
------- -------
bytes bytes
The cert file. The Ticket file from the NUS.
""" """
# Download the TMD and cetk for the System Menu 4.3U. # Build the download URL. The structure is download/<TID>/cetk, and cetk will only exist if this is a free
tmd = urllib3.request(method='GET', url='http://ccs.shop.wii.com/ccs/download/0000000100000002/tmd.513', # title.
headers={'User-Agent': 'wii libnup/1.0'}).data ticket_url = "http://ccs.shop.wii.com/ccs/download/" + title_id + "/cetk"
cetk = urllib3.request(method='GET', url='http://ccs.shop.wii.com/ccs/download/0000000100000002/cetk', # Make the request.
headers={'User-Agent': 'wii libnup/1.0'}).data ticket_response = urllib3.request(method='GET', url=ticket_url, headers={'User-Agent': 'wii libnup/1.0'})
# Assemble the certificate. if ticket_response.status != 200:
with io.BytesIO() as cert_data: raise ValueError("The requested Title ID does not exist, or refers to a non-free title. Tickets can only"
# Certificate Authority data. " be downloaded for titles that are free on the NUS.")
cert_data.write(cetk[0x2A4 + 768:]) # Save the raw cetk file.
# Certificate Policy data. cetk = ticket_response.data
cert_data.write(tmd[0x328:0x328 + 768]) # Use a Ticket object to load only the Ticket data from cetk and return it.
# XS data. ticket_temp = Ticket()
cert_data.write(cetk[0x2A4:0x2A4 + 768]) ticket_temp.load(cetk)
cert_data.seek(0x0) ticket = ticket_temp.dump()
self.cert = cert_data.read() return ticket
# Since the cert is always the same, check the hash to make sure nothing went wildly wrong.
if hashlib.sha1(self.cert).hexdigest() != "ace0f15d2a851c383fe4657afc3840d6ffe30ad0":
raise Exception("An unknown error has occurred downloading and creating the certificate.")
return self.cert
def download_content(self, content_id) -> bytes:
"""
Downloads a specified content for the title specified in the object.
Parameters def download_cert() -> bytes:
---------- """
content_id : int Downloads the signing certificate used by all WADs. This uses System Menu 4.3U as the source.
The Content ID of the content you wish to download.
Returns Returns
------- -------
bytes bytes
The downloaded content. The cert file.
""" """
# Build the download URL. The structure is download/<TID>/<Content ID>. # Download the TMD and cetk for the System Menu 4.3U.
content_id_hex = hex(content_id)[2:] tmd = urllib3.request(method='GET', url='http://ccs.shop.wii.com/ccs/download/0000000100000002/tmd.513',
if len(content_id_hex) < 2: headers={'User-Agent': 'wii libnup/1.0'}).data
content_id_hex = "0" + content_id_hex cetk = urllib3.request(method='GET', url='http://ccs.shop.wii.com/ccs/download/0000000100000002/cetk',
content_url = "http://ccs.shop.wii.com/ccs/download/" + self.title_id + "/000000" + content_id_hex headers={'User-Agent': 'wii libnup/1.0'}).data
# Make the request. # Assemble the certificate.
content_response = urllib3.request(method='GET', url=content_url, headers={'User-Agent': 'wii libnup/1.0'}) with io.BytesIO() as cert_data:
if content_response.status != 200: # Certificate Authority data.
raise ValueError("The requested Title ID does not exist, or an invalid Content ID is present in the" cert_data.write(cetk[0x2A4 + 768:])
" content records provided.\n Failed while downloading Content ID: 000000" + # Certificate Policy data.
content_id_hex) cert_data.write(tmd[0x328:0x328 + 768])
return content_response.data # XS data.
cert_data.write(cetk[0x2A4:0x2A4 + 768])
cert_data.seek(0x0)
cert = cert_data.read()
# Since the cert is always the same, check the hash to make sure nothing went wildly wrong.
if hashlib.sha1(cert).hexdigest() != "ace0f15d2a851c383fe4657afc3840d6ffe30ad0":
raise Exception("An unknown error has occurred downloading and creating the certificate.")
return cert
def download_contents(self, tmd: TMD) -> List[bytes]:
"""
Downloads all the contents for the title specified in the object. This requires a TMD to already be available
so that the content records can be accessed.
Parameters def download_content(title_id: str, content_id: int) -> bytes:
---------- """
tmd : TMD Downloads a specified content for the title specified in the object.
The TMD that matches the title that the contents being downloaded are from.
Returns Parameters
------- ----------
List[bytes] title_id : str
A list of all the downloaded contents. The Title ID of the title to download content from.
""" content_id : int
# Retrieve the content records from the TMD. The Content ID of the content you wish to download.
content_records = tmd.content_records
# Create a list of Content IDs to download. Returns
content_ids = [] -------
for content_record in content_records: bytes
content_ids.append(content_record.content_id) The downloaded content.
# Iterate over that list and download each content in it, then add it to the array of contents. """
for content_id in content_ids: # Build the download URL. The structure is download/<TID>/<Content ID>.
# Call self.download_content() for each Content ID. content_id_hex = hex(content_id)[2:]
content = self.download_content(content_id) if len(content_id_hex) < 2:
self.content_list.append(content) content_id_hex = "0" + content_id_hex
return self.content_list content_url = "http://ccs.shop.wii.com/ccs/download/" + title_id + "/000000" + content_id_hex
# Make the request.
content_response = urllib3.request(method='GET', url=content_url, headers={'User-Agent': 'wii libnup/1.0'})
if content_response.status != 200:
raise ValueError("The requested Title ID does not exist, or an invalid Content ID is present in the"
" content records provided.\n Failed while downloading Content ID: 000000" +
content_id_hex)
return content_response.data
def download_contents(title_id: str, tmd: TMD) -> List[bytes]:
"""
Downloads all the contents for the title specified in the object. This requires a TMD to already be available
so that the content records can be accessed.
Parameters
----------
title_id : str
The Title ID of the title to download content from.
tmd : TMD
The TMD that matches the title that the contents being downloaded are from.
Returns
-------
List[bytes]
A list of all the downloaded contents.
"""
# Retrieve the content records from the TMD.
content_records = tmd.content_records
# Create a list of Content IDs to download.
content_ids = []
for content_record in content_records:
content_ids.append(content_record.content_id)
# Iterate over that list and download each content in it, then add it to the array of contents.
content_list = []
for content_id in content_ids:
# Call self.download_content() for each Content ID.
content = download_content(title_id, content_id)
content_list.append(content)
return content_list