StreamingCommunity 3.3.9__py3-none-any.whl → 3.4.2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of StreamingCommunity might be problematic. Click here for more details.
- 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 -16
- StreamingCommunity/Api/Site/altadefinizione/series.py +3 -12
- StreamingCommunity/Api/Site/altadefinizione/site.py +2 -9
- 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 +4 -5
- StreamingCommunity/Api/Site/crunchyroll/series.py +5 -17
- StreamingCommunity/Api/Site/crunchyroll/site.py +4 -13
- StreamingCommunity/Api/Site/crunchyroll/util/ScrapeSerie.py +5 -27
- StreamingCommunity/Api/Site/crunchyroll/util/get_license.py +11 -26
- StreamingCommunity/Api/Site/guardaserie/series.py +3 -14
- 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 +4 -15
- StreamingCommunity/Api/Site/mediasetinfinity/site.py +16 -32
- 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 +7 -25
- StreamingCommunity/Api/Site/raiplay/film.py +6 -8
- StreamingCommunity/Api/Site/raiplay/series.py +5 -20
- StreamingCommunity/Api/Site/raiplay/site.py +45 -47
- StreamingCommunity/Api/Site/raiplay/util/ScrapeSerie.py +91 -55
- StreamingCommunity/Api/Site/raiplay/util/get_license.py +3 -12
- StreamingCommunity/Api/Site/streamingcommunity/film.py +5 -16
- StreamingCommunity/Api/Site/streamingcommunity/series.py +5 -10
- StreamingCommunity/Api/Site/streamingcommunity/site.py +3 -22
- StreamingCommunity/Api/Site/streamingcommunity/util/ScrapeSerie.py +11 -27
- StreamingCommunity/Api/Site/streamingwatch/__init__.py +1 -0
- StreamingCommunity/Api/Site/streamingwatch/film.py +4 -2
- StreamingCommunity/Api/Site/streamingwatch/series.py +4 -14
- StreamingCommunity/Api/Site/streamingwatch/site.py +4 -18
- StreamingCommunity/Api/Site/streamingwatch/util/ScrapeSerie.py +0 -3
- StreamingCommunity/Api/Template/Util/__init__.py +4 -2
- StreamingCommunity/Api/Template/Util/manage_ep.py +66 -0
- StreamingCommunity/Api/Template/config_loader.py +0 -7
- StreamingCommunity/Lib/Downloader/DASH/decrypt.py +54 -1
- StreamingCommunity/Lib/Downloader/DASH/downloader.py +186 -70
- StreamingCommunity/Lib/Downloader/DASH/parser.py +2 -3
- StreamingCommunity/Lib/Downloader/DASH/segments.py +109 -68
- StreamingCommunity/Lib/Downloader/HLS/downloader.py +100 -82
- StreamingCommunity/Lib/Downloader/HLS/segments.py +40 -28
- StreamingCommunity/Lib/Downloader/MP4/downloader.py +16 -4
- StreamingCommunity/Lib/FFmpeg/capture.py +37 -5
- StreamingCommunity/Lib/FFmpeg/command.py +32 -90
- StreamingCommunity/Lib/M3U8/estimator.py +47 -1
- StreamingCommunity/Lib/TMBD/tmdb.py +2 -4
- StreamingCommunity/TelegramHelp/config.json +0 -1
- StreamingCommunity/Upload/update.py +19 -6
- 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/Util/table.py +50 -8
- {streamingcommunity-3.3.9.dist-info → streamingcommunity-3.4.2.dist-info}/METADATA +1 -3
- streamingcommunity-3.4.2.dist-info/RECORD +111 -0
- streamingcommunity-3.3.9.dist-info/RECORD +0 -111
- {streamingcommunity-3.3.9.dist-info → streamingcommunity-3.4.2.dist-info}/WHEEL +0 -0
- {streamingcommunity-3.3.9.dist-info → streamingcommunity-3.4.2.dist-info}/entry_points.txt +0 -0
- {streamingcommunity-3.3.9.dist-info → streamingcommunity-3.4.2.dist-info}/licenses/LICENSE +0 -0
- {streamingcommunity-3.3.9.dist-info → streamingcommunity-3.4.2.dist-info}/top_level.txt +0 -0
|
@@ -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,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
|
|
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:
|
|
@@ -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.
|
|
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
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
|
-
#
|
|
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,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,6 +4,7 @@ import time
|
|
|
4
4
|
import logging
|
|
5
5
|
import threading
|
|
6
6
|
from collections import deque
|
|
7
|
+
from typing import Dict
|
|
7
8
|
|
|
8
9
|
|
|
9
10
|
# External libraries
|
|
@@ -165,4 +166,49 @@ class M3U8_Ts_Estimator:
|
|
|
165
166
|
"""Stop speed monitoring thread."""
|
|
166
167
|
self._running = False
|
|
167
168
|
if self.speed_thread.is_alive():
|
|
168
|
-
self.speed_thread.join(timeout=5.0)
|
|
169
|
+
self.speed_thread.join(timeout=5.0)
|
|
170
|
+
|
|
171
|
+
def get_speed_data(self) -> Dict[str, str]:
|
|
172
|
+
"""Returns current speed data thread-safe."""
|
|
173
|
+
with self.lock:
|
|
174
|
+
return self.speed.copy()
|
|
175
|
+
|
|
176
|
+
def get_average_segment_size(self) -> int:
|
|
177
|
+
"""Returns average segment size in bytes."""
|
|
178
|
+
with self.lock:
|
|
179
|
+
if not self.ts_file_sizes:
|
|
180
|
+
return 0
|
|
181
|
+
return int(sum(self.ts_file_sizes) / len(self.ts_file_sizes))
|
|
182
|
+
|
|
183
|
+
def get_stats(self, downloaded_count: int = None, total_segments: int = None) -> Dict:
|
|
184
|
+
"""Returns comprehensive statistics for API."""
|
|
185
|
+
with self.lock:
|
|
186
|
+
avg_size = self.get_average_segment_size()
|
|
187
|
+
total_downloaded = sum(self.ts_file_sizes)
|
|
188
|
+
|
|
189
|
+
# Calculate ETA
|
|
190
|
+
eta_seconds = 0
|
|
191
|
+
if downloaded_count is not None and total_segments is not None:
|
|
192
|
+
speed = self.speed.get('download', 'N/A')
|
|
193
|
+
if speed != 'N/A' and ' ' in speed:
|
|
194
|
+
try:
|
|
195
|
+
speed_value, speed_unit = speed.split(' ', 1)
|
|
196
|
+
speed_bps = float(speed_value) * (1024 * 1024 if 'MB/s' in speed_unit else 1024 if 'KB/s' in speed_unit else 1)
|
|
197
|
+
|
|
198
|
+
remaining_segments = total_segments - downloaded_count
|
|
199
|
+
if remaining_segments > 0 and avg_size > 0 and speed_bps > 0:
|
|
200
|
+
eta_seconds = int((avg_size * remaining_segments) / speed_bps)
|
|
201
|
+
|
|
202
|
+
except Exception:
|
|
203
|
+
pass
|
|
204
|
+
|
|
205
|
+
return {
|
|
206
|
+
'total_segments': self.total_segments,
|
|
207
|
+
'downloaded_count': len(self.ts_file_sizes),
|
|
208
|
+
'average_segment_size': avg_size,
|
|
209
|
+
'total_downloaded_bytes': total_downloaded,
|
|
210
|
+
'estimated_total_size': self.calculate_total_size(),
|
|
211
|
+
'upload_speed': self.speed.get('upload', 'N/A'),
|
|
212
|
+
'download_speed': self.speed.get('download', 'N/A'),
|
|
213
|
+
'eta_seconds': eta_seconds
|
|
214
|
+
}
|
|
@@ -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()
|
|
@@ -18,7 +18,6 @@ from StreamingCommunity.Util.config_json import config_manager
|
|
|
18
18
|
from StreamingCommunity.Util.headers import get_userAgent
|
|
19
19
|
|
|
20
20
|
|
|
21
|
-
|
|
22
21
|
# Variable
|
|
23
22
|
if getattr(sys, 'frozen', False): # Modalità PyInstaller
|
|
24
23
|
base_path = os.path.join(sys._MEIPASS, "StreamingCommunity")
|
|
@@ -26,6 +25,7 @@ else:
|
|
|
26
25
|
base_path = os.path.dirname(__file__)
|
|
27
26
|
console = Console()
|
|
28
27
|
|
|
28
|
+
|
|
29
29
|
async def fetch_github_data(client, url):
|
|
30
30
|
"""Helper function to fetch data from GitHub API"""
|
|
31
31
|
response = await client.get(
|
|
@@ -46,10 +46,23 @@ async def async_github_requests():
|
|
|
46
46
|
]
|
|
47
47
|
return await asyncio.gather(*tasks)
|
|
48
48
|
|
|
49
|
+
def get_execution_mode():
|
|
50
|
+
"""Get the execution mode of the application"""
|
|
51
|
+
if getattr(sys, 'frozen', False):
|
|
52
|
+
return "installer"
|
|
53
|
+
|
|
54
|
+
try:
|
|
55
|
+
package_location = importlib.metadata.files(__title__)
|
|
56
|
+
if any("site-packages" in str(path) for path in package_location):
|
|
57
|
+
return "pip"
|
|
58
|
+
|
|
59
|
+
except importlib.metadata.PackageNotFoundError:
|
|
60
|
+
pass
|
|
61
|
+
|
|
62
|
+
return "python"
|
|
63
|
+
|
|
49
64
|
def update():
|
|
50
|
-
"""
|
|
51
|
-
Check for updates on GitHub and display relevant information.
|
|
52
|
-
"""
|
|
65
|
+
"""Check for updates on GitHub and display relevant information."""
|
|
53
66
|
try:
|
|
54
67
|
# Run async requests concurrently
|
|
55
68
|
response_reposity, response_releases, response_commits = asyncio.run(async_github_requests())
|
|
@@ -93,7 +106,7 @@ def update():
|
|
|
93
106
|
console.print(f"\n[cyan]New version available: [yellow]{last_version}")
|
|
94
107
|
|
|
95
108
|
console.print(f"\n[red]{__title__} has been downloaded [yellow]{total_download_count} [red]times, but only [yellow]{percentual_stars}% [red]of users have starred it.\n\
|
|
96
|
-
[green]Current installed version: [yellow]{current_version} [green]last commit: [white]'
|
|
109
|
+
[yellow]{get_execution_mode()} - [green]Current installed version: [yellow]{current_version} [green]last commit: [white]'[yellow]{latest_commit_message.splitlines()[0]}[white]'\n\
|
|
97
110
|
[cyan]Help the repository grow today by leaving a [yellow]star [cyan]and [yellow]sharing [cyan]it with others online!")
|
|
98
111
|
|
|
99
|
-
time.sleep(
|
|
112
|
+
time.sleep(0.8)
|
|
@@ -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."""
|
StreamingCommunity/Util/table.py
CHANGED
|
@@ -12,7 +12,7 @@ from typing import Dict, List, Any
|
|
|
12
12
|
from rich.console import Console
|
|
13
13
|
from rich.table import Table
|
|
14
14
|
from rich.prompt import Prompt
|
|
15
|
-
from rich
|
|
15
|
+
from rich import box
|
|
16
16
|
|
|
17
17
|
|
|
18
18
|
# Internal utilities
|
|
@@ -38,6 +38,9 @@ class TVShowManager:
|
|
|
38
38
|
self.slice_end = 10
|
|
39
39
|
self.step = self.slice_end
|
|
40
40
|
self.column_info = []
|
|
41
|
+
self.table_title = None
|
|
42
|
+
self.table_style = "blue"
|
|
43
|
+
self.show_lines = False
|
|
41
44
|
|
|
42
45
|
def add_column(self, column_info: Dict[str, Dict[str, str]]) -> None:
|
|
43
46
|
"""
|
|
@@ -48,6 +51,26 @@ class TVShowManager:
|
|
|
48
51
|
"""
|
|
49
52
|
self.column_info = column_info
|
|
50
53
|
|
|
54
|
+
def set_table_title(self, title: str) -> None:
|
|
55
|
+
"""
|
|
56
|
+
Set the table title.
|
|
57
|
+
|
|
58
|
+
Parameters:
|
|
59
|
+
- title (str): The title to display above the table.
|
|
60
|
+
"""
|
|
61
|
+
self.table_title = title
|
|
62
|
+
|
|
63
|
+
def set_table_style(self, style: str = "blue", show_lines: bool = False) -> None:
|
|
64
|
+
"""
|
|
65
|
+
Set the table border style and row lines.
|
|
66
|
+
|
|
67
|
+
Parameters:
|
|
68
|
+
- style (str): Border color (e.g., "blue", "green", "magenta", "cyan")
|
|
69
|
+
- show_lines (bool): Whether to show lines between rows
|
|
70
|
+
"""
|
|
71
|
+
self.table_style = style
|
|
72
|
+
self.show_lines = show_lines
|
|
73
|
+
|
|
51
74
|
def add_tv_show(self, tv_show: Dict[str, Any]) -> None:
|
|
52
75
|
"""
|
|
53
76
|
Add a TV show to the list of TV shows.
|
|
@@ -73,19 +96,38 @@ class TVShowManager:
|
|
|
73
96
|
logging.error("Error: Column information not configured.")
|
|
74
97
|
return
|
|
75
98
|
|
|
76
|
-
table
|
|
99
|
+
# Create table with specified style
|
|
100
|
+
table = Table(
|
|
101
|
+
title=self.table_title,
|
|
102
|
+
box=box.ROUNDED,
|
|
103
|
+
show_header=True,
|
|
104
|
+
header_style="bold cyan",
|
|
105
|
+
border_style=self.table_style,
|
|
106
|
+
show_lines=self.show_lines,
|
|
107
|
+
padding=(0, 1)
|
|
108
|
+
)
|
|
77
109
|
|
|
78
110
|
# Add columns dynamically based on provided column information
|
|
79
111
|
for col_name, col_style in self.column_info.items():
|
|
80
|
-
color = col_style.get("color",
|
|
81
|
-
|
|
82
|
-
|
|
112
|
+
color = col_style.get("color", "white")
|
|
113
|
+
width = col_style.get("width", None)
|
|
114
|
+
justify = col_style.get("justify", "center")
|
|
115
|
+
|
|
116
|
+
table.add_column(
|
|
117
|
+
col_name,
|
|
118
|
+
style=color,
|
|
119
|
+
justify=justify,
|
|
120
|
+
width=width
|
|
121
|
+
)
|
|
83
122
|
|
|
84
123
|
# Add rows dynamically based on available TV show data
|
|
85
|
-
for entry in data_slice:
|
|
124
|
+
for idx, entry in enumerate(data_slice):
|
|
86
125
|
if entry:
|
|
87
126
|
row_data = [str(entry.get(col_name, '')) for col_name in self.column_info.keys()]
|
|
88
|
-
|
|
127
|
+
|
|
128
|
+
# Alternate row styling for better readability
|
|
129
|
+
style = "dim" if idx % 2 == 1 else None
|
|
130
|
+
table.add_row(*row_data, style=style)
|
|
89
131
|
|
|
90
132
|
self.console.print(table)
|
|
91
133
|
|
|
@@ -162,7 +204,7 @@ class TVShowManager:
|
|
|
162
204
|
|
|
163
205
|
self.display_data(current_slice)
|
|
164
206
|
|
|
165
|
-
#
|
|
207
|
+
# Get research function from call stack
|
|
166
208
|
research_func = next((
|
|
167
209
|
f for f in get_call_stack()
|
|
168
210
|
if f['function'] == 'search' and f['script'] == '__init__.py'
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: StreamingCommunity
|
|
3
|
-
Version: 3.
|
|
3
|
+
Version: 3.4.2
|
|
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
|