DDownloader 0.3.2__py3-none-any.whl → 0.3.4__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.
DDownloader/main.py CHANGED
@@ -1,107 +1,138 @@
1
- import os
2
- import re
3
- import logging
4
- import coloredlogs
5
- import time
1
+ import os, re, logging, coloredlogs, time, json
6
2
  from pathlib import Path
7
3
  from colorama import Fore, Style
8
- from DDownloader.modules.helper import download_binaries, detect_platform
4
+ from DDownloader.modules.helper import download_binaries, detect_platform, get_media_info
9
5
  from DDownloader.modules.args_parser import parse_arguments
10
- from DDownloader.modules.banners import clear_and_print
11
- from DDownloader.modules.dash_downloader import DASH
12
- from DDownloader.modules.hls_downloader import HLS
6
+ from DDownloader.modules.banners import clear_and_print, display_help
7
+ from DDownloader.modules.downloader import DOWNLOADER
13
8
 
14
- # Setup logger
15
9
  logger = logging.getLogger("+ MAIN + ")
16
10
  coloredlogs.install(level='DEBUG', logger=logger)
17
11
 
12
+ # =========================================================================================================== #
13
+
18
14
  def validate_directories():
19
15
  downloads_dir = 'downloads'
20
16
  if not os.path.exists(downloads_dir):
21
17
  os.makedirs(downloads_dir)
22
- # logger.debug(f"Created '{downloads_dir}' directory.")
23
-
24
- def display_help():
25
- """Display custom help message with emoji."""
26
- print(
27
- f"{Fore.WHITE}+" + "=" * 100 + f"+{Style.RESET_ALL}\n"
28
- f"{Fore.CYAN}{'Option':<40}{'Description':<90}{Style.RESET_ALL}\n"
29
- f"{Fore.WHITE}+" + "=" * 100 + f"+{Style.RESET_ALL}\n"
30
- f" {Fore.GREEN}-u, --url{' ' * 22}{Style.RESET_ALL}URL of the manifest (mpd/m3u8) 🌐\n"
31
- f" {Fore.GREEN}-p, --proxy{' ' * 20}{Style.RESET_ALL}A proxy with protocol (http://ip:port) 🌍\n"
32
- f" {Fore.GREEN}-o, --output{' ' * 19}{Style.RESET_ALL}Name of the output file 💾\n"
33
- f" {Fore.GREEN}-k, --key{' ' * 22}{Style.RESET_ALL}Decryption key in KID:KEY format 🔑\n"
34
- f" {Fore.GREEN}-H, --header{' ' * 19}{Style.RESET_ALL}Custom HTTP headers (e.g., User-Agent: value) 📋\n"
35
- f" {Fore.GREEN}-h, --help{' ' * 21}{Style.RESET_ALL}Show this help message and exit ❓\n"
36
- f"{Fore.WHITE}+" + "=" * 100 + f"+{Style.RESET_ALL}\n"
37
- )
18
+ logger.debug(f"Created '{downloads_dir}' directory.")
19
+ return downloads_dir
20
+
21
+ # =========================================================================================================== #
22
+
23
+ def process_media_info(directory="downloads", log_dir="logs"):
24
+ if not os.path.exists(log_dir):
25
+ os.makedirs(log_dir)
26
+ logger.info(f"Created logs directory: {log_dir}")
27
+
28
+ if not os.path.exists(directory):
29
+ logger.error(f"Directory '{directory}' does not exist. Please create it and add media files.")
30
+ return
31
+
32
+ mp4_files = [os.path.join(directory, f) for f in os.listdir(directory) if f.endswith(".mp4")]
33
+
34
+ if not mp4_files:
35
+ logger.info(f"No .mp4 files found in directory: {directory}")
36
+ return
37
+
38
+ logger.info(f"Found {len(mp4_files)} .mp4 file(s) in '{directory}'. Processing...")
39
+
40
+ for file_path in mp4_files:
41
+ try:
42
+ media_info = get_media_info(file_path)
43
+ if media_info:
44
+ base_name = os.path.splitext(os.path.basename(file_path))[0]
45
+ log_file_path = os.path.join(log_dir, f"{base_name}.log")
46
+ with open(log_file_path, "w", encoding="utf-8") as log_file:
47
+ json.dump(media_info, log_file, indent=4)
48
+ logger.info(f"Saved media information to: {log_file_path}")
49
+ print(Fore.RED + "═" * 100 + Fore.RESET + "\n")
50
+
51
+ except Exception as e:
52
+ logger.error(f"Failed to process {file_path}: {e}")
53
+
54
+ # =========================================================================================================== #
38
55
 
39
56
  def main():
40
57
  clear_and_print()
41
58
  platform_name = detect_platform()
42
- logger.info(f"Downloading binaries... Please wait!")
43
- print(Fore.MAGENTA + "=" * 100 + Fore.RESET)
59
+ logger.info("Please be patient...")
60
+ print(Fore.RED + "" * 100 + Fore.RESET)
44
61
  time.sleep(1)
45
62
  bin_dir = Path(__file__).resolve().parent / "bin"
46
63
  download_binaries(bin_dir, platform_name)
47
64
  clear_and_print()
48
65
 
49
- validate_directories()
66
+ downloads_dir = validate_directories()
50
67
  try:
51
68
  args = parse_arguments()
52
69
  except SystemExit:
53
70
  display_help()
54
71
  exit(1)
55
72
 
56
- downloader = None
57
- if re.search(r"\.mpd\b", args.url, re.IGNORECASE):
58
- logger.info("DASH stream detected. Initializing DASH downloader...")
59
- downloader = DASH()
60
- elif re.search(r"\.m3u8\b", args.url, re.IGNORECASE):
61
- logger.info("HLS stream detected. Initializing HLS downloader...")
62
- downloader = HLS()
63
- else:
64
- logger.error("Unsupported URL format. Please provide a valid DASH (.mpd) or HLS (.m3u8) URL.")
65
- exit(1)
73
+ downloader = DOWNLOADER()
66
74
 
67
- # Configure downloader
68
- downloader.manifest_url = args.url
69
- downloader.output_name = args.output
70
- downloader.decryption_keys = args.key or []
71
- downloader.headers = args.header or []
72
- downloader.proxy = args.proxy # Add proxy if provided
73
-
74
- if downloader.proxy:
75
- print(Fore.MAGENTA + "=" * 100 + Fore.RESET)
76
- if not downloader.proxy.startswith("http://"):
77
- downloader.proxy = f"http://{downloader.proxy}"
75
+ # Handle downloading if URL is provided
76
+ if args.url:
77
+ if re.search(r"\.mpd\b", args.url, re.IGNORECASE):
78
+ logger.info("DASH stream detected. Initializing DASH downloader...")
79
+ elif re.search(r"\.m3u8\b", args.url, re.IGNORECASE):
80
+ logger.info("HLS stream detected. Initializing HLS downloader...")
81
+ elif re.search(r"\.ism\b", args.url, re.IGNORECASE):
82
+ logger.info("ISM (Smooth Streaming) detected. Initializing ISM downloader...")
83
+ else:
84
+ logger.error("Unsupported URL format. Please provide a valid DASH (.mpd), HLS (.m3u8), or ISM (.ism) URL.")
85
+ exit(1)
86
+
87
+ downloader.manifest_url = args.url
88
+ downloader.output_name = args.output
89
+ downloader.decryption_keys = args.key or []
90
+ downloader.headers = args.header or []
91
+ downloader.proxy = args.proxy
92
+
93
+ if downloader.proxy:
94
+ if not downloader.proxy.startswith("http://"):
95
+ downloader.proxy = f"http://{downloader.proxy}"
78
96
  logger.info(f"Proxy: {downloader.proxy}")
79
- print(Fore.MAGENTA + "=" * 100 + Fore.RESET)
80
-
81
- # Log provided headers
82
- if downloader.headers:
83
- print(Fore.MAGENTA + "=" * 100 + Fore.RESET)
84
- logger.info("Headers provided:")
85
- for header in downloader.headers:
86
- logger.info(f" -H {header}")
87
- print(Fore.MAGENTA + "=" * 100 + Fore.RESET)
88
-
89
- # Log provided decryption keys
90
- if downloader.decryption_keys:
91
- logger.info("Decryption keys provided:")
92
- for key in downloader.decryption_keys:
93
- logger.info(f" --key {key}")
94
- print(Fore.MAGENTA + "=" * 100 + Fore.RESET)
95
-
96
- # Execute downloader
97
- try:
98
- if isinstance(downloader, DASH):
99
- downloader.dash_downloader()
100
- elif isinstance(downloader, HLS):
101
- downloader.hls_downloader()
102
- except Exception as e:
103
- logger.error(f"An error occurred during the download process: {e}")
104
- exit(1)
97
+ print(Fore.RED + "" * 100 + Fore.RESET + "\n")
98
+
99
+ if downloader.headers:
100
+ logger.info("Headers:")
101
+ for header in downloader.headers:
102
+ logger.info(f" - {header}")
103
+ print(Fore.RED + "═" * 100 + Fore.RESET + "\n")
104
+
105
+ if downloader.decryption_keys:
106
+ logger.info("Decryption keys:")
107
+ for key in downloader.decryption_keys:
108
+ logger.info(f" - {key}")
109
+ print(Fore.RED + "═" * 100 + Fore.RESET + "\n")
110
+
111
+ try:
112
+ downloader.drm_downloader()
113
+ except Exception as e:
114
+ logger.error(f"An error occurred during the download process: {e}")
115
+ exit(1)
116
+
117
+ process_media_info(downloads_dir)
118
+
119
+ if args.input and args.quality:
120
+ logger.info(f"Starting re-encode process for {args.input} to {args.quality.upper()} quality...")
121
+ output_file = downloader.re_encode_content(
122
+ input_file=args.input,
123
+ quality=args.quality,
124
+ codec="libx265",
125
+ crf=20,
126
+ preset="medium"
127
+ )
128
+
129
+ if output_file:
130
+ logger.info(f"Re-encoding completed successfully! Output saved to: {output_file}")
131
+ else:
132
+ logger.error("Re-encoding failed.")
133
+ exit(1)
134
+
135
+ # =========================================================================================================== #
105
136
 
106
137
  if __name__ == "__main__":
107
- main()
138
+ main()
@@ -1 +1 @@
1
-
1
+ __version__ = "0.3.4"
@@ -10,11 +10,13 @@ def parse_arguments():
10
10
  )
11
11
 
12
12
  # Add arguments (these will not include the default descriptions)
13
- parser.add_argument("-u", "--url", required=True, help=argparse.SUPPRESS)
13
+ parser.add_argument("-u", "--url", help=argparse.SUPPRESS)
14
14
  parser.add_argument("-p", "--proxy", help=argparse.SUPPRESS)
15
- parser.add_argument("-o", "--output", required=True, help=argparse.SUPPRESS)
15
+ parser.add_argument("-o", "--output", help=argparse.SUPPRESS)
16
16
  parser.add_argument("-k", "--key", action="append", help=argparse.SUPPRESS)
17
17
  parser.add_argument("-H", "--header", action="append", help=argparse.SUPPRESS)
18
+ parser.add_argument("-i", "--input", help=argparse.SUPPRESS)
19
+ parser.add_argument("-q", "--quality", help=argparse.SUPPRESS)
18
20
  parser.add_argument(
19
21
  "-h", "--help",
20
22
  action="help",
@@ -2,9 +2,13 @@ import os, time
2
2
  from sys import stdout
3
3
  from colorama import Fore, Style
4
4
 
5
+ # =========================================================================================================== #
6
+
5
7
  def clear_screen():
6
8
  os.system('cls' if os.name == 'nt' else 'clear')
7
-
9
+
10
+ # =========================================================================================================== #
11
+
8
12
  def banners():
9
13
  stdout.write(" \n")
10
14
  stdout.write(""+Fore.LIGHTRED_EX +"██████╗ ██████╗ ██████╗ ██╗ ██╗███╗ ██╗██╗ ██████╗ █████╗ ██████╗ █████╗ ███████╗██████╗ \n")
@@ -19,17 +23,33 @@ def banners():
19
23
  stdout.write(""+Fore.YELLOW +"╔════════════════════════════════════════════════════════════════════════════╝\n")
20
24
  stdout.write(""+Fore.YELLOW +"║ \x1b[38;2;255;20;147m• "+Fore.GREEN+"GITHUB "+Fore.RED+" |"+Fore.LIGHTWHITE_EX+" GITHUB.COM/THATNOTEASY "+Fore.YELLOW+"║\n")
21
25
  stdout.write(""+Fore.YELLOW +"╚════════════════════════════════════════════════════════════════════════════╝\n")
22
- print(f"{Fore.YELLOW}[DDownloader] - {Fore.GREEN}Download DASH or HLS streams with decryption keys. - {Fore.RED}[V0.3.2] \n{Fore.RESET}")
23
-
26
+ print(f"{Fore.YELLOW}[DDownloader] - {Fore.GREEN}A DRM-Protected Content Downloader - {Fore.RED}[V0.3.4] \n{Fore.RESET}")
27
+
28
+ # =========================================================================================================== #
29
+
24
30
  def clear_and_print():
25
31
  time.sleep(1)
26
32
  clear_screen()
27
33
  banners()
28
34
 
29
-
30
-
31
-
32
-
33
-
34
-
35
-
35
+ # =========================================================================================================== #
36
+
37
+ def display_help():
38
+ """Display custom help message with emoji."""
39
+ print(
40
+ f"{Fore.RED}.++" + "═" * 100 + f"++.{Style.RESET_ALL}\n"
41
+ f"{Fore.CYAN}{'Option':<40}{'Description':<90}{Style.RESET_ALL}\n"
42
+ f"{Fore.RED}.++" + "═" * 100 + f"++.{Style.RESET_ALL}\n"
43
+ f" {Fore.GREEN}-u, --url{' ' * 22}{Style.RESET_ALL}URL of the manifest (mpd/m3u8/ism) 🌐\n"
44
+ f" {Fore.GREEN}-p, --proxy{' ' * 20}{Style.RESET_ALL}A proxy with protocol (http://ip:port) 🌍\n"
45
+ f" {Fore.GREEN}-o, --output{' ' * 19}{Style.RESET_ALL}Name of the output file 💾\n"
46
+ f" {Fore.GREEN}-k, --key{' ' * 22}{Style.RESET_ALL}Decryption key in KID:KEY format 🔑\n"
47
+ f" {Fore.GREEN}-H, --header{' ' * 19}{Style.RESET_ALL}Custom HTTP headers (e.g., User-Agent: value) 📋\n"
48
+ f"{Fore.RED}.++" + "═" * 100 + f"++.{Style.RESET_ALL}\n"
49
+ f" {Fore.GREEN}-i, --input{' ' * 20}{Style.RESET_ALL}Input file for re-encoding. 📂\n"
50
+ f" {Fore.GREEN}-q, --quality{' ' * 18}{Style.RESET_ALL}Target quality: HD, FHD, UHD. 🎥\n"
51
+ f" {Fore.GREEN}-h, --help{' ' * 21}{Style.RESET_ALL}Show this help message and exit ❓\n"
52
+ f"{Fore.RED}.++" + "═" * 100 + f"++.{Style.RESET_ALL}\n"
53
+ )
54
+
55
+ # =========================================================================================================== #
@@ -0,0 +1,159 @@
1
+ import os
2
+ import subprocess
3
+ import logging
4
+ import platform
5
+ import coloredlogs
6
+ from colorama import Fore
7
+
8
+ logger = logging.getLogger(Fore.RED + "+ DDOWNLOADER + ")
9
+ coloredlogs.install(level='DEBUG', logger=logger)
10
+
11
+ class DOWNLOADER:
12
+ def __init__(self):
13
+ self.manifest_url = None
14
+ self.output_name = None
15
+ self.proxy = None
16
+ self.decryption_keys = []
17
+ self.headers = []
18
+ self.binary_path = None
19
+
20
+ # =========================================================================================================== #
21
+
22
+ def _get_binary_path(self, binary_type):
23
+ base_dir = os.path.dirname(os.path.abspath(__file__))
24
+ project_root = os.path.dirname(base_dir)
25
+ bin_dir = os.path.join(project_root, 'bin')
26
+
27
+ if binary_type == 'N_m3u8DL-RE':
28
+ binary_name = 'N_m3u8DL-RE.exe' if platform.system() == 'Windows' else 'N_m3u8DL-RE'
29
+ elif binary_type == 'ffmpeg':
30
+ binary_name = 'ffmpeg.exe' if platform.system() == 'Windows' else 'ffmpeg'
31
+ else:
32
+ raise ValueError(f"Unknown binary type: {binary_type}")
33
+
34
+ binary_path = os.path.join(bin_dir, binary_name)
35
+
36
+ if not os.path.isfile(binary_path):
37
+ logger.error(f"Binary not found: {binary_path}")
38
+ raise FileNotFoundError(f"Binary not found: {binary_path}")
39
+
40
+ if platform.system() == 'Linux':
41
+ chmod_command = ['chmod', '+x', binary_path]
42
+ try:
43
+ subprocess.run(chmod_command, check=True)
44
+ logger.info(Fore.CYAN + f"Set executable permission for: {binary_path}" + Fore.RESET)
45
+ except subprocess.CalledProcessError as e:
46
+ logger.error(Fore.RED + f"Failed to set executable permissions for: {binary_path}" + Fore.RESET)
47
+ raise RuntimeError(f"Could not set executable permissions for: {binary_path}") from e
48
+
49
+ return binary_path
50
+
51
+ # =========================================================================================================== #
52
+
53
+ def drm_downloader(self):
54
+ if not self.manifest_url:
55
+ logger.error("Manifest URL is not set.")
56
+ return
57
+ command = self._build_command()
58
+ self._execute_command(command)
59
+
60
+ # =========================================================================================================== #
61
+
62
+ def _build_command(self):
63
+ self.binary_path = self._get_binary_path("N_m3u8DL-RE")
64
+ command = [
65
+ self.binary_path,
66
+ f'"{self.manifest_url}"',
67
+ '--select-video', 'BEST',
68
+ '--select-audio', 'BEST',
69
+ '-mt',
70
+ '-M', 'format=mp4',
71
+ '--save-dir', '"downloads"',
72
+ '--tmp-dir', '"downloads"',
73
+ '--del-after-done',
74
+ '--save-name', f'"{self.output_name}"'
75
+ ]
76
+
77
+ for key in self.decryption_keys:
78
+ command.extend(['--key', f'"{key}"'])
79
+
80
+ if self.proxy:
81
+ if not self.proxy.startswith("http://"):
82
+ self.proxy = f"http://{self.proxy}"
83
+ command.extend(['--custom-proxy', f'"{self.proxy}"'])
84
+
85
+ for header in self.headers:
86
+ command.extend(['-H', f'"{header}"'])
87
+
88
+ return command
89
+
90
+ # =========================================================================================================== #
91
+
92
+ def _execute_command(self, command):
93
+ try:
94
+ command_str = ' '.join(command)
95
+ result = os.system(command_str)
96
+
97
+ if result == 0:
98
+ logger.info(Fore.GREEN + "Downloaded successfully. Bye!" + Fore.RESET)
99
+ print(Fore.RED + "═" * 100 + Fore.RESET + "\n")
100
+ else:
101
+ pass
102
+
103
+ except Exception as e:
104
+ logger.error(Fore.RED + f"An unexpected error occurred: {e}" + Fore.RESET)
105
+
106
+ # =========================================================================================================== #
107
+
108
+ def re_encode_content(self, input_file, quality, codec="libx265", crf=20, preset="medium", audio_bitrate="256k"):
109
+ resolutions = {
110
+ "HD": "1280:720",
111
+ "FHD": "1920:1080",
112
+ "UHD": "3840:2160"
113
+ }
114
+
115
+ quality = quality.upper()
116
+ if quality not in resolutions:
117
+ logger.error(f"Invalid quality '{quality}'. Choose from: HD, FHD, UHD.")
118
+ return None
119
+
120
+ input_file = os.path.abspath(input_file)
121
+ if not os.path.isfile(input_file):
122
+ logger.error(f"Input file does not exist: {input_file}")
123
+ return None
124
+
125
+ resolution = resolutions[quality]
126
+ base_name, ext = os.path.splitext(input_file)
127
+ output_file = os.path.abspath(f"{base_name}_{quality.lower()}{ext}")
128
+
129
+ self.binary_path = self._get_binary_path("ffmpeg")
130
+
131
+ logger.info(f"Re-encoding {input_file} to {quality} ({resolution}) using codec {codec}...")
132
+ logger.info(f"Output file: {output_file}")
133
+
134
+ os.makedirs(os.path.dirname(output_file), exist_ok=True)
135
+
136
+ # Build the ffmpeg command
137
+ command = [
138
+ self.binary_path,
139
+ "-i", f"\"{input_file}\"",
140
+ "-vf", f"scale={resolution}",
141
+ "-c:v", codec,
142
+ "-crf", str(crf),
143
+ "-preset", preset,
144
+ "-c:a", "aac",
145
+ "-b:a", audio_bitrate,
146
+ "-movflags", "+faststart",
147
+ f"\"{output_file}\""
148
+ ]
149
+
150
+ # Execute the command using `_execute_command`
151
+ self._execute_command(command)
152
+
153
+ # Check if output file exists to confirm success
154
+ if os.path.isfile(output_file):
155
+ logger.info(f"Re-encoding to {quality} completed successfully. Output saved to: {output_file}")
156
+ return output_file
157
+ else:
158
+ logger.error(f"Re-encoding failed. Output file not created: {output_file}")
159
+ return None
@@ -5,15 +5,15 @@ from colorama import Fore, Style, init
5
5
  import logging
6
6
  import coloredlogs
7
7
  import platform
8
+ from pymediainfo import MediaInfo
8
9
 
9
- # Initialize Colorama for Windows compatibility
10
10
  init(autoreset=True)
11
11
 
12
- # Logger setup
13
12
  logger = logging.getLogger(Fore.GREEN + "+ HELPER + ")
14
13
  coloredlogs.install(level='DEBUG', logger=logger)
15
14
 
16
- # Binaries with platform-specific handling
15
+ # =========================================================================================================== #
16
+
17
17
  binaries = {
18
18
  "Windows": [
19
19
  "https://github.com/ThatNotEasy/DDownloader/raw/refs/heads/main/DDownloader/bin/N_m3u8DL-RE.exe",
@@ -29,10 +29,9 @@ binaries = {
29
29
  ]
30
30
  }
31
31
 
32
+ # =========================================================================================================== #
33
+
32
34
  def download_binaries(bin_dir, platform_name):
33
- """
34
- Downloads platform-specific binaries to the specified directory.
35
- """
36
35
  os.makedirs(bin_dir, exist_ok=True)
37
36
  logger.info(f"Platform detected: {platform_name}")
38
37
  logger.info(f"Using binary directory: {bin_dir}")
@@ -56,7 +55,6 @@ def download_binaries(bin_dir, platform_name):
56
55
  response = requests.get(binary_url, stream=True, timeout=30)
57
56
  response.raise_for_status()
58
57
 
59
- # Total size for progress bar
60
58
  total_size = int(response.headers.get('content-length', 0))
61
59
  with open(filepath, "wb") as file, tqdm(
62
60
  total=total_size,
@@ -70,8 +68,6 @@ def download_binaries(bin_dir, platform_name):
70
68
  file.write(chunk)
71
69
  progress_bar.update(len(chunk))
72
70
 
73
- # logger.info(f"{Fore.GREEN}Downloaded and saved: {filepath}{Fore.RESET}")
74
- # Make binary executable on Linux
75
71
  if platform_name == "Linux":
76
72
  os.chmod(filepath, 0o755)
77
73
  except requests.exceptions.RequestException as e:
@@ -79,10 +75,9 @@ def download_binaries(bin_dir, platform_name):
79
75
  except Exception as e:
80
76
  logger.error(f"{Fore.RED}Unexpected error for {binary_url}: {e}{Fore.RESET}")
81
77
 
78
+ # =========================================================================================================== #
79
+
82
80
  def detect_platform():
83
- """
84
- Detects the current operating system platform.
85
- """
86
81
  system_platform = platform.system().lower()
87
82
  if system_platform == 'windows':
88
83
  return 'Windows'
@@ -91,4 +86,58 @@ def detect_platform():
91
86
  elif system_platform == 'darwin':
92
87
  return 'MacOS'
93
88
  else:
94
- return 'Unknown'
89
+ return 'Unknown'
90
+
91
+ # =========================================================================================================== #
92
+
93
+ def get_media_info(file_path):
94
+ try:
95
+ logger.info(f"Parsing media file: {file_path}")
96
+ media_info = MediaInfo.parse(file_path)
97
+ result = {"file_path": file_path, "tracks": []}
98
+
99
+ for track in media_info.tracks:
100
+ track_info = {"track_type": track.track_type}
101
+
102
+ if track.track_type == "Video":
103
+ track_info.update({
104
+ "codec": track.codec,
105
+ "width": track.width,
106
+ "height": track.height,
107
+ "frame_rate": track.frame_rate,
108
+ "bit_rate": track.bit_rate,
109
+ "duration": track.duration,
110
+ "aspect_ratio": track.display_aspect_ratio,
111
+ })
112
+ elif track.track_type == "Audio":
113
+ track_info.update({
114
+ "codec": track.codec,
115
+ "channels": track.channel_s,
116
+ "sample_rate": track.sampling_rate,
117
+ "bit_rate": track.bit_rate,
118
+ "duration": track.duration,
119
+ "language": track.language,
120
+ })
121
+ elif track.track_type == "Text":
122
+ track_info.update({
123
+ "language": track.language,
124
+ "format": track.format,
125
+ })
126
+ elif track.track_type == "General":
127
+ track_info.update({
128
+ "file_size": track.file_size,
129
+ "format": track.format,
130
+ "duration": track.duration,
131
+ "overall_bit_rate": track.overall_bit_rate,
132
+ })
133
+
134
+ result["tracks"].append(track_info)
135
+
136
+ logger.info(f"Successfully extracted media information for: {file_path}")
137
+ return result
138
+
139
+ except Exception as e:
140
+ logger.error(f"Error occurred while parsing media file '{file_path}': {e}")
141
+ return None
142
+
143
+ # =========================================================================================================== #
@@ -16,4 +16,4 @@ class STREAMLINK:
16
16
  self.binary_path = os.path.join(os.path.dirname(__file__), 'bin', 'streamlink')
17
17
 
18
18
  def streamlink_restream(self):
19
- pass
19
+ pass
@@ -0,0 +1,130 @@
1
+ Metadata-Version: 2.2
2
+ Name: DDownloader
3
+ Version: 0.3.4
4
+ Summary: A downloader for DRM-protected content.
5
+ Author-email: ThatNotEasy <apidotmy@proton.me>
6
+ License: MIT License
7
+
8
+ Copyright (c) [2024] [DDownloader]
9
+
10
+ Permission is hereby granted, free of charge, to any person obtaining a copy
11
+ of this software and associated documentation files (the "Software"), to deal
12
+ in the Software without restriction, including without limitation the rights
13
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
14
+ copies of the Software, and to permit persons to whom the Software is
15
+ furnished to do so, subject to the following condition:
16
+
17
+ The above copyright notice and this permission notice shall be included in
18
+ all copies or substantial portions of the Software.
19
+
20
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
21
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
22
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
23
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
24
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
25
+ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
26
+ IN THE SOFTWARE.
27
+ Classifier: Programming Language :: Python :: 3
28
+ Classifier: Programming Language :: Python :: 3.7
29
+ Classifier: Programming Language :: Python :: 3.8
30
+ Classifier: Programming Language :: Python :: 3.9
31
+ Classifier: Programming Language :: Python :: 3.10
32
+ Classifier: Programming Language :: Python :: 3.11
33
+ Classifier: License :: OSI Approved :: MIT License
34
+ Classifier: Operating System :: OS Independent
35
+ Requires-Python: >=3.7
36
+ Description-Content-Type: text/markdown
37
+ License-File: LICENSE
38
+
39
+ # DDownloader
40
+ _DDownloader is a powerful Python-based tool and library designed to download and decrypt DRM-protected content from DASH, HLS, and ISM manifests. It provides seamless support for encrypted media streams, extracting metadata and ensuring high compatibility with various DRM standards._
41
+
42
+ ## Features
43
+ - **Download and Decrypt**: Supports DASH, HLS, and ISM manifests with seamless decryption using provided keys.
44
+ - **Automatic Detection**: Automatically detects manifest types (.mpd, .m3u8, .ism) and processes accordingly.
45
+ - **Media Information Extraction**: Extracts metadata (e.g., codec, resolution, duration) for .mp4 files and saves it in a `logs/` directory.
46
+ - **CLI and Library Support**: Flexible usage via command-line or Python library.
47
+ - **Detailed Logging**: Provides real-time progress and logs errors for debugging.
48
+
49
+ ## Requirements
50
+
51
+ - **Python**: Version 3.7 or higher.
52
+ - **Required binaries**:
53
+
54
+ - `N_m3u8DL-RE` for downloading protected DRM content.
55
+ - `mp4decrypt` for decrypting protected media files.
56
+ - `ffmpeg` for re-encoding and muxer method
57
+ - a proper environment variable configuration for binaries.
58
+
59
+ ## Installation
60
+ - Install `DDownloader` using pip:
61
+
62
+ ```bash
63
+ pip install DDownloader
64
+ ```
65
+
66
+ ## Usage
67
+ - Download Content:
68
+
69
+ ```python
70
+ from DDownloader.modules.downloader import DOWNLOADER
71
+
72
+ downloader = DOWNLOADER()
73
+ downloader.manifest_url = "https://example.com/path/to/manifest" # DASH, HLS, or ISM manifest URL
74
+ downloader.output_name = "output.mp4" # Desired output file name
75
+ downloader.decryption_keys = ["12345:678910"] # Provide decryption keys if needed
76
+ downloader.download() # Start the downloading and decryption process
77
+ ```
78
+
79
+ - Extract Media Information:
80
+
81
+ ```python
82
+ from DDownloader.modules.helper import get_media_info
83
+
84
+ file_path = "downloads/example.mp4"
85
+ media_info = get_media_info(file_path)
86
+ print(media_info)
87
+ ```
88
+
89
+ - Re-encoding:
90
+
91
+ ```python
92
+ from DDownloader.modules.downloader import DOWNLOADER
93
+
94
+ re_encode = DOWNLOADER()
95
+ quality = ["HD", "FHD", "UHD"]
96
+ input_content = "downloads/example.mp4"
97
+ output_content = "/path/to/output.mp4"
98
+ re_encode.re_encode_content(input_file=input_content,quality=quality,codec="libx265",crf=20,preset="medium")
99
+ ```
100
+
101
+ ## CLI Usage
102
+ - Download Media
103
+
104
+ ```bash
105
+ DDownloader -u https://example.com/path/to/manifest -o output.mp4
106
+ ```
107
+
108
+ - Specify Decryption Keys
109
+
110
+ ```bash
111
+ DDownloader -u https://example.com/path/to/manifest -o output.mp4 -k 12345:678910
112
+ ```
113
+
114
+ - Re-encoding
115
+
116
+ ```bash
117
+ DDownloader -i "input.mp4" -o "output.mp4" -q "HD, FHD, UHD"
118
+ ```
119
+
120
+
121
+ - Display Help
122
+
123
+ ```bash
124
+ DDownloader -h
125
+ ```
126
+
127
+ - ![image](https://github.com/user-attachments/assets/5698d535-b818-4566-80f2-44f588646c0f)
128
+
129
+ ## THIS PROJECT STILL IN DEVELOPMENT
130
+ - Contributions are welcome! Feel free to open issues, create pull requests, or provide suggestions.
@@ -0,0 +1,14 @@
1
+ DDownloader/__init__.py,sha256=lVZwmZNId0Dai7XBQpxglmJtIxAtZplRHDsvobL2UNo,33
2
+ DDownloader/main.py,sha256=TQ58NNmigBGKGxMm-PAPL0RG5w8WOpBq0g4J6xQtoSg,5480
3
+ DDownloader/modules/__init__.py,sha256=UwIhDBoqqZjYW7sIs3nj3FDOHCGSvF-VwNKHwkA_fmI,21
4
+ DDownloader/modules/args_parser.py,sha256=Xc9ZzBu-QPFrBURIcq7rl8IJbrdPMy7EMWc-odVM2QU,1105
5
+ DDownloader/modules/banners.py,sha256=o178PwNttHPTlGvnciWG-0wU9NUa57o5sH6SMrd6Sec,5772
6
+ DDownloader/modules/downloader.py,sha256=PaM-khHiW5jIgfOR5_mMnjbJPWk5js3JKQTFBprQY8o,6031
7
+ DDownloader/modules/helper.py,sha256=b8h-h4_JqE02D26qBzSRW11LWhYUTnBkNIeYlWRO-ZY,5951
8
+ DDownloader/modules/streamlink.py,sha256=t7aaHCnINzSFybTmAd-dvfGFQkepFHJwrOBcNxyJviY,504
9
+ DDownloader-0.3.4.dist-info/LICENSE,sha256=cnjTim3BMjb9cVC_b3oS41FESKLuvuDsufVHa_ymZRw,1090
10
+ DDownloader-0.3.4.dist-info/METADATA,sha256=pZXGWFkU3dhIMevBrUGDHV-sU0OQMEiBh_cHqlxKdnU,4872
11
+ DDownloader-0.3.4.dist-info/WHEEL,sha256=In9FTNxeP60KnTkGw7wk6mJPYd_dQSjEZmXdBdMCI-8,91
12
+ DDownloader-0.3.4.dist-info/entry_points.txt,sha256=tCZVr_SRONlWlMFsVKgcPj3lxe9gBtWD4GuWukMv75g,54
13
+ DDownloader-0.3.4.dist-info/top_level.txt,sha256=INZYgY1vEHV1MIWTPXKJL8j8-ZXjWb8u4XLuU3S8umY,12
14
+ DDownloader-0.3.4.dist-info/RECORD,,
@@ -1,100 +0,0 @@
1
- import os
2
- import subprocess
3
- import logging
4
- import platform
5
- import coloredlogs
6
- from colorama import Fore
7
-
8
- logger = logging.getLogger(Fore.RED + "+ DASH + ")
9
- coloredlogs.install(level='DEBUG', logger=logger)
10
-
11
- class DASH:
12
- def __init__(self):
13
- self.manifest_url = None
14
- self.output_name = None
15
- self.proxy = None
16
- self.decryption_keys = []
17
- self.headers = []
18
- self.binary_path = self._get_binary_path()
19
-
20
- def _get_binary_path(self):
21
- """
22
- Dynamically determine the path to the binary file in the 'bin' directory relative to the main module.
23
- """
24
- # Locate the base directory for the project (relative to main.py)
25
- base_dir = os.path.dirname(os.path.abspath(__file__)) # Directory containing the current module
26
- project_root = os.path.dirname(base_dir) # Go up one level to the project root
27
- bin_dir = os.path.join(project_root, 'bin') # Bin directory is under the project root
28
-
29
- # Determine the binary file name based on the platform
30
- binary_name = 'N_m3u8DL-RE.exe' if platform.system() == 'Windows' else 'N_m3u8DL-RE'
31
- binary = os.path.join(bin_dir, binary_name)
32
-
33
- # Check if the binary exists
34
- if not os.path.isfile(binary):
35
- logger.error(f"Binary not found: {binary}")
36
- raise FileNotFoundError(f"Binary not found: {binary}")
37
-
38
- # Ensure the binary is executable on Linux
39
- if platform.system() == 'Linux':
40
- chmod_command = ['chmod', '+x', binary]
41
- try:
42
- subprocess.run(chmod_command, check=True)
43
- logger.info(Fore.CYAN + f"Set executable permission for: {binary}" + Fore.RESET)
44
- except subprocess.CalledProcessError as e:
45
- logger.error(Fore.RED + f"Failed to set executable permissions for: {binary}" + Fore.RESET)
46
- raise RuntimeError(f"Could not set executable permissions for: {binary}") from e
47
-
48
- return binary
49
-
50
- def dash_downloader(self):
51
- if not self.manifest_url:
52
- logger.error("Manifest URL is not set.")
53
- return
54
- command = self._build_command()
55
- self._execute_command(command)
56
-
57
- def _build_command(self):
58
- command = [
59
- self.binary_path,
60
- f'"{self.manifest_url}"',
61
- '--select-video', 'BEST',
62
- '--select-audio', 'BEST',
63
- '-mt',
64
- '-M', 'format=mp4',
65
- '--save-dir', '"downloads"',
66
- '--tmp-dir', '"downloads"',
67
- '--del-after-done',
68
- '--save-name', f'"{self.output_name}"'
69
- ]
70
-
71
- for key in self.decryption_keys:
72
- command.extend(['--key', f'"{key}"'])
73
-
74
- if self.proxy:
75
- if not self.proxy.startswith("http://"):
76
- self.proxy = f"http://{self.proxy}"
77
- command.extend(['--custom-proxy', f'"{self.proxy}"'])
78
-
79
- # Add headers if any are provided
80
- for header in self.headers:
81
- command.extend(['-H', f'"{header}"'])
82
-
83
- # logger.debug(f"Built command: {' '.join(command)}")
84
- return command
85
-
86
- def _execute_command(self, command):
87
- try:
88
- command_str = ' '.join(command)
89
- # logger.debug(f"Executing command: {command_str}")
90
- result = os.system(command_str)
91
-
92
- if result == 0:
93
- logger.info(Fore.GREEN + "Downloaded successfully. Bye!" + Fore.RESET)
94
- else:
95
- logger.info(Fore.GREEN + "Downloaded successfully. Bye!" + Fore.RESET)
96
- # logger.error(Fore.RED + f"Download failed with result code: {result}" + Fore.RESET)
97
- # logger.error(Fore.RED + f"Command: {command_str}" + Fore.RESET)
98
- except Exception as e:
99
- logger.error(Fore.RED + f"An unexpected error occurred: {e}" + Fore.RESET)
100
-
@@ -1,99 +0,0 @@
1
- import os
2
- import subprocess
3
- import logging
4
- import platform
5
- import coloredlogs
6
- from colorama import Fore
7
-
8
- logger = logging.getLogger(Fore.RED + "+ HLS + ")
9
- coloredlogs.install(level='DEBUG', logger=logger)
10
-
11
- class HLS:
12
- def __init__(self):
13
- self.manifest_url = None
14
- self.output_name = None
15
- self.proxy = None
16
- self.decryption_keys = []
17
- self.headers = []
18
- self.binary_path = self._get_binary_path()
19
-
20
- def _get_binary_path(self):
21
- """
22
- Dynamically determine the path to the binary file in the 'bin' directory relative to the main module.
23
- """
24
- # Locate the base directory for the project (relative to main.py)
25
- base_dir = os.path.dirname(os.path.abspath(__file__)) # Directory containing the current module
26
- project_root = os.path.dirname(base_dir) # Go up one level to the project root
27
- bin_dir = os.path.join(project_root, 'bin') # Bin directory is under the project root
28
-
29
- # Determine the binary file name based on the platform
30
- binary_name = 'N_m3u8DL-RE.exe' if platform.system() == 'Windows' else 'N_m3u8DL-RE'
31
- binary = os.path.join(bin_dir, binary_name)
32
-
33
- # Check if the binary exists
34
- if not os.path.isfile(binary):
35
- logger.error(f"Binary not found: {binary}")
36
- raise FileNotFoundError(f"Binary not found: {binary}")
37
-
38
- # Ensure the binary is executable on Linux
39
- if platform.system() == 'Linux':
40
- chmod_command = ['chmod', '+x', binary]
41
- try:
42
- subprocess.run(chmod_command, check=True)
43
- logger.info(Fore.CYAN + f"Set executable permission for: {binary}" + Fore.RESET)
44
- except subprocess.CalledProcessError as e:
45
- logger.error(Fore.RED + f"Failed to set executable permissions for: {binary}" + Fore.RESET)
46
- raise RuntimeError(f"Could not set executable permissions for: {binary}") from e
47
-
48
- return binary
49
-
50
- def hls_downloader(self):
51
- if not self.manifest_url:
52
- logger.error("Manifest URL is not set.")
53
- return
54
- command = self._build_command()
55
- self._execute_command(command)
56
-
57
- def _build_command(self):
58
- command = [
59
- self.binary_path,
60
- f'"{self.manifest_url}"',
61
- '--select-video', 'BEST',
62
- '--select-audio', 'BEST',
63
- '-mt',
64
- '-M', 'format=mp4',
65
- '--save-dir', '"downloads"',
66
- '--tmp-dir', '"downloads"',
67
- '--del-after-done',
68
- '--save-name', f'"{self.output_name}"'
69
- ]
70
-
71
- for key in self.decryption_keys:
72
- command.extend(['--key', f'"{key}"'])
73
-
74
- if self.proxy:
75
- if not self.proxy.startswith("http://"):
76
- self.proxy = f"http://{self.proxy}"
77
- command.extend(['--custom-proxy', f'"{self.proxy}"'])
78
-
79
- # Add headers if any are provided
80
- for header in self.headers:
81
- command.extend(['-H', f'"{header}"'])
82
-
83
- # logger.debug(f"Built command: {' '.join(command)}")
84
- return command
85
-
86
- def _execute_command(self, command):
87
- try:
88
- command_str = ' '.join(command)
89
- # logger.debug(f"Executing command: {command_str}")
90
- result = os.system(command_str)
91
-
92
- if result == 0:
93
- logger.info(Fore.GREEN + "Downloaded successfully. Bye!" + Fore.RESET)
94
- else:
95
- logger.info(Fore.GREEN + "Downloaded successfully. Bye!" + Fore.RESET)
96
- # logger.error(Fore.RED + f"Download failed with result code: {result}" + Fore.RESET)
97
- # logger.error(Fore.RED + f"Command: {command_str}" + Fore.RESET)
98
- except Exception as e:
99
- logger.error(Fore.RED + f"An unexpected error occurred: {e}" + Fore.RESET)
@@ -1,95 +0,0 @@
1
- Metadata-Version: 2.2
2
- Name: DDownloader
3
- Version: 0.3.2
4
- Summary: A downloader for DRM-protected content.
5
- Author-email: ThatNotEasy <apidotmy@proton.me>
6
- License: MIT License
7
-
8
- Copyright (c) [2024] [DDownloader]
9
-
10
- Permission is hereby granted, free of charge, to any person obtaining a copy
11
- of this software and associated documentation files (the "Software"), to deal
12
- in the Software without restriction, including without limitation the rights
13
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
14
- copies of the Software, and to permit persons to whom the Software is
15
- furnished to do so, subject to the following condition:
16
-
17
- The above copyright notice and this permission notice shall be included in
18
- all copies or substantial portions of the Software.
19
-
20
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
21
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
22
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
23
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
24
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
25
- FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
26
- IN THE SOFTWARE.
27
- Project-URL: homepage, https://github.com/ThatNotEasy/DDownloader
28
- Classifier: Programming Language :: Python :: 3
29
- Classifier: Programming Language :: Python :: 3.7
30
- Classifier: Programming Language :: Python :: 3.8
31
- Classifier: Programming Language :: Python :: 3.9
32
- Classifier: Programming Language :: Python :: 3.10
33
- Classifier: Programming Language :: Python :: 3.11
34
- Classifier: License :: OSI Approved :: MIT License
35
- Classifier: Operating System :: OS Independent
36
- Requires-Python: >=3.7
37
- Description-Content-Type: text/markdown
38
- License-File: LICENSE
39
- Requires-Dist: requests>=2.26.0
40
- Requires-Dist: coloredlogs>=15.0
41
- Requires-Dist: tqdm>=4.64.0
42
- Requires-Dist: colorama>=0.4.5
43
- Requires-Dist: loguru>=0.6.0
44
-
45
- # DDownloader
46
- - DDownloader is a Python library to download HLS and DASH manifests and decrypt media files.
47
-
48
- # Features
49
- - Download HLS streams using N_m3u8DL-RE.
50
- - Download DASH manifests and segments.
51
- - Decrypt media files using mp4decrypt.
52
-
53
- # Footprints Notes:
54
- - It is better if you have set your own environment variables.
55
-
56
- # Installation
57
- Use the package manager pip to install DDownloader.
58
- ```pip install DDownloader```
59
-
60
- # Usage
61
-
62
- - Download DASH content using the library:
63
-
64
- ```python
65
- from DDownloader.dash_downloader import DASH
66
-
67
- dash_downloader = DASH()
68
- dash_downloader.manifest_url = "https://example.com/path/to/manifest.mpd" # Set your DASH manifest URL
69
- dash_downloader.output_name = "output.mp4" # Set desired output name
70
- dash_downloader.decryption_key = "12345:678910" # Set decryption key if needed
71
- dash_downloader.dash_downloader()
72
- ```
73
-
74
- - Download HLS content using the library:
75
- ```python
76
- from DDownloader.hls_downloader import HLS
77
-
78
- hls_downloader = HLS()
79
- hls_downloader.manifest_url = "https://example.com/path/to/manifest.m3u8" # Set your HLS manifest URL
80
- hls_downloader.output_name = "output.mp4" # Set desired output name
81
- hls_downloader.decryption_key = "12345:678910" # Set decryption key if needed
82
- hls_downloader.hls_downloader() # Call the downloader method
83
- ```
84
-
85
- - CLI Usage:
86
- ```bash
87
- DDownloader -h
88
- ```
89
-
90
- - ![image](https://github.com/user-attachments/assets/5abdee78-2bb3-45be-b784-c8de86dac237)
91
-
92
-
93
- ## THIS PROJECT STILL IN DEVELOPMENT
94
-
95
- - Contributions are welcome! Feel free to open issues, create pull requests, or provide suggestions.
@@ -1,15 +0,0 @@
1
- DDownloader/__init__.py,sha256=lVZwmZNId0Dai7XBQpxglmJtIxAtZplRHDsvobL2UNo,33
2
- DDownloader/main.py,sha256=wB381d3Kf8ng9PPN52LVn_Ub5_7hF0Pnf0uZA_tJHvk,4211
3
- DDownloader/modules/__init__.py,sha256=frcCV1k9oG9oKj3dpUqdJg1PxRT2RSN_XKdLCPjaYaY,2
4
- DDownloader/modules/args_parser.py,sha256=DK_oNrR2c8hZO8Bn4IscHbzfwzM8x4RbZ54fPeF9X5M,1001
5
- DDownloader/modules/banners.py,sha256=aUoLVFkYjChEqmGNqrbFc2kh80Uv1Kp-eniOudot8cg,4059
6
- DDownloader/modules/dash_downloader.py,sha256=wrLm9euQcjr7fpSVbjl57-F_ZA431_W6zH9ITCWHUIs,3953
7
- DDownloader/modules/helper.py,sha256=zmJxs0dX72vWaYDKTWUy1KxiTcGQ4zJa6ZUz5RVz3NA,3811
8
- DDownloader/modules/hls_downloader.py,sha256=cHDQD_SPorkE3aYF9470pR6zYyKHcIiIsOH2znzWW-E,3948
9
- DDownloader/modules/streamlink.py,sha256=F8vneSkxgGgqxRBhCHxvID-KwltpDG2QerH6QsAHuxE,506
10
- DDownloader-0.3.2.dist-info/LICENSE,sha256=cnjTim3BMjb9cVC_b3oS41FESKLuvuDsufVHa_ymZRw,1090
11
- DDownloader-0.3.2.dist-info/METADATA,sha256=dfSD5WofrD92mdD1-9Xy5JrD64RjfrQzbXioAeci1GE,3754
12
- DDownloader-0.3.2.dist-info/WHEEL,sha256=In9FTNxeP60KnTkGw7wk6mJPYd_dQSjEZmXdBdMCI-8,91
13
- DDownloader-0.3.2.dist-info/entry_points.txt,sha256=tCZVr_SRONlWlMFsVKgcPj3lxe9gBtWD4GuWukMv75g,54
14
- DDownloader-0.3.2.dist-info/top_level.txt,sha256=INZYgY1vEHV1MIWTPXKJL8j8-ZXjWb8u4XLuU3S8umY,12
15
- DDownloader-0.3.2.dist-info/RECORD,,