55
66import requests
77#import hashlib
8- from typing import List
8+ from typing import Any , List , Protocol
99#from urllib.parse import urlparse as _urlparse
1010from .title import Title
1111from .tmd import TMD
1414_nus_endpoint = ["http://nus.cdn.shop.wii.com/ccs/download/" , "http://ccs.cdn.wup.shop.nintendo.net/ccs/download/" ]
1515
1616
17+ class DownloadCallback (Protocol ):
18+ """
19+ The format of a callable passed to a NUS download function.
20+ """
21+ def __call__ (self , done : int , total : int ) -> Any :
22+ """
23+ This function will be called with the current number of bytes downloaded and the total size of the file being
24+ downloaded.
25+
26+ Parameters
27+ ----------
28+ done : int
29+ The number of bytes already downloaded.
30+ total : int
31+ The total size of the file being downloaded.
32+ """
33+ ...
34+
35+
1736def download_title (title_id : str , title_version : int = None , wiiu_endpoint : bool = False ,
18- endpoint_override : str = None ) -> Title :
37+ endpoint_override : str = None , progress : DownloadCallback = lambda done , total : None ) -> Title :
1938 """
2039 Download an entire title and all of its contents, then load the downloaded components into a Title object for
21- further use. This method is NOT recommended for general use, as it has absolutely no verbosity. It is instead
40+ further use. This method is NOT recommended for general use, as it has extremely limited verbosity. It is instead
2241 recommended to call the individual download methods instead to provide more flexibility and output.
2342
43+ Be aware that you will receive fairly vague feedback from this function if you attach a progress callback. The
44+ callback will be connected to each of the individual functions called by this function, but there will be no
45+ indication of which function is currently running, just the progress of its download.
46+
2447 Parameters
2548 ----------
2649 title_id : str
@@ -32,27 +55,34 @@ def download_title(title_id: str, title_version: int = None, wiiu_endpoint: bool
3255 endpoint_override: str, optional
3356 A custom endpoint URL to use instead of the standard Wii or Wii U endpoints. Defaults to no override, and if
3457 set entirely overrides the "wiiu_endpoint" parameter.
58+ progress: DownloadCallback, optional
59+ A callback function used to return the progress of the downloads. The provided callable must match the signature
60+ defined in DownloadCallback.
3561
3662 Returns
3763 -------
3864 Title
3965 A Title object containing all the data from the downloaded title.
66+
67+ See Also
68+ --------
69+ libWiiPy.title.nus.DownloadCallback
4070 """
4171 # First, create the new title.
4272 title = Title ()
4373 # Download and load the certificate chain, TMD, and Ticket.
4474 title .load_cert_chain (download_cert_chain (wiiu_endpoint , endpoint_override ))
45- title .load_tmd (download_tmd (title_id , title_version , wiiu_endpoint , endpoint_override ))
46- title .load_ticket (download_ticket (title_id , wiiu_endpoint , endpoint_override ))
75+ title .load_tmd (download_tmd (title_id , title_version , wiiu_endpoint , endpoint_override , progress ))
76+ title .load_ticket (download_ticket (title_id , wiiu_endpoint , endpoint_override , progress ))
4777 # Download all contents
4878 title .load_content_records ()
49- title .content .content_list = download_contents (title_id , title .tmd , wiiu_endpoint , endpoint_override )
79+ title .content .content_list = download_contents (title_id , title .tmd , wiiu_endpoint , endpoint_override , progress )
5080 # Return the completed title.
5181 return title
5282
5383
5484def download_tmd (title_id : str , title_version : int = None , wiiu_endpoint : bool = False ,
55- endpoint_override : str = None ) -> bytes :
85+ endpoint_override : str = None , progress : DownloadCallback = lambda done , total : None ) -> bytes :
5686 """
5787 Downloads the TMD of the Title specified in the object. Will download the latest version by default, or another
5888 version if it was manually specified in the object.
@@ -68,11 +98,18 @@ def download_tmd(title_id: str, title_version: int = None, wiiu_endpoint: bool =
6898 endpoint_override: str, optional
6999 A custom endpoint URL to use instead of the standard Wii or Wii U endpoints. Defaults to no override, and if
70100 set entirely overrides the "wiiu_endpoint" parameter.
101+ progress: DownloadCallback, optional
102+ A callback function used to return the progress of the download. The provided callable must match the signature
103+ defined in DownloadCallback.
71104
72105 Returns
73106 -------
74107 bytes
75108 The TMD file from the NUS.
109+
110+ See Also
111+ --------
112+ libWiiPy.title.nus.DownloadCallback
76113 """
77114 # Build the download URL. The structure is download/<TID>/tmd for latest and download/<TID>/tmd.<version> for
78115 # when a specific version is requested.
@@ -89,27 +126,33 @@ def download_tmd(title_id: str, title_version: int = None, wiiu_endpoint: bool =
89126 tmd_url += "." + str (title_version )
90127 # Make the request.
91128 try :
92- tmd_request = requests .get (url = tmd_url , headers = {'User-Agent' : 'wii libnup/1.0' }, stream = True )
129+ response = requests .get (url = tmd_url , headers = {'User-Agent' : 'wii libnup/1.0' }, stream = True )
93130 except requests .exceptions .ConnectionError :
94131 if endpoint_override :
95132 raise ValueError ("A connection could not be made to the NUS endpoint. Please make sure that your endpoint "
96133 "override is valid." )
97134 else :
98135 raise Exception ("A connection could not be made to the NUS endpoint. The NUS may be unavailable." )
99136 # Handle a 404 if the TID/version doesn't exist.
100- if tmd_request .status_code != 200 :
137+ if response .status_code != 200 :
101138 raise ValueError ("The requested Title ID or TMD version does not exist. Please check the Title ID and Title"
102139 " version and then try again." )
103- # Save the raw TMD.
104- raw_tmd = tmd_request .content
140+ total_size = int (response .headers ["Content-Length" ])
141+ progress (0 , total_size )
142+ # Stream the TMD's data in chunks so that we can post updates to the callback function (assuming one was supplied).
143+ raw_tmd = b""
144+ for chunk in response .iter_content (512 ):
145+ raw_tmd += chunk
146+ progress (len (raw_tmd ), total_size )
105147 # Use a TMD object to load the data and then return only the actual TMD.
106148 tmd_temp = TMD ()
107149 tmd_temp .load (raw_tmd )
108150 tmd = tmd_temp .dump ()
109151 return tmd
110152
111153
112- def download_ticket (title_id : str , wiiu_endpoint : bool = False , endpoint_override : str = None ) -> bytes :
154+ def download_ticket (title_id : str , wiiu_endpoint : bool = False , endpoint_override : str = None ,
155+ progress : DownloadCallback = lambda done , total : None ) -> bytes :
113156 """
114157 Downloads the Ticket of the Title specified in the object. This will only work if the Title ID specified is for
115158 a free title.
@@ -123,11 +166,18 @@ def download_ticket(title_id: str, wiiu_endpoint: bool = False, endpoint_overrid
123166 endpoint_override: str, optional
124167 A custom endpoint URL to use instead of the standard Wii or Wii U endpoints. Defaults to no override, and if
125168 set entirely overrides the "wiiu_endpoint" parameter.
169+ progress: DownloadCallback, optional
170+ A callback function used to return the progress of the download. The provided callable must match the signature
171+ defined in DownloadCallback.
126172
127173 Returns
128174 -------
129175 bytes
130176 The Ticket file from the NUS.
177+
178+ See Also
179+ --------
180+ libWiiPy.title.nus.DownloadCallback
131181 """
132182 # Build the download URL. The structure is download/<TID>/cetk, and cetk will only exist if this is a free
133183 # title.
@@ -141,18 +191,23 @@ def download_ticket(title_id: str, wiiu_endpoint: bool = False, endpoint_overrid
141191 ticket_url = endpoint_url + title_id + "/cetk"
142192 # Make the request.
143193 try :
144- ticket_request = requests .get (url = ticket_url , headers = {'User-Agent' : 'wii libnup/1.0' }, stream = True )
194+ response = requests .get (url = ticket_url , headers = {'User-Agent' : 'wii libnup/1.0' }, stream = True )
145195 except requests .exceptions .ConnectionError :
146196 if endpoint_override :
147197 raise ValueError ("A connection could not be made to the NUS endpoint. Please make sure that your endpoint "
148198 "override is valid." )
149199 else :
150200 raise Exception ("A connection could not be made to the NUS endpoint. The NUS may be unavailable." )
151- if ticket_request .status_code != 200 :
201+ if response .status_code != 200 :
152202 raise ValueError ("The requested Title ID does not exist, or refers to a non-free title. Tickets can only"
153203 " be downloaded for titles that are free on the NUS." )
154- # Save the raw cetk file.
155- cetk = ticket_request .content
204+ total_size = int (response .headers ["Content-Length" ])
205+ progress (0 , total_size )
206+ # Stream the Ticket's data just like with the TMD.
207+ cetk = b""
208+ for chunk in response .iter_content (chunk_size = 1024 ):
209+ cetk += chunk
210+ progress (len (cetk ), total_size )
156211 # Use a Ticket object to load only the Ticket data from cetk and return it.
157212 ticket_temp = Ticket ()
158213 ticket_temp .load (cetk )
@@ -212,7 +267,7 @@ def download_cert_chain(wiiu_endpoint: bool = False, endpoint_override: str = No
212267
213268
214269def download_content (title_id : str , content_id : int , wiiu_endpoint : bool = False ,
215- endpoint_override : str = None ) -> bytes :
270+ endpoint_override : str = None , progress : DownloadCallback = lambda done , total : None ) -> bytes :
216271 """
217272 Downloads a specified content for the title specified in the object.
218273
@@ -227,11 +282,18 @@ def download_content(title_id: str, content_id: int, wiiu_endpoint: bool = False
227282 endpoint_override: str, optional
228283 A custom endpoint URL to use instead of the standard Wii or Wii U endpoints. Defaults to no override, and if
229284 set entirely overrides the "wiiu_endpoint" parameter.
285+ progress: DownloadCallback, optional
286+ A callback function used to return the progress of the download. The provided callable must match the signature
287+ defined in DownloadCallback.
230288
231289 Returns
232290 -------
233291 bytes
234292 The downloaded content.
293+
294+ See Also
295+ --------
296+ libWiiPy.title.nus.DownloadCallback
235297 """
236298 # Build the download URL. The structure is download/<TID>/<Content ID>.
237299 content_id_hex = hex (content_id )[2 :]
@@ -247,23 +309,29 @@ def download_content(title_id: str, content_id: int, wiiu_endpoint: bool = False
247309 content_url = endpoint_url + title_id + "/000000" + content_id_hex
248310 # Make the request.
249311 try :
250- content_request = requests .get (url = content_url , headers = {'User-Agent' : 'wii libnup/1.0' }, stream = True )
312+ response = requests .get (url = content_url , headers = {'User-Agent' : 'wii libnup/1.0' }, stream = True )
251313 except requests .exceptions .ConnectionError :
252314 if endpoint_override :
253315 raise ValueError ("A connection could not be made to the NUS endpoint. Please make sure that your endpoint "
254316 "override is valid." )
255317 else :
256318 raise Exception ("A connection could not be made to the NUS endpoint. The NUS may be unavailable." )
257- if content_request .status_code != 200 :
319+ if response .status_code != 200 :
258320 raise ValueError ("The requested Title ID does not exist, or an invalid Content ID is present in the"
259321 " content records provided.\n Failed while downloading Content ID: 000000" +
260322 content_id_hex )
261- content_data = content_request .content
262- return content_data
323+ total_size = int (response .headers ["Content-Length" ])
324+ progress (0 , total_size )
325+ # Stream the content just like the TMD/Ticket.
326+ content = b""
327+ for chunk in response .iter_content (chunk_size = 1024 ):
328+ content += chunk
329+ progress (len (content ), total_size )
330+ return content
263331
264332
265- def download_contents (title_id : str , tmd : TMD , wiiu_endpoint : bool = False ,
266- endpoint_override : str = None ) -> List [bytes ]:
333+ def download_contents (title_id : str , tmd : TMD , wiiu_endpoint : bool = False , endpoint_override : str = None ,
334+ progress : DownloadCallback = lambda done , total : None ) -> List [bytes ]:
267335 """
268336 Downloads all the contents for the title specified in the object. This requires a TMD to already be available
269337 so that the content records can be accessed.
@@ -279,11 +347,18 @@ def download_contents(title_id: str, tmd: TMD, wiiu_endpoint: bool = False,
279347 endpoint_override: str, optional
280348 A custom endpoint URL to use instead of the standard Wii or Wii U endpoints. Defaults to no override, and if
281349 set entirely overrides the "wiiu_endpoint" parameter.
350+ progress: DownloadCallback, optional
351+ A callback function used to return the progress of the downloads. The provided callable must match the signature
352+ defined in DownloadCallback.
282353
283354 Returns
284355 -------
285356 List[bytes]
286357 A list of all the downloaded contents.
358+
359+ See Also
360+ --------
361+ libWiiPy.title.nus.DownloadCallback
287362 """
288363 # Retrieve the content records from the TMD.
289364 content_records = tmd .content_records
@@ -295,7 +370,7 @@ def download_contents(title_id: str, tmd: TMD, wiiu_endpoint: bool = False,
295370 content_list = []
296371 for content_id in content_ids :
297372 # Call self.download_content() for each Content ID.
298- content = download_content (title_id , content_id , wiiu_endpoint , endpoint_override )
373+ content = download_content (title_id , content_id , wiiu_endpoint , endpoint_override , progress )
299374 content_list .append (content )
300375 return content_list
301376
0 commit comments