udemy-userAPI 0.1.6__tar.gz → 0.1.8__tar.gz

Sign up to get free protection for your applications and to get access to all the features.
Files changed (23) hide show
  1. {udemy_userapi-0.1.6/udemy_userAPI.egg-info → udemy_userapi-0.1.8}/PKG-INFO +3 -2
  2. {udemy_userapi-0.1.6 → udemy_userapi-0.1.8}/README.md +1 -1
  3. {udemy_userapi-0.1.6 → udemy_userapi-0.1.8}/README_PYPI.md +1 -1
  4. {udemy_userapi-0.1.6 → udemy_userapi-0.1.8}/setup.py +2 -1
  5. {udemy_userapi-0.1.6 → udemy_userapi-0.1.8}/udemy_userAPI/__version__.py +2 -2
  6. {udemy_userapi-0.1.6 → udemy_userapi-0.1.8}/udemy_userAPI/api.py +212 -4
  7. {udemy_userapi-0.1.6 → udemy_userapi-0.1.8}/udemy_userAPI/authenticate.py +84 -71
  8. {udemy_userapi-0.1.6 → udemy_userapi-0.1.8}/udemy_userAPI/bultins.py +98 -19
  9. udemy_userapi-0.1.8/udemy_userAPI/mpd_analyzer/__init__.py +3 -0
  10. udemy_userapi-0.1.8/udemy_userAPI/mpd_analyzer/mpd_parser.py +357 -0
  11. {udemy_userapi-0.1.6 → udemy_userapi-0.1.8/udemy_userAPI.egg-info}/PKG-INFO +3 -2
  12. {udemy_userapi-0.1.6 → udemy_userapi-0.1.8}/udemy_userAPI.egg-info/SOURCES.txt +3 -1
  13. {udemy_userapi-0.1.6 → udemy_userapi-0.1.8}/udemy_userAPI.egg-info/requires.txt +1 -0
  14. {udemy_userapi-0.1.6 → udemy_userapi-0.1.8}/LICENSE +0 -0
  15. {udemy_userapi-0.1.6 → udemy_userapi-0.1.8}/MANIFEST.in +0 -0
  16. {udemy_userapi-0.1.6 → udemy_userapi-0.1.8}/setup.cfg +0 -0
  17. {udemy_userapi-0.1.6 → udemy_userapi-0.1.8}/udemy_userAPI/__init__.py +0 -0
  18. {udemy_userapi-0.1.6 → udemy_userapi-0.1.8}/udemy_userAPI/exeptions.py +0 -0
  19. {udemy_userapi-0.1.6 → udemy_userapi-0.1.8}/udemy_userAPI/sections.py +0 -0
  20. {udemy_userapi-0.1.6 → udemy_userapi-0.1.8}/udemy_userAPI/udemy.py +0 -0
  21. {udemy_userapi-0.1.6 → udemy_userapi-0.1.8}/udemy_userAPI.egg-info/dependency_links.txt +0 -0
  22. {udemy_userapi-0.1.6 → udemy_userapi-0.1.8}/udemy_userAPI.egg-info/not-zip-safe +0 -0
  23. {udemy_userapi-0.1.6 → udemy_userapi-0.1.8}/udemy_userAPI.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: udemy_userAPI
3
- Version: 0.1.6
3
+ Version: 0.1.8
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
@@ -14,11 +14,12 @@ Description-Content-Type: text/markdown
14
14
  License-File: LICENSE
15
15
  Requires-Dist: requests
16
16
  Requires-Dist: cloudscraper
17
+ Requires-Dist: pywidevine
17
18
 
18
19
  # udemy-userAPI
19
20
 
20
21
 
21
- ![Versão](https://img.shields.io/badge/version-0.1.6-orange)
22
+ ![Versão](https://img.shields.io/badge/version-0.1.8-orange)
22
23
  ![Licença](https://img.shields.io/badge/license-MIT-orange)
23
24
  [![Sponsor](https://img.shields.io/badge/💲Donate-yellow)](https://apoia.se/paulocesar-dev404)
24
25
  [![Sponsor](https://img.shields.io/badge/Documentation-green)](https://github.com/PauloCesar-dev404/udemy-userAPI/wiki)
@@ -2,7 +2,7 @@
2
2
  <img src="assets/udemy_userAPI-logo.png" alt="udemy_userAPI-logo" width="200"/>
3
3
 
4
4
 
5
- ![Versão](https://img.shields.io/badge/version-0.1.6-orange)
5
+ ![Versão](https://img.shields.io/badge/version-0.1.8-orange)
6
6
  ![Licença](https://img.shields.io/badge/license-MIT-orange)
7
7
  [![Sponsor](https://img.shields.io/badge/💲Donate-yellow)](https://apoia.se/paulocesar-dev404)
8
8
  [![Sponsor](https://img.shields.io/badge/Documentation-green)](https://github.com/PauloCesar-dev404/udemy-userAPI/wiki)
@@ -1,7 +1,7 @@
1
1
  # udemy-userAPI
2
2
 
3
3
 
4
- ![Versão](https://img.shields.io/badge/version-0.1.6-orange)
4
+ ![Versão](https://img.shields.io/badge/version-0.1.8-orange)
5
5
  ![Licença](https://img.shields.io/badge/license-MIT-orange)
6
6
  [![Sponsor](https://img.shields.io/badge/💲Donate-yellow)](https://apoia.se/paulocesar-dev404)
7
7
  [![Sponsor](https://img.shields.io/badge/Documentation-green)](https://github.com/PauloCesar-dev404/udemy-userAPI/wiki)
@@ -18,7 +18,8 @@ setup(
18
18
  keywords=["udemy", "udemy python", "pyudemy","udemy_userAPI","udemy api"],
19
19
  install_requires=[
20
20
  'requests',
21
- 'cloudscraper'
21
+ 'cloudscraper',
22
+ 'pywidevine'
22
23
  ],
23
24
  packages=find_packages(),
24
25
  zip_safe=False,
@@ -1,5 +1,5 @@
1
- __version__ = '0.1.6'
2
- __lib_name__ = 'udemy_userAPI' # local name
1
+ __version__ = '0.1.8'
2
+ __lib_name__ = 'udemy_userAPI' # local name
3
3
  __repo_name__ = 'udemy-userAPI'
4
4
  __autor__ = 'PauloCesar-dev404'
5
5
  __repo__ = f'https://github.com/PauloCesar-dev404/{__repo_name__}'
@@ -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, '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(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
  """
@@ -76,10 +218,48 @@ def parser_chapers(results):
76
218
  'lecture_id': dictionary.get('id'),
77
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
 
@@ -42,7 +42,7 @@ class UdemyAuth:
42
42
  cookies_dict = {cookie.name: cookie.value for cookie in cookies}
43
43
  cookies_str = "; ".join([f"{key}={value}" for key, value in cookies_dict.items()])
44
44
  return cookies_str
45
- except FileNotFoundError:
45
+ except Exception as e:
46
46
  return False
47
47
 
48
48
  log = verif_config()
@@ -89,81 +89,94 @@ class UdemyAuth:
89
89
 
90
90
  def login(self, email: str, password: str):
91
91
  """efetuar login na udemy"""
92
- # Inicializa uma sessão usando cloudscraper para contornar a proteção Cloudflare
93
- s = cloudscraper.create_scraper()
94
-
95
- # Faz uma requisição GET à página de inscrição para obter o token CSRF
96
- r = s.get(
97
- "https://www.udemy.com/join/signup-popup/",
98
- headers={"User-Agent": "okhttp/4.9.2 UdemyAndroid 8.9.2(499) (phone)"},
99
- )
100
-
101
- # Extrai o token CSRF dos cookies
102
- csrf_token = r.cookies["csrftoken"]
103
-
104
- # Prepara os dados para o login
105
- data = {
106
- "csrfmiddlewaretoken": csrf_token,
107
- "locale": "pt_BR",
108
- "email": email,
109
- "password": password,
110
- }
111
-
112
- # Atualiza os cookies e cabeçalhos da sessão
113
- s.cookies.update(r.cookies)
114
- s.headers.update(
115
- {
116
- "User-Agent": "okhttp/4.9.2 UdemyAndroid 8.9.2(499) (phone)",
117
- "Accept": "application/json, text/plain, */*",
118
- "Accept-Language": "en-GB,en;q=0.5",
119
- "Referer": "https://www.udemy.com/join/login-popup/?locale=en_US&response_type=html&next=https%3A%2F"
120
- "%2Fwww.udemy.com%2F",
121
- "Origin": "https://www.udemy.com",
122
- "DNT": "1",
123
- "Connection": "keep-alive",
124
- "Sec-Fetch-Dest": "empty",
125
- "Sec-Fetch-Mode": "cors",
126
- "Sec-Fetch-Site": "same-origin",
127
- "Pragma": "no-cache",
128
- "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,
129
111
  }
130
- )
131
-
132
- # Tenta fazer login com as credenciais fornecidas
133
- r = s.post(
134
- "https://www.udemy.com/join/login-popup/?response_type=json",
135
- data=data,
136
- allow_redirects=False,
137
- )
138
-
139
- # Verifica a resposta para determinar se o login foi bem-sucedido
140
- if "returnUrl" in r.text:
141
- self.__make_cookies(r.cookies.get("client_id"), r.cookies.get("access_token"), csrf_token)
142
- self.__save_cookies(s.cookies)
143
- else:
144
- login_error = r.json().get("error", {}).get("data", {}).get("formErrors", [])[0]
145
- if login_error[0] == "Y":
146
- raise LoginException("Você excedeu o número máximo de solicitações por hora.")
147
- elif login_error[0] == "T":
148
- raise LoginException("Email ou senha incorretos")
149
- else:
150
- raise UnhandledExceptions(login_error)
151
112
 
152
- 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)
153
156
 
154
157
  def __save_cookies(self, cookies):
155
- with open(fr'{self.__file_path}', 'wb') as f:
156
- 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)
157
163
 
158
164
  @property
159
165
  def load_cookies(self) -> str:
160
166
  """carrega cookies e retorna-os em uma string formatada"""
161
- file = os.path.join(fr'{self.__file_path}')
162
- if os.path.exists(file):
163
- with open(fr'{self.__file_path}', 'rb') as f:
164
- cookies = pickle.load(f)
165
- cookies_dict = {cookie.name: cookie.value for cookie in cookies}
166
- cookies_str = "; ".join([f"{key}={value}" for key, value in cookies_dict.items()])
167
- return cookies_str
168
- else:
169
- return 'None'
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)
179
+
180
+ def remove_cookies(self):
181
+ if os.path.exists(self.__file_path):
182
+ os.remove(self.__file_path)
@@ -1,7 +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, assets_infor
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
5
31
 
6
32
 
7
33
  class Files:
@@ -10,30 +36,44 @@ class Files:
10
36
  self.__id_course = id_course
11
37
 
12
38
  @property
13
- def get_download_url(self) -> dict[list]:
39
+ def get_download_url(self) -> dict[str, Any | None] | list[dict[str, Any | None]]:
14
40
  """obter url de download de um arquivo quando disponivel(geralemnete para arquivos esta opção é valida"""
15
41
  da = {}
16
- download_urls = ''
42
+ download_urls = []
17
43
  for files in self.__data:
18
44
  lecture_id = files.get('lecture_id', None)
19
45
  asset_id = files.get('asset_id', None)
20
46
  title = files.get("title", None)
21
- 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:
22
56
  resp = requests.get(
23
- 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",
24
59
  headers=HEADERS_USER)
25
60
  if resp.status_code == 200:
26
61
  da = json.loads(resp.text)
27
- 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)
28
67
  return download_urls
29
68
 
30
69
 
31
70
  class Lecture:
32
71
  """CRIAR objetos aula(lecture) do curso e extrair os dados.."""
33
72
 
34
- def __init__(self, data: dict, course_id: int):
73
+ def __init__(self, data: dict, course_id: int, additional_files):
35
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
@@ -85,10 +125,15 @@ class Lecture:
85
125
  """obtem token de acesso a aula se tiver.."""
86
126
  return self.__asset.get('media_license_token')
87
127
 
88
- @property
89
- def course_is_drmed(self) -> bool:
90
- """verifica se possui DRM.."""
91
- return self.__asset.get('course_is_drmed')
128
+ def course_is_drmed(self) -> DRM:
129
+ """verifica se a aula possui DRM se sim retorna as keys da aula...
130
+ retorna 'kid:key' or None"""
131
+ if self.__asset.get('course_is_drmed'):
132
+ d = DRM(license_token=self.get_media_license_token,
133
+ get_media_sources=self.get_media_sources)
134
+ return d
135
+ else:
136
+ return self.__asset.get('course_is_drmed')
92
137
 
93
138
  @property
94
139
  def get_download_urls(self) -> list:
@@ -110,6 +155,13 @@ class Lecture:
110
155
  d = assets_infor(course_id=self.__course_id, id_lecture=self.get_lecture_id, assets_id=self.__asset.get("id"))
111
156
  return d
112
157
 
158
+ @property
159
+ def get_resources(self):
160
+ """obtem os recurso adconais reaioanddos a aula"""
161
+ files_add = get_files_aule(lecture_id_filter=self.get_lecture_id, data=self.__additional_files)
162
+ f = Files(files=files_add, id_course=self.__course_id).get_download_url
163
+ return f
164
+
113
165
 
114
166
  class Course:
115
167
  """receb um dict com os dados do curso"""
@@ -119,6 +171,7 @@ class Course:
119
171
  self.__data = self.__parser_chapers
120
172
  self.__course_id = course_id
121
173
  self.__results = results
174
+ self.__additional_files_data = get_add_files(course_id)
122
175
  self.__information = self.__load_infor_course()
123
176
 
124
177
  def __load_infor_course(self) -> dict:
@@ -179,34 +232,40 @@ class Course:
179
232
  def get_lectures(self) -> list:
180
233
  """Obtém uma lista com todas as aulas."""
181
234
  videos = []
235
+ section_order = 1 # Iniciar a numeração das seções (capítulos)
236
+
182
237
  for chapter in self.__data.values():
183
- for index, video in enumerate(chapter.get('videos_in_chapter', [])): # Corrigido o loop
184
- section = chapter.get('title_chapter')
238
+ for index, video in enumerate(chapter.get('videos_in_chapter', [])):
239
+ section = f"{section_order} {chapter.get('title_chapter')}" # Adicionar numeração da seção
185
240
  title = video.get('video_title')
186
241
  id_lecture = video.get('lecture_id')
187
242
  id_asset = video.get('asset_id')
188
- section_order = index + 1 # Adiciona 1 para começar a numeração em 1
189
243
  dt = {
190
244
  'section': section,
191
245
  'title': title,
192
246
  'lecture_id': id_lecture,
193
247
  'asset_id': id_asset,
194
- 'section_order': section_order
248
+ 'section_order': section_order,
249
+ 'lecture_order': index + 1 # Numeração da seção e vídeo
195
250
  }
196
251
  videos.append(dt)
252
+ section_order += 1 # Incrementar o número da seção após processar os vídeos do capítulo
197
253
  return videos
198
254
 
199
255
  def get_details_lecture(self, lecture_id: int) -> Lecture:
200
256
  """obter detalhes de uma aula específica, irá retornar o objeto Lecture"""
201
257
  links = get_links(course_id=self.__course_id, id_lecture=lecture_id)
202
- lecture = Lecture(data=links, course_id=self.__course_id)
258
+ additional_files = self.__load_assets()
259
+ # print(f'DEBUG files passados a lecture:\n{additional_files}\n\n')
260
+ lecture = Lecture(data=links, course_id=self.__course_id, additional_files=additional_files)
203
261
  return lecture
204
262
 
205
263
  @property
206
- def get_additional_files(self) -> dict[list]:
264
+ def get_additional_files(self) -> list[Any]:
207
265
  """Retorna a lista de arquivos adcionais de um curso."""
208
266
  supplementary_assets = []
209
- for item in self.__results.get('results', []):
267
+ files_downloader = []
268
+ for item in self.__additional_files_data.get('results', []):
210
269
  # Check if the item is a lecture with supplementary assets
211
270
  if item.get('_class') == 'lecture':
212
271
  id = item.get('id')
@@ -219,5 +278,25 @@ class Course:
219
278
  'asset': asset
220
279
  })
221
280
  files = extract_files(supplementary_assets)
281
+ # print(f'DEBUG files:\n{files}\n\n')
222
282
  files_objt = Files(files=files, id_course=self.__course_id).get_download_url
223
283
  return files_objt
284
+
285
+ def __load_assets(self):
286
+ """Retorna a lista de arquivos adcionais de um curso."""
287
+ supplementary_assets = []
288
+ files_downloader = []
289
+ for item in self.__additional_files_data.get('results', []):
290
+ # Check if the item is a lecture with supplementary assets
291
+ if item.get('_class') == 'lecture':
292
+ id = item.get('id')
293
+ title = item.get('title')
294
+ assets = item.get('supplementary_assets', [])
295
+ for asset in assets:
296
+ supplementary_assets.append({
297
+ 'lecture_id': id,
298
+ 'lecture_title': title,
299
+ 'asset': asset
300
+ })
301
+ files = extract_files(supplementary_assets)
302
+ 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()}
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: udemy_userAPI
3
- Version: 0.1.6
3
+ Version: 0.1.8
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
@@ -14,11 +14,12 @@ Description-Content-Type: text/markdown
14
14
  License-File: LICENSE
15
15
  Requires-Dist: requests
16
16
  Requires-Dist: cloudscraper
17
+ Requires-Dist: pywidevine
17
18
 
18
19
  # udemy-userAPI
19
20
 
20
21
 
21
- ![Versão](https://img.shields.io/badge/version-0.1.6-orange)
22
+ ![Versão](https://img.shields.io/badge/version-0.1.8-orange)
22
23
  ![Licença](https://img.shields.io/badge/license-MIT-orange)
23
24
  [![Sponsor](https://img.shields.io/badge/💲Donate-yellow)](https://apoia.se/paulocesar-dev404)
24
25
  [![Sponsor](https://img.shields.io/badge/Documentation-green)](https://github.com/PauloCesar-dev404/udemy-userAPI/wiki)
@@ -16,4 +16,6 @@ udemy_userAPI.egg-info/SOURCES.txt
16
16
  udemy_userAPI.egg-info/dependency_links.txt
17
17
  udemy_userAPI.egg-info/not-zip-safe
18
18
  udemy_userAPI.egg-info/requires.txt
19
- udemy_userAPI.egg-info/top_level.txt
19
+ udemy_userAPI.egg-info/top_level.txt
20
+ udemy_userAPI/mpd_analyzer/__init__.py
21
+ udemy_userAPI/mpd_analyzer/mpd_parser.py
@@ -1,2 +1,3 @@
1
1
  requests
2
2
  cloudscraper
3
+ pywidevine
File without changes
File without changes
File without changes