StreamingCommunity 3.3.7__py3-none-any.whl → 3.3.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/supervideo.py +1 -1
- StreamingCommunity/Api/Site/animeunity/serie.py +1 -1
- StreamingCommunity/Api/Site/animeworld/serie.py +1 -1
- StreamingCommunity/Api/Site/crunchyroll/film.py +13 -3
- StreamingCommunity/Api/Site/crunchyroll/series.py +6 -6
- StreamingCommunity/Api/Site/crunchyroll/site.py +13 -8
- StreamingCommunity/Api/Site/crunchyroll/util/ScrapeSerie.py +16 -41
- StreamingCommunity/Api/Site/crunchyroll/util/get_license.py +107 -101
- StreamingCommunity/Api/Site/mediasetinfinity/util/get_license.py +1 -1
- StreamingCommunity/Api/Site/raiplay/series.py +1 -10
- StreamingCommunity/Api/Site/raiplay/site.py +5 -13
- StreamingCommunity/Api/Site/raiplay/util/ScrapeSerie.py +12 -12
- StreamingCommunity/Api/Template/loader.py +13 -4
- StreamingCommunity/Lib/Downloader/DASH/cdm_helpher.py +8 -3
- StreamingCommunity/Lib/Downloader/DASH/decrypt.py +1 -0
- StreamingCommunity/Lib/Downloader/DASH/downloader.py +9 -2
- StreamingCommunity/Lib/Downloader/DASH/parser.py +456 -98
- StreamingCommunity/Lib/Downloader/DASH/segments.py +109 -64
- StreamingCommunity/Lib/Downloader/HLS/segments.py +261 -355
- StreamingCommunity/Lib/Downloader/MP4/downloader.py +1 -1
- StreamingCommunity/Lib/FFmpeg/command.py +3 -3
- StreamingCommunity/Lib/M3U8/estimator.py +0 -1
- StreamingCommunity/Upload/version.py +1 -1
- StreamingCommunity/Util/config_json.py +0 -3
- {streamingcommunity-3.3.7.dist-info → streamingcommunity-3.3.9.dist-info}/METADATA +1 -3
- {streamingcommunity-3.3.7.dist-info → streamingcommunity-3.3.9.dist-info}/RECORD +30 -30
- {streamingcommunity-3.3.7.dist-info → streamingcommunity-3.3.9.dist-info}/WHEEL +0 -0
- {streamingcommunity-3.3.7.dist-info → streamingcommunity-3.3.9.dist-info}/entry_points.txt +0 -0
- {streamingcommunity-3.3.7.dist-info → streamingcommunity-3.3.9.dist-info}/licenses/LICENSE +0 -0
- {streamingcommunity-3.3.7.dist-info → streamingcommunity-3.3.9.dist-info}/top_level.txt +0 -0
|
@@ -8,6 +8,7 @@ import time
|
|
|
8
8
|
# External libraries
|
|
9
9
|
import httpx
|
|
10
10
|
from tqdm import tqdm
|
|
11
|
+
from rich.console import Console
|
|
11
12
|
|
|
12
13
|
|
|
13
14
|
# Internal utilities
|
|
@@ -22,6 +23,11 @@ REQUEST_MAX_RETRY = config_manager.get_int('REQUESTS', 'max_retry')
|
|
|
22
23
|
DEFAULT_VIDEO_WORKERS = config_manager.get_int('M3U8_DOWNLOAD', 'default_video_workers')
|
|
23
24
|
DEFAULT_AUDIO_WORKERS = config_manager.get_int('M3U8_DOWNLOAD', 'default_audio_workers')
|
|
24
25
|
SEGMENT_MAX_TIMEOUT = config_manager.get_int("M3U8_DOWNLOAD", "segment_timeout")
|
|
26
|
+
LIMIT_SEGMENT = config_manager.get_int('M3U8_DOWNLOAD', 'limit_segment')
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
# Variable
|
|
30
|
+
console = Console()
|
|
25
31
|
|
|
26
32
|
|
|
27
33
|
class MPD_Segments:
|
|
@@ -38,7 +44,13 @@ class MPD_Segments:
|
|
|
38
44
|
self.tmp_folder = tmp_folder
|
|
39
45
|
self.selected_representation = representation
|
|
40
46
|
self.pssh = pssh
|
|
41
|
-
|
|
47
|
+
|
|
48
|
+
# Use LIMIT_SEGMENT from config if limit_segments is not specified or is 0
|
|
49
|
+
if limit_segments is None or limit_segments == 0:
|
|
50
|
+
self.limit_segments = LIMIT_SEGMENT if LIMIT_SEGMENT > 0 else None
|
|
51
|
+
else:
|
|
52
|
+
self.limit_segments = limit_segments
|
|
53
|
+
|
|
42
54
|
self.download_interrupted = False
|
|
43
55
|
self.info_nFailed = 0
|
|
44
56
|
|
|
@@ -50,6 +62,10 @@ class MPD_Segments:
|
|
|
50
62
|
# Progress
|
|
51
63
|
self._last_progress_update = 0
|
|
52
64
|
self._progress_update_interval = 0.1
|
|
65
|
+
|
|
66
|
+
# Segment tracking
|
|
67
|
+
self.segment_files = {}
|
|
68
|
+
self.segments_lock = asyncio.Lock()
|
|
53
69
|
|
|
54
70
|
def get_concat_path(self, output_dir: str = None):
|
|
55
71
|
"""
|
|
@@ -78,10 +94,9 @@ class MPD_Segments:
|
|
|
78
94
|
if self.limit_segments is not None:
|
|
79
95
|
orig_count = len(self.selected_representation.get('segment_urls', []))
|
|
80
96
|
if orig_count > self.limit_segments:
|
|
81
|
-
|
|
97
|
+
|
|
82
98
|
# Limit segment URLs
|
|
83
99
|
self.selected_representation['segment_urls'] = self.selected_representation['segment_urls'][:self.limit_segments]
|
|
84
|
-
print(f"[yellow]Limiting segments from {orig_count} to {self.limit_segments}")
|
|
85
100
|
|
|
86
101
|
# Run async download in sync mode
|
|
87
102
|
try:
|
|
@@ -89,7 +104,7 @@ class MPD_Segments:
|
|
|
89
104
|
|
|
90
105
|
except KeyboardInterrupt:
|
|
91
106
|
self.download_interrupted = True
|
|
92
|
-
print("\n[red]Download interrupted by user (Ctrl+C).")
|
|
107
|
+
console.print("\n[red]Download interrupted by user (Ctrl+C).")
|
|
93
108
|
|
|
94
109
|
return {
|
|
95
110
|
"concat_path": concat_path,
|
|
@@ -113,6 +128,9 @@ class MPD_Segments:
|
|
|
113
128
|
|
|
114
129
|
os.makedirs(output_dir or self.tmp_folder, exist_ok=True)
|
|
115
130
|
concat_path = os.path.join(output_dir or self.tmp_folder, f"{rep_id}_encrypted.m4s")
|
|
131
|
+
|
|
132
|
+
temp_dir = os.path.join(output_dir or self.tmp_folder, f"{rep_id}_segments")
|
|
133
|
+
os.makedirs(temp_dir, exist_ok=True)
|
|
116
134
|
|
|
117
135
|
# Determine stream type (video/audio) for progress bar
|
|
118
136
|
stream_type = description
|
|
@@ -132,7 +150,7 @@ class MPD_Segments:
|
|
|
132
150
|
# Initialize estimator
|
|
133
151
|
estimator = M3U8_Ts_Estimator(total_segments=len(segment_urls) + 1)
|
|
134
152
|
|
|
135
|
-
|
|
153
|
+
self.segment_files = {}
|
|
136
154
|
self.downloaded_segments = set()
|
|
137
155
|
self.info_nFailed = 0
|
|
138
156
|
self.download_interrupted = False
|
|
@@ -148,25 +166,25 @@ class MPD_Segments:
|
|
|
148
166
|
# Download init segment
|
|
149
167
|
await self._download_init_segment(client, init_url, concat_path, estimator, progress_bar)
|
|
150
168
|
|
|
151
|
-
# Download all segments (first batch)
|
|
169
|
+
# Download all segments (first batch) - writes to temp files
|
|
152
170
|
await self._download_segments_batch(
|
|
153
|
-
client, segment_urls,
|
|
171
|
+
client, segment_urls, temp_dir, semaphore, REQUEST_MAX_RETRY, estimator, progress_bar
|
|
154
172
|
)
|
|
155
173
|
|
|
156
174
|
# Retry failed segments
|
|
157
175
|
await self._retry_failed_segments(
|
|
158
|
-
client, segment_urls,
|
|
176
|
+
client, segment_urls, temp_dir, semaphore, REQUEST_MAX_RETRY, estimator, progress_bar
|
|
159
177
|
)
|
|
160
178
|
|
|
161
|
-
#
|
|
162
|
-
self.
|
|
179
|
+
# Concatenate all segment files in order
|
|
180
|
+
await self._concatenate_segments(concat_path, len(segment_urls))
|
|
163
181
|
|
|
164
182
|
except KeyboardInterrupt:
|
|
165
183
|
self.download_interrupted = True
|
|
166
|
-
print("\n[red]Download interrupted by user (Ctrl+C).")
|
|
184
|
+
console.print("\n[red]Download interrupted by user (Ctrl+C).")
|
|
167
185
|
|
|
168
186
|
finally:
|
|
169
|
-
self._cleanup_resources(
|
|
187
|
+
self._cleanup_resources(temp_dir, progress_bar)
|
|
170
188
|
|
|
171
189
|
self._verify_download_completion()
|
|
172
190
|
return self._generate_results(stream_type)
|
|
@@ -187,12 +205,9 @@ class MPD_Segments:
|
|
|
187
205
|
with open(concat_path, 'wb') as outfile:
|
|
188
206
|
if response.status_code == 200:
|
|
189
207
|
outfile.write(response.content)
|
|
190
|
-
# Update estimator with init segment size
|
|
191
208
|
estimator.add_ts_file(len(response.content))
|
|
192
209
|
|
|
193
210
|
progress_bar.update(1)
|
|
194
|
-
|
|
195
|
-
# Update progress bar with estimated info
|
|
196
211
|
self._throttled_progress_update(len(response.content), estimator, progress_bar)
|
|
197
212
|
|
|
198
213
|
except Exception as e:
|
|
@@ -208,9 +223,9 @@ class MPD_Segments:
|
|
|
208
223
|
estimator.update_progress_bar(content_size, progress_bar)
|
|
209
224
|
self._last_progress_update = current_time
|
|
210
225
|
|
|
211
|
-
async def _download_segments_batch(self, client, segment_urls,
|
|
226
|
+
async def _download_segments_batch(self, client, segment_urls, temp_dir, semaphore, max_retry, estimator, progress_bar):
|
|
212
227
|
"""
|
|
213
|
-
Download a batch of segments and
|
|
228
|
+
Download a batch of segments and write them to temp files immediately.
|
|
214
229
|
"""
|
|
215
230
|
async def download_single(url, idx):
|
|
216
231
|
async with semaphore:
|
|
@@ -218,14 +233,21 @@ class MPD_Segments:
|
|
|
218
233
|
|
|
219
234
|
for attempt in range(max_retry):
|
|
220
235
|
if self.download_interrupted:
|
|
221
|
-
return idx,
|
|
236
|
+
return idx, False, attempt
|
|
222
237
|
|
|
223
238
|
try:
|
|
224
239
|
timeout = min(SEGMENT_MAX_TIMEOUT, 10 + attempt * 3)
|
|
225
240
|
resp = await client.get(url, headers=headers, follow_redirects=True, timeout=timeout)
|
|
226
241
|
|
|
242
|
+
# Write to temp file immediately
|
|
227
243
|
if resp.status_code == 200:
|
|
228
|
-
|
|
244
|
+
temp_file = os.path.join(temp_dir, f"seg_{idx:06d}.tmp")
|
|
245
|
+
async with self.segments_lock:
|
|
246
|
+
with open(temp_file, 'wb') as f:
|
|
247
|
+
f.write(resp.content)
|
|
248
|
+
self.segment_files[idx] = temp_file
|
|
249
|
+
|
|
250
|
+
return idx, True, attempt, len(resp.content)
|
|
229
251
|
else:
|
|
230
252
|
if attempt < 2:
|
|
231
253
|
sleep_time = 0.5 + attempt * 0.5
|
|
@@ -237,17 +259,16 @@ class MPD_Segments:
|
|
|
237
259
|
sleep_time = min(2.0, 1.1 * (2 ** attempt))
|
|
238
260
|
await asyncio.sleep(sleep_time)
|
|
239
261
|
|
|
240
|
-
return idx,
|
|
262
|
+
return idx, False, max_retry, 0
|
|
241
263
|
|
|
242
264
|
# Initial download attempt
|
|
243
265
|
tasks = [download_single(url, i) for i, url in enumerate(segment_urls)]
|
|
244
266
|
|
|
245
267
|
for coro in asyncio.as_completed(tasks):
|
|
246
268
|
try:
|
|
247
|
-
idx,
|
|
248
|
-
results[idx] = data
|
|
269
|
+
idx, success, nretry, size = await coro
|
|
249
270
|
|
|
250
|
-
if
|
|
271
|
+
if success:
|
|
251
272
|
self.downloaded_segments.add(idx)
|
|
252
273
|
else:
|
|
253
274
|
self.info_nFailed += 1
|
|
@@ -257,19 +278,15 @@ class MPD_Segments:
|
|
|
257
278
|
self.info_nRetry += nretry
|
|
258
279
|
|
|
259
280
|
progress_bar.update(1)
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
estimator.add_ts_file(len(data))
|
|
263
|
-
|
|
264
|
-
# Update progress bar with estimated info and segment count
|
|
265
|
-
self._throttled_progress_update(len(data), estimator, progress_bar)
|
|
281
|
+
estimator.add_ts_file(size)
|
|
282
|
+
self._throttled_progress_update(size, estimator, progress_bar)
|
|
266
283
|
|
|
267
284
|
except KeyboardInterrupt:
|
|
268
285
|
self.download_interrupted = True
|
|
269
286
|
print("\n[red]Download interrupted by user (Ctrl+C).")
|
|
270
287
|
break
|
|
271
288
|
|
|
272
|
-
async def _retry_failed_segments(self, client, segment_urls,
|
|
289
|
+
async def _retry_failed_segments(self, client, segment_urls, temp_dir, semaphore, max_retry, estimator, progress_bar):
|
|
273
290
|
"""
|
|
274
291
|
Retry failed segments up to 3 times.
|
|
275
292
|
"""
|
|
@@ -277,11 +294,9 @@ class MPD_Segments:
|
|
|
277
294
|
global_retry_count = 0
|
|
278
295
|
|
|
279
296
|
while self.info_nFailed > 0 and global_retry_count < max_global_retries and not self.download_interrupted:
|
|
280
|
-
failed_indices = [i for i
|
|
297
|
+
failed_indices = [i for i in range(len(segment_urls)) if i not in self.downloaded_segments]
|
|
281
298
|
if not failed_indices:
|
|
282
299
|
break
|
|
283
|
-
|
|
284
|
-
print(f"[yellow]Retrying {len(failed_indices)} failed segments (attempt {global_retry_count+1}/{max_global_retries})...")
|
|
285
300
|
|
|
286
301
|
async def download_single(url, idx):
|
|
287
302
|
async with semaphore:
|
|
@@ -289,32 +304,37 @@ class MPD_Segments:
|
|
|
289
304
|
|
|
290
305
|
for attempt in range(max_retry):
|
|
291
306
|
if self.download_interrupted:
|
|
292
|
-
return idx,
|
|
307
|
+
return idx, False, attempt, 0
|
|
293
308
|
|
|
294
309
|
try:
|
|
295
310
|
timeout = min(SEGMENT_MAX_TIMEOUT, 15 + attempt * 5)
|
|
296
311
|
resp = await client.get(url, headers=headers, timeout=timeout)
|
|
297
312
|
|
|
313
|
+
# Write to temp file immediately
|
|
298
314
|
if resp.status_code == 200:
|
|
299
|
-
|
|
315
|
+
temp_file = os.path.join(temp_dir, f"seg_{idx:06d}.tmp")
|
|
316
|
+
async with self.segments_lock:
|
|
317
|
+
with open(temp_file, 'wb') as f:
|
|
318
|
+
f.write(resp.content)
|
|
319
|
+
self.segment_files[idx] = temp_file
|
|
320
|
+
|
|
321
|
+
return idx, True, attempt, len(resp.content)
|
|
300
322
|
else:
|
|
301
323
|
await asyncio.sleep(1.5 * (2 ** attempt))
|
|
302
324
|
|
|
303
325
|
except Exception:
|
|
304
326
|
await asyncio.sleep(1.5 * (2 ** attempt))
|
|
305
327
|
|
|
306
|
-
return idx,
|
|
328
|
+
return idx, False, max_retry, 0
|
|
307
329
|
|
|
308
330
|
retry_tasks = [download_single(segment_urls[i], i) for i in failed_indices]
|
|
309
331
|
|
|
310
|
-
# Reset nFailed for this round
|
|
311
332
|
nFailed_this_round = 0
|
|
312
333
|
for coro in asyncio.as_completed(retry_tasks):
|
|
313
334
|
try:
|
|
314
|
-
idx,
|
|
335
|
+
idx, success, nretry, size = await coro
|
|
315
336
|
|
|
316
|
-
if
|
|
317
|
-
results[idx] = data
|
|
337
|
+
if success:
|
|
318
338
|
self.downloaded_segments.add(idx)
|
|
319
339
|
else:
|
|
320
340
|
nFailed_this_round += 1
|
|
@@ -323,33 +343,39 @@ class MPD_Segments:
|
|
|
323
343
|
self.info_maxRetry = nretry
|
|
324
344
|
self.info_nRetry += nretry
|
|
325
345
|
|
|
326
|
-
progress_bar.update(0)
|
|
327
|
-
estimator.add_ts_file(
|
|
328
|
-
self._throttled_progress_update(
|
|
346
|
+
progress_bar.update(0)
|
|
347
|
+
estimator.add_ts_file(size)
|
|
348
|
+
self._throttled_progress_update(size, estimator, progress_bar)
|
|
329
349
|
|
|
330
350
|
except KeyboardInterrupt:
|
|
331
351
|
self.download_interrupted = True
|
|
332
|
-
print("\n[red]Download interrupted by user (Ctrl+C).")
|
|
352
|
+
console.print("\n[red]Download interrupted by user (Ctrl+C).")
|
|
333
353
|
break
|
|
334
354
|
|
|
335
355
|
self.info_nFailed = nFailed_this_round
|
|
336
356
|
global_retry_count += 1
|
|
337
357
|
|
|
338
|
-
def
|
|
358
|
+
async def _concatenate_segments(self, concat_path, total_segments):
|
|
339
359
|
"""
|
|
340
|
-
|
|
360
|
+
Concatenate all segment files in order to the final output file.
|
|
361
|
+
Skip missing segments and continue with available ones.
|
|
341
362
|
"""
|
|
363
|
+
successful_segments = 0
|
|
342
364
|
with open(concat_path, 'ab') as outfile:
|
|
343
|
-
for
|
|
344
|
-
if
|
|
345
|
-
|
|
365
|
+
for idx in range(total_segments):
|
|
366
|
+
if idx in self.segment_files:
|
|
367
|
+
temp_file = self.segment_files[idx]
|
|
368
|
+
if os.path.exists(temp_file):
|
|
369
|
+
with open(temp_file, 'rb') as infile:
|
|
370
|
+
outfile.write(infile.read())
|
|
371
|
+
successful_segments += 1
|
|
346
372
|
|
|
347
373
|
def _get_bar_format(self, description: str) -> str:
|
|
348
374
|
"""
|
|
349
375
|
Generate platform-appropriate progress bar format.
|
|
350
376
|
"""
|
|
351
377
|
return (
|
|
352
|
-
f"{Colors.YELLOW}
|
|
378
|
+
f"{Colors.YELLOW}DASH{Colors.CYAN} {description}{Colors.WHITE}: "
|
|
353
379
|
f"{Colors.MAGENTA}{{bar:40}} "
|
|
354
380
|
f"{Colors.LIGHT_GREEN}{{n_fmt}}{Colors.WHITE}/{Colors.CYAN}{{total_fmt}} {Colors.LIGHT_MAGENTA}TS {Colors.WHITE}"
|
|
355
381
|
f"{Colors.DARK_GRAY}[{Colors.YELLOW}{{elapsed}}{Colors.WHITE} < {Colors.CYAN}{{remaining}}{Colors.DARK_GRAY}] "
|
|
@@ -378,43 +404,62 @@ class MPD_Segments:
|
|
|
378
404
|
|
|
379
405
|
def _verify_download_completion(self) -> None:
|
|
380
406
|
"""
|
|
381
|
-
Validate final download integrity.
|
|
407
|
+
Validate final download integrity - allow partial downloads.
|
|
382
408
|
"""
|
|
383
409
|
total = len(self.selected_representation['segment_urls'])
|
|
384
410
|
completed = getattr(self, 'downloaded_segments', set())
|
|
385
411
|
|
|
386
|
-
# If interrupted, skip raising error
|
|
387
412
|
if self.download_interrupted:
|
|
388
413
|
return
|
|
389
414
|
|
|
390
415
|
if total == 0:
|
|
391
416
|
return
|
|
392
417
|
|
|
393
|
-
|
|
418
|
+
completion_rate = len(completed) / total
|
|
419
|
+
missing_count = total - len(completed)
|
|
420
|
+
|
|
421
|
+
# Allow downloads with up to 30 missing segments or 90% completion rate
|
|
422
|
+
if completion_rate >= 0.90 or missing_count <= 30:
|
|
423
|
+
return
|
|
424
|
+
|
|
425
|
+
else:
|
|
394
426
|
missing = sorted(set(range(total)) - completed)
|
|
395
|
-
|
|
427
|
+
console.print(f"[red]Missing segments: {missing[:10]}..." if len(missing) > 10 else f"[red]Missing segments: {missing}")
|
|
396
428
|
|
|
397
|
-
def _cleanup_resources(self,
|
|
429
|
+
def _cleanup_resources(self, temp_dir, progress_bar: tqdm) -> None:
|
|
398
430
|
"""
|
|
399
431
|
Ensure resource cleanup and final reporting.
|
|
400
432
|
"""
|
|
401
433
|
progress_bar.close()
|
|
434
|
+
|
|
435
|
+
# Delete temp segment files
|
|
436
|
+
if temp_dir and os.path.exists(temp_dir):
|
|
437
|
+
try:
|
|
438
|
+
for temp_file in self.segment_files.values():
|
|
439
|
+
if os.path.exists(temp_file):
|
|
440
|
+
os.remove(temp_file)
|
|
441
|
+
os.rmdir(temp_dir)
|
|
442
|
+
|
|
443
|
+
except Exception as e:
|
|
444
|
+
print(f"[yellow]Warning: Could not clean temp directory: {e}")
|
|
445
|
+
|
|
402
446
|
if getattr(self, 'info_nFailed', 0) > 0:
|
|
403
447
|
self._display_error_summary()
|
|
404
448
|
|
|
405
449
|
# Clear memory
|
|
406
|
-
self.
|
|
407
|
-
self.expected_index = 0
|
|
450
|
+
self.segment_files = {}
|
|
408
451
|
|
|
409
452
|
def _display_error_summary(self) -> None:
|
|
410
453
|
"""
|
|
411
454
|
Generate final error report.
|
|
412
455
|
"""
|
|
413
456
|
total_segments = len(self.selected_representation.get('segment_urls', []))
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
457
|
+
failed_indices = [i for i in range(total_segments) if i not in self.downloaded_segments]
|
|
458
|
+
successful_segments = len(self.downloaded_segments)
|
|
459
|
+
|
|
460
|
+
console.print(f"[green]Download Summary: "
|
|
461
|
+
f"[cyan]Successful: [red]{successful_segments}/{total_segments} "
|
|
462
|
+
f"[cyan]Max retries: [red]{getattr(self, 'info_maxRetry', 0)} "
|
|
463
|
+
f"[cyan]Total retries: [red]{getattr(self, 'info_nRetry', 0)} "
|
|
464
|
+
f"[cyan]Failed segments: [red]{getattr(self, 'info_nFailed', 0)} "
|
|
465
|
+
f"[cyan]Failed indices: [red]{failed_indices} \n")
|