pywget 2.0.2__tar.gz → 2.0.4__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {pywget-2.0.2/pywget.egg-info → pywget-2.0.4}/PKG-INFO +611 -370
- pywget-2.0.4/README.md +541 -0
- {pywget-2.0.2 → pywget-2.0.4}/pyproject.toml +1 -5
- {pywget-2.0.2 → pywget-2.0.4}/pywget/__init__.py +9 -0
- pywget-2.0.4/pywget/cli.py +514 -0
- pywget-2.0.4/pywget/client.py +127 -0
- pywget-2.0.4/pywget/clipboard.py +44 -0
- pywget-2.0.4/pywget/config.py +93 -0
- {pywget-2.0.2 → pywget-2.0.4}/pywget/core.py +5 -2
- {pywget-2.0.2 → pywget-2.0.4}/pywget/errors.py +11 -0
- pywget-2.0.4/pywget/jobs.py +612 -0
- pywget-2.0.4/pywget/server.py +320 -0
- {pywget-2.0.2 → pywget-2.0.4}/pywget/utils.py +19 -0
- pywget-2.0.4/pywget/webui.py +52 -0
- {pywget-2.0.2 → pywget-2.0.4/pywget.egg-info}/PKG-INFO +611 -370
- {pywget-2.0.2 → pywget-2.0.4}/pywget.egg-info/SOURCES.txt +7 -0
- {pywget-2.0.2 → pywget-2.0.4}/setup.cfg +4 -4
- pywget-2.0.4/tests/test_server.py +269 -0
- pywget-2.0.2/README.md +0 -300
- pywget-2.0.2/pywget/cli.py +0 -195
- {pywget-2.0.2 → pywget-2.0.4}/LICENSE +0 -0
- {pywget-2.0.2 → pywget-2.0.4}/pywget/__main__.py +0 -0
- {pywget-2.0.2 → pywget-2.0.4}/pywget/__version__.py +0 -0
- {pywget-2.0.2 → pywget-2.0.4}/pywget/progress.py +0 -0
- {pywget-2.0.2 → pywget-2.0.4}/pywget/py.typed +0 -0
- {pywget-2.0.2 → pywget-2.0.4}/pywget/wget.py +0 -0
- {pywget-2.0.2 → pywget-2.0.4}/pywget.egg-info/dependency_links.txt +0 -0
- {pywget-2.0.2 → pywget-2.0.4}/pywget.egg-info/entry_points.txt +0 -0
- {pywget-2.0.2 → pywget-2.0.4}/pywget.egg-info/requires.txt +0 -0
- {pywget-2.0.2 → pywget-2.0.4}/pywget.egg-info/top_level.txt +0 -0
- {pywget-2.0.2 → pywget-2.0.4}/tests/test_download.py +0 -0
- {pywget-2.0.2 → pywget-2.0.4}/tests/test_utils.py +0 -0
|
@@ -1,370 +1,611 @@
|
|
|
1
|
-
Metadata-Version: 2.4
|
|
2
|
-
Name: pywget
|
|
3
|
-
Version: 2.0.
|
|
4
|
-
Summary: Robust, resumable, multi-connection wget alternative - CLI and Python library, zero dependencies
|
|
5
|
-
Author-email: Hadi Cahyadi <cumulus13@gmail.com>
|
|
6
|
-
Maintainer-email: cumulus13 <cumulus13@gmail.com>
|
|
7
|
-
License: BSD 3-Clause License
|
|
8
|
-
|
|
9
|
-
Copyright (c) 2021-2026, Hadi Cahyadi
|
|
10
|
-
All rights reserved.
|
|
11
|
-
|
|
12
|
-
Redistribution and use in source and binary forms, with or without
|
|
13
|
-
modification, are permitted provided that the following conditions are met:
|
|
14
|
-
|
|
15
|
-
1. Redistributions of source code must retain the above copyright notice, this
|
|
16
|
-
list of conditions and the following disclaimer.
|
|
17
|
-
|
|
18
|
-
2. Redistributions in binary form must reproduce the above copyright notice,
|
|
19
|
-
this list of conditions and the following disclaimer in the documentation
|
|
20
|
-
and/or other materials provided with the distribution.
|
|
21
|
-
|
|
22
|
-
3. Neither the name of the copyright holder nor the names of its
|
|
23
|
-
contributors may be used to endorse or promote products derived from
|
|
24
|
-
this software without specific prior written permission.
|
|
25
|
-
|
|
26
|
-
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
|
27
|
-
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
|
28
|
-
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
|
29
|
-
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
|
30
|
-
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
|
31
|
-
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
|
32
|
-
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
|
33
|
-
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
|
34
|
-
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
|
35
|
-
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
36
|
-
|
|
37
|
-
Project-URL: Homepage, https://github.com/cumulus13/pywget
|
|
38
|
-
Project-URL: Documentation, https://github.com/cumulus13/pywget#readme
|
|
39
|
-
Project-URL: Source, https://github.com/cumulus13/pywget
|
|
40
|
-
Project-URL: Issues, https://github.com/cumulus13/pywget/issues
|
|
41
|
-
Project-URL: Changelog, https://github.com/cumulus13/pywget/blob/master/CHANGELOG.md
|
|
42
|
-
Keywords: wget,download,downloader,resume,multi-connection,cli,http
|
|
43
|
-
Classifier: Development Status :: 5 - Production/Stable
|
|
44
|
-
Classifier: Environment :: Console
|
|
45
|
-
Classifier: Intended Audience :: Developers
|
|
46
|
-
Classifier: Intended Audience :: System Administrators
|
|
47
|
-
Classifier: License :: OSI Approved :: BSD License
|
|
48
|
-
Classifier: Operating System :: OS Independent
|
|
49
|
-
Classifier: Programming Language :: Python :: 3
|
|
50
|
-
Classifier: Programming Language :: Python :: 3.8
|
|
51
|
-
Classifier: Programming Language :: Python :: 3.9
|
|
52
|
-
Classifier: Programming Language :: Python :: 3.10
|
|
53
|
-
Classifier: Programming Language :: Python :: 3.11
|
|
54
|
-
Classifier: Programming Language :: Python :: 3.12
|
|
55
|
-
Classifier: Programming Language :: Python :: 3.13
|
|
56
|
-
Classifier: Topic :: Internet :: WWW/HTTP
|
|
57
|
-
Classifier: Topic :: System :: Networking
|
|
58
|
-
Classifier: Typing :: Typed
|
|
59
|
-
Requires-Python: >=3.8
|
|
60
|
-
Description-Content-Type: text/markdown
|
|
61
|
-
License-File: LICENSE
|
|
62
|
-
Provides-Extra: dev
|
|
63
|
-
Requires-Dist: pytest>=7; extra == "dev"
|
|
64
|
-
Requires-Dist: pytest-cov; extra == "dev"
|
|
65
|
-
Requires-Dist: ruff; extra == "dev"
|
|
66
|
-
Requires-Dist: mypy; extra == "dev"
|
|
67
|
-
Requires-Dist: build; extra == "dev"
|
|
68
|
-
Requires-Dist: twine; extra == "dev"
|
|
69
|
-
Dynamic: license-file
|
|
70
|
-
|
|
71
|
-
# pywget
|
|
72
|
-
|
|
73
|
-
[](https://github.com/cumulus13/pywget/actions/workflows/ci.yml)
|
|
74
|
-
[](https://pypi.org/project/pywget/)
|
|
75
|
-
[](https://pypi.org/project/pywget/)
|
|
76
|
-
[](LICENSE)
|
|
77
|
-
|
|
78
|
-
A robust, resumable, **multi-connection** command-line downloader and Python library.
|
|
79
|
-
A drop-in-style `wget` alternative with zero third-party dependencies.
|
|
80
|
-
|
|
81
|
-
## Features
|
|
82
|
-
|
|
83
|
-
| Area | What you get |
|
|
84
|
-
|------|--------------|
|
|
85
|
-
| Speed | Parallel segmented download (`-n 1..32`), automatic single-stream fallback |
|
|
86
|
-
| Reliability | Crash-safe resume, retries with exponential backoff + jitter, `Retry-After`, atomic rename |
|
|
87
|
-
| Integrity | `--checksum sha256:...`, size verification, disk-space pre-check |
|
|
88
|
-
| Security | http/https only, credentials stripped on cross-origin redirects, path-traversal-proof filenames, TLS verification on by default |
|
|
89
|
-
| Control | Bandwidth limit, custom headers, basic auth, proxy, custom CA, timeouts |
|
|
90
|
-
| Automation | Batch input (`-i`), `--json` output, stable exit codes, SIGINT/SIGTERM-safe |
|
|
91
|
-
|
|
|
92
|
-
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
```
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
|
133
|
-
|
|
134
|
-
|
|
|
135
|
-
|
|
|
136
|
-
|
|
|
137
|
-
|
|
|
138
|
-
|
|
|
139
|
-
|
|
|
140
|
-
|
|
|
141
|
-
|
|
|
142
|
-
|
|
|
143
|
-
|
|
|
144
|
-
|
|
|
145
|
-
| `-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
|
150
|
-
|
|
151
|
-
|
|
|
152
|
-
|
|
|
153
|
-
|
|
|
154
|
-
|
|
|
155
|
-
|
|
|
156
|
-
|
|
|
157
|
-
|
|
|
158
|
-
|
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
)
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
```python
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
```
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
```python
|
|
195
|
-
from pywget import
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
```
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
[
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: pywget
|
|
3
|
+
Version: 2.0.4
|
|
4
|
+
Summary: Robust, resumable, multi-connection wget alternative - CLI and Python library, zero dependencies
|
|
5
|
+
Author-email: Hadi Cahyadi <cumulus13@gmail.com>
|
|
6
|
+
Maintainer-email: cumulus13 <cumulus13@gmail.com>
|
|
7
|
+
License: BSD 3-Clause License
|
|
8
|
+
|
|
9
|
+
Copyright (c) 2021-2026, Hadi Cahyadi
|
|
10
|
+
All rights reserved.
|
|
11
|
+
|
|
12
|
+
Redistribution and use in source and binary forms, with or without
|
|
13
|
+
modification, are permitted provided that the following conditions are met:
|
|
14
|
+
|
|
15
|
+
1. Redistributions of source code must retain the above copyright notice, this
|
|
16
|
+
list of conditions and the following disclaimer.
|
|
17
|
+
|
|
18
|
+
2. Redistributions in binary form must reproduce the above copyright notice,
|
|
19
|
+
this list of conditions and the following disclaimer in the documentation
|
|
20
|
+
and/or other materials provided with the distribution.
|
|
21
|
+
|
|
22
|
+
3. Neither the name of the copyright holder nor the names of its
|
|
23
|
+
contributors may be used to endorse or promote products derived from
|
|
24
|
+
this software without specific prior written permission.
|
|
25
|
+
|
|
26
|
+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
|
27
|
+
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
|
28
|
+
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
|
29
|
+
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
|
30
|
+
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
|
31
|
+
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
|
32
|
+
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
|
33
|
+
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
|
34
|
+
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
|
35
|
+
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
36
|
+
|
|
37
|
+
Project-URL: Homepage, https://github.com/cumulus13/pywget
|
|
38
|
+
Project-URL: Documentation, https://github.com/cumulus13/pywget#readme
|
|
39
|
+
Project-URL: Source, https://github.com/cumulus13/pywget
|
|
40
|
+
Project-URL: Issues, https://github.com/cumulus13/pywget/issues
|
|
41
|
+
Project-URL: Changelog, https://github.com/cumulus13/pywget/blob/master/CHANGELOG.md
|
|
42
|
+
Keywords: wget,download,downloader,resume,multi-connection,cli,http
|
|
43
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
44
|
+
Classifier: Environment :: Console
|
|
45
|
+
Classifier: Intended Audience :: Developers
|
|
46
|
+
Classifier: Intended Audience :: System Administrators
|
|
47
|
+
Classifier: License :: OSI Approved :: BSD License
|
|
48
|
+
Classifier: Operating System :: OS Independent
|
|
49
|
+
Classifier: Programming Language :: Python :: 3
|
|
50
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
51
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
52
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
53
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
54
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
55
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
56
|
+
Classifier: Topic :: Internet :: WWW/HTTP
|
|
57
|
+
Classifier: Topic :: System :: Networking
|
|
58
|
+
Classifier: Typing :: Typed
|
|
59
|
+
Requires-Python: >=3.8
|
|
60
|
+
Description-Content-Type: text/markdown
|
|
61
|
+
License-File: LICENSE
|
|
62
|
+
Provides-Extra: dev
|
|
63
|
+
Requires-Dist: pytest>=7; extra == "dev"
|
|
64
|
+
Requires-Dist: pytest-cov; extra == "dev"
|
|
65
|
+
Requires-Dist: ruff; extra == "dev"
|
|
66
|
+
Requires-Dist: mypy; extra == "dev"
|
|
67
|
+
Requires-Dist: build; extra == "dev"
|
|
68
|
+
Requires-Dist: twine; extra == "dev"
|
|
69
|
+
Dynamic: license-file
|
|
70
|
+
|
|
71
|
+
# pywget
|
|
72
|
+
|
|
73
|
+
[](https://github.com/cumulus13/pywget/actions/workflows/ci.yml)
|
|
74
|
+
[](https://pypi.org/project/pywget/)
|
|
75
|
+
[](https://pypi.org/project/pywget/)
|
|
76
|
+
[](LICENSE)
|
|
77
|
+
|
|
78
|
+
A robust, resumable, **multi-connection** command-line downloader and Python library.
|
|
79
|
+
A drop-in-style `wget` alternative with zero third-party dependencies.
|
|
80
|
+
|
|
81
|
+
## Features
|
|
82
|
+
|
|
83
|
+
| Area | What you get |
|
|
84
|
+
|------|--------------|
|
|
85
|
+
| Speed | Parallel segmented download (`-n 1..32`), automatic single-stream fallback |
|
|
86
|
+
| Reliability | Crash-safe resume, retries with exponential backoff + jitter, `Retry-After`, atomic rename |
|
|
87
|
+
| Integrity | `--checksum sha256:...`, size verification, disk-space pre-check |
|
|
88
|
+
| Security | http/https only, credentials stripped on cross-origin redirects, path-traversal-proof filenames, TLS verification on by default |
|
|
89
|
+
| Control | Bandwidth limit, custom headers, basic auth, proxy, custom CA, timeouts |
|
|
90
|
+
| Automation | Batch input (`-i`), `--json` output, stable exit codes, SIGINT/SIGTERM-safe |
|
|
91
|
+
| Server mode | `pywget serve`: persistent queue, HTTP/JSON API, SSE events, web UI, token auth |
|
|
92
|
+
| Agent mode | `pywget agent` / `--server`: send links from anywhere; offline-tolerant relay to a server |
|
|
93
|
+
| Library | Typed API, progress callbacks, cancel support, legacy `wget.download()` kept |
|
|
94
|
+
| Portability | Python 3.8+, Windows / Linux / macOS, no dependencies |
|
|
95
|
+
|
|
96
|
+
## Installation
|
|
97
|
+
|
|
98
|
+
```bash
|
|
99
|
+
pip install pywget
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
> **v2 note:** Python 2 is no longer supported. Python 3.8+ is required. The CLI and
|
|
103
|
+
> `wget.download(url, dir)` keep working as before.
|
|
104
|
+
|
|
105
|
+
## Quick start
|
|
106
|
+
|
|
107
|
+
```bash
|
|
108
|
+
# simple download into a directory (same as before)
|
|
109
|
+
pywget "https://example.com/file.iso" -p ~/Downloads
|
|
110
|
+
|
|
111
|
+
# 8 parallel connections, 2 MiB/s cap, verify checksum
|
|
112
|
+
pywget https://example.com/file.iso -n 8 --limit-rate 2M --checksum sha256:<hex>
|
|
113
|
+
|
|
114
|
+
# many URLs from a file (or stdin with -i -), JSON report
|
|
115
|
+
pywget -i urls.txt -p downloads --json
|
|
116
|
+
|
|
117
|
+
# auth + custom header
|
|
118
|
+
pywget https://api.example.com/export.zip --user me -H "Accept: application/zip"
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
```bash
|
|
122
|
+
# download manager mode: run a server, push links to it (see "Server mode and agent")
|
|
123
|
+
pywget serve --root ~/Downloads/pywget &
|
|
124
|
+
pywget send https://example.com/file.iso -r https://example.com/ --wait
|
|
125
|
+
pywget jobs list
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
Interrupted (Ctrl+C, crash, network loss)? Run the same command again - it resumes.
|
|
129
|
+
|
|
130
|
+
## CLI reference
|
|
131
|
+
|
|
132
|
+
| Option | Description |
|
|
133
|
+
|--------|-------------|
|
|
134
|
+
| `URL ...` | http(s) URLs, `c` = read links from the clipboard, or a text file of links |
|
|
135
|
+
| `-i, --input-file FILE` | Read URLs from file (`-` = stdin); `#` comments allowed |
|
|
136
|
+
| `-p, -P, --path DIR` | Download directory (default `.`) |
|
|
137
|
+
| `-O, --output NAME` | Output file name (single URL) |
|
|
138
|
+
| `-n, --connections N` | Parallel connections per file, 1-32 (default 4) |
|
|
139
|
+
| `-t, --retries N` | Retries per request/segment (default 5) |
|
|
140
|
+
| `-r, --referrer URL` | Referer header (many sites require it) |
|
|
141
|
+
| `-C, --cookie STR` | Cookie string `a=b; c=d` or JSON object |
|
|
142
|
+
| `-T, --timeout SEC` | Network timeout (default 30) |
|
|
143
|
+
| `--limit-rate RATE` | Total speed cap, e.g. `500k`, `2M` |
|
|
144
|
+
| `-H, --header 'K: V'` | Extra header (repeatable) |
|
|
145
|
+
| `-U, -ua, --user-agent UA` | Custom User-Agent |
|
|
146
|
+
| `--user`, `--password` | Basic auth (password also from `PYWGET_PASSWORD` or prompt) |
|
|
147
|
+
| `--proxy URL` | Proxy (default: environment) |
|
|
148
|
+
| `--no-check-certificate` | Disable TLS verification (not recommended) |
|
|
149
|
+
| `--ca-cert FILE` | Custom CA bundle |
|
|
150
|
+
| `--checksum ALGO:HEX` | Verify result (md5, sha1, sha256, sha512, ...) |
|
|
151
|
+
| `--overwrite` | Replace existing file and restart |
|
|
152
|
+
| `--no-resume` | Never resume partial files |
|
|
153
|
+
| `--no-mtime` | Do not copy `Last-Modified` to the file |
|
|
154
|
+
| `-q`, `-v` | Quiet / debug logging |
|
|
155
|
+
| `--json` | Machine-readable results on stdout |
|
|
156
|
+
| `-c` | Accepted for wget compatibility (resume is default) |
|
|
157
|
+
| `-s, --server URL` | Send the links to a pywget server/agent instead of downloading locally |
|
|
158
|
+
| `--token TOKEN` | API token for `--server` (default: `$PYWGET_TOKEN` or token file) |
|
|
159
|
+
| `-a, --add-only` | With `--server`: queue paused, do not start (like IDM's *add*) |
|
|
160
|
+
| `--wait` | With `--server`: wait for the jobs to finish |
|
|
161
|
+
|
|
162
|
+
### Exit codes
|
|
163
|
+
|
|
164
|
+
| Code | Meaning |
|
|
165
|
+
|------|---------|
|
|
166
|
+
| 0 | Success |
|
|
167
|
+
| 1 | Unexpected error |
|
|
168
|
+
| 2 | Bad usage / invalid URL |
|
|
169
|
+
| 3 | Network error |
|
|
170
|
+
| 4 | HTTP error status (404, 403, ...) |
|
|
171
|
+
| 5 | Checksum mismatch |
|
|
172
|
+
| 6 | Storage / I/O error (disk full, permissions) |
|
|
173
|
+
| 7 | Server / agent error (unreachable, 401, 409, ...) |
|
|
174
|
+
| 130 | Interrupted (partial data kept for resume) |
|
|
175
|
+
|
|
176
|
+
## Python API
|
|
177
|
+
|
|
178
|
+
```python
|
|
179
|
+
import pywget
|
|
180
|
+
|
|
181
|
+
path = pywget.download(
|
|
182
|
+
"https://example.com/file.iso",
|
|
183
|
+
"/data/downloads",
|
|
184
|
+
progress=True, # terminal bar
|
|
185
|
+
connections=8,
|
|
186
|
+
limit_rate="5M",
|
|
187
|
+
checksum="sha256:ab12...",
|
|
188
|
+
)
|
|
189
|
+
print(path)
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
Full control:
|
|
193
|
+
|
|
194
|
+
```python
|
|
195
|
+
from pywget import Downloader, Options, ProgressInfo
|
|
196
|
+
|
|
197
|
+
def on_progress(p: ProgressInfo) -> None:
|
|
198
|
+
print(f"{p.filename}: {p.percent or 0:.1f}% {p.speed/1e6:.1f} MB/s ETA {p.eta}")
|
|
199
|
+
|
|
200
|
+
dl = Downloader(Options(output_dir="out", retries=8, progress=on_progress,
|
|
201
|
+
headers={"Authorization": "Bearer TOKEN"}))
|
|
202
|
+
result = dl.fetch("https://example.com/big.bin")
|
|
203
|
+
print(result.path, result.size, result.resumed, result.connections)
|
|
204
|
+
|
|
205
|
+
# from another thread: dl.cancel() -> partial state is kept for resume
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
Talk to a server / agent, or embed one:
|
|
209
|
+
|
|
210
|
+
```python
|
|
211
|
+
from pywget import ServerClient, ServerConfig, ServerHandle
|
|
212
|
+
|
|
213
|
+
client = ServerClient("http://127.0.0.1:8765") # token: $PYWGET_TOKEN or token file
|
|
214
|
+
job = client.submit({"url": "https://example.com/f.iso", "path": "isos"})[0]
|
|
215
|
+
print(client.wait(job["id"])["status"])
|
|
216
|
+
|
|
217
|
+
cfg = ServerConfig(port=0, token="secret-token", root="/srv/dl", state_dir="/var/lib/pywget")
|
|
218
|
+
with ServerHandle(cfg) as server: # background server (tests, embedding)
|
|
219
|
+
print(server.url)
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
Legacy API (unchanged):
|
|
223
|
+
|
|
224
|
+
```python
|
|
225
|
+
from pywget import wget
|
|
226
|
+
|
|
227
|
+
wget.download("http://www.test.com/test.tar.gz", "/home/root/Downloads")
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
Errors derive from `pywget.PywgetError` (`InvalidURLError`, `NetworkError`,
|
|
231
|
+
`HTTPStatusError`, `ChecksumError`, `StorageError`, `DownloadCancelled`).
|
|
232
|
+
A `Downloader` runs one download at a time - use one instance per thread.
|
|
233
|
+
|
|
234
|
+
## Server mode and agent
|
|
235
|
+
|
|
236
|
+
Run pywget as a long-lived download manager and push links to it from anywhere - the
|
|
237
|
+
same idea as handing a link to IDM, but over a small authenticated HTTP API.
|
|
238
|
+
|
|
239
|
+
```bash
|
|
240
|
+
# 1. start the server (jobs may only write below --root)
|
|
241
|
+
pywget serve --root ~/Downloads/pywget # http://127.0.0.1:8765, token auto-created
|
|
242
|
+
|
|
243
|
+
# 2. send links (token is read from the token file on the same machine)
|
|
244
|
+
pywget send https://example.com/a.iso -p isos -r https://example.com/ --wait
|
|
245
|
+
pywget https://example.com/b.zip --server http://127.0.0.1:8765 # same, legacy form
|
|
246
|
+
pywget send c # links found in the clipboard
|
|
247
|
+
pywget send links.txt -a # a text file of links, queued paused (add only)
|
|
248
|
+
|
|
249
|
+
# 3. manage
|
|
250
|
+
pywget jobs list
|
|
251
|
+
pywget jobs pause|resume|cancel|retry|remove <id>
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
Open `http://127.0.0.1:8765/` for the web UI (paste the token once).
|
|
255
|
+
|
|
256
|
+
### Agent (relay)
|
|
257
|
+
|
|
258
|
+
An **agent** is a tiny server that does not download: it accepts links on a local port
|
|
259
|
+
and forwards them to an upstream server, keeping them in a persistent outbox when the
|
|
260
|
+
upstream is offline and mirroring progress back. Put it on the machine where links
|
|
261
|
+
appear (laptop, browser helper script) and let a server/NAS do the downloading.
|
|
262
|
+
|
|
263
|
+
```bash
|
|
264
|
+
# on the download box
|
|
265
|
+
pywget serve --host 0.0.0.0 --root /srv/downloads --tls-cert c.pem --tls-key k.pem
|
|
266
|
+
|
|
267
|
+
# on your workstation
|
|
268
|
+
pywget agent --upstream https://nas:8765 --upstream-token <TOKEN> --port 8766
|
|
269
|
+
pywget send https://example.com/file.iso --server http://127.0.0.1:8766 --wait
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
### System overview
|
|
273
|
+
|
|
274
|
+
```mermaid
|
|
275
|
+
flowchart LR
|
|
276
|
+
CLI["pywget send / --server"] --> AG
|
|
277
|
+
BR["Browser helper,<br/>scripts, curl"] --> AG
|
|
278
|
+
subgraph WS["Workstation"]
|
|
279
|
+
AG["Agent<br/>pywget agent<br/>persistent outbox"]
|
|
280
|
+
end
|
|
281
|
+
AG -- "HTTPS + token<br/>forward job, poll status" --> API
|
|
282
|
+
CLI -. "direct" .-> API
|
|
283
|
+
subgraph SV["Download server"]
|
|
284
|
+
API["HTTP API + web UI<br/>auth, Host check, root jail"] --> MGR["JobManager<br/>queue, N workers"]
|
|
285
|
+
MGR --> ENG["Downloader engine<br/>segments, resume, retry"]
|
|
286
|
+
MGR <--> ST[("jobs.json<br/>0600")]
|
|
287
|
+
ENG --> FS[("download root")]
|
|
288
|
+
end
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
[](https://i.ibb.co/96D8hz7/pywget-architecture7.jpg)
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
### Job flow
|
|
295
|
+
|
|
296
|
+
```mermaid
|
|
297
|
+
sequenceDiagram
|
|
298
|
+
autonumber
|
|
299
|
+
actor U as User
|
|
300
|
+
participant A as Agent
|
|
301
|
+
participant S as Server
|
|
302
|
+
participant W as Worker
|
|
303
|
+
U->>A: pywget send URL --wait
|
|
304
|
+
A->>A: persist job (outbox)
|
|
305
|
+
loop until accepted
|
|
306
|
+
A->>S: POST /api/v1/jobs (Bearer token)
|
|
307
|
+
S-->>A: 201 job id or error / offline then backoff
|
|
308
|
+
end
|
|
309
|
+
S->>W: enqueue
|
|
310
|
+
W->>W: Downloader.fetch (segments, resume)
|
|
311
|
+
loop every second
|
|
312
|
+
A->>S: GET /api/v1/jobs/id
|
|
313
|
+
S-->>A: status, percent, speed, ETA
|
|
314
|
+
end
|
|
315
|
+
W-->>S: completed (atomic rename in root)
|
|
316
|
+
A-->>U: OK path
|
|
317
|
+
```
|
|
318
|
+
|
|
319
|
+
[](https://i.ibb.co/4n0GBmSD/pywget-architecture8.jpg)
|
|
320
|
+
|
|
321
|
+
### Job states
|
|
322
|
+
|
|
323
|
+
```mermaid
|
|
324
|
+
stateDiagram-v2
|
|
325
|
+
[*] --> queued: add
|
|
326
|
+
[*] --> paused: add --add-only
|
|
327
|
+
queued --> running: worker free
|
|
328
|
+
running --> paused: pause (partial data kept)
|
|
329
|
+
paused --> queued: resume
|
|
330
|
+
running --> completed
|
|
331
|
+
running --> failed: error after retries
|
|
332
|
+
running --> cancelled: cancel (partial files removed)
|
|
333
|
+
queued --> cancelled
|
|
334
|
+
paused --> cancelled
|
|
335
|
+
failed --> queued: retry
|
|
336
|
+
cancelled --> queued: retry
|
|
337
|
+
running --> queued: server shutdown (auto-resumes on start)
|
|
338
|
+
completed --> [*]: remove
|
|
339
|
+
failed --> [*]: remove
|
|
340
|
+
cancelled --> [*]: remove
|
|
341
|
+
```
|
|
342
|
+
|
|
343
|
+
[](https://i.ibb.co/wNkVB4fp/pywget-architecture9.jpg)
|
|
344
|
+
|
|
345
|
+
### HTTP API
|
|
346
|
+
|
|
347
|
+
All `/api/v1/*` calls need `Authorization: Bearer <token>` (or `X-Pywget-Token`).
|
|
348
|
+
|
|
349
|
+
| Method and path | Purpose |
|
|
350
|
+
|-----------------|---------|
|
|
351
|
+
| `GET /health` | Liveness, version, mode (no auth) |
|
|
352
|
+
| `GET /` | Web UI (no auth, token entered in the page) |
|
|
353
|
+
| `POST /api/v1/jobs` | Create job(s): a job object, `{"urls": [...], ...common}` or `{"jobs": [...]}` |
|
|
354
|
+
| `GET /api/v1/jobs[?status=]` | List jobs |
|
|
355
|
+
| `GET /api/v1/jobs/{id}` | One job (secrets are never returned) |
|
|
356
|
+
| `POST /api/v1/jobs/{id}/pause\|resume\|cancel\|retry` | Control a job |
|
|
357
|
+
| `DELETE /api/v1/jobs/{id}` | Remove a finished job record |
|
|
358
|
+
| `GET /api/v1/stats` | Counts, total speed |
|
|
359
|
+
| `GET /api/v1/events` | Server-sent events (`event: job`) |
|
|
360
|
+
|
|
361
|
+
Job fields: `url`, `filename`, `path` (folder below the root), `referrer`, `cookie`
|
|
362
|
+
(string or object), `headers`, `user_agent`, `username`, `password`, `connections`,
|
|
363
|
+
`checksum`, `limit_rate`, `overwrite`, `add_only`.
|
|
364
|
+
|
|
365
|
+
```bash
|
|
366
|
+
curl -s -H "Authorization: Bearer $(cat ~/.config/pywget/token)" \
|
|
367
|
+
-d '{"url":"https://example.com/f.iso","path":"isos","referrer":"https://example.com/"}' \
|
|
368
|
+
http://127.0.0.1:8765/api/v1/jobs
|
|
369
|
+
```
|
|
370
|
+
|
|
371
|
+
### Security model
|
|
372
|
+
|
|
373
|
+
- **Token required** for every API call; generated once (`token` file, mode 0600) or given via `--token` / `$PYWGET_TOKEN`; compared in constant time.
|
|
374
|
+
- **Loopback by default.** Binding elsewhere prints a warning; use `--tls-cert/--tls-key` or an HTTPS reverse proxy.
|
|
375
|
+
- **Root jail:** job `path` can never leave `--root` (absolute paths and `..` are rejected, symlinks resolved).
|
|
376
|
+
- **No CORS** and a `Host` header allow-list (`--allowed-host`) against DNS-rebinding and drive-by browser requests.
|
|
377
|
+
- **`--deny-private-networks`** refuses URLs resolving to private/loopback/link-local addresses (SSRF guard for exposed servers).
|
|
378
|
+
- Request bodies capped at 1 MiB; headers checked for CR/LF injection; credentials and cookies are stored in `jobs.json` (0600) and never returned by the API.
|
|
379
|
+
- Not supported: POST-body downloads (IDM `post data`), FTP.
|
|
380
|
+
|
|
381
|
+
## How it works
|
|
382
|
+
|
|
383
|
+
### Download engine
|
|
384
|
+
|
|
385
|
+
```mermaid
|
|
386
|
+
flowchart LR
|
|
387
|
+
CLI["CLI<br/>pywget.cli"] --> DL
|
|
388
|
+
API["Python API<br/>pywget.download()"] --> DL
|
|
389
|
+
LEG["Legacy shim<br/>pywget.wget"] --> DL
|
|
390
|
+
JOB["LocalExecutor<br/>(server jobs)"] --> DL
|
|
391
|
+
|
|
392
|
+
subgraph Engine["pywget.core"]
|
|
393
|
+
DL["Downloader"] --> PROBE["Probe<br/>Range: bytes=0-0"]
|
|
394
|
+
PROBE --> PLAN{"Size known and<br/>ranges supported?"}
|
|
395
|
+
PLAN -- yes --> SEG["Segment workers<br/>1..N threads"]
|
|
396
|
+
PLAN -- no --> STR["Single stream<br/>worker"]
|
|
397
|
+
SEG --> MON["Monitor loop<br/>progress + state save"]
|
|
398
|
+
STR --> MON
|
|
399
|
+
MON --> VER["Verify<br/>size + checksum"]
|
|
400
|
+
end
|
|
401
|
+
|
|
402
|
+
SEG -. throttled by .-> RL["RateLimiter"]
|
|
403
|
+
STR -. throttled by .-> RL
|
|
404
|
+
MON --> TR["Tracker / ProgressBar"]
|
|
405
|
+
SEG <--> ST[("state file<br/>.pywget")]
|
|
406
|
+
VER --> OUT[("final file<br/>atomic rename")]
|
|
407
|
+
```
|
|
408
|
+
|
|
409
|
+
[](https://i.ibb.co/BVCnttHQ/pywget-architecture1.jpg)
|
|
410
|
+
|
|
411
|
+
### Download sequence
|
|
412
|
+
|
|
413
|
+
```mermaid
|
|
414
|
+
sequenceDiagram
|
|
415
|
+
autonumber
|
|
416
|
+
actor U as User
|
|
417
|
+
participant D as Downloader
|
|
418
|
+
participant S as Server
|
|
419
|
+
participant F as Disk
|
|
420
|
+
|
|
421
|
+
U->>D: fetch(url)
|
|
422
|
+
D->>S: GET Range: bytes=0-0
|
|
423
|
+
S-->>D: 206 + Content-Range (size, ETag)
|
|
424
|
+
D->>F: load .pywget state (if any)
|
|
425
|
+
alt state matches size / ETag / mtime
|
|
426
|
+
D->>D: resume remaining segments
|
|
427
|
+
else fresh start
|
|
428
|
+
D->>F: preallocate .part, split into N segments
|
|
429
|
+
end
|
|
430
|
+
par segment 1..N
|
|
431
|
+
D->>S: GET Range: bytes=a-b
|
|
432
|
+
S-->>D: 206 data
|
|
433
|
+
D->>F: write at offset
|
|
434
|
+
end
|
|
435
|
+
loop every second
|
|
436
|
+
D->>F: save state
|
|
437
|
+
D-->>U: progress callback
|
|
438
|
+
end
|
|
439
|
+
D->>F: verify size and checksum
|
|
440
|
+
D->>F: rename .part to final name
|
|
441
|
+
D-->>U: DownloadResult
|
|
442
|
+
```
|
|
443
|
+
|
|
444
|
+
[](https://i.ibb.co/DPgLTD6D/pywget-architecture2.jpg)
|
|
445
|
+
|
|
446
|
+
### Segment lifecycle and retries
|
|
447
|
+
|
|
448
|
+
```mermaid
|
|
449
|
+
stateDiagram-v2
|
|
450
|
+
[*] --> Probing
|
|
451
|
+
Probing --> Planning: size and ranges known
|
|
452
|
+
Probing --> Streaming: no range support
|
|
453
|
+
Planning --> Downloading
|
|
454
|
+
Streaming --> Verifying
|
|
455
|
+
Downloading --> Retrying: timeout / reset / 5xx / 429
|
|
456
|
+
Retrying --> Downloading: backoff + jitter, resume at offset
|
|
457
|
+
Retrying --> Failed: retries exhausted
|
|
458
|
+
Downloading --> Interrupted: Ctrl+C / SIGTERM / cancel()
|
|
459
|
+
Interrupted --> Planning: re-run, state validated
|
|
460
|
+
Downloading --> Verifying: all segments complete
|
|
461
|
+
Verifying --> Completed: size and checksum OK
|
|
462
|
+
Verifying --> Failed: mismatch, partial files removed
|
|
463
|
+
Completed --> [*]
|
|
464
|
+
Failed --> [*]
|
|
465
|
+
```
|
|
466
|
+
|
|
467
|
+
[](https://i.ibb.co/YBZKFsxG/pywget-architecture3.jpg)
|
|
468
|
+
|
|
469
|
+
### Resume decision
|
|
470
|
+
|
|
471
|
+
```mermaid
|
|
472
|
+
flowchart TD
|
|
473
|
+
A["Start"] --> B{".pywget state<br/>and .part exist?"}
|
|
474
|
+
B -- no --> N["Fresh download"]
|
|
475
|
+
B -- yes --> C{"URL, size, ETag,<br/>Last-Modified equal?"}
|
|
476
|
+
C -- no --> N
|
|
477
|
+
C -- yes --> D{"Segments valid and<br/>.part size correct?"}
|
|
478
|
+
D -- no --> N
|
|
479
|
+
D -- yes --> R["Resume each segment<br/>from its saved offset"]
|
|
480
|
+
```
|
|
481
|
+
|
|
482
|
+
[](https://i.ibb.co/nqGmzdpw/pywget-architecture4.jpg)
|
|
483
|
+
|
|
484
|
+
### Main types - engine
|
|
485
|
+
|
|
486
|
+
```mermaid
|
|
487
|
+
classDiagram
|
|
488
|
+
class Downloader {
|
|
489
|
+
+fetch(url) DownloadResult
|
|
490
|
+
+download(url) Path
|
|
491
|
+
+cancel()
|
|
492
|
+
+partial
|
|
493
|
+
}
|
|
494
|
+
class Options {
|
|
495
|
+
connections
|
|
496
|
+
retries
|
|
497
|
+
limit_rate
|
|
498
|
+
rate_limiter
|
|
499
|
+
checksum
|
|
500
|
+
progress
|
|
501
|
+
}
|
|
502
|
+
class DownloadResult {
|
|
503
|
+
path
|
|
504
|
+
size
|
|
505
|
+
resumed
|
|
506
|
+
connections
|
|
507
|
+
checksum
|
|
508
|
+
}
|
|
509
|
+
class Tracker
|
|
510
|
+
class ProgressBar
|
|
511
|
+
class RateLimiter
|
|
512
|
+
Downloader o-- Options
|
|
513
|
+
Downloader --> DownloadResult
|
|
514
|
+
Downloader --> Tracker
|
|
515
|
+
Downloader --> RateLimiter
|
|
516
|
+
ProgressBar ..> Tracker : renders snapshots
|
|
517
|
+
```
|
|
518
|
+
|
|
519
|
+
[](https://i.ibb.co/d0kCy05W/pywget-architecture5.jpg)
|
|
520
|
+
|
|
521
|
+
### Main types - server and agent
|
|
522
|
+
|
|
523
|
+
```mermaid
|
|
524
|
+
classDiagram
|
|
525
|
+
class ServerHandle {
|
|
526
|
+
+start()
|
|
527
|
+
+close()
|
|
528
|
+
+url
|
|
529
|
+
}
|
|
530
|
+
class PywgetHTTPServer
|
|
531
|
+
class ApiHandler {
|
|
532
|
+
auth, Host check
|
|
533
|
+
routes /api/v1/*
|
|
534
|
+
}
|
|
535
|
+
class JobManager {
|
|
536
|
+
+add(spec) Job
|
|
537
|
+
+pause(id)
|
|
538
|
+
+resume(id)
|
|
539
|
+
+cancel(id)
|
|
540
|
+
+retry(id)
|
|
541
|
+
+remove(id)
|
|
542
|
+
}
|
|
543
|
+
class Job
|
|
544
|
+
class JobStore
|
|
545
|
+
class EventBus
|
|
546
|
+
class LocalExecutor
|
|
547
|
+
class RelayExecutor
|
|
548
|
+
class ServerClient
|
|
549
|
+
class ServerConfig
|
|
550
|
+
ServerHandle --> PywgetHTTPServer
|
|
551
|
+
ServerHandle --> JobManager
|
|
552
|
+
PywgetHTTPServer --> ApiHandler
|
|
553
|
+
ApiHandler --> JobManager
|
|
554
|
+
JobManager o-- Job
|
|
555
|
+
JobManager --> JobStore : persists
|
|
556
|
+
JobManager --> EventBus : SSE
|
|
557
|
+
JobManager --> LocalExecutor : server mode
|
|
558
|
+
JobManager --> RelayExecutor : agent mode
|
|
559
|
+
LocalExecutor --> Downloader
|
|
560
|
+
RelayExecutor --> ServerClient : forwards to upstream
|
|
561
|
+
ServerHandle o-- ServerConfig
|
|
562
|
+
```
|
|
563
|
+
|
|
564
|
+
[](https://i.ibb.co/zVMXCFtz/pywget-architecture6.jpg)
|
|
565
|
+
|
|
566
|
+
### Module map
|
|
567
|
+
|
|
568
|
+
| Module | Role |
|
|
569
|
+
|--------|------|
|
|
570
|
+
| `pywget.core` | Download engine (`Downloader`, `Options`) |
|
|
571
|
+
| `pywget.progress`, `pywget.utils`, `pywget.errors` | Progress bar, helpers, exceptions and exit codes |
|
|
572
|
+
| `pywget.cli` | CLI: direct download, `serve`, `agent`, `send`, `jobs` |
|
|
573
|
+
| `pywget.server` | HTTP API, SSE, web UI hosting (`ServerHandle`, `run_server`) |
|
|
574
|
+
| `pywget.jobs` | `Job`, `JobManager`, persistence, `LocalExecutor` / `RelayExecutor` |
|
|
575
|
+
| `pywget.client` | `ServerClient` (used by `send`, `jobs` and the agent) |
|
|
576
|
+
| `pywget.config` | Token file, config dir, `ServerConfig` |
|
|
577
|
+
| `pywget.clipboard` | `c` input: clipboard and URL extraction |
|
|
578
|
+
| `pywget.webui` | Embedded single-page web UI |
|
|
579
|
+
| `pywget.wget` | Backward-compatible `wget.download()` |
|
|
580
|
+
|
|
581
|
+
## Behaviour details
|
|
582
|
+
|
|
583
|
+
- **Probing:** one `Range: bytes=0-0` request yields size, range support, ETag, `Last-Modified` and `Content-Disposition`.
|
|
584
|
+
- **Segments:** a file is split into `min(connections, size / 2 MiB)` parts; small files use one connection.
|
|
585
|
+
- **Existing file:** same size means skipped; different size is saved as `name.1` (wget style) unless `--overwrite`.
|
|
586
|
+
- **Partial files:** stored as `name.part` + `name.pywget`; removed on success. Changed remote file (size/ETag/mtime) restarts cleanly.
|
|
587
|
+
- **Redirects:** only http/https; `Authorization`/`Cookie` are dropped when the origin changes.
|
|
588
|
+
- **Server jobs:** persisted in `jobs.json`; a job running at shutdown is re-queued and resumes from its `.part` data.
|
|
589
|
+
- **Not supported (by design):** FTP, recursive mirroring, POST-body downloads.
|
|
590
|
+
|
|
591
|
+
## Development
|
|
592
|
+
|
|
593
|
+
```bash
|
|
594
|
+
git clone https://github.com/cumulus13/pywget && cd pywget
|
|
595
|
+
pip install -e ".[dev]"
|
|
596
|
+
pytest --cov=pywget
|
|
597
|
+
ruff check pywget tests && mypy pywget
|
|
598
|
+
python -m build
|
|
599
|
+
```
|
|
600
|
+
|
|
601
|
+
## License
|
|
602
|
+
|
|
603
|
+
[BSD](LICENSE)
|
|
604
|
+
|
|
605
|
+
## Author
|
|
606
|
+
|
|
607
|
+
[Hadi Cahyadi](https://github.com/cumulus13)
|
|
608
|
+
|
|
609
|
+
[](https://www.buymeacoffee.com/cumulus13)
|
|
610
|
+
[](https://ko-fi.com/cumulus13)
|
|
611
|
+
[Support me on Patreon](https://www.patreon.com/cumulus13)
|