асинхронная версия библиотеки
このコミットが含まれているのは:
コミット
62741bebc9
81
README.rst
81
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 <https://2.python-requests.org/en/master/user/advanced/#proxies>`_
|
||||
|
||||
Выполнение запросов с использование прокси в асинхронной версии:
|
||||
|
||||
.. 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 <https://docs.aiohttp.org/en/stable/client_advanced.html#proxy-support>`_
|
||||
|
||||
--------------------
|
||||
Изучение по примерам
|
||||
--------------------
|
||||
|
@ -211,6 +249,33 @@ music.yandex.ru/album/**1193829**/track/**10994777**
|
|||
Посетите `эту страницу <https://github.com/MarshalX/yandex-music-api/blob/main/examples/>`_
|
||||
чтобы изучить официальные примеры.
|
||||
|
||||
----------------------------------------------
|
||||
Особенности использования асинхронного клиента
|
||||
----------------------------------------------
|
||||
|
||||
При работе с асинхронной версией библиотеке стоит всегда помнить
|
||||
следующие особенности:
|
||||
|
||||
- Клиент следует импортировать с названием ``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()
|
||||
|
||||
-----------
|
||||
Логирование
|
||||
-----------
|
||||
|
|
|
@ -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, '', 'Треки:']
|
||||
|
|
|
@ -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]
|
||||
|
|
|
@ -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 = []
|
||||
|
|
|
@ -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 'Произошла ошибка'
|
||||
|
|
|
@ -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()
|
||||
# Последняя проигрываемая очередь всегда в начале списка
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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]
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
from yandex_music import Client
|
||||
|
||||
|
||||
client = Client()
|
||||
client = Client().init()
|
||||
|
||||
type_to_name = {
|
||||
'track': 'трек',
|
||||
|
|
|
@ -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])
|
2
setup.py
2
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',
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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(
|
||||
|
|
ファイル差分が大きすぎるため省略します
差分を読み込み
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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?
|
||||
|
|
|
@ -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?
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
読み込み中…
新しいイシューから参照