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