udemy-userAPI 0.2.5__py3-none-any.whl → 0.2.7__py3-none-any.whl
Sign up to get free protection for your applications and to get access to all the features.
- udemy_userAPI/__version__.py +1 -1
- udemy_userAPI/authenticate.py +134 -6
- udemy_userAPI/bultins.py +20 -12
- udemy_userAPI/udemy.py +7 -6
- {udemy_userAPI-0.2.5.dist-info → udemy_userAPI-0.2.7.dist-info}/METADATA +2 -2
- {udemy_userAPI-0.2.5.dist-info → udemy_userAPI-0.2.7.dist-info}/RECORD +9 -9
- udemy_userAPI-0.2.7.dist-info/top_level.txt +1 -0
- udemy_userAPI-0.2.5.dist-info/top_level.txt +0 -4
- {udemy_userAPI-0.2.5.dist-info → udemy_userAPI-0.2.7.dist-info}/LICENSE +0 -0
- {udemy_userAPI-0.2.5.dist-info → udemy_userAPI-0.2.7.dist-info}/WHEEL +0 -0
udemy_userAPI/__version__.py
CHANGED
udemy_userAPI/authenticate.py
CHANGED
@@ -2,7 +2,10 @@ import json
|
|
2
2
|
import os
|
3
3
|
import pickle
|
4
4
|
import traceback
|
5
|
-
|
5
|
+
import hashlib
|
6
|
+
import hmac
|
7
|
+
import math
|
8
|
+
from datetime import datetime
|
6
9
|
import requests
|
7
10
|
from .exeptions import UnhandledExceptions, UdemyUserApiExceptions, LoginException
|
8
11
|
import cloudscraper
|
@@ -142,7 +145,7 @@ class UdemyAuth:
|
|
142
145
|
|
143
146
|
# Verifica a resposta para determinar se o login foi bem-sucedido
|
144
147
|
if "returnUrl" in r.text:
|
145
|
-
self.
|
148
|
+
self._save_cookies(s.cookies)
|
146
149
|
else:
|
147
150
|
login_error = r.json().get("error", {}).get("data", {}).get("formErrors", [])[0]
|
148
151
|
if login_error[0] == "Y":
|
@@ -151,14 +154,12 @@ class UdemyAuth:
|
|
151
154
|
raise LoginException("Email ou senha incorretos")
|
152
155
|
else:
|
153
156
|
raise UnhandledExceptions(login_error)
|
154
|
-
|
155
|
-
return s
|
156
157
|
except Exception as e:
|
157
158
|
if DEBUG:
|
158
159
|
e = traceback.format_exc()
|
159
160
|
raise LoginException(e)
|
160
161
|
|
161
|
-
def
|
162
|
+
def _save_cookies(self, cookies):
|
162
163
|
try:
|
163
164
|
with open(fr'{self.__file_path}', 'wb') as f:
|
164
165
|
pickle.dump(cookies, f)
|
@@ -188,4 +189,131 @@ class UdemyAuth:
|
|
188
189
|
|
189
190
|
def remove_cookies(self):
|
190
191
|
if os.path.exists(self.__file_path):
|
191
|
-
|
192
|
+
with open(self.__file_path, 'wb') as f:
|
193
|
+
f.write(b'')
|
194
|
+
|
195
|
+
def login_passwordless(self, email: str, locale: str = 'pt-BR'):
|
196
|
+
"""
|
197
|
+
Realiza login na Udemy usando autenticação de dois fatores (2FA).
|
198
|
+
|
199
|
+
Este método utiliza o fluxo de autenticação OAuth da Udemy para enviar um
|
200
|
+
código de verificação por e-mail ao usuário. Após inserir o código recebido,
|
201
|
+
o login é concluído.
|
202
|
+
|
203
|
+
:param email: Email do usuário.
|
204
|
+
:param locale: Localização do usuário (recomendado para receber mensagens no idioma local).
|
205
|
+
:raises LoginException: Em caso de falha no processo de login.
|
206
|
+
"""
|
207
|
+
try:
|
208
|
+
# Inicializa uma sessão com proteção contra Cloudflare
|
209
|
+
session = cloudscraper.create_scraper()
|
210
|
+
|
211
|
+
# Requisita a página de inscrição para obter o token CSRF
|
212
|
+
signup_url = "https://www.udemy.com/join/signup-popup/"
|
213
|
+
headers = {"User-Agent": "okhttp/4.9.2 UdemyAndroid 8.9.2(499) (phone)"}
|
214
|
+
response = session.get(signup_url, headers=headers)
|
215
|
+
|
216
|
+
# Obtém o token CSRF dos cookies retornados
|
217
|
+
csrf_token = response.cookies.get("csrftoken")
|
218
|
+
if not csrf_token:
|
219
|
+
raise LoginException("Não foi possível obter o token CSRF.")
|
220
|
+
|
221
|
+
# Prepara os dados do login
|
222
|
+
data = {"email": email, "fullname": ""}
|
223
|
+
|
224
|
+
# Atualiza os cookies e cabeçalhos da sessão
|
225
|
+
session.cookies.update(response.cookies)
|
226
|
+
session.headers.update({
|
227
|
+
"User-Agent": "okhttp/4.9.2 UdemyAndroid 8.9.2(499) (phone)",
|
228
|
+
"Accept": "application/json, text/plain, */*",
|
229
|
+
"Accept-Language": locale,
|
230
|
+
"Referer": f"https://www.udemy.com/join/passwordless-auth/?locale={locale.replace('-', '_')}&next="
|
231
|
+
f"https%3A%2F%2Fwww.udemy.com%2Fmobile%2Fipad%2F&response_type=html",
|
232
|
+
"Origin": "https://www.udemy.com",
|
233
|
+
"DNT": "1",
|
234
|
+
"Connection": "keep-alive",
|
235
|
+
"Sec-Fetch-Dest": "empty",
|
236
|
+
"Sec-Fetch-Mode": "cors",
|
237
|
+
"Sec-Fetch-Site": "same-origin",
|
238
|
+
"Pragma": "no-cache",
|
239
|
+
"Cache-Control": "no-cache",
|
240
|
+
})
|
241
|
+
|
242
|
+
# Faz a requisição para iniciar o login
|
243
|
+
login_url = "https://www.udemy.com/api-2.0/auth/code-generation/login/4.0/"
|
244
|
+
response = session.post(login_url, data=data, allow_redirects=False)
|
245
|
+
if 'error_message' in response.text:
|
246
|
+
erro_data: dict = response.json()
|
247
|
+
error_message = erro_data.get('error_message', {})
|
248
|
+
raise LoginException(error_message)
|
249
|
+
for attempt in range(3):
|
250
|
+
# Solicita o código OTP ao usuário
|
251
|
+
otp = input("Digite o código de 6 dígitos enviado ao seu e-mail: ")
|
252
|
+
# Realiza o login com o código OTP
|
253
|
+
otp_login_url = "https://www.udemy.com/api-2.0/auth/udemy-passwordless/login/4.0/"
|
254
|
+
otp_data = {
|
255
|
+
"email": email,
|
256
|
+
"fullname": "",
|
257
|
+
"otp": otp,
|
258
|
+
"subscribeToEmails": "false",
|
259
|
+
"upow": J(email, 'login')
|
260
|
+
}
|
261
|
+
session.headers.update({
|
262
|
+
"Referer": f"https://www.udemy.com/join/passwordless-auth/?locale={locale}&next="
|
263
|
+
f"https%3A%2F%2Fwww.udemy.com%2Fmobile%2Fipad%2F&response_type=html"
|
264
|
+
})
|
265
|
+
response = session.post(otp_login_url, otp_data, allow_redirects=False)
|
266
|
+
# Verifica se o login foi bem-sucedido
|
267
|
+
if response.status_code == 200:
|
268
|
+
self._save_cookies(session.cookies)
|
269
|
+
else:
|
270
|
+
if 'error_message' in response.text:
|
271
|
+
erro_data: dict = response.json()
|
272
|
+
error_message = erro_data.get('error_message', {})
|
273
|
+
error_code = erro_data.get('error_code', {})
|
274
|
+
if error_code == '1538':
|
275
|
+
raise LoginException(error_message)
|
276
|
+
elif error_code == '2550':
|
277
|
+
print(error_message)
|
278
|
+
continue
|
279
|
+
elif error_code == '1330':
|
280
|
+
raise LoginException(error_message)
|
281
|
+
elif error_code == '1149':
|
282
|
+
LoginException(f"Erro interno ao enviar os dados veja os detalhes: '{error_message}'")
|
283
|
+
raise LoginException(response.text)
|
284
|
+
break
|
285
|
+
except Exception as e:
|
286
|
+
if DEBUG:
|
287
|
+
error_details = traceback.format_exc()
|
288
|
+
else:
|
289
|
+
error_details = str(e)
|
290
|
+
raise LoginException(error_details)
|
291
|
+
|
292
|
+
|
293
|
+
def J(e, t):
|
294
|
+
r = datetime.now()
|
295
|
+
s = r.isoformat()[:10]
|
296
|
+
return s + X(e, s, t)
|
297
|
+
|
298
|
+
|
299
|
+
def X(e, t, r):
|
300
|
+
s = 0
|
301
|
+
while True:
|
302
|
+
o = ee(s)
|
303
|
+
a = hmac.new(r.encode(), (e + t + o).encode(), hashlib.sha256).digest()
|
304
|
+
if te(16, a):
|
305
|
+
return o
|
306
|
+
s += 1
|
307
|
+
|
308
|
+
|
309
|
+
def ee(e):
|
310
|
+
if e < 0:
|
311
|
+
return ""
|
312
|
+
return ee(e // 26 - 1) + chr(65 + e % 26)
|
313
|
+
|
314
|
+
|
315
|
+
def te(e, t):
|
316
|
+
r = math.ceil(e / 8)
|
317
|
+
s = t[:r]
|
318
|
+
o = ''.join(format(byte, '08b') for byte in s)
|
319
|
+
return o.startswith('0' * e)
|
udemy_userAPI/bultins.py
CHANGED
@@ -10,9 +10,10 @@ from .mpd_analyzer import MPDParser
|
|
10
10
|
class DRM:
|
11
11
|
def __init__(self, license_token: str, get_media_sources: list):
|
12
12
|
self.__mpd_content = None
|
13
|
-
self.__dash_url = organize_streams(streams=get_media_sources).get('dash')
|
14
13
|
self.__token = license_token
|
15
|
-
self.
|
14
|
+
self.__dash_url = organize_streams(streams=get_media_sources).get('dash', {})
|
15
|
+
if not license_token or not get_media_sources or not self.__dash_url:
|
16
|
+
return
|
16
17
|
|
17
18
|
def get_key_for_lesson(self):
|
18
19
|
"""get keys for lesson"""
|
@@ -28,6 +29,12 @@ class DRM:
|
|
28
29
|
keys = extract(pssh=pssh, license_token=self.__token)
|
29
30
|
if keys:
|
30
31
|
return keys
|
32
|
+
else:
|
33
|
+
return None
|
34
|
+
else:
|
35
|
+
return None
|
36
|
+
else:
|
37
|
+
return None
|
31
38
|
|
32
39
|
|
33
40
|
class Files:
|
@@ -44,11 +51,13 @@ class Files:
|
|
44
51
|
lecture_id = files.get('lecture_id', None)
|
45
52
|
asset_id = files.get('asset_id', None)
|
46
53
|
title = files.get("title", None)
|
47
|
-
lecture_title = files.get('lecture_title')
|
48
|
-
external_link = files.get('ExternalLink')
|
54
|
+
lecture_title = files.get('lecture_title', None)
|
55
|
+
external_link = files.get('ExternalLink', None)
|
49
56
|
if external_link:
|
50
57
|
lnk = get_external_liks(course_id=self.__id_course, id_lecture=lecture_id, asset_id=asset_id)
|
51
|
-
dt_file = {'title-file': title,
|
58
|
+
dt_file = {'title-file': title,
|
59
|
+
'lecture_title': lecture_title,
|
60
|
+
'lecture_id': lecture_id,
|
52
61
|
'external_link': external_link,
|
53
62
|
'data-file': lnk.get('external_url', None)}
|
54
63
|
return dt_file
|
@@ -59,8 +68,9 @@ class Files:
|
|
59
68
|
headers=HEADERS_USER)
|
60
69
|
if resp.status_code == 200:
|
61
70
|
da = json.loads(resp.text)
|
62
|
-
|
63
|
-
|
71
|
+
dt_file = {'title-file': title,
|
72
|
+
'lecture_title': lecture_title,
|
73
|
+
'lecture_id': lecture_id,
|
64
74
|
'external_link': external_link,
|
65
75
|
'data-file': da['download_urls']}
|
66
76
|
download_urls.append(dt_file)
|
@@ -128,12 +138,10 @@ class Lecture:
|
|
128
138
|
def course_is_drmed(self) -> DRM:
|
129
139
|
"""verifica se a aula possui DRM se sim retorna as keys da aula...
|
130
140
|
retorna 'kid:key' or None"""
|
131
|
-
if self.__asset.get('course_is_drmed'):
|
141
|
+
if self.__asset.get('course_is_drmed', {}):
|
132
142
|
d = DRM(license_token=self.get_media_license_token,
|
133
143
|
get_media_sources=self.get_media_sources)
|
134
144
|
return d
|
135
|
-
else:
|
136
|
-
return self.__asset.get('course_is_drmed')
|
137
145
|
|
138
146
|
@property
|
139
147
|
def get_download_urls(self) -> list:
|
@@ -267,8 +275,8 @@ class Course:
|
|
267
275
|
for item in self.__additional_files_data.get('results', []):
|
268
276
|
# Check if the item is a lecture with supplementary assets
|
269
277
|
if item.get('_class') == 'lecture':
|
270
|
-
id = item.get('id')
|
271
|
-
title = item.get('title')
|
278
|
+
id = item.get('id', {})
|
279
|
+
title = item.get('title', {})
|
272
280
|
assets = item.get('supplementary_assets', [])
|
273
281
|
for asset in assets:
|
274
282
|
supplementary_assets.append({
|
udemy_userAPI/udemy.py
CHANGED
@@ -12,22 +12,23 @@ class Udemy:
|
|
12
12
|
"""wrapper para api de usuario da plataforma udemy"""
|
13
13
|
|
14
14
|
def __init__(self):
|
15
|
-
"""
|
16
|
-
cookies de seção.
|
17
|
-
"""
|
18
15
|
self.__headers = HEADERS_USER
|
19
16
|
if verif_login is None:
|
20
17
|
raise LoginException("User Not Logged!")
|
21
18
|
|
22
|
-
|
23
|
-
|
19
|
+
@staticmethod
|
20
|
+
def my_subscribed_courses_by_plan() -> list[dict]:
|
21
|
+
"""obtém os cursos que o usuário esatá inscrito, obtidos atraves de planos(assinatura)
|
22
|
+
:return:
|
23
|
+
"""
|
24
24
|
try:
|
25
25
|
courses = get_courses_plan(tipe='plan')
|
26
26
|
return courses
|
27
27
|
except UdemyUserApiExceptions as e:
|
28
28
|
UnhandledExceptions(e)
|
29
29
|
|
30
|
-
|
30
|
+
@staticmethod
|
31
|
+
def my_subscribed_courses() -> list[dict]:
|
31
32
|
"""Obtém os cursos que o usuário está inscrito, excluindo listas vazias ou nulas"""
|
32
33
|
try:
|
33
34
|
# Obtém os cursos
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.1
|
2
2
|
Name: udemy_userAPI
|
3
|
-
Version: 0.2.
|
3
|
+
Version: 0.2.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
|
@@ -19,7 +19,7 @@ Requires-Dist: pywidevine
|
|
19
19
|
# udemy-userAPI
|
20
20
|
|
21
21
|
|
22
|
-

|
23
23
|

|
24
24
|
[](https://apoia.se/paulocesar-dev404)
|
25
25
|
[](https://github.com/PauloCesar-dev404/udemy-userAPI/wiki)
|
@@ -11,19 +11,19 @@ m3u8_analyzer/__init__.py,sha256=v7CiVqsCq2YH347C-QR1kHPJtXFFdru8qole3E9adCY,217
|
|
11
11
|
m3u8_analyzer/__version__.py,sha256=YP3yT87ZKrU3eARUUdQ_pg4xAXLGfBXjH4ZgEoZSq1I,25
|
12
12
|
m3u8_analyzer/exeptions.py,sha256=fK6bU3YxNSbfsPmCp4yudUvmwy_g6dj2KwIkH0dW4LI,3672
|
13
13
|
udemy_userAPI/__init__.py,sha256=BPle89xE_CMTKKe_Lw6jioYLgpH-q_Lpho2S-n1PIUA,206
|
14
|
-
udemy_userAPI/__version__.py,sha256=
|
14
|
+
udemy_userAPI/__version__.py,sha256=FlHdJYlkLREupFjW2L1uKD5VxsxBYWG5W4dW5ylbmr8,405
|
15
15
|
udemy_userAPI/api.py,sha256=dpwFtXewQmKwgG1IvzDFYZoEHNTwZbLIuv4WKgbqjOg,18817
|
16
|
-
udemy_userAPI/authenticate.py,sha256=
|
17
|
-
udemy_userAPI/bultins.py,sha256=
|
16
|
+
udemy_userAPI/authenticate.py,sha256=84frcOMfOzfCBfXDtoTa3POqkwWwuqgJ6h4ROF0TVAM,13850
|
17
|
+
udemy_userAPI/bultins.py,sha256=_-CM8Y-EuOEyg3kbNI2LKUONdCn2d1El1AmoNqFo0EU,12426
|
18
18
|
udemy_userAPI/exeptions.py,sha256=nuZoAt4i-ctrW8zx9LZtejrngpFXDHOVE5cEXM4RtrY,508
|
19
19
|
udemy_userAPI/sections.py,sha256=zPyDhvTIQCL0nbf7OJZG28Kax_iooILQ_hywUwvHoL8,4043
|
20
|
-
udemy_userAPI/udemy.py,sha256=
|
20
|
+
udemy_userAPI/udemy.py,sha256=KMWMmid0zC9pUCULjLSAOK0P7yvCOtdShXpT6Q-fhro,2127
|
21
21
|
udemy_userAPI/.cache/.udemy_userAPI,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
22
22
|
udemy_userAPI/mpd_analyzer/__init__.py,sha256=i3JVWyvcFLaj5kPmx8c1PgjsLht7OUIQQClD4yqYbo8,102
|
23
23
|
udemy_userAPI/mpd_analyzer/bin.wvd,sha256=1rAJdCc120hQlX9qe5KUS628eY2ZHYxQSmyhGNefSzo,2956
|
24
24
|
udemy_userAPI/mpd_analyzer/mpd_parser.py,sha256=_vw1feJXDjw5fQLOmA5-H3UklX_30Pbl__HtDUqvp3c,17283
|
25
|
-
udemy_userAPI-0.2.
|
26
|
-
udemy_userAPI-0.2.
|
27
|
-
udemy_userAPI-0.2.
|
28
|
-
udemy_userAPI-0.2.
|
29
|
-
udemy_userAPI-0.2.
|
25
|
+
udemy_userAPI-0.2.7.dist-info/LICENSE,sha256=l4jdKYt8gSdDFOGr09vCKnMn_Im55XIcQKqTDEtFfNs,1095
|
26
|
+
udemy_userAPI-0.2.7.dist-info/METADATA,sha256=V833TKA7jMmFMj5aeLGOeSt6GSmpgeUzl2dS0Q33NJw,1394
|
27
|
+
udemy_userAPI-0.2.7.dist-info/WHEEL,sha256=PZUExdf71Ui_so67QXpySuHtCi3-J3wvF4ORK6k_S8U,91
|
28
|
+
udemy_userAPI-0.2.7.dist-info/top_level.txt,sha256=ijTINaSDRKhdahY_X7dmSRFTxBIwQErWv9ATCG55mog,14
|
29
|
+
udemy_userAPI-0.2.7.dist-info/RECORD,,
|
@@ -0,0 +1 @@
|
|
1
|
+
udemy_userAPI
|
File without changes
|
File without changes
|