StreamingCommunity 3.2.7__py3-none-any.whl → 3.2.9__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/Helper/Vixcloud/util.py +2 -1
- StreamingCommunity/Api/Player/hdplayer.py +2 -2
- StreamingCommunity/Api/Player/sweetpixel.py +5 -8
- StreamingCommunity/Api/Site/altadefinizione/__init__.py +2 -2
- StreamingCommunity/Api/Site/altadefinizione/film.py +10 -8
- StreamingCommunity/Api/Site/altadefinizione/series.py +9 -7
- StreamingCommunity/Api/Site/altadefinizione/site.py +1 -1
- StreamingCommunity/Api/Site/animeunity/__init__.py +2 -2
- StreamingCommunity/Api/Site/animeunity/serie.py +2 -2
- StreamingCommunity/Api/Site/animeworld/site.py +3 -5
- StreamingCommunity/Api/Site/animeworld/util/ScrapeSerie.py +8 -10
- StreamingCommunity/Api/Site/cb01new/film.py +7 -5
- StreamingCommunity/Api/Site/crunchyroll/__init__.py +1 -3
- StreamingCommunity/Api/Site/crunchyroll/film.py +9 -7
- StreamingCommunity/Api/Site/crunchyroll/series.py +9 -7
- StreamingCommunity/Api/Site/crunchyroll/site.py +10 -1
- StreamingCommunity/Api/Site/guardaserie/series.py +8 -6
- StreamingCommunity/Api/Site/guardaserie/site.py +0 -3
- StreamingCommunity/Api/Site/guardaserie/util/ScrapeSerie.py +1 -2
- StreamingCommunity/Api/Site/mediasetinfinity/__init__.py +1 -1
- StreamingCommunity/Api/Site/mediasetinfinity/film.py +10 -16
- StreamingCommunity/Api/Site/mediasetinfinity/series.py +12 -18
- StreamingCommunity/Api/Site/mediasetinfinity/site.py +11 -3
- StreamingCommunity/Api/Site/mediasetinfinity/util/ScrapeSerie.py +214 -180
- StreamingCommunity/Api/Site/mediasetinfinity/util/get_license.py +2 -31
- StreamingCommunity/Api/Site/raiplay/__init__.py +1 -1
- StreamingCommunity/Api/Site/raiplay/film.py +41 -10
- StreamingCommunity/Api/Site/raiplay/series.py +44 -12
- StreamingCommunity/Api/Site/raiplay/site.py +4 -1
- StreamingCommunity/Api/Site/raiplay/util/ScrapeSerie.py +2 -1
- StreamingCommunity/Api/Site/raiplay/util/get_license.py +40 -0
- StreamingCommunity/Api/Site/streamingcommunity/__init__.py +0 -1
- StreamingCommunity/Api/Site/streamingcommunity/film.py +7 -5
- StreamingCommunity/Api/Site/streamingcommunity/series.py +9 -7
- StreamingCommunity/Api/Site/streamingcommunity/site.py +4 -2
- StreamingCommunity/Api/Site/streamingwatch/film.py +7 -5
- StreamingCommunity/Api/Site/streamingwatch/series.py +8 -6
- StreamingCommunity/Api/Site/streamingwatch/site.py +3 -1
- StreamingCommunity/Api/Site/streamingwatch/util/ScrapeSerie.py +3 -3
- StreamingCommunity/Api/Template/Util/__init__.py +10 -1
- StreamingCommunity/Api/Template/Util/manage_ep.py +4 -4
- StreamingCommunity/Api/Template/__init__.py +5 -1
- StreamingCommunity/Api/Template/site.py +10 -6
- StreamingCommunity/Lib/Downloader/DASH/cdm_helpher.py +5 -12
- StreamingCommunity/Lib/Downloader/DASH/decrypt.py +1 -1
- StreamingCommunity/Lib/Downloader/DASH/downloader.py +1 -1
- StreamingCommunity/Lib/Downloader/DASH/parser.py +1 -1
- StreamingCommunity/Lib/Downloader/DASH/segments.py +4 -3
- StreamingCommunity/Lib/Downloader/HLS/downloader.py +11 -9
- StreamingCommunity/Lib/Downloader/HLS/segments.py +253 -144
- StreamingCommunity/Lib/Downloader/MP4/downloader.py +4 -3
- StreamingCommunity/Lib/Downloader/TOR/downloader.py +3 -5
- StreamingCommunity/Lib/Downloader/__init__.py +9 -1
- StreamingCommunity/Lib/FFmpeg/__init__.py +10 -1
- StreamingCommunity/Lib/FFmpeg/command.py +4 -6
- StreamingCommunity/Lib/FFmpeg/util.py +1 -1
- StreamingCommunity/Lib/M3U8/__init__.py +9 -1
- StreamingCommunity/Lib/M3U8/decryptor.py +8 -4
- StreamingCommunity/Lib/M3U8/estimator.py +0 -6
- StreamingCommunity/Lib/M3U8/parser.py +1 -1
- StreamingCommunity/Lib/M3U8/url_fixer.py +1 -1
- StreamingCommunity/Lib/TMBD/__init__.py +6 -1
- StreamingCommunity/TelegramHelp/config.json +1 -5
- StreamingCommunity/TelegramHelp/telegram_bot.py +9 -10
- StreamingCommunity/Upload/version.py +2 -2
- StreamingCommunity/Util/config_json.py +139 -59
- StreamingCommunity/Util/http_client.py +201 -0
- StreamingCommunity/Util/message.py +1 -1
- StreamingCommunity/Util/os.py +5 -5
- StreamingCommunity/Util/table.py +3 -3
- StreamingCommunity/__init__.py +9 -1
- StreamingCommunity/run.py +396 -260
- {streamingcommunity-3.2.7.dist-info → streamingcommunity-3.2.9.dist-info}/METADATA +143 -45
- streamingcommunity-3.2.9.dist-info/RECORD +113 -0
- streamingcommunity-3.2.7.dist-info/RECORD +0 -111
- {streamingcommunity-3.2.7.dist-info → streamingcommunity-3.2.9.dist-info}/WHEEL +0 -0
- {streamingcommunity-3.2.7.dist-info → streamingcommunity-3.2.9.dist-info}/entry_points.txt +0 -0
- {streamingcommunity-3.2.7.dist-info → streamingcommunity-3.2.9.dist-info}/licenses/LICENSE +0 -0
- {streamingcommunity-3.2.7.dist-info → streamingcommunity-3.2.9.dist-info}/top_level.txt +0 -0
|
@@ -2,10 +2,15 @@
|
|
|
2
2
|
|
|
3
3
|
import os
|
|
4
4
|
import sys
|
|
5
|
-
import
|
|
5
|
+
import time
|
|
6
|
+
import queue
|
|
7
|
+
import signal
|
|
6
8
|
import logging
|
|
7
9
|
import binascii
|
|
10
|
+
import threading
|
|
11
|
+
from queue import PriorityQueue
|
|
8
12
|
from urllib.parse import urljoin, urlparse
|
|
13
|
+
from concurrent.futures import ThreadPoolExecutor, as_completed
|
|
9
14
|
from typing import Dict
|
|
10
15
|
|
|
11
16
|
|
|
@@ -18,6 +23,7 @@ from rich.console import Console
|
|
|
18
23
|
# Internal utilities
|
|
19
24
|
from StreamingCommunity.Util.color import Colors
|
|
20
25
|
from StreamingCommunity.Util.headers import get_userAgent
|
|
26
|
+
from StreamingCommunity.Util.http_client import create_client
|
|
21
27
|
from StreamingCommunity.Util.config_json import config_manager
|
|
22
28
|
|
|
23
29
|
|
|
@@ -37,7 +43,8 @@ DEFAULT_VIDEO_WORKERS = config_manager.get_int('M3U8_DOWNLOAD', 'default_video_w
|
|
|
37
43
|
DEFAULT_AUDIO_WORKERS = config_manager.get_int('M3U8_DOWNLOAD', 'default_audio_workers')
|
|
38
44
|
MAX_TIMEOOUT = config_manager.get_int("REQUESTS", "timeout")
|
|
39
45
|
SEGMENT_MAX_TIMEOUT = config_manager.get_int("M3U8_DOWNLOAD", "segment_timeout")
|
|
40
|
-
|
|
46
|
+
TELEGRAM_BOT = config_manager.get_bool('DEFAULT', 'telegram_bot')
|
|
47
|
+
MAX_INTERRUPT_COUNT = 3
|
|
41
48
|
|
|
42
49
|
# Variable
|
|
43
50
|
console = Console()
|
|
@@ -56,18 +63,38 @@ class M3U8_Segments:
|
|
|
56
63
|
self.url = url
|
|
57
64
|
self.tmp_folder = tmp_folder
|
|
58
65
|
self.is_index_url = is_index_url
|
|
66
|
+
self.expected_real_time = None
|
|
59
67
|
self.tmp_file_path = os.path.join(self.tmp_folder, "0.ts")
|
|
60
68
|
os.makedirs(self.tmp_folder, exist_ok=True)
|
|
61
69
|
|
|
62
70
|
# Util class
|
|
63
71
|
self.decryption: M3U8_Decryption = None
|
|
72
|
+
self.class_ts_estimator = M3U8_Ts_Estimator(0, self)
|
|
64
73
|
self.class_url_fixer = M3U8_UrlFix(url)
|
|
65
|
-
|
|
66
|
-
#
|
|
74
|
+
|
|
75
|
+
# Sync
|
|
76
|
+
self.queue = PriorityQueue()
|
|
77
|
+
self.buffer = {}
|
|
78
|
+
self.expected_index = 0
|
|
79
|
+
|
|
80
|
+
self.stop_event = threading.Event()
|
|
67
81
|
self.downloaded_segments = set()
|
|
82
|
+
self.base_timeout = 0.5
|
|
83
|
+
self.current_timeout = 3.0
|
|
84
|
+
|
|
85
|
+
# Stopping
|
|
86
|
+
self.interrupt_flag = threading.Event()
|
|
68
87
|
self.download_interrupted = False
|
|
69
|
-
self.
|
|
88
|
+
self.interrupt_count = 0
|
|
89
|
+
self.force_stop = False
|
|
90
|
+
self.interrupt_lock = threading.Lock()
|
|
91
|
+
|
|
92
|
+
# OTHER INFO
|
|
93
|
+
self.info_maxRetry = 0
|
|
70
94
|
self.info_nRetry = 0
|
|
95
|
+
self.info_nFailed = 0
|
|
96
|
+
self.active_retries = 0
|
|
97
|
+
self.active_retries_lock = threading.Lock()
|
|
71
98
|
|
|
72
99
|
def __get_key__(self, m3u8_parser: M3U8_Parser) -> bytes:
|
|
73
100
|
"""
|
|
@@ -119,10 +146,16 @@ class M3U8_Segments:
|
|
|
119
146
|
if "http" not in seg else seg
|
|
120
147
|
for seg in m3u8_parser.segments
|
|
121
148
|
]
|
|
149
|
+
self.class_ts_estimator.total_segments = len(self.segments)
|
|
122
150
|
|
|
123
151
|
def get_info(self) -> None:
|
|
124
152
|
"""
|
|
125
153
|
Retrieves M3U8 playlist information from the given URL.
|
|
154
|
+
|
|
155
|
+
If the URL is an index URL, this method:
|
|
156
|
+
- Sends an HTTP GET request to fetch the M3U8 playlist.
|
|
157
|
+
- Parses the M3U8 content using `parse_data`.
|
|
158
|
+
- Saves the playlist to a temporary folder.
|
|
126
159
|
"""
|
|
127
160
|
if self.is_index_url:
|
|
128
161
|
try:
|
|
@@ -137,156 +170,235 @@ class M3U8_Segments:
|
|
|
137
170
|
except Exception as e:
|
|
138
171
|
raise RuntimeError(f"M3U8 info retrieval failed: {e}")
|
|
139
172
|
|
|
140
|
-
def
|
|
173
|
+
def setup_interrupt_handler(self):
|
|
141
174
|
"""
|
|
142
|
-
|
|
175
|
+
Set up a signal handler for graceful interruption.
|
|
143
176
|
"""
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
177
|
+
def interrupt_handler(signum, frame):
|
|
178
|
+
with self.interrupt_lock:
|
|
179
|
+
self.interrupt_count += 1
|
|
180
|
+
if self.interrupt_count >= MAX_INTERRUPT_COUNT:
|
|
181
|
+
self.force_stop = True
|
|
182
|
+
|
|
183
|
+
if self.force_stop:
|
|
184
|
+
console.print("\n[red]Force stop triggered! Exiting immediately.")
|
|
151
185
|
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
186
|
+
else:
|
|
187
|
+
if not self.interrupt_flag.is_set():
|
|
188
|
+
remaining = MAX_INTERRUPT_COUNT - self.interrupt_count
|
|
189
|
+
console.print(f"\n[red]- Stopping gracefully... (Ctrl+C {remaining}x to force)")
|
|
190
|
+
self.download_interrupted = True
|
|
191
|
+
|
|
192
|
+
if remaining == 1:
|
|
193
|
+
self.interrupt_flag.set()
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
if threading.current_thread() is threading.main_thread():
|
|
197
|
+
signal.signal(signal.SIGINT, interrupt_handler)
|
|
198
|
+
else:
|
|
199
|
+
print("Signal handler must be set in the main thread")
|
|
200
|
+
|
|
201
|
+
def _get_http_client(self):
|
|
202
|
+
return create_client(headers={'User-Agent': get_userAgent()}, follow_redirects=True)
|
|
203
|
+
|
|
204
|
+
def download_segment(self, ts_url: str, index: int, progress_bar: tqdm, backoff_factor: float = 1.1) -> None:
|
|
155
205
|
"""
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
progress_bar = tqdm(
|
|
159
|
-
total=len(self.segments),
|
|
160
|
-
unit='s',
|
|
161
|
-
ascii='░▒█',
|
|
162
|
-
bar_format=self._get_bar_format(description),
|
|
163
|
-
mininterval=0.6,
|
|
164
|
-
maxinterval=1.0,
|
|
165
|
-
file=sys.stdout
|
|
166
|
-
)
|
|
206
|
+
Downloads a TS segment and adds it to the segment queue with retry logic.
|
|
167
207
|
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
208
|
+
Parameters:
|
|
209
|
+
- ts_url (str): The URL of the TS segment.
|
|
210
|
+
- index (int): The index of the segment.
|
|
211
|
+
- progress_bar (tqdm): Progress counter for tracking download progress.
|
|
212
|
+
- backoff_factor (float): The backoff factor for exponential backoff (default is 1.5 seconds).
|
|
213
|
+
"""
|
|
214
|
+
for attempt in range(REQUEST_MAX_RETRY):
|
|
215
|
+
if self.interrupt_flag.is_set():
|
|
216
|
+
return
|
|
217
|
+
|
|
218
|
+
try:
|
|
219
|
+
with self._get_http_client() as client:
|
|
220
|
+
response = client.get(ts_url)
|
|
173
221
|
|
|
174
|
-
|
|
175
|
-
|
|
222
|
+
# Validate response and content
|
|
223
|
+
response.raise_for_status()
|
|
224
|
+
segment_content = response.content
|
|
225
|
+
content_size = len(segment_content)
|
|
226
|
+
|
|
227
|
+
# Decrypt if needed and verify decrypted content
|
|
228
|
+
if self.decryption is not None:
|
|
229
|
+
try:
|
|
230
|
+
segment_content = self.decryption.decrypt(segment_content)
|
|
231
|
+
|
|
232
|
+
except Exception as e:
|
|
233
|
+
logging.error(f"Decryption failed for segment {index}: {str(e)}")
|
|
234
|
+
self.interrupt_flag.set() # Interrupt the download process
|
|
235
|
+
self.stop_event.set() # Trigger the stopping event for all threads
|
|
236
|
+
break # Stop the current task immediately
|
|
237
|
+
|
|
238
|
+
self.class_ts_estimator.update_progress_bar(content_size, progress_bar)
|
|
239
|
+
self.queue.put((index, segment_content))
|
|
240
|
+
self.downloaded_segments.add(index)
|
|
241
|
+
progress_bar.update(1)
|
|
242
|
+
return
|
|
176
243
|
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
244
|
+
except Exception as e:
|
|
245
|
+
logging.info(f"Attempt {attempt + 1} failed for segment {index} - '{ts_url}': {e}")
|
|
246
|
+
|
|
247
|
+
if attempt > self.info_maxRetry:
|
|
248
|
+
self.info_maxRetry = ( attempt + 1 )
|
|
249
|
+
self.info_nRetry += 1
|
|
250
|
+
|
|
251
|
+
if attempt + 1 == REQUEST_MAX_RETRY:
|
|
252
|
+
console.log(f"[red]Final retry failed for segment: {index}")
|
|
253
|
+
self.queue.put((index, None)) # Marker for failed segment
|
|
254
|
+
progress_bar.update(1)
|
|
255
|
+
self.info_nFailed += 1
|
|
256
|
+
return
|
|
257
|
+
|
|
258
|
+
with self.active_retries_lock:
|
|
259
|
+
self.active_retries += 1
|
|
260
|
+
|
|
261
|
+
#sleep_time = backoff_factor * (2 ** attempt)
|
|
262
|
+
sleep_time = backoff_factor * (attempt + 1)
|
|
263
|
+
logging.info(f"Retrying segment {index} in {sleep_time} seconds...")
|
|
264
|
+
time.sleep(sleep_time)
|
|
265
|
+
|
|
266
|
+
with self.active_retries_lock:
|
|
267
|
+
self.active_retries -= 1
|
|
182
268
|
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
269
|
+
def write_segments_to_file(self):
|
|
270
|
+
"""
|
|
271
|
+
Writes segments to file with additional verification.
|
|
272
|
+
"""
|
|
273
|
+
with open(self.tmp_file_path, 'wb') as f:
|
|
274
|
+
while not self.stop_event.is_set() or not self.queue.empty():
|
|
275
|
+
if self.interrupt_flag.is_set():
|
|
276
|
+
break
|
|
277
|
+
|
|
278
|
+
try:
|
|
279
|
+
index, segment_content = self.queue.get(timeout=self.current_timeout)
|
|
188
280
|
|
|
189
|
-
|
|
190
|
-
|
|
281
|
+
# Successful queue retrieval: reduce timeout
|
|
282
|
+
self.current_timeout = max(self.base_timeout, self.current_timeout / 2)
|
|
191
283
|
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
284
|
+
# Handle failed segments
|
|
285
|
+
if segment_content is None:
|
|
286
|
+
if index == self.expected_index:
|
|
287
|
+
self.expected_index += 1
|
|
288
|
+
continue
|
|
195
289
|
|
|
196
|
-
|
|
197
|
-
|
|
290
|
+
# Write segment if it's the next expected one
|
|
291
|
+
if index == self.expected_index:
|
|
292
|
+
f.write(segment_content)
|
|
293
|
+
f.flush()
|
|
294
|
+
self.expected_index += 1
|
|
198
295
|
|
|
199
|
-
|
|
200
|
-
|
|
296
|
+
# Write any buffered segments that are now in order
|
|
297
|
+
while self.expected_index in self.buffer:
|
|
298
|
+
next_segment = self.buffer.pop(self.expected_index)
|
|
201
299
|
|
|
202
|
-
|
|
300
|
+
if next_segment is not None:
|
|
301
|
+
f.write(next_segment)
|
|
302
|
+
f.flush()
|
|
203
303
|
|
|
204
|
-
|
|
205
|
-
"""
|
|
206
|
-
Download a batch of segments with retry logic.
|
|
207
|
-
"""
|
|
208
|
-
async def download_single(url, idx):
|
|
209
|
-
async with semaphore:
|
|
210
|
-
for attempt in range(max_retry):
|
|
211
|
-
try:
|
|
212
|
-
resp = await client.get(url, headers={'User-Agent': get_userAgent()})
|
|
213
|
-
|
|
214
|
-
if resp.status_code == 200:
|
|
215
|
-
content = resp.content
|
|
216
|
-
|
|
217
|
-
if self.decryption:
|
|
218
|
-
content = self.decryption.decrypt(content)
|
|
219
|
-
return idx, content, attempt
|
|
220
|
-
|
|
221
|
-
await asyncio.sleep(1.1 * (2 ** attempt))
|
|
222
|
-
logging.info(f"Segment {idx} failed with status {resp.status_code}. Retrying...")
|
|
304
|
+
self.expected_index += 1
|
|
223
305
|
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
logging.info(f"Segment {idx} download failed: {sys.exc_info()[1]}. Retrying...")
|
|
306
|
+
else:
|
|
307
|
+
self.buffer[index] = segment_content
|
|
227
308
|
|
|
228
|
-
|
|
309
|
+
except queue.Empty:
|
|
310
|
+
self.current_timeout = min(MAX_TIMEOOUT, self.current_timeout * 1.1)
|
|
311
|
+
time.sleep(0.05)
|
|
229
312
|
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
for coro in asyncio.as_completed(tasks):
|
|
233
|
-
try:
|
|
234
|
-
idx, data, nretry = await coro
|
|
235
|
-
results[idx] = data
|
|
313
|
+
if self.stop_event.is_set():
|
|
314
|
+
break
|
|
236
315
|
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
316
|
+
except Exception as e:
|
|
317
|
+
logging.error(f"Error writing segment {index}: {str(e)}")
|
|
318
|
+
|
|
319
|
+
def download_streams(self, description: str, type: str):
|
|
320
|
+
"""
|
|
321
|
+
Downloads all TS segments in parallel and writes them to a file.
|
|
241
322
|
|
|
242
|
-
|
|
243
|
-
|
|
323
|
+
Parameters:
|
|
324
|
+
- description: Description to insert on tqdm bar
|
|
325
|
+
- type (str): Type of download: 'video' or 'audio'
|
|
326
|
+
"""
|
|
327
|
+
if TELEGRAM_BOT:
|
|
244
328
|
|
|
245
|
-
|
|
246
|
-
|
|
329
|
+
# Viene usato per lo screen
|
|
330
|
+
console.log("####")
|
|
331
|
+
|
|
332
|
+
self.get_info()
|
|
333
|
+
self.setup_interrupt_handler()
|
|
247
334
|
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
335
|
+
progress_bar = tqdm(
|
|
336
|
+
total=len(self.segments),
|
|
337
|
+
unit='s',
|
|
338
|
+
ascii='░▒█',
|
|
339
|
+
bar_format=self._get_bar_format(description),
|
|
340
|
+
mininterval=0.6,
|
|
341
|
+
maxinterval=1.0,
|
|
342
|
+
file=sys.stdout, # Using file=sys.stdout to force in-place updates because sys.stderr may not support carriage returns in this environment.
|
|
343
|
+
)
|
|
251
344
|
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
max_global_retries = 5
|
|
257
|
-
global_retry_count = 0
|
|
345
|
+
try:
|
|
346
|
+
writer_thread = threading.Thread(target=self.write_segments_to_file)
|
|
347
|
+
writer_thread.daemon = True
|
|
348
|
+
writer_thread.start()
|
|
258
349
|
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
not self.download_interrupted):
|
|
262
|
-
|
|
263
|
-
failed_indices = [i for i, data in enumerate(results) if not data]
|
|
264
|
-
if not failed_indices:
|
|
265
|
-
break
|
|
266
|
-
|
|
267
|
-
logging.info(f"[yellow]Retrying {len(failed_indices)} failed segments...")
|
|
268
|
-
|
|
269
|
-
retry_tasks = [
|
|
270
|
-
self._download_segments_batch(
|
|
271
|
-
client, [segment_urls[i]], [results[i]],
|
|
272
|
-
semaphore, max_retry, estimator, progress_bar
|
|
273
|
-
)
|
|
274
|
-
for i in failed_indices
|
|
275
|
-
]
|
|
350
|
+
# Configure workers and delay
|
|
351
|
+
max_workers = self._get_worker_count(type)
|
|
276
352
|
|
|
277
|
-
|
|
278
|
-
|
|
353
|
+
# Download segments with completion verification
|
|
354
|
+
with ThreadPoolExecutor(max_workers=max_workers) as executor:
|
|
355
|
+
futures = []
|
|
356
|
+
for index, segment_url in enumerate(self.segments):
|
|
357
|
+
|
|
358
|
+
# Check for interrupt before submitting each task
|
|
359
|
+
if self.interrupt_flag.is_set():
|
|
360
|
+
break
|
|
361
|
+
|
|
362
|
+
time.sleep(TQDM_DELAY_WORKER)
|
|
363
|
+
futures.append(executor.submit(self.download_segment, segment_url, index, progress_bar))
|
|
364
|
+
|
|
365
|
+
# Wait for futures with interrupt handling
|
|
366
|
+
for future in as_completed(futures):
|
|
367
|
+
if self.interrupt_flag.is_set():
|
|
368
|
+
break
|
|
369
|
+
try:
|
|
370
|
+
future.result()
|
|
371
|
+
except Exception as e:
|
|
372
|
+
logging.error(f"Error in download thread: {str(e)}")
|
|
373
|
+
|
|
374
|
+
# Interrupt handling for missing segments
|
|
375
|
+
if not self.interrupt_flag.is_set():
|
|
376
|
+
total_segments = len(self.segments)
|
|
377
|
+
completed_segments = len(self.downloaded_segments)
|
|
378
|
+
|
|
379
|
+
if completed_segments < total_segments:
|
|
380
|
+
missing_segments = set(range(total_segments)) - self.downloaded_segments
|
|
381
|
+
logging.warning(f"Missing segments: {sorted(missing_segments)}")
|
|
382
|
+
|
|
383
|
+
# Retry missing segments with interrupt check
|
|
384
|
+
for index in missing_segments:
|
|
385
|
+
if self.interrupt_flag.is_set():
|
|
386
|
+
break
|
|
279
387
|
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
for data in results:
|
|
286
|
-
if data:
|
|
287
|
-
f.write(data)
|
|
288
|
-
f.flush()
|
|
388
|
+
try:
|
|
389
|
+
self.download_segment(self.segments[index], index, progress_bar)
|
|
390
|
+
|
|
391
|
+
except Exception as e:
|
|
392
|
+
logging.error(f"Failed to retry segment {index}: {str(e)}")
|
|
289
393
|
|
|
394
|
+
finally:
|
|
395
|
+
self._cleanup_resources(writer_thread, progress_bar)
|
|
396
|
+
|
|
397
|
+
if not self.interrupt_flag.is_set():
|
|
398
|
+
self._verify_download_completion()
|
|
399
|
+
|
|
400
|
+
return self._generate_results(type)
|
|
401
|
+
|
|
290
402
|
def _get_bar_format(self, description: str) -> str:
|
|
291
403
|
"""
|
|
292
404
|
Generate platform-appropriate progress bar format.
|
|
@@ -310,9 +422,7 @@ class M3U8_Segments:
|
|
|
310
422
|
return base_workers
|
|
311
423
|
|
|
312
424
|
def _generate_results(self, stream_type: str) -> Dict:
|
|
313
|
-
"""
|
|
314
|
-
Package final download results.
|
|
315
|
-
"""
|
|
425
|
+
"""Package final download results."""
|
|
316
426
|
return {
|
|
317
427
|
'type': stream_type,
|
|
318
428
|
'nFailed': self.info_nFailed,
|
|
@@ -320,31 +430,30 @@ class M3U8_Segments:
|
|
|
320
430
|
}
|
|
321
431
|
|
|
322
432
|
def _verify_download_completion(self) -> None:
|
|
323
|
-
"""
|
|
324
|
-
Validate final download integrity.
|
|
325
|
-
"""
|
|
433
|
+
"""Validate final download integrity."""
|
|
326
434
|
total = len(self.segments)
|
|
327
435
|
if len(self.downloaded_segments) / total < 0.999:
|
|
328
436
|
missing = sorted(set(range(total)) - self.downloaded_segments)
|
|
329
437
|
raise RuntimeError(f"Download incomplete ({len(self.downloaded_segments)/total:.1%}). Missing segments: {missing}")
|
|
330
438
|
|
|
331
|
-
def _cleanup_resources(self, progress_bar: tqdm) -> None:
|
|
332
|
-
"""
|
|
333
|
-
|
|
334
|
-
|
|
439
|
+
def _cleanup_resources(self, writer_thread: threading.Thread, progress_bar: tqdm) -> None:
|
|
440
|
+
"""Ensure resource cleanup and final reporting."""
|
|
441
|
+
self.stop_event.set()
|
|
442
|
+
writer_thread.join(timeout=30)
|
|
335
443
|
progress_bar.close()
|
|
336
|
-
|
|
444
|
+
|
|
337
445
|
if self.info_nFailed > 0:
|
|
338
446
|
self._display_error_summary()
|
|
339
447
|
|
|
448
|
+
self.buffer = {}
|
|
449
|
+
self.expected_index = 0
|
|
450
|
+
|
|
340
451
|
def _display_error_summary(self) -> None:
|
|
341
|
-
"""
|
|
342
|
-
Generate final error report.
|
|
343
|
-
"""
|
|
452
|
+
"""Generate final error report."""
|
|
344
453
|
console.print(f"\n[cyan]Retry Summary: "
|
|
345
454
|
f"[white]Max retries: [green]{self.info_maxRetry} "
|
|
346
455
|
f"[white]Total retries: [green]{self.info_nRetry} "
|
|
347
456
|
f"[white]Failed segments: [red]{self.info_nFailed}")
|
|
348
457
|
|
|
349
458
|
if self.info_nRetry > len(self.segments) * 0.3:
|
|
350
|
-
console.print("[yellow]Warning: High retry count detected. Consider reducing worker count in config.")
|
|
459
|
+
console.print("[yellow]Warning: High retry count detected. Consider reducing worker count in config.")
|
|
@@ -10,7 +10,6 @@ from functools import partial
|
|
|
10
10
|
|
|
11
11
|
|
|
12
12
|
# External libraries
|
|
13
|
-
import httpx
|
|
14
13
|
from tqdm import tqdm
|
|
15
14
|
from rich.console import Console
|
|
16
15
|
from rich.prompt import Prompt
|
|
@@ -19,6 +18,7 @@ from rich.panel import Panel
|
|
|
19
18
|
|
|
20
19
|
# Internal utilities
|
|
21
20
|
from StreamingCommunity.Util.headers import get_userAgent
|
|
21
|
+
from StreamingCommunity.Util.http_client import create_client
|
|
22
22
|
from StreamingCommunity.Util.color import Colors
|
|
23
23
|
from StreamingCommunity.Util.config_json import config_manager
|
|
24
24
|
from StreamingCommunity.Util.os import internet_manager, os_manager
|
|
@@ -83,7 +83,7 @@ def MP4_downloader(url: str, path: str, referer: str = None, headers_: dict = No
|
|
|
83
83
|
if os.path.exists(path):
|
|
84
84
|
console.log("[red]Output file already exists.")
|
|
85
85
|
if TELEGRAM_BOT:
|
|
86
|
-
bot.send_message(
|
|
86
|
+
bot.send_message("Contenuto già scaricato!", None)
|
|
87
87
|
return None, False
|
|
88
88
|
|
|
89
89
|
if not (url.lower().startswith('http://') or url.lower().startswith('https://')):
|
|
@@ -110,7 +110,8 @@ def MP4_downloader(url: str, path: str, referer: str = None, headers_: dict = No
|
|
|
110
110
|
os.makedirs(os.path.dirname(path), exist_ok=True)
|
|
111
111
|
|
|
112
112
|
try:
|
|
113
|
-
|
|
113
|
+
# Use unified HTTP client (verify/timeout/proxy from config)
|
|
114
|
+
with create_client() as client:
|
|
114
115
|
with client.stream("GET", url, headers=headers) as response:
|
|
115
116
|
response.raise_for_status()
|
|
116
117
|
total = int(response.headers.get('content-length', 0))
|
|
@@ -229,7 +229,7 @@ class TOR_downloader:
|
|
|
229
229
|
torrent_info.num_seeds == 0 and
|
|
230
230
|
torrent_info.state in ('stalledDL', 'missingFiles', 'error')):
|
|
231
231
|
|
|
232
|
-
self.console.print(
|
|
232
|
+
self.console.print("[bold red]Torrent not downloadable. No seeds or peers available. Removing...[/bold red]")
|
|
233
233
|
self._remove_torrent(self.latest_torrent_hash)
|
|
234
234
|
self.latest_torrent_hash = None
|
|
235
235
|
return False
|
|
@@ -250,7 +250,7 @@ class TOR_downloader:
|
|
|
250
250
|
"""
|
|
251
251
|
try:
|
|
252
252
|
self.qb.torrents_delete(delete_files=delete_files, torrent_hashes=torrent_hash)
|
|
253
|
-
self.console.print(
|
|
253
|
+
self.console.print("[yellow]Torrent removed from client[/yellow]")
|
|
254
254
|
except Exception as e:
|
|
255
255
|
logging.error(f"Error removing torrent: {str(e)}")
|
|
256
256
|
|
|
@@ -356,10 +356,8 @@ class TOR_downloader:
|
|
|
356
356
|
|
|
357
357
|
# Get download statistics
|
|
358
358
|
download_speed = torrent_info.dlspeed
|
|
359
|
-
upload_speed = torrent_info.upspeed
|
|
360
359
|
total_size = torrent_info.size
|
|
361
360
|
downloaded_size = torrent_info.downloaded
|
|
362
|
-
eta = torrent_info.eta # eta in seconds
|
|
363
361
|
|
|
364
362
|
# Format sizes and speeds using the existing functions without modification
|
|
365
363
|
downloaded_size_str = internet_manager.format_file_size(downloaded_size)
|
|
@@ -465,5 +463,5 @@ class TOR_downloader:
|
|
|
465
463
|
|
|
466
464
|
try:
|
|
467
465
|
self.qb.auth_log_out()
|
|
468
|
-
except:
|
|
466
|
+
except Exception:
|
|
469
467
|
pass
|
|
@@ -2,4 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
from .HLS.downloader import HLS_Downloader
|
|
4
4
|
from .MP4.downloader import MP4_downloader
|
|
5
|
-
from .TOR.downloader import TOR_downloader
|
|
5
|
+
from .TOR.downloader import TOR_downloader
|
|
6
|
+
from .DASH.downloader import DASH_Downloader
|
|
7
|
+
|
|
8
|
+
__all__ = [
|
|
9
|
+
"HLS_Downloader",
|
|
10
|
+
"MP4_downloader",
|
|
11
|
+
"TOR_downloader",
|
|
12
|
+
"DASH_Downloader"
|
|
13
|
+
]
|
|
@@ -1,4 +1,13 @@
|
|
|
1
1
|
# 18.04.24
|
|
2
2
|
|
|
3
3
|
from .command import join_video, join_audios, join_subtitle
|
|
4
|
-
from .util import print_duration_table, get_video_duration
|
|
4
|
+
from .util import print_duration_table, get_video_duration
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
__all__ = [
|
|
8
|
+
"join_video",
|
|
9
|
+
"join_audios",
|
|
10
|
+
"join_subtitle",
|
|
11
|
+
"print_duration_table",
|
|
12
|
+
"get_video_duration",
|
|
13
|
+
]
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
# 31.01.24
|
|
2
2
|
|
|
3
|
-
import sys
|
|
4
3
|
import logging
|
|
5
4
|
import subprocess
|
|
6
5
|
from typing import List, Dict, Tuple, Optional
|
|
@@ -110,13 +109,12 @@ def join_video(video_path: str, out_path: str, codec: M3U8_Codec = None):
|
|
|
110
109
|
if need_to_force_to_ts(video_path):
|
|
111
110
|
#console.log("[red]Force input file to 'mpegts'.")
|
|
112
111
|
ffmpeg_cmd.extend(['-f', 'mpegts'])
|
|
113
|
-
vcodec = "libx264"
|
|
114
112
|
|
|
115
113
|
# Insert input video path
|
|
116
114
|
ffmpeg_cmd.extend(['-i', video_path])
|
|
117
115
|
|
|
118
116
|
# Add output Parameters
|
|
119
|
-
if USE_CODEC and codec
|
|
117
|
+
if USE_CODEC and codec is not None:
|
|
120
118
|
if USE_VCODEC:
|
|
121
119
|
if codec.video_codec_name:
|
|
122
120
|
if not USE_GPU:
|
|
@@ -162,7 +160,7 @@ def join_video(video_path: str, out_path: str, codec: M3U8_Codec = None):
|
|
|
162
160
|
print()
|
|
163
161
|
|
|
164
162
|
else:
|
|
165
|
-
console.log(
|
|
163
|
+
console.log("[purple]FFmpeg [white][[cyan]Join video[white]] ...")
|
|
166
164
|
with suppress_output():
|
|
167
165
|
capture_ffmpeg_real_time(ffmpeg_cmd, "[cyan]Join video")
|
|
168
166
|
print()
|
|
@@ -258,7 +256,7 @@ def join_audios(video_path: str, audio_tracks: List[Dict[str, str]], out_path: s
|
|
|
258
256
|
print()
|
|
259
257
|
|
|
260
258
|
else:
|
|
261
|
-
console.log(
|
|
259
|
+
console.log("[purple]FFmpeg [white][[cyan]Join audio[white]] ...")
|
|
262
260
|
with suppress_output():
|
|
263
261
|
capture_ffmpeg_real_time(ffmpeg_cmd, "[cyan]Join audio")
|
|
264
262
|
print()
|
|
@@ -313,7 +311,7 @@ def join_subtitle(video_path: str, subtitles_list: List[Dict[str, str]], out_pat
|
|
|
313
311
|
print()
|
|
314
312
|
|
|
315
313
|
else:
|
|
316
|
-
console.log(
|
|
314
|
+
console.log("[purple]FFmpeg [white][[cyan]Join subtitle[white]] ...")
|
|
317
315
|
with suppress_output():
|
|
318
316
|
capture_ffmpeg_real_time(ffmpeg_cmd, "[cyan]Join subtitle")
|
|
319
317
|
print()
|