StreamingCommunity 1.9.90__py3-none-any.whl → 2.0.5__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/Site/1337xx/__init__.py +1 -1
- StreamingCommunity/Api/Site/altadefinizione/__init__.py +1 -1
- StreamingCommunity/Api/Site/animeunity/__init__.py +1 -1
- StreamingCommunity/Api/Site/cb01new/__init__.py +1 -1
- StreamingCommunity/Api/Site/ddlstreamitaly/__init__.py +1 -3
- StreamingCommunity/Api/Site/ddlstreamitaly/series.py +1 -1
- StreamingCommunity/Api/Site/guardaserie/__init__.py +1 -3
- StreamingCommunity/Api/Site/guardaserie/series.py +2 -2
- StreamingCommunity/Api/Site/ilcorsaronero/__init__.py +1 -1
- StreamingCommunity/Api/Site/ilcorsaronero/util/ilCorsarScraper.py +14 -6
- StreamingCommunity/Api/Site/mostraguarda/__init__.py +1 -1
- StreamingCommunity/Api/Site/mostraguarda/film.py +5 -3
- StreamingCommunity/Api/Site/streamingcommunity/__init__.py +1 -1
- StreamingCommunity/Api/Site/streamingcommunity/film.py +1 -1
- StreamingCommunity/Api/Site/streamingcommunity/series.py +1 -1
- StreamingCommunity/Api/Site/streamingcommunity/site.py +8 -7
- StreamingCommunity/Api/Template/Util/get_domain.py +12 -11
- StreamingCommunity/Lib/Downloader/HLS/downloader.py +42 -34
- StreamingCommunity/Lib/Downloader/HLS/segments.py +10 -11
- StreamingCommunity/Lib/FFmpeg/capture.py +1 -1
- StreamingCommunity/Lib/FFmpeg/util.py +1 -1
- StreamingCommunity/Lib/M3U8/decryptor.py +2 -2
- StreamingCommunity/Lib/M3U8/estimator.py +53 -25
- StreamingCommunity/Upload/update.py +2 -3
- StreamingCommunity/Upload/version.py +1 -1
- StreamingCommunity/Util/ffmpeg_installer.py +65 -28
- StreamingCommunity/Util/os.py +16 -25
- StreamingCommunity/run.py +2 -3
- {StreamingCommunity-1.9.90.dist-info → StreamingCommunity-2.0.5.dist-info}/METADATA +73 -44
- {StreamingCommunity-1.9.90.dist-info → StreamingCommunity-2.0.5.dist-info}/RECORD +34 -34
- {StreamingCommunity-1.9.90.dist-info → StreamingCommunity-2.0.5.dist-info}/WHEEL +1 -1
- {StreamingCommunity-1.9.90.dist-info → StreamingCommunity-2.0.5.dist-info}/entry_points.txt +1 -0
- {StreamingCommunity-1.9.90.dist-info → StreamingCommunity-2.0.5.dist-info}/LICENSE +0 -0
- {StreamingCommunity-1.9.90.dist-info → StreamingCommunity-2.0.5.dist-info}/top_level.txt +0 -0
|
@@ -27,7 +27,7 @@ def search(string_to_search: str = None, get_onylDatabase: bool = False):
|
|
|
27
27
|
"""
|
|
28
28
|
|
|
29
29
|
if string_to_search is None:
|
|
30
|
-
string_to_search = msg.ask(f"\n[purple]Insert word to search in [
|
|
30
|
+
string_to_search = msg.ask(f"\n[purple]Insert word to search in [green]{SITE_NAME}").strip()
|
|
31
31
|
|
|
32
32
|
# Search on database
|
|
33
33
|
len_database = title_search(quote_plus(string_to_search))
|
|
@@ -27,7 +27,7 @@ def search(string_to_search: str = None, get_onylDatabase: bool = False):
|
|
|
27
27
|
"""
|
|
28
28
|
|
|
29
29
|
if string_to_search is None:
|
|
30
|
-
string_to_search = msg.ask(f"\n[purple]Insert word to search in [
|
|
30
|
+
string_to_search = msg.ask(f"\n[purple]Insert word to search in [green]{SITE_NAME}").strip()
|
|
31
31
|
|
|
32
32
|
# Search on database
|
|
33
33
|
len_database = title_search(quote_plus(string_to_search))
|
|
@@ -24,7 +24,7 @@ from .costant import SITE_NAME
|
|
|
24
24
|
def search(string_to_search: str = None, get_onylDatabase: bool = False):
|
|
25
25
|
|
|
26
26
|
if string_to_search is None:
|
|
27
|
-
string_to_search = msg.ask(f"\n[purple]Insert word to search in [
|
|
27
|
+
string_to_search = msg.ask(f"\n[purple]Insert word to search in [green]{SITE_NAME}").strip()
|
|
28
28
|
|
|
29
29
|
# Search on database
|
|
30
30
|
len_database = title_search(quote_plus(string_to_search))
|
|
@@ -27,7 +27,7 @@ def search(string_to_search: str = None, get_onylDatabase: bool = False):
|
|
|
27
27
|
"""
|
|
28
28
|
|
|
29
29
|
if string_to_search is None:
|
|
30
|
-
string_to_search = msg.ask(f"\n[purple]Insert word to search in [
|
|
30
|
+
string_to_search = msg.ask(f"\n[purple]Insert word to search in [green]{SITE_NAME}").strip()
|
|
31
31
|
|
|
32
32
|
# Search on database
|
|
33
33
|
len_database = title_search(quote_plus(string_to_search))
|
|
@@ -28,9 +28,7 @@ def search(string_to_search: str = None, get_onylDatabase: bool = False):
|
|
|
28
28
|
"""
|
|
29
29
|
|
|
30
30
|
if string_to_search is None:
|
|
31
|
-
|
|
32
|
-
# Make request to site to get content that corrsisponde to that string
|
|
33
|
-
string_to_search = msg.ask(f"\n[purple]Insert word to search in [red]{SITE_NAME}").strip()
|
|
31
|
+
string_to_search = msg.ask(f"\n[purple]Insert word to search in [green]{SITE_NAME}").strip()
|
|
34
32
|
|
|
35
33
|
# Search on database
|
|
36
34
|
len_database = title_search(quote_plus(string_to_search))
|
|
@@ -25,7 +25,6 @@ from StreamingCommunity.Api.Player.ddl import VideoSource
|
|
|
25
25
|
|
|
26
26
|
# Variable
|
|
27
27
|
from .costant import ROOT_PATH, SERIES_FOLDER
|
|
28
|
-
table_show_manager = TVShowManager()
|
|
29
28
|
|
|
30
29
|
|
|
31
30
|
|
|
@@ -120,6 +119,7 @@ def display_episodes_list(obj_episode_manager) -> str:
|
|
|
120
119
|
"""
|
|
121
120
|
|
|
122
121
|
# Set up table for displaying episodes
|
|
122
|
+
table_show_manager = TVShowManager()
|
|
123
123
|
table_show_manager.set_slice_end(10)
|
|
124
124
|
|
|
125
125
|
# Add columns to the table
|
|
@@ -27,9 +27,7 @@ def search(string_to_search: str = None, get_onylDatabase: bool = False):
|
|
|
27
27
|
"""
|
|
28
28
|
|
|
29
29
|
if string_to_search is None:
|
|
30
|
-
|
|
31
|
-
# Make request to site to get content that corrsisponde to that string
|
|
32
|
-
string_to_search = msg.ask(f"\n[purple]Insert word to search in [red]{SITE_NAME}").strip()
|
|
30
|
+
string_to_search = msg.ask(f"\n[purple]Insert word to search in [green]{SITE_NAME}").strip()
|
|
33
31
|
|
|
34
32
|
# Search on database
|
|
35
33
|
len_database = title_search(quote_plus(string_to_search))
|
|
@@ -25,7 +25,6 @@ from StreamingCommunity.Api.Player.supervideo import VideoSource
|
|
|
25
25
|
|
|
26
26
|
# Variable
|
|
27
27
|
from .costant import ROOT_PATH, SERIES_FOLDER
|
|
28
|
-
table_show_manager = TVShowManager()
|
|
29
28
|
|
|
30
29
|
|
|
31
30
|
|
|
@@ -171,8 +170,9 @@ def display_episodes_list(obj_episode_manager) -> str:
|
|
|
171
170
|
Returns:
|
|
172
171
|
last_command (str): Last command entered by the user.
|
|
173
172
|
"""
|
|
174
|
-
|
|
173
|
+
|
|
175
174
|
# Set up table for displaying episodes
|
|
175
|
+
table_show_manager = TVShowManager()
|
|
176
176
|
table_show_manager.set_slice_end(10)
|
|
177
177
|
|
|
178
178
|
# Add columns to the table
|
|
@@ -28,7 +28,7 @@ def search(string_to_search: str = None, get_onylDatabase: bool = False):
|
|
|
28
28
|
"""
|
|
29
29
|
|
|
30
30
|
if string_to_search is None:
|
|
31
|
-
string_to_search = msg.ask(f"\n[purple]Insert word to search in [
|
|
31
|
+
string_to_search = msg.ask(f"\n[purple]Insert word to search in [green]{SITE_NAME}").strip()
|
|
32
32
|
|
|
33
33
|
# Search on database
|
|
34
34
|
len_database = asyncio.run(title_search(quote_plus(string_to_search)))
|
|
@@ -25,13 +25,21 @@ class IlCorsaroNeroScraper:
|
|
|
25
25
|
self.base_url = base_url
|
|
26
26
|
self.max_page = max_page
|
|
27
27
|
self.headers = {
|
|
28
|
-
'
|
|
29
|
-
'
|
|
30
|
-
'
|
|
31
|
-
'
|
|
32
|
-
'
|
|
28
|
+
'accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7',
|
|
29
|
+
'accept-language': 'it-IT,it;q=0.9,en-US;q=0.8,en;q=0.7',
|
|
30
|
+
'cache-control': 'max-age=0',
|
|
31
|
+
'priority': 'u=0, i',
|
|
32
|
+
'sec-ch-ua': '"Google Chrome";v="131", "Chromium";v="131", "Not_A Brand";v="24"',
|
|
33
|
+
'sec-ch-ua-mobile': '?0',
|
|
34
|
+
'sec-ch-ua-platform': '"Windows"',
|
|
35
|
+
'sec-fetch-dest': 'document',
|
|
36
|
+
'sec-fetch-mode': 'navigate',
|
|
37
|
+
'sec-fetch-site': 'same-origin',
|
|
38
|
+
'sec-fetch-user': '?1',
|
|
39
|
+
'upgrade-insecure-requests': '1',
|
|
40
|
+
'user-agent': get_headers()
|
|
33
41
|
}
|
|
34
|
-
|
|
42
|
+
|
|
35
43
|
async def fetch_url(self, url: str) -> Optional[str]:
|
|
36
44
|
"""
|
|
37
45
|
Fetch the HTML content of a given URL.
|
|
@@ -27,7 +27,7 @@ def search(string_to_search: str = None, get_onylDatabase: bool = False):
|
|
|
27
27
|
"""
|
|
28
28
|
|
|
29
29
|
if string_to_search is None:
|
|
30
|
-
string_to_search = msg.ask(f"\n[purple]Insert word to search in [
|
|
30
|
+
string_to_search = msg.ask(f"\n[purple]Insert word to search in [green]{SITE_NAME}").strip()
|
|
31
31
|
|
|
32
32
|
# Not available for the moment
|
|
33
33
|
if get_onylDatabase:
|
|
@@ -61,15 +61,17 @@ def download_film(movie_details: Json_film) -> str:
|
|
|
61
61
|
logging.error(f"Not found in the server. Dict: {movie_details}")
|
|
62
62
|
raise
|
|
63
63
|
|
|
64
|
+
if "not found" in str(response.text):
|
|
65
|
+
logging.error(f"Cant find in the server, Element: {movie_details}")
|
|
66
|
+
raise
|
|
67
|
+
|
|
64
68
|
# Extract supervideo url
|
|
65
69
|
soup = BeautifulSoup(response.text, "html.parser")
|
|
66
70
|
player_links = soup.find("ul", class_ = "_player-mirrors").find_all("li")
|
|
67
71
|
supervideo_url = "https:" + player_links[0].get("data-link")
|
|
68
72
|
|
|
69
|
-
|
|
70
73
|
# Set domain and media ID for the video source
|
|
71
|
-
video_source = VideoSource()
|
|
72
|
-
video_source.setup(supervideo_url)
|
|
74
|
+
video_source = VideoSource(url=supervideo_url)
|
|
73
75
|
|
|
74
76
|
# Define output path
|
|
75
77
|
title_name = os_manager.get_sanitize_file(movie_details.title) + ".mp4"
|
|
@@ -28,7 +28,7 @@ def search(string_to_search: str = None, get_onylDatabase: bool = False):
|
|
|
28
28
|
"""
|
|
29
29
|
|
|
30
30
|
if string_to_search is None:
|
|
31
|
-
string_to_search = msg.ask(f"\n[purple]Insert word to search in [
|
|
31
|
+
string_to_search = msg.ask(f"\n[purple]Insert word to search in [green]{SITE_NAME}").strip()
|
|
32
32
|
|
|
33
33
|
# Get site domain and version and get result of the search
|
|
34
34
|
site_version, domain = get_version_and_domain()
|
|
@@ -39,7 +39,7 @@ def download_film(select_title: MediaItem) -> str:
|
|
|
39
39
|
|
|
40
40
|
# Start message and display film information
|
|
41
41
|
start_message()
|
|
42
|
-
console.print(f"[yellow]Download:
|
|
42
|
+
console.print(f"[yellow]Download: [red]{select_title.name} \n")
|
|
43
43
|
|
|
44
44
|
# Init class
|
|
45
45
|
video_source = VideoSource(SITE_NAME, False)
|
|
@@ -25,7 +25,6 @@ from StreamingCommunity.Api.Player.vixcloud import VideoSource
|
|
|
25
25
|
|
|
26
26
|
# Variable
|
|
27
27
|
from .costant import ROOT_PATH, SITE_NAME, SERIES_FOLDER
|
|
28
|
-
table_show_manager = TVShowManager()
|
|
29
28
|
|
|
30
29
|
|
|
31
30
|
|
|
@@ -178,6 +177,7 @@ def display_episodes_list(scrape_serie) -> str:
|
|
|
178
177
|
"""
|
|
179
178
|
|
|
180
179
|
# Set up table for displaying episodes
|
|
180
|
+
table_show_manager = TVShowManager()
|
|
181
181
|
table_show_manager.set_slice_end(10)
|
|
182
182
|
|
|
183
183
|
# Add columns to the table
|
|
@@ -76,13 +76,14 @@ def get_version_and_domain():
|
|
|
76
76
|
|
|
77
77
|
# Extract version from the response
|
|
78
78
|
try:
|
|
79
|
-
version = get_version(
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
'
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
)
|
|
79
|
+
version = get_version(
|
|
80
|
+
httpx.get(
|
|
81
|
+
url=base_url,
|
|
82
|
+
headers={'User-Agent': get_headers()},
|
|
83
|
+
timeout=max_timeout
|
|
84
|
+
).text
|
|
85
|
+
)
|
|
86
|
+
|
|
86
87
|
except:
|
|
87
88
|
console.print("[green]Auto generate version ...")
|
|
88
89
|
version = secrets.token_hex(32 // 2)
|
|
@@ -76,17 +76,19 @@ def get_final_redirect_url(initial_url, max_timeout):
|
|
|
76
76
|
console.print(f"\n[cyan]Test url[white]: [red]{initial_url}, [cyan]error[white]: [red]{e}")
|
|
77
77
|
return None
|
|
78
78
|
|
|
79
|
-
def search_domain(site_name: str, base_url: str):
|
|
79
|
+
def search_domain(site_name: str, base_url: str, get_first: bool = False):
|
|
80
80
|
"""
|
|
81
81
|
Search for a valid domain for the given site name and base URL.
|
|
82
82
|
|
|
83
83
|
Parameters:
|
|
84
84
|
- site_name (str): The name of the site to search the domain for.
|
|
85
85
|
- base_url (str): The base URL to construct complete URLs.
|
|
86
|
+
- get_first (bool): If True, automatically update to the first valid match without user confirmation.
|
|
86
87
|
|
|
87
88
|
Returns:
|
|
88
89
|
tuple: The found domain and the complete URL.
|
|
89
90
|
"""
|
|
91
|
+
|
|
90
92
|
# Extract config domain
|
|
91
93
|
max_timeout = config_manager.get_int("REQUESTS", "timeout")
|
|
92
94
|
domain = str(config_manager.get_dict("SITE", site_name)['domain'])
|
|
@@ -107,10 +109,10 @@ def search_domain(site_name: str, base_url: str):
|
|
|
107
109
|
|
|
108
110
|
except Exception as e:
|
|
109
111
|
query = base_url.split("/")[-1]
|
|
110
|
-
|
|
112
|
+
|
|
111
113
|
# Perform a Google search with multiple results
|
|
112
114
|
search_results = list(search(query, num_results=10, lang="it"))
|
|
113
|
-
console.print(f"\nGoogle search results: {search_results}")
|
|
115
|
+
#console.print(f"\nGoogle search results: {search_results}")
|
|
114
116
|
|
|
115
117
|
def normalize_for_comparison(url):
|
|
116
118
|
"""Normalize URL by removing protocol, www, and trailing slashes"""
|
|
@@ -121,15 +123,15 @@ def search_domain(site_name: str, base_url: str):
|
|
|
121
123
|
|
|
122
124
|
# Normalize the base_url we're looking for
|
|
123
125
|
target_url = normalize_for_comparison(base_url)
|
|
124
|
-
|
|
126
|
+
|
|
125
127
|
# Iterate through search results
|
|
126
128
|
for first_url in search_results:
|
|
127
129
|
console.print(f"[green]Checking url[white]: [red]{first_url}")
|
|
128
|
-
|
|
130
|
+
|
|
129
131
|
# Get just the domain part of the search result
|
|
130
132
|
parsed_result = urlparse(first_url)
|
|
131
133
|
result_domain = normalize_for_comparison(parsed_result.netloc)
|
|
132
|
-
|
|
134
|
+
|
|
133
135
|
# Compare with our target URL (without the protocol part)
|
|
134
136
|
if result_domain.startswith(target_url.split("/")[-1]):
|
|
135
137
|
try:
|
|
@@ -143,21 +145,20 @@ def search_domain(site_name: str, base_url: str):
|
|
|
143
145
|
|
|
144
146
|
new_domain_extract = extract_domain(str(final_url))
|
|
145
147
|
|
|
146
|
-
if msg.ask(f"\n[cyan]Do you want to auto update site[white] [red]'{site_name}'[cyan] with domain[white] [red]'{new_domain_extract}'.", choices=["y", "n"], default="y").lower() == "y":
|
|
147
|
-
|
|
148
|
+
if get_first or msg.ask(f"\n[cyan]Do you want to auto update site[white] [red]'{site_name}'[cyan] with domain[white] [red]'{new_domain_extract}'.", choices=["y", "n"], default="y").lower() == "y":
|
|
148
149
|
# Update domain in config.json
|
|
149
150
|
config_manager.config['SITE'][site_name]['domain'] = new_domain_extract
|
|
150
151
|
config_manager.write_config()
|
|
151
152
|
|
|
152
153
|
return new_domain_extract, f"{base_url}.{new_domain_extract}"
|
|
153
|
-
|
|
154
|
+
|
|
154
155
|
except Exception as redirect_error:
|
|
155
156
|
console.print(f"[red]Error following redirect for {first_url}: {redirect_error}")
|
|
156
157
|
continue
|
|
157
158
|
|
|
158
|
-
# If no matching URL is found
|
|
159
|
+
# If no matching URL is found return base domain
|
|
159
160
|
console.print("[bold red]No valid URL found matching the base URL.[/bold red]")
|
|
160
|
-
|
|
161
|
+
return domain, f"{base_url}.{domain}"
|
|
161
162
|
|
|
162
163
|
# Handle successful initial domain check
|
|
163
164
|
parsed_url = urlparse(str(response_follow.url))
|
|
@@ -176,9 +176,11 @@ class ContentExtractor:
|
|
|
176
176
|
set_language = DOWNLOAD_SPECIFIC_AUDIO
|
|
177
177
|
downloadable_languages = list(set(available_languages) & set(set_language))
|
|
178
178
|
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
179
|
+
# Only show if there is something available
|
|
180
|
+
if len(available_languages) > 0:
|
|
181
|
+
console.print(f"[cyan bold]Audio →[/cyan bold] [green]Available:[/green] [purple]{', '.join(available_languages)}[/purple] | "
|
|
182
|
+
f"[red]Set:[/red] [purple]{', '.join(set_language)}[/purple] | "
|
|
183
|
+
f"[yellow]Downloadable:[/yellow] [purple]{', '.join(downloadable_languages)}[/purple]")
|
|
182
184
|
|
|
183
185
|
else:
|
|
184
186
|
console.log("[red]Can't find a list of audios")
|
|
@@ -200,9 +202,11 @@ class ContentExtractor:
|
|
|
200
202
|
set_language = DOWNLOAD_SPECIFIC_SUBTITLE
|
|
201
203
|
downloadable_languages = list(set(available_languages) & set(set_language))
|
|
202
204
|
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
205
|
+
# Only show if there is something available
|
|
206
|
+
if len(available_languages) > 0:
|
|
207
|
+
console.print(f"[cyan bold]Subtitle →[/cyan bold] [green]Available:[/green] [purple]{', '.join(available_languages)}[/purple] | "
|
|
208
|
+
f"[red]Set:[/red] [purple]{', '.join(set_language)}[/purple] | "
|
|
209
|
+
f"[yellow]Downloadable:[/yellow] [purple]{', '.join(downloadable_languages)}[/purple]")
|
|
206
210
|
|
|
207
211
|
else:
|
|
208
212
|
console.log("[red]Can't find a list of subtitles")
|
|
@@ -212,13 +216,18 @@ class ContentExtractor:
|
|
|
212
216
|
It identifies the best video quality and displays relevant information to the user.
|
|
213
217
|
"""
|
|
214
218
|
logging.info(f"class 'ContentExtractor'; call _collect_video()")
|
|
219
|
+
set_resolution = "Best"
|
|
215
220
|
|
|
216
221
|
# Collect custom quality video if a specific resolution is set
|
|
217
222
|
if FILTER_CUSTOM_REOLUTION != -1:
|
|
218
223
|
self.m3u8_index, video_res = self.obj_parse._video.get_custom_uri(y_resolution=FILTER_CUSTOM_REOLUTION)
|
|
224
|
+
set_resolution = f"{FILTER_CUSTOM_REOLUTION}p"
|
|
219
225
|
|
|
220
|
-
|
|
221
|
-
|
|
226
|
+
else:
|
|
227
|
+
|
|
228
|
+
# Otherwise, get the best available video quality
|
|
229
|
+
self.m3u8_index, video_res = self.obj_parse._video.get_best_uri()
|
|
230
|
+
|
|
222
231
|
self.codec: M3U8_Codec = self.obj_parse.codec
|
|
223
232
|
|
|
224
233
|
# List all available resolutions
|
|
@@ -227,18 +236,34 @@ class ContentExtractor:
|
|
|
227
236
|
logging.info(f"M3U8 index selected: {self.m3u8_index}, with resolution: {video_res}")
|
|
228
237
|
|
|
229
238
|
# Create a formatted table to display video info
|
|
230
|
-
console.print(f"[cyan bold]Video
|
|
231
|
-
|
|
239
|
+
console.print(f"[cyan bold]Video →[/cyan bold] [green]Available:[/green] [purple]{', '.join(list_available_resolution)}[/purple] | "
|
|
240
|
+
f"[red]Set:[/red] [purple]{set_resolution}[/purple] | "
|
|
241
|
+
f"[yellow]Downloadable:[/yellow] [purple]{video_res[0]}x{video_res[1]}[/purple]")
|
|
232
242
|
|
|
233
243
|
if self.codec is not None:
|
|
244
|
+
|
|
245
|
+
# Generate the string for available codec information
|
|
246
|
+
available_codec_info = (
|
|
247
|
+
f"[green]v[/green]: [yellow]{self.codec.video_codec_name}[/yellow] "
|
|
248
|
+
f"([green]b[/green]: [yellow]{self.codec.video_bitrate // 1000}k[/yellow]), "
|
|
249
|
+
f"[green]a[/green]: [yellow]{self.codec.audio_codec_name}[/yellow] "
|
|
250
|
+
f"([green]b[/green]: [yellow]{self.codec.audio_bitrate // 1000}k[/yellow])"
|
|
251
|
+
)
|
|
252
|
+
|
|
253
|
+
# Determine what to display for "Set"
|
|
254
|
+
# If the codec usage is enabled in the configuration, use the detailed codec info
|
|
255
|
+
# Otherwise, display "copy"
|
|
234
256
|
if config_manager.get_bool("M3U8_CONVERSION", "use_codec"):
|
|
235
|
-
|
|
236
|
-
f"([green]b[/green]: [yellow]{self.codec.video_bitrate // 1000}k[/yellow]), "
|
|
237
|
-
f"[green]a[/green]: [yellow]{self.codec.audio_codec_name}[/yellow] "
|
|
238
|
-
f"([green]b[/green]: [yellow]{self.codec.audio_bitrate // 1000}k[/yellow])")
|
|
257
|
+
set_codec_info = available_codec_info
|
|
239
258
|
else:
|
|
240
|
-
|
|
241
|
-
|
|
259
|
+
set_codec_info = "[purple]copy[/purple]"
|
|
260
|
+
|
|
261
|
+
# Print the formatted result with "Available" and "Set" information
|
|
262
|
+
console.print(
|
|
263
|
+
f"[bold cyan]Codec →[/bold cyan] [green]Available:[/green] {available_codec_info} | "
|
|
264
|
+
f"[red]Set:[/red] {set_codec_info}"
|
|
265
|
+
)
|
|
266
|
+
|
|
242
267
|
|
|
243
268
|
# Fix the URL if it does not include the full protocol
|
|
244
269
|
if "http" not in self.m3u8_index:
|
|
@@ -486,19 +511,6 @@ class ContentJoiner:
|
|
|
486
511
|
self.there_is_audio = len(downloaded_audio) > 0
|
|
487
512
|
self.there_is_subtitle = len(downloaded_subtitle) > 0
|
|
488
513
|
|
|
489
|
-
if self.there_is_audio or self.there_is_subtitle:
|
|
490
|
-
|
|
491
|
-
# Display the status of available media
|
|
492
|
-
table = Table(show_header=False, box=None)
|
|
493
|
-
|
|
494
|
-
table.add_row(f"[green]Video - audio", f"[yellow]{self.there_is_audio}")
|
|
495
|
-
table.add_row(f"[green]Video - Subtitle", f"[yellow]{self.there_is_subtitle}")
|
|
496
|
-
|
|
497
|
-
print("")
|
|
498
|
-
console.rule("[bold green] JOIN ", style="bold red")
|
|
499
|
-
console.print(table)
|
|
500
|
-
print("")
|
|
501
|
-
|
|
502
514
|
# Start the joining process
|
|
503
515
|
self.conversione()
|
|
504
516
|
|
|
@@ -584,10 +596,6 @@ class ContentJoiner:
|
|
|
584
596
|
# Check if the joined video file already exists
|
|
585
597
|
if not os.path.exists(path_join_video):
|
|
586
598
|
|
|
587
|
-
# Set codec to None if not defined in class
|
|
588
|
-
#if not hasattr(self, 'codec'):
|
|
589
|
-
# self.codec = None
|
|
590
|
-
|
|
591
599
|
# Join the video segments into a single video file
|
|
592
600
|
join_video(
|
|
593
601
|
video_path=self.downloaded_video[0].get('path'),
|
|
@@ -844,7 +852,7 @@ class HLS_Downloader:
|
|
|
844
852
|
f"[bold green]Download completed![/bold green]\n"
|
|
845
853
|
f"[cyan]File size: [bold red]{formatted_size}[/bold red]\n"
|
|
846
854
|
f"[cyan]Duration: [bold]{formatted_duration}[/bold]\n"
|
|
847
|
-
f"[cyan]Output: [bold]{self.output_filename}[/bold]"
|
|
855
|
+
f"[cyan]Output: [bold]{os.path.abspath(self.output_filename)}[/bold]"
|
|
848
856
|
)
|
|
849
857
|
|
|
850
858
|
if missing_ts:
|
|
@@ -432,7 +432,7 @@ class M3U8_Segments:
|
|
|
432
432
|
if "audio" in str(type):
|
|
433
433
|
TQDM_MAX_WORKER = AUDIO_WORKERS
|
|
434
434
|
|
|
435
|
-
console.print(f"[cyan]Video workers[white]: [green]{VIDEO_WORKERS} [white]| [cyan]Audio workers[white]: [green]{AUDIO_WORKERS}")
|
|
435
|
+
#console.print(f"[cyan]Video workers[white]: [green]{VIDEO_WORKERS} [white]| [cyan]Audio workers[white]: [green]{AUDIO_WORKERS}")
|
|
436
436
|
|
|
437
437
|
# Custom bar for mobile and pc
|
|
438
438
|
if TQDM_USE_LARGE_BAR:
|
|
@@ -544,18 +544,17 @@ class M3U8_Segments:
|
|
|
544
544
|
file_size = os.path.getsize(self.tmp_file_path)
|
|
545
545
|
if file_size == 0:
|
|
546
546
|
raise Exception("Output file is empty")
|
|
547
|
-
|
|
548
|
-
#
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
547
|
+
|
|
548
|
+
# Display additional info only if there is failed segments
|
|
549
|
+
if self.info_nFailed > 0:
|
|
550
|
+
|
|
551
|
+
# Get expected time
|
|
552
|
+
ex_hours, ex_minutes, ex_seconds = format_duration(self.expected_real_time_s)
|
|
553
|
+
ex_formatted_duration = f"[yellow]{int(ex_hours)}[red]h [yellow]{int(ex_minutes)}[red]m [yellow]{int(ex_seconds)}[red]s"
|
|
554
|
+
console.print(f"[cyan]Max retry per URL[white]: [green]{self.info_maxRetry}[green] [white]| [cyan]Total retry done[white]: [green]{self.info_nRetry}[green] [white]| [cyan]Missing TS: [red]{self.info_nFailed} [white]| [cyan]Duration: {print_duration_table(self.tmp_file_path, None, True)} [white]| [cyan]Expected duation: {ex_formatted_duration} \n")
|
|
552
555
|
|
|
553
556
|
if self.info_nRetry >= len(self.segments) * (1/3.33):
|
|
554
|
-
console.print(
|
|
555
|
-
"[yellow]⚠ Warning:[/yellow] Too many retries detected! "
|
|
556
|
-
"Consider reducing the number of [cyan]workers[/cyan] in the [magenta]config.json[/magenta] file. "
|
|
557
|
-
"This will impact [bold]performance[/bold]. \n"
|
|
558
|
-
)
|
|
557
|
+
console.print("[yellow]⚠ Warning:[/yellow] Too many retries detected! Consider reducing the number of [cyan]workers[/cyan] in the [magenta]config.json[/magenta] file. This will impact [bold]performance[/bold]. \n")
|
|
559
558
|
|
|
560
559
|
# Info to return
|
|
561
560
|
return {'type': type, 'nFailed': self.info_nFailed}
|
|
@@ -59,7 +59,7 @@ def capture_output(process: subprocess.Popen, description: str) -> None:
|
|
|
59
59
|
|
|
60
60
|
|
|
61
61
|
# Construct the progress string with formatted output information
|
|
62
|
-
progress_string = (f"
|
|
62
|
+
progress_string = (f"→ {description}[white]: "
|
|
63
63
|
f"([green]'speed': [yellow]{data.get('speed', 'N/A')}[white], "
|
|
64
64
|
f"[green]'size': [yellow]{internet_manager.format_file_size(byte_size)}[white])")
|
|
65
65
|
max_length = max(max_length, len(progress_string))
|
|
@@ -208,7 +208,7 @@ def is_png_format_or_codec(file_info):
|
|
|
208
208
|
if not file_info:
|
|
209
209
|
return False
|
|
210
210
|
|
|
211
|
-
console.print(f"[yellow][FFmpeg] [cyan]Avaiable codec[white]: [red]{file_info['codec_names']}")
|
|
211
|
+
#console.print(f"[yellow][FFmpeg] [cyan]Avaiable codec[white]: [red]{file_info['codec_names']}")
|
|
212
212
|
return file_info['format_name'] == 'png_pipe' or 'png' in file_info['codec_names']
|
|
213
213
|
|
|
214
214
|
|
|
@@ -17,7 +17,7 @@ crypto_installed = crypto_spec is not None
|
|
|
17
17
|
|
|
18
18
|
|
|
19
19
|
if crypto_installed:
|
|
20
|
-
|
|
20
|
+
logging.info("[cyan]Decrypy use: Cryptodomex")
|
|
21
21
|
from Cryptodome.Cipher import AES
|
|
22
22
|
from Cryptodome.Util.Padding import unpad
|
|
23
23
|
|
|
@@ -93,7 +93,7 @@ else:
|
|
|
93
93
|
# Check if openssl command is available
|
|
94
94
|
try:
|
|
95
95
|
openssl_available = subprocess.run(["openssl", "version"], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL).returncode == 0
|
|
96
|
-
|
|
96
|
+
logging.info("[cyan]Decrypy use: OPENSSL")
|
|
97
97
|
except:
|
|
98
98
|
openssl_available = False
|
|
99
99
|
|