StreamingCommunity 3.3.8__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.

Files changed (26) hide show
  1. StreamingCommunity/Api/Player/supervideo.py +1 -1
  2. StreamingCommunity/Api/Site/crunchyroll/film.py +13 -3
  3. StreamingCommunity/Api/Site/crunchyroll/series.py +6 -6
  4. StreamingCommunity/Api/Site/crunchyroll/site.py +13 -8
  5. StreamingCommunity/Api/Site/crunchyroll/util/ScrapeSerie.py +16 -41
  6. StreamingCommunity/Api/Site/crunchyroll/util/get_license.py +107 -101
  7. StreamingCommunity/Api/Site/mediasetinfinity/util/get_license.py +1 -1
  8. StreamingCommunity/Api/Site/raiplay/series.py +1 -10
  9. StreamingCommunity/Api/Site/raiplay/site.py +5 -13
  10. StreamingCommunity/Api/Site/raiplay/util/ScrapeSerie.py +12 -12
  11. StreamingCommunity/Lib/Downloader/DASH/cdm_helpher.py +8 -3
  12. StreamingCommunity/Lib/Downloader/DASH/decrypt.py +1 -0
  13. StreamingCommunity/Lib/Downloader/DASH/downloader.py +9 -2
  14. StreamingCommunity/Lib/Downloader/DASH/parser.py +456 -98
  15. StreamingCommunity/Lib/Downloader/DASH/segments.py +109 -64
  16. StreamingCommunity/Lib/Downloader/HLS/segments.py +261 -355
  17. StreamingCommunity/Lib/Downloader/MP4/downloader.py +1 -1
  18. StreamingCommunity/Lib/FFmpeg/command.py +3 -3
  19. StreamingCommunity/Lib/M3U8/estimator.py +0 -1
  20. StreamingCommunity/Upload/version.py +1 -1
  21. {streamingcommunity-3.3.8.dist-info → streamingcommunity-3.3.9.dist-info}/METADATA +1 -1
  22. {streamingcommunity-3.3.8.dist-info → streamingcommunity-3.3.9.dist-info}/RECORD +26 -26
  23. {streamingcommunity-3.3.8.dist-info → streamingcommunity-3.3.9.dist-info}/WHEEL +0 -0
  24. {streamingcommunity-3.3.8.dist-info → streamingcommunity-3.3.9.dist-info}/entry_points.txt +0 -0
  25. {streamingcommunity-3.3.8.dist-info → streamingcommunity-3.3.9.dist-info}/licenses/LICENSE +0 -0
  26. {streamingcommunity-3.3.8.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
- self.limit_segments = limit_segments
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
- results = [None] * len(segment_urls)
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, results, semaphore, REQUEST_MAX_RETRY, estimator, progress_bar
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, results, semaphore, REQUEST_MAX_RETRY, estimator, progress_bar
176
+ client, segment_urls, temp_dir, semaphore, REQUEST_MAX_RETRY, estimator, progress_bar
159
177
  )
160
178
 
161
- # Write all results to file
162
- self._write_results_to_file(concat_path, results)
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(None, progress_bar)
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, results, semaphore, max_retry, estimator, progress_bar):
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 update results.
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, b'', attempt
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
- return idx, resp.content, attempt
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, b'', max_retry
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, data, nretry = await coro
248
- results[idx] = data
269
+ idx, success, nretry, size = await coro
249
270
 
250
- if data and len(data) > 0:
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
- # Update estimator with segment size
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, results, semaphore, max_retry, estimator, progress_bar):
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, data in enumerate(results) if not data or len(data) == 0]
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, b'', attempt
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
- return idx, resp.content, attempt
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, b'', max_retry
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, data, nretry = await coro
335
+ idx, success, nretry, size = await coro
315
336
 
316
- if data and len(data) > 0:
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) # No progress bar increment, already counted
327
- estimator.add_ts_file(len(data))
328
- self._throttled_progress_update(len(data), estimator, progress_bar)
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 _write_results_to_file(self, concat_path, results):
358
+ async def _concatenate_segments(self, concat_path, total_segments):
339
359
  """
340
- Write all downloaded segments to the output file.
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 data in results:
344
- if data:
345
- outfile.write(data)
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}[DASH]{Colors.CYAN} {description}{Colors.WHITE}: "
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
- if len(completed) / total < 0.999:
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
- raise RuntimeError(f"Download incomplete ({len(completed)/total:.1%}). Missing segments: {missing}")
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, writer_thread, progress_bar: tqdm) -> None:
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.buffer = {}
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
- print(f"\n[cyan]Retry Summary: "
415
- f"[white]Max retries: [green]{getattr(self, 'info_maxRetry', 0)} "
416
- f"[white]Total retries: [green]{getattr(self, 'info_nRetry', 0)} "
417
- f"[white]Failed segments: [red]{getattr(self, 'info_nFailed', 0)}")
418
-
419
- if getattr(self, 'info_nRetry', 0) > total_segments * 0.3:
420
- print("[yellow]Warning: High retry count detected. Consider reducing worker count in config.")
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")