StreamingCommunity 3.3.9__py3-none-any.whl → 3.4.2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of StreamingCommunity might be problematic. Click here for more details.
- StreamingCommunity/Api/Player/hdplayer.py +0 -5
- StreamingCommunity/Api/Player/mediapolisvod.py +4 -13
- StreamingCommunity/Api/Player/supervideo.py +3 -8
- StreamingCommunity/Api/Player/sweetpixel.py +1 -9
- StreamingCommunity/Api/Player/vixcloud.py +5 -16
- StreamingCommunity/Api/Site/altadefinizione/film.py +4 -16
- StreamingCommunity/Api/Site/altadefinizione/series.py +3 -12
- StreamingCommunity/Api/Site/altadefinizione/site.py +2 -9
- StreamingCommunity/Api/Site/altadefinizione/util/ScrapeSerie.py +2 -7
- StreamingCommunity/Api/Site/animeunity/site.py +9 -24
- StreamingCommunity/Api/Site/animeunity/util/ScrapeSerie.py +11 -27
- StreamingCommunity/Api/Site/animeworld/film.py +4 -2
- StreamingCommunity/Api/Site/animeworld/site.py +3 -11
- StreamingCommunity/Api/Site/animeworld/util/ScrapeSerie.py +1 -4
- StreamingCommunity/Api/Site/crunchyroll/film.py +4 -5
- StreamingCommunity/Api/Site/crunchyroll/series.py +5 -17
- StreamingCommunity/Api/Site/crunchyroll/site.py +4 -13
- StreamingCommunity/Api/Site/crunchyroll/util/ScrapeSerie.py +5 -27
- StreamingCommunity/Api/Site/crunchyroll/util/get_license.py +11 -26
- StreamingCommunity/Api/Site/guardaserie/series.py +3 -14
- StreamingCommunity/Api/Site/guardaserie/site.py +4 -12
- StreamingCommunity/Api/Site/guardaserie/util/ScrapeSerie.py +3 -10
- StreamingCommunity/Api/Site/mediasetinfinity/film.py +11 -12
- StreamingCommunity/Api/Site/mediasetinfinity/series.py +4 -15
- StreamingCommunity/Api/Site/mediasetinfinity/site.py +16 -32
- StreamingCommunity/Api/Site/mediasetinfinity/util/ScrapeSerie.py +39 -50
- StreamingCommunity/Api/Site/mediasetinfinity/util/fix_mpd.py +3 -3
- StreamingCommunity/Api/Site/mediasetinfinity/util/get_license.py +7 -25
- StreamingCommunity/Api/Site/raiplay/film.py +6 -8
- StreamingCommunity/Api/Site/raiplay/series.py +5 -20
- StreamingCommunity/Api/Site/raiplay/site.py +45 -47
- StreamingCommunity/Api/Site/raiplay/util/ScrapeSerie.py +91 -55
- StreamingCommunity/Api/Site/raiplay/util/get_license.py +3 -12
- StreamingCommunity/Api/Site/streamingcommunity/film.py +5 -16
- StreamingCommunity/Api/Site/streamingcommunity/series.py +5 -10
- StreamingCommunity/Api/Site/streamingcommunity/site.py +3 -22
- StreamingCommunity/Api/Site/streamingcommunity/util/ScrapeSerie.py +11 -27
- StreamingCommunity/Api/Site/streamingwatch/__init__.py +1 -0
- StreamingCommunity/Api/Site/streamingwatch/film.py +4 -2
- StreamingCommunity/Api/Site/streamingwatch/series.py +4 -14
- StreamingCommunity/Api/Site/streamingwatch/site.py +4 -18
- StreamingCommunity/Api/Site/streamingwatch/util/ScrapeSerie.py +0 -3
- StreamingCommunity/Api/Template/Util/__init__.py +4 -2
- StreamingCommunity/Api/Template/Util/manage_ep.py +66 -0
- StreamingCommunity/Api/Template/config_loader.py +0 -7
- StreamingCommunity/Lib/Downloader/DASH/decrypt.py +54 -1
- StreamingCommunity/Lib/Downloader/DASH/downloader.py +186 -70
- StreamingCommunity/Lib/Downloader/DASH/parser.py +2 -3
- StreamingCommunity/Lib/Downloader/DASH/segments.py +109 -68
- StreamingCommunity/Lib/Downloader/HLS/downloader.py +100 -82
- StreamingCommunity/Lib/Downloader/HLS/segments.py +40 -28
- StreamingCommunity/Lib/Downloader/MP4/downloader.py +16 -4
- StreamingCommunity/Lib/FFmpeg/capture.py +37 -5
- StreamingCommunity/Lib/FFmpeg/command.py +32 -90
- StreamingCommunity/Lib/M3U8/estimator.py +47 -1
- StreamingCommunity/Lib/TMBD/tmdb.py +2 -4
- StreamingCommunity/TelegramHelp/config.json +0 -1
- StreamingCommunity/Upload/update.py +19 -6
- StreamingCommunity/Upload/version.py +1 -1
- StreamingCommunity/Util/config_json.py +28 -21
- StreamingCommunity/Util/http_client.py +28 -0
- StreamingCommunity/Util/os.py +16 -6
- StreamingCommunity/Util/table.py +50 -8
- {streamingcommunity-3.3.9.dist-info → streamingcommunity-3.4.2.dist-info}/METADATA +1 -3
- streamingcommunity-3.4.2.dist-info/RECORD +111 -0
- streamingcommunity-3.3.9.dist-info/RECORD +0 -111
- {streamingcommunity-3.3.9.dist-info → streamingcommunity-3.4.2.dist-info}/WHEEL +0 -0
- {streamingcommunity-3.3.9.dist-info → streamingcommunity-3.4.2.dist-info}/entry_points.txt +0 -0
- {streamingcommunity-3.3.9.dist-info → streamingcommunity-3.4.2.dist-info}/licenses/LICENSE +0 -0
- {streamingcommunity-3.3.9.dist-info → streamingcommunity-3.4.2.dist-info}/top_level.txt +0 -0
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# 19.06.24
|
|
2
2
|
|
|
3
3
|
import sys
|
|
4
|
+
import time
|
|
4
5
|
import logging
|
|
5
6
|
from typing import List
|
|
6
7
|
|
|
@@ -209,6 +210,71 @@ def validate_episode_selection(list_episode_select: List[int], episodes_count: i
|
|
|
209
210
|
list_episode_select = list(map(int, input_episodes.split(',')))
|
|
210
211
|
|
|
211
212
|
|
|
213
|
+
def display_seasons_list(seasons_manager) -> str:
|
|
214
|
+
"""
|
|
215
|
+
Display seasons list and handle user input.
|
|
216
|
+
|
|
217
|
+
Parameters:
|
|
218
|
+
- seasons_manager: Manager object containing seasons information.
|
|
219
|
+
|
|
220
|
+
Returns:
|
|
221
|
+
last_command (str): Last command entered by the user.
|
|
222
|
+
"""
|
|
223
|
+
if len(seasons_manager.seasons) == 1:
|
|
224
|
+
console.print("\n[green]Only one season available, selecting it automatically[/green]")
|
|
225
|
+
time.sleep(1)
|
|
226
|
+
return "1"
|
|
227
|
+
|
|
228
|
+
# Set up table for displaying seasons
|
|
229
|
+
table_show_manager = TVShowManager()
|
|
230
|
+
|
|
231
|
+
# Check if 'type' and 'id' attributes exist in the first season
|
|
232
|
+
has_type = hasattr(seasons_manager.seasons[0], 'type') and (seasons_manager.seasons[0].type) is not None and str(seasons_manager.seasons[0].type) != ''
|
|
233
|
+
has_id = hasattr(seasons_manager.seasons[0], 'id') and (seasons_manager.seasons[0].id) is not None and str(seasons_manager.seasons[0].id) != ''
|
|
234
|
+
|
|
235
|
+
# Add columns to the table
|
|
236
|
+
column_info = {
|
|
237
|
+
"Index": {'color': 'red'},
|
|
238
|
+
"Name": {'color': 'yellow'}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
if has_type:
|
|
242
|
+
column_info["Type"] = {'color': 'magenta'}
|
|
243
|
+
|
|
244
|
+
if has_id:
|
|
245
|
+
column_info["ID"] = {'color': 'cyan'}
|
|
246
|
+
|
|
247
|
+
table_show_manager.add_column(column_info)
|
|
248
|
+
|
|
249
|
+
# Populate the table with seasons information
|
|
250
|
+
for i, season in enumerate(seasons_manager.seasons):
|
|
251
|
+
season_name = season.name if hasattr(season, 'name') else 'N/A'
|
|
252
|
+
season_info = {
|
|
253
|
+
'Index': str(i + 1),
|
|
254
|
+
'Name': season_name
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
# Add 'Type' and 'ID' if they exist
|
|
258
|
+
if has_type:
|
|
259
|
+
season_type = season.type if hasattr(season, 'type') else 'N/A'
|
|
260
|
+
season_info['Type'] = season_type
|
|
261
|
+
|
|
262
|
+
if has_id:
|
|
263
|
+
season_id = season.id if hasattr(season, 'id') else 'N/A'
|
|
264
|
+
season_info['ID'] = season_id
|
|
265
|
+
|
|
266
|
+
table_show_manager.add_tv_show(season_info)
|
|
267
|
+
|
|
268
|
+
# Run the table and handle user input
|
|
269
|
+
last_command = table_show_manager.run()
|
|
270
|
+
|
|
271
|
+
if last_command in ("q", "quit"):
|
|
272
|
+
console.print("\n[red]Quit ...")
|
|
273
|
+
sys.exit(0)
|
|
274
|
+
|
|
275
|
+
return last_command
|
|
276
|
+
|
|
277
|
+
|
|
212
278
|
def display_episodes_list(episodes_manager) -> str:
|
|
213
279
|
"""
|
|
214
280
|
Display episodes list and handle user input.
|
|
@@ -56,13 +56,6 @@ class SiteConstant:
|
|
|
56
56
|
base_path = os.path.join(base_path, self.SITE_NAME)
|
|
57
57
|
return os.path.join(base_path, config_manager.get('OUT_FOLDER', 'anime_folder_name'))
|
|
58
58
|
|
|
59
|
-
@property
|
|
60
|
-
def COOKIE(self):
|
|
61
|
-
try:
|
|
62
|
-
return config_manager.get_dict('SITE_EXTRA', self.SITE_NAME)
|
|
63
|
-
except KeyError:
|
|
64
|
-
return None
|
|
65
|
-
|
|
66
59
|
@property
|
|
67
60
|
def TELEGRAM_BOT(self):
|
|
68
61
|
return config_manager.get_bool('DEFAULT', 'telegram_bot')
|
|
@@ -3,25 +3,30 @@
|
|
|
3
3
|
import os
|
|
4
4
|
import subprocess
|
|
5
5
|
import logging
|
|
6
|
+
import threading
|
|
7
|
+
import time
|
|
6
8
|
|
|
7
9
|
|
|
8
10
|
# External libraries
|
|
9
11
|
from rich.console import Console
|
|
12
|
+
from tqdm import tqdm
|
|
10
13
|
|
|
11
14
|
|
|
12
15
|
# Internal utilities
|
|
13
16
|
from StreamingCommunity.Util.os import get_mp4decrypt_path
|
|
17
|
+
from StreamingCommunity.Util.color import Colors
|
|
14
18
|
|
|
15
19
|
# Variable
|
|
16
20
|
console = Console()
|
|
17
21
|
|
|
18
22
|
|
|
19
23
|
# NOTE!: SAREBBE MEGLIO FARLO PER OGNI FILE DURANTE IL DOWNLOAD ... MA PER ORA LO LASCIO COSI
|
|
20
|
-
def decrypt_with_mp4decrypt(encrypted_path, kid, key, output_path=None, cleanup=True):
|
|
24
|
+
def decrypt_with_mp4decrypt(type, encrypted_path, kid, key, output_path=None, cleanup=True):
|
|
21
25
|
"""
|
|
22
26
|
Decrypt an mp4/m4s file using mp4decrypt.
|
|
23
27
|
|
|
24
28
|
Args:
|
|
29
|
+
type (str): Type of file ('video' or 'audio').
|
|
25
30
|
encrypted_path (str): Path to encrypted file.
|
|
26
31
|
kid (str): Hexadecimal KID.
|
|
27
32
|
key (str): Hexadecimal key.
|
|
@@ -48,15 +53,63 @@ def decrypt_with_mp4decrypt(encrypted_path, kid, key, output_path=None, cleanup=
|
|
|
48
53
|
if not output_path:
|
|
49
54
|
output_path = os.path.splitext(encrypted_path)[0] + "_decrypted.mp4"
|
|
50
55
|
|
|
56
|
+
# Get file size for progress tracking
|
|
57
|
+
file_size = os.path.getsize(encrypted_path)
|
|
58
|
+
|
|
51
59
|
key_format = f"{kid.lower()}:{key.lower()}"
|
|
52
60
|
cmd = [get_mp4decrypt_path(), "--key", key_format, encrypted_path, output_path]
|
|
53
61
|
logging.info(f"Running command: {' '.join(cmd)}")
|
|
54
62
|
|
|
63
|
+
# Create progress bar with custom format
|
|
64
|
+
bar_format = (
|
|
65
|
+
f"{Colors.YELLOW}DECRYPT{Colors.CYAN} {type}{Colors.WHITE}: "
|
|
66
|
+
f"{Colors.MAGENTA}{{bar:40}} "
|
|
67
|
+
f"{Colors.LIGHT_GREEN}{{n_fmt}}{Colors.WHITE}/{Colors.CYAN}{{total_fmt}} "
|
|
68
|
+
f"{Colors.DARK_GRAY}[{Colors.YELLOW}{{elapsed}}{Colors.WHITE} < {Colors.CYAN}{{remaining}}{Colors.DARK_GRAY}] "
|
|
69
|
+
f"{Colors.WHITE}{{postfix}}"
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
progress_bar = tqdm(
|
|
73
|
+
total=100,
|
|
74
|
+
bar_format=bar_format,
|
|
75
|
+
unit="",
|
|
76
|
+
ncols=150
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
def monitor_output_file():
|
|
80
|
+
"""Monitor output file growth and update progress bar."""
|
|
81
|
+
last_size = 0
|
|
82
|
+
while True:
|
|
83
|
+
if os.path.exists(output_path):
|
|
84
|
+
current_size = os.path.getsize(output_path)
|
|
85
|
+
if current_size > 0:
|
|
86
|
+
progress_percent = min(int((current_size / file_size) * 100), 100)
|
|
87
|
+
progress_bar.n = progress_percent
|
|
88
|
+
progress_bar.refresh()
|
|
89
|
+
|
|
90
|
+
if current_size == last_size and current_size > 0:
|
|
91
|
+
# File stopped growing, likely finished
|
|
92
|
+
break
|
|
93
|
+
|
|
94
|
+
last_size = current_size
|
|
95
|
+
|
|
96
|
+
time.sleep(0.1)
|
|
97
|
+
|
|
98
|
+
# Start monitoring thread
|
|
99
|
+
monitor_thread = threading.Thread(target=monitor_output_file, daemon=True)
|
|
100
|
+
monitor_thread.start()
|
|
101
|
+
|
|
55
102
|
try:
|
|
56
103
|
result = subprocess.run(cmd, capture_output=True, text=True, timeout=300)
|
|
57
104
|
except Exception as e:
|
|
105
|
+
progress_bar.close()
|
|
58
106
|
console.print(f"[bold red] mp4decrypt execution failed: {e}[/bold red]")
|
|
59
107
|
return None
|
|
108
|
+
|
|
109
|
+
# Ensure progress bar reaches 100%
|
|
110
|
+
progress_bar.n = 100
|
|
111
|
+
progress_bar.refresh()
|
|
112
|
+
progress_bar.close()
|
|
60
113
|
|
|
61
114
|
if result.returncode == 0 and os.path.exists(output_path):
|
|
62
115
|
|
|
@@ -1,19 +1,19 @@
|
|
|
1
1
|
# 25.07.25
|
|
2
2
|
|
|
3
3
|
import os
|
|
4
|
-
import time
|
|
5
4
|
import shutil
|
|
5
|
+
import logging
|
|
6
|
+
from typing import Optional, Dict
|
|
6
7
|
|
|
7
8
|
|
|
8
9
|
# External libraries
|
|
9
10
|
from rich.console import Console
|
|
10
|
-
from rich.panel import Panel
|
|
11
11
|
from rich.table import Table
|
|
12
12
|
|
|
13
13
|
|
|
14
14
|
# Internal utilities
|
|
15
15
|
from StreamingCommunity.Util.config_json import config_manager
|
|
16
|
-
from StreamingCommunity.Util.os import os_manager, internet_manager
|
|
16
|
+
from StreamingCommunity.Util.os import os_manager, internet_manager, get_wvd_path
|
|
17
17
|
from StreamingCommunity.Util.http_client import create_client
|
|
18
18
|
from StreamingCommunity.Util.headers import get_userAgent
|
|
19
19
|
|
|
@@ -32,11 +32,11 @@ from ...FFmpeg import print_duration_table, join_audios, join_video, join_subtit
|
|
|
32
32
|
# Config
|
|
33
33
|
DOWNLOAD_SPECIFIC_AUDIO = config_manager.get_list('M3U8_DOWNLOAD', 'specific_list_audio')
|
|
34
34
|
DOWNLOAD_SPECIFIC_SUBTITLE = config_manager.get_list('M3U8_DOWNLOAD', 'specific_list_subtitles')
|
|
35
|
-
ENABLE_SUBTITLE = config_manager.get_bool('M3U8_DOWNLOAD', 'download_subtitle')
|
|
36
35
|
MERGE_SUBTITLE = config_manager.get_bool('M3U8_DOWNLOAD', 'merge_subs')
|
|
37
36
|
FILTER_CUSTOM_REOLUTION = str(config_manager.get('M3U8_CONVERSION', 'force_resolution')).strip().lower()
|
|
38
37
|
CLEANUP_TMP = config_manager.get_bool('M3U8_DOWNLOAD', 'cleanup_tmp_folder')
|
|
39
38
|
RETRY_LIMIT = config_manager.get_int('REQUESTS', 'max_retry')
|
|
39
|
+
EXTENSION_OUTPUT = config_manager.get("M3U8_CONVERSION", "extension")
|
|
40
40
|
|
|
41
41
|
|
|
42
42
|
# Variable
|
|
@@ -44,18 +44,17 @@ console = Console()
|
|
|
44
44
|
|
|
45
45
|
|
|
46
46
|
class DASH_Downloader:
|
|
47
|
-
def __init__(self,
|
|
47
|
+
def __init__(self, license_url, mpd_url, mpd_sub_list: list = None, output_path: str = None):
|
|
48
48
|
"""
|
|
49
49
|
Initialize the DASH Downloader with necessary parameters.
|
|
50
50
|
|
|
51
51
|
Parameters:
|
|
52
|
-
- cdm_device (str): Path to the CDM device for decryption.
|
|
53
52
|
- license_url (str): URL to obtain the license for decryption.
|
|
54
53
|
- mpd_url (str): URL of the MPD manifest file.
|
|
55
54
|
- mpd_sub_list (list): List of subtitle dicts with keys: 'language', 'url', 'format'.
|
|
56
55
|
- output_path (str): Path to save the final output file.
|
|
57
56
|
"""
|
|
58
|
-
self.cdm_device =
|
|
57
|
+
self.cdm_device = get_wvd_path()
|
|
59
58
|
self.license_url = license_url
|
|
60
59
|
self.mpd_url = mpd_url
|
|
61
60
|
self.mpd_sub_list = mpd_sub_list or []
|
|
@@ -75,6 +74,10 @@ class DASH_Downloader:
|
|
|
75
74
|
self.error = None
|
|
76
75
|
self.stopped = False
|
|
77
76
|
self.output_file = None
|
|
77
|
+
|
|
78
|
+
# For progress tracking
|
|
79
|
+
self.current_downloader: Optional[MPD_Segments] = None
|
|
80
|
+
self.current_download_type: Optional[str] = None
|
|
78
81
|
|
|
79
82
|
def _setup_temp_dirs(self):
|
|
80
83
|
"""
|
|
@@ -196,49 +199,28 @@ class DASH_Downloader:
|
|
|
196
199
|
Download subtitle files based on configuration with retry mechanism.
|
|
197
200
|
Returns True if successful or if no subtitles to download, False on critical error.
|
|
198
201
|
"""
|
|
199
|
-
|
|
200
|
-
return True
|
|
201
|
-
|
|
202
|
-
headers = {'User-Agent': get_userAgent()}
|
|
203
|
-
client = create_client(headers=headers)
|
|
202
|
+
client = create_client(headers={'User-Agent': get_userAgent()})
|
|
204
203
|
|
|
205
204
|
for sub in self.selected_subs:
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
response
|
|
220
|
-
response.raise_for_status()
|
|
221
|
-
|
|
222
|
-
# Save subtitle file
|
|
223
|
-
sub_filename = f"{language}.{fmt}"
|
|
224
|
-
sub_path = os.path.join(self.subs_dir, sub_filename)
|
|
225
|
-
|
|
226
|
-
with open(sub_path, 'wb') as f:
|
|
227
|
-
f.write(response.content)
|
|
228
|
-
|
|
229
|
-
success = True
|
|
230
|
-
break
|
|
205
|
+
try:
|
|
206
|
+
language = sub.get('language', 'unknown')
|
|
207
|
+
fmt = sub.get('format', 'vtt')
|
|
208
|
+
|
|
209
|
+
# Download subtitle
|
|
210
|
+
response = client.get(sub.get('url'))
|
|
211
|
+
response.raise_for_status()
|
|
212
|
+
|
|
213
|
+
# Save subtitle file and make request
|
|
214
|
+
sub_filename = f"{language}.{fmt}"
|
|
215
|
+
sub_path = os.path.join(self.subs_dir, sub_filename)
|
|
216
|
+
|
|
217
|
+
with open(sub_path, 'wb') as f:
|
|
218
|
+
f.write(response.content)
|
|
231
219
|
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
time.sleep(1.5 ** attempt)
|
|
236
|
-
else:
|
|
237
|
-
console.print(f"[yellow]Warning: Failed to download subtitle {language} after {RETRY_LIMIT} attempts: {e}[/yellow]")
|
|
220
|
+
except Exception as e:
|
|
221
|
+
console.print(f"[red]Error downloading subtitle {language}: {e}[/red]")
|
|
222
|
+
return False
|
|
238
223
|
|
|
239
|
-
if not success:
|
|
240
|
-
continue
|
|
241
|
-
|
|
242
224
|
return True
|
|
243
225
|
|
|
244
226
|
def download_and_decrypt(self, custom_headers=None, custom_payload=None):
|
|
@@ -256,7 +238,6 @@ class DASH_Downloader:
|
|
|
256
238
|
|
|
257
239
|
# Fetch keys immediately after obtaining PSSH
|
|
258
240
|
if not self.parser.pssh:
|
|
259
|
-
console.print("[red]No PSSH found: segments are not encrypted, skipping decryption.")
|
|
260
241
|
self.download_segments(clear=True)
|
|
261
242
|
return True
|
|
262
243
|
|
|
@@ -293,6 +274,10 @@ class DASH_Downloader:
|
|
|
293
274
|
pssh=self.parser.pssh
|
|
294
275
|
)
|
|
295
276
|
|
|
277
|
+
# Set current downloader for progress tracking
|
|
278
|
+
self.current_downloader = video_downloader
|
|
279
|
+
self.current_download_type = 'video'
|
|
280
|
+
|
|
296
281
|
try:
|
|
297
282
|
result = video_downloader.download_streams(description="Video")
|
|
298
283
|
|
|
@@ -312,11 +297,15 @@ class DASH_Downloader:
|
|
|
312
297
|
except Exception as ex:
|
|
313
298
|
self.error = str(ex)
|
|
314
299
|
return False
|
|
300
|
+
|
|
301
|
+
finally:
|
|
302
|
+
self.current_downloader = None
|
|
303
|
+
self.current_download_type = None
|
|
315
304
|
|
|
316
305
|
# Decrypt video
|
|
317
306
|
decrypted_path = os.path.join(self.decrypted_dir, "video.mp4")
|
|
318
307
|
result_path = decrypt_with_mp4decrypt(
|
|
319
|
-
encrypted_path, KID, KEY, output_path=decrypted_path
|
|
308
|
+
"Video", encrypted_path, KID, KEY, output_path=decrypted_path
|
|
320
309
|
)
|
|
321
310
|
|
|
322
311
|
if not result_path:
|
|
@@ -345,6 +334,10 @@ class DASH_Downloader:
|
|
|
345
334
|
limit_segments=video_segments_count if video_segments_count > 0 else None
|
|
346
335
|
)
|
|
347
336
|
|
|
337
|
+
# Set current downloader for progress tracking
|
|
338
|
+
self.current_downloader = audio_downloader
|
|
339
|
+
self.current_download_type = f"audio_{audio_language}"
|
|
340
|
+
|
|
348
341
|
try:
|
|
349
342
|
result = audio_downloader.download_streams(description=f"Audio {audio_language}")
|
|
350
343
|
|
|
@@ -361,11 +354,15 @@ class DASH_Downloader:
|
|
|
361
354
|
except Exception as ex:
|
|
362
355
|
self.error = str(ex)
|
|
363
356
|
return False
|
|
357
|
+
|
|
358
|
+
finally:
|
|
359
|
+
self.current_downloader = None
|
|
360
|
+
self.current_download_type = None
|
|
364
361
|
|
|
365
362
|
# Decrypt audio
|
|
366
363
|
decrypted_path = os.path.join(self.decrypted_dir, "audio.mp4")
|
|
367
364
|
result_path = decrypt_with_mp4decrypt(
|
|
368
|
-
encrypted_path, KID, KEY, output_path=decrypted_path
|
|
365
|
+
f"Audio {audio_language}", encrypted_path, KID, KEY, output_path=decrypted_path
|
|
369
366
|
)
|
|
370
367
|
|
|
371
368
|
if not result_path:
|
|
@@ -381,9 +378,126 @@ class DASH_Downloader:
|
|
|
381
378
|
return True
|
|
382
379
|
|
|
383
380
|
def download_segments(self, clear=False):
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
381
|
+
"""
|
|
382
|
+
Download video/audio segments without decryption (for clear content).
|
|
383
|
+
|
|
384
|
+
Parameters:
|
|
385
|
+
clear (bool): If True, content is not encrypted and doesn't need decryption
|
|
386
|
+
"""
|
|
387
|
+
if not clear:
|
|
388
|
+
console.print("[yellow]Warning: download_segments called with clear=False[/yellow]")
|
|
389
|
+
return False
|
|
390
|
+
|
|
391
|
+
video_segments_count = 0
|
|
392
|
+
|
|
393
|
+
# Download subtitles
|
|
394
|
+
self.download_subtitles()
|
|
395
|
+
|
|
396
|
+
# Download video
|
|
397
|
+
video_rep = self.get_representation_by_type("video")
|
|
398
|
+
if video_rep:
|
|
399
|
+
encrypted_path = os.path.join(self.encrypted_dir, f"{video_rep['id']}_encrypted.m4s")
|
|
400
|
+
|
|
401
|
+
# If m4s file doesn't exist, start downloading
|
|
402
|
+
if not os.path.exists(encrypted_path):
|
|
403
|
+
video_downloader = MPD_Segments(
|
|
404
|
+
tmp_folder=self.encrypted_dir,
|
|
405
|
+
representation=video_rep,
|
|
406
|
+
pssh=self.parser.pssh
|
|
407
|
+
)
|
|
408
|
+
|
|
409
|
+
# Set current downloader for progress tracking
|
|
410
|
+
self.current_downloader = video_downloader
|
|
411
|
+
self.current_download_type = 'video'
|
|
412
|
+
|
|
413
|
+
try:
|
|
414
|
+
result = video_downloader.download_streams(description="Video")
|
|
415
|
+
|
|
416
|
+
# Store the video segment count for limiting audio
|
|
417
|
+
video_segments_count = video_downloader.get_segments_count()
|
|
418
|
+
|
|
419
|
+
# Check for interruption or failure
|
|
420
|
+
if result.get("stopped"):
|
|
421
|
+
self.stopped = True
|
|
422
|
+
self.error = "Download interrupted"
|
|
423
|
+
return False
|
|
424
|
+
|
|
425
|
+
if result.get("nFailed", 0) > 0:
|
|
426
|
+
self.error = f"Failed segments: {result['nFailed']}"
|
|
427
|
+
return False
|
|
428
|
+
|
|
429
|
+
except Exception as ex:
|
|
430
|
+
self.error = str(ex)
|
|
431
|
+
console.print(f"[red]Error downloading video: {ex}[/red]")
|
|
432
|
+
return False
|
|
433
|
+
|
|
434
|
+
finally:
|
|
435
|
+
self.current_downloader = None
|
|
436
|
+
self.current_download_type = None
|
|
437
|
+
|
|
438
|
+
# NO DECRYPTION: just copy/move to decrypted folder
|
|
439
|
+
decrypted_path = os.path.join(self.decrypted_dir, "video.mp4")
|
|
440
|
+
if os.path.exists(encrypted_path) and not os.path.exists(decrypted_path):
|
|
441
|
+
shutil.copy2(encrypted_path, decrypted_path)
|
|
442
|
+
|
|
443
|
+
else:
|
|
444
|
+
self.error = "No video found"
|
|
445
|
+
console.print(f"[red]{self.error}[/red]")
|
|
446
|
+
return False
|
|
447
|
+
|
|
448
|
+
# Download audio with segment limiting
|
|
449
|
+
audio_rep = self.get_representation_by_type("audio")
|
|
450
|
+
if audio_rep:
|
|
451
|
+
encrypted_path = os.path.join(self.encrypted_dir, f"{audio_rep['id']}_encrypted.m4s")
|
|
452
|
+
|
|
453
|
+
# If m4s file doesn't exist, start downloading
|
|
454
|
+
if not os.path.exists(encrypted_path):
|
|
455
|
+
audio_language = audio_rep.get('language', 'Unknown')
|
|
456
|
+
|
|
457
|
+
audio_downloader = MPD_Segments(
|
|
458
|
+
tmp_folder=self.encrypted_dir,
|
|
459
|
+
representation=audio_rep,
|
|
460
|
+
pssh=self.parser.pssh,
|
|
461
|
+
limit_segments=video_segments_count if video_segments_count > 0 else None
|
|
462
|
+
)
|
|
463
|
+
|
|
464
|
+
# Set current downloader for progress tracking
|
|
465
|
+
self.current_downloader = audio_downloader
|
|
466
|
+
self.current_download_type = f"audio_{audio_language}"
|
|
467
|
+
|
|
468
|
+
try:
|
|
469
|
+
result = audio_downloader.download_streams(description=f"Audio {audio_language}")
|
|
470
|
+
|
|
471
|
+
# Check for interruption or failure
|
|
472
|
+
if result.get("stopped"):
|
|
473
|
+
self.stopped = True
|
|
474
|
+
self.error = "Download interrupted"
|
|
475
|
+
return False
|
|
476
|
+
|
|
477
|
+
if result.get("nFailed", 0) > 0:
|
|
478
|
+
self.error = f"Failed segments: {result['nFailed']}"
|
|
479
|
+
return False
|
|
480
|
+
|
|
481
|
+
except Exception as ex:
|
|
482
|
+
self.error = str(ex)
|
|
483
|
+
console.print(f"[red]Error downloading audio: {ex}[/red]")
|
|
484
|
+
return False
|
|
485
|
+
|
|
486
|
+
finally:
|
|
487
|
+
self.current_downloader = None
|
|
488
|
+
self.current_download_type = None
|
|
489
|
+
|
|
490
|
+
# NO DECRYPTION: just copy/move to decrypted folder
|
|
491
|
+
decrypted_path = os.path.join(self.decrypted_dir, "audio.mp4")
|
|
492
|
+
if os.path.exists(encrypted_path) and not os.path.exists(decrypted_path):
|
|
493
|
+
shutil.copy2(encrypted_path, decrypted_path)
|
|
494
|
+
|
|
495
|
+
else:
|
|
496
|
+
self.error = "No audio found"
|
|
497
|
+
console.print(f"[red]{self.error}[/red]")
|
|
498
|
+
return False
|
|
499
|
+
|
|
500
|
+
return True
|
|
387
501
|
|
|
388
502
|
def finalize_output(self):
|
|
389
503
|
"""
|
|
@@ -415,8 +529,8 @@ class DASH_Downloader:
|
|
|
415
529
|
console.print("[red]Video file missing, cannot export[/red]")
|
|
416
530
|
return None
|
|
417
531
|
|
|
418
|
-
# Merge subtitles if
|
|
419
|
-
if MERGE_SUBTITLE and
|
|
532
|
+
# Merge subtitles if available
|
|
533
|
+
if MERGE_SUBTITLE and self.selected_subs:
|
|
420
534
|
|
|
421
535
|
# Check which subtitle files actually exist
|
|
422
536
|
existing_sub_tracks = []
|
|
@@ -455,7 +569,7 @@ class DASH_Downloader:
|
|
|
455
569
|
|
|
456
570
|
# Handle failed sync case
|
|
457
571
|
if use_shortest:
|
|
458
|
-
new_filename = output_file.replace(
|
|
572
|
+
new_filename = output_file.replace(EXTENSION_OUTPUT, f"_failed_sync{EXTENSION_OUTPUT}")
|
|
459
573
|
if os.path.exists(output_file):
|
|
460
574
|
os.rename(output_file, new_filename)
|
|
461
575
|
output_file = new_filename
|
|
@@ -465,20 +579,7 @@ class DASH_Downloader:
|
|
|
465
579
|
if os.path.exists(output_file):
|
|
466
580
|
file_size = internet_manager.format_file_size(os.path.getsize(output_file))
|
|
467
581
|
duration = print_duration_table(output_file, description=False, return_string=True)
|
|
468
|
-
|
|
469
|
-
panel_content = (
|
|
470
|
-
f"[cyan]File size: [bold red]{file_size}[/bold red]\n"
|
|
471
|
-
f"[cyan]Duration: [bold]{duration}[/bold]\n"
|
|
472
|
-
f"[cyan]Output: [bold]{os.path.abspath(output_file)}[/bold]"
|
|
473
|
-
)
|
|
474
|
-
|
|
475
|
-
print("")
|
|
476
|
-
console.print(Panel(
|
|
477
|
-
panel_content,
|
|
478
|
-
title=f"{os.path.basename(output_file.replace('.mp4', ''))}",
|
|
479
|
-
border_style="green"
|
|
480
|
-
))
|
|
481
|
-
|
|
582
|
+
console.print(f"[yellow]Output [red]{os.path.abspath(output_file)} [cyan]with size [red]{file_size} [cyan]and duration [red]{duration}")
|
|
482
583
|
else:
|
|
483
584
|
console.print(f"[red]Output file not found: {output_file}")
|
|
484
585
|
|
|
@@ -514,4 +615,19 @@ class DASH_Downloader:
|
|
|
514
615
|
"path": self.output_file,
|
|
515
616
|
"error": self.error,
|
|
516
617
|
"stopped": self.stopped
|
|
517
|
-
}
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
def get_progress_data(self) -> Optional[Dict]:
|
|
621
|
+
"""Get current download progress data."""
|
|
622
|
+
if not self.current_downloader:
|
|
623
|
+
return None
|
|
624
|
+
|
|
625
|
+
try:
|
|
626
|
+
progress = self.current_downloader.get_progress_data()
|
|
627
|
+
if progress:
|
|
628
|
+
progress['download_type'] = self.current_download_type
|
|
629
|
+
return progress
|
|
630
|
+
|
|
631
|
+
except Exception as e:
|
|
632
|
+
logging.error(f"Error getting progress data: {e}")
|
|
633
|
+
return None
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# 25.07.25
|
|
2
2
|
|
|
3
3
|
import re
|
|
4
|
+
import logging
|
|
4
5
|
from urllib.parse import urljoin
|
|
5
6
|
import xml.etree.ElementTree as ET
|
|
6
7
|
from typing import List, Dict, Optional, Tuple, Any
|
|
@@ -456,6 +457,7 @@ class MPDParser:
|
|
|
456
457
|
)
|
|
457
458
|
|
|
458
459
|
response.raise_for_status()
|
|
460
|
+
logging.info(f"Successfully fetched MPD: {response.content}")
|
|
459
461
|
self.root = ET.fromstring(response.content)
|
|
460
462
|
break
|
|
461
463
|
|
|
@@ -480,9 +482,6 @@ class MPDParser:
|
|
|
480
482
|
self.pssh = pssh_element.text
|
|
481
483
|
break
|
|
482
484
|
|
|
483
|
-
if not self.pssh:
|
|
484
|
-
console.print("[bold red]PSSH not found in MPD![/bold red]")
|
|
485
|
-
|
|
486
485
|
def _parse_representations(self) -> None:
|
|
487
486
|
"""Parse all representations from the MPD"""
|
|
488
487
|
base_url = self._get_initial_base_url()
|