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.

Files changed (44) hide show
  1. StreamingCommunity/Api/Site/altadefinizione/__init__.py +17 -18
  2. StreamingCommunity/Api/Site/altadefinizione/series.py +4 -0
  3. StreamingCommunity/Api/Site/animeunity/__init__.py +14 -15
  4. StreamingCommunity/Api/Site/animeunity/serie.py +1 -1
  5. StreamingCommunity/Api/Site/animeworld/__init__.py +15 -13
  6. StreamingCommunity/Api/Site/animeworld/serie.py +1 -1
  7. StreamingCommunity/Api/Site/crunchyroll/__init__.py +16 -17
  8. StreamingCommunity/Api/Site/crunchyroll/series.py +6 -1
  9. StreamingCommunity/Api/Site/guardaserie/__init__.py +17 -19
  10. StreamingCommunity/Api/Site/guardaserie/series.py +4 -0
  11. StreamingCommunity/Api/Site/guardaserie/site.py +2 -7
  12. StreamingCommunity/Api/Site/mediasetinfinity/__init__.py +15 -15
  13. StreamingCommunity/Api/Site/mediasetinfinity/series.py +4 -0
  14. StreamingCommunity/Api/Site/mediasetinfinity/site.py +12 -2
  15. StreamingCommunity/Api/Site/mediasetinfinity/util/ScrapeSerie.py +67 -98
  16. StreamingCommunity/Api/Site/raiplay/__init__.py +15 -15
  17. StreamingCommunity/Api/Site/raiplay/series.py +5 -1
  18. StreamingCommunity/Api/Site/streamingcommunity/__init__.py +16 -14
  19. StreamingCommunity/Api/Site/streamingwatch/__init__.py +12 -12
  20. StreamingCommunity/Api/Site/streamingwatch/series.py +4 -0
  21. StreamingCommunity/Api/Template/Class/SearchType.py +0 -1
  22. StreamingCommunity/Api/Template/Util/manage_ep.py +1 -11
  23. StreamingCommunity/Api/Template/site.py +2 -3
  24. StreamingCommunity/Lib/Downloader/DASH/downloader.py +55 -17
  25. StreamingCommunity/Lib/Downloader/DASH/segments.py +73 -17
  26. StreamingCommunity/Lib/Downloader/HLS/downloader.py +282 -152
  27. StreamingCommunity/Lib/Downloader/HLS/segments.py +1 -5
  28. StreamingCommunity/Lib/FFmpeg/capture.py +1 -1
  29. StreamingCommunity/Lib/FFmpeg/command.py +6 -6
  30. StreamingCommunity/Lib/FFmpeg/util.py +11 -30
  31. StreamingCommunity/Lib/M3U8/estimator.py +27 -13
  32. StreamingCommunity/Upload/update.py +2 -2
  33. StreamingCommunity/Upload/version.py +1 -1
  34. StreamingCommunity/Util/installer/__init__.py +11 -0
  35. StreamingCommunity/Util/installer/device_install.py +1 -1
  36. StreamingCommunity/Util/os.py +2 -6
  37. StreamingCommunity/Util/table.py +40 -8
  38. StreamingCommunity/run.py +15 -8
  39. {streamingcommunity-3.3.5.dist-info → streamingcommunity-3.3.6.dist-info}/METADATA +38 -51
  40. {streamingcommunity-3.3.5.dist-info → streamingcommunity-3.3.6.dist-info}/RECORD +44 -43
  41. {streamingcommunity-3.3.5.dist-info → streamingcommunity-3.3.6.dist-info}/WHEEL +0 -0
  42. {streamingcommunity-3.3.5.dist-info → streamingcommunity-3.3.6.dist-info}/entry_points.txt +0 -0
  43. {streamingcommunity-3.3.5.dist-info → streamingcommunity-3.3.6.dist-info}/licenses/LICENSE +0 -0
  44. {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
- time.sleep(1.5 ** attempt)
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
- content = self.client.request(self.m3u8_url)
141
- if not content:
142
- raise ValueError("Failed to fetch M3U8 content")
143
-
144
- self.parser.parse_data(uri=self.m3u8_url, raw_content=content)
145
- self.url_fixer.set_playlist(self.m3u8_url)
146
- self.is_master = self.parser.is_master_playlist
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
- tuple_available_resolution = self.parser._video.get_list_resolution()
189
- list_available_resolution = [f"{r[0]}x{r[1]}" for r in tuple_available_resolution]
190
-
191
- console.print(
192
- f"[cyan bold]Video [/cyan bold] [green]Available:[/green] [purple]{', '.join(list_available_resolution)}[/purple] | "
193
- f"[red]Set:[/red] [purple]{FILTER_CUSTOM_RESOLUTION}[/purple] | "
194
- f"[yellow]Downloadable:[/yellow] [purple]{self.video_res[0]}x{self.video_res[1]}[/purple]"
195
- )
196
-
197
- if self.parser.codec is not None:
198
- available_codec_info = (
199
- f"[green]v[/green]: [yellow]{self.parser.codec.video_codec_name}[/yellow] "
200
- f"([green]b[/green]: [yellow]{self.parser.codec.video_bitrate // 1000}k[/yellow]), "
201
- f"[green]a[/green]: [yellow]{self.parser.codec.audio_codec_name}[/yellow] "
202
- f"([green]b[/green]: [yellow]{self.parser.codec.audio_bitrate // 1000}k[/yellow])"
203
- )
204
- set_codec_info = available_codec_info if config_manager.get_bool("M3U8_CONVERSION", "use_codec") else "[purple]copy[/purple]"
205
-
206
- console.print(
207
- f"[bold cyan]Codec [/bold cyan] [green]Available:[/green] {available_codec_info} | "
208
- f"[red]Set:[/red] {set_codec_info}"
209
- )
210
-
211
- # Get available subtitles and their languages
212
- available_subtitles = self.parser._subtitle.get_all_uris_and_names() or []
213
- available_sub_languages = [sub.get('language') for sub in available_subtitles]
214
-
215
- # If "*" is in DOWNLOAD_SPECIFIC_SUBTITLE, all languages are downloadable
216
- downloadable_sub_languages = available_sub_languages if "*" in DOWNLOAD_SPECIFIC_SUBTITLE else list(set(available_sub_languages) & set(DOWNLOAD_SPECIFIC_SUBTITLE))
217
-
218
- if available_sub_languages:
219
- console.print(
220
- f"[cyan bold]Subtitle [/cyan bold] [green]Available:[/green] [purple]{', '.join(available_sub_languages)}[/purple] | "
221
- f"[red]Set:[/red] [purple]{', '.join(DOWNLOAD_SPECIFIC_SUBTITLE)}[/purple] | "
222
- f"[yellow]Downloadable:[/yellow] [purple]{', '.join(downloadable_sub_languages)}[/purple]"
223
- )
224
-
225
- available_audio = self.parser._audio.get_all_uris_and_names() or []
226
- available_audio_languages = [audio.get('language') for audio in available_audio]
227
- downloadable_audio_languages = list(set(available_audio_languages) & set(DOWNLOAD_SPECIFIC_AUDIO))
228
- if available_audio_languages:
229
- console.print(
230
- f"[cyan bold]Audio [/cyan bold] [green]Available:[/green] [purple]{', '.join(available_audio_languages)}[/purple] | "
231
- f"[red]Set:[/red] [purple]{', '.join(DOWNLOAD_SPECIFIC_AUDIO)}[/purple] | "
232
- f"[yellow]Downloadable:[/yellow] [purple]{', '.join(downloadable_audio_languages)}[/purple]"
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
- """Downloads video segments from the M3U8 playlist."""
254
- video_full_url = self.url_fixer.generate_full_url(video_url)
255
- video_tmp_dir = os.path.join(self.temp_dir, 'video')
256
-
257
- downloader = M3U8_Segments(url=video_full_url, tmp_folder=video_tmp_dir)
258
- result = downloader.download_streams("Video", "video")
259
- self.missing_segments.append(result)
260
-
261
- if result.get('stopped', False):
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
- def download_subtitle(self, sub: Dict):
283
- """Downloads and saves subtitle file for a specific language."""
284
- #if self.stopped:
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
- raw_content = self.client.request(sub['uri'])
288
- if raw_content:
289
- sub_path = os.path.join(self.temp_dir, 'subs', f"{sub['language']}.vtt")
315
+ if result.get('stopped', False):
316
+ self.stopped = True
317
+ return False
290
318
 
291
- subtitle_parser = M3U8_Parser()
292
- subtitle_parser.parse_data(sub['uri'], raw_content)
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
- with open(sub_path, 'wb') as f:
295
- vtt_url = subtitle_parser.subtitle[-1]
296
- vtt_content = self.client.request(vtt_url, True)
297
- f.write(vtt_content)
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
- return self.stopped
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
- return_stopped = False
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
- if not return_stopped:
311
- return_stopped = True
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
- #if self.stopped:
315
- # break
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
- if self.download_audio(audio):
320
- if not return_stopped:
321
- return_stopped = True
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
- #if self.stopped:
325
- # break
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
- if self.download_subtitle(sub):
330
- if not return_stopped:
331
- return_stopped = True
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 return_stopped
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
- audio_tracks = [{
375
- 'path': os.path.join(self.temp_dir, 'audio', a['language'], '0.ts'),
376
- 'name': a['language']
377
- } for a in self.audio_streams]
378
-
379
- merged_audio_path = os.path.join(self.temp_dir, 'merged_audio.mp4')
380
- merged_file, use_shortest = join_audios(
381
- video_path=video_file,
382
- audio_tracks=audio_tracks,
383
- out_path=merged_audio_path,
384
- codec=self.parser.codec
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
- sub_tracks = [{
389
- 'path': os.path.join(self.temp_dir, 'subs', f"{s['language']}.vtt"),
390
- 'language': s['language']
391
- } for s in self.sub_streams]
392
-
393
- merged_subs_path = os.path.join(self.temp_dir, 'final.mp4')
394
- merged_file = join_subtitle(
395
- video_path=merged_file,
396
- subtitles_list=sub_tracks,
397
- out_path=merged_subs_path
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
- Or raises an exception if there's an error
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] \n")
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 was stopped
470
- download_stopped = self.download_manager.download_all(
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': None,
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': download_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 as e:
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" {description}[white]: "
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))