Skip to content

Commit 79ab33c

Browse files
committed
Added support for progress callbacks in NUS download functions
1 parent e06bb39 commit 79ab33c

6 files changed

Lines changed: 115 additions & 43 deletions

File tree

docs/source/conf.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,13 @@
1717
# -- General configuration ---------------------------------------------------
1818
# https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration
1919

20-
extensions = ['myst_parser', 'sphinx.ext.napoleon', 'sphinx_copybutton', 'sphinx_tippy', 'sphinx_design']
20+
extensions = [
21+
'myst_parser',
22+
'sphinx.ext.napoleon',
23+
'sphinx_copybutton',
24+
'sphinx_tippy',
25+
'sphinx_design'
26+
]
2127

2228
templates_path = ['_templates']
2329
exclude_patterns = ["Thumbs.db", ".DS_Store"]

docs/source/title/nus.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,4 +11,5 @@ The `libWiiPy.title.nus` module provides support for downloading digital Wii tit
1111
:members:
1212
:undoc-members:
1313
:show-inheritance:
14+
:special-members: __call__
1415
```

pyproject.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[project]
22
name = "libWiiPy"
3-
version = "0.6.1"
3+
version = "1.0.0"
44
authors = [
55
{ name="NinjaCheetah", email="ninjacheetah@ncxprogramming.com" },
66
{ name="Lillian Skinner", email="lillian@randommeaninglesscharacters.com" }
@@ -13,7 +13,7 @@ classifiers = [
1313
# 3 - Alpha
1414
# 4 - Beta
1515
# 5 - Production/Stable
16-
"Development Status :: 4 - Beta",
16+
"Development Status :: 5 - Production/Stable",
1717
"Intended Audience :: Developers",
1818
"License :: OSI Approved :: MIT License",
1919
"Operating System :: OS Independent",

src/libWiiPy/media/banner.py

Lines changed: 4 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -14,16 +14,10 @@ class IMD5Header:
1414
1515
An IMD5 header is always 32 bytes long.
1616
17-
Attributes
18-
----------
19-
magic : str
20-
Magic number for the header, should be "IMD5".
21-
file_size : int
22-
The size of the file this header precedes.
23-
zeros : int
24-
8 bytes of zero padding.
25-
md5_hash : bytes
26-
The MD5 hash of the file this header precedes.
17+
:ivar magic: Magic number for the header, should be "IMD5".
18+
:ivar file_size: The size of the file this header precedes.
19+
:ivar zeros: 8 bytes of zero padding.
20+
:ivar md5_hash: The MD5 hash of the file this header precedes.
2721
"""
2822
magic: str # Should always be "IMD5"
2923
file_size: int

src/libWiiPy/nand/emunand.py

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -174,12 +174,8 @@ class InstalledTitles:
174174
An InstalledTitles object that is used to track a title type and any titles that belong to that type that are
175175
installed to an EmuNAND.
176176
177-
Attributes
178-
----------
179-
type : str
180-
The type (Title ID high) of the installed titles.
181-
titles : List[str]
182-
The Title ID low of each installed title.
177+
:ivar type: The type (Title ID high) of the installed titles.
178+
:ivar titles: The Title ID low of each installed title.
183179
"""
184180
type: str
185181
titles: List[str]

src/libWiiPy/title/nus.py

Lines changed: 99 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55

66
import requests
77
#import hashlib
8-
from typing import List
8+
from typing import Any, List, Protocol
99
#from urllib.parse import urlparse as _urlparse
1010
from .title import Title
1111
from .tmd import TMD
@@ -14,13 +14,36 @@
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+
1736
def 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

5484
def 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

214269
def 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

Comments
 (0)