udemy-userAPI 0.3.6__py3-none-any.whl → 0.3.8__py3-none-any.whl
Sign up to get free protection for your applications and to get access to all the features.
- animation_consoles/__init__.py +1 -0
- animation_consoles/animation.py +64 -0
- ffmpeg_for_python/__config__.py +118 -0
- ffmpeg_for_python/__init__.py +8 -0
- ffmpeg_for_python/__utils.py +78 -0
- ffmpeg_for_python/__version__.py +6 -0
- ffmpeg_for_python/exeptions.py +91 -0
- ffmpeg_for_python/ffmpeg.py +203 -0
- m3u8_analyzer/M3u8Analyzer.py +807 -0
- m3u8_analyzer/__init__.py +7 -0
- m3u8_analyzer/__version__.py +1 -0
- m3u8_analyzer/exeptions.py +82 -0
- udemy_userAPI/.cache/.udemy_userAPI +0 -0
- udemy_userAPI/__version__.py +1 -1
- udemy_userAPI/api.py +52 -30
- udemy_userAPI/bultins.py +3 -7
- udemy_userAPI/sections.py +42 -16
- {udemy_userAPI-0.3.6.dist-info → udemy_userAPI-0.3.8.dist-info}/METADATA +2 -2
- udemy_userAPI-0.3.8.dist-info/RECORD +29 -0
- udemy_userAPI-0.3.6.dist-info/RECORD +0 -17
- {udemy_userAPI-0.3.6.dist-info → udemy_userAPI-0.3.8.dist-info}/LICENSE +0 -0
- {udemy_userAPI-0.3.6.dist-info → udemy_userAPI-0.3.8.dist-info}/WHEEL +0 -0
- {udemy_userAPI-0.3.6.dist-info → udemy_userAPI-0.3.8.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,807 @@
|
|
1
|
+
import os
|
2
|
+
import re
|
3
|
+
from typing import List, Tuple, Dict
|
4
|
+
from urllib.parse import urljoin
|
5
|
+
import requests
|
6
|
+
from colorama import Fore, Style
|
7
|
+
from .exeptions import M3u8Error, M3u8NetworkingError, M3u8FileError
|
8
|
+
|
9
|
+
__author__ = 'PauloCesar0073-dev404'
|
10
|
+
|
11
|
+
|
12
|
+
class M3u8Analyzer:
|
13
|
+
def __init__(self):
|
14
|
+
"""
|
15
|
+
análise e manipulação de streams M3U8 de maneira bruta
|
16
|
+
"""
|
17
|
+
pass
|
18
|
+
|
19
|
+
@staticmethod
|
20
|
+
def get_m3u8(url_m3u8: str, headers: dict = None, save_in_file=None, timeout: int = None):
|
21
|
+
"""
|
22
|
+
Obtém o conteúdo de um arquivo M3U8 a partir de uma URL HLS.
|
23
|
+
|
24
|
+
Este método permite acessar, visualizar ou salvar playlists M3U8 utilizadas em transmissões de vídeo sob
|
25
|
+
demanda.
|
26
|
+
|
27
|
+
Args: url_m3u8 (str): A URL do arquivo M3U8 que você deseja acessar. headers (dict, optional): Cabeçalhos
|
28
|
+
HTTP opcionais para a requisição. Se não forem fornecidos, serão usados cabeçalhos padrão. save_in_file (str,
|
29
|
+
optional): Nome do arquivo para salvar o conteúdo M3U8. Se fornecido, o conteúdo da playlist será salvo no
|
30
|
+
diretório atual com a extensão `.m3u8`. timeout (int, optional): Tempo máximo (em segundos) para aguardar uma
|
31
|
+
resposta do servidor. O padrão é 20 segundos.
|
32
|
+
|
33
|
+
Returns:
|
34
|
+
str: O conteúdo do arquivo M3U8 como uma string se a requisição for bem-sucedida.
|
35
|
+
None: Se a requisição falhar ou se o servidor não responder com sucesso.
|
36
|
+
|
37
|
+
Raises:
|
38
|
+
ValueError: Se a URL não for válida ou se os headers não forem um dicionário.
|
39
|
+
ConnectionAbortedError: Se o servidor encerrar a conexão inesperadamente.
|
40
|
+
ConnectionRefusedError: Se a conexão for recusada pelo servidor.
|
41
|
+
TimeoutError: Se o tempo de espera pela resposta do servidor for excedido.
|
42
|
+
ConnectionError: Se não for possível se conectar ao servidor por outros motivos.
|
43
|
+
|
44
|
+
Example:
|
45
|
+
```python
|
46
|
+
from m3u8_analyzer import M3u8Analyzer
|
47
|
+
|
48
|
+
url = "https://example.com/playlist.m3u8"
|
49
|
+
headers = {"User-Agent": "Mozilla/5.0"}
|
50
|
+
playlist_content = M3u8Analyzer.get_m3u8(url, headers=headers, save_in_file="minha_playlist", timeout=30)
|
51
|
+
|
52
|
+
if playlist_content:
|
53
|
+
print("Playlist obtida com sucesso!")
|
54
|
+
else:
|
55
|
+
print("Falha ao obter a playlist.")
|
56
|
+
```
|
57
|
+
"""
|
58
|
+
|
59
|
+
if headers:
|
60
|
+
if not isinstance(headers, dict):
|
61
|
+
raise M3u8Error("headers deve ser um dicionário válido!", errors=['headers not dict'])
|
62
|
+
if not (url_m3u8.startswith('https://') or url_m3u8.startswith('http://')):
|
63
|
+
raise M3u8Error(f"Este valor não se parece ser uma url válida!")
|
64
|
+
try:
|
65
|
+
time = 20
|
66
|
+
respo = ''
|
67
|
+
headers_default = {
|
68
|
+
"Accept": "application/json, text/plain, */*",
|
69
|
+
"Accept-Encoding": "gzip, deflate, br, zstd",
|
70
|
+
"Accept-Language": "pt-BR,pt;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6",
|
71
|
+
"Content-Length": "583",
|
72
|
+
"Content-Type": "text/plain",
|
73
|
+
"Sec-Fetch-Dest": "empty",
|
74
|
+
"Sec-Fetch-Mode": "cors",
|
75
|
+
"Sec-Fetch-Site": "same-origin",
|
76
|
+
"Sec-Ch-Ua": "\"Not:A-Brand\";v=\"99\", \"Google Chrome\";v=\"118\", \"Chromium\";v=\"118\"",
|
77
|
+
"Sec-Ch-Ua-Mobile": "?0",
|
78
|
+
"Sec-Ch-Ua-Platform": "\"Windows\"",
|
79
|
+
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) "
|
80
|
+
"Chrome/118.0.0.0 Safari/537.36"
|
81
|
+
}
|
82
|
+
session = requests.session()
|
83
|
+
if timeout:
|
84
|
+
time = timeout
|
85
|
+
if not headers:
|
86
|
+
headers = headers_default
|
87
|
+
r = session.get(url_m3u8, timeout=time, headers=headers)
|
88
|
+
if r.status_code == 200:
|
89
|
+
# Verificar o conteúdo do arquivo
|
90
|
+
if not "#EXTM3U" in r.text:
|
91
|
+
raise M3u8Error("A URL fornecida não parece ser um arquivo M3U8 válido.")
|
92
|
+
elif "#EXTM3U" in r.text:
|
93
|
+
if save_in_file:
|
94
|
+
local = os.getcwd()
|
95
|
+
with open(fr"{local}\{save_in_file}.m3u8", 'a', encoding='utf-8') as e:
|
96
|
+
e.write(r.text)
|
97
|
+
return r.text
|
98
|
+
else:
|
99
|
+
return None
|
100
|
+
else:
|
101
|
+
return "NULL"
|
102
|
+
except requests.exceptions.SSLError as e:
|
103
|
+
raise M3u8NetworkingError(f"Erro SSL: {e}")
|
104
|
+
except requests.exceptions.ProxyError as e:
|
105
|
+
raise M3u8NetworkingError(f"Erro de proxy: {e}")
|
106
|
+
except requests.exceptions.ConnectionError:
|
107
|
+
raise M3u8NetworkingError("Erro: O servidor ou o servidor encerrou a conexão.")
|
108
|
+
except requests.exceptions.HTTPError as e:
|
109
|
+
raise M3u8NetworkingError(f"Erro HTTP: {e}")
|
110
|
+
except requests.exceptions.Timeout:
|
111
|
+
raise M3u8NetworkingError("Erro de tempo esgotado: A conexão com o servidor demorou muito para responder.")
|
112
|
+
except requests.exceptions.TooManyRedirects:
|
113
|
+
raise M3u8NetworkingError("Erro de redirecionamento: Muitos redirecionamentos.")
|
114
|
+
except requests.exceptions.URLRequired:
|
115
|
+
raise M3u8NetworkingError("Erro: URL é necessária para a solicitação.")
|
116
|
+
except requests.exceptions.InvalidProxyURL as e:
|
117
|
+
raise M3u8NetworkingError(f"Erro: URL de proxy inválida: {e}")
|
118
|
+
except requests.exceptions.InvalidURL:
|
119
|
+
raise M3u8NetworkingError("Erro: URL inválida fornecida.")
|
120
|
+
except requests.exceptions.InvalidSchema:
|
121
|
+
raise M3u8NetworkingError("Erro: URL inválida, esquema não suportado.")
|
122
|
+
except requests.exceptions.MissingSchema:
|
123
|
+
raise M3u8NetworkingError("Erro: URL inválida, esquema ausente.")
|
124
|
+
except requests.exceptions.InvalidHeader as e:
|
125
|
+
raise M3u8NetworkingError(f"Erro de cabeçalho inválido: {e}")
|
126
|
+
except requests.exceptions.ChunkedEncodingError as e:
|
127
|
+
raise M3u8NetworkingError(f"Erro de codificação em partes: {e}")
|
128
|
+
except requests.exceptions.ContentDecodingError as e:
|
129
|
+
raise M3u8NetworkingError(f"Erro de decodificação de conteúdo: {e}")
|
130
|
+
except requests.exceptions.StreamConsumedError:
|
131
|
+
raise M3u8NetworkingError("Erro: Fluxo de resposta já consumido.")
|
132
|
+
except requests.exceptions.RetryError as e:
|
133
|
+
raise M3u8NetworkingError(f"Erro de tentativa: {e}")
|
134
|
+
except requests.exceptions.UnrewindableBodyError:
|
135
|
+
raise M3u8NetworkingError("Erro: Corpo da solicitação não pode ser rebobinado.")
|
136
|
+
except requests.exceptions.RequestException as e:
|
137
|
+
raise M3u8NetworkingError(f"Erro de conexão: Não foi possível se conectar ao servidor. Detalhes: {e}")
|
138
|
+
except requests.exceptions.BaseHTTPError as e:
|
139
|
+
raise M3u8NetworkingError(f"Erro HTTP básico: {e}")
|
140
|
+
|
141
|
+
@staticmethod
|
142
|
+
def get_high_resolution(m3u8_content: str):
|
143
|
+
"""
|
144
|
+
Obtém a maior resolução disponível em um arquivo M3U8 e o URL correspondente.
|
145
|
+
|
146
|
+
Este método analisa o conteúdo de um arquivo M3U8 para identificar a maior resolução
|
147
|
+
disponível entre os fluxos de vídeo listados. Também retorna o URL associado a essa
|
148
|
+
maior resolução, se disponível.
|
149
|
+
|
150
|
+
Args:
|
151
|
+
m3u8_content (str): O conteúdo do arquivo M3U8 como uma string. Este conteúdo deve
|
152
|
+
incluir as tags e atributos típicos de uma playlist HLS.
|
153
|
+
|
154
|
+
Returns:
|
155
|
+
tuple: Uma tupla contendo:
|
156
|
+
- str: A maior resolução disponível no formato 'Largura x Altura' (ex.: '1920x1080').
|
157
|
+
- str: O URL correspondente à maior resolução. Se o URL não for encontrado,
|
158
|
+
retorna None.
|
159
|
+
Se o tipo de playlist não contiver resoluções, retorna uma mensagem indicando
|
160
|
+
o tipo de playlist.
|
161
|
+
|
162
|
+
Raises:
|
163
|
+
ValueError: Se o conteúdo do M3U8 não contiver resoluções e a função não conseguir
|
164
|
+
determinar o tipo de playlist.
|
165
|
+
|
166
|
+
Examples:
|
167
|
+
```python
|
168
|
+
m3u8_content = '''
|
169
|
+
#EXTM3U
|
170
|
+
#EXT-X-STREAM-INF:BANDWIDTH=500000,RESOLUTION=640x360
|
171
|
+
http://example.com/360p.m3u8
|
172
|
+
#EXT-X-STREAM-INF:BANDWIDTH=1000000,RESOLUTION=1280x720
|
173
|
+
http://example.com/720p.m3u8
|
174
|
+
#EXT-X-STREAM-INF:BANDWIDTH=3000000,RESOLUTION=1920x1080
|
175
|
+
http://example.com/1080p.m3u8
|
176
|
+
'''
|
177
|
+
result = M3u8Analyzer.get_high_resolution(m3u8_content)
|
178
|
+
print(result) # Saída esperada: ('1920x1080', 'http://example.com/1080p.m3u8')
|
179
|
+
```
|
180
|
+
|
181
|
+
```python
|
182
|
+
m3u8_content_no_resolutions = '''
|
183
|
+
#EXTM3U
|
184
|
+
#EXT-X-STREAM-INF:BANDWIDTH=500000
|
185
|
+
http://example.com/360p.m3u8
|
186
|
+
'''
|
187
|
+
result = M3u8Analyzer.get_high_resolution(m3u8_content_no_resolutions)
|
188
|
+
print(result) # Saída esperada: 'Playlist type: <TIPO DA PLAYLIST> not resolutions...'
|
189
|
+
```
|
190
|
+
"""
|
191
|
+
resolutions = re.findall(r'RESOLUTION=(\d+x\d+)', m3u8_content)
|
192
|
+
if not resolutions:
|
193
|
+
tip = M3u8Analyzer.get_type_m3u8_content(m3u8_content=m3u8_content)
|
194
|
+
return f"Playlist type: {Fore.LIGHTRED_EX}{tip}{Style.RESET_ALL} not resolutions..."
|
195
|
+
max_resolution = max(resolutions, key=lambda res: int(res.split('x')[0]) * int(res.split('x')[1]))
|
196
|
+
url = re.search(rf'#EXT-X-STREAM-INF:[^\n]*RESOLUTION={max_resolution}[^\n]*\n([^\n]+)', m3u8_content).group(1)
|
197
|
+
if not url:
|
198
|
+
return max_resolution, None
|
199
|
+
if not max_resolution:
|
200
|
+
return None, url
|
201
|
+
else:
|
202
|
+
return max_resolution, url
|
203
|
+
|
204
|
+
@staticmethod
|
205
|
+
def get_type_m3u8_content(m3u8_content: str) -> str:
|
206
|
+
"""
|
207
|
+
Determina o tipo de conteúdo de um arquivo M3U8 (Master ou Segmentos).
|
208
|
+
|
209
|
+
Este método analisa o conteúdo de um arquivo M3U8 para identificar se ele é do tipo
|
210
|
+
'Master', 'Master encrypted', 'Segments', 'Segments encrypted', 'Segments Master', ou
|
211
|
+
'Desconhecido'. A identificação é baseada na presença de tags e chaves específicas no
|
212
|
+
conteúdo da playlist M3U8.
|
213
|
+
|
214
|
+
Args:
|
215
|
+
m3u8_content (str): O conteúdo do arquivo M3U8 como uma string. Pode ser uma URL ou o
|
216
|
+
próprio conteúdo da playlist.
|
217
|
+
|
218
|
+
Returns:
|
219
|
+
str: O tipo de conteúdo identificado. Os possíveis valores são:
|
220
|
+
- 'Master': Playlist mestre sem criptografia.
|
221
|
+
- 'Master encrypted': Playlist mestre com criptografia.
|
222
|
+
- 'Segments': Playlist de segmentos sem criptografia.
|
223
|
+
- 'Segments encrypted': Playlist de segmentos com criptografia.
|
224
|
+
- 'Segments .ts': Playlist de segmentos com URLs terminando em '.ts'.
|
225
|
+
- 'Segments .m4s': Playlist de segmentos com URLs terminando em '.m4s'.
|
226
|
+
- 'Segments Master': Playlist de segmentos com URLs variadas.
|
227
|
+
- 'Desconhecido': Se o tipo não puder ser identificado.
|
228
|
+
|
229
|
+
Examples:
|
230
|
+
```python
|
231
|
+
m3u8_content_master = '''
|
232
|
+
#EXTM3U
|
233
|
+
#EXT-X-STREAM-INF:BANDWIDTH=500000
|
234
|
+
http://example.com/master.m3u8
|
235
|
+
'''
|
236
|
+
result = M3u8Analyzer.get_type_m3u8_content(m3u8_content_master)
|
237
|
+
print(result) # Saída esperada: 'Master'
|
238
|
+
|
239
|
+
m3u8_content_master_encrypted = '''
|
240
|
+
#EXTM3U
|
241
|
+
#EXT-X-STREAM-INF:BANDWIDTH=500000
|
242
|
+
#EXT-X-KEY:METHOD=AES-128,URI="http://example.com/key.key"
|
243
|
+
http://example.com/master.m3u8
|
244
|
+
'''
|
245
|
+
result = M3u8Analyzer.get_type_m3u8_content(m3u8_content_master_encrypted)
|
246
|
+
print(result) # Saída esperada: 'Master encrypted'
|
247
|
+
|
248
|
+
m3u8_content_segments = '''
|
249
|
+
#EXTM3U
|
250
|
+
#EXTINF:10,
|
251
|
+
http://example.com/segment1.ts
|
252
|
+
#EXTINF:10,
|
253
|
+
http://example.com/segment2.ts
|
254
|
+
'''
|
255
|
+
result = M3u8Analyzer.get_type_m3u8_content(m3u8_content_segments)
|
256
|
+
print(result) # Saída esperada: 'Segments .ts'
|
257
|
+
|
258
|
+
m3u8_content_unknown = '''
|
259
|
+
#EXTM3U
|
260
|
+
#EXTINF:10,
|
261
|
+
http://example.com/unknown_segment
|
262
|
+
'''
|
263
|
+
result = M3u8Analyzer.get_type_m3u8_content(m3u8_content_unknown)
|
264
|
+
print(result) # Saída esperada: 'Segments Master'
|
265
|
+
```
|
266
|
+
|
267
|
+
Raises:
|
268
|
+
Exception: Em caso de erro durante o processamento do conteúdo, o método retornará uma
|
269
|
+
mensagem de erro descritiva.
|
270
|
+
"""
|
271
|
+
try:
|
272
|
+
conteudo = m3u8_content
|
273
|
+
if '#EXT-X-STREAM-INF' in conteudo:
|
274
|
+
if '#EXT-X-KEY' in conteudo:
|
275
|
+
return 'Master encrypted'
|
276
|
+
else:
|
277
|
+
return 'Master'
|
278
|
+
elif '#EXTINF' in conteudo:
|
279
|
+
if '#EXT-X-KEY' in conteudo:
|
280
|
+
return 'Segments encrypted'
|
281
|
+
else:
|
282
|
+
# Verifica se URLs dos segmentos possuem a extensão .ts ou .m4s
|
283
|
+
segment_urls = re.findall(r'#EXTINF:[0-9.]+,\n([^\n]+)', conteudo)
|
284
|
+
if all(url.endswith('.ts') for url in segment_urls):
|
285
|
+
return 'Segments .ts'
|
286
|
+
elif all(url.endswith('.m4s') for url in segment_urls):
|
287
|
+
return 'Segments .m4s'
|
288
|
+
else:
|
289
|
+
return 'Segments Master'
|
290
|
+
else:
|
291
|
+
return 'Desconhecido'
|
292
|
+
except re.error as e:
|
293
|
+
raise M3u8FileError(f"Erro ao processar o conteúdo M3U8: {str(e)}")
|
294
|
+
except Exception as e:
|
295
|
+
raise M3u8FileError(f"Erro inesperado ao processar o conteúdo M3U8: {str(e)}")
|
296
|
+
|
297
|
+
@staticmethod
|
298
|
+
def get_player_playlist(m3u8_url: str) -> str:
|
299
|
+
"""
|
300
|
+
Obtém o caminho do diretório base do arquivo M3U8, excluindo o nome do arquivo.
|
301
|
+
|
302
|
+
Este método analisa a URL fornecida do arquivo M3U8 e retorna o caminho do diretório onde o arquivo M3U8 está localizado.
|
303
|
+
A URL deve ser uma URL completa e o método irá extrair o caminho do diretório base.
|
304
|
+
|
305
|
+
Args:
|
306
|
+
m3u8_url (str): A URL completa do arquivo M3U8. Pode incluir o nome do arquivo e o caminho do diretório.
|
307
|
+
|
308
|
+
Returns:
|
309
|
+
str: O caminho do diretório onde o arquivo M3U8 está localizado. Se a URL não contiver um arquivo M3U8,
|
310
|
+
retornará uma string vazia.
|
311
|
+
|
312
|
+
Examples:
|
313
|
+
```python
|
314
|
+
# Exemplo 1
|
315
|
+
url = 'http://example.com/videos/playlist.m3u8'
|
316
|
+
path = M3u8Analyzer.get_player_playlist(url)
|
317
|
+
print(path) # Saída esperada: 'http://example.com/videos/'
|
318
|
+
|
319
|
+
# Exemplo 2
|
320
|
+
url = 'https://cdn.example.com/streams/segment.m3u8'
|
321
|
+
path = M3u8Analyzer.get_player_playlist(url)
|
322
|
+
print(path) # Saída esperada: 'https://cdn.example.com/streams/'
|
323
|
+
|
324
|
+
# Exemplo 3
|
325
|
+
url = 'https://example.com/playlist.m3u8'
|
326
|
+
path = M3u8Analyzer.get_player_playlist(url)
|
327
|
+
print(path) # Saída esperada: 'https://example.com/'
|
328
|
+
|
329
|
+
# Exemplo 4
|
330
|
+
url = 'https://example.com/videos/'
|
331
|
+
path = M3u8Analyzer.get_player_playlist(url)
|
332
|
+
print(path) # Saída esperada: ''
|
333
|
+
```
|
334
|
+
|
335
|
+
"""
|
336
|
+
if m3u8_url.endswith('/'):
|
337
|
+
m3u8_url = m3u8_url[:-1]
|
338
|
+
partes = m3u8_url.split('/')
|
339
|
+
for i, parte in enumerate(partes):
|
340
|
+
if '.m3u8' in parte:
|
341
|
+
return '/'.join(partes[:i]) + "/"
|
342
|
+
return ''
|
343
|
+
|
344
|
+
@staticmethod
|
345
|
+
def get_audio_playlist(m3u8_content: str):
|
346
|
+
"""
|
347
|
+
Extrai o URL da playlist de áudio de um conteúdo M3U8.
|
348
|
+
|
349
|
+
Este método analisa o conteúdo fornecido de um arquivo M3U8 e retorna o URL da playlist de áudio incluída na playlist M3U8.
|
350
|
+
O método busca a linha que contém a chave `#EXT-X-MEDIA` e extrai a URL associada ao áudio.
|
351
|
+
|
352
|
+
Args:
|
353
|
+
m3u8_content (str): Conteúdo da playlist M3U8 como uma string. Deve incluir informações sobre áudio se disponíveis.
|
354
|
+
|
355
|
+
Returns:
|
356
|
+
str: URL da playlist de áudio encontrada no conteúdo M3U8. Retorna `None` se a URL da playlist de áudio não for encontrada.
|
357
|
+
|
358
|
+
Examples:
|
359
|
+
```python
|
360
|
+
# Exemplo 1
|
361
|
+
content = '''
|
362
|
+
#EXTM3U
|
363
|
+
#EXT-X-VERSION:3
|
364
|
+
#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="audio",NAME="English",DEFAULT=YES,URI="http://example.com/audio.m3u8"
|
365
|
+
#EXT-X-STREAM-INF:BANDWIDTH=256000,AUDIO="audio"
|
366
|
+
http://example.com/stream.m3u8
|
367
|
+
'''
|
368
|
+
url = M3u8Analyzer.get_audio_playlist(content)
|
369
|
+
print(url) # Saída esperada: 'http://example.com/audio.m3u8'
|
370
|
+
|
371
|
+
# Exemplo 2
|
372
|
+
content = '''
|
373
|
+
#EXTM3U
|
374
|
+
#EXT-X-VERSION:3
|
375
|
+
#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="audio",NAME="French",DEFAULT=NO,URI="http://example.com/french_audio.m3u8"
|
376
|
+
#EXT-X-STREAM-INF:BANDWIDTH=256000,AUDIO="audio"
|
377
|
+
http://example.com/stream.m3u8
|
378
|
+
'''
|
379
|
+
url = M3u8Analyzer.get_audio_playlist(content)
|
380
|
+
print(url) # Saída esperada: 'http://example.com/french_audio.m3u8'
|
381
|
+
|
382
|
+
# Exemplo 3
|
383
|
+
content = '''
|
384
|
+
#EXTM3U
|
385
|
+
#EXT-X-VERSION:3
|
386
|
+
#EXT-X-STREAM-INF:BANDWIDTH=256000
|
387
|
+
http://example.com/stream.m3u8
|
388
|
+
'''
|
389
|
+
url = M3u8Analyzer.get_audio_playlist(content)
|
390
|
+
print(url) # Saída esperada: None
|
391
|
+
```
|
392
|
+
|
393
|
+
"""
|
394
|
+
match = re.search(r'#EXT-X-MEDIA:.*URI="([^"]+)"(?:.*,IV=(0x[0-9A-Fa-f]+))?', m3u8_content)
|
395
|
+
if match:
|
396
|
+
return match.group(1)
|
397
|
+
return None
|
398
|
+
|
399
|
+
@staticmethod
|
400
|
+
def get_segments(content: str, base_url: str) -> Dict[str, List[Tuple[str, str]]]:
|
401
|
+
"""
|
402
|
+
Extrai URLs de segmentos de uma playlist M3U8 e fornece informações detalhadas sobre os segmentos.
|
403
|
+
|
404
|
+
Este método analisa o conteúdo de uma playlist M3U8 para extrair URLs de segmentos, identificar resoluções associadas,
|
405
|
+
e retornar um dicionário com informações sobre as URLs dos segmentos, a quantidade total de segmentos,
|
406
|
+
e a ordem de cada URI. Completa URLs relativas com base na URL base da playlist.
|
407
|
+
|
408
|
+
Args:
|
409
|
+
content (str): Conteúdo da playlist M3U8 como uma string.
|
410
|
+
base_url (str): A URL base da playlist M3U8 para completar os caminhos relativos dos segmentos.
|
411
|
+
|
412
|
+
Returns:
|
413
|
+
dict: Um dicionário com as seguintes chaves:
|
414
|
+
- 'uris' (List[str]): Lista de URLs dos segmentos.
|
415
|
+
- 'urls' (List[str]): Lista de URLs de stream extraídas do conteúdo.
|
416
|
+
- 'len' (int): Contagem total de URLs de stream encontradas.
|
417
|
+
- 'enumerated_uris' (List[Tuple[int, str]]): Lista de tuplas contendo a ordem e o URL de cada segmento.
|
418
|
+
- 'resolutions' (Dict[str, str]): Dicionário mapeando resoluções para suas URLs correspondentes.
|
419
|
+
- 'codecs' (List[str]): Lista de codecs identificados nas streams.
|
420
|
+
|
421
|
+
Raises:
|
422
|
+
ValueError: Se o conteúdo fornecido for uma URL em vez de uma string de conteúdo M3U8.
|
423
|
+
"""
|
424
|
+
url_pattern = re.compile(r'^https?://', re.IGNORECASE)
|
425
|
+
|
426
|
+
if url_pattern.match(content):
|
427
|
+
raise ValueError("O conteúdo não deve ser uma URL, mas sim uma string de uma playlist M3U8.")
|
428
|
+
|
429
|
+
if content == "NULL":
|
430
|
+
raise ValueError("essa url não é de uma playlist m3u8!")
|
431
|
+
|
432
|
+
# Separação das linhas da playlist, ignorando linhas vazias e comentários
|
433
|
+
urls_segmentos = [linha for linha in content.splitlines() if linha and not linha.startswith('#')]
|
434
|
+
|
435
|
+
# Completa as URLs relativas usando a base_url
|
436
|
+
full_urls = [urljoin(base_url, url) for url in urls_segmentos]
|
437
|
+
|
438
|
+
# Inicializa o dicionário para armazenar os dados dos segmentos
|
439
|
+
data_segments = {
|
440
|
+
'uris': full_urls, # Armazena as URLs completas
|
441
|
+
'urls': [],
|
442
|
+
'len': 0,
|
443
|
+
'enumerated_uris': [(index + 1, url) for index, url in enumerate(full_urls)],
|
444
|
+
'resolutions': {},
|
445
|
+
'codecs': []
|
446
|
+
}
|
447
|
+
|
448
|
+
# Busca por resoluções na playlist e armazena suas URLs correspondentes
|
449
|
+
resolution_pattern = r'RESOLUTION=(\d+x\d+)'
|
450
|
+
resolutions = re.findall(resolution_pattern, content)
|
451
|
+
|
452
|
+
codec_pattern = r'CODECS="([^"]+)"'
|
453
|
+
codecs = re.findall(codec_pattern, content)
|
454
|
+
|
455
|
+
for res in resolutions:
|
456
|
+
match = re.search(rf'#EXT-X-STREAM-INF:[^\n]*RESOLUTION={re.escape(res)}[^\n]*\n([^\n]+)', content)
|
457
|
+
if match:
|
458
|
+
url = match.group(1)
|
459
|
+
data_segments['urls'].append(urljoin(base_url, url)) # Completa a URL se for relativa
|
460
|
+
data_segments['resolutions'][res] = urljoin(base_url, url)
|
461
|
+
|
462
|
+
# Adiciona os codecs encontrados, evitando repetições
|
463
|
+
for codec in codecs:
|
464
|
+
if codec not in data_segments['codecs']:
|
465
|
+
data_segments['codecs'].append(codec)
|
466
|
+
|
467
|
+
# Adiciona a contagem de URLs de stream encontradas ao dicionário
|
468
|
+
data_segments['len'] = len(data_segments['urls'])
|
469
|
+
|
470
|
+
return data_segments
|
471
|
+
|
472
|
+
|
473
|
+
class EncryptSuport:
|
474
|
+
"""
|
475
|
+
suporte a operações de criptografia AES-128 e SAMPLE-AES relacionadas a M3U8.
|
476
|
+
Fornece métodos para obter a URL da chave de criptografia e o IV (vetor de inicialização) associado,
|
477
|
+
necessários para descriptografar conteúdos M3U8 protegidos por AES-128.
|
478
|
+
|
479
|
+
Métodos:
|
480
|
+
- get_url_key_m3u8: Extrai a URL da chave de criptografia e o IV de um conteúdo M3U8.
|
481
|
+
"""
|
482
|
+
|
483
|
+
@staticmethod
|
484
|
+
def get_url_key_m3u8(m3u8_content: str, player: str, headers=None):
|
485
|
+
"""
|
486
|
+
Extrai a URL da chave de criptografia AES-128 e o IV (vetor de inicialização) de um conteúdo M3U8.
|
487
|
+
|
488
|
+
Este método analisa o conteúdo M3U8 para localizar a URL da chave de criptografia e o IV, se disponível.
|
489
|
+
Em seguida, faz uma requisição HTTP para obter a chave em formato hexadecimal.
|
490
|
+
|
491
|
+
Args:
|
492
|
+
m3u8_content (str): String contendo o conteúdo do arquivo M3U8.
|
493
|
+
player (str): URL base para formar o URL completo da chave, se necessário.
|
494
|
+
headers (dict, optional): Cabeçalhos HTTP opcionais para a requisição da chave. Se não fornecido,
|
495
|
+
cabeçalhos padrão serão utilizados.
|
496
|
+
|
497
|
+
Returns:
|
498
|
+
dict: Um dicionário contendo as seguintes chaves:
|
499
|
+
- 'key' (str): A chave de criptografia em hexadecimal.
|
500
|
+
- 'iv' (str): O vetor de inicialização (IV) em hexadecimal, se disponível.
|
501
|
+
|
502
|
+
Caso não seja possível extrair a URL da chave ou o IV, retorna None.
|
503
|
+
|
504
|
+
Examples:
|
505
|
+
```python
|
506
|
+
m3u8_content = '''
|
507
|
+
#EXTM3U
|
508
|
+
#EXT-X-KEY:METHOD=AES-128,URI="https://example.com/key.bin",IV=0x1234567890abcdef
|
509
|
+
#EXTINF:10.0,
|
510
|
+
http://example.com/segment1.ts
|
511
|
+
'''
|
512
|
+
player = "https://example.com"
|
513
|
+
result = EncryptSuport.get_url_key_m3u8(m3u8_content, player)
|
514
|
+
print(result)
|
515
|
+
# Saída esperada:
|
516
|
+
# {'key': 'aabbccddeeff...', 'iv': '1234567890abcdef'}
|
517
|
+
|
518
|
+
# Com cabeçalhos personalizados
|
519
|
+
headers = {
|
520
|
+
"Authorization": "Bearer your_token"
|
521
|
+
}
|
522
|
+
result = EncryptSuport.get_url_key_m3u8(m3u8_content, player, headers=headers)
|
523
|
+
print(result)
|
524
|
+
```
|
525
|
+
|
526
|
+
Raises:
|
527
|
+
requests.HTTPError: Se a requisição HTTP para a chave falhar.
|
528
|
+
"""
|
529
|
+
pattern = r'#EXT-X-KEY:.*URI="([^"]+)"(?:.*,IV=(0x[0-9A-Fa-f]+))?'
|
530
|
+
match = re.search(pattern, m3u8_content)
|
531
|
+
data = {}
|
532
|
+
if match:
|
533
|
+
url_key = f"{player}{match.group(1)}"
|
534
|
+
iv_hex = match.group(2)
|
535
|
+
if not headers:
|
536
|
+
headers_default = {
|
537
|
+
"Accept": "application/json, text/plain, */*",
|
538
|
+
"Accept-Encoding": "gzip, deflate, br, zstd",
|
539
|
+
"Accept-Language": "pt-BR,pt;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6",
|
540
|
+
"Content-Length": "583",
|
541
|
+
"Content-Type": "text/plain",
|
542
|
+
"Sec-Fetch-Dest": "empty",
|
543
|
+
"Sec-Fetch-Mode": "cors",
|
544
|
+
"Sec-Fetch-Site": "same-origin",
|
545
|
+
"Sec-Ch-Ua": "\"Not:A-Brand\";v=\"99\", \"Google Chrome\";v=\"118\", \"Chromium\";v=\"118\"",
|
546
|
+
"Sec-Ch-Ua-Mobile": "?0",
|
547
|
+
"Sec-Ch-Ua-Platform": "\"Windows\"",
|
548
|
+
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko)"
|
549
|
+
"Chrome/118.0.0.0 Safari/537.36"
|
550
|
+
}
|
551
|
+
headers = headers_default
|
552
|
+
|
553
|
+
try:
|
554
|
+
resp = requests.get(url_key, headers=headers)
|
555
|
+
resp.raise_for_status()
|
556
|
+
key_bytes = resp.content
|
557
|
+
key_hex = key_bytes.hex()
|
558
|
+
data['key'] = key_hex
|
559
|
+
if iv_hex:
|
560
|
+
data['iv'] = iv_hex[2:] # Remove '0x' prefix
|
561
|
+
return data
|
562
|
+
except requests.exceptions.InvalidProxyURL as e:
|
563
|
+
raise M3u8NetworkingError(f"Erro: URL de proxy inválida: {e}")
|
564
|
+
except requests.exceptions.InvalidURL:
|
565
|
+
raise M3u8NetworkingError("Erro: URL inválida fornecida.")
|
566
|
+
except requests.exceptions.InvalidSchema:
|
567
|
+
raise M3u8NetworkingError("Erro: URL inválida, esquema não suportado.")
|
568
|
+
except requests.exceptions.MissingSchema:
|
569
|
+
raise M3u8NetworkingError("Erro: URL inválida, esquema ausente.")
|
570
|
+
except requests.exceptions.InvalidHeader as e:
|
571
|
+
raise M3u8NetworkingError(f"Erro de cabeçalho inválido: {e}")
|
572
|
+
except ValueError as e:
|
573
|
+
raise M3u8FileError(f"Erro de valor: {e}")
|
574
|
+
except requests.exceptions.ContentDecodingError as e:
|
575
|
+
raise M3u8NetworkingError(f"Erro de decodificação de conteúdo: {e}")
|
576
|
+
except requests.exceptions.BaseHTTPError as e:
|
577
|
+
raise M3u8NetworkingError(f"Erro HTTP básico: {e}")
|
578
|
+
except requests.exceptions.SSLError as e:
|
579
|
+
raise M3u8NetworkingError(f"Erro SSL: {e}")
|
580
|
+
except requests.exceptions.ProxyError as e:
|
581
|
+
raise M3u8NetworkingError(f"Erro de proxy: {e}")
|
582
|
+
except requests.exceptions.ConnectionError:
|
583
|
+
raise M3u8NetworkingError("Erro: O servidor ou o servidor encerrou a conexão.")
|
584
|
+
except requests.exceptions.HTTPError as e:
|
585
|
+
raise M3u8NetworkingError(f"Erro HTTP: {e}")
|
586
|
+
except requests.exceptions.Timeout:
|
587
|
+
raise M3u8NetworkingError(
|
588
|
+
"Erro de tempo esgotado: A conexão com o servidor demorou muito para responder.")
|
589
|
+
except requests.exceptions.TooManyRedirects:
|
590
|
+
raise M3u8NetworkingError("Erro de redirecionamento: Muitos redirecionamentos.")
|
591
|
+
except requests.exceptions.URLRequired:
|
592
|
+
raise M3u8NetworkingError("Erro: URL é necessária para a solicitação.")
|
593
|
+
except requests.exceptions.ChunkedEncodingError as e:
|
594
|
+
raise M3u8NetworkingError(f"Erro de codificação em partes: {e}")
|
595
|
+
except requests.exceptions.StreamConsumedError:
|
596
|
+
raise M3u8NetworkingError("Erro: Fluxo de resposta já consumido.")
|
597
|
+
except requests.exceptions.RetryError as e:
|
598
|
+
raise M3u8NetworkingError(f"Erro de tentativa: {e}")
|
599
|
+
except requests.exceptions.UnrewindableBodyError:
|
600
|
+
raise M3u8NetworkingError("Erro: Corpo da solicitação não pode ser rebobinado.")
|
601
|
+
except requests.exceptions.RequestException as e:
|
602
|
+
raise M3u8NetworkingError(
|
603
|
+
f"Erro de conexão: Não foi possível se conectar ao servidor. Detalhes: {e}")
|
604
|
+
|
605
|
+
else:
|
606
|
+
return None
|
607
|
+
|
608
|
+
|
609
|
+
class M3U8Playlist:
|
610
|
+
"""análise de maneira mais limpa de m3u8"""
|
611
|
+
|
612
|
+
def __init__(self, url: str, headers: dict = None):
|
613
|
+
self.__parsing = M3u8Analyzer()
|
614
|
+
self.__url = url
|
615
|
+
self.__version = ''
|
616
|
+
self.__number_segments = []
|
617
|
+
self.__uris = []
|
618
|
+
self.__codecs = []
|
619
|
+
self.__playlist_type = None
|
620
|
+
self.__headers = headers
|
621
|
+
if not (url.startswith('https://') or url.startswith('http://')):
|
622
|
+
raise ValueError("O Manifesto deve ser uma URL HTTPS ou HTTP!")
|
623
|
+
|
624
|
+
self.__load_playlist()
|
625
|
+
|
626
|
+
def __load_playlist(self):
|
627
|
+
"""
|
628
|
+
Método privado para carregar a playlist a partir de uma URL ou arquivo.
|
629
|
+
"""
|
630
|
+
self.__parsing = M3u8Analyzer()
|
631
|
+
self.__content = self.__parsing.get_m3u8(url_m3u8=self.__url, headers=self.__headers)
|
632
|
+
# Simulação do carregamento de uma playlist para este exemplo:
|
633
|
+
self.__uris = self.__parsing.get_segments(self.__content, self.__url).get('enumerated_uris')
|
634
|
+
self.__number_segments = len(self.__uris)
|
635
|
+
self.__playlist_type = self.__parsing.get_type_m3u8_content(self.__content)
|
636
|
+
self.__version = self.__get_version_manifest(content=self.__content)
|
637
|
+
self.__resolutions = self.__parsing.get_segments(self.__content, self.__url).get('resolutions')
|
638
|
+
self.__codecs = self.__parsing.get_segments(self.__content, self.__url).get('codecs')
|
639
|
+
|
640
|
+
def __get_version_manifest(self, content):
|
641
|
+
"""
|
642
|
+
Obtém a versão do manifesto #EXTM em uma playlist m3u8.
|
643
|
+
#EXT-X-VERSION:4
|
644
|
+
#EXT-X-VERSION:3
|
645
|
+
etc...
|
646
|
+
:param content: Conteúdo da playlist m3u8.
|
647
|
+
:return: A versão do manifesto encontrada ou None se não for encontrado.
|
648
|
+
"""
|
649
|
+
# Expressão regular para encontrar o manifesto
|
650
|
+
pattern = re.compile(r'#EXT-X-VERSION:(\d*)')
|
651
|
+
match = pattern.search(content)
|
652
|
+
|
653
|
+
if match:
|
654
|
+
# Retorna a versão encontrada
|
655
|
+
ver = f"#EXT-X-VERSION:{match.group(1)}"
|
656
|
+
return ver
|
657
|
+
|
658
|
+
else:
|
659
|
+
return '#EXT-X-VERSION:Undefined' # Default para versão 1 se não houver número
|
660
|
+
|
661
|
+
def get_codecs(self):
|
662
|
+
"""obter codecs na playlist"""
|
663
|
+
return self.__codecs
|
664
|
+
|
665
|
+
def info(self):
|
666
|
+
"""
|
667
|
+
Retorna informações básicas sobre a playlist.
|
668
|
+
|
669
|
+
Returns:
|
670
|
+
dict: Informações sobre a URL, versão do manifesto, número de segmentos, tipo da playlist, se é criptografada e URIs dos segmentos.
|
671
|
+
"""
|
672
|
+
info = {
|
673
|
+
"url": self.__url,
|
674
|
+
"version_manifest": self.__version,
|
675
|
+
"number_of_segments": self.__number_segments,
|
676
|
+
"playlist_type": self.__playlist_type,
|
677
|
+
"codecs": self.__codecs,
|
678
|
+
"encript": self.__is_encrypted(url=self.__url, headers=self.__headers),
|
679
|
+
"uris": self.__uris,
|
680
|
+
}
|
681
|
+
return info
|
682
|
+
|
683
|
+
def __is_encrypted(self, url, headers: dict = None):
|
684
|
+
parser = M3u8Analyzer()
|
685
|
+
m3u8_content = parser.get_m3u8(url)
|
686
|
+
player = parser.get_player_playlist(url)
|
687
|
+
try:
|
688
|
+
cript = EncryptSuport.get_url_key_m3u8(m3u8_content=m3u8_content,
|
689
|
+
player=player,
|
690
|
+
headers=headers)
|
691
|
+
except Exception as e:
|
692
|
+
raise ValueError(f"erro {e}")
|
693
|
+
return cript
|
694
|
+
|
695
|
+
def this_encrypted(self):
|
696
|
+
"""
|
697
|
+
Verifica se a playlist M3U8 está criptografada.
|
698
|
+
|
699
|
+
Returns:
|
700
|
+
bool: True se a playlist estiver criptografada, False caso contrário.
|
701
|
+
"""
|
702
|
+
return self.__is_encrypted(url=self.__url, headers=self.__headers)
|
703
|
+
|
704
|
+
def uris(self):
|
705
|
+
"""
|
706
|
+
Retorna a lista de URIs dos segmentos.
|
707
|
+
|
708
|
+
Returns:
|
709
|
+
list: Lista de URIs dos segmentos.
|
710
|
+
"""
|
711
|
+
return self.__uris
|
712
|
+
|
713
|
+
def version_manifest(self):
|
714
|
+
"""
|
715
|
+
Retorna a versão do manifesto da playlist M3U8.
|
716
|
+
|
717
|
+
Returns:
|
718
|
+
str: Versão do manifesto.
|
719
|
+
"""
|
720
|
+
return self.__version
|
721
|
+
|
722
|
+
def number_segments(self):
|
723
|
+
"""
|
724
|
+
Retorna o número total de segmentos na playlist.
|
725
|
+
|
726
|
+
Returns:
|
727
|
+
int: Número de segmentos.
|
728
|
+
"""
|
729
|
+
return self.__number_segments
|
730
|
+
|
731
|
+
def playlist_type(self):
|
732
|
+
"""
|
733
|
+
Retorna o tipo da playlist M3U8.
|
734
|
+
|
735
|
+
Returns:
|
736
|
+
str: Tipo da playlist.
|
737
|
+
"""
|
738
|
+
return self.__playlist_type
|
739
|
+
|
740
|
+
def get_resolutions(self):
|
741
|
+
"""
|
742
|
+
Retorna as resoluções disponíveis na playlist M3U8.
|
743
|
+
|
744
|
+
Returns:
|
745
|
+
list: Lista de resoluções.
|
746
|
+
"""
|
747
|
+
data = self.__resolutions
|
748
|
+
resolutions = []
|
749
|
+
for r in data:
|
750
|
+
resolutions.append(r)
|
751
|
+
return resolutions
|
752
|
+
|
753
|
+
def filter_resolution(self, filtering: str):
|
754
|
+
"""
|
755
|
+
Filtra e retorna a URL do segmento com a resolução especificada.
|
756
|
+
|
757
|
+
Args:
|
758
|
+
filtering (str): Resolução desejada (ex: '1920x1080').
|
759
|
+
|
760
|
+
Returns:
|
761
|
+
Optional[str]: URL do segmento correspondente à resolução, ou None se não encontrado.
|
762
|
+
"""
|
763
|
+
data = self.__resolutions
|
764
|
+
if filtering in data:
|
765
|
+
return data.get(filtering)
|
766
|
+
else:
|
767
|
+
return None
|
768
|
+
|
769
|
+
|
770
|
+
class Wrapper:
|
771
|
+
"""Classe para parsear playlists M3U8."""
|
772
|
+
|
773
|
+
@staticmethod
|
774
|
+
def parsing_m3u8(url: str, headers: dict = None) -> M3U8Playlist:
|
775
|
+
"""
|
776
|
+
Cria uma instância de M3U8Playlist a partir de uma URL de playlist M3U8.
|
777
|
+
|
778
|
+
Este método estático é utilizado para inicializar e retornar um objeto da classe `M3U8Playlist`,
|
779
|
+
que fornece funcionalidades para análise e manipulação de playlists M3U8.
|
780
|
+
|
781
|
+
Args:
|
782
|
+
url (str): URL da playlist M3U8 que deve ser parseada.
|
783
|
+
headers (Optional[dict]): Cabeçalhos HTTP adicionais para a requisição (opcional).
|
784
|
+
|
785
|
+
Returns:
|
786
|
+
M3U8Playlist: Uma instância da classe `M3U8Playlist` inicializada com a URL fornecida.
|
787
|
+
|
788
|
+
Raises:
|
789
|
+
ValueError: Se a URL não for uma URL válida.
|
790
|
+
|
791
|
+
Examples:
|
792
|
+
```python
|
793
|
+
url_playlist = "https://example.com/playlist.m3u8"
|
794
|
+
headers = {
|
795
|
+
"User-Agent": "CustomAgent/1.0"
|
796
|
+
}
|
797
|
+
|
798
|
+
playlist = ParsingM3u8.parsing_m3u8(url=url_playlist, headers=headers)
|
799
|
+
|
800
|
+
print(playlist.info())
|
801
|
+
```
|
802
|
+
|
803
|
+
Notes:
|
804
|
+
- Certifique-se de que a URL fornecida é uma URL válida e acessível.
|
805
|
+
- Se os cabeçalhos forem fornecidos, eles serão utilizados na requisição para obter o conteúdo da playlist.
|
806
|
+
"""
|
807
|
+
return M3U8Playlist(url=url, headers=headers)
|