StreamingCommunity 3.2.1__py3-none-any.whl → 3.2.7__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 (67) hide show
  1. StreamingCommunity/Api/Player/Helper/Vixcloud/util.py +4 -0
  2. StreamingCommunity/Api/Player/hdplayer.py +2 -2
  3. StreamingCommunity/Api/Player/mixdrop.py +1 -1
  4. StreamingCommunity/Api/Player/vixcloud.py +4 -5
  5. StreamingCommunity/Api/Site/altadefinizione/film.py +2 -2
  6. StreamingCommunity/Api/Site/altadefinizione/series.py +1 -1
  7. StreamingCommunity/Api/Site/animeunity/serie.py +1 -1
  8. StreamingCommunity/Api/Site/animeworld/film.py +1 -1
  9. StreamingCommunity/Api/Site/animeworld/serie.py +1 -2
  10. StreamingCommunity/Api/Site/cb01new/film.py +1 -1
  11. StreamingCommunity/Api/Site/crunchyroll/__init__.py +103 -0
  12. StreamingCommunity/Api/Site/crunchyroll/film.py +82 -0
  13. StreamingCommunity/Api/Site/crunchyroll/series.py +186 -0
  14. StreamingCommunity/Api/Site/crunchyroll/site.py +113 -0
  15. StreamingCommunity/Api/Site/crunchyroll/util/ScrapeSerie.py +238 -0
  16. StreamingCommunity/Api/Site/crunchyroll/util/get_license.py +227 -0
  17. StreamingCommunity/Api/Site/guardaserie/series.py +1 -2
  18. StreamingCommunity/Api/Site/guardaserie/site.py +1 -2
  19. StreamingCommunity/Api/Site/guardaserie/util/ScrapeSerie.py +9 -8
  20. StreamingCommunity/Api/Site/mediasetinfinity/__init__.py +96 -0
  21. StreamingCommunity/Api/Site/mediasetinfinity/film.py +85 -0
  22. StreamingCommunity/Api/Site/mediasetinfinity/series.py +185 -0
  23. StreamingCommunity/Api/Site/mediasetinfinity/site.py +112 -0
  24. StreamingCommunity/Api/Site/mediasetinfinity/util/ScrapeSerie.py +259 -0
  25. StreamingCommunity/Api/Site/mediasetinfinity/util/fix_mpd.py +64 -0
  26. StreamingCommunity/Api/Site/mediasetinfinity/util/get_license.py +214 -0
  27. StreamingCommunity/Api/Site/raiplay/film.py +2 -2
  28. StreamingCommunity/Api/Site/raiplay/series.py +2 -1
  29. StreamingCommunity/Api/Site/streamingcommunity/__init__.py +6 -17
  30. StreamingCommunity/Api/Site/streamingcommunity/film.py +3 -3
  31. StreamingCommunity/Api/Site/streamingcommunity/series.py +11 -11
  32. StreamingCommunity/Api/Site/streamingcommunity/site.py +2 -4
  33. StreamingCommunity/Api/Site/streamingcommunity/util/ScrapeSerie.py +3 -6
  34. StreamingCommunity/Api/Site/streamingwatch/__init__.py +6 -14
  35. StreamingCommunity/Api/Site/streamingwatch/film.py +3 -3
  36. StreamingCommunity/Api/Site/streamingwatch/series.py +9 -9
  37. StreamingCommunity/Api/Site/streamingwatch/site.py +5 -7
  38. StreamingCommunity/Api/Site/streamingwatch/util/ScrapeSerie.py +2 -2
  39. StreamingCommunity/Lib/Downloader/DASH/cdm_helpher.py +131 -0
  40. StreamingCommunity/Lib/Downloader/DASH/decrypt.py +79 -0
  41. StreamingCommunity/Lib/Downloader/DASH/downloader.py +218 -0
  42. StreamingCommunity/Lib/Downloader/DASH/parser.py +249 -0
  43. StreamingCommunity/Lib/Downloader/DASH/segments.py +332 -0
  44. StreamingCommunity/Lib/Downloader/HLS/downloader.py +10 -30
  45. StreamingCommunity/Lib/Downloader/HLS/segments.py +146 -263
  46. StreamingCommunity/Lib/Downloader/MP4/downloader.py +0 -5
  47. StreamingCommunity/Lib/FFmpeg/capture.py +3 -3
  48. StreamingCommunity/Lib/FFmpeg/command.py +1 -1
  49. StreamingCommunity/TelegramHelp/config.json +3 -7
  50. StreamingCommunity/Upload/version.py +1 -1
  51. StreamingCommunity/Util/bento4_installer.py +191 -0
  52. StreamingCommunity/Util/config_json.py +1 -1
  53. StreamingCommunity/Util/headers.py +0 -3
  54. StreamingCommunity/Util/os.py +36 -46
  55. StreamingCommunity/__init__.py +2 -1
  56. StreamingCommunity/run.py +11 -10
  57. {streamingcommunity-3.2.1.dist-info → streamingcommunity-3.2.7.dist-info}/METADATA +7 -9
  58. streamingcommunity-3.2.7.dist-info/RECORD +111 -0
  59. StreamingCommunity/Api/Site/1337xx/__init__.py +0 -72
  60. StreamingCommunity/Api/Site/1337xx/site.py +0 -82
  61. StreamingCommunity/Api/Site/1337xx/title.py +0 -61
  62. StreamingCommunity/Lib/Proxies/proxy.py +0 -72
  63. streamingcommunity-3.2.1.dist-info/RECORD +0 -96
  64. {streamingcommunity-3.2.1.dist-info → streamingcommunity-3.2.7.dist-info}/WHEEL +0 -0
  65. {streamingcommunity-3.2.1.dist-info → streamingcommunity-3.2.7.dist-info}/entry_points.txt +0 -0
  66. {streamingcommunity-3.2.1.dist-info → streamingcommunity-3.2.7.dist-info}/licenses/LICENSE +0 -0
  67. {streamingcommunity-3.2.1.dist-info → streamingcommunity-3.2.7.dist-info}/top_level.txt +0 -0
@@ -36,7 +36,7 @@ msg = Prompt()
36
36
  console = Console()
37
37
 
38
38
 
39
- def download_video(index_season_selected: int, index_episode_selected: int, scrape_serie: GetSerieInfo, proxy=None) -> Tuple[str,bool]:
39
+ def download_video(index_season_selected: int, index_episode_selected: int, scrape_serie: GetSerieInfo) -> Tuple[str,bool]:
40
40
  """
41
41
  Downloads a specific episode from a specified season.
42
42
 
@@ -60,7 +60,7 @@ def download_video(index_season_selected: int, index_episode_selected: int, scra
60
60
  mp4_path = os.path.join(site_constant.SERIES_FOLDER, scrape_serie.series_name, f"S{index_season_selected}")
61
61
 
62
62
  # Retrieve scws and if available master playlist
63
- video_source = VideoSource(proxy)
63
+ video_source = VideoSource()
64
64
  master_playlist = video_source.get_m3u8_url(obj_episode.url)
65
65
 
66
66
  # Download the episode
@@ -76,7 +76,7 @@ def download_video(index_season_selected: int, index_episode_selected: int, scra
76
76
  return r_proc['path'], r_proc['stopped']
77
77
 
78
78
 
79
- def download_episode(index_season_selected: int, scrape_serie: GetSerieInfo, download_all: bool = False, episode_selection: str = None, proxy = None) -> None:
79
+ def download_episode(index_season_selected: int, scrape_serie: GetSerieInfo, download_all: bool = False, episode_selection: str = None) -> None:
80
80
  """
81
81
  Handle downloading episodes for a specific season.
82
82
 
@@ -92,7 +92,7 @@ def download_episode(index_season_selected: int, scrape_serie: GetSerieInfo, dow
92
92
 
93
93
  if download_all:
94
94
  for i_episode in range(1, episodes_count + 1):
95
- path, stopped = download_video(index_season_selected, i_episode, scrape_serie, proxy)
95
+ path, stopped = download_video(index_season_selected, i_episode, scrape_serie)
96
96
 
97
97
  if stopped:
98
98
  break
@@ -113,12 +113,12 @@ def download_episode(index_season_selected: int, scrape_serie: GetSerieInfo, dow
113
113
 
114
114
  # Download selected episodes if not stopped
115
115
  for i_episode in list_episode_select:
116
- path, stopped = download_video(index_season_selected, i_episode, scrape_serie, proxy)
116
+ path, stopped = download_video(index_season_selected, i_episode, scrape_serie)
117
117
 
118
118
  if stopped:
119
119
  break
120
120
 
121
- def download_series(select_season: MediaItem, season_selection: str = None, episode_selection: str = None, proxy = None) -> None:
121
+ def download_series(select_season: MediaItem, season_selection: str = None, episode_selection: str = None) -> None:
122
122
  """
123
123
  Handle downloading a complete series.
124
124
 
@@ -127,7 +127,7 @@ def download_series(select_season: MediaItem, season_selection: str = None, epis
127
127
  - season_selection (str, optional): Pre-defined season selection that bypasses manual input
128
128
  - episode_selection (str, optional): Pre-defined episode selection that bypasses manual input
129
129
  """
130
- scrape_serie = GetSerieInfo(select_season.url, proxy)
130
+ scrape_serie = GetSerieInfo(select_season.url)
131
131
 
132
132
  # Get total number of seasons
133
133
  seasons_count = scrape_serie.getNumberSeason()
@@ -154,7 +154,7 @@ def download_series(select_season: MediaItem, season_selection: str = None, epis
154
154
  for i_season in list_season_select:
155
155
  if len(list_season_select) > 1 or index_season_selected == "*":
156
156
  # Download all episodes if multiple seasons are selected or if '*' is used
157
- download_episode(i_season, scrape_serie, download_all=True, proxy=proxy)
157
+ download_episode(i_season, scrape_serie, download_all=True)
158
158
  else:
159
159
  # Otherwise, let the user select specific episodes for the single season
160
- download_episode(i_season, scrape_serie, download_all=False, episode_selection=episode_selection, proxy=proxy)
160
+ download_episode(i_season, scrape_serie, download_all=False, episode_selection=episode_selection)
@@ -27,13 +27,12 @@ table_show_manager = TVShowManager()
27
27
  max_timeout = config_manager.get_int("REQUESTS", "timeout")
28
28
 
29
29
 
30
- def extract_nonce(proxy) -> str:
30
+ def extract_nonce() -> str:
31
31
  """Extract nonce value from the page script"""
32
32
  response = httpx.get(
33
33
  site_constant.FULL_URL,
34
34
  headers={'user-agent': get_userAgent()},
35
- timeout=max_timeout,
36
- proxy=proxy
35
+ timeout=max_timeout
37
36
  )
38
37
 
39
38
  soup = BeautifulSoup(response.content, 'html.parser')
@@ -45,7 +44,7 @@ def extract_nonce(proxy) -> str:
45
44
  return ""
46
45
 
47
46
 
48
- def title_search(query: str, proxy: str) -> int:
47
+ def title_search(query: str) -> int:
49
48
  """
50
49
  Search for titles based on a search query.
51
50
 
@@ -62,7 +61,7 @@ def title_search(query: str, proxy: str) -> int:
62
61
  console.print(f"[cyan]Search url: [yellow]{search_url}")
63
62
 
64
63
  try:
65
- _wpnonce = extract_nonce(proxy)
64
+ _wpnonce = extract_nonce()
66
65
 
67
66
  if not _wpnonce:
68
67
  console.print("[red]Error: Failed to extract nonce")
@@ -81,8 +80,7 @@ def title_search(query: str, proxy: str) -> int:
81
80
  'user-agent': get_userAgent()
82
81
  },
83
82
  data=data,
84
- timeout=max_timeout,
85
- proxy=proxy
83
+ timeout=max_timeout
86
84
  )
87
85
  response.raise_for_status()
88
86
  soup = BeautifulSoup(response.text, 'html.parser')
@@ -19,13 +19,13 @@ max_timeout = config_manager.get_int("REQUESTS", "timeout")
19
19
 
20
20
 
21
21
  class GetSerieInfo:
22
- def __init__(self, url, proxy: str = None):
22
+ def __init__(self, url):
23
23
  self.headers = {'user-agent': get_userAgent()}
24
24
  self.url = url
25
25
  self.seasons_manager = SeasonManager()
26
26
  self.series_name = None
27
27
 
28
- self.client = httpx.Client(headers=self.headers, proxy=proxy, timeout=max_timeout)
28
+ self.client = httpx.Client(headers=self.headers, timeout=max_timeout)
29
29
 
30
30
  def collect_info_season(self) -> None:
31
31
  """
@@ -0,0 +1,131 @@
1
+ # 25.07.25
2
+
3
+ import base64
4
+ import logging
5
+ import os
6
+
7
+
8
+ # External libraries
9
+ import httpx
10
+ from rich.console import Console
11
+ from pywidevine.cdm import Cdm
12
+ from pywidevine.device import Device
13
+ from pywidevine.pssh import PSSH
14
+
15
+
16
+ # Variable
17
+ console = Console()
18
+
19
+
20
+ def get_widevine_keys(pssh, license_url, cdm_device_path, headers=None, payload=None):
21
+ """
22
+ Extract Widevine CONTENT keys (KID/KEY) from a license using pywidevine.
23
+
24
+ Args:
25
+ pssh (str): PSSH base64.
26
+ license_url (str): Widevine license URL.
27
+ cdm_device_path (str): Path to CDM file (device.wvd).
28
+ headers (dict): Optional HTTP headers.
29
+
30
+ Returns:
31
+ list: List of dicts {'kid': ..., 'key': ...} (only CONTENT keys) or None if error.
32
+ """
33
+
34
+ # Check if CDM file exists
35
+ if not os.path.isfile(cdm_device_path):
36
+ console.print(f"[bold red] CDM file not found: {cdm_device_path}[/bold red]")
37
+ return None
38
+
39
+ # Check if PSSH is a valid base64 string
40
+ try:
41
+ base64.b64decode(pssh)
42
+ except Exception:
43
+ console.print(f"[bold red] Invalid PSSH base64 string.[/bold red]")
44
+ return None
45
+
46
+ try:
47
+ device = Device.load(cdm_device_path)
48
+ cdm = Cdm.from_device(device)
49
+ session_id = cdm.open()
50
+
51
+ # Display security level in a more readable format
52
+ security_levels = {1: "L1 (Hardware)", 2: "L2 (Software)", 3: "L3 (Software)"}
53
+ security_level_str = security_levels.get(device.security_level, 'Unknown')
54
+ logging.info(f"Security Level: {security_level_str}")
55
+
56
+ # Only allow L3, otherwise warn and exit
57
+ if device.security_level != 3:
58
+ console.print(f"[bold yellow]⚠️ Only L3 (Software) security level is supported. Current: {security_level_str}[/bold yellow]")
59
+ return None
60
+
61
+ try:
62
+ challenge = cdm.get_license_challenge(session_id, PSSH(pssh))
63
+ req_headers = headers or {}
64
+ req_headers['Content-Type'] = 'application/octet-stream'
65
+
66
+ # Send license request
67
+ response = httpx.post(license_url, data=challenge, headers=req_headers, content=payload)
68
+
69
+ if response.status_code != 200:
70
+ console.print(f"[bold red]License error:[/bold red] {response.status_code}")
71
+ return None
72
+
73
+ # Handle (JSON) or classic (binary) license response
74
+ license_data = response.content
75
+ content_type = response.headers.get("Content-Type", "")
76
+ logging.info(f"License data: {license_data}, Content-Type: {content_type}")
77
+
78
+ # Check if license_data is empty
79
+ if not license_data:
80
+ console.print(f"[bold red]License response is empty.[/bold red]")
81
+ return None
82
+
83
+ if "application/json" in content_type:
84
+ try:
85
+
86
+ # Try to decode as JSON only if plausible
87
+ text = response.text
88
+ data = None
89
+ try:
90
+ data = response.json()
91
+ except Exception as e:
92
+ data = None
93
+
94
+ if data and "license" in data:
95
+ license_data = base64.b64decode(data["license"])
96
+
97
+ elif data is not None:
98
+ console.print("[bold red]'license' field not found in JSON response.[/bold red]")
99
+ return None
100
+
101
+ except Exception as e:
102
+ console.print(f"[bold red]Error parsing JSON license:[/bold red] {e}")
103
+
104
+ cdm.parse_license(session_id, license_data)
105
+
106
+ # Extract only CONTENT keys from the license
107
+ content_keys = []
108
+ for key in cdm.get_keys(session_id):
109
+ if key.type == "CONTENT":
110
+ kid = key.kid.hex() if isinstance(key.kid, bytes) else str(key.kid)
111
+ key_val = key.key.hex() if isinstance(key.key, bytes) else str(key.key)
112
+
113
+ content_keys.append({
114
+ 'kid': kid.replace('-', '').strip(),
115
+ 'key': key_val.replace('-', '').strip()
116
+ })
117
+ logging.info(f"Use kid: {kid}, key: {key_val}")
118
+
119
+ # Check if content_keys list is empty
120
+ if not content_keys:
121
+ console.print(f"[bold yellow]⚠️ No CONTENT keys found in license.[/bold yellow]")
122
+ return None
123
+
124
+ return content_keys
125
+
126
+ finally:
127
+ cdm.close(session_id)
128
+
129
+ except Exception as e:
130
+ console.print(f"[bold red]CDM error:[/bold red] {e}")
131
+ return None
@@ -0,0 +1,79 @@
1
+ # 25.07.25
2
+
3
+ import os
4
+ import subprocess
5
+ import logging
6
+
7
+
8
+ # External libraries
9
+ from rich.console import Console
10
+
11
+
12
+ # Variable
13
+ console = Console()
14
+
15
+
16
+ def decrypt_with_mp4decrypt(encrypted_path, kid, key, output_path=None, cleanup=True):
17
+ """
18
+ Decrypt an mp4/m4s file using mp4decrypt.
19
+
20
+ Args:
21
+ encrypted_path (str): Path to encrypted file.
22
+ kid (str): Hexadecimal KID.
23
+ key (str): Hexadecimal key.
24
+ output_path (str): Output decrypted file path (optional).
25
+ cleanup (bool): If True, remove temporary files after decryption.
26
+
27
+ Returns:
28
+ str: Path to decrypted file, or None if error.
29
+ """
30
+
31
+ # Check if input file exists
32
+ if not os.path.isfile(encrypted_path):
33
+ console.print(f"[bold red] Encrypted file not found: {encrypted_path}[/bold red]")
34
+ return None
35
+
36
+ # Check if kid and key are valid hex
37
+ try:
38
+ bytes.fromhex(kid)
39
+ bytes.fromhex(key)
40
+ except Exception:
41
+ console.print(f"[bold red] Invalid KID or KEY (not hex).[/bold red]")
42
+ return None
43
+
44
+ if not output_path:
45
+ output_path = os.path.splitext(encrypted_path)[0] + "_decrypted.mp4"
46
+
47
+ key_format = f"{kid.lower()}:{key.lower()}"
48
+ cmd = ["mp4decrypt", "--key", key_format, encrypted_path, output_path]
49
+ logging.info(f"Running command: {' '.join(cmd)}")
50
+
51
+ try:
52
+ result = subprocess.run(cmd, capture_output=True, text=True, timeout=300)
53
+ except Exception as e:
54
+ console.print(f"[bold red] mp4decrypt execution failed: {e}[/bold red]")
55
+ return None
56
+
57
+ if result.returncode == 0 and os.path.exists(output_path):
58
+
59
+ # Cleanup temporary files if requested
60
+ if cleanup:
61
+ if os.path.exists(encrypted_path):
62
+ os.remove(encrypted_path)
63
+
64
+ temp_dec = os.path.splitext(encrypted_path)[0] + "_decrypted.mp4"
65
+
66
+ # Do not delete the final output!
67
+ if temp_dec != output_path and os.path.exists(temp_dec):
68
+ os.remove(temp_dec)
69
+
70
+ # Check if output file is not empty
71
+ if os.path.getsize(output_path) == 0:
72
+ console.print(f"[bold red] Decrypted file is empty: {output_path}[/bold red]")
73
+ return None
74
+
75
+ return output_path
76
+
77
+ else:
78
+ console.print(f"[bold red] mp4decrypt failed:[/bold red] {result.stderr}")
79
+ return None
@@ -0,0 +1,218 @@
1
+ # 25.07.25
2
+
3
+ import os
4
+ import shutil
5
+
6
+
7
+ # External libraries
8
+ from rich.console import Console
9
+
10
+
11
+ # Internal utilities
12
+ from StreamingCommunity.Util.config_json import config_manager
13
+ from StreamingCommunity.Lib.FFmpeg.command import join_audios, join_video
14
+
15
+
16
+ # Logic class
17
+ from .parser import MPDParser
18
+ from .segments import MPD_Segments
19
+ from .decrypt import decrypt_with_mp4decrypt
20
+ from .cdm_helpher import get_widevine_keys
21
+
22
+
23
+ # Config
24
+ DOWNLOAD_SPECIFIC_AUDIO = config_manager.get_list('M3U8_DOWNLOAD', 'specific_list_audio')
25
+ FILTER_CUSTOM_REOLUTION = str(config_manager.get('M3U8_PARSER', 'force_resolution')).strip().lower()
26
+ CLEANUP_TMP = config_manager.get_bool('M3U8_DOWNLOAD', 'cleanup_tmp_folder')
27
+
28
+
29
+ # Variable
30
+ console = Console()
31
+
32
+
33
+ class DASH_Downloader:
34
+ def __init__(self, cdm_device, license_url, mpd_url, output_path):
35
+ self.cdm_device = cdm_device
36
+ self.license_url = license_url
37
+ self.mpd_url = mpd_url
38
+ self.original_output_path = os.path.abspath(str(output_path))
39
+ self.out_path = os.path.splitext(self.original_output_path)[0]
40
+ self.parser = None
41
+ self._setup_temp_dirs()
42
+
43
+ self.error = None
44
+ self.stopped = False
45
+ self.output_file = None
46
+
47
+ def _setup_temp_dirs(self):
48
+ """
49
+ Create temporary folder structure under out_path\tmp
50
+ """
51
+ self.tmp_dir = os.path.join(self.out_path, "tmp")
52
+ self.encrypted_dir = os.path.join(self.tmp_dir, "encrypted")
53
+ self.decrypted_dir = os.path.join(self.tmp_dir, "decrypted")
54
+ self.optimize_dir = os.path.join(self.tmp_dir, "optimize")
55
+
56
+ os.makedirs(self.encrypted_dir, exist_ok=True)
57
+ os.makedirs(self.decrypted_dir, exist_ok=True)
58
+ os.makedirs(self.optimize_dir, exist_ok=True)
59
+
60
+ def parse_manifest(self, custom_headers):
61
+ self.parser = MPDParser(self.mpd_url)
62
+ self.parser.parse(custom_headers)
63
+
64
+ # Video info
65
+ selected_video, list_available_resolution, filter_custom_resolution, downloadable_video = self.parser.select_video(FILTER_CUSTOM_REOLUTION)
66
+ console.print(
67
+ f"[cyan bold]Video [/cyan bold] [green]Available:[/green] [purple]{', '.join(list_available_resolution)}[/purple] | "
68
+ f"[red]Set:[/red] [purple]{filter_custom_resolution}[/purple] | "
69
+ f"[yellow]Downloadable:[/yellow] [purple]{downloadable_video}[/purple]"
70
+ )
71
+ self.selected_video = selected_video
72
+
73
+ # Audio info
74
+ selected_audio, list_available_audio_langs, filter_custom_audio, downloadable_audio = self.parser.select_audio(DOWNLOAD_SPECIFIC_AUDIO)
75
+ console.print(
76
+ f"[cyan bold]Audio [/cyan bold] [green]Available:[/green] [purple]{', '.join(list_available_audio_langs)}[/purple] | "
77
+ f"[red]Set:[/red] [purple]{filter_custom_audio}[/purple] | "
78
+ f"[yellow]Downloadable:[/yellow] [purple]{downloadable_audio}[/purple]"
79
+ )
80
+ self.selected_audio = selected_audio
81
+
82
+ def get_representation_by_type(self, typ):
83
+ if typ == "video":
84
+ return getattr(self, "selected_video", None)
85
+ elif typ == "audio":
86
+ return getattr(self, "selected_audio", None)
87
+ return None
88
+
89
+ def download_and_decrypt(self, custom_headers=None, custom_payload=None):
90
+ """
91
+ Download and decrypt video/audio streams. Sets self.error, self.stopped, self.output_file.
92
+ Returns True if successful, False otherwise.
93
+ """
94
+ self.error = None
95
+ self.stopped = False
96
+
97
+ for typ in ["video", "audio"]:
98
+ rep = self.get_representation_by_type(typ)
99
+ if rep:
100
+ encrypted_path = os.path.join(self.encrypted_dir, f"{rep['id']}_encrypted.m4s")
101
+
102
+ downloader = MPD_Segments(
103
+ tmp_folder=self.encrypted_dir,
104
+ representation=rep,
105
+ pssh=self.parser.pssh
106
+ )
107
+
108
+ try:
109
+ result = downloader.download_streams()
110
+
111
+ # Check for interruption or failure
112
+ if result.get("stopped"):
113
+ self.stopped = True
114
+ self.error = "Download interrupted"
115
+ return False
116
+
117
+ if result.get("nFailed", 0) > 0:
118
+ self.error = f"Failed segments: {result['nFailed']}"
119
+ return False
120
+
121
+ except Exception as ex:
122
+ self.error = str(ex)
123
+ return False
124
+
125
+ if not self.parser.pssh:
126
+ print("No PSSH found: segments are not encrypted, skipping decryption.")
127
+ self.download_segments(clear=True)
128
+ return True
129
+
130
+ keys = get_widevine_keys(
131
+ pssh=self.parser.pssh,
132
+ license_url=self.license_url,
133
+ cdm_device_path=self.cdm_device,
134
+ headers=custom_headers,
135
+ payload=custom_payload
136
+ )
137
+
138
+ if not keys:
139
+ self.error = f"No key found, cannot decrypt {typ}"
140
+ print(self.error)
141
+ return False
142
+
143
+ key = keys[0]
144
+ KID = key['kid']
145
+ KEY = key['key']
146
+
147
+ decrypted_path = os.path.join(self.decrypted_dir, f"{typ}.mp4")
148
+ result_path = decrypt_with_mp4decrypt(
149
+ encrypted_path, KID, KEY, output_path=decrypted_path
150
+ )
151
+
152
+ if not result_path:
153
+ self.error = f"Decryption of {typ} failed"
154
+ print(self.error)
155
+ return False
156
+
157
+ else:
158
+ self.error = f"No {typ} found"
159
+ print(self.error)
160
+ return False
161
+
162
+ return True
163
+
164
+ def download_segments(self, clear=False):
165
+ # Download segments and concatenate them
166
+ # clear=True: no decryption needed
167
+ pass
168
+
169
+ def finalize_output(self):
170
+ video_file = os.path.join(self.decrypted_dir, "video.mp4")
171
+ audio_file = os.path.join(self.decrypted_dir, "audio.mp4")
172
+
173
+ # fallback: if one of the two is missing, look in encrypted
174
+ if not os.path.exists(video_file):
175
+ for f in os.listdir(self.encrypted_dir):
176
+ if f.endswith("_encrypted.m4s") and ("video" in f or f.startswith("1_")):
177
+ video_file = os.path.join(self.encrypted_dir, f)
178
+ break
179
+ if not os.path.exists(audio_file):
180
+ for f in os.listdir(self.encrypted_dir):
181
+ if f.endswith("_encrypted.m4s") and ("audio" in f or f.startswith("0_")):
182
+ audio_file = os.path.join(self.encrypted_dir, f)
183
+ break
184
+
185
+ # Usa il nome file originale per il file finale
186
+ output_file = self.original_output_path
187
+
188
+ if os.path.exists(video_file) and os.path.exists(audio_file):
189
+ audio_tracks = [{"path": audio_file}]
190
+ join_audios(video_file, audio_tracks, output_file)
191
+ elif os.path.exists(video_file):
192
+ join_video(video_file, output_file, codec=None)
193
+ else:
194
+ print("Video file missing, cannot export")
195
+
196
+ # Clean up: delete all tmp
197
+ if os.path.exists(self.tmp_dir):
198
+ shutil.rmtree(self.tmp_dir, ignore_errors=True)
199
+
200
+ # Rimuovi la cartella principale se è vuota
201
+ try:
202
+ if os.path.exists(self.out_path) and not os.listdir(self.out_path):
203
+ os.rmdir(self.out_path)
204
+ except Exception as e:
205
+ print(f"[WARN] Impossibile eliminare la cartella {self.out_path}: {e}")
206
+
207
+
208
+ return self.output_file
209
+
210
+ def get_status(self):
211
+ """
212
+ Returns a dict with 'path', 'error', and 'stopped' for external use.
213
+ """
214
+ return {
215
+ "path": self.output_file,
216
+ "error": self.error,
217
+ "stopped": self.stopped
218
+ }