udemy-userAPI 0.1.6__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.
- udemy_userAPI/__version__.py +1 -1
- udemy_userAPI/api.py +212 -4
- udemy_userAPI/authenticate.py +84 -71
- udemy_userAPI/bultins.py +98 -18
- udemy_userAPI/mpd_analyzer/__init__.py +3 -0
- udemy_userAPI/mpd_analyzer/mpd_parser.py +357 -0
- {udemy_userAPI-0.1.6.dist-info → udemy_userAPI-0.1.7.dist-info}/METADATA +1 -1
- udemy_userAPI-0.1.7.dist-info/RECORD +15 -0
- {udemy_userAPI-0.1.6.dist-info → udemy_userAPI-0.1.7.dist-info}/WHEEL +1 -1
- udemy_userAPI-0.1.6.dist-info/RECORD +0 -13
- {udemy_userAPI-0.1.6.dist-info → udemy_userAPI-0.1.7.dist-info}/LICENSE +0 -0
- {udemy_userAPI-0.1.6.dist-info → udemy_userAPI-0.1.7.dist-info}/top_level.txt +0 -0
udemy_userAPI/__version__.py
CHANGED
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
|
"""
|
@@ -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
|
|
udemy_userAPI/authenticate.py
CHANGED
@@ -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
|
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
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
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
|
-
|
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
|
-
|
156
|
-
|
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
|
-
|
162
|
-
|
163
|
-
|
164
|
-
|
165
|
-
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
|
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)
|
udemy_userAPI/bultins.py
CHANGED
@@ -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 .
|
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
|
-
|
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/
|
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
|
-
|
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
|
@@ -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)
|
90
|
-
"""verifica se possui DRM
|
91
|
-
|
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:
|
@@ -110,6 +156,13 @@ class Lecture:
|
|
110
156
|
d = assets_infor(course_id=self.__course_id, id_lecture=self.get_lecture_id, assets_id=self.__asset.get("id"))
|
111
157
|
return d
|
112
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
|
+
|
113
166
|
|
114
167
|
class Course:
|
115
168
|
"""receb um dict com os dados do curso"""
|
@@ -119,6 +172,7 @@ class Course:
|
|
119
172
|
self.__data = self.__parser_chapers
|
120
173
|
self.__course_id = course_id
|
121
174
|
self.__results = results
|
175
|
+
self.__additional_files_data = get_add_files(course_id)
|
122
176
|
self.__information = self.__load_infor_course()
|
123
177
|
|
124
178
|
def __load_infor_course(self) -> dict:
|
@@ -179,34 +233,40 @@ class Course:
|
|
179
233
|
def get_lectures(self) -> list:
|
180
234
|
"""Obtém uma lista com todas as aulas."""
|
181
235
|
videos = []
|
236
|
+
section_order = 1 # Iniciar a numeração das seções (capítulos)
|
237
|
+
|
182
238
|
for chapter in self.__data.values():
|
183
|
-
for index, video in enumerate(chapter.get('videos_in_chapter', [])):
|
184
|
-
section = chapter.get('title_chapter')
|
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
|
185
241
|
title = video.get('video_title')
|
186
242
|
id_lecture = video.get('lecture_id')
|
187
243
|
id_asset = video.get('asset_id')
|
188
|
-
section_order = index + 1 # Adiciona 1 para começar a numeração em 1
|
189
244
|
dt = {
|
190
245
|
'section': section,
|
191
246
|
'title': title,
|
192
247
|
'lecture_id': id_lecture,
|
193
248
|
'asset_id': id_asset,
|
194
|
-
'section_order': section_order
|
249
|
+
'section_order': section_order,
|
250
|
+
'lecture_order': index + 1 # Numeração da seção e vídeo
|
195
251
|
}
|
196
252
|
videos.append(dt)
|
253
|
+
section_order += 1 # Incrementar o número da seção após processar os vídeos do capítulo
|
197
254
|
return videos
|
198
255
|
|
199
256
|
def get_details_lecture(self, lecture_id: int) -> Lecture:
|
200
257
|
"""obter detalhes de uma aula específica, irá retornar o objeto Lecture"""
|
201
258
|
links = get_links(course_id=self.__course_id, id_lecture=lecture_id)
|
202
|
-
|
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)
|
203
262
|
return lecture
|
204
263
|
|
205
264
|
@property
|
206
|
-
def get_additional_files(self) ->
|
265
|
+
def get_additional_files(self) -> list[Any]:
|
207
266
|
"""Retorna a lista de arquivos adcionais de um curso."""
|
208
267
|
supplementary_assets = []
|
209
|
-
|
268
|
+
files_downloader = []
|
269
|
+
for item in self.__additional_files_data.get('results', []):
|
210
270
|
# Check if the item is a lecture with supplementary assets
|
211
271
|
if item.get('_class') == 'lecture':
|
212
272
|
id = item.get('id')
|
@@ -219,5 +279,25 @@ class Course:
|
|
219
279
|
'asset': asset
|
220
280
|
})
|
221
281
|
files = extract_files(supplementary_assets)
|
282
|
+
# print(f'DEBUG files:\n{files}\n\n')
|
222
283
|
files_objt = Files(files=files, id_course=self.__course_id).get_download_url
|
223
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,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.
|
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
|
@@ -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,13 +0,0 @@
|
|
1
|
-
udemy_userAPI/__init__.py,sha256=BPle89xE_CMTKKe_Lw6jioYLgpH-q_Lpho2S-n1PIUA,206
|
2
|
-
udemy_userAPI/__version__.py,sha256=mAEr0zlmSEAjfDqxvgeci1fw5fg0S1-Kiprgz_SpQlE,404
|
3
|
-
udemy_userAPI/api.py,sha256=ttjrMsEa2WTn1TdR1YyfzD_aFnd3ePAQClCptoV8uL4,10728
|
4
|
-
udemy_userAPI/authenticate.py,sha256=RKq4CTKj1tufjyFQ-DTvuUhllcdPvNzJ4qLtEKUkXLA,7112
|
5
|
-
udemy_userAPI/bultins.py,sha256=1VMem8VNie85kSrwqiKsQ7iNYdbFHDtyRKAIZyTS_KE,8154
|
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-0.1.6.dist-info/LICENSE,sha256=l4jdKYt8gSdDFOGr09vCKnMn_Im55XIcQKqTDEtFfNs,1095
|
10
|
-
udemy_userAPI-0.1.6.dist-info/METADATA,sha256=2et7KgSg1hTE85oPFci5uDSroMqbG0mMjs7JmvSQ4eM,1368
|
11
|
-
udemy_userAPI-0.1.6.dist-info/WHEEL,sha256=GV9aMThwP_4oNCtvEC2ec3qUYutgWeAzklro_0m4WJQ,91
|
12
|
-
udemy_userAPI-0.1.6.dist-info/top_level.txt,sha256=ijTINaSDRKhdahY_X7dmSRFTxBIwQErWv9ATCG55mog,14
|
13
|
-
udemy_userAPI-0.1.6.dist-info/RECORD,,
|
File without changes
|
File without changes
|