udemy-userAPI 0.3.5__py3-none-any.whl → 0.3.7__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.
- udemy_userAPI/__version__.py +1 -1
- udemy_userAPI/api.py +139 -39
- udemy_userAPI/authenticate.py +1 -1
- udemy_userAPI/bultins.py +243 -56
- udemy_userAPI/sections.py +2 -1
- udemy_userAPI/udemy.py +13 -4
- {udemy_userAPI-0.3.5.dist-info → udemy_userAPI-0.3.7.dist-info}/METADATA +13 -3
- udemy_userAPI-0.3.7.dist-info/RECORD +17 -0
- {udemy_userAPI-0.3.5.dist-info → udemy_userAPI-0.3.7.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.7.dist-info}/LICENSE +0 -0
- {udemy_userAPI-0.3.5.dist-info → udemy_userAPI-0.3.7.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)
|