асинхронная версия библиотеки

このコミットが含まれているのは:
Il'ya (Marshal) 2022-02-19 18:59:53 +01:00
コミット 62741bebc9
36個のファイルの変更3789行の追加63行の削除

ファイルの表示

@ -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': 'трек',

93
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])

ファイルの表示

@ -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(

2730
yandex_music/client_async.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

ファイルの表示

@ -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)

323
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)