StreamingCommunity 3.2.8__py3-none-any.whl → 3.3.0__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/Helper/Vixcloud/util.py +2 -1
- StreamingCommunity/Api/Player/hdplayer.py +2 -2
- StreamingCommunity/Api/Player/sweetpixel.py +5 -8
- StreamingCommunity/Api/Site/altadefinizione/__init__.py +32 -15
- StreamingCommunity/Api/Site/altadefinizione/film.py +10 -8
- StreamingCommunity/Api/Site/altadefinizione/series.py +9 -7
- StreamingCommunity/Api/Site/altadefinizione/site.py +1 -1
- StreamingCommunity/Api/Site/animeunity/__init__.py +31 -15
- StreamingCommunity/Api/Site/animeunity/serie.py +2 -2
- StreamingCommunity/Api/Site/animeworld/__init__.py +33 -7
- StreamingCommunity/Api/Site/animeworld/site.py +3 -5
- StreamingCommunity/Api/Site/animeworld/util/ScrapeSerie.py +8 -10
- StreamingCommunity/Api/Site/crunchyroll/__init__.py +44 -12
- StreamingCommunity/Api/Site/crunchyroll/film.py +9 -7
- StreamingCommunity/Api/Site/crunchyroll/series.py +9 -7
- StreamingCommunity/Api/Site/crunchyroll/site.py +16 -1
- StreamingCommunity/Api/Site/guardaserie/__init__.py +36 -10
- StreamingCommunity/Api/Site/guardaserie/series.py +8 -6
- StreamingCommunity/Api/Site/guardaserie/site.py +0 -3
- StreamingCommunity/Api/Site/guardaserie/util/ScrapeSerie.py +1 -2
- StreamingCommunity/Api/Site/mediasetinfinity/__init__.py +37 -12
- StreamingCommunity/Api/Site/mediasetinfinity/film.py +10 -16
- StreamingCommunity/Api/Site/mediasetinfinity/series.py +12 -18
- StreamingCommunity/Api/Site/mediasetinfinity/site.py +18 -3
- StreamingCommunity/Api/Site/mediasetinfinity/util/ScrapeSerie.py +214 -180
- StreamingCommunity/Api/Site/mediasetinfinity/util/get_license.py +2 -31
- StreamingCommunity/Api/Site/raiplay/__init__.py +47 -12
- StreamingCommunity/Api/Site/raiplay/film.py +42 -10
- StreamingCommunity/Api/Site/raiplay/series.py +53 -11
- StreamingCommunity/Api/Site/raiplay/site.py +4 -1
- StreamingCommunity/Api/Site/raiplay/util/ScrapeSerie.py +2 -1
- StreamingCommunity/Api/Site/raiplay/util/get_license.py +40 -0
- StreamingCommunity/Api/Site/streamingcommunity/__init__.py +5 -8
- StreamingCommunity/Api/Site/streamingcommunity/film.py +7 -5
- StreamingCommunity/Api/Site/streamingcommunity/series.py +9 -7
- StreamingCommunity/Api/Site/streamingcommunity/site.py +8 -3
- StreamingCommunity/Api/Site/streamingcommunity/util/ScrapeSerie.py +5 -2
- StreamingCommunity/Api/Site/streamingwatch/__init__.py +43 -9
- StreamingCommunity/Api/Site/streamingwatch/film.py +7 -5
- StreamingCommunity/Api/Site/streamingwatch/series.py +8 -6
- StreamingCommunity/Api/Site/streamingwatch/site.py +3 -1
- StreamingCommunity/Api/Site/streamingwatch/util/ScrapeSerie.py +3 -3
- StreamingCommunity/Api/Template/Util/__init__.py +10 -1
- StreamingCommunity/Api/Template/Util/manage_ep.py +4 -4
- StreamingCommunity/Api/Template/__init__.py +5 -1
- StreamingCommunity/Api/Template/site.py +10 -6
- StreamingCommunity/Lib/Downloader/DASH/cdm_helpher.py +13 -12
- StreamingCommunity/Lib/Downloader/DASH/decrypt.py +1 -1
- StreamingCommunity/Lib/Downloader/DASH/downloader.py +24 -22
- StreamingCommunity/Lib/Downloader/DASH/parser.py +1 -1
- StreamingCommunity/Lib/Downloader/DASH/segments.py +4 -3
- StreamingCommunity/Lib/Downloader/HLS/downloader.py +11 -9
- StreamingCommunity/Lib/Downloader/HLS/segments.py +4 -9
- StreamingCommunity/Lib/Downloader/MP4/downloader.py +25 -6
- StreamingCommunity/Lib/Downloader/TOR/downloader.py +3 -5
- StreamingCommunity/Lib/Downloader/__init__.py +9 -1
- StreamingCommunity/Lib/FFmpeg/__init__.py +10 -1
- StreamingCommunity/Lib/FFmpeg/command.py +4 -6
- StreamingCommunity/Lib/FFmpeg/util.py +1 -1
- StreamingCommunity/Lib/M3U8/__init__.py +9 -1
- StreamingCommunity/Lib/M3U8/decryptor.py +8 -4
- StreamingCommunity/Lib/M3U8/estimator.py +0 -6
- StreamingCommunity/Lib/M3U8/parser.py +1 -1
- StreamingCommunity/Lib/M3U8/url_fixer.py +1 -1
- StreamingCommunity/Lib/TMBD/__init__.py +6 -1
- StreamingCommunity/TelegramHelp/config.json +1 -5
- StreamingCommunity/TelegramHelp/telegram_bot.py +9 -10
- StreamingCommunity/Upload/update.py +2 -2
- StreamingCommunity/Upload/version.py +1 -1
- StreamingCommunity/Util/config_json.py +139 -59
- StreamingCommunity/Util/http_client.py +201 -0
- StreamingCommunity/Util/message.py +1 -1
- StreamingCommunity/Util/os.py +8 -5
- StreamingCommunity/Util/table.py +3 -3
- StreamingCommunity/__init__.py +9 -1
- StreamingCommunity/run.py +394 -258
- {streamingcommunity-3.2.8.dist-info → streamingcommunity-3.3.0.dist-info}/METADATA +147 -45
- streamingcommunity-3.3.0.dist-info/RECORD +110 -0
- StreamingCommunity/Api/Site/cb01new/__init__.py +0 -72
- StreamingCommunity/Api/Site/cb01new/film.py +0 -62
- StreamingCommunity/Api/Site/cb01new/site.py +0 -78
- streamingcommunity-3.2.8.dist-info/RECORD +0 -111
- {streamingcommunity-3.2.8.dist-info → streamingcommunity-3.3.0.dist-info}/WHEEL +0 -0
- {streamingcommunity-3.2.8.dist-info → streamingcommunity-3.3.0.dist-info}/entry_points.txt +0 -0
- {streamingcommunity-3.2.8.dist-info → streamingcommunity-3.3.0.dist-info}/licenses/LICENSE +0 -0
- {streamingcommunity-3.2.8.dist-info → streamingcommunity-3.3.0.dist-info}/top_level.txt +0 -0
|
@@ -2,7 +2,6 @@
|
|
|
2
2
|
|
|
3
3
|
import base64
|
|
4
4
|
import logging
|
|
5
|
-
import os
|
|
6
5
|
|
|
7
6
|
|
|
8
7
|
# External libraries
|
|
@@ -31,16 +30,11 @@ def get_widevine_keys(pssh, license_url, cdm_device_path, headers=None, payload=
|
|
|
31
30
|
list: List of dicts {'kid': ..., 'key': ...} (only CONTENT keys) or None if error.
|
|
32
31
|
"""
|
|
33
32
|
|
|
34
|
-
# Check if CDM file exists
|
|
35
|
-
if not os.path.isfile(cdm_device_path):
|
|
36
|
-
console.print(f"[bold red] CDM file not found: {cdm_device_path}[/bold red]")
|
|
37
|
-
return None
|
|
38
|
-
|
|
39
33
|
# Check if PSSH is a valid base64 string
|
|
40
34
|
try:
|
|
41
35
|
base64.b64decode(pssh)
|
|
42
36
|
except Exception:
|
|
43
|
-
console.print(
|
|
37
|
+
console.print("[bold red] Invalid PSSH base64 string.[/bold red]")
|
|
44
38
|
return None
|
|
45
39
|
|
|
46
40
|
try:
|
|
@@ -67,7 +61,15 @@ def get_widevine_keys(pssh, license_url, cdm_device_path, headers=None, payload=
|
|
|
67
61
|
response = httpx.post(license_url, data=challenge, headers=req_headers, content=payload)
|
|
68
62
|
|
|
69
63
|
if response.status_code != 200:
|
|
70
|
-
console.print(f"[bold red]License error:[/bold red] {response.status_code}")
|
|
64
|
+
console.print(f"[bold red]License error:[/bold red] {response.status_code}, {response.text}")
|
|
65
|
+
console.print({
|
|
66
|
+
"url": license_url,
|
|
67
|
+
"headers": req_headers,
|
|
68
|
+
"content": payload,
|
|
69
|
+
"session_id": session_id.hex(),
|
|
70
|
+
"pssh": pssh
|
|
71
|
+
})
|
|
72
|
+
|
|
71
73
|
return None
|
|
72
74
|
|
|
73
75
|
# Handle (JSON) or classic (binary) license response
|
|
@@ -77,18 +79,17 @@ def get_widevine_keys(pssh, license_url, cdm_device_path, headers=None, payload=
|
|
|
77
79
|
|
|
78
80
|
# Check if license_data is empty
|
|
79
81
|
if not license_data:
|
|
80
|
-
console.print(
|
|
82
|
+
console.print("[bold red]License response is empty.[/bold red]")
|
|
81
83
|
return None
|
|
82
84
|
|
|
83
85
|
if "application/json" in content_type:
|
|
84
86
|
try:
|
|
85
87
|
|
|
86
88
|
# Try to decode as JSON only if plausible
|
|
87
|
-
text = response.text
|
|
88
89
|
data = None
|
|
89
90
|
try:
|
|
90
91
|
data = response.json()
|
|
91
|
-
except Exception
|
|
92
|
+
except Exception:
|
|
92
93
|
data = None
|
|
93
94
|
|
|
94
95
|
if data and "license" in data:
|
|
@@ -118,7 +119,7 @@ def get_widevine_keys(pssh, license_url, cdm_device_path, headers=None, payload=
|
|
|
118
119
|
|
|
119
120
|
# Check if content_keys list is empty
|
|
120
121
|
if not content_keys:
|
|
121
|
-
console.print(
|
|
122
|
+
console.print("[bold yellow]⚠️ No CONTENT keys found in license.[/bold yellow]")
|
|
122
123
|
return None
|
|
123
124
|
|
|
124
125
|
return content_keys
|
|
@@ -38,7 +38,7 @@ def decrypt_with_mp4decrypt(encrypted_path, kid, key, output_path=None, cleanup=
|
|
|
38
38
|
bytes.fromhex(kid)
|
|
39
39
|
bytes.fromhex(key)
|
|
40
40
|
except Exception:
|
|
41
|
-
console.print(
|
|
41
|
+
console.print("[bold red] Invalid KID or KEY (not hex).[/bold red]")
|
|
42
42
|
return None
|
|
43
43
|
|
|
44
44
|
if not output_path:
|
|
@@ -22,7 +22,7 @@ from .cdm_helpher import get_widevine_keys
|
|
|
22
22
|
|
|
23
23
|
# Config
|
|
24
24
|
DOWNLOAD_SPECIFIC_AUDIO = config_manager.get_list('M3U8_DOWNLOAD', 'specific_list_audio')
|
|
25
|
-
FILTER_CUSTOM_REOLUTION = str(config_manager.get('
|
|
25
|
+
FILTER_CUSTOM_REOLUTION = str(config_manager.get('M3U8_CONVERSION', 'force_resolution')).strip().lower()
|
|
26
26
|
CLEANUP_TMP = config_manager.get_bool('M3U8_DOWNLOAD', 'cleanup_tmp_folder')
|
|
27
27
|
|
|
28
28
|
|
|
@@ -99,28 +99,30 @@ class DASH_Downloader:
|
|
|
99
99
|
if rep:
|
|
100
100
|
encrypted_path = os.path.join(self.encrypted_dir, f"{rep['id']}_encrypted.m4s")
|
|
101
101
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
102
|
+
# If m4s file doesn't exist start downloading
|
|
103
|
+
if not os.path.exists(encrypted_path):
|
|
104
|
+
downloader = MPD_Segments(
|
|
105
|
+
tmp_folder=self.encrypted_dir,
|
|
106
|
+
representation=rep,
|
|
107
|
+
pssh=self.parser.pssh
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
try:
|
|
111
|
+
result = downloader.download_streams()
|
|
112
|
+
|
|
113
|
+
# Check for interruption or failure
|
|
114
|
+
if result.get("stopped"):
|
|
115
|
+
self.stopped = True
|
|
116
|
+
self.error = "Download interrupted"
|
|
117
|
+
return False
|
|
118
|
+
|
|
119
|
+
if result.get("nFailed", 0) > 0:
|
|
120
|
+
self.error = f"Failed segments: {result['nFailed']}"
|
|
121
|
+
return False
|
|
122
|
+
|
|
123
|
+
except Exception as ex:
|
|
124
|
+
self.error = str(ex)
|
|
115
125
|
return False
|
|
116
|
-
|
|
117
|
-
if result.get("nFailed", 0) > 0:
|
|
118
|
-
self.error = f"Failed segments: {result['nFailed']}"
|
|
119
|
-
return False
|
|
120
|
-
|
|
121
|
-
except Exception as ex:
|
|
122
|
-
self.error = str(ex)
|
|
123
|
-
return False
|
|
124
126
|
|
|
125
127
|
if not self.parser.pssh:
|
|
126
128
|
print("No PSSH found: segments are not encrypted, skipping decryption.")
|
|
@@ -61,7 +61,7 @@ class MPDParser:
|
|
|
61
61
|
self.base_url = mpd_url.rsplit('/', 1)[0] + '/'
|
|
62
62
|
|
|
63
63
|
def parse(self, custom_headers):
|
|
64
|
-
response = httpx.get(self.mpd_url, headers=custom_headers, timeout=max_timeout)
|
|
64
|
+
response = httpx.get(self.mpd_url, headers=custom_headers, timeout=max_timeout, follow_redirects=True)
|
|
65
65
|
response.raise_for_status()
|
|
66
66
|
|
|
67
67
|
root = ET.fromstring(response.content)
|
|
@@ -134,7 +134,7 @@ class MPD_Segments:
|
|
|
134
134
|
|
|
135
135
|
try:
|
|
136
136
|
headers = {'User-Agent': get_userAgent()}
|
|
137
|
-
response = await client.get(init_url, headers=headers)
|
|
137
|
+
response = await client.get(init_url, headers=headers, follow_redirects=True)
|
|
138
138
|
|
|
139
139
|
with open(concat_path, 'wb') as outfile:
|
|
140
140
|
if response.status_code == 200:
|
|
@@ -160,7 +160,8 @@ class MPD_Segments:
|
|
|
160
160
|
headers = {'User-Agent': get_userAgent()}
|
|
161
161
|
for attempt in range(max_retry):
|
|
162
162
|
try:
|
|
163
|
-
resp = await client.get(url, headers=headers)
|
|
163
|
+
resp = await client.get(url, headers=headers, follow_redirects=True)
|
|
164
|
+
|
|
164
165
|
if resp.status_code == 200:
|
|
165
166
|
return idx, resp.content, attempt
|
|
166
167
|
else:
|
|
@@ -214,7 +215,7 @@ class MPD_Segments:
|
|
|
214
215
|
for attempt in range(max_retry):
|
|
215
216
|
try:
|
|
216
217
|
resp = await client.get(url, headers=headers)
|
|
217
|
-
|
|
218
|
+
|
|
218
219
|
if resp.status_code == 200:
|
|
219
220
|
return idx, resp.content, attempt
|
|
220
221
|
else:
|
|
@@ -17,6 +17,7 @@ from rich.panel import Panel
|
|
|
17
17
|
# Internal utilities
|
|
18
18
|
from StreamingCommunity.Util.config_json import config_manager
|
|
19
19
|
from StreamingCommunity.Util.headers import get_userAgent
|
|
20
|
+
from StreamingCommunity.Util.http_client import create_client
|
|
20
21
|
from StreamingCommunity.Util.os import os_manager, internet_manager
|
|
21
22
|
from StreamingCommunity.TelegramHelp.telegram_bot import get_bot_instance
|
|
22
23
|
|
|
@@ -38,7 +39,7 @@ DOWNLOAD_SPECIFIC_AUDIO = config_manager.get_list('M3U8_DOWNLOAD', 'specific_lis
|
|
|
38
39
|
DOWNLOAD_SPECIFIC_SUBTITLE = config_manager.get_list('M3U8_DOWNLOAD', 'specific_list_subtitles')
|
|
39
40
|
MERGE_SUBTITLE = config_manager.get_bool('M3U8_DOWNLOAD', 'merge_subs')
|
|
40
41
|
CLEANUP_TMP = config_manager.get_bool('M3U8_DOWNLOAD', 'cleanup_tmp_folder')
|
|
41
|
-
|
|
42
|
+
FILTER_CUSTOM_RESOLUTION = str(config_manager.get('M3U8_CONVERSION', 'force_resolution')).strip().lower()
|
|
42
43
|
RETRY_LIMIT = config_manager.get_int('REQUESTS', 'max_retry')
|
|
43
44
|
MAX_TIMEOUT = config_manager.get_int("REQUESTS", "timeout")
|
|
44
45
|
TELEGRAM_BOT = config_manager.get_bool('DEFAULT', 'telegram_bot')
|
|
@@ -62,7 +63,8 @@ class HLSClient:
|
|
|
62
63
|
Returns:
|
|
63
64
|
Response content/text or None if all retries fail
|
|
64
65
|
"""
|
|
65
|
-
client
|
|
66
|
+
# Use unified HTTP client (inherits timeout/verify/proxy from config)
|
|
67
|
+
client = create_client(headers=self.headers)
|
|
66
68
|
|
|
67
69
|
for attempt in range(RETRY_LIMIT):
|
|
68
70
|
try:
|
|
@@ -154,12 +156,12 @@ class M3U8Manager:
|
|
|
154
156
|
self.sub_streams = []
|
|
155
157
|
|
|
156
158
|
else:
|
|
157
|
-
if str(
|
|
159
|
+
if str(FILTER_CUSTOM_RESOLUTION) == "best":
|
|
158
160
|
self.video_url, self.video_res = self.parser._video.get_best_uri()
|
|
159
|
-
elif str(
|
|
161
|
+
elif str(FILTER_CUSTOM_RESOLUTION) == "worst":
|
|
160
162
|
self.video_url, self.video_res = self.parser._video.get_worst_uri()
|
|
161
|
-
elif str(
|
|
162
|
-
resolution_value = int(str(
|
|
163
|
+
elif str(FILTER_CUSTOM_RESOLUTION).replace("p", "").replace("px", "").isdigit():
|
|
164
|
+
resolution_value = int(str(FILTER_CUSTOM_RESOLUTION).replace("p", "").replace("px", ""))
|
|
163
165
|
self.video_url, self.video_res = self.parser._video.get_custom_uri(resolution_value)
|
|
164
166
|
else:
|
|
165
167
|
logging.error("Resolution not recognized.")
|
|
@@ -188,7 +190,7 @@ class M3U8Manager:
|
|
|
188
190
|
|
|
189
191
|
console.print(
|
|
190
192
|
f"[cyan bold]Video [/cyan bold] [green]Available:[/green] [purple]{', '.join(list_available_resolution)}[/purple] | "
|
|
191
|
-
f"[red]Set:[/red] [purple]{
|
|
193
|
+
f"[red]Set:[/red] [purple]{FILTER_CUSTOM_RESOLUTION}[/purple] | "
|
|
192
194
|
f"[yellow]Downloadable:[/yellow] [purple]{self.video_res[0]}x{self.video_res[1]}[/purple]"
|
|
193
195
|
)
|
|
194
196
|
|
|
@@ -418,7 +420,7 @@ class HLS_Downloader:
|
|
|
418
420
|
- is_master: Whether the M3U8 was a master playlist
|
|
419
421
|
Or raises an exception if there's an error
|
|
420
422
|
"""
|
|
421
|
-
console.print(
|
|
423
|
+
console.print("[cyan]You can safely stop the download with [bold]Ctrl+c[bold] [cyan] \n")
|
|
422
424
|
|
|
423
425
|
if TELEGRAM_BOT:
|
|
424
426
|
bot = get_bot_instance()
|
|
@@ -435,7 +437,7 @@ class HLS_Downloader:
|
|
|
435
437
|
'stopped': False
|
|
436
438
|
}
|
|
437
439
|
if TELEGRAM_BOT:
|
|
438
|
-
bot.send_message(
|
|
440
|
+
bot.send_message("Contenuto già scaricato!", None)
|
|
439
441
|
return response
|
|
440
442
|
|
|
441
443
|
self.path_manager.setup_directories()
|
|
@@ -23,6 +23,7 @@ from rich.console import Console
|
|
|
23
23
|
# Internal utilities
|
|
24
24
|
from StreamingCommunity.Util.color import Colors
|
|
25
25
|
from StreamingCommunity.Util.headers import get_userAgent
|
|
26
|
+
from StreamingCommunity.Util.http_client import create_client
|
|
26
27
|
from StreamingCommunity.Util.config_json import config_manager
|
|
27
28
|
|
|
28
29
|
|
|
@@ -198,14 +199,7 @@ class M3U8_Segments:
|
|
|
198
199
|
print("Signal handler must be set in the main thread")
|
|
199
200
|
|
|
200
201
|
def _get_http_client(self):
|
|
201
|
-
|
|
202
|
-
'headers': {'User-Agent': get_userAgent()},
|
|
203
|
-
'timeout': SEGMENT_MAX_TIMEOUT,
|
|
204
|
-
'follow_redirects': True,
|
|
205
|
-
'http2': False,
|
|
206
|
-
'verify': REQUEST_VERIFY
|
|
207
|
-
}
|
|
208
|
-
return httpx.Client(**client_params)
|
|
202
|
+
return create_client(headers={'User-Agent': get_userAgent()}, follow_redirects=True)
|
|
209
203
|
|
|
210
204
|
def download_segment(self, ts_url: str, index: int, progress_bar: tqdm, backoff_factor: float = 1.1) -> None:
|
|
211
205
|
"""
|
|
@@ -264,7 +258,8 @@ class M3U8_Segments:
|
|
|
264
258
|
with self.active_retries_lock:
|
|
265
259
|
self.active_retries += 1
|
|
266
260
|
|
|
267
|
-
sleep_time = backoff_factor * (2 ** attempt)
|
|
261
|
+
#sleep_time = backoff_factor * (2 ** attempt)
|
|
262
|
+
sleep_time = backoff_factor * (attempt + 1)
|
|
268
263
|
logging.info(f"Retrying segment {index} in {sleep_time} seconds...")
|
|
269
264
|
time.sleep(sleep_time)
|
|
270
265
|
|
|
@@ -7,10 +7,10 @@ import time
|
|
|
7
7
|
import signal
|
|
8
8
|
import logging
|
|
9
9
|
from functools import partial
|
|
10
|
+
import threading
|
|
10
11
|
|
|
11
12
|
|
|
12
13
|
# External libraries
|
|
13
|
-
import httpx
|
|
14
14
|
from tqdm import tqdm
|
|
15
15
|
from rich.console import Console
|
|
16
16
|
from rich.prompt import Prompt
|
|
@@ -19,6 +19,7 @@ from rich.panel import Panel
|
|
|
19
19
|
|
|
20
20
|
# Internal utilities
|
|
21
21
|
from StreamingCommunity.Util.headers import get_userAgent
|
|
22
|
+
from StreamingCommunity.Util.http_client import create_client
|
|
22
23
|
from StreamingCommunity.Util.color import Colors
|
|
23
24
|
from StreamingCommunity.Util.config_json import config_manager
|
|
24
25
|
from StreamingCommunity.Util.os import internet_manager, os_manager
|
|
@@ -83,7 +84,7 @@ def MP4_downloader(url: str, path: str, referer: str = None, headers_: dict = No
|
|
|
83
84
|
if os.path.exists(path):
|
|
84
85
|
console.log("[red]Output file already exists.")
|
|
85
86
|
if TELEGRAM_BOT:
|
|
86
|
-
bot.send_message(
|
|
87
|
+
bot.send_message("Contenuto già scaricato!", None)
|
|
87
88
|
return None, False
|
|
88
89
|
|
|
89
90
|
if not (url.lower().startswith('http://') or url.lower().startswith('https://')):
|
|
@@ -101,16 +102,30 @@ def MP4_downloader(url: str, path: str, referer: str = None, headers_: dict = No
|
|
|
101
102
|
else:
|
|
102
103
|
headers['User-Agent'] = get_userAgent()
|
|
103
104
|
|
|
104
|
-
# Set interrupt handler
|
|
105
|
+
# Set interrupt handler (only in main thread). In background threads (e.g., Django), skip custom signal handling.
|
|
105
106
|
temp_path = f"{path}.temp"
|
|
106
107
|
interrupt_handler = InterruptHandler()
|
|
107
|
-
original_handler =
|
|
108
|
+
original_handler = None
|
|
109
|
+
try:
|
|
110
|
+
if threading.current_thread() is threading.main_thread():
|
|
111
|
+
original_handler = signal.signal(
|
|
112
|
+
signal.SIGINT,
|
|
113
|
+
partial(
|
|
114
|
+
signal_handler,
|
|
115
|
+
interrupt_handler=interrupt_handler,
|
|
116
|
+
original_handler=signal.getsignal(signal.SIGINT),
|
|
117
|
+
),
|
|
118
|
+
)
|
|
119
|
+
except Exception:
|
|
120
|
+
# If setting signal handler fails (non-main thread), continue without it
|
|
121
|
+
original_handler = None
|
|
108
122
|
|
|
109
123
|
# Ensure the output directory exists
|
|
110
124
|
os.makedirs(os.path.dirname(path), exist_ok=True)
|
|
111
125
|
|
|
112
126
|
try:
|
|
113
|
-
|
|
127
|
+
# Use unified HTTP client (verify/timeout/proxy from config)
|
|
128
|
+
with create_client() as client:
|
|
114
129
|
with client.stream("GET", url, headers=headers) as response:
|
|
115
130
|
response.raise_for_status()
|
|
116
131
|
total = int(response.headers.get('content-length', 0))
|
|
@@ -183,4 +198,8 @@ def MP4_downloader(url: str, path: str, referer: str = None, headers_: dict = No
|
|
|
183
198
|
return None, interrupt_handler.kill_download
|
|
184
199
|
|
|
185
200
|
finally:
|
|
186
|
-
|
|
201
|
+
if original_handler is not None:
|
|
202
|
+
try:
|
|
203
|
+
signal.signal(signal.SIGINT, original_handler)
|
|
204
|
+
except Exception:
|
|
205
|
+
pass
|
|
@@ -229,7 +229,7 @@ class TOR_downloader:
|
|
|
229
229
|
torrent_info.num_seeds == 0 and
|
|
230
230
|
torrent_info.state in ('stalledDL', 'missingFiles', 'error')):
|
|
231
231
|
|
|
232
|
-
self.console.print(
|
|
232
|
+
self.console.print("[bold red]Torrent not downloadable. No seeds or peers available. Removing...[/bold red]")
|
|
233
233
|
self._remove_torrent(self.latest_torrent_hash)
|
|
234
234
|
self.latest_torrent_hash = None
|
|
235
235
|
return False
|
|
@@ -250,7 +250,7 @@ class TOR_downloader:
|
|
|
250
250
|
"""
|
|
251
251
|
try:
|
|
252
252
|
self.qb.torrents_delete(delete_files=delete_files, torrent_hashes=torrent_hash)
|
|
253
|
-
self.console.print(
|
|
253
|
+
self.console.print("[yellow]Torrent removed from client[/yellow]")
|
|
254
254
|
except Exception as e:
|
|
255
255
|
logging.error(f"Error removing torrent: {str(e)}")
|
|
256
256
|
|
|
@@ -356,10 +356,8 @@ class TOR_downloader:
|
|
|
356
356
|
|
|
357
357
|
# Get download statistics
|
|
358
358
|
download_speed = torrent_info.dlspeed
|
|
359
|
-
upload_speed = torrent_info.upspeed
|
|
360
359
|
total_size = torrent_info.size
|
|
361
360
|
downloaded_size = torrent_info.downloaded
|
|
362
|
-
eta = torrent_info.eta # eta in seconds
|
|
363
361
|
|
|
364
362
|
# Format sizes and speeds using the existing functions without modification
|
|
365
363
|
downloaded_size_str = internet_manager.format_file_size(downloaded_size)
|
|
@@ -465,5 +463,5 @@ class TOR_downloader:
|
|
|
465
463
|
|
|
466
464
|
try:
|
|
467
465
|
self.qb.auth_log_out()
|
|
468
|
-
except:
|
|
466
|
+
except Exception:
|
|
469
467
|
pass
|
|
@@ -2,4 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
from .HLS.downloader import HLS_Downloader
|
|
4
4
|
from .MP4.downloader import MP4_downloader
|
|
5
|
-
from .TOR.downloader import TOR_downloader
|
|
5
|
+
from .TOR.downloader import TOR_downloader
|
|
6
|
+
from .DASH.downloader import DASH_Downloader
|
|
7
|
+
|
|
8
|
+
__all__ = [
|
|
9
|
+
"HLS_Downloader",
|
|
10
|
+
"MP4_downloader",
|
|
11
|
+
"TOR_downloader",
|
|
12
|
+
"DASH_Downloader"
|
|
13
|
+
]
|
|
@@ -1,4 +1,13 @@
|
|
|
1
1
|
# 18.04.24
|
|
2
2
|
|
|
3
3
|
from .command import join_video, join_audios, join_subtitle
|
|
4
|
-
from .util import print_duration_table, get_video_duration
|
|
4
|
+
from .util import print_duration_table, get_video_duration
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
__all__ = [
|
|
8
|
+
"join_video",
|
|
9
|
+
"join_audios",
|
|
10
|
+
"join_subtitle",
|
|
11
|
+
"print_duration_table",
|
|
12
|
+
"get_video_duration",
|
|
13
|
+
]
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
# 31.01.24
|
|
2
2
|
|
|
3
|
-
import sys
|
|
4
3
|
import logging
|
|
5
4
|
import subprocess
|
|
6
5
|
from typing import List, Dict, Tuple, Optional
|
|
@@ -110,13 +109,12 @@ def join_video(video_path: str, out_path: str, codec: M3U8_Codec = None):
|
|
|
110
109
|
if need_to_force_to_ts(video_path):
|
|
111
110
|
#console.log("[red]Force input file to 'mpegts'.")
|
|
112
111
|
ffmpeg_cmd.extend(['-f', 'mpegts'])
|
|
113
|
-
vcodec = "libx264"
|
|
114
112
|
|
|
115
113
|
# Insert input video path
|
|
116
114
|
ffmpeg_cmd.extend(['-i', video_path])
|
|
117
115
|
|
|
118
116
|
# Add output Parameters
|
|
119
|
-
if USE_CODEC and codec
|
|
117
|
+
if USE_CODEC and codec is not None:
|
|
120
118
|
if USE_VCODEC:
|
|
121
119
|
if codec.video_codec_name:
|
|
122
120
|
if not USE_GPU:
|
|
@@ -162,7 +160,7 @@ def join_video(video_path: str, out_path: str, codec: M3U8_Codec = None):
|
|
|
162
160
|
print()
|
|
163
161
|
|
|
164
162
|
else:
|
|
165
|
-
console.log(
|
|
163
|
+
console.log("[purple]FFmpeg [white][[cyan]Join video[white]] ...")
|
|
166
164
|
with suppress_output():
|
|
167
165
|
capture_ffmpeg_real_time(ffmpeg_cmd, "[cyan]Join video")
|
|
168
166
|
print()
|
|
@@ -258,7 +256,7 @@ def join_audios(video_path: str, audio_tracks: List[Dict[str, str]], out_path: s
|
|
|
258
256
|
print()
|
|
259
257
|
|
|
260
258
|
else:
|
|
261
|
-
console.log(
|
|
259
|
+
console.log("[purple]FFmpeg [white][[cyan]Join audio[white]] ...")
|
|
262
260
|
with suppress_output():
|
|
263
261
|
capture_ffmpeg_real_time(ffmpeg_cmd, "[cyan]Join audio")
|
|
264
262
|
print()
|
|
@@ -313,7 +311,7 @@ def join_subtitle(video_path: str, subtitles_list: List[Dict[str, str]], out_pat
|
|
|
313
311
|
print()
|
|
314
312
|
|
|
315
313
|
else:
|
|
316
|
-
console.log(
|
|
314
|
+
console.log("[purple]FFmpeg [white][[cyan]Join subtitle[white]] ...")
|
|
317
315
|
with suppress_output():
|
|
318
316
|
capture_ffmpeg_real_time(ffmpeg_cmd, "[cyan]Join subtitle")
|
|
319
317
|
print()
|
|
@@ -3,4 +3,12 @@
|
|
|
3
3
|
from .decryptor import M3U8_Decryption
|
|
4
4
|
from .estimator import M3U8_Ts_Estimator
|
|
5
5
|
from .parser import M3U8_Parser, M3U8_Codec
|
|
6
|
-
from .url_fixer import M3U8_UrlFix
|
|
6
|
+
from .url_fixer import M3U8_UrlFix
|
|
7
|
+
|
|
8
|
+
__all__ = [
|
|
9
|
+
"M3U8_Decryption",
|
|
10
|
+
"M3U8_Ts_Estimator",
|
|
11
|
+
"M3U8_Parser",
|
|
12
|
+
"M3U8_Codec",
|
|
13
|
+
"M3U8_UrlFix"
|
|
14
|
+
]
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
# 03.04.24
|
|
2
2
|
|
|
3
3
|
import sys
|
|
4
|
-
import time
|
|
5
4
|
import logging
|
|
6
5
|
import importlib.util
|
|
7
6
|
|
|
@@ -10,18 +9,23 @@ import importlib.util
|
|
|
10
9
|
from rich.console import Console
|
|
11
10
|
|
|
12
11
|
|
|
12
|
+
# Cryptodome imports
|
|
13
|
+
from Cryptodome.Cipher import AES
|
|
14
|
+
from Cryptodome.Util.Padding import unpad
|
|
15
|
+
|
|
16
|
+
|
|
13
17
|
# Check if Cryptodome module is installed
|
|
14
18
|
console = Console()
|
|
15
19
|
crypto_spec = importlib.util.find_spec("Cryptodome")
|
|
16
20
|
crypto_installed = crypto_spec is not None
|
|
17
21
|
|
|
22
|
+
|
|
18
23
|
if not crypto_installed:
|
|
19
24
|
console.log("[red]pycryptodomex non è installato. Per favore installalo. Leggi readme.md [Requirement].")
|
|
20
25
|
sys.exit(0)
|
|
21
26
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
from Cryptodome.Util.Padding import unpad
|
|
27
|
+
|
|
28
|
+
logging.info("[cyan]Decryption use: Cryptodomex")
|
|
25
29
|
|
|
26
30
|
|
|
27
31
|
|
|
@@ -121,12 +121,6 @@ class M3U8_Ts_Estimator:
|
|
|
121
121
|
number_file_total_size = file_total_size.split(' ')[0]
|
|
122
122
|
units_file_total_size = file_total_size.split(' ')[1]
|
|
123
123
|
|
|
124
|
-
# Reduce lock contention by acquiring data with minimal synchronization
|
|
125
|
-
retry_count = 0
|
|
126
|
-
if self.segments_instance:
|
|
127
|
-
with self.segments_instance.active_retries_lock:
|
|
128
|
-
retry_count = self.segments_instance.active_retries
|
|
129
|
-
|
|
130
124
|
# Get speed data outside of any locks
|
|
131
125
|
speed_data = ["N/A", ""]
|
|
132
126
|
with self.lock:
|
|
@@ -485,7 +485,7 @@ class M3U8_Parser:
|
|
|
485
485
|
try:
|
|
486
486
|
for playlist in m3u8_obj.playlists:
|
|
487
487
|
|
|
488
|
-
there_is_codec =
|
|
488
|
+
there_is_codec = playlist.stream_info.codecs is not None
|
|
489
489
|
logging.info(f"There is coded: {there_is_codec}")
|
|
490
490
|
|
|
491
491
|
if there_is_codec:
|
|
@@ -2,10 +2,8 @@
|
|
|
2
2
|
"DEFAULT": {
|
|
3
3
|
"debug": false,
|
|
4
4
|
"show_message": true,
|
|
5
|
-
"clean_console": true,
|
|
6
5
|
"show_trending": true,
|
|
7
6
|
"use_api": true,
|
|
8
|
-
"not_close": false,
|
|
9
7
|
"telegram_bot": true,
|
|
10
8
|
"download_site_data": true,
|
|
11
9
|
"validate_github_config": true
|
|
@@ -45,9 +43,7 @@
|
|
|
45
43
|
"use_acodec": true,
|
|
46
44
|
"use_bitrate": true,
|
|
47
45
|
"use_gpu": false,
|
|
48
|
-
"default_preset": "ultrafast"
|
|
49
|
-
},
|
|
50
|
-
"M3U8_PARSER": {
|
|
46
|
+
"default_preset": "ultrafast",
|
|
51
47
|
"force_resolution": "Best"
|
|
52
48
|
},
|
|
53
49
|
"REQUESTS": {
|