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.
- animation_consoles/__init__.py +1 -0
- animation_consoles/animation.py +64 -0
- ffmpeg_for_python/__config__.py +118 -0
- ffmpeg_for_python/__init__.py +8 -0
- ffmpeg_for_python/__utils.py +78 -0
- ffmpeg_for_python/__version__.py +6 -0
- ffmpeg_for_python/exeptions.py +91 -0
- ffmpeg_for_python/ffmpeg.py +203 -0
- m3u8_analyzer/M3u8Analyzer.py +807 -0
- m3u8_analyzer/__init__.py +7 -0
- m3u8_analyzer/__version__.py +1 -0
- m3u8_analyzer/exeptions.py +82 -0
- udemy_userAPI/.cache/.udemy_userAPI +0 -0
- udemy_userAPI/__init__.py +7 -0
- udemy_userAPI/__version__.py +6 -0
- udemy_userAPI/api.py +691 -0
- udemy_userAPI/authenticate.py +311 -0
- udemy_userAPI/bultins.py +495 -0
- udemy_userAPI/exeptions.py +22 -0
- udemy_userAPI/mpd_analyzer/__init__.py +3 -0
- udemy_userAPI/mpd_analyzer/bin.wvd +0 -0
- udemy_userAPI/mpd_analyzer/mpd_parser.py +224 -0
- udemy_userAPI/sections.py +117 -0
- udemy_userAPI/udemy.py +93 -0
- udemy_userAPI-0.3.2.dist-info/LICENSE +21 -0
- udemy_userAPI-0.3.2.dist-info/METADATA +34 -0
- udemy_userAPI-0.3.2.dist-info/RECORD +29 -0
- udemy_userAPI-0.3.2.dist-info/WHEEL +5 -0
- udemy_userAPI-0.3.2.dist-info/top_level.txt +1 -0
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(' ', ' ')
|
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)
|