StreamingCommunity 3.3.6__py3-none-any.whl → 3.3.8__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 (48) hide show
  1. StreamingCommunity/Api/Site/altadefinizione/film.py +1 -1
  2. StreamingCommunity/Api/Site/altadefinizione/series.py +1 -1
  3. StreamingCommunity/Api/Site/animeunity/serie.py +2 -2
  4. StreamingCommunity/Api/Site/animeworld/film.py +1 -1
  5. StreamingCommunity/Api/Site/animeworld/serie.py +2 -2
  6. StreamingCommunity/Api/Site/crunchyroll/film.py +3 -2
  7. StreamingCommunity/Api/Site/crunchyroll/series.py +3 -2
  8. StreamingCommunity/Api/Site/crunchyroll/site.py +0 -8
  9. StreamingCommunity/Api/Site/crunchyroll/util/get_license.py +11 -105
  10. StreamingCommunity/Api/Site/guardaserie/series.py +1 -1
  11. StreamingCommunity/Api/Site/mediasetinfinity/film.py +1 -1
  12. StreamingCommunity/Api/Site/mediasetinfinity/series.py +7 -9
  13. StreamingCommunity/Api/Site/mediasetinfinity/site.py +29 -66
  14. StreamingCommunity/Api/Site/mediasetinfinity/util/ScrapeSerie.py +5 -1
  15. StreamingCommunity/Api/Site/mediasetinfinity/util/get_license.py +151 -233
  16. StreamingCommunity/Api/Site/raiplay/film.py +2 -10
  17. StreamingCommunity/Api/Site/raiplay/series.py +2 -10
  18. StreamingCommunity/Api/Site/raiplay/site.py +1 -0
  19. StreamingCommunity/Api/Site/raiplay/util/ScrapeSerie.py +7 -1
  20. StreamingCommunity/Api/Site/streamingcommunity/film.py +1 -1
  21. StreamingCommunity/Api/Site/streamingcommunity/series.py +1 -1
  22. StreamingCommunity/Api/Site/streamingwatch/film.py +1 -1
  23. StreamingCommunity/Api/Site/streamingwatch/series.py +1 -1
  24. StreamingCommunity/Api/Template/loader.py +158 -0
  25. StreamingCommunity/Lib/Downloader/DASH/downloader.py +267 -51
  26. StreamingCommunity/Lib/Downloader/DASH/segments.py +46 -15
  27. StreamingCommunity/Lib/Downloader/HLS/downloader.py +51 -36
  28. StreamingCommunity/Lib/Downloader/HLS/segments.py +105 -25
  29. StreamingCommunity/Lib/Downloader/MP4/downloader.py +12 -13
  30. StreamingCommunity/Lib/FFmpeg/command.py +18 -81
  31. StreamingCommunity/Lib/FFmpeg/util.py +14 -10
  32. StreamingCommunity/Lib/M3U8/estimator.py +13 -12
  33. StreamingCommunity/Lib/M3U8/parser.py +16 -16
  34. StreamingCommunity/Upload/update.py +2 -4
  35. StreamingCommunity/Upload/version.py +2 -2
  36. StreamingCommunity/Util/config_json.py +3 -132
  37. StreamingCommunity/Util/installer/bento4_install.py +21 -31
  38. StreamingCommunity/Util/installer/device_install.py +0 -1
  39. StreamingCommunity/Util/installer/ffmpeg_install.py +0 -1
  40. StreamingCommunity/Util/message.py +8 -9
  41. StreamingCommunity/Util/os.py +0 -8
  42. StreamingCommunity/run.py +4 -44
  43. {streamingcommunity-3.3.6.dist-info → streamingcommunity-3.3.8.dist-info}/METADATA +1 -3
  44. {streamingcommunity-3.3.6.dist-info → streamingcommunity-3.3.8.dist-info}/RECORD +48 -47
  45. {streamingcommunity-3.3.6.dist-info → streamingcommunity-3.3.8.dist-info}/WHEEL +0 -0
  46. {streamingcommunity-3.3.6.dist-info → streamingcommunity-3.3.8.dist-info}/entry_points.txt +0 -0
  47. {streamingcommunity-3.3.6.dist-info → streamingcommunity-3.3.8.dist-info}/licenses/LICENSE +0 -0
  48. {streamingcommunity-3.3.6.dist-info → streamingcommunity-3.3.8.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 optional pssh.
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.5
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 = rep.get('type', description)
118
+ stream_type = description
88
119
  if concurrent_downloads is None:
89
- concurrent_downloads = self._get_worker_count(stream_type)
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}[MPD] ({Colors.CYAN}{description}{Colors.WHITE}): "
323
- f"{Colors.RED}{{percentage:.2f}}% "
324
- f"{Colors.MAGENTA}{{bar}} "
325
- f"{Colors.YELLOW}{{elapsed}}{Colors.WHITE} < {Colors.CYAN}{{remaining}}{Colors.WHITE}{{postfix}}{Colors.WHITE}"
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
- 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])
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
- 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])
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 = M3U8_Segments(url=video_full_url, tmp_folder=video_tmp_dir)
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 = M3U8_Segments(url=audio_full_url, tmp_folder=audio_tmp_dir)
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.client = HLSClient()
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
- self.m3u8_manager.log_selection()
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.5
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 = {'headers': {'User-Agent': get_userAgent()}, 'timeout': MAX_TIMEOOUT, 'verify': REQUEST_VERIFY}
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
- self.segments = [
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 = {'headers': {'User-Agent': get_userAgent()}, 'timeout': MAX_TIMEOOUT, 'verify': REQUEST_VERIFY}
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={"User-Agent": get_userAgent()})
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, index, progress_bar))
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] {Colors.WHITE}({Colors.CYAN}{description}{Colors.WHITE}): "
458
- f"{Colors.RED}{{percentage:.2f}}% "
459
- f"{Colors.MAGENTA}{{bar}} "
460
- f"{Colors.YELLOW}{{elapsed}}{Colors.WHITE} < {Colors.CYAN}{{remaining}}{Colors.WHITE}{{postfix}}{Colors.WHITE}"
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). In background threads (e.g., Django), skip custom signal handling.
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 a fancy progress bar
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:.2f}}% {Colors.MAGENTA}{{bar}} {Colors.WHITE}[ "
143
- f"{Colors.YELLOW}{{n_fmt}}{Colors.WHITE} / {Colors.RED}{{total_fmt}} {Colors.WHITE}] "
144
- f"{Colors.YELLOW}{{elapsed}} {Colors.WHITE}< {Colors.CYAN}{{remaining}}{Colors.WHITE}, "
145
- f"{Colors.YELLOW}{{rate_fmt}}{{postfix}} ",
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
- desc='Downloading',
146
+ unit_divisor=1024,
149
147
  mininterval=0.05,
150
- file=sys.stdout # Using file=sys.stdout to force in-place updates because sys.stderr may not support carriage returns in this environment.
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