StreamingCommunity 3.3.5__py3-none-any.whl → 3.3.6__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/__init__.py +17 -18
- StreamingCommunity/Api/Site/altadefinizione/series.py +4 -0
- StreamingCommunity/Api/Site/animeunity/__init__.py +14 -15
- StreamingCommunity/Api/Site/animeunity/serie.py +1 -1
- StreamingCommunity/Api/Site/animeworld/__init__.py +15 -13
- StreamingCommunity/Api/Site/animeworld/serie.py +1 -1
- StreamingCommunity/Api/Site/crunchyroll/__init__.py +16 -17
- StreamingCommunity/Api/Site/crunchyroll/series.py +6 -1
- StreamingCommunity/Api/Site/guardaserie/__init__.py +17 -19
- StreamingCommunity/Api/Site/guardaserie/series.py +4 -0
- StreamingCommunity/Api/Site/guardaserie/site.py +2 -7
- StreamingCommunity/Api/Site/mediasetinfinity/__init__.py +15 -15
- StreamingCommunity/Api/Site/mediasetinfinity/series.py +4 -0
- StreamingCommunity/Api/Site/mediasetinfinity/site.py +12 -2
- StreamingCommunity/Api/Site/mediasetinfinity/util/ScrapeSerie.py +67 -98
- StreamingCommunity/Api/Site/raiplay/__init__.py +15 -15
- StreamingCommunity/Api/Site/raiplay/series.py +5 -1
- StreamingCommunity/Api/Site/streamingcommunity/__init__.py +16 -14
- StreamingCommunity/Api/Site/streamingwatch/__init__.py +12 -12
- StreamingCommunity/Api/Site/streamingwatch/series.py +4 -0
- StreamingCommunity/Api/Template/Class/SearchType.py +0 -1
- StreamingCommunity/Api/Template/Util/manage_ep.py +1 -11
- StreamingCommunity/Api/Template/site.py +2 -3
- StreamingCommunity/Lib/Downloader/DASH/downloader.py +55 -17
- StreamingCommunity/Lib/Downloader/DASH/segments.py +73 -17
- StreamingCommunity/Lib/Downloader/HLS/downloader.py +282 -152
- StreamingCommunity/Lib/Downloader/HLS/segments.py +1 -5
- StreamingCommunity/Lib/FFmpeg/capture.py +1 -1
- StreamingCommunity/Lib/FFmpeg/command.py +6 -6
- StreamingCommunity/Lib/FFmpeg/util.py +11 -30
- StreamingCommunity/Lib/M3U8/estimator.py +27 -13
- StreamingCommunity/Upload/update.py +2 -2
- StreamingCommunity/Upload/version.py +1 -1
- StreamingCommunity/Util/installer/__init__.py +11 -0
- StreamingCommunity/Util/installer/device_install.py +1 -1
- StreamingCommunity/Util/os.py +2 -6
- StreamingCommunity/Util/table.py +40 -8
- StreamingCommunity/run.py +15 -8
- {streamingcommunity-3.3.5.dist-info → streamingcommunity-3.3.6.dist-info}/METADATA +38 -51
- {streamingcommunity-3.3.5.dist-info → streamingcommunity-3.3.6.dist-info}/RECORD +44 -43
- {streamingcommunity-3.3.5.dist-info → streamingcommunity-3.3.6.dist-info}/WHEEL +0 -0
- {streamingcommunity-3.3.5.dist-info → streamingcommunity-3.3.6.dist-info}/entry_points.txt +0 -0
- {streamingcommunity-3.3.5.dist-info → streamingcommunity-3.3.6.dist-info}/licenses/LICENSE +0 -0
- {streamingcommunity-3.3.5.dist-info → streamingcommunity-3.3.6.dist-info}/top_level.txt +0 -0
|
@@ -12,6 +12,7 @@ from typing import Any, Dict, List, Optional
|
|
|
12
12
|
import httpx
|
|
13
13
|
from rich.console import Console
|
|
14
14
|
from rich.panel import Panel
|
|
15
|
+
from rich.table import Table
|
|
15
16
|
|
|
16
17
|
|
|
17
18
|
# Internal utilities
|
|
@@ -64,6 +65,11 @@ class HLSClient:
|
|
|
64
65
|
Returns:
|
|
65
66
|
Response content/text or None if all retries fail
|
|
66
67
|
"""
|
|
68
|
+
# Check if URL is None or empty
|
|
69
|
+
if not url:
|
|
70
|
+
logging.error("URL is None or empty, cannot make request")
|
|
71
|
+
return None
|
|
72
|
+
|
|
67
73
|
client = create_client(headers=self.headers)
|
|
68
74
|
|
|
69
75
|
for attempt in range(RETRY_LIMIT):
|
|
@@ -73,8 +79,11 @@ class HLSClient:
|
|
|
73
79
|
return response.content if return_content else response.text
|
|
74
80
|
|
|
75
81
|
except Exception as e:
|
|
76
|
-
logging.error(f"Attempt {attempt+1} failed: {str(e)}")
|
|
77
|
-
|
|
82
|
+
logging.error(f"Attempt {attempt+1} failed for URL {url}: {str(e)}")
|
|
83
|
+
if attempt < RETRY_LIMIT - 1: # Don't sleep on last attempt
|
|
84
|
+
time.sleep(1.5 ** attempt)
|
|
85
|
+
|
|
86
|
+
logging.error(f"All {RETRY_LIMIT} attempts failed for URL: {url}")
|
|
78
87
|
return None
|
|
79
88
|
|
|
80
89
|
|
|
@@ -96,6 +105,9 @@ class PathManager:
|
|
|
96
105
|
Ensures output path is valid and follows expected format.
|
|
97
106
|
Creates a hash-based filename if no path is provided.
|
|
98
107
|
"""
|
|
108
|
+
if not path:
|
|
109
|
+
path = "download.mp4"
|
|
110
|
+
|
|
99
111
|
if not path.endswith(".mp4"):
|
|
100
112
|
path += ".mp4"
|
|
101
113
|
|
|
@@ -132,18 +144,28 @@ class M3U8Manager:
|
|
|
132
144
|
self.sub_streams = []
|
|
133
145
|
self.is_master = False
|
|
134
146
|
|
|
135
|
-
def parse(self):
|
|
147
|
+
def parse(self) -> bool:
|
|
136
148
|
"""
|
|
137
149
|
Fetches and parses the M3U8 playlist content.
|
|
138
150
|
Determines if it's a master playlist (index) or media playlist.
|
|
151
|
+
|
|
152
|
+
Returns:
|
|
153
|
+
bool: True if parsing was successful, False otherwise
|
|
139
154
|
"""
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
155
|
+
try:
|
|
156
|
+
content = self.client.request(self.m3u8_url)
|
|
157
|
+
if not content:
|
|
158
|
+
logging.error(f"Failed to fetch M3U8 content from {self.m3u8_url}")
|
|
159
|
+
return False
|
|
160
|
+
|
|
161
|
+
self.parser.parse_data(uri=self.m3u8_url, raw_content=content)
|
|
162
|
+
self.url_fixer.set_playlist(self.m3u8_url)
|
|
163
|
+
self.is_master = self.parser.is_master_playlist
|
|
164
|
+
return True
|
|
165
|
+
|
|
166
|
+
except Exception as e:
|
|
167
|
+
logging.error(f"Error parsing M3U8 from {self.m3u8_url}: {str(e)}")
|
|
168
|
+
return False
|
|
147
169
|
|
|
148
170
|
def select_streams(self):
|
|
149
171
|
"""
|
|
@@ -177,7 +199,6 @@ class M3U8Manager:
|
|
|
177
199
|
if ENABLE_SUBTITLE:
|
|
178
200
|
if "*" in DOWNLOAD_SPECIFIC_SUBTITLE:
|
|
179
201
|
self.sub_streams = self.parser._subtitle.get_all_uris_and_names() or []
|
|
180
|
-
|
|
181
202
|
else:
|
|
182
203
|
self.sub_streams = [
|
|
183
204
|
s for s in (self.parser._subtitle.get_all_uris_and_names() or [])
|
|
@@ -185,55 +206,82 @@ class M3U8Manager:
|
|
|
185
206
|
]
|
|
186
207
|
|
|
187
208
|
def log_selection(self):
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
f"
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
f"[
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
)
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
)
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
)
|
|
209
|
+
"""Log the stream selection information in a formatted table."""
|
|
210
|
+
def calculate_column_widths():
|
|
211
|
+
data_rows = []
|
|
212
|
+
|
|
213
|
+
# Video information
|
|
214
|
+
tuple_available_resolution = self.parser._video.get_list_resolution() or []
|
|
215
|
+
list_available_resolution = [f"{r[0]}x{r[1]}" for r in tuple_available_resolution]
|
|
216
|
+
available_video = ', '.join(list_available_resolution) if list_available_resolution else "Nothing"
|
|
217
|
+
|
|
218
|
+
downloadable_video = "Nothing"
|
|
219
|
+
if isinstance(self.video_res, tuple) and len(self.video_res) >= 2:
|
|
220
|
+
downloadable_video = f"{self.video_res[0]}x{self.video_res[1]}"
|
|
221
|
+
elif self.video_res and self.video_res != "undefined":
|
|
222
|
+
downloadable_video = str(self.video_res)
|
|
223
|
+
|
|
224
|
+
data_rows.append(["Video", available_video, str(FILTER_CUSTOM_RESOLUTION), downloadable_video])
|
|
225
|
+
|
|
226
|
+
# Codec information
|
|
227
|
+
if self.parser.codec is not None:
|
|
228
|
+
available_codec_info = (
|
|
229
|
+
f"v: {self.parser.codec.video_codec_name} "
|
|
230
|
+
f"(b: {self.parser.codec.video_bitrate // 1000}k), "
|
|
231
|
+
f"a: {self.parser.codec.audio_codec_name} "
|
|
232
|
+
f"(b: {self.parser.codec.audio_bitrate // 1000}k)"
|
|
233
|
+
)
|
|
234
|
+
set_codec_info = available_codec_info if config_manager.get_bool("M3U8_CONVERSION", "use_codec") else "copy"
|
|
235
|
+
|
|
236
|
+
data_rows.append(["Codec", available_codec_info, set_codec_info, set_codec_info])
|
|
237
|
+
|
|
238
|
+
# Subtitle information
|
|
239
|
+
available_subtitles = self.parser._subtitle.get_all_uris_and_names() or []
|
|
240
|
+
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
|
+
|
|
243
|
+
downloadable_sub_languages = available_sub_languages if "*" in DOWNLOAD_SPECIFIC_SUBTITLE else list(set(available_sub_languages) & set(DOWNLOAD_SPECIFIC_SUBTITLE))
|
|
244
|
+
downloadable_subs = ', '.join(downloadable_sub_languages) if downloadable_sub_languages else "Nothing"
|
|
245
|
+
|
|
246
|
+
data_rows.append(["Subtitle", available_subs, ', '.join(DOWNLOAD_SPECIFIC_SUBTITLE), downloadable_subs])
|
|
247
|
+
|
|
248
|
+
# Audio information
|
|
249
|
+
available_audio = self.parser._audio.get_all_uris_and_names() or []
|
|
250
|
+
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
|
+
|
|
253
|
+
downloadable_audio_languages = list(set(available_audio_languages) & set(DOWNLOAD_SPECIFIC_AUDIO))
|
|
254
|
+
downloadable_audios = ', '.join(downloadable_audio_languages) if downloadable_audio_languages else "Nothing"
|
|
255
|
+
|
|
256
|
+
data_rows.append(["Audio", available_audios, ', '.join(DOWNLOAD_SPECIFIC_AUDIO), downloadable_audios])
|
|
257
|
+
|
|
258
|
+
# Calculate max width for each column
|
|
259
|
+
headers = ["Type", "Available", "Set", "Downloadable"]
|
|
260
|
+
max_widths = [len(header) for header in headers]
|
|
261
|
+
|
|
262
|
+
for row in data_rows:
|
|
263
|
+
for i, cell in enumerate(row):
|
|
264
|
+
max_widths[i] = max(max_widths[i], len(str(cell)))
|
|
265
|
+
|
|
266
|
+
# Add some padding
|
|
267
|
+
max_widths = [w + 2 for w in max_widths]
|
|
268
|
+
|
|
269
|
+
return data_rows, max_widths
|
|
270
|
+
|
|
271
|
+
data_rows, column_widths = calculate_column_widths()
|
|
272
|
+
|
|
273
|
+
table = Table(show_header=True, header_style="bold cyan", border_style="blue")
|
|
274
|
+
table.add_column("Type", style="cyan bold", width=column_widths[0])
|
|
275
|
+
table.add_column("Available", style="green", width=column_widths[1])
|
|
276
|
+
table.add_column("Set", style="red", width=column_widths[2])
|
|
277
|
+
table.add_column("Downloadable", style="yellow", width=column_widths[3])
|
|
278
|
+
|
|
279
|
+
for row in data_rows:
|
|
280
|
+
table.add_row(*row)
|
|
281
|
+
|
|
282
|
+
console.print(table)
|
|
234
283
|
print("")
|
|
235
|
-
|
|
236
|
-
|
|
284
|
+
|
|
237
285
|
class DownloadManager:
|
|
238
286
|
"""Manages downloading of video, audio, and subtitle streams."""
|
|
239
287
|
def __init__(self, temp_dir: str, client: HLSClient, url_fixer: M3U8_UrlFix):
|
|
@@ -249,88 +297,124 @@ class DownloadManager:
|
|
|
249
297
|
self.missing_segments = []
|
|
250
298
|
self.stopped = False
|
|
251
299
|
|
|
252
|
-
def download_video(self, video_url: str):
|
|
253
|
-
"""
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
self.stopped = True
|
|
263
|
-
|
|
264
|
-
return self.stopped
|
|
265
|
-
|
|
266
|
-
def download_audio(self, audio: Dict):
|
|
267
|
-
"""Downloads audio segments for a specific language track."""
|
|
268
|
-
#if self.stopped:
|
|
269
|
-
# return True
|
|
270
|
-
|
|
271
|
-
audio_full_url = self.url_fixer.generate_full_url(audio['uri'])
|
|
272
|
-
audio_tmp_dir = os.path.join(self.temp_dir, 'audio', audio['language'])
|
|
273
|
-
|
|
274
|
-
downloader = M3U8_Segments(url=audio_full_url, tmp_folder=audio_tmp_dir)
|
|
275
|
-
result = downloader.download_streams(f"Audio {audio['language']}", "audio")
|
|
276
|
-
self.missing_segments.append(result)
|
|
277
|
-
|
|
278
|
-
if result.get('stopped', False):
|
|
279
|
-
self.stopped = True
|
|
280
|
-
return self.stopped
|
|
300
|
+
def download_video(self, video_url: str) -> bool:
|
|
301
|
+
"""
|
|
302
|
+
Downloads video segments from the M3U8 playlist.
|
|
303
|
+
|
|
304
|
+
Returns:
|
|
305
|
+
bool: True if download was successful, False otherwise
|
|
306
|
+
"""
|
|
307
|
+
try:
|
|
308
|
+
video_full_url = self.url_fixer.generate_full_url(video_url)
|
|
309
|
+
video_tmp_dir = os.path.join(self.temp_dir, 'video')
|
|
281
310
|
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
# return True
|
|
311
|
+
downloader = M3U8_Segments(url=video_full_url, tmp_folder=video_tmp_dir)
|
|
312
|
+
result = downloader.download_streams("Video", "video")
|
|
313
|
+
self.missing_segments.append(result)
|
|
286
314
|
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
315
|
+
if result.get('stopped', False):
|
|
316
|
+
self.stopped = True
|
|
317
|
+
return False
|
|
290
318
|
|
|
291
|
-
|
|
292
|
-
|
|
319
|
+
return True
|
|
320
|
+
|
|
321
|
+
except Exception as e:
|
|
322
|
+
logging.error(f"Error downloading video from {video_url}: {str(e)}")
|
|
323
|
+
return False
|
|
293
324
|
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
325
|
+
def download_audio(self, audio: Dict) -> bool:
|
|
326
|
+
"""
|
|
327
|
+
Downloads audio segments for a specific language track.
|
|
328
|
+
|
|
329
|
+
Returns:
|
|
330
|
+
bool: True if download was successful, False otherwise
|
|
331
|
+
"""
|
|
332
|
+
try:
|
|
333
|
+
audio_full_url = self.url_fixer.generate_full_url(audio['uri'])
|
|
334
|
+
audio_tmp_dir = os.path.join(self.temp_dir, 'audio', audio['language'])
|
|
335
|
+
|
|
336
|
+
downloader = M3U8_Segments(url=audio_full_url, tmp_folder=audio_tmp_dir)
|
|
337
|
+
result = downloader.download_streams(f"Audio {audio['language']}", "audio")
|
|
338
|
+
self.missing_segments.append(result)
|
|
339
|
+
|
|
340
|
+
if result.get('stopped', False):
|
|
341
|
+
self.stopped = True
|
|
342
|
+
return False
|
|
343
|
+
|
|
344
|
+
return True
|
|
345
|
+
|
|
346
|
+
except Exception as e:
|
|
347
|
+
logging.error(f"Error downloading audio {audio.get('language', 'unknown')}: {str(e)}")
|
|
348
|
+
return False
|
|
298
349
|
|
|
299
|
-
|
|
350
|
+
def download_subtitle(self, sub: Dict) -> bool:
|
|
351
|
+
"""
|
|
352
|
+
Downloads and saves subtitle file for a specific language.
|
|
353
|
+
|
|
354
|
+
Returns:
|
|
355
|
+
bool: True if download was successful, False otherwise
|
|
356
|
+
"""
|
|
357
|
+
try:
|
|
358
|
+
raw_content = self.client.request(sub['uri'])
|
|
359
|
+
if raw_content:
|
|
360
|
+
sub_path = os.path.join(self.temp_dir, 'subs', f"{sub['language']}.vtt")
|
|
361
|
+
|
|
362
|
+
subtitle_parser = M3U8_Parser()
|
|
363
|
+
subtitle_parser.parse_data(sub['uri'], raw_content)
|
|
364
|
+
|
|
365
|
+
with open(sub_path, 'wb') as f:
|
|
366
|
+
vtt_url = subtitle_parser.subtitle[-1]
|
|
367
|
+
vtt_content = self.client.request(vtt_url, True)
|
|
368
|
+
if vtt_content:
|
|
369
|
+
f.write(vtt_content)
|
|
370
|
+
return True
|
|
371
|
+
|
|
372
|
+
return False
|
|
373
|
+
|
|
374
|
+
except Exception as e:
|
|
375
|
+
logging.error(f"Error downloading subtitle {sub.get('language', 'unknown')}: {str(e)}")
|
|
376
|
+
return False
|
|
300
377
|
|
|
301
|
-
def download_all(self, video_url: str, audio_streams: List[Dict], sub_streams: List[Dict]):
|
|
378
|
+
def download_all(self, video_url: str, audio_streams: List[Dict], sub_streams: List[Dict]) -> bool:
|
|
302
379
|
"""
|
|
303
380
|
Downloads all selected streams (video, audio, subtitles).
|
|
381
|
+
For multiple downloads, continues even if individual downloads fail.
|
|
382
|
+
|
|
383
|
+
Returns:
|
|
384
|
+
bool: True if any critical download failed and should stop processing
|
|
304
385
|
"""
|
|
305
|
-
|
|
386
|
+
critical_failure = False
|
|
306
387
|
video_file = os.path.join(self.temp_dir, 'video', '0.ts')
|
|
307
388
|
|
|
389
|
+
# Download video (this is critical)
|
|
308
390
|
if not os.path.exists(video_file):
|
|
309
|
-
if self.download_video(video_url):
|
|
310
|
-
|
|
311
|
-
|
|
391
|
+
if not self.download_video(video_url):
|
|
392
|
+
logging.error("Critical failure: Video download failed")
|
|
393
|
+
critical_failure = True
|
|
312
394
|
|
|
395
|
+
# Download audio streams (continue even if some fail)
|
|
313
396
|
for audio in audio_streams:
|
|
314
|
-
|
|
315
|
-
|
|
397
|
+
if self.stopped:
|
|
398
|
+
break
|
|
316
399
|
|
|
317
400
|
audio_file = os.path.join(self.temp_dir, 'audio', audio['language'], '0.ts')
|
|
318
401
|
if not os.path.exists(audio_file):
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
402
|
+
success = self.download_audio(audio)
|
|
403
|
+
if not success:
|
|
404
|
+
logging.warning(f"Audio download failed for language {audio.get('language', 'unknown')}, continuing...")
|
|
322
405
|
|
|
406
|
+
# Download subtitle streams (continue even if some fail)
|
|
323
407
|
for sub in sub_streams:
|
|
324
|
-
|
|
325
|
-
|
|
408
|
+
if self.stopped:
|
|
409
|
+
break
|
|
326
410
|
|
|
327
411
|
sub_file = os.path.join(self.temp_dir, 'subs', f"{sub['language']}.vtt")
|
|
328
412
|
if not os.path.exists(sub_file):
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
413
|
+
success = self.download_subtitle(sub)
|
|
414
|
+
if not success:
|
|
415
|
+
logging.warning(f"Subtitle download failed for language {sub.get('language', 'unknown')}, continuing...")
|
|
332
416
|
|
|
333
|
-
return
|
|
417
|
+
return critical_failure or self.stopped
|
|
334
418
|
|
|
335
419
|
|
|
336
420
|
class MergeManager:
|
|
@@ -348,10 +432,10 @@ class MergeManager:
|
|
|
348
432
|
self.audio_streams = audio_streams
|
|
349
433
|
self.sub_streams = sub_streams
|
|
350
434
|
|
|
351
|
-
def merge(self) -> str:
|
|
435
|
+
def merge(self) -> tuple[str, bool]:
|
|
352
436
|
"""
|
|
353
437
|
Merges downloaded streams into final video file.
|
|
354
|
-
Returns path to the final merged file.
|
|
438
|
+
Returns path to the final merged file and use_shortest flag.
|
|
355
439
|
|
|
356
440
|
Process:
|
|
357
441
|
1. If no audio/subs, just process video
|
|
@@ -371,31 +455,45 @@ class MergeManager:
|
|
|
371
455
|
|
|
372
456
|
else:
|
|
373
457
|
if self.audio_streams:
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
458
|
+
|
|
459
|
+
# Only include audio tracks that actually exist
|
|
460
|
+
existing_audio_tracks = []
|
|
461
|
+
for a in self.audio_streams:
|
|
462
|
+
audio_path = os.path.join(self.temp_dir, 'audio', a['language'], '0.ts')
|
|
463
|
+
if os.path.exists(audio_path):
|
|
464
|
+
existing_audio_tracks.append({
|
|
465
|
+
'path': audio_path,
|
|
466
|
+
'name': a['language']
|
|
467
|
+
})
|
|
468
|
+
|
|
469
|
+
if existing_audio_tracks:
|
|
470
|
+
merged_audio_path = os.path.join(self.temp_dir, 'merged_audio.mp4')
|
|
471
|
+
merged_file, use_shortest = join_audios(
|
|
472
|
+
video_path=video_file,
|
|
473
|
+
audio_tracks=existing_audio_tracks,
|
|
474
|
+
out_path=merged_audio_path,
|
|
475
|
+
codec=self.parser.codec
|
|
476
|
+
)
|
|
386
477
|
|
|
387
478
|
if MERGE_SUBTITLE and self.sub_streams:
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
479
|
+
|
|
480
|
+
# Only include subtitle tracks that actually exist
|
|
481
|
+
existing_sub_tracks = []
|
|
482
|
+
for s in self.sub_streams:
|
|
483
|
+
sub_path = os.path.join(self.temp_dir, 'subs', f"{s['language']}.vtt")
|
|
484
|
+
if os.path.exists(sub_path):
|
|
485
|
+
existing_sub_tracks.append({
|
|
486
|
+
'path': sub_path,
|
|
487
|
+
'language': s['language']
|
|
488
|
+
})
|
|
489
|
+
|
|
490
|
+
if existing_sub_tracks:
|
|
491
|
+
merged_subs_path = os.path.join(self.temp_dir, 'final.mp4')
|
|
492
|
+
merged_file = join_subtitle(
|
|
493
|
+
video_path=merged_file,
|
|
494
|
+
subtitles_list=existing_sub_tracks,
|
|
495
|
+
out_path=merged_subs_path
|
|
496
|
+
)
|
|
399
497
|
|
|
400
498
|
return merged_file, use_shortest
|
|
401
499
|
|
|
@@ -413,13 +511,16 @@ class HLS_Downloader:
|
|
|
413
511
|
def start(self) -> Dict[str, Any]:
|
|
414
512
|
"""
|
|
415
513
|
Main execution flow with handling for both index and playlist M3U8s.
|
|
514
|
+
Returns False for this download and continues with the next one in case of failure.
|
|
416
515
|
|
|
417
516
|
Returns:
|
|
418
517
|
Dict containing:
|
|
419
518
|
- path: Output file path
|
|
420
519
|
- url: Original M3U8 URL
|
|
421
520
|
- is_master: Whether the M3U8 was a master playlist
|
|
422
|
-
|
|
521
|
+
- msg: Status message
|
|
522
|
+
- error: Error message if any
|
|
523
|
+
- stopped: Whether download was stopped
|
|
423
524
|
"""
|
|
424
525
|
|
|
425
526
|
if GET_ONLY_LINK:
|
|
@@ -433,7 +534,7 @@ class HLS_Downloader:
|
|
|
433
534
|
'stopped': True
|
|
434
535
|
}
|
|
435
536
|
|
|
436
|
-
console.print("[cyan]You can safely stop the download with [bold]Ctrl+c[bold] [cyan]
|
|
537
|
+
console.print("[cyan]You can safely stop the download with [bold]Ctrl+c[bold] [cyan]")
|
|
437
538
|
|
|
438
539
|
if TELEGRAM_BOT:
|
|
439
540
|
bot = get_bot_instance()
|
|
@@ -466,13 +567,26 @@ class HLS_Downloader:
|
|
|
466
567
|
url_fixer=self.m3u8_manager.url_fixer
|
|
467
568
|
)
|
|
468
569
|
|
|
469
|
-
# Check if download
|
|
470
|
-
|
|
570
|
+
# Check if download had critical failures
|
|
571
|
+
download_failed = self.download_manager.download_all(
|
|
471
572
|
video_url=self.m3u8_manager.video_url,
|
|
472
573
|
audio_streams=self.m3u8_manager.audio_streams,
|
|
473
574
|
sub_streams=self.m3u8_manager.sub_streams
|
|
474
575
|
)
|
|
475
576
|
|
|
577
|
+
if download_failed:
|
|
578
|
+
error_msg = "Critical download failure occurred"
|
|
579
|
+
console.print(f"[red]Download failed: {error_msg}[/red]")
|
|
580
|
+
self.path_manager.cleanup()
|
|
581
|
+
return {
|
|
582
|
+
'path': None,
|
|
583
|
+
'url': self.m3u8_url,
|
|
584
|
+
'is_master': self.m3u8_manager.is_master,
|
|
585
|
+
'msg': None,
|
|
586
|
+
'error': error_msg,
|
|
587
|
+
'stopped': self.download_manager.stopped
|
|
588
|
+
}
|
|
589
|
+
|
|
476
590
|
self.merge_manager = MergeManager(
|
|
477
591
|
temp_dir=self.path_manager.temp_dir,
|
|
478
592
|
parser=self.m3u8_manager.parser,
|
|
@@ -489,15 +603,30 @@ class HLS_Downloader:
|
|
|
489
603
|
'path': self.path_manager.output_path,
|
|
490
604
|
'url': self.m3u8_url,
|
|
491
605
|
'is_master': self.m3u8_manager.is_master,
|
|
492
|
-
'msg':
|
|
606
|
+
'msg': 'Download completed successfully',
|
|
607
|
+
'error': None,
|
|
608
|
+
'stopped': self.download_manager.stopped
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
except KeyboardInterrupt:
|
|
612
|
+
console.print("\n[yellow]Download interrupted by user[/yellow]")
|
|
613
|
+
self.path_manager.cleanup()
|
|
614
|
+
return {
|
|
615
|
+
'path': None,
|
|
616
|
+
'url': self.m3u8_url,
|
|
617
|
+
'is_master': getattr(self.m3u8_manager, 'is_master', None),
|
|
618
|
+
'msg': 'Download interrupted by user',
|
|
493
619
|
'error': None,
|
|
494
|
-
'stopped':
|
|
620
|
+
'stopped': True
|
|
495
621
|
}
|
|
496
622
|
|
|
497
623
|
except Exception as e:
|
|
498
624
|
error_msg = str(e)
|
|
499
625
|
console.print(f"[red]Download failed: {error_msg}[/red]")
|
|
500
|
-
logging.error("Download error", exc_info=True)
|
|
626
|
+
logging.error(f"Download error for {self.m3u8_url}", exc_info=True)
|
|
627
|
+
|
|
628
|
+
# Cleanup on error
|
|
629
|
+
self.path_manager.cleanup()
|
|
501
630
|
|
|
502
631
|
return {
|
|
503
632
|
'path': None,
|
|
@@ -508,7 +637,7 @@ class HLS_Downloader:
|
|
|
508
637
|
'stopped': False
|
|
509
638
|
}
|
|
510
639
|
|
|
511
|
-
def _print_summary(self, use_shortest):
|
|
640
|
+
def _print_summary(self, use_shortest: bool):
|
|
512
641
|
"""Prints download summary including file size, duration, and any missing segments."""
|
|
513
642
|
if TELEGRAM_BOT:
|
|
514
643
|
bot = get_bot_instance()
|
|
@@ -549,8 +678,9 @@ class HLS_Downloader:
|
|
|
549
678
|
os.rename(self.path_manager.output_path, new_filename)
|
|
550
679
|
self.path_manager.output_path = new_filename
|
|
551
680
|
|
|
681
|
+
print("")
|
|
552
682
|
console.print(Panel(
|
|
553
683
|
panel_content,
|
|
554
684
|
title=f"{os.path.basename(self.path_manager.output_path.replace('.mp4', ''))}",
|
|
555
685
|
border_style="green"
|
|
556
|
-
))
|
|
686
|
+
))
|
|
@@ -287,11 +287,7 @@ class M3U8_Segments:
|
|
|
287
287
|
progress_bar.update(1)
|
|
288
288
|
return
|
|
289
289
|
|
|
290
|
-
except Exception
|
|
291
|
-
error_msg = str(e)
|
|
292
|
-
|
|
293
|
-
if attempt == 0:
|
|
294
|
-
logging.warning(f"Segment {index} failed on first attempt: {error_msg}")
|
|
290
|
+
except Exception:
|
|
295
291
|
|
|
296
292
|
if attempt > self.info_maxRetry:
|
|
297
293
|
self.info_maxRetry = attempt + 1
|
|
@@ -57,7 +57,7 @@ def capture_output(process: subprocess.Popen, description: str) -> None:
|
|
|
57
57
|
|
|
58
58
|
|
|
59
59
|
# Construct the progress string with formatted output information
|
|
60
|
-
progress_string = (f"
|
|
60
|
+
progress_string = (f"{description}[white]: "
|
|
61
61
|
f"([green]'speed': [yellow]{data.get('speed', 'N/A')}[white], "
|
|
62
62
|
f"[green]'size': [yellow]{internet_manager.format_file_size(byte_size)}[white])")
|
|
63
63
|
max_length = max(max_length, len(progress_string))
|