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.
@@ -1,4 +1,4 @@
1
- __version__ = '0.2.5'
1
+ __version__ = '0.2.7'
2
2
  __lib_name__ = 'udemy_userAPI' # local name
3
3
  __repo_name__ = 'udemy-userAPI'
4
4
  __autor__ = 'PauloCesar-dev404'
@@ -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.__save_cookies(s.cookies)
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 __save_cookies(self, cookies):
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
- os.remove(self.__file_path)
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.get_key_for_lesson()
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, 'lecture_title': lecture_title, 'lecture_id': lecture_id,
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
- ## para cdaa dict de um fle colocar seu titulo:
63
- dt_file = {'title-file': title, 'lecture_title': lecture_title, 'lecture_id': lecture_id,
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
- def my_subscribed_courses_by_plan(self) -> list[dict]:
23
- """obtém os cursos que o usuário esatá inscrito, obtidos atraves de planos(assinatura)"""
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
- def my_subscribed_courses(self) -> list[dict]:
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.5
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
- ![Versão](https://img.shields.io/badge/version-0.2.4-orange)
22
+ ![Versão](https://img.shields.io/badge/version-0.2.7-orange)
23
23
  ![Licença](https://img.shields.io/badge/license-MIT-orange)
24
24
  [![Sponsor](https://img.shields.io/badge/💲Donate-yellow)](https://apoia.se/paulocesar-dev404)
25
25
  [![Sponsor](https://img.shields.io/badge/Documentation-green)](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=av-dYivm9FvwaeydwdCTzwY6FYMnAjRiqjIqUkZFNAs,405
14
+ udemy_userAPI/__version__.py,sha256=FlHdJYlkLREupFjW2L1uKD5VxsxBYWG5W4dW5ylbmr8,405
15
15
  udemy_userAPI/api.py,sha256=dpwFtXewQmKwgG1IvzDFYZoEHNTwZbLIuv4WKgbqjOg,18817
16
- udemy_userAPI/authenticate.py,sha256=faIoOZdU5StQvUZfLEwZ3xfPba2FrFbc2HKEOeVY7w4,8246
17
- udemy_userAPI/bultins.py,sha256=XCXMe_5qKig_q5vbSFXBtx1vM87pE3UvEMKyUy6JCRo,12202
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=2UIy70wxWJAU7qZxvMDb3sHd1uQaffUXhib04ShIuI8,2124
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.5.dist-info/LICENSE,sha256=l4jdKYt8gSdDFOGr09vCKnMn_Im55XIcQKqTDEtFfNs,1095
26
- udemy_userAPI-0.2.5.dist-info/METADATA,sha256=jx7z2l2U-YUTezhxrBjEgC6Ohq01YsqjjRXJ5jPYoGM,1394
27
- udemy_userAPI-0.2.5.dist-info/WHEEL,sha256=PZUExdf71Ui_so67QXpySuHtCi3-J3wvF4ORK6k_S8U,91
28
- udemy_userAPI-0.2.5.dist-info/top_level.txt,sha256=SrUygl6WJt34qYigm-FiGqKCs2M4S_f-aU2i6oJ3nDM,65
29
- udemy_userAPI-0.2.5.dist-info/RECORD,,
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
@@ -1,4 +0,0 @@
1
- animation_consoles
2
- ffmpeg_for_python
3
- m3u8_analyzer
4
- udemy_userAPI