StreamingCommunity 3.3.8__py3-none-any.whl → 3.4.0__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 (64) 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 -15
  7. StreamingCommunity/Api/Site/altadefinizione/site.py +2 -7
  8. StreamingCommunity/Api/Site/altadefinizione/util/ScrapeSerie.py +2 -7
  9. StreamingCommunity/Api/Site/animeunity/site.py +9 -24
  10. StreamingCommunity/Api/Site/animeunity/util/ScrapeSerie.py +11 -27
  11. StreamingCommunity/Api/Site/animeworld/film.py +4 -2
  12. StreamingCommunity/Api/Site/animeworld/site.py +3 -11
  13. StreamingCommunity/Api/Site/animeworld/util/ScrapeSerie.py +1 -4
  14. StreamingCommunity/Api/Site/crunchyroll/film.py +17 -8
  15. StreamingCommunity/Api/Site/crunchyroll/series.py +8 -9
  16. StreamingCommunity/Api/Site/crunchyroll/site.py +14 -16
  17. StreamingCommunity/Api/Site/crunchyroll/util/ScrapeSerie.py +18 -65
  18. StreamingCommunity/Api/Site/crunchyroll/util/get_license.py +97 -106
  19. StreamingCommunity/Api/Site/guardaserie/site.py +4 -12
  20. StreamingCommunity/Api/Site/guardaserie/util/ScrapeSerie.py +3 -10
  21. StreamingCommunity/Api/Site/mediasetinfinity/film.py +11 -12
  22. StreamingCommunity/Api/Site/mediasetinfinity/series.py +1 -2
  23. StreamingCommunity/Api/Site/mediasetinfinity/site.py +3 -11
  24. StreamingCommunity/Api/Site/mediasetinfinity/util/ScrapeSerie.py +39 -50
  25. StreamingCommunity/Api/Site/mediasetinfinity/util/fix_mpd.py +3 -3
  26. StreamingCommunity/Api/Site/mediasetinfinity/util/get_license.py +8 -26
  27. StreamingCommunity/Api/Site/raiplay/film.py +6 -7
  28. StreamingCommunity/Api/Site/raiplay/series.py +1 -12
  29. StreamingCommunity/Api/Site/raiplay/site.py +8 -24
  30. StreamingCommunity/Api/Site/raiplay/util/ScrapeSerie.py +15 -22
  31. StreamingCommunity/Api/Site/raiplay/util/get_license.py +3 -12
  32. StreamingCommunity/Api/Site/streamingcommunity/film.py +5 -16
  33. StreamingCommunity/Api/Site/streamingcommunity/site.py +3 -22
  34. StreamingCommunity/Api/Site/streamingcommunity/util/ScrapeSerie.py +11 -26
  35. StreamingCommunity/Api/Site/streamingwatch/__init__.py +1 -0
  36. StreamingCommunity/Api/Site/streamingwatch/film.py +4 -2
  37. StreamingCommunity/Api/Site/streamingwatch/series.py +1 -1
  38. StreamingCommunity/Api/Site/streamingwatch/site.py +4 -18
  39. StreamingCommunity/Api/Site/streamingwatch/util/ScrapeSerie.py +0 -3
  40. StreamingCommunity/Api/Template/config_loader.py +0 -7
  41. StreamingCommunity/Lib/Downloader/DASH/cdm_helpher.py +8 -3
  42. StreamingCommunity/Lib/Downloader/DASH/decrypt.py +55 -1
  43. StreamingCommunity/Lib/Downloader/DASH/downloader.py +139 -55
  44. StreamingCommunity/Lib/Downloader/DASH/parser.py +458 -101
  45. StreamingCommunity/Lib/Downloader/DASH/segments.py +131 -74
  46. StreamingCommunity/Lib/Downloader/HLS/downloader.py +31 -50
  47. StreamingCommunity/Lib/Downloader/HLS/segments.py +266 -365
  48. StreamingCommunity/Lib/Downloader/MP4/downloader.py +1 -1
  49. StreamingCommunity/Lib/FFmpeg/capture.py +37 -5
  50. StreamingCommunity/Lib/FFmpeg/command.py +35 -93
  51. StreamingCommunity/Lib/M3U8/estimator.py +0 -1
  52. StreamingCommunity/Lib/TMBD/tmdb.py +2 -4
  53. StreamingCommunity/TelegramHelp/config.json +0 -1
  54. StreamingCommunity/Upload/version.py +1 -1
  55. StreamingCommunity/Util/config_json.py +28 -21
  56. StreamingCommunity/Util/http_client.py +28 -0
  57. StreamingCommunity/Util/os.py +16 -6
  58. {streamingcommunity-3.3.8.dist-info → streamingcommunity-3.4.0.dist-info}/METADATA +1 -3
  59. streamingcommunity-3.4.0.dist-info/RECORD +111 -0
  60. streamingcommunity-3.3.8.dist-info/RECORD +0 -111
  61. {streamingcommunity-3.3.8.dist-info → streamingcommunity-3.4.0.dist-info}/WHEEL +0 -0
  62. {streamingcommunity-3.3.8.dist-info → streamingcommunity-3.4.0.dist-info}/entry_points.txt +0 -0
  63. {streamingcommunity-3.3.8.dist-info → streamingcommunity-3.4.0.dist-info}/licenses/LICENSE +0 -0
  64. {streamingcommunity-3.3.8.dist-info → streamingcommunity-3.4.0.dist-info}/top_level.txt +0 -0
@@ -5,21 +5,15 @@ import logging
5
5
 
6
6
 
7
7
  # External libraries
8
- import httpx
9
8
  from bs4 import BeautifulSoup
10
9
 
11
10
 
12
11
  # Internal utilities
13
- from StreamingCommunity.Util.headers import get_userAgent
14
- from StreamingCommunity.Util.config_json import config_manager
12
+ from StreamingCommunity.Util.headers import get_headers
13
+ from StreamingCommunity.Util.http_client import create_client
15
14
  from StreamingCommunity.Api.Player.Helper.Vixcloud.util import SeasonManager
16
15
 
17
16
 
18
- # Variable
19
- max_timeout = config_manager.get_int("REQUESTS", "timeout")
20
- ssl_verify = config_manager.get_bool("REQUESTS", "verify")
21
-
22
-
23
17
  class GetSerieInfo:
24
18
  def __init__(self, url, media_id: int = None, series_name: str = None):
25
19
  """
@@ -31,7 +25,7 @@ class GetSerieInfo:
31
25
  - series_name (str, optional): Name of the TV series
32
26
  """
33
27
  self.is_series = False
34
- self.headers = {'user-agent': get_userAgent()}
28
+ self.headers = get_headers()
35
29
  self.url = url
36
30
  self.media_id = media_id
37
31
  self.seasons_manager = SeasonManager()
@@ -48,12 +42,7 @@ class GetSerieInfo:
48
42
  Exception: If there's an error fetching series information
49
43
  """
50
44
  try:
51
- response = httpx.get(
52
- url=f"{self.url}/titles/{self.media_id}-{self.series_name}",
53
- headers=self.headers,
54
- timeout=max_timeout,
55
- verify=ssl_verify
56
- )
45
+ response = create_client(headers=self.headers).get(f"{self.url}/titles/{self.media_id}-{self.series_name}")
57
46
  response.raise_for_status()
58
47
 
59
48
  # Extract series info from JSON response
@@ -98,17 +87,13 @@ class GetSerieInfo:
98
87
  if not season:
99
88
  logging.error(f"Season {number_season} not found")
100
89
  return
101
-
102
- response = httpx.get(
103
- url=f'{self.url}/titles/{self.media_id}-{self.series_name}/season-{number_season}',
104
- headers={
105
- 'User-Agent': self.headers['user-agent'],
106
- 'x-inertia': 'true',
107
- 'x-inertia-version': self.version,
108
- },
109
- timeout=max_timeout,
110
- verify=ssl_verify
111
- )
90
+
91
+ custom_headers = self.headers.copy()
92
+ custom_headers.update({
93
+ 'x-inertia': 'true',
94
+ 'x-inertia-version': self.version,
95
+ })
96
+ response = create_client(headers=custom_headers).get(f"{self.url}/titles/{self.media_id}-{self.series_name}/season-{number_season}")
112
97
 
113
98
  # Extract episodes from JSON response
114
99
  json_response = response.json().get('props', {}).get('loadedSeason', {}).get('episodes', [])
@@ -3,6 +3,7 @@
3
3
  import sys
4
4
  import subprocess
5
5
 
6
+
6
7
  # External library
7
8
  from rich.console import Console
8
9
  from rich.prompt import Prompt
@@ -9,6 +9,7 @@ from rich.console import Console
9
9
 
10
10
  # Internal utilities
11
11
  from StreamingCommunity.Util.os import os_manager
12
+ from StreamingCommunity.Util.config_json import config_manager
12
13
  from StreamingCommunity.Util.message import start_message
13
14
 
14
15
 
@@ -24,6 +25,7 @@ from StreamingCommunity.Api.Player.hdplayer import VideoSource
24
25
 
25
26
  # Variable
26
27
  console = Console()
28
+ extension_output = config_manager.get("M3U8_CONVERSION", "extension")
27
29
 
28
30
 
29
31
  def download_film(select_title: MediaItem) -> str:
@@ -45,8 +47,8 @@ def download_film(select_title: MediaItem) -> str:
45
47
  master_playlist = video_source.get_m3u8_url(select_title.url)
46
48
 
47
49
  # Define the filename and path for the downloaded film
48
- title_name = os_manager.get_sanitize_file(select_title.name) + ".mp4"
49
- mp4_path = os.path.join(site_constant.MOVIE_FOLDER, title_name.replace(".mp4", ""))
50
+ title_name = os_manager.get_sanitize_file(select_title.name, select_title.date) + extension_output
51
+ mp4_path = os.path.join(site_constant.MOVIE_FOLDER, title_name.replace(extension_output, ""))
50
52
 
51
53
  # Download the film using the m3u8 playlist, and output filename
52
54
  hls_process = HLS_Downloader(
@@ -53,7 +53,7 @@ def download_video(index_season_selected: int, index_episode_selected: int, scra
53
53
 
54
54
  # Get episode information
55
55
  obj_episode = scrape_serie.selectEpisode(index_season_selected, index_episode_selected-1)
56
- console.print(f"\n[/bold yellow] [red]{site_constant.SITE_NAME}[/red] → [cyan]{scrape_serie.series_name}[/cyan] \\ [bold magenta]{obj_episode.name}[/bold magenta] ([cyan]S{index_season_selected}E{index_episode_selected}[/cyan]) \n")
56
+ console.print(f"\n[bold yellow]Download:[/bold yellow] [red]{site_constant.SITE_NAME}[/red] → [cyan]{scrape_serie.series_name}[/cyan] \\ [bold magenta]{obj_episode.name}[/bold magenta] ([cyan]S{index_season_selected}E{index_episode_selected}[/cyan]) \n")
57
57
 
58
58
  # Define filename and path for the downloaded video
59
59
  mp4_name = f"{map_episode_title(scrape_serie.series_name, index_season_selected, index_episode_selected, obj_episode.name)}.mp4"
@@ -4,13 +4,12 @@ import re
4
4
 
5
5
 
6
6
  # External libraries
7
- import httpx
8
7
  from bs4 import BeautifulSoup
9
8
  from rich.console import Console
10
9
 
11
10
 
12
11
  # Internal utilities
13
- from StreamingCommunity.Util.config_json import config_manager
12
+ from StreamingCommunity.Util.http_client import create_client
14
13
  from StreamingCommunity.Util.headers import get_userAgent
15
14
  from StreamingCommunity.Util.table import TVShowManager
16
15
 
@@ -24,18 +23,13 @@ from StreamingCommunity.Api.Template.Class.SearchType import MediaManager
24
23
  console = Console()
25
24
  media_search_manager = MediaManager()
26
25
  table_show_manager = TVShowManager()
27
- max_timeout = config_manager.get_int("REQUESTS", "timeout")
28
26
 
29
27
 
30
28
  def extract_nonce() -> str:
31
29
  """Extract nonce value from the page script"""
32
- response = httpx.get(
33
- site_constant.FULL_URL,
34
- headers={'user-agent': get_userAgent()},
35
- timeout=max_timeout
36
- )
37
-
30
+ response = create_client(headers={'user-agent': get_userAgent()}).get(site_constant.FULL_URL)
38
31
  soup = BeautifulSoup(response.content, 'html.parser')
32
+
39
33
  script = soup.find('script', id='live-search-js-extra')
40
34
  if script:
41
35
  match = re.search(r'"admin_ajax_nonce":"([^"]+)"', script.text)
@@ -73,15 +67,7 @@ def title_search(query: str) -> int:
73
67
  '_wpnonce': _wpnonce
74
68
  }
75
69
 
76
- response = httpx.post(
77
- search_url,
78
- headers={
79
- 'origin': site_constant.FULL_URL,
80
- 'user-agent': get_userAgent()
81
- },
82
- data=data,
83
- timeout=max_timeout
84
- )
70
+ response = create_client(headers={'origin': site_constant.FULL_URL, 'user-agent': get_userAgent()}).post(search_url, data=data)
85
71
  response.raise_for_status()
86
72
  soup = BeautifulSoup(response.text, 'html.parser')
87
73
 
@@ -11,12 +11,9 @@ from bs4 import BeautifulSoup
11
11
  # Internal utilities
12
12
  from StreamingCommunity.Util.headers import get_userAgent
13
13
  from StreamingCommunity.Util.http_client import create_client
14
- from StreamingCommunity.Util.config_json import config_manager
15
14
  from StreamingCommunity.Api.Player.Helper.Vixcloud.util import SeasonManager, Episode
16
15
 
17
16
 
18
- # Variable
19
- max_timeout = config_manager.get_int("REQUESTS", "timeout")
20
17
 
21
18
 
22
19
  class GetSerieInfo:
@@ -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')
@@ -5,7 +5,7 @@ import logging
5
5
 
6
6
 
7
7
  # External libraries
8
- import httpx
8
+ from curl_cffi import requests
9
9
  from rich.console import Console
10
10
  from pywidevine.cdm import Cdm
11
11
  from pywidevine.device import Device
@@ -39,8 +39,13 @@ def get_widevine_keys(pssh, license_url, cdm_device_path, headers=None, payload=
39
39
  req_headers = headers or {}
40
40
  req_headers['Content-Type'] = 'application/octet-stream'
41
41
 
42
- # Send license request
43
- response = httpx.post(license_url, data=challenge, headers=req_headers, content=payload)
42
+ # Send license request using curl_cffi
43
+ try:
44
+ # response = httpx.post(license_url, data=challenge, headers=req_headers, content=payload)
45
+ response = requests.post(license_url, data=challenge, headers=req_headers, json=payload, impersonate="chrome124")
46
+ except Exception as e:
47
+ console.print(f"[bold red]Request error:[/bold red] {e}")
48
+ return None
44
49
 
45
50
  if response.status_code != 200:
46
51
  console.print(f"[bold red]License error:[/bold red] {response.status_code}, {response.text}")
@@ -3,24 +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
- def decrypt_with_mp4decrypt(encrypted_path, kid, key, output_path=None, cleanup=True):
23
+ # NOTE!: SAREBBE MEGLIO FARLO PER OGNI FILE DURANTE IL DOWNLOAD ... MA PER ORA LO LASCIO COSI
24
+ def decrypt_with_mp4decrypt(type, encrypted_path, kid, key, output_path=None, cleanup=True):
20
25
  """
21
26
  Decrypt an mp4/m4s file using mp4decrypt.
22
27
 
23
28
  Args:
29
+ type (str): Type of file ('video' or 'audio').
24
30
  encrypted_path (str): Path to encrypted file.
25
31
  kid (str): Hexadecimal KID.
26
32
  key (str): Hexadecimal key.
@@ -47,15 +53,63 @@ def decrypt_with_mp4decrypt(encrypted_path, kid, key, output_path=None, cleanup=
47
53
  if not output_path:
48
54
  output_path = os.path.splitext(encrypted_path)[0] + "_decrypted.mp4"
49
55
 
56
+ # Get file size for progress tracking
57
+ file_size = os.path.getsize(encrypted_path)
58
+
50
59
  key_format = f"{kid.lower()}:{key.lower()}"
51
60
  cmd = [get_mp4decrypt_path(), "--key", key_format, encrypted_path, output_path]
52
61
  logging.info(f"Running command: {' '.join(cmd)}")
53
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
+
54
102
  try:
55
103
  result = subprocess.run(cmd, capture_output=True, text=True, timeout=300)
56
104
  except Exception as e:
105
+ progress_bar.close()
57
106
  console.print(f"[bold red] mp4decrypt execution failed: {e}[/bold red]")
58
107
  return None
108
+
109
+ # Ensure progress bar reaches 100%
110
+ progress_bar.n = 100
111
+ progress_bar.refresh()
112
+ progress_bar.close()
59
113
 
60
114
  if result.returncode == 0 and os.path.exists(output_path):
61
115
 
@@ -1,7 +1,6 @@
1
1
  # 25.07.25
2
2
 
3
3
  import os
4
- import time
5
4
  import shutil
6
5
 
7
6
 
@@ -13,7 +12,7 @@ from rich.table import Table
13
12
 
14
13
  # Internal utilities
15
14
  from StreamingCommunity.Util.config_json import config_manager
16
- from StreamingCommunity.Util.os import internet_manager
15
+ from StreamingCommunity.Util.os import os_manager, internet_manager, get_wvd_path
17
16
  from StreamingCommunity.Util.http_client import create_client
18
17
  from StreamingCommunity.Util.headers import get_userAgent
19
18
 
@@ -32,11 +31,11 @@ from ...FFmpeg import print_duration_table, join_audios, join_video, join_subtit
32
31
  # Config
33
32
  DOWNLOAD_SPECIFIC_AUDIO = config_manager.get_list('M3U8_DOWNLOAD', 'specific_list_audio')
34
33
  DOWNLOAD_SPECIFIC_SUBTITLE = config_manager.get_list('M3U8_DOWNLOAD', 'specific_list_subtitles')
35
- ENABLE_SUBTITLE = config_manager.get_bool('M3U8_DOWNLOAD', 'download_subtitle')
36
34
  MERGE_SUBTITLE = config_manager.get_bool('M3U8_DOWNLOAD', 'merge_subs')
37
35
  FILTER_CUSTOM_REOLUTION = str(config_manager.get('M3U8_CONVERSION', 'force_resolution')).strip().lower()
38
36
  CLEANUP_TMP = config_manager.get_bool('M3U8_DOWNLOAD', 'cleanup_tmp_folder')
39
37
  RETRY_LIMIT = config_manager.get_int('REQUESTS', 'max_retry')
38
+ EXTENSION_OUTPUT = config_manager.get("M3U8_CONVERSION", "extension")
40
39
 
41
40
 
42
41
  # Variable
@@ -44,25 +43,31 @@ console = Console()
44
43
 
45
44
 
46
45
  class DASH_Downloader:
47
- def __init__(self, cdm_device, license_url, mpd_url, mpd_sub_list: list = None, output_path: str = None):
46
+ def __init__(self, license_url, mpd_url, mpd_sub_list: list = None, output_path: str = None):
48
47
  """
49
48
  Initialize the DASH Downloader with necessary parameters.
50
49
 
51
50
  Parameters:
52
- - cdm_device (str): Path to the CDM device for decryption.
53
51
  - license_url (str): URL to obtain the license for decryption.
54
52
  - mpd_url (str): URL of the MPD manifest file.
55
53
  - mpd_sub_list (list): List of subtitle dicts with keys: 'language', 'url', 'format'.
56
54
  - output_path (str): Path to save the final output file.
57
55
  """
58
- self.cdm_device = cdm_device
56
+ self.cdm_device = get_wvd_path()
59
57
  self.license_url = license_url
60
58
  self.mpd_url = mpd_url
61
59
  self.mpd_sub_list = mpd_sub_list or []
62
- self.out_path = os.path.splitext(os.path.abspath(str(output_path)))[0]
60
+ self.out_path = os.path.splitext(os.path.abspath(os_manager.get_sanitize_path(output_path)))[0]
63
61
  self.original_output_path = output_path
64
62
  self.file_already_exists = os.path.exists(self.original_output_path)
65
63
  self.parser = None
64
+
65
+ # Added defaults to avoid AttributeError when no subtitles/audio/video are present
66
+ # Non la soluzione migliore ma evita crash in assenza di audio/video/subs
67
+ self.selected_subs = []
68
+ self.selected_video = None
69
+ self.selected_audio = None
70
+
66
71
  self._setup_temp_dirs()
67
72
 
68
73
  self.error = None
@@ -189,49 +194,28 @@ class DASH_Downloader:
189
194
  Download subtitle files based on configuration with retry mechanism.
190
195
  Returns True if successful or if no subtitles to download, False on critical error.
191
196
  """
192
- if not ENABLE_SUBTITLE or not self.selected_subs:
193
- return True
194
-
195
- headers = {'User-Agent': get_userAgent()}
196
- client = create_client(headers=headers)
197
+ client = create_client(headers={'User-Agent': get_userAgent()})
197
198
 
198
199
  for sub in self.selected_subs:
199
- language = sub.get('language', 'unknown')
200
- url = sub.get('url')
201
- fmt = sub.get('format', 'vtt')
202
-
203
- if not url:
204
- console.print(f"[yellow]Warning: No URL for subtitle {language}[/yellow]")
205
- continue
206
-
207
- # Retry mechanism for downloading subtitles
208
- success = False
209
- for attempt in range(RETRY_LIMIT):
210
- try:
211
- # Download subtitle
212
- response = client.get(url)
213
- response.raise_for_status()
214
-
215
- # Save subtitle file
216
- sub_filename = f"{language}.{fmt}"
217
- sub_path = os.path.join(self.subs_dir, sub_filename)
218
-
219
- with open(sub_path, 'wb') as f:
220
- f.write(response.content)
221
-
222
- success = True
223
- break
200
+ try:
201
+ language = sub.get('language', 'unknown')
202
+ fmt = sub.get('format', 'vtt')
203
+
204
+ # Download subtitle
205
+ response = client.get(sub.get('url'))
206
+ response.raise_for_status()
207
+
208
+ # Save subtitle file and make request
209
+ sub_filename = f"{language}.{fmt}"
210
+ sub_path = os.path.join(self.subs_dir, sub_filename)
211
+
212
+ with open(sub_path, 'wb') as f:
213
+ f.write(response.content)
224
214
 
225
- except Exception as e:
226
- if attempt < RETRY_LIMIT - 1:
227
- console.print(f"[yellow]Attempt {attempt + 1}/{RETRY_LIMIT} failed for subtitle {language}: {e}. Retrying...[/yellow]")
228
- time.sleep(1.5 ** attempt)
229
- else:
230
- console.print(f"[yellow]Warning: Failed to download subtitle {language} after {RETRY_LIMIT} attempts: {e}[/yellow]")
215
+ except Exception as e:
216
+ console.print(f"[red]Error downloading subtitle {language}: {e}[/red]")
217
+ return False
231
218
 
232
- if not success:
233
- continue
234
-
235
219
  return True
236
220
 
237
221
  def download_and_decrypt(self, custom_headers=None, custom_payload=None):
@@ -249,7 +233,6 @@ class DASH_Downloader:
249
233
 
250
234
  # Fetch keys immediately after obtaining PSSH
251
235
  if not self.parser.pssh:
252
- console.print("[red]No PSSH found: segments are not encrypted, skipping decryption.")
253
236
  self.download_segments(clear=True)
254
237
  return True
255
238
 
@@ -309,7 +292,7 @@ class DASH_Downloader:
309
292
  # Decrypt video
310
293
  decrypted_path = os.path.join(self.decrypted_dir, "video.mp4")
311
294
  result_path = decrypt_with_mp4decrypt(
312
- encrypted_path, KID, KEY, output_path=decrypted_path
295
+ "Video", encrypted_path, KID, KEY, output_path=decrypted_path
313
296
  )
314
297
 
315
298
  if not result_path:
@@ -358,7 +341,7 @@ class DASH_Downloader:
358
341
  # Decrypt audio
359
342
  decrypted_path = os.path.join(self.decrypted_dir, "audio.mp4")
360
343
  result_path = decrypt_with_mp4decrypt(
361
- encrypted_path, KID, KEY, output_path=decrypted_path
344
+ f"Audio {audio_language}", encrypted_path, KID, KEY, output_path=decrypted_path
362
345
  )
363
346
 
364
347
  if not result_path:
@@ -374,9 +357,110 @@ class DASH_Downloader:
374
357
  return True
375
358
 
376
359
  def download_segments(self, clear=False):
377
- # Download segments and concatenate them
378
- # clear=True: no decryption needed
379
- pass
360
+ """
361
+ Download video/audio segments without decryption (for clear content).
362
+
363
+ Parameters:
364
+ clear (bool): If True, content is not encrypted and doesn't need decryption
365
+ """
366
+ if not clear:
367
+ console.print("[yellow]Warning: download_segments called with clear=False[/yellow]")
368
+ return False
369
+
370
+ video_segments_count = 0
371
+
372
+ # Download subtitles
373
+ self.download_subtitles()
374
+
375
+ # Download video
376
+ video_rep = self.get_representation_by_type("video")
377
+ if video_rep:
378
+ encrypted_path = os.path.join(self.encrypted_dir, f"{video_rep['id']}_encrypted.m4s")
379
+
380
+ # If m4s file doesn't exist, start downloading
381
+ if not os.path.exists(encrypted_path):
382
+ video_downloader = MPD_Segments(
383
+ tmp_folder=self.encrypted_dir,
384
+ representation=video_rep,
385
+ pssh=self.parser.pssh
386
+ )
387
+
388
+ try:
389
+ result = video_downloader.download_streams(description="Video")
390
+
391
+ # Store the video segment count for limiting audio
392
+ video_segments_count = video_downloader.get_segments_count()
393
+
394
+ # Check for interruption or failure
395
+ if result.get("stopped"):
396
+ self.stopped = True
397
+ self.error = "Download interrupted"
398
+ return False
399
+
400
+ if result.get("nFailed", 0) > 0:
401
+ self.error = f"Failed segments: {result['nFailed']}"
402
+ return False
403
+
404
+ except Exception as ex:
405
+ self.error = str(ex)
406
+ console.print(f"[red]Error downloading video: {ex}[/red]")
407
+ return False
408
+
409
+ # NO DECRYPTION: just copy/move to decrypted folder
410
+ decrypted_path = os.path.join(self.decrypted_dir, "video.mp4")
411
+ if os.path.exists(encrypted_path) and not os.path.exists(decrypted_path):
412
+ shutil.copy2(encrypted_path, decrypted_path)
413
+
414
+ else:
415
+ self.error = "No video found"
416
+ console.print(f"[red]{self.error}[/red]")
417
+ return False
418
+
419
+ # Download audio with segment limiting
420
+ audio_rep = self.get_representation_by_type("audio")
421
+ if audio_rep:
422
+ encrypted_path = os.path.join(self.encrypted_dir, f"{audio_rep['id']}_encrypted.m4s")
423
+
424
+ # If m4s file doesn't exist, start downloading
425
+ if not os.path.exists(encrypted_path):
426
+ audio_language = audio_rep.get('language', 'Unknown')
427
+
428
+ audio_downloader = MPD_Segments(
429
+ tmp_folder=self.encrypted_dir,
430
+ representation=audio_rep,
431
+ pssh=self.parser.pssh,
432
+ limit_segments=video_segments_count if video_segments_count > 0 else None
433
+ )
434
+
435
+ try:
436
+ result = audio_downloader.download_streams(description=f"Audio {audio_language}")
437
+
438
+ # Check for interruption or failure
439
+ if result.get("stopped"):
440
+ self.stopped = True
441
+ self.error = "Download interrupted"
442
+ return False
443
+
444
+ if result.get("nFailed", 0) > 0:
445
+ self.error = f"Failed segments: {result['nFailed']}"
446
+ return False
447
+
448
+ except Exception as ex:
449
+ self.error = str(ex)
450
+ console.print(f"[red]Error downloading audio: {ex}[/red]")
451
+ return False
452
+
453
+ # NO DECRYPTION: just copy/move to decrypted folder
454
+ decrypted_path = os.path.join(self.decrypted_dir, "audio.mp4")
455
+ if os.path.exists(encrypted_path) and not os.path.exists(decrypted_path):
456
+ shutil.copy2(encrypted_path, decrypted_path)
457
+
458
+ else:
459
+ self.error = "No audio found"
460
+ console.print(f"[red]{self.error}[/red]")
461
+ return False
462
+
463
+ return True
380
464
 
381
465
  def finalize_output(self):
382
466
  """
@@ -408,8 +492,8 @@ class DASH_Downloader:
408
492
  console.print("[red]Video file missing, cannot export[/red]")
409
493
  return None
410
494
 
411
- # Merge subtitles if enabled and available
412
- if MERGE_SUBTITLE and ENABLE_SUBTITLE and self.selected_subs:
495
+ # Merge subtitles if available
496
+ if MERGE_SUBTITLE and self.selected_subs:
413
497
 
414
498
  # Check which subtitle files actually exist
415
499
  existing_sub_tracks = []
@@ -448,7 +532,7 @@ class DASH_Downloader:
448
532
 
449
533
  # Handle failed sync case
450
534
  if use_shortest:
451
- new_filename = output_file.replace(".mp4", "_failed_sync.mp4")
535
+ new_filename = output_file.replace(EXTENSION_OUTPUT, f"_failed_sync{EXTENSION_OUTPUT}")
452
536
  if os.path.exists(output_file):
453
537
  os.rename(output_file, new_filename)
454
538
  output_file = new_filename