StreamingCommunity 3.3.9__py3-none-any.whl → 3.4.2__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 (70) hide show
  1. StreamingCommunity/Api/Player/hdplayer.py +0 -5
  2. StreamingCommunity/Api/Player/mediapolisvod.py +4 -13
  3. StreamingCommunity/Api/Player/supervideo.py +3 -8
  4. StreamingCommunity/Api/Player/sweetpixel.py +1 -9
  5. StreamingCommunity/Api/Player/vixcloud.py +5 -16
  6. StreamingCommunity/Api/Site/altadefinizione/film.py +4 -16
  7. StreamingCommunity/Api/Site/altadefinizione/series.py +3 -12
  8. StreamingCommunity/Api/Site/altadefinizione/site.py +2 -9
  9. StreamingCommunity/Api/Site/altadefinizione/util/ScrapeSerie.py +2 -7
  10. StreamingCommunity/Api/Site/animeunity/site.py +9 -24
  11. StreamingCommunity/Api/Site/animeunity/util/ScrapeSerie.py +11 -27
  12. StreamingCommunity/Api/Site/animeworld/film.py +4 -2
  13. StreamingCommunity/Api/Site/animeworld/site.py +3 -11
  14. StreamingCommunity/Api/Site/animeworld/util/ScrapeSerie.py +1 -4
  15. StreamingCommunity/Api/Site/crunchyroll/film.py +4 -5
  16. StreamingCommunity/Api/Site/crunchyroll/series.py +5 -17
  17. StreamingCommunity/Api/Site/crunchyroll/site.py +4 -13
  18. StreamingCommunity/Api/Site/crunchyroll/util/ScrapeSerie.py +5 -27
  19. StreamingCommunity/Api/Site/crunchyroll/util/get_license.py +11 -26
  20. StreamingCommunity/Api/Site/guardaserie/series.py +3 -14
  21. StreamingCommunity/Api/Site/guardaserie/site.py +4 -12
  22. StreamingCommunity/Api/Site/guardaserie/util/ScrapeSerie.py +3 -10
  23. StreamingCommunity/Api/Site/mediasetinfinity/film.py +11 -12
  24. StreamingCommunity/Api/Site/mediasetinfinity/series.py +4 -15
  25. StreamingCommunity/Api/Site/mediasetinfinity/site.py +16 -32
  26. StreamingCommunity/Api/Site/mediasetinfinity/util/ScrapeSerie.py +39 -50
  27. StreamingCommunity/Api/Site/mediasetinfinity/util/fix_mpd.py +3 -3
  28. StreamingCommunity/Api/Site/mediasetinfinity/util/get_license.py +7 -25
  29. StreamingCommunity/Api/Site/raiplay/film.py +6 -8
  30. StreamingCommunity/Api/Site/raiplay/series.py +5 -20
  31. StreamingCommunity/Api/Site/raiplay/site.py +45 -47
  32. StreamingCommunity/Api/Site/raiplay/util/ScrapeSerie.py +91 -55
  33. StreamingCommunity/Api/Site/raiplay/util/get_license.py +3 -12
  34. StreamingCommunity/Api/Site/streamingcommunity/film.py +5 -16
  35. StreamingCommunity/Api/Site/streamingcommunity/series.py +5 -10
  36. StreamingCommunity/Api/Site/streamingcommunity/site.py +3 -22
  37. StreamingCommunity/Api/Site/streamingcommunity/util/ScrapeSerie.py +11 -27
  38. StreamingCommunity/Api/Site/streamingwatch/__init__.py +1 -0
  39. StreamingCommunity/Api/Site/streamingwatch/film.py +4 -2
  40. StreamingCommunity/Api/Site/streamingwatch/series.py +4 -14
  41. StreamingCommunity/Api/Site/streamingwatch/site.py +4 -18
  42. StreamingCommunity/Api/Site/streamingwatch/util/ScrapeSerie.py +0 -3
  43. StreamingCommunity/Api/Template/Util/__init__.py +4 -2
  44. StreamingCommunity/Api/Template/Util/manage_ep.py +66 -0
  45. StreamingCommunity/Api/Template/config_loader.py +0 -7
  46. StreamingCommunity/Lib/Downloader/DASH/decrypt.py +54 -1
  47. StreamingCommunity/Lib/Downloader/DASH/downloader.py +186 -70
  48. StreamingCommunity/Lib/Downloader/DASH/parser.py +2 -3
  49. StreamingCommunity/Lib/Downloader/DASH/segments.py +109 -68
  50. StreamingCommunity/Lib/Downloader/HLS/downloader.py +100 -82
  51. StreamingCommunity/Lib/Downloader/HLS/segments.py +40 -28
  52. StreamingCommunity/Lib/Downloader/MP4/downloader.py +16 -4
  53. StreamingCommunity/Lib/FFmpeg/capture.py +37 -5
  54. StreamingCommunity/Lib/FFmpeg/command.py +32 -90
  55. StreamingCommunity/Lib/M3U8/estimator.py +47 -1
  56. StreamingCommunity/Lib/TMBD/tmdb.py +2 -4
  57. StreamingCommunity/TelegramHelp/config.json +0 -1
  58. StreamingCommunity/Upload/update.py +19 -6
  59. StreamingCommunity/Upload/version.py +1 -1
  60. StreamingCommunity/Util/config_json.py +28 -21
  61. StreamingCommunity/Util/http_client.py +28 -0
  62. StreamingCommunity/Util/os.py +16 -6
  63. StreamingCommunity/Util/table.py +50 -8
  64. {streamingcommunity-3.3.9.dist-info → streamingcommunity-3.4.2.dist-info}/METADATA +1 -3
  65. streamingcommunity-3.4.2.dist-info/RECORD +111 -0
  66. streamingcommunity-3.3.9.dist-info/RECORD +0 -111
  67. {streamingcommunity-3.3.9.dist-info → streamingcommunity-3.4.2.dist-info}/WHEEL +0 -0
  68. {streamingcommunity-3.3.9.dist-info → streamingcommunity-3.4.2.dist-info}/entry_points.txt +0 -0
  69. {streamingcommunity-3.3.9.dist-info → streamingcommunity-3.4.2.dist-info}/licenses/LICENSE +0 -0
  70. {streamingcommunity-3.3.9.dist-info → streamingcommunity-3.4.2.dist-info}/top_level.txt +0 -0
@@ -1,23 +1,20 @@
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
- from rich.panel import Panel
14
11
  from rich.table import Table
15
12
 
16
13
 
17
14
  # Internal utilities
18
15
  from StreamingCommunity.Util.config_json import config_manager
19
16
  from StreamingCommunity.Util.headers import get_userAgent
20
- from StreamingCommunity.Util.http_client import create_client
17
+ from StreamingCommunity.Util.http_client import fetch
21
18
  from StreamingCommunity.Util.os import os_manager, internet_manager
22
19
 
23
20
 
@@ -33,15 +30,13 @@ from .segments import M3U8_Segments
33
30
 
34
31
 
35
32
  # Config
36
- ENABLE_SUBTITLE = config_manager.get_bool('M3U8_DOWNLOAD', 'download_subtitle')
37
33
  DOWNLOAD_SPECIFIC_AUDIO = config_manager.get_list('M3U8_DOWNLOAD', 'specific_list_audio')
38
34
  DOWNLOAD_SPECIFIC_SUBTITLE = config_manager.get_list('M3U8_DOWNLOAD', 'specific_list_subtitles')
39
35
  MERGE_SUBTITLE = config_manager.get_bool('M3U8_DOWNLOAD', 'merge_subs')
40
36
  CLEANUP_TMP = config_manager.get_bool('M3U8_DOWNLOAD', 'cleanup_tmp_folder')
41
37
  GET_ONLY_LINK = config_manager.get_int('M3U8_DOWNLOAD', 'get_only_link')
42
38
  FILTER_CUSTOM_RESOLUTION = str(config_manager.get('M3U8_CONVERSION', 'force_resolution')).strip().lower()
43
- RETRY_LIMIT = config_manager.get_int('REQUESTS', 'max_retry')
44
- MAX_TIMEOUT = config_manager.get_int("REQUESTS", "timeout")
39
+ EXTENSION_OUTPUT = config_manager.get("M3U8_CONVERSION", "extension")
45
40
 
46
41
  console = Console()
47
42
 
@@ -51,9 +46,9 @@ class HLSClient:
51
46
  def __init__(self, custom_headers: Optional[Dict[str, str]] = None):
52
47
  self.headers = custom_headers if custom_headers else {'User-Agent': get_userAgent()}
53
48
 
54
- def request(self, url: str, return_content: bool = False) -> Optional[httpx.Response]:
49
+ def request(self, url: str, return_content: bool = False) -> Optional[Union[str, bytes]]:
55
50
  """
56
- Makes HTTP GET requests with retry logic.
51
+ Makes HTTP GET requests with retry logic using http_client.
57
52
 
58
53
  Args:
59
54
  url: Target URL to request
@@ -67,21 +62,12 @@ class HLSClient:
67
62
  logging.error("URL is None or empty, cannot make request")
68
63
  return None
69
64
 
70
- client = create_client(headers=self.headers)
71
-
72
- for attempt in range(RETRY_LIMIT):
73
- try:
74
- response = client.get(url)
75
- response.raise_for_status()
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
65
+ return fetch(
66
+ url,
67
+ method="GET",
68
+ headers=self.headers,
69
+ return_content=return_content
70
+ )
85
71
 
86
72
 
87
73
  class PathManager:
@@ -94,7 +80,7 @@ class PathManager:
94
80
  """
95
81
  self.m3u8_url = m3u8_url
96
82
  self.output_path = self._sanitize_output_path(output_path)
97
- base_name = os.path.basename(self.output_path).replace(".mp4", "")
83
+ base_name = os.path.basename(self.output_path).replace(EXTENSION_OUTPUT, "")
98
84
  self.temp_dir = os.path.join(os.path.dirname(self.output_path), f"{base_name}_tmp")
99
85
 
100
86
  def _sanitize_output_path(self, path: Optional[str]) -> str:
@@ -103,10 +89,10 @@ class PathManager:
103
89
  Creates a hash-based filename if no path is provided.
104
90
  """
105
91
  if not path:
106
- path = "download.mp4"
92
+ path = f"download{EXTENSION_OUTPUT}"
107
93
 
108
- if not path.endswith(".mp4"):
109
- path += ".mp4"
94
+ if not path.endswith(EXTENSION_OUTPUT):
95
+ path += EXTENSION_OUTPUT
110
96
 
111
97
  return os_manager.get_sanitize_path(path)
112
98
 
@@ -168,6 +154,7 @@ class M3U8Manager:
168
154
  """
169
155
  Selects video, audio, and subtitle streams based on configuration.
170
156
  If it's a master playlist, only selects video stream.
157
+ Auto-selects first audio if only one is available and none match filters.
171
158
  """
172
159
  if not self.is_master:
173
160
  self.video_url, self.video_res = self.m3u8_url, "undefined"
@@ -175,6 +162,7 @@ class M3U8Manager:
175
162
  self.sub_streams = []
176
163
 
177
164
  else:
165
+ # Video selection logic
178
166
  if str(FILTER_CUSTOM_RESOLUTION) == "best":
179
167
  self.video_url, self.video_res = self.parser._video.get_best_uri()
180
168
  elif str(FILTER_CUSTOM_RESOLUTION) == "worst":
@@ -182,25 +170,41 @@ class M3U8Manager:
182
170
  elif str(FILTER_CUSTOM_RESOLUTION).replace("p", "").replace("px", "").isdigit():
183
171
  resolution_value = int(str(FILTER_CUSTOM_RESOLUTION).replace("p", "").replace("px", ""))
184
172
  self.video_url, self.video_res = self.parser._video.get_custom_uri(resolution_value)
173
+
174
+ # Fallback to best if custom resolution not found
175
+ if self.video_url is None:
176
+ self.video_url, self.video_res = self.parser._video.get_best_uri()
185
177
  else:
186
178
  logging.error("Resolution not recognized.")
187
179
  self.video_url, self.video_res = self.parser._video.get_best_uri()
188
180
 
189
- # Audio info
181
+ # Audio selection with auto-select fallback
182
+ all_audio = self.parser._audio.get_all_uris_and_names() or []
183
+
184
+ # Try to match with configured languages
190
185
  self.audio_streams = [
191
- s for s in (self.parser._audio.get_all_uris_and_names() or [])
186
+ s for s in all_audio
192
187
  if s.get('language') in DOWNLOAD_SPECIFIC_AUDIO
193
188
  ]
194
-
189
+
190
+ # Auto-select first audio if:
191
+ # 1. No audio matched the filters
192
+ # 2. At least one audio track is available
193
+ # 3. Filters are configured (not empty)
194
+ if not self.audio_streams and all_audio and DOWNLOAD_SPECIFIC_AUDIO:
195
+ first_audio_lang = all_audio[0].get('language', 'unknown')
196
+ console.print(f"\n[yellow]Auto-selecting first available audio track: {first_audio_lang}[/yellow]")
197
+ self.audio_streams = [all_audio[0]]
198
+
199
+ # Subtitle selection
195
200
  self.sub_streams = []
196
- if ENABLE_SUBTITLE:
197
- if "*" in DOWNLOAD_SPECIFIC_SUBTITLE:
198
- self.sub_streams = self.parser._subtitle.get_all_uris_and_names() or []
199
- else:
200
- self.sub_streams = [
201
- s for s in (self.parser._subtitle.get_all_uris_and_names() or [])
202
- if s.get('language') in DOWNLOAD_SPECIFIC_SUBTITLE
203
- ]
201
+ if "*" in DOWNLOAD_SPECIFIC_SUBTITLE:
202
+ self.sub_streams = self.parser._subtitle.get_all_uris_and_names() or []
203
+ else:
204
+ self.sub_streams = [
205
+ s for s in (self.parser._subtitle.get_all_uris_and_names() or [])
206
+ if s.get('language') in DOWNLOAD_SPECIFIC_SUBTITLE
207
+ ]
204
208
 
205
209
  def log_selection(self):
206
210
  """Log the stream selection information in a formatted table."""
@@ -220,38 +224,22 @@ class M3U8Manager:
220
224
 
221
225
  data_rows.append(["Video", available_video, str(FILTER_CUSTOM_RESOLUTION), downloadable_video])
222
226
 
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
-
235
227
  # Subtitle information
236
228
  available_subtitles = self.parser._subtitle.get_all_uris_and_names() or []
237
- available_sub_languages = [sub.get('language') for sub in available_subtitles]
238
-
239
- if available_sub_languages:
229
+ if available_subtitles:
230
+ available_sub_languages = [sub.get('language') for sub in available_subtitles]
240
231
  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))
232
+ downloadable_sub_languages = [sub.get('language') for sub in self.sub_streams]
243
233
  downloadable_subs = ', '.join(downloadable_sub_languages) if downloadable_sub_languages else "Nothing"
244
234
 
245
235
  data_rows.append(["Subtitle", available_subs, ', '.join(DOWNLOAD_SPECIFIC_SUBTITLE), downloadable_subs])
246
236
 
247
237
  # Audio information
248
238
  available_audio = self.parser._audio.get_all_uris_and_names() or []
249
- available_audio_languages = [audio.get('language') for audio in available_audio]
250
-
251
- if available_audio_languages:
239
+ if available_audio:
240
+ available_audio_languages = [audio.get('language') for audio in available_audio]
252
241
  available_audios = ', '.join(available_audio_languages)
253
-
254
- downloadable_audio_languages = list(set(available_audio_languages) & set(DOWNLOAD_SPECIFIC_AUDIO))
242
+ downloadable_audio_languages = [audio.get('language') for audio in self.audio_streams]
255
243
  downloadable_audios = ', '.join(downloadable_audio_languages) if downloadable_audio_languages else "Nothing"
256
244
 
257
245
  data_rows.append(["Audio", available_audios, ', '.join(DOWNLOAD_SPECIFIC_AUDIO), downloadable_audios])
@@ -282,7 +270,8 @@ class M3U8Manager:
282
270
 
283
271
  console.print(table)
284
272
  print("")
285
-
273
+
274
+
286
275
  class DownloadManager:
287
276
  """Manages downloading of video, audio, and subtitle streams."""
288
277
  def __init__(self, temp_dir: str, client: HLSClient, url_fixer: M3U8_UrlFix, custom_headers: Optional[Dict[str, str]] = None):
@@ -301,6 +290,10 @@ class DownloadManager:
301
290
  self.stopped = False
302
291
  self.video_segments_count = 0
303
292
 
293
+ # For progress tracking
294
+ self.current_downloader: Optional[M3U8_Segments] = None
295
+ self.current_download_type: Optional[str] = None
296
+
304
297
  def download_video(self, video_url: str) -> bool:
305
298
  """
306
299
  Downloads video segments from the M3U8 playlist.
@@ -318,12 +311,20 @@ class DownloadManager:
318
311
  tmp_folder=video_tmp_dir,
319
312
  custom_headers=self.custom_headers
320
313
  )
314
+
315
+ # Set current downloader for progress tracking
316
+ self.current_downloader = downloader
317
+ self.current_download_type = 'video'
321
318
 
322
319
  # Download video and get segment count
323
320
  result = downloader.download_streams("Video", "video")
324
321
  self.video_segments_count = downloader.get_segments_count()
325
322
  self.missing_segments.append(result)
326
323
 
324
+ # Reset current downloader after completion
325
+ self.current_downloader = None
326
+ self.current_download_type = None
327
+
327
328
  if result.get('stopped', False):
328
329
  self.stopped = True
329
330
  return False
@@ -332,6 +333,8 @@ class DownloadManager:
332
333
 
333
334
  except Exception as e:
334
335
  logging.error(f"Error downloading video from {video_url}: {str(e)}")
336
+ self.current_downloader = None
337
+ self.current_download_type = None
335
338
  return False
336
339
 
337
340
  def download_audio(self, audio: Dict) -> bool:
@@ -353,10 +356,19 @@ class DownloadManager:
353
356
  limit_segments=self.video_segments_count if self.video_segments_count > 0 else None,
354
357
  custom_headers=self.custom_headers
355
358
  )
356
-
359
+
360
+ # Set current downloader for progress tracking
361
+ self.current_downloader = downloader
362
+ self.current_download_type = f"audio_{audio['language']}"
363
+
364
+ # Download audio
357
365
  result = downloader.download_streams(f"Audio {audio['language']}", "audio")
358
366
  self.missing_segments.append(result)
359
367
 
368
+ # Reset current downloader after completion
369
+ self.current_downloader = None
370
+ self.current_download_type = None
371
+
360
372
  if result.get('stopped', False):
361
373
  self.stopped = True
362
374
  return False
@@ -365,6 +377,8 @@ class DownloadManager:
365
377
 
366
378
  except Exception as e:
367
379
  logging.error(f"Error downloading audio {audio.get('language', 'unknown')}: {str(e)}")
380
+ self.current_downloader = None
381
+ self.current_download_type = None
368
382
  return False
369
383
 
370
384
  def download_subtitle(self, sub: Dict) -> bool:
@@ -664,6 +678,7 @@ class HLS_Downloader:
664
678
  """Prints download summary including file size, duration, and any missing segments."""
665
679
  missing_ts = False
666
680
  missing_info = ""
681
+
667
682
  for item in self.download_manager.missing_segments:
668
683
  if int(item['nFailed']) >= 1:
669
684
  missing_ts = True
@@ -672,30 +687,33 @@ class HLS_Downloader:
672
687
  file_size = internet_manager.format_file_size(os.path.getsize(self.path_manager.output_path))
673
688
  duration = print_duration_table(self.path_manager.output_path, description=False, return_string=True)
674
689
 
675
- panel_content = (
676
- f"[cyan]File size: [bold red]{file_size}[/bold red]\n"
677
- f"[cyan]Duration: [bold]{duration}[/bold]\n"
678
- f"[cyan]Output: [bold]{os.path.abspath(self.path_manager.output_path)}[/bold]"
679
- )
680
-
681
- if missing_ts:
682
- panel_content += f"\n{missing_info}"
683
-
690
+ # Rename output file if there were missing segments or shortest used
684
691
  new_filename = self.path_manager.output_path
685
692
  if missing_ts and use_shortest:
686
- new_filename = new_filename.replace(".mp4", "_failed_sync_ts.mp4")
693
+ new_filename = new_filename.replace(EXTENSION_OUTPUT, f"_failed_sync_ts{EXTENSION_OUTPUT}")
687
694
  elif missing_ts:
688
- new_filename = new_filename.replace(".mp4", "_failed_ts.mp4")
695
+ new_filename = new_filename.replace(EXTENSION_OUTPUT, f"_failed_ts{EXTENSION_OUTPUT}")
689
696
  elif use_shortest:
690
- new_filename = new_filename.replace(".mp4", "_failed_sync.mp4")
697
+ new_filename = new_filename.replace(EXTENSION_OUTPUT, f"_failed_sync{EXTENSION_OUTPUT}")
691
698
 
699
+ # Rename the file accordingly
692
700
  if missing_ts or use_shortest:
693
701
  os.rename(self.path_manager.output_path, new_filename)
694
702
  self.path_manager.output_path = new_filename
695
703
 
696
- print("")
697
- console.print(Panel(
698
- panel_content,
699
- title=f"{os.path.basename(self.path_manager.output_path.replace('.mp4', ''))}",
700
- border_style="green"
701
- ))
704
+ console.print(f"[yellow]Output [red]{os.path.abspath(self.path_manager.output_path)} [cyan]with size [red]{file_size} [cyan]and duration [red]{duration}")
705
+
706
+ def get_progress_data(self) -> Optional[Dict]:
707
+ """Get current download progress data."""
708
+ if not self.download_manager.current_downloader:
709
+ return None
710
+
711
+ try:
712
+ progress = self.download_manager.current_downloader.get_progress_data()
713
+ if progress:
714
+ progress['download_type'] = self.download_manager.current_download_type
715
+ return progress
716
+
717
+ except Exception as e:
718
+ logging.error(f"Error getting progress data: {e}")
719
+ return None
@@ -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
- client_params = {
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
- client_params = {
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}.tmp")
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
- return index, True, attempt, len(segment_content)
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 segment files
473
+ # Delete temp directory if exists
477
474
  if temp_dir and os.path.exists(temp_dir):
478
475
  try:
479
- for idx in range(len(self.segments)):
480
- temp_file = self._get_temp_segment_path(temp_dir, idx)
481
- if os.path.exists(temp_file):
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,23 @@ class M3U8_Segments:
489
485
 
490
486
  def _display_error_summary(self) -> None:
491
487
  """Generate final error report."""
492
- console.print(f"\n[green]Retry Summary: "
493
- f"[cyan]Max retries: [red]{self.info_maxRetry} "
494
- f"[cyan]Total retries: [red]{self.info_nRetry} "
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}")
491
+
492
+ def get_progress_data(self) -> Dict:
493
+ """Returns current download progress data for API consumption."""
494
+ total = self.get_segments_count()
495
+ downloaded = len(self.downloaded_segments)
496
+ percentage = (downloaded / total * 100) if total > 0 else 0
497
+ stats = self.class_ts_estimator.get_stats(downloaded, total)
498
+
499
+ return {
500
+ 'total_segments': total,
501
+ 'downloaded_segments': downloaded,
502
+ 'failed_segments': self.info_nFailed,
503
+ 'current_speed': stats['download_speed'],
504
+ 'estimated_size': stats['estimated_total_size'],
505
+ 'percentage': round(percentage, 2),
506
+ 'eta_seconds': stats['eta_seconds']
507
+ }
@@ -76,6 +76,7 @@ def MP4_downloader(url: str, path: str, referer: str = None, headers_: dict = No
76
76
  - Single Ctrl+C: Completes download gracefully
77
77
  - Triple Ctrl+C: Saves partial download and exits
78
78
  """
79
+ url = url.strip()
79
80
  if TELEGRAM_BOT:
80
81
  bot = get_bot_instance()
81
82
  console.log("####")
@@ -134,20 +135,23 @@ def MP4_downloader(url: str, path: str, referer: str = None, headers_: dict = No
134
135
 
135
136
  # Create progress bar with percentage instead of n_fmt/total_fmt
136
137
  console.print("[cyan]You can safely stop the download with [bold]Ctrl+c[bold] [cyan]")
138
+
137
139
  progress_bar = tqdm(
138
140
  total=total,
139
141
  ascii='░▒█',
140
142
  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}}",
143
+ f"{Colors.MAGENTA}{{bar:40}} "
144
+ f"{Colors.LIGHT_GREEN}{{n_fmt}}{Colors.WHITE}/{Colors.CYAN}{{total_fmt}}"
145
+ f" {Colors.DARK_GRAY}[{Colors.YELLOW}{{elapsed}}{Colors.WHITE} < {Colors.CYAN}{{remaining}}{Colors.DARK_GRAY}]"
146
+ f"{Colors.WHITE}{{postfix}} ",
144
147
  unit='B',
145
148
  unit_scale=True,
146
149
  unit_divisor=1024,
147
150
  mininterval=0.05,
148
151
  file=sys.stdout
149
152
  )
150
-
153
+
154
+ start_time = time.time()
151
155
  downloaded = 0
152
156
  with open(temp_path, 'wb') as file, progress_bar as bar:
153
157
  try:
@@ -160,6 +164,14 @@ def MP4_downloader(url: str, path: str, referer: str = None, headers_: dict = No
160
164
  size = file.write(chunk)
161
165
  downloaded += size
162
166
  bar.update(size)
167
+
168
+ # Update postfix with speed and final size
169
+ elapsed = time.time() - start_time
170
+ if elapsed > 0:
171
+ speed = downloaded / elapsed
172
+ speed_str = internet_manager.format_transfer_speed(speed)
173
+ postfix_str = f"{Colors.LIGHT_MAGENTA}@ {Colors.LIGHT_CYAN}{speed_str}"
174
+ bar.set_postfix_str(postfix_str)
163
175
 
164
176
  except KeyboardInterrupt:
165
177
  if not interrupt_handler.force_quit:
@@ -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 = (f"{description}[white]: "
61
- f"([green]'speed': [yellow]{data.get('speed', 'N/A')}[white], "
62
- f"[green]'size': [yellow]{internet_manager.format_file_size(byte_size)}[white])")
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