StreamingCommunity 1.9.5__py3-none-any.whl → 1.9.90__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/Helper/Vixcloud/js_parser.py +143 -0
- StreamingCommunity/Api/Player/Helper/Vixcloud/util.py +145 -0
- StreamingCommunity/Api/Player/ddl.py +89 -0
- StreamingCommunity/Api/Player/maxstream.py +151 -0
- StreamingCommunity/Api/Player/supervideo.py +194 -0
- StreamingCommunity/Api/Player/vixcloud.py +273 -0
- StreamingCommunity/Api/Site/1337xx/__init__.py +51 -0
- StreamingCommunity/Api/Site/1337xx/costant.py +15 -0
- StreamingCommunity/Api/Site/1337xx/site.py +86 -0
- StreamingCommunity/Api/Site/1337xx/title.py +66 -0
- StreamingCommunity/Api/Site/altadefinizione/__init__.py +51 -0
- StreamingCommunity/Api/Site/altadefinizione/costant.py +15 -0
- StreamingCommunity/Api/Site/altadefinizione/film.py +74 -0
- StreamingCommunity/Api/Site/altadefinizione/site.py +89 -0
- StreamingCommunity/Api/Site/animeunity/__init__.py +51 -0
- StreamingCommunity/Api/Site/animeunity/costant.py +15 -0
- StreamingCommunity/Api/Site/animeunity/film_serie.py +135 -0
- StreamingCommunity/Api/Site/animeunity/site.py +167 -0
- StreamingCommunity/Api/Site/animeunity/util/ScrapeSerie.py +97 -0
- StreamingCommunity/Api/Site/cb01new/__init__.py +52 -0
- StreamingCommunity/Api/Site/cb01new/costant.py +15 -0
- StreamingCommunity/Api/Site/cb01new/film.py +73 -0
- StreamingCommunity/Api/Site/cb01new/site.py +76 -0
- StreamingCommunity/Api/Site/ddlstreamitaly/__init__.py +58 -0
- StreamingCommunity/Api/Site/ddlstreamitaly/costant.py +16 -0
- StreamingCommunity/Api/Site/ddlstreamitaly/series.py +146 -0
- StreamingCommunity/Api/Site/ddlstreamitaly/site.py +95 -0
- StreamingCommunity/Api/Site/ddlstreamitaly/util/ScrapeSerie.py +85 -0
- StreamingCommunity/Api/Site/guardaserie/__init__.py +53 -0
- StreamingCommunity/Api/Site/guardaserie/costant.py +15 -0
- StreamingCommunity/Api/Site/guardaserie/series.py +199 -0
- StreamingCommunity/Api/Site/guardaserie/site.py +86 -0
- StreamingCommunity/Api/Site/guardaserie/util/ScrapeSerie.py +110 -0
- StreamingCommunity/Api/Site/ilcorsaronero/__init__.py +52 -0
- StreamingCommunity/Api/Site/ilcorsaronero/costant.py +15 -0
- StreamingCommunity/Api/Site/ilcorsaronero/site.py +63 -0
- StreamingCommunity/Api/Site/ilcorsaronero/title.py +46 -0
- StreamingCommunity/Api/Site/ilcorsaronero/util/ilCorsarScraper.py +141 -0
- StreamingCommunity/Api/Site/mostraguarda/__init__.py +49 -0
- StreamingCommunity/Api/Site/mostraguarda/costant.py +15 -0
- StreamingCommunity/Api/Site/mostraguarda/film.py +99 -0
- StreamingCommunity/Api/Site/streamingcommunity/__init__.py +56 -0
- StreamingCommunity/Api/Site/streamingcommunity/costant.py +15 -0
- StreamingCommunity/Api/Site/streamingcommunity/film.py +75 -0
- StreamingCommunity/Api/Site/streamingcommunity/series.py +206 -0
- StreamingCommunity/Api/Site/streamingcommunity/site.py +137 -0
- StreamingCommunity/Api/Site/streamingcommunity/util/ScrapeSerie.py +123 -0
- StreamingCommunity/Api/Template/Class/SearchType.py +101 -0
- StreamingCommunity/Api/Template/Util/__init__.py +5 -0
- StreamingCommunity/Api/Template/Util/get_domain.py +173 -0
- StreamingCommunity/Api/Template/Util/manage_ep.py +179 -0
- StreamingCommunity/Api/Template/Util/recall_search.py +37 -0
- StreamingCommunity/Api/Template/__init__.py +3 -0
- StreamingCommunity/Api/Template/site.py +87 -0
- StreamingCommunity/Lib/Downloader/HLS/downloader.py +946 -0
- StreamingCommunity/Lib/Downloader/HLS/proxyes.py +110 -0
- StreamingCommunity/Lib/Downloader/HLS/segments.py +561 -0
- StreamingCommunity/Lib/Downloader/MP4/downloader.py +155 -0
- StreamingCommunity/Lib/Downloader/TOR/downloader.py +296 -0
- StreamingCommunity/Lib/Downloader/__init__.py +5 -0
- StreamingCommunity/Lib/FFmpeg/__init__.py +4 -0
- StreamingCommunity/Lib/FFmpeg/capture.py +170 -0
- StreamingCommunity/Lib/FFmpeg/command.py +296 -0
- StreamingCommunity/Lib/FFmpeg/util.py +249 -0
- StreamingCommunity/Lib/M3U8/__init__.py +6 -0
- StreamingCommunity/Lib/M3U8/decryptor.py +164 -0
- StreamingCommunity/Lib/M3U8/estimator.py +176 -0
- StreamingCommunity/Lib/M3U8/parser.py +666 -0
- StreamingCommunity/Lib/M3U8/url_fixer.py +52 -0
- StreamingCommunity/Lib/TMBD/__init__.py +2 -0
- StreamingCommunity/Lib/TMBD/obj_tmbd.py +39 -0
- StreamingCommunity/Lib/TMBD/tmdb.py +346 -0
- StreamingCommunity/Upload/update.py +68 -0
- StreamingCommunity/Upload/version.py +5 -0
- StreamingCommunity/Util/_jsonConfig.py +204 -0
- StreamingCommunity/Util/call_stack.py +42 -0
- StreamingCommunity/Util/color.py +20 -0
- StreamingCommunity/Util/console.py +12 -0
- StreamingCommunity/Util/ffmpeg_installer.py +311 -0
- StreamingCommunity/Util/headers.py +147 -0
- StreamingCommunity/Util/logger.py +53 -0
- StreamingCommunity/Util/message.py +64 -0
- StreamingCommunity/Util/os.py +554 -0
- StreamingCommunity/Util/table.py +229 -0
- StreamingCommunity/__init__.py +0 -0
- StreamingCommunity/run.py +2 -11
- {StreamingCommunity-1.9.5.dist-info → StreamingCommunity-1.9.90.dist-info}/METADATA +10 -27
- StreamingCommunity-1.9.90.dist-info/RECORD +92 -0
- {StreamingCommunity-1.9.5.dist-info → StreamingCommunity-1.9.90.dist-info}/WHEEL +1 -1
- {StreamingCommunity-1.9.5.dist-info → StreamingCommunity-1.9.90.dist-info}/entry_points.txt +0 -1
- StreamingCommunity-1.9.5.dist-info/RECORD +0 -7
- {StreamingCommunity-1.9.5.dist-info → StreamingCommunity-1.9.90.dist-info}/LICENSE +0 -0
- {StreamingCommunity-1.9.5.dist-info → StreamingCommunity-1.9.90.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,946 @@
|
|
|
1
|
+
# 17.10.24
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import sys
|
|
5
|
+
import logging
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
# External libraries
|
|
9
|
+
import httpx
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
# Internal utilities
|
|
13
|
+
from StreamingCommunity.Util._jsonConfig import config_manager
|
|
14
|
+
from StreamingCommunity.Util.console import console, Panel, Table
|
|
15
|
+
from StreamingCommunity.Util.color import Colors
|
|
16
|
+
from StreamingCommunity.Util.os import (
|
|
17
|
+
compute_sha1_hash,
|
|
18
|
+
os_manager,
|
|
19
|
+
internet_manager
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
# Logic class
|
|
23
|
+
from ...FFmpeg import (
|
|
24
|
+
print_duration_table,
|
|
25
|
+
join_video,
|
|
26
|
+
join_audios,
|
|
27
|
+
join_subtitle
|
|
28
|
+
)
|
|
29
|
+
from ...M3U8 import (
|
|
30
|
+
M3U8_Parser,
|
|
31
|
+
M3U8_Codec,
|
|
32
|
+
M3U8_UrlFix
|
|
33
|
+
)
|
|
34
|
+
from .segments import M3U8_Segments
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
# Config
|
|
38
|
+
DOWNLOAD_SPECIFIC_AUDIO = config_manager.get_list('M3U8_DOWNLOAD', 'specific_list_audio')
|
|
39
|
+
DOWNLOAD_SPECIFIC_SUBTITLE = config_manager.get_list('M3U8_DOWNLOAD', 'specific_list_subtitles')
|
|
40
|
+
DOWNLOAD_VIDEO = config_manager.get_bool('M3U8_DOWNLOAD', 'download_video')
|
|
41
|
+
DOWNLOAD_AUDIO = config_manager.get_bool('M3U8_DOWNLOAD', 'download_audio')
|
|
42
|
+
MERGE_AUDIO = config_manager.get_bool('M3U8_DOWNLOAD', 'merge_audio')
|
|
43
|
+
DOWNLOAD_SUBTITLE = config_manager.get_bool('M3U8_DOWNLOAD', 'download_sub')
|
|
44
|
+
MERGE_SUBTITLE = config_manager.get_bool('M3U8_DOWNLOAD', 'merge_subs')
|
|
45
|
+
REMOVE_SEGMENTS_FOLDER = config_manager.get_bool('M3U8_DOWNLOAD', 'cleanup_tmp_folder')
|
|
46
|
+
FILTER_CUSTOM_REOLUTION = config_manager.get_int('M3U8_PARSER', 'force_resolution')
|
|
47
|
+
GET_ONLY_LINK = config_manager.get_bool('M3U8_PARSER', 'get_only_link')
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
# Variable
|
|
51
|
+
max_timeout = config_manager.get_int("REQUESTS", "timeout")
|
|
52
|
+
m3u8_url_fixer = M3U8_UrlFix()
|
|
53
|
+
list_MissingTs = []
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class PathManager:
|
|
58
|
+
def __init__(self, output_filename):
|
|
59
|
+
"""
|
|
60
|
+
Initializes the PathManager with the output filename.
|
|
61
|
+
|
|
62
|
+
Args:
|
|
63
|
+
output_filename (str): The name of the output file (should end with .mp4).
|
|
64
|
+
"""
|
|
65
|
+
self.output_filename = output_filename
|
|
66
|
+
|
|
67
|
+
# Create the base path by removing the '.mp4' extension from the output filename
|
|
68
|
+
self.base_path = str(output_filename).replace(".mp4", "")
|
|
69
|
+
logging.info(f"class 'PathManager'; set base path: {self.base_path}")
|
|
70
|
+
|
|
71
|
+
# Define the path for a temporary directory where segments will be stored
|
|
72
|
+
self.base_temp = os.path.join(self.base_path, "tmp")
|
|
73
|
+
self.video_segments_path = os.path.join(self.base_temp, "video")
|
|
74
|
+
self.audio_segments_path = os.path.join(self.base_temp, "audio")
|
|
75
|
+
self.subtitle_segments_path = os.path.join(self.base_temp, "subtitle")
|
|
76
|
+
|
|
77
|
+
def create_directories(self):
|
|
78
|
+
"""
|
|
79
|
+
Creates the necessary directories for storing video, audio, and subtitle segments.
|
|
80
|
+
"""
|
|
81
|
+
|
|
82
|
+
os.makedirs(self.base_temp, exist_ok=True)
|
|
83
|
+
os.makedirs(self.video_segments_path, exist_ok=True)
|
|
84
|
+
os.makedirs(self.audio_segments_path, exist_ok=True)
|
|
85
|
+
os.makedirs(self.subtitle_segments_path, exist_ok=True)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
class HttpClient:
|
|
89
|
+
def __init__(self, headers: str = None):
|
|
90
|
+
"""
|
|
91
|
+
Initializes the HttpClient with specified headers.
|
|
92
|
+
"""
|
|
93
|
+
self.headers = headers
|
|
94
|
+
|
|
95
|
+
def get(self, url: str):
|
|
96
|
+
"""
|
|
97
|
+
Sends a GET request to the specified URL and returns the response as text.
|
|
98
|
+
|
|
99
|
+
Returns:
|
|
100
|
+
str: The response body as text if the request is successful, None otherwise.
|
|
101
|
+
"""
|
|
102
|
+
logging.info(f"class 'HttpClient'; make request: {url}")
|
|
103
|
+
try:
|
|
104
|
+
response = httpx.get(
|
|
105
|
+
url=url,
|
|
106
|
+
headers=self.headers,
|
|
107
|
+
timeout=max_timeout
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
response.raise_for_status()
|
|
111
|
+
return response.text
|
|
112
|
+
|
|
113
|
+
except Exception as e:
|
|
114
|
+
logging.info(f"Request to {url} failed with error: {e}")
|
|
115
|
+
return 404
|
|
116
|
+
|
|
117
|
+
def get_content(self, url):
|
|
118
|
+
"""
|
|
119
|
+
Sends a GET request to the specified URL and returns the raw response content.
|
|
120
|
+
|
|
121
|
+
Returns:
|
|
122
|
+
bytes: The response content as bytes if the request is successful, None otherwise.
|
|
123
|
+
"""
|
|
124
|
+
logging.info(f"class 'HttpClient'; make request: {url}")
|
|
125
|
+
try:
|
|
126
|
+
response = httpx.get(
|
|
127
|
+
url=url,
|
|
128
|
+
headers=self.headers,
|
|
129
|
+
timeout=max_timeout
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
response.raise_for_status()
|
|
133
|
+
return response.content # Return the raw response content
|
|
134
|
+
|
|
135
|
+
except Exception as e:
|
|
136
|
+
logging.error(f"Request to {url} failed: {response.status_code} when get content.")
|
|
137
|
+
return None
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
class ContentExtractor:
|
|
141
|
+
def __init__(self):
|
|
142
|
+
"""
|
|
143
|
+
This class is responsible for extracting audio, subtitle, and video information from an M3U8 playlist.
|
|
144
|
+
"""
|
|
145
|
+
pass
|
|
146
|
+
|
|
147
|
+
def start(self, obj_parse: M3U8_Parser):
|
|
148
|
+
"""
|
|
149
|
+
Starts the extraction process by parsing the M3U8 playlist and collecting audio, subtitle, and video data.
|
|
150
|
+
|
|
151
|
+
Args:
|
|
152
|
+
obj_parse (str): The M3U8_Parser obj of the M3U8 playlist.
|
|
153
|
+
"""
|
|
154
|
+
|
|
155
|
+
self.obj_parse = obj_parse
|
|
156
|
+
|
|
157
|
+
# Collect audio, subtitle, and video information
|
|
158
|
+
self._collect_audio()
|
|
159
|
+
self._collect_subtitle()
|
|
160
|
+
self._collect_video()
|
|
161
|
+
|
|
162
|
+
def _collect_audio(self):
|
|
163
|
+
"""
|
|
164
|
+
It checks for available audio languages and the specific audio tracks to download.
|
|
165
|
+
"""
|
|
166
|
+
logging.info(f"class 'ContentExtractor'; call _collect_audio()")
|
|
167
|
+
|
|
168
|
+
# Collect available audio tracks and their corresponding URIs and names
|
|
169
|
+
self.list_available_audio = self.obj_parse._audio.get_all_uris_and_names()
|
|
170
|
+
|
|
171
|
+
# Check if there are any audio tracks available; if not, disable download
|
|
172
|
+
if self.list_available_audio is not None:
|
|
173
|
+
|
|
174
|
+
# Extract available languages from the audio tracks
|
|
175
|
+
available_languages = [obj_audio.get('language') for obj_audio in self.list_available_audio]
|
|
176
|
+
set_language = DOWNLOAD_SPECIFIC_AUDIO
|
|
177
|
+
downloadable_languages = list(set(available_languages) & set(set_language))
|
|
178
|
+
|
|
179
|
+
console.print(f"[cyan bold]Audio:[/cyan bold] [green]Available:[/green] [purple]{', '.join(available_languages)}[/purple] | "
|
|
180
|
+
f"[red]Set:[/red] [purple]{', '.join(set_language)}[/purple] | "
|
|
181
|
+
f"[yellow]Downloadable:[/yellow] [purple]{', '.join(downloadable_languages)}[/purple]")
|
|
182
|
+
|
|
183
|
+
else:
|
|
184
|
+
console.log("[red]Can't find a list of audios")
|
|
185
|
+
|
|
186
|
+
def _collect_subtitle(self):
|
|
187
|
+
"""
|
|
188
|
+
It checks for available subtitle languages and the specific subtitles to download.
|
|
189
|
+
"""
|
|
190
|
+
logging.info(f"class 'ContentExtractor'; call _collect_subtitle()")
|
|
191
|
+
|
|
192
|
+
# Collect available subtitles and their corresponding URIs and names
|
|
193
|
+
self.list_available_subtitles = self.obj_parse._subtitle.get_all_uris_and_names()
|
|
194
|
+
|
|
195
|
+
# Check if there are any subtitles available; if not, disable download
|
|
196
|
+
if self.list_available_subtitles is not None:
|
|
197
|
+
|
|
198
|
+
# Extract available languages from the subtitles
|
|
199
|
+
available_languages = [obj_subtitle.get('language') for obj_subtitle in self.list_available_subtitles]
|
|
200
|
+
set_language = DOWNLOAD_SPECIFIC_SUBTITLE
|
|
201
|
+
downloadable_languages = list(set(available_languages) & set(set_language))
|
|
202
|
+
|
|
203
|
+
console.print(f"[cyan bold]Subtitle:[/cyan bold] [green]Available:[/green] [purple]{', '.join(available_languages)}[/purple] | "
|
|
204
|
+
f"[red]Set:[/red] [purple]{', '.join(set_language)}[/purple] | "
|
|
205
|
+
f"[yellow]Downloadable:[/yellow] [purple]{', '.join(downloadable_languages)}[/purple]")
|
|
206
|
+
|
|
207
|
+
else:
|
|
208
|
+
console.log("[red]Can't find a list of subtitles")
|
|
209
|
+
|
|
210
|
+
def _collect_video(self):
|
|
211
|
+
"""
|
|
212
|
+
It identifies the best video quality and displays relevant information to the user.
|
|
213
|
+
"""
|
|
214
|
+
logging.info(f"class 'ContentExtractor'; call _collect_video()")
|
|
215
|
+
|
|
216
|
+
# Collect custom quality video if a specific resolution is set
|
|
217
|
+
if FILTER_CUSTOM_REOLUTION != -1:
|
|
218
|
+
self.m3u8_index, video_res = self.obj_parse._video.get_custom_uri(y_resolution=FILTER_CUSTOM_REOLUTION)
|
|
219
|
+
|
|
220
|
+
# Otherwise, get the best available video quality
|
|
221
|
+
self.m3u8_index, video_res = self.obj_parse._video.get_best_uri()
|
|
222
|
+
self.codec: M3U8_Codec = self.obj_parse.codec
|
|
223
|
+
|
|
224
|
+
# List all available resolutions
|
|
225
|
+
tuple_available_resolution = self.obj_parse._video.get_list_resolution()
|
|
226
|
+
list_available_resolution = [str(resolution[0]) + "x" + str(resolution[1]) for resolution in tuple_available_resolution]
|
|
227
|
+
logging.info(f"M3U8 index selected: {self.m3u8_index}, with resolution: {video_res}")
|
|
228
|
+
|
|
229
|
+
# Create a formatted table to display video info
|
|
230
|
+
console.print(f"[cyan bold]Video:[/cyan bold] [green]Available resolutions:[/green] [purple]{', '.join(list_available_resolution)}[/purple] | "
|
|
231
|
+
f"[yellow]Downloadable:[/yellow] [purple]{video_res[0]}x{video_res[1]}[/purple]")
|
|
232
|
+
|
|
233
|
+
if self.codec is not None:
|
|
234
|
+
if config_manager.get_bool("M3U8_CONVERSION", "use_codec"):
|
|
235
|
+
codec_info = (f"[green]v[/green]: [yellow]{self.codec.video_codec_name}[/yellow] "
|
|
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])")
|
|
239
|
+
else:
|
|
240
|
+
codec_info = "[cyan]copy[/cyan]"
|
|
241
|
+
console.print(f"[bold green]Codec:[/bold green] {codec_info}")
|
|
242
|
+
|
|
243
|
+
# Fix the URL if it does not include the full protocol
|
|
244
|
+
if "http" not in self.m3u8_index:
|
|
245
|
+
|
|
246
|
+
# Generate the full URL
|
|
247
|
+
self.m3u8_index = m3u8_url_fixer.generate_full_url(self.m3u8_index)
|
|
248
|
+
logging.info(f"Generated index URL: {self.m3u8_index}")
|
|
249
|
+
|
|
250
|
+
# Check if a valid HTTPS URL is obtained
|
|
251
|
+
if self.m3u8_index is not None and "https" in self.m3u8_index:
|
|
252
|
+
#console.print(f"[cyan]Found m3u8 index [white]=> [red]{self.m3u8_index}")
|
|
253
|
+
print()
|
|
254
|
+
|
|
255
|
+
else:
|
|
256
|
+
logging.error("[download_m3u8] Can't find a valid m3u8 index")
|
|
257
|
+
raise ValueError("Invalid m3u8 index URL")
|
|
258
|
+
|
|
259
|
+
print("")
|
|
260
|
+
|
|
261
|
+
class DownloadTracker:
|
|
262
|
+
def __init__(self, path_manager: PathManager):
|
|
263
|
+
"""
|
|
264
|
+
Initializes the DownloadTracker with paths for audio, subtitle, and video segments.
|
|
265
|
+
|
|
266
|
+
Args:
|
|
267
|
+
path_manager (PathManager): An instance of the PathManager class to manage file paths.
|
|
268
|
+
"""
|
|
269
|
+
|
|
270
|
+
# Initialize lists to track downloaded audio, subtitles, and video
|
|
271
|
+
self.downloaded_audio = []
|
|
272
|
+
self.downloaded_subtitle = []
|
|
273
|
+
self.downloaded_video = []
|
|
274
|
+
|
|
275
|
+
self.video_segment_path = path_manager.video_segments_path
|
|
276
|
+
self.audio_segments_path = path_manager.audio_segments_path
|
|
277
|
+
self.subtitle_segments_path = path_manager.subtitle_segments_path
|
|
278
|
+
|
|
279
|
+
def add_video(self, available_video):
|
|
280
|
+
"""
|
|
281
|
+
Adds a single video to the list of downloaded videos.
|
|
282
|
+
|
|
283
|
+
Args:
|
|
284
|
+
available_video (str): The URL of the video to be downloaded.
|
|
285
|
+
"""
|
|
286
|
+
logging.info(f"class 'DownloadTracker'; call add_video() with parameter: {available_video}")
|
|
287
|
+
|
|
288
|
+
self.downloaded_video.append({
|
|
289
|
+
'type': 'video',
|
|
290
|
+
'url': available_video,
|
|
291
|
+
'path': os.path.join(self.video_segment_path, "0.ts")
|
|
292
|
+
})
|
|
293
|
+
|
|
294
|
+
def add_audio(self, list_available_audio):
|
|
295
|
+
"""
|
|
296
|
+
Adds available audio tracks to the list of downloaded audio.
|
|
297
|
+
|
|
298
|
+
Args:
|
|
299
|
+
list_available_audio (list): A list of available audio track objects.
|
|
300
|
+
"""
|
|
301
|
+
logging.info(f"class 'DownloadTracker'; call add_audio() with parameter: {list_available_audio}")
|
|
302
|
+
|
|
303
|
+
for obj_audio in list_available_audio:
|
|
304
|
+
|
|
305
|
+
# Check if specific audio languages are set for download
|
|
306
|
+
if len(DOWNLOAD_SPECIFIC_AUDIO) > 0:
|
|
307
|
+
|
|
308
|
+
# Skip this audio track if its language is not in the specified list
|
|
309
|
+
if obj_audio.get('language') not in DOWNLOAD_SPECIFIC_AUDIO:
|
|
310
|
+
continue
|
|
311
|
+
|
|
312
|
+
# Construct the full path for the audio segment directory
|
|
313
|
+
full_path_audio = os.path.join(self.audio_segments_path, obj_audio.get('language'))
|
|
314
|
+
|
|
315
|
+
# Append the audio information to the downloaded audio list
|
|
316
|
+
self.downloaded_audio.append({
|
|
317
|
+
'type': 'audio',
|
|
318
|
+
'url': obj_audio.get('uri'),
|
|
319
|
+
'language': obj_audio.get('language'),
|
|
320
|
+
'path': os.path.join(full_path_audio, "0.ts")
|
|
321
|
+
})
|
|
322
|
+
|
|
323
|
+
def add_subtitle(self, list_available_subtitles):
|
|
324
|
+
"""
|
|
325
|
+
Adds available subtitles to the list of downloaded subtitles.
|
|
326
|
+
|
|
327
|
+
Args:
|
|
328
|
+
list_available_subtitles (list): A list of available subtitle objects.
|
|
329
|
+
"""
|
|
330
|
+
logging.info(f"class 'DownloadTracker'; call add_subtitle() with parameter: {list_available_subtitles}")
|
|
331
|
+
|
|
332
|
+
for obj_subtitle in list_available_subtitles:
|
|
333
|
+
|
|
334
|
+
# Check if specific subtitle languages are set for download
|
|
335
|
+
if len(DOWNLOAD_SPECIFIC_SUBTITLE) > 0:
|
|
336
|
+
|
|
337
|
+
# Skip this subtitle if its language is not in the specified list
|
|
338
|
+
if obj_subtitle.get('language') not in DOWNLOAD_SPECIFIC_SUBTITLE:
|
|
339
|
+
continue
|
|
340
|
+
|
|
341
|
+
sub_language = obj_subtitle.get('language')
|
|
342
|
+
|
|
343
|
+
# Construct the full path for the subtitle file
|
|
344
|
+
sub_full_path = os.path.join(self.subtitle_segments_path, sub_language + ".vtt")
|
|
345
|
+
|
|
346
|
+
self.downloaded_subtitle.append({
|
|
347
|
+
'type': 'sub',
|
|
348
|
+
'url': obj_subtitle.get('uri'),
|
|
349
|
+
'language': obj_subtitle.get('language'),
|
|
350
|
+
'path': sub_full_path
|
|
351
|
+
})
|
|
352
|
+
|
|
353
|
+
|
|
354
|
+
class ContentDownloader:
|
|
355
|
+
def __init__(self):
|
|
356
|
+
"""
|
|
357
|
+
Initializes the ContentDownloader class.
|
|
358
|
+
|
|
359
|
+
Attributes:
|
|
360
|
+
expected_real_time (float): Expected real-time duration of the video download.
|
|
361
|
+
"""
|
|
362
|
+
self.expected_real_time = None
|
|
363
|
+
|
|
364
|
+
def download_video(self, downloaded_video):
|
|
365
|
+
"""
|
|
366
|
+
Downloads the video if it doesn't already exist.
|
|
367
|
+
|
|
368
|
+
Args:
|
|
369
|
+
downloaded_video (list): A list containing information about the video to download.
|
|
370
|
+
"""
|
|
371
|
+
logging.info(f"class 'ContentDownloader'; call download_video() with parameter: {downloaded_video}")
|
|
372
|
+
|
|
373
|
+
# Check if the video file already exists
|
|
374
|
+
if not os.path.exists(downloaded_video[0].get('path')):
|
|
375
|
+
folder_name = os.path.dirname(downloaded_video[0].get('path'))
|
|
376
|
+
|
|
377
|
+
# Create an instance of M3U8_Segments to handle video segments download
|
|
378
|
+
video_m3u8 = M3U8_Segments(downloaded_video[0].get('url'), folder_name)
|
|
379
|
+
|
|
380
|
+
# Get information about the video segments (e.g., duration, ts files to download)
|
|
381
|
+
video_m3u8.get_info()
|
|
382
|
+
|
|
383
|
+
# Store the expected real-time duration of the video
|
|
384
|
+
self.expected_real_time = video_m3u8.expected_real_time
|
|
385
|
+
|
|
386
|
+
# Download the video streams and print status
|
|
387
|
+
info_dw = video_m3u8.download_streams(f"{Colors.MAGENTA}video", "video")
|
|
388
|
+
list_MissingTs.append(info_dw)
|
|
389
|
+
|
|
390
|
+
# Print duration information of the downloaded video
|
|
391
|
+
#print_duration_table(downloaded_video[0].get('path'))
|
|
392
|
+
|
|
393
|
+
else:
|
|
394
|
+
console.log("[cyan]Video [red]already exists.")
|
|
395
|
+
|
|
396
|
+
def download_audio(self, downloaded_audio):
|
|
397
|
+
"""
|
|
398
|
+
Downloads audio tracks if they don't already exist.
|
|
399
|
+
|
|
400
|
+
Args:
|
|
401
|
+
downloaded_audio (list): A list containing information about audio tracks to download.
|
|
402
|
+
"""
|
|
403
|
+
logging.info(f"class 'ContentDownloader'; call download_audio() with parameter: {downloaded_audio}")
|
|
404
|
+
|
|
405
|
+
for obj_audio in downloaded_audio:
|
|
406
|
+
folder_name = os.path.dirname(obj_audio.get('path'))
|
|
407
|
+
|
|
408
|
+
# Check if the audio file already exists
|
|
409
|
+
if not os.path.exists(obj_audio.get('path')):
|
|
410
|
+
|
|
411
|
+
# Create an instance of M3U8_Segments to handle audio segments download
|
|
412
|
+
audio_m3u8 = M3U8_Segments(obj_audio.get('url'), folder_name)
|
|
413
|
+
|
|
414
|
+
# Get information about the audio segments (e.g., duration, ts files to download)
|
|
415
|
+
audio_m3u8.get_info()
|
|
416
|
+
|
|
417
|
+
# Download the audio segments and print status
|
|
418
|
+
info_dw = audio_m3u8.download_streams(f"{Colors.MAGENTA}audio {Colors.RED}{obj_audio.get('language')}", f"audio_{obj_audio.get('language')}")
|
|
419
|
+
list_MissingTs.append(info_dw)
|
|
420
|
+
|
|
421
|
+
# Print duration information of the downloaded audio
|
|
422
|
+
#print_duration_table(obj_audio.get('path'))
|
|
423
|
+
|
|
424
|
+
else:
|
|
425
|
+
console.log(f"[cyan]Audio [white]([green]{obj_audio.get('language')}[white]) [red]already exists.")
|
|
426
|
+
|
|
427
|
+
def download_subtitle(self, downloaded_subtitle):
|
|
428
|
+
"""
|
|
429
|
+
Downloads subtitle files if they don't already exist.
|
|
430
|
+
|
|
431
|
+
Args:
|
|
432
|
+
downloaded_subtitle (list): A list containing information about subtitles to download.
|
|
433
|
+
"""
|
|
434
|
+
logging.info(f"class 'ContentDownloader'; call download_subtitle() with parameter: {downloaded_subtitle}")
|
|
435
|
+
|
|
436
|
+
for obj_subtitle in downloaded_subtitle:
|
|
437
|
+
sub_language = obj_subtitle.get('language')
|
|
438
|
+
|
|
439
|
+
# Check if the subtitle file already exists
|
|
440
|
+
if os.path.exists(obj_subtitle.get("path")):
|
|
441
|
+
console.log(f"[cyan]Subtitle [white]([green]{sub_language}[white]) [red]already exists.")
|
|
442
|
+
continue # Skip to the next subtitle if it exists
|
|
443
|
+
|
|
444
|
+
# Parse the M3U8 file to get the subtitle URI
|
|
445
|
+
m3u8_sub_parser = M3U8_Parser()
|
|
446
|
+
m3u8_sub_parser.parse_data(
|
|
447
|
+
uri=obj_subtitle.get('uri'),
|
|
448
|
+
raw_content=httpx.get(obj_subtitle.get('url')).text # Fetch subtitle content
|
|
449
|
+
)
|
|
450
|
+
|
|
451
|
+
# Print the status of the subtitle download
|
|
452
|
+
#console.print(f"[cyan]Downloading subtitle: [red]{sub_language.lower()}")
|
|
453
|
+
|
|
454
|
+
# Write the content to the specified file
|
|
455
|
+
with open(obj_subtitle.get("path"), "wb") as f:
|
|
456
|
+
f.write(HttpClient().get_content(m3u8_sub_parser.subtitle[-1]))
|
|
457
|
+
|
|
458
|
+
|
|
459
|
+
class ContentJoiner:
|
|
460
|
+
def __init__(self, path_manager):
|
|
461
|
+
"""
|
|
462
|
+
Initializes the ContentJoiner class.
|
|
463
|
+
|
|
464
|
+
Args:
|
|
465
|
+
path_manager (PathManager): An instance of PathManager to manage output paths.
|
|
466
|
+
"""
|
|
467
|
+
self.path_manager: PathManager = path_manager
|
|
468
|
+
|
|
469
|
+
def setup(self, downloaded_video, downloaded_audio, downloaded_subtitle, codec = None):
|
|
470
|
+
"""
|
|
471
|
+
Sets up the content joiner with downloaded media files.
|
|
472
|
+
|
|
473
|
+
Args:
|
|
474
|
+
downloaded_video (list): List of downloaded video information.
|
|
475
|
+
downloaded_audio (list): List of downloaded audio information.
|
|
476
|
+
downloaded_subtitle (list): List of downloaded subtitle information.
|
|
477
|
+
"""
|
|
478
|
+
self.downloaded_video = downloaded_video
|
|
479
|
+
self.downloaded_audio = downloaded_audio
|
|
480
|
+
self.downloaded_subtitle = downloaded_subtitle
|
|
481
|
+
self.codec = codec
|
|
482
|
+
|
|
483
|
+
# Initialize flags to check if media is available
|
|
484
|
+
self.converted_out_path = None
|
|
485
|
+
self.there_is_video = len(downloaded_video) > 0
|
|
486
|
+
self.there_is_audio = len(downloaded_audio) > 0
|
|
487
|
+
self.there_is_subtitle = len(downloaded_subtitle) > 0
|
|
488
|
+
|
|
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
|
+
# Start the joining process
|
|
503
|
+
self.conversione()
|
|
504
|
+
|
|
505
|
+
def conversione(self):
|
|
506
|
+
"""
|
|
507
|
+
Handles the joining of video, audio, and subtitles based on availability.
|
|
508
|
+
"""
|
|
509
|
+
|
|
510
|
+
# Join audio and video if audio is available
|
|
511
|
+
if self.there_is_audio:
|
|
512
|
+
if MERGE_AUDIO:
|
|
513
|
+
|
|
514
|
+
# Join video with audio tracks
|
|
515
|
+
self.converted_out_path = self._join_video_audio()
|
|
516
|
+
|
|
517
|
+
else:
|
|
518
|
+
|
|
519
|
+
# Process each available audio track
|
|
520
|
+
for obj_audio in self.downloaded_audio:
|
|
521
|
+
language = obj_audio.get('language')
|
|
522
|
+
path = obj_audio.get('path')
|
|
523
|
+
|
|
524
|
+
# Set the new path for regular audio
|
|
525
|
+
new_path = self.path_manager.output_filename.replace(".mp4", f"_{language}.mp4")
|
|
526
|
+
|
|
527
|
+
try:
|
|
528
|
+
|
|
529
|
+
# Rename the audio file to the new path
|
|
530
|
+
os.rename(path, new_path)
|
|
531
|
+
logging.info(f"Audio moved to {new_path}")
|
|
532
|
+
|
|
533
|
+
except Exception as e:
|
|
534
|
+
logging.error(f"Failed to move audio {path} to {new_path}: {e}")
|
|
535
|
+
|
|
536
|
+
# Convert video if available
|
|
537
|
+
if self.there_is_video:
|
|
538
|
+
self.converted_out_path = self._join_video()
|
|
539
|
+
|
|
540
|
+
# If no audio but video is available, join video
|
|
541
|
+
else:
|
|
542
|
+
if self.there_is_video:
|
|
543
|
+
self.converted_out_path = self._join_video()
|
|
544
|
+
|
|
545
|
+
# Join subtitles if available
|
|
546
|
+
if self.there_is_subtitle:
|
|
547
|
+
if MERGE_SUBTITLE:
|
|
548
|
+
if self.converted_out_path is not None:
|
|
549
|
+
self.converted_out_path = self._join_video_subtitles(self.converted_out_path)
|
|
550
|
+
|
|
551
|
+
else:
|
|
552
|
+
|
|
553
|
+
# Process each available subtitle track
|
|
554
|
+
for obj_sub in self.downloaded_subtitle:
|
|
555
|
+
language = obj_sub.get('language')
|
|
556
|
+
path = obj_sub.get('path')
|
|
557
|
+
forced = 'forced' in language
|
|
558
|
+
|
|
559
|
+
# Adjust the language name and set the new path based on forced status
|
|
560
|
+
if forced:
|
|
561
|
+
language = language.replace("forced-", "")
|
|
562
|
+
new_path = self.path_manager.output_filename.replace(".mp4", f".{language}.forced.vtt")
|
|
563
|
+
else:
|
|
564
|
+
new_path = self.path_manager.output_filename.replace(".mp4", f".{language}.vtt")
|
|
565
|
+
|
|
566
|
+
try:
|
|
567
|
+
# Rename the subtitle file to the new path
|
|
568
|
+
os.rename(path, new_path)
|
|
569
|
+
logging.info(f"Subtitle moved to {new_path}")
|
|
570
|
+
|
|
571
|
+
except Exception as e:
|
|
572
|
+
logging.error(f"Failed to move subtitle {path} to {new_path}: {e}")
|
|
573
|
+
|
|
574
|
+
def _join_video(self):
|
|
575
|
+
"""
|
|
576
|
+
Joins video segments into a single video file.
|
|
577
|
+
|
|
578
|
+
Returns:
|
|
579
|
+
str: The path to the joined video file.
|
|
580
|
+
"""
|
|
581
|
+
path_join_video = os.path.join(self.path_manager.base_path, "v_v.mp4")
|
|
582
|
+
logging.info(f"JOIN video path: {path_join_video}")
|
|
583
|
+
|
|
584
|
+
# Check if the joined video file already exists
|
|
585
|
+
if not os.path.exists(path_join_video):
|
|
586
|
+
|
|
587
|
+
# Set codec to None if not defined in class
|
|
588
|
+
#if not hasattr(self, 'codec'):
|
|
589
|
+
# self.codec = None
|
|
590
|
+
|
|
591
|
+
# Join the video segments into a single video file
|
|
592
|
+
join_video(
|
|
593
|
+
video_path=self.downloaded_video[0].get('path'),
|
|
594
|
+
out_path=path_join_video,
|
|
595
|
+
codec=self.codec
|
|
596
|
+
)
|
|
597
|
+
|
|
598
|
+
else:
|
|
599
|
+
console.log("[red]Output join video already exists.")
|
|
600
|
+
|
|
601
|
+
return path_join_video
|
|
602
|
+
|
|
603
|
+
def _join_video_audio(self):
|
|
604
|
+
"""
|
|
605
|
+
Joins video segments with audio tracks into a single video with audio file.
|
|
606
|
+
|
|
607
|
+
Returns:
|
|
608
|
+
str: The path to the joined video with audio file.
|
|
609
|
+
"""
|
|
610
|
+
path_join_video_audio = os.path.join(self.path_manager.base_path, "v_a.mp4")
|
|
611
|
+
logging.info(f"JOIN audio path: {path_join_video_audio}")
|
|
612
|
+
|
|
613
|
+
# Check if the joined video with audio file already exists
|
|
614
|
+
if not os.path.exists(path_join_video_audio):
|
|
615
|
+
|
|
616
|
+
# Set codec to None if not defined in class
|
|
617
|
+
#if not hasattr(self, 'codec'):
|
|
618
|
+
# self.codec = None
|
|
619
|
+
|
|
620
|
+
# Join the video with audio segments
|
|
621
|
+
join_audios(
|
|
622
|
+
video_path=self.downloaded_video[0].get('path'),
|
|
623
|
+
audio_tracks=self.downloaded_audio,
|
|
624
|
+
out_path=path_join_video_audio,
|
|
625
|
+
codec=self.codec
|
|
626
|
+
)
|
|
627
|
+
|
|
628
|
+
else:
|
|
629
|
+
console.log("[red]Output join video and audio already exists.")
|
|
630
|
+
|
|
631
|
+
return path_join_video_audio
|
|
632
|
+
|
|
633
|
+
def _join_video_subtitles(self, input_path):
|
|
634
|
+
"""
|
|
635
|
+
Joins subtitles with the video.
|
|
636
|
+
|
|
637
|
+
Args:
|
|
638
|
+
input_path (str): The path to the video file to which subtitles will be added.
|
|
639
|
+
|
|
640
|
+
Returns:
|
|
641
|
+
str: The path to the video with subtitles file.
|
|
642
|
+
"""
|
|
643
|
+
path_join_video_subtitle = os.path.join(self.path_manager.base_path, "v_s.mp4")
|
|
644
|
+
logging.info(f"JOIN subtitle path: {path_join_video_subtitle}")
|
|
645
|
+
|
|
646
|
+
# Check if the video with subtitles file already exists
|
|
647
|
+
if not os.path.exists(path_join_video_subtitle):
|
|
648
|
+
|
|
649
|
+
# Join the video with subtitles
|
|
650
|
+
join_subtitle(
|
|
651
|
+
input_path,
|
|
652
|
+
self.downloaded_subtitle,
|
|
653
|
+
path_join_video_subtitle
|
|
654
|
+
)
|
|
655
|
+
|
|
656
|
+
return path_join_video_subtitle
|
|
657
|
+
|
|
658
|
+
|
|
659
|
+
class HLS_Downloader:
|
|
660
|
+
def __init__(self, output_filename: str=None, m3u8_playlist: str=None, m3u8_index: str=None, is_playlist_url: bool=True, is_index_url: bool=True):
|
|
661
|
+
"""
|
|
662
|
+
Initializes the HLS_Downloader class.
|
|
663
|
+
|
|
664
|
+
Args:
|
|
665
|
+
output_filename (str): The desired output filename for the downloaded content.
|
|
666
|
+
m3u8_playlist (str): The URL or content of the m3u8 playlist.
|
|
667
|
+
m3u8_index (str): The index URL for m3u8 streams.
|
|
668
|
+
is_playlist_url (bool): Flag indicating if the m3u8_playlist is a URL.
|
|
669
|
+
is_index_url (bool): Flag indicating if the m3u8_index is a URL.
|
|
670
|
+
"""
|
|
671
|
+
if ((m3u8_playlist == None or m3u8_playlist == "") and output_filename is None) or ((m3u8_index == None or m3u8_index == "") and output_filename is None):
|
|
672
|
+
logging.info(f"class 'HLS_Downloader'; call __init__(); no parameter")
|
|
673
|
+
sys.exit(0)
|
|
674
|
+
|
|
675
|
+
self.output_filename = self._generate_output_filename(output_filename, m3u8_playlist, m3u8_index)
|
|
676
|
+
self.path_manager = PathManager(self.output_filename)
|
|
677
|
+
self.download_tracker = DownloadTracker(self.path_manager)
|
|
678
|
+
self.content_extractor = ContentExtractor()
|
|
679
|
+
self.content_downloader = ContentDownloader()
|
|
680
|
+
self.content_joiner = ContentJoiner(self.path_manager)
|
|
681
|
+
|
|
682
|
+
self.m3u8_playlist = m3u8_playlist
|
|
683
|
+
self.m3u8_index = m3u8_index
|
|
684
|
+
self.is_playlist_url = is_playlist_url
|
|
685
|
+
self.is_index_url = is_index_url
|
|
686
|
+
self.expected_real_time = None
|
|
687
|
+
self.instace_parserClass = M3U8_Parser()
|
|
688
|
+
|
|
689
|
+
self.request_m3u8_playlist = None
|
|
690
|
+
self.request_m3u8_index = None
|
|
691
|
+
if (m3u8_playlist == None or m3u8_playlist == ""):
|
|
692
|
+
self.request_m3u8_index = HttpClient().get(self.m3u8_index)
|
|
693
|
+
if (m3u8_index == None or m3u8_index == ""):
|
|
694
|
+
self.request_m3u8_playlist = HttpClient().get(self.m3u8_playlist)
|
|
695
|
+
|
|
696
|
+
def _generate_output_filename(self, output_filename, m3u8_playlist, m3u8_index):
|
|
697
|
+
"""
|
|
698
|
+
Generates a valid output filename based on provided parameters.
|
|
699
|
+
|
|
700
|
+
Args:
|
|
701
|
+
output_filename (str): The desired output filename.
|
|
702
|
+
m3u8_playlist (str): The m3u8 playlist URL or content.
|
|
703
|
+
m3u8_index (str): The m3u8 index URL.
|
|
704
|
+
|
|
705
|
+
Returns:
|
|
706
|
+
str: The generated output filename.
|
|
707
|
+
"""
|
|
708
|
+
root_path = config_manager.get('DEFAULT', 'root_path')
|
|
709
|
+
new_filename = None
|
|
710
|
+
new_folder = os.path.join(root_path, "undefined")
|
|
711
|
+
logging.info(f"class 'HLS_Downloader'; call _generate_output_filename(); destination folder: {new_folder}")
|
|
712
|
+
|
|
713
|
+
# Auto-generate output file name if not present
|
|
714
|
+
if (output_filename is None) or ("mp4" not in output_filename):
|
|
715
|
+
if m3u8_playlist is not None:
|
|
716
|
+
new_filename = os.path.join(new_folder, compute_sha1_hash(m3u8_playlist) + ".mp4")
|
|
717
|
+
else:
|
|
718
|
+
new_filename = os.path.join(new_folder, compute_sha1_hash(m3u8_index) + ".mp4")
|
|
719
|
+
|
|
720
|
+
else:
|
|
721
|
+
|
|
722
|
+
# Check if output_filename contains a folder path
|
|
723
|
+
folder, base_name = os.path.split(output_filename)
|
|
724
|
+
|
|
725
|
+
# If no folder is specified, default to 'undefined'
|
|
726
|
+
if not folder:
|
|
727
|
+
folder = new_folder
|
|
728
|
+
|
|
729
|
+
# Sanitize base name and folder
|
|
730
|
+
folder = os_manager.get_sanitize_path(folder)
|
|
731
|
+
base_name = os_manager.get_sanitize_file(base_name)
|
|
732
|
+
os_manager.create_path(folder)
|
|
733
|
+
|
|
734
|
+
# Parse to only ASCII for compatibility across platforms
|
|
735
|
+
new_filename = os.path.join(folder, base_name)
|
|
736
|
+
|
|
737
|
+
logging.info(f"class 'HLS_Downloader'; call _generate_output_filename(); return path: {new_filename}")
|
|
738
|
+
return new_filename
|
|
739
|
+
|
|
740
|
+
def start(self):
|
|
741
|
+
"""
|
|
742
|
+
Initiates the downloading process. Checks if the output file already exists and proceeds with processing the playlist or index.
|
|
743
|
+
"""
|
|
744
|
+
if os.path.exists(self.output_filename):
|
|
745
|
+
console.log("[red]Output file already exists.")
|
|
746
|
+
return 400
|
|
747
|
+
|
|
748
|
+
self.path_manager.create_directories()
|
|
749
|
+
|
|
750
|
+
# Determine whether to process a playlist or index
|
|
751
|
+
if self.m3u8_playlist:
|
|
752
|
+
if self.m3u8_playlist is not None:
|
|
753
|
+
if self.request_m3u8_playlist != 404:
|
|
754
|
+
logging.info(f"class 'HLS_Downloader'; call start(); parse m3u8 data")
|
|
755
|
+
|
|
756
|
+
self.instace_parserClass.parse_data(uri=self.m3u8_playlist, raw_content=self.request_m3u8_playlist)
|
|
757
|
+
is_masterPlaylist = self.instace_parserClass.is_master_playlist
|
|
758
|
+
|
|
759
|
+
# Check if it's a real master playlist
|
|
760
|
+
if is_masterPlaylist:
|
|
761
|
+
if not GET_ONLY_LINK:
|
|
762
|
+
r_proc = self._process_playlist()
|
|
763
|
+
|
|
764
|
+
if r_proc == 404:
|
|
765
|
+
return 404
|
|
766
|
+
else:
|
|
767
|
+
return None
|
|
768
|
+
|
|
769
|
+
else:
|
|
770
|
+
return {
|
|
771
|
+
'path': self.output_filename,
|
|
772
|
+
'url': self.m3u8_playlist
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
else:
|
|
776
|
+
console.log("[red]Error: URL passed to M3U8_Parser is an index playlist; expected a master playlist. Crucimorfo strikes again!")
|
|
777
|
+
else:
|
|
778
|
+
console.log(f"[red]Error: m3u8_playlist failed request for: {self.m3u8_playlist}")
|
|
779
|
+
else:
|
|
780
|
+
console.log("[red]Error: m3u8_playlist is None")
|
|
781
|
+
|
|
782
|
+
elif self.m3u8_index:
|
|
783
|
+
if self.m3u8_index is not None:
|
|
784
|
+
if self.request_m3u8_index != 404:
|
|
785
|
+
logging.info(f"class 'HLS_Downloader'; call start(); parse m3u8 data")
|
|
786
|
+
|
|
787
|
+
self.instace_parserClass.parse_data(uri=self.m3u8_index, raw_content=self.request_m3u8_index)
|
|
788
|
+
is_masterPlaylist = self.instace_parserClass.is_master_playlist
|
|
789
|
+
|
|
790
|
+
# Check if it's a real index playlist
|
|
791
|
+
if not is_masterPlaylist:
|
|
792
|
+
if not GET_ONLY_LINK:
|
|
793
|
+
self._process_index()
|
|
794
|
+
return None
|
|
795
|
+
|
|
796
|
+
else:
|
|
797
|
+
return {
|
|
798
|
+
'path': self.output_filename,
|
|
799
|
+
'url': self.m3u8_index
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
else:
|
|
803
|
+
console.log("[red]Error: URL passed to M3U8_Parser is an master playlist; expected a index playlist. Crucimorfo strikes again!")
|
|
804
|
+
else:
|
|
805
|
+
console.log("[red]Error: m3u8_index failed request")
|
|
806
|
+
else:
|
|
807
|
+
console.log("[red]Error: m3u8_index is None")
|
|
808
|
+
|
|
809
|
+
def _clean(self, out_path: str) -> None:
|
|
810
|
+
"""
|
|
811
|
+
Cleans up temporary files and folders after downloading and processing.
|
|
812
|
+
|
|
813
|
+
Args:
|
|
814
|
+
out_path (str): The path of the output file to be cleaned up.
|
|
815
|
+
"""
|
|
816
|
+
|
|
817
|
+
# Check if the final output file exists
|
|
818
|
+
logging.info(f"Check if end file converted exists: {out_path}")
|
|
819
|
+
if out_path is None or not os.path.isfile(out_path):
|
|
820
|
+
logging.error("Video file converted does not exist.")
|
|
821
|
+
sys.exit(0)
|
|
822
|
+
|
|
823
|
+
# Rename the output file to the desired output filename if it does not already exist
|
|
824
|
+
if not os.path.exists(self.output_filename):
|
|
825
|
+
missing_ts = False
|
|
826
|
+
missing_info = ""
|
|
827
|
+
|
|
828
|
+
# Rename the converted file to the specified output filename
|
|
829
|
+
os.rename(out_path, self.output_filename)
|
|
830
|
+
|
|
831
|
+
# Calculate file size and duration for reporting
|
|
832
|
+
formatted_size = internet_manager.format_file_size(os.path.getsize(self.output_filename))
|
|
833
|
+
formatted_duration = print_duration_table(self.output_filename, description=False, return_string=True)
|
|
834
|
+
|
|
835
|
+
# Collect info about type missing
|
|
836
|
+
for item in list_MissingTs:
|
|
837
|
+
if int(item['nFailed']) >= 1:
|
|
838
|
+
missing_ts = True
|
|
839
|
+
missing_info += f"[red]TS Failed: {item['nFailed']} {item['type']} tracks[/red]\n"
|
|
840
|
+
|
|
841
|
+
# Prepare the report panel content
|
|
842
|
+
print("")
|
|
843
|
+
panel_content = (
|
|
844
|
+
f"[bold green]Download completed![/bold green]\n"
|
|
845
|
+
f"[cyan]File size: [bold red]{formatted_size}[/bold red]\n"
|
|
846
|
+
f"[cyan]Duration: [bold]{formatted_duration}[/bold]\n"
|
|
847
|
+
f"[cyan]Output: [bold]{self.output_filename}[/bold]"
|
|
848
|
+
)
|
|
849
|
+
|
|
850
|
+
if missing_ts:
|
|
851
|
+
panel_content += f"\n{missing_info}"
|
|
852
|
+
|
|
853
|
+
# Display the download completion message
|
|
854
|
+
console.print(Panel(
|
|
855
|
+
panel_content,
|
|
856
|
+
title=f"{os.path.basename(self.output_filename.replace('.mp4', ''))}",
|
|
857
|
+
border_style="green"
|
|
858
|
+
))
|
|
859
|
+
|
|
860
|
+
# Handle missing segments
|
|
861
|
+
if missing_ts:
|
|
862
|
+
os.rename(self.output_filename, self.output_filename.replace(".mp4", "_failed.mp4"))
|
|
863
|
+
|
|
864
|
+
# Delete all temporary files except for the output file
|
|
865
|
+
os_manager.remove_files_except_one(self.path_manager.base_path, os.path.basename(self.output_filename.replace(".mp4", "_failed.mp4")))
|
|
866
|
+
|
|
867
|
+
# Remove the base folder if specified
|
|
868
|
+
if REMOVE_SEGMENTS_FOLDER:
|
|
869
|
+
os_manager.remove_folder(self.path_manager.base_path)
|
|
870
|
+
|
|
871
|
+
else:
|
|
872
|
+
logging.info("Video file converted already exists.")
|
|
873
|
+
|
|
874
|
+
def _valida_playlist(self):
|
|
875
|
+
"""
|
|
876
|
+
Validates the m3u8 playlist content, saves it to a temporary file, and collects playlist information.
|
|
877
|
+
"""
|
|
878
|
+
logging.info("class 'HLS_Downloader'; call _valida_playlist()")
|
|
879
|
+
|
|
880
|
+
# Retrieve the m3u8 playlist content
|
|
881
|
+
if self.is_playlist_url:
|
|
882
|
+
if self.request_m3u8_playlist != 404:
|
|
883
|
+
m3u8_playlist_text = self.request_m3u8_playlist
|
|
884
|
+
m3u8_url_fixer.set_playlist(self.m3u8_playlist)
|
|
885
|
+
|
|
886
|
+
else:
|
|
887
|
+
logging.info(f"class 'HLS_Downloader'; call _process_playlist(); return 404")
|
|
888
|
+
return 404
|
|
889
|
+
|
|
890
|
+
else:
|
|
891
|
+
m3u8_playlist_text = self.m3u8_playlist
|
|
892
|
+
|
|
893
|
+
# Check if the m3u8 content is valid
|
|
894
|
+
if m3u8_playlist_text is None:
|
|
895
|
+
console.log("[red]Playlist m3u8 to download is empty.")
|
|
896
|
+
sys.exit(0)
|
|
897
|
+
|
|
898
|
+
# Save the m3u8 playlist text to a temporary file
|
|
899
|
+
open(os.path.join(self.path_manager.base_temp, "playlist.m3u8"), "w+", encoding="utf-8").write(m3u8_playlist_text)
|
|
900
|
+
|
|
901
|
+
# Collect information about the playlist
|
|
902
|
+
if self.is_playlist_url:
|
|
903
|
+
self.content_extractor.start(self.instace_parserClass)
|
|
904
|
+
else:
|
|
905
|
+
self.content_extractor.start("https://fake.com", m3u8_playlist_text)
|
|
906
|
+
|
|
907
|
+
def _process_playlist(self):
|
|
908
|
+
"""
|
|
909
|
+
Processes the m3u8 playlist to download video, audio, and subtitles.
|
|
910
|
+
"""
|
|
911
|
+
self._valida_playlist()
|
|
912
|
+
|
|
913
|
+
# Add downloaded elements to the tracker
|
|
914
|
+
self.download_tracker.add_video(self.content_extractor.m3u8_index)
|
|
915
|
+
self.download_tracker.add_audio(self.content_extractor.list_available_audio)
|
|
916
|
+
self.download_tracker.add_subtitle(self.content_extractor.list_available_subtitles)
|
|
917
|
+
|
|
918
|
+
# Download each type of content
|
|
919
|
+
if DOWNLOAD_VIDEO and len(self.download_tracker.downloaded_video) > 0:
|
|
920
|
+
self.content_downloader.download_video(self.download_tracker.downloaded_video)
|
|
921
|
+
if DOWNLOAD_AUDIO and len(self.download_tracker.downloaded_audio) > 0:
|
|
922
|
+
self.content_downloader.download_audio(self.download_tracker.downloaded_audio)
|
|
923
|
+
if DOWNLOAD_SUBTITLE and len(self.download_tracker.downloaded_subtitle) > 0:
|
|
924
|
+
self.content_downloader.download_subtitle(self.download_tracker.downloaded_subtitle)
|
|
925
|
+
|
|
926
|
+
# Join downloaded content
|
|
927
|
+
self.content_joiner.setup(self.download_tracker.downloaded_video, self.download_tracker.downloaded_audio, self.download_tracker.downloaded_subtitle, self.content_extractor.codec)
|
|
928
|
+
|
|
929
|
+
# Clean up temporary files and directories
|
|
930
|
+
self._clean(self.content_joiner.converted_out_path)
|
|
931
|
+
|
|
932
|
+
def _process_index(self):
|
|
933
|
+
"""
|
|
934
|
+
Processes the m3u8 index to download only video.
|
|
935
|
+
"""
|
|
936
|
+
m3u8_url_fixer.set_playlist(self.m3u8_index)
|
|
937
|
+
|
|
938
|
+
# Download video
|
|
939
|
+
self.download_tracker.add_video(self.m3u8_index)
|
|
940
|
+
self.content_downloader.download_video(self.download_tracker.downloaded_video)
|
|
941
|
+
|
|
942
|
+
# Join video
|
|
943
|
+
self.content_joiner.setup(self.download_tracker.downloaded_video, [], [])
|
|
944
|
+
|
|
945
|
+
# Clean up temporary files and directories
|
|
946
|
+
self._clean(self.content_joiner.converted_out_path)
|