| Hash | Commit message | Author | Date | Files | + | - |
1 | commit 10e281a67d4d73e059989654db399521dfdb21b1 |
2 | Author: Connor Etherington <[email protected]> |
3 | Date: Tue Nov 21 01:16:11 2023 +0200 |
4 | |
5 | Auto-Commit Update - 20231121 |
6 | --- |
7 | PKGBUILD | 2 +- |
8 | README.md | 2 +- |
9 | build/lib/ptrack/__init__.py | 16 --- |
10 | build/lib/ptrack/main.py | 215 ----------------------------------- |
11 | build/lib/ptrack/methods.py | 147 ------------------------ |
12 | dist/ptrack-1.0.0-py3-none-any.whl | Bin 6744 -> 0 bytes |
13 | dist/ptrack-1.0.0.tar.gz | Bin 6921 -> 0 bytes |
14 | ptrack.1.gz | Bin 808 -> 807 bytes |
15 | ptrack.egg-info/PKG-INFO | 13 --- |
16 | ptrack.egg-info/SOURCES.txt | 12 -- |
17 | ptrack.egg-info/dependency_links.txt | 1 - |
18 | ptrack.egg-info/entry_points.txt | 5 - |
19 | ptrack.egg-info/requires.txt | 6 - |
20 | ptrack.egg-info/top_level.txt | 1 - |
21 | ptrack/__init__.py | 3 +- |
22 | ptrack/main.py | 154 ++++++++++++++++--------- |
23 | ptrack/media.py | 113 ++++++++++++++++++ |
24 | ptrack/methods.py | 54 ++++++++- |
25 | ptrack/urlDeconstruct.py | 85 ++++++++++++++ |
26 | recipe/meta.yaml | 2 +- |
27 | requirements.txt | 6 + |
28 | setup.py | 2 +- |
29 | 22 files changed, 366 insertions(+), 473 deletions(-) |
30 | |
31 | diff --git a/PKGBUILD b/PKGBUILD |
32 | index 8a3542f..4714fd3 100644 |
33 | --- a/PKGBUILD |
34 | +++ b/PKGBUILD |
35 | @@ -1,7 +1,7 @@ |
36 | # Maintainer: Connor Etherington <[email protected]> |
37 | # --- |
38 | pkgname=ptrack |
39 | -pkgver=1.0.0 |
40 | +pkgver=2.0.0 |
41 | pkgrel=1 |
42 | pkgdesc="A simple CLI utility for asthetically tracking progress when copying, moving or downloading files." |
43 | arch=(x86_64) |
44 | diff --git a/README.md b/README.md |
45 | index 631b844..a3eb98b 100644 |
46 | --- a/README.md |
47 | +++ b/README.md |
48 | @@ -3,7 +3,7 @@ |
49 | ### Welcome to ptrack, a powerful and user-friendly CLI utility for tracking the progress of your file operations. |
50 | ### Designed to be concise, efficient and performance-optimized, ptrack works swiftly and accurately, while providing in-depth insight into the progress of the task at hand. |
51 | |
52 | -*Version: 1.0.0* |
53 | +*Version: 2.0.0* |
54 | |
55 | *** |
56 | |
57 | diff --git a/build/lib/ptrack/__init__.py b/build/lib/ptrack/__init__.py |
58 | deleted file mode 100644 |
59 | index bf11915..0000000 |
60 | --- a/build/lib/ptrack/__init__.py |
61 | +++ /dev/null |
62 | @@ -1,16 +0,0 @@ |
63 | -import argparse |
64 | -version="1.0.0" |
65 | - |
66 | -parser = argparse.ArgumentParser(description='A simple CLI utility for asthetically tracking progress when copying or moving files.') |
67 | -parser.add_argument('-v', '--verbose', action='store_true', help='verbose output') |
68 | -parser.add_argument('-c', '--copy', action='store_true', help='copy files (You can use `ptc` instead of `ptrack -c`)') |
69 | -parser.add_argument('-m', '--move', action='store_true', help='move files (You can use `ptm` instead of `ptrack -m`)') |
70 | -parser.add_argument('-d', '--download', action='store_true', help='download files (You can use `ptd` instead of `ptrack -d`)') |
71 | -parser.add_argument('-V', '--version', action='version', version='%(prog)s' + version) |
72 | - |
73 | -args, unknown = parser.parse_known_args() |
74 | - |
75 | -verbose = args.verbose |
76 | -copy = args.copy |
77 | -move = args.move |
78 | -download = args.download |
79 | diff --git a/build/lib/ptrack/main.py b/build/lib/ptrack/main.py |
80 | deleted file mode 100644 |
81 | index 44e9aa6..0000000 |
82 | --- a/build/lib/ptrack/main.py |
83 | +++ /dev/null |
84 | @@ -1,215 +0,0 @@ |
85 | -import os |
86 | -import re |
87 | -import sys |
88 | -import ptrack |
89 | -from ptrack.methods import format_file_size, regular_copy, verbose_copy, hlp, getTotalSize, CustomFileSizeColumn |
90 | -from rich.progress import Progress, BarColumn, TextColumn, TimeRemainingColumn, FileSizeColumn |
91 | -from rich.console import Console |
92 | -from datetime import timedelta |
93 | -import shutil |
94 | -import requests |
95 | -import validators |
96 | - |
97 | -verbose = ptrack.verbose |
98 | -argCopy = ptrack.copy |
99 | -argMove = ptrack.move |
100 | -argDownload = ptrack.download |
101 | - |
102 | - |
103 | -def run(process): |
104 | - console = Console() |
105 | - |
106 | - if len(sys.argv) < 3: |
107 | - hlp() |
108 | - if process == "Copying": |
109 | - console.print("[bold cyan]Usage: ptc [OPTIONS] SOURCE... DESTINATION[/bold cyan]") |
110 | - elif process == "Moving": |
111 | - console.print("[bold cyan]Usage: ptm [OPTIONS] SOURCE... DESTINATION[/bold cyan]") |
112 | - sys.exit(1) |
113 | - |
114 | - src_paths = sys.argv[1:-1] |
115 | - dst = sys.argv[-1] |
116 | - srcPaths = [] |
117 | - |
118 | - for path in src_paths: |
119 | - if path.endswith('/'): |
120 | - path = path[:-1] |
121 | - srcPaths.append(path) |
122 | - |
123 | - if os.path.isdir(dst): |
124 | - dst_dir = dst |
125 | - new_name = None |
126 | - else: |
127 | - dst_dir = os.path.dirname(dst) |
128 | - new_name = os.path.basename(dst) |
129 | - |
130 | - total_files = sum(len(files) for path in srcPaths for r, d, files in os.walk(path) if os.path.isdir(path)) + sum(1 for path in srcPaths if os.path.isfile(path)) |
131 | - total_size = getTotalSize(srcPaths) |
132 | - destination_path = os.path.join(dst_dir, os.path.basename(srcPaths[0]) if not new_name else new_name) |
133 | - |
134 | - current_file = 1 |
135 | - |
136 | - if total_files > 1: |
137 | - console.print(f"\n[#ea2a6f]{process}:[/#ea2a6f] [bold cyan]{total_files} files[/bold cyan]\n") |
138 | - else: |
139 | - for src_path in srcPaths: |
140 | - if os.path.isfile(src_path): |
141 | - console.print(f"\n[#ea2a6f]{process}:[/#ea2a6f] [bold cyan] {os.path.basename(src_path)} [/bold cyan]\n") |
142 | - |
143 | - if verbose: |
144 | - for src_path in srcPaths: |
145 | - if os.path.isfile(src_path): |
146 | - dst_path = os.path.join(dst_dir, os.path.basename(src_path) if not new_name else new_name) |
147 | - terminate = verbose_copy(src_path, dst_path, console, current_file, total_files, file_name=os.path.basename(src_path)) |
148 | - current_file += 1 |
149 | - if terminate == 'c': |
150 | - console.print("\n[bold red]\[-][/bold red][bold white] Operation cancelled by user.[/bold white]\n") |
151 | - sys.exit(1) |
152 | - else: |
153 | - for root, dirs, files in os.walk(src_path): |
154 | - for file in files: |
155 | - src_file_path = os.path.join(root, file) |
156 | - relative_path = os.path.relpath(src_file_path, start=src_path) |
157 | - dst_file_path = os.path.join(dst_dir, os.path.basename(src_path) if not new_name else new_name, relative_path) |
158 | - os.makedirs(os.path.dirname(src_file_path), exist_ok=True) |
159 | - terminate = verbose_copy(src_file_path, dst_file_path, console, current_file, total_files, file_name=file) |
160 | - current_file += 1 |
161 | - if terminate == 'c': |
162 | - console.print("\n[bold red]\[-][/bold red][bold white] Operation cancelled by user.[/bold white]\n") |
163 | - sys.exit(1) |
164 | - else: |
165 | - with Progress( |
166 | - BarColumn(bar_width=50), |
167 | - "[progress.percentage]{task.percentage:>3.0f}%", |
168 | - TimeRemainingColumn(), |
169 | - "[#ea2a6f][[/#ea2a6f]", |
170 | - FileSizeColumn(), |
171 | - "[#ea2a6f]/[/#ea2a6f]", |
172 | - TextColumn("[bold cyan]{task.fields[total_size]}[/bold cyan]"), |
173 | - "[#ea2a6f]][/#ea2a6f]", |
174 | - TextColumn("-[bold yellow] {task.fields[current_file_name]}[/bold yellow]"), |
175 | - console=console, |
176 | - auto_refresh=False |
177 | - ) as progress: |
178 | - task = progress.add_task("", total=total_size, total_size=format_file_size(total_size), current_file_name="Initializing...") |
179 | - |
180 | - try: |
181 | - for src_path in srcPaths: |
182 | - if os.path.isfile(src_path): |
183 | - src_file_path = src_path |
184 | - dst_file_path = os.path.join(dst_dir, os.path.basename(src_path) if not new_name else new_name) |
185 | - file_premissions = os.stat(src_file_path).st_mode |
186 | - progress.update(task, current_file_name=os.path.basename(src_path), refresh=True) # Force refresh |
187 | - terminate = regular_copy(src_path, dst_file_path, console, task, progress, file_name=os.path.basename(src_path)) |
188 | - if terminate == 'c': |
189 | - console.print("\n[bold red]\[-][/bold red][bold white] Operation cancelled by user.[/bold white]\n") |
190 | - sys.exit(1) |
191 | - else: |
192 | - os.chmod(dst_file_path, file_premissions) |
193 | - |
194 | - else: |
195 | - for root, dirs, files in os.walk(src_path): |
196 | - for file in files: |
197 | - src_file_path = os.path.join(root, file) |
198 | - relative_path = os.path.relpath(src_file_path, start=src_path) |
199 | - dst_file_path = os.path.join(dst_dir, os.path.basename(src_path) if not new_name else new_name, relative_path) |
200 | - os.makedirs(os.path.dirname(dst_file_path), exist_ok=True) |
201 | - progress.update(task, current_file_name=file, refresh=True) # Force refresh |
202 | - regular_copy(src_file_path, dst_file_path, console, task, progress, file_name=file) |
203 | - |
204 | - except KeyboardInterrupt: |
205 | - console.print("\n[bold red]\[-][/bold red][bold white] Operation cancelled by user.[/bold white]\n") |
206 | - sys.exit(1) |
207 | - |
208 | - return srcPaths |
209 | - |
210 | - |
211 | -def download(): |
212 | - console = Console() |
213 | - urls = sys.argv[1:] |
214 | - |
215 | - if len(urls) == 0: |
216 | - console.print("\n[bold red][-][/bold red] No URL provided.\n") |
217 | - sys.exit() |
218 | - |
219 | - num_urls = len(urls) |
220 | - for url in urls: |
221 | - if url.startswith('-'): |
222 | - num_urls -= 1 |
223 | - elif not validators.url(url): |
224 | - console.print(f"\n[bold red][-][/bold red] Invalid URL: [bold yellow]{url}[/bold yellow]\n") |
225 | - sys.exit() |
226 | - |
227 | - console.print(f"\n[#ea2a6f]Downloading:[/#ea2a6f] [bold yellow]{num_urls}[/bold yellow] [bold cyan]files[/bold cyan]\n") |
228 | - |
229 | - errors = [] |
230 | - for url in urls: |
231 | - try: |
232 | - if url.startswith('-'): |
233 | - continue |
234 | - |
235 | - response = requests.get(url, stream=True, allow_redirects=True) |
236 | - total_size_in_bytes = int(response.headers.get('content-length', 0)) |
237 | - content_disposition = response.headers.get('content-disposition') |
238 | - destination_path = re.findall('filename="(.+)"', content_disposition)[0] if content_disposition and re.findall('filename="(.+)"', content_disposition) else os.path.basename(url) |
239 | - |
240 | - with Progress( |
241 | - BarColumn(bar_width=50), |
242 | - "[progress.percentage]{task.percentage:>3.0f}%", |
243 | - TimeRemainingColumn(), |
244 | - "[#ea2a6f][[/#ea2a6f]", |
245 | - CustomFileSizeColumn(), |
246 | - "[#ea2a6f]][/#ea2a6f]", |
247 | - f" {destination_path}", # This line will print the filename at the end |
248 | - console=console, |
249 | - auto_refresh=True |
250 | - ) as progress: |
251 | - task_id = progress.add_task("Downloading", total=total_size_in_bytes) |
252 | - block_size = 1024 # 1 Kibibyte |
253 | - with open(destination_path, 'wb') as file: |
254 | - for data in response.iter_content(block_size): |
255 | - file.write(data) |
256 | - progress.update(task_id, advance=block_size) |
257 | - except KeyboardInterrupt: |
258 | - console.print("\n[bold red]\[-][/bold red][bold white] Operation cancelled by user.[/bold white]\n") |
259 | - sys.exit(1) |
260 | - |
261 | - except Exception as e: |
262 | - console.print(f"\n[bold red]\[-][/bold red][bold white] Could not download file: [bold yellow]{url}[/bold yellow]\n") |
263 | - print(e) |
264 | - errors.append(url) |
265 | - |
266 | - if len(errors) == 0: |
267 | - console.print("\n[bold green]Download completed![/bold green]\n") |
268 | - else: |
269 | - console.print("[bold red]The following files could not be downloaded:[/bold red]\n") |
270 | - for error in errors: |
271 | - console.print(f"[bold red] -[/bold red][bold yellow]{error}[/bold yellow]\n") |
272 | - |
273 | - |
274 | -def copy(): |
275 | - run('Copying') |
276 | - |
277 | - |
278 | -def move(): |
279 | - src_paths = run('Moving') |
280 | - for src_path in src_paths: |
281 | - if os.path.isfile(src_path): |
282 | - os.remove(src_path) |
283 | - else: |
284 | - shutil.rmtree(src_path) |
285 | - |
286 | - |
287 | -def main(): |
288 | - if argMove: |
289 | - move() |
290 | - elif argCopy: |
291 | - copy() |
292 | - elif argDownload: |
293 | - download() |
294 | - else: |
295 | - hlp() |
296 | - |
297 | - |
298 | -if __name__ == "__main__": |
299 | - main() |
300 | diff --git a/build/lib/ptrack/methods.py b/build/lib/ptrack/methods.py |
301 | deleted file mode 100644 |
302 | index 01a2d63..0000000 |
303 | --- a/build/lib/ptrack/methods.py |
304 | +++ /dev/null |
305 | @@ -1,147 +0,0 @@ |
306 | -import os |
307 | -import sys |
308 | -import requests |
309 | -from rich.console import Console |
310 | -from rich.progress import Progress, TextColumn, BarColumn, TimeRemainingColumn, FileSizeColumn, Task, DownloadColumn, TimeElapsedColumn |
311 | -from rich.text import Text |
312 | -from datetime import timedelta |
313 | -from humanize import naturalsize |
314 | -import shutil |
315 | - |
316 | -console = Console() |
317 | -operation_cancelled = False |
318 | - |
319 | - |
320 | -def getTotalSize(srcPaths): |
321 | - total_size = 0 |
322 | - for path in srcPaths: |
323 | - if os.path.isfile(path): |
324 | - total_size += os.path.getsize(path) |
325 | - else: |
326 | - for r, d, files in os.walk(path): |
327 | - for f in files: |
328 | - fp = os.path.join(r, f) |
329 | - total_size += os.path.getsize(fp) |
330 | - return total_size |
331 | - |
332 | - |
333 | -def format_file_size(file_size): |
334 | - if file_size >= 1000 ** 4: # Terabyte |
335 | - return f"{round(file_size / (1000 ** 4))} TB" |
336 | - elif file_size >= 1000 ** 3: # Gigabyte |
337 | - return f"{round(file_size / (1000 ** 3))} GB" |
338 | - elif file_size >= 1000 ** 2: # Megabyte |
339 | - return f"{round(file_size / (1000 ** 2))} MB" |
340 | - elif file_size >= 1000: # Kilobyte |
341 | - return f"{round(file_size / 1000)} kB" |
342 | - else: # Byte |
343 | - return f"{file_size} bytes" |
344 | - |
345 | - |
346 | - |
347 | -def regular_copy(src, dst, console, task, progress, file_name): |
348 | - |
349 | - global operation_cancelled |
350 | - |
351 | - try: |
352 | - with open(src, 'rb') as fsrc, open(dst, 'wb') as fdst: |
353 | - while True: |
354 | - buf = fsrc.read(1024*1024) |
355 | - if not buf or operation_cancelled: |
356 | - break |
357 | - filePremissions = os.stat(src).st_mode |
358 | - fdst.write(buf) |
359 | - progress.update(task, advance=len(buf)) |
360 | - progress.refresh() |
361 | - |
362 | - os.chmod(dst, filePremissions) |
363 | - |
364 | - except KeyboardInterrupt: |
365 | - operation_cancelled = True |
366 | - progress.stop() |
367 | - return "c" |
368 | - |
369 | - |
370 | -def verbose_copy(src, dst, console, current, total_files, file_name): |
371 | - operation_cancelled = False |
372 | - file_size = os.path.getsize(src) |
373 | - |
374 | - with Progress( |
375 | - BarColumn(bar_width=50), |
376 | - "[progress.percentage]{task.percentage:>3.0f}%", |
377 | - TimeRemainingColumn(), |
378 | - "[#ea2a6f][[/#ea2a6f]", |
379 | - FileSizeColumn(), |
380 | - "[#ea2a6f]/[/#ea2a6f]", |
381 | - TextColumn(f"[bold cyan]{format_file_size(file_size)}[/bold cyan]"), |
382 | - "[#ea2a6f]][/#ea2a6f]", |
383 | - f"({current} of {total_files}) - {file_name}", |
384 | - console=console, |
385 | - auto_refresh=False |
386 | - ) as progress: |
387 | - task = progress.add_task("", total=file_size, file_size=format_file_size(file_size)) |
388 | - |
389 | - try: |
390 | - with open(src, 'rb') as fsrc, open(dst, 'wb') as fdst: |
391 | - while not progress.finished: |
392 | - buf = fsrc.read(1024*1024) |
393 | - if not buf or operation_cancelled: |
394 | - break |
395 | - fdst.write(buf) |
396 | - progress.update(task, advance=len(buf)) |
397 | - progress.refresh() |
398 | - |
399 | - shutil.copystat(src, dst) |
400 | - |
401 | - except KeyboardInterrupt: |
402 | - operation_cancelled = True |
403 | - progress.stop() |
404 | - return "c" |
405 | - |
406 | - |
407 | -def hlp(): |
408 | - print(""" |
409 | -usage: ptrack [-h] [-v] [-c] [-m] [-d] [-V] |
410 | - |
411 | -A simple CLI utility for asthetically tracking progress when copying or moving files. |
412 | - |
413 | -options: |
414 | - -h, --help show this help message and exit |
415 | - -v, --verbose verbose output |
416 | - -c, --copy copy files (You can use `ptc` instead of `ptrack -c`) |
417 | - -m, --move move files (You can use `ptm` instead of `ptrack -m`) |
418 | - -d, --download download files (You can use `ptd` instead of `ptrack -d`) |
419 | - -V, --version show program's version number and exit |
420 | -""") |
421 | - |
422 | - |
423 | -class CustomFileSizeColumn(FileSizeColumn, TimeElapsedColumn): |
424 | - def render(self, task): |
425 | - completed = task.completed |
426 | - total = task.total |
427 | - elapsed = task.elapsed |
428 | - |
429 | - if elapsed > 0.0: # Prevent division by zero |
430 | - download_speed = completed / elapsed # calculate download rate |
431 | - else: |
432 | - download_speed = 0 |
433 | - |
434 | - if total: |
435 | - size = Text.assemble( |
436 | - (f"{self._human_readable_size(completed)}", "green"), # completed |
437 | - (" / ", "none"), # separator |
438 | - (f"{self._human_readable_size(total)}", "red"), # total |
439 | - (" [", "none"), # opening square bracket |
440 | - (f"{self._human_readable_size(download_speed)}/s", "blue"), # download rate |
441 | - ("]", "none"), # closing square bracket |
442 | - ) |
443 | - else: |
444 | - size = Text(str(self._human_readable_size(completed))) |
445 | - return size |
446 | - |
447 | - def _human_readable_size(self, size: int) -> str: |
448 | - for unit in ['B', 'KB', 'MB', 'GB', 'TB']: |
449 | - if abs(size) < 1024.0: |
450 | - return f"{size:.1f}{unit}" |
451 | - size /= 1024.0 |
452 | - return f"{size:.1f}PB" |
453 | diff --git a/dist/ptrack-1.0.0-py3-none-any.whl b/dist/ptrack-1.0.0-py3-none-any.whl |
454 | deleted file mode 100644 |
455 | index c1fa573..0000000 |
456 | Binary files a/dist/ptrack-1.0.0-py3-none-any.whl and /dev/null differ |
457 | diff --git a/dist/ptrack-1.0.0.tar.gz b/dist/ptrack-1.0.0.tar.gz |
458 | deleted file mode 100644 |
459 | index c127219..0000000 |
460 | Binary files a/dist/ptrack-1.0.0.tar.gz and /dev/null differ |
461 | diff --git a/ptrack.1.gz b/ptrack.1.gz |
462 | index 4833759..2933b5f 100644 |
463 | Binary files a/ptrack.1.gz and b/ptrack.1.gz differ |
464 | diff --git a/ptrack.egg-info/PKG-INFO b/ptrack.egg-info/PKG-INFO |
465 | deleted file mode 100644 |
466 | index e28cefc..0000000 |
467 | --- a/ptrack.egg-info/PKG-INFO |
468 | +++ /dev/null |
469 | @@ -1,13 +0,0 @@ |
470 | -Metadata-Version: 2.1 |
471 | -Name: ptrack |
472 | -Version: 1.0.0 |
473 | -Summary: A simple CLI utility for asthetically tracking progress when copying, moving or downloading files. |
474 | -Author: Connor Etherington |
475 | -Author-email: [email protected] |
476 | -License-File: LICENSE |
477 | -Requires-Dist: rich |
478 | -Requires-Dist: argparse |
479 | -Requires-Dist: requests |
480 | -Requires-Dist: validators |
481 | -Requires-Dist: setuptools |
482 | -Requires-Dist: humanize |
483 | diff --git a/ptrack.egg-info/SOURCES.txt b/ptrack.egg-info/SOURCES.txt |
484 | deleted file mode 100644 |
485 | index 086a784..0000000 |
486 | --- a/ptrack.egg-info/SOURCES.txt |
487 | +++ /dev/null |
488 | @@ -1,12 +0,0 @@ |
489 | -LICENSE |
490 | -README.md |
491 | -setup.py |
492 | -ptrack/__init__.py |
493 | -ptrack/main.py |
494 | -ptrack/methods.py |
495 | -ptrack.egg-info/PKG-INFO |
496 | -ptrack.egg-info/SOURCES.txt |
497 | -ptrack.egg-info/dependency_links.txt |
498 | -ptrack.egg-info/entry_points.txt |
499 | -ptrack.egg-info/requires.txt |
500 | -ptrack.egg-info/top_level.txt |
501 | diff --git a/ptrack.egg-info/dependency_links.txt b/ptrack.egg-info/dependency_links.txt |
502 | deleted file mode 100644 |
503 | index 8b13789..0000000 |
504 | --- a/ptrack.egg-info/dependency_links.txt |
505 | +++ /dev/null |
506 | @@ -1 +0,0 @@ |
507 | - |
508 | diff --git a/ptrack.egg-info/entry_points.txt b/ptrack.egg-info/entry_points.txt |
509 | deleted file mode 100644 |
510 | index ea851b3..0000000 |
511 | --- a/ptrack.egg-info/entry_points.txt |
512 | +++ /dev/null |
513 | @@ -1,5 +0,0 @@ |
514 | -[console_scripts] |
515 | -ptc = ptrack.main:copy |
516 | -ptd = ptrack.main:download |
517 | -ptm = ptrack.main:move |
518 | -ptrack = ptrack.main:main |
519 | diff --git a/ptrack.egg-info/requires.txt b/ptrack.egg-info/requires.txt |
520 | deleted file mode 100644 |
521 | index d19a378..0000000 |
522 | --- a/ptrack.egg-info/requires.txt |
523 | +++ /dev/null |
524 | @@ -1,6 +0,0 @@ |
525 | -rich |
526 | -argparse |
527 | -requests |
528 | -validators |
529 | -setuptools |
530 | -humanize |
531 | diff --git a/ptrack.egg-info/top_level.txt b/ptrack.egg-info/top_level.txt |
532 | deleted file mode 100644 |
533 | index c003217..0000000 |
534 | --- a/ptrack.egg-info/top_level.txt |
535 | +++ /dev/null |
536 | @@ -1 +0,0 @@ |
537 | -ptrack |
538 | diff --git a/ptrack/__init__.py b/ptrack/__init__.py |
539 | index bf11915..7e46d28 100644 |
540 | --- a/ptrack/__init__.py |
541 | +++ b/ptrack/__init__.py |
542 | @@ -1,5 +1,5 @@ |
543 | import argparse |
544 | -version="1.0.0" |
545 | +version="2.0.0" |
546 | |
547 | parser = argparse.ArgumentParser(description='A simple CLI utility for asthetically tracking progress when copying or moving files.') |
548 | parser.add_argument('-v', '--verbose', action='store_true', help='verbose output') |
549 | @@ -14,3 +14,4 @@ verbose = args.verbose |
550 | copy = args.copy |
551 | move = args.move |
552 | download = args.download |
553 | [41m+ |
554 | diff --git a/ptrack/main.py b/ptrack/main.py |
555 | index 44e9aa6..c7c6938 100644 |
556 | --- a/ptrack/main.py |
557 | +++ b/ptrack/main.py |
558 | @@ -2,20 +2,24 @@ import os |
559 | import re |
560 | import sys |
561 | import ptrack |
562 | -from ptrack.methods import format_file_size, regular_copy, verbose_copy, hlp, getTotalSize, CustomFileSizeColumn |
563 | +from ptrack.methods import format_file_size, regular_copy, verbose_copy, hlp, getTotalSize, CustomFileSizeColumn, isMediaUrl, CustomDLColumn |
564 | +from ptrack.media import mediaDownload |
565 | from rich.progress import Progress, BarColumn, TextColumn, TimeRemainingColumn, FileSizeColumn |
566 | from rich.console import Console |
567 | from datetime import timedelta |
568 | +from concurrent.futures import ThreadPoolExecutor, as_completed |
569 | import shutil |
570 | import requests |
571 | import validators |
572 | +from threading import Lock |
573 | + |
574 | +lock = Lock() |
575 | |
576 | verbose = ptrack.verbose |
577 | argCopy = ptrack.copy |
578 | argMove = ptrack.move |
579 | argDownload = ptrack.download |
580 | |
581 | - |
582 | def run(process): |
583 | console = Console() |
584 | |
585 | @@ -45,7 +49,6 @@ def run(process): |
586 | |
587 | total_files = sum(len(files) for path in srcPaths for r, d, files in os.walk(path) if os.path.isdir(path)) + sum(1 for path in srcPaths if os.path.isfile(path)) |
588 | total_size = getTotalSize(srcPaths) |
589 | - destination_path = os.path.join(dst_dir, os.path.basename(srcPaths[0]) if not new_name else new_name) |
590 | |
591 | current_file = 1 |
592 | |
593 | @@ -78,7 +81,7 @@ def run(process): |
594 | console.print("\n[bold red]\[-][/bold red][bold white] Operation cancelled by user.[/bold white]\n") |
595 | sys.exit(1) |
596 | else: |
597 | - with Progress( |
598 | + columns= [ |
599 | BarColumn(bar_width=50), |
600 | "[progress.percentage]{task.percentage:>3.0f}%", |
601 | TimeRemainingColumn(), |
602 | @@ -87,35 +90,50 @@ def run(process): |
603 | "[#ea2a6f]/[/#ea2a6f]", |
604 | TextColumn("[bold cyan]{task.fields[total_size]}[/bold cyan]"), |
605 | "[#ea2a6f]][/#ea2a6f]", |
606 | - TextColumn("-[bold yellow] {task.fields[current_file_name]}[/bold yellow]"), |
607 | - console=console, |
608 | - auto_refresh=False |
609 | - ) as progress: |
610 | + "[bold purple] - [/bold purple]", |
611 | + TextColumn("[bold yellow]{task.fields[current_file_name]}[/bold yellow]", justify="left"), |
612 | + ] |
613 | + |
614 | + with Progress(*columns, console=console, auto_refresh=False) as progress: |
615 | + |
616 | task = progress.add_task("", total=total_size, total_size=format_file_size(total_size), current_file_name="Initializing...") |
617 | |
618 | + def threaded_copy(src, dst, file_permissions, console, task, progress): |
619 | + terminate = regular_copy(src, dst, console, task, progress, lock) |
620 | + if terminate != 'c': |
621 | + os.chmod(dst, file_permissions) |
622 | + |
623 | try: |
624 | - for src_path in srcPaths: |
625 | - if os.path.isfile(src_path): |
626 | - src_file_path = src_path |
627 | - dst_file_path = os.path.join(dst_dir, os.path.basename(src_path) if not new_name else new_name) |
628 | - file_premissions = os.stat(src_file_path).st_mode |
629 | - progress.update(task, current_file_name=os.path.basename(src_path), refresh=True) # Force refresh |
630 | - terminate = regular_copy(src_path, dst_file_path, console, task, progress, file_name=os.path.basename(src_path)) |
631 | - if terminate == 'c': |
632 | - console.print("\n[bold red]\[-][/bold red][bold white] Operation cancelled by user.[/bold white]\n") |
633 | - sys.exit(1) |
634 | + with ThreadPoolExecutor() as executor: |
635 | + futures = [] |
636 | + for src_path in srcPaths: |
637 | + if os.path.isfile(src_path): |
638 | + src_file_path = src_path |
639 | + dst_file_path = os.path.join(dst_dir, os.path.basename(src_path) if not new_name else new_name) |
640 | + file_permissions = os.stat(src_file_path).st_mode |
641 | + progress.update(task, current_file_name=os.path.basename(src_path), refresh=True) |
642 | + future = executor.submit(threaded_copy, src_path, dst_file_path, file_permissions, console, task, progress) |
643 | + futures.append(future) |
644 | + |
645 | + for future in as_completed(futures): |
646 | + with lock: |
647 | + progress.update(task, advance=future.result()) |
648 | + |
649 | else: |
650 | - os.chmod(dst_file_path, file_premissions) |
651 | - |
652 | - else: |
653 | - for root, dirs, files in os.walk(src_path): |
654 | - for file in files: |
655 | - src_file_path = os.path.join(root, file) |
656 | - relative_path = os.path.relpath(src_file_path, start=src_path) |
657 | - dst_file_path = os.path.join(dst_dir, os.path.basename(src_path) if not new_name else new_name, relative_path) |
658 | - os.makedirs(os.path.dirname(dst_file_path), exist_ok=True) |
659 | - progress.update(task, current_file_name=file, refresh=True) # Force refresh |
660 | - regular_copy(src_file_path, dst_file_path, console, task, progress, file_name=file) |
661 | + for root, dirs, files in os.walk(src_path): |
662 | + for file in files: |
663 | + src_file_path = os.path.join(root, file) |
664 | + relative_path = os.path.relpath(src_file_path, start=src_path) |
665 | + dst_file_path = os.path.join(dst_dir, os.path.basename(src_path) if not new_name else new_name, relative_path) |
666 | + os.makedirs(os.path.dirname(dst_file_path), exist_ok=True) |
667 | + file_permissions = os.stat(src_file_path).st_mode |
668 | + progress.update(task, current_file_name=file, refresh=True) |
669 | + future = executor.submit(threaded_copy, src_path, dst_file_path, file_permissions, console, task, progress) |
670 | + futures.append(future) |
671 | + |
672 | + for future in as_completed(futures): |
673 | + with lock: |
674 | + progress.update(task, advance=future.result()) |
675 | |
676 | except KeyboardInterrupt: |
677 | console.print("\n[bold red]\[-][/bold red][bold white] Operation cancelled by user.[/bold white]\n") |
678 | @@ -147,29 +165,63 @@ def download(): |
679 | try: |
680 | if url.startswith('-'): |
681 | continue |
682 | + else: |
683 | + downloaded_size = 0 |
684 | + total_size = 0 |
685 | + |
686 | + custom_columns = [ |
687 | + BarColumn(bar_width=50), |
688 | + "[progress.percentage]{task.percentage:>3.0f}%", |
689 | + TimeRemainingColumn(), |
690 | + "[#ea2a6f][[/#ea2a6f]", |
691 | + FileSizeColumn(), |
692 | + "[#ea2a6f]/[/#ea2a6f]", |
693 | + TextColumn(f"[bold cyan]{total_size}[/bold cyan]"), |
694 | + "[#ea2a6f]][/#ea2a6f]", |
695 | + TextColumn(f"[bold purple] - [/bold purple][bold yellow]{url}[/bold yellow][bold purple] | [/bold purple]Processing filetype...", justify="left"), |
696 | + ] |
697 | + |
698 | + with Progress(*custom_columns) as progress: |
699 | + task = progress.add_task("", total=100, file_size="0 KB", start=False) |
700 | + |
701 | + if isMediaUrl(url): |
702 | + def wipe(): |
703 | + sys.stdout.write("\033[F") # Cursor up one line |
704 | + sys.stdout.write("\033[K") # Clear to the end of line |
705 | + sys.stdout.flush() |
706 | + sys.stdout.write("\033[F") # Cursor up one line |
707 | + |
708 | + mediaDownload(url, progress, wipe) |
709 | + continue |
710 | + |
711 | + response = requests.get(url, stream=True, allow_redirects=True) |
712 | + total_size_in_bytes = int(response.headers.get('content-length', 0)) |
713 | + content_disposition = response.headers.get('content-disposition') |
714 | + destination_path = re.findall('filename="(.+)"', content_disposition)[0] if content_disposition and re.findall('filename="(.+)"', content_disposition) else os.path.basename(url) |
715 | + |
716 | + size = format_file_size(total_size_in_bytes) |
717 | + |
718 | + with Progress( |
719 | + BarColumn(bar_width=50), |
720 | + "[progress.percentage]{task.percentage:>3.0f}%", |
721 | + TimeRemainingColumn(), |
722 | + "[#ea2a6f][[/#ea2a6f]", |
723 | + FileSizeColumn(), |
724 | + "[#ea2a6f]/[/#ea2a6f]", |
725 | + TextColumn(f"[bold cyan]{size}[/bold cyan]"), |
726 | + "[#ea2a6f]][/#ea2a6f]", |
727 | + "[bold purple] - [/bold purple]", |
728 | + TextColumn(f"[bold yellow]{destination_path}[/bold yellow]", justify="left"), |
729 | + console=console, |
730 | + auto_refresh=True |
731 | + ) as progress: |
732 | + task_id = progress.add_task("Downloading", total=total_size_in_bytes) |
733 | + block_size = 1024 # 1 Kibibyte |
734 | + with open(destination_path, 'wb') as file: |
735 | + for data in response.iter_content(block_size): |
736 | + file.write(data) |
737 | + progress.update(task_id, advance=block_size) |
738 | |
739 | - response = requests.get(url, stream=True, allow_redirects=True) |
740 | - total_size_in_bytes = int(response.headers.get('content-length', 0)) |
741 | - content_disposition = response.headers.get('content-disposition') |
742 | - destination_path = re.findall('filename="(.+)"', content_disposition)[0] if content_disposition and re.findall('filename="(.+)"', content_disposition) else os.path.basename(url) |
743 | - |
744 | - with Progress( |
745 | - BarColumn(bar_width=50), |
746 | - "[progress.percentage]{task.percentage:>3.0f}%", |
747 | - TimeRemainingColumn(), |
748 | - "[#ea2a6f][[/#ea2a6f]", |
749 | - CustomFileSizeColumn(), |
750 | - "[#ea2a6f]][/#ea2a6f]", |
751 | - f" {destination_path}", # This line will print the filename at the end |
752 | - console=console, |
753 | - auto_refresh=True |
754 | - ) as progress: |
755 | - task_id = progress.add_task("Downloading", total=total_size_in_bytes) |
756 | - block_size = 1024 # 1 Kibibyte |
757 | - with open(destination_path, 'wb') as file: |
758 | - for data in response.iter_content(block_size): |
759 | - file.write(data) |
760 | - progress.update(task_id, advance=block_size) |
761 | except KeyboardInterrupt: |
762 | console.print("\n[bold red]\[-][/bold red][bold white] Operation cancelled by user.[/bold white]\n") |
763 | sys.exit(1) |
764 | diff --git a/ptrack/media.py b/ptrack/media.py |
765 | new file mode 100644 |
766 | index 0000000..8699ada |
767 | --- /dev/null |
768 | +++ b/ptrack/media.py |
769 | @@ -0,0 +1,113 @@ |
770 | + |
771 | +import subprocess |
772 | +from ptrack.urlDeconstruct import get_urls |
773 | +from ptrack.methods import CustomFileSizeColumn, format_file_size |
774 | +import ptrack |
775 | +from rich.progress import Progress, TextColumn, BarColumn, TimeRemainingColumn, Task, DownloadColumn, TimeElapsedColumn, FileSizeColumn |
776 | +from rich.console import Console |
777 | +import threading |
778 | +import time |
779 | +import os |
780 | + |
781 | +console = Console() |
782 | + |
783 | +def run_ffmpeg(ffmpeg_command): |
784 | + with subprocess.Popen(ffmpeg_command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True) as process: |
785 | + while True: |
786 | + line = process.stderr.readline() |
787 | + if not line: |
788 | + break |
789 | + |
790 | +def get_file_size(filename): |
791 | + return os.path.getsize(filename) if os.path.exists(filename) else 0 |
792 | + |
793 | + |
794 | +#def FileSizeColumn(file): |
795 | +# global file_size |
796 | +# file_size = get_file_size(file) if os.path.exists(file) else 0 |
797 | +# |
798 | +# return f"[bold green]{format_file_size(file_size)}[/bold green]" |
799 | + |
800 | + |
801 | +def download_with_progress(ffmpeg_command, title, size, accuracy, progress, wipe): |
802 | + output_file = ffmpeg_command[-1] |
803 | + downloaded_size = format_file_size(get_file_size(output_file)) |
804 | + total_size = format_file_size(size) |
805 | + |
806 | + if not accuracy: |
807 | + title = f"[bold purple]- [/bold purple]⚠ [#ea2a6f]Filesize may be inaccurate[/#ea2a6f][bold purple] - [/bold purple][bold yellow]{title}[/bold yellow]" |
808 | + else: |
809 | + title = f"[bold purple]- [/bold purple][bold yellow]{title}[/bold yellow]" |
810 | + |
811 | + ffmpeg_command.extend(['-flush_packets', '1']) |
812 | + |
813 | + custom_columns = [ |
814 | + BarColumn(bar_width=50), |
815 | + "[progress.percentage]{task.percentage:>3.0f}%", |
816 | + TimeRemainingColumn(), |
817 | + "[#ea2a6f][[/#ea2a6f]", |
818 | + FileSizeColumn(), |
819 | + "[#ea2a6f]/[/#ea2a6f]", |
820 | + TextColumn(f"[bold cyan]{total_size}[/bold cyan]"), |
821 | + "[#ea2a6f]][/#ea2a6f]", |
822 | + TextColumn(title, justify="left"), |
823 | + ] |
824 | + |
825 | + progress.stop() |
826 | + wipe() |
827 | + |
828 | + with Progress(*custom_columns) as progress: |
829 | + task = progress.add_task("", total=size, downloaded_size=format_file_size(get_file_size(output_file))) |
830 | + process = subprocess.Popen(ffmpeg_command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True) |
831 | + |
832 | + while True: |
833 | + output = process.stderr.readline() |
834 | + if output == '' and process.poll() is not None: |
835 | + break |
836 | + if output: |
837 | + progress.update(task, completed=get_file_size(output_file)) |
838 | + |
839 | + time.sleep(0.3) |
840 | + |
841 | + if process.returncode == 0: |
842 | + progress.columns = list(progress.columns) |
843 | + progress.columns[-1].renderable = TextColumn(f"[bold yellow]{title}i[/bold yellow][bold green] ✓[/bold green]") |
844 | + progress.columns = tuple(progress.columns) |
845 | + else: |
846 | + progress.columns = list(progress.columns) |
847 | + progress.columns[-1].renderable = TextColumn(f"[bold yellow]{title}[/bold yellow][bold red] ✗[/bold red]") |
848 | + progress.columns = tuple(progress.columns) |
849 | + |
850 | + |
851 | + |
852 | + progress.stop() |
853 | + |
854 | + |
855 | +def mediaDownload(url, progress, wipe): |
856 | + |
857 | + fetched_urls = get_urls([url]) |
858 | + |
859 | + print() |
860 | + |
861 | + for fetched_url in fetched_urls: |
862 | + if fetched_url: |
863 | + video_url, audio_url, title, size, accuracy = fetched_url |
864 | + safe_name = make_filename_safe(title) |
865 | + ffmpeg_command = [ |
866 | + 'ffmpeg', '-i', video_url, '-i', audio_url, |
867 | + '-c:v', 'libx264', '-c:a', 'aac', '-strict', 'experimental', |
868 | + '-b:v', '1M', '-b:a', '128k', f'{safe_name}.mp4' |
869 | + ] if video_url.endswith('.m3u8') else [ |
870 | + 'ffmpeg', '-i', video_url, '-i', audio_url, |
871 | + '-c:v', 'copy', '-c:a', 'copy', f'{safe_name}.mp4' |
872 | + ] |
873 | + |
874 | + download_with_progress(ffmpeg_command, title, size, accuracy, progress, wipe) |
875 | + else: |
876 | + print("Error: Unable to fetch URLs for one of the videos.") |
877 | + |
878 | + |
879 | + |
880 | +def make_filename_safe(name): |
881 | + return name.replace(' ', '_').replace('(', '').replace(')', '').replace('/', '_') |
882 | + |
883 | diff --git a/ptrack/methods.py b/ptrack/methods.py |
884 | index 01a2d63..d72319e 100644 |
885 | --- a/ptrack/methods.py |
886 | +++ b/ptrack/methods.py |
887 | @@ -6,7 +6,15 @@ from rich.progress import Progress, TextColumn, BarColumn, TimeRemainingColumn, |
888 | from rich.text import Text |
889 | from datetime import timedelta |
890 | from humanize import naturalsize |
891 | +from bs4 import BeautifulSoup |
892 | import shutil |
893 | +import mimetypes |
894 | +import re |
895 | +from concurrent.futures import ThreadPoolExecutor |
896 | +import subprocess |
897 | +import shlex |
898 | +from functools import lru_cache |
899 | + |
900 | |
901 | console = Console() |
902 | operation_cancelled = False |
903 | @@ -75,7 +83,7 @@ def verbose_copy(src, dst, console, current, total_files, file_name): |
904 | "[#ea2a6f]/[/#ea2a6f]", |
905 | TextColumn(f"[bold cyan]{format_file_size(file_size)}[/bold cyan]"), |
906 | "[#ea2a6f]][/#ea2a6f]", |
907 | - f"({current} of {total_files}) - {file_name}", |
908 | + f"({current} of {total_files})[bold purple] - [/bold purple][bold yellow]{file_name}[/bold yellow]", |
909 | console=console, |
910 | auto_refresh=False |
911 | ) as progress: |
912 | @@ -145,3 +153,47 @@ class CustomFileSizeColumn(FileSizeColumn, TimeElapsedColumn): |
913 | return f"{size:.1f}{unit}" |
914 | size /= 1024.0 |
915 | return f"{size:.1f}PB" |
916 | + |
917 | +class CustomDLColumn(FileSizeColumn, TimeElapsedColumn): |
918 | + def render(self, task): |
919 | + completed = task.completed |
920 | + total = task.total |
921 | + elapsed = task.elapsed |
922 | + |
923 | + if elapsed > 0.0: # Prevent division by zero |
924 | + download_speed = completed / elapsed # calculate download rate |
925 | + else: |
926 | + download_speed = 0 |
927 | + |
928 | + if total: |
929 | + size = Text.assemble( |
930 | + (f"{self._human_readable_size(completed)}", "green"), # completed |
931 | + (" / ", "none"), # separator |
932 | + (f"{self._human_readable_size(total)}", "red"), # total |
933 | + (" [", "none"), # opening square bracket |
934 | + (f"{self._human_readable_size(download_speed)}/s", "blue"), # download rate |
935 | + ("]", "none"), # closing square bracket |
936 | + ) |
937 | + else: |
938 | + size = Text(str(self._human_readable_size(completed))) |
939 | + return size |
940 | + |
941 | + def _human_readable_size(self, size: int) -> str: |
942 | + for unit in ['B', 'KB', 'MB', 'GB', 'TB']: |
943 | + if abs(size) < 1024.0: |
944 | + return f"{size:.1f}{unit}" |
945 | + size /= 1024.0 |
946 | + return f"{size:.1f}PB" |
947 | + |
948 | + |
949 | +def isMediaUrl(url): |
950 | + command = shlex.split(f'yt-dlp --get-url {url}') |
951 | + try: |
952 | + process = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE) |
953 | + stdout, stderr = process.communicate() |
954 | + return process.returncode == 0 and bool(stdout.strip()) |
955 | + except Exception as e: |
956 | + print(f"Error checking URL type: {e}") |
957 | + return False |
958 | [41m+ |
959 | [41m+ |
960 | diff --git a/ptrack/urlDeconstruct.py b/ptrack/urlDeconstruct.py |
961 | new file mode 100644 |
962 | index 0000000..23f9c89 |
963 | --- /dev/null |
964 | +++ b/ptrack/urlDeconstruct.py |
965 | @@ -0,0 +1,85 @@ |
966 | +import requests |
967 | +from bs4 import BeautifulSoup |
968 | +import subprocess |
969 | +from tempfile import NamedTemporaryFile |
970 | +from yt_dlp import main as yt_dlp |
971 | +import json |
972 | +import ascii_magic |
973 | +import tkinter as tk |
974 | +from PIL import Image, ImageTk |
975 | +from io import BytesIO |
976 | +from urllib3 import PoolManager |
977 | + |
978 | + |
979 | +def getData(url): |
980 | + process = subprocess.Popen(['yt-dlp', '--format', 'bestvideo+bestaudio/best', url, '--dump-json'], stdout=subprocess.PIPE, stderr=subprocess.PIPE) |
981 | + out, err = process.communicate() |
982 | + if err: |
983 | + print(f"Error: {err}") |
984 | + return None |
985 | + try: |
986 | + data = json.loads(out) |
987 | + return data |
988 | + except json.JSONDecodeError: |
989 | + print("Failed to decode JSON from yt-dlp output.") |
990 | + |
991 | + |
992 | + |
993 | +def download_image(url): |
994 | + response = requests.get(url) |
995 | + if response.status_code == 200: |
996 | + with NamedTemporaryFile(delete=False, suffix='.png') as img_temp: |
997 | + img = Image.open(requests.get(url, stream=True).raw) |
998 | + img.save(img_temp, 'PNG') |
999 | + return img_temp.name |