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.
- StreamingCommunity/Api/Player/hdplayer.py +0 -5
- StreamingCommunity/Api/Player/mediapolisvod.py +4 -13
- StreamingCommunity/Api/Player/supervideo.py +3 -8
- StreamingCommunity/Api/Player/sweetpixel.py +1 -9
- StreamingCommunity/Api/Player/vixcloud.py +5 -16
- StreamingCommunity/Api/Site/altadefinizione/film.py +4 -15
- StreamingCommunity/Api/Site/altadefinizione/site.py +2 -7
- StreamingCommunity/Api/Site/altadefinizione/util/ScrapeSerie.py +2 -7
- StreamingCommunity/Api/Site/animeunity/site.py +9 -24
- StreamingCommunity/Api/Site/animeunity/util/ScrapeSerie.py +11 -27
- StreamingCommunity/Api/Site/animeworld/film.py +4 -2
- StreamingCommunity/Api/Site/animeworld/site.py +3 -11
- StreamingCommunity/Api/Site/animeworld/util/ScrapeSerie.py +1 -4
- StreamingCommunity/Api/Site/crunchyroll/film.py +17 -8
- StreamingCommunity/Api/Site/crunchyroll/series.py +8 -9
- StreamingCommunity/Api/Site/crunchyroll/site.py +14 -16
- StreamingCommunity/Api/Site/crunchyroll/util/ScrapeSerie.py +18 -65
- StreamingCommunity/Api/Site/crunchyroll/util/get_license.py +97 -106
- StreamingCommunity/Api/Site/guardaserie/site.py +4 -12
- StreamingCommunity/Api/Site/guardaserie/util/ScrapeSerie.py +3 -10
- StreamingCommunity/Api/Site/mediasetinfinity/film.py +11 -12
- StreamingCommunity/Api/Site/mediasetinfinity/series.py +1 -2
- StreamingCommunity/Api/Site/mediasetinfinity/site.py +3 -11
- StreamingCommunity/Api/Site/mediasetinfinity/util/ScrapeSerie.py +39 -50
- StreamingCommunity/Api/Site/mediasetinfinity/util/fix_mpd.py +3 -3
- StreamingCommunity/Api/Site/mediasetinfinity/util/get_license.py +8 -26
- StreamingCommunity/Api/Site/raiplay/film.py +6 -7
- StreamingCommunity/Api/Site/raiplay/series.py +1 -12
- StreamingCommunity/Api/Site/raiplay/site.py +8 -24
- StreamingCommunity/Api/Site/raiplay/util/ScrapeSerie.py +15 -22
- StreamingCommunity/Api/Site/raiplay/util/get_license.py +3 -12
- StreamingCommunity/Api/Site/streamingcommunity/film.py +5 -16
- StreamingCommunity/Api/Site/streamingcommunity/site.py +3 -22
- StreamingCommunity/Api/Site/streamingcommunity/util/ScrapeSerie.py +11 -26
- StreamingCommunity/Api/Site/streamingwatch/__init__.py +1 -0
- StreamingCommunity/Api/Site/streamingwatch/film.py +4 -2
- StreamingCommunity/Api/Site/streamingwatch/series.py +1 -1
- StreamingCommunity/Api/Site/streamingwatch/site.py +4 -18
- StreamingCommunity/Api/Site/streamingwatch/util/ScrapeSerie.py +0 -3
- StreamingCommunity/Api/Template/config_loader.py +0 -7
- StreamingCommunity/Lib/Downloader/DASH/cdm_helpher.py +8 -3
- StreamingCommunity/Lib/Downloader/DASH/decrypt.py +55 -1
- StreamingCommunity/Lib/Downloader/DASH/downloader.py +139 -55
- StreamingCommunity/Lib/Downloader/DASH/parser.py +458 -101
- StreamingCommunity/Lib/Downloader/DASH/segments.py +131 -74
- StreamingCommunity/Lib/Downloader/HLS/downloader.py +31 -50
- StreamingCommunity/Lib/Downloader/HLS/segments.py +266 -365
- StreamingCommunity/Lib/Downloader/MP4/downloader.py +1 -1
- StreamingCommunity/Lib/FFmpeg/capture.py +37 -5
- StreamingCommunity/Lib/FFmpeg/command.py +35 -93
- StreamingCommunity/Lib/M3U8/estimator.py +0 -1
- StreamingCommunity/Lib/TMBD/tmdb.py +2 -4
- StreamingCommunity/TelegramHelp/config.json +0 -1
- StreamingCommunity/Upload/version.py +1 -1
- StreamingCommunity/Util/config_json.py +28 -21
- StreamingCommunity/Util/http_client.py +28 -0
- StreamingCommunity/Util/os.py +16 -6
- {streamingcommunity-3.3.8.dist-info → streamingcommunity-3.4.0.dist-info}/METADATA +1 -3
- streamingcommunity-3.4.0.dist-info/RECORD +111 -0
- streamingcommunity-3.3.8.dist-info/RECORD +0 -111
- {streamingcommunity-3.3.8.dist-info → streamingcommunity-3.4.0.dist-info}/WHEEL +0 -0
- {streamingcommunity-3.3.8.dist-info → streamingcommunity-3.4.0.dist-info}/entry_points.txt +0 -0
- {streamingcommunity-3.3.8.dist-info → streamingcommunity-3.4.0.dist-info}/licenses/LICENSE +0 -0
- {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}
|
|
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 = (
|
|
61
|
-
|
|
62
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
117
|
-
|
|
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
|
-
#
|
|
151
|
-
ffmpeg_cmd
|
|
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]
|
|
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.
|
|
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.
|
|
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
|
-
|
|
247
|
-
|
|
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
|
-
#
|
|
260
|
-
ffmpeg_cmd
|
|
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]
|
|
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
|
-
#
|
|
298
|
-
|
|
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]
|
|
254
|
+
capture_ffmpeg_real_time(ffmpeg_cmd, "[yellow]FFMPEG [cyan]Join subtitle")
|
|
313
255
|
print()
|
|
314
256
|
|
|
315
257
|
return out_path
|
|
@@ -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.
|
|
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 =
|
|
114
|
+
response = create_client().get(url, params=params)
|
|
117
115
|
response.raise_for_status()
|
|
118
116
|
|
|
119
117
|
return response.json()
|
|
@@ -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)
|
StreamingCommunity/Util/os.py
CHANGED
|
@@ -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
|
-
#
|
|
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(
|
|
91
|
-
|
|
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
|
|
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
|
+
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
|