StreamingCommunity 3.3.9__py3-none-any.whl → 3.4.2__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of StreamingCommunity might be problematic. Click here for more details.

Files changed (70) hide show
  1. StreamingCommunity/Api/Player/hdplayer.py +0 -5
  2. StreamingCommunity/Api/Player/mediapolisvod.py +4 -13
  3. StreamingCommunity/Api/Player/supervideo.py +3 -8
  4. StreamingCommunity/Api/Player/sweetpixel.py +1 -9
  5. StreamingCommunity/Api/Player/vixcloud.py +5 -16
  6. StreamingCommunity/Api/Site/altadefinizione/film.py +4 -16
  7. StreamingCommunity/Api/Site/altadefinizione/series.py +3 -12
  8. StreamingCommunity/Api/Site/altadefinizione/site.py +2 -9
  9. StreamingCommunity/Api/Site/altadefinizione/util/ScrapeSerie.py +2 -7
  10. StreamingCommunity/Api/Site/animeunity/site.py +9 -24
  11. StreamingCommunity/Api/Site/animeunity/util/ScrapeSerie.py +11 -27
  12. StreamingCommunity/Api/Site/animeworld/film.py +4 -2
  13. StreamingCommunity/Api/Site/animeworld/site.py +3 -11
  14. StreamingCommunity/Api/Site/animeworld/util/ScrapeSerie.py +1 -4
  15. StreamingCommunity/Api/Site/crunchyroll/film.py +4 -5
  16. StreamingCommunity/Api/Site/crunchyroll/series.py +5 -17
  17. StreamingCommunity/Api/Site/crunchyroll/site.py +4 -13
  18. StreamingCommunity/Api/Site/crunchyroll/util/ScrapeSerie.py +5 -27
  19. StreamingCommunity/Api/Site/crunchyroll/util/get_license.py +11 -26
  20. StreamingCommunity/Api/Site/guardaserie/series.py +3 -14
  21. StreamingCommunity/Api/Site/guardaserie/site.py +4 -12
  22. StreamingCommunity/Api/Site/guardaserie/util/ScrapeSerie.py +3 -10
  23. StreamingCommunity/Api/Site/mediasetinfinity/film.py +11 -12
  24. StreamingCommunity/Api/Site/mediasetinfinity/series.py +4 -15
  25. StreamingCommunity/Api/Site/mediasetinfinity/site.py +16 -32
  26. StreamingCommunity/Api/Site/mediasetinfinity/util/ScrapeSerie.py +39 -50
  27. StreamingCommunity/Api/Site/mediasetinfinity/util/fix_mpd.py +3 -3
  28. StreamingCommunity/Api/Site/mediasetinfinity/util/get_license.py +7 -25
  29. StreamingCommunity/Api/Site/raiplay/film.py +6 -8
  30. StreamingCommunity/Api/Site/raiplay/series.py +5 -20
  31. StreamingCommunity/Api/Site/raiplay/site.py +45 -47
  32. StreamingCommunity/Api/Site/raiplay/util/ScrapeSerie.py +91 -55
  33. StreamingCommunity/Api/Site/raiplay/util/get_license.py +3 -12
  34. StreamingCommunity/Api/Site/streamingcommunity/film.py +5 -16
  35. StreamingCommunity/Api/Site/streamingcommunity/series.py +5 -10
  36. StreamingCommunity/Api/Site/streamingcommunity/site.py +3 -22
  37. StreamingCommunity/Api/Site/streamingcommunity/util/ScrapeSerie.py +11 -27
  38. StreamingCommunity/Api/Site/streamingwatch/__init__.py +1 -0
  39. StreamingCommunity/Api/Site/streamingwatch/film.py +4 -2
  40. StreamingCommunity/Api/Site/streamingwatch/series.py +4 -14
  41. StreamingCommunity/Api/Site/streamingwatch/site.py +4 -18
  42. StreamingCommunity/Api/Site/streamingwatch/util/ScrapeSerie.py +0 -3
  43. StreamingCommunity/Api/Template/Util/__init__.py +4 -2
  44. StreamingCommunity/Api/Template/Util/manage_ep.py +66 -0
  45. StreamingCommunity/Api/Template/config_loader.py +0 -7
  46. StreamingCommunity/Lib/Downloader/DASH/decrypt.py +54 -1
  47. StreamingCommunity/Lib/Downloader/DASH/downloader.py +186 -70
  48. StreamingCommunity/Lib/Downloader/DASH/parser.py +2 -3
  49. StreamingCommunity/Lib/Downloader/DASH/segments.py +109 -68
  50. StreamingCommunity/Lib/Downloader/HLS/downloader.py +100 -82
  51. StreamingCommunity/Lib/Downloader/HLS/segments.py +40 -28
  52. StreamingCommunity/Lib/Downloader/MP4/downloader.py +16 -4
  53. StreamingCommunity/Lib/FFmpeg/capture.py +37 -5
  54. StreamingCommunity/Lib/FFmpeg/command.py +32 -90
  55. StreamingCommunity/Lib/M3U8/estimator.py +47 -1
  56. StreamingCommunity/Lib/TMBD/tmdb.py +2 -4
  57. StreamingCommunity/TelegramHelp/config.json +0 -1
  58. StreamingCommunity/Upload/update.py +19 -6
  59. StreamingCommunity/Upload/version.py +1 -1
  60. StreamingCommunity/Util/config_json.py +28 -21
  61. StreamingCommunity/Util/http_client.py +28 -0
  62. StreamingCommunity/Util/os.py +16 -6
  63. StreamingCommunity/Util/table.py +50 -8
  64. {streamingcommunity-3.3.9.dist-info → streamingcommunity-3.4.2.dist-info}/METADATA +1 -3
  65. streamingcommunity-3.4.2.dist-info/RECORD +111 -0
  66. streamingcommunity-3.3.9.dist-info/RECORD +0 -111
  67. {streamingcommunity-3.3.9.dist-info → streamingcommunity-3.4.2.dist-info}/WHEEL +0 -0
  68. {streamingcommunity-3.3.9.dist-info → streamingcommunity-3.4.2.dist-info}/entry_points.txt +0 -0
  69. {streamingcommunity-3.3.9.dist-info → streamingcommunity-3.4.2.dist-info}/licenses/LICENSE +0 -0
  70. {streamingcommunity-3.3.9.dist-info → streamingcommunity-3.4.2.dist-info}/top_level.txt +0 -0
@@ -1,6 +1,7 @@
1
1
  # 19.06.24
2
2
 
3
3
  import sys
4
+ import time
4
5
  import logging
5
6
  from typing import List
6
7
 
@@ -209,6 +210,71 @@ def validate_episode_selection(list_episode_select: List[int], episodes_count: i
209
210
  list_episode_select = list(map(int, input_episodes.split(',')))
210
211
 
211
212
 
213
+ def display_seasons_list(seasons_manager) -> str:
214
+ """
215
+ Display seasons list and handle user input.
216
+
217
+ Parameters:
218
+ - seasons_manager: Manager object containing seasons information.
219
+
220
+ Returns:
221
+ last_command (str): Last command entered by the user.
222
+ """
223
+ if len(seasons_manager.seasons) == 1:
224
+ console.print("\n[green]Only one season available, selecting it automatically[/green]")
225
+ time.sleep(1)
226
+ return "1"
227
+
228
+ # Set up table for displaying seasons
229
+ table_show_manager = TVShowManager()
230
+
231
+ # Check if 'type' and 'id' attributes exist in the first season
232
+ has_type = hasattr(seasons_manager.seasons[0], 'type') and (seasons_manager.seasons[0].type) is not None and str(seasons_manager.seasons[0].type) != ''
233
+ has_id = hasattr(seasons_manager.seasons[0], 'id') and (seasons_manager.seasons[0].id) is not None and str(seasons_manager.seasons[0].id) != ''
234
+
235
+ # Add columns to the table
236
+ column_info = {
237
+ "Index": {'color': 'red'},
238
+ "Name": {'color': 'yellow'}
239
+ }
240
+
241
+ if has_type:
242
+ column_info["Type"] = {'color': 'magenta'}
243
+
244
+ if has_id:
245
+ column_info["ID"] = {'color': 'cyan'}
246
+
247
+ table_show_manager.add_column(column_info)
248
+
249
+ # Populate the table with seasons information
250
+ for i, season in enumerate(seasons_manager.seasons):
251
+ season_name = season.name if hasattr(season, 'name') else 'N/A'
252
+ season_info = {
253
+ 'Index': str(i + 1),
254
+ 'Name': season_name
255
+ }
256
+
257
+ # Add 'Type' and 'ID' if they exist
258
+ if has_type:
259
+ season_type = season.type if hasattr(season, 'type') else 'N/A'
260
+ season_info['Type'] = season_type
261
+
262
+ if has_id:
263
+ season_id = season.id if hasattr(season, 'id') else 'N/A'
264
+ season_info['ID'] = season_id
265
+
266
+ table_show_manager.add_tv_show(season_info)
267
+
268
+ # Run the table and handle user input
269
+ last_command = table_show_manager.run()
270
+
271
+ if last_command in ("q", "quit"):
272
+ console.print("\n[red]Quit ...")
273
+ sys.exit(0)
274
+
275
+ return last_command
276
+
277
+
212
278
  def display_episodes_list(episodes_manager) -> str:
213
279
  """
214
280
  Display episodes list and handle user input.
@@ -56,13 +56,6 @@ class SiteConstant:
56
56
  base_path = os.path.join(base_path, self.SITE_NAME)
57
57
  return os.path.join(base_path, config_manager.get('OUT_FOLDER', 'anime_folder_name'))
58
58
 
59
- @property
60
- def COOKIE(self):
61
- try:
62
- return config_manager.get_dict('SITE_EXTRA', self.SITE_NAME)
63
- except KeyError:
64
- return None
65
-
66
59
  @property
67
60
  def TELEGRAM_BOT(self):
68
61
  return config_manager.get_bool('DEFAULT', 'telegram_bot')
@@ -3,25 +3,30 @@
3
3
  import os
4
4
  import subprocess
5
5
  import logging
6
+ import threading
7
+ import time
6
8
 
7
9
 
8
10
  # External libraries
9
11
  from rich.console import Console
12
+ from tqdm import tqdm
10
13
 
11
14
 
12
15
  # Internal utilities
13
16
  from StreamingCommunity.Util.os import get_mp4decrypt_path
17
+ from StreamingCommunity.Util.color import Colors
14
18
 
15
19
  # Variable
16
20
  console = Console()
17
21
 
18
22
 
19
23
  # NOTE!: SAREBBE MEGLIO FARLO PER OGNI FILE DURANTE IL DOWNLOAD ... MA PER ORA LO LASCIO COSI
20
- def decrypt_with_mp4decrypt(encrypted_path, kid, key, output_path=None, cleanup=True):
24
+ def decrypt_with_mp4decrypt(type, encrypted_path, kid, key, output_path=None, cleanup=True):
21
25
  """
22
26
  Decrypt an mp4/m4s file using mp4decrypt.
23
27
 
24
28
  Args:
29
+ type (str): Type of file ('video' or 'audio').
25
30
  encrypted_path (str): Path to encrypted file.
26
31
  kid (str): Hexadecimal KID.
27
32
  key (str): Hexadecimal key.
@@ -48,15 +53,63 @@ def decrypt_with_mp4decrypt(encrypted_path, kid, key, output_path=None, cleanup=
48
53
  if not output_path:
49
54
  output_path = os.path.splitext(encrypted_path)[0] + "_decrypted.mp4"
50
55
 
56
+ # Get file size for progress tracking
57
+ file_size = os.path.getsize(encrypted_path)
58
+
51
59
  key_format = f"{kid.lower()}:{key.lower()}"
52
60
  cmd = [get_mp4decrypt_path(), "--key", key_format, encrypted_path, output_path]
53
61
  logging.info(f"Running command: {' '.join(cmd)}")
54
62
 
63
+ # Create progress bar with custom format
64
+ bar_format = (
65
+ f"{Colors.YELLOW}DECRYPT{Colors.CYAN} {type}{Colors.WHITE}: "
66
+ f"{Colors.MAGENTA}{{bar:40}} "
67
+ f"{Colors.LIGHT_GREEN}{{n_fmt}}{Colors.WHITE}/{Colors.CYAN}{{total_fmt}} "
68
+ f"{Colors.DARK_GRAY}[{Colors.YELLOW}{{elapsed}}{Colors.WHITE} < {Colors.CYAN}{{remaining}}{Colors.DARK_GRAY}] "
69
+ f"{Colors.WHITE}{{postfix}}"
70
+ )
71
+
72
+ progress_bar = tqdm(
73
+ total=100,
74
+ bar_format=bar_format,
75
+ unit="",
76
+ ncols=150
77
+ )
78
+
79
+ def monitor_output_file():
80
+ """Monitor output file growth and update progress bar."""
81
+ last_size = 0
82
+ while True:
83
+ if os.path.exists(output_path):
84
+ current_size = os.path.getsize(output_path)
85
+ if current_size > 0:
86
+ progress_percent = min(int((current_size / file_size) * 100), 100)
87
+ progress_bar.n = progress_percent
88
+ progress_bar.refresh()
89
+
90
+ if current_size == last_size and current_size > 0:
91
+ # File stopped growing, likely finished
92
+ break
93
+
94
+ last_size = current_size
95
+
96
+ time.sleep(0.1)
97
+
98
+ # Start monitoring thread
99
+ monitor_thread = threading.Thread(target=monitor_output_file, daemon=True)
100
+ monitor_thread.start()
101
+
55
102
  try:
56
103
  result = subprocess.run(cmd, capture_output=True, text=True, timeout=300)
57
104
  except Exception as e:
105
+ progress_bar.close()
58
106
  console.print(f"[bold red] mp4decrypt execution failed: {e}[/bold red]")
59
107
  return None
108
+
109
+ # Ensure progress bar reaches 100%
110
+ progress_bar.n = 100
111
+ progress_bar.refresh()
112
+ progress_bar.close()
60
113
 
61
114
  if result.returncode == 0 and os.path.exists(output_path):
62
115
 
@@ -1,19 +1,19 @@
1
1
  # 25.07.25
2
2
 
3
3
  import os
4
- import time
5
4
  import shutil
5
+ import logging
6
+ from typing import Optional, Dict
6
7
 
7
8
 
8
9
  # External libraries
9
10
  from rich.console import Console
10
- from rich.panel import Panel
11
11
  from rich.table import Table
12
12
 
13
13
 
14
14
  # Internal utilities
15
15
  from StreamingCommunity.Util.config_json import config_manager
16
- from StreamingCommunity.Util.os import os_manager, internet_manager
16
+ from StreamingCommunity.Util.os import os_manager, internet_manager, get_wvd_path
17
17
  from StreamingCommunity.Util.http_client import create_client
18
18
  from StreamingCommunity.Util.headers import get_userAgent
19
19
 
@@ -32,11 +32,11 @@ from ...FFmpeg import print_duration_table, join_audios, join_video, join_subtit
32
32
  # Config
33
33
  DOWNLOAD_SPECIFIC_AUDIO = config_manager.get_list('M3U8_DOWNLOAD', 'specific_list_audio')
34
34
  DOWNLOAD_SPECIFIC_SUBTITLE = config_manager.get_list('M3U8_DOWNLOAD', 'specific_list_subtitles')
35
- ENABLE_SUBTITLE = config_manager.get_bool('M3U8_DOWNLOAD', 'download_subtitle')
36
35
  MERGE_SUBTITLE = config_manager.get_bool('M3U8_DOWNLOAD', 'merge_subs')
37
36
  FILTER_CUSTOM_REOLUTION = str(config_manager.get('M3U8_CONVERSION', 'force_resolution')).strip().lower()
38
37
  CLEANUP_TMP = config_manager.get_bool('M3U8_DOWNLOAD', 'cleanup_tmp_folder')
39
38
  RETRY_LIMIT = config_manager.get_int('REQUESTS', 'max_retry')
39
+ EXTENSION_OUTPUT = config_manager.get("M3U8_CONVERSION", "extension")
40
40
 
41
41
 
42
42
  # Variable
@@ -44,18 +44,17 @@ console = Console()
44
44
 
45
45
 
46
46
  class DASH_Downloader:
47
- def __init__(self, cdm_device, license_url, mpd_url, mpd_sub_list: list = None, output_path: str = None):
47
+ def __init__(self, license_url, mpd_url, mpd_sub_list: list = None, output_path: str = None):
48
48
  """
49
49
  Initialize the DASH Downloader with necessary parameters.
50
50
 
51
51
  Parameters:
52
- - cdm_device (str): Path to the CDM device for decryption.
53
52
  - license_url (str): URL to obtain the license for decryption.
54
53
  - mpd_url (str): URL of the MPD manifest file.
55
54
  - mpd_sub_list (list): List of subtitle dicts with keys: 'language', 'url', 'format'.
56
55
  - output_path (str): Path to save the final output file.
57
56
  """
58
- self.cdm_device = cdm_device
57
+ self.cdm_device = get_wvd_path()
59
58
  self.license_url = license_url
60
59
  self.mpd_url = mpd_url
61
60
  self.mpd_sub_list = mpd_sub_list or []
@@ -75,6 +74,10 @@ class DASH_Downloader:
75
74
  self.error = None
76
75
  self.stopped = False
77
76
  self.output_file = None
77
+
78
+ # For progress tracking
79
+ self.current_downloader: Optional[MPD_Segments] = None
80
+ self.current_download_type: Optional[str] = None
78
81
 
79
82
  def _setup_temp_dirs(self):
80
83
  """
@@ -196,49 +199,28 @@ class DASH_Downloader:
196
199
  Download subtitle files based on configuration with retry mechanism.
197
200
  Returns True if successful or if no subtitles to download, False on critical error.
198
201
  """
199
- if not ENABLE_SUBTITLE or not self.selected_subs:
200
- return True
201
-
202
- headers = {'User-Agent': get_userAgent()}
203
- client = create_client(headers=headers)
202
+ client = create_client(headers={'User-Agent': get_userAgent()})
204
203
 
205
204
  for sub in self.selected_subs:
206
- language = sub.get('language', 'unknown')
207
- url = sub.get('url')
208
- fmt = sub.get('format', 'vtt')
209
-
210
- if not url:
211
- console.print(f"[yellow]Warning: No URL for subtitle {language}[/yellow]")
212
- continue
213
-
214
- # Retry mechanism for downloading subtitles
215
- success = False
216
- for attempt in range(RETRY_LIMIT):
217
- try:
218
- # Download subtitle
219
- response = client.get(url)
220
- response.raise_for_status()
221
-
222
- # Save subtitle file
223
- sub_filename = f"{language}.{fmt}"
224
- sub_path = os.path.join(self.subs_dir, sub_filename)
225
-
226
- with open(sub_path, 'wb') as f:
227
- f.write(response.content)
228
-
229
- success = True
230
- break
205
+ try:
206
+ language = sub.get('language', 'unknown')
207
+ fmt = sub.get('format', 'vtt')
208
+
209
+ # Download subtitle
210
+ response = client.get(sub.get('url'))
211
+ response.raise_for_status()
212
+
213
+ # Save subtitle file and make request
214
+ sub_filename = f"{language}.{fmt}"
215
+ sub_path = os.path.join(self.subs_dir, sub_filename)
216
+
217
+ with open(sub_path, 'wb') as f:
218
+ f.write(response.content)
231
219
 
232
- except Exception as e:
233
- if attempt < RETRY_LIMIT - 1:
234
- console.print(f"[yellow]Attempt {attempt + 1}/{RETRY_LIMIT} failed for subtitle {language}: {e}. Retrying...[/yellow]")
235
- time.sleep(1.5 ** attempt)
236
- else:
237
- console.print(f"[yellow]Warning: Failed to download subtitle {language} after {RETRY_LIMIT} attempts: {e}[/yellow]")
220
+ except Exception as e:
221
+ console.print(f"[red]Error downloading subtitle {language}: {e}[/red]")
222
+ return False
238
223
 
239
- if not success:
240
- continue
241
-
242
224
  return True
243
225
 
244
226
  def download_and_decrypt(self, custom_headers=None, custom_payload=None):
@@ -256,7 +238,6 @@ class DASH_Downloader:
256
238
 
257
239
  # Fetch keys immediately after obtaining PSSH
258
240
  if not self.parser.pssh:
259
- console.print("[red]No PSSH found: segments are not encrypted, skipping decryption.")
260
241
  self.download_segments(clear=True)
261
242
  return True
262
243
 
@@ -293,6 +274,10 @@ class DASH_Downloader:
293
274
  pssh=self.parser.pssh
294
275
  )
295
276
 
277
+ # Set current downloader for progress tracking
278
+ self.current_downloader = video_downloader
279
+ self.current_download_type = 'video'
280
+
296
281
  try:
297
282
  result = video_downloader.download_streams(description="Video")
298
283
 
@@ -312,11 +297,15 @@ class DASH_Downloader:
312
297
  except Exception as ex:
313
298
  self.error = str(ex)
314
299
  return False
300
+
301
+ finally:
302
+ self.current_downloader = None
303
+ self.current_download_type = None
315
304
 
316
305
  # Decrypt video
317
306
  decrypted_path = os.path.join(self.decrypted_dir, "video.mp4")
318
307
  result_path = decrypt_with_mp4decrypt(
319
- encrypted_path, KID, KEY, output_path=decrypted_path
308
+ "Video", encrypted_path, KID, KEY, output_path=decrypted_path
320
309
  )
321
310
 
322
311
  if not result_path:
@@ -345,6 +334,10 @@ class DASH_Downloader:
345
334
  limit_segments=video_segments_count if video_segments_count > 0 else None
346
335
  )
347
336
 
337
+ # Set current downloader for progress tracking
338
+ self.current_downloader = audio_downloader
339
+ self.current_download_type = f"audio_{audio_language}"
340
+
348
341
  try:
349
342
  result = audio_downloader.download_streams(description=f"Audio {audio_language}")
350
343
 
@@ -361,11 +354,15 @@ class DASH_Downloader:
361
354
  except Exception as ex:
362
355
  self.error = str(ex)
363
356
  return False
357
+
358
+ finally:
359
+ self.current_downloader = None
360
+ self.current_download_type = None
364
361
 
365
362
  # Decrypt audio
366
363
  decrypted_path = os.path.join(self.decrypted_dir, "audio.mp4")
367
364
  result_path = decrypt_with_mp4decrypt(
368
- encrypted_path, KID, KEY, output_path=decrypted_path
365
+ f"Audio {audio_language}", encrypted_path, KID, KEY, output_path=decrypted_path
369
366
  )
370
367
 
371
368
  if not result_path:
@@ -381,9 +378,126 @@ class DASH_Downloader:
381
378
  return True
382
379
 
383
380
  def download_segments(self, clear=False):
384
- # Download segments and concatenate them
385
- # clear=True: no decryption needed
386
- pass
381
+ """
382
+ Download video/audio segments without decryption (for clear content).
383
+
384
+ Parameters:
385
+ clear (bool): If True, content is not encrypted and doesn't need decryption
386
+ """
387
+ if not clear:
388
+ console.print("[yellow]Warning: download_segments called with clear=False[/yellow]")
389
+ return False
390
+
391
+ video_segments_count = 0
392
+
393
+ # Download subtitles
394
+ self.download_subtitles()
395
+
396
+ # Download video
397
+ video_rep = self.get_representation_by_type("video")
398
+ if video_rep:
399
+ encrypted_path = os.path.join(self.encrypted_dir, f"{video_rep['id']}_encrypted.m4s")
400
+
401
+ # If m4s file doesn't exist, start downloading
402
+ if not os.path.exists(encrypted_path):
403
+ video_downloader = MPD_Segments(
404
+ tmp_folder=self.encrypted_dir,
405
+ representation=video_rep,
406
+ pssh=self.parser.pssh
407
+ )
408
+
409
+ # Set current downloader for progress tracking
410
+ self.current_downloader = video_downloader
411
+ self.current_download_type = 'video'
412
+
413
+ try:
414
+ result = video_downloader.download_streams(description="Video")
415
+
416
+ # Store the video segment count for limiting audio
417
+ video_segments_count = video_downloader.get_segments_count()
418
+
419
+ # Check for interruption or failure
420
+ if result.get("stopped"):
421
+ self.stopped = True
422
+ self.error = "Download interrupted"
423
+ return False
424
+
425
+ if result.get("nFailed", 0) > 0:
426
+ self.error = f"Failed segments: {result['nFailed']}"
427
+ return False
428
+
429
+ except Exception as ex:
430
+ self.error = str(ex)
431
+ console.print(f"[red]Error downloading video: {ex}[/red]")
432
+ return False
433
+
434
+ finally:
435
+ self.current_downloader = None
436
+ self.current_download_type = None
437
+
438
+ # NO DECRYPTION: just copy/move to decrypted folder
439
+ decrypted_path = os.path.join(self.decrypted_dir, "video.mp4")
440
+ if os.path.exists(encrypted_path) and not os.path.exists(decrypted_path):
441
+ shutil.copy2(encrypted_path, decrypted_path)
442
+
443
+ else:
444
+ self.error = "No video found"
445
+ console.print(f"[red]{self.error}[/red]")
446
+ return False
447
+
448
+ # Download audio with segment limiting
449
+ audio_rep = self.get_representation_by_type("audio")
450
+ if audio_rep:
451
+ encrypted_path = os.path.join(self.encrypted_dir, f"{audio_rep['id']}_encrypted.m4s")
452
+
453
+ # If m4s file doesn't exist, start downloading
454
+ if not os.path.exists(encrypted_path):
455
+ audio_language = audio_rep.get('language', 'Unknown')
456
+
457
+ audio_downloader = MPD_Segments(
458
+ tmp_folder=self.encrypted_dir,
459
+ representation=audio_rep,
460
+ pssh=self.parser.pssh,
461
+ limit_segments=video_segments_count if video_segments_count > 0 else None
462
+ )
463
+
464
+ # Set current downloader for progress tracking
465
+ self.current_downloader = audio_downloader
466
+ self.current_download_type = f"audio_{audio_language}"
467
+
468
+ try:
469
+ result = audio_downloader.download_streams(description=f"Audio {audio_language}")
470
+
471
+ # Check for interruption or failure
472
+ if result.get("stopped"):
473
+ self.stopped = True
474
+ self.error = "Download interrupted"
475
+ return False
476
+
477
+ if result.get("nFailed", 0) > 0:
478
+ self.error = f"Failed segments: {result['nFailed']}"
479
+ return False
480
+
481
+ except Exception as ex:
482
+ self.error = str(ex)
483
+ console.print(f"[red]Error downloading audio: {ex}[/red]")
484
+ return False
485
+
486
+ finally:
487
+ self.current_downloader = None
488
+ self.current_download_type = None
489
+
490
+ # NO DECRYPTION: just copy/move to decrypted folder
491
+ decrypted_path = os.path.join(self.decrypted_dir, "audio.mp4")
492
+ if os.path.exists(encrypted_path) and not os.path.exists(decrypted_path):
493
+ shutil.copy2(encrypted_path, decrypted_path)
494
+
495
+ else:
496
+ self.error = "No audio found"
497
+ console.print(f"[red]{self.error}[/red]")
498
+ return False
499
+
500
+ return True
387
501
 
388
502
  def finalize_output(self):
389
503
  """
@@ -415,8 +529,8 @@ class DASH_Downloader:
415
529
  console.print("[red]Video file missing, cannot export[/red]")
416
530
  return None
417
531
 
418
- # Merge subtitles if enabled and available
419
- if MERGE_SUBTITLE and ENABLE_SUBTITLE and self.selected_subs:
532
+ # Merge subtitles if available
533
+ if MERGE_SUBTITLE and self.selected_subs:
420
534
 
421
535
  # Check which subtitle files actually exist
422
536
  existing_sub_tracks = []
@@ -455,7 +569,7 @@ class DASH_Downloader:
455
569
 
456
570
  # Handle failed sync case
457
571
  if use_shortest:
458
- new_filename = output_file.replace(".mp4", "_failed_sync.mp4")
572
+ new_filename = output_file.replace(EXTENSION_OUTPUT, f"_failed_sync{EXTENSION_OUTPUT}")
459
573
  if os.path.exists(output_file):
460
574
  os.rename(output_file, new_filename)
461
575
  output_file = new_filename
@@ -465,20 +579,7 @@ class DASH_Downloader:
465
579
  if os.path.exists(output_file):
466
580
  file_size = internet_manager.format_file_size(os.path.getsize(output_file))
467
581
  duration = print_duration_table(output_file, description=False, return_string=True)
468
-
469
- panel_content = (
470
- f"[cyan]File size: [bold red]{file_size}[/bold red]\n"
471
- f"[cyan]Duration: [bold]{duration}[/bold]\n"
472
- f"[cyan]Output: [bold]{os.path.abspath(output_file)}[/bold]"
473
- )
474
-
475
- print("")
476
- console.print(Panel(
477
- panel_content,
478
- title=f"{os.path.basename(output_file.replace('.mp4', ''))}",
479
- border_style="green"
480
- ))
481
-
582
+ console.print(f"[yellow]Output [red]{os.path.abspath(output_file)} [cyan]with size [red]{file_size} [cyan]and duration [red]{duration}")
482
583
  else:
483
584
  console.print(f"[red]Output file not found: {output_file}")
484
585
 
@@ -514,4 +615,19 @@ class DASH_Downloader:
514
615
  "path": self.output_file,
515
616
  "error": self.error,
516
617
  "stopped": self.stopped
517
- }
618
+ }
619
+
620
+ def get_progress_data(self) -> Optional[Dict]:
621
+ """Get current download progress data."""
622
+ if not self.current_downloader:
623
+ return None
624
+
625
+ try:
626
+ progress = self.current_downloader.get_progress_data()
627
+ if progress:
628
+ progress['download_type'] = self.current_download_type
629
+ return progress
630
+
631
+ except Exception as e:
632
+ logging.error(f"Error getting progress data: {e}")
633
+ return None
@@ -1,6 +1,7 @@
1
1
  # 25.07.25
2
2
 
3
3
  import re
4
+ import logging
4
5
  from urllib.parse import urljoin
5
6
  import xml.etree.ElementTree as ET
6
7
  from typing import List, Dict, Optional, Tuple, Any
@@ -456,6 +457,7 @@ class MPDParser:
456
457
  )
457
458
 
458
459
  response.raise_for_status()
460
+ logging.info(f"Successfully fetched MPD: {response.content}")
459
461
  self.root = ET.fromstring(response.content)
460
462
  break
461
463
 
@@ -480,9 +482,6 @@ class MPDParser:
480
482
  self.pssh = pssh_element.text
481
483
  break
482
484
 
483
- if not self.pssh:
484
- console.print("[bold red]PSSH not found in MPD![/bold red]")
485
-
486
485
  def _parse_representations(self) -> None:
487
486
  """Parse all representations from the MPD"""
488
487
  base_url = self._get_initial_base_url()