From 922424feb5117de833742a01d2020f25517b646c Mon Sep 17 00:00:00 2001 From: Marshal Date: Thu, 23 May 2019 12:10:14 +0300 Subject: [PATCH] Added ability to download tracks --- yandex_music/client.py | 10 ++++---- yandex_music/download_info.py | 37 +++++++++++++++++++++++++++++- yandex_music/likes/tracks_likes.py | 6 +++++ yandex_music/track.py | 15 ++++++++++++ yandex_music/track_short.py | 11 +++++++++ yandex_music/utils/request.py | 27 ++++++++++++++-------- yandex_music/utils/response.py | 8 ++++++- 7 files changed, 99 insertions(+), 15 deletions(-) diff --git a/yandex_music/client.py b/yandex_music/client.py index 9c3d261..581339f 100644 --- a/yandex_music/client.py +++ b/yandex_music/client.py @@ -109,12 +109,12 @@ class Client(YandexMusicObject): return Feed.de_json(result, self) - def tracks_download_info(self, track_id: str or int, timeout=None, *args, **kwargs): + def tracks_download_info(self, track_id: str or int, get_direct_links=False, timeout=None, *args, **kwargs): url = f'{self.base_url}/tracks/{track_id}/download-info' result = self._request.get(url, timeout=timeout, *args, **kwargs) - return DownloadInfo.de_list(result, self) + return DownloadInfo.de_list(result, self, get_direct_links) def play_audio(self, track_id: str or int, @@ -287,8 +287,10 @@ class Client(YandexMusicObject): return de_list.get(object_type)(result, self) - def users_likes_tracks(self, user_id: int or str = None, if_modified_since_revision=0, timeout=None, *args, **kwargs): - return self._get_likes('track', user_id, {'if-modified-since-revision': if_modified_since_revision}, timeout, *args, **kwargs) + def users_likes_tracks(self, user_id: int or str = None, if_modified_since_revision=0, timeout=None, + *args, **kwargs): + return self._get_likes('track', user_id, {'if-modified-since-revision': if_modified_since_revision}, timeout, + *args, **kwargs) def users_likes_albums(self, user_id: int or str = None, rich=True, timeout=None, *args, **kwargs): return self._get_likes('album', user_id, {'rich': rich}, timeout, *args, **kwargs) diff --git a/yandex_music/download_info.py b/yandex_music/download_info.py index fbf1d11..c56ced3 100644 --- a/yandex_music/download_info.py +++ b/yandex_music/download_info.py @@ -1,3 +1,5 @@ +import xml.dom.minidom as minidom + from yandex_music import YandexMusicObject @@ -16,8 +18,37 @@ class DownloadInfo(YandexMusicObject): self.preview = preview self.download_info_url = download_info_url + self.direct_link = None + self.client = client + @staticmethod + def _get_text_node_data(elements): + for element in elements: + nodes = element.childNodes + for node in nodes: + if node.nodeType == node.TEXT_NODE: + return node.data + + def get_direct_link(self): + # Available within one minute after receiving download_info, otherwise 410! + result = self.client._request.retrieve(self.download_info_url) + + doc = minidom.parseString(result.text) + host = self._get_text_node_data(doc.getElementsByTagName('host')) + path = self._get_text_node_data(doc.getElementsByTagName('path')) + ts = self._get_text_node_data(doc.getElementsByTagName('ts')) + + self.direct_link = f'https://{host}/get-{self.codec}/randomTrash/{ts}{path}' + + return self.direct_link + + def download(self, filename): + if self.direct_link is None: + self.get_direct_link() + + self.client._request.download(self.direct_link, filename) + @classmethod def de_json(cls, data, client): if not data: @@ -28,7 +59,7 @@ class DownloadInfo(YandexMusicObject): return cls(client=client, **data) @classmethod - def de_list(cls, data, client): + def de_list(cls, data, client, get_direct_links=False): if not data: return [] @@ -36,4 +67,8 @@ class DownloadInfo(YandexMusicObject): for download_info in data: downloads_info.append(cls.de_json(download_info, client)) + if get_direct_links: + for info in downloads_info: + info.get_direct_link() + return downloads_info diff --git a/yandex_music/likes/tracks_likes.py b/yandex_music/likes/tracks_likes.py index 320bfb3..7f7f6ca 100644 --- a/yandex_music/likes/tracks_likes.py +++ b/yandex_music/likes/tracks_likes.py @@ -14,6 +14,12 @@ class TracksLikes(YandexMusicObject): self.client = client + def __getitem__(self, item): + return self.tracks[item] + + def __iter__(self): + return iter(self.tracks) + @property def tracks_ids(self): return [track.track_id for track in self.tracks] diff --git a/yandex_music/track.py b/yandex_music/track.py index c838663..51f9c3c 100644 --- a/yandex_music/track.py +++ b/yandex_music/track.py @@ -49,9 +49,24 @@ class Track(YandexMusicObject): self.content_warning = content_warning self.explicit = explicit + self.download_info = None + self.client = client self._id_attrs = (self.id,) + def get_download_info(self, get_direct_links=False): + self.download_info = self.client.tracks_download_info(self.track_id, get_direct_links) + + return self.download_info + + def download(self, filename, codec='mp3', bitrate_in_kbps=192): + if self.download_info is None: + self.get_download_info() + + for info in self.download_info: + if info.codec == codec and info.bitrate_in_kbps == bitrate_in_kbps: + info.download(filename) + @property def track_id(self): return f'{self.id}' diff --git a/yandex_music/track_short.py b/yandex_music/track_short.py index 81394ab..b768f3b 100644 --- a/yandex_music/track_short.py +++ b/yandex_music/track_short.py @@ -14,8 +14,19 @@ class TrackShort(YandexMusicObject): self.album_id = album_id self.timestamp = datetime.fromisoformat(timestamp) + self._track = None + self.client = client + @property + def track(self): + if self._track: + return self._track + + self._track = self.client.tracks(self.track_id)[0] + + return self._track + @property def track_id(self): return f'{self.id}:{self.album_id}' diff --git a/yandex_music/utils/request.py b/yandex_music/utils/request.py index 045689e..a9cbcd1 100644 --- a/yandex_music/utils/request.py +++ b/yandex_music/utils/request.py @@ -76,14 +76,9 @@ class Request(object): raise NetworkError(e) if 200 <= resp.status_code <= 299: - return self._parse(resp.content).result + return resp - try: - message = self._parse(resp.content).error - if message is None: - raise ValueError() - except ValueError: - message = 'Unknown HTTPError' + message = self._parse(resp.content).error or 'Unknown HTTPError' if resp.status_code in (401, 403): raise Unauthorized(message) @@ -98,8 +93,22 @@ class Request(object): raise NetworkError(f'{message} ({resp.status_code})') def get(self, url, params=None, timeout=5, *args, **kwargs): - return self._request_wrapper('GET', url, params=params, headers=self.headers, timeout=timeout, *args, **kwargs) + result = self._request_wrapper('GET', url, params=params, headers=self.headers, timeout=timeout, + *args, **kwargs) + + return self._parse(result.content).result def post(self, url, data=None, timeout=5, *args, **kwargs): - return self._request_wrapper('POST', url, headers=self.headers, data=data, timeout=timeout, *args, **kwargs) + result = self._request_wrapper('POST', url, headers=self.headers, data=data, timeout=timeout, + *args, **kwargs) + return self._parse(result.content).result + + def retrieve(self, url, params=None, timeout=5, *args, **kwargs): + return self._request_wrapper('GET', url, params=params, headers=self.headers, timeout=timeout, + *args, **kwargs) + + def download(self, url, filename, timeout=5, *args, **kwargs): + result = self.retrieve(url, timeout=timeout, *args, *kwargs) + with open(filename, 'wb') as f: + f.write(result.content) diff --git a/yandex_music/utils/response.py b/yandex_music/utils/response.py index 2dd4cba..6978393 100644 --- a/yandex_music/utils/response.py +++ b/yandex_music/utils/response.py @@ -7,15 +7,21 @@ class Response(YandexMusicObject): invocation_info=None, result=None, error=None, + error_description=None, client=None, **kwargs): self.data = data self.invocation_info = invocation_info self._result = result - self.error = error + self._error = error + self.error_description = error_description self.client = client + @property + def error(self): + return f'{self._error} {self.error_description if self.error_description else ""}' + @property def result(self): return self._result or self.data