udemy-userAPI 0.2.5__py3-none-any.whl → 0.2.7__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/__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
|