StreamingCommunity 1.7.6__py3-none-any.whl → 1.9.1__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/{Src/Api → Api}/Player/Helper/Vixcloud/js_parser.py +4 -1
- StreamingCommunity/{Src/Api → Api}/Player/Helper/Vixcloud/util.py +166 -166
- StreamingCommunity/{Src/Api → Api}/Player/ddl.py +89 -89
- StreamingCommunity/{Src/Api → Api}/Player/maxstream.py +151 -151
- StreamingCommunity/{Src/Api → Api}/Player/supervideo.py +193 -193
- StreamingCommunity/{Src/Api → Api}/Player/vixcloud.py +224 -212
- StreamingCommunity/{Src/Api → Api}/Site/1337xx/__init__.py +50 -50
- StreamingCommunity/{Src/Api → Api}/Site/1337xx/costant.py +15 -15
- StreamingCommunity/{Src/Api → Api}/Site/1337xx/site.py +83 -83
- StreamingCommunity/{Src/Api → Api}/Site/1337xx/title.py +66 -66
- StreamingCommunity/{Src/Api → Api}/Site/altadefinizione/__init__.py +50 -50
- StreamingCommunity/{Src/Api/Site/mostraguarda → Api/Site/altadefinizione}/costant.py +15 -15
- StreamingCommunity/{Src/Api → Api}/Site/altadefinizione/film.py +69 -69
- StreamingCommunity/{Src/Api → Api}/Site/altadefinizione/site.py +86 -86
- StreamingCommunity/{Src/Api → Api}/Site/animeunity/__init__.py +50 -50
- StreamingCommunity/{Src/Api/Site/altadefinizione → Api/Site/animeunity}/costant.py +15 -15
- StreamingCommunity/{Src/Api → Api}/Site/animeunity/film_serie.py +130 -131
- StreamingCommunity/{Src/Api → Api}/Site/animeunity/site.py +164 -164
- StreamingCommunity/{Src/Api → Api}/Site/animeunity/util/ScrapeSerie.py +3 -3
- StreamingCommunity/{Src/Api → Api}/Site/bitsearch/__init__.py +51 -51
- StreamingCommunity/{Src/Api → Api}/Site/bitsearch/costant.py +15 -15
- StreamingCommunity/{Src/Api → Api}/Site/bitsearch/site.py +84 -84
- StreamingCommunity/{Src/Api → Api}/Site/bitsearch/title.py +47 -47
- StreamingCommunity/{Src/Api → Api}/Site/cb01new/__init__.py +51 -51
- StreamingCommunity/{Src/Api → Api}/Site/cb01new/costant.py +15 -15
- StreamingCommunity/{Src/Api → Api}/Site/cb01new/film.py +69 -69
- StreamingCommunity/{Src/Api → Api}/Site/cb01new/site.py +74 -74
- StreamingCommunity/{Src/Api → Api}/Site/ddlstreamitaly/__init__.py +57 -57
- StreamingCommunity/{Src/Api → Api}/Site/ddlstreamitaly/costant.py +16 -16
- StreamingCommunity/{Src/Api → Api}/Site/ddlstreamitaly/series.py +141 -142
- StreamingCommunity/{Src/Api → Api}/Site/ddlstreamitaly/site.py +92 -92
- StreamingCommunity/{Src/Api → Api}/Site/ddlstreamitaly/util/ScrapeSerie.py +84 -82
- StreamingCommunity/{Src/Api → Api}/Site/guardaserie/__init__.py +52 -52
- StreamingCommunity/{Src/Api/Site/piratebays → Api/Site/guardaserie}/costant.py +15 -15
- StreamingCommunity/{Src/Api → Api}/Site/guardaserie/series.py +195 -195
- StreamingCommunity/{Src/Api → Api}/Site/guardaserie/site.py +84 -84
- StreamingCommunity/{Src/Api → Api}/Site/guardaserie/util/ScrapeSerie.py +110 -110
- StreamingCommunity/{Src/Api → Api}/Site/mostraguarda/__init__.py +48 -48
- StreamingCommunity/{Src/Api/Site/animeunity → Api/Site/mostraguarda}/costant.py +15 -15
- StreamingCommunity/{Src/Api → Api}/Site/mostraguarda/film.py +94 -94
- StreamingCommunity/{Src/Api → Api}/Site/piratebays/__init__.py +50 -50
- StreamingCommunity/{Src/Api/Site/guardaserie → Api/Site/piratebays}/costant.py +15 -15
- StreamingCommunity/{Src/Api → Api}/Site/piratebays/site.py +88 -88
- StreamingCommunity/{Src/Api → Api}/Site/piratebays/title.py +45 -45
- StreamingCommunity/{Src/Api → Api}/Site/streamingcommunity/__init__.py +55 -55
- StreamingCommunity/{Src/Api → Api}/Site/streamingcommunity/costant.py +15 -15
- StreamingCommunity/{Src/Api → Api}/Site/streamingcommunity/film.py +70 -70
- StreamingCommunity/{Src/Api → Api}/Site/streamingcommunity/series.py +205 -203
- StreamingCommunity/{Src/Api → Api}/Site/streamingcommunity/site.py +125 -125
- StreamingCommunity/{Src/Api → Api}/Site/streamingcommunity/util/ScrapeSerie.py +3 -3
- StreamingCommunity/{Src/Api → Api}/Template/Class/SearchType.py +101 -101
- StreamingCommunity/{Src/Api → Api}/Template/Util/__init__.py +4 -4
- StreamingCommunity/{Src/Api → Api}/Template/Util/get_domain.py +137 -137
- StreamingCommunity/{Src/Api → Api}/Template/Util/manage_ep.py +153 -153
- StreamingCommunity/{Src/Api → Api}/Template/Util/recall_search.py +37 -37
- StreamingCommunity/Api/Template/__init__.py +3 -0
- StreamingCommunity/{Src/Api → Api}/Template/site.py +87 -87
- StreamingCommunity/{Src/Lib → Lib}/Downloader/HLS/downloader.py +968 -968
- StreamingCommunity/{Src/Lib → Lib}/Downloader/HLS/proxyes.py +110 -110
- StreamingCommunity/{Src/Lib → Lib}/Downloader/HLS/segments.py +538 -540
- StreamingCommunity/{Src/Lib → Lib}/Downloader/MP4/downloader.py +156 -156
- StreamingCommunity/{Src/Lib → Lib}/Downloader/TOR/downloader.py +222 -222
- StreamingCommunity/{Src/Lib → Lib}/Downloader/__init__.py +4 -4
- StreamingCommunity/{Src/Lib → Lib}/Driver/driver_1.py +76 -76
- StreamingCommunity/{Src/Lib → Lib}/FFmpeg/__init__.py +4 -4
- StreamingCommunity/{Src/Lib → Lib}/FFmpeg/capture.py +170 -170
- StreamingCommunity/{Src/Lib → Lib}/FFmpeg/command.py +292 -292
- StreamingCommunity/{Src/Lib → Lib}/FFmpeg/util.py +241 -241
- StreamingCommunity/{Src/Lib → Lib}/M3U8/__init__.py +5 -5
- StreamingCommunity/{Src/Lib → Lib}/M3U8/decryptor.py +164 -129
- StreamingCommunity/{Src/Lib → Lib}/M3U8/estimator.py +175 -172
- StreamingCommunity/{Src/Lib → Lib}/M3U8/parser.py +666 -666
- StreamingCommunity/{Src/Lib → Lib}/M3U8/url_fixer.py +51 -51
- StreamingCommunity/Lib/TMBD/__init__.py +2 -0
- StreamingCommunity/{Src/Lib → Lib}/TMBD/obj_tmbd.py +39 -39
- StreamingCommunity/{Src/Lib → Lib}/TMBD/tmdb.py +345 -345
- StreamingCommunity/{Src/Upload → Upload}/update.py +68 -64
- StreamingCommunity/{Src/Upload → Upload}/version.py +5 -5
- StreamingCommunity/{Src/Util → Util}/_jsonConfig.py +204 -204
- StreamingCommunity/{Src/Util → Util}/call_stack.py +42 -42
- StreamingCommunity/{Src/Util → Util}/color.py +20 -20
- StreamingCommunity/{Src/Util → Util}/console.py +12 -12
- StreamingCommunity/Util/ffmpeg_installer.py +275 -0
- StreamingCommunity/{Src/Util → Util}/headers.py +147 -147
- StreamingCommunity/{Src/Util → Util}/logger.py +53 -53
- StreamingCommunity/{Src/Util → Util}/message.py +46 -46
- StreamingCommunity/{Src/Util → Util}/os.py +514 -417
- StreamingCommunity/{Src/Util → Util}/table.py +163 -163
- StreamingCommunity/run.py +202 -196
- {StreamingCommunity-1.7.6.dist-info → StreamingCommunity-1.9.1.dist-info}/METADATA +126 -60
- StreamingCommunity-1.9.1.dist-info/RECORD +95 -0
- {StreamingCommunity-1.7.6.dist-info → StreamingCommunity-1.9.1.dist-info}/WHEEL +1 -1
- StreamingCommunity/Src/Api/Site/animeunity/anime.py +0 -126
- StreamingCommunity/Src/Api/Site/ddlstreamitaly/Player/ScrapeSerie.py +0 -83
- StreamingCommunity/Src/Api/Site/guardaserie/Player/ScrapeSerie.py +0 -110
- StreamingCommunity/Src/Api/Template/__init__.py +0 -3
- StreamingCommunity/Src/Lib/TMBD/__init__.py +0 -2
- StreamingCommunity-1.7.6.dist-info/RECORD +0 -97
- {StreamingCommunity-1.7.6.dist-info → StreamingCommunity-1.9.1.dist-info}/LICENSE +0 -0
- {StreamingCommunity-1.7.6.dist-info → StreamingCommunity-1.9.1.dist-info}/entry_points.txt +0 -0
- {StreamingCommunity-1.7.6.dist-info → StreamingCommunity-1.9.1.dist-info}/top_level.txt +0 -0
|
@@ -1,666 +1,666 @@
|
|
|
1
|
-
# 20.04.25
|
|
2
|
-
|
|
3
|
-
import sys
|
|
4
|
-
import logging
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
# Internal utilities
|
|
8
|
-
from m3u8 import loads
|
|
9
|
-
from StreamingCommunity.
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
# External libraries
|
|
13
|
-
import httpx
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
# Costant
|
|
17
|
-
CODEC_MAPPINGS = {
|
|
18
|
-
"video": {
|
|
19
|
-
"avc1": "libx264",
|
|
20
|
-
"avc2": "libx264",
|
|
21
|
-
"avc3": "libx264",
|
|
22
|
-
"avc4": "libx264",
|
|
23
|
-
"hev1": "libx265",
|
|
24
|
-
"hev2": "libx265",
|
|
25
|
-
"hvc1": "libx265",
|
|
26
|
-
"hvc2": "libx265",
|
|
27
|
-
"vp8": "libvpx",
|
|
28
|
-
"vp9": "libvpx-vp9",
|
|
29
|
-
"vp10": "libvpx-vp9"
|
|
30
|
-
},
|
|
31
|
-
"audio": {
|
|
32
|
-
"mp4a": "aac",
|
|
33
|
-
"mp3": "libmp3lame",
|
|
34
|
-
"ac-3": "ac3",
|
|
35
|
-
"ec-3": "eac3",
|
|
36
|
-
"opus": "libopus",
|
|
37
|
-
"vorbis": "libvorbis"
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
RESOLUTIONS = [
|
|
42
|
-
(7680, 4320),
|
|
43
|
-
(3840, 2160),
|
|
44
|
-
(2560, 1440),
|
|
45
|
-
(1920, 1080),
|
|
46
|
-
(1280, 720),
|
|
47
|
-
(640, 480)
|
|
48
|
-
]
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
class M3U8_Codec:
|
|
53
|
-
def __init__(self, bandwidth, codecs):
|
|
54
|
-
"""
|
|
55
|
-
Initializes the M3U8Codec object with the provided parameters.
|
|
56
|
-
|
|
57
|
-
Parameters:
|
|
58
|
-
- bandwidth (int): Bandwidth of the codec.
|
|
59
|
-
- codecs (str): Codecs information in the format "avc1.xxxxxx,mp4a.xx".
|
|
60
|
-
"""
|
|
61
|
-
self.bandwidth = bandwidth
|
|
62
|
-
self.codecs = codecs
|
|
63
|
-
self.audio_codec = None
|
|
64
|
-
self.video_codec = None
|
|
65
|
-
self.video_codec_name = None
|
|
66
|
-
self.audio_codec_name = None
|
|
67
|
-
self.extract_codecs()
|
|
68
|
-
self.parse_codecs()
|
|
69
|
-
self.calculate_bitrates()
|
|
70
|
-
|
|
71
|
-
def extract_codecs(self):
|
|
72
|
-
"""
|
|
73
|
-
Parses the codecs information to extract audio and video codecs.
|
|
74
|
-
Extracted codecs are set as attributes: audio_codec and video_codec.
|
|
75
|
-
"""
|
|
76
|
-
try:
|
|
77
|
-
# Split the codecs string by comma
|
|
78
|
-
codecs_list = self.codecs.split(',')
|
|
79
|
-
except Exception as e:
|
|
80
|
-
logging.error(f"Can't split codec list: {self.codecs} with error {e}")
|
|
81
|
-
return
|
|
82
|
-
|
|
83
|
-
# Separate audio and video codecs
|
|
84
|
-
for codec in codecs_list:
|
|
85
|
-
if codec.startswith('avc'):
|
|
86
|
-
self.video_codec = codec
|
|
87
|
-
elif codec.startswith('mp4a'):
|
|
88
|
-
self.audio_codec = codec
|
|
89
|
-
|
|
90
|
-
def convert_video_codec(self, video_codec_identifier) -> str:
|
|
91
|
-
"""
|
|
92
|
-
Convert video codec identifier to codec name.
|
|
93
|
-
|
|
94
|
-
Parameters:
|
|
95
|
-
- video_codec_identifier (str): Identifier of the video codec.
|
|
96
|
-
|
|
97
|
-
Returns:
|
|
98
|
-
str: Codec name corresponding to the identifier.
|
|
99
|
-
"""
|
|
100
|
-
if not video_codec_identifier:
|
|
101
|
-
logging.warning("No video codec identifier provided. Using default codec libx264.")
|
|
102
|
-
return "libx264" # Default
|
|
103
|
-
|
|
104
|
-
# Extract codec type from the identifier
|
|
105
|
-
codec_type = video_codec_identifier.split('.')[0]
|
|
106
|
-
|
|
107
|
-
# Retrieve codec mapping from the provided mappings or fallback to static mappings
|
|
108
|
-
video_codec_mapping = CODEC_MAPPINGS.get('video', {})
|
|
109
|
-
codec_name = video_codec_mapping.get(codec_type)
|
|
110
|
-
|
|
111
|
-
if codec_name:
|
|
112
|
-
return codec_name
|
|
113
|
-
else:
|
|
114
|
-
logging.warning(f"No corresponding video codec found for {video_codec_identifier}. Using default codec libx264.")
|
|
115
|
-
return "libx264" # Default
|
|
116
|
-
|
|
117
|
-
def convert_audio_codec(self, audio_codec_identifier) -> str:
|
|
118
|
-
"""
|
|
119
|
-
Convert audio codec identifier to codec name.
|
|
120
|
-
|
|
121
|
-
Parameters:
|
|
122
|
-
- audio_codec_identifier (str): Identifier of the audio codec.
|
|
123
|
-
|
|
124
|
-
Returns:
|
|
125
|
-
str: Codec name corresponding to the identifier.
|
|
126
|
-
"""
|
|
127
|
-
if not audio_codec_identifier:
|
|
128
|
-
logging.warning("No audio codec identifier provided. Using default codec aac.")
|
|
129
|
-
return "aac" # Default
|
|
130
|
-
|
|
131
|
-
# Extract codec type from the identifier
|
|
132
|
-
codec_type = audio_codec_identifier.split('.')[0]
|
|
133
|
-
|
|
134
|
-
# Retrieve codec mapping from the provided mappings or fallback to static mappings
|
|
135
|
-
audio_codec_mapping = CODEC_MAPPINGS.get('audio', {})
|
|
136
|
-
codec_name = audio_codec_mapping.get(codec_type)
|
|
137
|
-
|
|
138
|
-
if codec_name:
|
|
139
|
-
return codec_name
|
|
140
|
-
else:
|
|
141
|
-
logging.warning(f"No corresponding audio codec found for {audio_codec_identifier}. Using default codec aac.")
|
|
142
|
-
return "aac" # Default
|
|
143
|
-
|
|
144
|
-
def parse_codecs(self):
|
|
145
|
-
"""
|
|
146
|
-
Parse video and audio codecs.
|
|
147
|
-
This method updates `video_codec_name` and `audio_codec_name` attributes.
|
|
148
|
-
"""
|
|
149
|
-
self.video_codec_name = self.convert_video_codec(self.video_codec)
|
|
150
|
-
self.audio_codec_name = self.convert_audio_codec(self.audio_codec)
|
|
151
|
-
|
|
152
|
-
def calculate_bitrates(self):
|
|
153
|
-
"""
|
|
154
|
-
Calculate video and audio bitrates based on the available bandwidth.
|
|
155
|
-
"""
|
|
156
|
-
if self.bandwidth:
|
|
157
|
-
|
|
158
|
-
# Define the video and audio bitrates
|
|
159
|
-
video_bitrate = int(self.bandwidth * 0.8) # Using 80% of bandwidth for video
|
|
160
|
-
audio_bitrate = self.bandwidth - video_bitrate
|
|
161
|
-
|
|
162
|
-
self.video_bitrate = video_bitrate
|
|
163
|
-
self.audio_bitrate = audio_bitrate
|
|
164
|
-
else:
|
|
165
|
-
logging.warning("No bandwidth provided. Bitrates cannot be calculated.")
|
|
166
|
-
|
|
167
|
-
def __str__(self):
|
|
168
|
-
return (f"M3U8_Codec(bandwidth={self.bandwidth}, "
|
|
169
|
-
f"codecs='{self.codecs}', "
|
|
170
|
-
f"audio_codec='{self.audio_codec}', "
|
|
171
|
-
f"video_codec='{self.video_codec}', "
|
|
172
|
-
f"audio_codec_name='{self.audio_codec_name}', "
|
|
173
|
-
f"video_codec_name='{self.video_codec_name}')")
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
class M3U8_Video:
|
|
177
|
-
def __init__(self, video_playlist) -> None:
|
|
178
|
-
"""
|
|
179
|
-
Initializes an M3U8_Video object with the provided video playlist.
|
|
180
|
-
|
|
181
|
-
Parameters:
|
|
182
|
-
- video_playlist (M3U8): An M3U8 object representing the video playlist.
|
|
183
|
-
"""
|
|
184
|
-
self.video_playlist = video_playlist
|
|
185
|
-
|
|
186
|
-
def get_best_uri(self):
|
|
187
|
-
"""
|
|
188
|
-
Returns the URI with the highest resolution from the video playlist.
|
|
189
|
-
|
|
190
|
-
Returns:
|
|
191
|
-
tuple or None: A tuple containing the URI with the highest resolution and its resolution value, or None if the video list is empty.
|
|
192
|
-
"""
|
|
193
|
-
if not self.video_playlist:
|
|
194
|
-
return None
|
|
195
|
-
|
|
196
|
-
best_uri = max(self.video_playlist, key=lambda x: x['resolution'])
|
|
197
|
-
return best_uri['uri'], best_uri['resolution']
|
|
198
|
-
|
|
199
|
-
def get_worst_uri(self):
|
|
200
|
-
"""
|
|
201
|
-
Returns the URI with the lowest resolution from the video playlist.
|
|
202
|
-
|
|
203
|
-
Returns:
|
|
204
|
-
- tuple or None: A tuple containing the URI with the lowest resolution and its resolution value, or None if the video list is empty.
|
|
205
|
-
"""
|
|
206
|
-
if not self.video_playlist:
|
|
207
|
-
return None
|
|
208
|
-
|
|
209
|
-
worst_uri = min(self.video_playlist, key=lambda x: x['resolution'])
|
|
210
|
-
return worst_uri['uri'], worst_uri['resolution']
|
|
211
|
-
|
|
212
|
-
def get_custom_uri(self, y_resolution):
|
|
213
|
-
"""
|
|
214
|
-
Returns the URI corresponding to a custom resolution from the video list.
|
|
215
|
-
|
|
216
|
-
Parameters:
|
|
217
|
-
- video_list (list): A list of dictionaries containing video URIs and resolutions.
|
|
218
|
-
- custom_resolution (tuple): A tuple representing the custom resolution.
|
|
219
|
-
|
|
220
|
-
Returns:
|
|
221
|
-
str or None: The URI corresponding to the custom resolution, or None if not found.
|
|
222
|
-
"""
|
|
223
|
-
for video in self.video_playlist:
|
|
224
|
-
logging.info(f"Check resolution from playlist: {int(video['resolution'][1])}, with input: {int(y_resolution)}")
|
|
225
|
-
|
|
226
|
-
if int(video['resolution'][1]) == int(y_resolution):
|
|
227
|
-
return video['uri'], video['resolution']
|
|
228
|
-
|
|
229
|
-
return None, None
|
|
230
|
-
|
|
231
|
-
def get_list_resolution(self):
|
|
232
|
-
"""
|
|
233
|
-
Retrieve a list of resolutions from the video playlist.
|
|
234
|
-
|
|
235
|
-
Returns:
|
|
236
|
-
list: A list of resolutions extracted from the video playlist.
|
|
237
|
-
"""
|
|
238
|
-
return [video['resolution'] for video in self.video_playlist]
|
|
239
|
-
|
|
240
|
-
def get_list_resolution_and_size(self, duration):
|
|
241
|
-
"""
|
|
242
|
-
Retrieve a list of resolutions and size from the video playlist.
|
|
243
|
-
|
|
244
|
-
Parameters:
|
|
245
|
-
- duration (int): Total duration of the video in 's'.
|
|
246
|
-
|
|
247
|
-
Returns:
|
|
248
|
-
list: A list of resolutions extracted from the video playlist.
|
|
249
|
-
"""
|
|
250
|
-
result = []
|
|
251
|
-
|
|
252
|
-
for video in self.video_playlist:
|
|
253
|
-
video_size = internet_manager.format_file_size((video['bandwidth'] * duration) / 8)
|
|
254
|
-
result.append((video_size))
|
|
255
|
-
|
|
256
|
-
return result
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
class M3U8_Audio:
|
|
260
|
-
def __init__(self, audio_playlist) -> None:
|
|
261
|
-
"""
|
|
262
|
-
Initializes an M3U8_Audio object with the provided audio playlist.
|
|
263
|
-
|
|
264
|
-
Parameters:
|
|
265
|
-
- audio_playlist (M3U8): An M3U8 object representing the audio playlist.
|
|
266
|
-
"""
|
|
267
|
-
self.audio_playlist = audio_playlist
|
|
268
|
-
|
|
269
|
-
def get_uri_by_language(self, language):
|
|
270
|
-
"""
|
|
271
|
-
Returns a dictionary with 'name' and 'uri' given a specific language.
|
|
272
|
-
|
|
273
|
-
Parameters:
|
|
274
|
-
- audio_list (list): List of dictionaries containing audio information.
|
|
275
|
-
- language (str): The desired language.
|
|
276
|
-
|
|
277
|
-
Returns:
|
|
278
|
-
dict or None: Dictionary with 'name', 'language', and 'uri' for the specified language, or None if not found.
|
|
279
|
-
"""
|
|
280
|
-
for audio in self.audio_playlist:
|
|
281
|
-
if audio['language'] == language:
|
|
282
|
-
return {'name': audio['name'], 'language': audio['language'], 'uri': audio['uri']}
|
|
283
|
-
return None
|
|
284
|
-
|
|
285
|
-
def get_all_uris_and_names(self):
|
|
286
|
-
"""
|
|
287
|
-
Returns a list of dictionaries containing all URIs and names.
|
|
288
|
-
|
|
289
|
-
Parameters:
|
|
290
|
-
- audio_list (list): List of dictionaries containing audio information.
|
|
291
|
-
|
|
292
|
-
Returns:
|
|
293
|
-
list: List of dictionaries containing 'name', 'language', and 'uri' for all audio in the list.
|
|
294
|
-
"""
|
|
295
|
-
audios_list = [{'name': audio['name'], 'language': audio['language'], 'uri': audio['uri']} for audio in self.audio_playlist]
|
|
296
|
-
unique_audios_dict = {}
|
|
297
|
-
|
|
298
|
-
# Remove duplicate
|
|
299
|
-
for audio in audios_list:
|
|
300
|
-
unique_audios_dict[audio['language']] = audio
|
|
301
|
-
|
|
302
|
-
return list(unique_audios_dict.values())
|
|
303
|
-
|
|
304
|
-
def get_default_uri(self):
|
|
305
|
-
"""
|
|
306
|
-
Returns the dictionary with 'default' equal to 'YES'.
|
|
307
|
-
|
|
308
|
-
Parameters:
|
|
309
|
-
- audio_list (list): List of dictionaries containing audio information.
|
|
310
|
-
|
|
311
|
-
Returns:
|
|
312
|
-
dict or None: Dictionary with 'default' equal to 'YES', or None if not found.
|
|
313
|
-
"""
|
|
314
|
-
for audio in self.audio_playlist:
|
|
315
|
-
if audio['default'] == 'YES':
|
|
316
|
-
return audio.get('uri')
|
|
317
|
-
return None
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
class M3U8_Subtitle:
|
|
321
|
-
def __init__(self, subtitle_playlist) -> None:
|
|
322
|
-
"""
|
|
323
|
-
Initializes an M3U8_Subtitle object with the provided subtitle playlist.
|
|
324
|
-
|
|
325
|
-
Parameters:
|
|
326
|
-
- subtitle_playlist (M3U8): An M3U8 object representing the subtitle playlist.
|
|
327
|
-
"""
|
|
328
|
-
self.subtitle_playlist = subtitle_playlist
|
|
329
|
-
|
|
330
|
-
def get_uri_by_language(self, language):
|
|
331
|
-
"""
|
|
332
|
-
Returns a dictionary with 'name' and 'uri' given a specific language for subtitles.
|
|
333
|
-
|
|
334
|
-
Parameters:
|
|
335
|
-
- subtitle_list (list): List of dictionaries containing subtitle information.
|
|
336
|
-
- language (str): The desired language.
|
|
337
|
-
|
|
338
|
-
Returns:
|
|
339
|
-
dict or None: Dictionary with 'name' and 'uri' for the specified language for subtitles, or None if not found.
|
|
340
|
-
"""
|
|
341
|
-
for subtitle in self.subtitle_playlist:
|
|
342
|
-
if subtitle['language'] == language:
|
|
343
|
-
return {'name': subtitle['name'], 'uri': subtitle['uri']}
|
|
344
|
-
return None
|
|
345
|
-
|
|
346
|
-
def get_all_uris_and_names(self):
|
|
347
|
-
"""
|
|
348
|
-
Returns a list of dictionaries containing all URIs and names of subtitles.
|
|
349
|
-
|
|
350
|
-
Parameters:
|
|
351
|
-
- subtitle_list (list): List of dictionaries containing subtitle information.
|
|
352
|
-
|
|
353
|
-
Returns:
|
|
354
|
-
list: List of dictionaries containing 'name' and 'uri' for all subtitles in the list.
|
|
355
|
-
"""
|
|
356
|
-
subtitles_list = [{'name': subtitle['name'], 'language': subtitle['language'], 'uri': subtitle['uri']} for subtitle in self.subtitle_playlist]
|
|
357
|
-
unique_subtitles_dict = {}
|
|
358
|
-
|
|
359
|
-
# Remove duplicate
|
|
360
|
-
for subtitle in subtitles_list:
|
|
361
|
-
unique_subtitles_dict[subtitle['language']] = subtitle
|
|
362
|
-
|
|
363
|
-
return list(unique_subtitles_dict.values())
|
|
364
|
-
|
|
365
|
-
def get_default_uri(self):
|
|
366
|
-
"""
|
|
367
|
-
Returns the dictionary with 'default' equal to 'YES' for subtitles.
|
|
368
|
-
|
|
369
|
-
Parameters:
|
|
370
|
-
- subtitle_list (list): List of dictionaries containing subtitle information.
|
|
371
|
-
|
|
372
|
-
Returns:
|
|
373
|
-
dict or None: Dictionary with 'default' equal to 'YES' for subtitles, or None if not found.
|
|
374
|
-
"""
|
|
375
|
-
for subtitle in self.subtitle_playlist:
|
|
376
|
-
if subtitle['default'] == 'YES':
|
|
377
|
-
return subtitle
|
|
378
|
-
return None
|
|
379
|
-
|
|
380
|
-
def download_all(self, custom_subtitle):
|
|
381
|
-
"""
|
|
382
|
-
Download all subtitles listed in the object's attributes, filtering based on a provided list of custom subtitles.
|
|
383
|
-
|
|
384
|
-
Parameters:
|
|
385
|
-
- custom_subtitle (list): A list of custom subtitles to download.
|
|
386
|
-
|
|
387
|
-
Returns:
|
|
388
|
-
list: A list containing dictionaries with subtitle information including name, language, and URI.
|
|
389
|
-
"""
|
|
390
|
-
|
|
391
|
-
output = [] # Initialize an empty list to store subtitle information
|
|
392
|
-
|
|
393
|
-
# Iterate through all available subtitles
|
|
394
|
-
for obj_subtitle in self.subtitle_get_all_uris_and_names():
|
|
395
|
-
|
|
396
|
-
# Check if the subtitle name is not in the list of custom subtitles, and skip if not found
|
|
397
|
-
if obj_subtitle.get('name') not in custom_subtitle:
|
|
398
|
-
continue
|
|
399
|
-
|
|
400
|
-
# Send a request to retrieve the subtitle content
|
|
401
|
-
logging.info(f"Download subtitle: {obj_subtitle.get('name')}")
|
|
402
|
-
response_subitle = httpx.get(obj_subtitle.get('uri'))
|
|
403
|
-
|
|
404
|
-
try:
|
|
405
|
-
# Try to extract the VTT URL from the subtitle content
|
|
406
|
-
sub_parse = M3U8_Parser()
|
|
407
|
-
sub_parse.parse_data(obj_subtitle.get('uri'), response_subitle.text)
|
|
408
|
-
url_subititle = sub_parse.subtitle[0]
|
|
409
|
-
|
|
410
|
-
output.append({
|
|
411
|
-
'name': obj_subtitle.get('name'),
|
|
412
|
-
'language': obj_subtitle.get('language'),
|
|
413
|
-
'uri': url_subititle
|
|
414
|
-
})
|
|
415
|
-
|
|
416
|
-
except Exception as e:
|
|
417
|
-
logging.error(f"Cant download: {obj_subtitle.get('name')}, error: {e}")
|
|
418
|
-
|
|
419
|
-
return output
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
class M3U8_Parser:
|
|
423
|
-
def __init__(self):
|
|
424
|
-
self.is_master_playlist = None
|
|
425
|
-
self.segments = []
|
|
426
|
-
self.video_playlist = []
|
|
427
|
-
self.keys = None
|
|
428
|
-
self.subtitle_playlist = []
|
|
429
|
-
self.subtitle = []
|
|
430
|
-
self.audio_playlist = []
|
|
431
|
-
self.codec: M3U8_Codec = None
|
|
432
|
-
self._video: M3U8_Video = None
|
|
433
|
-
self._audio: M3U8_Audio = None
|
|
434
|
-
self._subtitle: M3U8_Subtitle = None
|
|
435
|
-
self.duration: float = 0
|
|
436
|
-
|
|
437
|
-
self.__create_variable__()
|
|
438
|
-
|
|
439
|
-
def parse_data(self, uri, raw_content) -> None:
|
|
440
|
-
"""
|
|
441
|
-
Extracts all information present in the provided M3U8 content.
|
|
442
|
-
|
|
443
|
-
Parameters:
|
|
444
|
-
- m3u8_content (str): The content of the M3U8 file.
|
|
445
|
-
"""
|
|
446
|
-
|
|
447
|
-
# Get obj of the m3u8 text content download, dictionary with video, audio, segments, subtitles
|
|
448
|
-
m3u8_obj = loads(raw_content, uri)
|
|
449
|
-
|
|
450
|
-
self.__parse_video_info__(m3u8_obj)
|
|
451
|
-
self.__parse_subtitles_and_audio__(m3u8_obj)
|
|
452
|
-
self.__parse_segments__(m3u8_obj)
|
|
453
|
-
self.is_master_playlist = self.__is_master__(m3u8_obj)
|
|
454
|
-
|
|
455
|
-
@staticmethod
|
|
456
|
-
def extract_resolution(uri: str) -> int:
|
|
457
|
-
"""
|
|
458
|
-
Extracts the video resolution from the given URI.
|
|
459
|
-
|
|
460
|
-
Parameters:
|
|
461
|
-
- uri (str): The URI containing video information.
|
|
462
|
-
|
|
463
|
-
Returns:
|
|
464
|
-
int: The video resolution if found, otherwise 0.
|
|
465
|
-
"""
|
|
466
|
-
|
|
467
|
-
# Log
|
|
468
|
-
logging.info(f"Try extract resolution from: {uri}")
|
|
469
|
-
|
|
470
|
-
for resolution in RESOLUTIONS:
|
|
471
|
-
if "http" in str(uri):
|
|
472
|
-
if str(resolution[1]) in uri:
|
|
473
|
-
return resolution
|
|
474
|
-
|
|
475
|
-
# Default resolution return (not best)
|
|
476
|
-
logging.warning("No resolution found with custom parsing.")
|
|
477
|
-
return (0, 0)
|
|
478
|
-
|
|
479
|
-
def __is_master__(self, m3u8_obj) -> bool:
|
|
480
|
-
"""
|
|
481
|
-
Determines if the given M3U8 object is a master playlist.
|
|
482
|
-
|
|
483
|
-
Parameters:
|
|
484
|
-
- m3u8_obj (m3u8.M3U8): The parsed M3U8 object.
|
|
485
|
-
|
|
486
|
-
Returns:
|
|
487
|
-
- bool: True if it's a master playlist, False if it's a media playlist, None if unknown.
|
|
488
|
-
"""
|
|
489
|
-
|
|
490
|
-
# Check if the playlist contains variants (master playlist)
|
|
491
|
-
if m3u8_obj.is_variant:
|
|
492
|
-
return True
|
|
493
|
-
|
|
494
|
-
# Check if the playlist contains segments directly (media playlist)
|
|
495
|
-
elif m3u8_obj.segments:
|
|
496
|
-
return False
|
|
497
|
-
|
|
498
|
-
# Return None if the playlist type is undetermined
|
|
499
|
-
return None
|
|
500
|
-
|
|
501
|
-
def __parse_video_info__(self, m3u8_obj) -> None:
|
|
502
|
-
"""
|
|
503
|
-
Extracts video information from the M3U8 object.
|
|
504
|
-
|
|
505
|
-
Parameters:
|
|
506
|
-
- m3u8_obj: The M3U8 object containing video playlists.
|
|
507
|
-
"""
|
|
508
|
-
|
|
509
|
-
try:
|
|
510
|
-
for playlist in m3u8_obj.playlists:
|
|
511
|
-
|
|
512
|
-
there_is_codec = not playlist.stream_info.codecs is None
|
|
513
|
-
logging.info(f"There is coded: {there_is_codec}")
|
|
514
|
-
|
|
515
|
-
if there_is_codec:
|
|
516
|
-
self.codec = M3U8_Codec(
|
|
517
|
-
playlist.stream_info.bandwidth,
|
|
518
|
-
playlist.stream_info.codecs
|
|
519
|
-
)
|
|
520
|
-
|
|
521
|
-
# Direct access resolutions in m3u8 obj
|
|
522
|
-
if playlist.stream_info.resolution is not None:
|
|
523
|
-
|
|
524
|
-
self.video_playlist.append({
|
|
525
|
-
"uri": playlist.uri,
|
|
526
|
-
"resolution": playlist.stream_info.resolution,
|
|
527
|
-
"bandwidth": playlist.stream_info.bandwidth
|
|
528
|
-
})
|
|
529
|
-
|
|
530
|
-
if there_is_codec:
|
|
531
|
-
self.codec.resolution = playlist.stream_info.resolution
|
|
532
|
-
|
|
533
|
-
# Find resolutions in uri
|
|
534
|
-
else:
|
|
535
|
-
|
|
536
|
-
self.video_playlist.append({
|
|
537
|
-
"uri": playlist.uri,
|
|
538
|
-
"resolution": M3U8_Parser.extract_resolution(playlist.uri),
|
|
539
|
-
"bandwidth": playlist.stream_info.bandwidth
|
|
540
|
-
})
|
|
541
|
-
|
|
542
|
-
if there_is_codec:
|
|
543
|
-
self.codec.resolution = M3U8_Parser.extract_resolution(playlist.uri)
|
|
544
|
-
|
|
545
|
-
continue
|
|
546
|
-
|
|
547
|
-
except Exception as e:
|
|
548
|
-
logging.error(f"Error parsing video info: {e}")
|
|
549
|
-
|
|
550
|
-
def __parse_encryption_keys__(self, m3u8_obj) -> None:
|
|
551
|
-
"""
|
|
552
|
-
Extracts encryption keys from the M3U8 object.
|
|
553
|
-
|
|
554
|
-
Parameters:
|
|
555
|
-
- m3u8_obj: The M3U8 object containing encryption keys.
|
|
556
|
-
"""
|
|
557
|
-
try:
|
|
558
|
-
|
|
559
|
-
if m3u8_obj.key is not None:
|
|
560
|
-
if self.keys is None:
|
|
561
|
-
self.keys = {
|
|
562
|
-
'method': m3u8_obj.key.method,
|
|
563
|
-
'iv': m3u8_obj.key.iv,
|
|
564
|
-
'uri': m3u8_obj.key.uri
|
|
565
|
-
}
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
except Exception as e:
|
|
569
|
-
logging.error(f"Error parsing encryption keys: {e}")
|
|
570
|
-
sys.exit(0)
|
|
571
|
-
pass
|
|
572
|
-
|
|
573
|
-
def __parse_subtitles_and_audio__(self, m3u8_obj) -> None:
|
|
574
|
-
"""
|
|
575
|
-
Extracts subtitles and audio information from the M3U8 object.
|
|
576
|
-
|
|
577
|
-
Parameters:
|
|
578
|
-
- m3u8_obj: The M3U8 object containing subtitles and audio data.
|
|
579
|
-
"""
|
|
580
|
-
try:
|
|
581
|
-
for media in m3u8_obj.media:
|
|
582
|
-
if media.type == "SUBTITLES":
|
|
583
|
-
self.subtitle_playlist.append({
|
|
584
|
-
"type": media.type,
|
|
585
|
-
"name": media.name,
|
|
586
|
-
"default": media.default,
|
|
587
|
-
"language": media.language,
|
|
588
|
-
"uri": media.uri
|
|
589
|
-
})
|
|
590
|
-
|
|
591
|
-
if media.type == "AUDIO":
|
|
592
|
-
self.audio_playlist.append({
|
|
593
|
-
"type": media.type,
|
|
594
|
-
"name": media.name,
|
|
595
|
-
"default": media.default,
|
|
596
|
-
"language": media.language,
|
|
597
|
-
"uri": media.uri
|
|
598
|
-
})
|
|
599
|
-
|
|
600
|
-
except Exception as e:
|
|
601
|
-
logging.error(f"Error parsing subtitles and audio: {e}")
|
|
602
|
-
|
|
603
|
-
def __parse_segments__(self, m3u8_obj) -> None:
|
|
604
|
-
"""
|
|
605
|
-
Extracts segment information from the M3U8 object.
|
|
606
|
-
|
|
607
|
-
Parameters:
|
|
608
|
-
- m3u8_obj: The M3U8 object containing segment data.
|
|
609
|
-
"""
|
|
610
|
-
|
|
611
|
-
try:
|
|
612
|
-
for segment in m3u8_obj.segments:
|
|
613
|
-
|
|
614
|
-
# Parse key
|
|
615
|
-
self.__parse_encryption_keys__(segment)
|
|
616
|
-
|
|
617
|
-
# Collect all index duration
|
|
618
|
-
self.duration += segment.duration
|
|
619
|
-
|
|
620
|
-
if "vtt" not in segment.uri:
|
|
621
|
-
self.segments.append(segment.uri)
|
|
622
|
-
else:
|
|
623
|
-
self.subtitle.append(segment.uri)
|
|
624
|
-
|
|
625
|
-
except Exception as e:
|
|
626
|
-
logging.error(f"Error parsing segments: {e}")
|
|
627
|
-
|
|
628
|
-
def __create_variable__(self):
|
|
629
|
-
"""
|
|
630
|
-
Initialize variables for video, audio, and subtitle playlists.
|
|
631
|
-
"""
|
|
632
|
-
|
|
633
|
-
self._video = M3U8_Video(self.video_playlist)
|
|
634
|
-
self._audio = M3U8_Audio(self.audio_playlist)
|
|
635
|
-
self._subtitle = M3U8_Subtitle(self.subtitle_playlist)
|
|
636
|
-
|
|
637
|
-
def get_duration(self, return_string:bool = True):
|
|
638
|
-
"""
|
|
639
|
-
Convert duration from seconds to hours, minutes, and remaining seconds.
|
|
640
|
-
|
|
641
|
-
Parameters:
|
|
642
|
-
- return_string (bool): If True, returns the formatted duration string.
|
|
643
|
-
If False, returns a dictionary with hours, minutes, and seconds.
|
|
644
|
-
|
|
645
|
-
Returns:
|
|
646
|
-
- formatted_duration (str): Formatted duration string with hours, minutes, and seconds if return_string is True.
|
|
647
|
-
- duration_dict (dict): Dictionary with keys 'h', 'm', 's' representing hours, minutes, and seconds respectively if return_string is False.
|
|
648
|
-
|
|
649
|
-
Example usage:
|
|
650
|
-
>>> obj = YourClass(duration=3661)
|
|
651
|
-
>>> obj.get_duration()
|
|
652
|
-
'[yellow]1[red]h [yellow]1[red]m [yellow]1[red]s'
|
|
653
|
-
>>> obj.get_duration(return_string=False)
|
|
654
|
-
{'h': 1, 'm': 1, 's': 1}
|
|
655
|
-
"""
|
|
656
|
-
|
|
657
|
-
# Calculate hours, minutes, and remaining seconds
|
|
658
|
-
hours, remainder = divmod(self.duration, 3600)
|
|
659
|
-
minutes, seconds = divmod(remainder, 60)
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
# Format the duration string with colors
|
|
663
|
-
if return_string:
|
|
664
|
-
return f"[yellow]{int(hours)}[red]h [yellow]{int(minutes)}[red]m [yellow]{int(seconds)}[red]s"
|
|
665
|
-
else:
|
|
666
|
-
return {'h': int(hours), 'm': int(minutes), 's': int(seconds)}
|
|
1
|
+
# 20.04.25
|
|
2
|
+
|
|
3
|
+
import sys
|
|
4
|
+
import logging
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
# Internal utilities
|
|
8
|
+
from m3u8 import loads
|
|
9
|
+
from StreamingCommunity.Util.os import internet_manager
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
# External libraries
|
|
13
|
+
import httpx
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
# Costant
|
|
17
|
+
CODEC_MAPPINGS = {
|
|
18
|
+
"video": {
|
|
19
|
+
"avc1": "libx264",
|
|
20
|
+
"avc2": "libx264",
|
|
21
|
+
"avc3": "libx264",
|
|
22
|
+
"avc4": "libx264",
|
|
23
|
+
"hev1": "libx265",
|
|
24
|
+
"hev2": "libx265",
|
|
25
|
+
"hvc1": "libx265",
|
|
26
|
+
"hvc2": "libx265",
|
|
27
|
+
"vp8": "libvpx",
|
|
28
|
+
"vp9": "libvpx-vp9",
|
|
29
|
+
"vp10": "libvpx-vp9"
|
|
30
|
+
},
|
|
31
|
+
"audio": {
|
|
32
|
+
"mp4a": "aac",
|
|
33
|
+
"mp3": "libmp3lame",
|
|
34
|
+
"ac-3": "ac3",
|
|
35
|
+
"ec-3": "eac3",
|
|
36
|
+
"opus": "libopus",
|
|
37
|
+
"vorbis": "libvorbis"
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
RESOLUTIONS = [
|
|
42
|
+
(7680, 4320),
|
|
43
|
+
(3840, 2160),
|
|
44
|
+
(2560, 1440),
|
|
45
|
+
(1920, 1080),
|
|
46
|
+
(1280, 720),
|
|
47
|
+
(640, 480)
|
|
48
|
+
]
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class M3U8_Codec:
|
|
53
|
+
def __init__(self, bandwidth, codecs):
|
|
54
|
+
"""
|
|
55
|
+
Initializes the M3U8Codec object with the provided parameters.
|
|
56
|
+
|
|
57
|
+
Parameters:
|
|
58
|
+
- bandwidth (int): Bandwidth of the codec.
|
|
59
|
+
- codecs (str): Codecs information in the format "avc1.xxxxxx,mp4a.xx".
|
|
60
|
+
"""
|
|
61
|
+
self.bandwidth = bandwidth
|
|
62
|
+
self.codecs = codecs
|
|
63
|
+
self.audio_codec = None
|
|
64
|
+
self.video_codec = None
|
|
65
|
+
self.video_codec_name = None
|
|
66
|
+
self.audio_codec_name = None
|
|
67
|
+
self.extract_codecs()
|
|
68
|
+
self.parse_codecs()
|
|
69
|
+
self.calculate_bitrates()
|
|
70
|
+
|
|
71
|
+
def extract_codecs(self):
|
|
72
|
+
"""
|
|
73
|
+
Parses the codecs information to extract audio and video codecs.
|
|
74
|
+
Extracted codecs are set as attributes: audio_codec and video_codec.
|
|
75
|
+
"""
|
|
76
|
+
try:
|
|
77
|
+
# Split the codecs string by comma
|
|
78
|
+
codecs_list = self.codecs.split(',')
|
|
79
|
+
except Exception as e:
|
|
80
|
+
logging.error(f"Can't split codec list: {self.codecs} with error {e}")
|
|
81
|
+
return
|
|
82
|
+
|
|
83
|
+
# Separate audio and video codecs
|
|
84
|
+
for codec in codecs_list:
|
|
85
|
+
if codec.startswith('avc'):
|
|
86
|
+
self.video_codec = codec
|
|
87
|
+
elif codec.startswith('mp4a'):
|
|
88
|
+
self.audio_codec = codec
|
|
89
|
+
|
|
90
|
+
def convert_video_codec(self, video_codec_identifier) -> str:
|
|
91
|
+
"""
|
|
92
|
+
Convert video codec identifier to codec name.
|
|
93
|
+
|
|
94
|
+
Parameters:
|
|
95
|
+
- video_codec_identifier (str): Identifier of the video codec.
|
|
96
|
+
|
|
97
|
+
Returns:
|
|
98
|
+
str: Codec name corresponding to the identifier.
|
|
99
|
+
"""
|
|
100
|
+
if not video_codec_identifier:
|
|
101
|
+
logging.warning("No video codec identifier provided. Using default codec libx264.")
|
|
102
|
+
return "libx264" # Default
|
|
103
|
+
|
|
104
|
+
# Extract codec type from the identifier
|
|
105
|
+
codec_type = video_codec_identifier.split('.')[0]
|
|
106
|
+
|
|
107
|
+
# Retrieve codec mapping from the provided mappings or fallback to static mappings
|
|
108
|
+
video_codec_mapping = CODEC_MAPPINGS.get('video', {})
|
|
109
|
+
codec_name = video_codec_mapping.get(codec_type)
|
|
110
|
+
|
|
111
|
+
if codec_name:
|
|
112
|
+
return codec_name
|
|
113
|
+
else:
|
|
114
|
+
logging.warning(f"No corresponding video codec found for {video_codec_identifier}. Using default codec libx264.")
|
|
115
|
+
return "libx264" # Default
|
|
116
|
+
|
|
117
|
+
def convert_audio_codec(self, audio_codec_identifier) -> str:
|
|
118
|
+
"""
|
|
119
|
+
Convert audio codec identifier to codec name.
|
|
120
|
+
|
|
121
|
+
Parameters:
|
|
122
|
+
- audio_codec_identifier (str): Identifier of the audio codec.
|
|
123
|
+
|
|
124
|
+
Returns:
|
|
125
|
+
str: Codec name corresponding to the identifier.
|
|
126
|
+
"""
|
|
127
|
+
if not audio_codec_identifier:
|
|
128
|
+
logging.warning("No audio codec identifier provided. Using default codec aac.")
|
|
129
|
+
return "aac" # Default
|
|
130
|
+
|
|
131
|
+
# Extract codec type from the identifier
|
|
132
|
+
codec_type = audio_codec_identifier.split('.')[0]
|
|
133
|
+
|
|
134
|
+
# Retrieve codec mapping from the provided mappings or fallback to static mappings
|
|
135
|
+
audio_codec_mapping = CODEC_MAPPINGS.get('audio', {})
|
|
136
|
+
codec_name = audio_codec_mapping.get(codec_type)
|
|
137
|
+
|
|
138
|
+
if codec_name:
|
|
139
|
+
return codec_name
|
|
140
|
+
else:
|
|
141
|
+
logging.warning(f"No corresponding audio codec found for {audio_codec_identifier}. Using default codec aac.")
|
|
142
|
+
return "aac" # Default
|
|
143
|
+
|
|
144
|
+
def parse_codecs(self):
|
|
145
|
+
"""
|
|
146
|
+
Parse video and audio codecs.
|
|
147
|
+
This method updates `video_codec_name` and `audio_codec_name` attributes.
|
|
148
|
+
"""
|
|
149
|
+
self.video_codec_name = self.convert_video_codec(self.video_codec)
|
|
150
|
+
self.audio_codec_name = self.convert_audio_codec(self.audio_codec)
|
|
151
|
+
|
|
152
|
+
def calculate_bitrates(self):
|
|
153
|
+
"""
|
|
154
|
+
Calculate video and audio bitrates based on the available bandwidth.
|
|
155
|
+
"""
|
|
156
|
+
if self.bandwidth:
|
|
157
|
+
|
|
158
|
+
# Define the video and audio bitrates
|
|
159
|
+
video_bitrate = int(self.bandwidth * 0.8) # Using 80% of bandwidth for video
|
|
160
|
+
audio_bitrate = self.bandwidth - video_bitrate
|
|
161
|
+
|
|
162
|
+
self.video_bitrate = video_bitrate
|
|
163
|
+
self.audio_bitrate = audio_bitrate
|
|
164
|
+
else:
|
|
165
|
+
logging.warning("No bandwidth provided. Bitrates cannot be calculated.")
|
|
166
|
+
|
|
167
|
+
def __str__(self):
|
|
168
|
+
return (f"M3U8_Codec(bandwidth={self.bandwidth}, "
|
|
169
|
+
f"codecs='{self.codecs}', "
|
|
170
|
+
f"audio_codec='{self.audio_codec}', "
|
|
171
|
+
f"video_codec='{self.video_codec}', "
|
|
172
|
+
f"audio_codec_name='{self.audio_codec_name}', "
|
|
173
|
+
f"video_codec_name='{self.video_codec_name}')")
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
class M3U8_Video:
|
|
177
|
+
def __init__(self, video_playlist) -> None:
|
|
178
|
+
"""
|
|
179
|
+
Initializes an M3U8_Video object with the provided video playlist.
|
|
180
|
+
|
|
181
|
+
Parameters:
|
|
182
|
+
- video_playlist (M3U8): An M3U8 object representing the video playlist.
|
|
183
|
+
"""
|
|
184
|
+
self.video_playlist = video_playlist
|
|
185
|
+
|
|
186
|
+
def get_best_uri(self):
|
|
187
|
+
"""
|
|
188
|
+
Returns the URI with the highest resolution from the video playlist.
|
|
189
|
+
|
|
190
|
+
Returns:
|
|
191
|
+
tuple or None: A tuple containing the URI with the highest resolution and its resolution value, or None if the video list is empty.
|
|
192
|
+
"""
|
|
193
|
+
if not self.video_playlist:
|
|
194
|
+
return None
|
|
195
|
+
|
|
196
|
+
best_uri = max(self.video_playlist, key=lambda x: x['resolution'])
|
|
197
|
+
return best_uri['uri'], best_uri['resolution']
|
|
198
|
+
|
|
199
|
+
def get_worst_uri(self):
|
|
200
|
+
"""
|
|
201
|
+
Returns the URI with the lowest resolution from the video playlist.
|
|
202
|
+
|
|
203
|
+
Returns:
|
|
204
|
+
- tuple or None: A tuple containing the URI with the lowest resolution and its resolution value, or None if the video list is empty.
|
|
205
|
+
"""
|
|
206
|
+
if not self.video_playlist:
|
|
207
|
+
return None
|
|
208
|
+
|
|
209
|
+
worst_uri = min(self.video_playlist, key=lambda x: x['resolution'])
|
|
210
|
+
return worst_uri['uri'], worst_uri['resolution']
|
|
211
|
+
|
|
212
|
+
def get_custom_uri(self, y_resolution):
|
|
213
|
+
"""
|
|
214
|
+
Returns the URI corresponding to a custom resolution from the video list.
|
|
215
|
+
|
|
216
|
+
Parameters:
|
|
217
|
+
- video_list (list): A list of dictionaries containing video URIs and resolutions.
|
|
218
|
+
- custom_resolution (tuple): A tuple representing the custom resolution.
|
|
219
|
+
|
|
220
|
+
Returns:
|
|
221
|
+
str or None: The URI corresponding to the custom resolution, or None if not found.
|
|
222
|
+
"""
|
|
223
|
+
for video in self.video_playlist:
|
|
224
|
+
logging.info(f"Check resolution from playlist: {int(video['resolution'][1])}, with input: {int(y_resolution)}")
|
|
225
|
+
|
|
226
|
+
if int(video['resolution'][1]) == int(y_resolution):
|
|
227
|
+
return video['uri'], video['resolution']
|
|
228
|
+
|
|
229
|
+
return None, None
|
|
230
|
+
|
|
231
|
+
def get_list_resolution(self):
|
|
232
|
+
"""
|
|
233
|
+
Retrieve a list of resolutions from the video playlist.
|
|
234
|
+
|
|
235
|
+
Returns:
|
|
236
|
+
list: A list of resolutions extracted from the video playlist.
|
|
237
|
+
"""
|
|
238
|
+
return [video['resolution'] for video in self.video_playlist]
|
|
239
|
+
|
|
240
|
+
def get_list_resolution_and_size(self, duration):
|
|
241
|
+
"""
|
|
242
|
+
Retrieve a list of resolutions and size from the video playlist.
|
|
243
|
+
|
|
244
|
+
Parameters:
|
|
245
|
+
- duration (int): Total duration of the video in 's'.
|
|
246
|
+
|
|
247
|
+
Returns:
|
|
248
|
+
list: A list of resolutions extracted from the video playlist.
|
|
249
|
+
"""
|
|
250
|
+
result = []
|
|
251
|
+
|
|
252
|
+
for video in self.video_playlist:
|
|
253
|
+
video_size = internet_manager.format_file_size((video['bandwidth'] * duration) / 8)
|
|
254
|
+
result.append((video_size))
|
|
255
|
+
|
|
256
|
+
return result
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
class M3U8_Audio:
|
|
260
|
+
def __init__(self, audio_playlist) -> None:
|
|
261
|
+
"""
|
|
262
|
+
Initializes an M3U8_Audio object with the provided audio playlist.
|
|
263
|
+
|
|
264
|
+
Parameters:
|
|
265
|
+
- audio_playlist (M3U8): An M3U8 object representing the audio playlist.
|
|
266
|
+
"""
|
|
267
|
+
self.audio_playlist = audio_playlist
|
|
268
|
+
|
|
269
|
+
def get_uri_by_language(self, language):
|
|
270
|
+
"""
|
|
271
|
+
Returns a dictionary with 'name' and 'uri' given a specific language.
|
|
272
|
+
|
|
273
|
+
Parameters:
|
|
274
|
+
- audio_list (list): List of dictionaries containing audio information.
|
|
275
|
+
- language (str): The desired language.
|
|
276
|
+
|
|
277
|
+
Returns:
|
|
278
|
+
dict or None: Dictionary with 'name', 'language', and 'uri' for the specified language, or None if not found.
|
|
279
|
+
"""
|
|
280
|
+
for audio in self.audio_playlist:
|
|
281
|
+
if audio['language'] == language:
|
|
282
|
+
return {'name': audio['name'], 'language': audio['language'], 'uri': audio['uri']}
|
|
283
|
+
return None
|
|
284
|
+
|
|
285
|
+
def get_all_uris_and_names(self):
|
|
286
|
+
"""
|
|
287
|
+
Returns a list of dictionaries containing all URIs and names.
|
|
288
|
+
|
|
289
|
+
Parameters:
|
|
290
|
+
- audio_list (list): List of dictionaries containing audio information.
|
|
291
|
+
|
|
292
|
+
Returns:
|
|
293
|
+
list: List of dictionaries containing 'name', 'language', and 'uri' for all audio in the list.
|
|
294
|
+
"""
|
|
295
|
+
audios_list = [{'name': audio['name'], 'language': audio['language'], 'uri': audio['uri']} for audio in self.audio_playlist]
|
|
296
|
+
unique_audios_dict = {}
|
|
297
|
+
|
|
298
|
+
# Remove duplicate
|
|
299
|
+
for audio in audios_list:
|
|
300
|
+
unique_audios_dict[audio['language']] = audio
|
|
301
|
+
|
|
302
|
+
return list(unique_audios_dict.values())
|
|
303
|
+
|
|
304
|
+
def get_default_uri(self):
|
|
305
|
+
"""
|
|
306
|
+
Returns the dictionary with 'default' equal to 'YES'.
|
|
307
|
+
|
|
308
|
+
Parameters:
|
|
309
|
+
- audio_list (list): List of dictionaries containing audio information.
|
|
310
|
+
|
|
311
|
+
Returns:
|
|
312
|
+
dict or None: Dictionary with 'default' equal to 'YES', or None if not found.
|
|
313
|
+
"""
|
|
314
|
+
for audio in self.audio_playlist:
|
|
315
|
+
if audio['default'] == 'YES':
|
|
316
|
+
return audio.get('uri')
|
|
317
|
+
return None
|
|
318
|
+
|
|
319
|
+
|
|
320
|
+
class M3U8_Subtitle:
|
|
321
|
+
def __init__(self, subtitle_playlist) -> None:
|
|
322
|
+
"""
|
|
323
|
+
Initializes an M3U8_Subtitle object with the provided subtitle playlist.
|
|
324
|
+
|
|
325
|
+
Parameters:
|
|
326
|
+
- subtitle_playlist (M3U8): An M3U8 object representing the subtitle playlist.
|
|
327
|
+
"""
|
|
328
|
+
self.subtitle_playlist = subtitle_playlist
|
|
329
|
+
|
|
330
|
+
def get_uri_by_language(self, language):
|
|
331
|
+
"""
|
|
332
|
+
Returns a dictionary with 'name' and 'uri' given a specific language for subtitles.
|
|
333
|
+
|
|
334
|
+
Parameters:
|
|
335
|
+
- subtitle_list (list): List of dictionaries containing subtitle information.
|
|
336
|
+
- language (str): The desired language.
|
|
337
|
+
|
|
338
|
+
Returns:
|
|
339
|
+
dict or None: Dictionary with 'name' and 'uri' for the specified language for subtitles, or None if not found.
|
|
340
|
+
"""
|
|
341
|
+
for subtitle in self.subtitle_playlist:
|
|
342
|
+
if subtitle['language'] == language:
|
|
343
|
+
return {'name': subtitle['name'], 'uri': subtitle['uri']}
|
|
344
|
+
return None
|
|
345
|
+
|
|
346
|
+
def get_all_uris_and_names(self):
|
|
347
|
+
"""
|
|
348
|
+
Returns a list of dictionaries containing all URIs and names of subtitles.
|
|
349
|
+
|
|
350
|
+
Parameters:
|
|
351
|
+
- subtitle_list (list): List of dictionaries containing subtitle information.
|
|
352
|
+
|
|
353
|
+
Returns:
|
|
354
|
+
list: List of dictionaries containing 'name' and 'uri' for all subtitles in the list.
|
|
355
|
+
"""
|
|
356
|
+
subtitles_list = [{'name': subtitle['name'], 'language': subtitle['language'], 'uri': subtitle['uri']} for subtitle in self.subtitle_playlist]
|
|
357
|
+
unique_subtitles_dict = {}
|
|
358
|
+
|
|
359
|
+
# Remove duplicate
|
|
360
|
+
for subtitle in subtitles_list:
|
|
361
|
+
unique_subtitles_dict[subtitle['language']] = subtitle
|
|
362
|
+
|
|
363
|
+
return list(unique_subtitles_dict.values())
|
|
364
|
+
|
|
365
|
+
def get_default_uri(self):
|
|
366
|
+
"""
|
|
367
|
+
Returns the dictionary with 'default' equal to 'YES' for subtitles.
|
|
368
|
+
|
|
369
|
+
Parameters:
|
|
370
|
+
- subtitle_list (list): List of dictionaries containing subtitle information.
|
|
371
|
+
|
|
372
|
+
Returns:
|
|
373
|
+
dict or None: Dictionary with 'default' equal to 'YES' for subtitles, or None if not found.
|
|
374
|
+
"""
|
|
375
|
+
for subtitle in self.subtitle_playlist:
|
|
376
|
+
if subtitle['default'] == 'YES':
|
|
377
|
+
return subtitle
|
|
378
|
+
return None
|
|
379
|
+
|
|
380
|
+
def download_all(self, custom_subtitle):
|
|
381
|
+
"""
|
|
382
|
+
Download all subtitles listed in the object's attributes, filtering based on a provided list of custom subtitles.
|
|
383
|
+
|
|
384
|
+
Parameters:
|
|
385
|
+
- custom_subtitle (list): A list of custom subtitles to download.
|
|
386
|
+
|
|
387
|
+
Returns:
|
|
388
|
+
list: A list containing dictionaries with subtitle information including name, language, and URI.
|
|
389
|
+
"""
|
|
390
|
+
|
|
391
|
+
output = [] # Initialize an empty list to store subtitle information
|
|
392
|
+
|
|
393
|
+
# Iterate through all available subtitles
|
|
394
|
+
for obj_subtitle in self.subtitle_get_all_uris_and_names():
|
|
395
|
+
|
|
396
|
+
# Check if the subtitle name is not in the list of custom subtitles, and skip if not found
|
|
397
|
+
if obj_subtitle.get('name') not in custom_subtitle:
|
|
398
|
+
continue
|
|
399
|
+
|
|
400
|
+
# Send a request to retrieve the subtitle content
|
|
401
|
+
logging.info(f"Download subtitle: {obj_subtitle.get('name')}")
|
|
402
|
+
response_subitle = httpx.get(obj_subtitle.get('uri'))
|
|
403
|
+
|
|
404
|
+
try:
|
|
405
|
+
# Try to extract the VTT URL from the subtitle content
|
|
406
|
+
sub_parse = M3U8_Parser()
|
|
407
|
+
sub_parse.parse_data(obj_subtitle.get('uri'), response_subitle.text)
|
|
408
|
+
url_subititle = sub_parse.subtitle[0]
|
|
409
|
+
|
|
410
|
+
output.append({
|
|
411
|
+
'name': obj_subtitle.get('name'),
|
|
412
|
+
'language': obj_subtitle.get('language'),
|
|
413
|
+
'uri': url_subititle
|
|
414
|
+
})
|
|
415
|
+
|
|
416
|
+
except Exception as e:
|
|
417
|
+
logging.error(f"Cant download: {obj_subtitle.get('name')}, error: {e}")
|
|
418
|
+
|
|
419
|
+
return output
|
|
420
|
+
|
|
421
|
+
|
|
422
|
+
class M3U8_Parser:
|
|
423
|
+
def __init__(self):
|
|
424
|
+
self.is_master_playlist = None
|
|
425
|
+
self.segments = []
|
|
426
|
+
self.video_playlist = []
|
|
427
|
+
self.keys = None
|
|
428
|
+
self.subtitle_playlist = []
|
|
429
|
+
self.subtitle = []
|
|
430
|
+
self.audio_playlist = []
|
|
431
|
+
self.codec: M3U8_Codec = None
|
|
432
|
+
self._video: M3U8_Video = None
|
|
433
|
+
self._audio: M3U8_Audio = None
|
|
434
|
+
self._subtitle: M3U8_Subtitle = None
|
|
435
|
+
self.duration: float = 0
|
|
436
|
+
|
|
437
|
+
self.__create_variable__()
|
|
438
|
+
|
|
439
|
+
def parse_data(self, uri, raw_content) -> None:
|
|
440
|
+
"""
|
|
441
|
+
Extracts all information present in the provided M3U8 content.
|
|
442
|
+
|
|
443
|
+
Parameters:
|
|
444
|
+
- m3u8_content (str): The content of the M3U8 file.
|
|
445
|
+
"""
|
|
446
|
+
|
|
447
|
+
# Get obj of the m3u8 text content download, dictionary with video, audio, segments, subtitles
|
|
448
|
+
m3u8_obj = loads(raw_content, uri)
|
|
449
|
+
|
|
450
|
+
self.__parse_video_info__(m3u8_obj)
|
|
451
|
+
self.__parse_subtitles_and_audio__(m3u8_obj)
|
|
452
|
+
self.__parse_segments__(m3u8_obj)
|
|
453
|
+
self.is_master_playlist = self.__is_master__(m3u8_obj)
|
|
454
|
+
|
|
455
|
+
@staticmethod
|
|
456
|
+
def extract_resolution(uri: str) -> int:
|
|
457
|
+
"""
|
|
458
|
+
Extracts the video resolution from the given URI.
|
|
459
|
+
|
|
460
|
+
Parameters:
|
|
461
|
+
- uri (str): The URI containing video information.
|
|
462
|
+
|
|
463
|
+
Returns:
|
|
464
|
+
int: The video resolution if found, otherwise 0.
|
|
465
|
+
"""
|
|
466
|
+
|
|
467
|
+
# Log
|
|
468
|
+
logging.info(f"Try extract resolution from: {uri}")
|
|
469
|
+
|
|
470
|
+
for resolution in RESOLUTIONS:
|
|
471
|
+
if "http" in str(uri):
|
|
472
|
+
if str(resolution[1]) in uri:
|
|
473
|
+
return resolution
|
|
474
|
+
|
|
475
|
+
# Default resolution return (not best)
|
|
476
|
+
logging.warning("No resolution found with custom parsing.")
|
|
477
|
+
return (0, 0)
|
|
478
|
+
|
|
479
|
+
def __is_master__(self, m3u8_obj) -> bool:
|
|
480
|
+
"""
|
|
481
|
+
Determines if the given M3U8 object is a master playlist.
|
|
482
|
+
|
|
483
|
+
Parameters:
|
|
484
|
+
- m3u8_obj (m3u8.M3U8): The parsed M3U8 object.
|
|
485
|
+
|
|
486
|
+
Returns:
|
|
487
|
+
- bool: True if it's a master playlist, False if it's a media playlist, None if unknown.
|
|
488
|
+
"""
|
|
489
|
+
|
|
490
|
+
# Check if the playlist contains variants (master playlist)
|
|
491
|
+
if m3u8_obj.is_variant:
|
|
492
|
+
return True
|
|
493
|
+
|
|
494
|
+
# Check if the playlist contains segments directly (media playlist)
|
|
495
|
+
elif m3u8_obj.segments:
|
|
496
|
+
return False
|
|
497
|
+
|
|
498
|
+
# Return None if the playlist type is undetermined
|
|
499
|
+
return None
|
|
500
|
+
|
|
501
|
+
def __parse_video_info__(self, m3u8_obj) -> None:
|
|
502
|
+
"""
|
|
503
|
+
Extracts video information from the M3U8 object.
|
|
504
|
+
|
|
505
|
+
Parameters:
|
|
506
|
+
- m3u8_obj: The M3U8 object containing video playlists.
|
|
507
|
+
"""
|
|
508
|
+
|
|
509
|
+
try:
|
|
510
|
+
for playlist in m3u8_obj.playlists:
|
|
511
|
+
|
|
512
|
+
there_is_codec = not playlist.stream_info.codecs is None
|
|
513
|
+
logging.info(f"There is coded: {there_is_codec}")
|
|
514
|
+
|
|
515
|
+
if there_is_codec:
|
|
516
|
+
self.codec = M3U8_Codec(
|
|
517
|
+
playlist.stream_info.bandwidth,
|
|
518
|
+
playlist.stream_info.codecs
|
|
519
|
+
)
|
|
520
|
+
|
|
521
|
+
# Direct access resolutions in m3u8 obj
|
|
522
|
+
if playlist.stream_info.resolution is not None:
|
|
523
|
+
|
|
524
|
+
self.video_playlist.append({
|
|
525
|
+
"uri": playlist.uri,
|
|
526
|
+
"resolution": playlist.stream_info.resolution,
|
|
527
|
+
"bandwidth": playlist.stream_info.bandwidth
|
|
528
|
+
})
|
|
529
|
+
|
|
530
|
+
if there_is_codec:
|
|
531
|
+
self.codec.resolution = playlist.stream_info.resolution
|
|
532
|
+
|
|
533
|
+
# Find resolutions in uri
|
|
534
|
+
else:
|
|
535
|
+
|
|
536
|
+
self.video_playlist.append({
|
|
537
|
+
"uri": playlist.uri,
|
|
538
|
+
"resolution": M3U8_Parser.extract_resolution(playlist.uri),
|
|
539
|
+
"bandwidth": playlist.stream_info.bandwidth
|
|
540
|
+
})
|
|
541
|
+
|
|
542
|
+
if there_is_codec:
|
|
543
|
+
self.codec.resolution = M3U8_Parser.extract_resolution(playlist.uri)
|
|
544
|
+
|
|
545
|
+
continue
|
|
546
|
+
|
|
547
|
+
except Exception as e:
|
|
548
|
+
logging.error(f"Error parsing video info: {e}")
|
|
549
|
+
|
|
550
|
+
def __parse_encryption_keys__(self, m3u8_obj) -> None:
|
|
551
|
+
"""
|
|
552
|
+
Extracts encryption keys from the M3U8 object.
|
|
553
|
+
|
|
554
|
+
Parameters:
|
|
555
|
+
- m3u8_obj: The M3U8 object containing encryption keys.
|
|
556
|
+
"""
|
|
557
|
+
try:
|
|
558
|
+
|
|
559
|
+
if m3u8_obj.key is not None:
|
|
560
|
+
if self.keys is None:
|
|
561
|
+
self.keys = {
|
|
562
|
+
'method': m3u8_obj.key.method,
|
|
563
|
+
'iv': m3u8_obj.key.iv,
|
|
564
|
+
'uri': m3u8_obj.key.uri
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
|
|
568
|
+
except Exception as e:
|
|
569
|
+
logging.error(f"Error parsing encryption keys: {e}")
|
|
570
|
+
sys.exit(0)
|
|
571
|
+
pass
|
|
572
|
+
|
|
573
|
+
def __parse_subtitles_and_audio__(self, m3u8_obj) -> None:
|
|
574
|
+
"""
|
|
575
|
+
Extracts subtitles and audio information from the M3U8 object.
|
|
576
|
+
|
|
577
|
+
Parameters:
|
|
578
|
+
- m3u8_obj: The M3U8 object containing subtitles and audio data.
|
|
579
|
+
"""
|
|
580
|
+
try:
|
|
581
|
+
for media in m3u8_obj.media:
|
|
582
|
+
if media.type == "SUBTITLES":
|
|
583
|
+
self.subtitle_playlist.append({
|
|
584
|
+
"type": media.type,
|
|
585
|
+
"name": media.name,
|
|
586
|
+
"default": media.default,
|
|
587
|
+
"language": media.language,
|
|
588
|
+
"uri": media.uri
|
|
589
|
+
})
|
|
590
|
+
|
|
591
|
+
if media.type == "AUDIO":
|
|
592
|
+
self.audio_playlist.append({
|
|
593
|
+
"type": media.type,
|
|
594
|
+
"name": media.name,
|
|
595
|
+
"default": media.default,
|
|
596
|
+
"language": media.language,
|
|
597
|
+
"uri": media.uri
|
|
598
|
+
})
|
|
599
|
+
|
|
600
|
+
except Exception as e:
|
|
601
|
+
logging.error(f"Error parsing subtitles and audio: {e}")
|
|
602
|
+
|
|
603
|
+
def __parse_segments__(self, m3u8_obj) -> None:
|
|
604
|
+
"""
|
|
605
|
+
Extracts segment information from the M3U8 object.
|
|
606
|
+
|
|
607
|
+
Parameters:
|
|
608
|
+
- m3u8_obj: The M3U8 object containing segment data.
|
|
609
|
+
"""
|
|
610
|
+
|
|
611
|
+
try:
|
|
612
|
+
for segment in m3u8_obj.segments:
|
|
613
|
+
|
|
614
|
+
# Parse key
|
|
615
|
+
self.__parse_encryption_keys__(segment)
|
|
616
|
+
|
|
617
|
+
# Collect all index duration
|
|
618
|
+
self.duration += segment.duration
|
|
619
|
+
|
|
620
|
+
if "vtt" not in segment.uri:
|
|
621
|
+
self.segments.append(segment.uri)
|
|
622
|
+
else:
|
|
623
|
+
self.subtitle.append(segment.uri)
|
|
624
|
+
|
|
625
|
+
except Exception as e:
|
|
626
|
+
logging.error(f"Error parsing segments: {e}")
|
|
627
|
+
|
|
628
|
+
def __create_variable__(self):
|
|
629
|
+
"""
|
|
630
|
+
Initialize variables for video, audio, and subtitle playlists.
|
|
631
|
+
"""
|
|
632
|
+
|
|
633
|
+
self._video = M3U8_Video(self.video_playlist)
|
|
634
|
+
self._audio = M3U8_Audio(self.audio_playlist)
|
|
635
|
+
self._subtitle = M3U8_Subtitle(self.subtitle_playlist)
|
|
636
|
+
|
|
637
|
+
def get_duration(self, return_string:bool = True):
|
|
638
|
+
"""
|
|
639
|
+
Convert duration from seconds to hours, minutes, and remaining seconds.
|
|
640
|
+
|
|
641
|
+
Parameters:
|
|
642
|
+
- return_string (bool): If True, returns the formatted duration string.
|
|
643
|
+
If False, returns a dictionary with hours, minutes, and seconds.
|
|
644
|
+
|
|
645
|
+
Returns:
|
|
646
|
+
- formatted_duration (str): Formatted duration string with hours, minutes, and seconds if return_string is True.
|
|
647
|
+
- duration_dict (dict): Dictionary with keys 'h', 'm', 's' representing hours, minutes, and seconds respectively if return_string is False.
|
|
648
|
+
|
|
649
|
+
Example usage:
|
|
650
|
+
>>> obj = YourClass(duration=3661)
|
|
651
|
+
>>> obj.get_duration()
|
|
652
|
+
'[yellow]1[red]h [yellow]1[red]m [yellow]1[red]s'
|
|
653
|
+
>>> obj.get_duration(return_string=False)
|
|
654
|
+
{'h': 1, 'm': 1, 's': 1}
|
|
655
|
+
"""
|
|
656
|
+
|
|
657
|
+
# Calculate hours, minutes, and remaining seconds
|
|
658
|
+
hours, remainder = divmod(self.duration, 3600)
|
|
659
|
+
minutes, seconds = divmod(remainder, 60)
|
|
660
|
+
|
|
661
|
+
|
|
662
|
+
# Format the duration string with colors
|
|
663
|
+
if return_string:
|
|
664
|
+
return f"[yellow]{int(hours)}[red]h [yellow]{int(minutes)}[red]m [yellow]{int(seconds)}[red]s"
|
|
665
|
+
else:
|
|
666
|
+
return {'h': int(hours), 'm': int(minutes), 's': int(seconds)}
|