download_info.py 8.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212
  1. import xml.dom.minidom as minidom
  2. from hashlib import md5
  3. from typing import TYPE_CHECKING, List, Optional
  4. from yandex_music import YandexMusicModel
  5. from yandex_music.utils import model
  6. if TYPE_CHECKING:
  7. from xml.dom.minicompat import NodeList
  8. from yandex_music import ClientType, JSONType
  9. SIGN_SALT = 'XGRlBW9FXlekgbPrRHuSiA'
  10. @model
  11. class DownloadInfo(YandexMusicModel):
  12. """Класс, представляющий информацию о вариантах загрузки трека.
  13. Attributes:
  14. codec (:obj:`str`): Кодек аудиофайла.
  15. bitrate_in_kbps (:obj:`int`): Битрейт аудиофайла в кбит/с.
  16. gain (:obj:`bool`): Усиление TODO.
  17. preview (:obj:`bool`): Предварительный просмотр TODO.
  18. download_info_url (:obj:`str`): Ссылка на XML документ содержащий данные для загрузки трека.
  19. direct (:obj:`bool`): Прямая ли ссылка.
  20. client (:obj:`yandex_music.Client`, optional): Клиент Yandex Music.
  21. """
  22. codec: str
  23. bitrate_in_kbps: int
  24. gain: bool
  25. preview: bool
  26. download_info_url: str
  27. direct: bool
  28. client: Optional['ClientType'] = None
  29. def __post_init__(self) -> None:
  30. self.direct_link = None
  31. self._id_attrs = (self.codec, self.bitrate_in_kbps, self.gain, self.preview, self.download_info_url)
  32. @staticmethod
  33. def _get_text_node_data(elements: 'NodeList') -> Optional[str]:
  34. """:obj:`str`: Получение текстовой информации из узлов XML элемента."""
  35. for element in elements:
  36. nodes = element.childNodes
  37. for node in nodes:
  38. if node.nodeType == node.TEXT_NODE:
  39. return node.data
  40. return None
  41. def __build_direct_link(self, xml: bytes) -> str:
  42. doc = minidom.parseString(xml) # noqa: S318
  43. host = self._get_text_node_data(doc.getElementsByTagName('host'))
  44. path = self._get_text_node_data(doc.getElementsByTagName('path'))
  45. ts = self._get_text_node_data(doc.getElementsByTagName('ts'))
  46. s = self._get_text_node_data(doc.getElementsByTagName('s'))
  47. sign = md5((SIGN_SALT + path[1::] + s).encode('UTF-8')).hexdigest() # noqa: S324
  48. return f'https://{host}/get-mp3/{sign}/{ts}{path}'
  49. def get_direct_link(self) -> str:
  50. """Получение прямой ссылки на загрузку из XML ответа.
  51. Метод доступен только одну минуту с момента получения информации о загрузке, иначе 410 ошибка!
  52. Returns:
  53. :obj:`str`: Прямая ссылка на загрузку трека.
  54. """
  55. assert self.valid_client(self.client)
  56. result = self.client.request.retrieve(self.download_info_url)
  57. self.direct_link = self.__build_direct_link(result)
  58. return self.direct_link
  59. async def get_direct_link_async(self) -> str:
  60. """Получение прямой ссылки на загрузку из XML ответа.
  61. Метод доступен только одну минуту с момента получения информации о загрузке, иначе 410 ошибка!
  62. Returns:
  63. :obj:`str`: Прямая ссылка на загрузку трека.
  64. """
  65. assert self.valid_async_client(self.client)
  66. result = await self.client.request.retrieve(self.download_info_url)
  67. self.direct_link = self.__build_direct_link(result)
  68. return self.direct_link
  69. def download(self, filename: str) -> None:
  70. """Загрузка трека.
  71. Args:
  72. filename (:obj:`str`): Путь и(или) название файла вместе с расширением.
  73. """
  74. if self.direct_link is None:
  75. self.direct_link = self.get_direct_link()
  76. assert self.valid_client(self.client)
  77. self.client.request.download(self.direct_link, filename)
  78. async def download_async(self, filename: str) -> None:
  79. """Загрузка трека.
  80. Args:
  81. filename (:obj:`str`): Путь и(или) название файла вместе с расширением.
  82. """
  83. if self.direct_link is None:
  84. self.direct_link = await self.get_direct_link_async()
  85. assert self.valid_async_client(self.client)
  86. await self.client.request.download(self.direct_link, filename)
  87. def download_bytes(self) -> bytes:
  88. """Загрузка трека и возврат в виде байтов.
  89. Returns:
  90. :obj:`bytes`: Трек в виде байтов.
  91. """
  92. if self.direct_link is None:
  93. self.direct_link = self.get_direct_link()
  94. assert self.valid_client(self.client)
  95. return self.client.request.retrieve(self.direct_link)
  96. async def download_bytes_async(self) -> bytes:
  97. """Загрузка трека и возврат в виде байтов.
  98. Returns:
  99. :obj:`bytes`: Трек в виде байтов.
  100. """
  101. if self.direct_link is None:
  102. self.direct_link = await self.get_direct_link_async()
  103. assert self.valid_async_client(self.client)
  104. return await self.client.request.retrieve(self.direct_link)
  105. @classmethod
  106. def de_list(cls, data: 'JSONType', client: 'ClientType', get_direct_links: bool = False) -> List['DownloadInfo']:
  107. """Десериализация списка объектов.
  108. Args:
  109. data (:obj:`list`): Список словарей с полями и значениями десериализуемого объекта.
  110. get_direct_links (:obj:`bool`): Получать ли сразу прямые ссылки на загрузку.
  111. client (:obj:`yandex_music.Client`, optional): Клиент Yandex Music.
  112. Returns:
  113. :obj:`list` из :obj:`yandex_music.DownloadInfo`: Варианты загрузки треков.
  114. """
  115. if not cls.is_array_model_data(data):
  116. return []
  117. download_infos: List[DownloadInfo] = []
  118. for raw_download_info in data:
  119. download_info = cls.de_json(raw_download_info, client)
  120. if download_info:
  121. download_infos.append(download_info)
  122. if get_direct_links:
  123. for info in download_infos:
  124. info.get_direct_link()
  125. return download_infos
  126. @classmethod
  127. async def de_list_async(
  128. cls, data: 'JSONType', client: 'ClientType', get_direct_links: bool = False
  129. ) -> List['DownloadInfo']:
  130. """Десериализация списка объектов.
  131. Args:
  132. data (:obj:`list`): Список словарей с полями и значениями десериализуемого объекта.
  133. get_direct_links (:obj:`bool`): Получать ли сразу прямые ссылки на загрузку.
  134. client (:obj:`yandex_music.Client`, optional): Клиент Yandex Music.
  135. Returns:
  136. :obj:`list` из :obj:`yandex_music.DownloadInfo`: Варианты загрузки треков.
  137. """
  138. if not cls.is_array_model_data(data):
  139. return []
  140. download_infos: List[DownloadInfo] = []
  141. for raw_download_info in data:
  142. download_info = cls.de_json(raw_download_info, client)
  143. if download_info:
  144. download_infos.append(download_info)
  145. if get_direct_links:
  146. for info in download_infos:
  147. # FIXME (MarshalX): gather or something?
  148. await info.get_direct_link_async()
  149. return download_infos
  150. # camelCase псевдонимы
  151. #: Псевдоним для :attr:`get_direct_link`
  152. getDirectLink = get_direct_link
  153. #: Псевдоним для :attr:`get_direct_link_async`
  154. getDirectLinkAsync = get_direct_link_async
  155. #: Псевдоним для :attr:`download_async`
  156. downloadAsync = download_async
  157. #: Псевдоним для :attr:`download_bytes`
  158. downloadBytes = download_bytes
  159. #: Псевдоним для :attr:`download_bytes_async`
  160. downloadBytesAsync = download_bytes_async