udemy-userAPI 0.2.8__py3-none-any.whl → 0.2.9__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 +1 -2
- udemy_userAPI/bultins.py +32 -29
- udemy_userAPI/mpd_analyzer/mpd_parser.py +149 -282
- {udemy_userAPI-0.2.8.dist-info → udemy_userAPI-0.2.9.dist-info}/METADATA +2 -2
- {udemy_userAPI-0.2.8.dist-info → udemy_userAPI-0.2.9.dist-info}/RECORD +9 -9
- {udemy_userAPI-0.2.8.dist-info → udemy_userAPI-0.2.9.dist-info}/LICENSE +0 -0
- {udemy_userAPI-0.2.8.dist-info → udemy_userAPI-0.2.9.dist-info}/WHEEL +0 -0
- {udemy_userAPI-0.2.8.dist-info → udemy_userAPI-0.2.9.dist-info}/top_level.txt +0 -0
udemy_userAPI/__version__.py
CHANGED
udemy_userAPI/api.py
CHANGED
@@ -168,7 +168,7 @@ def get_mpd_file(mpd_url):
|
|
168
168
|
data = []
|
169
169
|
# Exibe o código de status
|
170
170
|
if response.status_code == 200:
|
171
|
-
return response.
|
171
|
+
return response.text
|
172
172
|
else:
|
173
173
|
UnhandledExceptions(f"erro ao obter dados de aulas!! {response.status_code}")
|
174
174
|
except requests.ConnectionError as e:
|
@@ -186,7 +186,6 @@ def get_mpd_file(mpd_url):
|
|
186
186
|
def parser_chapers(results):
|
187
187
|
"""
|
188
188
|
:param results:
|
189
|
-
:param tip: chaper,videos
|
190
189
|
:return:
|
191
190
|
"""
|
192
191
|
if not results:
|
udemy_userAPI/bultins.py
CHANGED
@@ -9,32 +9,35 @@ from .mpd_analyzer import MPDParser
|
|
9
9
|
|
10
10
|
class DRM:
|
11
11
|
def __init__(self, license_token: str, get_media_sources: list):
|
12
|
-
self.
|
12
|
+
self.__mpd_file_path = None
|
13
13
|
self.__token = license_token
|
14
14
|
self.__dash_url = organize_streams(streams=get_media_sources).get('dash', {})
|
15
|
-
if not license_token or
|
15
|
+
if not license_token or get_media_sources:
|
16
16
|
return
|
17
17
|
|
18
18
|
def get_key_for_lesson(self):
|
19
19
|
"""get keys for lesson"""
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
20
|
+
try:
|
21
|
+
if self.__dash_url:
|
22
|
+
self.__mpd_file_path = get_mpd_file(mpd_url=self.__dash_url[0].get('src'))
|
23
|
+
parser = MPDParser(mpd_content=self.__mpd_file_path)
|
24
|
+
resolutions = get_highest_resolution(parser.get_all_video_resolutions())
|
25
|
+
parser.set_selected_resolution(resolution=resolutions)
|
26
|
+
init_url = parser.get_selected_video_init_url()
|
27
|
+
if init_url:
|
28
|
+
pssh = get_pssh(init_url=init_url)
|
29
|
+
if pssh:
|
30
|
+
keys = extract(pssh=pssh, license_token=self.__token)
|
31
|
+
if keys:
|
32
|
+
return keys
|
33
|
+
else:
|
34
|
+
return None
|
35
|
+
else:
|
36
|
+
return None
|
34
37
|
else:
|
35
38
|
return None
|
36
|
-
|
37
|
-
|
39
|
+
except Exception as e:
|
40
|
+
raise Exception(f"Não foi possível obter as chaves!\n{e}")
|
38
41
|
|
39
42
|
|
40
43
|
class Files:
|
@@ -44,8 +47,7 @@ class Files:
|
|
44
47
|
|
45
48
|
@property
|
46
49
|
def get_download_url(self) -> dict[str, Any | None] | list[dict[str, Any | None]]:
|
47
|
-
"""
|
48
|
-
da = {}
|
50
|
+
"""Obter url de ‘download’ de um arquivo quando disponivel(geralemnete para arquivos esta opção é valida"""
|
49
51
|
download_urls = []
|
50
52
|
for files in self.__data:
|
51
53
|
lecture_id = files.get('lecture_id', None)
|
@@ -68,6 +70,7 @@ class Files:
|
|
68
70
|
headers=HEADERS_USER)
|
69
71
|
if resp.status_code == 200:
|
70
72
|
da = json.loads(resp.text)
|
73
|
+
# para cdaa dict de um fle colocar seu titulo:
|
71
74
|
dt_file = {'title-file': title,
|
72
75
|
'lecture_title': lecture_title,
|
73
76
|
'lecture_id': lecture_id,
|
@@ -84,12 +87,12 @@ class Lecture:
|
|
84
87
|
self.__course_id = course_id
|
85
88
|
self.__data = data
|
86
89
|
self.__additional_files = additional_files
|
87
|
-
self.__asset = self.__data.get("asset")
|
90
|
+
self.__asset = self.__data.get("asset", {})
|
88
91
|
|
89
92
|
@property
|
90
93
|
def get_lecture_id(self) -> int:
|
91
94
|
"""Obtém o ID da lecture"""
|
92
|
-
return self.__data.get('id')
|
95
|
+
return self.__data.get('id', 0)
|
93
96
|
|
94
97
|
@property
|
95
98
|
def get_description(self) -> str:
|
@@ -138,10 +141,12 @@ class Lecture:
|
|
138
141
|
def course_is_drmed(self) -> DRM:
|
139
142
|
"""verifica se a aula possui DRM se sim retorna as keys da aula...
|
140
143
|
retorna 'kid:key' or None"""
|
141
|
-
|
144
|
+
try:
|
142
145
|
d = DRM(license_token=self.get_media_license_token,
|
143
146
|
get_media_sources=self.get_media_sources)
|
144
147
|
return d
|
148
|
+
except Exception as e:
|
149
|
+
DeprecationWarning(e)
|
145
150
|
|
146
151
|
@property
|
147
152
|
def get_download_urls(self) -> list:
|
@@ -271,16 +276,15 @@ class Course:
|
|
271
276
|
def get_additional_files(self) -> list[Any]:
|
272
277
|
"""Retorna a lista de arquivos adcionais de um curso."""
|
273
278
|
supplementary_assets = []
|
274
|
-
files_downloader = []
|
275
279
|
for item in self.__additional_files_data.get('results', []):
|
276
280
|
# Check if the item is a lecture with supplementary assets
|
277
281
|
if item.get('_class') == 'lecture':
|
278
|
-
|
282
|
+
id_l = item.get('id', {})
|
279
283
|
title = item.get('title', {})
|
280
284
|
assets = item.get('supplementary_assets', [])
|
281
285
|
for asset in assets:
|
282
286
|
supplementary_assets.append({
|
283
|
-
'lecture_id':
|
287
|
+
'lecture_id': id_l,
|
284
288
|
'lecture_title': title,
|
285
289
|
'asset': asset
|
286
290
|
})
|
@@ -292,16 +296,15 @@ class Course:
|
|
292
296
|
def __load_assets(self):
|
293
297
|
"""Retorna a lista de arquivos adcionais de um curso."""
|
294
298
|
supplementary_assets = []
|
295
|
-
files_downloader = []
|
296
299
|
for item in self.__additional_files_data.get('results', []):
|
297
300
|
# Check if the item is a lecture with supplementary assets
|
298
301
|
if item.get('_class') == 'lecture':
|
299
|
-
|
302
|
+
id_l = item.get('id')
|
300
303
|
title = item.get('title')
|
301
304
|
assets = item.get('supplementary_assets', [])
|
302
305
|
for asset in assets:
|
303
306
|
supplementary_assets.append({
|
304
|
-
'lecture_id':
|
307
|
+
'lecture_id': id_l,
|
305
308
|
'lecture_title': title,
|
306
309
|
'asset': asset
|
307
310
|
})
|
@@ -1,304 +1,191 @@
|
|
1
1
|
import re
|
2
|
-
import xml.etree.ElementTree as
|
2
|
+
import xml.etree.ElementTree as Et
|
3
|
+
|
4
|
+
|
5
|
+
def calculate_segment_url2(media_template, segment_number, segment_time, rep_id):
|
6
|
+
"""
|
7
|
+
Calcula a URL de um segmento específico, substituindo variáveis no template.
|
8
|
+
"""
|
9
|
+
url = media_template.replace('$Number$', str(segment_number))
|
10
|
+
url = url.replace('$RepresentationID$', rep_id)
|
11
|
+
if '$Time$' in url:
|
12
|
+
url = url.replace('$Time$', str(segment_time))
|
13
|
+
return url
|
14
|
+
|
15
|
+
|
16
|
+
def build_url2(template, rep_id):
|
17
|
+
"""
|
18
|
+
Constrói a URL substituindo variáveis no template com base nos atributos.
|
19
|
+
"""
|
20
|
+
if '$RepresentationID$' in template:
|
21
|
+
template = template.replace('$RepresentationID$', rep_id)
|
22
|
+
return template
|
23
|
+
|
24
|
+
|
25
|
+
def parse_duration(duration_str):
|
26
|
+
"""
|
27
|
+
Converte uma duração em formato ISO 8601 (ex: "PT163.633S") para segundos (float).
|
28
|
+
"""
|
29
|
+
match = re.match(r'PT(?:(\d+)H)?(?:(\d+)M)?(?:(\d+(?:\.\d+)?)S)?', duration_str)
|
30
|
+
if match:
|
31
|
+
hours = int(match.group(1)) if match.group(1) else 0
|
32
|
+
minutes = int(match.group(2)) if match.group(2) else 0
|
33
|
+
seconds = float(match.group(3)) if match.group(3) else 0.0
|
34
|
+
return hours * 3600 + minutes * 60 + seconds
|
35
|
+
return 0.0
|
3
36
|
|
4
37
|
|
5
38
|
class MPDParser:
|
6
39
|
"""
|
7
40
|
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
|
41
|
+
com foco em arquivos VOD (Video on Demand). Atualmente, não oferece suporte para transmissões ao vivo.
|
9
42
|
"""
|
10
43
|
|
11
|
-
def __init__(self,
|
44
|
+
def __init__(self, mpd_content: str):
|
12
45
|
"""
|
13
|
-
Inicializa o parser para um arquivo MPD
|
46
|
+
Inicializa o parser para um arquivo MPD.
|
14
47
|
|
15
48
|
Args:
|
16
|
-
|
17
|
-
headers (dict, opcional): Headers HTTP adicionais para requisição, caso necessário.
|
49
|
+
mpd_content (str): Caminho do arquivo MPD ou conteúdo bruto.
|
18
50
|
"""
|
19
|
-
self.
|
20
|
-
self.
|
21
|
-
self.
|
22
|
-
self.
|
23
|
-
self.
|
24
|
-
|
25
|
-
|
26
|
-
self.
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
def __parse_mpd(self):
|
51
|
+
self._mpd_content = mpd_content
|
52
|
+
self._video_representations = {}
|
53
|
+
self._audio_representations = {}
|
54
|
+
self._content_protection = {}
|
55
|
+
self._selected_resolution = None
|
56
|
+
|
57
|
+
# Tenta fazer o parsing com diferentes métodos
|
58
|
+
if not self.__parse_mpd_v2():
|
59
|
+
self.__parse_mpd_v1()
|
60
|
+
|
61
|
+
def __parse_mpd_v1(self):
|
32
62
|
"""
|
33
|
-
|
34
|
-
extrai informações sobre segmentos de vídeo e áudio, além de proteção de conteúdo.
|
63
|
+
Parsing básico do MPD (versão 1).
|
35
64
|
"""
|
36
|
-
|
37
|
-
|
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)
|
65
|
+
content = self._mpd_content
|
66
|
+
root = Et.fromstring(content)
|
51
67
|
ns = {'dash': 'urn:mpeg:dash:schema:mpd:2011'}
|
52
68
|
|
53
|
-
# Processa cada AdaptationSet para extração de representações de áudio e vídeo
|
54
69
|
for adaptation_set in root.findall('.//dash:AdaptationSet', ns):
|
55
70
|
mime_type = adaptation_set.attrib.get('mimeType', '')
|
56
|
-
|
57
|
-
|
58
|
-
|
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):
|
71
|
+
self.__parse_adaptation_set(adaptation_set, mime_type, ns)
|
72
|
+
|
73
|
+
def __parse_mpd_v2(self):
|
117
74
|
"""
|
118
|
-
|
119
|
-
extrai informações sobre segmentos de vídeo e áudio, além de proteção de conteúdo.
|
75
|
+
Parsing avançado do MPD (versão 2).
|
120
76
|
"""
|
121
|
-
|
122
|
-
|
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)
|
77
|
+
content = self._mpd_content
|
78
|
+
root = Et.fromstring(content)
|
137
79
|
ns = {'dash': 'urn:mpeg:dash:schema:mpd:2011'}
|
138
80
|
|
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
81
|
for adaptation_set in root.findall('.//dash:AdaptationSet', ns):
|
143
82
|
mime_type = adaptation_set.attrib.get('mimeType', '')
|
83
|
+
self.__parse_adaptation_set(adaptation_set, mime_type, ns)
|
84
|
+
return True
|
144
85
|
|
145
|
-
|
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):
|
86
|
+
def __parse_adaptation_set(self, adaptation_set, mime_type, ns):
|
206
87
|
"""
|
207
|
-
|
88
|
+
Analisa um AdaptationSet para representações de vídeo ou áudio.
|
89
|
+
|
90
|
+
Args:
|
91
|
+
adaptation_set (ET.Element): Elemento do AdaptationSet.
|
92
|
+
mime_type (str): Tipo MIME (vídeo ou áudio).
|
93
|
+
ns (dict): Namespace para parsing do XML.
|
208
94
|
"""
|
209
|
-
|
210
|
-
|
211
|
-
|
212
|
-
|
213
|
-
|
214
|
-
return hours * 3600 + minutes * 60 + seconds
|
215
|
-
return 0.0
|
95
|
+
# Extrai informações de proteção de conteúdo
|
96
|
+
for content_protection in adaptation_set.findall('dash:ContentProtection', ns):
|
97
|
+
scheme_id_uri = content_protection.attrib.get('schemeIdUri', '')
|
98
|
+
value = content_protection.attrib.get('value', '')
|
99
|
+
self._content_protection[scheme_id_uri] = value
|
216
100
|
|
217
|
-
|
218
|
-
|
101
|
+
# Processa representações dentro do AdaptationSet
|
102
|
+
for representation in adaptation_set.findall('dash:Representation', ns):
|
103
|
+
self.__process_representation(representation, mime_type, ns)
|
104
|
+
|
105
|
+
def __process_representation(self, representation, mime_type, ns):
|
219
106
|
"""
|
220
|
-
|
107
|
+
Processa uma representação de mídia (vídeo ou áudio).
|
221
108
|
|
222
109
|
Args:
|
223
|
-
|
110
|
+
representation (ET.Element): Elemento da Representação.
|
111
|
+
mime_type (str): Tipo MIME da mídia.
|
112
|
+
ns (dict): Namespace para parsing do XML.
|
113
|
+
"""
|
114
|
+
rep_id = representation.attrib.get('id')
|
115
|
+
width = int(representation.attrib.get('width', 0))
|
116
|
+
height = int(representation.attrib.get('height', 0))
|
117
|
+
resolution = (width, height) if width and height else None
|
118
|
+
bandwidth = int(representation.attrib.get('bandwidth', 0))
|
119
|
+
|
120
|
+
# Extrai informações do SegmentTemplate
|
121
|
+
segment_template = representation.find('dash:SegmentTemplate', ns)
|
122
|
+
if segment_template:
|
123
|
+
init_url = self.__build_url(segment_template.get('initialization'), rep_id, bandwidth)
|
124
|
+
segments = self.__generate_segments(segment_template, ns, rep_id, bandwidth)
|
125
|
+
|
126
|
+
representation_info = {
|
127
|
+
'id': rep_id,
|
128
|
+
'resolution': resolution,
|
129
|
+
'bandwidth': bandwidth,
|
130
|
+
'init_url': init_url,
|
131
|
+
'segments': segments,
|
132
|
+
}
|
133
|
+
if 'video' in mime_type:
|
134
|
+
self._video_representations[resolution] = representation_info
|
135
|
+
elif 'audio' in mime_type:
|
136
|
+
self._audio_representations[bandwidth] = representation_info
|
137
|
+
|
138
|
+
def __generate_segments(self, segment_template, ns, rep_id, bandwidth):
|
139
|
+
"""
|
140
|
+
Gera a lista de URLs de segmentos com base no SegmentTemplate.
|
141
|
+
|
142
|
+
Args:
|
143
|
+
segment_template (ET.Element): Elemento do SegmentTemplate.
|
144
|
+
ns (dict): Namespace para parsing do XML.
|
224
145
|
rep_id (str): ID da representação.
|
225
146
|
bandwidth (int): Largura de banda da representação.
|
226
147
|
|
227
148
|
Returns:
|
228
|
-
|
229
|
-
"""
|
230
|
-
|
231
|
-
|
232
|
-
|
233
|
-
|
234
|
-
|
235
|
-
|
236
|
-
|
237
|
-
|
238
|
-
|
239
|
-
|
240
|
-
|
241
|
-
|
242
|
-
|
149
|
+
list: URLs dos segmentos.
|
150
|
+
"""
|
151
|
+
segments = []
|
152
|
+
media_template = segment_template.get('media')
|
153
|
+
segment_timeline = segment_template.find('dash:SegmentTimeline', ns)
|
154
|
+
|
155
|
+
if segment_timeline:
|
156
|
+
segment_number = int(segment_template.get('startNumber', 1))
|
157
|
+
for segment in segment_timeline.findall('dash:S', ns):
|
158
|
+
t = int(segment.get('t', 0))
|
159
|
+
d = int(segment.get('d'))
|
160
|
+
r = int(segment.get('r', 0))
|
161
|
+
for i in range(r + 1):
|
162
|
+
segment_time = t + i * d
|
163
|
+
segments.append(self.__build_url(media_template, rep_id, bandwidth, segment_time, segment_number))
|
164
|
+
segment_number += 1
|
165
|
+
return segments
|
243
166
|
|
244
167
|
@staticmethod
|
245
|
-
def
|
168
|
+
def __build_url(template, rep_id, bandwidth, segment_time=None, segment_number=None):
|
246
169
|
"""
|
247
|
-
Constrói
|
170
|
+
Constrói uma URL substituindo placeholders.
|
248
171
|
|
249
172
|
Args:
|
250
|
-
|
251
|
-
segment_number (int): Número do segmento.
|
252
|
-
segment_time (int): Timestamp do segmento.
|
173
|
+
template (str): Template de URL.
|
253
174
|
rep_id (str): ID da representação.
|
254
|
-
bandwidth (int): Largura de banda
|
175
|
+
bandwidth (int): Largura de banda.
|
176
|
+
segment_time (int, opcional): Timestamp do segmento.
|
177
|
+
segment_number (int, opcional): Número do segmento.
|
255
178
|
|
256
179
|
Returns:
|
257
|
-
str: URL
|
180
|
+
str: URL formatada.
|
258
181
|
"""
|
259
|
-
url =
|
260
|
-
|
261
|
-
if '$Time$' in url:
|
182
|
+
url = template.replace('$RepresentationID$', rep_id).replace('$Bandwidth$', str(bandwidth))
|
183
|
+
if segment_time is not None:
|
262
184
|
url = url.replace('$Time$', str(segment_time))
|
185
|
+
if segment_number is not None:
|
186
|
+
url = url.replace('$Number$', str(segment_number))
|
263
187
|
return url
|
264
188
|
|
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
189
|
def set_selected_resolution(self, resolution: tuple):
|
303
190
|
"""
|
304
191
|
Define a resolução selecionada para a recuperação de segmentos de vídeo.
|
@@ -309,23 +196,12 @@ class MPDParser:
|
|
309
196
|
Raises:
|
310
197
|
Exception: Se a resolução não estiver disponível no manifesto.
|
311
198
|
"""
|
312
|
-
if resolution in self.
|
313
|
-
self.
|
199
|
+
if resolution in self._video_representations:
|
200
|
+
self._selected_resolution = resolution
|
314
201
|
else:
|
315
202
|
raise Exception(
|
316
|
-
f'A resolução {resolution} não está disponível!\n\n
|
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()')
|
203
|
+
f'A resolução {resolution} não está disponível!\n\n'
|
204
|
+
f'\t=> Resoluções disponíveis no arquivo: {self.get_all_video_resolutions()}')
|
329
205
|
|
330
206
|
def get_selected_video_init_url(self):
|
331
207
|
"""
|
@@ -334,8 +210,8 @@ class MPDParser:
|
|
334
210
|
Returns:
|
335
211
|
str: URL de inicialização do vídeo, ou None se não houver resolução selecionada.
|
336
212
|
"""
|
337
|
-
if self.
|
338
|
-
return self.
|
213
|
+
if self._selected_resolution:
|
214
|
+
return self._video_representations[self._selected_resolution].get('init_url')
|
339
215
|
return None
|
340
216
|
|
341
217
|
def get_all_video_resolutions(self):
|
@@ -343,15 +219,6 @@ class MPDParser:
|
|
343
219
|
Retorna uma lista de todas as resoluções de vídeo disponíveis.
|
344
220
|
|
345
221
|
Returns:
|
346
|
-
list:
|
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.
|
222
|
+
list: lista de tuplas com resoluções de vídeo (largura, altura).
|
356
223
|
"""
|
357
|
-
return
|
224
|
+
return list(self._video_representations.keys())
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.1
|
2
2
|
Name: udemy_userAPI
|
3
|
-
Version: 0.2.
|
3
|
+
Version: 0.2.9
|
4
4
|
Summary: Obtenha detalhes de cursos que o usuário esteja inscrito da plataforma Udemy,usando o EndPoint de usuário o mesmo que o navegador utiliza para acessar e redenrizar os cursos.
|
5
5
|
Author: PauloCesar-dev404
|
6
6
|
Author-email: paulocesar0073dev404@gmail.com
|
@@ -19,7 +19,7 @@ Requires-Dist: pywidevine
|
|
19
19
|
# udemy-userAPI
|
20
20
|
|
21
21
|
|
22
|
-

|
23
23
|

|
24
24
|
[](https://apoia.se/paulocesar-dev404)
|
25
25
|
[](https://github.com/PauloCesar-dev404/udemy-userAPI/blob/main/docs/iniciando.md)
|
@@ -11,19 +11,19 @@ m3u8_analyzer/__init__.py,sha256=v7CiVqsCq2YH347C-QR1kHPJtXFFdru8qole3E9adCY,217
|
|
11
11
|
m3u8_analyzer/__version__.py,sha256=YP3yT87ZKrU3eARUUdQ_pg4xAXLGfBXjH4ZgEoZSq1I,25
|
12
12
|
m3u8_analyzer/exeptions.py,sha256=fK6bU3YxNSbfsPmCp4yudUvmwy_g6dj2KwIkH0dW4LI,3672
|
13
13
|
udemy_userAPI/__init__.py,sha256=BPle89xE_CMTKKe_Lw6jioYLgpH-q_Lpho2S-n1PIUA,206
|
14
|
-
udemy_userAPI/__version__.py,sha256=
|
15
|
-
udemy_userAPI/api.py,sha256=
|
14
|
+
udemy_userAPI/__version__.py,sha256=ddg6Jc4hdL3Ucjndnb_uXJAIG6W_GhKfswf9LQ5rImo,405
|
15
|
+
udemy_userAPI/api.py,sha256=M061zpgrwigktMeb9JINmblqsojkSdTwdzeRwimlGlw,18783
|
16
16
|
udemy_userAPI/authenticate.py,sha256=84frcOMfOzfCBfXDtoTa3POqkwWwuqgJ6h4ROF0TVAM,13850
|
17
|
-
udemy_userAPI/bultins.py,sha256=
|
17
|
+
udemy_userAPI/bultins.py,sha256=Z3Jw-e4HUE-5LGfOtAkoyHFGfdyOSXpAinsaNPa41Bc,12597
|
18
18
|
udemy_userAPI/exeptions.py,sha256=nuZoAt4i-ctrW8zx9LZtejrngpFXDHOVE5cEXM4RtrY,508
|
19
19
|
udemy_userAPI/sections.py,sha256=zPyDhvTIQCL0nbf7OJZG28Kax_iooILQ_hywUwvHoL8,4043
|
20
20
|
udemy_userAPI/udemy.py,sha256=KMWMmid0zC9pUCULjLSAOK0P7yvCOtdShXpT6Q-fhro,2127
|
21
21
|
udemy_userAPI/.cache/.udemy_userAPI,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
22
22
|
udemy_userAPI/mpd_analyzer/__init__.py,sha256=i3JVWyvcFLaj5kPmx8c1PgjsLht7OUIQQClD4yqYbo8,102
|
23
23
|
udemy_userAPI/mpd_analyzer/bin.wvd,sha256=1rAJdCc120hQlX9qe5KUS628eY2ZHYxQSmyhGNefSzo,2956
|
24
|
-
udemy_userAPI/mpd_analyzer/mpd_parser.py,sha256=
|
25
|
-
udemy_userAPI-0.2.
|
26
|
-
udemy_userAPI-0.2.
|
27
|
-
udemy_userAPI-0.2.
|
28
|
-
udemy_userAPI-0.2.
|
29
|
-
udemy_userAPI-0.2.
|
24
|
+
udemy_userAPI/mpd_analyzer/mpd_parser.py,sha256=PgUkHc5x8FTuXFCuYkWPZr9TaO_nsKalb02EFYl_zeA,8926
|
25
|
+
udemy_userAPI-0.2.9.dist-info/LICENSE,sha256=l4jdKYt8gSdDFOGr09vCKnMn_Im55XIcQKqTDEtFfNs,1095
|
26
|
+
udemy_userAPI-0.2.9.dist-info/METADATA,sha256=nFDTysEImMWRp_2-d9VreIuk7TAkWMkAa6p35KHjJy4,1417
|
27
|
+
udemy_userAPI-0.2.9.dist-info/WHEEL,sha256=PZUExdf71Ui_so67QXpySuHtCi3-J3wvF4ORK6k_S8U,91
|
28
|
+
udemy_userAPI-0.2.9.dist-info/top_level.txt,sha256=ijTINaSDRKhdahY_X7dmSRFTxBIwQErWv9ATCG55mog,14
|
29
|
+
udemy_userAPI-0.2.9.dist-info/RECORD,,
|
File without changes
|
File without changes
|
File without changes
|