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.
Files changed (26) hide show
  1. {udemy_userapi-0.2.8/udemy_userAPI.egg-info → udemy_userapi-0.2.9}/PKG-INFO +2 -2
  2. {udemy_userapi-0.2.8 → udemy_userapi-0.2.9}/README.md +2 -2
  3. {udemy_userapi-0.2.8 → udemy_userapi-0.2.9}/README_PYPI.md +1 -1
  4. {udemy_userapi-0.2.8 → udemy_userapi-0.2.9}/udemy_userAPI/__version__.py +1 -1
  5. {udemy_userapi-0.2.8 → udemy_userapi-0.2.9}/udemy_userAPI/api.py +1 -2
  6. {udemy_userapi-0.2.8 → udemy_userapi-0.2.9}/udemy_userAPI/bultins.py +32 -29
  7. udemy_userapi-0.2.9/udemy_userAPI/mpd_analyzer/mpd_parser.py +224 -0
  8. {udemy_userapi-0.2.8 → udemy_userapi-0.2.9/udemy_userAPI.egg-info}/PKG-INFO +2 -2
  9. udemy_userapi-0.2.8/udemy_userAPI/mpd_analyzer/mpd_parser.py +0 -357
  10. {udemy_userapi-0.2.8 → udemy_userapi-0.2.9}/LICENSE +0 -0
  11. {udemy_userapi-0.2.8 → udemy_userapi-0.2.9}/MANIFEST.in +0 -0
  12. {udemy_userapi-0.2.8 → udemy_userapi-0.2.9}/setup.cfg +0 -0
  13. {udemy_userapi-0.2.8 → udemy_userapi-0.2.9}/setup.py +0 -0
  14. {udemy_userapi-0.2.8 → udemy_userapi-0.2.9}/udemy_userAPI/.cache/.udemy_userAPI +0 -0
  15. {udemy_userapi-0.2.8 → udemy_userapi-0.2.9}/udemy_userAPI/__init__.py +0 -0
  16. {udemy_userapi-0.2.8 → udemy_userapi-0.2.9}/udemy_userAPI/authenticate.py +0 -0
  17. {udemy_userapi-0.2.8 → udemy_userapi-0.2.9}/udemy_userAPI/exeptions.py +0 -0
  18. {udemy_userapi-0.2.8 → udemy_userapi-0.2.9}/udemy_userAPI/mpd_analyzer/__init__.py +0 -0
  19. {udemy_userapi-0.2.8 → udemy_userapi-0.2.9}/udemy_userAPI/mpd_analyzer/bin.wvd +0 -0
  20. {udemy_userapi-0.2.8 → udemy_userapi-0.2.9}/udemy_userAPI/sections.py +0 -0
  21. {udemy_userapi-0.2.8 → udemy_userapi-0.2.9}/udemy_userAPI/udemy.py +0 -0
  22. {udemy_userapi-0.2.8 → udemy_userapi-0.2.9}/udemy_userAPI.egg-info/SOURCES.txt +0 -0
  23. {udemy_userapi-0.2.8 → udemy_userapi-0.2.9}/udemy_userAPI.egg-info/dependency_links.txt +0 -0
  24. {udemy_userapi-0.2.8 → udemy_userapi-0.2.9}/udemy_userAPI.egg-info/not-zip-safe +0 -0
  25. {udemy_userapi-0.2.8 → udemy_userapi-0.2.9}/udemy_userAPI.egg-info/requires.txt +0 -0
  26. {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.8
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
- ![Versão](https://img.shields.io/badge/version-0.2.8-orange)
22
+ ![Versão](https://img.shields.io/badge/version-0.2.9-orange)
23
23
  ![Licença](https://img.shields.io/badge/license-MIT-orange)
24
24
  [![Sponsor](https://img.shields.io/badge/💲Donate-yellow)](https://apoia.se/paulocesar-dev404)
25
25
  [![Sponsor](https://img.shields.io/badge/Documentation-green)](https://github.com/PauloCesar-dev404/udemy-userAPI/blob/main/docs/iniciando.md)
@@ -3,9 +3,9 @@
3
3
 
4
4
 
5
5
 
6
- ![Versão](https://img.shields.io/badge/version-0.2.8-orange)
6
+ ![Versão](https://img.shields.io/badge/version-0.2.9-orange)
7
7
  ![Licença](https://img.shields.io/badge/license-MIT-orange)
8
- [![Sponsor](https://img.shields.io/badge/💲Donate-yellow)](https://apoia.se/paulocesar-dev404)
8
+ [![Sponsor](https://img.shields.io/badge/💲Donate-yellow)](https://paulocesar-dev404.github.io/me-apoiando-online/)
9
9
  [![Sponsor](https://img.shields.io/badge/Documentation-green)](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
- ![Versão](https://img.shields.io/badge/version-0.2.8-orange)
4
+ ![Versão](https://img.shields.io/badge/version-0.2.9-orange)
5
5
  ![Licença](https://img.shields.io/badge/license-MIT-orange)
6
6
  [![Sponsor](https://img.shields.io/badge/💲Donate-yellow)](https://apoia.se/paulocesar-dev404)
7
7
  [![Sponsor](https://img.shields.io/badge/Documentation-green)](https://github.com/PauloCesar-dev404/udemy-userAPI/blob/main/docs/iniciando.md)
@@ -1,4 +1,4 @@
1
- __version__ = '0.2.8'
1
+ __version__ = '0.2.9'
2
2
  __lib_name__ = 'udemy_userAPI' # local name
3
3
  __repo_name__ = 'udemy-userAPI'
4
4
  __autor__ = 'PauloCesar-dev404'
@@ -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.content
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.__mpd_content = None
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 not get_media_sources or not self.__dash_url:
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
- if self.__dash_url:
21
- self.__mpd_content = get_mpd_file(mpd_url=self.__dash_url[0].get('src'))
22
- parser = MPDParser(mpd_file_path=self.__mpd_content, is_file=True, headers=HEADERS_USER)
23
- resolutions = get_highest_resolution(parser.get_all_video_resolutions())
24
- parser.set_selected_resolution(resolution=resolutions)
25
- init_url = parser.get_selected_video_init_url()
26
- if init_url:
27
- pssh = get_pssh(init_url=init_url)
28
- if pssh:
29
- keys = extract(pssh=pssh, license_token=self.__token)
30
- if keys:
31
- return keys
32
- else:
33
- return None
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
- else:
37
- return None
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
- """obter url de download de um arquivo quando disponivel(geralemnete para arquivos esta opção é valida"""
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
- if self.__asset.get('course_is_drmed', {}):
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
- id = item.get('id', {})
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': 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
- id = item.get('id')
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': 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.8
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
- ![Versão](https://img.shields.io/badge/version-0.2.8-orange)
22
+ ![Versão](https://img.shields.io/badge/version-0.2.9-orange)
23
23
  ![Licença](https://img.shields.io/badge/license-MIT-orange)
24
24
  [![Sponsor](https://img.shields.io/badge/💲Donate-yellow)](https://apoia.se/paulocesar-dev404)
25
25
  [![Sponsor](https://img.shields.io/badge/Documentation-green)](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