udemy-userAPI 0.2.8__tar.gz → 0.2.9__tar.gz
Sign up to get free protection for your applications and to get access to all the features.
- {udemy_userapi-0.2.8/udemy_userAPI.egg-info → udemy_userapi-0.2.9}/PKG-INFO +2 -2
- {udemy_userapi-0.2.8 → udemy_userapi-0.2.9}/README.md +2 -2
- {udemy_userapi-0.2.8 → udemy_userapi-0.2.9}/README_PYPI.md +1 -1
- {udemy_userapi-0.2.8 → udemy_userapi-0.2.9}/udemy_userAPI/__version__.py +1 -1
- {udemy_userapi-0.2.8 → udemy_userapi-0.2.9}/udemy_userAPI/api.py +1 -2
- {udemy_userapi-0.2.8 → udemy_userapi-0.2.9}/udemy_userAPI/bultins.py +32 -29
- udemy_userapi-0.2.9/udemy_userAPI/mpd_analyzer/mpd_parser.py +224 -0
- {udemy_userapi-0.2.8 → udemy_userapi-0.2.9/udemy_userAPI.egg-info}/PKG-INFO +2 -2
- udemy_userapi-0.2.8/udemy_userAPI/mpd_analyzer/mpd_parser.py +0 -357
- {udemy_userapi-0.2.8 → udemy_userapi-0.2.9}/LICENSE +0 -0
- {udemy_userapi-0.2.8 → udemy_userapi-0.2.9}/MANIFEST.in +0 -0
- {udemy_userapi-0.2.8 → udemy_userapi-0.2.9}/setup.cfg +0 -0
- {udemy_userapi-0.2.8 → udemy_userapi-0.2.9}/setup.py +0 -0
- {udemy_userapi-0.2.8 → udemy_userapi-0.2.9}/udemy_userAPI/.cache/.udemy_userAPI +0 -0
- {udemy_userapi-0.2.8 → udemy_userapi-0.2.9}/udemy_userAPI/__init__.py +0 -0
- {udemy_userapi-0.2.8 → udemy_userapi-0.2.9}/udemy_userAPI/authenticate.py +0 -0
- {udemy_userapi-0.2.8 → udemy_userapi-0.2.9}/udemy_userAPI/exeptions.py +0 -0
- {udemy_userapi-0.2.8 → udemy_userapi-0.2.9}/udemy_userAPI/mpd_analyzer/__init__.py +0 -0
- {udemy_userapi-0.2.8 → udemy_userapi-0.2.9}/udemy_userAPI/mpd_analyzer/bin.wvd +0 -0
- {udemy_userapi-0.2.8 → udemy_userapi-0.2.9}/udemy_userAPI/sections.py +0 -0
- {udemy_userapi-0.2.8 → udemy_userapi-0.2.9}/udemy_userAPI/udemy.py +0 -0
- {udemy_userapi-0.2.8 → udemy_userapi-0.2.9}/udemy_userAPI.egg-info/SOURCES.txt +0 -0
- {udemy_userapi-0.2.8 → udemy_userapi-0.2.9}/udemy_userAPI.egg-info/dependency_links.txt +0 -0
- {udemy_userapi-0.2.8 → udemy_userapi-0.2.9}/udemy_userAPI.egg-info/not-zip-safe +0 -0
- {udemy_userapi-0.2.8 → udemy_userapi-0.2.9}/udemy_userAPI.egg-info/requires.txt +0 -0
- {udemy_userapi-0.2.8 → udemy_userapi-0.2.9}/udemy_userAPI.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.1
|
2
2
|
Name: udemy_userAPI
|
3
|
-
Version: 0.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)
|
@@ -3,9 +3,9 @@
|
|
3
3
|
|
4
4
|
|
5
5
|
|
6
|
-

|
7
7
|

|
8
|
-
[](https://
|
8
|
+
[](https://paulocesar-dev404.github.io/me-apoiando-online/)
|
9
9
|
[](https://github.com/PauloCesar-dev404/udemy-userAPI/blob/main/docs/iniciando.md)
|
10
10
|
|
11
11
|
<i>Obtenha detalhes completos de cursos cujo o usuário esteja inscrito ,da plataforma [Udemy](https://www.udemy.com/) com esta biblioteca</i>
|
@@ -1,7 +1,7 @@
|
|
1
1
|
# udemy-userAPI
|
2
2
|
|
3
3
|
|
4
|
-

|
5
5
|

|
6
6
|
[](https://apoia.se/paulocesar-dev404)
|
7
7
|
[](https://github.com/PauloCesar-dev404/udemy-userAPI/blob/main/docs/iniciando.md)
|
@@ -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:
|
@@ -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
|
})
|
@@ -0,0 +1,224 @@
|
|
1
|
+
import re
|
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
|
36
|
+
|
37
|
+
|
38
|
+
class MPDParser:
|
39
|
+
"""
|
40
|
+
Classe para analisar e extrair informações de manifestos MPD (Media Presentation Description),
|
41
|
+
com foco em arquivos VOD (Video on Demand). Atualmente, não oferece suporte para transmissões ao vivo.
|
42
|
+
"""
|
43
|
+
|
44
|
+
def __init__(self, mpd_content: str):
|
45
|
+
"""
|
46
|
+
Inicializa o parser para um arquivo MPD.
|
47
|
+
|
48
|
+
Args:
|
49
|
+
mpd_content (str): Caminho do arquivo MPD ou conteúdo bruto.
|
50
|
+
"""
|
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):
|
62
|
+
"""
|
63
|
+
Parsing básico do MPD (versão 1).
|
64
|
+
"""
|
65
|
+
content = self._mpd_content
|
66
|
+
root = Et.fromstring(content)
|
67
|
+
ns = {'dash': 'urn:mpeg:dash:schema:mpd:2011'}
|
68
|
+
|
69
|
+
for adaptation_set in root.findall('.//dash:AdaptationSet', ns):
|
70
|
+
mime_type = adaptation_set.attrib.get('mimeType', '')
|
71
|
+
self.__parse_adaptation_set(adaptation_set, mime_type, ns)
|
72
|
+
|
73
|
+
def __parse_mpd_v2(self):
|
74
|
+
"""
|
75
|
+
Parsing avançado do MPD (versão 2).
|
76
|
+
"""
|
77
|
+
content = self._mpd_content
|
78
|
+
root = Et.fromstring(content)
|
79
|
+
ns = {'dash': 'urn:mpeg:dash:schema:mpd:2011'}
|
80
|
+
|
81
|
+
for adaptation_set in root.findall('.//dash:AdaptationSet', ns):
|
82
|
+
mime_type = adaptation_set.attrib.get('mimeType', '')
|
83
|
+
self.__parse_adaptation_set(adaptation_set, mime_type, ns)
|
84
|
+
return True
|
85
|
+
|
86
|
+
def __parse_adaptation_set(self, adaptation_set, mime_type, ns):
|
87
|
+
"""
|
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.
|
94
|
+
"""
|
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
|
100
|
+
|
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):
|
106
|
+
"""
|
107
|
+
Processa uma representação de mídia (vídeo ou áudio).
|
108
|
+
|
109
|
+
Args:
|
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.
|
145
|
+
rep_id (str): ID da representação.
|
146
|
+
bandwidth (int): Largura de banda da representação.
|
147
|
+
|
148
|
+
Returns:
|
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
|
166
|
+
|
167
|
+
@staticmethod
|
168
|
+
def __build_url(template, rep_id, bandwidth, segment_time=None, segment_number=None):
|
169
|
+
"""
|
170
|
+
Constrói uma URL substituindo placeholders.
|
171
|
+
|
172
|
+
Args:
|
173
|
+
template (str): Template de URL.
|
174
|
+
rep_id (str): ID da representação.
|
175
|
+
bandwidth (int): Largura de banda.
|
176
|
+
segment_time (int, opcional): Timestamp do segmento.
|
177
|
+
segment_number (int, opcional): Número do segmento.
|
178
|
+
|
179
|
+
Returns:
|
180
|
+
str: URL formatada.
|
181
|
+
"""
|
182
|
+
url = template.replace('$RepresentationID$', rep_id).replace('$Bandwidth$', str(bandwidth))
|
183
|
+
if segment_time is not None:
|
184
|
+
url = url.replace('$Time$', str(segment_time))
|
185
|
+
if segment_number is not None:
|
186
|
+
url = url.replace('$Number$', str(segment_number))
|
187
|
+
return url
|
188
|
+
|
189
|
+
def set_selected_resolution(self, resolution: tuple):
|
190
|
+
"""
|
191
|
+
Define a resolução selecionada para a recuperação de segmentos de vídeo.
|
192
|
+
|
193
|
+
Args:
|
194
|
+
resolution (tuple): Resolução desejada (largura, altura).
|
195
|
+
|
196
|
+
Raises:
|
197
|
+
Exception: Se a resolução não estiver disponível no manifesto.
|
198
|
+
"""
|
199
|
+
if resolution in self._video_representations:
|
200
|
+
self._selected_resolution = resolution
|
201
|
+
else:
|
202
|
+
raise Exception(
|
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()}')
|
205
|
+
|
206
|
+
def get_selected_video_init_url(self):
|
207
|
+
"""
|
208
|
+
Retorna o URL de inicialização para a resolução de vídeo selecionada.
|
209
|
+
|
210
|
+
Returns:
|
211
|
+
str: URL de inicialização do vídeo, ou None se não houver resolução selecionada.
|
212
|
+
"""
|
213
|
+
if self._selected_resolution:
|
214
|
+
return self._video_representations[self._selected_resolution].get('init_url')
|
215
|
+
return None
|
216
|
+
|
217
|
+
def get_all_video_resolutions(self):
|
218
|
+
"""
|
219
|
+
Retorna uma lista de todas as resoluções de vídeo disponíveis.
|
220
|
+
|
221
|
+
Returns:
|
222
|
+
list: lista de tuplas com resoluções de vídeo (largura, altura).
|
223
|
+
"""
|
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)
|
@@ -1,357 +0,0 @@
|
|
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()}
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|