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,2 @@
1
+ from .cli import main
2
+ main()
@@ -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()