udemy-userAPI 0.3.2__py3-none-any.whl

Sign up to get free protection for your applications and to get access to all the features.
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)