udemy-userAPI 0.1.6__py3-none-any.whl → 0.1.7__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- udemy_userAPI/__version__.py +1 -1
- udemy_userAPI/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
|