diff --git a/README.rst b/README.rst index c6e0d55..99bfeab 100644 --- a/README.rst +++ b/README.rst @@ -89,17 +89,13 @@ Yandex Music API классов-обёрток объектов высокого уровня дабы сделать разработку клиентов и скриптов простой и понятной. --------------------------------- -Доступ к вашим данным на Яндексе --------------------------------- +----------------------------------- +Доступ к вашим данным Яндекс.Музыка +----------------------------------- -Значения констант -`CLIENT_ID и CLIENT_SECRET `_ -позаимствовано у официального приложения-клиента сервиса Яндекс.Музыка из магазина -Microsoft Store. Так как API является закрытым и используется только внутри -компании Яндекс сейчас невозможно зарегистрировать своё собственное приложение на -`oauth.yandex.ru `_, а следовательно, использовать свои -значения констант. +Начиная с версии 2.0.0 библиотека больше не предоставляет интерфейсы для работы +с OAuth Яндекс и Яндекс.Паспорт. Задача по получению токена для доступа к данным +на плечах разработчиков использующих данную библиотеку. ========= Установка @@ -133,26 +129,20 @@ Microsoft Store. Так как API является закрытым и испо client = Client() +Работа без авторизации ограничена. Так, например, для загрузки будут доступны +только первые 30 секунд аудиофайла. Для понимания всех ограничений зайдите на +сайт Яндекс.Музыка под инкогнито и воспользуйтесь сервисом. + Для доступа к своим личным данным следует авторизоваться. -Это можно осуществить через OAuth токен или логин с паролем. +Это осуществляется через токен аккаунта Яндекс.Музыка. -Авторизация по логину и паролю: - -.. code:: python - - from yandex_music import Client - - client = Client.from_credentials('example@yandex.com', 'password') - -Авторизация по токену: +Авторизация: .. code:: python from yandex_music import Client client = Client('token') - # или - client = Client.from_token('token') После успешного создания клиента Вы вольны в выборе необходимого метода из API. Все они доступны у объекта класса ``Client``. Подробнее в методах клиента @@ -164,7 +154,7 @@ Microsoft Store. Так как API является закрытым и испо from yandex_music import Client - client = Client.from_credentials('example@yandex.com', 'password') + client = Client('token') client.users_likes_tracks()[0].fetch_track().download('example.mp3') В примере выше клиент получает список треков которые были отмечены как @@ -208,36 +198,6 @@ music.yandex.ru/album/**1193829**/track/**10994777** Больше примеров тут: `proxies - advanced usage - requests `_ -Пример инициализации клиента с обработкой капчи при помощи callback-функции: - -.. code:: python - - def proc_captcha(captcha_image_url): - print(captcha_image_url) - return input('Код с картинки: ') - - client = Client.from_credentials('login', 'pass', captcha_callback=proc_captcha) - -Пример инициализации клиента с обработкой капчи: - -.. code:: python - - def init_client(): - client = track_id = captcha_image_url = captcha_answer = None - while not client: - try: - client = Client.from_credentials('login', 'pass', track_id, captcha_answer) - except Captcha as e: - track_id = e.track_id - if e.captcha_image_url: - captcha_image_url = e.captcha_image_url - else: - print('Вы отправили ответ не посмотрев на картинку..') - - captcha_answer = input(f'{captcha_image_url}\nВведите код с картинки: ') - - return client - -------------------- Изучение по примерам -------------------- diff --git a/examples/daily_playlist_updater.py b/examples/daily_playlist_updater.py index 8c47f79..81ea31e 100644 --- a/examples/daily_playlist_updater.py +++ b/examples/daily_playlist_updater.py @@ -6,15 +6,10 @@ from yandex_music.client import Client if len(sys.argv) == 1 or len(sys.argv) > 3: print('Usage: DailyPlaylistUpdater.py token') print('token - Authentication token') - print('\nUsage: DailyPlaylistUpdater.py username password') - print('username - username in format \'example@yandex.ru\'') - print('password - your password') quit() # Authorization elif len(sys.argv) == 2: - client = Client.fromToken(sys.argv[1]) -elif len(sys.argv) == 3: - client = Client.fromCredentials(sys.argv[1], sys.argv[2]) + client = Client(sys.argv[1]) # Current daily playlist PersonalPlaylistBlocks = client.landing(blocks=['personalplaylists']).blocks[0] diff --git a/examples/player.py b/examples/player.py index 475f7bd..366f57a 100644 --- a/examples/player.py +++ b/examples/player.py @@ -53,7 +53,7 @@ else: print('Config file not found. Use --token to create it') sys.exit(2) -client = Client.from_token(args.token, report_new_fields=False) +client = Client(args.token, report_unknown_fields=False) print('Hello,', client.me.account.first_name) if client.me.account.now and client.me.account.now.split('T')[0] == client.me.account.birthday: @@ -65,7 +65,7 @@ if args.playlist == 'user': print('specify --playlist-name', list(p.title for p in user_playlists)) sys.exit(1) playlist = next((p for p in user_playlists if p.title == args.playlist_name), None) - if playlist == None: + if playlist is None: print(f'playlist "{args.playlist_name}" not found') sys.exit(1) total_tracks = playlist.track_count diff --git a/examples/radio_example/radio_example.py b/examples/radio_example/radio_example.py index 0647455..a6fe708 100644 --- a/examples/radio_example/radio_example.py +++ b/examples/radio_example/radio_example.py @@ -7,7 +7,7 @@ from yandex_music import Client from radio import Radio # API instance -client = Client(token="YOUR_TOKEN_HERE") +client = Client(token='YOUR_TOKEN_HERE') # Get random station _stations = client.rotor_stations_list() @@ -21,10 +21,10 @@ radio = Radio(client) # start radio and get first track first_track = radio.start_radio(_station_id, _station_from) -print("[Radio] First track is:", first_track) +print('[Radio] First track is:', first_track) # get new track every 5 sec while True: sleep(5) next_track = radio.play_next() - print("[Radio] Next track is:", next_track) + print('[Radio] Next track is:', next_track) diff --git a/examples/radio_example/stream_example.py b/examples/radio_example/stream_example.py index 6706fcf..89bf2c3 100644 --- a/examples/radio_example/stream_example.py +++ b/examples/radio_example/stream_example.py @@ -5,7 +5,7 @@ from yandex_music import Client from radio import Radio # API instance -client = Client(token="YOUR_API_KEY_HERE") +client = Client(token='YOUR_API_KEY_HERE') # get some track track = client.tracks(['2816574:303266'])[0] @@ -26,10 +26,10 @@ radio = Radio(client) # start radio and get first track first_track = radio.start_radio(_station_id, _station_from) -print("[Radio] First track is:", first_track) +print('[Radio] First track is:', first_track) # get new track every 5 sec while True: sleep(5) next_track = radio.play_next() - print("[Radio] Next track is:", next_track) + print('[Radio] Next track is:', next_track) diff --git a/yandex_music/client.py b/yandex_music/client.py index e33daba..3da2d65 100644 --- a/yandex_music/client.py +++ b/yandex_music/client.py @@ -42,16 +42,9 @@ from yandex_music import ( __license__, __version__, ) -from yandex_music.exceptions import BadRequest, Captcha, CaptchaNotShown, CaptchaRequired, Unauthorized from yandex_music.utils.difference import Difference from yandex_music.utils.request import Request - -CLIENT_ID = '23cabbbdc6cd418abb4b39c32c41195d' -CLIENT_SECRET = '53bc75238f0c4d08a118e51fe9203300' -X_TOKEN_CLIENT_ID = 'c0ebe342af7d48fbbbfcf2d2eedb8f9e' -X_TOKEN_CLIENT_SECRET = 'ad0a908f0aa341a182a37ecd75bc319e' - de_list = { 'artist': Artist.de_list, 'album': Album.de_list, @@ -96,7 +89,6 @@ class Client(YandexMusicObject): logger (:obj:`logging.Logger`): Объект логгера. token (:obj:`str`): Уникальный ключ для аутентификации. base_url (:obj:`str`): Ссылка на API Yandex Music. - passport_url (:obj:`str`): Ссылка на Mobileproxy Passport Yandex Music. me (:obj:`yandex_music.Status`): Информация об аккаунте. device (:obj:`str`): Строка, содержащая сведения об устройстве, с которого выполняются запросы. report_unknown_fields (:obj:`bool`): Включены ли предупреждения о неизвестных полях от API, @@ -106,7 +98,6 @@ class Client(YandexMusicObject): token (:obj:`str`, optional): Уникальный ключ для аутентификации. fetch_account_status (:obj:`bool`, optional): Получить ли информацию об аккаунте при инициализации объекта. base_url (:obj:`str`, optional): Ссылка на API Yandex Music. - passport_url (:obj:`str`, optional): Ссылка на Mobileproxy Passport Yandex Music. request (:obj:`yandex_music.utils.request.Request`, optional): Пре-инициализация :class:`yandex_music.utils.request.Request`. language (:obj:`str`, optional): Язык, на котором будут приходить ответы от API. @@ -121,7 +112,6 @@ class Client(YandexMusicObject): token: str = None, fetch_account_status: bool = True, base_url: str = None, - passport_url: str = None, request: Request = None, language: str = 'ru', report_unknown_fields=True, @@ -136,11 +126,8 @@ class Client(YandexMusicObject): if base_url is None: base_url = 'https://api.music.yandex.net' - if passport_url is None: - passport_url = 'https://mobileproxy.passport.yandex.net/' self.base_url = base_url - self.passport_url = passport_url self.report_new_fields = report_unknown_fields @@ -157,222 +144,11 @@ class Client(YandexMusicObject): 'os=Python; os_version=; manufacturer=Marshal; ' 'model=Yandex Music API; clid=; device_id=random; uuid=random' ) - self._auth_sdk_params = 'app_id=ru.yandex.mobile.music&app_version_name=5.18&app_platform=iPad' self.me = None if fetch_account_status: self.me = self.account_status() - @classmethod - def from_credentials( - cls, - login: str, - password: str, - track_id: str = None, - captcha_answer: str = None, - captcha_callback: Callable[[str], str] = None, - timeout: Union[int, float] = None, - *args, - **kwargs, - ) -> 'Client': - """Инициализция клиента по логину и паролю. - - Note: - Данный метод получает токен каждый раз при вызове. Рекомендуется сгенерировать его самостоятельно, сохранить - и использовать при следующих инициализациях клиента. Не храните логины и пароли! - - Args: - login (:obj:`str`): Логин клиента (идентификатор). - password (:obj:`str`): Пароль клиента (аутентификатор). - track_id (:obj:`str`): Идентификатора сессии аутентификации (для ввода капчи на старой сессии). - captcha_answer (:obj:`str`, optional): Ответ на капчу (цифры с картинки). - captcha_callback (:obj:`function`, optional): Функция обратного вызова для обработки капчи, должна - принимать строку с ссылкой на капчу и возвращать проверочный код. - timeout (:obj:`int` | :obj:`float`, optional): Если это значение указано, используется как время ожидания - ответа от сервера вместо указанного при создании пула. - *args: Произвольные аргументы для `requests.request` и `Client`. - **kwargs: Произвольные ключевые аргументы для `requests.request` и `Client`. - - Returns: - :obj:`yandex_music.Client`. - - Raises: - :class:`yandex_music.exceptions.CaptchaRequired`: При необходимости пройти капчу и отсутствию коллбека. - :class:`yandex_music.exceptions.CaptchaNotShown`: При попытке отправить ответ на капчу не посмотрев её. - :class:`yandex_music.exceptions.YandexMusicError`: Базовое исключение библиотеки. - """ - - client = cls(*args, **kwargs) - - if not track_id: - track_id = client._start_authentication(login, timeout, *args, **kwargs) - - x_token = None - while not x_token: - try: - x_token = client._send_authentication_password( - track_id, password, captcha_answer, timeout, *args, **kwargs - ) - except Captcha as e: - if not captcha_callback or not e.captcha_image_url: - raise e - - captcha_answer = captcha_callback(e.captcha_image_url) - - token = client._generate_yandex_music_token_by_x_token(x_token, timeout, *args, **kwargs) - return cls(token, *args, **kwargs) - - @classmethod - def from_token(cls, token: str, *args, **kwargs) -> 'Client': - """Инициализация клиента по токену. - - Note: - Ничем не отличается от `Client(token)`. Так исторически сложилось. - - Args: - token (:obj:`str`, optional): Уникальный ключ для аутентификации. - *args: Произвольные аргументы для `Client`. - **kwargs: Произвольные ключевые аргументы для `Client`. - - Returns: - :obj:`yandex_music.Client`. - """ - - return cls(token, *args, **kwargs) - - @log - def _start_authentication(self, login: Union[str, int], timeout: Union[int, float] = None, *args, **kwargs) -> str: - """Отправка логина и проверка на его существование. Получение идентификатора аутентификации. - - Args: - login (:obj:`str`): Логин клиента (идентификатор). - timeout (:obj:`int` | :obj:`float`, optional): Если это значение указано, используется как время ожидания - ответа от сервера вместо указанного при создании пула. - *args: Произвольные аргументы для `requests.request`. - **kwargs: Произвольные ключевые аргументы для `requests.request`. - - Returns: - :obj:`str`: `track_id` необходимый для отправки пароля к логину. - - Raises: - :class:`yandex_music.exceptions.BadRequest`: При неправильном запросе. - """ - - url = f'{self.passport_url}/2/bundle/mobile/start' - - data = { - 'client_id': CLIENT_ID, - 'client_secret': CLIENT_SECRET, - 'display_language': self.language, - 'login': login, - 'x_token_client_id': X_TOKEN_CLIENT_ID, - 'x_token_client_secret': X_TOKEN_CLIENT_SECRET, - } - - result = self._request.post(url, data, timeout=timeout, *args, **kwargs) - - if result.get('status', 'error') == 'error': - raise BadRequest(result) - - return result['track_id'] - - @log - def _send_authentication_password( - self, - track_id: str, - password: str, - captcha_answer: str = None, - timeout: Union[int, float] = None, - *args, - **kwargs, - ) -> str: - """Отправка пароля к существующему логину. - - Notes: - Капча появляется после 5 неудачных попыток ввода пароля. - - При получении капчи обязательно выполнить запрос на `captcha_image_url` (посмотреть капчу) перед - отправкой ответа. - - Args: - track_id (:obj:`str`): Идентификатор аутентификации (получен на шаге валидации логина). - password (:obj:`str`): Пароль клиента (аутентификатор). - timeout (:obj:`int` | :obj:`float`, optional): Если это значение указано, используется как время ожидания - ответа от сервера вместо указанного при создании пула. - *args: Произвольные аргументы для `requests.request`. - **kwargs: Произвольные ключевые аргументы для `requests.request`. - - Returns: - :obj:`str`: `X-Token` необходимый для получения токена ЯМ. - - Raises: - :class:`yandex_music.exceptions.TimedOut`: При превышении времени ожидания. - :class:`yandex_music.exceptions.BadRequest`: При неправильном или неизвестном библиотеке запросе. - :class:`yandex_music.exceptions.Unauthorized`: При неправильном пароле. - :class:`yandex_music.exceptions.CaptchaRequired`: При необходимости пройти капчу. - :class:`yandex_music.exceptions.CaptchaNotShown`: При попытке отправить ответ на капчу не посмотрев её. - """ - url = f'{self.passport_url}/1/bundle/mobile/auth/password' - - data = { - 'track_id': track_id, - 'password': password, - } - - if captcha_answer: - data['captcha_answer'] = captcha_answer - - result = self._request.post(url, data, timeout=timeout, *args, **kwargs) - - status = result.get('status', 'error') - if status == 'ok': - return result['x_token'] - - if 'password.not_matched' in result['errors']: - raise Unauthorized(result) - elif 'captcha.required' in result['errors']: - raise CaptchaRequired('captcha.required', track_id, result['captcha_image_url']) - elif 'captcha.not_shown' in result['errors']: - raise CaptchaNotShown('captcha.not_shown', track_id) - else: - raise BadRequest(result) - - @log - def _generate_yandex_music_token_by_x_token( - self, x_token: str, timeout: Union[int, float] = None, *args, **kwargs - ) -> str: - """Получения токена для Яндекс.Музыка по X-Token. - - Args: - x_token (:obj:`str`): X-Token полученный в ответ на отправленный пароль. - timeout (:obj:`int` | :obj:`float`, optional): Если это значение указано, используется как время ожидания - ответа от сервера вместо указанного при создании пула. - *args: Произвольные аргументы для `requests.request`. - **kwargs: Произвольные ключевые аргументы для `requests.request`. - - Returns: - :obj:`str`: `access_token` токен Яндекс.Музыка пригодный для использования в клиенте. - - Raises: - :class:`yandex_music.exceptions.BadRequest`: При неправильном запросе. - """ - - url = f'{self.passport_url}/1/token/?{self._auth_sdk_params}' - - data = { - 'access_token': x_token, - 'client_id': CLIENT_ID, - 'client_secret': CLIENT_SECRET, - 'grant_type': 'x-token', - } - - result = self._request.post(url, data, timeout=timeout, *args, **kwargs) - - if 'access_token' in result: - return result['access_token'] - - raise BadRequest(result) - @property def request(self) -> Request: """:obj:`yandex_music.utils.request.Request`: Объект вспомогательного класса для отправки запросов.""" @@ -2816,10 +2592,6 @@ class Client(YandexMusicObject): # camelCase псевдонимы - #: Псевдоним для :attr:`from_credentials` - fromCredentials = from_credentials - #: Псевдоним для :attr:`from_token` - fromToken = from_token #: Псевдоним для :attr:`account_status` accountStatus = account_status #: Псевдоним для :attr:`account_settings` diff --git a/yandex_music/exceptions.py b/yandex_music/exceptions.py index fe28526..4458caf 100644 --- a/yandex_music/exceptions.py +++ b/yandex_music/exceptions.py @@ -1,6 +1,3 @@ -from typing import TYPE_CHECKING - - class YandexMusicError(Exception): """Базовый класс, представляющий исключения общего характера.""" @@ -17,39 +14,6 @@ class InvalidBitrate(YandexMusicError): """ -class Captcha(YandexMusicError): - """Базовый класс, представляющий исключение связанное с капчей. - - Attributes: - track_id (:obj:`str`): Идентификатора сессии аутентификации. - captcha_image_url (:obj:`str` | :obj:`None`): Ссылка на изображение с капчей. - - Args: - msg (:obj:`str`): Сообщение с ошибкой. - track_id (:obj:`str`, optional): Идентификатора сессии аутентификации. - captcha_image_url (:obj:`str`, optional): Ссылка на изображение с капчей. - """ - - def __init__(self, msg: str, track_id: str = None, captcha_image_url: str = None, *args): - super().__init__(msg, *args) - self.track_id = track_id - self.captcha_image_url = captcha_image_url - - -class CaptchaRequired(Captcha): - """Класс исключения, вызываемый в случае необходимости ввода проверочного кода.""" - - -class CaptchaNotShown(Captcha): - """Класс исключения, вызываемый в случае когда капча была получена, но изображение не было загружено. - - Notes: - Будет вызвано если не сделать запрос на `captcha_image_url` полученный в `CaptchaRequired`. Получив данное - исключение можно выполнить запрос еще раз для получения новой капчи. - - """ - - class NetworkError(YandexMusicError): """Базовый класс исключений, вызываемых для ошибок, связанных с запросами к серверу.