udemy-userAPI 0.3.5__py3-none-any.whl → 0.3.6__py3-none-any.whl
Sign up to get free protection for your applications and to get access to all the features.
- udemy_userAPI/__version__.py +1 -1
- udemy_userAPI/api.py +139 -39
- udemy_userAPI/authenticate.py +1 -1
- udemy_userAPI/bultins.py +127 -50
- {udemy_userAPI-0.3.5.dist-info → udemy_userAPI-0.3.6.dist-info}/METADATA +13 -3
- udemy_userAPI-0.3.6.dist-info/RECORD +17 -0
- {udemy_userAPI-0.3.5.dist-info → udemy_userAPI-0.3.6.dist-info}/WHEEL +1 -1
- animation_consoles/__init__.py +0 -1
- animation_consoles/animation.py +0 -64
- ffmpeg_for_python/__config__.py +0 -118
- ffmpeg_for_python/__init__.py +0 -8
- ffmpeg_for_python/__utils.py +0 -78
- ffmpeg_for_python/__version__.py +0 -6
- ffmpeg_for_python/exeptions.py +0 -91
- ffmpeg_for_python/ffmpeg.py +0 -203
- m3u8_analyzer/M3u8Analyzer.py +0 -807
- m3u8_analyzer/__init__.py +0 -7
- m3u8_analyzer/__version__.py +0 -1
- m3u8_analyzer/exeptions.py +0 -82
- udemy_userAPI-0.3.5.dist-info/RECORD +0 -29
- {udemy_userAPI-0.3.5.dist-info → udemy_userAPI-0.3.6.dist-info}/LICENSE +0 -0
- {udemy_userAPI-0.3.5.dist-info → udemy_userAPI-0.3.6.dist-info}/top_level.txt +0 -0
m3u8_analyzer/M3u8Analyzer.py
DELETED
@@ -1,807 +0,0 @@
|
|
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)
|