StreamingCommunity 2.5.7__py3-none-any.whl → 2.5.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 (65) hide show
  1. StreamingCommunity/Api/Player/ddl.py +2 -3
  2. StreamingCommunity/Api/Site/1337xx/__init__.py +5 -6
  3. StreamingCommunity/Api/Site/1337xx/site.py +7 -14
  4. StreamingCommunity/Api/Site/1337xx/title.py +3 -5
  5. StreamingCommunity/Api/Site/altadefinizionegratis/__init__.py +7 -6
  6. StreamingCommunity/Api/Site/altadefinizionegratis/film.py +14 -19
  7. StreamingCommunity/Api/Site/altadefinizionegratis/site.py +6 -14
  8. StreamingCommunity/Api/Site/animeunity/__init__.py +7 -7
  9. StreamingCommunity/Api/Site/animeunity/film_serie.py +29 -31
  10. StreamingCommunity/Api/Site/animeunity/site.py +14 -22
  11. StreamingCommunity/Api/Site/cb01new/__init__.py +5 -4
  12. StreamingCommunity/Api/Site/cb01new/film.py +2 -5
  13. StreamingCommunity/Api/Site/cb01new/site.py +5 -13
  14. StreamingCommunity/Api/Site/ddlstreamitaly/__init__.py +5 -4
  15. StreamingCommunity/Api/Site/ddlstreamitaly/series.py +12 -49
  16. StreamingCommunity/Api/Site/ddlstreamitaly/site.py +6 -16
  17. StreamingCommunity/Api/Site/ddlstreamitaly/util/ScrapeSerie.py +2 -3
  18. StreamingCommunity/Api/Site/guardaserie/__init__.py +5 -4
  19. StreamingCommunity/Api/Site/guardaserie/series.py +11 -46
  20. StreamingCommunity/Api/Site/guardaserie/site.py +5 -13
  21. StreamingCommunity/Api/Site/guardaserie/util/ScrapeSerie.py +10 -14
  22. StreamingCommunity/Api/Site/ilcorsaronero/__init__.py +5 -4
  23. StreamingCommunity/Api/Site/ilcorsaronero/site.py +5 -13
  24. StreamingCommunity/Api/Site/ilcorsaronero/title.py +3 -5
  25. StreamingCommunity/Api/Site/mostraguarda/__init__.py +2 -2
  26. StreamingCommunity/Api/Site/mostraguarda/film.py +4 -8
  27. StreamingCommunity/Api/Site/streamingcommunity/__init__.py +8 -7
  28. StreamingCommunity/Api/Site/streamingcommunity/film.py +14 -18
  29. StreamingCommunity/Api/Site/streamingcommunity/series.py +25 -76
  30. StreamingCommunity/Api/Site/streamingcommunity/site.py +11 -23
  31. StreamingCommunity/Api/Template/Util/__init__.py +8 -1
  32. StreamingCommunity/Api/Template/Util/manage_ep.py +46 -2
  33. StreamingCommunity/Api/Template/config_loader.py +71 -0
  34. StreamingCommunity/Lib/Downloader/HLS/downloader.py +60 -59
  35. StreamingCommunity/Lib/Downloader/HLS/segments.py +40 -14
  36. StreamingCommunity/Lib/Downloader/MP4/downloader.py +47 -40
  37. StreamingCommunity/Lib/FFmpeg/command.py +59 -3
  38. StreamingCommunity/Lib/M3U8/estimator.py +5 -5
  39. StreamingCommunity/Lib/M3U8/parser.py +12 -51
  40. StreamingCommunity/Lib/TMBD/tmdb.py +66 -99
  41. StreamingCommunity/TelegramHelp/telegram_bot.py +222 -68
  42. StreamingCommunity/Util/_jsonConfig.py +14 -13
  43. StreamingCommunity/Util/ffmpeg_installer.py +70 -64
  44. StreamingCommunity/Util/headers.py +11 -122
  45. StreamingCommunity/Util/os.py +64 -55
  46. StreamingCommunity/Util/table.py +62 -108
  47. StreamingCommunity/run.py +15 -10
  48. {StreamingCommunity-2.5.7.dist-info → StreamingCommunity-2.5.8.dist-info}/METADATA +56 -22
  49. StreamingCommunity-2.5.8.dist-info/RECORD +86 -0
  50. StreamingCommunity/Api/Site/1337xx/costant.py +0 -15
  51. StreamingCommunity/Api/Site/altadefinizionegratis/costant.py +0 -21
  52. StreamingCommunity/Api/Site/animeunity/costant.py +0 -21
  53. StreamingCommunity/Api/Site/cb01new/costant.py +0 -19
  54. StreamingCommunity/Api/Site/ddlstreamitaly/costant.py +0 -20
  55. StreamingCommunity/Api/Site/guardaserie/costant.py +0 -19
  56. StreamingCommunity/Api/Site/ilcorsaronero/costant.py +0 -19
  57. StreamingCommunity/Api/Site/mostraguarda/costant.py +0 -19
  58. StreamingCommunity/Api/Site/streamingcommunity/costant.py +0 -21
  59. StreamingCommunity/TelegramHelp/request_manager.py +0 -82
  60. StreamingCommunity/TelegramHelp/session.py +0 -56
  61. StreamingCommunity-2.5.7.dist-info/RECORD +0 -96
  62. {StreamingCommunity-2.5.7.dist-info → StreamingCommunity-2.5.8.dist-info}/LICENSE +0 -0
  63. {StreamingCommunity-2.5.7.dist-info → StreamingCommunity-2.5.8.dist-info}/WHEEL +0 -0
  64. {StreamingCommunity-2.5.7.dist-info → StreamingCommunity-2.5.8.dist-info}/entry_points.txt +0 -0
  65. {StreamingCommunity-2.5.7.dist-info → StreamingCommunity-2.5.8.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,71 @@
1
+ # 11.02.25
2
+
3
+ import os
4
+ import inspect
5
+
6
+
7
+ # Internal utilities
8
+ from StreamingCommunity.Util._jsonConfig import config_manager
9
+
10
+
11
+ def get_site_name_from_stack():
12
+ for frame_info in inspect.stack():
13
+ file_path = frame_info.filename
14
+
15
+ if "__init__" in file_path:
16
+ parts = file_path.split(f"Site{os.sep}")
17
+
18
+ if len(parts) > 1:
19
+ site_name = parts[1].split(os.sep)[0]
20
+ return site_name
21
+
22
+ return None
23
+
24
+
25
+ class SiteConstant:
26
+ @property
27
+ def SITE_NAME(self):
28
+ return get_site_name_from_stack()
29
+
30
+ @property
31
+ def ROOT_PATH(self):
32
+ return config_manager.get('DEFAULT', 'root_path')
33
+
34
+ @property
35
+ def DOMAIN_NOW(self):
36
+ return config_manager.get_dict('SITE', self.SITE_NAME)['domain']
37
+
38
+ @property
39
+ def SERIES_FOLDER(self):
40
+ base_path = self.ROOT_PATH
41
+ if config_manager.get_bool("DEFAULT", "add_siteName"):
42
+ base_path = os.path.join(base_path, self.SITE_NAME)
43
+ return os.path.join(base_path, config_manager.get('DEFAULT', 'serie_folder_name'))
44
+
45
+ @property
46
+ def MOVIE_FOLDER(self):
47
+ base_path = self.ROOT_PATH
48
+ if config_manager.get_bool("DEFAULT", "add_siteName"):
49
+ base_path = os.path.join(base_path, self.SITE_NAME)
50
+ return os.path.join(base_path, config_manager.get('DEFAULT', 'movie_folder_name'))
51
+
52
+ @property
53
+ def ANIME_FOLDER(self):
54
+ base_path = self.ROOT_PATH
55
+ if config_manager.get_bool("DEFAULT", "add_siteName"):
56
+ base_path = os.path.join(base_path, self.SITE_NAME)
57
+ return os.path.join(base_path, config_manager.get('DEFAULT', 'anime_folder_name'))
58
+
59
+ @property
60
+ def COOKIE(self):
61
+ try:
62
+ return config_manager.get_dict('SITE', self.SITE_NAME)['extra']
63
+ except KeyError:
64
+ return None
65
+
66
+ @property
67
+ def TELEGRAM_BOT(self):
68
+ return config_manager.get_bool('DEFAULT', 'telegram_bot')
69
+
70
+
71
+ site_constant = SiteConstant()
@@ -43,7 +43,7 @@ DOWNLOAD_SPECIFIC_SUBTITLE = config_manager.get_list('M3U8_DOWNLOAD', 'specific_
43
43
  MERGE_AUDIO = config_manager.get_bool('M3U8_DOWNLOAD', 'merge_audio')
44
44
  MERGE_SUBTITLE = config_manager.get_bool('M3U8_DOWNLOAD', 'merge_subs')
45
45
  CLEANUP_TMP = config_manager.get_bool('M3U8_DOWNLOAD', 'cleanup_tmp_folder')
46
- FILTER_CUSTOM_REOLUTION = config_manager.get_int('M3U8_PARSER', 'force_resolution')
46
+ FILTER_CUSTOM_REOLUTION = str(config_manager.get('M3U8_PARSER', 'force_resolution')).strip().lower()
47
47
  GET_ONLY_LINK = config_manager.get_bool('M3U8_PARSER', 'get_only_link')
48
48
  RETRY_LIMIT = config_manager.get_int('REQUESTS', 'max_retry')
49
49
  MAX_TIMEOUT = config_manager.get_int("REQUESTS", "timeout")
@@ -60,11 +60,11 @@ class HLSClient:
60
60
  def request(self, url: str, return_content: bool = False) -> Optional[httpx.Response]:
61
61
  """
62
62
  Makes HTTP GET requests with retry logic.
63
-
63
+
64
64
  Args:
65
65
  url: Target URL to request
66
66
  return_content: If True, returns response content instead of text
67
-
67
+
68
68
  Returns:
69
69
  Response content/text or None if all retries fail
70
70
  """
@@ -74,7 +74,7 @@ class HLSClient:
74
74
  response = client.get(url)
75
75
  response.raise_for_status()
76
76
  return response.content if return_content else response.text
77
-
77
+
78
78
  except Exception as e:
79
79
  logging.error(f"Attempt {attempt+1} failed: {str(e)}")
80
80
  time.sleep(1.5 ** attempt)
@@ -103,7 +103,7 @@ class PathManager:
103
103
  root = config_manager.get('DEFAULT', 'root_path')
104
104
  hash_name = compute_sha1_hash(self.m3u8_url) + ".mp4"
105
105
  return os.path.join(root, "undefined", hash_name)
106
-
106
+
107
107
  if not path.endswith(".mp4"):
108
108
  path += ".mp4"
109
109
 
@@ -148,7 +148,7 @@ class M3U8Manager:
148
148
  content = self.client.request(self.m3u8_url)
149
149
  if not content:
150
150
  raise ValueError("Failed to fetch M3U8 content")
151
-
151
+
152
152
  self.parser.parse_data(uri=self.m3u8_url, raw_content=content)
153
153
  self.url_fixer.set_playlist(self.m3u8_url)
154
154
  self.is_master = self.parser.is_master_playlist
@@ -159,18 +159,19 @@ class M3U8Manager:
159
159
  If it's a master playlist, only selects video stream.
160
160
  """
161
161
  if not self.is_master:
162
- if FILTER_CUSTOM_REOLUTION != -1:
163
- self.video_url, self.video_res = self.parser._video.get_custom_uri(y_resolution=FILTER_CUSTOM_REOLUTION)
164
- else:
165
- self.video_url, self.video_res = self.parser._video.get_best_uri()
166
-
162
+ self.video_url, self.video_res = self.m3u8_url, "0p"
167
163
  self.audio_streams = []
168
164
  self.sub_streams = []
169
-
165
+
170
166
  else:
171
- if FILTER_CUSTOM_REOLUTION != -1:
172
- self.video_url, self.video_res = self.parser._video.get_custom_uri(y_resolution=FILTER_CUSTOM_REOLUTION)
167
+ if str(FILTER_CUSTOM_REOLUTION) == "best":
168
+ self.video_url, self.video_res = self.parser._video.get_best_uri()
169
+ elif str(FILTER_CUSTOM_REOLUTION) == "worst":
170
+ self.video_url, self.video_res = self.parser._video.get_worst_uri()
171
+ elif "p" in str(FILTER_CUSTOM_REOLUTION):
172
+ self.video_url, self.video_res = self.parser._video.get_custom_uri(int(FILTER_CUSTOM_REOLUTION.replace("p", "")))
173
173
  else:
174
+ logging.error("Resolution not recognized.")
174
175
  self.video_url, self.video_res = self.parser._video.get_best_uri()
175
176
 
176
177
  self.audio_streams = []
@@ -188,17 +189,12 @@ class M3U8Manager:
188
189
  ]
189
190
 
190
191
  def log_selection(self):
191
- if FILTER_CUSTOM_REOLUTION == -1:
192
- set_resolution = "Best"
193
- else:
194
- set_resolution = f"{FILTER_CUSTOM_REOLUTION}p"
195
-
196
192
  tuple_available_resolution = self.parser._video.get_list_resolution()
197
193
  list_available_resolution = [f"{r[0]}x{r[1]}" for r in tuple_available_resolution]
198
194
 
199
195
  console.print(
200
196
  f"[cyan bold]Video →[/cyan bold] [green]Available:[/green] [purple]{', '.join(list_available_resolution)}[/purple] | "
201
- f"[red]Set:[/red] [purple]{set_resolution}[/purple] | "
197
+ f"[red]Set:[/red] [purple]{FILTER_CUSTOM_REOLUTION}[/purple] | "
202
198
  f"[yellow]Downloadable:[/yellow] [purple]{self.video_res[0]}x{self.video_res[1]}[/purple]"
203
199
  )
204
200
 
@@ -264,13 +260,14 @@ class DownloadManager:
264
260
 
265
261
  if result.get('stopped', False):
266
262
  self.stopped = True
263
+
267
264
  return self.stopped
268
265
 
269
266
  def download_audio(self, audio: Dict):
270
267
  """Downloads audio segments for a specific language track."""
271
- if self.stopped:
272
- return True
273
-
268
+ #if self.stopped:
269
+ # return True
270
+
274
271
  audio_full_url = self.url_fixer.generate_full_url(audio['uri'])
275
272
  audio_tmp_dir = os.path.join(self.temp_dir, 'audio', audio['language'])
276
273
 
@@ -284,14 +281,20 @@ class DownloadManager:
284
281
 
285
282
  def download_subtitle(self, sub: Dict):
286
283
  """Downloads and saves subtitle file for a specific language."""
287
- if self.stopped:
288
- return True
289
-
290
- content = self.client.request(sub['uri'])
291
- if content:
284
+ #if self.stopped:
285
+ # return True
286
+
287
+ raw_content = self.client.request(sub['uri'])
288
+ if raw_content:
292
289
  sub_path = os.path.join(self.temp_dir, 'subs', f"{sub['language']}.vtt")
293
- with open(sub_path, 'w', encoding='utf-8') as f:
294
- f.write(content)
290
+
291
+ subtitle_parser = M3U8_Parser()
292
+ subtitle_parser.parse_data(sub['uri'], raw_content)
293
+
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)
295
298
 
296
299
  return self.stopped
297
300
 
@@ -299,30 +302,35 @@ class DownloadManager:
299
302
  """
300
303
  Downloads all selected streams (video, audio, subtitles).
301
304
  """
305
+ return_stopped = False
306
+
302
307
  video_file = os.path.join(self.temp_dir, 'video', '0.ts')
303
308
  if not os.path.exists(video_file):
304
309
  if self.download_video(video_url):
305
- return True
306
-
310
+ if not return_stopped:
311
+ return_stopped = True
312
+
307
313
  for audio in audio_streams:
308
- if self.stopped:
309
- break
314
+ #if self.stopped:
315
+ # break
310
316
 
311
317
  audio_file = os.path.join(self.temp_dir, 'audio', audio['language'], '0.ts')
312
318
  if not os.path.exists(audio_file):
313
319
  if self.download_audio(audio):
314
- return True
320
+ if not return_stopped:
321
+ return_stopped = True
315
322
 
316
323
  for sub in sub_streams:
317
- if self.stopped:
318
- break
324
+ #if self.stopped:
325
+ # break
319
326
 
320
327
  sub_file = os.path.join(self.temp_dir, 'subs', f"{sub['language']}.vtt")
321
328
  if not os.path.exists(sub_file):
322
329
  if self.download_subtitle(sub):
323
- return True
324
-
325
- return self.stopped
330
+ if not return_stopped:
331
+ return_stopped = True
332
+
333
+ return return_stopped
326
334
 
327
335
 
328
336
  class MergeManager:
@@ -344,7 +352,7 @@ class MergeManager:
344
352
  """
345
353
  Merges downloaded streams into final video file.
346
354
  Returns path to the final merged file.
347
-
355
+
348
356
  Process:
349
357
  1. If no audio/subs, just process video
350
358
  2. If audio exists, merge with video
@@ -387,7 +395,7 @@ class MergeManager:
387
395
  subtitles_list=sub_tracks,
388
396
  out_path=merged_subs_path
389
397
  )
390
-
398
+
391
399
  return merged_file
392
400
 
393
401
 
@@ -404,14 +412,16 @@ class HLS_Downloader:
404
412
  def start(self) -> Dict[str, Any]:
405
413
  """
406
414
  Main execution flow with handling for both index and playlist M3U8s.
407
-
415
+
408
416
  Returns:
409
417
  Dict containing:
410
418
  - path: Output file path
411
419
  - url: Original M3U8 URL
412
420
  - is_master: Whether the M3U8 was a master playlist
413
421
  Or raises an exception if there's an error
414
- """
422
+ """
423
+ console.print(f"[cyan]You can safely stop the download with [bold]Ctrl+c[bold] [cyan] \n")
424
+
415
425
  if TELEGRAM_BOT:
416
426
  bot = get_bot_instance()
417
427
 
@@ -421,12 +431,12 @@ class HLS_Downloader:
421
431
  response = {
422
432
  'path': self.path_manager.output_path,
423
433
  'url': self.m3u8_url,
424
- 'is_master': False,
434
+ 'is_master': False,
425
435
  'error': 'File already exists',
426
436
  'stopped': False
427
437
  }
428
438
  if TELEGRAM_BOT:
429
- bot.send_message(response)
439
+ bot.send_message(f"Contenuto già scaricato!", None)
430
440
  return response
431
441
 
432
442
  self.path_manager.setup_directories()
@@ -441,7 +451,7 @@ class HLS_Downloader:
441
451
  client=self.client,
442
452
  url_fixer=self.m3u8_manager.url_fixer
443
453
  )
444
-
454
+
445
455
  # Check if download was stopped
446
456
  download_stopped = self.download_manager.download_all(
447
457
  video_url=self.m3u8_manager.video_url,
@@ -449,15 +459,6 @@ class HLS_Downloader:
449
459
  sub_streams=self.m3u8_manager.sub_streams
450
460
  )
451
461
 
452
- if download_stopped:
453
- return {
454
- 'path': None,
455
- 'url': self.m3u8_url,
456
- 'is_master': self.m3u8_manager.is_master,
457
- 'error': 'Download stopped by user',
458
- 'stopped': True
459
- }
460
-
461
462
  self.merge_manager = MergeManager(
462
463
  temp_dir=self.path_manager.temp_dir,
463
464
  parser=self.m3u8_manager.parser,
@@ -475,14 +476,14 @@ class HLS_Downloader:
475
476
  'path': self.path_manager.output_path,
476
477
  'url': self.m3u8_url,
477
478
  'is_master': self.m3u8_manager.is_master,
478
- 'stopped': False
479
+ 'stopped': download_stopped
479
480
  }
480
481
 
481
482
  except Exception as e:
482
483
  error_msg = str(e)
483
484
  console.print(f"[red]Download failed: {error_msg}[/red]")
484
485
  logging.error("Download error", exc_info=True)
485
-
486
+
486
487
  return {
487
488
  'path': None,
488
489
  'url': self.m3u8_url,
@@ -490,7 +491,7 @@ class HLS_Downloader:
490
491
  'error': error_msg,
491
492
  'stopped': False
492
493
  }
493
-
494
+
494
495
  def _print_summary(self):
495
496
  """Prints download summary including file size, duration, and any missing segments."""
496
497
  if TELEGRAM_BOT:
@@ -47,6 +47,9 @@ PROXY_START_MAX = config_manager.get_float('REQUESTS', 'proxy_start_max')
47
47
  DEFAULT_VIDEO_WORKERS = config_manager.get_int('M3U8_DOWNLOAD', 'default_video_workser')
48
48
  DEFAULT_AUDIO_WORKERS = config_manager.get_int('M3U8_DOWNLOAD', 'default_audio_workser')
49
49
  MAX_TIMEOOUT = config_manager.get_int("REQUESTS", "timeout")
50
+ MAX_INTERRUPT_COUNT = 3
51
+ SEGMENT_MAX_TIMEOUT = config_manager.get_int("M3U8_DOWNLOAD", "segment_timeout")
52
+ TELEGRAM_BOT = config_manager.get_bool('DEFAULT', 'telegram_bot')
50
53
 
51
54
 
52
55
 
@@ -82,12 +85,14 @@ class M3U8_Segments:
82
85
  # Stopping
83
86
  self.interrupt_flag = threading.Event()
84
87
  self.download_interrupted = False
88
+ self.interrupt_count = 0
89
+ self.force_stop = False
90
+ self.interrupt_lock = threading.Lock()
85
91
 
86
92
  # OTHER INFO
87
93
  self.info_maxRetry = 0
88
94
  self.info_nRetry = 0
89
95
  self.info_nFailed = 0
90
-
91
96
  self.active_retries = 0
92
97
  self.active_retries_lock = threading.Lock()
93
98
 
@@ -156,12 +161,24 @@ class M3U8_Segments:
156
161
  Set up a signal handler for graceful interruption.
157
162
  """
158
163
  def interrupt_handler(signum, frame):
159
- if not self.interrupt_flag.is_set():
160
- console.log("\n[red] Stopping download gracefully...")
161
- self.interrupt_flag.set()
162
- self.download_interrupted = True
163
- self.stop_event.set()
164
+ with self.interrupt_lock:
165
+ self.interrupt_count += 1
166
+ if self.interrupt_count >= MAX_INTERRUPT_COUNT:
167
+ self.force_stop = True
168
+
169
+ if self.force_stop:
170
+ console.print("\n[red]Force stop triggered! Exiting immediately.")
171
+
172
+ else:
173
+ if not self.interrupt_flag.is_set():
174
+ remaining = MAX_INTERRUPT_COUNT - self.interrupt_count
175
+ console.print(f"\n[red]- Stopping gracefully... (Ctrl+C {remaining}x to force)")
176
+ self.download_interrupted = True
177
+
178
+ if remaining == 1:
179
+ self.interrupt_flag.set()
164
180
 
181
+
165
182
  if threading.current_thread() is threading.main_thread():
166
183
  signal.signal(signal.SIGINT, interrupt_handler)
167
184
  else:
@@ -169,8 +186,9 @@ class M3U8_Segments:
169
186
 
170
187
  def _get_http_client(self, index: int = None):
171
188
  client_params = {
172
- 'headers': random_headers(self.key_base_url) if hasattr(self, 'key_base_url') else {'User-Agent': get_headers()},
173
- 'timeout': MAX_TIMEOOUT,
189
+ #'headers': random_headers(self.key_base_url) if hasattr(self, 'key_base_url') else {'User-Agent': get_headers()},
190
+ 'headers': {'User-Agent': get_headers()},
191
+ 'timeout': SEGMENT_MAX_TIMEOUT,
174
192
  'follow_redirects': True,
175
193
  'http2': False
176
194
  }
@@ -189,7 +207,7 @@ class M3U8_Segments:
189
207
  - index (int): The index of the segment.
190
208
  - progress_bar (tqdm): Progress counter for tracking download progress.
191
209
  - backoff_factor (float): The backoff factor for exponential backoff (default is 1.5 seconds).
192
- """
210
+ """
193
211
  for attempt in range(REQUEST_MAX_RETRY):
194
212
  if self.interrupt_flag.is_set():
195
213
  return
@@ -291,6 +309,8 @@ class M3U8_Segments:
291
309
 
292
310
  except queue.Empty:
293
311
  self.current_timeout = min(MAX_TIMEOOUT, self.current_timeout * 1.1)
312
+ time.sleep(0.05)
313
+
294
314
  if self.stop_event.is_set():
295
315
  break
296
316
 
@@ -305,6 +325,11 @@ class M3U8_Segments:
305
325
  - description: Description to insert on tqdm bar
306
326
  - type (str): Type of download: 'video' or 'audio'
307
327
  """
328
+ if TELEGRAM_BOT:
329
+
330
+ # Viene usato per lo screen
331
+ console.log("####")
332
+
308
333
  self.get_info()
309
334
  self.setup_interrupt_handler()
310
335
 
@@ -313,7 +338,9 @@ class M3U8_Segments:
313
338
  unit='s',
314
339
  ascii='░▒█',
315
340
  bar_format=self._get_bar_format(description),
316
- mininterval=0.05
341
+ mininterval=0.6,
342
+ maxinterval=1.0,
343
+ file=sys.stdout, # Using file=sys.stdout to force in-place updates because sys.stderr may not support carriage returns in this environment.
317
344
  )
318
345
 
319
346
  try:
@@ -382,7 +409,6 @@ class M3U8_Segments:
382
409
  return (
383
410
  f"{Colors.YELLOW}Proc{Colors.WHITE}: "
384
411
  f"{Colors.RED}{{percentage:.2f}}% "
385
- f"{Colors.WHITE}| "
386
412
  f"{Colors.CYAN}{{remaining}}{{postfix}} {Colors.WHITE}]"
387
413
  )
388
414
 
@@ -391,7 +417,7 @@ class M3U8_Segments:
391
417
  f"{Colors.YELLOW}[HLS] {Colors.WHITE}({Colors.CYAN}{description}{Colors.WHITE}): "
392
418
  f"{Colors.RED}{{percentage:.2f}}% "
393
419
  f"{Colors.MAGENTA}{{bar}} "
394
- f"{Colors.WHITE}[ {Colors.YELLOW}{{elapsed}}{Colors.WHITE} < {Colors.CYAN}{{remaining}}{Colors.WHITE}{{postfix}}{Colors.WHITE} ]"
420
+ f"{Colors.YELLOW}{{elapsed}}{Colors.WHITE} < {Colors.CYAN}{{remaining}}{Colors.WHITE}{{postfix}}{Colors.WHITE}"
395
421
  )
396
422
 
397
423
  def _get_worker_count(self, stream_type: str) -> int:
@@ -428,8 +454,8 @@ class M3U8_Segments:
428
454
  writer_thread.join(timeout=30)
429
455
  progress_bar.close()
430
456
 
431
- if self.download_interrupted:
432
- console.print("\n[red]Download terminated by user")
457
+ #if self.download_interrupted:
458
+ # console.print("\n[red]Download terminated by user")
433
459
 
434
460
  if self.info_nFailed > 0:
435
461
  self._display_error_summary()
@@ -3,6 +3,7 @@
3
3
  import os
4
4
  import re
5
5
  import sys
6
+ import time
6
7
  import signal
7
8
  import logging
8
9
  from functools import partial
@@ -39,21 +40,38 @@ TELEGRAM_BOT = config_manager.get_bool('DEFAULT', 'telegram_bot')
39
40
 
40
41
 
41
42
 
42
- def signal_handler(signum, frame, kill_handler):
43
- """Signal handler for graceful interruption"""
44
- kill_handler[0] = True
45
- print("\nReceived interrupt signal. Completing current download...")
43
+ class InterruptHandler:
44
+ def __init__(self):
45
+ self.interrupt_count = 0
46
+ self.last_interrupt_time = 0
47
+ self.kill_download = False
48
+ self.force_quit = False
46
49
 
50
+ def signal_handler(signum, frame, interrupt_handler, original_handler):
51
+ """Enhanced signal handler for multiple interrupt scenarios"""
52
+ current_time = time.time()
53
+
54
+ # Reset counter if more than 2 seconds have passed since last interrupt
55
+ if current_time - interrupt_handler.last_interrupt_time > 2:
56
+ interrupt_handler.interrupt_count = 0
57
+
58
+ interrupt_handler.interrupt_count += 1
59
+ interrupt_handler.last_interrupt_time = current_time
60
+
61
+ if interrupt_handler.interrupt_count == 1:
62
+ interrupt_handler.kill_download = True
63
+ console.print("\n[bold yellow]First interrupt received. Download will complete and save. Press Ctrl+C three times quickly to force quit.[/bold yellow]")
64
+
65
+ elif interrupt_handler.interrupt_count >= 3:
66
+ interrupt_handler.force_quit = True
67
+ console.print("\n[bold red]Force quit activated. Saving partial download...[/bold red]")
68
+ signal.signal(signum, original_handler)
47
69
 
48
70
  def MP4_downloader(url: str, path: str, referer: str = None, headers_: dict = None):
49
71
  """
50
- Downloads an MP4 video from a given URL with robust error handling and SSL bypass.
51
-
52
- Parameters:
53
- - url (str): The URL of the MP4 video to download.
54
- - path (str): The local path where the downloaded MP4 file will be saved.
55
- - referer (str, optional): The referer header value.
56
- - headers_ (dict, optional): Custom headers for the request.
72
+ Downloads an MP4 video with enhanced interrupt handling.
73
+ - Single Ctrl+C: Completes download gracefully
74
+ - Triple Ctrl+C: Saves partial download and exits
57
75
  """
58
76
  if TELEGRAM_BOT:
59
77
  bot = get_bot_instance()
@@ -65,23 +83,19 @@ def MP4_downloader(url: str, path: str, referer: str = None, headers_: dict = No
65
83
  bot.send_message(f"Contenuto già scaricato!", None)
66
84
  return 400
67
85
 
68
- # Early return for link-only mode
69
86
  if GET_ONLY_LINK:
70
87
  return {'path': path, 'url': url}
71
88
 
72
- # Validate URL
73
89
  if not (url.lower().startswith('http://') or url.lower().startswith('https://')):
74
90
  logging.error(f"Invalid URL: {url}")
75
91
  console.print(f"[bold red]Invalid URL: {url}[/bold red]")
76
92
  return None
77
93
 
78
- # Prepare headers
79
94
  try:
80
95
  headers = {}
81
96
  if referer:
82
97
  headers['Referer'] = referer
83
98
 
84
- # Use custom headers if provided, otherwise use default user agent
85
99
  if headers_:
86
100
  headers.update(headers_)
87
101
  else:
@@ -93,17 +107,12 @@ def MP4_downloader(url: str, path: str, referer: str = None, headers_: dict = No
93
107
  return None
94
108
 
95
109
  temp_path = f"{path}.temp"
96
- kill_handler = [False] # Using list for mutable state
97
- original_handler = signal.signal(signal.SIGINT, partial(signal_handler, kill_handler=kill_handler))
110
+ interrupt_handler = InterruptHandler()
111
+ original_handler = signal.signal(signal.SIGINT, partial(signal_handler, interrupt_handler=interrupt_handler, original_handler=signal.getsignal(signal.SIGINT)))
98
112
 
99
113
  try:
100
- # Create a custom transport that bypasses SSL verification
101
- transport = httpx.HTTPTransport(
102
- verify=False,
103
- http2=True
104
- )
114
+ transport = httpx.HTTPTransport(verify=False, http2=True)
105
115
 
106
- # Download with streaming and progress tracking
107
116
  with httpx.Client(transport=transport, timeout=httpx.Timeout(60)) as client:
108
117
  with client.stream("GET", url, headers=headers, timeout=REQUEST_TIMEOUT) as response:
109
118
  response.raise_for_status()
@@ -119,21 +128,22 @@ def MP4_downloader(url: str, path: str, referer: str = None, headers_: dict = No
119
128
  bar_format=f"{Colors.YELLOW}[MP4]{Colors.WHITE}: "
120
129
  f"{Colors.RED}{{percentage:.2f}}% {Colors.MAGENTA}{{bar}} {Colors.WHITE}[ "
121
130
  f"{Colors.YELLOW}{{n_fmt}}{Colors.WHITE} / {Colors.RED}{{total_fmt}} {Colors.WHITE}] "
122
- f"{Colors.YELLOW}{{elapsed}} {Colors.WHITE}< {Colors.CYAN}{{remaining}} {Colors.WHITE}| "
123
- f"{Colors.YELLOW}{{rate_fmt}}{{postfix}} {Colors.WHITE}]",
131
+ f"{Colors.YELLOW}{{elapsed}} {Colors.WHITE}< {Colors.CYAN}{{remaining}}{Colors.WHITE}, "
132
+ f"{Colors.YELLOW}{{rate_fmt}}{{postfix}} ",
124
133
  unit='iB',
125
134
  unit_scale=True,
126
135
  desc='Downloading',
127
- mininterval=0.05
136
+ mininterval=0.05,
137
+ file=sys.stdout # Using file=sys.stdout to force in-place updates because sys.stderr may not support carriage returns in this environment.
128
138
  )
129
139
 
130
140
  downloaded = 0
131
141
  with open(temp_path, 'wb') as file, progress_bar as bar:
132
142
  try:
133
143
  for chunk in response.iter_bytes(chunk_size=1024):
134
- if kill_handler[0]:
135
- console.print("\n[bold yellow]Interrupting download...[/bold yellow]")
136
- return None, True
144
+ if interrupt_handler.force_quit:
145
+ console.print("\n[bold red]Force quitting... Saving partial download.[/bold red]")
146
+ break
137
147
 
138
148
  if chunk:
139
149
  size = file.write(chunk)
@@ -141,18 +151,15 @@ def MP4_downloader(url: str, path: str, referer: str = None, headers_: dict = No
141
151
  bar.update(size)
142
152
 
143
153
  except KeyboardInterrupt:
144
- console.print("\n[bold red]Download interrupted by user.[/bold red]")
145
- if os.path.exists(temp_path):
146
- os.remove(temp_path)
147
- return None, True
154
+ if not interrupt_handler.force_quit:
155
+ interrupt_handler.kill_download = True
148
156
 
149
- # Rename temp file to final file
150
157
  if os.path.exists(temp_path):
151
158
  os.rename(temp_path, path)
152
159
 
153
160
  if os.path.exists(path):
154
161
  console.print(Panel(
155
- f"[bold green]Download completed![/bold green]\n"
162
+ f"[bold green]Download completed{' (Partial)' if interrupt_handler.force_quit else ''}![/bold green]\n"
156
163
  f"[cyan]File size: [bold red]{internet_manager.format_file_size(os.path.getsize(path))}[/bold red]\n"
157
164
  f"[cyan]Duration: [bold]{print_duration_table(path, description=False, return_string=True)}[/bold]",
158
165
  title=f"{os.path.basename(path.replace('.mp4', ''))}",
@@ -160,22 +167,22 @@ def MP4_downloader(url: str, path: str, referer: str = None, headers_: dict = No
160
167
  ))
161
168
 
162
169
  if TELEGRAM_BOT:
163
- message = f"Download completato\nDimensione: {internet_manager.format_file_size(os.path.getsize(path))}\nDurata: {print_duration_table(path, description=False, return_string=True)}\nTitolo: {os.path.basename(path.replace('.mp4', ''))}"
170
+ message = f"Download completato{'(Parziale)' if interrupt_handler.force_quit else ''}\nDimensione: {internet_manager.format_file_size(os.path.getsize(path))}\nDurata: {print_duration_table(path, description=False, return_string=True)}\nTitolo: {os.path.basename(path.replace('.mp4', ''))}"
164
171
  clean_message = re.sub(r'\[[a-zA-Z]+\]', '', message)
165
172
  bot.send_message(clean_message, None)
166
173
 
167
- return path, kill_handler[0]
174
+ return path, interrupt_handler.kill_download
175
+
168
176
  else:
169
177
  console.print("[bold red]Download failed or file is empty.[/bold red]")
170
- return None, kill_handler[0]
178
+ return None, interrupt_handler.kill_download
171
179
 
172
180
  except Exception as e:
173
181
  logging.error(f"Unexpected error: {e}")
174
182
  console.print(f"[bold red]Unexpected Error: {e}[/bold red]")
175
183
  if os.path.exists(temp_path):
176
184
  os.remove(temp_path)
177
- return None, kill_handler[0]
185
+ return None, interrupt_handler.kill_download
178
186
 
179
187
  finally:
180
- # Restore original signal handler
181
188
  signal.signal(signal.SIGINT, original_handler)