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.
@@ -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)