StreamingCommunity 3.2.7__py3-none-any.whl → 3.2.9__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 (79) hide show
  1. StreamingCommunity/Api/Player/Helper/Vixcloud/util.py +2 -1
  2. StreamingCommunity/Api/Player/hdplayer.py +2 -2
  3. StreamingCommunity/Api/Player/sweetpixel.py +5 -8
  4. StreamingCommunity/Api/Site/altadefinizione/__init__.py +2 -2
  5. StreamingCommunity/Api/Site/altadefinizione/film.py +10 -8
  6. StreamingCommunity/Api/Site/altadefinizione/series.py +9 -7
  7. StreamingCommunity/Api/Site/altadefinizione/site.py +1 -1
  8. StreamingCommunity/Api/Site/animeunity/__init__.py +2 -2
  9. StreamingCommunity/Api/Site/animeunity/serie.py +2 -2
  10. StreamingCommunity/Api/Site/animeworld/site.py +3 -5
  11. StreamingCommunity/Api/Site/animeworld/util/ScrapeSerie.py +8 -10
  12. StreamingCommunity/Api/Site/cb01new/film.py +7 -5
  13. StreamingCommunity/Api/Site/crunchyroll/__init__.py +1 -3
  14. StreamingCommunity/Api/Site/crunchyroll/film.py +9 -7
  15. StreamingCommunity/Api/Site/crunchyroll/series.py +9 -7
  16. StreamingCommunity/Api/Site/crunchyroll/site.py +10 -1
  17. StreamingCommunity/Api/Site/guardaserie/series.py +8 -6
  18. StreamingCommunity/Api/Site/guardaserie/site.py +0 -3
  19. StreamingCommunity/Api/Site/guardaserie/util/ScrapeSerie.py +1 -2
  20. StreamingCommunity/Api/Site/mediasetinfinity/__init__.py +1 -1
  21. StreamingCommunity/Api/Site/mediasetinfinity/film.py +10 -16
  22. StreamingCommunity/Api/Site/mediasetinfinity/series.py +12 -18
  23. StreamingCommunity/Api/Site/mediasetinfinity/site.py +11 -3
  24. StreamingCommunity/Api/Site/mediasetinfinity/util/ScrapeSerie.py +214 -180
  25. StreamingCommunity/Api/Site/mediasetinfinity/util/get_license.py +2 -31
  26. StreamingCommunity/Api/Site/raiplay/__init__.py +1 -1
  27. StreamingCommunity/Api/Site/raiplay/film.py +41 -10
  28. StreamingCommunity/Api/Site/raiplay/series.py +44 -12
  29. StreamingCommunity/Api/Site/raiplay/site.py +4 -1
  30. StreamingCommunity/Api/Site/raiplay/util/ScrapeSerie.py +2 -1
  31. StreamingCommunity/Api/Site/raiplay/util/get_license.py +40 -0
  32. StreamingCommunity/Api/Site/streamingcommunity/__init__.py +0 -1
  33. StreamingCommunity/Api/Site/streamingcommunity/film.py +7 -5
  34. StreamingCommunity/Api/Site/streamingcommunity/series.py +9 -7
  35. StreamingCommunity/Api/Site/streamingcommunity/site.py +4 -2
  36. StreamingCommunity/Api/Site/streamingwatch/film.py +7 -5
  37. StreamingCommunity/Api/Site/streamingwatch/series.py +8 -6
  38. StreamingCommunity/Api/Site/streamingwatch/site.py +3 -1
  39. StreamingCommunity/Api/Site/streamingwatch/util/ScrapeSerie.py +3 -3
  40. StreamingCommunity/Api/Template/Util/__init__.py +10 -1
  41. StreamingCommunity/Api/Template/Util/manage_ep.py +4 -4
  42. StreamingCommunity/Api/Template/__init__.py +5 -1
  43. StreamingCommunity/Api/Template/site.py +10 -6
  44. StreamingCommunity/Lib/Downloader/DASH/cdm_helpher.py +5 -12
  45. StreamingCommunity/Lib/Downloader/DASH/decrypt.py +1 -1
  46. StreamingCommunity/Lib/Downloader/DASH/downloader.py +1 -1
  47. StreamingCommunity/Lib/Downloader/DASH/parser.py +1 -1
  48. StreamingCommunity/Lib/Downloader/DASH/segments.py +4 -3
  49. StreamingCommunity/Lib/Downloader/HLS/downloader.py +11 -9
  50. StreamingCommunity/Lib/Downloader/HLS/segments.py +253 -144
  51. StreamingCommunity/Lib/Downloader/MP4/downloader.py +4 -3
  52. StreamingCommunity/Lib/Downloader/TOR/downloader.py +3 -5
  53. StreamingCommunity/Lib/Downloader/__init__.py +9 -1
  54. StreamingCommunity/Lib/FFmpeg/__init__.py +10 -1
  55. StreamingCommunity/Lib/FFmpeg/command.py +4 -6
  56. StreamingCommunity/Lib/FFmpeg/util.py +1 -1
  57. StreamingCommunity/Lib/M3U8/__init__.py +9 -1
  58. StreamingCommunity/Lib/M3U8/decryptor.py +8 -4
  59. StreamingCommunity/Lib/M3U8/estimator.py +0 -6
  60. StreamingCommunity/Lib/M3U8/parser.py +1 -1
  61. StreamingCommunity/Lib/M3U8/url_fixer.py +1 -1
  62. StreamingCommunity/Lib/TMBD/__init__.py +6 -1
  63. StreamingCommunity/TelegramHelp/config.json +1 -5
  64. StreamingCommunity/TelegramHelp/telegram_bot.py +9 -10
  65. StreamingCommunity/Upload/version.py +2 -2
  66. StreamingCommunity/Util/config_json.py +139 -59
  67. StreamingCommunity/Util/http_client.py +201 -0
  68. StreamingCommunity/Util/message.py +1 -1
  69. StreamingCommunity/Util/os.py +5 -5
  70. StreamingCommunity/Util/table.py +3 -3
  71. StreamingCommunity/__init__.py +9 -1
  72. StreamingCommunity/run.py +396 -260
  73. {streamingcommunity-3.2.7.dist-info → streamingcommunity-3.2.9.dist-info}/METADATA +143 -45
  74. streamingcommunity-3.2.9.dist-info/RECORD +113 -0
  75. streamingcommunity-3.2.7.dist-info/RECORD +0 -111
  76. {streamingcommunity-3.2.7.dist-info → streamingcommunity-3.2.9.dist-info}/WHEEL +0 -0
  77. {streamingcommunity-3.2.7.dist-info → streamingcommunity-3.2.9.dist-info}/entry_points.txt +0 -0
  78. {streamingcommunity-3.2.7.dist-info → streamingcommunity-3.2.9.dist-info}/licenses/LICENSE +0 -0
  79. {streamingcommunity-3.2.7.dist-info → streamingcommunity-3.2.9.dist-info}/top_level.txt +0 -0
@@ -2,10 +2,15 @@
2
2
 
3
3
  import os
4
4
  import sys
5
- import asyncio
5
+ import time
6
+ import queue
7
+ import signal
6
8
  import logging
7
9
  import binascii
10
+ import threading
11
+ from queue import PriorityQueue
8
12
  from urllib.parse import urljoin, urlparse
13
+ from concurrent.futures import ThreadPoolExecutor, as_completed
9
14
  from typing import Dict
10
15
 
11
16
 
@@ -18,6 +23,7 @@ from rich.console import Console
18
23
  # Internal utilities
19
24
  from StreamingCommunity.Util.color import Colors
20
25
  from StreamingCommunity.Util.headers import get_userAgent
26
+ from StreamingCommunity.Util.http_client import create_client
21
27
  from StreamingCommunity.Util.config_json import config_manager
22
28
 
23
29
 
@@ -37,7 +43,8 @@ DEFAULT_VIDEO_WORKERS = config_manager.get_int('M3U8_DOWNLOAD', 'default_video_w
37
43
  DEFAULT_AUDIO_WORKERS = config_manager.get_int('M3U8_DOWNLOAD', 'default_audio_workers')
38
44
  MAX_TIMEOOUT = config_manager.get_int("REQUESTS", "timeout")
39
45
  SEGMENT_MAX_TIMEOUT = config_manager.get_int("M3U8_DOWNLOAD", "segment_timeout")
40
-
46
+ TELEGRAM_BOT = config_manager.get_bool('DEFAULT', 'telegram_bot')
47
+ MAX_INTERRUPT_COUNT = 3
41
48
 
42
49
  # Variable
43
50
  console = Console()
@@ -56,18 +63,38 @@ class M3U8_Segments:
56
63
  self.url = url
57
64
  self.tmp_folder = tmp_folder
58
65
  self.is_index_url = is_index_url
66
+ self.expected_real_time = None
59
67
  self.tmp_file_path = os.path.join(self.tmp_folder, "0.ts")
60
68
  os.makedirs(self.tmp_folder, exist_ok=True)
61
69
 
62
70
  # Util class
63
71
  self.decryption: M3U8_Decryption = None
72
+ self.class_ts_estimator = M3U8_Ts_Estimator(0, self)
64
73
  self.class_url_fixer = M3U8_UrlFix(url)
65
-
66
- # Download tracking
74
+
75
+ # Sync
76
+ self.queue = PriorityQueue()
77
+ self.buffer = {}
78
+ self.expected_index = 0
79
+
80
+ self.stop_event = threading.Event()
67
81
  self.downloaded_segments = set()
82
+ self.base_timeout = 0.5
83
+ self.current_timeout = 3.0
84
+
85
+ # Stopping
86
+ self.interrupt_flag = threading.Event()
68
87
  self.download_interrupted = False
69
- self.info_nFailed = 0
88
+ self.interrupt_count = 0
89
+ self.force_stop = False
90
+ self.interrupt_lock = threading.Lock()
91
+
92
+ # OTHER INFO
93
+ self.info_maxRetry = 0
70
94
  self.info_nRetry = 0
95
+ self.info_nFailed = 0
96
+ self.active_retries = 0
97
+ self.active_retries_lock = threading.Lock()
71
98
 
72
99
  def __get_key__(self, m3u8_parser: M3U8_Parser) -> bytes:
73
100
  """
@@ -119,10 +146,16 @@ class M3U8_Segments:
119
146
  if "http" not in seg else seg
120
147
  for seg in m3u8_parser.segments
121
148
  ]
149
+ self.class_ts_estimator.total_segments = len(self.segments)
122
150
 
123
151
  def get_info(self) -> None:
124
152
  """
125
153
  Retrieves M3U8 playlist information from the given URL.
154
+
155
+ If the URL is an index URL, this method:
156
+ - Sends an HTTP GET request to fetch the M3U8 playlist.
157
+ - Parses the M3U8 content using `parse_data`.
158
+ - Saves the playlist to a temporary folder.
126
159
  """
127
160
  if self.is_index_url:
128
161
  try:
@@ -137,156 +170,235 @@ class M3U8_Segments:
137
170
  except Exception as e:
138
171
  raise RuntimeError(f"M3U8 info retrieval failed: {e}")
139
172
 
140
- def download_streams(self, description: str, type: str):
173
+ def setup_interrupt_handler(self):
141
174
  """
142
- Synchronous wrapper for async download.
175
+ Set up a signal handler for graceful interruption.
143
176
  """
144
- try:
145
- return asyncio.run(self.download_segments(description=description, type=type))
146
-
147
- except KeyboardInterrupt:
148
- self.download_interrupted = True
149
- console.print("\n[red]Download interrupted by user (Ctrl+C).")
150
- return self._generate_results(type)
177
+ def interrupt_handler(signum, frame):
178
+ with self.interrupt_lock:
179
+ self.interrupt_count += 1
180
+ if self.interrupt_count >= MAX_INTERRUPT_COUNT:
181
+ self.force_stop = True
182
+
183
+ if self.force_stop:
184
+ console.print("\n[red]Force stop triggered! Exiting immediately.")
151
185
 
152
- async def download_segments(self, description: str, type: str, concurrent_downloads: int = 8):
153
- """
154
- Download segments asynchronously.
186
+ else:
187
+ if not self.interrupt_flag.is_set():
188
+ remaining = MAX_INTERRUPT_COUNT - self.interrupt_count
189
+ console.print(f"\n[red]- Stopping gracefully... (Ctrl+C {remaining}x to force)")
190
+ self.download_interrupted = True
191
+
192
+ if remaining == 1:
193
+ self.interrupt_flag.set()
194
+
195
+
196
+ if threading.current_thread() is threading.main_thread():
197
+ signal.signal(signal.SIGINT, interrupt_handler)
198
+ else:
199
+ print("Signal handler must be set in the main thread")
200
+
201
+ def _get_http_client(self):
202
+ return create_client(headers={'User-Agent': get_userAgent()}, follow_redirects=True)
203
+
204
+ def download_segment(self, ts_url: str, index: int, progress_bar: tqdm, backoff_factor: float = 1.1) -> None:
155
205
  """
156
- self.get_info()
157
-
158
- progress_bar = tqdm(
159
- total=len(self.segments),
160
- unit='s',
161
- ascii='░▒█',
162
- bar_format=self._get_bar_format(description),
163
- mininterval=0.6,
164
- maxinterval=1.0,
165
- file=sys.stdout
166
- )
206
+ Downloads a TS segment and adds it to the segment queue with retry logic.
167
207
 
168
- # Initialize estimator
169
- estimator = M3U8_Ts_Estimator(total_segments=len(self.segments))
170
- semaphore = asyncio.Semaphore(self._get_worker_count(type))
171
-
172
- results = [None] * len(self.segments)
208
+ Parameters:
209
+ - ts_url (str): The URL of the TS segment.
210
+ - index (int): The index of the segment.
211
+ - progress_bar (tqdm): Progress counter for tracking download progress.
212
+ - backoff_factor (float): The backoff factor for exponential backoff (default is 1.5 seconds).
213
+ """
214
+ for attempt in range(REQUEST_MAX_RETRY):
215
+ if self.interrupt_flag.is_set():
216
+ return
217
+
218
+ try:
219
+ with self._get_http_client() as client:
220
+ response = client.get(ts_url)
173
221
 
174
- try:
175
- async with httpx.AsyncClient(timeout=SEGMENT_MAX_TIMEOUT) as client:
222
+ # Validate response and content
223
+ response.raise_for_status()
224
+ segment_content = response.content
225
+ content_size = len(segment_content)
226
+
227
+ # Decrypt if needed and verify decrypted content
228
+ if self.decryption is not None:
229
+ try:
230
+ segment_content = self.decryption.decrypt(segment_content)
231
+
232
+ except Exception as e:
233
+ logging.error(f"Decryption failed for segment {index}: {str(e)}")
234
+ self.interrupt_flag.set() # Interrupt the download process
235
+ self.stop_event.set() # Trigger the stopping event for all threads
236
+ break # Stop the current task immediately
237
+
238
+ self.class_ts_estimator.update_progress_bar(content_size, progress_bar)
239
+ self.queue.put((index, segment_content))
240
+ self.downloaded_segments.add(index)
241
+ progress_bar.update(1)
242
+ return
176
243
 
177
- # Download all segments (first batch)
178
- await self._download_segments_batch(
179
- client, self.segments, results, semaphore,
180
- REQUEST_MAX_RETRY, estimator, progress_bar
181
- )
244
+ except Exception as e:
245
+ logging.info(f"Attempt {attempt + 1} failed for segment {index} - '{ts_url}': {e}")
246
+
247
+ if attempt > self.info_maxRetry:
248
+ self.info_maxRetry = ( attempt + 1 )
249
+ self.info_nRetry += 1
250
+
251
+ if attempt + 1 == REQUEST_MAX_RETRY:
252
+ console.log(f"[red]Final retry failed for segment: {index}")
253
+ self.queue.put((index, None)) # Marker for failed segment
254
+ progress_bar.update(1)
255
+ self.info_nFailed += 1
256
+ return
257
+
258
+ with self.active_retries_lock:
259
+ self.active_retries += 1
260
+
261
+ #sleep_time = backoff_factor * (2 ** attempt)
262
+ sleep_time = backoff_factor * (attempt + 1)
263
+ logging.info(f"Retrying segment {index} in {sleep_time} seconds...")
264
+ time.sleep(sleep_time)
265
+
266
+ with self.active_retries_lock:
267
+ self.active_retries -= 1
182
268
 
183
- # Retry failed segments
184
- await self._retry_failed_segments(
185
- client, self.segments, results, semaphore,
186
- REQUEST_MAX_RETRY, estimator, progress_bar
187
- )
269
+ def write_segments_to_file(self):
270
+ """
271
+ Writes segments to file with additional verification.
272
+ """
273
+ with open(self.tmp_file_path, 'wb') as f:
274
+ while not self.stop_event.is_set() or not self.queue.empty():
275
+ if self.interrupt_flag.is_set():
276
+ break
277
+
278
+ try:
279
+ index, segment_content = self.queue.get(timeout=self.current_timeout)
188
280
 
189
- # Write results
190
- self._write_results_to_file(results)
281
+ # Successful queue retrieval: reduce timeout
282
+ self.current_timeout = max(self.base_timeout, self.current_timeout / 2)
191
283
 
192
- except Exception as e:
193
- logging.error(f"Download error: {e}")
194
- raise
284
+ # Handle failed segments
285
+ if segment_content is None:
286
+ if index == self.expected_index:
287
+ self.expected_index += 1
288
+ continue
195
289
 
196
- finally:
197
- self._cleanup_resources(progress_bar)
290
+ # Write segment if it's the next expected one
291
+ if index == self.expected_index:
292
+ f.write(segment_content)
293
+ f.flush()
294
+ self.expected_index += 1
198
295
 
199
- if not self.download_interrupted:
200
- self._verify_download_completion()
296
+ # Write any buffered segments that are now in order
297
+ while self.expected_index in self.buffer:
298
+ next_segment = self.buffer.pop(self.expected_index)
201
299
 
202
- return self._generate_results(type)
300
+ if next_segment is not None:
301
+ f.write(next_segment)
302
+ f.flush()
203
303
 
204
- async def _download_segments_batch(self, client, segment_urls, results, semaphore, max_retry, estimator, progress_bar):
205
- """
206
- Download a batch of segments with retry logic.
207
- """
208
- async def download_single(url, idx):
209
- async with semaphore:
210
- for attempt in range(max_retry):
211
- try:
212
- resp = await client.get(url, headers={'User-Agent': get_userAgent()})
213
-
214
- if resp.status_code == 200:
215
- content = resp.content
216
-
217
- if self.decryption:
218
- content = self.decryption.decrypt(content)
219
- return idx, content, attempt
220
-
221
- await asyncio.sleep(1.1 * (2 ** attempt))
222
- logging.info(f"Segment {idx} failed with status {resp.status_code}. Retrying...")
304
+ self.expected_index += 1
223
305
 
224
- except Exception:
225
- await asyncio.sleep(1.1 * (2 ** attempt))
226
- logging.info(f"Segment {idx} download failed: {sys.exc_info()[1]}. Retrying...")
306
+ else:
307
+ self.buffer[index] = segment_content
227
308
 
228
- return idx, b'', max_retry
309
+ except queue.Empty:
310
+ self.current_timeout = min(MAX_TIMEOOUT, self.current_timeout * 1.1)
311
+ time.sleep(0.05)
229
312
 
230
- tasks = [download_single(url, i) for i, url in enumerate(segment_urls)]
231
-
232
- for coro in asyncio.as_completed(tasks):
233
- try:
234
- idx, data, nretry = await coro
235
- results[idx] = data
313
+ if self.stop_event.is_set():
314
+ break
236
315
 
237
- if data:
238
- self.downloaded_segments.add(idx)
239
- estimator.add_ts_file(len(data))
240
- estimator.update_progress_bar(len(data), progress_bar)
316
+ except Exception as e:
317
+ logging.error(f"Error writing segment {index}: {str(e)}")
318
+
319
+ def download_streams(self, description: str, type: str):
320
+ """
321
+ Downloads all TS segments in parallel and writes them to a file.
241
322
 
242
- else:
243
- self.info_nFailed += 1
323
+ Parameters:
324
+ - description: Description to insert on tqdm bar
325
+ - type (str): Type of download: 'video' or 'audio'
326
+ """
327
+ if TELEGRAM_BOT:
244
328
 
245
- self.info_nRetry += nretry
246
- progress_bar.update(1)
329
+ # Viene usato per lo screen
330
+ console.log("####")
331
+
332
+ self.get_info()
333
+ self.setup_interrupt_handler()
247
334
 
248
- except KeyboardInterrupt:
249
- self.download_interrupted = True
250
- break
335
+ progress_bar = tqdm(
336
+ total=len(self.segments),
337
+ unit='s',
338
+ ascii='░▒█',
339
+ bar_format=self._get_bar_format(description),
340
+ mininterval=0.6,
341
+ maxinterval=1.0,
342
+ file=sys.stdout, # Using file=sys.stdout to force in-place updates because sys.stderr may not support carriage returns in this environment.
343
+ )
251
344
 
252
- async def _retry_failed_segments(self, client, segment_urls, results, semaphore, max_retry, estimator, progress_bar):
253
- """
254
- Retry failed segments with exponential backoff.
255
- """
256
- max_global_retries = 5
257
- global_retry_count = 0
345
+ try:
346
+ writer_thread = threading.Thread(target=self.write_segments_to_file)
347
+ writer_thread.daemon = True
348
+ writer_thread.start()
258
349
 
259
- while (self.info_nFailed > 0 and
260
- global_retry_count < max_global_retries and
261
- not self.download_interrupted):
262
-
263
- failed_indices = [i for i, data in enumerate(results) if not data]
264
- if not failed_indices:
265
- break
266
-
267
- logging.info(f"[yellow]Retrying {len(failed_indices)} failed segments...")
268
-
269
- retry_tasks = [
270
- self._download_segments_batch(
271
- client, [segment_urls[i]], [results[i]],
272
- semaphore, max_retry, estimator, progress_bar
273
- )
274
- for i in failed_indices
275
- ]
350
+ # Configure workers and delay
351
+ max_workers = self._get_worker_count(type)
276
352
 
277
- await asyncio.gather(*retry_tasks)
278
- global_retry_count += 1
353
+ # Download segments with completion verification
354
+ with ThreadPoolExecutor(max_workers=max_workers) as executor:
355
+ futures = []
356
+ for index, segment_url in enumerate(self.segments):
357
+
358
+ # Check for interrupt before submitting each task
359
+ if self.interrupt_flag.is_set():
360
+ break
361
+
362
+ time.sleep(TQDM_DELAY_WORKER)
363
+ futures.append(executor.submit(self.download_segment, segment_url, index, progress_bar))
364
+
365
+ # Wait for futures with interrupt handling
366
+ for future in as_completed(futures):
367
+ if self.interrupt_flag.is_set():
368
+ break
369
+ try:
370
+ future.result()
371
+ except Exception as e:
372
+ logging.error(f"Error in download thread: {str(e)}")
373
+
374
+ # Interrupt handling for missing segments
375
+ if not self.interrupt_flag.is_set():
376
+ total_segments = len(self.segments)
377
+ completed_segments = len(self.downloaded_segments)
378
+
379
+ if completed_segments < total_segments:
380
+ missing_segments = set(range(total_segments)) - self.downloaded_segments
381
+ logging.warning(f"Missing segments: {sorted(missing_segments)}")
382
+
383
+ # Retry missing segments with interrupt check
384
+ for index in missing_segments:
385
+ if self.interrupt_flag.is_set():
386
+ break
279
387
 
280
- def _write_results_to_file(self, results):
281
- """
282
- Write downloaded segments to file.
283
- """
284
- with open(self.tmp_file_path, 'wb') as f:
285
- for data in results:
286
- if data:
287
- f.write(data)
288
- f.flush()
388
+ try:
389
+ self.download_segment(self.segments[index], index, progress_bar)
390
+
391
+ except Exception as e:
392
+ logging.error(f"Failed to retry segment {index}: {str(e)}")
289
393
 
394
+ finally:
395
+ self._cleanup_resources(writer_thread, progress_bar)
396
+
397
+ if not self.interrupt_flag.is_set():
398
+ self._verify_download_completion()
399
+
400
+ return self._generate_results(type)
401
+
290
402
  def _get_bar_format(self, description: str) -> str:
291
403
  """
292
404
  Generate platform-appropriate progress bar format.
@@ -310,9 +422,7 @@ class M3U8_Segments:
310
422
  return base_workers
311
423
 
312
424
  def _generate_results(self, stream_type: str) -> Dict:
313
- """
314
- Package final download results.
315
- """
425
+ """Package final download results."""
316
426
  return {
317
427
  'type': stream_type,
318
428
  'nFailed': self.info_nFailed,
@@ -320,31 +430,30 @@ class M3U8_Segments:
320
430
  }
321
431
 
322
432
  def _verify_download_completion(self) -> None:
323
- """
324
- Validate final download integrity.
325
- """
433
+ """Validate final download integrity."""
326
434
  total = len(self.segments)
327
435
  if len(self.downloaded_segments) / total < 0.999:
328
436
  missing = sorted(set(range(total)) - self.downloaded_segments)
329
437
  raise RuntimeError(f"Download incomplete ({len(self.downloaded_segments)/total:.1%}). Missing segments: {missing}")
330
438
 
331
- def _cleanup_resources(self, progress_bar: tqdm) -> None:
332
- """
333
- Ensure resource cleanup and final reporting.
334
- """
439
+ def _cleanup_resources(self, writer_thread: threading.Thread, progress_bar: tqdm) -> None:
440
+ """Ensure resource cleanup and final reporting."""
441
+ self.stop_event.set()
442
+ writer_thread.join(timeout=30)
335
443
  progress_bar.close()
336
-
444
+
337
445
  if self.info_nFailed > 0:
338
446
  self._display_error_summary()
339
447
 
448
+ self.buffer = {}
449
+ self.expected_index = 0
450
+
340
451
  def _display_error_summary(self) -> None:
341
- """
342
- Generate final error report.
343
- """
452
+ """Generate final error report."""
344
453
  console.print(f"\n[cyan]Retry Summary: "
345
454
  f"[white]Max retries: [green]{self.info_maxRetry} "
346
455
  f"[white]Total retries: [green]{self.info_nRetry} "
347
456
  f"[white]Failed segments: [red]{self.info_nFailed}")
348
457
 
349
458
  if self.info_nRetry > len(self.segments) * 0.3:
350
- console.print("[yellow]Warning: High retry count detected. Consider reducing worker count in config.")
459
+ console.print("[yellow]Warning: High retry count detected. Consider reducing worker count in config.")
@@ -10,7 +10,6 @@ from functools import partial
10
10
 
11
11
 
12
12
  # External libraries
13
- import httpx
14
13
  from tqdm import tqdm
15
14
  from rich.console import Console
16
15
  from rich.prompt import Prompt
@@ -19,6 +18,7 @@ from rich.panel import Panel
19
18
 
20
19
  # Internal utilities
21
20
  from StreamingCommunity.Util.headers import get_userAgent
21
+ from StreamingCommunity.Util.http_client import create_client
22
22
  from StreamingCommunity.Util.color import Colors
23
23
  from StreamingCommunity.Util.config_json import config_manager
24
24
  from StreamingCommunity.Util.os import internet_manager, os_manager
@@ -83,7 +83,7 @@ def MP4_downloader(url: str, path: str, referer: str = None, headers_: dict = No
83
83
  if os.path.exists(path):
84
84
  console.log("[red]Output file already exists.")
85
85
  if TELEGRAM_BOT:
86
- bot.send_message(f"Contenuto già scaricato!", None)
86
+ bot.send_message("Contenuto già scaricato!", None)
87
87
  return None, False
88
88
 
89
89
  if not (url.lower().startswith('http://') or url.lower().startswith('https://')):
@@ -110,7 +110,8 @@ def MP4_downloader(url: str, path: str, referer: str = None, headers_: dict = No
110
110
  os.makedirs(os.path.dirname(path), exist_ok=True)
111
111
 
112
112
  try:
113
- with httpx.Client(verify=REQUEST_VERIFY) as client:
113
+ # Use unified HTTP client (verify/timeout/proxy from config)
114
+ with create_client() as client:
114
115
  with client.stream("GET", url, headers=headers) as response:
115
116
  response.raise_for_status()
116
117
  total = int(response.headers.get('content-length', 0))
@@ -229,7 +229,7 @@ class TOR_downloader:
229
229
  torrent_info.num_seeds == 0 and
230
230
  torrent_info.state in ('stalledDL', 'missingFiles', 'error')):
231
231
 
232
- self.console.print(f"[bold red]Torrent not downloadable. No seeds or peers available. Removing...[/bold red]")
232
+ self.console.print("[bold red]Torrent not downloadable. No seeds or peers available. Removing...[/bold red]")
233
233
  self._remove_torrent(self.latest_torrent_hash)
234
234
  self.latest_torrent_hash = None
235
235
  return False
@@ -250,7 +250,7 @@ class TOR_downloader:
250
250
  """
251
251
  try:
252
252
  self.qb.torrents_delete(delete_files=delete_files, torrent_hashes=torrent_hash)
253
- self.console.print(f"[yellow]Torrent removed from client[/yellow]")
253
+ self.console.print("[yellow]Torrent removed from client[/yellow]")
254
254
  except Exception as e:
255
255
  logging.error(f"Error removing torrent: {str(e)}")
256
256
 
@@ -356,10 +356,8 @@ class TOR_downloader:
356
356
 
357
357
  # Get download statistics
358
358
  download_speed = torrent_info.dlspeed
359
- upload_speed = torrent_info.upspeed
360
359
  total_size = torrent_info.size
361
360
  downloaded_size = torrent_info.downloaded
362
- eta = torrent_info.eta # eta in seconds
363
361
 
364
362
  # Format sizes and speeds using the existing functions without modification
365
363
  downloaded_size_str = internet_manager.format_file_size(downloaded_size)
@@ -465,5 +463,5 @@ class TOR_downloader:
465
463
 
466
464
  try:
467
465
  self.qb.auth_log_out()
468
- except:
466
+ except Exception:
469
467
  pass
@@ -2,4 +2,12 @@
2
2
 
3
3
  from .HLS.downloader import HLS_Downloader
4
4
  from .MP4.downloader import MP4_downloader
5
- from .TOR.downloader import TOR_downloader
5
+ from .TOR.downloader import TOR_downloader
6
+ from .DASH.downloader import DASH_Downloader
7
+
8
+ __all__ = [
9
+ "HLS_Downloader",
10
+ "MP4_downloader",
11
+ "TOR_downloader",
12
+ "DASH_Downloader"
13
+ ]
@@ -1,4 +1,13 @@
1
1
  # 18.04.24
2
2
 
3
3
  from .command import join_video, join_audios, join_subtitle
4
- from .util import print_duration_table, get_video_duration
4
+ from .util import print_duration_table, get_video_duration
5
+
6
+
7
+ __all__ = [
8
+ "join_video",
9
+ "join_audios",
10
+ "join_subtitle",
11
+ "print_duration_table",
12
+ "get_video_duration",
13
+ ]
@@ -1,6 +1,5 @@
1
1
  # 31.01.24
2
2
 
3
- import sys
4
3
  import logging
5
4
  import subprocess
6
5
  from typing import List, Dict, Tuple, Optional
@@ -110,13 +109,12 @@ def join_video(video_path: str, out_path: str, codec: M3U8_Codec = None):
110
109
  if need_to_force_to_ts(video_path):
111
110
  #console.log("[red]Force input file to 'mpegts'.")
112
111
  ffmpeg_cmd.extend(['-f', 'mpegts'])
113
- vcodec = "libx264"
114
112
 
115
113
  # Insert input video path
116
114
  ffmpeg_cmd.extend(['-i', video_path])
117
115
 
118
116
  # Add output Parameters
119
- if USE_CODEC and codec != None:
117
+ if USE_CODEC and codec is not None:
120
118
  if USE_VCODEC:
121
119
  if codec.video_codec_name:
122
120
  if not USE_GPU:
@@ -162,7 +160,7 @@ def join_video(video_path: str, out_path: str, codec: M3U8_Codec = None):
162
160
  print()
163
161
 
164
162
  else:
165
- console.log(f"[purple]FFmpeg [white][[cyan]Join video[white]] ...")
163
+ console.log("[purple]FFmpeg [white][[cyan]Join video[white]] ...")
166
164
  with suppress_output():
167
165
  capture_ffmpeg_real_time(ffmpeg_cmd, "[cyan]Join video")
168
166
  print()
@@ -258,7 +256,7 @@ def join_audios(video_path: str, audio_tracks: List[Dict[str, str]], out_path: s
258
256
  print()
259
257
 
260
258
  else:
261
- console.log(f"[purple]FFmpeg [white][[cyan]Join audio[white]] ...")
259
+ console.log("[purple]FFmpeg [white][[cyan]Join audio[white]] ...")
262
260
  with suppress_output():
263
261
  capture_ffmpeg_real_time(ffmpeg_cmd, "[cyan]Join audio")
264
262
  print()
@@ -313,7 +311,7 @@ def join_subtitle(video_path: str, subtitles_list: List[Dict[str, str]], out_pat
313
311
  print()
314
312
 
315
313
  else:
316
- console.log(f"[purple]FFmpeg [white][[cyan]Join subtitle[white]] ...")
314
+ console.log("[purple]FFmpeg [white][[cyan]Join subtitle[white]] ...")
317
315
  with suppress_output():
318
316
  capture_ffmpeg_real_time(ffmpeg_cmd, "[cyan]Join subtitle")
319
317
  print()
@@ -76,7 +76,7 @@ def get_video_duration(file_path: str) -> float:
76
76
  try:
77
77
  return float(probe_result['format']['duration'])
78
78
 
79
- except:
79
+ except Exception:
80
80
  return 1
81
81
 
82
82
  except Exception as e: