nercone-fastget 4.6.2__py3-none-any.whl
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.
|
File without changes
|
nercone_fastget/cli.py
ADDED
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
import os, math, requests, argparse
|
|
2
|
+
from importlib.metadata import version
|
|
3
|
+
from typing import Union, Literal
|
|
4
|
+
from decimal import Decimal, ROUND_HALF_UP, ROUND_DOWN
|
|
5
|
+
from threading import Thread, Lock, Event
|
|
6
|
+
from urllib.parse import urlparse, unquote
|
|
7
|
+
from datetime import datetime, timezone
|
|
8
|
+
from nercone_modern.logging import ModernLogging
|
|
9
|
+
from nercone_modern.progressbar import ModernProgressBar
|
|
10
|
+
|
|
11
|
+
VERSION = version("nercone-fastget")
|
|
12
|
+
CHUNK = 1024 * 128 # 128KB
|
|
13
|
+
|
|
14
|
+
progress_lock = Lock()
|
|
15
|
+
stop_event = Event()
|
|
16
|
+
|
|
17
|
+
def get_file_name(url):
|
|
18
|
+
return unquote(os.path.basename(urlparse(url).path))
|
|
19
|
+
|
|
20
|
+
def get_file_size(url):
|
|
21
|
+
response = requests.head(url, allow_redirects=True)
|
|
22
|
+
if response.status_code in [200, 302]:
|
|
23
|
+
file_size = int(response.headers.get('Content-Length', 0))
|
|
24
|
+
accept_ranges = response.headers.get('Accept-Ranges', 'none')
|
|
25
|
+
reject_fastget = response.headers.get('RejectFastGet', '').strip().lower() in ['1', 'y', 'yes', 'true', 'enabled']
|
|
26
|
+
return file_size, accept_ranges.lower() == 'bytes', reject_fastget
|
|
27
|
+
else:
|
|
28
|
+
raise Exception(f"Failed to retrieve file info. Status code: {response.status_code}")
|
|
29
|
+
|
|
30
|
+
def format_filesize(num_bytes: Union[int, float], *, decimals: int = 2, rounding: Literal["half_up", "floor"] = "half_up", strip_trailing_zeros: bool = False) -> str:
|
|
31
|
+
units = ["B", "KB", "MB", "GB", "TB", "PB", "EB"]
|
|
32
|
+
try:
|
|
33
|
+
n = int(num_bytes)
|
|
34
|
+
except Exception as e:
|
|
35
|
+
raise TypeError("num_bytes must be a number that can be converted to an integer") from e
|
|
36
|
+
sign = "-" if n < 0 else ""
|
|
37
|
+
b = abs(n)
|
|
38
|
+
if b < 1000:
|
|
39
|
+
return f"{sign}{b} B ({sign}{b:,} B)"
|
|
40
|
+
value = Decimal(b)
|
|
41
|
+
idx = 0
|
|
42
|
+
thousand = Decimal(1000)
|
|
43
|
+
while value >= thousand and idx < len(units) - 1:
|
|
44
|
+
value /= thousand
|
|
45
|
+
idx += 1
|
|
46
|
+
q = Decimal(f"1e-{decimals}") if decimals > 0 else Decimal(1)
|
|
47
|
+
mode = ROUND_HALF_UP if rounding == "half_up" else ROUND_DOWN
|
|
48
|
+
rounded = value.quantize(q, rounding=mode)
|
|
49
|
+
if strip_trailing_zeros and decimals > 0:
|
|
50
|
+
short = f"{rounded.normalize()}"
|
|
51
|
+
if "E" in short or "e" in short:
|
|
52
|
+
short = f"{rounded:.{decimals}f}".rstrip("0").rstrip(".")
|
|
53
|
+
else:
|
|
54
|
+
short = f"{rounded:.{decimals}f}" if decimals > 0 else f"{int(rounded)}"
|
|
55
|
+
return f"{sign}{short}{units[idx]} ({sign}{b:,} B)"
|
|
56
|
+
|
|
57
|
+
def download_range(url, start, end, part_num, output, threads, all_bar: ModernProgressBar, thread_bar: ModernProgressBar, headers=None, stop: Event = None):
|
|
58
|
+
headers = headers or {}
|
|
59
|
+
headers.update({'User-Agent': f'FastGet/{VERSION} (Downloading with {threads} Thread(s), {part_num} Part(s), https://github.com/DiamondGotCat/FastGet/)'})
|
|
60
|
+
headers.update({'Range': f'bytes={start}-{end}'})
|
|
61
|
+
if stop and stop.is_set():
|
|
62
|
+
return
|
|
63
|
+
try:
|
|
64
|
+
response = requests.get(url, headers=headers, stream=True)
|
|
65
|
+
response.raise_for_status()
|
|
66
|
+
except requests.RequestException as e:
|
|
67
|
+
thread_bar.setMessage(f"RequestException: {e}")
|
|
68
|
+
return
|
|
69
|
+
|
|
70
|
+
part_path = f"{output}.part{part_num}"
|
|
71
|
+
try:
|
|
72
|
+
with open(part_path, 'wb') as f:
|
|
73
|
+
for chunk in response.iter_content(chunk_size=CHUNK):
|
|
74
|
+
if stop and stop.is_set():
|
|
75
|
+
break
|
|
76
|
+
if not chunk:
|
|
77
|
+
continue
|
|
78
|
+
f.write(chunk)
|
|
79
|
+
with progress_lock:
|
|
80
|
+
thread_bar.update()
|
|
81
|
+
all_bar.update()
|
|
82
|
+
finally:
|
|
83
|
+
try:
|
|
84
|
+
response.close()
|
|
85
|
+
except Exception:
|
|
86
|
+
pass
|
|
87
|
+
|
|
88
|
+
def merge_files(parts, output_file):
|
|
89
|
+
total_size = 0
|
|
90
|
+
for part in parts:
|
|
91
|
+
try:
|
|
92
|
+
total_size += os.path.getsize(part)
|
|
93
|
+
except OSError:
|
|
94
|
+
pass
|
|
95
|
+
|
|
96
|
+
total_steps = max(1, math.ceil(total_size / CHUNK))
|
|
97
|
+
merge_bar = ModernProgressBar(total=total_steps, process_name="Marge", spinner_mode=False, box_left="(", box_right=")", show_vertical_bar=True)
|
|
98
|
+
merge_bar.start()
|
|
99
|
+
|
|
100
|
+
try:
|
|
101
|
+
with open(output_file, 'wb') as outfile:
|
|
102
|
+
for part in parts:
|
|
103
|
+
if not os.path.exists(part):
|
|
104
|
+
continue
|
|
105
|
+
with open(part, 'rb') as infile:
|
|
106
|
+
while True:
|
|
107
|
+
chunk = infile.read(CHUNK)
|
|
108
|
+
if not chunk:
|
|
109
|
+
break
|
|
110
|
+
outfile.write(chunk)
|
|
111
|
+
merge_bar.update()
|
|
112
|
+
finally:
|
|
113
|
+
merge_bar.finish()
|
|
114
|
+
|
|
115
|
+
for part in parts:
|
|
116
|
+
try:
|
|
117
|
+
os.remove(part)
|
|
118
|
+
except OSError:
|
|
119
|
+
pass
|
|
120
|
+
|
|
121
|
+
def main():
|
|
122
|
+
logger = ModernLogging("fastget", show_level=False, show_proc=False)
|
|
123
|
+
parser = argparse.ArgumentParser(prog='fastget', description='High-speed File Downloading Tool')
|
|
124
|
+
parser.add_argument('url')
|
|
125
|
+
parser.add_argument('-o', '--output', default=None)
|
|
126
|
+
parser.add_argument('-t', '--threads', default=4, type=int)
|
|
127
|
+
args = parser.parse_args()
|
|
128
|
+
|
|
129
|
+
if args.output is None:
|
|
130
|
+
args.output = get_file_name(args.url)
|
|
131
|
+
|
|
132
|
+
try:
|
|
133
|
+
file_size, is_resumable, is_fastget_rejected = get_file_size(args.url)
|
|
134
|
+
except Exception as e:
|
|
135
|
+
logger.log(f"{e}", "CRITICAL")
|
|
136
|
+
return
|
|
137
|
+
|
|
138
|
+
logger.log(f"Total file size: {format_filesize(file_size)}")
|
|
139
|
+
|
|
140
|
+
threads = args.threads
|
|
141
|
+
cannot_use_parallel_downloads = False
|
|
142
|
+
if is_fastget_rejected:
|
|
143
|
+
cannot_use_parallel_downloads = True
|
|
144
|
+
logger.log("Server has rejected FastGet parallel downloads.", "ERROR")
|
|
145
|
+
elif not is_resumable:
|
|
146
|
+
cannot_use_parallel_downloads = True
|
|
147
|
+
logger.log("The specified server does not support ranged file downloads.", "ERROR")
|
|
148
|
+
logger.log("Therefore, parallel downloads are not available.", "ERROR")
|
|
149
|
+
if cannot_use_parallel_downloads:
|
|
150
|
+
use_regular_download_instead = logger.prompt("Would you like to use a single-threaded regular download instead?", default="Y", choices=["Y", "n"], interrupt_default="n")
|
|
151
|
+
if use_regular_download_instead == "Y":
|
|
152
|
+
threads = 1
|
|
153
|
+
else:
|
|
154
|
+
logger.log("Aborted.")
|
|
155
|
+
return
|
|
156
|
+
|
|
157
|
+
part_size = file_size // threads if threads > 0 else file_size
|
|
158
|
+
parts = [f"{args.output}.part{i}" for i in range(threads)]
|
|
159
|
+
|
|
160
|
+
total_download_steps = max(1, math.ceil(file_size / CHUNK))
|
|
161
|
+
download_bar_all = ModernProgressBar(total=total_download_steps, process_name="DL All", spinner_mode=False, box_left="(", box_right=")", show_vertical_bar=True)
|
|
162
|
+
|
|
163
|
+
thread_bars = []
|
|
164
|
+
for i in range(threads):
|
|
165
|
+
start = part_size * i
|
|
166
|
+
end = file_size - 1 if i == threads - 1 else start + part_size - 1
|
|
167
|
+
part_bytes = max(0, end - start + 1)
|
|
168
|
+
part_steps = max(1, math.ceil(part_bytes / CHUNK))
|
|
169
|
+
bar = ModernProgressBar(total=part_steps, process_name=f"DL #{i + 1}", spinner_mode=False, box_left="(", box_right=")", show_vertical_bar=True)
|
|
170
|
+
thread_bars.append(bar)
|
|
171
|
+
|
|
172
|
+
download_bar_all.start()
|
|
173
|
+
for bar in thread_bars:
|
|
174
|
+
bar.start()
|
|
175
|
+
|
|
176
|
+
start_time = datetime.now(timezone.utc)
|
|
177
|
+
thread_objs = []
|
|
178
|
+
try:
|
|
179
|
+
for i in range(threads):
|
|
180
|
+
start = part_size * i
|
|
181
|
+
end = file_size - 1 if i == threads - 1 else start + part_size - 1
|
|
182
|
+
thread = Thread(
|
|
183
|
+
target=download_range,
|
|
184
|
+
args=(args.url, start, end, i, args.output, threads, download_bar_all, thread_bars[i], None, stop_event)
|
|
185
|
+
)
|
|
186
|
+
thread_objs.append(thread)
|
|
187
|
+
thread.start()
|
|
188
|
+
|
|
189
|
+
for thread in thread_objs:
|
|
190
|
+
thread.join()
|
|
191
|
+
except KeyboardInterrupt:
|
|
192
|
+
stop_event.set()
|
|
193
|
+
logger.log("Interrupted. Stopping threads and cleaning up...")
|
|
194
|
+
for thread in thread_objs:
|
|
195
|
+
try:
|
|
196
|
+
thread.join()
|
|
197
|
+
except Exception:
|
|
198
|
+
pass
|
|
199
|
+
for part in parts:
|
|
200
|
+
try:
|
|
201
|
+
if os.path.exists(part):
|
|
202
|
+
os.remove(part)
|
|
203
|
+
except Exception:
|
|
204
|
+
pass
|
|
205
|
+
try:
|
|
206
|
+
if os.path.exists(args.output):
|
|
207
|
+
os.remove(args.output)
|
|
208
|
+
except Exception:
|
|
209
|
+
pass
|
|
210
|
+
return
|
|
211
|
+
|
|
212
|
+
end_time = datetime.now(timezone.utc)
|
|
213
|
+
delta = end_time - start_time
|
|
214
|
+
duration_ms = delta.days*24*3600*1000 + delta.seconds*1000 + delta.microseconds//1000
|
|
215
|
+
|
|
216
|
+
download_bar_all.finish()
|
|
217
|
+
for bar in thread_bars:
|
|
218
|
+
bar.finish()
|
|
219
|
+
|
|
220
|
+
try:
|
|
221
|
+
merge_files(parts, args.output)
|
|
222
|
+
logger.log(f"Download completed in {duration_ms}ms")
|
|
223
|
+
logger.log(f"Saved to: {args.output}")
|
|
224
|
+
except Exception as e:
|
|
225
|
+
logger.log(f"Error merging files: {e}", "CRITICAL")
|
|
226
|
+
|
|
227
|
+
if __name__ == "__main__":
|
|
228
|
+
main()
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
|
+
Name: nercone-fastget
|
|
3
|
+
Version: 4.6.2
|
|
4
|
+
Summary: High-speed File Downloading Tool
|
|
5
|
+
Author: Nercone
|
|
6
|
+
Author-email: Nercone <nercone@diamondgotcat.net>
|
|
7
|
+
License: MIT
|
|
8
|
+
Classifier: Programming Language :: Python :: 3
|
|
9
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
10
|
+
Classifier: Operating System :: OS Independent
|
|
11
|
+
Requires-Dist: nercone-modern
|
|
12
|
+
Requires-Dist: rich
|
|
13
|
+
Requires-Dist: requests
|
|
14
|
+
Requires-Python: >=3.8
|
|
15
|
+
Description-Content-Type: text/markdown
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
<img width="1920" alt="fastget" src="https://github.com/user-attachments/assets/6a8dfeb7-7714-4bf5-a890-555a49436ef8" />
|
|
19
|
+
|
|
20
|
+
# FastGet
|
|
21
|
+
High-speed File Downloading Tool
|
|
22
|
+
|
|
23
|
+
## How
|
|
24
|
+
It's using Multiple Thread Download.
|
|
25
|
+
|
|
26
|
+
This is need Range-select feature, in Server-side.
|
|
27
|
+
|
|
28
|
+
## Requiments
|
|
29
|
+
- CPython 3.9+
|
|
30
|
+
- `uv` [PyPI↗︎](https://pypi.org/project/uv/) or `pip3` [PyPI↗︎](https://pypi.org/project/pip/)
|
|
31
|
+
- `nercone-modern` [PyPI↗︎](https://pypi.org/project/nercone-modern/)
|
|
32
|
+
- `rich` [PyPI↗︎](https://pypi.org/project/rich/)
|
|
33
|
+
- `requests` [PyPI↗︎](https://pypi.org/project/requests/)
|
|
34
|
+
|
|
35
|
+
## Installation
|
|
36
|
+
|
|
37
|
+
### using uv (recommended)
|
|
38
|
+
```
|
|
39
|
+
uv tool install nercone-fastget
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
### using pip3
|
|
43
|
+
|
|
44
|
+
**System Python:**
|
|
45
|
+
```
|
|
46
|
+
pip3 install nercone-fastget --break-system-packages
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
**Venv Python:**
|
|
50
|
+
```
|
|
51
|
+
pip3 install nercone-fastget
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## Update
|
|
55
|
+
|
|
56
|
+
### using uv (recommended)
|
|
57
|
+
```
|
|
58
|
+
uv tool install nercone-fastget --upgrade
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
### using pip3
|
|
62
|
+
|
|
63
|
+
**System Python:**
|
|
64
|
+
```
|
|
65
|
+
pip3 install nercone-fastget --upgrade --break-system-packages
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
**Venv Python:**
|
|
69
|
+
```
|
|
70
|
+
pip3 install nercone-fastget --upgrade
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
## Usage
|
|
74
|
+
|
|
75
|
+
### Show helps
|
|
76
|
+
```
|
|
77
|
+
fastget [-h] [--help]
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
```
|
|
81
|
+
nercone@demo ~> fastget -h
|
|
82
|
+
usage: FastGet [-h] [-o OUTPUT] [-t THREADS] url
|
|
83
|
+
|
|
84
|
+
High-speed File Downloading Tool
|
|
85
|
+
|
|
86
|
+
positional arguments:
|
|
87
|
+
url
|
|
88
|
+
|
|
89
|
+
options:
|
|
90
|
+
-h, --help show this help message and exit
|
|
91
|
+
-o OUTPUT, --output OUTPUT
|
|
92
|
+
-t THREADS, --threads THREADS
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
### Download with default number of threads
|
|
96
|
+
```
|
|
97
|
+
fastget <url>
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
```
|
|
101
|
+
nercone@demo ~> fastget https://download.fedoraproject.org/pub/fedora/linux/releases/43/Workstation/x86_64/iso/Fedora-Workstation-Live-43-1.6.x86_64.iso
|
|
102
|
+
Total file size: 2.74GB (2,742,190,080 B)
|
|
103
|
+
(---------------------) DL All - 7% ( 1552/20922) | No Message
|
|
104
|
+
(---------------------) DL #1 - 7% ( 348/5231) | No Message
|
|
105
|
+
(---------------------) DL #2 - 12% ( 637/5231) | No Message
|
|
106
|
+
(---------------------) DL #3 - 6% ( 315/5231) | No Message
|
|
107
|
+
(---------------------) DL #4 - 5% ( 252/5231) | No Message
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
### Download with custom number of threads
|
|
111
|
+
```
|
|
112
|
+
fastget <url> [-t <number of threads>]
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
```
|
|
116
|
+
nercone@demo ~> fastget https://download.fedoraproject.org/pub/fedora/linux/releases/43/Workstation/x86_64/iso/Fedora-Workstation-Live-43-1.6.x86_64.iso -t 8
|
|
117
|
+
Total file size: 2.74GB (2,742,190,080 B)
|
|
118
|
+
(---------------------) DL All - 15% ( 3142/20922) | No Message
|
|
119
|
+
(---------------------) DL #1 - 19% ( 496/2616) | No Message
|
|
120
|
+
(---------------------) DL #2 - 12% ( 306/2616) | No Message
|
|
121
|
+
(---------------------) DL #3 - 21% ( 562/2616) | No Message
|
|
122
|
+
(---------------------) DL #4 - 14% ( 361/2616) | No Message
|
|
123
|
+
(---------------------) DL #5 - 20% ( 533/2616) | No Message
|
|
124
|
+
(---------------------) DL #6 - 9% ( 225/2616) | No Message
|
|
125
|
+
(---------------------) DL #7 - 14% ( 368/2616) | No Message
|
|
126
|
+
(---------------------) DL #8 - 11% ( 292/2616) | No Message
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
---
|
|
130
|
+
|
|
131
|
+

|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
nercone_fastget/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
|
+
nercone_fastget/__main__.py,sha256=z3OQolCKD8rUd3pKYp_FISq2VctFhFl1mV-DVnaoNOM,29
|
|
3
|
+
nercone_fastget/cli.py,sha256=AaofoYTq3P4_QLUdFETFPplCcSjkroU06kpFZ-4hEuc,8592
|
|
4
|
+
nercone_fastget-4.6.2.dist-info/WHEEL,sha256=YUH1mBqsx8Dh2cQG2rlcuRYUhJddG9iClegy4IgnHik,79
|
|
5
|
+
nercone_fastget-4.6.2.dist-info/entry_points.txt,sha256=-1A9hV2XHhnm318USQju0yNuQWRhtHpnZw9ba9GxYRE,54
|
|
6
|
+
nercone_fastget-4.6.2.dist-info/METADATA,sha256=cEtLbPUv87GRWPimArKLIwKRNJjHuLlbyMQ6hLaUh2s,3396
|
|
7
|
+
nercone_fastget-4.6.2.dist-info/RECORD,,
|