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.

Files changed (101) hide show
  1. StreamingCommunity/{Src/Api → Api}/Player/Helper/Vixcloud/js_parser.py +4 -1
  2. StreamingCommunity/{Src/Api → Api}/Player/Helper/Vixcloud/util.py +166 -166
  3. StreamingCommunity/{Src/Api → Api}/Player/ddl.py +89 -89
  4. StreamingCommunity/{Src/Api → Api}/Player/maxstream.py +151 -151
  5. StreamingCommunity/{Src/Api → Api}/Player/supervideo.py +193 -193
  6. StreamingCommunity/{Src/Api → Api}/Player/vixcloud.py +224 -212
  7. StreamingCommunity/{Src/Api → Api}/Site/1337xx/__init__.py +50 -50
  8. StreamingCommunity/{Src/Api → Api}/Site/1337xx/costant.py +15 -15
  9. StreamingCommunity/{Src/Api → Api}/Site/1337xx/site.py +83 -83
  10. StreamingCommunity/{Src/Api → Api}/Site/1337xx/title.py +66 -66
  11. StreamingCommunity/{Src/Api → Api}/Site/altadefinizione/__init__.py +50 -50
  12. StreamingCommunity/{Src/Api/Site/mostraguarda → Api/Site/altadefinizione}/costant.py +15 -15
  13. StreamingCommunity/{Src/Api → Api}/Site/altadefinizione/film.py +69 -69
  14. StreamingCommunity/{Src/Api → Api}/Site/altadefinizione/site.py +86 -86
  15. StreamingCommunity/{Src/Api → Api}/Site/animeunity/__init__.py +50 -50
  16. StreamingCommunity/{Src/Api/Site/altadefinizione → Api/Site/animeunity}/costant.py +15 -15
  17. StreamingCommunity/{Src/Api → Api}/Site/animeunity/film_serie.py +130 -131
  18. StreamingCommunity/{Src/Api → Api}/Site/animeunity/site.py +164 -164
  19. StreamingCommunity/{Src/Api → Api}/Site/animeunity/util/ScrapeSerie.py +3 -3
  20. StreamingCommunity/{Src/Api → Api}/Site/bitsearch/__init__.py +51 -51
  21. StreamingCommunity/{Src/Api → Api}/Site/bitsearch/costant.py +15 -15
  22. StreamingCommunity/{Src/Api → Api}/Site/bitsearch/site.py +84 -84
  23. StreamingCommunity/{Src/Api → Api}/Site/bitsearch/title.py +47 -47
  24. StreamingCommunity/{Src/Api → Api}/Site/cb01new/__init__.py +51 -51
  25. StreamingCommunity/{Src/Api → Api}/Site/cb01new/costant.py +15 -15
  26. StreamingCommunity/{Src/Api → Api}/Site/cb01new/film.py +69 -69
  27. StreamingCommunity/{Src/Api → Api}/Site/cb01new/site.py +74 -74
  28. StreamingCommunity/{Src/Api → Api}/Site/ddlstreamitaly/__init__.py +57 -57
  29. StreamingCommunity/{Src/Api → Api}/Site/ddlstreamitaly/costant.py +16 -16
  30. StreamingCommunity/{Src/Api → Api}/Site/ddlstreamitaly/series.py +141 -142
  31. StreamingCommunity/{Src/Api → Api}/Site/ddlstreamitaly/site.py +92 -92
  32. StreamingCommunity/{Src/Api → Api}/Site/ddlstreamitaly/util/ScrapeSerie.py +84 -82
  33. StreamingCommunity/{Src/Api → Api}/Site/guardaserie/__init__.py +52 -52
  34. StreamingCommunity/{Src/Api/Site/piratebays → Api/Site/guardaserie}/costant.py +15 -15
  35. StreamingCommunity/{Src/Api → Api}/Site/guardaserie/series.py +195 -195
  36. StreamingCommunity/{Src/Api → Api}/Site/guardaserie/site.py +84 -84
  37. StreamingCommunity/{Src/Api → Api}/Site/guardaserie/util/ScrapeSerie.py +110 -110
  38. StreamingCommunity/{Src/Api → Api}/Site/mostraguarda/__init__.py +48 -48
  39. StreamingCommunity/{Src/Api/Site/animeunity → Api/Site/mostraguarda}/costant.py +15 -15
  40. StreamingCommunity/{Src/Api → Api}/Site/mostraguarda/film.py +94 -94
  41. StreamingCommunity/{Src/Api → Api}/Site/piratebays/__init__.py +50 -50
  42. StreamingCommunity/{Src/Api/Site/guardaserie → Api/Site/piratebays}/costant.py +15 -15
  43. StreamingCommunity/{Src/Api → Api}/Site/piratebays/site.py +88 -88
  44. StreamingCommunity/{Src/Api → Api}/Site/piratebays/title.py +45 -45
  45. StreamingCommunity/{Src/Api → Api}/Site/streamingcommunity/__init__.py +55 -55
  46. StreamingCommunity/{Src/Api → Api}/Site/streamingcommunity/costant.py +15 -15
  47. StreamingCommunity/{Src/Api → Api}/Site/streamingcommunity/film.py +70 -70
  48. StreamingCommunity/{Src/Api → Api}/Site/streamingcommunity/series.py +205 -203
  49. StreamingCommunity/{Src/Api → Api}/Site/streamingcommunity/site.py +125 -125
  50. StreamingCommunity/{Src/Api → Api}/Site/streamingcommunity/util/ScrapeSerie.py +3 -3
  51. StreamingCommunity/{Src/Api → Api}/Template/Class/SearchType.py +101 -101
  52. StreamingCommunity/{Src/Api → Api}/Template/Util/__init__.py +4 -4
  53. StreamingCommunity/{Src/Api → Api}/Template/Util/get_domain.py +137 -137
  54. StreamingCommunity/{Src/Api → Api}/Template/Util/manage_ep.py +153 -153
  55. StreamingCommunity/{Src/Api → Api}/Template/Util/recall_search.py +37 -37
  56. StreamingCommunity/Api/Template/__init__.py +3 -0
  57. StreamingCommunity/{Src/Api → Api}/Template/site.py +87 -87
  58. StreamingCommunity/{Src/Lib → Lib}/Downloader/HLS/downloader.py +968 -968
  59. StreamingCommunity/{Src/Lib → Lib}/Downloader/HLS/proxyes.py +110 -110
  60. StreamingCommunity/{Src/Lib → Lib}/Downloader/HLS/segments.py +538 -540
  61. StreamingCommunity/{Src/Lib → Lib}/Downloader/MP4/downloader.py +156 -156
  62. StreamingCommunity/{Src/Lib → Lib}/Downloader/TOR/downloader.py +222 -222
  63. StreamingCommunity/{Src/Lib → Lib}/Downloader/__init__.py +4 -4
  64. StreamingCommunity/{Src/Lib → Lib}/Driver/driver_1.py +76 -76
  65. StreamingCommunity/{Src/Lib → Lib}/FFmpeg/__init__.py +4 -4
  66. StreamingCommunity/{Src/Lib → Lib}/FFmpeg/capture.py +170 -170
  67. StreamingCommunity/{Src/Lib → Lib}/FFmpeg/command.py +292 -292
  68. StreamingCommunity/{Src/Lib → Lib}/FFmpeg/util.py +241 -241
  69. StreamingCommunity/{Src/Lib → Lib}/M3U8/__init__.py +5 -5
  70. StreamingCommunity/{Src/Lib → Lib}/M3U8/decryptor.py +164 -129
  71. StreamingCommunity/{Src/Lib → Lib}/M3U8/estimator.py +175 -172
  72. StreamingCommunity/{Src/Lib → Lib}/M3U8/parser.py +666 -666
  73. StreamingCommunity/{Src/Lib → Lib}/M3U8/url_fixer.py +51 -51
  74. StreamingCommunity/Lib/TMBD/__init__.py +2 -0
  75. StreamingCommunity/{Src/Lib → Lib}/TMBD/obj_tmbd.py +39 -39
  76. StreamingCommunity/{Src/Lib → Lib}/TMBD/tmdb.py +345 -345
  77. StreamingCommunity/{Src/Upload → Upload}/update.py +68 -64
  78. StreamingCommunity/{Src/Upload → Upload}/version.py +5 -5
  79. StreamingCommunity/{Src/Util → Util}/_jsonConfig.py +204 -204
  80. StreamingCommunity/{Src/Util → Util}/call_stack.py +42 -42
  81. StreamingCommunity/{Src/Util → Util}/color.py +20 -20
  82. StreamingCommunity/{Src/Util → Util}/console.py +12 -12
  83. StreamingCommunity/Util/ffmpeg_installer.py +275 -0
  84. StreamingCommunity/{Src/Util → Util}/headers.py +147 -147
  85. StreamingCommunity/{Src/Util → Util}/logger.py +53 -53
  86. StreamingCommunity/{Src/Util → Util}/message.py +46 -46
  87. StreamingCommunity/{Src/Util → Util}/os.py +514 -417
  88. StreamingCommunity/{Src/Util → Util}/table.py +163 -163
  89. StreamingCommunity/run.py +202 -196
  90. {StreamingCommunity-1.7.6.dist-info → StreamingCommunity-1.9.1.dist-info}/METADATA +126 -60
  91. StreamingCommunity-1.9.1.dist-info/RECORD +95 -0
  92. {StreamingCommunity-1.7.6.dist-info → StreamingCommunity-1.9.1.dist-info}/WHEEL +1 -1
  93. StreamingCommunity/Src/Api/Site/animeunity/anime.py +0 -126
  94. StreamingCommunity/Src/Api/Site/ddlstreamitaly/Player/ScrapeSerie.py +0 -83
  95. StreamingCommunity/Src/Api/Site/guardaserie/Player/ScrapeSerie.py +0 -110
  96. StreamingCommunity/Src/Api/Template/__init__.py +0 -3
  97. StreamingCommunity/Src/Lib/TMBD/__init__.py +0 -2
  98. StreamingCommunity-1.7.6.dist-info/RECORD +0 -97
  99. {StreamingCommunity-1.7.6.dist-info → StreamingCommunity-1.9.1.dist-info}/LICENSE +0 -0
  100. {StreamingCommunity-1.7.6.dist-info → StreamingCommunity-1.9.1.dist-info}/entry_points.txt +0 -0
  101. {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.Src.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)}
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)}