nercone-fastget 4.3.0__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.
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
|
+
Name: nercone-fastget
|
|
3
|
+
Version: 4.3.0
|
|
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/70174f0e-8cb3-4521-961b-c910b9be2e29" />
|
|
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
|
+
- `requests` [PyPI↗︎](https://pypi.org/project/requests/)
|
|
31
|
+
- `rich` [PyPI↗︎](https://pypi.org/project/rich/)
|
|
32
|
+
- `nercone-modern` [PyPI↗︎](https://pypi.org/project/nercone-modern/)
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
|
|
2
|
+
<img width="1920" alt="fastget" src="https://github.com/user-attachments/assets/70174f0e-8cb3-4521-961b-c910b9be2e29" />
|
|
3
|
+
|
|
4
|
+
# FastGet
|
|
5
|
+
High-speed File Downloading Tool
|
|
6
|
+
|
|
7
|
+
## How
|
|
8
|
+
It's using Multiple Thread Download.
|
|
9
|
+
|
|
10
|
+
This is need Range-select feature, in Server-side.
|
|
11
|
+
|
|
12
|
+
## Requiments
|
|
13
|
+
- CPython 3.9+
|
|
14
|
+
- `requests` [PyPI↗︎](https://pypi.org/project/requests/)
|
|
15
|
+
- `rich` [PyPI↗︎](https://pypi.org/project/rich/)
|
|
16
|
+
- `nercone-modern` [PyPI↗︎](https://pypi.org/project/nercone-modern/)
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["uv_build>=0.9.5,<0.10.0"]
|
|
3
|
+
build-backend = "uv_build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "nercone-fastget"
|
|
7
|
+
version = "4.3.0"
|
|
8
|
+
description = "High-speed File Downloading Tool"
|
|
9
|
+
readme = { file = "README.md", content-type = "text/markdown" }
|
|
10
|
+
authors = [
|
|
11
|
+
{ name = "Nercone", email = "nercone@diamondgotcat.net" }
|
|
12
|
+
]
|
|
13
|
+
license = { text = "MIT" }
|
|
14
|
+
requires-python = ">=3.8"
|
|
15
|
+
dependencies = [
|
|
16
|
+
"nercone-modern",
|
|
17
|
+
"rich",
|
|
18
|
+
"requests"
|
|
19
|
+
]
|
|
20
|
+
classifiers = [
|
|
21
|
+
"Programming Language :: Python :: 3",
|
|
22
|
+
"License :: OSI Approved :: MIT License",
|
|
23
|
+
"Operating System :: OS Independent"
|
|
24
|
+
]
|
|
25
|
+
|
|
26
|
+
[project.scripts]
|
|
27
|
+
fastget = "fastget.cli:main"
|
|
File without changes
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
import os, math, requests, argparse
|
|
2
|
+
from typing import Union, Literal
|
|
3
|
+
from decimal import Decimal, ROUND_HALF_UP, ROUND_DOWN
|
|
4
|
+
from threading import Thread, Lock, Event
|
|
5
|
+
from urllib.parse import urlparse, unquote
|
|
6
|
+
from rich.console import Console
|
|
7
|
+
from rich.prompt import Prompt
|
|
8
|
+
from datetime import datetime, timezone
|
|
9
|
+
from nercone_modern.progressbar import ModernProgressBar
|
|
10
|
+
|
|
11
|
+
console = Console()
|
|
12
|
+
|
|
13
|
+
VERSION = "4.2"
|
|
14
|
+
CHUNK = 1024 * 128 # 128KB
|
|
15
|
+
|
|
16
|
+
progress_lock = Lock()
|
|
17
|
+
stop_event = Event()
|
|
18
|
+
|
|
19
|
+
def get_file_name(url):
|
|
20
|
+
return unquote(os.path.basename(urlparse(url).path))
|
|
21
|
+
|
|
22
|
+
def get_file_size(url):
|
|
23
|
+
response = requests.head(url, allow_redirects=True)
|
|
24
|
+
if response.status_code in [200, 302]:
|
|
25
|
+
file_size = int(response.headers.get('Content-Length', 0))
|
|
26
|
+
accept_ranges = response.headers.get('Accept-Ranges', 'none')
|
|
27
|
+
reject_fastget = response.headers.get('RejectFastGet', '').strip().lower() in ['1', 'y', 'yes', 'true', 'enabled']
|
|
28
|
+
return file_size, accept_ranges.lower() == 'bytes', reject_fastget
|
|
29
|
+
else:
|
|
30
|
+
raise Exception(f"Failed to retrieve file info. Status code: {response.status_code}")
|
|
31
|
+
|
|
32
|
+
def format_filesize(num_bytes: Union[int, float], *, decimals: int = 2, rounding: Literal["half_up", "floor"] = "half_up", strip_trailing_zeros: bool = False) -> str:
|
|
33
|
+
units = ["B", "KB", "MB", "GB", "TB", "PB", "EB"]
|
|
34
|
+
try:
|
|
35
|
+
n = int(num_bytes)
|
|
36
|
+
except Exception as e:
|
|
37
|
+
raise TypeError("num_bytes must be a number that can be converted to an integer") from e
|
|
38
|
+
sign = "-" if n < 0 else ""
|
|
39
|
+
b = abs(n)
|
|
40
|
+
if b < 1000:
|
|
41
|
+
return f"{sign}{b} B ({sign}{b:,} B)"
|
|
42
|
+
value = Decimal(b)
|
|
43
|
+
idx = 0
|
|
44
|
+
thousand = Decimal(1000)
|
|
45
|
+
while value >= thousand and idx < len(units) - 1:
|
|
46
|
+
value /= thousand
|
|
47
|
+
idx += 1
|
|
48
|
+
q = Decimal(f"1e-{decimals}") if decimals > 0 else Decimal(1)
|
|
49
|
+
mode = ROUND_HALF_UP if rounding == "half_up" else ROUND_DOWN
|
|
50
|
+
rounded = value.quantize(q, rounding=mode)
|
|
51
|
+
if strip_trailing_zeros and decimals > 0:
|
|
52
|
+
short = f"{rounded.normalize()}"
|
|
53
|
+
if "E" in short or "e" in short:
|
|
54
|
+
short = f"{rounded:.{decimals}f}".rstrip("0").rstrip(".")
|
|
55
|
+
else:
|
|
56
|
+
short = f"{rounded:.{decimals}f}" if decimals > 0 else f"{int(rounded)}"
|
|
57
|
+
return f"{sign}{short}{units[idx]} ({sign}{b:,} B)"
|
|
58
|
+
|
|
59
|
+
def download_range(url, start, end, part_num, output, threads, all_bar: ModernProgressBar, thread_bar: ModernProgressBar, headers=None, stop: Event = None):
|
|
60
|
+
headers = headers or {}
|
|
61
|
+
headers.update({'User-Agent': f'FastGet/{VERSION} (Downloading with {threads} Thread(s), {part_num} Part(s), https://github.com/DiamondGotCat/FastGet/)'})
|
|
62
|
+
headers.update({'Range': f'bytes={start}-{end}'})
|
|
63
|
+
if stop and stop.is_set():
|
|
64
|
+
return
|
|
65
|
+
try:
|
|
66
|
+
response = requests.get(url, headers=headers, stream=True)
|
|
67
|
+
response.raise_for_status()
|
|
68
|
+
except requests.RequestException as e:
|
|
69
|
+
thread_bar.setMessage(f"RequestException: {e}")
|
|
70
|
+
return
|
|
71
|
+
|
|
72
|
+
part_path = f"{output}.part{part_num}"
|
|
73
|
+
try:
|
|
74
|
+
with open(part_path, 'wb') as f:
|
|
75
|
+
for chunk in response.iter_content(chunk_size=CHUNK):
|
|
76
|
+
if stop and stop.is_set():
|
|
77
|
+
break
|
|
78
|
+
if not chunk:
|
|
79
|
+
continue
|
|
80
|
+
f.write(chunk)
|
|
81
|
+
with progress_lock:
|
|
82
|
+
thread_bar.update()
|
|
83
|
+
all_bar.update()
|
|
84
|
+
finally:
|
|
85
|
+
try:
|
|
86
|
+
response.close()
|
|
87
|
+
except Exception:
|
|
88
|
+
pass
|
|
89
|
+
|
|
90
|
+
def merge_files(parts, output_file):
|
|
91
|
+
total_size = 0
|
|
92
|
+
for part in parts:
|
|
93
|
+
try:
|
|
94
|
+
total_size += os.path.getsize(part)
|
|
95
|
+
except OSError:
|
|
96
|
+
pass
|
|
97
|
+
|
|
98
|
+
total_steps = max(1, math.ceil(total_size / CHUNK))
|
|
99
|
+
merge_bar = ModernProgressBar(total=total_steps, process_name="Marge", spinner_mode=False)
|
|
100
|
+
merge_bar.start()
|
|
101
|
+
|
|
102
|
+
try:
|
|
103
|
+
with open(output_file, 'wb') as outfile:
|
|
104
|
+
for part in parts:
|
|
105
|
+
if not os.path.exists(part):
|
|
106
|
+
continue
|
|
107
|
+
with open(part, 'rb') as infile:
|
|
108
|
+
while True:
|
|
109
|
+
chunk = infile.read(CHUNK)
|
|
110
|
+
if not chunk:
|
|
111
|
+
break
|
|
112
|
+
outfile.write(chunk)
|
|
113
|
+
merge_bar.update()
|
|
114
|
+
finally:
|
|
115
|
+
merge_bar.finish()
|
|
116
|
+
|
|
117
|
+
for part in parts:
|
|
118
|
+
try:
|
|
119
|
+
os.remove(part)
|
|
120
|
+
except OSError:
|
|
121
|
+
pass
|
|
122
|
+
|
|
123
|
+
def main():
|
|
124
|
+
parser = argparse.ArgumentParser(prog='FastGet', description='High-speed File Downloading Tool')
|
|
125
|
+
parser.add_argument('url')
|
|
126
|
+
parser.add_argument('-o', '--output', default=None)
|
|
127
|
+
parser.add_argument('-t', '--threads', default=4, type=int)
|
|
128
|
+
args = parser.parse_args()
|
|
129
|
+
|
|
130
|
+
if args.output is None:
|
|
131
|
+
args.output = get_file_name(args.url)
|
|
132
|
+
|
|
133
|
+
try:
|
|
134
|
+
file_size, is_resumable, is_fastget_rejected = get_file_size(args.url)
|
|
135
|
+
except Exception as e:
|
|
136
|
+
console.print(f"[red]Error: {e}[/red]")
|
|
137
|
+
return
|
|
138
|
+
|
|
139
|
+
console.print(f"[blue][bold]Total file size:[/bold] [not bold]{format_filesize(file_size)}[/not bold][/blue]")
|
|
140
|
+
|
|
141
|
+
threads = args.threads
|
|
142
|
+
if is_fastget_rejected:
|
|
143
|
+
console.print("[yellow]Server has rejected FastGet parallel downloads. Downloading in single thread...[/yellow]")
|
|
144
|
+
threads = 1
|
|
145
|
+
elif not is_resumable:
|
|
146
|
+
console.print("[yellow]Server has not supported multiple threads. Downloading in single thread...[/yellow]")
|
|
147
|
+
threads = 1
|
|
148
|
+
|
|
149
|
+
part_size = file_size // threads if threads > 0 else file_size
|
|
150
|
+
parts = [f"{args.output}.part{i}" for i in range(threads)]
|
|
151
|
+
|
|
152
|
+
total_download_steps = max(1, math.ceil(file_size / CHUNK))
|
|
153
|
+
download_bar_all = ModernProgressBar(total=total_download_steps, process_name="DL All", spinner_mode=False)
|
|
154
|
+
|
|
155
|
+
thread_bars = []
|
|
156
|
+
for i in range(threads):
|
|
157
|
+
start = part_size * i
|
|
158
|
+
end = file_size - 1 if i == threads - 1 else start + part_size - 1
|
|
159
|
+
part_bytes = max(0, end - start + 1)
|
|
160
|
+
part_steps = max(1, math.ceil(part_bytes / CHUNK))
|
|
161
|
+
bar = ModernProgressBar(total=part_steps, process_name=f"DL #{i + 1}", spinner_mode=False)
|
|
162
|
+
thread_bars.append(bar)
|
|
163
|
+
|
|
164
|
+
download_bar_all.start()
|
|
165
|
+
for bar in thread_bars:
|
|
166
|
+
bar.start()
|
|
167
|
+
|
|
168
|
+
start_time = datetime.now(timezone.utc)
|
|
169
|
+
thread_objs = []
|
|
170
|
+
try:
|
|
171
|
+
for i in range(threads):
|
|
172
|
+
start = part_size * i
|
|
173
|
+
end = file_size - 1 if i == threads - 1 else start + part_size - 1
|
|
174
|
+
thread = Thread(
|
|
175
|
+
target=download_range,
|
|
176
|
+
args=(args.url, start, end, i, args.output, threads, download_bar_all, thread_bars[i], None, stop_event)
|
|
177
|
+
)
|
|
178
|
+
thread_objs.append(thread)
|
|
179
|
+
thread.start()
|
|
180
|
+
|
|
181
|
+
for thread in thread_objs:
|
|
182
|
+
thread.join()
|
|
183
|
+
except KeyboardInterrupt:
|
|
184
|
+
stop_event.set()
|
|
185
|
+
console.print("[red]Interrupted. Stopping threads and cleaning up... [/red]", end="")
|
|
186
|
+
for thread in thread_objs:
|
|
187
|
+
try:
|
|
188
|
+
thread.join()
|
|
189
|
+
except Exception:
|
|
190
|
+
pass
|
|
191
|
+
for part in parts:
|
|
192
|
+
try:
|
|
193
|
+
if os.path.exists(part):
|
|
194
|
+
os.remove(part)
|
|
195
|
+
except Exception:
|
|
196
|
+
pass
|
|
197
|
+
try:
|
|
198
|
+
if os.path.exists(args.output):
|
|
199
|
+
os.remove(args.output)
|
|
200
|
+
except Exception:
|
|
201
|
+
pass
|
|
202
|
+
console.print("[red]Done.[/red]")
|
|
203
|
+
return
|
|
204
|
+
|
|
205
|
+
end_time = datetime.now(timezone.utc)
|
|
206
|
+
delta = end_time - start_time
|
|
207
|
+
duration_ms = delta.days*24*3600*1000 + delta.seconds*1000 + delta.microseconds//1000
|
|
208
|
+
|
|
209
|
+
download_bar_all.finish()
|
|
210
|
+
for bar in thread_bars:
|
|
211
|
+
bar.finish()
|
|
212
|
+
|
|
213
|
+
try:
|
|
214
|
+
merge_files(parts, args.output)
|
|
215
|
+
console.print(f"[green]Download completed in {duration_ms}ms: {args.output}[/green]")
|
|
216
|
+
except Exception as e:
|
|
217
|
+
console.print(f"[red]Error merging files: {e}[/red]")
|
|
218
|
+
|
|
219
|
+
if __name__ == "__main__":
|
|
220
|
+
main()
|