StreamingCommunity 3.3.9__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 (60) 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 +4 -5
  15. StreamingCommunity/Api/Site/crunchyroll/series.py +2 -3
  16. StreamingCommunity/Api/Site/crunchyroll/site.py +2 -9
  17. StreamingCommunity/Api/Site/crunchyroll/util/ScrapeSerie.py +5 -27
  18. StreamingCommunity/Api/Site/crunchyroll/util/get_license.py +11 -26
  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 +7 -25
  27. StreamingCommunity/Api/Site/raiplay/film.py +6 -7
  28. StreamingCommunity/Api/Site/raiplay/series.py +0 -2
  29. StreamingCommunity/Api/Site/raiplay/site.py +3 -11
  30. StreamingCommunity/Api/Site/raiplay/util/ScrapeSerie.py +4 -11
  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/decrypt.py +54 -1
  42. StreamingCommunity/Lib/Downloader/DASH/downloader.py +131 -54
  43. StreamingCommunity/Lib/Downloader/DASH/parser.py +2 -3
  44. StreamingCommunity/Lib/Downloader/DASH/segments.py +66 -54
  45. StreamingCommunity/Lib/Downloader/HLS/downloader.py +31 -50
  46. StreamingCommunity/Lib/Downloader/HLS/segments.py +23 -28
  47. StreamingCommunity/Lib/FFmpeg/capture.py +37 -5
  48. StreamingCommunity/Lib/FFmpeg/command.py +32 -90
  49. StreamingCommunity/Lib/TMBD/tmdb.py +2 -4
  50. StreamingCommunity/TelegramHelp/config.json +0 -1
  51. StreamingCommunity/Upload/version.py +1 -1
  52. StreamingCommunity/Util/config_json.py +28 -21
  53. StreamingCommunity/Util/http_client.py +28 -0
  54. StreamingCommunity/Util/os.py +16 -6
  55. {streamingcommunity-3.3.9.dist-info → streamingcommunity-3.4.0.dist-info}/METADATA +1 -3
  56. {streamingcommunity-3.3.9.dist-info → streamingcommunity-3.4.0.dist-info}/RECORD +60 -60
  57. {streamingcommunity-3.3.9.dist-info → streamingcommunity-3.4.0.dist-info}/WHEEL +0 -0
  58. {streamingcommunity-3.3.9.dist-info → streamingcommunity-3.4.0.dist-info}/entry_points.txt +0 -0
  59. {streamingcommunity-3.3.9.dist-info → streamingcommunity-3.4.0.dist-info}/licenses/LICENSE +0 -0
  60. {streamingcommunity-3.3.9.dist-info → streamingcommunity-3.4.0.dist-info}/top_level.txt +0 -0
@@ -1,14 +1,12 @@
1
1
  # 17.10.24
2
2
 
3
3
  import os
4
- import time
5
4
  import logging
6
5
  import shutil
7
- from typing import Any, Dict, List, Optional
6
+ from typing import Any, Dict, List, Optional, Union
8
7
 
9
8
 
10
9
  # External libraries
11
- import httpx
12
10
  from rich.console import Console
13
11
  from rich.panel import Panel
14
12
  from rich.table import Table
@@ -17,7 +15,7 @@ from rich.table import Table
17
15
  # Internal utilities
18
16
  from StreamingCommunity.Util.config_json import config_manager
19
17
  from StreamingCommunity.Util.headers import get_userAgent
20
- from StreamingCommunity.Util.http_client import create_client
18
+ from StreamingCommunity.Util.http_client import fetch
21
19
  from StreamingCommunity.Util.os import os_manager, internet_manager
22
20
 
23
21
 
@@ -33,15 +31,13 @@ from .segments import M3U8_Segments
33
31
 
34
32
 
35
33
  # Config
36
- ENABLE_SUBTITLE = config_manager.get_bool('M3U8_DOWNLOAD', 'download_subtitle')
37
34
  DOWNLOAD_SPECIFIC_AUDIO = config_manager.get_list('M3U8_DOWNLOAD', 'specific_list_audio')
38
35
  DOWNLOAD_SPECIFIC_SUBTITLE = config_manager.get_list('M3U8_DOWNLOAD', 'specific_list_subtitles')
39
36
  MERGE_SUBTITLE = config_manager.get_bool('M3U8_DOWNLOAD', 'merge_subs')
40
37
  CLEANUP_TMP = config_manager.get_bool('M3U8_DOWNLOAD', 'cleanup_tmp_folder')
41
38
  GET_ONLY_LINK = config_manager.get_int('M3U8_DOWNLOAD', 'get_only_link')
42
39
  FILTER_CUSTOM_RESOLUTION = str(config_manager.get('M3U8_CONVERSION', 'force_resolution')).strip().lower()
43
- RETRY_LIMIT = config_manager.get_int('REQUESTS', 'max_retry')
44
- MAX_TIMEOUT = config_manager.get_int("REQUESTS", "timeout")
40
+ EXTENSION_OUTPUT = config_manager.get("M3U8_CONVERSION", "extension")
45
41
 
46
42
  console = Console()
47
43
 
@@ -51,9 +47,9 @@ class HLSClient:
51
47
  def __init__(self, custom_headers: Optional[Dict[str, str]] = None):
52
48
  self.headers = custom_headers if custom_headers else {'User-Agent': get_userAgent()}
53
49
 
54
- def request(self, url: str, return_content: bool = False) -> Optional[httpx.Response]:
50
+ def request(self, url: str, return_content: bool = False) -> Optional[Union[str, bytes]]:
55
51
  """
56
- Makes HTTP GET requests with retry logic.
52
+ Makes HTTP GET requests with retry logic using http_client.
57
53
 
58
54
  Args:
59
55
  url: Target URL to request
@@ -67,21 +63,12 @@ class HLSClient:
67
63
  logging.error("URL is None or empty, cannot make request")
68
64
  return None
69
65
 
70
- client = create_client(headers=self.headers)
71
-
72
- for attempt in range(RETRY_LIMIT):
73
- try:
74
- response = client.get(url)
75
- response.raise_for_status()
76
- return response.content if return_content else response.text
77
-
78
- except Exception as e:
79
- logging.error(f"Attempt {attempt+1} failed for URL {url}: {str(e)}")
80
- if attempt < RETRY_LIMIT - 1: # Don't sleep on last attempt
81
- time.sleep(1.5 ** attempt)
82
-
83
- logging.error(f"All {RETRY_LIMIT} attempts failed for URL: {url}")
84
- return None
66
+ return fetch(
67
+ url,
68
+ method="GET",
69
+ headers=self.headers,
70
+ return_content=return_content
71
+ )
85
72
 
86
73
 
87
74
  class PathManager:
@@ -94,7 +81,7 @@ class PathManager:
94
81
  """
95
82
  self.m3u8_url = m3u8_url
96
83
  self.output_path = self._sanitize_output_path(output_path)
97
- base_name = os.path.basename(self.output_path).replace(".mp4", "")
84
+ base_name = os.path.basename(self.output_path).replace(EXTENSION_OUTPUT, "")
98
85
  self.temp_dir = os.path.join(os.path.dirname(self.output_path), f"{base_name}_tmp")
99
86
 
100
87
  def _sanitize_output_path(self, path: Optional[str]) -> str:
@@ -103,10 +90,10 @@ class PathManager:
103
90
  Creates a hash-based filename if no path is provided.
104
91
  """
105
92
  if not path:
106
- path = "download.mp4"
93
+ path = f"download{EXTENSION_OUTPUT}"
107
94
 
108
- if not path.endswith(".mp4"):
109
- path += ".mp4"
95
+ if not path.endswith(EXTENSION_OUTPUT):
96
+ path += EXTENSION_OUTPUT
110
97
 
111
98
  return os_manager.get_sanitize_path(path)
112
99
 
@@ -182,6 +169,11 @@ class M3U8Manager:
182
169
  elif str(FILTER_CUSTOM_RESOLUTION).replace("p", "").replace("px", "").isdigit():
183
170
  resolution_value = int(str(FILTER_CUSTOM_RESOLUTION).replace("p", "").replace("px", ""))
184
171
  self.video_url, self.video_res = self.parser._video.get_custom_uri(resolution_value)
172
+
173
+ # Fallback to best if custom resolution not found
174
+ if self.video_url is None:
175
+ self.video_url, self.video_res = self.parser._video.get_best_uri()
176
+
185
177
  else:
186
178
  logging.error("Resolution not recognized.")
187
179
  self.video_url, self.video_res = self.parser._video.get_best_uri()
@@ -192,15 +184,15 @@ class M3U8Manager:
192
184
  if s.get('language') in DOWNLOAD_SPECIFIC_AUDIO
193
185
  ]
194
186
 
187
+ # Subtitle info
195
188
  self.sub_streams = []
196
- if ENABLE_SUBTITLE:
197
- if "*" in DOWNLOAD_SPECIFIC_SUBTITLE:
198
- self.sub_streams = self.parser._subtitle.get_all_uris_and_names() or []
199
- else:
200
- self.sub_streams = [
201
- s for s in (self.parser._subtitle.get_all_uris_and_names() or [])
202
- if s.get('language') in DOWNLOAD_SPECIFIC_SUBTITLE
203
- ]
189
+ if "*" in DOWNLOAD_SPECIFIC_SUBTITLE:
190
+ self.sub_streams = self.parser._subtitle.get_all_uris_and_names() or []
191
+ else:
192
+ self.sub_streams = [
193
+ s for s in (self.parser._subtitle.get_all_uris_and_names() or [])
194
+ if s.get('language') in DOWNLOAD_SPECIFIC_SUBTITLE
195
+ ]
204
196
 
205
197
  def log_selection(self):
206
198
  """Log the stream selection information in a formatted table."""
@@ -220,17 +212,6 @@ class M3U8Manager:
220
212
 
221
213
  data_rows.append(["Video", available_video, str(FILTER_CUSTOM_RESOLUTION), downloadable_video])
222
214
 
223
- # Codec information
224
- if self.parser.codec is not None:
225
- available_codec_info = (
226
- f"v: {self.parser.codec.video_codec_name} "
227
- f"(b: {self.parser.codec.video_bitrate // 1000}k), "
228
- f"a: {self.parser.codec.audio_codec_name} "
229
- f"(b: {self.parser.codec.audio_bitrate // 1000}k)"
230
- )
231
- set_codec_info = available_codec_info if config_manager.get_bool("M3U8_CONVERSION", "use_codec") else "copy"
232
-
233
- data_rows.append(["Codec", available_codec_info, set_codec_info, set_codec_info])
234
215
 
235
216
  # Subtitle information
236
217
  available_subtitles = self.parser._subtitle.get_all_uris_and_names() or []
@@ -683,11 +664,11 @@ class HLS_Downloader:
683
664
 
684
665
  new_filename = self.path_manager.output_path
685
666
  if missing_ts and use_shortest:
686
- new_filename = new_filename.replace(".mp4", "_failed_sync_ts.mp4")
667
+ new_filename = new_filename.replace(EXTENSION_OUTPUT, f"_failed_sync_ts{EXTENSION_OUTPUT}")
687
668
  elif missing_ts:
688
- new_filename = new_filename.replace(".mp4", "_failed_ts.mp4")
669
+ new_filename = new_filename.replace(EXTENSION_OUTPUT, f"_failed_ts{EXTENSION_OUTPUT}")
689
670
  elif use_shortest:
690
- new_filename = new_filename.replace(".mp4", "_failed_sync.mp4")
671
+ new_filename = new_filename.replace(EXTENSION_OUTPUT, f"_failed_sync{EXTENSION_OUTPUT}")
691
672
 
692
673
  if missing_ts or use_shortest:
693
674
  os.rename(self.path_manager.output_path, new_filename)
@@ -18,6 +18,7 @@ from rich.console import Console
18
18
  # Internal utilities
19
19
  from StreamingCommunity.Util.color import Colors
20
20
  from StreamingCommunity.Util.headers import get_userAgent
21
+ from StreamingCommunity.Util.http_client import create_client_curl
21
22
  from StreamingCommunity.Util.config_json import config_manager
22
23
 
23
24
 
@@ -38,6 +39,7 @@ DEFAULT_AUDIO_WORKERS = config_manager.get_int('M3U8_DOWNLOAD', 'default_audio_w
38
39
  MAX_TIMEOUT = config_manager.get_int("REQUESTS", "timeout")
39
40
  SEGMENT_MAX_TIMEOUT = config_manager.get_int("M3U8_DOWNLOAD", "segment_timeout")
40
41
  LIMIT_SEGMENT = config_manager.get_int('M3U8_DOWNLOAD', 'limit_segment')
42
+ ENABLE_RETRY = config_manager.get_bool('M3U8_DOWNLOAD', 'enable_retry')
41
43
 
42
44
 
43
45
  # Variable
@@ -68,6 +70,8 @@ class M3U8_Segments:
68
70
  self.limit_segments = LIMIT_SEGMENT if LIMIT_SEGMENT > 0 else None
69
71
  else:
70
72
  self.limit_segments = limit_segments
73
+
74
+ self.enable_retry = ENABLE_RETRY
71
75
 
72
76
  # Util class
73
77
  self.decryption: M3U8_Decryption = None
@@ -94,12 +98,7 @@ class M3U8_Segments:
94
98
  self.key_base_url = f"{parsed_url.scheme}://{parsed_url.netloc}/"
95
99
 
96
100
  try:
97
- client_params = {
98
- 'headers': self.custom_headers,
99
- 'timeout': MAX_TIMEOUT,
100
- 'verify': REQUEST_VERIFY
101
- }
102
- response = httpx.get(url=key_uri, **client_params)
101
+ response = create_client_curl(headers=self.custom_headers).get(key_uri)
103
102
  response.raise_for_status()
104
103
 
105
104
  hex_content = binascii.hexlify(response.content).decode('utf-8')
@@ -146,12 +145,7 @@ class M3U8_Segments:
146
145
  """
147
146
  if self.is_index_url:
148
147
  try:
149
- client_params = {
150
- 'headers': self.custom_headers,
151
- 'timeout': MAX_TIMEOUT,
152
- 'verify': REQUEST_VERIFY
153
- }
154
- response = httpx.get(self.url, **client_params, follow_redirects=True)
148
+ response = create_client_curl(headers=self.custom_headers).get(self.url)
155
149
  response.raise_for_status()
156
150
 
157
151
  self.parse_data(response.text)
@@ -174,7 +168,7 @@ class M3U8_Segments:
174
168
  """
175
169
  Get the file path for a temporary segment.
176
170
  """
177
- return os.path.join(temp_dir, f"seg_{index:06d}.tmp")
171
+ return os.path.join(temp_dir, f"seg_{index:06d}.ts")
178
172
 
179
173
  async def _download_init_segment(self, client: httpx.AsyncClient, output_path: str, progress_bar: tqdm) -> bool:
180
174
  """
@@ -220,7 +214,7 @@ class M3U8_Segments:
220
214
  async def _download_single_segment(self, client: httpx.AsyncClient, ts_url: str, index: int, temp_dir: str,
221
215
  semaphore: asyncio.Semaphore, max_retry: int) -> tuple:
222
216
  """
223
- Downloads a single TS segment and saves to temp file.
217
+ Downloads a single TS segment and saves to temp file IMMEDIATELY.
224
218
 
225
219
  Returns:
226
220
  tuple: (index, success, retry_count, file_size)
@@ -248,11 +242,13 @@ class M3U8_Segments:
248
242
  return index, False, attempt, 0
249
243
  raise e
250
244
 
251
- # Write to temp file
245
+ # Write segment to temp file IMMEDIATELY
252
246
  with open(temp_file, 'wb') as f:
253
247
  f.write(segment_content)
254
-
255
- return index, True, attempt, len(segment_content)
248
+
249
+ size = len(segment_content)
250
+ del segment_content
251
+ return index, True, attempt, size
256
252
 
257
253
  except Exception:
258
254
  if attempt + 1 == max_retry:
@@ -296,8 +292,8 @@ class M3U8_Segments:
296
292
  console.print("\n[red]Download interrupted by user (Ctrl+C).")
297
293
  break
298
294
 
299
- # Retry failed segments
300
- if not self.download_interrupted:
295
+ # Retry failed segments only if enabled
296
+ if self.enable_retry and not self.download_interrupted:
301
297
  await self._retry_failed_segments(client, temp_dir, semaphore, progress_bar)
302
298
 
303
299
  async def _retry_failed_segments(self, client: httpx.AsyncClient, temp_dir: str, semaphore: asyncio.Semaphore,
@@ -356,6 +352,7 @@ class M3U8_Segments:
356
352
  if os.path.exists(temp_file):
357
353
  with open(temp_file, 'rb') as infile:
358
354
  outfile.write(infile.read())
355
+ os.remove(temp_file)
359
356
 
360
357
  async def download_segments_async(self, description: str, type: str):
361
358
  """
@@ -473,13 +470,12 @@ class M3U8_Segments:
473
470
  """Ensure resource cleanup and final reporting."""
474
471
  progress_bar.close()
475
472
 
476
- # Delete temp segment files
473
+ # Delete temp directory if exists
477
474
  if temp_dir and os.path.exists(temp_dir):
478
475
  try:
479
- for idx in range(len(self.segments)):
480
- temp_file = self._get_temp_segment_path(temp_dir, idx)
481
- if os.path.exists(temp_file):
482
- os.remove(temp_file)
476
+ # Remove any remaining files (in case of interruption)
477
+ for file in os.listdir(temp_dir):
478
+ os.remove(os.path.join(temp_dir, file))
483
479
  os.rmdir(temp_dir)
484
480
  except Exception as e:
485
481
  console.print(f"[yellow]Warning: Could not clean temp directory: {e}")
@@ -489,7 +485,6 @@ class M3U8_Segments:
489
485
 
490
486
  def _display_error_summary(self) -> None:
491
487
  """Generate final error report."""
492
- console.print(f"\n[green]Retry Summary: "
493
- f"[cyan]Max retries: [red]{self.info_maxRetry} "
494
- f"[cyan]Total retries: [red]{self.info_nRetry} "
495
- f"[cyan]Failed segments: [red]{self.info_nFailed}")
488
+ console.print(f" [cyan]Max retries: [red]{self.info_maxRetry} [white] | "
489
+ f"[cyan]Total retries: [red]{self.info_nRetry} [white] | "
490
+ f"[cyan]Failed segments: [red]{self.info_nFailed}")
@@ -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,42 +122,11 @@ 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:
@@ -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,59 +190,25 @@ 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
213
  capture_ffmpeg_real_time(ffmpeg_cmd, "[yellow]FFMPEG [cyan]Join audio")
268
214
  print()
@@ -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,7 +250,6 @@ 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
254
  capture_ffmpeg_real_time(ffmpeg_cmd, "[yellow]FFMPEG [cyan]Join subtitle")
313
255
  print()
@@ -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.9'
2
+ __version__ = '3.4.0'
3
3
  __author__ = 'Arrowar'
4
4
  __description__ = 'A command-line program to download film'
5
5
  __copyright__ = 'Copyright 2025'