diff --git a/README.rst b/README.rst index 99bfeab..ef3d532 100644 --- a/README.rst +++ b/README.rst @@ -48,6 +48,8 @@ Yandex Music API #. `Изучение по примерам`_ + #. `Особенности использования асинхронной версии библиотеки`_ + #. `Логирование`_ #. `Документация`_ @@ -83,11 +85,13 @@ Yandex Music API Эта библиотека предоставляется Python интерфейс для никем незадокументированного и сделанного только для себя API Яндекс Музыки. -Она совместима с версиями Python 3.7+. +Она совместима с версиями Python 3.7+ и поддерживает работу как с синхронном, +так и асинхронным (asyncio) кодом. В дополнение к реализации чистого API данная библиотека имеет ряд классов-обёрток объектов высокого уровня дабы сделать разработку клиентов -и скриптов простой и понятной. +и скриптов простой и понятной. Вся документация была написана с нуля исходя +из логического анализа в ходе обратной разработки (reverse engineering) API. ----------------------------------- Доступ к вашим данным Яндекс.Музыка @@ -121,13 +125,33 @@ Yandex Music API Приступив к работе первым делом необходимо создать экземпляр клиента. -Инициализация клиента: +Инициализация синхронного клиента: .. code:: python from yandex_music import Client client = Client() + client.init() + + # или + + client = Client().init() + +Инициализация асинхронного клиента: + +.. code:: python + + from yandex_music import ClientAsync + + client = ClientAsync() + await client.init() + + # или + + client = await Client().init() + +Вызов ``init()`` необходим для получение информации для упрощения будущих запросов. Работа без авторизации ограничена. Так, например, для загрузки будут доступны только первые 30 секунд аудиофайла. Для понимания всех ограничений зайдите на @@ -142,7 +166,7 @@ Yandex Music API from yandex_music import Client - client = Client('token') + client = Client('token').init() После успешного создания клиента Вы вольны в выборе необходимого метода из API. Все они доступны у объекта класса ``Client``. Подробнее в методах клиента @@ -154,7 +178,7 @@ Yandex Music API from yandex_music import Client - client = Client('token') + client = Client('token').init() client.users_likes_tracks()[0].fetch_track().download('example.mp3') В примере выше клиент получает список треков которые были отмечены как @@ -172,14 +196,14 @@ Yandex Music API from yandex_music import Client - client = Client() + client = Client().init() client.tracks(['10994777:1193829', '40133452:5206873', '48966383:6693286', '51385674:7163467']) В качестве ID трека выступает его уникальный номер и номер альбома. Первым треком из примера является следующий трек: music.yandex.ru/album/**1193829**/track/**10994777** -Выполнение запросов с использование прокси: +Выполнение запросов с использование прокси в синхронной версии: .. code:: python @@ -187,7 +211,7 @@ music.yandex.ru/album/**1193829**/track/**10994777** from yandex_music import Client request = Request(proxy_url='socks5://user:password@host:port') - client = Client(request=request) + client = Client(request=request).init() Примеры proxy url: @@ -198,6 +222,20 @@ music.yandex.ru/album/**1193829**/track/**10994777** Больше примеров тут: `proxies - advanced usage - requests `_ +Выполнение запросов с использование прокси в асинхронной версии: + +.. code:: python + + from yandex_music.utils.request_async import Request + from yandex_music import ClientAsync + + request = Request(proxy_url='http://user:pass@some.proxy.com') + client = await ClientAsync(request=request).init() + +Socks прокси не поддерживаются в асинхронной версии. + +Про поддерживаемые прокси тут: `proxy support - advanced usage - aiohttp `_ + -------------------- Изучение по примерам -------------------- @@ -211,6 +249,33 @@ music.yandex.ru/album/**1193829**/track/**10994777** Посетите `эту страницу `_ чтобы изучить официальные примеры. +---------------------------------------------- +Особенности использования асинхронного клиента +---------------------------------------------- + +При работе с асинхронной версией библиотеке стоит всегда помнить +следующие особенности: + +- Клиент следует импортировать с названием ``ClientAsync``, а не просто ``Client``. +- При использовании методов-сокращений нужно выбирать метод с суффиксом ``_async``. + +Пояснение ко второму пункту: + +.. code:: python + + from yandex_music import ClientAsync + + client = await ClientAsync('token').init() + liked_short_track = (await client.users_likes_tracks())[0] + + # правильно + full_track = await liked_short_track.fetch_track_async() + await full_track.download_async() + + # НЕПРАВИЛЬНО + full_track = await liked_short_track.fetch_track() + await full_track.download() + ----------- Логирование ----------- diff --git a/examples/chart.py b/examples/chart.py index 48e4db4..dfb390d 100644 --- a/examples/chart.py +++ b/examples/chart.py @@ -6,7 +6,7 @@ from yandex_music import Client CHART_ID = 'world' TOKEN = os.environ.get('TOKEN') -client = Client(TOKEN) +client = Client(TOKEN).init() chart = client.chart(CHART_ID).chart text = [f'🏆 {chart.title}', chart.description, '', 'Треки:'] diff --git a/examples/daily_playlist_updater.py b/examples/daily_playlist_updater.py index 81ea31e..bfa72e3 100644 --- a/examples/daily_playlist_updater.py +++ b/examples/daily_playlist_updater.py @@ -9,7 +9,7 @@ if len(sys.argv) == 1 or len(sys.argv) > 3: quit() # Authorization elif len(sys.argv) == 2: - client = Client(sys.argv[1]) + client = Client(sys.argv[1]).init() # Current daily playlist PersonalPlaylistBlocks = client.landing(blocks=['personalplaylists']).blocks[0] diff --git a/examples/get_album_with_tracks.py b/examples/get_album_with_tracks.py index 9138df7..e617eaa 100644 --- a/examples/get_album_with_tracks.py +++ b/examples/get_album_with_tracks.py @@ -6,7 +6,7 @@ from yandex_music import Client TOKEN = os.environ.get('TOKEN') ALBUM_ID = 2832563 -client = Client(TOKEN) +client = Client(TOKEN).init() album = client.albums_with_tracks(ALBUM_ID) tracks = [] diff --git a/examples/like_and_dislike.py b/examples/like_and_dislike.py index fa484d2..25fffde 100644 --- a/examples/like_and_dislike.py +++ b/examples/like_and_dislike.py @@ -6,7 +6,7 @@ from yandex_music import Client TOKEN = os.environ.get('TOKEN') ALBUM_ID = 2832563 -client = Client(TOKEN) +client = Client(TOKEN).init() success = client.users_likes_albums_add(ALBUM_ID) answer = 'Лайкнут' if success else 'Произошла ошибка' diff --git a/examples/lyrics_playing_track.py b/examples/lyrics_playing_track.py index b23e831..baaa349 100644 --- a/examples/lyrics_playing_track.py +++ b/examples/lyrics_playing_track.py @@ -5,7 +5,7 @@ from yandex_music import Client TOKEN = os.environ.get('TOKEN') -client = Client(TOKEN) +client = Client(TOKEN).init() queues = client.queues_list() # Последняя проигрываемая очередь всегда в начале списка diff --git a/examples/player.py b/examples/player.py index 366f57a..7b8d924 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(args.token, report_unknown_fields=False) +client = Client(args.token, report_unknown_fields=False).init() print('Hello,', client.me.account.first_name) if client.me.account.now and client.me.account.now.split('T')[0] == client.me.account.birthday: diff --git a/examples/proxy.py b/examples/proxy.py index 07f9f09..b00237c 100644 --- a/examples/proxy.py +++ b/examples/proxy.py @@ -12,12 +12,12 @@ try: raise YandexMusicError() # подключаемся без прокси для получения информации об аккаунте (доступно из других стран) - client = Client(yandex_music_token, request=Request()) + client = Client(yandex_music_token, request=Request()).init() # проверяем отсутствие подписки у пользователя if client.me and client.me.plus and not client.me.plus.has_plus: # если подписки нет - пересоздаем клиент с использованием прокси - client = Client(yandex_music_token, request=proxied_request) + client = Client(yandex_music_token, request=proxied_request).init() except YandexMusicError: # если есть проблемы с авторизацией, токеном или чем-либо еще, то инициализируем клиент без авторизации # так как сервисом можно пользоваться будучи гостем, но со своими ограничениями - client = Client(request=proxied_request, fetch_account_status=False) + client = Client(request=proxied_request) diff --git a/examples/radio_example/stream_example.py b/examples/radio_example/stream_example.py index 89bf2c3..8153ce0 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').init() # get some track track = client.tracks(['2816574:303266'])[0] diff --git a/examples/search.py b/examples/search.py index 58909cd..8670dc2 100644 --- a/examples/search.py +++ b/examples/search.py @@ -1,7 +1,7 @@ from yandex_music import Client -client = Client() +client = Client().init() type_to_name = { 'track': 'трек', diff --git a/generate_async_version.py b/generate_async_version.py new file mode 100644 index 0000000..299d084 --- /dev/null +++ b/generate_async_version.py @@ -0,0 +1,93 @@ +import subprocess + + +DISCLAIMER = '# THIS IS AUTO GENERATED COPY OF client.py. DON\'T EDIT IN BY HANDS #' +DISCLAIMER = f'{"#" * len(DISCLAIMER)}\n{DISCLAIMER}\n{"#" * len(DISCLAIMER)}\n\n' + +REQUEST_METHODS = ('_request_wrapper', 'get', 'post', 'retrieve', 'download') + + +def gen_request(output_request_filename): + with open('yandex_music/utils/request.py', 'r') as f: + code = f.read() + + code = code.replace('import requests', 'import asyncio\nimport aiohttp\nimport aiofiles') + + # order make sense + code = code.replace('resp.content', 'content') + code = code.replace( + 'resp = requests.request(*args, **kwargs)', + f'async with aiohttp.request(*args, **kwargs) as _resp:\n{" " * 16}resp = _resp\n{" " * 16}content = await resp.content.read()', + ) + + code = code.replace('except requests.Timeout', 'except asyncio.TimeoutError') + code = code.replace('except requests.RequestException', 'except aiohttp.ClientError') + code = code.replace('resp.status_code', 'resp.status') + + for method in REQUEST_METHODS: + code = code.replace(f'def {method}', f'async def {method}') + code = code.replace(f'self.{method}(', f'await self.{method}(') + + code = code.replace('proxies=self.proxies', 'proxy=self.proxy_url') + code = code.replace('timeout=timeout', 'timeout=aiohttp.ClientTimeout(total=timeout)') + # undo one specific case + code = code.replace( + 'self.retrieve(url, timeout=aiohttp.ClientTimeout(total=timeout)', 'self.retrieve(url, timeout=timeout' + ) + + # download method + code = code.replace('with open', 'async with aiofiles.open') + code = code.replace('f.write', 'await f.write') + + # docs + code = code.replace('`requests`', '`aiohttp`') + code = code.replace('requests.request', 'aiohttp.request') + + code = DISCLAIMER + code + with open(output_request_filename, 'w') as f: + f.write(code) + + +def gen_client(output_client_filename): + with open('yandex_music/client.py', 'r') as f: + code = f.read() + + code = code.replace('Client', 'ClientAsync') + code = code.replace( + 'from yandex_music.utils.request import Request', 'from yandex_music.utils.request_async import Request' + ) + + code = code.replace('def wrapper', 'async def wrapper') + code = code.replace('result = method(', 'result = await method(') + code = code.replace('@log\n def', '@log\n async def') + code = code.replace('self.account_status', 'await self.account_status') + + for method in REQUEST_METHODS: + code = code.replace(f'self._request.{method}', f'await self._request.{method}') + for method in ('_like_action', '_dislike_action', '_get_list', '_get_likes'): + code = code.replace(f'def {method}', f'async def {method}') + code = code.replace(f'self.{method}(', f'await self.{method}(') + + # specific cases + code = code.replace( + 'self.users_playlists_change(', + 'await self.users_playlists_change(' + ) + code = code.replace( + 'self.rotor_station_feedback(', + 'await self.rotor_station_feedback(' + ) + + code = DISCLAIMER + code + with open(output_client_filename, 'w') as f: + f.write(code) + + +if __name__ == '__main__': + request_filename = 'yandex_music/utils/request_async.py' + client_filename = 'yandex_music/client_async.py' + gen_request(request_filename) + gen_client(client_filename) + + for file in (request_filename, client_filename): + subprocess.run(['black', '--config', 'black.toml', file]) diff --git a/setup.py b/setup.py index 682d9f9..360565b 100644 --- a/setup.py +++ b/setup.py @@ -30,7 +30,7 @@ setup( description='Неофициальная Python библиотека для работы с API сервиса Яндекс.Музыка.', long_description=readme, packages=find_packages(), - install_requires=['requests[socks]'], + install_requires=['requests[socks]', 'aiohttp', 'aiofiles'], include_package_data=True, classifiers=[ 'Development Status :: 5 - Production/Stable', diff --git a/yandex_music/__init__.py b/yandex_music/__init__.py index f6d478f..5c61a82 100644 --- a/yandex_music/__init__.py +++ b/yandex_music/__init__.py @@ -135,11 +135,16 @@ from .invocation_info import InvocationInfo from .track_short import TrackShort from .icon import Icon from .client import Client +from .client_async import ClientAsync __all__ = [ + '__copyright__', + '__license__', + '__version__', 'YandexMusicObject', 'Client', + 'ClientAsync', 'Account', 'PassportPhone', 'InvocationInfo', diff --git a/yandex_music/album/album.py b/yandex_music/album/album.py index 44446ee..c9a4bfe 100644 --- a/yandex_music/album/album.py +++ b/yandex_music/album/album.py @@ -126,6 +126,13 @@ class Album(YandexMusicObject): """ return self.client.albums_with_tracks(self.id, *args, **kwargs) + async def with_tracks_async(self, *args, **kwargs) -> Optional['Album']: + """Сокращение для:: + + await client.albums_with_tracks(album.id, *args, **kwargs) + """ + return await self.client.albums_with_tracks(self.id, *args, **kwargs) + def download_cover(self, filename: str, size: str = '200x200') -> None: """Загрузка обложки. @@ -135,6 +142,15 @@ class Album(YandexMusicObject): """ self.client.request.download(f'https://{self.cover_uri.replace("%%", size)}', filename) + async def download_cover_async(self, filename: str, size: str = '200x200') -> None: + """Загрузка обложки. + + Args: + filename (:obj:`str`): Путь для сохранения файла с названием и расширением. + size (:obj:`str`, optional): Размер обложки. + """ + await self.client.request.download(f'https://{self.cover_uri.replace("%%", size)}', filename) + def download_og_image(self, filename: str, size: str = '200x200') -> None: """Загрузка обложки. @@ -146,6 +162,17 @@ class Album(YandexMusicObject): """ self.client.request.download(f'https://{self.og_image.replace("%%", size)}', filename) + async def download_og_image_async(self, filename: str, size: str = '200x200') -> None: + """Загрузка обложки. + + Предпочтительнее использовать `self.download_cover_async()`. + + Args: + filename (:obj:`str`): Путь для сохранения файла с названием и расширением. + size (:obj:`str`, optional): Размер обложки. + """ + await self.client.request.download(f'https://{self.og_image.replace("%%", size)}', filename) + def like(self, *args, **kwargs) -> bool: """Сокращение для:: @@ -153,6 +180,13 @@ class Album(YandexMusicObject): """ return self.client.users_likes_albums_add(self.id, self.client.me.account.uid, *args, **kwargs) + async def like_async(self, *args, **kwargs) -> bool: + """Сокращение для:: + + await client.users_likes_albums_add(album.id, user.id *args, **kwargs) + """ + return await self.client.users_likes_albums_add(self.id, self.client.me.account.uid, *args, **kwargs) + def dislike(self, *args, **kwargs) -> bool: """Сокращение для:: @@ -160,6 +194,13 @@ class Album(YandexMusicObject): """ return self.client.users_likes_albums_remove(self.id, self.client.me.account.uid, *args, **kwargs) + async def dislike_async(self, *args, **kwargs) -> bool: + """Сокращение для:: + + await client.users_likes_albums_remove(album.id, user.id *args, **kwargs) + """ + return await self.client.users_likes_albums_remove(self.id, self.client.me.account.uid, *args, **kwargs) + def artists_name(self) -> List[str]: """Получает имена всех исполнителей. @@ -217,9 +258,15 @@ class Album(YandexMusicObject): #: Псевдоним для :attr:`with_tracks` withTracks = with_tracks + #: Псевдоним для :attr:`with_tracks_async` + withTracksAsync = with_tracks_async #: Псевдоним для :attr:`download_cover` downloadCover = download_cover + #: Псевдоним для :attr:`download_cover_async` + downloadCoverAsync = download_cover_async #: Псевдоним для :attr:`download_og_image` downloadOgImage = download_og_image + #: Псевдоним для :attr:`download_og_image_async` + downloadOgImageAsync = download_og_image_async #: Псевдоним для :attr:`artists_name` artistsName = artists_name diff --git a/yandex_music/artist/artist.py b/yandex_music/artist/artist.py index 4c9d75e..02eab5e 100644 --- a/yandex_music/artist/artist.py +++ b/yandex_music/artist/artist.py @@ -90,6 +90,15 @@ class Artist(YandexMusicObject): """ self.client.request.download(f'https://{self.og_image.replace("%%", size)}', filename) + async def download_og_image_async(self, filename: str, size: str = '200x200') -> None: + """Загрузка изображения для Open Graph. + + Args: + filename (:obj:`str`): Путь для сохранения файла с названием и расширением. + size (:obj:`str`, optional): Размер обложки. + """ + await self.client.request.download(f'https://{self.og_image.replace("%%", size)}', filename) + def download_op_image(self, filename: str, size: str = '200x200') -> None: """Загрузка обложки. @@ -102,6 +111,18 @@ class Artist(YandexMusicObject): """ self.client.request.download(f'https://{self.op_image.replace("%%", size)}', filename) + async def download_op_image_async(self, filename: str, size: str = '200x200') -> None: + """Загрузка обложки. + + Notes: + Используйте это только когда нет self.cover! + + Args: + filename (:obj:`str`): Путь для сохранения файла с названием и расширением. + size (:obj:`str`, optional): Размер обложки. + """ + await self.client.request.download(f'https://{self.op_image.replace("%%", size)}', filename) + def like(self, *args, **kwargs) -> bool: """Сокращение для:: @@ -109,6 +130,13 @@ class Artist(YandexMusicObject): """ return self.client.users_likes_artists_add(self.id, self.client.me.account.uid, *args, **kwargs) + async def like_async(self, *args, **kwargs) -> bool: + """Сокращение для:: + + await client.users_likes_artists_add(artist.id, user.id *args, **kwargs) + """ + return await self.client.users_likes_artists_add(self.id, self.client.me.account.uid, *args, **kwargs) + def dislike(self, *args, **kwargs) -> bool: """Сокращение для:: @@ -116,6 +144,13 @@ class Artist(YandexMusicObject): """ return self.client.users_likes_artists_remove(self.id, self.client.me.account.uid, *args, **kwargs) + async def dislike_async(self, *args, **kwargs) -> bool: + """Сокращение для:: + + await client.users_likes_artists_remove(artist.id, user.id *args, **kwargs) + """ + return await self.client.users_likes_artists_remove(self.id, self.client.me.account.uid, *args, **kwargs) + def get_tracks(self, page=0, page_size=20, *args, **kwargs) -> Optional['ArtistTracks']: """Сокращение для:: @@ -123,6 +158,13 @@ class Artist(YandexMusicObject): """ return self.client.artists_tracks(self.id, page, page_size, *args, **kwargs) + async def get_tracks_async(self, page=0, page_size=20, *args, **kwargs) -> Optional['ArtistTracks']: + """Сокращение для:: + + await client.artists_tracks(artist.id, page, page_size, *args, **kwargs) + """ + return await self.client.artists_tracks(self.id, page, page_size, *args, **kwargs) + def get_albums(self, page=0, page_size=20, sort_by='year', *args, **kwargs) -> Optional['ArtistAlbums']: """Сокращение для:: @@ -130,6 +172,13 @@ class Artist(YandexMusicObject): """ return self.client.artists_direct_albums(self.id, page, page_size, sort_by, *args, **kwargs) + async def get_albums_async(self, page=0, page_size=20, sort_by='year', *args, **kwargs) -> Optional['ArtistAlbums']: + """Сокращение для:: + + await client.artists_direct_albums(artist.id, page, page_size, sort_by, *args, **kwargs) + """ + return await self.client.artists_direct_albums(self.id, page, page_size, sort_by, *args, **kwargs) + @classmethod def de_json(cls, data: dict, client: 'Client') -> Optional['Artist']: """Десериализация объекта. @@ -186,9 +235,21 @@ class Artist(YandexMusicObject): #: Псевдоним для :attr:`download_og_image` downloadOgImage = download_og_image + #: Псевдоним для :attr:`download_og_image_async` + downloadOgImageAsync = download_og_image_async #: Псевдоним для :attr:`download_op_image` downloadOpImage = download_op_image + #: Псевдоним для :attr:`download_op_image_async` + downloadOpImageAsync = download_op_image_async #: Псевдоним для :attr:`get_tracks` getTracks = get_tracks + #: Псевдоним для :attr:`get_tracks_async` + getTracksAsync = get_tracks_async #: Псевдоним для :attr:`get_albums` getAlbums = get_albums + #: Псевдоним для :attr:`get_albums_async` + getAlbumsAsync = get_albums_async + #: Псевдоним для :attr:`like_async` + likeAsync = like_async + #: Псевдоним для :attr:`dislike_async` + dislikeAsync = dislike_async diff --git a/yandex_music/client.py b/yandex_music/client.py index 3da2d65..d8249b7 100644 --- a/yandex_music/client.py +++ b/yandex_music/client.py @@ -1,7 +1,7 @@ import functools import logging from datetime import datetime -from typing import Callable, Dict, List, Optional, Union +from typing import Dict, List, Optional, Union from yandex_music import ( Album, @@ -96,7 +96,6 @@ class Client(YandexMusicObject): Args: token (:obj:`str`, optional): Уникальный ключ для аутентификации. - fetch_account_status (:obj:`bool`, optional): Получить ли информацию об аккаунте при инициализации объекта. base_url (:obj:`str`, optional): Ссылка на API Yandex Music. request (:obj:`yandex_music.utils.request.Request`, optional): Пре-инициализация :class:`yandex_music.utils.request.Request`. @@ -110,7 +109,6 @@ class Client(YandexMusicObject): def __init__( self, token: str = None, - fetch_account_status: bool = True, base_url: str = None, request: Request = None, language: str = 'ru', @@ -146,14 +144,18 @@ class Client(YandexMusicObject): ) self.me = None - if fetch_account_status: - self.me = self.account_status() @property def request(self) -> Request: """:obj:`yandex_music.utils.request.Request`: Объект вспомогательного класса для отправки запросов.""" return self._request + @log + def init(self): + """Получение информацию об аккаунте использующихся в других запросах.""" + self.me = self.account_status() + return self + @log def account_status(self, timeout: Union[int, float] = None, *args, **kwargs) -> Optional[Status]: """Получение статуса аккаунта. Нет обязательных параметров. @@ -689,6 +691,7 @@ class Client(YandexMusicObject): return result == 'ok' + @log def albums_with_tracks( self, album_id: Union[str, int], timeout: Union[int, float] = None, *args, **kwargs ) -> Optional[Album]: @@ -1799,6 +1802,7 @@ class Client(YandexMusicObject): """ return self._like_action('artist', artist_ids, False, user_id, timeout, *args, **kwargs) + @log def users_likes_artists_remove( self, artist_ids: Union[List[Union[str, int]], str, int], @@ -1982,7 +1986,7 @@ class Client(YandexMusicObject): result = self._request.post(url, params, timeout=timeout, *args, **kwargs) - return de_list.get(object_type)(result, self) + return de_list[object_type](result, self) @log def artists( diff --git a/yandex_music/client_async.py b/yandex_music/client_async.py new file mode 100644 index 0000000..e18d2e1 --- /dev/null +++ b/yandex_music/client_async.py @@ -0,0 +1,2730 @@ +#################################################################### +# THIS IS AUTO GENERATED COPY OF client.py. DON'T EDIT IN BY HANDS # +#################################################################### + +import functools +import logging +from datetime import datetime +from typing import Dict, List, Optional, Union + +from yandex_music import ( + Album, + Artist, + ArtistAlbums, + ArtistTracks, + BriefInfo, + Dashboard, + DownloadInfo, + Experiments, + Feed, + Genre, + Landing, + Like, + PermissionAlerts, + Playlist, + PromoCodeStatus, + Search, + Settings, + ShotEvent, + Supplement, + StationResult, + StationTracksResult, + Status, + Suggestions, + SimilarTracks, + Track, + TracksList, + UserSettings, + YandexMusicObject, + ChartInfo, + TagResult, + PlaylistRecommendations, + LandingList, + QueueItem, + Queue, + __copyright__, + __license__, + __version__, +) +from yandex_music.utils.difference import Difference +from yandex_music.utils.request_async import Request + +de_list = { + 'artist': Artist.de_list, + 'album': Album.de_list, + 'track': Track.de_list, + 'playlist': Playlist.de_list, +} + +logging.getLogger(__name__).addHandler(logging.NullHandler()) + + +def log(method): + logger = logging.getLogger(method.__module__) + + @functools.wraps(method) + async def wrapper(*args, **kwargs): + logger.debug(f'Entering: {method.__name__}') + + result = await method(*args, **kwargs) + logger.debug(result) + + logger.debug(f'Exiting: {method.__name__}') + + return result + + return wrapper + + +class ClientAsync(YandexMusicObject): + """Класс, представляющий клиент Yandex Music. + + Note: + При `fetch_account_status = False` многие сокращения перестанут работать в связи с тем, что неоткуда будет взять + uid аккаунта для отправки запроса. Так же в большинстве методов придётся передавать `uid` явно. + + Для отключения предупреждений о новых полях установите `report_new_fields` в `False`. + + Доступные языки: en, uz, uk, us, ru, kk, hy. + + Поле `device` используется только при работе с очередью прослушивания. + + Attributes: + logger (:obj:`logging.Logger`): Объект логгера. + token (:obj:`str`): Уникальный ключ для аутентификации. + base_url (:obj:`str`): Ссылка на API Yandex Music. + me (:obj:`yandex_music.Status`): Информация об аккаунте. + device (:obj:`str`): Строка, содержащая сведения об устройстве, с которого выполняются запросы. + report_unknown_fields (:obj:`bool`): Включены ли предупреждения о неизвестных полях от API, + которых нет в библиотеке. + + Args: + token (:obj:`str`, optional): Уникальный ключ для аутентификации. + base_url (:obj:`str`, optional): Ссылка на API Yandex Music. + request (:obj:`yandex_music.utils.request.Request`, optional): Пре-инициализация + :class:`yandex_music.utils.request.Request`. + language (:obj:`str`, optional): Язык, на котором будут приходить ответы от API. + report_unknown_fields (:obj:`bool`, optional): Включены ли предупреждения о неизвестных полях от API, + которых нет в библиотеке. + """ + + notice_displayed = False + + def __init__( + self, + token: str = None, + base_url: str = None, + request: Request = None, + language: str = 'ru', + report_unknown_fields=True, + ) -> None: + if not ClientAsync.notice_displayed: + print(f'Yandex Music API v{__version__}, {__copyright__}') + print(f'Licensed under the terms of the {__license__}', end='\n\n') + ClientAsync.notice_displayed = True + + self.logger = logging.getLogger(__name__) + self.token = token + + if base_url is None: + base_url = 'https://api.music.yandex.net' + + self.base_url = base_url + + self.report_new_fields = report_unknown_fields + + if request: + self._request = request + self._request.set_and_return_client(self) + else: + self._request = Request(self) + + self.language = language + self._request.set_language(self.language) + + self.device = ( + 'os=Python; os_version=; manufacturer=Marshal; ' + 'model=Yandex Music API; clid=; device_id=random; uuid=random' + ) + + self.me = None + + @property + def request(self) -> Request: + """:obj:`yandex_music.utils.request.Request`: Объект вспомогательного класса для отправки запросов.""" + return self._request + + @log + async def init(self): + """Получение информацию об аккаунте использующихся в других запросах.""" + self.me = await self.account_status() + return self + + @log + async def account_status(self, timeout: Union[int, float] = None, *args, **kwargs) -> Optional[Status]: + """Получение статуса аккаунта. Нет обязательных параметров. + + Args: + timeout (:obj:`int` | :obj:`float`, optional): Если это значение указано, используется как время ожидания + ответа от сервера вместо указанного при создании пула. + **kwargs (:obj:`dict`, optional): Произвольные аргументы (будут переданы в запрос). + + Returns: + :obj:`yandex_music.Status` | :obj:`None`: Информация об аккаунте если он валиден, иначе :obj:`None`. + + Raises: + :class:`yandex_music.exceptions.YandexMusicError`: Базовое исключение библиотеки. + """ + + url = f'{self.base_url}/account/status' + + result = await self._request.get(url, timeout=timeout, *args, **kwargs) + + return Status.de_json(result, self) + + @log + async def account_settings(self, timeout: Union[int, float] = None, *args, **kwargs) -> Optional[UserSettings]: + """Получение настроек текущего пользователя. + + Args: + timeout (:obj:`int` | :obj:`float`, optional): Если это значение указано, используется как время ожидания + ответа от сервера вместо указанного при создании пула. + **kwargs (:obj:`dict`, optional): Произвольные аргументы (будут переданы в запрос). + + Returns: + :obj:`yandex_music.UserSettings` | :obj:`None`: Настройки пользователя если аккаунт валиден, + иначе :obj:`None`. + + Raises: + :class:`yandex_music.exceptions.YandexMusicError`: Базовое исключение библиотеки. + """ + + url = f'{self.base_url}/account/settings' + + result = await self._request.get(url, timeout=timeout, *args, **kwargs) + + return UserSettings.de_json(result, self) + + @log + async def account_settings_set( + self, + param: str = None, + value: Union[str, int, bool] = None, + data: Dict[str, Union[str, int, bool]] = None, + timeout: Union[int, float] = None, + *args, + **kwargs, + ) -> Optional[UserSettings]: + """Изменение настроек текущего пользователя. + + Note: + Доступные названия параметров есть поля в классе :class:`yandex_music.UserSettings`, только в CamelCase. + + Args: + param (:obj:`str`): Название параметра для изменения. + value (:obj:`str` | :obj:`int` | :obj:`bool`): Значение параметра. + data (:obj:`dict`): Словарь параметров и значений для множественного изменения. + timeout (:obj:`int` | :obj:`float`, optional): Если это значение указано, используется как время ожидания + ответа от сервера вместо указанного при создании пула. + **kwargs (:obj:`dict`, optional): Произвольные аргументы (будут переданы в запрос). + + Returns: + :obj:`yandex_music.UserSettings` | :obj:`None`: Настройки пользователя или :obj:`None`. + + Raises: + :class:`yandex_music.exceptions.YandexMusicError`: Базовое исключение библиотеки. + """ + + url = f'{self.base_url}/account/settings' + + if not data: + data = {param: value} + + result = await self._request.post(url, data=data, timeout=timeout, *args, **kwargs) + + return UserSettings.de_json(result, self) + + @log + async def settings(self, timeout: Union[int, float] = None, *args, **kwargs) -> Optional[Settings]: + """Получение предложений по покупке. Нет обязательных параметров. + + Args: + timeout (:obj:`int` | :obj:`float`, optional): Если это значение указано, используется как время ожидания + ответа от сервера вместо указанного при создании пула. + **kwargs (:obj:`dict`, optional): Произвольные аргументы (будут переданы в запрос). + + Returns: + :obj:`yandex_music.Settings` | :obj:`None`: Информацию о предлагаемых продуктах если аккаунт валиден + или :obj:`None`. + + Raises: + :class:`yandex_music.exceptions.YandexMusicError`: Базовое исключение библиотеки. + """ + + url = f'{self.base_url}/settings' + + result = await self._request.get(url, timeout=timeout, *args, **kwargs) + + return Settings.de_json(result, self) + + @log + async def permission_alerts(self, timeout: Union[int, float] = None, *args, **kwargs) -> Optional[PermissionAlerts]: + """Получение оповещений. Нет обязательных параметров. + + Args: + timeout (:obj:`int` | :obj:`float`, optional): Если это значение указано, используется как время ожидания + ответа от сервера вместо указанного при создании пула. + **kwargs (:obj:`dict`, optional): Произвольные аргументы (будут переданы в запрос). + + Returns: + :obj:`yandex_music.PermissionAlerts` | :obj:`None`: Оповещения если аккаунт валиден или :obj:`None`. + + Raises: + :class:`yandex_music.exceptions.YandexMusicError`: Базовое исключение библиотеки. + """ + + url = f'{self.base_url}/permission-alerts' + + result = await self._request.get(url, timeout=timeout, *args, **kwargs) + + return PermissionAlerts.de_json(result, self) + + @log + async def account_experiments(self, timeout: Union[int, float] = None, *args, **kwargs) -> Optional[Experiments]: + """Получение значений экспериментальных функций аккаунта. + + Args: + timeout (:obj:`int` | :obj:`float`, optional): Если это значение указано, используется как время ожидания + ответа от сервера вместо указанного при создании пула. + **kwargs (:obj:`dict`, optional): Произвольные аргументы (будут переданы в запрос). + + Returns: + :obj:`yandex_music.Experiments` | :obj:`None`: Состояние экспериментальных функций или :obj:`None`. + + Raises: + :class:`yandex_music.exceptions.YandexMusicError`: Базовое исключение библиотеки. + """ + + url = f'{self.base_url}/account/experiments' + + result = await self._request.get(url, timeout=timeout, *args, **kwargs) + + return Experiments.de_json(result, self) + + @log + async def consume_promo_code( + self, code: str, language: str = 'en', timeout: Union[int, float] = None, *args, **kwargs + ) -> Optional[PromoCodeStatus]: + """Активация промо-кода. + + Args: + code (:obj:`str`): Промо-код. + language (:obj:`str`, optional): Язык ответа API в ISO 639-1. + timeout (:obj:`int` | :obj:`float`, optional): Если это значение указано, используется как время ожидания + ответа от сервера вместо указанного при создании пула. + **kwargs (:obj:`dict`, optional): Произвольные аргументы (будут переданы в запрос). + + Returns: + :obj:`yandex_music.PromoCodeStatus` | :obj:`None`: Информация об активации промо-кода или :obj:`None`. + + Raises: + :class:`yandex_music.exceptions.YandexMusicError`: Базовое исключение библиотеки. + """ + + url = f'{self.base_url}/account/consume-promo-code' + + result = await self._request.post(url, {'code': code, 'language': language}, timeout=timeout, *args, **kwargs) + + return PromoCodeStatus.de_json(result, self) + + @log + async def feed(self, timeout: Union[int, float] = None, *args, **kwargs) -> Optional[Feed]: + """Получение потока информации (фида) подобранного под пользователя. Содержит умные плейлисты. + + Args: + timeout (:obj:`int` | :obj:`float`, optional): Если это значение указано, используется как время ожидания + ответа от сервера вместо указанного при создании пула. + **kwargs (:obj:`dict`, optional): Произвольные аргументы (будут переданы в запрос). + + Returns: + :obj:`yandex_music.Feed` | :obj:`None`: Умные плейлисты пользователя или :obj:`None`. + + Raises: + :class:`yandex_music.exceptions.YandexMusicError`: Базовое исключение библиотеки. + """ + + url = f'{self.base_url}/feed' + + result = await self._request.get(url, timeout=timeout, *args, **kwargs) + + return Feed.de_json(result, self) + + @log + async def feed_wizard_is_passed(self, timeout: Union[int, float] = None, *args, **kwargs) -> bool: + url = f'{self.base_url}/feed/wizard/is-passed' + + result = await self._request.get(url, timeout=timeout, *args, **kwargs) + + return result.get('is_wizard_passed') or False + + @log + async def landing( + self, blocks: Union[str, List[str]], timeout: Union[int, float] = None, *args, **kwargs + ) -> Optional[Landing]: + """Получение лендинг-страницы содержащий блоки с новыми релизами, чартами, плейлистами с новинками и т.д. + + Note: + Поддерживаемые типы блоков: `personalplaylists`, `promotions`, `new-releases`, `new-playlists`, `mixes`, + `chart`, `artists`, `albums`, `playlists`, `play_contexts`. + + Args: + blocks (:obj:`str` | :obj:`list` из :obj:`str`): Блок или список блоков необходимых для выдачи. + timeout (:obj:`int` | :obj:`float`, optional): Если это значение указано, используется как время ожидания + ответа от сервера вместо указанного при создании пула. + **kwargs (:obj:`dict`, optional): Произвольные аргументы (будут переданы в запрос). + + Returns: + :obj:`yandex_music.Landing` | :obj:`None`: Лендинг-страница или :obj:`None`. + + Raises: + :class:`yandex_music.exceptions.YandexMusicError`: Базовое исключение библиотеки. + """ + + url = f'{self.base_url}/landing3' + + result = await self._request.get( + url, {'blocks': blocks, 'eitherUserId': '10254713668400548221'}, timeout=timeout, *args, **kwargs + ) + + return Landing.de_json(result, self) + + @log + async def chart( + self, chart_option: str = '', timeout: Union[int, float] = None, *args, **kwargs + ) -> Optional[ChartInfo]: + """Получение чарта. + + Note: + `chart_option` - это постфикс к запросу из поля `menu` чарта. + Например, на сайте можно выбрать глобальный (world) чарт или российский (russia). + + Args: + chart_option (:obj:`str` optional): Параметры чарта. + timeout (:obj:`int` | :obj:`float`, optional): Если это значение указано, используется как время ожидания + ответа от сервера вместо указанного при создании пула. + **kwargs (:obj:`dict`, optional): Произвольные аргументы (будут переданы в запрос). + + Returns: + :obj:`yandex_music.ChartInfo`: Чарт. + + Raises: + :class:`yandex_music.exceptions.YandexMusicError`: Базовое исключение библиотеки. + """ + + url = f'{self.base_url}/landing3/chart' + + if chart_option: + url = f'{url}/{chart_option}' + + result = await self._request.get(url, timeout=timeout, *args, **kwargs) + + return ChartInfo.de_json(result, self) + + @log + async def new_releases(self, timeout: Union[int, float] = None, *args, **kwargs) -> Optional[LandingList]: + """Получение полного списка всех новых релизов (альбомов). + + Args: + timeout (:obj:`int` | :obj:`float`, optional): Если это значение указано, используется как время ожидания + ответа от сервера вместо указанного при создании пула. + **kwargs (:obj:`dict`, optional): Произвольные аргументы (будут переданы в запрос). + + Returns: + :obj:`yandex_music.LandingList`: Список новых альбомов. + + Raises: + :class:`yandex_music.exceptions.YandexMusicError`: Базовое исключение библиотеки. + """ + + url = f'{self.base_url}/landing3/new-releases' + + result = await self._request.get(url, timeout=timeout, *args, **kwargs) + + return LandingList.de_json(result, self) + + @log + async def new_playlists(self, timeout: Union[int, float] = None, *args, **kwargs) -> Optional[LandingList]: + """Получение полного списка всех новых плейлистов. + + Args: + timeout (:obj:`int` | :obj:`float`, optional): Если это значение указано, используется как время ожидания + ответа от сервера вместо указанного при создании пула. + **kwargs (:obj:`dict`, optional): Произвольные аргументы (будут переданы в запрос). + + Returns: + :obj:`yandex_music.LandingList`: Список новых плейлистов. + + Raises: + :class:`yandex_music.exceptions.YandexMusicError`: Базовое исключение библиотеки. + """ + + url = f'{self.base_url}/landing3/new-playlists' + + result = await self._request.get(url, timeout=timeout, *args, **kwargs) + + return LandingList.de_json(result, self) + + @log + async def podcasts(self, timeout: Union[int, float] = None, *args, **kwargs) -> Optional[LandingList]: + """Получение подкастов с лендинга. + + Args: + timeout (:obj:`int` | :obj:`float`, optional): Если это значение указано, используется как время ожидания + ответа от сервера вместо указанного при создании пула. + **kwargs (:obj:`dict`, optional): Произвольные аргументы (будут переданы в запрос). + + Returns: + :obj:`yandex_music.LandingList`: Список подскастов. + + Raises: + :class:`yandex_music.exceptions.YandexMusicError`: Базовое исключение библиотеки. + """ + + url = f'{self.base_url}/landing3/podcasts' + + result = await self._request.get(url, timeout=timeout, *args, **kwargs) + + return LandingList.de_json(result, self) + + @log + async def genres(self, timeout: Union[int, float] = None, *args, **kwargs) -> List[Genre]: + """Получение жанров музыки. + + Args: + timeout (:obj:`int` | :obj:`float`, optional): Если это значение указано, используется как время ожидания + ответа от сервера вместо указанного при создании пула. + **kwargs (:obj:`dict`, optional): Произвольные аргументы (будут переданы в запрос). + + Returns: + :obj:`list` из :obj:`yandex_music.Genre` | :obj:`None`: Жанры музыки или :obj:`None`. + + Raises: + :class:`yandex_music.exceptions.YandexMusicError`: Базовое исключение библиотеки. + """ + + url = f'{self.base_url}/genres' + + result = await self._request.get(url, timeout=timeout, *args, **kwargs) + + return Genre.de_list(result, self) + + @log + async def tags(self, tag_id: str, timeout: Union[int, float] = None, *args, **kwargs) -> Optional[TagResult]: + """Получение тега (подборки). + + Note: + Теги есть в `MixLink` у `Landing`, а также плейлистов в `.tags`. + + У `MixLink` есть `URL`, но `tag_id` только его последняя часть. + Например, `/tag/belarus/`. `Tag` - `belarus`. + + Args: + tag_id (:obj:`str`): Уникальный идентификатор тега. + timeout (:obj:`int` | :obj:`float`, optional): Если это значение указано, используется как время ожидания + ответа от сервера вместо указанного при создании пула. + **kwargs (:obj:`dict`, optional): Произвольные аргументы (будут переданы в запрос). + + Returns: + :obj:`yandex_music.TagResult`: Тег с плейлистами. + + Raises: + :class:`yandex_music.exceptions.YandexMusicError`: Базовое исключение библиотеки. + """ + + url = f'{self.base_url}/tags/{tag_id}/playlist-ids' + + result = await self._request.get(url, timeout=timeout, *args, **kwargs) + + return TagResult.de_json(result, self) + + @log + async def tracks_download_info( + self, + track_id: Union[str, int], + get_direct_links: bool = False, + timeout: Union[int, float] = None, + *args, + **kwargs, + ) -> List[DownloadInfo]: + """Получение информации о доступных вариантах загрузки трека. + + Args: + track_id (:obj:`str` | :obj:`list` из :obj:`str`): Уникальный идентификатор трека или треков. + get_direct_links (:obj:`bool`, optional): Получить ли при вызове метода прямую ссылку на загрузку. + timeout (:obj:`int` | :obj:`float`, optional): Если это значение указано, используется как время ожидания + ответа от сервера вместо указанного при создании пула. + **kwargs (:obj:`dict`, optional): Произвольные аргументы (будут переданы в запрос). + + Returns: + :obj:`list` из :obj:`yandex_music.DownloadInfo` | :obj:`None`: Варианты загрузки трека или :obj:`None`. + + Raises: + :class:`yandex_music.exceptions.YandexMusicError`: Базовое исключение библиотеки. + """ + + url = f'{self.base_url}/tracks/{track_id}/download-info' + + result = await self._request.get(url, timeout=timeout, *args, **kwargs) + + return DownloadInfo.de_list(result, self, get_direct_links) + + @log + async def track_supplement( + self, track_id: Union[str, int], timeout: Union[int, float] = None, *args, **kwargs + ) -> Optional[Supplement]: + """Получение дополнительной информации о треке. + + Args: + track_id (:obj:`str`): Уникальный идентификатор трека. + timeout (:obj:`int` | :obj:`float`, optional): Если это значение указано, используется как время ожидания + ответа от сервера вместо указанного при создании пула. + **kwargs (:obj:`dict`, optional): Произвольные аргументы (будут переданы в запрос). + + Returns: + :obj:`yandex_music.Supplement`: Дополнительная информация о треке. + + Raises: + :class:`yandex_music.exceptions.YandexMusicError`: Базовое исключение библиотеки. + """ + + url = f'{self.base_url}/tracks/{track_id}/supplement' + + result = await self._request.get(url, timeout=timeout, *args, **kwargs) + + return Supplement.de_json(result, self) + + @log + async def tracks_similar( + self, track_id: Union[str, int], timeout: Union[int, float] = None, *args, **kwargs + ) -> Optional[SimilarTracks]: + """Получение похожих треков. + + Args: + track_id (:obj:`str`): Уникальный идентификатор трека. + timeout (:obj:`int` | :obj:`float`, optional): Если это значение указано, используется как время ожидания + ответа от сервера вместо указанного при создании пула. + **kwargs (:obj:`dict`, optional): Произвольные аргументы (будут переданы в запрос). + + Returns: + :obj:`yandex_music.SimilarTracks`: Похожие треки на другой трек. + + Raises: + :class:`yandex_music.exceptions.YandexMusicError`: Базовое исключение библиотеки. + """ + + url = f'{self.base_url}/tracks/{track_id}/similar' + + result = await self._request.get(url, timeout=timeout, *args, **kwargs) + + return SimilarTracks.de_json(result, self) + + @log + async def play_audio( + self, + track_id: Union[str, int], + from_: str, + album_id: Union[str, int], + playlist_id: str = None, + from_cache: bool = False, + play_id: str = None, + uid: int = None, + timestamp: str = None, + track_length_seconds: int = 0, + total_played_seconds: int = 0, + end_position_seconds: int = 0, + client_now: str = None, + timeout: Union[int, float] = None, + *args, + **kwargs, + ) -> bool: + """Метод для отправки текущего состояния прослушиваемого трека. + + Args: + track_id (:obj:`str` | :obj:`int`): Уникальный идентификатор трека. + from_ (:obj:`str`): Наименования клиента с которого происходит прослушивание. + album_id (:obj:`str` | :obj:`int`): Уникальный идентификатор альбома. + playlist_id (:obj:`str`, optional): Уникальный идентификатор плейлиста, если таковой прослушивается. + from_cache (:obj:`bool`, optional): Проигрывается ли трек с кеша. + play_id (:obj:`str`, optional): Уникальный идентификатор проигрывания. + uid (:obj:`int`, optional): Уникальный идентификатор пользователя. + timestamp (:obj:`str`, optional): Текущая дата и время в ISO. + track_length_seconds (:obj:`int`, optional): Продолжительность трека в секундах. + total_played_seconds (:obj:`int`, optional): Сколько было всего воспроизведено трека в секундах. + end_position_seconds (:obj:`int`, optional): Окончательное значение воспроизведенных секунд. + client_now (:obj:`str`, optional): Текущая дата и время клиента в ISO. + timeout (:obj:`int` | :obj:`float`, optional): Если это значение указано, используется как время ожидания + ответа от сервера вместо указанного при создании пула. + **kwargs (:obj:`dict`, optional): Произвольные аргументы (будут переданы в запрос). + + Returns: + :obj:`bool`: :obj:`True` при успешном выполнении запроса, иначе :obj:`False`. + + Raises: + :class:`yandex_music.exceptions.YandexMusicError`: Базовое исключение библиотеки. + """ + + if uid is None and self.me is not None: + uid = self.me.account.uid + + url = f'{self.base_url}/play-audio' + + data = { + 'track-id': track_id, + 'from-cache': from_cache, + 'from': from_, + 'play-id': play_id or '', + 'uid': uid, + 'timestamp': timestamp or f'{datetime.now().isoformat()}Z', + 'track-length-seconds': track_length_seconds, + 'total-played-seconds': total_played_seconds, + 'end-position-seconds': end_position_seconds, + 'album-id': album_id, + 'playlist-id': playlist_id, + 'client-now': client_now or f'{datetime.now().isoformat()}Z', + } + + result = await self._request.post(url, data, timeout=timeout, *args, **kwargs) + + return result == 'ok' + + @log + async def albums_with_tracks( + self, album_id: Union[str, int], timeout: Union[int, float] = None, *args, **kwargs + ) -> Optional[Album]: + """Получение альбома по его уникальному идентификатору вместе с треками. + + Args: + album_id (:obj:`str` | :obj:`int`): Уникальный идентификатор альбома. + timeout (:obj:`int` | :obj:`float`, optional): Если это значение указано, используется как время ожидания + ответа от сервера вместо указанного при создании пула. + **kwargs (:obj:`dict`, optional): Произвольные аргументы (будут переданы в запрос). + + Returns: + :obj:`list` из :obj:`yandex_music.Album` | :obj:`None`: Альбом или :obj:`None`. + + Raises: + :class:`yandex_music.exceptions.YandexMusicError`: Базовое исключение библиотеки. + """ + + url = f'{self.base_url}/albums/{album_id}/with-tracks' + + result = await self._request.get(url, timeout=timeout, *args, **kwargs) + + return Album.de_json(result, self) + + @log + async def search( + self, + text: str, + nocorrect: bool = False, + type_: str = 'all', + page: int = 0, + playlist_in_best: bool = True, + timeout: Union[int, float] = None, + *args, + **kwargs, + ) -> Optional[Search]: + """Осуществление поиска по запросу и типу, получение результатов. + + Note: + Известные значения для поля `type_`: `all`, `artist`, `user`, `album`, `playlist`, `track`, `podcast`, + `podcast_episode`. + + При поиске `type=all` не возвращаются подкасты и эпизоды. Указывайте конкретный тип для поиска. + + Args: + text (:obj:`str`): Текст запроса. + nocorrect (:obj:`bool`): Если :obj:`False`, то ошибочный запрос будет исправлен. Например, запрос + "Гражданская абарона" будет исправлен на "Гражданская оборона". + type_ (:obj:`str`): Среди какого типа искать (трек, плейлист, альбом, исполнитель, пользователь, подкаст). + page (:obj:`int`): Номер страницы. + playlist_in_best (:obj:`bool`): Выдавать ли плейлисты лучшим вариантом поиска. + timeout (:obj:`int` | :obj:`float`, optional): Если это значение указано, используется как время ожидания + ответа от сервера вместо указанного при создании пула. + **kwargs (:obj:`dict`, optional): Произвольные аргументы (будут переданы в запрос). + + Returns: + :obj:`yandex_music.Search` | :obj:`None`: Результаты поиска или :obj:`None`. + + Raises: + :class:`yandex_music.exceptions.YandexMusicError`: Базовое исключение библиотеки. + """ + + url = f'{self.base_url}/search' + + params = { + 'text': text, + 'nocorrect': nocorrect, + 'type': type_, + 'page': page, + 'playlist-in-best': playlist_in_best, + } + + result = await self._request.get(url, params, timeout=timeout, *args, **kwargs) + + return Search.de_json(result, self) + + @log + async def search_suggest( + self, part: str, timeout: Union[int, float] = None, *args, **kwargs + ) -> Optional[Suggestions]: + """Получение подсказок по введенной части поискового запроса. + + Args: + part (:obj:`str`): Часть поискового запроса. + timeout (:obj:`int` | :obj:`float`, optional): Если это значение указано, используется как время ожидания + ответа от сервера вместо указанного при создании пула. + **kwargs (:obj:`dict`, optional): Произвольные аргументы (будут переданы в запрос). + + Returns: + :obj:`yandex_music.Suggestions` | :obj:`None`: Подсказки для запроса или :obj:`None`. + + Raises: + :class:`yandex_music.exceptions.YandexMusicError`: Базовое исключение библиотеки. + """ + + url = f'{self.base_url}/search/suggest' + + result = await self._request.get(url, {'part': part}, timeout=timeout, *args, **kwargs) + + return Suggestions.de_json(result, self) + + @log + async def users_settings( + self, user_id: Union[str, int] = None, timeout: Union[int, float] = None, *args, **kwargs + ) -> Optional[UserSettings]: + """Получение настроек пользователя. + + Note: + Для получения настроек пользователя нужно быть авторизованным или владеть `user_id`. + + Args: + user_id (:obj:`str` | :obj:`int`, optional): Уникальный идентификатор пользователя чьи настройки хотим + получить. + timeout (:obj:`int` | :obj:`float`, optional): Если это значение указано, используется как время ожидания + ответа от сервера вместо указанного при создании пула. + **kwargs (:obj:`dict`, optional): Произвольные аргументы (будут переданы в запрос). + + Returns: + :obj:`yandex_music.UserSettings` | :obj:`None`: Настройки пользователя или :obj:`None`. + + Raises: + :class:`yandex_music.exceptions.YandexMusicError`: Базовое исключение библиотеки. + """ + + if user_id is None and self.me is not None: + user_id = self.me.account.uid + + url = f'{self.base_url}/users/{user_id}/settings' + + result = await self._request.get(url, timeout=timeout, *args, **kwargs) + + return UserSettings.de_json(result.get('user_settings'), self) + + @log + async def users_playlists( + self, + kind: Union[List[Union[str, int]], str, int], + user_id: Union[str, int] = None, + timeout: Union[int, float] = None, + *args, + **kwargs, + ) -> Union[Playlist, List[Playlist]]: + """Получение плейлиста или списка плейлистов по уникальным идентификаторам. + + Note: + Если передан один `kind`, то вернётся не список плейлистов, а один плейлист. + + Args: + kind (:obj:`str` | :obj:`int` | :obj:`list` из :obj:`str` | :obj:`int`): Уникальный идентификатор плейлиста + или их список. + user_id (:obj:`str` | :obj:`int`, optional): Уникальный идентификатор пользователя владеющим плейлистом. + timeout (:obj:`int` | :obj:`float`, optional): Если это значение указано, используется как время ожидания + ответа от сервера вместо указанного при создании пула. + **kwargs (:obj:`dict`, optional): Произвольные аргументы (будут переданы в запрос). + + Returns: + :obj:`list` из :obj:`yandex_music.Playlist` | :obj:`yandex_music.Playlist` | :obj:`None`: + Список плейлистов или плейлист, иначе :obj:`None`. + + Raises: + :class:`yandex_music.exceptions.YandexMusicError`: Базовое исключение библиотеки. + """ + + if user_id is None and self.me is not None: + user_id = self.me.account.uid + + if isinstance(kind, list): + url = f'{self.base_url}/users/{user_id}/playlists' + + data = {'kinds': kind} + + result = await self._request.post(url, data, timeout=timeout, *args, **kwargs) + + return Playlist.de_list(result, self) + else: + url = f'{self.base_url}/users/{user_id}/playlists/{kind}' + result = await self._request.get(url, timeout=timeout, *args, **kwargs) + + return Playlist.de_json(result, self) + + @log + async def users_playlists_recommendations( + self, kind: Union[str, int], user_id: Union[str, int] = None, timeout: Union[int, float] = None, *args, **kwargs + ): + """Получение рекомендаций для плейлиста. + + Args: + kind (:obj:`str` | :obj:`int`): Уникальный идентификатор плейлиста. + user_id (:obj:`str` | :obj:`int`): Уникальный идентификатор пользователя владеющим плейлистом. + timeout (:obj:`int` | :obj:`float`, optional): Если это значение указано, используется как время ожидания + ответа от сервера вместо указанного при создании пула. + **kwargs (:obj:`dict`, optional): Произвольные аргументы (будут переданы в запрос). + + Returns: + :obj:`yandex_music.PlaylistRecommendations` | :obj:`None`: Рекомендации для плейлиста или :obj:`None`. + + Raises: + :class:`yandex_music.exceptions.YandexMusicError`: Базовое исключение библиотеки. + """ + if user_id is None and self.me is not None: + user_id = self.me.account.uid + + url = f'{self.base_url}/users/{user_id}/playlists/{kind}/recommendations' + + result = await self._request.get(url, timeout=timeout, *args, **kwargs) + + return PlaylistRecommendations.de_json(result, self) + + @log + async def users_playlists_create( + self, + title: str, + visibility: str = 'public', + user_id: Union[str, int] = None, + timeout: Union[int, float] = None, + *args, + **kwargs, + ) -> Optional[Playlist]: + """Создание плейлиста. + + Args: + title (:obj:`str`): Название. + visibility (:obj:`str`, optional): Модификатор доступа. + user_id (:obj:`str` | :obj:`int`, optional): Уникальный идентификатор пользователя владеющим плейлистом. + timeout (:obj:`int` | :obj:`float`, optional): Если это значение указано, используется как время ожидания + ответа от сервера вместо указанного при создании пула. + **kwargs (:obj:`dict`, optional): Произвольные аргументы (будут переданы в запрос). + + Returns: + :obj:`yandex_music.Playlist` | :obj:`None`: Созданный плейлист или :obj:`None`. + + Raises: + :class:`yandex_music.exceptions.YandexMusicError`: Базовое исключение библиотеки. + """ + + if user_id is None and self.me is not None: + user_id = self.me.account.uid + + url = f'{self.base_url}/users/{user_id}/playlists/create' + + data = {'title': title, 'visibility': visibility} + + result = await self._request.post(url, data, timeout=timeout, *args, **kwargs) + + return Playlist.de_json(result, self) + + @log + async def users_playlists_delete( + self, kind: Union[str, int], user_id: Union[str, int] = None, timeout: Union[int, float] = None, *args, **kwargs + ) -> bool: + """Удаление плейлиста. + + Args: + kind (:obj:`str` | :obj:`int`): Уникальный идентификатор плейлиста. + user_id (:obj:`str` | :obj:`int`, optional): Уникальный идентификатор пользователя владеющим плейлистом. + timeout (:obj:`int` | :obj:`float`, optional): Если это значение указано, используется как время ожидания + ответа от сервера вместо указанного при создании пула. + **kwargs (:obj:`dict`, optional): Произвольные аргументы (будут переданы в запрос). + + Returns: + :obj:`bool`: :obj:`True` при успешном выполнении запроса, иначе :obj:`False`. + + Raises: + :class:`yandex_music.exceptions.YandexMusicError`: Базовое исключение библиотеки. + """ + + if user_id is None and self.me is not None: + user_id = self.me.account.uid + + url = f'{self.base_url}/users/{user_id}/playlists/{kind}/delete' + + result = await self._request.post(url, timeout=timeout, *args, **kwargs) + + return result == 'ok' + + @log + async def users_playlists_name( + self, + kind: Union[str, int], + name: str, + user_id: Union[str, int] = None, + timeout: Union[int, float] = None, + *args, + **kwargs, + ) -> Optional[Playlist]: + """Изменение названия плейлиста. + + Args: + kind (:obj:`str` | :obj:`int`): Уникальный идентификатор плейлиста. + name (:obj:`str`): Новое название. + user_id (:obj:`str` | :obj:`int`, optional): Уникальный идентификатор пользователя владеющим плейлистом. + timeout (:obj:`int` | :obj:`float`, optional): Если это значение указано, используется как время ожидания + ответа от сервера вместо указанного при создании пула. + **kwargs (:obj:`dict`, optional): Произвольные аргументы (будут переданы в запрос). + + Returns: + :obj:`yandex_music.Playlist` | :obj:`None`: Изменённый плейлист или :obj:`None`. + + Raises: + :class:`yandex_music.exceptions.YandexMusicError`: Базовое исключение библиотеки. + """ + + if user_id is None and self.me is not None: + user_id = self.me.account.uid + + url = f'{self.base_url}/users/{user_id}/playlists/{kind}/name' + + result = await self._request.post(url, {'value': name}, timeout=timeout, *args, **kwargs) + + return Playlist.de_json(result, self) + + @log + async def users_playlists_visibility( + self, + kind: Union[str, int], + visibility: str, + user_id: Union[str, int] = None, + timeout: Union[int, float] = None, + *args, + **kwargs, + ) -> Optional[Playlist]: + """Изменение видимости плейлиста. + + Note: + Видимость (`visibility`) может быть задана только одним из двух значений: `private`, `public`. + + Args: + kind (:obj:`str` | :obj:`int`): Уникальный идентификатор плейлиста. + visibility (:obj:`str`): Новое название. + user_id (:obj:`str` | :obj:`int`, optional): Уникальный идентификатор пользователя владеющим плейлистом. + timeout (:obj:`int` | :obj:`float`, optional): Если это значение указано, используется как время ожидания + ответа от сервера вместо указанного при создании пула. + **kwargs (:obj:`dict`, optional): Произвольные аргументы (будут переданы в запрос). + + Returns: + :obj:`yandex_music.Playlist` | :obj:`None`: Изменённый плейлист или :obj:`None`. + + Raises: + :class:`yandex_music.exceptions.YandexMusicError`: Базовое исключение библиотеки. + """ + + if user_id is None and self.me is not None: + user_id = self.me.account.uid + + url = f'{self.base_url}/users/{user_id}/playlists/{kind}/visibility' + + result = await self._request.post(url, {'value': visibility}, timeout=timeout, *args, **kwargs) + + return Playlist.de_json(result, self) + + @log + async def users_playlists_change( + self, + kind: Union[str, int], + diff: str, + revision: int = 1, + user_id: Union[str, int] = None, + timeout: Union[int, float] = None, + *args, + **kwargs, + ) -> Optional[Playlist]: + """Изменение плейлиста. + + Note: + Для получения отличий есть вспомогательный класс :class:`yandex_music.utils.difference.Difference`. + + Так же существуют уже готовые методы-обёртки над операциями. + + Args: + kind (:obj:`str` | :obj:`int`): Уникальный идентификатор плейлиста. + revision (:obj:`int`): TODO. + diff (:obj:`str`): JSON представления отличий старого и нового плейлиста. + user_id (:obj:`str` | :obj:`int`, optional): Уникальный идентификатор пользователя владеющим плейлистом. + timeout (:obj:`int` | :obj:`float`, optional): Если это значение указано, используется как время ожидания + ответа от сервера вместо указанного при создании пула. + **kwargs (:obj:`dict`, optional): Произвольные аргументы (будут переданы в запрос). + + Returns: + :obj:`yandex_music.Playlist`: Изменённый плейлист или :obj:`None`. + + Raises: + :class:`yandex_music.exceptions.YandexMusicError`: Базовое исключение библиотеки. + """ + + if user_id is None and self.me is not None: + user_id = self.me.account.uid + + url = f'{self.base_url}/users/{user_id}/playlists/{kind}/change' + + data = {'kind': kind, 'revision': revision, 'diff': diff} + + result = await self._request.post(url, data, timeout=timeout, *args, **kwargs) + + return Playlist.de_json(result, self) + + @log + async def users_playlists_insert_track( + self, + kind: Union[str, int], + track_id: Union[str, int], + album_id: Union[str, int], + at: int = 0, + revision: int = 1, + user_id: Union[str, int] = None, + timeout: Union[int, float] = None, + *args, + **kwargs, + ) -> Optional[Playlist]: + """Добавление трека в плейлист. + + Note: + Трек можно вставить с любое место плейлиста задав индекс вставки (аргумент `at`). + + Args: + kind (:obj:`str` | :obj:`int`): Уникальный идентификатор плейлиста. + track_id (:obj:`str` | :obj:`int`): Уникальный идентификатор трека. + album_id (:obj:`str` | :obj:`int`): Уникальный идентификатор альбома. + at (:obj:`int`): Индекс для вставки. + revision (:obj:`int`): TODO. + user_id (:obj:`str` | :obj:`int`, optional): Уникальный идентификатор пользователя владеющим плейлистом. + timeout (:obj:`int` | :obj:`float`, optional): Если это значение указано, используется как время ожидания + ответа от сервера вместо указанного при создании пула. + **kwargs (:obj:`dict`, optional): Произвольные аргументы (будут переданы в запрос). + + Returns: + :obj:`yandex_music.Playlist`: Изменённый плейлист или :obj:`None`. + + Raises: + :class:`yandex_music.exceptions.YandexMusicError`: Базовое исключение библиотеки. + """ + + if user_id is None and self.me is not None: + user_id = self.me.account.uid + + diff = Difference().add_insert(at, {'id': track_id, 'album_id': album_id}) + + return await self.users_playlists_change(kind, diff.to_json(), revision, user_id, timeout, *args, **kwargs) + + @log + async def users_playlists_delete_track( + self, + kind: Union[str, int], + from_: int, + to: int, + revision: int = 1, + user_id: Union[str, int] = None, + timeout: Union[int, float] = None, + *args, + **kwargs, + ) -> Optional[Playlist]: + """Удаление треков из плейлиста. + + Note: + Для удаление необходимо указать границы с какого по какой элемент (трек) удалить. + + Args: + kind (:obj:`str` | :obj:`int`): Уникальный идентификатор плейлиста. + from_ (:obj:`int`): С какого индекса. + to (:obj:`int`): По какой индекс. + revision (:obj:`int`): TODO. + user_id (:obj:`str` | :obj:`int`, optional): Уникальный идентификатор пользователя владеющим плейлистом. + timeout (:obj:`int` | :obj:`float`, optional): Если это значение указано, используется как время ожидания + ответа от сервера вместо указанного при создании пула. + **kwargs (:obj:`dict`, optional): Произвольные аргументы (будут переданы в запрос). + + Returns: + :obj:`yandex_music.Playlist` | :obj:`None`: Изменённый плейлист или :obj:`None`. + + Raises: + :class:`yandex_music.exceptions.YandexMusicError`: Базовое исключение библиотеки. + """ + + if user_id is None and self.me is not None: + user_id = self.me.account.uid + + diff = Difference().add_delete(from_, to) + + return await self.users_playlists_change(kind, diff.to_json(), revision, user_id, timeout, *args, **kwargs) + + @log + async def rotor_account_status(self, timeout: Union[int, float] = None, *args, **kwargs) -> Optional[Status]: + """Получение статуса пользователя с дополнителньыми полями. + + Note: + Данный статус отличается от обычного наличием дополнительных полей, например, `skips_per_hour`. + + Args: + timeout (:obj:`int` | :obj:`float`, optional): Если это значение указано, используется как время ожидания + ответа от сервера вместо указанного при создании пула. + **kwargs (:obj:`dict`, optional): Произвольные аргументы (будут переданы в запрос). + + Returns: + :obj:`yandex_music.Status` | :obj:`None`: Статус пользователя с дополнительными полями от радио или + :obj:`None`. + + Raises: + :class:`yandex_music.exceptions.YandexMusicError`: Базовое исключение библиотеки. + """ + + url = f'{self.base_url}/rotor/account/status' + + result = await self._request.get(url, timeout=timeout, *args, **kwargs) + + return Status.de_json(result, self) + + @log + async def rotor_stations_dashboard(self, timeout: Union[int, float] = None, *args, **kwargs) -> Optional[Dashboard]: + """Получение рекомендованных станций текущего пользователя. + + Args: + timeout (:obj:`int` | :obj:`float`, optional): Если это значение указано, используется как время ожидания + ответа от сервера вместо указанного при создании пула. + **kwargs (:obj:`dict`, optional): Произвольные аргументы (будут переданы в запрос). + + Returns: + :obj:`yandex_music.Dashboard` | :obj:`None`: Рекомендованные станции или :obj:`None`. + + Raises: + :class:`yandex_music.exceptions.YandexMusicError`: Базовое исключение библиотеки. + """ + + url = f'{self.base_url}/rotor/stations/dashboard' + + result = await self._request.get(url, timeout=timeout, *args, **kwargs) + + return Dashboard.de_json(result, self) + + @log + async def rotor_stations_list( + self, language: str = 'ru', timeout: Union[int, float] = None, *args, **kwargs + ) -> List[StationResult]: + """Получение всех радиостанций с настройками пользователя. + + Note: + Чтобы определить что за тип станции (жанры, настроения, занятие и т.д.) необходимо смотреть в поле + `id_for_from`. + + Args: + language (:obj:`str`): Язык, на котором будет информация о станциях. + timeout (:obj:`int` | :obj:`float`, optional): Если это значение указано, используется как время ожидания + ответа от сервера вместо указанного при создании пула. + **kwargs (:obj:`dict`, optional): Произвольные аргументы (будут переданы в запрос). + + Returns: + :obj:`list` из :obj:`yandex_music.StationResult` | :obj:`None`: Станции или :obj:`None`. + + Raises: + :class:`yandex_music.exceptions.YandexMusicError`: Базовое исключение библиотеки. + """ + + url = f'{self.base_url}/rotor/stations/list' + + result = await self._request.get(url, {'language': language}, timeout=timeout, *args, **kwargs) + + return StationResult.de_list(result, self) + + @log + async def rotor_station_feedback( + self, + station: str, + type_: str, + timestamp: Union[str, float, int] = None, + from_: str = None, + batch_id: str = None, + total_played_seconds: Union[int, float] = None, + track_id: Union[str, int] = None, + timeout: Union[int, float] = None, + *args, + **kwargs, + ) -> bool: + """Отправка ответной реакции на происходящее при прослушивании радио. + + Note: + Сообщения о начале прослушивания радио, начале и конце трека, его пропуска. + + Известные типы фидбека: `radioStarted`, `trackStarted`, `trackFinished`, `skip`. + + Пример `station`: `user:onyourwave`, `genre:allrock`. + + Пример `from_`: `mobile-radio-user-123456789`. + + Args: + station (:obj:`str`): Станция. + type_ (:obj:`str`): Тип отправляемого фидбека. + timestamp (:obj:`str` | :obj:`float` | :obj:`int`, optional): Текущее время и дата. + from_ (:obj:`str`, optional): Откуда начато воспроизведение радио. + batch_id (:obj:`str`, optional): Уникальный идентификатор партии треков. Возвращается при получении треков. + total_played_seconds (:obj:`int` |:obj:`float`, optional): Сколько было проиграно секунд трека + перед действием. + track_id (:obj:`int` | :obj:`str`, optional): Уникальной идентификатор трека. + timeout (:obj:`int` | :obj:`float`, optional): Если это значение указано, используется как время ожидания + ответа от сервера вместо указанного при создании пула. + **kwargs (:obj:`dict`, optional): Произвольные аргументы (будут переданы в запрос). + + Returns: + :obj:`bool`: :obj:`True` при успешном выполнении запроса, иначе :obj:`False`. + + Raises: + :class:`yandex_music.exceptions.YandexMusicError`: Базовое исключение библиотеки. + """ + + if timestamp is None: + timestamp = datetime.now().timestamp() + + url = f'{self.base_url}/rotor/station/{station}/feedback' + + params = {} + data = {'type': type_, 'timestamp': timestamp} + + if batch_id: + params = {'batch-id': batch_id} + + if track_id: + data.update({'trackId': track_id}) + + if from_: + data.update({'from': from_}) + + if total_played_seconds: + data.update({'totalPlayedSeconds': total_played_seconds}) + + result = await self._request.post(url, params=params, json=data, timeout=timeout, *args, **kwargs) + + return result == 'ok' + + @log + async def rotor_station_feedback_radio_started( + self, + station: str, + from_: str, + batch_id: str = None, + timestamp: Union[str, float, int] = None, + timeout: Union[int, float] = None, + *args, + **kwargs, + ) -> bool: + """Сокращение для:: + + client.rotor_station_feedback(station, 'radioStarted', timestamp, from, *args, **kwargs) + + Returns: + :obj:`bool`: :obj:`True` при успешном выполнении запроса, иначе :obj:`False`. + + Raises: + :class:`yandex_music.exceptions.YandexMusicError`: Базовое исключение библиотеки. + """ + return await self.rotor_station_feedback( + station, 'radioStarted', timestamp, from_=from_, batch_id=batch_id, timeout=timeout, *args, **kwargs + ) + + @log + async def rotor_station_feedback_track_started( + self, + station: str, + track_id: Union[str, int], + batch_id: str = None, + timestamp: Union[str, float, int] = None, + timeout: Union[int, float] = None, + *args, + **kwargs, + ) -> bool: + """Сокращение для:: + + client.rotor_station_feedback(station, 'trackStarted', timestamp, track_id, *args, **kwargs) + + Returns: + :obj:`bool`: :obj:`True` при успешном выполнении запроса, иначе :obj:`False`. + + Raises: + :class:`yandex_music.exceptions.YandexMusicError`: Базовое исключение библиотеки. + """ + return await self.rotor_station_feedback( + station, 'trackStarted', timestamp, track_id=track_id, batch_id=batch_id, timeout=timeout, *args, **kwargs + ) + + @log + async def rotor_station_feedback_track_finished( + self, + station: str, + track_id: Union[str, int], + total_played_seconds: float, + batch_id: str = None, + timestamp: Union[str, float, int] = None, + timeout: Union[int, float] = None, + *args, + **kwargs, + ) -> bool: + """Сокращение для:: + + client.rotor_station_feedback(station, 'trackFinished', timestamp, track_id, total_played_seconds, + *args, **kwargs) + + Returns: + :obj:`bool`: :obj:`True` при успешном выполнении запроса, иначе :obj:`False`. + + Raises: + :class:`yandex_music.exceptions.YandexMusicError`: Базовое исключение библиотеки. + """ + return await self.rotor_station_feedback( + station, + 'trackFinished', + timestamp, + track_id=track_id, + total_played_seconds=total_played_seconds, + batch_id=batch_id, + timeout=timeout, + *args, + **kwargs, + ) + + @log + async def rotor_station_feedback_skip( + self, + station: str, + track_id: Union[str, int], + total_played_seconds: float, + batch_id: str = None, + timestamp: Union[str, float, int] = None, + timeout: Union[int, float] = None, + *args, + **kwargs, + ) -> bool: + """Сокращение для:: + + client.rotor_station_feedback(station, 'skip', timestamp, track_id, total_played_seconds, + *args, **kwargs) + + Returns: + :obj:`bool`: :obj:`True` при успешном выполнении запроса, иначе :obj:`False`. + + Raises: + :class:`yandex_music.exceptions.YandexMusicError`: Базовое исключение библиотеки. + """ + return await self.rotor_station_feedback( + station, + 'skip', + timestamp, + track_id=track_id, + total_played_seconds=total_played_seconds, + batch_id=batch_id, + timeout=timeout, + *args, + **kwargs, + ) + + @log + async def rotor_station_info( + self, station: str, timeout: Union[int, float] = None, *args, **kwargs + ) -> List[StationResult]: + """Получение информации о станции и пользовательских настроек на неё. + + Args: + station (:obj:`str`): Станция. + timeout (:obj:`int` | :obj:`float`, optional): Если это значение указано, используется как время ожидания + ответа от сервера вместо указанного при создании пула. + **kwargs (:obj:`dict`, optional): Произвольные аргументы (будут переданы в запрос). + + Returns: + :obj:`list` из :obj:`yandex_music.StationResult` | :obj:`None`: Информация о станции или :obj:`None`. + + Raises: + :class:`yandex_music.exceptions.YandexMusicError`: Базовое исключение библиотеки. + """ + + url = f'{self.base_url}/rotor/station/{station}/info' + + result = await self._request.get(url, timeout=timeout, *args, **kwargs) + + return StationResult.de_list(result, self) + + @log + async def rotor_station_settings2( + self, + station: str, + mood_energy: str, + diversity: str, + language: str = 'not-russian', + type_: str = 'rotor', + timeout: Union[int, float] = None, + *args, + **kwargs, + ) -> bool: + """Изменение настроек определённой станции. + + Note: + Доступные значения для `mood_energy`: `fun`, `active`, `calm`, `sad`, `all`. + + Доступные значения для `diversity`: `favorite`, `popular`, `discover`, `default`. + + Доступные значения для `language`: `not-russian`, `russian`, `any`. + + Доступные значения для `type_`: `rotor`, `generative`. + + У станций в `restrictions` есть Enum'ы, а в них `possible_values` - доступные значения для поля. + + Args: + station (:obj:`str`): Станция. + mood_energy (:obj:`str`): Настроение. + diversity (:obj:`str`): Треки. + language (:obj:`str`): Язык. + type_ (:obj:`str`): Тип. + timeout (:obj:`int` | :obj:`float`, optional): Если это значение указано, используется как время ожидания + ответа от сервера вместо указанного при создании пула. + **kwargs (:obj:`dict`, optional): Произвольные аргументы (будут переданы в запрос). + + Returns: + :obj:`bool`: :obj:`True` при успешном выполнении запроса, иначе :obj:`False`. + + Raises: + :class:`yandex_music.exceptions.YandexMusicError`: Базовое исключение библиотеки. + """ + + url = f'{self.base_url}/rotor/station/{station}/settings3' + + data = {'moodEnergy': mood_energy, 'diversity': diversity, 'type': type_} + + if language: + data.update({'language': language}) + + result = await self._request.post(url, json=data, timeout=timeout, *args, **kwargs) + + return result == 'ok' + + @log + async def rotor_station_tracks( + self, + station: str, + settings2: bool = True, + queue: Union[str, int] = None, + timeout: Union[int, float] = None, + *args, + **kwargs, + ) -> Optional[StationTracksResult]: + """Получение цепочки треков определённой станции. + + Note: + Запуск потока по сущности сервиса осуществляется через станцию `:`. + Например, станцией для запуска потока по треку будет `track:1234`. + + Для продолжения цепочки треков необходимо: + + 1. Передавать `ID` трека, что был до этого (первый в цепочки). + 2. Отправить фидбек о конче или скипе трека, что был передан в `queue`. + 3. Отправить фидбек о начале следующего трека (второй в цепочки). + 4. Выполнить запрос получения треков. В ответе придёт новые треки или произойдёт сдвиг цепочки на 1 элемент. + + Проход по цепочке до коцна не изучен. Часто встречаются дубликаты. + + Все официальные клиенты выполняют запросы с `settings2 = True`. + + Args: + station (:obj:`str`): Станция. + settings2 (:obj:`bool`, optional): Использовать ли второй набор настроек. + queue (:obj:`str` | :obj:`int` , optional): Уникальной идентификатор трека, который только что был. + timeout (:obj:`int` | :obj:`float`, optional): Если это значение указано, используется как время ожидания + ответа от сервера вместо указанного при создании пула. + **kwargs (:obj:`dict`, optional): Произвольные аргументы (будут переданы в запрос). + + Returns: + :obj:`yandex_music.StationTracksResult` | :obj:`None`: Последовательность треков станции или :obj:`None`. + + Raises: + :class:`yandex_music.exceptions.YandexMusicError`: Базовое исключение библиотеки. + """ + + url = f'{self.base_url}/rotor/station/{station}/tracks' + + params = {} + if settings2: + params = {'settings2': True} + + if queue: + params = {'queue': queue} + + result = await self._request.get(url, params=params, timeout=timeout, *args, **kwargs) + + return StationTracksResult.de_json(result, self) + + @log + async def artists_brief_info( + self, artist_id: Union[str, int], timeout: Union[int, float] = None, *args, **kwargs + ) -> Optional[BriefInfo]: + """Получение информации об артисте. + + Args: + artist_id (:obj:`str` | :obj:`int`): Уникальный идентификатор исполнителя. + timeout (:obj:`int` | :obj:`float`, optional): Если это значение указано, используется как время ожидания + ответа от сервера вместо указанного при создании пула. + **kwargs (:obj:`dict`, optional): Произвольные аргументы (будут переданы в запрос). + + Returns: + :obj:`yandex_music.BriefInfo` | :obj:`None`: Информация об артисте или :obj:`None`. + + Raises: + :class:`yandex_music.exceptions.YandexMusicError`: Базовое исключение библиотеки. + """ + url = f'{self.base_url}/artists/{artist_id}/brief-info' + + result = await self._request.get(url, timeout=timeout, *args, **kwargs) + + return BriefInfo.de_json(result, self) + + @log + async def artists_tracks( + self, + artist_id: Union[str, int], + page: int = 0, + page_size: int = 20, + timeout: Union[int, float] = None, + *args, + **kwargs, + ) -> Optional[ArtistTracks]: + """Получение треков артиста. + + Args: + artist_id (:obj:`str` | :obj:`int`): Уникальный идентификатор артиста. + page (:obj:`int`, optional): Номер страницы. + page_size (:obj:`int`, optional): Количество треков на странице. + timeout (:obj:`int` | :obj:`float`, optional): Если это значение указано, используется как время ожидания + ответа от сервера вместо указанного при создании пула. + **kwargs (:obj:`dict`, optional): Произвольные аргументы (будут переданы в запрос). + + Returns: + :obj:`yandex_music.ArtistsTracks` | :obj:`None`: Страница списка треков артиста или :obj:`None`. + + Raises: + :class:`yandex_music.exceptions.YandexMusicError`: Базовое исключение библиотеки. + """ + + url = f'{self.base_url}/artists/{artist_id}/tracks' + + params = {'page': page, 'page-size': page_size} + + result = await self._request.get(url, params, timeout=timeout, *args, **kwargs) + + return ArtistTracks.de_json(result, self) + + @log + async def artists_direct_albums( + self, + artist_id: Union[str, int], + page: int = 0, + page_size: int = 20, + sort_by: str = 'year', + timeout: Union[int, float] = None, + *args, + **kwargs, + ) -> Optional[ArtistAlbums]: + """Получение альбомов артиста. + + Note: + Известные значения для `sort_by`: `year`, `rating`. + + Args: + artist_id (:obj:`str` | :obj:`int`): Уникальный идентификатор артиста. + page (:obj:`int`, optional): Номер страницы. + page_size (:obj:`int`, optional): Количество альбомов на странице. + sort_by (:obj:`str`, optional): Параметр для сортировки. + timeout (:obj:`int` | :obj:`float`, optional): Если это значение указано, используется как время ожидания + ответа от сервера вместо указанного при создании пула. + **kwargs (:obj:`dict`, optional): Произвольные аргументы (будут переданы в запрос). + + Returns: + :obj:`yandex_music.ArtistAlbums` | :obj:`None`: Страница списка альбомов артиста или :obj:`None`. + + Raises: + :class:`yandex_music.exceptions.YandexMusicError`: Базовое исключение библиотеки. + """ + + url = f'{self.base_url}/artists/{artist_id}/direct-albums' + + params = {'sort-by': sort_by, 'page': page, 'page-size': page_size} + + result = await self._request.get(url, params, timeout=timeout, *args, **kwargs) + + return ArtistAlbums.de_json(result, self) + + async def _like_action( + self, + object_type: str, + ids: Union[List[Union[str, int]], str, int], + remove: bool = False, + user_id: Union[str, int] = None, + timeout: Union[int, float] = None, + *args, + **kwargs, + ) -> bool: + """Действия с отметкой "Мне нравится". + + Note: + Типы объектов: `track` - трек, `artist` - исполнитель, `playlist` - плейлист, `album` - альбом. + + Идентификатор плейлиста указывается в формате `owner_id:playlist_id`. Где `playlist_id` - идентификатор + плейлиста, `owner_id` - уникальный идентификатор владельца плейлиста. + + Args: + object_type (:obj:`str`): Тип объекта. + ids (:obj:`str` | :obj:`int` | :obj:`list` из :obj:`str` | :obj:`list` из :obj:`int`): Уникальный + идентификатор объекта или объектов. + remove (:obj:`bool`, optional): Если :obj:`True` то снимает отметку, иначе ставит. + user_id (:obj:`str` | :obj:`int`, optional): Уникальный идентификатор пользователя. Если не указан + используется ID текущего пользователя. + timeout (:obj:`int` | :obj:`float`, optional): Если это значение указано, используется как время ожидания + ответа от сервера вместо указанного при создании пула. + **kwargs (:obj:`dict`, optional): Произвольные аргументы (будут переданы в запрос). + + Returns: + :obj:`bool`: :obj:`True` при успешном выполнении запроса, иначе :obj:`False`. + + Raises: + :class:`yandex_music.exceptions.YandexMusicError`: Базовое исключение библиотеки. + """ + if user_id is None and self.me is not None: + user_id = self.me.account.uid + + action = 'remove' if remove else 'add-multiple' + url = f'{self.base_url}/users/{user_id}/likes/{object_type}s/{action}' + + result = await self._request.post(url, {f'{object_type}-ids': ids}, timeout=timeout, *args, **kwargs) + + if object_type == 'track': + return 'revision' in result + + return result == 'ok' + + @log + async def users_likes_tracks_add( + self, + track_ids: Union[List[Union[str, int]], str, int], + user_id: Union[str, int] = None, + timeout: Union[int, float] = None, + *args, + **kwargs, + ) -> bool: + """Поставить отметку "Мне нравится" треку/трекам. + + Note: + Так же снимает отметку "Не рекомендовать" если она есть. + + Args: + track_ids (:obj:`str` | :obj:`int` | :obj:`list` из :obj:`str` | :obj:`list` из :obj:`int`): Уникальный + идентификатор трека или треков. + user_id (:obj:`str` | :obj:`int`, optional): Уникальный идентификатор пользователя. Если не указан + используется ID текущего пользователя. + timeout (:obj:`int` | :obj:`float`, optional): Если это значение указано, используется как время ожидания + ответа от сервера вместо указанного при создании пула. + **kwargs (:obj:`dict`, optional): Произвольные аргументы (будут переданы в запрос). + + Returns: + :obj:`bool`: :obj:`True` при успешном выполнении запроса, иначе :obj:`False`. + + Raises: + :class:`yandex_music.exceptions.YandexMusicError`: Базовое исключение библиотеки. + """ + return await self._like_action('track', track_ids, False, user_id, timeout, *args, **kwargs) + + @log + async def users_likes_tracks_remove( + self, + track_ids: Union[List[Union[str, int]], str, int], + user_id: Union[str, int] = None, + timeout: Union[int, float] = None, + *args, + **kwargs, + ) -> bool: + """Снять отметку "Мне нравится" у трека/треков. + + Args: + track_ids (:obj:`str` | :obj:`int` | :obj:`list` из :obj:`str` | :obj:`list` из :obj:`int`): Уникальный + идентификатор трека или треков. + user_id (:obj:`str` | :obj:`int`, optional): Уникальный идентификатор пользователя. Если не указан + используется ID текущего пользователя. + timeout (:obj:`int` | :obj:`float`, optional): Если это значение указано, используется как время ожидания + ответа от сервера вместо указанного при создании пула. + **kwargs (:obj:`dict`, optional): Произвольные аргументы (будут переданы в запрос). + + Returns: + :obj:`bool`: :obj:`True` при успешном выполнении запроса, иначе :obj:`False`. + + Raises: + :class:`yandex_music.exceptions.YandexMusicError`: Базовое исключение библиотеки. + """ + return await self._like_action('track', track_ids, True, user_id, timeout, *args, **kwargs) + + @log + async def users_likes_artists_add( + self, + artist_ids: Union[List[Union[str, int]], str, int], + user_id: Union[str, int] = None, + timeout: Union[int, float] = None, + *args, + **kwargs, + ) -> bool: + """Поставить отметку "Мне нравится" исполнителю/исполнителям. + + Args: + artist_ids (:obj:`str` | :obj:`int` | :obj:`list` из :obj:`str` | :obj:`list` из :obj:`int`): Уникальный + идентификатор артиста или артистов. + user_id (:obj:`str` | :obj:`int`, optional): Уникальный идентификатор пользователя. Если не указан + используется ID текущего пользователя. + timeout (:obj:`int` | :obj:`float`, optional): Если это значение указано, используется как время ожидания + ответа от сервера вместо указанного при создании пула. + **kwargs (:obj:`dict`, optional): Произвольные аргументы (будут переданы в запрос). + + Returns: + :obj:`bool`: :obj:`True` при успешном выполнении запроса, иначе :obj:`False`. + + Raises: + :class:`yandex_music.exceptions.YandexMusicError`: Базовое исключение библиотеки. + """ + return await self._like_action('artist', artist_ids, False, user_id, timeout, *args, **kwargs) + + @log + async def users_likes_artists_remove( + self, + artist_ids: Union[List[Union[str, int]], str, int], + user_id: Union[str, int] = None, + timeout: Union[int, float] = None, + *args, + **kwargs, + ) -> bool: + """Снять отметку "Мне нравится" у исполнителя/исполнителей. + + Args: + artist_ids (:obj:`str` | :obj:`int` | :obj:`list` из :obj:`str` | :obj:`list` из :obj:`int`): Уникальный + идентификатор артиста или артистов. + user_id (:obj:`str` | :obj:`int`, optional): Уникальный идентификатор пользователя. Если не указан + используется ID текущего пользователя. + timeout (:obj:`int` | :obj:`float`, optional): Если это значение указано, используется как время ожидания + ответа от сервера вместо указанного при создании пула. + **kwargs (:obj:`dict`, optional): Произвольные аргументы (будут переданы в запрос). + + Returns: + :obj:`bool`: :obj:`True` при успешном выполнении запроса, иначе :obj:`False`. + + Raises: + :class:`yandex_music.exceptions.YandexMusicError`: Базовое исключение библиотеки. + """ + return await self._like_action('artist', artist_ids, True, user_id, timeout, *args, **kwargs) + + @log + async def users_likes_playlists_add( + self, + playlist_ids: Union[List[Union[str, int]], str, int], + user_id: Union[str, int] = None, + timeout: Union[int, float] = None, + *args, + **kwargs, + ) -> bool: + """Поставить отметку "Мне нравится" плейлисту/плейлистам. + + Note: + Идентификатор плейлиста указывается в формате `owner_id:playlist_id`. Где `playlist_id` - идентификатор + плейлиста, `owner_id` - уникальный идентификатор владельца плейлиста. + + Args: + playlist_ids (:obj:`str` | :obj:`int` | :obj:`list` из :obj:`str` | :obj:`list` из :obj:`int`): Уникальный + идентификатор плейлиста или плейлистов. + user_id (:obj:`str` | :obj:`int`, optional): Уникальный идентификатор пользователя. Если не указан + используется ID текущего пользователя. + timeout (:obj:`int` | :obj:`float`, optional): Если это значение указано, используется как время ожидания + ответа от сервера вместо указанного при создании пула. + **kwargs (:obj:`dict`, optional): Произвольные аргументы (будут переданы в запрос). + + Returns: + :obj:`bool`: :obj:`True` при успешном выполнении запроса, иначе :obj:`False`. + + Raises: + :class:`yandex_music.exceptions.YandexMusicError`: Базовое исключение библиотеки. + """ + return await self._like_action('playlist', playlist_ids, False, user_id, timeout, *args, **kwargs) + + @log + async def users_likes_playlists_remove( + self, + playlist_ids: Union[List[Union[str, int]], str, int], + user_id: Union[str, int] = None, + timeout: Union[int, float] = None, + *args, + **kwargs, + ) -> bool: + """Снять отметку "Мне нравится" у плейлиста/плейлистов. + + Note: + Идентификатор плейлиста указывается в формате `owner_id:playlist_id`. Где `playlist_id` - идентификатор + плейлиста, `owner_id` - уникальный идентификатор владельца плейлиста. + + Args: + playlist_ids (:obj:`str` | :obj:`int` | :obj:`list` из :obj:`str` | :obj:`list` из :obj:`int`): Уникальный + идентификатор плейлиста или плейлистов. + user_id (:obj:`str` | :obj:`int`, optional): Уникальный идентификатор пользователя. Если не указан + используется ID текущего пользователя. + timeout (:obj:`int` | :obj:`float`, optional): Если это значение указано, используется как время ожидания + ответа от сервера вместо указанного при создании пула. + **kwargs (:obj:`dict`, optional): Произвольные аргументы (будут переданы в запрос). + + Returns: + :obj:`bool`: :obj:`True` при успешном выполнении запроса, иначе :obj:`False`. + + Raises: + :class:`yandex_music.exceptions.YandexMusicError`: Базовое исключение библиотеки. + """ + return await self._like_action('playlist', playlist_ids, True, user_id, timeout, *args, **kwargs) + + @log + async def users_likes_albums_add( + self, + album_ids: Union[List[Union[str, int]], str, int], + user_id: Union[str, int] = None, + timeout: Union[int, float] = None, + *args, + **kwargs, + ) -> bool: + """Поставить отметку "Мне нравится" альбому/альбомам. + + Args: + album_ids (:obj:`str` | :obj:`int` | :obj:`list` из :obj:`str` | :obj:`list` из :obj:`int`): Уникальный + идентификатор артиста или артистов. + user_id (:obj:`str` | :obj:`int`, optional): Уникальный идентификатор пользователя. Если не указан + используется ID текущего пользователя. + timeout (:obj:`int` | :obj:`float`, optional): Если это значение указано, используется как время ожидания + ответа от сервера вместо указанного при создании пула. + **kwargs (:obj:`dict`, optional): Произвольные аргументы (будут переданы в запрос). + + Returns: + :obj:`bool`: :obj:`True` при успешном выполнении запроса, иначе :obj:`False`. + + Raises: + :class:`yandex_music.exceptions.YandexMusicError`: Базовое исключение библиотеки. + """ + return await self._like_action('album', album_ids, False, user_id, timeout, *args, **kwargs) + + @log + async def users_likes_albums_remove( + self, + album_ids: Union[List[Union[str, int]], str, int], + user_id: Union[str, int] = None, + timeout: Union[int, float] = None, + *args, + **kwargs, + ) -> bool: + """Снять отметку "Мне нравится" у альбома/альбомов. + + Args: + album_ids (:obj:`str` | :obj:`int` | :obj:`list` из :obj:`str` | :obj:`list` из :obj:`int`): Уникальный + идентификатор артиста или артистов. + user_id (:obj:`str` | :obj:`int`, optional): Уникальный идентификатор пользователя. Если не указан + используется ID текущего пользователя. + timeout (:obj:`int` | :obj:`float`, optional): Если это значение указано, используется как время ожидания + ответа от сервера вместо указанного при создании пула. + **kwargs (:obj:`dict`, optional): Произвольные аргументы (будут переданы в запрос). + + Returns: + :obj:`bool`: :obj:`True` при успешном выполнении запроса, иначе :obj:`False`. + + Raises: + :class:`yandex_music.exceptions.YandexMusicError`: Базовое исключение библиотеки. + """ + return await self._like_action('album', album_ids, True, user_id, timeout, *args, **kwargs) + + async def _get_list( + self, + object_type: str, + ids: Union[List[Union[str, int]], int, str], + params: dict = None, + timeout: Union[int, float] = None, + *args, + **kwargs, + ) -> List[Union[Artist, Album, Track, Playlist]]: + """Получение объекта/объектов. + + Args: + object_type (:obj:`str`): Тип объекта. + ids (:obj:`str` | :obj:`int` | :obj:`list` из :obj:`str` | :obj:`list` из :obj:`int`): Уникальный + идентификатор объекта или объектов. + params (:obj:`dict`, optional): Параметры, которые будут переданы в запрос. + timeout (:obj:`int` | :obj:`float`, optional): Если это значение указано, используется как время ожидания + ответа от сервера вместо указанного при создании пула. + **kwargs (:obj:`dict`, optional): Произвольные аргументы (будут переданы в запрос). + + Returns: + :obj:`list` из :obj:`yandex_music.Artist` | :obj:`list` из :obj:`yandex_music.Album` | + :obj:`list` из :obj:`yandex_music.Track` | :obj:`list` из :obj:`yandex_music.Playlist`: Запрошенный + объект. + + Raises: + :class:`yandex_music.exceptions.YandexMusicError`: Базовое исключение библиотеки. + """ + if params is None: + params = {} + params.update({f'{object_type}-ids': ids}) + + url = f'{self.base_url}/{object_type}s' + ('/list' if object_type == 'playlist' else '') + + result = await self._request.post(url, params, timeout=timeout, *args, **kwargs) + + return de_list[object_type](result, self) + + @log + async def artists( + self, artist_ids: Union[List[Union[str, int]], int, str], timeout: Union[int, float] = None, *args, **kwargs + ) -> List[Artist]: + """Получение исполнителя/исполнителей. + + Args: + artist_ids (:obj:`str` | :obj:`int` | :obj:`list` из :obj:`str` | :obj:`list` из :obj:`int`): Уникальный + идентификатор исполнителя или исполнителей. + timeout (:obj:`int` | :obj:`float`, optional): Если это значение указано, используется как время ожидания + ответа от сервера вместо указанного при создании пула. + **kwargs (:obj:`dict`, optional): Произвольные аргументы (будут переданы в запрос). + + Returns: + :obj:`list` из :obj:`yandex_music.Artist`: Исполнитель или исполнители. + + Raises: + :class:`yandex_music.exceptions.YandexMusicError`: Базовое исключение библиотеки. + """ + return await self._get_list('artist', artist_ids, timeout=timeout, *args, **kwargs) + + @log + async def albums( + self, album_ids: Union[List[Union[str, int]], int, str], timeout: Union[int, float] = None, *args, **kwargs + ) -> List[Album]: + """Получение альбома/альбомов. + + Args: + album_ids (:obj:`str` | :obj:`int` | :obj:`list` из :obj:`str` | :obj:`list` из :obj:`int`): Уникальный + идентификатор альбома или альбомов. + timeout (:obj:`int` | :obj:`float`, optional): Если это значение указано, используется как время ожидания + ответа от сервера вместо указанного при создании пула. + **kwargs (:obj:`dict`, optional): Произвольные аргументы (будут переданы в запрос). + + Returns: + :obj:`list` из :obj:`yandex_music.Album`: Альбом или альбомы. + + Raises: + :class:`yandex_music.exceptions.YandexMusicError`: Базовое исключение библиотеки. + """ + return await self._get_list('album', album_ids, timeout=timeout, *args, **kwargs) + + @log + async def tracks( + self, + track_ids: Union[List[Union[str, int]], int, str], + with_positions: bool = True, + timeout: Union[int, float] = None, + *args, + **kwargs, + ) -> List[Track]: + """Получение трека/треков. + + Args: + track_ids (:obj:`str` | :obj:`int` | :obj:`list` из :obj:`str` | :obj:`list` из :obj:`int`): Уникальный + идентификатор трека или треков. + with_positions (:obj:`bool`, optional): С позициями TODO. + timeout (:obj:`int` | :obj:`float`, optional): Если это значение указано, используется как время ожидания + ответа от сервера вместо указанного при создании пула. + **kwargs (:obj:`dict`, optional): Произвольные аргументы (будут переданы в запрос). + + Returns: + :obj:`list` из :obj:`yandex_music.Track`: Трек или Треки. + + Raises: + :class:`yandex_music.exceptions.YandexMusicError`: Базовое исключение библиотеки. + """ + return await self._get_list('track', track_ids, {'with-positions': with_positions}, timeout, *args, **kwargs) + + @log + async def playlists_list( + self, playlist_ids: Union[List[Union[str, int]], int, str], timeout: Union[int, float] = None, *args, **kwargs + ) -> List[Playlist]: + """Получение плейлиста/плейлистов. + + Note: + Идентификатор плейлиста указывается в формате `owner_id:playlist_id`. Где `playlist_id` - идентификатор + плейлиста, `owner_id` - уникальный идентификатор владельца плейлиста. + + Args: + playlist_ids (:obj:`str` | :obj:`int` | :obj:`list` из :obj:`str` | :obj:`list` из :obj:`int`): Уникальный + идентификатор плейлиста или плейлистов. + timeout (:obj:`int` | :obj:`float`, optional): Если это значение указано, используется как время ожидания + ответа от сервера вместо указанного при создании пула. + **kwargs (:obj:`dict`, optional): Произвольные аргументы (будут переданы в запрос). + + Returns: + :obj:`list` из :obj:`yandex_music.Playlist`: Плейлист или плейлисты. + + Raises: + :class:`yandex_music.exceptions.YandexMusicError`: Базовое исключение библиотеки. + """ + return await self._get_list('playlist', playlist_ids, timeout=timeout, *args, **kwargs) + + @log + async def playlists_collective_join( + self, user_id: int, token: str, timeout: Union[int, float] = None, *args, **kwargs + ) -> bool: + """Присоединение к плейлисту как соавтор. + + Note: + В качестве `user_id` принимается исключительно числовой уникальный идентификатор пользователя, не username. + + Токен можно получить в Web-версии. Для этого, на странице плейлиста нужно нажать на + "Добавить соавтора". В полученной ссылке GET параметр `token` и будет токеном для присоединения. + + Args: + user_id (:obj:`int`): Владелец плейлиста. + token (:obj:`str`): Токен для присоединения. + timeout (:obj:`int` | :obj:`float`, optional): Если это значение указано, используется как время ожидания + ответа от сервера вместо указанного при создании пула. + **kwargs: Произвольные аргументы (будут переданы в запрос). + + Returns: + :obj:`bool`: :obj:`True` при успешном выполнении запроса, иначе :obj:`False`. + + Raises: + :class:`yandex_music.exceptions.YandexMusicError`: Базовое исключение библиотеки. + """ + url = f'{self.base_url}/playlists/collective/join' + + params = {'uid': user_id, 'token': token} + + result = await self._request.post(url, params=params, timeout=timeout, *args, **kwargs) + + return result == 'ok' + + @log + async def users_playlists_list( + self, user_id: Union[str, int] = None, timeout: Union[int, float] = None, *args, **kwargs + ) -> List[Playlist]: + """Получение списка плейлистов пользователя. + + Args: + user_id (:obj:`str` | :obj:`int`, optional): Уникальный идентификатор пользователя. Если не указан + используется ID текущего пользователя. + timeout (:obj:`int` | :obj:`float`, optional): Если это значение указано, используется как время ожидания + ответа от сервера вместо указанного при создании пула. + **kwargs (:obj:`dict`, optional): Произвольные аргументы (будут переданы в запрос). + + Returns: + :obj:`list` из :obj:`yandex_music.Playlist`: Плейлисты пользователя. + + Raises: + :class:`yandex_music.exceptions.YandexMusicError`: Базовое исключение библиотеки. + """ + if user_id is None and self.me is not None: + user_id = self.me.account.uid + + url = f'{self.base_url}/users/{user_id}/playlists/list' + + result = await self._request.get(url, timeout=timeout, *args, **kwargs) + + return Playlist.de_list(result, self) + + async def _get_likes( + self, + object_type: str, + user_id: Union[str, int] = None, + params: dict = None, + timeout: Union[int, float] = None, + *args, + **kwargs, + ) -> Union[List[Like], Optional[TracksList]]: + """Получение объектов с отметкой "Мне нравится". + + Args: + object_type (:obj:`str`): Тип объекта. + user_id (:obj:`str` | :obj:`int`, optional): Уникальный идентификатор пользователя. Если не указан + используется ID текущего пользователя. + params (:obj:`dict`, optional): Параметры, которые будут переданы в запрос. + timeout (:obj:`int` | :obj:`float`, optional): Если это значение указано, используется как время ожидания + ответа от сервера вместо указанного при создании пула. + **kwargs (:obj:`dict`, optional): Произвольные аргументы (будут переданы в запрос). + + Returns: + :obj:`list` из :obj:`yandex_music.Like` | :obj:`yandex_music.TracksList`: Объекты с отметкой "Мне нравится". + + Raises: + :class:`yandex_music.exceptions.YandexMusicError`: Базовое исключение библиотеки. + """ + if user_id is None and self.me is not None: + user_id = self.me.account.uid + + url = f'{self.base_url}/users/{user_id}/likes/{object_type}s' + + result = await self._request.get(url, params, timeout=timeout, *args, **kwargs) + + if object_type == 'track': + return TracksList.de_json(result.get('library'), self) + + return Like.de_list(result, self, object_type) + + @log + async def users_likes_tracks( + self, + user_id: Union[str, int] = None, + if_modified_since_revision: int = 0, + timeout: Union[int, float] = None, + *args, + **kwargs, + ) -> Optional[TracksList]: + """Получение треков с отметкой "Мне нравится". + + Args: + user_id (:obj:`str` | :obj:`int`, optional): Уникальный идентификатор пользователя. Если не указан + используется ID текущего пользователя. + if_modified_since_revision (:obj:`int`, optional): TODO. + timeout (:obj:`int` | :obj:`float`, optional): Если это значение указано, используется как время ожидания + ответа от сервера вместо указанного при создании пула. + **kwargs (:obj:`dict`, optional): Произвольные аргументы (будут переданы в запрос). + + Returns: + :obj:`yandex_music.TracksList`: Треки с отметкой "Мне нравится". + + Raises: + :class:`yandex_music.exceptions.YandexMusicError`: Базовое исключение библиотеки. + """ + return await self._get_likes( + 'track', user_id, {'if-modified-since-revision': if_modified_since_revision}, timeout, *args, **kwargs + ) + + @log + async def users_likes_albums( + self, user_id: Union[str, int] = None, rich: bool = True, timeout: Union[int, float] = None, *args, **kwargs + ) -> List[Like]: + """Получение альбомов с отметкой "Мне нравится". + + Args: + user_id (:obj:`str` | :obj:`int`, optional): Уникальный идентификатор пользователя. Если не указан + используется ID текущего пользователя. + rich (:obj:`bool`, optional): Если False, то приходит укороченная версия. + timeout (:obj:`int` | :obj:`float`, optional): Если это значение указано, используется как время ожидания + ответа от сервера вместо указанного при создании пула. + **kwargs (:obj:`dict`, optional): Произвольные аргументы (будут переданы в запрос). + + Returns: + :obj:`list` из :obj:`yandex_music.Like`: Альбомы с отметкой "Мне нравится". + + Raises: + :class:`yandex_music.exceptions.YandexMusicError`: Базовое исключение библиотеки. + """ + return await self._get_likes('album', user_id, {'rich': rich}, timeout, *args, **kwargs) + + @log + async def users_likes_artists( + self, + user_id: Union[str, int] = None, + with_timestamps: bool = True, + timeout: Union[int, float] = None, + *args, + **kwargs, + ) -> List[Like]: + """Получение артистов с отметкой "Мне нравится". + + Args: + user_id (:obj:`str` | :obj:`int`, optional): Уникальный идентификатор пользователя. Если не указан + используется ID текущего пользователя. + with_timestamps (:obj:`bool`, optional): С временными метками TODO. + timeout (:obj:`int` | :obj:`float`, optional): Если это значение указано, используется как время ожидания + ответа от сервера вместо указанного при создании пула. + **kwargs (:obj:`dict`, optional): Произвольные аргументы (будут переданы в запрос). + + Returns: + :obj:`list` из :obj:`yandex_music.Like`: Артисты с отметкой "Мне нравится". + + Raises: + :class:`yandex_music.exceptions.YandexMusicError`: Базовое исключение библиотеки. + """ + return await self._get_likes('artist', user_id, {'with-timestamps': with_timestamps}, timeout, *args, **kwargs) + + @log + async def users_likes_playlists( + self, user_id: Union[str, int] = None, timeout: Union[int, float] = None, *args, **kwargs + ) -> List[Like]: + """Получение плейлистов с отметкой "Мне нравится". + + Args: + user_id (:obj:`str` | :obj:`int`, optional): Уникальный идентификатор пользователя. Если не указан + используется ID текущего пользователя. + timeout (:obj:`int` | :obj:`float`, optional): Если это значение указано, используется как время ожидания + ответа от сервера вместо указанного при создании пула. + **kwargs (:obj:`dict`, optional): Произвольные аргументы (будут переданы в запрос). + + Returns: + :obj:`list` из :obj:`yandex_music.Like`: Плейлисты с отметкой "Мне нравится". + + Raises: + :class:`yandex_music.exceptions.YandexMusicError`: Базовое исключение библиотеки. + """ + return await self._get_likes('playlist', user_id, timeout=timeout, *args, **kwargs) + + @log + async def users_dislikes_tracks( + self, + user_id: Union[str, int] = None, + if_modified_since_revision: int = 0, + timeout: Union[int, float] = None, + *args, + **kwargs, + ) -> Optional[TracksList]: + """Получение треков с отметкой "Не рекомендовать". + + Args: + user_id (:obj:`str` | :obj:`int`, optional): Уникальный идентификатор пользователя. Если не указан + используется ID текущего пользователя. + if_modified_since_revision (:obj:`bool`, optional): TODO. + timeout (:obj:`int` | :obj:`float`, optional): Если это значение указано, используется как время ожидания + ответа от сервера вместо указанного при создании пула. + **kwargs (:obj:`dict`, optional): Произвольные аргументы (будут переданы в запрос). + + Returns: + :obj:`list` из :obj:`yandex_music.TracksList`: Треки с отметкой "Не рекомендовать". + + Raises: + :class:`yandex_music.exceptions.YandexMusicError`: Базовое исключение библиотеки. + """ + if user_id is None and self.me is not None: + user_id = self.me.account.uid + + url = f'{self.base_url}/users/{user_id}/dislikes/tracks' + + result = await self._request.get( + url, {'if_modified_since_revision': if_modified_since_revision}, timeout=timeout, *args, **kwargs + ) + + return TracksList.de_json(result.get('library'), self) + + async def _dislike_action( + self, + ids: Union[List[Union[str, int]], str, int], + remove: bool = False, + user_id: Union[str, int] = None, + timeout: Union[int, float] = None, + *args, + **kwargs, + ) -> bool: + """Действия с отметкой "Не рекомендовать". + + Args: + ids (:obj:`str` | :obj:`int` | :obj:`list` из :obj:`str` | :obj:`list` из :obj:`int`): Уникальный + идентификатор объекта или объектов. + remove (:obj:`bool`, optional): Если :obj:`True`, то снимает отметку, иначе ставит. + user_id (:obj:`str` | :obj:`int`, optional): Уникальный идентификатор пользователя. Если не указан + используется ID текущего пользователя. + timeout (:obj:`int` | :obj:`float`, optional): Если это значение указано, используется как время ожидания + ответа от сервера вместо указанного при создании пула. + **kwargs (:obj:`dict`, optional): Произвольные аргументы (будут переданы в запрос). + + Returns: + :obj:`bool`: :obj:`True` при успешном выполнении запроса, иначе :obj:`False`. + + Raises: + :class:`yandex_music.exceptions.YandexMusicError`: Базовое исключение библиотеки. + """ + if user_id is None and self.me is not None: + user_id = self.me.account.uid + + action = 'remove' if remove else 'add-multiple' + url = f'{self.base_url}/users/{user_id}/dislikes/tracks/{action}' + + result = await self._request.post(url, {f'track-ids': ids}, timeout=timeout, *args, **kwargs) + + return 'revision' in result + + @log + async def users_dislikes_tracks_add( + self, + track_ids: Union[List[Union[str, int]], str, int], + user_id: Union[str, int] = None, + timeout: Union[int, float] = None, + *args, + **kwargs, + ) -> bool: + """Поставить отметку "Не рекомендовать" треку/трекам. + + Note: + Так же снимает отметку "Мне нравится" если она есть. + + Args: + track_ids (:obj:`str` | :obj:`int` | :obj:`list` из :obj:`str` | :obj:`list` из :obj:`int`): Уникальный + идентификатор трека или треков. + user_id (:obj:`str` | :obj:`int`, optional): Уникальный идентификатор пользователя. Если не указан + используется ID текущего пользователя. + timeout (:obj:`int` | :obj:`float`, optional): Если это значение указано, используется как время ожидания + ответа от сервера вместо указанного при создании пула. + **kwargs (:obj:`dict`, optional): Произвольные аргументы (будут переданы в запрос). + + Returns: + :obj:`bool`: :obj:`True` при успешном выполнении запроса, иначе :obj:`False`. + + Raises: + :class:`yandex_music.exceptions.YandexMusicError`: Базовое исключение библиотеки. + """ + return await self._dislike_action(track_ids, False, user_id, timeout, *args, **kwargs) + + @log + async def users_dislikes_tracks_remove( + self, + track_ids: Union[List[Union[str, int]], str, int], + user_id: Union[str, int] = None, + timeout: Union[int, float] = None, + *args, + **kwargs, + ) -> bool: + """Снять отметку "Не рекомендовать" у трека/треков. + + Args: + track_ids (:obj:`str` | :obj:`int` | :obj:`list` из :obj:`str` | :obj:`list` из :obj:`int`): Уникальный + идентификатор трека или треков. + user_id (:obj:`str` | :obj:`int`, optional): Уникальный идентификатор пользователя. Если не указан + используется ID текущего пользователя. + timeout (:obj:`int` | :obj:`float`, optional): Если это значение указано, используется как время ожидания + ответа от сервера вместо указанного при создании пула. + **kwargs (:obj:`dict`, optional): Произвольные аргументы (будут переданы в запрос). + + Returns: + :obj:`bool`: :obj:`True` при успешном выполнении запроса, иначе :obj:`False`. + + Raises: + :class:`yandex_music.exceptions.YandexMusicError`: Базовое исключение библиотеки. + """ + return await self._dislike_action(track_ids, True, user_id, timeout, *args, **kwargs) + + @log + async def after_track( + self, + next_track_id: Union[str, int], + context_item: str, + prev_track_id: Union[str, int] = None, + context: str = 'playlist', + types: str = 'shot', + from_: str = 'mobile-landing-origin-default', + timeout: Union[int, float] = None, + *args, + **kwargs, + ) -> Optional[ShotEvent]: + """Получение рекламы или шота от Алисы после трека. + + Note: + При получения шота от Алисы `prev_track_id` можно не указывать. + + Если `context = 'playlist'`, то в `context_item` необходимо передать `{OWNER_PLAYLIST}:{ID_PLAYLIST}`. + Плейлист с Алисой имеет владельца с `id = 940441070`. + + ID плейлиста можно получить из блоков landing'a. Получить шот чужого плейлиста нельзя. + + Известные значения `context`: `playlist`. + + Известные значения `types`: `shot`, `ad`. + + Args: + prev_track_id (:obj:`str` | :obj:`int`): Уникальный идентификатор предыдущего трека. + next_track_id (:obj:`str` | :obj:`int`): Уникальный идентификатор следующего трека. + context_item (:obj:`str`): Уникальный идентификатор контекста. + context (:obj:`str`, optional): Место, откуда было вызвано получение. + types (:obj:`str`, optional): Тип того, что вернуть после трека. + from_ (:obj:`str`, optional): Место, с которого попали в контекст. + timeout (:obj:`int` | :obj:`float`, optional): Если это значение указано, используется как время ожидания + ответа от сервера вместо указанного при создании пула. + **kwargs (:obj:`dict`, optional): Произвольные аргументы (будут переданы в запрос). + + Returns: + :obj:`yandex_music.ShotEvent`: Шот от Алисы или :obj:`None`. + + Raises: + :class:`yandex_music.exceptions.YandexMusicError`: Базовое исключение библиотеки. + """ + + url = f'{self.base_url}/after-track' + + params = { + 'from': from_, + 'prevTrackId': prev_track_id, + 'nextTrackId': next_track_id, + 'context': context, + 'contextItem': context_item, + 'types': types, + } + + result = await self._request.get(url, params=params, timeout=timeout, *args, **kwargs) + + # TODO судя по всему эндпоинт ещё возвращает рекламу после треков для пользователей без подписки. + return ShotEvent.de_json(result.get('shot_event'), self) + + @log + async def queues_list( + self, device: str = None, timeout: Union[int, float] = None, *args, **kwargs + ) -> List[QueueItem]: + """Получение всех очередей треков с разных устройств для синхронизации между ними. + + Note: + Именно к `device` привязывается очередь. На одном устройстве может быть создана одна очередь. + + Аргумент `device` имеет следующий формат: `ключ=значение; ключ2=значение2`. Обязательные паля указы в + значении по умолчанию. + + Args: + device (:obj:`str`, optional): Содержит информацию об устройстве с которого выполняется запрос. + timeout (:obj:`int` | :obj:`float`, optional): Если это значение указано, используется как время ожидания + ответа от сервера вместо указанного при создании пула. + **kwargs (:obj:`dict`, optional): Произвольные аргументы (будут переданы в запрос). + + Returns: + :obj:`list` из :obj:`yandex_music.QueueItem`: Элементы очереди всех устройств. + + Raises: + :class:`yandex_music.exceptions.YandexMusicError`: Базовое исключение библиотеки. + """ + if not device: + device = self.device + + url = f'{self.base_url}/queues' + + self._request.headers['X-Yandex-Music-Device'] = device + result = await self._request.get(url, timeout=timeout, *args, **kwargs) + + return QueueItem.de_list(result.get('queues'), self) + + @log + async def queue(self, queue_id: str, timeout: Union[int, float] = None, *args, **kwargs) -> Optional[Queue]: + """Получение информации об очереди треков и самих треков в ней. + + Args: + queue_id (:obj:`str`): Уникальный идентификатор очереди. + timeout (:obj:`int` | :obj:`float`, optional): Если это значение указано, используется как время ожидания + ответа от сервера вместо указанного при создании пула. + **kwargs (:obj:`dict`, optional): Произвольные аргументы (будут переданы в запрос). + + Returns: + :obj:`yandex_music.Queue`: Очередь или :obj:`None`. + + Raises: + :class:`yandex_music.exceptions.YandexMusicError`: Базовое исключение библиотеки. + """ + url = f'{self.base_url}/queues/{queue_id}' + + result = await self._request.get(url, timeout=timeout, *args, **kwargs) + + return Queue.de_json(result, self) + + @log + async def queue_update_position( + self, queue_id: str, current_index: int, device: str = None, timeout: Union[int, float] = None, *args, **kwargs + ) -> bool: + """Установка текущего индекса проигрываемого трека в очереди треков. + + Note: + Изменить можно только у той очереди, которая была создана с переданного `device`! + + Args: + queue_id (:obj:`str`): Уникальный идентификатор очереди. + current_index (:obj:`int`): Текущий индекс. + device (:obj:`str`, optional): Содержит информацию об устройстве с которого выполняется запрос. + timeout (:obj:`int` | :obj:`float`, optional): Если это значение указано, используется как время ожидания + ответа от сервера вместо указанного при создании пула. + **kwargs (:obj:`dict`, optional): Произвольные аргументы (будут переданы в запрос). + + Returns: + :obj:`bool`: :obj:`True` при успешном выполнении запроса, иначе :obj:`False`. + + Raises: + :class:`yandex_music.exceptions.YandexMusicError`: Базовое исключение библиотеки. + """ + if not device: + device = self.device + + url = f'{self.base_url}/queues/{queue_id}/update-position' + + self._request.headers['X-Yandex-Music-Device'] = device + result = await self._request.post( + url, {'isInteractive': False}, params={'currentIndex': current_index}, timeout=timeout, *args, **kwargs + ) + + return result.get('status') == 'ok' + + @log + async def queue_create( + self, queue: Union[Queue, str], device: str = None, timeout: Union[int, float] = None, *args, **kwargs + ) -> Optional[str]: + """Создание новой очереди треков. + + Args: + queue (:obj:`yandex_music.Queue` | :obj:`str`): Объект очереди или JSON строка с этим объектом. + device (:obj:`str`, optional): Содержит информацию об устройстве с которого выполняется запрос. + timeout (:obj:`int` | :obj:`float`, optional): Если это значение указано, используется как время ожидания + ответа от сервера вместо указанного при создании пула. + **kwargs (:obj:`dict`, optional): Произвольные аргументы (будут переданы в запрос). + + Returns: + :obj:`str`: Вернёт уникальный идентификатор созданной очереди, иначе :obj:`None`. + + Raises: + :class:`yandex_music.exceptions.YandexMusicError`: Базовое исключение библиотеки. + """ + if not device: + device = self.device + + if isinstance(queue, Queue): + queue = queue.to_json(True) + + url = f'{self.base_url}/queues' + + self._request.headers['X-Yandex-Music-Device'] = device + result = await self._request.post(url, queue, timeout=timeout, *args, **kwargs) + + return result.get('id_') + + # camelCase псевдонимы + + #: Псевдоним для :attr:`account_status` + accountStatus = account_status + #: Псевдоним для :attr:`account_settings` + accountSettings = account_settings + #: Псевдоним для :attr:`account_settings_set` + accountSettingsSet = account_settings_set + #: Псевдоним для :attr:`permission_alerts` + permissionAlerts = permission_alerts + #: Псевдоним для :attr:`account_experiments` + accountExperiments = account_experiments + #: Псевдоним для :attr:`consume_promo_code` + consumePromoCode = consume_promo_code + #: Псевдоним для :attr:`feed_wizard_is_passed` + feedWizardIsPassed = feed_wizard_is_passed + #: Псевдоним для :attr:`new_releases` + newReleases = new_releases + #: Псевдоним для :attr:`new_playlists` + newPlaylists = new_playlists + #: Псевдоним для :attr:`tracks_download_info` + tracksDownloadInfo = tracks_download_info + #: Псевдоним для :attr:`track_supplement` + trackSupplement = track_supplement + #: Псевдоним для :attr:`tracks_similar` + tracksSimilar = tracks_similar + #: Псевдоним для :attr:`play_audio` + playAudio = play_audio + #: Псевдоним для :attr:`albums_with_tracks` + albumsWithTracks = albums_with_tracks + #: Псевдоним для :attr:`search_suggest` + searchSuggest = search_suggest + #: Псевдоним для :attr:`users_settings` + usersSettings = users_settings + #: Псевдоним для :attr:`users_playlists` + usersPlaylists = users_playlists + #: Псевдоним для :attr:`users_playlists_recommendations` + usersPlaylistsRecommendations = users_playlists_recommendations + #: Псевдоним для :attr:`users_playlists_create` + usersPlaylistsCreate = users_playlists_create + #: Псевдоним для :attr:`users_playlists_delete` + usersPlaylistsDelete = users_playlists_delete + #: Псевдоним для :attr:`users_playlists_name` + usersPlaylistsName = users_playlists_name + #: Псевдоним для :attr:`users_playlists_visibility` + usersPlaylistsVisibility = users_playlists_visibility + #: Псевдоним для :attr:`users_playlists_change` + usersPlaylistsChange = users_playlists_change + #: Псевдоним для :attr:`users_playlists_insert_track` + usersPlaylistsInsertTrack = users_playlists_insert_track + #: Псевдоним для :attr:`users_playlists_delete_track` + usersPlaylistsDeleteTrack = users_playlists_delete_track + #: Псевдоним для :attr:`rotor_account_status` + rotorAccountStatus = rotor_account_status + #: Псевдоним для :attr:`rotor_stations_dashboard` + rotorStationsDashboard = rotor_stations_dashboard + #: Псевдоним для :attr:`rotor_stations_list` + rotorStationsList = rotor_stations_list + #: Псевдоним для :attr:`rotor_station_feedback` + rotorStationFeedback = rotor_station_feedback + #: Псевдоним для :attr:`rotor_station_feedback_radio_started` + rotorStationFeedbackRadioStarted = rotor_station_feedback_radio_started + #: Псевдоним для :attr:`rotor_station_feedback_track_started` + rotorStationFeedbackTrackStarted = rotor_station_feedback_track_started + #: Псевдоним для :attr:`rotor_station_feedback_track_finished` + rotorStationFeedbackTrackFinished = rotor_station_feedback_track_finished + #: Псевдоним для :attr:`rotor_station_feedback_skip` + rotorStationFeedbackSkip = rotor_station_feedback_skip + #: Псевдоним для :attr:`rotor_station_info` + rotorStationInfo = rotor_station_info + #: Псевдоним для :attr:`rotor_station_settings2` + rotorStationSettings2 = rotor_station_settings2 + #: Псевдоним для :attr:`rotor_station_tracks` + rotorStationTracks = rotor_station_tracks + #: Псевдоним для :attr:`artists_brief_info` + artistsBriefInfo = artists_brief_info + #: Псевдоним для :attr:`artists_tracks` + artistsTracks = artists_tracks + #: Псевдоним для :attr:`artists_direct_albums` + artistsDirectAlbums = artists_direct_albums + #: Псевдоним для :attr:`users_likes_tracks_add` + usersLikesTracksAdd = users_likes_tracks_add + #: Псевдоним для :attr:`users_likes_tracks_remove` + usersLikesTracksRemove = users_likes_tracks_remove + #: Псевдоним для :attr:`users_likes_artists_add` + usersLikesArtistsAdd = users_likes_artists_add + #: Псевдоним для :attr:`users_likes_artists_remove` + usersLikesArtistsRemove = users_likes_artists_remove + #: Псевдоним для :attr:`users_likes_playlists_add` + usersLikesPlaylistsAdd = users_likes_playlists_add + #: Псевдоним для :attr:`users_likes_playlists_remove` + usersLikesPlaylistsRemove = users_likes_playlists_remove + #: Псевдоним для :attr:`users_likes_albums_add` + usersLikesAlbumsAdd = users_likes_albums_add + #: Псевдоним для :attr:`users_likes_albums_remove` + usersLikesAlbumsRemove = users_likes_albums_remove + #: Псевдоним для :attr:`playlists_list` + playlistsList = playlists_list + #: Псевдоним для :attr:`playlists_collective_join` + playlistsCollectiveJoin = playlists_collective_join + #: Псевдоним для :attr:`users_playlists_list` + usersPlaylistsList = users_playlists_list + #: Псевдоним для :attr:`users_likes_tracks` + usersLikesTracks = users_likes_tracks + #: Псевдоним для :attr:`users_likes_albums` + usersLikesAlbums = users_likes_albums + #: Псевдоним для :attr:`users_likes_artists` + usersLikesArtists = users_likes_artists + #: Псевдоним для :attr:`users_likes_playlists` + usersLikesPlaylists = users_likes_playlists + #: Псевдоним для :attr:`users_dislikes_tracks` + usersDislikesTracks = users_dislikes_tracks + #: Псевдоним для :attr:`users_dislikes_tracks_add` + usersDislikesTracksAdd = users_dislikes_tracks_add + #: Псевдоним для :attr:`users_dislikes_tracks_remove` + usersDislikesTracksRemove = users_dislikes_tracks_remove + #: Псевдоним для :attr:`after_track` + afterTrack = after_track + #: Псевдоним для :attr:`queues_list` + queuesList = queues_list + #: Псевдоним для :attr:`queue_update_position` + queueUpdatePosition = queue_update_position + #: Псевдоним для :attr:`queue_create` + queueCreate = queue_create diff --git a/yandex_music/cover.py b/yandex_music/cover.py index 0f0d087..6677d89 100644 --- a/yandex_music/cover.py +++ b/yandex_music/cover.py @@ -54,6 +54,18 @@ class Cover(YandexMusicObject): self.client.request.download(f'https://{uri.replace("%%", size)}', filename) + async def download_async(self, filename: str, index: int = 0, size: str = '200x200') -> None: + """Загрузка обложки. + + Args: + filename (:obj:`str`): Путь для сохранения файла с названием и расширением. + index (:obj:`int`, optional): Индекс элемента в списке ссылок на обложки если нет `self.uri`. + size (:obj:`str`, optional): Размер изображения. + """ + uri = self.uri or self.items_uri[index] + + await self.client.request.download(f'https://{uri.replace("%%", size)}', filename) + @classmethod def de_json(cls, data: dict, client: 'Client') -> Optional['Cover']: """Десериализация объекта. @@ -91,3 +103,8 @@ class Cover(YandexMusicObject): covers.append(cls.de_json(cover, client)) return covers + + # camelCase псевдонимы + + #: Псевдоним для :attr:`download_async` + downloadAsync = download_async diff --git a/yandex_music/download_info.py b/yandex_music/download_info.py index b156242..19ca5d6 100644 --- a/yandex_music/download_info.py +++ b/yandex_music/download_info.py @@ -34,6 +34,7 @@ class DownloadInfo(YandexMusicObject): client: Optional['Client'] = None def __post_init__(self): + self.direct_link = None self._id_attrs = (self.codec, self.bitrate_in_kbps, self.gain, self.preview, self.download_info_url) @staticmethod @@ -45,6 +46,16 @@ class DownloadInfo(YandexMusicObject): if node.nodeType == node.TEXT_NODE: return node.data + def __build_direct_link(self, xml: str) -> str: + doc = minidom.parseString(xml) + 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')) + s = self._get_text_node_data(doc.getElementsByTagName('s')) + sign = md5(('XGRlBW9FXlekgbPrRHuSiA' + path[1::] + s).encode('utf-8')).hexdigest() + + return f'https://{host}/get-mp3/{sign}/{ts}{path}' + def get_direct_link(self) -> str: """Получение прямой ссылки на загрузку из XML ответа. @@ -56,14 +67,22 @@ class DownloadInfo(YandexMusicObject): """ 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')) - s = self._get_text_node_data(doc.getElementsByTagName('s')) - sign = md5(('XGRlBW9FXlekgbPrRHuSiA' + path[1::] + s).encode('utf-8')).hexdigest() + self.direct_link = self.__build_direct_link(result) - self.direct_link = f'https://{host}/get-mp3/{sign}/{ts}{path}' + return self.direct_link + + async def get_direct_link_async(self) -> str: + """Получение прямой ссылки на загрузку из XML ответа. + + Метод доступен только одну минуту с момента получения информации о загрузке, иначе 410 ошибка! + + Returns: + :obj:`str`: Прямая ссылка на загрузку трека. + + """ + result = await self.client.request.retrieve(self.download_info_url) + + self.direct_link = self.__build_direct_link(result) return self.direct_link @@ -78,6 +97,17 @@ class DownloadInfo(YandexMusicObject): self.client.request.download(self.direct_link, filename) + async def download_async(self, filename: str) -> None: + """Загрузка трека. + + Args: + filename (:obj:`str`): Путь и(или) название файла вместе с расширением. + """ + if self.direct_link is None: + await self.get_direct_link_async() + + await self.client.request.download(self.direct_link, filename) + @classmethod def de_json(cls, data: dict, client: 'Client') -> Optional['DownloadInfo']: """Десериализация объекта. @@ -125,3 +155,7 @@ class DownloadInfo(YandexMusicObject): #: Псевдоним для :attr:`get_direct_link` getDirectLink = get_direct_link + #: Псевдоним для :attr:`get_direct_link_async` + getDirectLinkAsync = get_direct_link_async + #: Псевдоним для :attr:`download_async` + downloadAsync = download_async diff --git a/yandex_music/icon.py b/yandex_music/icon.py index eaba8fb..a815a84 100644 --- a/yandex_music/icon.py +++ b/yandex_music/icon.py @@ -33,6 +33,15 @@ class Icon(YandexMusicObject): """ self.client.request.download(self.get_url(size), filename) + async def download_async(self, filename: str, size: str = '200x200') -> None: + """Загрузка иконки. + + Args: + filename (:obj:`str`): Путь для сохранения файла с названием и расширением. + size (:obj:`str`, optional): Размер иконки. + """ + await self.client.request.download(self.get_url(size), filename) + def get_url(self, size: str = '200x200'): """Получение URL иконки. @@ -58,3 +67,8 @@ class Icon(YandexMusicObject): data = super(Icon, cls).de_json(data, client) return cls(client=client, **data) + + # camelCase псевдонимы + + #: Псевдоним для :attr:`download_async` + downloadAsync = download_async diff --git a/yandex_music/landing/mix_link.py b/yandex_music/landing/mix_link.py index e9f7070..ea0a8d3 100644 --- a/yandex_music/landing/mix_link.py +++ b/yandex_music/landing/mix_link.py @@ -58,6 +58,15 @@ class MixLink(YandexMusicObject): """ self.client.request.download(f'https://{self.background_image_uri.replace("%%", size)}', filename) + async def download_background_image_async(self, filename: str, size: str = '200x200') -> None: + """Загрузка заднего фона. + + Args: + filename (:obj:`str`): Путь для сохранения файла с названием и расширением. + size (:obj:`str`, optional): Размер заднего фона. + """ + await self.client.request.download(f'https://{self.background_image_uri.replace("%%", size)}', filename) + def download_cover_white(self, filename: str, size: str = '200x200') -> None: """Загрузка обложки TODO. @@ -67,6 +76,15 @@ class MixLink(YandexMusicObject): """ self.client.request.download(f'https://{self.cover_white.replace("%%", size)}', filename) + async def download_cover_white_async(self, filename: str, size: str = '200x200') -> None: + """Загрузка обложки TODO. + + Args: + filename (:obj:`str`): Путь для сохранения файла с названием и расширением. + size (:obj:`str`, optional): Размер обложки. + """ + await self.client.request.download(f'https://{self.cover_white.replace("%%", size)}', filename) + def download_cover_uri(self, filename: str, size: str = '200x200') -> None: """Загрузка обложки. @@ -76,6 +94,15 @@ class MixLink(YandexMusicObject): """ self.client.request.download(f'https://{self.cover_uri.replace("%%", size)}', filename) + async def download_cover_uri_async(self, filename: str, size: str = '200x200') -> None: + """Загрузка обложки. + + Args: + filename (:obj:`str`): Путь для сохранения файла с названием и расширением. + size (:obj:`str`, optional): Размер обложки. + """ + await self.client.request.download(f'https://{self.cover_uri.replace("%%", size)}', filename) + @classmethod def de_json(cls, data: dict, client: 'Client') -> Optional['MixLink']: """Десериализация объекта. @@ -118,7 +145,13 @@ class MixLink(YandexMusicObject): #: Псевдоним для :attr:`download_background_image` downloadBackgroundImage = download_background_image + #: Псевдоним для :attr:`download_background_image_async` + downloadBackgroundImageAsync = download_background_image_async #: Псевдоним для :attr:`download_cover_white` downloadCoverWhite = download_cover_white + #: Псевдоним для :attr:`download_cover_white_async` + downloadCoverWhiteAsync = download_cover_white_async #: Псевдоним для :attr:`download_cover_uri` downloadCoverUri = download_cover_uri + #: Псевдоним для :attr:`download_cover_uri_async` + downloadCoverUriAsync = download_cover_uri_async diff --git a/yandex_music/landing/promotion.py b/yandex_music/landing/promotion.py index 5613802..3a9df76 100644 --- a/yandex_music/landing/promotion.py +++ b/yandex_music/landing/promotion.py @@ -62,6 +62,15 @@ class Promotion(YandexMusicObject): """ self.client.request.download(f'https://{self.image.replace("%%", size)}', filename) + async def download_image_async(self, filename: str, size: str = '300x300') -> None: + """Загрузка рекламного изображения. + + Args: + filename (:obj:`str`): Путь для сохранения файла с названием и расширением. + size (:obj:`str`, optional): Размер изображения. + """ + await self.client.request.download(f'https://{self.image.replace("%%", size)}', filename) + @classmethod def de_json(cls, data: dict, client: 'Client') -> Optional['Promotion']: """Десериализация объекта. @@ -100,5 +109,9 @@ class Promotion(YandexMusicObject): return promotions + # camelCase псевдонимы + #: Псевдоним для :attr:`download_image` downloadImage = download_image + #: Псевдоним для :attr:`download_image_async` + downloadImageAsync = download_image_async diff --git a/yandex_music/landing/track_id.py b/yandex_music/landing/track_id.py index 608a3c2..386eac2 100644 --- a/yandex_music/landing/track_id.py +++ b/yandex_music/landing/track_id.py @@ -51,6 +51,14 @@ class TrackId(YandexMusicObject): """ return self.client.tracks(self.track_full_id, *args, **kwargs)[0] + async def fetch_track_async(self, *args, **kwargs) -> 'Track': + """Получение полной версии трека. + + Returns: + :obj:`yandex_music.Track`: Полная версия. + """ + return (await self.client.tracks(self.track_full_id, *args, **kwargs))[0] + @classmethod def de_json(cls, data: dict, client: 'Client') -> Optional['TrackId']: """Десериализация объекта. @@ -89,5 +97,7 @@ class TrackId(YandexMusicObject): #: Псевдоним для :attr:`fetch_track` fetchTrack = fetch_track + #: Псевдоним для :attr:`fetch_track_async` + fetchTrackAsync = fetch_track_async #: Псевдоним для :attr:`track_full_id` trackFullId = track_full_id diff --git a/yandex_music/playlist/playlist.py b/yandex_music/playlist/playlist.py index 32aefe0..ebf17b3 100644 --- a/yandex_music/playlist/playlist.py +++ b/yandex_music/playlist/playlist.py @@ -172,6 +172,13 @@ class Playlist(YandexMusicObject): """ return self.client.users_playlists_recommendations(self.kind, self.owner.uid, *args, **kwargs) + async def get_recommendations_async(self, *args, **kwargs) -> Optional['PlaylistRecommendations']: + """Сокращение для:: + + await client.users_playlists_recommendations(playlist.kind, playlist.owner.uid, *args, **kwargs) + """ + return await self.client.users_playlists_recommendations(self.kind, self.owner.uid, *args, **kwargs) + def download_animated_cover(self, filename: str, size: str = '200x200') -> None: """Загрузка анимированной обложки. @@ -181,6 +188,15 @@ class Playlist(YandexMusicObject): """ self.client.request.download(f'https://{self.animated_cover_uri.replace("%%", size)}', filename) + async def download_animated_cover_async(self, filename: str, size: str = '200x200') -> None: + """Загрузка анимированной обложки. + + Args: + filename (:obj:`str`): Путь для сохранения файла с названием и расширением (GIF). + size (:obj:`str`, optional): Размер анимированной обложки. + """ + await self.client.request.download(f'https://{self.animated_cover_uri.replace("%%", size)}', filename) + def download_og_image(self, filename: str, size: str = '200x200') -> None: """Загрузка обложки. @@ -192,12 +208,29 @@ class Playlist(YandexMusicObject): """ self.client.request.download(f'https://{self.og_image.replace("%%", size)}', filename) + async def download_og_image_async(self, filename: str, size: str = '200x200') -> None: + """Загрузка обложки. + + Используйте это только когда нет self.cover! + + Args: + filename (:obj:`str`): Путь для сохранения файла с названием и расширением. + size (:obj:`str`, optional): Размер обложки. + """ + await self.client.request.download(f'https://{self.og_image.replace("%%", size)}', filename) + def rename(self, name: str) -> None: client, kind = self.client, self.kind self.__dict__.clear() self.__dict__.update(client.users_playlists_name(kind, name).__dict__) + async def rename_async(self, name: str) -> None: + client, kind = self.client, self.kind + + self.__dict__.clear() + self.__dict__.update((await client.users_playlists_name(kind, name)).__dict__) + def like(self, *args, **kwargs) -> bool: """Сокращение для:: @@ -205,6 +238,13 @@ class Playlist(YandexMusicObject): """ return self.client.users_likes_playlists_add(self.uid, self.client.me.account.uid, *args, **kwargs) + async def like_async(self, *args, **kwargs) -> bool: + """Сокращение для:: + + await client.users_likes_playlists_add(playlist.uid, user.id *args, **kwargs) + """ + return await self.client.users_likes_playlists_add(self.uid, self.client.me.account.uid, *args, **kwargs) + def dislike(self, *args, **kwargs) -> bool: """Сокращение для:: @@ -212,6 +252,13 @@ class Playlist(YandexMusicObject): """ return self.client.users_likes_playlists_remove(self.uid, self.client.me.account.uid, *args, **kwargs) + async def dislike_async(self, *args, **kwargs) -> bool: + """Сокращение для:: + + await client.users_likes_playlists_remove(playlist.uid, user.id *args, **kwargs) + """ + return await self.client.users_likes_playlists_remove(self.uid, self.client.me.account.uid, *args, **kwargs) + def fetch_tracks(self, *args, **kwargs) -> List['TrackShort']: """Сокращение для:: @@ -219,6 +266,13 @@ class Playlist(YandexMusicObject): """ return self.client.users_playlists(self.kind, self.owner.uid, *args, **kwargs).tracks + async def fetch_tracks_async(self, *args, **kwargs) -> List['TrackShort']: + """Сокращение для:: + + await client.users_playlists(playlist.kind, playlist.owner.id, *args, **kwargs).tracks + """ + return await self.client.users_playlists(self.kind, self.owner.uid, *args, **kwargs).tracks + def insert_track(self, track_id: int, album_id: int, *args, **kwargs) -> Optional['Playlist']: """Сокращение для:: @@ -229,6 +283,16 @@ class Playlist(YandexMusicObject): self.kind, track_id, album_id, user_id=self.owner.uid, revision=self.revision, *args, **kwargs ) + async def insert_track_async(self, track_id: int, album_id: int, *args, **kwargs) -> Optional['Playlist']: + """Сокращение для:: + + await client.users_playlists_insert_track(self.kind, track_id, album_id, user_id=self.owner.uid, + revision=self.revision, *args, **kwargs) + """ + return await self.client.users_playlists_insert_track( + self.kind, track_id, album_id, user_id=self.owner.uid, revision=self.revision, *args, **kwargs + ) + def delete_tracks(self, from_: int, to: int, *args, **kwargs) -> Optional['Playlist']: """Сокращение для:: @@ -238,6 +302,15 @@ class Playlist(YandexMusicObject): self.kind, from_, to, self.revision, self.owner.uid, *args, **kwargs ) + async def delete_tracks_async(self, from_: int, to: int, *args, **kwargs) -> Optional['Playlist']: + """Сокращение для:: + + await client.users_playlists_delete_track(self.kind, from_, to, self.revision, self.owner.uid, *args, **kwargs) + """ + return await self.client.users_playlists_delete_track( + self.kind, from_, to, self.revision, self.owner.uid, *args, **kwargs + ) + def delete(self, *args, **kwargs): """Сокращение для:: @@ -245,6 +318,13 @@ class Playlist(YandexMusicObject): """ return self.client.users_playlists_delete(self.kind, self.owner.uid, *args, **kwargs) + async def delete_async(self, *args, **kwargs): + """Сокращение для:: + + await client.users_playlists_delete(self.kind, self.owner.uid) + """ + return await self.client.users_playlists_delete(self.kind, self.owner.uid, *args, **kwargs) + @classmethod def de_json(cls, data: dict, client: 'Client') -> Optional['Playlist']: """Десериализация объекта. @@ -322,13 +402,33 @@ class Playlist(YandexMusicObject): playlistId = playlist_id #: Псевдоним для :attr:`get_recommendations` getRecommendations = get_recommendations + #: Псевдоним для :attr:`get_recommendations_async` + getRecommendationsAsync = get_recommendations_async #: Псевдоним для :attr:`download_animated_cover` downloadAnimatedCover = download_animated_cover + #: Псевдоним для :attr:`download_animated_cover_async` + downloadAnimatedCoverAsync = download_animated_cover_async #: Псевдоним для :attr:`download_og_image` downloadOgImage = download_og_image + #: Псевдоним для :attr:`download_og_image_async` + downloadOgImageAsync = download_og_image_async #: Псевдоним для :attr:`fetch_tracks` fetchTracks = fetch_tracks + #: Псевдоним для :attr:`fetch_tracks_async` + fetchTracksAsync = fetch_tracks_async #: Псевдоним для :attr:`insert_track` insertTrack = insert_track + #: Псевдоним для :attr:`insert_track_async` + insertTrackAsync = insert_track_async #: Псевдоним для :attr:`delete_tracks` deleteTracks = delete_tracks + #: Псевдоним для :attr:`delete_tracks_async` + deleteTracksAsync = delete_tracks_async + #: Псевдоним для :attr:`rename_async` + renameAsync = rename_async + #: Псевдоним для :attr:`like_async` + likeAsync = like_async + #: Псевдоним для :attr:`dislike_async` + dislikeAsync = dislike_async + #: Псевдоним для :attr:`delete_async` + deleteAsync = delete_async diff --git a/yandex_music/playlist/playlist_id.py b/yandex_music/playlist/playlist_id.py index 24796aa..9cdaf00 100644 --- a/yandex_music/playlist/playlist_id.py +++ b/yandex_music/playlist/playlist_id.py @@ -35,6 +35,13 @@ class PlaylistId(YandexMusicObject): """ return self.client.users_playlists(self.kind, self.uid, *args, **kwargs) + async def fetch_playlist_async(self, *args, **kwargs): + """Сокращение для:: + + await client.users_playlists(kind, uid, *args, **kwargs) + """ + return await self.client.users_playlists(self.kind, self.uid, *args, **kwargs) + @classmethod def de_json(cls, data: dict, client: 'Client') -> Optional['PlaylistId']: """Десериализация объекта. @@ -79,3 +86,5 @@ class PlaylistId(YandexMusicObject): playlistId = playlist_id #: Псевдоним для :attr:`fetch_playlist` fetchPlaylist = fetch_playlist + #: Псевдоним для :attr:`fetch_playlist_async` + fetchPlaylistAsync = fetch_playlist_async diff --git a/yandex_music/playlist/tag.py b/yandex_music/playlist/tag.py index 869daba..782f571 100644 --- a/yandex_music/playlist/tag.py +++ b/yandex_music/playlist/tag.py @@ -47,3 +47,5 @@ class Tag(YandexMusicObject): data = super(Tag, cls).de_json(data, client) return cls(client=client, **data) + + # TODO (MarshalX) add download_og_image shortcut? diff --git a/yandex_music/playlist/tag_result.py b/yandex_music/playlist/tag_result.py index 2a8a1f9..e810089 100644 --- a/yandex_music/playlist/tag_result.py +++ b/yandex_music/playlist/tag_result.py @@ -45,3 +45,5 @@ class TagResult(YandexMusicObject): data['ids'] = PlaylistId.de_list(data.get('ids'), client) return cls(client=client, **data) + + # TODO (MarshalX) add fetch_playlists shortcut? diff --git a/yandex_music/playlist/user.py b/yandex_music/playlist/user.py index 7eac556..b78ab51 100644 --- a/yandex_music/playlist/user.py +++ b/yandex_music/playlist/user.py @@ -45,15 +45,6 @@ class User(YandexMusicObject): def __post_init__(self): self._id_attrs = (self.uid, self.login) - def download_avatar(self, filename: str, format_: str = 'normal') -> None: - """Загрузка изображения пользователя. - - Args: - filename (:obj:`str`): Путь для сохранения файла с названием и расширением. - format_ (:obj:`str`, optional): Формат желаемого изображения (`normal`, `orig`, `small`, `big`). - """ - self.client.request.download(f'https://upics.yandex.net/{self.uid}/{format_}', filename) - @classmethod def de_json(cls, data: dict, client: 'Client') -> Optional['User']: """Десериализация объекта. @@ -87,8 +78,3 @@ class User(YandexMusicObject): return [] return [cls.de_json(user, client) for user in data] - - # camelCase псевдонимы - - #: Псевдоним для :attr:`download_avatar` - downloadAvatar = download_avatar diff --git a/yandex_music/queue/queue_item.py b/yandex_music/queue/queue_item.py index cedf541..6715e5e 100644 --- a/yandex_music/queue/queue_item.py +++ b/yandex_music/queue/queue_item.py @@ -33,6 +33,13 @@ class QueueItem(YandexMusicObject): """ return self.client.queue(self.id, *args, **kwargs) + async def fetch_queue_async(self, *args, **kwargs) -> Optional['Queue']: + """Сокращение для:: + + await client.queue(id, *args, **kwargs) + """ + return await self.client.queue(self.id, *args, **kwargs) + @classmethod def de_json(cls, data: dict, client: 'Client') -> Optional['QueueItem']: """Десериализация объекта. @@ -74,3 +81,5 @@ class QueueItem(YandexMusicObject): #: Псевдоним для :attr:`fetch_queue` fetchQueue = fetch_queue + #: Псевдоним для :attr:`fetch_queue_async` + fetchQueueAsync = fetch_queue_async diff --git a/yandex_music/search/search.py b/yandex_music/search/search.py index fd4aca7..2cb733e 100644 --- a/yandex_music/search/search.py +++ b/yandex_music/search/search.py @@ -79,6 +79,17 @@ class Search(YandexMusicObject): """ return self.client.search(self.text, self.nocorrect, self.type_, page, *args, **kwargs) + async def get_page_async(self, page: int, *args, **kwargs) -> Optional['Search']: + """Получение определеной страницы поиска. + + Args: + page (:obj:`int`): Номер страницы. + + Returns: + :obj:`yandex_music.Search` | :obj:`None`: Страница результата поиска или :obj:`None`. + """ + return await self.client.search(self.text, self.nocorrect, self.type_, page, *args, **kwargs) + def next_page(self, *args, **kwargs) -> Optional['Search']: """Получение следующей страницы поиска. @@ -87,6 +98,14 @@ class Search(YandexMusicObject): """ return self.get_page(self.page + 1, *args, **kwargs) + async def next_page_async(self, *args, **kwargs) -> Optional['Search']: + """Получение следующей страницы поиска. + + Returns: + :obj:`yandex_music.Search` | :obj:`None`: Следующая страница результата поиска или :obj:`None`. + """ + return await self.get_page_async(self.page + 1, *args, **kwargs) + def prev_page(self, *args, **kwargs) -> Optional['Search']: """Получение предыдущей страницы поиска. @@ -95,6 +114,14 @@ class Search(YandexMusicObject): """ return self.get_page(self.page - 1, *args, **kwargs) + async def prev_page_async(self, *args, **kwargs) -> Optional['Search']: + """Получение предыдущей страницы поиска. + + Returns: + :obj:`yandex_music.Search` | :obj:`None`: Предыдущая страница результата поиска или :obj:`None`. + """ + return await self.get_page_async(self.page - 1, *args, **kwargs) + @classmethod def de_json(cls, data: dict, client: 'Client') -> Optional['Search']: """Десериализация объекта. @@ -126,9 +153,15 @@ class Search(YandexMusicObject): # camelCase псевдонимы - #: Псевдоним для :attr:`next_page` - nextPage = next_page - #: Псевдоним для :attr:`prev_page` - prevPage = prev_page #: Псевдоним для :attr:`get_page` getPage = get_page + #: Псевдоним для :attr:`get_page_async` + getPageAsync = get_page_async + #: Псевдоним для :attr:`next_page` + nextPage = next_page + #: Псевдоним для :attr:`next_page_async` + nextPageASync = next_page_async + #: Псевдоним для :attr:`prev_page` + prevPage = prev_page + #: Псевдоним для :attr:`prev_page_async` + prevPageAsync = prev_page_async diff --git a/yandex_music/shot/shot_data.py b/yandex_music/shot/shot_data.py index 7845ec5..12a6e9c 100644 --- a/yandex_music/shot/shot_data.py +++ b/yandex_music/shot/shot_data.py @@ -37,6 +37,15 @@ class ShotData(YandexMusicObject): """ self.client.request.download(f'https://{self.cover_uri.replace("%%", size)}', filename) + async def download_cover_async(self, filename: str, size: str = '200x200') -> None: + """Загрузка обложки. + + Args: + filename (:obj:`str`): Путь для сохранения файла с названием и расширением. + size (:obj:`str`, optional): Размер обложки. + """ + await self.client.request.download(f'https://{self.cover_uri.replace("%%", size)}', filename) + def download_mds(self, filename: str) -> None: """Загрузка аудиоверсии шота. @@ -45,6 +54,14 @@ class ShotData(YandexMusicObject): """ self.client.request.download(self.mds_url, filename) + async def download_mds_async(self, filename: str) -> None: + """Загрузка аудиоверсии шота. + + Args: + filename (:obj:`str`): Путь для сохранения файла с названием и расширением. + """ + await self.client.request.download(self.mds_url, filename) + @classmethod def de_json(cls, data: dict, client: 'Client') -> Optional['ShotData']: """Десериализация объекта. @@ -70,5 +87,9 @@ class ShotData(YandexMusicObject): #: Псевдоним для :attr:`download_cover` downloadCover = download_cover + #: Псевдоним для :attr:`download_cover_async` + downloadCoverAsync = download_cover_async #: Псевдоним для :attr:`download_mds` downloadMds = download_mds + #: Псевдоним для :attr:`download_mds_async` + downloadMdsAsync = download_mds_async diff --git a/yandex_music/track/track.py b/yandex_music/track/track.py index fe3e60b..91f8784 100644 --- a/yandex_music/track/track.py +++ b/yandex_music/track/track.py @@ -119,6 +119,7 @@ class Track(YandexMusicObject): client: Optional['Client'] = None def __post_init__(self): + self.download_info = None self._id_attrs = (self.id,) def get_download_info(self, get_direct_links=False) -> List['DownloadInfo']: @@ -130,6 +131,15 @@ class Track(YandexMusicObject): return self.download_info + async def get_download_info_async(self, get_direct_links=False) -> List['DownloadInfo']: + """Сокращение для:: + + await client.tracks_download_info(self.track_id, get_direct_links) + """ + self.download_info = await self.client.tracks_download_info(self.track_id, get_direct_links) + + return self.download_info + def get_supplement(self, *args, **kwargs) -> Optional['Supplement']: """Сокращение для:: @@ -137,6 +147,13 @@ class Track(YandexMusicObject): """ return self.client.track_supplement(self.id, *args, **kwargs) + async def get_supplement_async(self, *args, **kwargs) -> Optional['Supplement']: + """Сокращение для:: + + await client.track_supplement(track.id, *args, **kwargs) + """ + return await self.client.track_supplement(self.id, *args, **kwargs) + def download_cover(self, filename: str, size: str = '200x200') -> None: """Загрузка обложки. @@ -146,6 +163,15 @@ class Track(YandexMusicObject): """ self.client.request.download(f'https://{self.cover_uri.replace("%%", size)}', filename) + async def download_cover_async(self, filename: str, size: str = '200x200') -> None: + """Загрузка обложки. + + Args: + filename (:obj:`str`): Путь для сохранения файла с названием и расширением. + size (:obj:`str`, optional): Размер обложки. + """ + await self.client.request.download(f'https://{self.cover_uri.replace("%%", size)}', filename) + def download_og_image(self, filename: str, size: str = '200x200') -> None: """Загрузка обложки. @@ -157,6 +183,17 @@ class Track(YandexMusicObject): """ self.client.request.download(f'https://{self.og_image.replace("%%", size)}', filename) + async def download_og_image_async(self, filename: str, size: str = '200x200') -> None: + """Загрузка обложки. + + Предпочтительнее использовать `self.download_cover_async()`. + + Args: + filename (:obj:`str`): Путь для сохранения файла с названием и расширением. + size (:obj:`str`, optional): Размер обложки. + """ + await self.client.request.download(f'https://{self.og_image.replace("%%", size)}', filename) + def download(self, filename: str, codec: str = 'mp3', bitrate_in_kbps: int = 192) -> None: """Загрузка трека. @@ -183,6 +220,32 @@ class Track(YandexMusicObject): else: raise InvalidBitrate('Unavailable bitrate') + async def download_async(self, filename: str, codec: str = 'mp3', bitrate_in_kbps: int = 192) -> None: + """Загрузка трека. + + Note: + Известные значения `codec`: `mp3`, `aac`. + + Известные значения `bitrate_in_kbps`: `64`, `128`, `192`, `320`. + + Args: + filename (:obj:`str`): Путь для сохранения файла с названием и расширением. + codec (:obj:`str`, optional): Кодек из доступных в `self.download_info`. + bitrate_in_kbps (:obj:`int`, optional): Битрейт из доступных в `self.download_info` для данного кодека. + + Raises: + :class:`yandex_music.exceptions.InvalidBitrate`: Если в `self.download_info` не найден подходящий трек. + """ + if self.download_info is None: + await self.get_download_info_async() + + for info in self.download_info: + if info.codec == codec and info.bitrate_in_kbps == bitrate_in_kbps: + await info.download_async(filename) + break + else: + raise InvalidBitrate('Unavailable bitrate') + def like(self, *args, **kwargs) -> bool: """Сокращение для:: @@ -190,6 +253,13 @@ class Track(YandexMusicObject): """ return self.client.users_likes_tracks_add(self.track_id, self.client.me.account.uid, *args, **kwargs) + async def like_async(self, *args, **kwargs) -> bool: + """Сокращение для:: + + await client.users_likes_tracks_add(track.id, user.id, *args, **kwargs) + """ + return await self.client.users_likes_tracks_add(self.track_id, self.client.me.account.uid, *args, **kwargs) + def dislike(self, *args, **kwargs) -> bool: """Сокращение для:: @@ -197,6 +267,13 @@ class Track(YandexMusicObject): """ return self.client.users_likes_tracks_remove(self.track_id, self.client.me.account.uid, *args, **kwargs) + async def dislike_async(self, *args, **kwargs) -> bool: + """Сокращение для:: + + await client.users_likes_tracks_remove(track.id, user.id *args, **kwargs) + """ + return await self.client.users_likes_tracks_remove(self.track_id, self.client.me.account.uid, *args, **kwargs) + def artists_name(self) -> List[str]: """Получает имена всех исполнителей. @@ -262,11 +339,25 @@ class Track(YandexMusicObject): #: Псевдоним для :attr:`get_download_info` getDownloadInfo = get_download_info + #: Псевдоним для :attr:`get_download_info_async` + getDownloadInfoAsync = get_download_info_async #: Псевдоним для :attr:`get_supplement` getSupplement = get_supplement + #: Псевдоним для :attr:`get_supplement_async` + getSupplementAsync = get_supplement_async #: Псевдоним для :attr:`download_cover` downloadCover = download_cover + #: Псевдоним для :attr:`download_cover_async` + downloadCoverAsync = download_cover_async #: Псевдоним для :attr:`download_og_image` downloadOgImage = download_og_image + #: Псевдоним для :attr:`download_og_image_async` + downloadOgImageAsync = download_og_image_async #: Псевдоним для :attr:`track_id` trackId = track_id + #: Псевдоним для :attr:`like_async` + likeAsync = like_async + #: Псевдоним для :attr:`dislike_async` + dislike_async = dislike_async + #: Псевдоним для :attr:`download_async` + downloadAsync = download_async diff --git a/yandex_music/track_short.py b/yandex_music/track_short.py index 2f30ad8..4c67cb9 100644 --- a/yandex_music/track_short.py +++ b/yandex_music/track_short.py @@ -45,6 +45,14 @@ class TrackShort(YandexMusicObject): """ return self.client.tracks(self.track_id)[0] + async def fetch_track_async(self) -> 'Track': + """Получение полной версии трека. + + Returns: + :obj:`yandex_music.Track`: Полная версия трека. + """ + return (await self.client.tracks(self.track_id))[0] + @property def track_id(self) -> str: """:obj:`str`: Уникальный идентификатор трека состоящий из его номера и номера альбома или просто из номера.""" @@ -95,5 +103,7 @@ class TrackShort(YandexMusicObject): #: Псевдоним для :attr:`fetch_track` fetchTrack = fetch_track + #: Псевдоним для :attr:`fetch_track_async` + fetchTrackAsync = fetch_track_async #: Псевдоним для :attr:`track_id` trackId = track_id diff --git a/yandex_music/tracks_list.py b/yandex_music/tracks_list.py index eca7dc6..8e6135f 100644 --- a/yandex_music/tracks_list.py +++ b/yandex_music/tracks_list.py @@ -48,6 +48,14 @@ class TracksList(YandexMusicObject): """ return self.client.tracks(self.tracks_ids) + async def fetch_tracks_async(self) -> List['Track']: + """Получение полных версии треков. + + Returns: + :obj:`list` из :obj:`yandex_music.Track`: Полная версия трека. + """ + return await self.client.tracks(self.tracks_ids) + @classmethod def de_json(cls, data: dict, client: 'Client') -> Optional['TracksList']: """Десериализация объекта. @@ -75,3 +83,5 @@ class TracksList(YandexMusicObject): tracksIds = tracks_ids #: Псевдоним для :attr:`fetch_tracks` fetchTracks = fetch_tracks + #: Псевдоним для :attr:`fetch_tracks_async` + fetchTracksAsync = fetch_tracks_async diff --git a/yandex_music/utils/request.py b/yandex_music/utils/request.py index f433b56..28bf920 100644 --- a/yandex_music/utils/request.py +++ b/yandex_music/utils/request.py @@ -49,6 +49,10 @@ class Request: self.client = self.set_and_return_client(client) + # aiohttp + self.proxy_url = proxy_url + + # requests self.proxies = {'http': proxy_url, 'https': proxy_url} if proxy_url else None def set_language(self, lang: str) -> None: @@ -174,7 +178,7 @@ class Request: **kwargs: Произвольные ключевые аргументы для `requests.request`. Returns: - :obj:`yandex_music.utils.response.Response`: Ответ API. + :obj:`bytes`: Тело ответа. Raises: :class:`yandex_music.exceptions.TimedOut`: При превышении времени ожидания. @@ -195,7 +199,7 @@ class Request: raise NetworkError(e) if 200 <= resp.status_code <= 299: - return resp + return resp.content parse = self._parse(resp.content) message = parse.get_error() or 'Unknown HTTPError' @@ -212,7 +216,7 @@ class Request: else: raise NetworkError(f'{message} ({resp.status_code})') - def get(self, url: str, params: dict = None, timeout: Union[int, float] = 5, *args, **kwargs): + def get(self, url: str, params: dict = None, timeout: Union[int, float] = 5, *args, **kwargs) -> dict: """Отправка GET запроса. Args: @@ -224,7 +228,7 @@ class Request: **kwargs: Произвольные ключевые аргументы для `requests.request`. Returns: - :obj:`yandex_music.utils.response.Response`: Ответ API. + :obj:`dict`: Обработанное тело ответа. Raises: :class:`yandex_music.exceptions.YandexMusicError`: Базовое исключение библиотеки. @@ -233,9 +237,9 @@ class Request: 'GET', url, params=params, headers=self.headers, proxies=self.proxies, timeout=timeout, *args, **kwargs ) - return self._parse(result.content).get_result() + return self._parse(result).get_result() - def post(self, url, data=None, timeout=5, *args, **kwargs): + def post(self, url, data=None, timeout=5, *args, **kwargs) -> dict: """Отправка POST запроса. Args: @@ -247,7 +251,7 @@ class Request: **kwargs: Произвольные ключевые аргументы для `requests.request`. Returns: - :obj:`yandex_music.utils.response.Response`: Ответ API. + :obj:`dict`: Обработанное тело ответа. Raises: :class:`yandex_music.exceptions.YandexMusicError`: Базовое исключение библиотеки. @@ -256,9 +260,9 @@ class Request: 'POST', url, headers=self.headers, proxies=self.proxies, data=data, timeout=timeout, *args, **kwargs ) - return self._parse(result.content).get_result() + return self._parse(result).get_result() - def retrieve(self, url, timeout=5, *args, **kwargs): + def retrieve(self, url, timeout=5, *args, **kwargs) -> bytes: """Отправка GET запроса и получение содержимого без обработки (парсинга). Args: @@ -269,14 +273,14 @@ class Request: **kwargs: Произвольные ключевые аргументы для `requests.request`. Returns: - :obj:`Response`: Экземляр объекта ответа библиотеки `requests`. + :obj:`bytes`: Тело ответа. Raises: :class:`yandex_music.exceptions.YandexMusicError`: Базовое исключение библиотеки. """ return self._request_wrapper('GET', url, proxies=self.proxies, timeout=timeout, *args, **kwargs) - def download(self, url, filename, timeout=5, *args, **kwargs): + def download(self, url, filename, timeout=5, *args, **kwargs) -> None: """Отправка запроса на получение содержимого и его запись в файл. Args: @@ -292,4 +296,4 @@ class Request: """ result = self.retrieve(url, timeout=timeout, *args, *kwargs) with open(filename, 'wb') as f: - f.write(result.content) + f.write(result) diff --git a/yandex_music/utils/request_async.py b/yandex_music/utils/request_async.py new file mode 100644 index 0000000..94535e2 --- /dev/null +++ b/yandex_music/utils/request_async.py @@ -0,0 +1,323 @@ +#################################################################### +# THIS IS AUTO GENERATED COPY OF client.py. DON'T EDIT IN BY HANDS # +#################################################################### + +import re +import logging +import keyword + +from typing import TYPE_CHECKING, Optional, Union + +# Не используется ujson из-за отсутствия в нём object_hook'a +# Отправка вообще application/x-www-form-urlencoded, а не JSON'a +# https://github.com/psf/requests/blob/master/requests/models.py#L508 +import json + +import asyncio +import aiohttp +import aiofiles + +from yandex_music.utils.response import Response +from yandex_music.exceptions import ( + Unauthorized, + BadRequest, + NetworkError, + YandexMusicError, + TimedOut, +) + +if TYPE_CHECKING: + from yandex_music import Client + + +USER_AGENT = 'Yandex-Music-API' +HEADERS = { + 'X-Yandex-Music-Client': 'YandexMusicAndroid/23020251', +} + +reserved_names = keyword.kwlist + ['client'] + +logging.getLogger('urllib3').setLevel(logging.WARNING) + + +class Request: + """Вспомогательный класс для yandex_music, представляющий методы для выполнения POST и GET запросов, скачивания + файлов. + + Args: + client (:obj:`yandex_music.Client`, optional): Клиент Yandex Music. + headers (:obj:`dict`, optional): Заголовки передаваемые с каждым запросом. + proxy_url (:obj:`str`, optional): Прокси. + """ + + def __init__(self, client=None, headers=None, proxy_url=None): + self.headers = headers or HEADERS.copy() + + self.client = self.set_and_return_client(client) + + # aiohttp + self.proxy_url = proxy_url + + # requests + self.proxies = {'http': proxy_url, 'https': proxy_url} if proxy_url else None + + def set_language(self, lang: str) -> None: + """Добавляет заголовок языка для каждого запроса. + + Note: + Возможные значения `lang`: en/uz/uk/us/ru/kk/hy. + + Args: + lang (:obj:`str`): Язык. + """ + self.headers.update({'Accept-Language': lang}) + + def set_authorization(self, token: str) -> None: + """Добавляет заголовок авторизации для каждого запроса. + + Note: + Используется при передаче своего экземпляра Request'a клиенту. + + Args: + token (:obj:`str`): OAuth токен. + """ + self.headers.update({'Authorization': f'OAuth {token}'}) + + def set_and_return_client(self, client) -> 'Client': + """Принимает клиент и присваивает его текущему объекту. При наличии авторизации добавляет заголовок. + + Args: + client (:obj:`yandex_music.Client`): Клиент Yandex Music. + + Returns: + :obj:`yandex_music.Client`: Клиент Yandex Music. + """ + self.client = client + + if self.client and self.client.token: + self.set_authorization(self.client.token) + + return self.client + + @staticmethod + def _convert_camel_to_snake(text: str) -> str: + """Конвертация CamelCase в SnakeCase. + + Args: + text (:obj:`str`): Название переменной в CamelCase. + + Returns: + :obj:`str`: Название переменной в SnakeCase. + """ + s = re.sub('(.)([A-Z][a-z]+)', r'\1_\2', text) + return re.sub('([a-z0-9])([A-Z])', r'\1_\2', s).lower() + + @staticmethod + def _object_hook(obj: dict) -> dict: + """Нормализация имён переменных пришедших с API. + + Note: + В названии переменной заменяет "-" на "_", конвертирует в SnakeCase, если название является + зарезервированным словом или "client" - добавляет "_" в конец. Если название переменной начинается с цифры - + добавляет в начало "_". + + Args: + obj (:obj:`dict`): Словарь, где ключ название переменной, а значение - содержимое. + + Returns: + :obj:`dict`: Тот же словарь, что и на входе, но с нормализованными ключами. + """ + cleaned_object = {} + for key, value in obj.items(): + key = Request._convert_camel_to_snake(key.replace('-', '_')) + key = key.lower() + + if key in reserved_names: + key += '_' + + if len(key) and key[0].isdigit(): + key = '_' + key + + cleaned_object.update({key: value}) + + return cleaned_object + + def _parse(self, json_data: bytes) -> Optional[Response]: + """Разбор ответа от API. + + Note: + Если данные отсутствуют в `result`, то переформировывает ответ используя данные из корня. + + Args: + json_data (:obj:`bytes`): Ответ от API. + + Returns: + :obj:`yandex_music.utils.response.Response`: Ответ API. + + Raises: + :class:`yandex_music.exceptions.YandexMusicError`: Базовое исключение библиотеки. + """ + try: + decoded_s = json_data.decode('utf-8') + data = json.loads(decoded_s, object_hook=Request._object_hook) + + except UnicodeDecodeError: + logging.getLogger(__name__).debug('Logging raw invalid UTF-8 response:\n%r', json_data) + raise YandexMusicError('Server response could not be decoded using UTF-8') + except (AttributeError, ValueError): + raise YandexMusicError('Invalid server response') + + if data.get('result') is None: + data = {'result': data, 'error': data.get('error'), 'error_description': data.get('error_description')} + + return Response.de_json(data, self.client) + + async def _request_wrapper(self, *args, **kwargs): + """Обёртка над запросом библиотеки `aiohttp`. + + Note: + Добавляет необходимые заголовки для запроса, обрабатывает статус коды, следит за таймаутом, кидает + необходимые исключения, возвращает ответ. Передаёт пользовательские аргументы в запрос. + + Args: + *args: Произвольные аргументы для `aiohttp.request`. + **kwargs: Произвольные ключевые аргументы для `aiohttp.request`. + + Returns: + :obj:`bytes`: Тело ответа. + + Raises: + :class:`yandex_music.exceptions.TimedOut`: При превышении времени ожидания. + :class:`yandex_music.exceptions.Unauthorized`: При невалидном токене, долгом ожидании прямой ссылки на файл. + :class:`yandex_music.exceptions.BadRequest`: При неправильном запросе. + :class:`yandex_music.exceptions.NetworkError`: При проблемах с сетью. + """ + if 'headers' not in kwargs: + kwargs['headers'] = {} + + kwargs['headers']['User-Agent'] = USER_AGENT + + try: + async with aiohttp.request(*args, **kwargs) as _resp: + resp = _resp + content = await resp.content.read() + except asyncio.TimeoutError: + raise TimedOut() + except aiohttp.ClientError as e: + raise NetworkError(e) + + if 200 <= resp.status <= 299: + return content + + parse = self._parse(content) + message = parse.get_error() or 'Unknown HTTPError' + + if resp.status in (401, 403): + raise Unauthorized(message) + elif resp.status == 400: + raise BadRequest(message) + elif resp.status in (404, 409, 413): + raise NetworkError(message) + + elif resp.status == 502: + raise NetworkError('Bad Gateway') + else: + raise NetworkError(f'{message} ({resp.status})') + + async def get(self, url: str, params: dict = None, timeout: Union[int, float] = 5, *args, **kwargs) -> dict: + """Отправка GET запроса. + + Args: + url (:obj:`str`): Адрес для запроса. + params (:obj:`str`): GET параметры для запроса. + timeout (:obj:`int` | :obj:`float`): Используется как время ожидания ответа от сервера вместо указанного + при создании пула. + *args: Произвольные аргументы для `aiohttp.request`. + **kwargs: Произвольные ключевые аргументы для `aiohttp.request`. + + Returns: + :obj:`dict`: Обработанное тело ответа. + + Raises: + :class:`yandex_music.exceptions.YandexMusicError`: Базовое исключение библиотеки. + """ + result = await self._request_wrapper( + 'GET', + url, + params=params, + headers=self.headers, + proxy=self.proxy_url, + timeout=aiohttp.ClientTimeout(total=timeout), + *args, + **kwargs, + ) + + return self._parse(result).get_result() + + async def post(self, url, data=None, timeout=5, *args, **kwargs) -> dict: + """Отправка POST запроса. + + Args: + url (:obj:`str`): Адрес для запроса. + data (:obj:`str`): POST тело запроса. + timeout (:obj:`int` | :obj:`float`): Используется как время ожидания ответа от сервера вместо указанного + при создании пула. + *args: Произвольные аргументы для `aiohttp.request`. + **kwargs: Произвольные ключевые аргументы для `aiohttp.request`. + + Returns: + :obj:`dict`: Обработанное тело ответа. + + Raises: + :class:`yandex_music.exceptions.YandexMusicError`: Базовое исключение библиотеки. + """ + result = await self._request_wrapper( + 'POST', + url, + headers=self.headers, + proxy=self.proxy_url, + data=data, + timeout=aiohttp.ClientTimeout(total=timeout), + *args, + **kwargs, + ) + + return self._parse(result).get_result() + + async def retrieve(self, url, timeout=5, *args, **kwargs) -> bytes: + """Отправка GET запроса и получение содержимого без обработки (парсинга). + + Args: + url (:obj:`str`): Адрес для запроса. + timeout (:obj:`int` | :obj:`float`): Используется как время ожидания ответа от сервера вместо указанного + при создании пула. + *args: Произвольные аргументы для `aiohttp.request`. + **kwargs: Произвольные ключевые аргументы для `aiohttp.request`. + + Returns: + :obj:`bytes`: Тело ответа. + + Raises: + :class:`yandex_music.exceptions.YandexMusicError`: Базовое исключение библиотеки. + """ + return await self._request_wrapper( + 'GET', url, proxy=self.proxy_url, timeout=aiohttp.ClientTimeout(total=timeout), *args, **kwargs + ) + + async def download(self, url, filename, timeout=5, *args, **kwargs) -> None: + """Отправка запроса на получение содержимого и его запись в файл. + + Args: + url (:obj:`str`): Адрес для запроса. + filename (:obj:`str`): Путь и(или) название файла вместе с расширением. + timeout (:obj:`int` | :obj:`float`): Используется как время ожидания ответа от сервера вместо указанного + при создании пула. + *args: Произвольные аргументы для `aiohttp.request`. + **kwargs: Произвольные ключевые аргументы для `aiohttp.request`. + + Raises: + :class:`yandex_music.exceptions.YandexMusicError`: Базовое исключение библиотеки. + """ + result = await self.retrieve(url, timeout=timeout, *args, *kwargs) + async with aiofiles.open(filename, 'wb') as f: + await f.write(result)