StreamingCommunity 3.3.9__py3-none-any.whl → 3.4.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/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 -15
- StreamingCommunity/Api/Site/altadefinizione/site.py +2 -7
- 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 +2 -3
- StreamingCommunity/Api/Site/crunchyroll/site.py +2 -9
- StreamingCommunity/Api/Site/crunchyroll/util/ScrapeSerie.py +5 -27
- StreamingCommunity/Api/Site/crunchyroll/util/get_license.py +11 -26
- 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 +1 -2
- StreamingCommunity/Api/Site/mediasetinfinity/site.py +3 -11
- 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 -7
- StreamingCommunity/Api/Site/raiplay/series.py +0 -2
- StreamingCommunity/Api/Site/raiplay/site.py +3 -11
- StreamingCommunity/Api/Site/raiplay/util/ScrapeSerie.py +4 -11
- StreamingCommunity/Api/Site/raiplay/util/get_license.py +3 -12
- StreamingCommunity/Api/Site/streamingcommunity/film.py +5 -16
- StreamingCommunity/Api/Site/streamingcommunity/site.py +3 -22
- StreamingCommunity/Api/Site/streamingcommunity/util/ScrapeSerie.py +11 -26
- StreamingCommunity/Api/Site/streamingwatch/__init__.py +1 -0
- StreamingCommunity/Api/Site/streamingwatch/film.py +4 -2
- StreamingCommunity/Api/Site/streamingwatch/series.py +1 -1
- StreamingCommunity/Api/Site/streamingwatch/site.py +4 -18
- StreamingCommunity/Api/Site/streamingwatch/util/ScrapeSerie.py +0 -3
- StreamingCommunity/Api/Template/config_loader.py +0 -7
- StreamingCommunity/Lib/Downloader/DASH/decrypt.py +54 -1
- StreamingCommunity/Lib/Downloader/DASH/downloader.py +131 -54
- StreamingCommunity/Lib/Downloader/DASH/parser.py +2 -3
- StreamingCommunity/Lib/Downloader/DASH/segments.py +66 -54
- StreamingCommunity/Lib/Downloader/HLS/downloader.py +31 -50
- StreamingCommunity/Lib/Downloader/HLS/segments.py +23 -28
- StreamingCommunity/Lib/FFmpeg/capture.py +37 -5
- StreamingCommunity/Lib/FFmpeg/command.py +32 -90
- StreamingCommunity/Lib/TMBD/tmdb.py +2 -4
- StreamingCommunity/TelegramHelp/config.json +0 -1
- 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-3.3.9.dist-info → streamingcommunity-3.4.0.dist-info}/METADATA +1 -3
- {streamingcommunity-3.3.9.dist-info → streamingcommunity-3.4.0.dist-info}/RECORD +60 -60
- {streamingcommunity-3.3.9.dist-info → streamingcommunity-3.4.0.dist-info}/WHEEL +0 -0
- {streamingcommunity-3.3.9.dist-info → streamingcommunity-3.4.0.dist-info}/entry_points.txt +0 -0
- {streamingcommunity-3.3.9.dist-info → streamingcommunity-3.4.0.dist-info}/licenses/LICENSE +0 -0
- {streamingcommunity-3.3.9.dist-info → streamingcommunity-3.4.0.dist-info}/top_level.txt +0 -0
|
@@ -1,14 +1,12 @@
|
|
|
1
1
|
# 17.10.24
|
|
2
2
|
|
|
3
3
|
import os
|
|
4
|
-
import time
|
|
5
4
|
import logging
|
|
6
5
|
import shutil
|
|
7
|
-
from typing import Any, Dict, List, Optional
|
|
6
|
+
from typing import Any, Dict, List, Optional, Union
|
|
8
7
|
|
|
9
8
|
|
|
10
9
|
# External libraries
|
|
11
|
-
import httpx
|
|
12
10
|
from rich.console import Console
|
|
13
11
|
from rich.panel import Panel
|
|
14
12
|
from rich.table import Table
|
|
@@ -17,7 +15,7 @@ from rich.table import Table
|
|
|
17
15
|
# Internal utilities
|
|
18
16
|
from StreamingCommunity.Util.config_json import config_manager
|
|
19
17
|
from StreamingCommunity.Util.headers import get_userAgent
|
|
20
|
-
from StreamingCommunity.Util.http_client import
|
|
18
|
+
from StreamingCommunity.Util.http_client import fetch
|
|
21
19
|
from StreamingCommunity.Util.os import os_manager, internet_manager
|
|
22
20
|
|
|
23
21
|
|
|
@@ -33,15 +31,13 @@ from .segments import M3U8_Segments
|
|
|
33
31
|
|
|
34
32
|
|
|
35
33
|
# Config
|
|
36
|
-
ENABLE_SUBTITLE = config_manager.get_bool('M3U8_DOWNLOAD', 'download_subtitle')
|
|
37
34
|
DOWNLOAD_SPECIFIC_AUDIO = config_manager.get_list('M3U8_DOWNLOAD', 'specific_list_audio')
|
|
38
35
|
DOWNLOAD_SPECIFIC_SUBTITLE = config_manager.get_list('M3U8_DOWNLOAD', 'specific_list_subtitles')
|
|
39
36
|
MERGE_SUBTITLE = config_manager.get_bool('M3U8_DOWNLOAD', 'merge_subs')
|
|
40
37
|
CLEANUP_TMP = config_manager.get_bool('M3U8_DOWNLOAD', 'cleanup_tmp_folder')
|
|
41
38
|
GET_ONLY_LINK = config_manager.get_int('M3U8_DOWNLOAD', 'get_only_link')
|
|
42
39
|
FILTER_CUSTOM_RESOLUTION = str(config_manager.get('M3U8_CONVERSION', 'force_resolution')).strip().lower()
|
|
43
|
-
|
|
44
|
-
MAX_TIMEOUT = config_manager.get_int("REQUESTS", "timeout")
|
|
40
|
+
EXTENSION_OUTPUT = config_manager.get("M3U8_CONVERSION", "extension")
|
|
45
41
|
|
|
46
42
|
console = Console()
|
|
47
43
|
|
|
@@ -51,9 +47,9 @@ class HLSClient:
|
|
|
51
47
|
def __init__(self, custom_headers: Optional[Dict[str, str]] = None):
|
|
52
48
|
self.headers = custom_headers if custom_headers else {'User-Agent': get_userAgent()}
|
|
53
49
|
|
|
54
|
-
def request(self, url: str, return_content: bool = False) -> Optional[
|
|
50
|
+
def request(self, url: str, return_content: bool = False) -> Optional[Union[str, bytes]]:
|
|
55
51
|
"""
|
|
56
|
-
Makes HTTP GET requests with retry logic.
|
|
52
|
+
Makes HTTP GET requests with retry logic using http_client.
|
|
57
53
|
|
|
58
54
|
Args:
|
|
59
55
|
url: Target URL to request
|
|
@@ -67,21 +63,12 @@ class HLSClient:
|
|
|
67
63
|
logging.error("URL is None or empty, cannot make request")
|
|
68
64
|
return None
|
|
69
65
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
return response.content if return_content else response.text
|
|
77
|
-
|
|
78
|
-
except Exception as e:
|
|
79
|
-
logging.error(f"Attempt {attempt+1} failed for URL {url}: {str(e)}")
|
|
80
|
-
if attempt < RETRY_LIMIT - 1: # Don't sleep on last attempt
|
|
81
|
-
time.sleep(1.5 ** attempt)
|
|
82
|
-
|
|
83
|
-
logging.error(f"All {RETRY_LIMIT} attempts failed for URL: {url}")
|
|
84
|
-
return None
|
|
66
|
+
return fetch(
|
|
67
|
+
url,
|
|
68
|
+
method="GET",
|
|
69
|
+
headers=self.headers,
|
|
70
|
+
return_content=return_content
|
|
71
|
+
)
|
|
85
72
|
|
|
86
73
|
|
|
87
74
|
class PathManager:
|
|
@@ -94,7 +81,7 @@ class PathManager:
|
|
|
94
81
|
"""
|
|
95
82
|
self.m3u8_url = m3u8_url
|
|
96
83
|
self.output_path = self._sanitize_output_path(output_path)
|
|
97
|
-
base_name = os.path.basename(self.output_path).replace(
|
|
84
|
+
base_name = os.path.basename(self.output_path).replace(EXTENSION_OUTPUT, "")
|
|
98
85
|
self.temp_dir = os.path.join(os.path.dirname(self.output_path), f"{base_name}_tmp")
|
|
99
86
|
|
|
100
87
|
def _sanitize_output_path(self, path: Optional[str]) -> str:
|
|
@@ -103,10 +90,10 @@ class PathManager:
|
|
|
103
90
|
Creates a hash-based filename if no path is provided.
|
|
104
91
|
"""
|
|
105
92
|
if not path:
|
|
106
|
-
path = "download
|
|
93
|
+
path = f"download{EXTENSION_OUTPUT}"
|
|
107
94
|
|
|
108
|
-
if not path.endswith(
|
|
109
|
-
path +=
|
|
95
|
+
if not path.endswith(EXTENSION_OUTPUT):
|
|
96
|
+
path += EXTENSION_OUTPUT
|
|
110
97
|
|
|
111
98
|
return os_manager.get_sanitize_path(path)
|
|
112
99
|
|
|
@@ -182,6 +169,11 @@ class M3U8Manager:
|
|
|
182
169
|
elif str(FILTER_CUSTOM_RESOLUTION).replace("p", "").replace("px", "").isdigit():
|
|
183
170
|
resolution_value = int(str(FILTER_CUSTOM_RESOLUTION).replace("p", "").replace("px", ""))
|
|
184
171
|
self.video_url, self.video_res = self.parser._video.get_custom_uri(resolution_value)
|
|
172
|
+
|
|
173
|
+
# Fallback to best if custom resolution not found
|
|
174
|
+
if self.video_url is None:
|
|
175
|
+
self.video_url, self.video_res = self.parser._video.get_best_uri()
|
|
176
|
+
|
|
185
177
|
else:
|
|
186
178
|
logging.error("Resolution not recognized.")
|
|
187
179
|
self.video_url, self.video_res = self.parser._video.get_best_uri()
|
|
@@ -192,15 +184,15 @@ class M3U8Manager:
|
|
|
192
184
|
if s.get('language') in DOWNLOAD_SPECIFIC_AUDIO
|
|
193
185
|
]
|
|
194
186
|
|
|
187
|
+
# Subtitle info
|
|
195
188
|
self.sub_streams = []
|
|
196
|
-
if
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
self.
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
]
|
|
189
|
+
if "*" in DOWNLOAD_SPECIFIC_SUBTITLE:
|
|
190
|
+
self.sub_streams = self.parser._subtitle.get_all_uris_and_names() or []
|
|
191
|
+
else:
|
|
192
|
+
self.sub_streams = [
|
|
193
|
+
s for s in (self.parser._subtitle.get_all_uris_and_names() or [])
|
|
194
|
+
if s.get('language') in DOWNLOAD_SPECIFIC_SUBTITLE
|
|
195
|
+
]
|
|
204
196
|
|
|
205
197
|
def log_selection(self):
|
|
206
198
|
"""Log the stream selection information in a formatted table."""
|
|
@@ -220,17 +212,6 @@ class M3U8Manager:
|
|
|
220
212
|
|
|
221
213
|
data_rows.append(["Video", available_video, str(FILTER_CUSTOM_RESOLUTION), downloadable_video])
|
|
222
214
|
|
|
223
|
-
# Codec information
|
|
224
|
-
if self.parser.codec is not None:
|
|
225
|
-
available_codec_info = (
|
|
226
|
-
f"v: {self.parser.codec.video_codec_name} "
|
|
227
|
-
f"(b: {self.parser.codec.video_bitrate // 1000}k), "
|
|
228
|
-
f"a: {self.parser.codec.audio_codec_name} "
|
|
229
|
-
f"(b: {self.parser.codec.audio_bitrate // 1000}k)"
|
|
230
|
-
)
|
|
231
|
-
set_codec_info = available_codec_info if config_manager.get_bool("M3U8_CONVERSION", "use_codec") else "copy"
|
|
232
|
-
|
|
233
|
-
data_rows.append(["Codec", available_codec_info, set_codec_info, set_codec_info])
|
|
234
215
|
|
|
235
216
|
# Subtitle information
|
|
236
217
|
available_subtitles = self.parser._subtitle.get_all_uris_and_names() or []
|
|
@@ -683,11 +664,11 @@ class HLS_Downloader:
|
|
|
683
664
|
|
|
684
665
|
new_filename = self.path_manager.output_path
|
|
685
666
|
if missing_ts and use_shortest:
|
|
686
|
-
new_filename = new_filename.replace(
|
|
667
|
+
new_filename = new_filename.replace(EXTENSION_OUTPUT, f"_failed_sync_ts{EXTENSION_OUTPUT}")
|
|
687
668
|
elif missing_ts:
|
|
688
|
-
new_filename = new_filename.replace(
|
|
669
|
+
new_filename = new_filename.replace(EXTENSION_OUTPUT, f"_failed_ts{EXTENSION_OUTPUT}")
|
|
689
670
|
elif use_shortest:
|
|
690
|
-
new_filename = new_filename.replace(
|
|
671
|
+
new_filename = new_filename.replace(EXTENSION_OUTPUT, f"_failed_sync{EXTENSION_OUTPUT}")
|
|
691
672
|
|
|
692
673
|
if missing_ts or use_shortest:
|
|
693
674
|
os.rename(self.path_manager.output_path, new_filename)
|
|
@@ -18,6 +18,7 @@ from rich.console import Console
|
|
|
18
18
|
# Internal utilities
|
|
19
19
|
from StreamingCommunity.Util.color import Colors
|
|
20
20
|
from StreamingCommunity.Util.headers import get_userAgent
|
|
21
|
+
from StreamingCommunity.Util.http_client import create_client_curl
|
|
21
22
|
from StreamingCommunity.Util.config_json import config_manager
|
|
22
23
|
|
|
23
24
|
|
|
@@ -38,6 +39,7 @@ DEFAULT_AUDIO_WORKERS = config_manager.get_int('M3U8_DOWNLOAD', 'default_audio_w
|
|
|
38
39
|
MAX_TIMEOUT = config_manager.get_int("REQUESTS", "timeout")
|
|
39
40
|
SEGMENT_MAX_TIMEOUT = config_manager.get_int("M3U8_DOWNLOAD", "segment_timeout")
|
|
40
41
|
LIMIT_SEGMENT = config_manager.get_int('M3U8_DOWNLOAD', 'limit_segment')
|
|
42
|
+
ENABLE_RETRY = config_manager.get_bool('M3U8_DOWNLOAD', 'enable_retry')
|
|
41
43
|
|
|
42
44
|
|
|
43
45
|
# Variable
|
|
@@ -68,6 +70,8 @@ class M3U8_Segments:
|
|
|
68
70
|
self.limit_segments = LIMIT_SEGMENT if LIMIT_SEGMENT > 0 else None
|
|
69
71
|
else:
|
|
70
72
|
self.limit_segments = limit_segments
|
|
73
|
+
|
|
74
|
+
self.enable_retry = ENABLE_RETRY
|
|
71
75
|
|
|
72
76
|
# Util class
|
|
73
77
|
self.decryption: M3U8_Decryption = None
|
|
@@ -94,12 +98,7 @@ class M3U8_Segments:
|
|
|
94
98
|
self.key_base_url = f"{parsed_url.scheme}://{parsed_url.netloc}/"
|
|
95
99
|
|
|
96
100
|
try:
|
|
97
|
-
|
|
98
|
-
'headers': self.custom_headers,
|
|
99
|
-
'timeout': MAX_TIMEOUT,
|
|
100
|
-
'verify': REQUEST_VERIFY
|
|
101
|
-
}
|
|
102
|
-
response = httpx.get(url=key_uri, **client_params)
|
|
101
|
+
response = create_client_curl(headers=self.custom_headers).get(key_uri)
|
|
103
102
|
response.raise_for_status()
|
|
104
103
|
|
|
105
104
|
hex_content = binascii.hexlify(response.content).decode('utf-8')
|
|
@@ -146,12 +145,7 @@ class M3U8_Segments:
|
|
|
146
145
|
"""
|
|
147
146
|
if self.is_index_url:
|
|
148
147
|
try:
|
|
149
|
-
|
|
150
|
-
'headers': self.custom_headers,
|
|
151
|
-
'timeout': MAX_TIMEOUT,
|
|
152
|
-
'verify': REQUEST_VERIFY
|
|
153
|
-
}
|
|
154
|
-
response = httpx.get(self.url, **client_params, follow_redirects=True)
|
|
148
|
+
response = create_client_curl(headers=self.custom_headers).get(self.url)
|
|
155
149
|
response.raise_for_status()
|
|
156
150
|
|
|
157
151
|
self.parse_data(response.text)
|
|
@@ -174,7 +168,7 @@ class M3U8_Segments:
|
|
|
174
168
|
"""
|
|
175
169
|
Get the file path for a temporary segment.
|
|
176
170
|
"""
|
|
177
|
-
return os.path.join(temp_dir, f"seg_{index:06d}.
|
|
171
|
+
return os.path.join(temp_dir, f"seg_{index:06d}.ts")
|
|
178
172
|
|
|
179
173
|
async def _download_init_segment(self, client: httpx.AsyncClient, output_path: str, progress_bar: tqdm) -> bool:
|
|
180
174
|
"""
|
|
@@ -220,7 +214,7 @@ class M3U8_Segments:
|
|
|
220
214
|
async def _download_single_segment(self, client: httpx.AsyncClient, ts_url: str, index: int, temp_dir: str,
|
|
221
215
|
semaphore: asyncio.Semaphore, max_retry: int) -> tuple:
|
|
222
216
|
"""
|
|
223
|
-
Downloads a single TS segment and saves to temp file.
|
|
217
|
+
Downloads a single TS segment and saves to temp file IMMEDIATELY.
|
|
224
218
|
|
|
225
219
|
Returns:
|
|
226
220
|
tuple: (index, success, retry_count, file_size)
|
|
@@ -248,11 +242,13 @@ class M3U8_Segments:
|
|
|
248
242
|
return index, False, attempt, 0
|
|
249
243
|
raise e
|
|
250
244
|
|
|
251
|
-
# Write to temp file
|
|
245
|
+
# Write segment to temp file IMMEDIATELY
|
|
252
246
|
with open(temp_file, 'wb') as f:
|
|
253
247
|
f.write(segment_content)
|
|
254
|
-
|
|
255
|
-
|
|
248
|
+
|
|
249
|
+
size = len(segment_content)
|
|
250
|
+
del segment_content
|
|
251
|
+
return index, True, attempt, size
|
|
256
252
|
|
|
257
253
|
except Exception:
|
|
258
254
|
if attempt + 1 == max_retry:
|
|
@@ -296,8 +292,8 @@ class M3U8_Segments:
|
|
|
296
292
|
console.print("\n[red]Download interrupted by user (Ctrl+C).")
|
|
297
293
|
break
|
|
298
294
|
|
|
299
|
-
# Retry failed segments
|
|
300
|
-
if not self.download_interrupted:
|
|
295
|
+
# Retry failed segments only if enabled
|
|
296
|
+
if self.enable_retry and not self.download_interrupted:
|
|
301
297
|
await self._retry_failed_segments(client, temp_dir, semaphore, progress_bar)
|
|
302
298
|
|
|
303
299
|
async def _retry_failed_segments(self, client: httpx.AsyncClient, temp_dir: str, semaphore: asyncio.Semaphore,
|
|
@@ -356,6 +352,7 @@ class M3U8_Segments:
|
|
|
356
352
|
if os.path.exists(temp_file):
|
|
357
353
|
with open(temp_file, 'rb') as infile:
|
|
358
354
|
outfile.write(infile.read())
|
|
355
|
+
os.remove(temp_file)
|
|
359
356
|
|
|
360
357
|
async def download_segments_async(self, description: str, type: str):
|
|
361
358
|
"""
|
|
@@ -473,13 +470,12 @@ class M3U8_Segments:
|
|
|
473
470
|
"""Ensure resource cleanup and final reporting."""
|
|
474
471
|
progress_bar.close()
|
|
475
472
|
|
|
476
|
-
# Delete temp
|
|
473
|
+
# Delete temp directory if exists
|
|
477
474
|
if temp_dir and os.path.exists(temp_dir):
|
|
478
475
|
try:
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
os.remove(temp_file)
|
|
476
|
+
# Remove any remaining files (in case of interruption)
|
|
477
|
+
for file in os.listdir(temp_dir):
|
|
478
|
+
os.remove(os.path.join(temp_dir, file))
|
|
483
479
|
os.rmdir(temp_dir)
|
|
484
480
|
except Exception as e:
|
|
485
481
|
console.print(f"[yellow]Warning: Could not clean temp directory: {e}")
|
|
@@ -489,7 +485,6 @@ class M3U8_Segments:
|
|
|
489
485
|
|
|
490
486
|
def _display_error_summary(self) -> None:
|
|
491
487
|
"""Generate final error report."""
|
|
492
|
-
console.print(f"
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
f"[cyan]Failed segments: [red]{self.info_nFailed}")
|
|
488
|
+
console.print(f" [cyan]Max retries: [red]{self.info_maxRetry} [white] | "
|
|
489
|
+
f"[cyan]Total retries: [red]{self.info_nRetry} [white] | "
|
|
490
|
+
f"[cyan]Failed segments: [red]{self.info_nFailed}")
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# 16.04.24
|
|
2
2
|
|
|
3
3
|
import re
|
|
4
|
+
import time
|
|
4
5
|
import logging
|
|
5
6
|
import threading
|
|
6
7
|
import subprocess
|
|
@@ -29,6 +30,7 @@ def capture_output(process: subprocess.Popen, description: str) -> None:
|
|
|
29
30
|
"""
|
|
30
31
|
try:
|
|
31
32
|
max_length = 0
|
|
33
|
+
start_time = time.time()
|
|
32
34
|
|
|
33
35
|
for line in iter(process.stdout.readline, ''):
|
|
34
36
|
try:
|
|
@@ -44,8 +46,7 @@ def capture_output(process: subprocess.Popen, description: str) -> None:
|
|
|
44
46
|
|
|
45
47
|
if "size=" in line:
|
|
46
48
|
try:
|
|
47
|
-
|
|
48
|
-
# Parse the output line to extract relevant information
|
|
49
|
+
elapsed_time = time.time() - start_time
|
|
49
50
|
data = parse_output_line(line)
|
|
50
51
|
|
|
51
52
|
if 'q' in data:
|
|
@@ -55,11 +56,25 @@ def capture_output(process: subprocess.Popen, description: str) -> None:
|
|
|
55
56
|
else:
|
|
56
57
|
byte_size = int(re.findall(r'\d+', data.get('size', '0'))[0]) * 1000
|
|
57
58
|
|
|
59
|
+
# Extract additional information
|
|
60
|
+
fps = data.get('fps', 'N/A')
|
|
61
|
+
time_processed = data.get('time', 'N/A')
|
|
62
|
+
bitrate = data.get('bitrate', 'N/A')
|
|
63
|
+
speed = data.get('speed', 'N/A')
|
|
64
|
+
|
|
65
|
+
# Format elapsed time as HH:MM:SS
|
|
66
|
+
elapsed_formatted = format_time(elapsed_time)
|
|
58
67
|
|
|
59
68
|
# Construct the progress string with formatted output information
|
|
60
|
-
progress_string = (
|
|
61
|
-
|
|
62
|
-
|
|
69
|
+
progress_string = (
|
|
70
|
+
f"{description}[white]: "
|
|
71
|
+
f"([green]'fps': [yellow]{fps}[white], "
|
|
72
|
+
f"[green]'speed': [yellow]{speed}[white], "
|
|
73
|
+
f"[green]'size': [yellow]{internet_manager.format_file_size(byte_size)}[white], "
|
|
74
|
+
f"[green]'time': [yellow]{time_processed}[white], "
|
|
75
|
+
f"[green]'bitrate': [yellow]{bitrate}[white], "
|
|
76
|
+
f"[green]'elapsed': [yellow]{elapsed_formatted}[white])"
|
|
77
|
+
)
|
|
63
78
|
max_length = max(max_length, len(progress_string))
|
|
64
79
|
|
|
65
80
|
# Print the progress string to the console, overwriting the previous line
|
|
@@ -81,6 +96,19 @@ def capture_output(process: subprocess.Popen, description: str) -> None:
|
|
|
81
96
|
logging.error(f"Error terminating process: {e}")
|
|
82
97
|
|
|
83
98
|
|
|
99
|
+
def format_time(seconds: float) -> str:
|
|
100
|
+
"""
|
|
101
|
+
Format seconds into HH:MM:SS format.
|
|
102
|
+
|
|
103
|
+
Parameters:
|
|
104
|
+
- seconds (float): Time in seconds.
|
|
105
|
+
"""
|
|
106
|
+
hours = int(seconds // 3600)
|
|
107
|
+
minutes = int((seconds % 3600) // 60)
|
|
108
|
+
secs = int(seconds % 60)
|
|
109
|
+
return f"{hours:02d}:{minutes:02d}:{secs:02d}"
|
|
110
|
+
|
|
111
|
+
|
|
84
112
|
def parse_output_line(line: str) -> dict:
|
|
85
113
|
"""
|
|
86
114
|
Function to parse the output line and extract relevant information.
|
|
@@ -101,6 +129,10 @@ def parse_output_line(line: str) -> dict:
|
|
|
101
129
|
if len(key_value) == 2:
|
|
102
130
|
key = key_value[0]
|
|
103
131
|
value = key_value[1]
|
|
132
|
+
|
|
133
|
+
# Remove milliseconds from time value
|
|
134
|
+
if key == 'time' and isinstance(value, str) and '.' in value:
|
|
135
|
+
value = value.split('.')[0]
|
|
104
136
|
data[key] = value
|
|
105
137
|
|
|
106
138
|
return data
|
|
@@ -23,18 +23,30 @@ from ..M3U8 import M3U8_Codec
|
|
|
23
23
|
# Config
|
|
24
24
|
DEBUG_MODE = config_manager.get_bool("DEFAULT", "debug")
|
|
25
25
|
DEBUG_FFMPEG = "debug" if DEBUG_MODE else "error"
|
|
26
|
-
USE_CODEC = config_manager.get_bool("M3U8_CONVERSION", "use_codec")
|
|
27
|
-
USE_VCODEC = config_manager.get_bool("M3U8_CONVERSION", "use_vcodec")
|
|
28
|
-
USE_ACODEC = config_manager.get_bool("M3U8_CONVERSION", "use_acodec")
|
|
29
|
-
USE_BITRATE = config_manager.get_bool("M3U8_CONVERSION", "use_bitrate")
|
|
30
26
|
USE_GPU = config_manager.get_bool("M3U8_CONVERSION", "use_gpu")
|
|
31
|
-
|
|
27
|
+
PARAM_VIDEO = config_manager.get_list("M3U8_CONVERSION", "param_video")
|
|
28
|
+
PARAM_AUDIO = config_manager.get_list("M3U8_CONVERSION", "param_audio")
|
|
29
|
+
PARAM_FINAL = config_manager.get_list("M3U8_CONVERSION", "param_final")
|
|
32
30
|
|
|
33
31
|
|
|
34
32
|
# Variable
|
|
35
33
|
console = Console()
|
|
36
34
|
|
|
37
35
|
|
|
36
|
+
def add_encoding_params(ffmpeg_cmd: List[str]):
|
|
37
|
+
"""
|
|
38
|
+
Add encoding parameters to the ffmpeg command.
|
|
39
|
+
|
|
40
|
+
Parameters:
|
|
41
|
+
ffmpeg_cmd (List[str]): List of the FFmpeg command to modify
|
|
42
|
+
"""
|
|
43
|
+
if PARAM_FINAL:
|
|
44
|
+
ffmpeg_cmd.extend(PARAM_FINAL)
|
|
45
|
+
else:
|
|
46
|
+
ffmpeg_cmd.extend(PARAM_VIDEO)
|
|
47
|
+
ffmpeg_cmd.extend(PARAM_AUDIO)
|
|
48
|
+
|
|
49
|
+
|
|
38
50
|
def check_subtitle_encoders() -> Tuple[Optional[bool], Optional[bool]]:
|
|
39
51
|
"""
|
|
40
52
|
Executes 'ffmpeg -encoders' and checks if 'mov_text' and 'webvtt' encoders are available.
|
|
@@ -51,7 +63,6 @@ def check_subtitle_encoders() -> Tuple[Optional[bool], Optional[bool]]:
|
|
|
51
63
|
check=True
|
|
52
64
|
)
|
|
53
65
|
|
|
54
|
-
# Check for encoder presence in output
|
|
55
66
|
output = result.stdout
|
|
56
67
|
mov_text_supported = "mov_text" in output
|
|
57
68
|
webvtt_supported = "webvtt" in output
|
|
@@ -74,11 +85,9 @@ def select_subtitle_encoder() -> Optional[str]:
|
|
|
74
85
|
"""
|
|
75
86
|
mov_text_supported, webvtt_supported = check_subtitle_encoders()
|
|
76
87
|
|
|
77
|
-
# Return early if check failed
|
|
78
88
|
if mov_text_supported is None:
|
|
79
89
|
return None
|
|
80
90
|
|
|
81
|
-
# Prioritize mov_text over webvtt
|
|
82
91
|
if mov_text_supported:
|
|
83
92
|
logging.info("Using 'mov_text' as the subtitle encoder.")
|
|
84
93
|
return "mov_text"
|
|
@@ -98,7 +107,7 @@ def join_video(video_path: str, out_path: str, codec: M3U8_Codec = None):
|
|
|
98
107
|
Parameters:
|
|
99
108
|
- video_path (str): The path to the video file.
|
|
100
109
|
- out_path (str): The path to save the output file.
|
|
101
|
-
- codec (M3U8_Codec): The video codec to use
|
|
110
|
+
- codec (M3U8_Codec): The video codec to use (non utilizzato con nuova configurazione).
|
|
102
111
|
"""
|
|
103
112
|
ffmpeg_cmd = [get_ffmpeg_path()]
|
|
104
113
|
|
|
@@ -113,42 +122,11 @@ def join_video(video_path: str, out_path: str, codec: M3U8_Codec = None):
|
|
|
113
122
|
# Insert input video path
|
|
114
123
|
ffmpeg_cmd.extend(['-i', video_path])
|
|
115
124
|
|
|
116
|
-
# Add output
|
|
117
|
-
|
|
118
|
-
if USE_VCODEC:
|
|
119
|
-
if codec.video_codec_name:
|
|
120
|
-
if not USE_GPU:
|
|
121
|
-
ffmpeg_cmd.extend(['-c:v', codec.video_codec_name])
|
|
122
|
-
else:
|
|
123
|
-
ffmpeg_cmd.extend(['-c:v', 'h264_nvenc'])
|
|
124
|
-
else:
|
|
125
|
-
console.log("[red]Cant find vcodec for 'join_audios'")
|
|
126
|
-
else:
|
|
127
|
-
if USE_GPU:
|
|
128
|
-
ffmpeg_cmd.extend(['-c:v', 'h264_nvenc'])
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
if USE_ACODEC:
|
|
132
|
-
if codec.audio_codec_name:
|
|
133
|
-
ffmpeg_cmd.extend(['-c:a', codec.audio_codec_name])
|
|
134
|
-
else:
|
|
135
|
-
console.log("[red]Cant find acodec for 'join_audios'")
|
|
136
|
-
|
|
137
|
-
if USE_BITRATE:
|
|
138
|
-
ffmpeg_cmd.extend(['-b:v', f'{codec.video_bitrate // 1000}k'])
|
|
139
|
-
ffmpeg_cmd.extend(['-b:a', f'{codec.audio_bitrate // 1000}k'])
|
|
140
|
-
|
|
141
|
-
else:
|
|
142
|
-
ffmpeg_cmd.extend(['-c', 'copy'])
|
|
143
|
-
|
|
144
|
-
# Ultrafast preset always or fast for gpu
|
|
145
|
-
if not USE_GPU:
|
|
146
|
-
ffmpeg_cmd.extend(['-preset', FFMPEG_DEFAULT_PRESET])
|
|
147
|
-
else:
|
|
148
|
-
ffmpeg_cmd.extend(['-preset', 'fast'])
|
|
125
|
+
# Add encoding parameters (prima dell'output)
|
|
126
|
+
add_encoding_params(ffmpeg_cmd)
|
|
149
127
|
|
|
150
|
-
#
|
|
151
|
-
ffmpeg_cmd
|
|
128
|
+
# Output file and overwrite
|
|
129
|
+
ffmpeg_cmd.extend([out_path, '-y'])
|
|
152
130
|
|
|
153
131
|
# Run join
|
|
154
132
|
if DEBUG_MODE:
|
|
@@ -169,6 +147,8 @@ def join_audios(video_path: str, audio_tracks: List[Dict[str, str]], out_path: s
|
|
|
169
147
|
- audio_tracks (list[dict[str, str]]): A list of dictionaries containing information about audio tracks.
|
|
170
148
|
Each dictionary should contain the 'path' and 'name' keys.
|
|
171
149
|
- out_path (str): The path to save the output file.
|
|
150
|
+
- codec (M3U8_Codec): The video codec to use (non utilizzato con nuova configurazione).
|
|
151
|
+
- limit_duration_diff (float): Maximum duration difference in seconds.
|
|
172
152
|
"""
|
|
173
153
|
use_shortest = False
|
|
174
154
|
duration_diffs = []
|
|
@@ -210,59 +190,25 @@ def join_audios(video_path: str, audio_tracks: List[Dict[str, str]], out_path: s
|
|
|
210
190
|
for i, audio_track in enumerate(audio_tracks):
|
|
211
191
|
ffmpeg_cmd.extend(['-i', audio_track.get('path')])
|
|
212
192
|
|
|
213
|
-
|
|
214
193
|
# Map the video and audio streams
|
|
215
|
-
ffmpeg_cmd.
|
|
216
|
-
ffmpeg_cmd.append('0:v') # Map video stream from the first input (video_path)
|
|
194
|
+
ffmpeg_cmd.extend(['-map', '0:v'])
|
|
217
195
|
|
|
218
196
|
for i in range(1, len(audio_tracks) + 1):
|
|
219
|
-
ffmpeg_cmd.
|
|
220
|
-
ffmpeg_cmd.append(f'{i}:a') # Map audio streams from subsequent inputs
|
|
221
|
-
|
|
222
|
-
# Add output Parameters
|
|
223
|
-
if USE_CODEC:
|
|
224
|
-
if USE_VCODEC:
|
|
225
|
-
if codec.video_codec_name:
|
|
226
|
-
if not USE_GPU:
|
|
227
|
-
ffmpeg_cmd.extend(['-c:v', codec.video_codec_name])
|
|
228
|
-
else:
|
|
229
|
-
ffmpeg_cmd.extend(['-c:v', 'h264_nvenc'])
|
|
230
|
-
else:
|
|
231
|
-
console.log("[red]Cant find vcodec for 'join_audios'")
|
|
232
|
-
else:
|
|
233
|
-
if USE_GPU:
|
|
234
|
-
ffmpeg_cmd.extend(['-c:v', 'h264_nvenc'])
|
|
235
|
-
|
|
236
|
-
if USE_ACODEC:
|
|
237
|
-
if codec.audio_codec_name:
|
|
238
|
-
ffmpeg_cmd.extend(['-c:a', codec.audio_codec_name])
|
|
239
|
-
else:
|
|
240
|
-
console.log("[red]Cant find acodec for 'join_audios'")
|
|
241
|
-
|
|
242
|
-
if USE_BITRATE:
|
|
243
|
-
ffmpeg_cmd.extend(['-b:v', f'{codec.video_bitrate // 1000}k'])
|
|
244
|
-
ffmpeg_cmd.extend(['-b:a', f'{codec.audio_bitrate // 1000}k'])
|
|
197
|
+
ffmpeg_cmd.extend(['-map', f'{i}:a'])
|
|
245
198
|
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
# Ultrafast preset always or fast for gpu
|
|
250
|
-
if not USE_GPU:
|
|
251
|
-
ffmpeg_cmd.extend(['-preset', FFMPEG_DEFAULT_PRESET])
|
|
252
|
-
else:
|
|
253
|
-
ffmpeg_cmd.extend(['-preset', 'fast'])
|
|
199
|
+
# Add encoding parameters (prima di -shortest e output)
|
|
200
|
+
add_encoding_params(ffmpeg_cmd)
|
|
254
201
|
|
|
255
202
|
# Use shortest input path if any audio track has significant difference
|
|
256
203
|
if use_shortest:
|
|
257
204
|
ffmpeg_cmd.extend(['-shortest', '-strict', 'experimental'])
|
|
258
205
|
|
|
259
|
-
#
|
|
260
|
-
ffmpeg_cmd
|
|
206
|
+
# Output file and overwrite
|
|
207
|
+
ffmpeg_cmd.extend([out_path, '-y'])
|
|
261
208
|
|
|
262
209
|
# Run join
|
|
263
210
|
if DEBUG_MODE:
|
|
264
211
|
subprocess.run(ffmpeg_cmd, check=True)
|
|
265
|
-
|
|
266
212
|
else:
|
|
267
213
|
capture_ffmpeg_real_time(ffmpeg_cmd, "[yellow]FFMPEG [cyan]Join audio")
|
|
268
214
|
print()
|
|
@@ -294,11 +240,8 @@ def join_subtitle(video_path: str, subtitles_list: List[Dict[str, str]], out_pat
|
|
|
294
240
|
ffmpeg_cmd += ["-map", f"{idx + 1}:s"]
|
|
295
241
|
ffmpeg_cmd += ["-metadata:s:s:{}".format(idx), "title={}".format(subtitle['language'])]
|
|
296
242
|
|
|
297
|
-
#
|
|
298
|
-
|
|
299
|
-
ffmpeg_cmd.extend(['-c:v', 'copy', '-c:a', 'copy', '-c:s', select_subtitle_encoder()])
|
|
300
|
-
else:
|
|
301
|
-
ffmpeg_cmd.extend(['-c', 'copy', '-c:s', select_subtitle_encoder()])
|
|
243
|
+
# For subtitles, we always use copy for video/audio and only encoder for subtitles
|
|
244
|
+
ffmpeg_cmd.extend(['-c:v', 'copy', '-c:a', 'copy', '-c:s', select_subtitle_encoder()])
|
|
302
245
|
|
|
303
246
|
# Overwrite
|
|
304
247
|
ffmpeg_cmd += [out_path, "-y"]
|
|
@@ -307,7 +250,6 @@ def join_subtitle(video_path: str, subtitles_list: List[Dict[str, str]], out_pat
|
|
|
307
250
|
# Run join
|
|
308
251
|
if DEBUG_MODE:
|
|
309
252
|
subprocess.run(ffmpeg_cmd, check=True)
|
|
310
|
-
|
|
311
253
|
else:
|
|
312
254
|
capture_ffmpeg_real_time(ffmpeg_cmd, "[yellow]FFMPEG [cyan]Join subtitle")
|
|
313
255
|
print()
|
|
@@ -4,13 +4,12 @@ import sys
|
|
|
4
4
|
|
|
5
5
|
|
|
6
6
|
# External libraries
|
|
7
|
-
import httpx
|
|
8
7
|
from rich.console import Console
|
|
9
8
|
|
|
10
9
|
|
|
11
10
|
# Internal utilities
|
|
12
11
|
from .obj_tmbd import Json_film
|
|
13
|
-
from StreamingCommunity.Util.
|
|
12
|
+
from StreamingCommunity.Util.http_client import create_client
|
|
14
13
|
from StreamingCommunity.Util.table import TVShowManager
|
|
15
14
|
|
|
16
15
|
|
|
@@ -18,7 +17,6 @@ from StreamingCommunity.Util.table import TVShowManager
|
|
|
18
17
|
console = Console()
|
|
19
18
|
table_show_manager = TVShowManager()
|
|
20
19
|
api_key = "a800ed6c93274fb857ea61bd9e7256c5"
|
|
21
|
-
MAX_TIMEOUT = config_manager.get_int("REQUESTS", "timeout")
|
|
22
20
|
|
|
23
21
|
|
|
24
22
|
def get_select_title(table_show_manager, generic_obj):
|
|
@@ -113,7 +111,7 @@ class TheMovieDB:
|
|
|
113
111
|
|
|
114
112
|
params['api_key'] = self.api_key
|
|
115
113
|
url = f"{self.base_url}/{endpoint}"
|
|
116
|
-
response =
|
|
114
|
+
response = create_client().get(url, params=params)
|
|
117
115
|
response.raise_for_status()
|
|
118
116
|
|
|
119
117
|
return response.json()
|