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
@@ -10,12 +10,12 @@ from rich.console import Console
10
10
 
11
11
 
12
12
  # Internal utilities
13
- from StreamingCommunity.Util.config_json import config_manager, get_use_large_bar
14
- from StreamingCommunity.Util.os import os_manager, suppress_output, get_ffmpeg_path
13
+ from StreamingCommunity.Util.config_json import config_manager
14
+ from StreamingCommunity.Util.os import get_ffmpeg_path
15
15
 
16
16
 
17
17
  # Logic class
18
- from .util import need_to_force_to_ts, check_duration_v_a, get_video_duration
18
+ from .util import need_to_force_to_ts, check_duration_v_a
19
19
  from .capture import capture_ffmpeg_real_time
20
20
  from ..M3U8 import M3U8_Codec
21
21
 
@@ -82,6 +82,7 @@ def select_subtitle_encoder() -> Optional[str]:
82
82
  if mov_text_supported:
83
83
  logging.info("Using 'mov_text' as the subtitle encoder.")
84
84
  return "mov_text"
85
+
85
86
  elif webvtt_supported:
86
87
  logging.info("Using 'webvtt' as the subtitle encoder.")
87
88
  return "webvtt"
@@ -99,14 +100,6 @@ def join_video(video_path: str, out_path: str, codec: M3U8_Codec = None):
99
100
  - out_path (str): The path to save the output file.
100
101
  - codec (M3U8_Codec): The video codec to use. Defaults to 'copy'.
101
102
  """
102
- if video_path is None:
103
- console.log("[red]No video path provided for joining.")
104
- return None
105
-
106
- if out_path is None:
107
- console.log("[red]No output path provided for joining.")
108
- return None
109
-
110
103
  ffmpeg_cmd = [get_ffmpeg_path()]
111
104
 
112
105
  # Enabled the use of gpu
@@ -115,7 +108,6 @@ def join_video(video_path: str, out_path: str, codec: M3U8_Codec = None):
115
108
 
116
109
  # Add mpegts to force to detect input file as ts file
117
110
  if need_to_force_to_ts(video_path):
118
- #console.log("[red]Force input file to 'mpegts'.")
119
111
  ffmpeg_cmd.extend(['-f', 'mpegts'])
120
112
 
121
113
  # Insert input video path
@@ -162,21 +154,13 @@ def join_video(video_path: str, out_path: str, codec: M3U8_Codec = None):
162
154
  if DEBUG_MODE:
163
155
  subprocess.run(ffmpeg_cmd, check=True)
164
156
  else:
165
-
166
- if get_use_large_bar():
167
- capture_ffmpeg_real_time(ffmpeg_cmd, "[yellow][FFMPEG] [cyan]Join video")
168
- print()
169
-
170
- else:
171
- console.log("[purple]FFmpeg [white][[cyan]Join video[white]] ...")
172
- with suppress_output():
173
- capture_ffmpeg_real_time(ffmpeg_cmd, "[yellow][FFMPEG] [cyan]Join video")
174
- print()
157
+ capture_ffmpeg_real_time(ffmpeg_cmd, "[yellow][FFMPEG] [cyan]Join video")
158
+ print()
175
159
 
176
160
  return out_path
177
161
 
178
162
 
179
- def join_audios(video_path: str, audio_tracks: List[Dict[str, str]], out_path: str, codec: M3U8_Codec = None):
163
+ def join_audios(video_path: str, audio_tracks: List[Dict[str, str]], out_path: str, codec: M3U8_Codec = None, limit_duration_diff: float = 2.0):
180
164
  """
181
165
  Joins audio tracks with a video file using FFmpeg.
182
166
 
@@ -186,47 +170,31 @@ def join_audios(video_path: str, audio_tracks: List[Dict[str, str]], out_path: s
186
170
  Each dictionary should contain the 'path' and 'name' keys.
187
171
  - out_path (str): The path to save the output file.
188
172
  """
189
- if video_path is None:
190
- console.log("[red]No video path provided for joining audios.")
191
- return None, False
192
-
193
- if audio_tracks is None or len(audio_tracks) == 0:
194
- console.log("[red]No audio tracks provided for joining.")
195
- return None, False
196
-
197
- if out_path is None:
198
- console.log("[red]No output path provided for joining audios.")
199
- return None, False
200
-
201
173
  use_shortest = False
202
174
  duration_diffs = []
203
175
 
204
- # Get video duration first
205
- video_duration = get_video_duration(video_path, None)
206
-
207
176
  for audio_track in audio_tracks:
208
177
  audio_path = audio_track.get('path')
209
178
  audio_lang = audio_track.get('name', 'unknown')
210
- audio_duration, diff = check_duration_v_a(video_path, audio_path)
179
+ is_matched, diff, video_duration, audio_duration = check_duration_v_a(video_path, audio_path)
211
180
 
212
181
  duration_diffs.append({
213
182
  'language': audio_lang,
214
183
  'difference': diff,
215
- 'has_error': diff > 0.5,
184
+ 'has_error': diff > limit_duration_diff,
216
185
  'video_duration': video_duration,
217
186
  'audio_duration': audio_duration
218
187
  })
219
188
 
220
- if diff > 0.5:
189
+ # If any audio track has a significant duration difference, use -shortest
190
+ if diff > limit_duration_diff:
221
191
  use_shortest = True
222
- console.log("[red]Warning: Some audio tracks have duration differences (>0.5s)")
223
192
 
224
193
  # Print duration differences for each track
225
194
  if use_shortest:
226
195
  for track in duration_diffs:
227
196
  color = "red" if track['has_error'] else "green"
228
197
  console.print(f"[{color}]Audio {track['language']}: Video duration: {track['video_duration']:.2f}s, Audio duration: {track['audio_duration']:.2f}s, Difference: {track['difference']:.2f}s[/{color}]")
229
-
230
198
 
231
199
  # Start command with locate ffmpeg
232
200
  ffmpeg_cmd = [get_ffmpeg_path()]
@@ -240,10 +208,8 @@ def join_audios(video_path: str, audio_tracks: List[Dict[str, str]], out_path: s
240
208
 
241
209
  # Add audio tracks as input
242
210
  for i, audio_track in enumerate(audio_tracks):
243
- if os_manager.check_file(audio_track.get('path')):
244
- ffmpeg_cmd.extend(['-i', audio_track.get('path')])
245
- else:
246
- logging.error(f"Skip audio join: {audio_track.get('path')} dont exist")
211
+ ffmpeg_cmd.extend(['-i', audio_track.get('path')])
212
+
247
213
 
248
214
  # Map the video and audio streams
249
215
  ffmpeg_cmd.append('-map')
@@ -298,15 +264,8 @@ def join_audios(video_path: str, audio_tracks: List[Dict[str, str]], out_path: s
298
264
  subprocess.run(ffmpeg_cmd, check=True)
299
265
 
300
266
  else:
301
- if get_use_large_bar():
302
- capture_ffmpeg_real_time(ffmpeg_cmd, "[yellow][FFMPEG] [cyan]Join audio")
303
- print()
304
-
305
- else:
306
- console.log("[purple]FFmpeg [white][[cyan]Join audio[white]] ...")
307
- with suppress_output():
308
- capture_ffmpeg_real_time(ffmpeg_cmd, "[yellow][FFMPEG] [cyan]Join audio")
309
- print()
267
+ capture_ffmpeg_real_time(ffmpeg_cmd, "[yellow][FFMPEG] [cyan]Join audio")
268
+ print()
310
269
 
311
270
  return out_path, use_shortest
312
271
 
@@ -321,26 +280,11 @@ def join_subtitle(video_path: str, subtitles_list: List[Dict[str, str]], out_pat
321
280
  Each dictionary should contain the 'path' key with the path to the subtitle file and the 'name' key with the name of the subtitle.
322
281
  - out_path (str): The path to save the output file.
323
282
  """
324
- if video_path is None:
325
- console.log("[red]No video path provided for joining subtitles.")
326
- return None
327
-
328
- if subtitles_list is None or len(subtitles_list) == 0:
329
- console.log("[red]No subtitles provided for joining.")
330
- return None
331
-
332
- if out_path is None:
333
- console.log("[red]No output path provided for joining subtitles.")
334
- return None
335
-
336
283
  ffmpeg_cmd = [get_ffmpeg_path(), "-i", video_path]
337
284
 
338
285
  # Add subtitle input files first
339
286
  for subtitle in subtitles_list:
340
- if os_manager.check_file(subtitle.get('path')):
341
- ffmpeg_cmd += ["-i", subtitle['path']]
342
- else:
343
- logging.error(f"Skip subtitle join: {subtitle.get('path')} doesn't exist")
287
+ ffmpeg_cmd += ["-i", subtitle['path']]
344
288
 
345
289
  # Add maps for video and audio streams
346
290
  ffmpeg_cmd += ["-map", "0:v", "-map", "0:a"]
@@ -365,14 +309,7 @@ def join_subtitle(video_path: str, subtitles_list: List[Dict[str, str]], out_pat
365
309
  subprocess.run(ffmpeg_cmd, check=True)
366
310
 
367
311
  else:
368
- if get_use_large_bar():
369
- capture_ffmpeg_real_time(ffmpeg_cmd, "[yellow][FFMPEG] [cyan]Join subtitle")
370
- print()
371
-
372
- else:
373
- console.log("[purple]FFmpeg [white][[cyan]Join subtitle[white]] ...")
374
- with suppress_output():
375
- capture_ffmpeg_real_time(ffmpeg_cmd, "[yellow][FFMPEG] [cyan]Join subtitle")
376
- print()
312
+ capture_ffmpeg_real_time(ffmpeg_cmd, "[yellow][FFMPEG] [cyan]Join subtitle")
313
+ print()
377
314
 
378
315
  return out_path
@@ -154,7 +154,7 @@ def get_ffprobe_info(file_path):
154
154
  cmd,
155
155
  capture_output=True,
156
156
  text=True,
157
- check=False # Don't raise exception on non-zero exit
157
+ check=False
158
158
  )
159
159
 
160
160
  if result.returncode != 0:
@@ -206,7 +206,7 @@ def need_to_force_to_ts(file_path):
206
206
  file_info = get_ffprobe_info(file_path)
207
207
 
208
208
  if is_png_format_or_codec(file_info):
209
- console.print(f"[yellow]Warning: The input file [green]{os.path.basename(file_path)}[/green] is in PNG format or contains a PNG codec. It will be converted to TS format for processing.[/yellow]")
209
+ logging.info(f"File {file_path} is in PNG format or contains a PNG codec. Need to convert to TS format.")
210
210
  return True
211
211
 
212
212
  return False
@@ -222,7 +222,11 @@ def check_duration_v_a(video_path, audio_path, tolerance=1.0):
222
222
  - tolerance (float): Allowed tolerance for the duration difference (in seconds).
223
223
 
224
224
  Returns:
225
- - tuple: (bool, float) -> True if the duration of the video and audio matches, False otherwise, along with the difference in duration.
225
+ - tuple: (bool, float, float, float) ->
226
+ - Bool: True if the duration of the video and audio matches within tolerance
227
+ - Float: Difference in duration
228
+ - Float: Video duration
229
+ - Float: Audio duration
226
230
  """
227
231
  video_duration = get_video_duration(video_path, file_type="video")
228
232
  audio_duration = get_video_duration(audio_path, file_type="audio")
@@ -230,21 +234,21 @@ def check_duration_v_a(video_path, audio_path, tolerance=1.0):
230
234
  # Check if either duration is None and specify which one is None
231
235
  if video_duration is None and audio_duration is None:
232
236
  console.print("[yellow]Warning: Both video and audio durations are None. Returning 0 as duration difference.[/yellow]")
233
- return False, 0.0
237
+ return False, 0.0, 0.0, 0.0
234
238
 
235
239
  elif video_duration is None:
236
- console.print("[yellow]Warning: Video duration is None. Returning 0 as duration difference.[/yellow]")
237
- return False, 0.0
240
+ console.print("[yellow]Warning: Video duration is None. Using audio duration for calculation.[/yellow]")
241
+ return False, 0.0, 0.0, audio_duration
238
242
 
239
243
  elif audio_duration is None:
240
- console.print("[yellow]Warning: Audio duration is None. Returning 0 as duration difference.[/yellow]")
241
- return False, 0.0
244
+ console.print("[yellow]Warning: Audio duration is None. Using video duration for calculation.[/yellow]")
245
+ return False, 0.0, video_duration, 0.0
242
246
 
243
247
  # Calculate the duration difference
244
248
  duration_difference = abs(video_duration - audio_duration)
245
249
 
246
250
  # Check if the duration difference is within the tolerance
247
251
  if duration_difference <= tolerance:
248
- return True, duration_difference
252
+ return True, duration_difference, video_duration, audio_duration
249
253
  else:
250
- return False, duration_difference
254
+ return False, duration_difference, video_duration, audio_duration
@@ -54,6 +54,7 @@ class M3U8_Ts_Estimator:
54
54
  speed_buffer = deque(maxlen=3)
55
55
  error_count = 0
56
56
  max_errors = 5
57
+ current_interval = 0.1
57
58
 
58
59
  while self._running:
59
60
  try:
@@ -64,12 +65,16 @@ class M3U8_Ts_Estimator:
64
65
  current_upload, current_download = io_counters.bytes_sent, io_counters.bytes_recv
65
66
 
66
67
  if last_upload and last_download:
67
- upload_speed = (current_upload - last_upload) / interval
68
- download_speed = (current_download - last_download) / interval
68
+ upload_speed = (current_upload - last_upload) / current_interval
69
+ download_speed = (current_download - last_download) / current_interval
69
70
 
70
71
  if download_speed > 1024:
71
72
  speed_buffer.append(download_speed)
72
73
 
74
+ # Increase interval if we have a stable speed measurement
75
+ if len(speed_buffer) >= 2:
76
+ current_interval = min(interval, current_interval * 1.5)
77
+
73
78
  if speed_buffer:
74
79
  avg_speed = sum(speed_buffer) / len(speed_buffer)
75
80
 
@@ -77,7 +82,6 @@ class M3U8_Ts_Estimator:
77
82
  formatted_upload = internet_manager.format_transfer_speed(max(0, upload_speed))
78
83
  formatted_download = internet_manager.format_transfer_speed(avg_speed)
79
84
 
80
- # Lock minimale
81
85
  with self.lock:
82
86
  self.speed = {
83
87
  "upload": formatted_upload,
@@ -99,9 +103,9 @@ class M3U8_Ts_Estimator:
99
103
  if error_count > max_errors:
100
104
  with self.lock:
101
105
  self.speed = {"upload": "N/A", "download": "N/A"}
102
- interval = 10.0
103
-
104
- time.sleep(interval)
106
+ current_interval = 10.0
107
+
108
+ time.sleep(current_interval)
105
109
 
106
110
  def calculate_total_size(self) -> str:
107
111
  """
@@ -133,11 +137,8 @@ class M3U8_Ts_Estimator:
133
137
  """
134
138
  try:
135
139
  self.add_ts_file(segment_size)
136
-
137
- with self.lock:
138
- self.downloaded_segments_count += 1
139
-
140
140
  file_total_size = self.calculate_total_size()
141
+
141
142
  if file_total_size == "Error":
142
143
  return
143
144
 
@@ -152,8 +153,8 @@ class M3U8_Ts_Estimator:
152
153
  average_internet_speed, average_internet_unit = "N/A", ""
153
154
 
154
155
  progress_str = (
155
- f"{Colors.GREEN}{number_file_total_size} {Colors.RED}{units_file_total_size}"
156
- f"{Colors.WHITE}, {Colors.CYAN}{average_internet_speed} {Colors.RED}{average_internet_unit} "
156
+ f"{Colors.LIGHT_GREEN}{number_file_total_size} {Colors.LIGHT_MAGENTA}{units_file_total_size} {Colors.WHITE}"
157
+ f"{Colors.DARK_GRAY}@ {Colors.LIGHT_CYAN}{average_internet_speed} {Colors.LIGHT_MAGENTA}{average_internet_unit}"
157
158
  )
158
159
 
159
160
  progress_counter.set_postfix_str(progress_str)
@@ -381,6 +381,7 @@ class M3U8_Subtitle:
381
381
  class M3U8_Parser:
382
382
  def __init__(self):
383
383
  self.is_master_playlist = None
384
+ self.init_segment = None
384
385
  self.segments = []
385
386
  self.video_playlist = []
386
387
  self.keys = None
@@ -403,11 +404,12 @@ class M3U8_Parser:
403
404
  - m3u8_content (str): The content of the M3U8 file.
404
405
  """
405
406
  m3u8_obj = loads(raw_content, uri)
406
-
407
+
407
408
  self.__parse_video_info__(m3u8_obj)
408
409
  self.__parse_subtitles_and_audio__(m3u8_obj)
409
410
  self.__parse_segments__(m3u8_obj)
410
411
  self.is_master_playlist = self.__is_master__(m3u8_obj)
412
+ self.init_segment = self.__parse_init_segment__(m3u8_obj)
411
413
 
412
414
  @staticmethod
413
415
  def extract_resolution(uri: str) -> int:
@@ -457,9 +459,6 @@ class M3U8_Parser:
457
459
  """
458
460
  Determines if the given M3U8 object is a master playlist.
459
461
 
460
- Parameters:
461
- - m3u8_obj (m3u8.M3U8): The parsed M3U8 object.
462
-
463
462
  Returns:
464
463
  - bool: True if it's a master playlist, False if it's a media playlist, None if unknown.
465
464
  """
@@ -478,9 +477,6 @@ class M3U8_Parser:
478
477
  def __parse_video_info__(self, m3u8_obj) -> None:
479
478
  """
480
479
  Extracts video information from the M3U8 object.
481
-
482
- Parameters:
483
- - m3u8_obj: The M3U8 object containing video playlists.
484
480
  """
485
481
  try:
486
482
  for playlist in m3u8_obj.playlists:
@@ -526,9 +522,6 @@ class M3U8_Parser:
526
522
  def __parse_encryption_keys__(self, obj) -> None:
527
523
  """
528
524
  Extracts encryption keys either from the M3U8 object or from individual segments.
529
-
530
- Parameters:
531
- - obj: Either the main M3U8 object or an individual segment.
532
525
  """
533
526
  try:
534
527
  if hasattr(obj, 'key') and obj.key is not None:
@@ -557,9 +550,6 @@ class M3U8_Parser:
557
550
  def __parse_subtitles_and_audio__(self, m3u8_obj) -> None:
558
551
  """
559
552
  Extracts subtitles and audio information from the M3U8 object.
560
-
561
- Parameters:
562
- - m3u8_obj: The M3U8 object containing subtitles and audio data.
563
553
  """
564
554
  try:
565
555
  for media in m3u8_obj.media:
@@ -587,9 +577,6 @@ class M3U8_Parser:
587
577
  def __parse_segments__(self, m3u8_obj) -> None:
588
578
  """
589
579
  Extracts segment information from the M3U8 object.
590
-
591
- Parameters:
592
- - m3u8_obj: The M3U8 object containing segment data.
593
580
  """
594
581
  try:
595
582
  for segment in m3u8_obj.segments:
@@ -612,6 +599,19 @@ class M3U8_Parser:
612
599
  except Exception as e:
613
600
  logging.error(f"Error parsing segments: {e}")
614
601
 
602
+ def __parse_init_segment__(self, m3u8_obj) -> None:
603
+ """
604
+ Extracts initialization segment information from the M3U8 object.
605
+ """
606
+ try:
607
+ if len(m3u8_obj.segment_map) > 0:
608
+ init_segment = m3u8_obj.segment_map[0].uri
609
+ return init_segment
610
+
611
+ except Exception as e:
612
+ logging.error(f"Error parsing initialization segment: {e}")
613
+ return None
614
+
615
615
  def __create_variable__(self):
616
616
  """
617
617
  Initialize variables for video, audio, and subtitle playlists.
@@ -88,14 +88,12 @@ def update():
88
88
  latest_commit_message = latest_commit.get('commit', {}).get('message', 'No commit message')
89
89
  else:
90
90
  latest_commit_message = 'No commit history available'
91
-
92
- console.print(f"\n[cyan]Current installed version: [yellow]{current_version}")
93
- console.print(f"[cyan]Last commit: [yellow]{latest_commit_message.splitlines()[0]}")
94
91
 
95
92
  if str(current_version).replace('v', '') != str(last_version).replace('v', ''):
96
93
  console.print(f"\n[cyan]New version available: [yellow]{last_version}")
97
94
 
98
95
  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\
99
97
  [cyan]Help the repository grow today by leaving a [yellow]star [cyan]and [yellow]sharing [cyan]it with others online!")
100
98
 
101
- time.sleep(2)
99
+ time.sleep(1)
@@ -1,5 +1,5 @@
1
1
  __title__ = 'StreamingCommunity'
2
- __version__ = '3.3.6'
2
+ __version__ = '3.3.8'
3
3
  __author__ = 'Arrowar'
4
4
  __description__ = 'A command-line program to download film'
5
- __copyright__ = 'Copyright 2025'
5
+ __copyright__ = 'Copyright 2025'
@@ -53,7 +53,6 @@ class ConfigManager:
53
53
  self.cache = {}
54
54
 
55
55
  self.fetch_domain_online = True
56
- self.validate_github_config = False
57
56
 
58
57
  console.print(f"[bold cyan]Initializing ConfigManager:[/bold cyan] [green]{self.file_path}[/green]")
59
58
 
@@ -64,7 +63,7 @@ class ConfigManager:
64
63
  """Load the configuration and initialize all settings."""
65
64
  if not os.path.exists(self.file_path):
66
65
  console.print(f"[bold red]WARNING: Configuration file not found:[/bold red] {self.file_path}")
67
- console.print("[bold yellow]Attempting to download from reference repository...[/bold yellow]")
66
+ console.print("[bold yellow]Downloading from repository...[/bold yellow]")
68
67
  self._download_reference_config()
69
68
 
70
69
  # Load the configuration file
@@ -75,12 +74,6 @@ class ConfigManager:
75
74
 
76
75
  # Update settings from the configuration
77
76
  self._update_settings_from_config()
78
-
79
- # Validate and update the configuration if requested
80
- if self.validate_github_config:
81
- self._validate_and_update_config()
82
- else:
83
- console.print("[bold yellow]GitHub validation disabled[/bold yellow]")
84
77
 
85
78
  # Load site data based on fetch_domain_online setting
86
79
  self._load_site_data()
@@ -115,14 +108,12 @@ class ConfigManager:
115
108
 
116
109
  # Get fetch_domain_online setting (True by default)
117
110
  self.fetch_domain_online = default_section.get('fetch_domain_online', True)
118
- self.validate_github_config = default_section.get('validate_github_config', False)
119
111
 
120
112
  console.print(f"[bold cyan]Fetch domains online:[/bold cyan] [{'green' if self.fetch_domain_online else 'yellow'}]{self.fetch_domain_online}[/{'green' if self.fetch_domain_online else 'yellow'}]")
121
- console.print(f"[bold cyan]GitHub configuration validation:[/bold cyan] [{'green' if self.validate_github_config else 'yellow'}]{self.validate_github_config}[/{'green' if self.validate_github_config else 'yellow'}]")
122
113
 
123
114
  def _download_reference_config(self) -> None:
124
115
  """Download the reference configuration from GitHub."""
125
- console.print(f"[bold cyan]Downloading reference configuration:[/bold cyan] [green]{self.reference_config_url}[/green]")
116
+ console.print(f"[bold cyan]Downloading configuration:[/bold cyan] [green]{self.reference_config_url}[/green]")
126
117
 
127
118
  try:
128
119
  response = requests.get(self.reference_config_url, timeout=8, headers={'User-Agent': get_userAgent()})
@@ -133,7 +124,6 @@ class ConfigManager:
133
124
  file_size = len(response.content) / 1024
134
125
  console.print(f"[bold green]Download complete:[/bold green] {os.path.basename(self.file_path)} ({file_size:.2f} KB)")
135
126
  else:
136
-
137
127
  error_msg = f"HTTP Error: {response.status_code}, Response: {response.text[:100]}"
138
128
  console.print(f"[bold red]Download failed:[/bold red] {error_msg}")
139
129
  raise Exception(error_msg)
@@ -142,107 +132,6 @@ class ConfigManager:
142
132
  console.print(f"[bold red]Download error:[/bold red] {str(e)}")
143
133
  raise
144
134
 
145
- def _validate_and_update_config(self) -> None:
146
- """Validate the local configuration against the reference one and update missing keys."""
147
- try:
148
- # Download the reference configuration
149
- console.print("[bold cyan]Validating configuration with GitHub...[/bold cyan]")
150
- response = requests.get(self.reference_config_url, timeout=8, headers={'User-Agent': get_userAgent()})
151
-
152
- if not response.ok:
153
- raise Exception(f"Error downloading reference configuration. Code: {response.status_code}")
154
-
155
- reference_config = response.json()
156
-
157
- # Compare and update missing keys
158
- merged_config = self._deep_merge_configs(self.config, reference_config)
159
-
160
- if merged_config != self.config:
161
- added_keys = self._get_added_keys(self.config, merged_config)
162
-
163
- # Save the merged configuration
164
- with open(self.file_path, 'w') as f:
165
- json.dump(merged_config, f, indent=4)
166
-
167
- key_examples = ', '.join(added_keys[:5])
168
- if len(added_keys) > 5:
169
- key_examples += ' and others...'
170
-
171
- console.print(f"[bold green]Configuration updated with {len(added_keys)} new keys:[/bold green] {key_examples}")
172
-
173
- # Update the configuration in memory
174
- self.config = merged_config
175
- self._update_settings_from_config()
176
- else:
177
- console.print("[bold green]The configuration is up to date.[/bold green]")
178
-
179
- except Exception as e:
180
- console.print(f"[bold red]Error validating configuration:[/bold red] {str(e)}")
181
-
182
- def _get_added_keys(self, old_config: dict, new_config: dict, prefix="") -> list:
183
- """
184
- Get the list of keys added in the new configuration compared to the old one.
185
-
186
- Args:
187
- old_config (dict): Original configuration
188
- new_config (dict): New configuration
189
- prefix (str): Prefix for nested keys
190
-
191
- Returns:
192
- list: List of added key names
193
- """
194
- added_keys = []
195
-
196
- for key, value in new_config.items():
197
- full_key = f"{prefix}.{key}" if prefix else key
198
-
199
- if key not in old_config:
200
- added_keys.append(full_key)
201
- elif isinstance(value, dict) and isinstance(old_config.get(key), dict):
202
- added_keys.extend(self._get_added_keys(old_config[key], value, full_key))
203
-
204
- return added_keys
205
-
206
- def _deep_merge_configs(self, local_config: dict, reference_config: dict) -> dict:
207
- """
208
- Recursively merge the reference configuration into the local one, preserving local values.
209
-
210
- Args:
211
- local_config (dict): Local configuration
212
- reference_config (dict): Reference configuration
213
-
214
- Returns:
215
- dict: Merged configuration
216
- """
217
- merged = local_config.copy()
218
-
219
- for key, value in reference_config.items():
220
- if key not in merged:
221
-
222
- # Create the key if it doesn't exist
223
- merged[key] = value
224
- elif isinstance(value, dict) and isinstance(merged[key], dict):
225
-
226
- # Handle the DEFAULT section specially
227
- if key == 'DEFAULT':
228
-
229
- # Make sure control keys maintain local values
230
- merged_section = self._deep_merge_configs(merged[key], value)
231
-
232
- # Preserve local values for critical settings
233
- if 'fetch_domain_online' in merged[key]:
234
- merged_section['fetch_domain_online'] = merged[key]['fetch_domain_online']
235
- if 'validate_github_config' in merged[key]:
236
- merged_section['validate_github_config'] = merged[key]['validate_github_config']
237
-
238
- merged[key] = merged_section
239
- else:
240
-
241
- # Normal merge for other sections
242
- merged[key] = self._deep_merge_configs(merged[key], value)
243
-
244
- return merged
245
-
246
135
  def _load_site_data(self) -> None:
247
136
  """Load site data based on fetch_domain_online setting."""
248
137
  if self.fetch_domain_online:
@@ -258,7 +147,6 @@ class ConfigManager:
258
147
  }
259
148
 
260
149
  try:
261
- console.print("[bold cyan]Fetching domains from GitHub:[/bold cyan]")
262
150
  response = requests.get(domains_github_url, timeout=8, headers=headers)
263
151
 
264
152
  if response.ok:
@@ -267,9 +155,6 @@ class ConfigManager:
267
155
  # Determine which file to save to
268
156
  self._save_domains_to_appropriate_location()
269
157
 
270
- site_count = len(self.configSite) if isinstance(self.configSite, dict) else 0
271
- console.print(f"[bold green]Domains loaded from GitHub:[/bold green] {site_count} streaming services found.")
272
-
273
158
  else:
274
159
  console.print(f"[bold red]GitHub request failed:[/bold red] HTTP {response.status_code}, {response.text[:100]}")
275
160
  self._handle_site_data_fallback()
@@ -296,21 +181,18 @@ class ConfigManager:
296
181
 
297
182
  try:
298
183
  if os.path.exists(github_domains_path):
299
-
184
+
300
185
  # Update existing GitHub structure file
301
186
  with open(github_domains_path, 'w', encoding='utf-8') as f:
302
187
  json.dump(self.configSite, f, indent=4, ensure_ascii=False)
303
- console.print(f"[bold green]Domains updated in GitHub structure:[/bold green] {github_domains_path}")
304
188
 
305
189
  elif not os.path.exists(self.domains_path):
306
-
307
190
  # Save to root only if it doesn't exist and GitHub structure doesn't exist
308
191
  with open(self.domains_path, 'w', encoding='utf-8') as f:
309
192
  json.dump(self.configSite, f, indent=4, ensure_ascii=False)
310
193
  console.print(f"[bold green]Domains saved to:[/bold green] {self.domains_path}")
311
194
 
312
195
  else:
313
-
314
196
  # Root file exists, don't overwrite it
315
197
  console.print(f"[bold yellow]Local domains.json already exists, not overwriting:[/bold yellow] {self.domains_path}")
316
198
  console.print("[bold yellow]Tip: Delete the file if you want to recreate it from GitHub[/bold yellow]")
@@ -636,16 +518,5 @@ class ConfigManager:
636
518
  return section in config_source
637
519
 
638
520
 
639
- def get_use_large_bar():
640
- """
641
- Determine if the large bar feature should be enabled.
642
-
643
- Returns:
644
- bool: True if running on PC (Windows, macOS, Linux),
645
- False if running on Android or iOS.
646
- """
647
- return not any(platform in sys.platform for platform in ("android", "ios"))
648
-
649
-
650
521
  # Initialize the ConfigManager when the module is imported
651
522
  config_manager = ConfigManager()