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
@@ -0,0 +1,2 @@
1
+ from .cli import main
2
+ main()
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
+ ![PyPI - Version](https://img.shields.io/pypi/v/nercone-fastget)
@@ -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,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: uv 0.9.11
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,3 @@
1
+ [console_scripts]
2
+ fastget = nercone_fastget.cli:main
3
+