txa-m 2.1.1__tar.gz → 2.1.3__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.
@@ -1,9 +1,11 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: txa-m
3
- Version: 2.1.1
3
+ Version: 2.1.3
4
4
  Summary: A modern, high-speed downloader for MediaFire files and folders
5
5
  Home-page: https://github.com/TXAVLOG/TXA-MEDIAFIRE
6
6
  Author: TXA
7
+ Author-email: viptretrauc@gmail.com
8
+ License: MIT
7
9
  Project-URL: Bug Tracker, https://github.com/TXAVLOG/TXA-MEDIAFIRE/issues
8
10
  Project-URL: Source Code, https://github.com/TXAVLOG/TXA-MEDIAFIRE
9
11
  Classifier: Programming Language :: Python :: 3
@@ -18,10 +20,12 @@ Requires-Dist: requests
18
20
  Requires-Dist: gazpacho
19
21
  Requires-Dist: rich
20
22
  Dynamic: author
23
+ Dynamic: author-email
21
24
  Dynamic: classifier
22
25
  Dynamic: description
23
26
  Dynamic: description-content-type
24
27
  Dynamic: home-page
28
+ Dynamic: license
25
29
  Dynamic: project-url
26
30
  Dynamic: requires-dist
27
31
  Dynamic: requires-python
@@ -2,7 +2,7 @@ from setuptools import setup, find_packages
2
2
 
3
3
  setup(
4
4
  name="txa-m",
5
- version="2.1.1",
5
+ version="2.1.3",
6
6
  packages=find_packages(),
7
7
  install_requires=[
8
8
  "requests",
@@ -19,6 +19,8 @@ setup(
19
19
  ],
20
20
  },
21
21
  author="TXA",
22
+ author_email="viptretrauc@gmail.com",
23
+ license="MIT",
22
24
  description="A modern, high-speed downloader for MediaFire files and folders",
23
25
  long_description=open("README.md", encoding="utf-8").read() if open("README.md", encoding="utf-8") else "",
24
26
  long_description_content_type="text/markdown",
@@ -1,9 +1,11 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: txa-m
3
- Version: 2.1.1
3
+ Version: 2.1.3
4
4
  Summary: A modern, high-speed downloader for MediaFire files and folders
5
5
  Home-page: https://github.com/TXAVLOG/TXA-MEDIAFIRE
6
6
  Author: TXA
7
+ Author-email: viptretrauc@gmail.com
8
+ License: MIT
7
9
  Project-URL: Bug Tracker, https://github.com/TXAVLOG/TXA-MEDIAFIRE/issues
8
10
  Project-URL: Source Code, https://github.com/TXAVLOG/TXA-MEDIAFIRE
9
11
  Classifier: Programming Language :: Python :: 3
@@ -18,10 +20,12 @@ Requires-Dist: requests
18
20
  Requires-Dist: gazpacho
19
21
  Requires-Dist: rich
20
22
  Dynamic: author
23
+ Dynamic: author-email
21
24
  Dynamic: classifier
22
25
  Dynamic: description
23
26
  Dynamic: description-content-type
24
27
  Dynamic: home-page
28
+ Dynamic: license
25
29
  Dynamic: project-url
26
30
  Dynamic: requires-dist
27
31
  Dynamic: requires-python
@@ -2,8 +2,6 @@
2
2
 
3
3
  import base64
4
4
  import hashlib
5
- import http.client
6
- import urllib.parse
7
5
  import importlib.resources
8
6
  import json
9
7
  import platform
@@ -11,8 +9,6 @@ import subprocess
11
9
  import sys
12
10
  from re import findall, search
13
11
  from time import sleep
14
- from io import BytesIO
15
- from gzip import GzipFile
16
12
  from datetime import datetime
17
13
  from requests import get
18
14
  from gazpacho import Soup
@@ -21,7 +17,7 @@ from os import path, makedirs, remove, environ
21
17
  from threading import BoundedSemaphore, Thread, Event, Lock
22
18
 
23
19
  # --- Rich UI Imports ---
24
- from rich.console import Console
20
+ from rich.console import Console, Group
25
21
  from rich.panel import Panel
26
22
  from rich.text import Text
27
23
  from rich.progress import (
@@ -41,7 +37,7 @@ from rich.theme import Theme
41
37
  from rich import box
42
38
 
43
39
  # --- Configuration ---
44
- APP_VERSION = "2.1.1"
40
+ APP_VERSION = "2.1.3"
45
41
 
46
42
  # Default ignore lists
47
43
  IGNORE_EXTENSIONS = {".pyc", ".pyo", ".pyd", ".DS_Store", "Thumbs.db"}
@@ -568,7 +564,8 @@ def main():
568
564
  event = Event()
569
565
  limiter = BoundedSemaphore(args.threads)
570
566
 
571
- progress = Progress(
567
+ # Progress for Bytes (Total + Individual)
568
+ p_bytes = Progress(
572
569
  SpinnerColumn(),
573
570
  TextColumn("[progress.description]{task.description}"),
574
571
  BarColumn(bar_width=None),
@@ -579,23 +576,38 @@ def main():
579
576
  TransferSpeedColumn(),
580
577
  "•",
581
578
  TimeRemainingColumn(),
579
+ console=console,
580
+ expand=True
581
+ )
582
+
583
+ # Progress for File Counts
584
+ p_count = Progress(
585
+ TextColumn("[progress.description]{task.description}"),
586
+ BarColumn(bar_width=None),
587
+ "[progress.percentage]{task.percentage:>3.0f}%",
582
588
  "•",
583
- ClockColumn(),
589
+ TextColumn("[bold blue]{task.completed}/{task.total}[/bold blue] " + T["files_count"].split()[0]),
584
590
  console=console,
585
591
  expand=True
586
592
  )
587
593
 
588
- with progress:
589
- overall_task: TaskID = progress.add_task(f"[bold info]{T['total_progress']}", total=stats.total_size)
590
- file_count_task: TaskID = progress.add_task(f"[bold info]{T['files_count']} (0/{stats.total_files})", total=stats.total_files)
594
+ ui_group = Group(
595
+ Panel(p_count, title=f"[bold cyan]{T['files_count']}", border_style="cyan"),
596
+ Panel(p_bytes, title=f"[bold magenta]{T['total_progress']}", border_style="magenta")
597
+ )
598
+
599
+ with Live(ui_group, console=console, refresh_per_second=10):
600
+ overall_task = p_bytes.add_task(f"[bold]{T['total_progress']}", total=stats.total_size)
601
+ file_count_task = p_count.add_task(f"[bold]{T['files_count']}", total=stats.total_files)
591
602
 
592
603
  def update_overall():
593
- progress.update(overall_task, completed=stats.downloaded_bytes)
594
- progress.update(file_count_task, completed=stats.downloaded_files, description=f"[bold info]{T['files_count']} ({stats.downloaded_files}/{stats.total_files})")
604
+ p_bytes.update(overall_task, completed=stats.downloaded_bytes)
605
+ p_count.update(file_count_task, completed=stats.downloaded_files,
606
+ description=f"[bold]{T['files_count']} ({stats.downloaded_files}/{stats.total_files})")
595
607
 
596
608
  threads = []
597
609
  for file_info, destination in all_tasks:
598
- threads.append(Thread(target=download_file_worker, args=(file_info, destination, event, limiter, progress, update_overall)))
610
+ threads.append(Thread(target=download_file_worker, args=(file_info, destination, event, limiter, p_bytes, update_overall)))
599
611
 
600
612
  for t in threads:
601
613
  t.start()
@@ -698,7 +710,6 @@ def download_file_worker(file, output_dir, event, limiter, progress, update_cb):
698
710
 
699
711
  try:
700
712
  # We ensure dir exists in main loop, but recursive structures might need this
701
- # Using output_dir which already includes subfolder path from discover_all_files
702
713
  makedirs(output_dir, exist_ok=True)
703
714
 
704
715
  if path.exists(file_path):
@@ -710,93 +721,89 @@ def download_file_worker(file, output_dir, event, limiter, progress, update_cb):
710
721
  stats.downloaded_files += 1
711
722
  stats.downloaded_bytes += file_size
712
723
  update_cb()
713
- limiter.release()
714
724
  return
715
725
 
716
726
  if event.is_set():
717
- limiter.release()
718
727
  return
719
728
 
720
729
  # Show task now that we know we might download it
721
730
  task_id = progress.add_task(f"[cyan]{filename}[/cyan]", total=file_size, visible=True)
722
731
 
723
732
  download_link = file["links"]["normal_download"]
724
- parsed_url = urllib.parse.urlparse(download_link)
725
-
726
- conn = http.client.HTTPConnection(parsed_url.netloc, timeout=60)
727
- conn.request("GET", parsed_url.path, headers=HEADERS)
728
- response = conn.getresponse()
729
-
730
- if response.getheader("Content-Encoding") == "gzip":
731
- compressed_data = response.read()
732
- conn.close()
733
- with GzipFile(fileobj=BytesIO(compressed_data)) as f:
734
- html = f.read().decode("utf-8", errors="ignore")
735
-
736
- soup = Soup(html)
737
- download_btn = soup.find("a", {"id": "downloadButton"})
738
- real_link = None
739
-
740
- if download_btn and download_btn.attrs.get("href"):
741
- real_link = download_btn.attrs["href"]
742
-
743
- if not real_link and download_btn and download_btn.attrs.get("data-scrambled-url"):
744
- try:
745
- real_link = base64.b64decode(download_btn.attrs["data-scrambled-url"]).decode("utf-8")
746
- except Exception: pass
747
-
748
- if not real_link:
749
- # Fallback Strategy: Regex
750
- console.log(f"[dim]{T['try_fallback']}: {filename}[/dim]")
751
- match = search(r'href=["\'](https?://download[^"\']+)["\']', html)
752
- if match:
753
- real_link = match.group(1)
754
-
755
- if not real_link:
756
- progress.console.log(f"[error]{T['fail_link']} {filename}[/error]")
757
- with stats.lock: stats.failed += 1
758
- if task_id is not None: progress.remove_task(task_id)
759
- limiter.release()
760
- return
761
-
762
- parsed_url = urllib.parse.urlparse(real_link)
763
- conn = http.client.HTTPConnection(parsed_url.netloc, timeout=60)
764
- conn.request("GET", parsed_url.path + ("?" + parsed_url.query if parsed_url.query else ""), headers=HEADERS)
765
- response = conn.getresponse()
733
+
734
+ # Step 1: Get the real download button/link
735
+ response = get(download_link, headers=HEADERS, timeout=60)
736
+ if response.status_code != 200:
737
+ progress.console.log(f"[error]{T['http_err']} {response.status_code} for {filename}[/error]")
738
+ with stats.lock: stats.failed += 1
739
+ if task_id is not None: progress.remove_task(task_id)
740
+ return
766
741
 
767
- if 400 <= response.status < 600:
768
- conn.close()
769
- progress.console.log(f"[error]{T['http_err']} {response.status} for {filename}[/error]")
742
+ html = response.text
743
+ soup = Soup(html)
744
+ download_btn = soup.find("a", {"id": "downloadButton"})
745
+ real_link = None
746
+
747
+ if download_btn and download_btn.attrs.get("href"):
748
+ real_link = download_btn.attrs["href"]
749
+
750
+ if not real_link and download_btn and download_btn.attrs.get("data-scrambled-url"):
751
+ try:
752
+ real_link = base64.b64decode(download_btn.attrs["data-scrambled-url"]).decode("utf-8")
753
+ except Exception: pass
754
+
755
+ if not real_link:
756
+ # Fallback Strategy: Regex
757
+ match = search(r'href=[\"\'](https?://download[^\"\']+)[\"\']', html)
758
+ if match:
759
+ real_link = match.group(1)
760
+
761
+ if not real_link:
762
+ progress.console.log(f"[error]{T['fail_link']} {filename}[/error]")
770
763
  with stats.lock: stats.failed += 1
771
764
  if task_id is not None: progress.remove_task(task_id)
772
- limiter.release()
773
765
  return
774
-
766
+
767
+ # Step 2: Download the actual file
768
+ res = get(real_link, headers=HEADERS, stream=True, timeout=60)
769
+ if res.status_code != 200:
770
+ progress.console.log(f"[error]{T['http_err']} {res.status_code} for {filename}[/error]")
771
+ with stats.lock: stats.failed += 1
772
+ if task_id is not None: progress.remove_task(task_id)
773
+ return
774
+
775
+ downloaded_for_this_file = 0
775
776
  with open(file_path, "wb") as f:
776
- while not event.is_set():
777
- chunk = response.read(65536)
778
- if not chunk:
777
+ for chunk in res.iter_content(chunk_size=65536):
778
+ if event.is_set():
779
779
  break
780
- f.write(chunk)
781
- with stats.lock:
782
- stats.downloaded_bytes += len(chunk)
783
- progress.update(task_id, advance=len(chunk))
784
- update_cb()
780
+ if chunk:
781
+ f.write(chunk)
782
+ chunk_len = len(chunk)
783
+ downloaded_for_this_file += chunk_len
784
+ with stats.lock:
785
+ stats.downloaded_bytes += chunk_len
786
+ progress.update(task_id, advance=chunk_len)
787
+ update_cb()
785
788
 
786
- conn.close()
787
789
  # Remove task when done to keep the list clean
788
790
  if task_id is not None:
789
791
  progress.remove_task(task_id)
790
792
 
791
793
  if event.is_set():
792
794
  if path.exists(file_path): remove(file_path)
793
- limiter.release()
794
795
  return
795
796
 
796
- with stats.lock:
797
- stats.downloaded_files += 1
798
- save_to_history(filename, format_size(file_size))
799
- update_cb()
797
+ # Verify if we actually got data
798
+ if downloaded_for_this_file > 0:
799
+ with stats.lock:
800
+ stats.downloaded_files += 1
801
+ save_to_history(filename, format_size(file_size))
802
+ update_cb()
803
+ else:
804
+ progress.console.log(f"[error]{T['err_download']} {filename}: 0 bytes received[/error]")
805
+ with stats.lock: stats.failed += 1
806
+ if path.exists(file_path): remove(file_path)
800
807
 
801
808
  except Exception as e:
802
809
  console.log(f"[error]{T['err_download']} {filename}:[/error] {e}")
File without changes
File without changes
File without changes
File without changes
File without changes