StreamingCommunity 3.4.0__py3-none-any.whl → 3.4.2__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of StreamingCommunity might be problematic. Click here for more details.

Files changed (34) hide show
  1. StreamingCommunity/Api/Site/altadefinizione/film.py +0 -1
  2. StreamingCommunity/Api/Site/altadefinizione/series.py +3 -12
  3. StreamingCommunity/Api/Site/altadefinizione/site.py +0 -2
  4. StreamingCommunity/Api/Site/animeunity/site.py +3 -3
  5. StreamingCommunity/Api/Site/animeunity/util/ScrapeSerie.py +3 -3
  6. StreamingCommunity/Api/Site/crunchyroll/series.py +3 -14
  7. StreamingCommunity/Api/Site/crunchyroll/site.py +2 -4
  8. StreamingCommunity/Api/Site/guardaserie/series.py +3 -14
  9. StreamingCommunity/Api/Site/mediasetinfinity/series.py +3 -13
  10. StreamingCommunity/Api/Site/mediasetinfinity/site.py +14 -22
  11. StreamingCommunity/Api/Site/raiplay/film.py +0 -1
  12. StreamingCommunity/Api/Site/raiplay/series.py +5 -18
  13. StreamingCommunity/Api/Site/raiplay/site.py +42 -36
  14. StreamingCommunity/Api/Site/raiplay/util/ScrapeSerie.py +88 -45
  15. StreamingCommunity/Api/Site/streamingcommunity/series.py +5 -10
  16. StreamingCommunity/Api/Site/streamingcommunity/util/ScrapeSerie.py +0 -1
  17. StreamingCommunity/Api/Site/streamingwatch/series.py +3 -13
  18. StreamingCommunity/Api/Template/Util/__init__.py +4 -2
  19. StreamingCommunity/Api/Template/Util/manage_ep.py +66 -0
  20. StreamingCommunity/Lib/Downloader/DASH/downloader.py +55 -16
  21. StreamingCommunity/Lib/Downloader/DASH/segments.py +45 -16
  22. StreamingCommunity/Lib/Downloader/HLS/downloader.py +71 -34
  23. StreamingCommunity/Lib/Downloader/HLS/segments.py +18 -1
  24. StreamingCommunity/Lib/Downloader/MP4/downloader.py +16 -4
  25. StreamingCommunity/Lib/M3U8/estimator.py +47 -1
  26. StreamingCommunity/Upload/update.py +19 -6
  27. StreamingCommunity/Upload/version.py +1 -1
  28. StreamingCommunity/Util/table.py +50 -8
  29. {streamingcommunity-3.4.0.dist-info → streamingcommunity-3.4.2.dist-info}/METADATA +1 -1
  30. {streamingcommunity-3.4.0.dist-info → streamingcommunity-3.4.2.dist-info}/RECORD +34 -34
  31. {streamingcommunity-3.4.0.dist-info → streamingcommunity-3.4.2.dist-info}/WHEEL +0 -0
  32. {streamingcommunity-3.4.0.dist-info → streamingcommunity-3.4.2.dist-info}/entry_points.txt +0 -0
  33. {streamingcommunity-3.4.0.dist-info → streamingcommunity-3.4.2.dist-info}/licenses/LICENSE +0 -0
  34. {streamingcommunity-3.4.0.dist-info → streamingcommunity-3.4.2.dist-info}/top_level.txt +0 -0
@@ -3,6 +3,7 @@
3
3
  import os
4
4
  import asyncio
5
5
  import time
6
+ from typing import Dict, Optional
6
7
 
7
8
 
8
9
  # External libraries
@@ -68,6 +69,9 @@ class MPD_Segments:
68
69
  # Segment tracking - store only metadata, not content
69
70
  self.segment_status = {} # {idx: {'downloaded': bool, 'size': int}}
70
71
  self.segments_lock = asyncio.Lock()
72
+
73
+ # Estimator for progress tracking
74
+ self.estimator: Optional[M3U8_Ts_Estimator] = None
71
75
 
72
76
  def get_concat_path(self, output_dir: str = None):
73
77
  """
@@ -150,7 +154,7 @@ class MPD_Segments:
150
154
  semaphore = asyncio.Semaphore(concurrent_downloads)
151
155
 
152
156
  # Initialize estimator
153
- estimator = M3U8_Ts_Estimator(total_segments=len(segment_urls) + 1)
157
+ self.estimator = M3U8_Ts_Estimator(total_segments=len(segment_urls) + 1)
154
158
 
155
159
  self.segment_status = {}
156
160
  self.downloaded_segments = set()
@@ -166,17 +170,17 @@ class MPD_Segments:
166
170
  async with httpx.AsyncClient(timeout=timeout_config, limits=limits) as client:
167
171
 
168
172
  # Download init segment
169
- await self._download_init_segment(client, init_url, concat_path, estimator, progress_bar)
173
+ await self._download_init_segment(client, init_url, concat_path, progress_bar)
170
174
 
171
175
  # Download all segments to temp files
172
176
  await self._download_segments_batch(
173
- client, segment_urls, temp_dir, semaphore, REQUEST_MAX_RETRY, estimator, progress_bar
177
+ client, segment_urls, temp_dir, semaphore, REQUEST_MAX_RETRY, progress_bar
174
178
  )
175
179
 
176
180
  # Retry failed segments only if enabled
177
181
  if self.enable_retry:
178
182
  await self._retry_failed_segments(
179
- client, segment_urls, temp_dir, semaphore, REQUEST_MAX_RETRY, estimator, progress_bar
183
+ client, segment_urls, temp_dir, semaphore, REQUEST_MAX_RETRY, progress_bar
180
184
  )
181
185
 
182
186
  # Concatenate all segments IN ORDER
@@ -192,7 +196,7 @@ class MPD_Segments:
192
196
  self._verify_download_completion()
193
197
  return self._generate_results(stream_type)
194
198
 
195
- async def _download_init_segment(self, client, init_url, concat_path, estimator, progress_bar):
199
+ async def _download_init_segment(self, client, init_url, concat_path, progress_bar):
196
200
  """
197
201
  Download the init segment and update progress/estimator.
198
202
  """
@@ -208,25 +212,28 @@ class MPD_Segments:
208
212
  with open(concat_path, 'wb') as outfile:
209
213
  if response.status_code == 200:
210
214
  outfile.write(response.content)
211
- estimator.add_ts_file(len(response.content))
215
+ if self.estimator:
216
+ self.estimator.add_ts_file(len(response.content))
212
217
 
213
218
  progress_bar.update(1)
214
- self._throttled_progress_update(len(response.content), estimator, progress_bar)
219
+ if self.estimator:
220
+ self._throttled_progress_update(len(response.content), progress_bar)
215
221
 
216
222
  except Exception as e:
217
223
  progress_bar.close()
218
224
  raise RuntimeError(f"Error downloading init segment: {e}")
219
225
 
220
- def _throttled_progress_update(self, content_size: int, estimator, progress_bar):
226
+ def _throttled_progress_update(self, content_size: int, progress_bar):
221
227
  """
222
228
  Throttled progress update to reduce CPU usage.
223
229
  """
224
230
  current_time = time.time()
225
231
  if current_time - self._last_progress_update > self._progress_update_interval:
226
- estimator.update_progress_bar(content_size, progress_bar)
232
+ if self.estimator:
233
+ self.estimator.update_progress_bar(content_size, progress_bar)
227
234
  self._last_progress_update = current_time
228
235
 
229
- async def _download_segments_batch(self, client, segment_urls, temp_dir, semaphore, max_retry, estimator, progress_bar):
236
+ async def _download_segments_batch(self, client, segment_urls, temp_dir, semaphore, max_retry, progress_bar):
230
237
  """
231
238
  Download segments to temporary files - write immediately to disk, not memory.
232
239
  """
@@ -287,15 +294,16 @@ class MPD_Segments:
287
294
  self.info_nRetry += nretry
288
295
 
289
296
  progress_bar.update(1)
290
- estimator.add_ts_file(size)
291
- self._throttled_progress_update(size, estimator, progress_bar)
297
+ if self.estimator:
298
+ self.estimator.add_ts_file(size)
299
+ self._throttled_progress_update(size, progress_bar)
292
300
 
293
301
  except KeyboardInterrupt:
294
302
  self.download_interrupted = True
295
303
  console.print("\n[red]Download interrupted by user (Ctrl+C).")
296
304
  break
297
305
 
298
- async def _retry_failed_segments(self, client, segment_urls, temp_dir, semaphore, max_retry, estimator, progress_bar):
306
+ async def _retry_failed_segments(self, client, segment_urls, temp_dir, semaphore, max_retry, progress_bar):
299
307
  """
300
308
  Retry failed segments up to 3 times.
301
309
  """
@@ -354,8 +362,9 @@ class MPD_Segments:
354
362
  self.info_nRetry += nretry
355
363
 
356
364
  progress_bar.update(0)
357
- estimator.add_ts_file(size)
358
- self._throttled_progress_update(size, estimator, progress_bar)
365
+ if self.estimator:
366
+ self.estimator.add_ts_file(size)
367
+ self._throttled_progress_update(size, progress_bar)
359
368
 
360
369
  except KeyboardInterrupt:
361
370
  self.download_interrupted = True
@@ -474,4 +483,24 @@ class MPD_Segments:
474
483
  console.print(f" [cyan]Max retries: [red]{getattr(self, 'info_maxRetry', 0)} [white]| "
475
484
  f"[cyan]Total retries: [red]{getattr(self, 'info_nRetry', 0)} [white]| "
476
485
  f"[cyan]Failed segments: [red]{getattr(self, 'info_nFailed', 0)} [white]| "
477
- f"[cyan]Failed indices: [red]{failed_indices}")
486
+ f"[cyan]Failed indices: [red]{failed_indices}")
487
+
488
+ def get_progress_data(self) -> Dict:
489
+ """Returns current download progress data for API."""
490
+ if not self.estimator:
491
+ return None
492
+
493
+ total = self.get_segments_count()
494
+ downloaded = len(self.downloaded_segments)
495
+ percentage = (downloaded / total * 100) if total > 0 else 0
496
+ stats = self.estimator.get_stats(downloaded, total)
497
+
498
+ return {
499
+ 'total_segments': total,
500
+ 'downloaded_segments': downloaded,
501
+ 'failed_segments': self.info_nFailed,
502
+ 'current_speed': stats['download_speed'],
503
+ 'estimated_size': stats['estimated_total_size'],
504
+ 'percentage': round(percentage, 2),
505
+ 'eta_seconds': stats['eta_seconds']
506
+ }
@@ -8,7 +8,6 @@ from typing import Any, Dict, List, Optional, Union
8
8
 
9
9
  # External libraries
10
10
  from rich.console import Console
11
- from rich.panel import Panel
12
11
  from rich.table import Table
13
12
 
14
13
 
@@ -155,6 +154,7 @@ class M3U8Manager:
155
154
  """
156
155
  Selects video, audio, and subtitle streams based on configuration.
157
156
  If it's a master playlist, only selects video stream.
157
+ Auto-selects first audio if only one is available and none match filters.
158
158
  """
159
159
  if not self.is_master:
160
160
  self.video_url, self.video_res = self.m3u8_url, "undefined"
@@ -162,6 +162,7 @@ class M3U8Manager:
162
162
  self.sub_streams = []
163
163
 
164
164
  else:
165
+ # Video selection logic
165
166
  if str(FILTER_CUSTOM_RESOLUTION) == "best":
166
167
  self.video_url, self.video_res = self.parser._video.get_best_uri()
167
168
  elif str(FILTER_CUSTOM_RESOLUTION) == "worst":
@@ -173,18 +174,29 @@ class M3U8Manager:
173
174
  # Fallback to best if custom resolution not found
174
175
  if self.video_url is None:
175
176
  self.video_url, self.video_res = self.parser._video.get_best_uri()
176
-
177
177
  else:
178
178
  logging.error("Resolution not recognized.")
179
179
  self.video_url, self.video_res = self.parser._video.get_best_uri()
180
180
 
181
- # Audio info
181
+ # Audio selection with auto-select fallback
182
+ all_audio = self.parser._audio.get_all_uris_and_names() or []
183
+
184
+ # Try to match with configured languages
182
185
  self.audio_streams = [
183
- s for s in (self.parser._audio.get_all_uris_and_names() or [])
186
+ s for s in all_audio
184
187
  if s.get('language') in DOWNLOAD_SPECIFIC_AUDIO
185
188
  ]
186
-
187
- # Subtitle info
189
+
190
+ # Auto-select first audio if:
191
+ # 1. No audio matched the filters
192
+ # 2. At least one audio track is available
193
+ # 3. Filters are configured (not empty)
194
+ if not self.audio_streams and all_audio and DOWNLOAD_SPECIFIC_AUDIO:
195
+ first_audio_lang = all_audio[0].get('language', 'unknown')
196
+ console.print(f"\n[yellow]Auto-selecting first available audio track: {first_audio_lang}[/yellow]")
197
+ self.audio_streams = [all_audio[0]]
198
+
199
+ # Subtitle selection
188
200
  self.sub_streams = []
189
201
  if "*" in DOWNLOAD_SPECIFIC_SUBTITLE:
190
202
  self.sub_streams = self.parser._subtitle.get_all_uris_and_names() or []
@@ -212,27 +224,22 @@ class M3U8Manager:
212
224
 
213
225
  data_rows.append(["Video", available_video, str(FILTER_CUSTOM_RESOLUTION), downloadable_video])
214
226
 
215
-
216
227
  # Subtitle information
217
228
  available_subtitles = self.parser._subtitle.get_all_uris_and_names() or []
218
- available_sub_languages = [sub.get('language') for sub in available_subtitles]
219
-
220
- if available_sub_languages:
229
+ if available_subtitles:
230
+ available_sub_languages = [sub.get('language') for sub in available_subtitles]
221
231
  available_subs = ', '.join(available_sub_languages)
222
-
223
- downloadable_sub_languages = available_sub_languages if "*" in DOWNLOAD_SPECIFIC_SUBTITLE else list(set(available_sub_languages) & set(DOWNLOAD_SPECIFIC_SUBTITLE))
232
+ downloadable_sub_languages = [sub.get('language') for sub in self.sub_streams]
224
233
  downloadable_subs = ', '.join(downloadable_sub_languages) if downloadable_sub_languages else "Nothing"
225
234
 
226
235
  data_rows.append(["Subtitle", available_subs, ', '.join(DOWNLOAD_SPECIFIC_SUBTITLE), downloadable_subs])
227
236
 
228
237
  # Audio information
229
238
  available_audio = self.parser._audio.get_all_uris_and_names() or []
230
- available_audio_languages = [audio.get('language') for audio in available_audio]
231
-
232
- if available_audio_languages:
239
+ if available_audio:
240
+ available_audio_languages = [audio.get('language') for audio in available_audio]
233
241
  available_audios = ', '.join(available_audio_languages)
234
-
235
- downloadable_audio_languages = list(set(available_audio_languages) & set(DOWNLOAD_SPECIFIC_AUDIO))
242
+ downloadable_audio_languages = [audio.get('language') for audio in self.audio_streams]
236
243
  downloadable_audios = ', '.join(downloadable_audio_languages) if downloadable_audio_languages else "Nothing"
237
244
 
238
245
  data_rows.append(["Audio", available_audios, ', '.join(DOWNLOAD_SPECIFIC_AUDIO), downloadable_audios])
@@ -263,7 +270,8 @@ class M3U8Manager:
263
270
 
264
271
  console.print(table)
265
272
  print("")
266
-
273
+
274
+
267
275
  class DownloadManager:
268
276
  """Manages downloading of video, audio, and subtitle streams."""
269
277
  def __init__(self, temp_dir: str, client: HLSClient, url_fixer: M3U8_UrlFix, custom_headers: Optional[Dict[str, str]] = None):
@@ -282,6 +290,10 @@ class DownloadManager:
282
290
  self.stopped = False
283
291
  self.video_segments_count = 0
284
292
 
293
+ # For progress tracking
294
+ self.current_downloader: Optional[M3U8_Segments] = None
295
+ self.current_download_type: Optional[str] = None
296
+
285
297
  def download_video(self, video_url: str) -> bool:
286
298
  """
287
299
  Downloads video segments from the M3U8 playlist.
@@ -299,12 +311,20 @@ class DownloadManager:
299
311
  tmp_folder=video_tmp_dir,
300
312
  custom_headers=self.custom_headers
301
313
  )
314
+
315
+ # Set current downloader for progress tracking
316
+ self.current_downloader = downloader
317
+ self.current_download_type = 'video'
302
318
 
303
319
  # Download video and get segment count
304
320
  result = downloader.download_streams("Video", "video")
305
321
  self.video_segments_count = downloader.get_segments_count()
306
322
  self.missing_segments.append(result)
307
323
 
324
+ # Reset current downloader after completion
325
+ self.current_downloader = None
326
+ self.current_download_type = None
327
+
308
328
  if result.get('stopped', False):
309
329
  self.stopped = True
310
330
  return False
@@ -313,6 +333,8 @@ class DownloadManager:
313
333
 
314
334
  except Exception as e:
315
335
  logging.error(f"Error downloading video from {video_url}: {str(e)}")
336
+ self.current_downloader = None
337
+ self.current_download_type = None
316
338
  return False
317
339
 
318
340
  def download_audio(self, audio: Dict) -> bool:
@@ -334,10 +356,19 @@ class DownloadManager:
334
356
  limit_segments=self.video_segments_count if self.video_segments_count > 0 else None,
335
357
  custom_headers=self.custom_headers
336
358
  )
337
-
359
+
360
+ # Set current downloader for progress tracking
361
+ self.current_downloader = downloader
362
+ self.current_download_type = f"audio_{audio['language']}"
363
+
364
+ # Download audio
338
365
  result = downloader.download_streams(f"Audio {audio['language']}", "audio")
339
366
  self.missing_segments.append(result)
340
367
 
368
+ # Reset current downloader after completion
369
+ self.current_downloader = None
370
+ self.current_download_type = None
371
+
341
372
  if result.get('stopped', False):
342
373
  self.stopped = True
343
374
  return False
@@ -346,6 +377,8 @@ class DownloadManager:
346
377
 
347
378
  except Exception as e:
348
379
  logging.error(f"Error downloading audio {audio.get('language', 'unknown')}: {str(e)}")
380
+ self.current_downloader = None
381
+ self.current_download_type = None
349
382
  return False
350
383
 
351
384
  def download_subtitle(self, sub: Dict) -> bool:
@@ -645,6 +678,7 @@ class HLS_Downloader:
645
678
  """Prints download summary including file size, duration, and any missing segments."""
646
679
  missing_ts = False
647
680
  missing_info = ""
681
+
648
682
  for item in self.download_manager.missing_segments:
649
683
  if int(item['nFailed']) >= 1:
650
684
  missing_ts = True
@@ -653,15 +687,7 @@ class HLS_Downloader:
653
687
  file_size = internet_manager.format_file_size(os.path.getsize(self.path_manager.output_path))
654
688
  duration = print_duration_table(self.path_manager.output_path, description=False, return_string=True)
655
689
 
656
- panel_content = (
657
- f"[cyan]File size: [bold red]{file_size}[/bold red]\n"
658
- f"[cyan]Duration: [bold]{duration}[/bold]\n"
659
- f"[cyan]Output: [bold]{os.path.abspath(self.path_manager.output_path)}[/bold]"
660
- )
661
-
662
- if missing_ts:
663
- panel_content += f"\n{missing_info}"
664
-
690
+ # Rename output file if there were missing segments or shortest used
665
691
  new_filename = self.path_manager.output_path
666
692
  if missing_ts and use_shortest:
667
693
  new_filename = new_filename.replace(EXTENSION_OUTPUT, f"_failed_sync_ts{EXTENSION_OUTPUT}")
@@ -670,13 +696,24 @@ class HLS_Downloader:
670
696
  elif use_shortest:
671
697
  new_filename = new_filename.replace(EXTENSION_OUTPUT, f"_failed_sync{EXTENSION_OUTPUT}")
672
698
 
699
+ # Rename the file accordingly
673
700
  if missing_ts or use_shortest:
674
701
  os.rename(self.path_manager.output_path, new_filename)
675
702
  self.path_manager.output_path = new_filename
676
703
 
677
- print("")
678
- console.print(Panel(
679
- panel_content,
680
- title=f"{os.path.basename(self.path_manager.output_path.replace('.mp4', ''))}",
681
- border_style="green"
682
- ))
704
+ console.print(f"[yellow]Output [red]{os.path.abspath(self.path_manager.output_path)} [cyan]with size [red]{file_size} [cyan]and duration [red]{duration}")
705
+
706
+ def get_progress_data(self) -> Optional[Dict]:
707
+ """Get current download progress data."""
708
+ if not self.download_manager.current_downloader:
709
+ return None
710
+
711
+ try:
712
+ progress = self.download_manager.current_downloader.get_progress_data()
713
+ if progress:
714
+ progress['download_type'] = self.download_manager.current_download_type
715
+ return progress
716
+
717
+ except Exception as e:
718
+ logging.error(f"Error getting progress data: {e}")
719
+ return None
@@ -487,4 +487,21 @@ class M3U8_Segments:
487
487
  """Generate final error report."""
488
488
  console.print(f" [cyan]Max retries: [red]{self.info_maxRetry} [white] | "
489
489
  f"[cyan]Total retries: [red]{self.info_nRetry} [white] | "
490
- f"[cyan]Failed segments: [red]{self.info_nFailed}")
490
+ f"[cyan]Failed segments: [red]{self.info_nFailed}")
491
+
492
+ def get_progress_data(self) -> Dict:
493
+ """Returns current download progress data for API consumption."""
494
+ total = self.get_segments_count()
495
+ downloaded = len(self.downloaded_segments)
496
+ percentage = (downloaded / total * 100) if total > 0 else 0
497
+ stats = self.class_ts_estimator.get_stats(downloaded, total)
498
+
499
+ return {
500
+ 'total_segments': total,
501
+ 'downloaded_segments': downloaded,
502
+ 'failed_segments': self.info_nFailed,
503
+ 'current_speed': stats['download_speed'],
504
+ 'estimated_size': stats['estimated_total_size'],
505
+ 'percentage': round(percentage, 2),
506
+ 'eta_seconds': stats['eta_seconds']
507
+ }
@@ -76,6 +76,7 @@ def MP4_downloader(url: str, path: str, referer: str = None, headers_: dict = No
76
76
  - Single Ctrl+C: Completes download gracefully
77
77
  - Triple Ctrl+C: Saves partial download and exits
78
78
  """
79
+ url = url.strip()
79
80
  if TELEGRAM_BOT:
80
81
  bot = get_bot_instance()
81
82
  console.log("####")
@@ -134,20 +135,23 @@ def MP4_downloader(url: str, path: str, referer: str = None, headers_: dict = No
134
135
 
135
136
  # Create progress bar with percentage instead of n_fmt/total_fmt
136
137
  console.print("[cyan]You can safely stop the download with [bold]Ctrl+c[bold] [cyan]")
138
+
137
139
  progress_bar = tqdm(
138
140
  total=total,
139
141
  ascii='░▒█',
140
142
  bar_format=f"{Colors.YELLOW}MP4{Colors.CYAN} Downloading{Colors.WHITE}: "
141
- f"{Colors.RED}{{percentage:.1f}}% {Colors.MAGENTA}{{bar:40}} {Colors.WHITE}"
142
- f"{Colors.DARK_GRAY}[{Colors.YELLOW}{{elapsed}}{Colors.WHITE} < {Colors.CYAN}{{remaining}}{Colors.DARK_GRAY}] "
143
- f"{Colors.LIGHT_CYAN}{{rate_fmt}}",
143
+ f"{Colors.MAGENTA}{{bar:40}} "
144
+ f"{Colors.LIGHT_GREEN}{{n_fmt}}{Colors.WHITE}/{Colors.CYAN}{{total_fmt}}"
145
+ f" {Colors.DARK_GRAY}[{Colors.YELLOW}{{elapsed}}{Colors.WHITE} < {Colors.CYAN}{{remaining}}{Colors.DARK_GRAY}]"
146
+ f"{Colors.WHITE}{{postfix}} ",
144
147
  unit='B',
145
148
  unit_scale=True,
146
149
  unit_divisor=1024,
147
150
  mininterval=0.05,
148
151
  file=sys.stdout
149
152
  )
150
-
153
+
154
+ start_time = time.time()
151
155
  downloaded = 0
152
156
  with open(temp_path, 'wb') as file, progress_bar as bar:
153
157
  try:
@@ -160,6 +164,14 @@ def MP4_downloader(url: str, path: str, referer: str = None, headers_: dict = No
160
164
  size = file.write(chunk)
161
165
  downloaded += size
162
166
  bar.update(size)
167
+
168
+ # Update postfix with speed and final size
169
+ elapsed = time.time() - start_time
170
+ if elapsed > 0:
171
+ speed = downloaded / elapsed
172
+ speed_str = internet_manager.format_transfer_speed(speed)
173
+ postfix_str = f"{Colors.LIGHT_MAGENTA}@ {Colors.LIGHT_CYAN}{speed_str}"
174
+ bar.set_postfix_str(postfix_str)
163
175
 
164
176
  except KeyboardInterrupt:
165
177
  if not interrupt_handler.force_quit:
@@ -4,6 +4,7 @@ import time
4
4
  import logging
5
5
  import threading
6
6
  from collections import deque
7
+ from typing import Dict
7
8
 
8
9
 
9
10
  # External libraries
@@ -165,4 +166,49 @@ class M3U8_Ts_Estimator:
165
166
  """Stop speed monitoring thread."""
166
167
  self._running = False
167
168
  if self.speed_thread.is_alive():
168
- self.speed_thread.join(timeout=5.0)
169
+ self.speed_thread.join(timeout=5.0)
170
+
171
+ def get_speed_data(self) -> Dict[str, str]:
172
+ """Returns current speed data thread-safe."""
173
+ with self.lock:
174
+ return self.speed.copy()
175
+
176
+ def get_average_segment_size(self) -> int:
177
+ """Returns average segment size in bytes."""
178
+ with self.lock:
179
+ if not self.ts_file_sizes:
180
+ return 0
181
+ return int(sum(self.ts_file_sizes) / len(self.ts_file_sizes))
182
+
183
+ def get_stats(self, downloaded_count: int = None, total_segments: int = None) -> Dict:
184
+ """Returns comprehensive statistics for API."""
185
+ with self.lock:
186
+ avg_size = self.get_average_segment_size()
187
+ total_downloaded = sum(self.ts_file_sizes)
188
+
189
+ # Calculate ETA
190
+ eta_seconds = 0
191
+ if downloaded_count is not None and total_segments is not None:
192
+ speed = self.speed.get('download', 'N/A')
193
+ if speed != 'N/A' and ' ' in speed:
194
+ try:
195
+ speed_value, speed_unit = speed.split(' ', 1)
196
+ speed_bps = float(speed_value) * (1024 * 1024 if 'MB/s' in speed_unit else 1024 if 'KB/s' in speed_unit else 1)
197
+
198
+ remaining_segments = total_segments - downloaded_count
199
+ if remaining_segments > 0 and avg_size > 0 and speed_bps > 0:
200
+ eta_seconds = int((avg_size * remaining_segments) / speed_bps)
201
+
202
+ except Exception:
203
+ pass
204
+
205
+ return {
206
+ 'total_segments': self.total_segments,
207
+ 'downloaded_count': len(self.ts_file_sizes),
208
+ 'average_segment_size': avg_size,
209
+ 'total_downloaded_bytes': total_downloaded,
210
+ 'estimated_total_size': self.calculate_total_size(),
211
+ 'upload_speed': self.speed.get('upload', 'N/A'),
212
+ 'download_speed': self.speed.get('download', 'N/A'),
213
+ 'eta_seconds': eta_seconds
214
+ }
@@ -18,7 +18,6 @@ from StreamingCommunity.Util.config_json import config_manager
18
18
  from StreamingCommunity.Util.headers import get_userAgent
19
19
 
20
20
 
21
-
22
21
  # Variable
23
22
  if getattr(sys, 'frozen', False): # Modalità PyInstaller
24
23
  base_path = os.path.join(sys._MEIPASS, "StreamingCommunity")
@@ -26,6 +25,7 @@ else:
26
25
  base_path = os.path.dirname(__file__)
27
26
  console = Console()
28
27
 
28
+
29
29
  async def fetch_github_data(client, url):
30
30
  """Helper function to fetch data from GitHub API"""
31
31
  response = await client.get(
@@ -46,10 +46,23 @@ async def async_github_requests():
46
46
  ]
47
47
  return await asyncio.gather(*tasks)
48
48
 
49
+ def get_execution_mode():
50
+ """Get the execution mode of the application"""
51
+ if getattr(sys, 'frozen', False):
52
+ return "installer"
53
+
54
+ try:
55
+ package_location = importlib.metadata.files(__title__)
56
+ if any("site-packages" in str(path) for path in package_location):
57
+ return "pip"
58
+
59
+ except importlib.metadata.PackageNotFoundError:
60
+ pass
61
+
62
+ return "python"
63
+
49
64
  def update():
50
- """
51
- Check for updates on GitHub and display relevant information.
52
- """
65
+ """Check for updates on GitHub and display relevant information."""
53
66
  try:
54
67
  # Run async requests concurrently
55
68
  response_reposity, response_releases, response_commits = asyncio.run(async_github_requests())
@@ -93,7 +106,7 @@ def update():
93
106
  console.print(f"\n[cyan]New version available: [yellow]{last_version}")
94
107
 
95
108
  console.print(f"\n[red]{__title__} has been downloaded [yellow]{total_download_count} [red]times, but only [yellow]{percentual_stars}% [red]of users have starred it.\n\
96
- [green]Current installed version: [yellow]{current_version} [green]last commit: [white]''[yellow]{latest_commit_message.splitlines()[0]}[white]''\n\
109
+ [yellow]{get_execution_mode()} - [green]Current installed version: [yellow]{current_version} [green]last commit: [white]'[yellow]{latest_commit_message.splitlines()[0]}[white]'\n\
97
110
  [cyan]Help the repository grow today by leaving a [yellow]star [cyan]and [yellow]sharing [cyan]it with others online!")
98
111
 
99
- time.sleep(1)
112
+ time.sleep(0.8)
@@ -1,5 +1,5 @@
1
1
  __title__ = 'StreamingCommunity'
2
- __version__ = '3.4.0'
2
+ __version__ = '3.4.2'
3
3
  __author__ = 'Arrowar'
4
4
  __description__ = 'A command-line program to download film'
5
5
  __copyright__ = 'Copyright 2025'
@@ -12,7 +12,7 @@ from typing import Dict, List, Any
12
12
  from rich.console import Console
13
13
  from rich.table import Table
14
14
  from rich.prompt import Prompt
15
- from rich.style import Style
15
+ from rich import box
16
16
 
17
17
 
18
18
  # Internal utilities
@@ -38,6 +38,9 @@ class TVShowManager:
38
38
  self.slice_end = 10
39
39
  self.step = self.slice_end
40
40
  self.column_info = []
41
+ self.table_title = None
42
+ self.table_style = "blue"
43
+ self.show_lines = False
41
44
 
42
45
  def add_column(self, column_info: Dict[str, Dict[str, str]]) -> None:
43
46
  """
@@ -48,6 +51,26 @@ class TVShowManager:
48
51
  """
49
52
  self.column_info = column_info
50
53
 
54
+ def set_table_title(self, title: str) -> None:
55
+ """
56
+ Set the table title.
57
+
58
+ Parameters:
59
+ - title (str): The title to display above the table.
60
+ """
61
+ self.table_title = title
62
+
63
+ def set_table_style(self, style: str = "blue", show_lines: bool = False) -> None:
64
+ """
65
+ Set the table border style and row lines.
66
+
67
+ Parameters:
68
+ - style (str): Border color (e.g., "blue", "green", "magenta", "cyan")
69
+ - show_lines (bool): Whether to show lines between rows
70
+ """
71
+ self.table_style = style
72
+ self.show_lines = show_lines
73
+
51
74
  def add_tv_show(self, tv_show: Dict[str, Any]) -> None:
52
75
  """
53
76
  Add a TV show to the list of TV shows.
@@ -73,19 +96,38 @@ class TVShowManager:
73
96
  logging.error("Error: Column information not configured.")
74
97
  return
75
98
 
76
- table = Table(border_style="white")
99
+ # Create table with specified style
100
+ table = Table(
101
+ title=self.table_title,
102
+ box=box.ROUNDED,
103
+ show_header=True,
104
+ header_style="bold cyan",
105
+ border_style=self.table_style,
106
+ show_lines=self.show_lines,
107
+ padding=(0, 1)
108
+ )
77
109
 
78
110
  # Add columns dynamically based on provided column information
79
111
  for col_name, col_style in self.column_info.items():
80
- color = col_style.get("color", None)
81
- style = Style(color=color) if color else None
82
- table.add_column(col_name, style=style, justify='center')
112
+ color = col_style.get("color", "white")
113
+ width = col_style.get("width", None)
114
+ justify = col_style.get("justify", "center")
115
+
116
+ table.add_column(
117
+ col_name,
118
+ style=color,
119
+ justify=justify,
120
+ width=width
121
+ )
83
122
 
84
123
  # Add rows dynamically based on available TV show data
85
- for entry in data_slice:
124
+ for idx, entry in enumerate(data_slice):
86
125
  if entry:
87
126
  row_data = [str(entry.get(col_name, '')) for col_name in self.column_info.keys()]
88
- table.add_row(*row_data)
127
+
128
+ # Alternate row styling for better readability
129
+ style = "dim" if idx % 2 == 1 else None
130
+ table.add_row(*row_data, style=style)
89
131
 
90
132
  self.console.print(table)
91
133
 
@@ -162,7 +204,7 @@ class TVShowManager:
162
204
 
163
205
  self.display_data(current_slice)
164
206
 
165
- # Resto del codice rimane uguale...
207
+ # Get research function from call stack
166
208
  research_func = next((
167
209
  f for f in get_call_stack()
168
210
  if f['function'] == 'search' and f['script'] == '__init__.py'