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.
- StreamingCommunity/Api/Player/hdplayer.py +0 -5
- StreamingCommunity/Api/Player/mediapolisvod.py +4 -13
- StreamingCommunity/Api/Player/supervideo.py +3 -8
- StreamingCommunity/Api/Player/sweetpixel.py +1 -9
- StreamingCommunity/Api/Player/vixcloud.py +5 -16
- StreamingCommunity/Api/Site/altadefinizione/film.py +4 -15
- StreamingCommunity/Api/Site/altadefinizione/site.py +2 -7
- StreamingCommunity/Api/Site/altadefinizione/util/ScrapeSerie.py +2 -7
- StreamingCommunity/Api/Site/animeunity/site.py +9 -24
- StreamingCommunity/Api/Site/animeunity/util/ScrapeSerie.py +11 -27
- StreamingCommunity/Api/Site/animeworld/film.py +4 -2
- StreamingCommunity/Api/Site/animeworld/site.py +3 -11
- StreamingCommunity/Api/Site/animeworld/util/ScrapeSerie.py +1 -4
- StreamingCommunity/Api/Site/crunchyroll/film.py +17 -8
- StreamingCommunity/Api/Site/crunchyroll/series.py +8 -9
- StreamingCommunity/Api/Site/crunchyroll/site.py +14 -16
- StreamingCommunity/Api/Site/crunchyroll/util/ScrapeSerie.py +18 -65
- StreamingCommunity/Api/Site/crunchyroll/util/get_license.py +97 -106
- StreamingCommunity/Api/Site/guardaserie/site.py +4 -12
- StreamingCommunity/Api/Site/guardaserie/util/ScrapeSerie.py +3 -10
- StreamingCommunity/Api/Site/mediasetinfinity/film.py +11 -12
- StreamingCommunity/Api/Site/mediasetinfinity/series.py +1 -2
- StreamingCommunity/Api/Site/mediasetinfinity/site.py +3 -11
- StreamingCommunity/Api/Site/mediasetinfinity/util/ScrapeSerie.py +39 -50
- StreamingCommunity/Api/Site/mediasetinfinity/util/fix_mpd.py +3 -3
- StreamingCommunity/Api/Site/mediasetinfinity/util/get_license.py +8 -26
- StreamingCommunity/Api/Site/raiplay/film.py +6 -7
- StreamingCommunity/Api/Site/raiplay/series.py +1 -12
- StreamingCommunity/Api/Site/raiplay/site.py +8 -24
- StreamingCommunity/Api/Site/raiplay/util/ScrapeSerie.py +15 -22
- StreamingCommunity/Api/Site/raiplay/util/get_license.py +3 -12
- StreamingCommunity/Api/Site/streamingcommunity/film.py +5 -16
- StreamingCommunity/Api/Site/streamingcommunity/site.py +3 -22
- StreamingCommunity/Api/Site/streamingcommunity/util/ScrapeSerie.py +11 -26
- StreamingCommunity/Api/Site/streamingwatch/__init__.py +1 -0
- StreamingCommunity/Api/Site/streamingwatch/film.py +4 -2
- StreamingCommunity/Api/Site/streamingwatch/series.py +1 -1
- StreamingCommunity/Api/Site/streamingwatch/site.py +4 -18
- StreamingCommunity/Api/Site/streamingwatch/util/ScrapeSerie.py +0 -3
- StreamingCommunity/Api/Template/config_loader.py +0 -7
- StreamingCommunity/Lib/Downloader/DASH/cdm_helpher.py +8 -3
- StreamingCommunity/Lib/Downloader/DASH/decrypt.py +55 -1
- StreamingCommunity/Lib/Downloader/DASH/downloader.py +139 -55
- StreamingCommunity/Lib/Downloader/DASH/parser.py +458 -101
- StreamingCommunity/Lib/Downloader/DASH/segments.py +131 -74
- StreamingCommunity/Lib/Downloader/HLS/downloader.py +31 -50
- StreamingCommunity/Lib/Downloader/HLS/segments.py +266 -365
- StreamingCommunity/Lib/Downloader/MP4/downloader.py +1 -1
- StreamingCommunity/Lib/FFmpeg/capture.py +37 -5
- StreamingCommunity/Lib/FFmpeg/command.py +35 -93
- StreamingCommunity/Lib/M3U8/estimator.py +0 -1
- StreamingCommunity/Lib/TMBD/tmdb.py +2 -4
- StreamingCommunity/TelegramHelp/config.json +0 -1
- StreamingCommunity/Upload/version.py +1 -1
- StreamingCommunity/Util/config_json.py +28 -21
- StreamingCommunity/Util/http_client.py +28 -0
- StreamingCommunity/Util/os.py +16 -6
- {streamingcommunity-3.3.8.dist-info → streamingcommunity-3.4.0.dist-info}/METADATA +1 -3
- streamingcommunity-3.4.0.dist-info/RECORD +111 -0
- streamingcommunity-3.3.8.dist-info/RECORD +0 -111
- {streamingcommunity-3.3.8.dist-info → streamingcommunity-3.4.0.dist-info}/WHEEL +0 -0
- {streamingcommunity-3.3.8.dist-info → streamingcommunity-3.4.0.dist-info}/entry_points.txt +0 -0
- {streamingcommunity-3.3.8.dist-info → streamingcommunity-3.4.0.dist-info}/licenses/LICENSE +0 -0
- {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
|
|
14
|
-
from StreamingCommunity.Util.
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
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', [])
|
|
@@ -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) +
|
|
49
|
-
mp4_path = os.path.join(site_constant.MOVIE_FOLDER, title_name.replace(
|
|
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.
|
|
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 =
|
|
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 =
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
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 =
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
response
|
|
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
|
-
|
|
226
|
-
|
|
227
|
-
|
|
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
|
-
|
|
378
|
-
|
|
379
|
-
|
|
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
|
|
412
|
-
if MERGE_SUBTITLE and
|
|
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(
|
|
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
|