StreamingCommunity 3.3.6__py3-none-any.whl → 3.3.7__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/Site/altadefinizione/film.py +1 -1
- StreamingCommunity/Api/Site/altadefinizione/series.py +1 -1
- StreamingCommunity/Api/Site/animeunity/serie.py +1 -1
- StreamingCommunity/Api/Site/animeworld/film.py +1 -1
- StreamingCommunity/Api/Site/animeworld/serie.py +1 -1
- StreamingCommunity/Api/Site/crunchyroll/film.py +3 -2
- StreamingCommunity/Api/Site/crunchyroll/series.py +3 -2
- StreamingCommunity/Api/Site/crunchyroll/site.py +0 -8
- StreamingCommunity/Api/Site/crunchyroll/util/get_license.py +11 -105
- StreamingCommunity/Api/Site/guardaserie/series.py +1 -1
- StreamingCommunity/Api/Site/mediasetinfinity/film.py +1 -1
- StreamingCommunity/Api/Site/mediasetinfinity/series.py +7 -9
- StreamingCommunity/Api/Site/mediasetinfinity/site.py +29 -66
- StreamingCommunity/Api/Site/mediasetinfinity/util/ScrapeSerie.py +5 -1
- StreamingCommunity/Api/Site/mediasetinfinity/util/get_license.py +151 -233
- StreamingCommunity/Api/Site/raiplay/film.py +2 -10
- StreamingCommunity/Api/Site/raiplay/series.py +2 -10
- StreamingCommunity/Api/Site/raiplay/site.py +1 -0
- StreamingCommunity/Api/Site/raiplay/util/ScrapeSerie.py +7 -1
- StreamingCommunity/Api/Site/streamingcommunity/film.py +1 -1
- StreamingCommunity/Api/Site/streamingcommunity/series.py +1 -1
- StreamingCommunity/Api/Site/streamingwatch/film.py +1 -1
- StreamingCommunity/Api/Site/streamingwatch/series.py +1 -1
- StreamingCommunity/Api/Template/loader.py +149 -0
- StreamingCommunity/Lib/Downloader/DASH/downloader.py +267 -51
- StreamingCommunity/Lib/Downloader/DASH/segments.py +46 -15
- StreamingCommunity/Lib/Downloader/HLS/downloader.py +51 -36
- StreamingCommunity/Lib/Downloader/HLS/segments.py +105 -25
- StreamingCommunity/Lib/Downloader/MP4/downloader.py +12 -13
- StreamingCommunity/Lib/FFmpeg/command.py +18 -81
- StreamingCommunity/Lib/FFmpeg/util.py +14 -10
- StreamingCommunity/Lib/M3U8/estimator.py +13 -12
- StreamingCommunity/Lib/M3U8/parser.py +16 -16
- StreamingCommunity/Upload/update.py +2 -4
- StreamingCommunity/Upload/version.py +2 -2
- StreamingCommunity/Util/config_json.py +3 -129
- StreamingCommunity/Util/installer/bento4_install.py +21 -31
- StreamingCommunity/Util/installer/device_install.py +0 -1
- StreamingCommunity/Util/installer/ffmpeg_install.py +0 -1
- StreamingCommunity/Util/message.py +8 -9
- StreamingCommunity/Util/os.py +0 -8
- StreamingCommunity/run.py +4 -44
- {streamingcommunity-3.3.6.dist-info → streamingcommunity-3.3.7.dist-info}/METADATA +1 -1
- {streamingcommunity-3.3.6.dist-info → streamingcommunity-3.3.7.dist-info}/RECORD +48 -47
- {streamingcommunity-3.3.6.dist-info → streamingcommunity-3.3.7.dist-info}/WHEEL +0 -0
- {streamingcommunity-3.3.6.dist-info → streamingcommunity-3.3.7.dist-info}/entry_points.txt +0 -0
- {streamingcommunity-3.3.6.dist-info → streamingcommunity-3.3.7.dist-info}/licenses/LICENSE +0 -0
- {streamingcommunity-3.3.6.dist-info → streamingcommunity-3.3.7.dist-info}/top_level.txt +0 -0
|
@@ -25,13 +25,20 @@ SEGMENT_MAX_TIMEOUT = config_manager.get_int("M3U8_DOWNLOAD", "segment_timeout")
|
|
|
25
25
|
|
|
26
26
|
|
|
27
27
|
class MPD_Segments:
|
|
28
|
-
def __init__(self, tmp_folder: str, representation: dict, pssh: str = None):
|
|
28
|
+
def __init__(self, tmp_folder: str, representation: dict, pssh: str = None, limit_segments: int = None):
|
|
29
29
|
"""
|
|
30
|
-
Initialize MPD_Segments with temp folder, representation, and
|
|
30
|
+
Initialize MPD_Segments with temp folder, representation, optional pssh, and segment limit.
|
|
31
|
+
|
|
32
|
+
Parameters:
|
|
33
|
+
- tmp_folder (str): Temporary folder to store downloaded segments
|
|
34
|
+
- representation (dict): Selected representation with segment URLs
|
|
35
|
+
- pssh (str, optional): PSSH string for decryption
|
|
36
|
+
- limit_segments (int, optional): Optional limit for number of segments to download
|
|
31
37
|
"""
|
|
32
38
|
self.tmp_folder = tmp_folder
|
|
33
39
|
self.selected_representation = representation
|
|
34
40
|
self.pssh = pssh
|
|
41
|
+
self.limit_segments = limit_segments
|
|
35
42
|
self.download_interrupted = False
|
|
36
43
|
self.info_nFailed = 0
|
|
37
44
|
|
|
@@ -42,7 +49,7 @@ class MPD_Segments:
|
|
|
42
49
|
|
|
43
50
|
# Progress
|
|
44
51
|
self._last_progress_update = 0
|
|
45
|
-
self._progress_update_interval = 0.
|
|
52
|
+
self._progress_update_interval = 0.1
|
|
46
53
|
|
|
47
54
|
def get_concat_path(self, output_dir: str = None):
|
|
48
55
|
"""
|
|
@@ -50,16 +57,35 @@ class MPD_Segments:
|
|
|
50
57
|
"""
|
|
51
58
|
rep_id = self.selected_representation['id']
|
|
52
59
|
return os.path.join(output_dir or self.tmp_folder, f"{rep_id}_encrypted.m4s")
|
|
60
|
+
|
|
61
|
+
def get_segments_count(self) -> int:
|
|
62
|
+
"""
|
|
63
|
+
Returns the total number of segments available in the representation.
|
|
64
|
+
"""
|
|
65
|
+
return len(self.selected_representation.get('segment_urls', []))
|
|
53
66
|
|
|
54
|
-
def download_streams(self, output_dir: str = None):
|
|
67
|
+
def download_streams(self, output_dir: str = None, description: str = "DASH"):
|
|
55
68
|
"""
|
|
56
69
|
Synchronous wrapper for download_segments, compatible with legacy calls.
|
|
70
|
+
|
|
71
|
+
Parameters:
|
|
72
|
+
- output_dir (str): Output directory for segments
|
|
73
|
+
- description (str): Description for progress bar (e.g., "Video", "Audio Italian")
|
|
57
74
|
"""
|
|
58
75
|
concat_path = self.get_concat_path(output_dir)
|
|
59
76
|
|
|
77
|
+
# Apply segment limit if specified
|
|
78
|
+
if self.limit_segments is not None:
|
|
79
|
+
orig_count = len(self.selected_representation.get('segment_urls', []))
|
|
80
|
+
if orig_count > self.limit_segments:
|
|
81
|
+
|
|
82
|
+
# Limit segment URLs
|
|
83
|
+
self.selected_representation['segment_urls'] = self.selected_representation['segment_urls'][:self.limit_segments]
|
|
84
|
+
print(f"[yellow]Limiting segments from {orig_count} to {self.limit_segments}")
|
|
85
|
+
|
|
60
86
|
# Run async download in sync mode
|
|
61
87
|
try:
|
|
62
|
-
asyncio.run(self.download_segments(output_dir=output_dir))
|
|
88
|
+
asyncio.run(self.download_segments(output_dir=output_dir, description=description))
|
|
63
89
|
|
|
64
90
|
except KeyboardInterrupt:
|
|
65
91
|
self.download_interrupted = True
|
|
@@ -74,6 +100,11 @@ class MPD_Segments:
|
|
|
74
100
|
async def download_segments(self, output_dir: str = None, concurrent_downloads: int = None, description: str = "DASH"):
|
|
75
101
|
"""
|
|
76
102
|
Download and concatenate all segments (including init) asynchronously and in order.
|
|
103
|
+
|
|
104
|
+
Parameters:
|
|
105
|
+
- output_dir (str): Output directory for segments
|
|
106
|
+
- concurrent_downloads (int): Number of concurrent downloads
|
|
107
|
+
- description (str): Description for progress bar (e.g., "Video", "Audio Italian")
|
|
77
108
|
"""
|
|
78
109
|
rep = self.selected_representation
|
|
79
110
|
rep_id = rep['id']
|
|
@@ -84,16 +115,15 @@ class MPD_Segments:
|
|
|
84
115
|
concat_path = os.path.join(output_dir or self.tmp_folder, f"{rep_id}_encrypted.m4s")
|
|
85
116
|
|
|
86
117
|
# Determine stream type (video/audio) for progress bar
|
|
87
|
-
stream_type =
|
|
118
|
+
stream_type = description
|
|
88
119
|
if concurrent_downloads is None:
|
|
89
|
-
|
|
120
|
+
worker_type = 'video' if 'Video' in description else 'audio'
|
|
121
|
+
concurrent_downloads = self._get_worker_count(worker_type)
|
|
90
122
|
|
|
91
123
|
progress_bar = tqdm(
|
|
92
124
|
total=len(segment_urls) + 1,
|
|
93
125
|
desc=f"Downloading {rep_id}",
|
|
94
|
-
bar_format=self._get_bar_format(stream_type)
|
|
95
|
-
mininterval=1.0,
|
|
96
|
-
maxinterval=2.5,
|
|
126
|
+
bar_format=self._get_bar_format(stream_type)
|
|
97
127
|
)
|
|
98
128
|
|
|
99
129
|
# Define semaphore for concurrent downloads
|
|
@@ -231,7 +261,7 @@ class MPD_Segments:
|
|
|
231
261
|
# Update estimator with segment size
|
|
232
262
|
estimator.add_ts_file(len(data))
|
|
233
263
|
|
|
234
|
-
# Update progress bar with estimated info
|
|
264
|
+
# Update progress bar with estimated info and segment count
|
|
235
265
|
self._throttled_progress_update(len(data), estimator, progress_bar)
|
|
236
266
|
|
|
237
267
|
except KeyboardInterrupt:
|
|
@@ -319,10 +349,11 @@ class MPD_Segments:
|
|
|
319
349
|
Generate platform-appropriate progress bar format.
|
|
320
350
|
"""
|
|
321
351
|
return (
|
|
322
|
-
f"{Colors.YELLOW}[
|
|
323
|
-
f"{Colors.
|
|
324
|
-
f"{Colors.
|
|
325
|
-
f"{Colors.YELLOW}{{elapsed}}{Colors.WHITE} < {Colors.CYAN}{{remaining}}{Colors.
|
|
352
|
+
f"{Colors.YELLOW}[DASH]{Colors.CYAN} {description}{Colors.WHITE}: "
|
|
353
|
+
f"{Colors.MAGENTA}{{bar:40}} "
|
|
354
|
+
f"{Colors.LIGHT_GREEN}{{n_fmt}}{Colors.WHITE}/{Colors.CYAN}{{total_fmt}} {Colors.LIGHT_MAGENTA}TS {Colors.WHITE}"
|
|
355
|
+
f"{Colors.DARK_GRAY}[{Colors.YELLOW}{{elapsed}}{Colors.WHITE} < {Colors.CYAN}{{remaining}}{Colors.DARK_GRAY}] "
|
|
356
|
+
f"{Colors.WHITE}{{postfix}}"
|
|
326
357
|
)
|
|
327
358
|
|
|
328
359
|
def _get_worker_count(self, stream_type: str) -> int:
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
# 17.10.24
|
|
2
2
|
|
|
3
3
|
import os
|
|
4
|
-
import re
|
|
5
4
|
import time
|
|
6
5
|
import logging
|
|
7
6
|
import shutil
|
|
@@ -20,7 +19,6 @@ from StreamingCommunity.Util.config_json import config_manager
|
|
|
20
19
|
from StreamingCommunity.Util.headers import get_userAgent
|
|
21
20
|
from StreamingCommunity.Util.http_client import create_client
|
|
22
21
|
from StreamingCommunity.Util.os import os_manager, internet_manager
|
|
23
|
-
from StreamingCommunity.TelegramHelp.telegram_bot import get_bot_instance
|
|
24
22
|
|
|
25
23
|
|
|
26
24
|
# Logic class
|
|
@@ -44,15 +42,14 @@ GET_ONLY_LINK = config_manager.get_int('M3U8_DOWNLOAD', 'get_only_link')
|
|
|
44
42
|
FILTER_CUSTOM_RESOLUTION = str(config_manager.get('M3U8_CONVERSION', 'force_resolution')).strip().lower()
|
|
45
43
|
RETRY_LIMIT = config_manager.get_int('REQUESTS', 'max_retry')
|
|
46
44
|
MAX_TIMEOUT = config_manager.get_int("REQUESTS", "timeout")
|
|
47
|
-
TELEGRAM_BOT = config_manager.get_bool('DEFAULT', 'telegram_bot')
|
|
48
45
|
|
|
49
46
|
console = Console()
|
|
50
47
|
|
|
51
48
|
|
|
52
49
|
class HLSClient:
|
|
53
50
|
"""Client for making HTTP requests to HLS endpoints with retry mechanism."""
|
|
54
|
-
def __init__(self):
|
|
55
|
-
self.headers = {'User-Agent': get_userAgent()}
|
|
51
|
+
def __init__(self, custom_headers: Optional[Dict[str, str]] = None):
|
|
52
|
+
self.headers = custom_headers if custom_headers else {'User-Agent': get_userAgent()}
|
|
56
53
|
|
|
57
54
|
def request(self, url: str, return_content: bool = False) -> Optional[httpx.Response]:
|
|
58
55
|
"""
|
|
@@ -238,22 +235,26 @@ class M3U8Manager:
|
|
|
238
235
|
# Subtitle information
|
|
239
236
|
available_subtitles = self.parser._subtitle.get_all_uris_and_names() or []
|
|
240
237
|
available_sub_languages = [sub.get('language') for sub in available_subtitles]
|
|
241
|
-
available_subs = ', '.join(available_sub_languages) if available_sub_languages else "Nothing"
|
|
242
238
|
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
239
|
+
if available_sub_languages:
|
|
240
|
+
available_subs = ', '.join(available_sub_languages)
|
|
241
|
+
|
|
242
|
+
downloadable_sub_languages = available_sub_languages if "*" in DOWNLOAD_SPECIFIC_SUBTITLE else list(set(available_sub_languages) & set(DOWNLOAD_SPECIFIC_SUBTITLE))
|
|
243
|
+
downloadable_subs = ', '.join(downloadable_sub_languages) if downloadable_sub_languages else "Nothing"
|
|
244
|
+
|
|
245
|
+
data_rows.append(["Subtitle", available_subs, ', '.join(DOWNLOAD_SPECIFIC_SUBTITLE), downloadable_subs])
|
|
247
246
|
|
|
248
247
|
# Audio information
|
|
249
248
|
available_audio = self.parser._audio.get_all_uris_and_names() or []
|
|
250
249
|
available_audio_languages = [audio.get('language') for audio in available_audio]
|
|
251
|
-
available_audios = ', '.join(available_audio_languages) if available_audio_languages else "Nothing"
|
|
252
250
|
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
251
|
+
if available_audio_languages:
|
|
252
|
+
available_audios = ', '.join(available_audio_languages)
|
|
253
|
+
|
|
254
|
+
downloadable_audio_languages = list(set(available_audio_languages) & set(DOWNLOAD_SPECIFIC_AUDIO))
|
|
255
|
+
downloadable_audios = ', '.join(downloadable_audio_languages) if downloadable_audio_languages else "Nothing"
|
|
256
|
+
|
|
257
|
+
data_rows.append(["Audio", available_audios, ', '.join(DOWNLOAD_SPECIFIC_AUDIO), downloadable_audios])
|
|
257
258
|
|
|
258
259
|
# Calculate max width for each column
|
|
259
260
|
headers = ["Type", "Available", "Set", "Downloadable"]
|
|
@@ -284,18 +285,21 @@ class M3U8Manager:
|
|
|
284
285
|
|
|
285
286
|
class DownloadManager:
|
|
286
287
|
"""Manages downloading of video, audio, and subtitle streams."""
|
|
287
|
-
def __init__(self, temp_dir: str, client: HLSClient, url_fixer: M3U8_UrlFix):
|
|
288
|
+
def __init__(self, temp_dir: str, client: HLSClient, url_fixer: M3U8_UrlFix, custom_headers: Optional[Dict[str, str]] = None):
|
|
288
289
|
"""
|
|
289
290
|
Args:
|
|
290
291
|
temp_dir: Directory for storing temporary files
|
|
291
292
|
client: HLSClient instance for making requests
|
|
292
293
|
url_fixer: URL fixer instance for generating complete URLs
|
|
294
|
+
custom_headers: Optional custom headers to use for all requests
|
|
293
295
|
"""
|
|
294
296
|
self.temp_dir = temp_dir
|
|
295
297
|
self.client = client
|
|
296
298
|
self.url_fixer = url_fixer
|
|
299
|
+
self.custom_headers = custom_headers
|
|
297
300
|
self.missing_segments = []
|
|
298
301
|
self.stopped = False
|
|
302
|
+
self.video_segments_count = 0
|
|
299
303
|
|
|
300
304
|
def download_video(self, video_url: str) -> bool:
|
|
301
305
|
"""
|
|
@@ -308,8 +312,16 @@ class DownloadManager:
|
|
|
308
312
|
video_full_url = self.url_fixer.generate_full_url(video_url)
|
|
309
313
|
video_tmp_dir = os.path.join(self.temp_dir, 'video')
|
|
310
314
|
|
|
311
|
-
downloader
|
|
315
|
+
# Create downloader without segment limit for video
|
|
316
|
+
downloader = M3U8_Segments(
|
|
317
|
+
url=video_full_url,
|
|
318
|
+
tmp_folder=video_tmp_dir,
|
|
319
|
+
custom_headers=self.custom_headers
|
|
320
|
+
)
|
|
321
|
+
|
|
322
|
+
# Download video and get segment count
|
|
312
323
|
result = downloader.download_streams("Video", "video")
|
|
324
|
+
self.video_segments_count = downloader.get_segments_count()
|
|
313
325
|
self.missing_segments.append(result)
|
|
314
326
|
|
|
315
327
|
if result.get('stopped', False):
|
|
@@ -325,6 +337,7 @@ class DownloadManager:
|
|
|
325
337
|
def download_audio(self, audio: Dict) -> bool:
|
|
326
338
|
"""
|
|
327
339
|
Downloads audio segments for a specific language track.
|
|
340
|
+
Uses video segment count as a limit if available.
|
|
328
341
|
|
|
329
342
|
Returns:
|
|
330
343
|
bool: True if download was successful, False otherwise
|
|
@@ -332,8 +345,15 @@ class DownloadManager:
|
|
|
332
345
|
try:
|
|
333
346
|
audio_full_url = self.url_fixer.generate_full_url(audio['uri'])
|
|
334
347
|
audio_tmp_dir = os.path.join(self.temp_dir, 'audio', audio['language'])
|
|
335
|
-
|
|
336
|
-
downloader
|
|
348
|
+
|
|
349
|
+
# Create downloader with segment limit for audio
|
|
350
|
+
downloader = M3U8_Segments(
|
|
351
|
+
url=audio_full_url,
|
|
352
|
+
tmp_folder=audio_tmp_dir,
|
|
353
|
+
limit_segments=self.video_segments_count if self.video_segments_count > 0 else None,
|
|
354
|
+
custom_headers=self.custom_headers
|
|
355
|
+
)
|
|
356
|
+
|
|
337
357
|
result = downloader.download_streams(f"Audio {audio['language']}", "audio")
|
|
338
358
|
self.missing_segments.append(result)
|
|
339
359
|
|
|
@@ -500,10 +520,14 @@ class MergeManager:
|
|
|
500
520
|
|
|
501
521
|
class HLS_Downloader:
|
|
502
522
|
"""Main class for HLS video download and processing."""
|
|
503
|
-
def __init__(self, m3u8_url: str, output_path: Optional[str] = None):
|
|
523
|
+
def __init__(self, m3u8_url: str, output_path: Optional[str] = None, headers: Optional[Dict[str, str]] = None):
|
|
524
|
+
"""
|
|
525
|
+
Initializes the HLS_Downloader with parameters.
|
|
526
|
+
"""
|
|
504
527
|
self.m3u8_url = m3u8_url
|
|
505
528
|
self.path_manager = PathManager(m3u8_url, output_path)
|
|
506
|
-
self.
|
|
529
|
+
self.custom_headers = headers
|
|
530
|
+
self.client = HLSClient(custom_headers=self.custom_headers)
|
|
507
531
|
self.m3u8_manager = M3U8Manager(m3u8_url, self.client)
|
|
508
532
|
self.download_manager: Optional[DownloadManager] = None
|
|
509
533
|
self.merge_manager: Optional[MergeManager] = None
|
|
@@ -536,9 +560,6 @@ class HLS_Downloader:
|
|
|
536
560
|
|
|
537
561
|
console.print("[cyan]You can safely stop the download with [bold]Ctrl+c[bold] [cyan]")
|
|
538
562
|
|
|
539
|
-
if TELEGRAM_BOT:
|
|
540
|
-
bot = get_bot_instance()
|
|
541
|
-
|
|
542
563
|
try:
|
|
543
564
|
if os.path.exists(self.path_manager.output_path):
|
|
544
565
|
console.print(f"[red]Output file {self.path_manager.output_path} already exists![/red]")
|
|
@@ -550,8 +571,6 @@ class HLS_Downloader:
|
|
|
550
571
|
'error': None,
|
|
551
572
|
'stopped': False
|
|
552
573
|
}
|
|
553
|
-
if TELEGRAM_BOT:
|
|
554
|
-
bot.send_message("Contenuto già scaricato!", None)
|
|
555
574
|
return response
|
|
556
575
|
|
|
557
576
|
self.path_manager.setup_directories()
|
|
@@ -559,12 +578,16 @@ class HLS_Downloader:
|
|
|
559
578
|
# Parse M3U8 and determine if it's a master playlist
|
|
560
579
|
self.m3u8_manager.parse()
|
|
561
580
|
self.m3u8_manager.select_streams()
|
|
562
|
-
|
|
581
|
+
|
|
582
|
+
if self.m3u8_manager.is_master:
|
|
583
|
+
logging.info("Detected media playlist (not master)")
|
|
584
|
+
self.m3u8_manager.log_selection()
|
|
563
585
|
|
|
564
586
|
self.download_manager = DownloadManager(
|
|
565
587
|
temp_dir=self.path_manager.temp_dir,
|
|
566
588
|
client=self.client,
|
|
567
|
-
url_fixer=self.m3u8_manager.url_fixer
|
|
589
|
+
url_fixer=self.m3u8_manager.url_fixer,
|
|
590
|
+
custom_headers=self.custom_headers
|
|
568
591
|
)
|
|
569
592
|
|
|
570
593
|
# Check if download had critical failures
|
|
@@ -639,9 +662,6 @@ class HLS_Downloader:
|
|
|
639
662
|
|
|
640
663
|
def _print_summary(self, use_shortest: bool):
|
|
641
664
|
"""Prints download summary including file size, duration, and any missing segments."""
|
|
642
|
-
if TELEGRAM_BOT:
|
|
643
|
-
bot = get_bot_instance()
|
|
644
|
-
|
|
645
665
|
missing_ts = False
|
|
646
666
|
missing_info = ""
|
|
647
667
|
for item in self.download_manager.missing_segments:
|
|
@@ -658,11 +678,6 @@ class HLS_Downloader:
|
|
|
658
678
|
f"[cyan]Output: [bold]{os.path.abspath(self.path_manager.output_path)}[/bold]"
|
|
659
679
|
)
|
|
660
680
|
|
|
661
|
-
if TELEGRAM_BOT:
|
|
662
|
-
message = f"Download completato\nDimensione: {file_size}\nDurata: {duration}\nPercorso: {os.path.abspath(self.path_manager.output_path)}"
|
|
663
|
-
clean_message = re.sub(r'\[[a-zA-Z]+\]', '', message)
|
|
664
|
-
bot.send_message(clean_message, None)
|
|
665
|
-
|
|
666
681
|
if missing_ts:
|
|
667
682
|
panel_content += f"\n{missing_info}"
|
|
668
683
|
|
|
@@ -11,7 +11,7 @@ import threading
|
|
|
11
11
|
from queue import PriorityQueue
|
|
12
12
|
from urllib.parse import urljoin, urlparse
|
|
13
13
|
from concurrent.futures import ThreadPoolExecutor, as_completed
|
|
14
|
-
from typing import Dict
|
|
14
|
+
from typing import Dict, Optional
|
|
15
15
|
|
|
16
16
|
|
|
17
17
|
# External libraries
|
|
@@ -51,7 +51,7 @@ console = Console()
|
|
|
51
51
|
|
|
52
52
|
|
|
53
53
|
class M3U8_Segments:
|
|
54
|
-
def __init__(self, url: str, tmp_folder: str, is_index_url: bool = True):
|
|
54
|
+
def __init__(self, url: str, tmp_folder: str, is_index_url: bool = True, limit_segments: int = None, custom_headers: Optional[Dict[str, str]] = None):
|
|
55
55
|
"""
|
|
56
56
|
Initializes the M3U8_Segments object.
|
|
57
57
|
|
|
@@ -59,10 +59,14 @@ class M3U8_Segments:
|
|
|
59
59
|
- url (str): The URL of the M3U8 playlist.
|
|
60
60
|
- tmp_folder (str): The temporary folder to store downloaded segments.
|
|
61
61
|
- is_index_url (bool): Flag indicating if `m3u8_index` is a URL (default True).
|
|
62
|
+
- limit_segments (int): Optional limit for number of segments to process.
|
|
63
|
+
- custom_headers (Dict[str, str]): Optional custom headers to use for all requests.
|
|
62
64
|
"""
|
|
63
65
|
self.url = url
|
|
64
66
|
self.tmp_folder = tmp_folder
|
|
65
67
|
self.is_index_url = is_index_url
|
|
68
|
+
self.limit_segments = limit_segments
|
|
69
|
+
self.custom_headers = custom_headers if custom_headers else {'User-Agent': get_userAgent()}
|
|
66
70
|
self.expected_real_time = None
|
|
67
71
|
self.tmp_file_path = os.path.join(self.tmp_folder, "0.ts")
|
|
68
72
|
os.makedirs(self.tmp_folder, exist_ok=True)
|
|
@@ -103,7 +107,7 @@ class M3U8_Segments:
|
|
|
103
107
|
self.active_retries_lock = threading.Lock()
|
|
104
108
|
|
|
105
109
|
self._last_progress_update = 0
|
|
106
|
-
self._progress_update_interval = 0.
|
|
110
|
+
self._progress_update_interval = 0.1
|
|
107
111
|
|
|
108
112
|
def __get_key__(self, m3u8_parser: M3U8_Parser) -> bytes:
|
|
109
113
|
"""
|
|
@@ -120,7 +124,11 @@ class M3U8_Segments:
|
|
|
120
124
|
self.key_base_url = f"{parsed_url.scheme}://{parsed_url.netloc}/"
|
|
121
125
|
|
|
122
126
|
try:
|
|
123
|
-
client_params = {
|
|
127
|
+
client_params = {
|
|
128
|
+
'headers': self.custom_headers,
|
|
129
|
+
'timeout': MAX_TIMEOOUT,
|
|
130
|
+
'verify': REQUEST_VERIFY
|
|
131
|
+
}
|
|
124
132
|
response = httpx.get(url=key_uri, **client_params)
|
|
125
133
|
response.raise_for_status()
|
|
126
134
|
|
|
@@ -141,21 +149,31 @@ class M3U8_Segments:
|
|
|
141
149
|
m3u8_parser.parse_data(uri=self.url, raw_content=m3u8_content)
|
|
142
150
|
|
|
143
151
|
self.expected_real_time_s = m3u8_parser.duration
|
|
152
|
+
self.segment_init_url = m3u8_parser.init_segment
|
|
153
|
+
self.has_init_segment = self.segment_init_url is not None
|
|
144
154
|
|
|
145
155
|
if m3u8_parser.keys:
|
|
146
156
|
key = self.__get_key__(m3u8_parser)
|
|
147
|
-
self.decryption = M3U8_Decryption(
|
|
148
|
-
key,
|
|
149
|
-
m3u8_parser.keys.get('iv'),
|
|
150
|
-
m3u8_parser.keys.get('method')
|
|
151
|
-
)
|
|
157
|
+
self.decryption = M3U8_Decryption(key, m3u8_parser.keys.get('iv'), m3u8_parser.keys.get('method'))
|
|
152
158
|
|
|
153
|
-
|
|
159
|
+
segments = [
|
|
154
160
|
self.class_url_fixer.generate_full_url(seg)
|
|
155
161
|
if "http" not in seg else seg
|
|
156
162
|
for seg in m3u8_parser.segments
|
|
157
163
|
]
|
|
164
|
+
|
|
165
|
+
if self.limit_segments and len(segments) > self.limit_segments:
|
|
166
|
+
logging.info(f"Limiting segments from {len(segments)} to {self.limit_segments}")
|
|
167
|
+
segments = segments[:self.limit_segments]
|
|
168
|
+
|
|
169
|
+
self.segments = segments
|
|
158
170
|
self.class_ts_estimator.total_segments = len(self.segments)
|
|
171
|
+
|
|
172
|
+
def get_segments_count(self) -> int:
|
|
173
|
+
"""
|
|
174
|
+
Returns the total number of segments.
|
|
175
|
+
"""
|
|
176
|
+
return len(self.segments) if hasattr(self, 'segments') else 0
|
|
159
177
|
|
|
160
178
|
def get_info(self) -> None:
|
|
161
179
|
"""
|
|
@@ -163,7 +181,11 @@ class M3U8_Segments:
|
|
|
163
181
|
"""
|
|
164
182
|
if self.is_index_url:
|
|
165
183
|
try:
|
|
166
|
-
client_params = {
|
|
184
|
+
client_params = {
|
|
185
|
+
'headers': self.custom_headers,
|
|
186
|
+
'timeout': MAX_TIMEOOUT,
|
|
187
|
+
'verify': REQUEST_VERIFY
|
|
188
|
+
}
|
|
167
189
|
response = httpx.get(self.url, **client_params, follow_redirects=True)
|
|
168
190
|
response.raise_for_status()
|
|
169
191
|
|
|
@@ -205,11 +227,12 @@ class M3U8_Segments:
|
|
|
205
227
|
def _get_http_client(self):
|
|
206
228
|
"""
|
|
207
229
|
Get a reusable HTTP client using the centralized factory.
|
|
208
|
-
Uses optimized settings for segment downloading.
|
|
230
|
+
Uses optimized settings for segment downloading with custom headers.
|
|
209
231
|
"""
|
|
210
232
|
if self._client is None:
|
|
211
233
|
with self._client_lock:
|
|
212
234
|
self._client = create_client(
|
|
235
|
+
headers=self.custom_headers,
|
|
213
236
|
timeout=SEGMENT_MAX_TIMEOUT
|
|
214
237
|
)
|
|
215
238
|
|
|
@@ -242,8 +265,8 @@ class M3U8_Segments:
|
|
|
242
265
|
client = self._get_http_client()
|
|
243
266
|
timeout = min(SEGMENT_MAX_TIMEOUT, 10 + attempt * 5)
|
|
244
267
|
|
|
245
|
-
# Make request
|
|
246
|
-
response = client.get(ts_url, timeout=timeout, headers=
|
|
268
|
+
# Make request with custom headers
|
|
269
|
+
response = client.get(ts_url, timeout=timeout, headers=self.custom_headers)
|
|
247
270
|
response.raise_for_status()
|
|
248
271
|
segment_content = response.content
|
|
249
272
|
content_size = len(segment_content)
|
|
@@ -294,7 +317,7 @@ class M3U8_Segments:
|
|
|
294
317
|
self.info_nRetry += 1
|
|
295
318
|
|
|
296
319
|
if attempt + 1 == REQUEST_MAX_RETRY:
|
|
297
|
-
console.print(f"[red]Final retry failed for segment: {index}")
|
|
320
|
+
console.print(f" -- [red]Final retry failed for segment: {index}")
|
|
298
321
|
|
|
299
322
|
try:
|
|
300
323
|
self.queue.put((index, None), timeout=0.1)
|
|
@@ -363,6 +386,52 @@ class M3U8_Segments:
|
|
|
363
386
|
except Exception as e:
|
|
364
387
|
logging.error(f"Error writing segment {index}: {str(e)}")
|
|
365
388
|
|
|
389
|
+
def download_init_segment(self) -> bool:
|
|
390
|
+
"""
|
|
391
|
+
Downloads the initialization segment if available.
|
|
392
|
+
|
|
393
|
+
Returns:
|
|
394
|
+
bool: True if init segment was downloaded successfully, False otherwise
|
|
395
|
+
"""
|
|
396
|
+
if not self.has_init_segment:
|
|
397
|
+
return False
|
|
398
|
+
|
|
399
|
+
init_url = self.segment_init_url
|
|
400
|
+
if not init_url.startswith("http"):
|
|
401
|
+
init_url = self.class_url_fixer.generate_full_url(init_url)
|
|
402
|
+
|
|
403
|
+
try:
|
|
404
|
+
client = self._get_http_client()
|
|
405
|
+
response = client.get(
|
|
406
|
+
init_url,
|
|
407
|
+
timeout=SEGMENT_MAX_TIMEOUT,
|
|
408
|
+
headers=self.custom_headers
|
|
409
|
+
)
|
|
410
|
+
response.raise_for_status()
|
|
411
|
+
init_content = response.content
|
|
412
|
+
|
|
413
|
+
# Decrypt if needed (although init segments are typically not encrypted)
|
|
414
|
+
if self.decryption is not None:
|
|
415
|
+
try:
|
|
416
|
+
init_content = self.decryption.decrypt(init_content)
|
|
417
|
+
|
|
418
|
+
except Exception as e:
|
|
419
|
+
logging.error(f"Decryption failed for init segment: {str(e)}")
|
|
420
|
+
return False
|
|
421
|
+
|
|
422
|
+
# Put init segment in queue with highest priority (0)
|
|
423
|
+
self.queue.put((0, init_content))
|
|
424
|
+
self.downloaded_segments.add(0)
|
|
425
|
+
|
|
426
|
+
# Adjust expected_index to 1 since we've handled index 0 separately
|
|
427
|
+
self.expected_index = 0
|
|
428
|
+
logging.info("Init segment downloaded successfully")
|
|
429
|
+
return True
|
|
430
|
+
|
|
431
|
+
except Exception as e:
|
|
432
|
+
logging.error(f"Failed to download init segment: {str(e)}")
|
|
433
|
+
return False
|
|
434
|
+
|
|
366
435
|
def download_streams(self, description: str, type: str):
|
|
367
436
|
"""
|
|
368
437
|
Downloads all TS segments in parallel and writes them to a file.
|
|
@@ -378,33 +447,42 @@ class M3U8_Segments:
|
|
|
378
447
|
self.setup_interrupt_handler()
|
|
379
448
|
|
|
380
449
|
progress_bar = tqdm(
|
|
381
|
-
total=len(self.segments),
|
|
382
|
-
unit='s',
|
|
383
|
-
ascii='░▒█',
|
|
450
|
+
total=len(self.segments) + (1 if self.has_init_segment else 0),
|
|
384
451
|
bar_format=self._get_bar_format(description),
|
|
385
|
-
mininterval=2.0,
|
|
386
|
-
maxinterval=5.0,
|
|
387
452
|
file=sys.stdout,
|
|
388
453
|
)
|
|
389
454
|
|
|
390
455
|
try:
|
|
456
|
+
self.class_ts_estimator.total_segments = len(self.segments)
|
|
457
|
+
|
|
391
458
|
writer_thread = threading.Thread(target=self.write_segments_to_file)
|
|
392
459
|
writer_thread.daemon = True
|
|
393
460
|
writer_thread.start()
|
|
394
461
|
max_workers = self._get_worker_count(type)
|
|
395
462
|
|
|
463
|
+
# First download the init segment if available
|
|
464
|
+
if self.has_init_segment:
|
|
465
|
+
if self.download_init_segment():
|
|
466
|
+
progress_bar.update(1)
|
|
467
|
+
|
|
396
468
|
with ThreadPoolExecutor(max_workers=max_workers) as executor:
|
|
397
469
|
futures = []
|
|
398
470
|
|
|
471
|
+
# Start segment indices from 1 if we have an init segment
|
|
472
|
+
start_idx = 1 if self.has_init_segment else 0
|
|
473
|
+
|
|
399
474
|
for index, segment_url in enumerate(self.segments):
|
|
400
475
|
if self.interrupt_flag.is_set():
|
|
401
476
|
break
|
|
402
477
|
|
|
478
|
+
# Adjust index if we have an init segment
|
|
479
|
+
queue_index = index + start_idx
|
|
480
|
+
|
|
403
481
|
# Delay every 200 submissions to reduce CPU usage
|
|
404
482
|
if index % 200 == 0 and index > 0:
|
|
405
483
|
time.sleep(TQDM_DELAY_WORKER)
|
|
406
484
|
|
|
407
|
-
futures.append(executor.submit(self.download_segment, segment_url,
|
|
485
|
+
futures.append(executor.submit(self.download_segment, segment_url, queue_index, progress_bar))
|
|
408
486
|
|
|
409
487
|
# Process completed futures
|
|
410
488
|
for future in as_completed(futures):
|
|
@@ -449,15 +527,17 @@ class M3U8_Segments:
|
|
|
449
527
|
|
|
450
528
|
return self._generate_results(type)
|
|
451
529
|
|
|
530
|
+
|
|
452
531
|
def _get_bar_format(self, description: str) -> str:
|
|
453
532
|
"""
|
|
454
533
|
Generate platform-appropriate progress bar format.
|
|
455
534
|
"""
|
|
456
535
|
return (
|
|
457
|
-
f"{Colors.YELLOW}[HLS]
|
|
458
|
-
f"{Colors.
|
|
459
|
-
f"{Colors.
|
|
460
|
-
f"{Colors.YELLOW}{{elapsed}}{Colors.WHITE} < {Colors.CYAN}{{remaining}}{Colors.
|
|
536
|
+
f"{Colors.YELLOW}[HLS]{Colors.CYAN} {description}{Colors.WHITE}: "
|
|
537
|
+
f"{Colors.MAGENTA}{{bar:40}} "
|
|
538
|
+
f"{Colors.LIGHT_GREEN}{{n_fmt}}{Colors.WHITE}/{Colors.CYAN}{{total_fmt}} {Colors.LIGHT_MAGENTA}TS {Colors.WHITE}"
|
|
539
|
+
f"{Colors.DARK_GRAY}[{Colors.YELLOW}{{elapsed}}{Colors.WHITE} < {Colors.CYAN}{{remaining}}{Colors.DARK_GRAY}] "
|
|
540
|
+
f"{Colors.WHITE}{{postfix}}"
|
|
461
541
|
)
|
|
462
542
|
|
|
463
543
|
def _get_worker_count(self, stream_type: str) -> int:
|
|
@@ -102,7 +102,7 @@ def MP4_downloader(url: str, path: str, referer: str = None, headers_: dict = No
|
|
|
102
102
|
else:
|
|
103
103
|
headers['User-Agent'] = get_userAgent()
|
|
104
104
|
|
|
105
|
-
# Set interrupt handler (only in main thread)
|
|
105
|
+
# Set interrupt handler (only in main thread)
|
|
106
106
|
temp_path = f"{path}.temp"
|
|
107
107
|
interrupt_handler = InterruptHandler()
|
|
108
108
|
original_handler = None
|
|
@@ -117,14 +117,12 @@ def MP4_downloader(url: str, path: str, referer: str = None, headers_: dict = No
|
|
|
117
117
|
),
|
|
118
118
|
)
|
|
119
119
|
except Exception:
|
|
120
|
-
# If setting signal handler fails (non-main thread), continue without it
|
|
121
120
|
original_handler = None
|
|
122
121
|
|
|
123
122
|
# Ensure the output directory exists
|
|
124
123
|
os.makedirs(os.path.dirname(path), exist_ok=True)
|
|
125
124
|
|
|
126
125
|
try:
|
|
127
|
-
# Use unified HTTP client (verify/timeout/proxy from config)
|
|
128
126
|
with create_client() as client:
|
|
129
127
|
with client.stream("GET", url, headers=headers) as response:
|
|
130
128
|
response.raise_for_status()
|
|
@@ -134,20 +132,20 @@ def MP4_downloader(url: str, path: str, referer: str = None, headers_: dict = No
|
|
|
134
132
|
console.print("[bold red]No video stream found.[/bold red]")
|
|
135
133
|
return None, False
|
|
136
134
|
|
|
137
|
-
# Create
|
|
135
|
+
# Create progress bar with percentage instead of n_fmt/total_fmt
|
|
136
|
+
console.print("[cyan]You can safely stop the download with [bold]Ctrl+c[bold] [cyan]")
|
|
138
137
|
progress_bar = tqdm(
|
|
139
138
|
total=total,
|
|
140
139
|
ascii='░▒█',
|
|
141
|
-
bar_format=f"{Colors.YELLOW}[MP4]{Colors.WHITE}: "
|
|
142
|
-
f"{Colors.RED}{{percentage:.
|
|
143
|
-
f"{Colors.YELLOW}{{
|
|
144
|
-
f"{Colors.
|
|
145
|
-
|
|
146
|
-
unit='iB',
|
|
140
|
+
bar_format=f"{Colors.YELLOW}[MP4]{Colors.CYAN} Downloading{Colors.WHITE}: "
|
|
141
|
+
f"{Colors.RED}{{percentage:.1f}}% {Colors.MAGENTA}{{bar:40}} {Colors.WHITE}"
|
|
142
|
+
f"{Colors.DARK_GRAY}[{Colors.YELLOW}{{elapsed}}{Colors.WHITE} < {Colors.CYAN}{{remaining}}{Colors.DARK_GRAY}] "
|
|
143
|
+
f"{Colors.LIGHT_CYAN}{{rate_fmt}}",
|
|
144
|
+
unit='B',
|
|
147
145
|
unit_scale=True,
|
|
148
|
-
|
|
146
|
+
unit_divisor=1024,
|
|
149
147
|
mininterval=0.05,
|
|
150
|
-
file=sys.stdout
|
|
148
|
+
file=sys.stdout
|
|
151
149
|
)
|
|
152
150
|
|
|
153
151
|
downloaded = 0
|
|
@@ -171,6 +169,7 @@ def MP4_downloader(url: str, path: str, referer: str = None, headers_: dict = No
|
|
|
171
169
|
os.rename(temp_path, path)
|
|
172
170
|
|
|
173
171
|
if os.path.exists(path):
|
|
172
|
+
print("")
|
|
174
173
|
console.print(Panel(
|
|
175
174
|
f"[bold green]Download completed{' (Partial)' if interrupt_handler.force_quit else ''}![/bold green]\n"
|
|
176
175
|
f"[cyan]File size: [bold red]{internet_manager.format_file_size(os.path.getsize(path))}[/bold red]\n"
|
|
@@ -202,4 +201,4 @@ def MP4_downloader(url: str, path: str, referer: str = None, headers_: dict = No
|
|
|
202
201
|
try:
|
|
203
202
|
signal.signal(signal.SIGINT, original_handler)
|
|
204
203
|
except Exception:
|
|
205
|
-
pass
|
|
204
|
+
pass
|