udemy-userAPI 0.3.2__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/api.py ADDED
@@ -0,0 +1,691 @@
1
+ import json
2
+ import hashlib
3
+ import hmac
4
+ import math
5
+ from datetime import datetime
6
+ from .exeptions import UdemyUserApiExceptions, UnhandledExceptions, LoginException
7
+ from .authenticate import UdemyAuth
8
+ import os.path
9
+ from pywidevine.cdm import Cdm
10
+ from pywidevine.device import Device
11
+ from pywidevine.pssh import PSSH
12
+ import requests
13
+ import base64
14
+
15
+
16
+ AUTH = UdemyAuth()
17
+ COOKIES = AUTH._load_cookies()
18
+
19
+ HEADERS_USER = {
20
+ "accept": "*/*",
21
+ "accept-language": "pt-BR,pt;q=0.9,en-US;q=0.8,en;q=0.7",
22
+ "cache-control": "no-cache",
23
+ "Content-Type": "text/plain",
24
+ "pragma": "no-cache",
25
+ "sec-ch-ua": "\"Chromium\";v=\"118\", \"Google Chrome\";v=\"118\", \"Not=A?Brand\";v=\"99\"",
26
+ "sec-ch-ua-mobile": "?0",
27
+ "sec-ch-ua-platform": "\"Windows\"",
28
+ "sec-fetch-dest": "empty",
29
+ "sec-fetch-mode": "cors",
30
+ "sec-fetch-site": "cross-site",
31
+ "Cookie": COOKIES,
32
+ "Referer": "https://www.udemy.com/"}
33
+ HEADERS_octet_stream = {
34
+ 'authority': 'www.udemy.com',
35
+ 'pragma': 'no-cache',
36
+ 'cache-control': 'no-cache',
37
+ 'sec-ch-ua': '" Not;A Brand";v="99", "Google Chrome";v="97", "Chromium";v="97"',
38
+ 'accept': 'application/json, text/plain, */*',
39
+ "Cookie": COOKIES,
40
+ 'dnt': '1',
41
+ 'content-type': 'application/octet-stream',
42
+ 'sec-ch-ua-mobile': '?0',
43
+ 'sec-ch-ua-platform': '"Windows"',
44
+ 'origin': 'https://www.udemy.com',
45
+ 'sec-fetch-site': 'same-origin',
46
+ 'sec-fetch-mode': 'cors',
47
+ 'sec-fetch-dest': 'empty',
48
+ 'accept-language': 'en-US,en;q=0.9',
49
+ }
50
+
51
+ locate = os.path.dirname(__file__)
52
+ WVD_FILE_PATH = os.path.join(locate, 'mpd_analyzer', 'bin.wvd')
53
+ device = Device.load(WVD_FILE_PATH)
54
+ cdm = Cdm.from_device(device)
55
+
56
+
57
+ def read_pssh_from_bytes(bytess):
58
+ pssh_offset = bytess.rfind(b'pssh')
59
+ _start = pssh_offset - 4
60
+ _end = pssh_offset - 4 + bytess[pssh_offset - 1]
61
+ pssh = bytess[_start:_end]
62
+ return pssh
63
+
64
+
65
+ def get_pssh(init_url):
66
+ from .authenticate import UdemyAuth
67
+ auth = UdemyAuth()
68
+ if not auth.verif_login():
69
+ raise LoginException("Sessão expirada!")
70
+ res = requests.get(init_url, headers=HEADERS_octet_stream)
71
+ if not res.ok:
72
+ return
73
+ pssh = read_pssh_from_bytes(res.content)
74
+ return base64.b64encode(pssh).decode("utf-8")
75
+
76
+
77
+ def get_highest_resolution(resolutions):
78
+ """
79
+ Retorna a maior resolução em uma lista de resoluções.
80
+
81
+ Args:
82
+ resolutions (list of tuple): Lista de resoluções, onde cada tupla representa (largura, altura).
83
+
84
+ Returns:
85
+ tuple: A maior resolução em termos de largura e altura.
86
+ """
87
+ if not resolutions:
88
+ return None
89
+ return max(resolutions, key=lambda res: (res[0], res[1]))
90
+
91
+
92
+ def organize_streams(streams):
93
+ if not streams:
94
+ return {}
95
+ organized_streams = {
96
+ 'dash': [],
97
+ 'hls': []
98
+ }
99
+
100
+ best_video = None
101
+
102
+ for stream in streams:
103
+ # Verifica e adiciona streams DASH
104
+ if stream['type'] == 'application/dash+xml':
105
+ organized_streams['dash'].append({
106
+ 'src': stream['src'],
107
+ 'label': stream.get('label', 'unknown')
108
+ })
109
+
110
+ # Verifica e adiciona streams HLS (m3u8)
111
+ elif stream['type'] == 'application/x-mpegURL':
112
+ organized_streams['hls'].append({
113
+ 'src': stream['src'],
114
+ 'label': stream.get('label', 'auto')
115
+ })
116
+
117
+ # Verifica streams de vídeo (mp4)
118
+ elif stream['type'] == 'video/mp4':
119
+ # Seleciona o vídeo com a maior resolução (baseado no label)
120
+ if best_video is None or int(stream['label']) > int(best_video['label']):
121
+ best_video = {
122
+ 'src': stream['src'],
123
+ 'label': stream['label']
124
+ }
125
+
126
+ # Adiciona o melhor vídeo encontrado na lista 'hls'
127
+ if best_video:
128
+ organized_streams['hls'].append(best_video)
129
+
130
+ return organized_streams
131
+
132
+
133
+ def extract(pssh, license_token):
134
+ from .authenticate import UdemyAuth
135
+ auth = UdemyAuth()
136
+ if not auth.verif_login():
137
+ raise LoginException("Sessão expirada!")
138
+ license_url = (f"https://www.udemy.com/api-2.0/media-license-server/validate-auth-token?drm_type=widevine"
139
+ f"&auth_token={license_token}")
140
+ session_id = cdm.open()
141
+ challenge = cdm.get_license_challenge(session_id, PSSH(pssh))
142
+ license_file = requests.post(license_url, headers=HEADERS_octet_stream, data=challenge)
143
+ try:
144
+ str(license_file.content, "utf-8")
145
+ except Exception as e:
146
+ base64.b64encode(license_file.content).decode()
147
+ else:
148
+ if "CAIS" not in license_file.text:
149
+ return
150
+ cdm.parse_license(session_id, license_file.content)
151
+ final_keys = ""
152
+ for key in cdm.get_keys(session_id):
153
+ if key.type == "CONTENT":
154
+ final_keys += f"{key.kid.hex}:{key.key.hex()}"
155
+ cdm.close(session_id)
156
+
157
+ if final_keys == "":
158
+ return
159
+ return final_keys.strip()
160
+
161
+
162
+ def get_mpd_file(mpd_url):
163
+ from .authenticate import UdemyAuth
164
+ auth = UdemyAuth()
165
+ if not auth.verif_login():
166
+ raise LoginException("Sessão expirada!")
167
+ try:
168
+ # Faz a solicitação GET com os cabeçalhos
169
+ response = requests.get(mpd_url, headers=HEADERS_USER)
170
+ # Exibe o código de status
171
+ if response.status_code == 200:
172
+ return response.text
173
+ else:
174
+ raise UnhandledExceptions(f"erro ao obter dados de aulas!! {response.status_code}")
175
+ except requests.ConnectionError as e:
176
+ raise UdemyUserApiExceptions(f"Erro de conexão: {e}")
177
+ except requests.Timeout as e:
178
+ raise UdemyUserApiExceptions(f"Tempo de requisição excedido: {e}")
179
+ except requests.TooManyRedirects as e:
180
+ raise UdemyUserApiExceptions(f"Limite de redirecionamentos excedido: {e}")
181
+ except requests.HTTPError as e:
182
+ raise UdemyUserApiExceptions(f"Erro HTTP: {e}")
183
+ except Exception as e:
184
+ raise UnhandledExceptions(f"Errro Ao Obter Mídias:{e}")
185
+
186
+
187
+ def parser_chapers(results):
188
+ """
189
+ :param results:
190
+ :return:
191
+ """
192
+ if not results:
193
+ raise UdemyUserApiExceptions("Não foi possível obter detalhes do curso!")
194
+ results = results.get('results', None)
195
+ if not results:
196
+ raise UdemyUserApiExceptions("Não foi possível obter detalhes do curso!")
197
+ chapters_dict = {} # Dicionário para armazenar os capítulos e seus vídeos correspondentes
198
+
199
+ # Primeiro, construímos um dicionário de capítulos
200
+ current_chapter = None
201
+ for dictionary in results:
202
+ _class = dictionary.get('_class')
203
+
204
+ if _class == 'chapter':
205
+ chapter_index = dictionary.get('object_index')
206
+ current_chapter = {
207
+ 'title_chapter': dictionary.get('title'),
208
+ 'videos_in_chapter': []
209
+ }
210
+ chapters_dict[f"chapter_{chapter_index}"] = current_chapter
211
+ elif _class == 'lecture' and current_chapter is not None:
212
+ asset = dictionary.get('asset')
213
+ if asset:
214
+ video_title = dictionary.get('title', None)
215
+ if not video_title:
216
+ video_title = 'Files'
217
+ current_chapter['videos_in_chapter'].append({
218
+ 'video_title': video_title,
219
+ 'title_lecture': dictionary.get('title'),
220
+ 'lecture_id': dictionary.get('id'),
221
+ 'asset_id': asset.get('id')
222
+ })
223
+ return chapters_dict
224
+
225
+
226
+ def get_add_files(course_id: int):
227
+ """
228
+ Obtém arquivos adicionais de um curso.
229
+
230
+ Args:
231
+ course_id (int): ID do curso.
232
+
233
+ Returns:
234
+ dict: Um dicionário contendo os arquivos adicionais do curso.
235
+
236
+ Raises: LoginException: Se a sessão estiver expirada. UdemyUserApiExceptions: Se houver erro de conexão,
237
+ tempo de requisição excedido, limite de redirecionamentos excedido ou erro HTTP. UnhandledExceptions: Se houver
238
+ erro ao obter dados das aulas.
239
+ """
240
+ from .authenticate import UdemyAuth
241
+ auth = UdemyAuth()
242
+ if not auth.verif_login():
243
+ raise LoginException("Sessão expirada!")
244
+ url = (f'https://www.udemy.com/api-2.0/courses/{course_id}/subscriber-curriculum-items/?page_size=2000&fields['
245
+ f'lecture]=title,object_index,is_published,sort_order,created,asset,supplementary_assets,is_free&fields['
246
+ f'quiz]=title,object_index,is_published,sort_order,type&fields[practice]=title,object_index,is_published,'
247
+ f'sort_order&fields[chapter]=title,object_index,is_published,sort_order&fields[asset]=title,filename,'
248
+ f'asset_type,status,time_estimation,is_external&caching_intent=True')
249
+ try:
250
+ # Faz a solicitação GET com os cabeçalhos
251
+ response = requests.get(url, headers=HEADERS_USER)
252
+ data = []
253
+ # Exibe o código de status
254
+ if response.status_code == 200:
255
+ a = json.loads(response.text)
256
+ return a
257
+ else:
258
+ raise UnhandledExceptions(f"Erro ao obter dados de aulas! Código de status: {response.status_code}")
259
+
260
+ except requests.ConnectionError as e:
261
+ raise UdemyUserApiExceptions(f"Erro de conexão: {e}")
262
+ except requests.Timeout as e:
263
+ raise UdemyUserApiExceptions(f"Tempo de requisição excedido: {e}")
264
+ except requests.TooManyRedirects as e:
265
+ raise UdemyUserApiExceptions(f"Limite de redirecionamentos excedido: {e}")
266
+ except requests.HTTPError as e:
267
+ raise UdemyUserApiExceptions(f"Erro HTTP: {e}")
268
+ except Exception as e:
269
+ raise UnhandledExceptions(f"Erro ao obter mídias: {e}")
270
+
271
+
272
+ def get_files_aule(lecture_id_filter, data: list):
273
+ """
274
+ Filtra e obtém arquivos adicionais para uma aula específica.
275
+
276
+ Args:
277
+ lecture_id_filter: ID da aula a ser filtrada.
278
+ data (list): Lista de dados contendo informações dos arquivos.
279
+
280
+ Returns:
281
+ list: Lista de arquivos filtrados.
282
+ """
283
+ files = []
284
+ for files_data in data:
285
+ lecture_id = files_data.get('lecture_id')
286
+ if lecture_id == lecture_id_filter:
287
+ files.append(files_data)
288
+ return files
289
+
290
+
291
+ def get_links(course_id: int, id_lecture: int):
292
+ """
293
+ Obtém links e informações de uma aula específica.
294
+
295
+ Args:
296
+ course_id (int): ID do curso.
297
+ id_lecture (int): ID da aula.
298
+
299
+ Returns:
300
+ dict: Um dicionário contendo links e informações da aula.
301
+
302
+ Raises: LoginException: Se a sessão estiver expirada. UdemyUserApiExceptions: Se houver erro de conexão,
303
+ tempo de requisição excedido, limite de redirecionamentos excedido ou erro HTTP. UnhandledExceptions: Se houver
304
+ erro ao obter dados das aulas.
305
+ """
306
+ get = (f"https://www.udemy.com/api-2.0/users/me/subscribed-courses/{course_id}/lectures/{id_lecture}/?"
307
+ f"fields[lecture]"
308
+ f"=asset,description,download_url,is_free,last_watched_second&fields[asset]=asset_type,length,"
309
+ f"media_license_token,course_is_drmed,media_sources,captions,thumbnail_sprite,slides,slide_urls,"
310
+ f"download_urls,"
311
+ f"external_url&q=0.3108014137011559/?fields[asset]=download_urls")
312
+ from .authenticate import UdemyAuth
313
+ auth = UdemyAuth()
314
+ if not auth.verif_login():
315
+ raise LoginException("Sessão expirada!")
316
+ try:
317
+ # Faz a solicitação GET com os cabeçalhos
318
+ response = requests.get(get, headers=HEADERS_USER)
319
+ data = []
320
+ # Exibe o código de status
321
+ if response.status_code == 200:
322
+ a = json.loads(response.text)
323
+ return a
324
+ else:
325
+ raise UnhandledExceptions(f"Erro ao obter dados de aulas! Código de status: {response.status_code}")
326
+
327
+ except requests.ConnectionError as e:
328
+ raise UdemyUserApiExceptions(f"Erro de conexão: {e}")
329
+ except requests.Timeout as e:
330
+ raise UdemyUserApiExceptions(f"Tempo de requisição excedido: {e}")
331
+ except requests.TooManyRedirects as e:
332
+ raise UdemyUserApiExceptions(f"Limite de redirecionamentos excedido: {e}")
333
+ except requests.HTTPError as e:
334
+ raise UdemyUserApiExceptions(f"Erro HTTP: {e}")
335
+ except Exception as e:
336
+ raise UnhandledExceptions(f"Erro ao obter mídias: {e}")
337
+
338
+
339
+ def remove_tag(d: str):
340
+ new = d.replace("<p>", '').replace("</p>", '').replace('&nbsp;', ' ')
341
+ return new
342
+
343
+
344
+ def get_external_liks(course_id: int, id_lecture, asset_id):
345
+ """
346
+ Obtém links externos para um asset específico de uma aula.
347
+
348
+ Args:
349
+ course_id (int): ID do curso.
350
+ id_lecture: ID da aula.
351
+ asset_id: ID do asset.
352
+
353
+ Returns:
354
+ dict: Um dicionário contendo os links externos do asset.
355
+
356
+ Raises: LoginException: Se a sessão estiver expirada. UdemyUserApiExceptions: Se houver erro de conexão,
357
+ tempo de requisição excedido, limite de redirecionamentos excedido ou erro HTTP. UnhandledExceptions: Se houver
358
+ erro ao obter dados das aulas.
359
+ """
360
+ from .authenticate import UdemyAuth
361
+ auth = UdemyAuth()
362
+ if not auth.verif_login():
363
+ raise LoginException("Sessão expirada!")
364
+ url = (f'https://www.udemy.com/api-2.0/users/me/subscribed-courses/{course_id}/lectures/{id_lecture}/'
365
+ f'supplementary-assets/{asset_id}/?fields[asset]=external_url')
366
+ try:
367
+ # Faz a solicitação GET com os cabeçalhos
368
+ response = requests.get(url, headers=HEADERS_USER)
369
+ data = []
370
+ # Exibe o código de status
371
+ if response.status_code == 200:
372
+ a = json.loads(response.text)
373
+ return a
374
+ else:
375
+ raise UnhandledExceptions(f"Erro ao obter dados de aulas! Código de status: {response.status_code}")
376
+
377
+ except requests.ConnectionError as e:
378
+ raise UdemyUserApiExceptions(f"Erro de conexão: {e}")
379
+ except requests.Timeout as e:
380
+ raise UdemyUserApiExceptions(f"Tempo de requisição excedido: {e}")
381
+ except requests.TooManyRedirects as e:
382
+ raise UdemyUserApiExceptions(f"Limite de redirecionamentos excedido: {e}")
383
+ except requests.HTTPError as e:
384
+ raise UdemyUserApiExceptions(f"Erro HTTP: {e}")
385
+ except Exception as e:
386
+ raise UnhandledExceptions(f"Erro ao obter mídias: {e}")
387
+
388
+
389
+ def extract_files(supplementary_assets: list) -> list:
390
+ """
391
+ Obtém o ID da lecture, o ID do asset, o asset_type e o filename.
392
+
393
+ Args:
394
+ supplementary_assets (list): Lista de assets suplementares.
395
+
396
+ Returns:
397
+ list: Lista de dicionários contendo informações dos assets.
398
+ """
399
+ files = []
400
+ for item in supplementary_assets:
401
+ lecture_title = item.get('lecture_title')
402
+ lecture_id = item.get('lecture_id')
403
+ asset = item.get('asset', {})
404
+ asset_id = asset.get('id')
405
+ asset_type = asset.get('asset_type')
406
+ filename = asset.get('filename')
407
+ title = asset.get('title')
408
+ external_url = asset.get('is_external', None)
409
+ files.append({
410
+ 'lecture_id': lecture_id,
411
+ 'asset_id': asset_id,
412
+ 'asset_type': asset_type,
413
+ 'filename': filename,
414
+ 'title': title,
415
+ 'lecture_title': lecture_title,
416
+ 'ExternalLink': external_url
417
+ })
418
+ return files
419
+
420
+
421
+ def extract_course_data(course_dict) -> dict:
422
+ """
423
+ Extrai dados do curso de um dicionário de informações do curso.
424
+
425
+ Args:
426
+ course_dict (dict): Dicionário contendo dados do curso.
427
+
428
+ Returns:
429
+ dict: Dicionário contendo dados extraídos do curso.
430
+ """
431
+ # Extrair informações principais
432
+ course_id = course_dict.get('id')
433
+ title = course_dict.get('title')
434
+ num_subscribers = course_dict.get('num_subscribers')
435
+ avg_rating_recent = course_dict.get('avg_rating_recent')
436
+ estimated_content_length = course_dict.get('estimated_content_length')
437
+
438
+ # Extrair informações dos instrutores
439
+ instructors = course_dict.get('visible_instructors', [])
440
+ instructor_data = []
441
+ for instructor in instructors:
442
+ instructor_data.append({
443
+ 'id': instructor.get('id'),
444
+ 'title': instructor.get('title'),
445
+ 'name': instructor.get('name'),
446
+ 'display_name': instructor.get('display_name'),
447
+ 'job_title': instructor.get('job_title'),
448
+ 'image_50x50': instructor.get('image_50x50'),
449
+ 'image_100x100': instructor.get('image_100x100'),
450
+ 'initials': instructor.get('initials'),
451
+ 'url': instructor.get('url'),
452
+ })
453
+
454
+ # Extrair informações de localização
455
+ locale = course_dict.get('locale', {})
456
+ locale_data = {
457
+ 'locale': locale.get('locale'),
458
+ 'title': locale.get('title'),
459
+ 'english_title': locale.get('english_title'),
460
+ 'simple_english_title': locale.get('simple_english_title'),
461
+ }
462
+
463
+ # Extrair informações de categorias e subcategorias
464
+ primary_category = course_dict.get('primary_category', {})
465
+ primary_category_data = {
466
+ 'id': primary_category.get('id'),
467
+ 'title': primary_category.get('title'),
468
+ 'title_cleaned': primary_category.get('title_cleaned'),
469
+ 'url': primary_category.get('url'),
470
+ 'icon_class': primary_category.get('icon_class'),
471
+ 'type': primary_category.get('type'),
472
+ }
473
+
474
+ primary_subcategory = course_dict.get('primary_subcategory', {})
475
+ primary_subcategory_data = {
476
+ 'id': primary_subcategory.get('id'),
477
+ 'title': primary_subcategory.get('title'),
478
+ 'title_cleaned': primary_subcategory.get('title_cleaned'),
479
+ 'url': primary_subcategory.get('url'),
480
+ 'icon_class': primary_subcategory.get('icon_class'),
481
+ 'type': primary_subcategory.get('type'),
482
+ }
483
+
484
+ # Extrair informações contextuais
485
+ context_info = course_dict.get('context_info', {})
486
+ category_info = context_info.get('category', {})
487
+ label_info = context_info.get('label', {})
488
+
489
+ category_data = {
490
+ 'id': category_info.get('id'),
491
+ 'title': category_info.get('title'),
492
+ 'url': category_info.get('url'),
493
+ 'tracking_object_type': category_info.get('tracking_object_type'),
494
+ }
495
+
496
+ label_data = {
497
+ 'id': label_info.get('id'),
498
+ 'display_name': label_info.get('display_name'),
499
+ 'title': label_info.get('title'),
500
+ 'topic_channel_url': label_info.get('topic_channel_url'),
501
+ 'url': label_info.get('url'),
502
+ 'tracking_object_type': label_info.get('tracking_object_type'),
503
+ }
504
+
505
+ # Compilar todos os dados em um dicionário
506
+ result = {
507
+ 'course_id': course_id,
508
+ 'title': title,
509
+ 'num_subscribers': num_subscribers,
510
+ 'avg_rating_recent': avg_rating_recent,
511
+ 'estimated_content_length': estimated_content_length,
512
+ 'instructors': instructor_data,
513
+ 'locale': locale_data,
514
+ 'primary_category': primary_category_data,
515
+ 'primary_subcategory': primary_subcategory_data,
516
+ 'category_info': category_data,
517
+ 'label_info': label_data,
518
+ }
519
+
520
+ return result
521
+
522
+
523
+ def format_size(byte_size):
524
+ # Constantes para conversão
525
+ KB = 1024
526
+ MB = KB ** 2
527
+ GB = KB ** 3
528
+ TB = KB ** 4
529
+ try:
530
+ byte_size = int(byte_size)
531
+
532
+ if byte_size < KB:
533
+ return f"{byte_size} bytes"
534
+ elif byte_size < MB:
535
+ return f"{byte_size / KB:.2f} KB"
536
+ elif byte_size < GB:
537
+ return f"{byte_size / MB:.2f} MB"
538
+ elif byte_size < TB:
539
+ return f"{byte_size / GB:.2f} GB"
540
+ else:
541
+ return f"{byte_size / TB:.2f} TB"
542
+ except Exception as e:
543
+ return byte_size
544
+
545
+
546
+ def lecture_infor(course_id: int, id_lecture: int):
547
+ """
548
+ Obtém informações de uma aula específica.
549
+
550
+ Args:
551
+ course_id (int): ID do curso.
552
+ id_lecture (int): ID da aula.
553
+
554
+ Returns:
555
+ dict: Um dicionário contendo as informações da aula.
556
+
557
+ Raises:
558
+ LoginException: Se a sessão estiver expirada.
559
+ ConnectionError: Se houver erro ao obter as informações da aula.
560
+ """
561
+ from .authenticate import UdemyAuth
562
+ auth = UdemyAuth()
563
+ if not auth.verif_login():
564
+ raise LoginException("Sessão expirada!")
565
+ edpoint = (f"https://www.udemy.com/api-2.0/users/me/subscribed-courses/{course_id}/lectures/{id_lecture}/?"
566
+ f"fields[asset]=media_license_token")
567
+ r = requests.get(edpoint, headers=HEADERS_USER)
568
+ if r.status_code == 200:
569
+ return json.loads(r.text)
570
+ else:
571
+ raise ConnectionError(f"Erro ao obter informações da aula:{r.status_code}"
572
+ f"\n\n"
573
+ f"{r.text}")
574
+
575
+
576
+ def assets_infor(course_id: int, id_lecture: int, assets_id: int):
577
+ """
578
+ Obtém informações de um asset específico de uma aula.
579
+
580
+ Args:
581
+ course_id (int): ID do curso.
582
+ id_lecture (int): ID da aula.
583
+ assets_id (int): ID do asset.
584
+
585
+ Returns:
586
+ str: Conteúdo HTML do asset.
587
+
588
+ Raises:
589
+ LoginException: Se a sessão estiver expirada.
590
+ ConnectionError: Se houver erro ao obter as informações do asset.
591
+ """
592
+ from .authenticate import UdemyAuth
593
+ auth = UdemyAuth()
594
+ if not auth.verif_login():
595
+ raise LoginException("Sessão expirada!")
596
+ endpoint = (f'https://www.udemy.com/api-2.0/assets/{assets_id}/?fields[asset]=@min,status,delayed_asset_message,'
597
+ f'processing_errors,body&course_id={course_id}&lecture_id={id_lecture}')
598
+ r = requests.get(endpoint, headers=HEADERS_USER)
599
+ if r.status_code == 200:
600
+ dt = json.loads(r.text)
601
+ body = dt.get("body")
602
+ title = lecture_infor(course_id=course_id, id_lecture=id_lecture).get("title")
603
+ return save_html(body, title_lecture=title)
604
+ else:
605
+ raise ConnectionError(f"Erro ao obter informações de assets! {r.status_code}"
606
+ f"\n\n"
607
+ f"{r.text}")
608
+
609
+
610
+ def save_html(body, title_lecture):
611
+ html_content = f"""<!DOCTYPE html>
612
+ <html lang="en">
613
+ <head>
614
+ <meta charset="UTF-8">
615
+ <title>{title_lecture}</title>
616
+ </head>
617
+ <body>
618
+ {body}
619
+ </body>
620
+ </html>"""
621
+
622
+ return html_content
623
+
624
+
625
+ def J(e, t):
626
+ """
627
+ Gera um identificador único baseado na data atual e nas funções X e ee.
628
+
629
+ Args:
630
+ e (str): Um identificador.
631
+ t (str): Um tipo de identificador.
632
+
633
+ Returns:
634
+ str: Um identificador único.
635
+ """
636
+ r = datetime.now()
637
+ s = r.isoformat()[:10]
638
+ return s + X(e, s, t)
639
+
640
+
641
+ def X(e, t, r):
642
+ """
643
+ Gera um código HMAC-SHA256 baseado nos parâmetros fornecidos.
644
+
645
+ Args:
646
+ e (str): Um identificador.
647
+ t (str): Um timestamp.
648
+ r (str): Um identificador de tipo.
649
+
650
+ Returns:
651
+ str: Um código gerado.
652
+ """
653
+ s = 0
654
+ while True:
655
+ o = ee(s)
656
+ a = hmac.new(r.encode(), (e + t + o).encode(), hashlib.sha256).digest()
657
+ if te(16, a):
658
+ return o
659
+ s += 1
660
+
661
+
662
+ def ee(e):
663
+ """
664
+ Gera uma string baseada no valor do contador.
665
+
666
+ Args:
667
+ e (int): Um valor do contador.
668
+
669
+ Returns:
670
+ str: Uma string gerada.
671
+ """
672
+ if e < 0:
673
+ return ""
674
+ return ee(e // 26 - 1) + chr(65 + e % 26)
675
+
676
+
677
+ def te(e, t):
678
+ """
679
+ Verifica se a sequência de bits gerada começa com um número específico de zeros.
680
+
681
+ Args:
682
+ e (int): O número de zeros.
683
+ t (bytes): A sequência de bytes.
684
+
685
+ Returns:
686
+ bool: True se a sequência começa com o número especificado de zeros, False caso contrário.
687
+ """
688
+ r = math.ceil(e / 8)
689
+ s = t[:r]
690
+ o = ''.join(format(byte, '08b') for byte in s)
691
+ return o.startswith('0' * e)