StreamingCommunity 3.3.8__py3-none-any.whl → 3.4.0__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 (64) hide show
  1. StreamingCommunity/Api/Player/hdplayer.py +0 -5
  2. StreamingCommunity/Api/Player/mediapolisvod.py +4 -13
  3. StreamingCommunity/Api/Player/supervideo.py +3 -8
  4. StreamingCommunity/Api/Player/sweetpixel.py +1 -9
  5. StreamingCommunity/Api/Player/vixcloud.py +5 -16
  6. StreamingCommunity/Api/Site/altadefinizione/film.py +4 -15
  7. StreamingCommunity/Api/Site/altadefinizione/site.py +2 -7
  8. StreamingCommunity/Api/Site/altadefinizione/util/ScrapeSerie.py +2 -7
  9. StreamingCommunity/Api/Site/animeunity/site.py +9 -24
  10. StreamingCommunity/Api/Site/animeunity/util/ScrapeSerie.py +11 -27
  11. StreamingCommunity/Api/Site/animeworld/film.py +4 -2
  12. StreamingCommunity/Api/Site/animeworld/site.py +3 -11
  13. StreamingCommunity/Api/Site/animeworld/util/ScrapeSerie.py +1 -4
  14. StreamingCommunity/Api/Site/crunchyroll/film.py +17 -8
  15. StreamingCommunity/Api/Site/crunchyroll/series.py +8 -9
  16. StreamingCommunity/Api/Site/crunchyroll/site.py +14 -16
  17. StreamingCommunity/Api/Site/crunchyroll/util/ScrapeSerie.py +18 -65
  18. StreamingCommunity/Api/Site/crunchyroll/util/get_license.py +97 -106
  19. StreamingCommunity/Api/Site/guardaserie/site.py +4 -12
  20. StreamingCommunity/Api/Site/guardaserie/util/ScrapeSerie.py +3 -10
  21. StreamingCommunity/Api/Site/mediasetinfinity/film.py +11 -12
  22. StreamingCommunity/Api/Site/mediasetinfinity/series.py +1 -2
  23. StreamingCommunity/Api/Site/mediasetinfinity/site.py +3 -11
  24. StreamingCommunity/Api/Site/mediasetinfinity/util/ScrapeSerie.py +39 -50
  25. StreamingCommunity/Api/Site/mediasetinfinity/util/fix_mpd.py +3 -3
  26. StreamingCommunity/Api/Site/mediasetinfinity/util/get_license.py +8 -26
  27. StreamingCommunity/Api/Site/raiplay/film.py +6 -7
  28. StreamingCommunity/Api/Site/raiplay/series.py +1 -12
  29. StreamingCommunity/Api/Site/raiplay/site.py +8 -24
  30. StreamingCommunity/Api/Site/raiplay/util/ScrapeSerie.py +15 -22
  31. StreamingCommunity/Api/Site/raiplay/util/get_license.py +3 -12
  32. StreamingCommunity/Api/Site/streamingcommunity/film.py +5 -16
  33. StreamingCommunity/Api/Site/streamingcommunity/site.py +3 -22
  34. StreamingCommunity/Api/Site/streamingcommunity/util/ScrapeSerie.py +11 -26
  35. StreamingCommunity/Api/Site/streamingwatch/__init__.py +1 -0
  36. StreamingCommunity/Api/Site/streamingwatch/film.py +4 -2
  37. StreamingCommunity/Api/Site/streamingwatch/series.py +1 -1
  38. StreamingCommunity/Api/Site/streamingwatch/site.py +4 -18
  39. StreamingCommunity/Api/Site/streamingwatch/util/ScrapeSerie.py +0 -3
  40. StreamingCommunity/Api/Template/config_loader.py +0 -7
  41. StreamingCommunity/Lib/Downloader/DASH/cdm_helpher.py +8 -3
  42. StreamingCommunity/Lib/Downloader/DASH/decrypt.py +55 -1
  43. StreamingCommunity/Lib/Downloader/DASH/downloader.py +139 -55
  44. StreamingCommunity/Lib/Downloader/DASH/parser.py +458 -101
  45. StreamingCommunity/Lib/Downloader/DASH/segments.py +131 -74
  46. StreamingCommunity/Lib/Downloader/HLS/downloader.py +31 -50
  47. StreamingCommunity/Lib/Downloader/HLS/segments.py +266 -365
  48. StreamingCommunity/Lib/Downloader/MP4/downloader.py +1 -1
  49. StreamingCommunity/Lib/FFmpeg/capture.py +37 -5
  50. StreamingCommunity/Lib/FFmpeg/command.py +35 -93
  51. StreamingCommunity/Lib/M3U8/estimator.py +0 -1
  52. StreamingCommunity/Lib/TMBD/tmdb.py +2 -4
  53. StreamingCommunity/TelegramHelp/config.json +0 -1
  54. StreamingCommunity/Upload/version.py +1 -1
  55. StreamingCommunity/Util/config_json.py +28 -21
  56. StreamingCommunity/Util/http_client.py +28 -0
  57. StreamingCommunity/Util/os.py +16 -6
  58. {streamingcommunity-3.3.8.dist-info → streamingcommunity-3.4.0.dist-info}/METADATA +1 -3
  59. streamingcommunity-3.4.0.dist-info/RECORD +111 -0
  60. streamingcommunity-3.3.8.dist-info/RECORD +0 -111
  61. {streamingcommunity-3.3.8.dist-info → streamingcommunity-3.4.0.dist-info}/WHEEL +0 -0
  62. {streamingcommunity-3.3.8.dist-info → streamingcommunity-3.4.0.dist-info}/entry_points.txt +0 -0
  63. {streamingcommunity-3.3.8.dist-info → streamingcommunity-3.4.0.dist-info}/licenses/LICENSE +0 -0
  64. {streamingcommunity-3.3.8.dist-info → streamingcommunity-3.4.0.dist-info}/top_level.txt +0 -0
@@ -137,7 +137,7 @@ def MP4_downloader(url: str, path: str, referer: str = None, headers_: dict = No
137
137
  progress_bar = tqdm(
138
138
  total=total,
139
139
  ascii='░▒█',
140
- bar_format=f"{Colors.YELLOW}[MP4]{Colors.CYAN} Downloading{Colors.WHITE}: "
140
+ bar_format=f"{Colors.YELLOW}MP4{Colors.CYAN} Downloading{Colors.WHITE}: "
141
141
  f"{Colors.RED}{{percentage:.1f}}% {Colors.MAGENTA}{{bar:40}} {Colors.WHITE}"
142
142
  f"{Colors.DARK_GRAY}[{Colors.YELLOW}{{elapsed}}{Colors.WHITE} < {Colors.CYAN}{{remaining}}{Colors.DARK_GRAY}] "
143
143
  f"{Colors.LIGHT_CYAN}{{rate_fmt}}",
@@ -1,6 +1,7 @@
1
1
  # 16.04.24
2
2
 
3
3
  import re
4
+ import time
4
5
  import logging
5
6
  import threading
6
7
  import subprocess
@@ -29,6 +30,7 @@ def capture_output(process: subprocess.Popen, description: str) -> None:
29
30
  """
30
31
  try:
31
32
  max_length = 0
33
+ start_time = time.time()
32
34
 
33
35
  for line in iter(process.stdout.readline, ''):
34
36
  try:
@@ -44,8 +46,7 @@ def capture_output(process: subprocess.Popen, description: str) -> None:
44
46
 
45
47
  if "size=" in line:
46
48
  try:
47
-
48
- # Parse the output line to extract relevant information
49
+ elapsed_time = time.time() - start_time
49
50
  data = parse_output_line(line)
50
51
 
51
52
  if 'q' in data:
@@ -55,11 +56,25 @@ def capture_output(process: subprocess.Popen, description: str) -> None:
55
56
  else:
56
57
  byte_size = int(re.findall(r'\d+', data.get('size', '0'))[0]) * 1000
57
58
 
59
+ # Extract additional information
60
+ fps = data.get('fps', 'N/A')
61
+ time_processed = data.get('time', 'N/A')
62
+ bitrate = data.get('bitrate', 'N/A')
63
+ speed = data.get('speed', 'N/A')
64
+
65
+ # Format elapsed time as HH:MM:SS
66
+ elapsed_formatted = format_time(elapsed_time)
58
67
 
59
68
  # Construct the progress string with formatted output information
60
- progress_string = (f"{description}[white]: "
61
- f"([green]'speed': [yellow]{data.get('speed', 'N/A')}[white], "
62
- f"[green]'size': [yellow]{internet_manager.format_file_size(byte_size)}[white])")
69
+ progress_string = (
70
+ f"{description}[white]: "
71
+ f"([green]'fps': [yellow]{fps}[white], "
72
+ f"[green]'speed': [yellow]{speed}[white], "
73
+ f"[green]'size': [yellow]{internet_manager.format_file_size(byte_size)}[white], "
74
+ f"[green]'time': [yellow]{time_processed}[white], "
75
+ f"[green]'bitrate': [yellow]{bitrate}[white], "
76
+ f"[green]'elapsed': [yellow]{elapsed_formatted}[white])"
77
+ )
63
78
  max_length = max(max_length, len(progress_string))
64
79
 
65
80
  # Print the progress string to the console, overwriting the previous line
@@ -81,6 +96,19 @@ def capture_output(process: subprocess.Popen, description: str) -> None:
81
96
  logging.error(f"Error terminating process: {e}")
82
97
 
83
98
 
99
+ def format_time(seconds: float) -> str:
100
+ """
101
+ Format seconds into HH:MM:SS format.
102
+
103
+ Parameters:
104
+ - seconds (float): Time in seconds.
105
+ """
106
+ hours = int(seconds // 3600)
107
+ minutes = int((seconds % 3600) // 60)
108
+ secs = int(seconds % 60)
109
+ return f"{hours:02d}:{minutes:02d}:{secs:02d}"
110
+
111
+
84
112
  def parse_output_line(line: str) -> dict:
85
113
  """
86
114
  Function to parse the output line and extract relevant information.
@@ -101,6 +129,10 @@ def parse_output_line(line: str) -> dict:
101
129
  if len(key_value) == 2:
102
130
  key = key_value[0]
103
131
  value = key_value[1]
132
+
133
+ # Remove milliseconds from time value
134
+ if key == 'time' and isinstance(value, str) and '.' in value:
135
+ value = value.split('.')[0]
104
136
  data[key] = value
105
137
 
106
138
  return data
@@ -23,18 +23,30 @@ from ..M3U8 import M3U8_Codec
23
23
  # Config
24
24
  DEBUG_MODE = config_manager.get_bool("DEFAULT", "debug")
25
25
  DEBUG_FFMPEG = "debug" if DEBUG_MODE else "error"
26
- USE_CODEC = config_manager.get_bool("M3U8_CONVERSION", "use_codec")
27
- USE_VCODEC = config_manager.get_bool("M3U8_CONVERSION", "use_vcodec")
28
- USE_ACODEC = config_manager.get_bool("M3U8_CONVERSION", "use_acodec")
29
- USE_BITRATE = config_manager.get_bool("M3U8_CONVERSION", "use_bitrate")
30
26
  USE_GPU = config_manager.get_bool("M3U8_CONVERSION", "use_gpu")
31
- FFMPEG_DEFAULT_PRESET = config_manager.get("M3U8_CONVERSION", "default_preset")
27
+ PARAM_VIDEO = config_manager.get_list("M3U8_CONVERSION", "param_video")
28
+ PARAM_AUDIO = config_manager.get_list("M3U8_CONVERSION", "param_audio")
29
+ PARAM_FINAL = config_manager.get_list("M3U8_CONVERSION", "param_final")
32
30
 
33
31
 
34
32
  # Variable
35
33
  console = Console()
36
34
 
37
35
 
36
+ def add_encoding_params(ffmpeg_cmd: List[str]):
37
+ """
38
+ Add encoding parameters to the ffmpeg command.
39
+
40
+ Parameters:
41
+ ffmpeg_cmd (List[str]): List of the FFmpeg command to modify
42
+ """
43
+ if PARAM_FINAL:
44
+ ffmpeg_cmd.extend(PARAM_FINAL)
45
+ else:
46
+ ffmpeg_cmd.extend(PARAM_VIDEO)
47
+ ffmpeg_cmd.extend(PARAM_AUDIO)
48
+
49
+
38
50
  def check_subtitle_encoders() -> Tuple[Optional[bool], Optional[bool]]:
39
51
  """
40
52
  Executes 'ffmpeg -encoders' and checks if 'mov_text' and 'webvtt' encoders are available.
@@ -51,7 +63,6 @@ def check_subtitle_encoders() -> Tuple[Optional[bool], Optional[bool]]:
51
63
  check=True
52
64
  )
53
65
 
54
- # Check for encoder presence in output
55
66
  output = result.stdout
56
67
  mov_text_supported = "mov_text" in output
57
68
  webvtt_supported = "webvtt" in output
@@ -74,11 +85,9 @@ def select_subtitle_encoder() -> Optional[str]:
74
85
  """
75
86
  mov_text_supported, webvtt_supported = check_subtitle_encoders()
76
87
 
77
- # Return early if check failed
78
88
  if mov_text_supported is None:
79
89
  return None
80
90
 
81
- # Prioritize mov_text over webvtt
82
91
  if mov_text_supported:
83
92
  logging.info("Using 'mov_text' as the subtitle encoder.")
84
93
  return "mov_text"
@@ -98,7 +107,7 @@ def join_video(video_path: str, out_path: str, codec: M3U8_Codec = None):
98
107
  Parameters:
99
108
  - video_path (str): The path to the video file.
100
109
  - out_path (str): The path to save the output file.
101
- - codec (M3U8_Codec): The video codec to use. Defaults to 'copy'.
110
+ - codec (M3U8_Codec): The video codec to use (non utilizzato con nuova configurazione).
102
111
  """
103
112
  ffmpeg_cmd = [get_ffmpeg_path()]
104
113
 
@@ -113,48 +122,17 @@ def join_video(video_path: str, out_path: str, codec: M3U8_Codec = None):
113
122
  # Insert input video path
114
123
  ffmpeg_cmd.extend(['-i', video_path])
115
124
 
116
- # Add output Parameters
117
- if USE_CODEC and codec is not None:
118
- if USE_VCODEC:
119
- if codec.video_codec_name:
120
- if not USE_GPU:
121
- ffmpeg_cmd.extend(['-c:v', codec.video_codec_name])
122
- else:
123
- ffmpeg_cmd.extend(['-c:v', 'h264_nvenc'])
124
- else:
125
- console.log("[red]Cant find vcodec for 'join_audios'")
126
- else:
127
- if USE_GPU:
128
- ffmpeg_cmd.extend(['-c:v', 'h264_nvenc'])
129
-
130
-
131
- if USE_ACODEC:
132
- if codec.audio_codec_name:
133
- ffmpeg_cmd.extend(['-c:a', codec.audio_codec_name])
134
- else:
135
- console.log("[red]Cant find acodec for 'join_audios'")
136
-
137
- if USE_BITRATE:
138
- ffmpeg_cmd.extend(['-b:v', f'{codec.video_bitrate // 1000}k'])
139
- ffmpeg_cmd.extend(['-b:a', f'{codec.audio_bitrate // 1000}k'])
140
-
141
- else:
142
- ffmpeg_cmd.extend(['-c', 'copy'])
143
-
144
- # Ultrafast preset always or fast for gpu
145
- if not USE_GPU:
146
- ffmpeg_cmd.extend(['-preset', FFMPEG_DEFAULT_PRESET])
147
- else:
148
- ffmpeg_cmd.extend(['-preset', 'fast'])
125
+ # Add encoding parameters (prima dell'output)
126
+ add_encoding_params(ffmpeg_cmd)
149
127
 
150
- # Overwrite
151
- ffmpeg_cmd += [out_path, "-y"]
128
+ # Output file and overwrite
129
+ ffmpeg_cmd.extend([out_path, '-y'])
152
130
 
153
131
  # Run join
154
132
  if DEBUG_MODE:
155
133
  subprocess.run(ffmpeg_cmd, check=True)
156
134
  else:
157
- capture_ffmpeg_real_time(ffmpeg_cmd, "[yellow][FFMPEG] [cyan]Join video")
135
+ capture_ffmpeg_real_time(ffmpeg_cmd, "[yellow]FFMPEG [cyan]Join video")
158
136
  print()
159
137
 
160
138
  return out_path
@@ -169,6 +147,8 @@ def join_audios(video_path: str, audio_tracks: List[Dict[str, str]], out_path: s
169
147
  - audio_tracks (list[dict[str, str]]): A list of dictionaries containing information about audio tracks.
170
148
  Each dictionary should contain the 'path' and 'name' keys.
171
149
  - out_path (str): The path to save the output file.
150
+ - codec (M3U8_Codec): The video codec to use (non utilizzato con nuova configurazione).
151
+ - limit_duration_diff (float): Maximum duration difference in seconds.
172
152
  """
173
153
  use_shortest = False
174
154
  duration_diffs = []
@@ -210,61 +190,27 @@ def join_audios(video_path: str, audio_tracks: List[Dict[str, str]], out_path: s
210
190
  for i, audio_track in enumerate(audio_tracks):
211
191
  ffmpeg_cmd.extend(['-i', audio_track.get('path')])
212
192
 
213
-
214
193
  # Map the video and audio streams
215
- ffmpeg_cmd.append('-map')
216
- ffmpeg_cmd.append('0:v') # Map video stream from the first input (video_path)
194
+ ffmpeg_cmd.extend(['-map', '0:v'])
217
195
 
218
196
  for i in range(1, len(audio_tracks) + 1):
219
- ffmpeg_cmd.append('-map')
220
- ffmpeg_cmd.append(f'{i}:a') # Map audio streams from subsequent inputs
221
-
222
- # Add output Parameters
223
- if USE_CODEC:
224
- if USE_VCODEC:
225
- if codec.video_codec_name:
226
- if not USE_GPU:
227
- ffmpeg_cmd.extend(['-c:v', codec.video_codec_name])
228
- else:
229
- ffmpeg_cmd.extend(['-c:v', 'h264_nvenc'])
230
- else:
231
- console.log("[red]Cant find vcodec for 'join_audios'")
232
- else:
233
- if USE_GPU:
234
- ffmpeg_cmd.extend(['-c:v', 'h264_nvenc'])
235
-
236
- if USE_ACODEC:
237
- if codec.audio_codec_name:
238
- ffmpeg_cmd.extend(['-c:a', codec.audio_codec_name])
239
- else:
240
- console.log("[red]Cant find acodec for 'join_audios'")
241
-
242
- if USE_BITRATE:
243
- ffmpeg_cmd.extend(['-b:v', f'{codec.video_bitrate // 1000}k'])
244
- ffmpeg_cmd.extend(['-b:a', f'{codec.audio_bitrate // 1000}k'])
197
+ ffmpeg_cmd.extend(['-map', f'{i}:a'])
245
198
 
246
- else:
247
- ffmpeg_cmd.extend(['-c', 'copy'])
248
-
249
- # Ultrafast preset always or fast for gpu
250
- if not USE_GPU:
251
- ffmpeg_cmd.extend(['-preset', FFMPEG_DEFAULT_PRESET])
252
- else:
253
- ffmpeg_cmd.extend(['-preset', 'fast'])
199
+ # Add encoding parameters (prima di -shortest e output)
200
+ add_encoding_params(ffmpeg_cmd)
254
201
 
255
202
  # Use shortest input path if any audio track has significant difference
256
203
  if use_shortest:
257
204
  ffmpeg_cmd.extend(['-shortest', '-strict', 'experimental'])
258
205
 
259
- # Overwrite
260
- ffmpeg_cmd += [out_path, "-y"]
206
+ # Output file and overwrite
207
+ ffmpeg_cmd.extend([out_path, '-y'])
261
208
 
262
209
  # Run join
263
210
  if DEBUG_MODE:
264
211
  subprocess.run(ffmpeg_cmd, check=True)
265
-
266
212
  else:
267
- capture_ffmpeg_real_time(ffmpeg_cmd, "[yellow][FFMPEG] [cyan]Join audio")
213
+ capture_ffmpeg_real_time(ffmpeg_cmd, "[yellow]FFMPEG [cyan]Join audio")
268
214
  print()
269
215
 
270
216
  return out_path, use_shortest
@@ -294,11 +240,8 @@ def join_subtitle(video_path: str, subtitles_list: List[Dict[str, str]], out_pat
294
240
  ffmpeg_cmd += ["-map", f"{idx + 1}:s"]
295
241
  ffmpeg_cmd += ["-metadata:s:s:{}".format(idx), "title={}".format(subtitle['language'])]
296
242
 
297
- # Add output Parameters
298
- if USE_CODEC:
299
- ffmpeg_cmd.extend(['-c:v', 'copy', '-c:a', 'copy', '-c:s', select_subtitle_encoder()])
300
- else:
301
- ffmpeg_cmd.extend(['-c', 'copy', '-c:s', select_subtitle_encoder()])
243
+ # For subtitles, we always use copy for video/audio and only encoder for subtitles
244
+ ffmpeg_cmd.extend(['-c:v', 'copy', '-c:a', 'copy', '-c:s', select_subtitle_encoder()])
302
245
 
303
246
  # Overwrite
304
247
  ffmpeg_cmd += [out_path, "-y"]
@@ -307,9 +250,8 @@ def join_subtitle(video_path: str, subtitles_list: List[Dict[str, str]], out_pat
307
250
  # Run join
308
251
  if DEBUG_MODE:
309
252
  subprocess.run(ffmpeg_cmd, check=True)
310
-
311
253
  else:
312
- capture_ffmpeg_real_time(ffmpeg_cmd, "[yellow][FFMPEG] [cyan]Join subtitle")
254
+ capture_ffmpeg_real_time(ffmpeg_cmd, "[yellow]FFMPEG [cyan]Join subtitle")
313
255
  print()
314
256
 
315
257
  return out_path
@@ -42,7 +42,6 @@ class M3U8_Ts_Estimator:
42
42
  def add_ts_file(self, size: int):
43
43
  """Add a file size to the list of file sizes."""
44
44
  if size <= 0:
45
- logging.error(f"Invalid input values: size={size}")
46
45
  return
47
46
 
48
47
  with self.lock:
@@ -4,13 +4,12 @@ import sys
4
4
 
5
5
 
6
6
  # External libraries
7
- import httpx
8
7
  from rich.console import Console
9
8
 
10
9
 
11
10
  # Internal utilities
12
11
  from .obj_tmbd import Json_film
13
- from StreamingCommunity.Util.config_json import config_manager
12
+ from StreamingCommunity.Util.http_client import create_client
14
13
  from StreamingCommunity.Util.table import TVShowManager
15
14
 
16
15
 
@@ -18,7 +17,6 @@ from StreamingCommunity.Util.table import TVShowManager
18
17
  console = Console()
19
18
  table_show_manager = TVShowManager()
20
19
  api_key = "a800ed6c93274fb857ea61bd9e7256c5"
21
- MAX_TIMEOUT = config_manager.get_int("REQUESTS", "timeout")
22
20
 
23
21
 
24
22
  def get_select_title(table_show_manager, generic_obj):
@@ -113,7 +111,7 @@ class TheMovieDB:
113
111
 
114
112
  params['api_key'] = self.api_key
115
113
  url = f"{self.base_url}/{endpoint}"
116
- response = httpx.get(url, params=params, timeout=MAX_TIMEOUT)
114
+ response = create_client().get(url, params=params)
117
115
  response.raise_for_status()
118
116
 
119
117
  return response.json()
@@ -29,7 +29,6 @@
29
29
  "specific_list_audio": [
30
30
  "ita"
31
31
  ],
32
- "download_subtitle": true,
33
32
  "merge_subs": true,
34
33
  "specific_list_subtitles": [
35
34
  "ita",
@@ -1,5 +1,5 @@
1
1
  __title__ = 'StreamingCommunity'
2
- __version__ = '3.3.8'
2
+ __version__ = '3.4.0'
3
3
  __author__ = 'Arrowar'
4
4
  __description__ = 'A command-line program to download film'
5
5
  __copyright__ = 'Copyright 2025'
@@ -318,7 +318,7 @@ class ConfigManager:
318
318
  logging.error(f"Download of {filename} failed: {e}")
319
319
  raise
320
320
 
321
- def get(self, section: str, key: str, data_type: type = str, from_site: bool = False) -> Any:
321
+ def get(self, section: str, key: str, data_type: type = str, from_site: bool = False, default: Any = None) -> Any:
322
322
  """
323
323
  Read a value from the configuration.
324
324
 
@@ -327,9 +327,10 @@ class ConfigManager:
327
327
  key (str): Key to read
328
328
  data_type (type, optional): Expected data type. Default: str
329
329
  from_site (bool, optional): Whether to read from the site configuration. Default: False
330
+ default (Any, optional): Default value if key is not found. Default: None
330
331
 
331
332
  Returns:
332
- Any: The key value converted to the specified data type
333
+ Any: The key value converted to the specified data type, or default if not found
333
334
  """
334
335
  cache_key = f"{'site' if from_site else 'config'}.{section}.{key}"
335
336
  logging.info(f"Reading key: {cache_key}")
@@ -343,9 +344,15 @@ class ConfigManager:
343
344
 
344
345
  # Check if the section and key exist
345
346
  if section not in config_source:
347
+ if default is not None:
348
+ logging.info(f"Section '{section}' not found. Returning default value.")
349
+ return default
346
350
  raise ValueError(f"Section '{section}' not found in {'site' if from_site else 'main'} configuration")
347
351
 
348
352
  if key not in config_source[section]:
353
+ if default is not None:
354
+ logging.info(f"Key '{key}' not found in section '{section}'. Returning default value.")
355
+ return default
349
356
  raise ValueError(f"Key '{key}' not found in section '{section}' of {'site' if from_site else 'main'} configuration")
350
357
 
351
358
  # Get and convert the value
@@ -356,7 +363,7 @@ class ConfigManager:
356
363
  self.cache[cache_key] = converted_value
357
364
 
358
365
  return converted_value
359
-
366
+
360
367
  def _convert_to_data_type(self, value: Any, data_type: type) -> Any:
361
368
  """
362
369
  Convert the value to the specified data type.
@@ -399,30 +406,30 @@ class ConfigManager:
399
406
  raise ValueError(f"Cannot convert '{value}' to {data_type.__name__}: {str(e)}")
400
407
 
401
408
  # Getters for main configuration
402
- def get_string(self, section: str, key: str) -> str:
409
+ def get_string(self, section: str, key: str, default: str = None) -> str:
403
410
  """Read a string from the main configuration."""
404
- return self.get(section, key, str)
405
-
406
- def get_int(self, section: str, key: str) -> int:
411
+ return self.get(section, key, str, default=default)
412
+
413
+ def get_int(self, section: str, key: str, default: int = None) -> int:
407
414
  """Read an integer from the main configuration."""
408
- return self.get(section, key, int)
409
-
410
- def get_float(self, section: str, key: str) -> float:
415
+ return self.get(section, key, int, default=default)
416
+
417
+ def get_float(self, section: str, key: str, default: float = None) -> float:
411
418
  """Read a float from the main configuration."""
412
- return self.get(section, key, float)
413
-
414
- def get_bool(self, section: str, key: str) -> bool:
419
+ return self.get(section, key, float, default=default)
420
+
421
+ def get_bool(self, section: str, key: str, default: bool = None) -> bool:
415
422
  """Read a boolean from the main configuration."""
416
- return self.get(section, key, bool)
417
-
418
- def get_list(self, section: str, key: str) -> List[str]:
423
+ return self.get(section, key, bool, default=default)
424
+
425
+ def get_list(self, section: str, key: str, default: List[str] = None) -> List[str]:
419
426
  """Read a list from the main configuration."""
420
- return self.get(section, key, list)
421
-
422
- def get_dict(self, section: str, key: str) -> dict:
427
+ return self.get(section, key, list, default=default)
428
+
429
+ def get_dict(self, section: str, key: str, default: dict = None) -> dict:
423
430
  """Read a dictionary from the main configuration."""
424
- return self.get(section, key, dict)
425
-
431
+ return self.get(section, key, dict, default=default)
432
+
426
433
  # Getters for site configuration
427
434
  def get_site(self, section: str, key: str) -> Any:
428
435
  """Read a value from the site configuration."""
@@ -8,6 +8,7 @@ from typing import Any, Dict, Optional, Union
8
8
 
9
9
  # External library
10
10
  import httpx
11
+ from curl_cffi import requests
11
12
 
12
13
 
13
14
  # Logic class
@@ -104,6 +105,33 @@ def create_async_client(
104
105
  )
105
106
 
106
107
 
108
+ def create_client_curl(
109
+ *,
110
+ headers: Optional[Dict[str, str]] = None,
111
+ cookies: Optional[Dict[str, str]] = None,
112
+ timeout: Optional[Union[int, float]] = None,
113
+ verify: Optional[bool] = None,
114
+ proxies: Optional[Dict[str, str]] = None,
115
+ impersonate: str = "chrome136",
116
+ allow_redirects: bool = True,
117
+ ):
118
+ """Factory for a configured curl_cffi session."""
119
+ session = requests.Session()
120
+ session.headers.update(_default_headers(headers))
121
+ if cookies:
122
+ session.cookies.update(cookies)
123
+ session.timeout = timeout if timeout is not None else _get_timeout()
124
+ session.verify = _get_verify() if verify is None else verify
125
+ if proxies is not None:
126
+ session.proxies = proxies
127
+ elif _get_proxies():
128
+ session.proxies = _get_proxies()
129
+ session.impersonate = impersonate
130
+ session.allow_redirects = allow_redirects
131
+
132
+ return session
133
+
134
+
107
135
  def _sleep_with_backoff(attempt: int, base: float = 1.1, cap: float = 10.0) -> None:
108
136
  """Exponential backoff with jitter."""
109
137
  delay = min(base * (2 ** attempt), cap)
@@ -71,27 +71,37 @@ class OsManager:
71
71
 
72
72
  return normalized
73
73
 
74
- def get_sanitize_file(self, filename: str) -> str:
75
- """Sanitize filename."""
74
+ def get_sanitize_file(self, filename: str, year: str = None) -> str:
75
+ """Sanitize filename. Optionally append a year in format ' (YYYY)' if year is provided and valid."""
76
76
  if not filename:
77
77
  return filename
78
78
 
79
- # Decode and sanitize
79
+ # Extract and validate year if provided
80
+ year_str = ""
81
+ if year:
82
+ y = str(year).split('-')[0].strip()
83
+ if y.isdigit() and len(y) == 4:
84
+ year_str = f" ({y})"
85
+
86
+ # Decode and sanitize base filename
80
87
  decoded = unidecode(filename)
81
88
  sanitized = sanitize_filename(decoded)
82
89
 
83
90
  # Split name and extension
84
91
  name, ext = os.path.splitext(sanitized)
85
92
 
93
+ # Append year if present
94
+ name_with_year = name + year_str
95
+
86
96
  # Calculate available length for name considering the '...' and extension
87
97
  max_name_length = self.max_length - len('...') - len(ext)
88
98
 
89
99
  # Truncate name if it exceeds the max name length
90
- if len(name) > max_name_length:
91
- name = name[:max_name_length] + '...'
100
+ if len(name_with_year) > max_name_length:
101
+ name_with_year = name_with_year[:max_name_length] + '...'
92
102
 
93
103
  # Ensure the final file name includes the extension
94
- return name + ext
104
+ return name_with_year + ext
95
105
 
96
106
  def get_sanitize_path(self, path: str) -> str:
97
107
  """Sanitize complete path."""
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: StreamingCommunity
3
- Version: 3.3.8
3
+ Version: 3.4.0
4
4
  Home-page: https://github.com/Arrowar/StreamingCommunity
5
5
  Author: Arrowar
6
6
  Project-URL: Bug Reports, https://github.com/Arrowar/StreamingCommunity/issues
@@ -182,7 +182,6 @@ mpd_url = "https://example.com/stream.mpd"
182
182
  license_url = "https://example.com/get_license"
183
183
 
184
184
  dash_process = DASH_Downloader(
185
- cdm_device=get_wvd_path(),
186
185
  license_url=license_url,
187
186
  mpd_url=mpd_url,
188
187
  output_path="output.mp4",
@@ -267,7 +266,6 @@ To enable qBittorrent integration, follow the setup guide [here](https://github.
267
266
  "specific_list_audio": [
268
267
  "ita"
269
268
  ],
270
- "download_subtitle": true,
271
269
  "merge_subs": true,
272
270
  "specific_list_subtitles": [
273
271
  "ita", // Specify language codes or use ["*"] to download all available subtitles