DDownloader 0.3.1__tar.gz → 0.3.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.
- ddownloader-0.3.3/DDownloader/main.py +120 -0
- ddownloader-0.3.3/DDownloader/modules/__init__.py +1 -0
- {ddownloader-0.3.1 → ddownloader-0.3.3}/DDownloader/modules/banners.py +27 -10
- ddownloader-0.3.1/DDownloader/modules/dash_downloader.py → ddownloader-0.3.3/DDownloader/modules/downloader.py +18 -21
- {ddownloader-0.3.1 → ddownloader-0.3.3}/DDownloader/modules/helper.py +62 -13
- {ddownloader-0.3.1 → ddownloader-0.3.3}/DDownloader/modules/streamlink.py +1 -1
- {ddownloader-0.3.1 → ddownloader-0.3.3}/DDownloader.egg-info/PKG-INFO +1 -7
- {ddownloader-0.3.1 → ddownloader-0.3.3}/DDownloader.egg-info/SOURCES.txt +1 -3
- {ddownloader-0.3.1 → ddownloader-0.3.3}/PKG-INFO +1 -7
- {ddownloader-0.3.1 → ddownloader-0.3.3}/pyproject.toml +20 -9
- ddownloader-0.3.1/DDownloader/main.py +0 -100
- ddownloader-0.3.1/DDownloader/modules/__init__.py +0 -1
- ddownloader-0.3.1/DDownloader/modules/hls_downloader.py +0 -99
- ddownloader-0.3.1/DDownloader.egg-info/requires.txt +0 -5
- {ddownloader-0.3.1 → ddownloader-0.3.3}/DDownloader/__init__.py +0 -0
- {ddownloader-0.3.1 → ddownloader-0.3.3}/DDownloader/modules/args_parser.py +0 -0
- {ddownloader-0.3.1 → ddownloader-0.3.3}/DDownloader.egg-info/dependency_links.txt +0 -0
- {ddownloader-0.3.1 → ddownloader-0.3.3}/DDownloader.egg-info/entry_points.txt +0 -0
- {ddownloader-0.3.1 → ddownloader-0.3.3}/DDownloader.egg-info/top_level.txt +0 -0
- {ddownloader-0.3.1 → ddownloader-0.3.3}/LICENSE +0 -0
- {ddownloader-0.3.1 → ddownloader-0.3.3}/README.md +0 -0
- {ddownloader-0.3.1 → ddownloader-0.3.3}/setup.cfg +0 -0
@@ -0,0 +1,120 @@
|
|
1
|
+
import os, re, logging, coloredlogs, time, json
|
2
|
+
from pathlib import Path
|
3
|
+
from colorama import Fore, Style
|
4
|
+
from DDownloader.modules.helper import download_binaries, detect_platform, get_media_info
|
5
|
+
from DDownloader.modules.args_parser import parse_arguments
|
6
|
+
from DDownloader.modules.banners import clear_and_print, display_help
|
7
|
+
from DDownloader.modules.downloader import DOWNLOADER
|
8
|
+
|
9
|
+
logger = logging.getLogger("+ MAIN + ")
|
10
|
+
coloredlogs.install(level='DEBUG', logger=logger)
|
11
|
+
|
12
|
+
# =========================================================================================================== #
|
13
|
+
|
14
|
+
def validate_directories():
|
15
|
+
downloads_dir = 'downloads'
|
16
|
+
if not os.path.exists(downloads_dir):
|
17
|
+
os.makedirs(downloads_dir)
|
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 + "═" * 80 + Fore.RESET + "\n")
|
50
|
+
|
51
|
+
except Exception as e:
|
52
|
+
logger.error(f"Failed to process {file_path}: {e}")
|
53
|
+
|
54
|
+
# =========================================================================================================== #
|
55
|
+
|
56
|
+
def main():
|
57
|
+
clear_and_print()
|
58
|
+
platform_name = detect_platform()
|
59
|
+
logger.info("Downloading binaries... Please wait!")
|
60
|
+
print(Fore.MAGENTA + "=" * 100 + Fore.RESET)
|
61
|
+
time.sleep(1)
|
62
|
+
bin_dir = Path(__file__).resolve().parent / "bin"
|
63
|
+
download_binaries(bin_dir, platform_name)
|
64
|
+
clear_and_print()
|
65
|
+
|
66
|
+
downloads_dir = validate_directories()
|
67
|
+
try:
|
68
|
+
args = parse_arguments()
|
69
|
+
except SystemExit:
|
70
|
+
display_help()
|
71
|
+
exit(1)
|
72
|
+
|
73
|
+
downloader = DOWNLOADER()
|
74
|
+
|
75
|
+
if re.search(r"\.mpd\b", args.url, re.IGNORECASE):
|
76
|
+
logger.info("DASH stream detected. Initializing DASH downloader...")
|
77
|
+
elif re.search(r"\.m3u8\b", args.url, re.IGNORECASE):
|
78
|
+
logger.info("HLS stream detected. Initializing HLS downloader...")
|
79
|
+
elif re.search(r"\.ism\b", args.url, re.IGNORECASE):
|
80
|
+
logger.info("ISM (Smooth Streaming) detected. Initializing ISM downloader...")
|
81
|
+
else:
|
82
|
+
logger.error("Unsupported URL format. Please provide a valid DASH (.mpd), HLS (.m3u8), or ISM (.ism) URL.")
|
83
|
+
exit(1)
|
84
|
+
|
85
|
+
downloader.manifest_url = args.url
|
86
|
+
downloader.output_name = args.output
|
87
|
+
downloader.decryption_keys = args.key or []
|
88
|
+
downloader.headers = args.header or []
|
89
|
+
downloader.proxy = args.proxy
|
90
|
+
|
91
|
+
if downloader.proxy:
|
92
|
+
if not downloader.proxy.startswith("http://"):
|
93
|
+
downloader.proxy = f"http://{downloader.proxy}"
|
94
|
+
logger.info(f"Proxy: {downloader.proxy}")
|
95
|
+
print(Fore.RED + "═" * 80 + Fore.RESET + "\n")
|
96
|
+
|
97
|
+
if downloader.headers:
|
98
|
+
logger.info("Headers:")
|
99
|
+
for header in downloader.headers:
|
100
|
+
logger.info(f" - {header}")
|
101
|
+
print(Fore.RED + "═" * 80 + Fore.RESET + "\n")
|
102
|
+
|
103
|
+
if downloader.decryption_keys:
|
104
|
+
logger.info("Decryption keys:")
|
105
|
+
for key in downloader.decryption_keys:
|
106
|
+
logger.info(f" - {key}")
|
107
|
+
print(Fore.RED + "═" * 80 + Fore.RESET + "\n")
|
108
|
+
|
109
|
+
try:
|
110
|
+
downloader.drm_downloader()
|
111
|
+
except Exception as e:
|
112
|
+
logger.error(f"An error occurred during the download process: {e}")
|
113
|
+
exit(1)
|
114
|
+
|
115
|
+
process_media_info(downloads_dir)
|
116
|
+
|
117
|
+
# =========================================================================================================== #
|
118
|
+
|
119
|
+
if __name__ == "__main__":
|
120
|
+
main()
|
@@ -0,0 +1 @@
|
|
1
|
+
__version__ = "0.3.3"
|
@@ -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,30 @@ 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.
|
23
|
-
|
26
|
+
print(f"{Fore.YELLOW}[DDownloader] - {Fore.GREEN}Download DASH or HLS streams with decryption keys. - {Fore.RED}[V0.3.3] \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.WHITE}+" + "=" * 100 + f"+{Style.RESET_ALL}\n"
|
41
|
+
f"{Fore.CYAN}{'Option':<40}{'Description':<90}{Style.RESET_ALL}\n"
|
42
|
+
f"{Fore.WHITE}+" + "=" * 100 + f"+{Style.RESET_ALL}\n"
|
43
|
+
f" {Fore.GREEN}-u, --url{' ' * 22}{Style.RESET_ALL}URL of the manifest (mpd/m3u8) 🌐\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.GREEN}-h, --help{' ' * 21}{Style.RESET_ALL}Show this help message and exit ❓\n"
|
49
|
+
f"{Fore.WHITE}+" + "=" * 100 + f"+{Style.RESET_ALL}\n"
|
50
|
+
)
|
51
|
+
|
52
|
+
# =========================================================================================================== #
|
@@ -8,7 +8,7 @@ from colorama import Fore
|
|
8
8
|
logger = logging.getLogger(Fore.RED + "+ DASH + ")
|
9
9
|
coloredlogs.install(level='DEBUG', logger=logger)
|
10
10
|
|
11
|
-
class
|
11
|
+
class DOWNLOADER:
|
12
12
|
def __init__(self):
|
13
13
|
self.manifest_url = None
|
14
14
|
self.output_name = None
|
@@ -17,25 +17,20 @@ class DASH:
|
|
17
17
|
self.headers = []
|
18
18
|
self.binary_path = self._get_binary_path()
|
19
19
|
|
20
|
+
# =========================================================================================================== #
|
21
|
+
|
20
22
|
def _get_binary_path(self):
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
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
|
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
|
+
|
30
27
|
binary_name = 'N_m3u8DL-RE.exe' if platform.system() == 'Windows' else 'N_m3u8DL-RE'
|
31
28
|
binary = os.path.join(bin_dir, binary_name)
|
32
29
|
|
33
|
-
# Check if the binary exists
|
34
30
|
if not os.path.isfile(binary):
|
35
31
|
logger.error(f"Binary not found: {binary}")
|
36
32
|
raise FileNotFoundError(f"Binary not found: {binary}")
|
37
33
|
|
38
|
-
# Ensure the binary is executable on Linux
|
39
34
|
if platform.system() == 'Linux':
|
40
35
|
chmod_command = ['chmod', '+x', binary]
|
41
36
|
try:
|
@@ -44,16 +39,19 @@ class DASH:
|
|
44
39
|
except subprocess.CalledProcessError as e:
|
45
40
|
logger.error(Fore.RED + f"Failed to set executable permissions for: {binary}" + Fore.RESET)
|
46
41
|
raise RuntimeError(f"Could not set executable permissions for: {binary}") from e
|
47
|
-
|
48
42
|
return binary
|
49
43
|
|
50
|
-
|
44
|
+
# =========================================================================================================== #
|
45
|
+
|
46
|
+
def drm_downloader(self):
|
51
47
|
if not self.manifest_url:
|
52
48
|
logger.error("Manifest URL is not set.")
|
53
49
|
return
|
54
50
|
command = self._build_command()
|
55
51
|
self._execute_command(command)
|
56
52
|
|
53
|
+
# =========================================================================================================== #
|
54
|
+
|
57
55
|
def _build_command(self):
|
58
56
|
command = [
|
59
57
|
self.binary_path,
|
@@ -76,25 +74,24 @@ class DASH:
|
|
76
74
|
self.proxy = f"http://{self.proxy}"
|
77
75
|
command.extend(['--custom-proxy', f'"{self.proxy}"'])
|
78
76
|
|
79
|
-
# Add headers if any are provided
|
80
77
|
for header in self.headers:
|
81
78
|
command.extend(['-H', f'"{header}"'])
|
82
79
|
|
83
|
-
# logger.debug(f"Built command: {' '.join(command)}")
|
84
80
|
return command
|
85
81
|
|
82
|
+
# =========================================================================================================== #
|
83
|
+
|
86
84
|
def _execute_command(self, command):
|
87
85
|
try:
|
88
86
|
command_str = ' '.join(command)
|
89
|
-
# logger.debug(f"Executing command: {command_str}")
|
90
87
|
result = os.system(command_str)
|
91
88
|
|
92
89
|
if result == 0:
|
93
90
|
logger.info(Fore.GREEN + "Downloaded successfully. Bye!" + Fore.RESET)
|
94
91
|
else:
|
95
|
-
|
96
|
-
|
97
|
-
# logger.error(Fore.RED + f"Command: {command_str}" + Fore.RESET)
|
92
|
+
pass
|
93
|
+
|
98
94
|
except Exception as e:
|
99
95
|
logger.error(Fore.RED + f"An unexpected error occurred: {e}" + Fore.RESET)
|
100
|
-
|
96
|
+
|
97
|
+
# =========================================================================================================== #
|
@@ -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
|
-
#
|
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
|
+
# =========================================================================================================== #
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.2
|
2
2
|
Name: DDownloader
|
3
|
-
Version: 0.3.
|
3
|
+
Version: 0.3.3
|
4
4
|
Summary: A downloader for DRM-protected content.
|
5
5
|
Author-email: ThatNotEasy <apidotmy@proton.me>
|
6
6
|
License: MIT License
|
@@ -24,7 +24,6 @@ License: MIT License
|
|
24
24
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
25
25
|
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
|
26
26
|
IN THE SOFTWARE.
|
27
|
-
Project-URL: homepage, https://github.com/ThatNotEasy/DDownloader
|
28
27
|
Classifier: Programming Language :: Python :: 3
|
29
28
|
Classifier: Programming Language :: Python :: 3.7
|
30
29
|
Classifier: Programming Language :: Python :: 3.8
|
@@ -36,11 +35,6 @@ Classifier: Operating System :: OS Independent
|
|
36
35
|
Requires-Python: >=3.7
|
37
36
|
Description-Content-Type: text/markdown
|
38
37
|
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
38
|
|
45
39
|
# DDownloader
|
46
40
|
- DDownloader is a Python library to download HLS and DASH manifests and decrypt media files.
|
@@ -7,12 +7,10 @@ DDownloader.egg-info/PKG-INFO
|
|
7
7
|
DDownloader.egg-info/SOURCES.txt
|
8
8
|
DDownloader.egg-info/dependency_links.txt
|
9
9
|
DDownloader.egg-info/entry_points.txt
|
10
|
-
DDownloader.egg-info/requires.txt
|
11
10
|
DDownloader.egg-info/top_level.txt
|
12
11
|
DDownloader/modules/__init__.py
|
13
12
|
DDownloader/modules/args_parser.py
|
14
13
|
DDownloader/modules/banners.py
|
15
|
-
DDownloader/modules/
|
14
|
+
DDownloader/modules/downloader.py
|
16
15
|
DDownloader/modules/helper.py
|
17
|
-
DDownloader/modules/hls_downloader.py
|
18
16
|
DDownloader/modules/streamlink.py
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.2
|
2
2
|
Name: DDownloader
|
3
|
-
Version: 0.3.
|
3
|
+
Version: 0.3.3
|
4
4
|
Summary: A downloader for DRM-protected content.
|
5
5
|
Author-email: ThatNotEasy <apidotmy@proton.me>
|
6
6
|
License: MIT License
|
@@ -24,7 +24,6 @@ License: MIT License
|
|
24
24
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
25
25
|
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
|
26
26
|
IN THE SOFTWARE.
|
27
|
-
Project-URL: homepage, https://github.com/ThatNotEasy/DDownloader
|
28
27
|
Classifier: Programming Language :: Python :: 3
|
29
28
|
Classifier: Programming Language :: Python :: 3.7
|
30
29
|
Classifier: Programming Language :: Python :: 3.8
|
@@ -36,11 +35,6 @@ Classifier: Operating System :: OS Independent
|
|
36
35
|
Requires-Python: >=3.7
|
37
36
|
Description-Content-Type: text/markdown
|
38
37
|
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
38
|
|
45
39
|
# DDownloader
|
46
40
|
- DDownloader is a Python library to download HLS and DASH manifests and decrypt media files.
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
4
4
|
|
5
5
|
[project]
|
6
6
|
name = "DDownloader"
|
7
|
-
version = "0.3.
|
7
|
+
version = "0.3.3"
|
8
8
|
description = "A downloader for DRM-protected content."
|
9
9
|
readme = { file = "README.md", content-type = "text/markdown" }
|
10
10
|
authors = [
|
@@ -22,19 +22,30 @@ classifiers = [
|
|
22
22
|
"License :: OSI Approved :: MIT License",
|
23
23
|
"Operating System :: OS Independent"
|
24
24
|
]
|
25
|
-
dependencies = [
|
26
|
-
"requests>=2.26.0",
|
27
|
-
"coloredlogs>=15.0",
|
28
|
-
"tqdm>=4.64.0",
|
29
|
-
"colorama>=0.4.5",
|
30
|
-
"loguru>=0.6.0"
|
31
|
-
]
|
32
25
|
|
33
|
-
[
|
26
|
+
[tool.poetry]
|
27
|
+
name = "DDownloader"
|
28
|
+
version = "0.3.3"
|
29
|
+
description = "A downloader for DRM-protected content."
|
30
|
+
authors = ["ThatNotEasy <apidotmy@proton.me>"]
|
31
|
+
license = "MIT"
|
32
|
+
readme = "README.md"
|
34
33
|
homepage = "https://github.com/ThatNotEasy/DDownloader"
|
35
34
|
|
35
|
+
[tool.poetry.dependencies]
|
36
|
+
python = ">=3.7"
|
37
|
+
requests = ">=2.26.0"
|
38
|
+
coloredlogs = ">=15.0"
|
39
|
+
tqdm = ">=4.64.0"
|
40
|
+
colorama = ">=0.4.5"
|
41
|
+
loguru = ">=0.6.0"
|
42
|
+
pymediainfo = "^6.1.0" # Added pymediainfo
|
43
|
+
|
36
44
|
[tool.setuptools.packages.find]
|
37
45
|
include = ["DDownloader", "DDownloader.*"]
|
38
46
|
|
39
47
|
[project.scripts]
|
40
48
|
DDownloader = "DDownloader.main:main"
|
49
|
+
|
50
|
+
[tool.poetry.scripts]
|
51
|
+
DDownloader = "DDownloader.main:main"
|
@@ -1,100 +0,0 @@
|
|
1
|
-
import os
|
2
|
-
import re
|
3
|
-
import logging
|
4
|
-
import coloredlogs
|
5
|
-
import time
|
6
|
-
from pathlib import Path
|
7
|
-
from colorama import Fore, Style
|
8
|
-
from DDownloader.modules.helper import download_binaries, detect_platform
|
9
|
-
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
|
13
|
-
|
14
|
-
# Setup logger
|
15
|
-
logger = logging.getLogger("+ MAIN + ")
|
16
|
-
coloredlogs.install(level='DEBUG', logger=logger)
|
17
|
-
|
18
|
-
def validate_directories():
|
19
|
-
downloads_dir = 'downloads'
|
20
|
-
if not os.path.exists(downloads_dir):
|
21
|
-
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
|
-
)
|
38
|
-
|
39
|
-
def main():
|
40
|
-
clear_and_print()
|
41
|
-
platform_name = detect_platform()
|
42
|
-
logger.info(f"Downloading binaries... Please wait!")
|
43
|
-
print(Fore.MAGENTA + "=" * 100 + Fore.RESET)
|
44
|
-
time.sleep(1)
|
45
|
-
bin_dir = Path(__file__).resolve().parent / "bin"
|
46
|
-
download_binaries(bin_dir, platform_name)
|
47
|
-
clear_and_print()
|
48
|
-
|
49
|
-
validate_directories()
|
50
|
-
try:
|
51
|
-
args = parse_arguments()
|
52
|
-
except SystemExit:
|
53
|
-
display_help()
|
54
|
-
exit(1)
|
55
|
-
|
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)
|
66
|
-
|
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
|
-
# Log provided headers
|
75
|
-
if downloader.headers:
|
76
|
-
print(Fore.MAGENTA + "=" * 100 + Fore.RESET)
|
77
|
-
logger.info("Headers provided:")
|
78
|
-
for header in downloader.headers:
|
79
|
-
logger.info(f" -H {header}")
|
80
|
-
print(Fore.MAGENTA + "=" * 100 + Fore.RESET)
|
81
|
-
|
82
|
-
# Log provided decryption keys
|
83
|
-
if downloader.decryption_keys:
|
84
|
-
logger.info("Decryption keys provided:")
|
85
|
-
for key in downloader.decryption_keys:
|
86
|
-
logger.info(f" --key {key}")
|
87
|
-
print(Fore.MAGENTA + "=" * 100 + Fore.RESET)
|
88
|
-
|
89
|
-
# Execute downloader
|
90
|
-
try:
|
91
|
-
if isinstance(downloader, DASH):
|
92
|
-
downloader.dash_downloader()
|
93
|
-
elif isinstance(downloader, HLS):
|
94
|
-
downloader.hls_downloader()
|
95
|
-
except Exception as e:
|
96
|
-
logger.error(f"An error occurred during the download process: {e}")
|
97
|
-
exit(1)
|
98
|
-
|
99
|
-
if __name__ == "__main__":
|
100
|
-
main()
|
@@ -1 +0,0 @@
|
|
1
|
-
|
@@ -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)
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|