udemy-userAPI 0.1.5__tar.gz → 0.1.7__tar.gz

Sign up to get free protection for your applications and to get access to all the features.
Files changed (25) hide show
  1. {udemy_userapi-0.1.5/udemy_userAPI.egg-info → udemy_userapi-0.1.7}/PKG-INFO +2 -2
  2. {udemy_userapi-0.1.5 → udemy_userapi-0.1.7}/README.md +1 -1
  3. {udemy_userapi-0.1.5 → udemy_userapi-0.1.7}/README_PYPI.md +1 -1
  4. {udemy_userapi-0.1.5 → udemy_userapi-0.1.7}/udemy_userAPI/__version__.py +1 -1
  5. udemy_userapi-0.1.7/udemy_userAPI/api.py +501 -0
  6. udemy_userapi-0.1.7/udemy_userAPI/authenticate.py +182 -0
  7. {udemy_userapi-0.1.5 → udemy_userapi-0.1.7}/udemy_userAPI/bultins.py +114 -21
  8. udemy_userapi-0.1.7/udemy_userAPI/mpd_analyzer/__init__.py +3 -0
  9. udemy_userapi-0.1.7/udemy_userAPI/mpd_analyzer/mpd_parser.py +357 -0
  10. {udemy_userapi-0.1.5 → udemy_userapi-0.1.7}/udemy_userAPI/udemy.py +18 -3
  11. {udemy_userapi-0.1.5 → udemy_userapi-0.1.7/udemy_userAPI.egg-info}/PKG-INFO +2 -2
  12. {udemy_userapi-0.1.5 → udemy_userapi-0.1.7}/udemy_userAPI.egg-info/SOURCES.txt +3 -1
  13. udemy_userapi-0.1.5/udemy_userAPI/api.py +0 -259
  14. udemy_userapi-0.1.5/udemy_userAPI/authenticate.py +0 -162
  15. {udemy_userapi-0.1.5 → udemy_userapi-0.1.7}/LICENSE +0 -0
  16. {udemy_userapi-0.1.5 → udemy_userapi-0.1.7}/MANIFEST.in +0 -0
  17. {udemy_userapi-0.1.5 → udemy_userapi-0.1.7}/setup.cfg +0 -0
  18. {udemy_userapi-0.1.5 → udemy_userapi-0.1.7}/setup.py +0 -0
  19. {udemy_userapi-0.1.5 → udemy_userapi-0.1.7}/udemy_userAPI/__init__.py +0 -0
  20. {udemy_userapi-0.1.5 → udemy_userapi-0.1.7}/udemy_userAPI/exeptions.py +0 -0
  21. {udemy_userapi-0.1.5 → udemy_userapi-0.1.7}/udemy_userAPI/sections.py +0 -0
  22. {udemy_userapi-0.1.5 → udemy_userapi-0.1.7}/udemy_userAPI.egg-info/dependency_links.txt +0 -0
  23. {udemy_userapi-0.1.5 → udemy_userapi-0.1.7}/udemy_userAPI.egg-info/not-zip-safe +0 -0
  24. {udemy_userapi-0.1.5 → udemy_userapi-0.1.7}/udemy_userAPI.egg-info/requires.txt +0 -0
  25. {udemy_userapi-0.1.5 → udemy_userapi-0.1.7}/udemy_userAPI.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: udemy_userAPI
3
- Version: 0.1.5
3
+ Version: 0.1.7
4
4
  Summary: Obtenha detalhes de cursos que o usuário esteja inscrito da plataforma Udemy,usando o EndPoint de usuário o mesmo que o navegador utiliza para acessar e redenrizar os cursos.
5
5
  Author: PauloCesar-dev404
6
6
  Author-email: paulocesar0073dev404@gmail.com
@@ -18,7 +18,7 @@ Requires-Dist: cloudscraper
18
18
  # udemy-userAPI
19
19
 
20
20
 
21
- ![Versão](https://img.shields.io/badge/version-0.1.5-orange)
21
+ ![Versão](https://img.shields.io/badge/version-0.1.6-orange)
22
22
  ![Licença](https://img.shields.io/badge/license-MIT-orange)
23
23
  [![Sponsor](https://img.shields.io/badge/💲Donate-yellow)](https://apoia.se/paulocesar-dev404)
24
24
  [![Sponsor](https://img.shields.io/badge/Documentation-green)](https://github.com/PauloCesar-dev404/udemy-userAPI/wiki)
@@ -2,7 +2,7 @@
2
2
  <img src="assets/udemy_userAPI-logo.png" alt="udemy_userAPI-logo" width="200"/>
3
3
 
4
4
 
5
- ![Versão](https://img.shields.io/badge/version-0.1.5-orange)
5
+ ![Versão](https://img.shields.io/badge/version-0.1.6-orange)
6
6
  ![Licença](https://img.shields.io/badge/license-MIT-orange)
7
7
  [![Sponsor](https://img.shields.io/badge/💲Donate-yellow)](https://apoia.se/paulocesar-dev404)
8
8
  [![Sponsor](https://img.shields.io/badge/Documentation-green)](https://github.com/PauloCesar-dev404/udemy-userAPI/wiki)
@@ -1,7 +1,7 @@
1
1
  # udemy-userAPI
2
2
 
3
3
 
4
- ![Versão](https://img.shields.io/badge/version-0.1.5-orange)
4
+ ![Versão](https://img.shields.io/badge/version-0.1.6-orange)
5
5
  ![Licença](https://img.shields.io/badge/license-MIT-orange)
6
6
  [![Sponsor](https://img.shields.io/badge/💲Donate-yellow)](https://apoia.se/paulocesar-dev404)
7
7
  [![Sponsor](https://img.shields.io/badge/Documentation-green)](https://github.com/PauloCesar-dev404/udemy-userAPI/wiki)
@@ -1,4 +1,4 @@
1
- __version__ = '0.1.5'
1
+ __version__ = '0.1.7'
2
2
  __lib_name__ = 'udemy_userAPI' # local name
3
3
  __repo_name__ = 'udemy-userAPI'
4
4
  __autor__ = 'PauloCesar-dev404'
@@ -0,0 +1,501 @@
1
+ import json
2
+ from .exeptions import UdemyUserApiExceptions, UnhandledExceptions
3
+ from .authenticate import UdemyAuth
4
+ import os.path
5
+ from pywidevine.cdm import Cdm
6
+ from pywidevine.device import Device
7
+ from pywidevine.pssh import PSSH
8
+ import requests
9
+ import base64
10
+ import logging
11
+
12
+ AUTH = UdemyAuth()
13
+ COOKIES = AUTH.load_cookies
14
+
15
+ HEADERS_USER = {
16
+ "accept": "*/*",
17
+ "accept-language": "pt-BR,pt;q=0.9,en-US;q=0.8,en;q=0.7",
18
+ "cache-control": "no-cache",
19
+ "Content-Type": "text/plain",
20
+ "pragma": "no-cache",
21
+ "sec-ch-ua": "\"Chromium\";v=\"118\", \"Google Chrome\";v=\"118\", \"Not=A?Brand\";v=\"99\"",
22
+ "sec-ch-ua-mobile": "?0",
23
+ "sec-ch-ua-platform": "\"Windows\"",
24
+ "sec-fetch-dest": "empty",
25
+ "sec-fetch-mode": "cors",
26
+ "sec-fetch-site": "cross-site",
27
+ "Cookie": COOKIES,
28
+ "Referer": "https://www.udemy.com/"}
29
+ HEADERS_octet_stream = {
30
+ 'authority': 'www.udemy.com',
31
+ 'pragma': 'no-cache',
32
+ 'cache-control': 'no-cache',
33
+ 'sec-ch-ua': '" Not;A Brand";v="99", "Google Chrome";v="97", "Chromium";v="97"',
34
+ 'accept': 'application/json, text/plain, */*',
35
+ "Cookie": COOKIES,
36
+ 'dnt': '1',
37
+ 'content-type': 'application/octet-stream',
38
+ 'sec-ch-ua-mobile': '?0',
39
+ 'sec-ch-ua-platform': '"Windows"',
40
+ 'origin': 'https://www.udemy.com',
41
+ 'sec-fetch-site': 'same-origin',
42
+ 'sec-fetch-mode': 'cors',
43
+ 'sec-fetch-dest': 'empty',
44
+ 'accept-language': 'en-US,en;q=0.9',
45
+ }
46
+
47
+ logger = logging.getLogger(__name__)
48
+ logger.setLevel(logging.WARNING)
49
+ # Obtém o diretório do arquivo em execução
50
+ locate = os.path.dirname(__file__)
51
+ # Cria o caminho para o arquivo bin.wvd na subpasta bin
52
+ WVD_FILE_PATH = os.path.join(locate, 'bin.wvd')
53
+ device = Device.load(WVD_FILE_PATH)
54
+ cdm = Cdm.from_device(device)
55
+
56
+
57
+ def read_pssh_from_bytes(bytes):
58
+ pssh_offset = bytes.rfind(b'pssh')
59
+ _start = pssh_offset - 4
60
+ _end = pssh_offset - 4 + bytes[pssh_offset - 1]
61
+ pssh = bytes[_start:_end]
62
+ return pssh
63
+
64
+
65
+ def get_pssh(init_url):
66
+ logger.info(f"INIT URL: {init_url}")
67
+ res = requests.get(init_url, headers=HEADERS_octet_stream)
68
+ if not res.ok:
69
+ logger.exception("Could not download init segment: " + res.text)
70
+ return
71
+ pssh = read_pssh_from_bytes(res.content)
72
+ return base64.b64encode(pssh).decode("utf-8")
73
+
74
+
75
+ def get_highest_resolution(resolutions):
76
+ """
77
+ Retorna a maior resolução em uma lista de resoluções.
78
+
79
+ Args:
80
+ resolutions (list of tuple): Lista de resoluções, onde cada tupla representa (largura, altura).
81
+
82
+ Returns:
83
+ tuple: A maior resolução em termos de largura e altura.
84
+ """
85
+ if not resolutions:
86
+ return None
87
+ return max(resolutions, key=lambda res: (res[0], res[1]))
88
+
89
+
90
+ def organize_streams(streams):
91
+ organized_streams = {
92
+ 'dash': [],
93
+ 'hls': []
94
+ }
95
+
96
+ best_video = None
97
+
98
+ for stream in streams:
99
+ # Verifica e adiciona streams DASH
100
+ if stream['type'] == 'application/dash+xml':
101
+ organized_streams['dash'].append({
102
+ 'src': stream['src'],
103
+ 'label': stream.get('label', 'unknown')
104
+ })
105
+
106
+ # Verifica e adiciona streams HLS (m3u8)
107
+ elif stream['type'] == 'application/x-mpegURL':
108
+ organized_streams['hls'].append({
109
+ 'src': stream['src'],
110
+ 'label': stream.get('label', 'auto')
111
+ })
112
+
113
+ # Verifica streams de vídeo (mp4)
114
+ elif stream['type'] == 'video/mp4':
115
+ # Seleciona o vídeo com a maior resolução (baseado no label)
116
+ if best_video is None or int(stream['label']) > int(best_video['label']):
117
+ best_video = {
118
+ 'src': stream['src'],
119
+ 'label': stream['label']
120
+ }
121
+
122
+ # Adiciona o melhor vídeo encontrado na lista 'hls'
123
+ if best_video:
124
+ organized_streams['hls'].append(best_video)
125
+
126
+ return organized_streams
127
+
128
+
129
+ def extract(pssh, license_token):
130
+ license_url = (f"https://www.udemy.com/api-2.0/media-license-server/validate-auth-token?drm_type=widevine"
131
+ f"&auth_token={license_token}")
132
+ logger.info(f"License URL: {license_url}")
133
+ session_id = cdm.open()
134
+ challenge = cdm.get_license_challenge(session_id, PSSH(pssh))
135
+ logger.info("Sending license request now")
136
+ license = requests.post(license_url, headers=HEADERS_octet_stream, data=challenge)
137
+ try:
138
+ str(license.content, "utf-8")
139
+ except:
140
+ base64_license = base64.b64encode(license.content).decode()
141
+ logger.info("[+] Acquired license sucessfully!")
142
+ else:
143
+ if "CAIS" not in license.text:
144
+ logger.exception("[-] Couldn't to get license: [{}]\n{}".format(license.status_code, license.text))
145
+ return
146
+
147
+ logger.info("Trying to get keys now")
148
+ cdm.parse_license(session_id, license.content)
149
+ final_keys = ""
150
+ for key in cdm.get_keys(session_id):
151
+ logger.info(f"[+] Keys: [{key.type}] - {key.kid.hex}:{key.key.hex()}")
152
+ if key.type == "CONTENT":
153
+ final_keys += f"{key.kid.hex}:{key.key.hex()}"
154
+ cdm.close(session_id)
155
+
156
+ if final_keys == "":
157
+ logger.exception("Keys were not extracted sucessfully.")
158
+ return
159
+ return final_keys.strip()
160
+
161
+
162
+ def get_mpd_file(mpd_url):
163
+ try:
164
+ # Faz a solicitação GET com os cabeçalhos
165
+ response = requests.get(mpd_url, headers=HEADERS_USER)
166
+ data = []
167
+ # Exibe o código de status
168
+ if response.status_code == 200:
169
+ return response.content
170
+ else:
171
+ UnhandledExceptions(f"erro ao obter dados de aulas!! {response.status_code}")
172
+ except requests.ConnectionError as e:
173
+ UdemyUserApiExceptions(f"Erro de conexão: {e}")
174
+ except requests.Timeout as e:
175
+ UdemyUserApiExceptions(f"Tempo de requisição excedido: {e}")
176
+ except requests.TooManyRedirects as e:
177
+ UdemyUserApiExceptions(f"Limite de redirecionamentos excedido: {e}")
178
+ except requests.HTTPError as e:
179
+ UdemyUserApiExceptions(f"Erro HTTP: {e}")
180
+ except Exception as e:
181
+ UnhandledExceptions(f"Errro Ao Obter Mídias:{e}")
182
+
183
+
184
+ def parser_chapers(results):
185
+ """
186
+ :param results:
187
+ :param tip: chaper,videos
188
+ :return:
189
+ """
190
+ if not results:
191
+ raise UdemyUserApiExceptions("Não foi possível obter detalhes do curso!")
192
+ results = results.get('results', None)
193
+ if not results:
194
+ raise UdemyUserApiExceptions("Não foi possível obter detalhes do curso!")
195
+ chapters_dict = {} # Dicionário para armazenar os capítulos e seus vídeos correspondentes
196
+
197
+ # Primeiro, construímos um dicionário de capítulos
198
+ current_chapter = None
199
+ for dictionary in results:
200
+ _class = dictionary.get('_class')
201
+
202
+ if _class == 'chapter':
203
+ chapter_index = dictionary.get('object_index')
204
+ current_chapter = {
205
+ 'title_chapter': dictionary.get('title'),
206
+ 'videos_in_chapter': []
207
+ }
208
+ chapters_dict[f"chapter_{chapter_index}"] = current_chapter
209
+ elif _class == 'lecture' and current_chapter is not None:
210
+ asset = dictionary.get('asset')
211
+ if asset:
212
+ video_title = dictionary.get('title', None)
213
+ if not video_title:
214
+ video_title = 'Files'
215
+ current_chapter['videos_in_chapter'].append({
216
+ 'video_title': video_title,
217
+ 'title_lecture': dictionary.get('title'),
218
+ 'lecture_id': dictionary.get('id'),
219
+ 'asset_id': asset.get('id')
220
+ })
221
+ return chapters_dict
222
+
223
+
224
+ def get_add_files(course_id: int):
225
+ url = (f'https://www.udemy.com/api-2.0/courses/{course_id}/subscriber-curriculum-items/?page_size=2000&fields['
226
+ f'lecture]=title,object_index,is_published,sort_order,created,asset,supplementary_assets,is_free&fields['
227
+ f'quiz]=title,object_index,is_published,sort_order,type&fields[practice]=title,object_index,is_published,'
228
+ f'sort_order&fields[chapter]=title,object_index,is_published,sort_order&fields[asset]=title,filename,'
229
+ f'asset_type,status,time_estimation,is_external&caching_intent=True')
230
+ try:
231
+ # Faz a solicitação GET com os cabeçalhos
232
+ response = requests.get(url, headers=HEADERS_USER)
233
+ data = []
234
+ # Exibe o código de status
235
+ if response.status_code == 200:
236
+ a = json.loads(response.text)
237
+ return a
238
+ else:
239
+ UnhandledExceptions(f"erro ao obter dados de aulas!! {response.status_code}")
240
+
241
+ except requests.ConnectionError as e:
242
+ UdemyUserApiExceptions(f"Erro de conexão: {e}")
243
+ except requests.Timeout as e:
244
+ UdemyUserApiExceptions(f"Tempo de requisição excedido: {e}")
245
+ except requests.TooManyRedirects as e:
246
+ UdemyUserApiExceptions(f"Limite de redirecionamentos excedido: {e}")
247
+ except requests.HTTPError as e:
248
+ UdemyUserApiExceptions(f"Erro HTTP: {e}")
249
+ except Exception as e:
250
+ UnhandledExceptions(f"Errro Ao Obter Mídias:{e}")
251
+
252
+
253
+ def get_files_aule(lecture_id_filter, data: list):
254
+ files = []
255
+ # print(f'DEBUG:\n\n{data}')
256
+ for files_data in data:
257
+ lecture_id = files_data.get('lecture_id')
258
+ if lecture_id == lecture_id_filter:
259
+ files.append(files_data)
260
+ return files
261
+
262
+
263
+ def get_links(course_id: int, id_lecture: int):
264
+ """
265
+ :param course_id: id do curso
266
+ :param id_lecture: id da aula
267
+ :return: dict
268
+ """
269
+ get = (f"https://www.udemy.com/api-2.0/users/me/subscribed-courses/{course_id}/lectures/{id_lecture}/?"
270
+ f"fields[lecture]"
271
+ f"=asset,description,download_url,is_free,last_watched_second&fields[asset]=asset_type,length,"
272
+ f"media_license_token,course_is_drmed,media_sources,captions,thumbnail_sprite,slides,slide_urls,"
273
+ f"download_urls,"
274
+ f"external_url&q=0.3108014137011559/?fields[asset]=download_urls")
275
+ try:
276
+ # Faz a solicitação GET com os cabeçalhos
277
+ response = requests.get(get, headers=HEADERS_USER)
278
+ data = []
279
+ # Exibe o código de status
280
+ if response.status_code == 200:
281
+ a = json.loads(response.text)
282
+ return a
283
+ else:
284
+ UnhandledExceptions(f"erro ao obter dados de aulas!! {response.status_code}")
285
+
286
+ except requests.ConnectionError as e:
287
+ UdemyUserApiExceptions(f"Erro de conexão: {e}")
288
+ except requests.Timeout as e:
289
+ UdemyUserApiExceptions(f"Tempo de requisição excedido: {e}")
290
+ except requests.TooManyRedirects as e:
291
+ UdemyUserApiExceptions(f"Limite de redirecionamentos excedido: {e}")
292
+ except requests.HTTPError as e:
293
+ UdemyUserApiExceptions(f"Erro HTTP: {e}")
294
+ except Exception as e:
295
+ UnhandledExceptions(f"Errro Ao Obter Mídias:{e}")
296
+
297
+
298
+ def remove_tag(d: str):
299
+ new = d.replace("<p>", '').replace("</p>", '').replace('&nbsp;', ' ')
300
+ return new
301
+
302
+
303
+ def get_external_liks(course_id: int, id_lecture, asset_id):
304
+ url = (f'https://www.udemy.com/api-2.0/users/me/subscribed-courses/{course_id}/lectures/{id_lecture}/'
305
+ f'supplementary-assets/{asset_id}/?fields[asset]=external_url')
306
+ try:
307
+ # Faz a solicitação GET com os cabeçalhos
308
+ response = requests.get(url, headers=HEADERS_USER)
309
+ data = []
310
+ # Exibe o código de status
311
+ if response.status_code == 200:
312
+ a = json.loads(response.text)
313
+ return a
314
+ else:
315
+ UnhandledExceptions(f"erro ao obter dados de aulas!! {response.status_code}")
316
+
317
+ except requests.ConnectionError as e:
318
+ UdemyUserApiExceptions(f"Erro de conexão: {e}")
319
+ except requests.Timeout as e:
320
+ UdemyUserApiExceptions(f"Tempo de requisição excedido: {e}")
321
+ except requests.TooManyRedirects as e:
322
+ UdemyUserApiExceptions(f"Limite de redirecionamentos excedido: {e}")
323
+ except requests.HTTPError as e:
324
+ UdemyUserApiExceptions(f"Erro HTTP: {e}")
325
+ except Exception as e:
326
+ UnhandledExceptions(f"Errro Ao Obter Mídias:{e}")
327
+
328
+
329
+ def extract_files(supplementary_assets: list) -> list:
330
+ """Obtém o ID da lecture, o ID do asset, o asset_type e o filename."""
331
+ files = []
332
+ for item in supplementary_assets:
333
+ # print(f'DEBUG files:\n{item}\n\n')
334
+ lecture_title = item.get('lecture_title')
335
+ lecture_id = item.get('lecture_id')
336
+ asset = item.get('asset', {})
337
+ asset_id = asset.get('id')
338
+ asset_type = asset.get('asset_type')
339
+ filename = asset.get('filename')
340
+ title = asset.get('title')
341
+ external_url = asset.get('is_external', None)
342
+ files.append({
343
+ 'lecture_id': lecture_id,
344
+ 'asset_id': asset_id,
345
+ 'asset_type': asset_type,
346
+ 'filename': filename,
347
+ 'title': title,
348
+ 'lecture_title': lecture_title,
349
+ 'ExternalLink': external_url
350
+ })
351
+ return files
352
+
353
+
354
+ def extract_course_data(course_dict) -> dict:
355
+ # Extrair informações principais
356
+ course_id = course_dict.get('id')
357
+ title = course_dict.get('title')
358
+ num_subscribers = course_dict.get('num_subscribers')
359
+ avg_rating_recent = course_dict.get('avg_rating_recent')
360
+ estimated_content_length = course_dict.get('estimated_content_length')
361
+
362
+ # Extrair informações dos instrutores
363
+ instructors = course_dict.get('visible_instructors', [])
364
+ instructor_data = []
365
+ for instructor in instructors:
366
+ instructor_data.append({
367
+ 'id': instructor.get('id'),
368
+ 'title': instructor.get('title'),
369
+ 'name': instructor.get('name'),
370
+ 'display_name': instructor.get('display_name'),
371
+ 'job_title': instructor.get('job_title'),
372
+ 'image_50x50': instructor.get('image_50x50'),
373
+ 'image_100x100': instructor.get('image_100x100'),
374
+ 'initials': instructor.get('initials'),
375
+ 'url': instructor.get('url'),
376
+ })
377
+
378
+ # Extrair informações de localização
379
+ locale = course_dict.get('locale', {})
380
+ locale_data = {
381
+ 'locale': locale.get('locale'),
382
+ 'title': locale.get('title'),
383
+ 'english_title': locale.get('english_title'),
384
+ 'simple_english_title': locale.get('simple_english_title'),
385
+ }
386
+
387
+ # Extrair informações de categorias e subcategorias
388
+ primary_category = course_dict.get('primary_category', {})
389
+ primary_category_data = {
390
+ 'id': primary_category.get('id'),
391
+ 'title': primary_category.get('title'),
392
+ 'title_cleaned': primary_category.get('title_cleaned'),
393
+ 'url': primary_category.get('url'),
394
+ 'icon_class': primary_category.get('icon_class'),
395
+ 'type': primary_category.get('type'),
396
+ }
397
+
398
+ primary_subcategory = course_dict.get('primary_subcategory', {})
399
+ primary_subcategory_data = {
400
+ 'id': primary_subcategory.get('id'),
401
+ 'title': primary_subcategory.get('title'),
402
+ 'title_cleaned': primary_subcategory.get('title_cleaned'),
403
+ 'url': primary_subcategory.get('url'),
404
+ 'icon_class': primary_subcategory.get('icon_class'),
405
+ 'type': primary_subcategory.get('type'),
406
+ }
407
+
408
+ # Extrair informações contextuais
409
+ context_info = course_dict.get('context_info', {})
410
+ category_info = context_info.get('category', {})
411
+ label_info = context_info.get('label', {})
412
+
413
+ category_data = {
414
+ 'id': category_info.get('id'),
415
+ 'title': category_info.get('title'),
416
+ 'url': category_info.get('url'),
417
+ 'tracking_object_type': category_info.get('tracking_object_type'),
418
+ }
419
+
420
+ label_data = {
421
+ 'id': label_info.get('id'),
422
+ 'display_name': label_info.get('display_name'),
423
+ 'title': label_info.get('title'),
424
+ 'topic_channel_url': label_info.get('topic_channel_url'),
425
+ 'url': label_info.get('url'),
426
+ 'tracking_object_type': label_info.get('tracking_object_type'),
427
+ }
428
+
429
+ # Compilar todos os dados em um dicionário
430
+ result = {
431
+ 'course_id': course_id,
432
+ 'title': title,
433
+ 'num_subscribers': num_subscribers,
434
+ 'avg_rating_recent': avg_rating_recent,
435
+ 'estimated_content_length': estimated_content_length,
436
+ 'instructors': instructor_data,
437
+ 'locale': locale_data,
438
+ 'primary_category': primary_category_data,
439
+ 'primary_subcategory': primary_subcategory_data,
440
+ 'category_info': category_data,
441
+ 'label_info': label_data,
442
+ }
443
+
444
+ return result
445
+
446
+
447
+ def format_size(byte_size):
448
+ # Constantes para conversão
449
+ KB = 1024
450
+ MB = KB ** 2
451
+ GB = KB ** 3
452
+ TB = KB ** 4
453
+ try:
454
+ byte_size = int(byte_size)
455
+
456
+ if byte_size < KB:
457
+ return f"{byte_size} bytes"
458
+ elif byte_size < MB:
459
+ return f"{byte_size / KB:.2f} KB"
460
+ elif byte_size < GB:
461
+ return f"{byte_size / MB:.2f} MB"
462
+ elif byte_size < TB:
463
+ return f"{byte_size / GB:.2f} GB"
464
+ else:
465
+ return f"{byte_size / TB:.2f} TB"
466
+ except Exception as e:
467
+ return byte_size
468
+
469
+
470
+ def lecture_infor(course_id: int, id_lecture: int):
471
+ edpoint = (f"https://www.udemy.com/api-2.0/users/me/subscribed-courses/{course_id}/lectures/{id_lecture}/?"
472
+ f"fields[asset]=media_license_token&q=0.06925737374647678")
473
+ r = requests.get(edpoint, headers=HEADERS_USER)
474
+ if r.status_code == 200:
475
+ return json.loads(r.text)
476
+
477
+
478
+ def assets_infor(course_id: int, id_lecture: int, assets_id: int):
479
+ endpoint = (f'https://www.udemy.com/api-2.0/assets/{assets_id}/?fields[asset]=@min,status,delayed_asset_message,'
480
+ f'processing_errors,body&course_id={course_id}&lecture_id={id_lecture}')
481
+ r = requests.get(endpoint, headers=HEADERS_USER)
482
+ if r.status_code == 200:
483
+ dt = json.loads(r.text)
484
+ body = dt.get("body")
485
+ title = lecture_infor(course_id=course_id, id_lecture=id_lecture).get("title")
486
+ return save_html(body, title_lecture=title)
487
+
488
+
489
+ def save_html(body, title_lecture):
490
+ html_content = f"""<!DOCTYPE html>
491
+ <html lang="en">
492
+ <head>
493
+ <meta charset="UTF-8">
494
+ <title>{title_lecture}</title>
495
+ </head>
496
+ <body>
497
+ {body}
498
+ </body>
499
+ </html>"""
500
+
501
+ return html_content