udemy-userAPI 0.1.5__py3-none-any.whl → 0.1.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.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'
udemy_userAPI/api.py CHANGED
@@ -1,7 +1,13 @@
1
1
  import json
2
- import requests
3
2
  from .exeptions import UdemyUserApiExceptions, UnhandledExceptions
4
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
5
11
 
6
12
  AUTH = UdemyAuth()
7
13
  COOKIES = AUTH.load_cookies
@@ -38,6 +44,142 @@ HEADERS_octet_stream = {
38
44
  'accept-language': 'en-US,en;q=0.9',
39
45
  }
40
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
+
41
183
 
42
184
  def parser_chapers(results):
43
185
  """
@@ -67,19 +209,57 @@ def parser_chapers(results):
67
209
  elif _class == 'lecture' and current_chapter is not None:
68
210
  asset = dictionary.get('asset')
69
211
  if asset:
70
- video_title = asset.get('title', None)
212
+ video_title = dictionary.get('title', None)
71
213
  if not video_title:
72
214
  video_title = 'Files'
73
215
  current_chapter['videos_in_chapter'].append({
74
216
  'video_title': video_title,
75
217
  'title_lecture': dictionary.get('title'),
76
- 'id_lecture': dictionary.get('id'),
77
- 'id_asset': asset.get('id')
218
+ 'lecture_id': dictionary.get('id'),
219
+ 'asset_id': asset.get('id')
78
220
  })
79
-
80
221
  return chapters_dict
81
222
 
82
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
+
83
263
  def get_links(course_id: int, id_lecture: int):
84
264
  """
85
265
  :param course_id: id do curso
@@ -120,10 +300,37 @@ def remove_tag(d: str):
120
300
  return new
121
301
 
122
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
+
123
329
  def extract_files(supplementary_assets: list) -> list:
124
330
  """Obtém o ID da lecture, o ID do asset, o asset_type e o filename."""
125
331
  files = []
126
332
  for item in supplementary_assets:
333
+ # print(f'DEBUG files:\n{item}\n\n')
127
334
  lecture_title = item.get('lecture_title')
128
335
  lecture_id = item.get('lecture_id')
129
336
  asset = item.get('asset', {})
@@ -131,14 +338,15 @@ def extract_files(supplementary_assets: list) -> list:
131
338
  asset_type = asset.get('asset_type')
132
339
  filename = asset.get('filename')
133
340
  title = asset.get('title')
134
-
341
+ external_url = asset.get('is_external', None)
135
342
  files.append({
136
343
  'lecture_id': lecture_id,
137
344
  'asset_id': asset_id,
138
345
  'asset_type': asset_type,
139
346
  'filename': filename,
140
347
  'title': title,
141
- 'lecture_title': lecture_title
348
+ 'lecture_title': lecture_title,
349
+ 'ExternalLink': external_url
142
350
  })
143
351
  return files
144
352
 
@@ -257,3 +465,37 @@ def format_size(byte_size):
257
465
  return f"{byte_size / TB:.2f} TB"
258
466
  except Exception as e:
259
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
@@ -11,9 +11,17 @@ class UdemyAuth:
11
11
  """Autenticação na plataforma udemy de maneira segura, atencao ao limite de logins,recomendo que apos logar
12
12
  nao use novamnete o metodo login use apenas o verifcador de login para evitar bloqueios temporários..."""
13
13
  self.__cookie_dict = {}
14
- dir_out = '.udemy_userAPI'
15
- os.makedirs(os.path.join(os.path.expanduser('~'), dir_out), exist_ok=True)
16
- self.__user_dir = os.path.join(os.path.expanduser('~'), dir_out)
14
+ # Diretório do arquivo atual
15
+ current_directory = os.path.dirname(__file__)
16
+ # dir cache
17
+ cache = '.cache'
18
+ cache_dir = os.path.join(current_directory, cache)
19
+ os.makedirs(cache_dir, exist_ok=True)
20
+ # Cria o diretório completo para a API do usuário
21
+ self.__user_dir = os.path.join(cache_dir)
22
+ # Cria o caminho completo para um arquivo específico
23
+ file_name = '.udemy_userAPI' # Nome do arquivo
24
+ self.__file_path = os.path.join(self.__user_dir, file_name)
17
25
 
18
26
  def __make_cookies(self, client_id: str, access_token: str, csrf_token: str):
19
27
  self.__cookie_dict = {
@@ -29,12 +37,12 @@ class UdemyAuth:
29
37
  def verif_config():
30
38
  # Verificar se o arquivo .userLogin existe e carregar cookies se existir
31
39
  try:
32
- with open(fr'{self.__user_dir}\.Udemy_userLogin', 'rb') as f:
40
+ with open(fr'{self.__file_path}', 'rb') as f:
33
41
  cookies = pickle.load(f)
34
42
  cookies_dict = {cookie.name: cookie.value for cookie in cookies}
35
43
  cookies_str = "; ".join([f"{key}={value}" for key, value in cookies_dict.items()])
36
44
  return cookies_str
37
- except FileNotFoundError:
45
+ except Exception as e:
38
46
  return False
39
47
 
40
48
  log = verif_config()
@@ -81,82 +89,94 @@ class UdemyAuth:
81
89
 
82
90
  def login(self, email: str, password: str):
83
91
  """efetuar login na udemy"""
84
- # Inicializa uma sessão usando cloudscraper para contornar a proteção Cloudflare
85
- s = cloudscraper.create_scraper()
86
-
87
- # Faz uma requisição GET à página de inscrição para obter o token CSRF
88
- r = s.get(
89
- "https://www.udemy.com/join/signup-popup/",
90
- headers={"User-Agent": "okhttp/4.9.2 UdemyAndroid 8.9.2(499) (phone)"},
91
- )
92
-
93
- # Extrai o token CSRF dos cookies
94
- csrf_token = r.cookies["csrftoken"]
95
-
96
- # Prepara os dados para o login
97
- data = {
98
- "csrfmiddlewaretoken": csrf_token,
99
- "locale": "pt_BR",
100
- "email": email,
101
- "password": password,
102
- }
103
-
104
- # Atualiza os cookies e cabeçalhos da sessão
105
- s.cookies.update(r.cookies)
106
- s.headers.update(
107
- {
108
- "User-Agent": "okhttp/4.9.2 UdemyAndroid 8.9.2(499) (phone)",
109
- "Accept": "application/json, text/plain, */*",
110
- "Accept-Language": "en-GB,en;q=0.5",
111
- "Referer": "https://www.udemy.com/join/login-popup/?locale=en_US&response_type=html&next=https%3A%2F"
112
- "%2Fwww.udemy.com%2F",
113
- "Origin": "https://www.udemy.com",
114
- "DNT": "1",
115
- "Connection": "keep-alive",
116
- "Sec-Fetch-Dest": "empty",
117
- "Sec-Fetch-Mode": "cors",
118
- "Sec-Fetch-Site": "same-origin",
119
- "Pragma": "no-cache",
120
- "Cache-Control": "no-cache",
92
+ try:
93
+ # Inicializa uma sessão usando cloudscraper para contornar a proteção Cloudflare
94
+ s = cloudscraper.create_scraper()
95
+
96
+ # Faz uma requisição GET à página de inscrição para obter o token CSRF
97
+ r = s.get(
98
+ "https://www.udemy.com/join/signup-popup/",
99
+ headers={"User-Agent": "okhttp/4.9.2 UdemyAndroid 8.9.2(499) (phone)"},
100
+ )
101
+
102
+ # Extrai o token CSRF dos cookies
103
+ csrf_token = r.cookies["csrftoken"]
104
+
105
+ # Prepara os dados para o login
106
+ data = {
107
+ "csrfmiddlewaretoken": csrf_token,
108
+ "locale": "pt_BR",
109
+ "email": email,
110
+ "password": password,
121
111
  }
122
- )
123
-
124
- # Tenta fazer login com as credenciais fornecidas
125
- r = s.post(
126
- "https://www.udemy.com/join/login-popup/?response_type=json",
127
- data=data,
128
- allow_redirects=False,
129
- )
130
-
131
- # Verifica a resposta para determinar se o login foi bem-sucedido
132
- if "returnUrl" in r.text:
133
- self.__make_cookies(r.cookies.get("client_id"), r.cookies.get("access_token"), csrf_token)
134
- self.__save_cookies(s.cookies)
135
- else:
136
- login_error = r.json().get("error", {}).get("data", {}).get("formErrors", [])[0]
137
- if login_error[0] == "Y":
138
- raise LoginException("Você excedeu o número máximo de solicitações por hora.")
139
- elif login_error[0] == "T":
140
- raise LoginException("Email ou senha incorretos")
141
- else:
142
- raise UnhandledExceptions(login_error)
143
112
 
144
- return s
113
+ # Atualiza os cookies e cabeçalhos da sessão
114
+ s.cookies.update(r.cookies)
115
+ s.headers.update(
116
+ {
117
+ "User-Agent": "okhttp/4.9.2 UdemyAndroid 8.9.2(499) (phone)",
118
+ "Accept": "application/json, text/plain, */*",
119
+ "Accept-Language": "en-GB,en;q=0.5",
120
+ "Referer": "https://www.udemy.com/join/login-popup/?locale=en_US&response_type=html&next=https%3A%2F"
121
+ "%2Fwww.udemy.com%2F",
122
+ "Origin": "https://www.udemy.com",
123
+ "DNT": "1",
124
+ "Connection": "keep-alive",
125
+ "Sec-Fetch-Dest": "empty",
126
+ "Sec-Fetch-Mode": "cors",
127
+ "Sec-Fetch-Site": "same-origin",
128
+ "Pragma": "no-cache",
129
+ "Cache-Control": "no-cache",
130
+ }
131
+ )
132
+
133
+ # Tenta fazer login com as credenciais fornecidas
134
+ r = s.post(
135
+ "https://www.udemy.com/join/login-popup/?response_type=json",
136
+ data=data,
137
+ allow_redirects=False,
138
+ )
139
+
140
+ # Verifica a resposta para determinar se o login foi bem-sucedido
141
+ if "returnUrl" in r.text:
142
+ self.__make_cookies(r.cookies.get("client_id"), r.cookies.get("access_token"), csrf_token)
143
+ self.__save_cookies(s.cookies)
144
+ else:
145
+ login_error = r.json().get("error", {}).get("data", {}).get("formErrors", [])[0]
146
+ if login_error[0] == "Y":
147
+ raise LoginException("Você excedeu o número máximo de solicitações por hora.")
148
+ elif login_error[0] == "T":
149
+ raise LoginException("Email ou senha incorretos")
150
+ else:
151
+ raise UnhandledExceptions(login_error)
152
+
153
+ return s
154
+ except Exception as e:
155
+ LoginException(e)
145
156
 
146
157
  def __save_cookies(self, cookies):
147
- with open(fr'{self.__user_dir}\.Udemy_userLogin', 'wb') as f:
148
- pickle.dump(cookies, f)
158
+ try:
159
+ with open(fr'{self.__file_path}', 'wb') as f:
160
+ pickle.dump(cookies, f)
161
+ except Exception as e:
162
+ LoginException(e)
149
163
 
150
164
  @property
151
165
  def load_cookies(self) -> str:
152
166
  """carrega cookies e retorna-os em uma string formatada"""
153
- file = os.path.join(fr'{self.__user_dir}\.Udemy_userLogin')
154
- if os.path.exists(file):
155
- with open(fr'{self.__user_dir}\.Udemy_userLogin', 'rb') as f:
156
- cookies = pickle.load(f)
157
- cookies_dict = {cookie.name: cookie.value for cookie in cookies}
158
- cookies_str = "; ".join([f"{key}={value}" for key, value in cookies_dict.items()])
159
- return cookies_str
160
- else:
161
- raise LoginException()
167
+ try:
168
+ file = os.path.join(fr'{self.__file_path}')
169
+ if os.path.exists(file):
170
+ with open(fr'{self.__file_path}', 'rb') as f:
171
+ cookies = pickle.load(f)
172
+ cookies_dict = {cookie.name: cookie.value for cookie in cookies}
173
+ cookies_str = "; ".join([f"{key}={value}" for key, value in cookies_dict.items()])
174
+ return cookies_str
175
+ else:
176
+ return 'None'
177
+ except Exception as e:
178
+ LoginException(e)
162
179
 
180
+ def remove_cookies(self):
181
+ if os.path.exists(self.__file_path):
182
+ os.remove(self.__file_path)
udemy_userAPI/bultins.py CHANGED
@@ -1,8 +1,33 @@
1
- import requests
2
1
  import json
2
+ from typing import Any
3
+ import requests
4
+ from .api import get_links, remove_tag, parser_chapers, extract_files, HEADERS_USER, assets_infor, get_add_files, \
5
+ get_files_aule, get_external_liks, extract, get_pssh, organize_streams, get_mpd_file, get_highest_resolution
3
6
  from .sections import get_course_infor
4
- from .api import get_links, remove_tag, parser_chapers, extract_files, HEADERS_USER
5
-
7
+ from .mpd_analyzer import MPDParser
8
+
9
+
10
+ class DRM:
11
+ def __init__(self, license_token: str, get_media_sources: list):
12
+ self.__mpd_content = None
13
+ self.__dash_url = organize_streams(streams=get_media_sources).get('dash')
14
+ self.__token = license_token
15
+ self.get_key_for_lesson()
16
+
17
+ def get_key_for_lesson(self):
18
+ """get keys for lesson"""
19
+ if self.__dash_url:
20
+ self.__mpd_content = get_mpd_file(mpd_url=self.__dash_url[0].get('src'))
21
+ parser = MPDParser(mpd_file_path=self.__mpd_content, is_file=True, headers=HEADERS_USER)
22
+ resolutions = get_highest_resolution(parser.get_all_video_resolutions())
23
+ parser.set_selected_resolution(resolution=resolutions)
24
+ init_url = parser.get_selected_video_init_url()
25
+ if init_url:
26
+ pssh = get_pssh(init_url=init_url)
27
+ if pssh:
28
+ keys = extract(pssh=pssh, license_token=self.__token)
29
+ if keys:
30
+ return keys
6
31
 
7
32
 
8
33
  class Files:
@@ -11,29 +36,44 @@ class Files:
11
36
  self.__id_course = id_course
12
37
 
13
38
  @property
14
- def get_download_url(self) -> dict[list]:
39
+ def get_download_url(self) -> dict[str, Any | None] | list[dict[str, Any | None]]:
15
40
  """obter url de download de um arquivo quando disponivel(geralemnete para arquivos esta opção é valida"""
16
41
  da = {}
17
- download_urls = ''
42
+ download_urls = []
18
43
  for files in self.__data:
19
44
  lecture_id = files.get('lecture_id', None)
20
45
  asset_id = files.get('asset_id', None)
21
46
  title = files.get("title", None)
22
- if asset_id and title and lecture_id:
47
+ lecture_title = files.get('lecture_title')
48
+ external_link = files.get('ExternalLink')
49
+ if external_link:
50
+ 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,
52
+ 'external_link': external_link,
53
+ 'data-file': lnk.get('external_url', None)}
54
+ return dt_file
55
+ if asset_id and title and lecture_id and not external_link:
23
56
  resp = requests.get(
24
- f"https://www.udemy.com/api-2.0/users/me/subscribed-courses/{self.__id_course}/lectures/{lecture_id}/supplementary-assets/{asset_id}/?fields[asset]=download_urls",
57
+ f"https://www.udemy.com/api-2.0/users/me/subscribed-courses/{self.__id_course}/lectures/"
58
+ f"{lecture_id}/supplementary-assets/{asset_id}/?fields[asset]=download_urls",
25
59
  headers=HEADERS_USER)
26
60
  if resp.status_code == 200:
27
61
  da = json.loads(resp.text)
28
- download_urls = da['download_urls']
62
+ ## para cdaa dict de um fle colocar seu titulo:
63
+ dt_file = {'title-file': title, 'lecture_title': lecture_title, 'lecture_id': lecture_id,
64
+ 'external_link': external_link,
65
+ 'data-file': da['download_urls']}
66
+ download_urls.append(dt_file)
29
67
  return download_urls
30
68
 
31
69
 
32
70
  class Lecture:
33
71
  """CRIAR objetos aula(lecture) do curso e extrair os dados.."""
34
72
 
35
- def __init__(self, data: dict):
73
+ def __init__(self, data: dict, course_id: int, additional_files):
74
+ self.__course_id = course_id
36
75
  self.__data = data
76
+ self.__additional_files = additional_files
37
77
  self.__asset = self.__data.get("asset")
38
78
 
39
79
  @property
@@ -86,9 +126,15 @@ class Lecture:
86
126
  return self.__asset.get('media_license_token')
87
127
 
88
128
  @property
89
- def course_is_drmed(self) -> bool:
90
- """verifica se possui DRM.."""
91
- return self.__asset.get('course_is_drmed')
129
+ def course_is_drmed(self):
130
+ """verifica se a aula possui DRM se sim retorna as keys da aula...
131
+ retorna 'kid:key' or None"""
132
+ if self.__asset.get('course_is_drmed'):
133
+ d = DRM(license_token=self.get_media_license_token,
134
+ get_media_sources=self.get_media_sources)
135
+ return d
136
+ else:
137
+ return self.__asset.get('course_is_drmed')
92
138
 
93
139
  @property
94
140
  def get_download_urls(self) -> list:
@@ -105,6 +151,18 @@ class Lecture:
105
151
  """obtem slides se tiver.."""
106
152
  return self.__asset.get('slides')
107
153
 
154
+ @property
155
+ def get_articles(self):
156
+ d = assets_infor(course_id=self.__course_id, id_lecture=self.get_lecture_id, assets_id=self.__asset.get("id"))
157
+ return d
158
+
159
+ @property
160
+ def get_resources(self):
161
+ """obtem os recurso adconais reaioanddos a aula"""
162
+ files_add = get_files_aule(lecture_id_filter=self.get_lecture_id, data=self.__additional_files)
163
+ f = Files(files=files_add, id_course=self.__course_id).get_download_url
164
+ return f
165
+
108
166
 
109
167
  class Course:
110
168
  """receb um dict com os dados do curso"""
@@ -114,6 +172,7 @@ class Course:
114
172
  self.__data = self.__parser_chapers
115
173
  self.__course_id = course_id
116
174
  self.__results = results
175
+ self.__additional_files_data = get_add_files(course_id)
117
176
  self.__information = self.__load_infor_course()
118
177
 
119
178
  def __load_infor_course(self) -> dict:
@@ -172,28 +231,42 @@ class Course:
172
231
 
173
232
  @property
174
233
  def get_lectures(self) -> list:
175
- """Obtém uma lista com todos as aulas"""
234
+ """Obtém uma lista com todas as aulas."""
176
235
  videos = []
236
+ section_order = 1 # Iniciar a numeração das seções (capítulos)
237
+
177
238
  for chapter in self.__data.values():
178
- for video in chapter.get('videos_in_chapter', []):
179
- title = video['video_title']
180
- id_lecture = video['id_lecture']
181
- id_asset = video['id_asset']
182
- dt = {"title": title, 'id_lecture': id_lecture, 'id_asset': id_asset}
239
+ for index, video in enumerate(chapter.get('videos_in_chapter', [])):
240
+ section = f"{section_order} {chapter.get('title_chapter')}" # Adicionar numeração da seção
241
+ title = video.get('video_title')
242
+ id_lecture = video.get('lecture_id')
243
+ id_asset = video.get('asset_id')
244
+ dt = {
245
+ 'section': section,
246
+ 'title': title,
247
+ 'lecture_id': id_lecture,
248
+ 'asset_id': id_asset,
249
+ 'section_order': section_order,
250
+ 'lecture_order': index + 1 # Numeração da seção e vídeo
251
+ }
183
252
  videos.append(dt)
253
+ section_order += 1 # Incrementar o número da seção após processar os vídeos do capítulo
184
254
  return videos
185
255
 
186
256
  def get_details_lecture(self, lecture_id: int) -> Lecture:
187
257
  """obter detalhes de uma aula específica, irá retornar o objeto Lecture"""
188
258
  links = get_links(course_id=self.__course_id, id_lecture=lecture_id)
189
- lecture = Lecture(data=links)
259
+ additional_files = self.__load_assets()
260
+ # print(f'DEBUG files passados a lecture:\n{additional_files}\n\n')
261
+ lecture = Lecture(data=links, course_id=self.__course_id, additional_files=additional_files)
190
262
  return lecture
191
263
 
192
264
  @property
193
- def get_additional_files(self) -> dict[list]:
265
+ def get_additional_files(self) -> list[Any]:
194
266
  """Retorna a lista de arquivos adcionais de um curso."""
195
267
  supplementary_assets = []
196
- for item in self.__results.get('results', []):
268
+ files_downloader = []
269
+ for item in self.__additional_files_data.get('results', []):
197
270
  # Check if the item is a lecture with supplementary assets
198
271
  if item.get('_class') == 'lecture':
199
272
  id = item.get('id')
@@ -206,5 +279,25 @@ class Course:
206
279
  'asset': asset
207
280
  })
208
281
  files = extract_files(supplementary_assets)
282
+ # print(f'DEBUG files:\n{files}\n\n')
209
283
  files_objt = Files(files=files, id_course=self.__course_id).get_download_url
210
284
  return files_objt
285
+
286
+ def __load_assets(self):
287
+ """Retorna a lista de arquivos adcionais de um curso."""
288
+ supplementary_assets = []
289
+ files_downloader = []
290
+ for item in self.__additional_files_data.get('results', []):
291
+ # Check if the item is a lecture with supplementary assets
292
+ if item.get('_class') == 'lecture':
293
+ id = item.get('id')
294
+ title = item.get('title')
295
+ assets = item.get('supplementary_assets', [])
296
+ for asset in assets:
297
+ supplementary_assets.append({
298
+ 'lecture_id': id,
299
+ 'lecture_title': title,
300
+ 'asset': asset
301
+ })
302
+ files = extract_files(supplementary_assets)
303
+ return files
@@ -0,0 +1,3 @@
1
+ # Biblioteca para análise de arquivos MPD
2
+ from .mpd_parser import MPDParser
3
+ __version__ = '0.1.0'
@@ -0,0 +1,357 @@
1
+ import re
2
+ import xml.etree.ElementTree as ET
3
+
4
+
5
+ class MPDParser:
6
+ """
7
+ Classe para analisar e extrair informações de manifestos MPD (Media Presentation Description),
8
+ com foco em arquivos VOD (Video on Demand). Atualmente, não oferece suporte para transmissões ao vivo
9
+ """
10
+
11
+ def __init__(self, mpd_file_path: str, headers=None,is_file=None):
12
+ """
13
+ Inicializa o parser para um arquivo MPD a partir de um caminho de arquivo.
14
+
15
+ Args:
16
+ mpd_file_path (str): Caminho do arquivo MPD.
17
+ headers (dict, opcional): Headers HTTP adicionais para requisição, caso necessário.
18
+ """
19
+ self.is_file = is_file
20
+ self.availability_start_time = None
21
+ self.mpd_file_path = mpd_file_path
22
+ self.headers = headers if headers else {}
23
+ self.video_representations = {} # Armazena representações de vídeo, organizadas por resolução
24
+ self.audio_representations = {} # Armazena representações de áudio, organizadas por taxa de bits
25
+ self.content_protection = {} # Armazena informações de proteção de conteúdo
26
+ self.selected_resolution = None # Resolução selecionada para recuperação de segmentos de vídeo
27
+ self.initi = self.__parse_mpd2()
28
+ if not self.initi:
29
+ initi = self.__parse_mpd()
30
+
31
+ def __parse_mpd(self):
32
+ """
33
+ Faz o parsing do arquivo MPD localizado no caminho especificado e
34
+ extrai informações sobre segmentos de vídeo e áudio, além de proteção de conteúdo.
35
+ """
36
+ mpd_content = ''
37
+ if not self.is_file:
38
+ try:
39
+ with open(self.mpd_file_path, 'r', encoding='utf-8') as file:
40
+ mpd_content = file.read()
41
+ except FileNotFoundError:
42
+ print(f"Erro: Arquivo '{self.mpd_file_path}' não encontrado.")
43
+ return
44
+ except IOError:
45
+ print(f"Erro ao ler o arquivo '{self.mpd_file_path}'.")
46
+ return
47
+ else:
48
+ mpd_content = self.mpd_file_path
49
+ # Analisa o conteúdo MPD usando namespaces XML para acessar nós DASH
50
+ root = ET.fromstring(mpd_content)
51
+ ns = {'dash': 'urn:mpeg:dash:schema:mpd:2011'}
52
+
53
+ # Processa cada AdaptationSet para extração de representações de áudio e vídeo
54
+ for adaptation_set in root.findall('.//dash:AdaptationSet', ns):
55
+ mime_type = adaptation_set.attrib.get('mimeType', '')
56
+ # Extrai informações de proteção de conteúdo, se presentes
57
+ for content_protection in adaptation_set.findall('dash:ContentProtection', ns):
58
+ scheme_id_uri = content_protection.attrib.get('schemeIdUri', '')
59
+ value = content_protection.attrib.get('value', '')
60
+ self.content_protection[scheme_id_uri] = value
61
+
62
+ # Extrai informações de cada representação de mídia
63
+ for representation in adaptation_set.findall('dash:Representation', ns):
64
+ rep_id = representation.attrib.get('id')
65
+ width = int(representation.attrib.get('width', 0))
66
+ height = int(representation.attrib.get('height', 0))
67
+ resolution = (width, height) if width and height else None
68
+ bandwidth = int(representation.attrib.get('bandwidth', 0))
69
+ # Obtém a quantidade de canais de áudio, se disponível
70
+ audio_channels = representation.find('dash:AudioChannelConfiguration', ns)
71
+ audio_channels_count = int(audio_channels.attrib.get('value', 0)) if audio_channels is not None else 0
72
+
73
+ # Processa SegmentTemplate para URLs de inicialização e segmentos
74
+ segment_template = representation.find('dash:SegmentTemplate', ns)
75
+ if segment_template is not None:
76
+ init_template = segment_template.get('initialization')
77
+ init_url = self.__build_url(init_template, rep_id, bandwidth) if init_template else None
78
+
79
+ media_url_template = segment_template.get('media')
80
+ timescale = int(segment_template.get('timescale', 1))
81
+
82
+ # Processa SegmentTimeline para obtenção de segmentos individuais
83
+ segment_timeline = segment_template.find('dash:SegmentTimeline', ns)
84
+ segments = []
85
+ if segment_timeline is not None:
86
+ segment_number = int(segment_template.get('startNumber', 1))
87
+ start_time = 0
88
+ for segment in segment_timeline.findall('dash:S', ns):
89
+ t = int(segment.get('t', start_time))
90
+ d = int(segment.get('d'))
91
+ r = int(segment.get('r', 0))
92
+
93
+ # Adiciona segmentos repetidos se necessário
94
+ for _ in range(r + 1):
95
+ segments.append(
96
+ self.__calculate_segment_url(media_url_template, segment_number, t, rep_id,
97
+ bandwidth)
98
+ )
99
+ t += d
100
+ segment_number += 1
101
+
102
+ # Armazena informações de representação com resolução ou taxa de bits como chave
103
+ representation_info = {
104
+ 'id': rep_id,
105
+ 'resolution': resolution,
106
+ 'bandwidth': bandwidth,
107
+ 'audio_channels': audio_channels_count,
108
+ 'init_url': init_url,
109
+ 'segments': segments,
110
+ }
111
+ if 'video' in mime_type:
112
+ self.video_representations[resolution] = representation_info
113
+ elif 'audio' in mime_type:
114
+ self.audio_representations[bandwidth] = representation_info
115
+
116
+ def __parse_mpd2(self):
117
+ """
118
+ Faz o parsing do arquivo MPD localizado no caminho especificado e
119
+ extrai informações sobre segmentos de vídeo e áudio, além de proteção de conteúdo.
120
+ """
121
+ mpd_content = ''
122
+ if not self.is_file:
123
+ try:
124
+ with open(self.mpd_file_path, 'r', encoding='utf-8') as file:
125
+ mpd_content = file.read()
126
+ except FileNotFoundError:
127
+ print(f"Erro: Arquivo '{self.mpd_file_path}' não encontrado.")
128
+ return
129
+ except IOError:
130
+ print(f"Erro ao ler o arquivo '{self.mpd_file_path}'.")
131
+ return
132
+ else:
133
+ mpd_content = self.mpd_file_path
134
+
135
+ # Analisar o conteúdo MPD
136
+ root = ET.fromstring(mpd_content)
137
+ ns = {'dash': 'urn:mpeg:dash:schema:mpd:2011'}
138
+
139
+ # Extrai a duração total da apresentação em segundos
140
+ self.media_presentation_duration = self.parse_duration(root.attrib.get('mediaPresentationDuration', 'PT0S'))
141
+
142
+ for adaptation_set in root.findall('.//dash:AdaptationSet', ns):
143
+ mime_type = adaptation_set.attrib.get('mimeType', '')
144
+
145
+ # Extrai proteção de conteúdo
146
+ for content_protection in adaptation_set.findall('dash:ContentProtection', ns):
147
+ scheme_id_uri = content_protection.attrib.get('schemeIdUri', '')
148
+ value = content_protection.attrib.get('value', '')
149
+ self.content_protection[scheme_id_uri] = value
150
+
151
+ # Extrai representações de vídeo e áudio
152
+ for representation in adaptation_set.findall('dash:Representation', ns):
153
+ rep_id = representation.attrib.get('id')
154
+ width = int(representation.attrib.get('width', 0))
155
+ height = int(representation.attrib.get('height', 0))
156
+ resolution = (width, height) if width and height else None
157
+ bandwidth = int(representation.attrib.get('bandwidth', 0))
158
+
159
+ # SegmentTemplate e SegmentTimeline
160
+ segment_template = adaptation_set.find('dash:SegmentTemplate', ns)
161
+ if segment_template is not None:
162
+ init_template = segment_template.get('initialization')
163
+ media_template = segment_template.get('media')
164
+ timescale = int(segment_template.get('timescale', 1))
165
+ start_number = int(segment_template.get('startNumber', 1))
166
+
167
+ # Processa SegmentTimeline
168
+ segment_timeline = segment_template.find('dash:SegmentTimeline', ns)
169
+ segments = []
170
+ if segment_timeline is not None:
171
+ segment_number = start_number
172
+ for segment in segment_timeline.findall('dash:S', ns):
173
+ t = int(segment.get('t', 0))
174
+ d = int(segment.get('d'))
175
+ r = int(segment.get('r', 0)) # Quantidade de repetições
176
+
177
+ for i in range(r + 1): # Inclui o segmento e suas repetições
178
+ segment_time = t + i * d
179
+ segment_url = self.__calculate_segment_url2(media_template, segment_number, segment_time,
180
+ rep_id)
181
+ segments.append(segment_url)
182
+ segment_number += 1
183
+ else:
184
+ # No SegmentTimeline, gera segmentos contínuos
185
+ duration = int(segment_template.get('duration', 1))
186
+ total_segments = int((self.media_presentation_duration * timescale) // duration)
187
+ for segment_number in range(start_number, start_number + total_segments):
188
+ segment_time = (segment_number - 1) * duration
189
+ segment_url = self.__calculate_segment_url2(media_template, segment_number, segment_time,
190
+ rep_id)
191
+ segments.append(segment_url)
192
+
193
+ # Armazena representações de vídeo e áudio com URLs de segmentos
194
+ representation_info = {
195
+ 'id': rep_id,
196
+ 'resolution': resolution,
197
+ 'bandwidth': bandwidth,
198
+ 'init_url': self.__build_url2(init_template, rep_id),
199
+ 'segments': segments,
200
+ }
201
+ if 'video' in mime_type:
202
+ self.video_representations[resolution] = representation_info
203
+ elif 'audio' in mime_type:
204
+ self.audio_representations[bandwidth] = representation_info
205
+ def parse_duration(self, duration_str):
206
+ """
207
+ Converte uma duração em formato ISO 8601 (ex: "PT163.633S") para segundos (float).
208
+ """
209
+ match = re.match(r'PT(?:(\d+)H)?(?:(\d+)M)?(?:(\d+(?:\.\d+)?)S)?', duration_str)
210
+ if match:
211
+ hours = int(match.group(1)) if match.group(1) else 0
212
+ minutes = int(match.group(2)) if match.group(2) else 0
213
+ seconds = float(match.group(3)) if match.group(3) else 0.0
214
+ return hours * 3600 + minutes * 60 + seconds
215
+ return 0.0
216
+
217
+ @staticmethod
218
+ def __build_url(template, rep_id, bandwidth):
219
+ """
220
+ Constrói uma URL substituindo placeholders em um template de URL.
221
+
222
+ Args:
223
+ template (str): Template de URL com placeholders.
224
+ rep_id (str): ID da representação.
225
+ bandwidth (int): Largura de banda da representação.
226
+
227
+ Returns:
228
+ str: URL formatada com placeholders substituídos.
229
+ """
230
+ if '$RepresentationID$' in template:
231
+ template = template.replace('$RepresentationID$', rep_id)
232
+ if '$Bandwidth$' in template:
233
+ template = template.replace('$Bandwidth$', str(bandwidth))
234
+ return template
235
+
236
+ def __build_url2(self, template, rep_id):
237
+ """
238
+ Constrói a URL substituindo variáveis no template com base nos atributos.
239
+ """
240
+ if '$RepresentationID$' in template:
241
+ template = template.replace('$RepresentationID$', rep_id)
242
+ return template
243
+
244
+ @staticmethod
245
+ def __calculate_segment_url(media_template, segment_number, segment_time, rep_id, bandwidth):
246
+ """
247
+ Constrói a URL de um segmento substituindo placeholders por valores reais.
248
+
249
+ Args:
250
+ media_template (str): Template de URL do segmento.
251
+ segment_number (int): Número do segmento.
252
+ segment_time (int): Timestamp do segmento.
253
+ rep_id (str): ID da representação.
254
+ bandwidth (int): Largura de banda da representação.
255
+
256
+ Returns:
257
+ str: URL do segmento com placeholders substituídos.
258
+ """
259
+ url = media_template.replace('$Number$', str(segment_number))
260
+ url = url.replace('$RepresentationID$', rep_id).replace('$Bandwidth$', str(bandwidth))
261
+ if '$Time$' in url:
262
+ url = url.replace('$Time$', str(segment_time))
263
+ return url
264
+
265
+ def __calculate_segment_url2(self, media_template, segment_number, segment_time, rep_id):
266
+ """
267
+ Calcula a URL de um segmento específico, substituindo variáveis no template.
268
+ """
269
+ url = media_template.replace('$Number$', str(segment_number))
270
+ url = url.replace('$RepresentationID$', rep_id)
271
+ if '$Time$' in url:
272
+ url = url.replace('$Time$', str(segment_time))
273
+ return url
274
+
275
+ def get_video_representations(self):
276
+ """
277
+ Retorna as representações de vídeo extraídas do arquivo MPD.
278
+
279
+ Returns:
280
+ dict: Representações de vídeo com resoluções como chaves.
281
+ """
282
+ return self.video_representations
283
+
284
+ def get_audio_representations(self):
285
+ """
286
+ Retorna as representações de áudio extraídas do arquivo MPD.
287
+
288
+ Returns:
289
+ dict: Representações de áudio com taxas de bits como chaves.
290
+ """
291
+ return self.audio_representations
292
+
293
+ def get_content_protection_info(self):
294
+ """
295
+ Retorna as informações de proteção de conteúdo extraídas do MPD.
296
+
297
+ Returns:
298
+ dict: Dados de proteção de conteúdo com URI do esquema como chaves.
299
+ """
300
+ return self.content_protection
301
+
302
+ def set_selected_resolution(self, resolution: tuple):
303
+ """
304
+ Define a resolução selecionada para a recuperação de segmentos de vídeo.
305
+
306
+ Args:
307
+ resolution (tuple): Resolução desejada (largura, altura).
308
+
309
+ Raises:
310
+ Exception: Se a resolução não estiver disponível no manifesto.
311
+ """
312
+ if resolution in self.video_representations:
313
+ self.selected_resolution = resolution
314
+ else:
315
+ raise Exception(
316
+ f'A resolução {resolution} não está disponível!\n\n\t=> Resoluções disponíveis no arquivo: {self.get_all_video_resolutions()}')
317
+
318
+ def get_selected_video_segments(self):
319
+ """
320
+ Retorna os URLs dos segmentos de vídeo para a resolução selecionada.
321
+
322
+ Returns:
323
+ list: URLs dos segmentos de vídeo para a resolução selecionada.
324
+ """
325
+ if self.selected_resolution:
326
+ return self.video_representations[self.selected_resolution].get('segments', [])
327
+ else:
328
+ raise Exception(f'Você deve selecioanar uma resolução no método self.set_selected_resolution()')
329
+
330
+ def get_selected_video_init_url(self):
331
+ """
332
+ Retorna o URL de inicialização para a resolução de vídeo selecionada.
333
+
334
+ Returns:
335
+ str: URL de inicialização do vídeo, ou None se não houver resolução selecionada.
336
+ """
337
+ if self.selected_resolution:
338
+ return self.video_representations[self.selected_resolution].get('init_url')
339
+ return None
340
+
341
+ def get_all_video_resolutions(self):
342
+ """
343
+ Retorna uma lista de todas as resoluções de vídeo disponíveis.
344
+
345
+ Returns:
346
+ list: Lista de tuplas com resoluções de vídeo (largura, altura).
347
+ """
348
+ return list(self.video_representations.keys())
349
+
350
+ def get_audio_channels_count(self):
351
+ """
352
+ Retorna um dicionário com a quantidade de canais de áudio para cada taxa de bits de áudio.
353
+
354
+ Returns:
355
+ dict: Quantidade de canais de áudio para cada taxa de bits.
356
+ """
357
+ return {bandwidth: info['audio_channels'] for bandwidth, info in self.audio_representations.items()}
udemy_userAPI/udemy.py CHANGED
@@ -30,10 +30,25 @@ class Udemy:
30
30
 
31
31
  @property
32
32
  def my_subscribed_courses(self) -> list[dict]:
33
- """obtém os cursos que o usuário esatá inscrito"""
33
+ """Obtém os cursos que o usuário está inscrito, excluindo listas vazias ou nulas"""
34
34
  try:
35
- courses = get_courses_plan(tipe='default')
36
- return courses
35
+ # Obtém os cursos
36
+ courses1 = get_courses_plan(tipe='default') # lista de cursos padrão
37
+ courses2 = get_courses_plan(tipe='plan') # lista de cursos de um plano
38
+
39
+ # Cria uma lista vazia para armazenar os cursos válidos
40
+ all_courses = []
41
+
42
+ # Adiciona a lista somente se não estiver vazia ou nula
43
+ if courses1:
44
+ for i in courses1:
45
+ all_courses.extend(i)
46
+ if courses2:
47
+ for i in courses2:
48
+ all_courses.extend(i)
49
+
50
+ return all_courses
51
+
37
52
  except UdemyUserApiExceptions as e:
38
53
  UnhandledExceptions(e)
39
54
 
@@ -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)
@@ -0,0 +1,15 @@
1
+ udemy_userAPI/__init__.py,sha256=BPle89xE_CMTKKe_Lw6jioYLgpH-q_Lpho2S-n1PIUA,206
2
+ udemy_userAPI/__version__.py,sha256=LQkqzSPXq6RAMSeuzbyVp9NNPiqy1dPbWwBYG106raU,404
3
+ udemy_userAPI/api.py,sha256=7wWkuXwBvTpBLsAl7LaZnJKA6If0cL942CMbjY4GZJs,18782
4
+ udemy_userAPI/authenticate.py,sha256=inKtOzVEDExmo88-hq1_WQKrZmPsfx9r93AqCq0vnLo,7720
5
+ udemy_userAPI/bultins.py,sha256=bvgYC8cYMH26kqNzqhFlrCHR3M9aGQV5UsuemjKdM0k,12310
6
+ udemy_userAPI/exeptions.py,sha256=nuZoAt4i-ctrW8zx9LZtejrngpFXDHOVE5cEXM4RtrY,508
7
+ udemy_userAPI/sections.py,sha256=zPyDhvTIQCL0nbf7OJZG28Kax_iooILQ_hywUwvHoL8,4043
8
+ udemy_userAPI/udemy.py,sha256=ceaXVTbQhSYkHnPIpYWUVtT6IT6jBvzYO_k4B7EFyj8,2154
9
+ udemy_userAPI/mpd_analyzer/__init__.py,sha256=i3JVWyvcFLaj5kPmx8c1PgjsLht7OUIQQClD4yqYbo8,102
10
+ udemy_userAPI/mpd_analyzer/mpd_parser.py,sha256=_vw1feJXDjw5fQLOmA5-H3UklX_30Pbl__HtDUqvp3c,17283
11
+ udemy_userAPI-0.1.7.dist-info/LICENSE,sha256=l4jdKYt8gSdDFOGr09vCKnMn_Im55XIcQKqTDEtFfNs,1095
12
+ udemy_userAPI-0.1.7.dist-info/METADATA,sha256=VyI2ApvWAQ3MAMyAyCylR1xlPy8WsxRtPBAHBtRVewU,1368
13
+ udemy_userAPI-0.1.7.dist-info/WHEEL,sha256=P9jw-gEje8ByB7_hXoICnHtVCrEwMQh-630tKvQWehc,91
14
+ udemy_userAPI-0.1.7.dist-info/top_level.txt,sha256=ijTINaSDRKhdahY_X7dmSRFTxBIwQErWv9ATCG55mog,14
15
+ udemy_userAPI-0.1.7.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (74.1.2)
2
+ Generator: setuptools (75.3.0)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5
 
@@ -1,13 +0,0 @@
1
- udemy_userAPI/__init__.py,sha256=BPle89xE_CMTKKe_Lw6jioYLgpH-q_Lpho2S-n1PIUA,206
2
- udemy_userAPI/__version__.py,sha256=3f2y6FLB2-B7X9VGnzA9QP98hyUkSzMQJL2MbMFp6wY,404
3
- udemy_userAPI/api.py,sha256=5WRyVWHOCCNC0zFJudA91--7xwGHSmpO1_xcDz7fFqQ,9545
4
- udemy_userAPI/authenticate.py,sha256=DrO-kBX5GG46cz4cHoTZhDkMkoZB1fxhfUeU9e0wTfY,6832
5
- udemy_userAPI/bultins.py,sha256=egnphL3M7lEd1Hh80vHRVdAInxAr_YbwNHSwgt8NWec,7503
6
- udemy_userAPI/exeptions.py,sha256=nuZoAt4i-ctrW8zx9LZtejrngpFXDHOVE5cEXM4RtrY,508
7
- udemy_userAPI/sections.py,sha256=zPyDhvTIQCL0nbf7OJZG28Kax_iooILQ_hywUwvHoL8,4043
8
- udemy_userAPI/udemy.py,sha256=VcGcy0nDWTLj5pv51tUtAXmKAGaTauVCnquSdmZWE4Q,1584
9
- udemy_userAPI-0.1.5.dist-info/LICENSE,sha256=l4jdKYt8gSdDFOGr09vCKnMn_Im55XIcQKqTDEtFfNs,1095
10
- udemy_userAPI-0.1.5.dist-info/METADATA,sha256=VesuLzA3JqGbJs4yBcfz4lqo0aH_k2x07t-EFdBC03A,1368
11
- udemy_userAPI-0.1.5.dist-info/WHEEL,sha256=cVxcB9AmuTcXqmwrtPhNK88dr7IR_b6qagTj0UvIEbY,91
12
- udemy_userAPI-0.1.5.dist-info/top_level.txt,sha256=ijTINaSDRKhdahY_X7dmSRFTxBIwQErWv9ATCG55mog,14
13
- udemy_userAPI-0.1.5.dist-info/RECORD,,