Плейлист с Алисой и поддержка её шотов
このコミットが含まれているのは:
コミット
6b1c12844d
|
@ -20,7 +20,8 @@ jobs:
|
|||
python-version: ${{ matrix.python-version }}
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
curl https://bootstrap.pypa.io/get-pip.py -o get-pip.py
|
||||
python get-pip.py
|
||||
pip install pipenv
|
||||
pipenv install --dev --deploy --system
|
||||
shell: bash
|
||||
|
|
|
@ -20,7 +20,8 @@ jobs:
|
|||
python-version: ${{ matrix.python-version }}
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
curl https://bootstrap.pypa.io/get-pip.py -o get-pip.py
|
||||
python get-pip.py
|
||||
pip install pipenv
|
||||
pipenv install --dev --deploy --system
|
||||
shell: bash
|
||||
|
|
|
@ -19,7 +19,8 @@ jobs:
|
|||
python-version: ${{ matrix.python-version }}
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
curl https://bootstrap.pypa.io/get-pip.py -o get-pip.py
|
||||
python get-pip.py
|
||||
pip install pipenv twine
|
||||
pipenv install --dev --deploy --system
|
||||
shell: bash
|
||||
|
|
|
@ -23,7 +23,8 @@ jobs:
|
|||
python-version: ${{ matrix.python-version }}
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
curl https://bootstrap.pypa.io/get-pip.py -o get-pip.py
|
||||
python get-pip.py
|
||||
pip install pipenv
|
||||
pipenv install --dev --deploy --system
|
||||
shell: bash
|
||||
|
|
|
@ -70,3 +70,6 @@ from .test_value import TestValue
|
|||
from .test_video import TestVideo
|
||||
from .test_video_supplement import TestVideoSupplement
|
||||
from .test_vinyl import TestVinyl
|
||||
from .test_shot_type import TestShotType
|
||||
from .test_shot_data import TestShotData
|
||||
from .test_shot import TestShot
|
||||
|
|
|
@ -6,7 +6,7 @@ from yandex_music import Counts, TrackId, CaseForms, Ratings, Icon, Album, Lyric
|
|||
Account, Client, TrackShort, Value, DiscreteScale, PlaylistId, MixLink, Link, PassportPhone, User, Promotion, \
|
||||
PersonalPlaylistsData, RotorSettings, TrackShortOld, PlayContextsData, Status, Settings, StationResult, Enum, \
|
||||
TrackWithAds, VideoSupplement, ArtistEvent, ChartItem, Event, AlbumEvent, Day, PlayContext, Plus, Title, Label, \
|
||||
GeneratedPlaylist, Video, Vinyl, SearchResult, BlockEntity, Block, PlaylistAbsence
|
||||
GeneratedPlaylist, Video, Vinyl, SearchResult, BlockEntity, Block, PlaylistAbsence, ShotType, ShotData, Shot
|
||||
from . import TestCounts, TestTrackId, TestCaseForms, TestRatings, TestIcon, TestAlbum, TestLyrics, \
|
||||
TestTrack, TestInvocationInfo, TestPlaylist, TestAutoRenewable, TestStation, TestNormalization, TestMajor, \
|
||||
TestTrackPosition, TestBest, TestChart, TestPermissions, TestPlus, TestProduct, TestCover, TestPlayCounter, \
|
||||
|
@ -15,7 +15,7 @@ from . import TestCounts, TestTrackId, TestCaseForms, TestRatings, TestIcon, Tes
|
|||
TestUser, TestPassportPhone, TestPromotion, TestTitle, TestPersonalPlaylistsData, TestRotorSettings, \
|
||||
TestTrackShortOld, TestPager, TestStatus, TestSettings, TestStationResult, TestLabel, TestTrackWithAds, \
|
||||
TestVideoSupplement, TestEvent, TestDay, TestPlayContext, TestGeneratedPlaylist, TestVideo, TestVinyl, \
|
||||
TestSearchResult, TestBlockEntity, TestBlock, TestPlaylistAbsence
|
||||
TestSearchResult, TestBlockEntity, TestBlock, TestPlaylistAbsence, TestShot, TestShotData, TestShotType
|
||||
|
||||
|
||||
@pytest.fixture(scope='session')
|
||||
|
@ -438,6 +438,21 @@ def station(id_, icon, restrictions):
|
|||
return Station(id_, TestStation.name, icon, icon, icon, TestStation.id_for_from, restrictions, restrictions, id_)
|
||||
|
||||
|
||||
@pytest.fixture(scope='session')
|
||||
def shot_type():
|
||||
return ShotType(TestShotType.id, TestShotType.title)
|
||||
|
||||
|
||||
@pytest.fixture(scope='session')
|
||||
def shot_data(shot_type):
|
||||
return ShotData(TestShotData.cover_uri, TestShotData.mds_url, TestShotData.shot_text, shot_type)
|
||||
|
||||
|
||||
@pytest.fixture(scope='session')
|
||||
def shot(shot_data):
|
||||
return Shot(TestShot.order, TestShot.played, shot_data, TestShot.shot_id, TestShot.status)
|
||||
|
||||
|
||||
@pytest.fixture(scope='session')
|
||||
def chart_item(track, chart):
|
||||
return ChartItem(track, chart)
|
||||
|
|
|
@ -0,0 +1,53 @@
|
|||
from yandex_music import Shot
|
||||
|
||||
|
||||
class TestShot:
|
||||
order = 0
|
||||
played = False
|
||||
shot_id = '1036797'
|
||||
status = 'ready'
|
||||
|
||||
def test_expected_values(self, shot, shot_data):
|
||||
assert shot.order == self.order
|
||||
assert shot.played == self.played
|
||||
assert shot.shot_id == self.shot_id
|
||||
assert shot.status == self.status
|
||||
assert shot.shot_data == shot_data
|
||||
|
||||
def test_de_json_none(self, client):
|
||||
assert Shot.de_json({}, client) is None
|
||||
|
||||
def test_de_json_required(self, client, shot_data):
|
||||
json_dict = {'order': self.order, 'played': self.played, 'shot_id': self.shot_id,
|
||||
'status': self.status, 'shot_data': shot_data.to_dict()}
|
||||
shot = Shot.de_json(json_dict, client)
|
||||
|
||||
assert shot.order == self.order
|
||||
assert shot.played == self.played
|
||||
assert shot.shot_id == self.shot_id
|
||||
assert shot.status == self.status
|
||||
assert shot.shot_data == shot_data
|
||||
|
||||
def test_de_json_all(self, client, shot_data):
|
||||
json_dict = {'order': self.order, 'played': self.played, 'shot_id': self.shot_id,
|
||||
'status': self.status, 'shot_data': shot_data.to_dict()}
|
||||
shot = Shot.de_json(json_dict, client)
|
||||
|
||||
assert shot.order == self.order
|
||||
assert shot.played == self.played
|
||||
assert shot.shot_id == self.shot_id
|
||||
assert shot.status == self.status
|
||||
assert shot.shot_data == shot_data
|
||||
|
||||
def test_equality(self, shot_data):
|
||||
a = Shot(self.order, self.played, shot_data, self.shot_id, self.status)
|
||||
b = Shot(self.order, True, shot_data, self.shot_id, self.status)
|
||||
c = Shot(self.order, self.played, shot_data, '10', self.status)
|
||||
d = Shot(self.order, self.played, shot_data, self.shot_id, self.status)
|
||||
|
||||
assert a != b != c != d
|
||||
assert hash(a) != hash(b) != hash(c) != hash(d)
|
||||
assert a is not b is not c is not d
|
||||
|
||||
assert a == d
|
||||
assert hash(a) == hash(d)
|
|
@ -0,0 +1,49 @@
|
|||
from yandex_music import ShotData
|
||||
|
||||
|
||||
class TestShotData:
|
||||
cover_uri = 'avatars.mds.yandex.net/get-music-misc/49997/img.5da435f1da39b871a74270e2/%%'
|
||||
mds_url = 'https://storage.mds.yandex.net/get-music/1634376/public/shots/1036797_1574621686'
|
||||
shot_text = 'Бард - это не просто певец, это поющий поэт.'
|
||||
|
||||
def test_expected_values(self, shot_data, shot_type):
|
||||
assert shot_data.cover_uri == self.cover_uri
|
||||
assert shot_data.mds_url == self.mds_url
|
||||
assert shot_data.shot_text == self.shot_text
|
||||
assert shot_data.shot_type == shot_type
|
||||
|
||||
def test_de_json_none(self, client):
|
||||
assert ShotData.de_json({}, client) is None
|
||||
|
||||
def test_de_json_required(self, client, shot_type):
|
||||
json_dict = {'cover_uri': self.cover_uri, 'mds_url': self.mds_url, 'shot_text': self.shot_text,
|
||||
'shot_type': shot_type.to_dict()}
|
||||
shot_data = ShotData.de_json(json_dict, client)
|
||||
|
||||
assert shot_data.cover_uri == self.cover_uri
|
||||
assert shot_data.mds_url == self.mds_url
|
||||
assert shot_data.shot_text == self.shot_text
|
||||
assert shot_data.shot_type == shot_type
|
||||
|
||||
def test_de_json_all(self, client, shot_type):
|
||||
json_dict = {'cover_uri': self.cover_uri, 'mds_url': self.mds_url, 'shot_text': self.shot_text,
|
||||
'shot_type': shot_type.to_dict()}
|
||||
shot_data = ShotData.de_json(json_dict, client)
|
||||
|
||||
assert shot_data.cover_uri == self.cover_uri
|
||||
assert shot_data.mds_url == self.mds_url
|
||||
assert shot_data.shot_text == self.shot_text
|
||||
assert shot_data.shot_type == shot_type
|
||||
|
||||
def test_equality(self, shot_type):
|
||||
a = ShotData(self.cover_uri, self.mds_url, self.shot_text, shot_type)
|
||||
b = ShotData('', self.mds_url, self.shot_text, shot_type)
|
||||
c = ShotData(self.cover_uri, '', self.shot_text, shot_type)
|
||||
d = ShotData(self.cover_uri, self.mds_url, self.shot_text, shot_type)
|
||||
|
||||
assert a != b != c != d
|
||||
assert hash(a) != hash(b) != hash(c) != hash(d)
|
||||
assert a is not b is not c is not d
|
||||
|
||||
assert a == d
|
||||
assert hash(a) == hash(d)
|
|
@ -0,0 +1,46 @@
|
|||
import pytest
|
||||
|
||||
from yandex_music import ShotEvent
|
||||
|
||||
|
||||
@pytest.fixture(scope='class')
|
||||
def shot_event(shot):
|
||||
return ShotEvent(TestShotType.event_id, [shot])
|
||||
|
||||
|
||||
class TestShotType:
|
||||
event_id = '5e25fb2c0cf28e741cb996eb'
|
||||
|
||||
def test_expected_values(self, shot_event, shot):
|
||||
assert shot_event.event_id == self.event_id
|
||||
assert shot_event.shots == [shot]
|
||||
|
||||
def test_de_json_none(self, client):
|
||||
assert ShotEvent.de_json({}, client) is None
|
||||
|
||||
def test_de_json_required(self, client, shot):
|
||||
json_dict = {'event_id': self.event_id, 'shots': [shot.to_dict()]}
|
||||
shot_event = ShotEvent.de_json(json_dict, client)
|
||||
|
||||
assert shot_event.event_id == self.event_id
|
||||
assert shot_event.shots == [shot]
|
||||
|
||||
def test_de_json_all(self, client, shot):
|
||||
json_dict = {'event_id': self.event_id, 'shots': [shot.to_dict()]}
|
||||
shot_event = ShotEvent.de_json(json_dict, client)
|
||||
|
||||
assert shot_event.event_id == self.event_id
|
||||
assert shot_event.shots == [shot]
|
||||
|
||||
def test_equality(self, shot):
|
||||
a = ShotEvent(self.event_id, [shot])
|
||||
b = ShotEvent('', [shot])
|
||||
c = ShotEvent(self.event_id, [])
|
||||
d = ShotEvent(self.event_id, [shot])
|
||||
|
||||
assert a != b != c != d
|
||||
assert hash(a) != hash(b) != hash(c) != hash(d)
|
||||
assert a is not b is not c is not d
|
||||
|
||||
assert a == d
|
||||
assert hash(a) == hash(d)
|
|
@ -0,0 +1,41 @@
|
|||
from yandex_music import ShotType
|
||||
|
||||
|
||||
class TestShotType:
|
||||
id = 'alice'
|
||||
title = 'Шот от Алисы'
|
||||
|
||||
def test_expected_values(self, shot_type):
|
||||
assert shot_type.id_ == self.id
|
||||
assert shot_type.title == self.title
|
||||
|
||||
def test_de_json_none(self, client):
|
||||
assert ShotType.de_json({}, client) is None
|
||||
|
||||
def test_de_json_required(self, client):
|
||||
json_dict = {'id_': self.id, 'title': self.title}
|
||||
shot_type = ShotType.de_json(json_dict, client)
|
||||
|
||||
assert shot_type.id_ == self.id
|
||||
assert shot_type.title == self.title
|
||||
|
||||
def test_de_json_all(self, client):
|
||||
json_dict = {'id_': self.id, 'title': self.title}
|
||||
shot_type = ShotType.de_json(json_dict, client)
|
||||
|
||||
assert shot_type.id_ == self.id
|
||||
assert shot_type.title == self.title
|
||||
|
||||
def test_equality(self):
|
||||
a = ShotType(self.id, self.title)
|
||||
b = ShotType('', self.title)
|
||||
c = ShotType(self.id, '')
|
||||
d = ShotType('', '')
|
||||
e = ShotType(self.id, self.title)
|
||||
|
||||
assert a != b != c != d != e
|
||||
assert hash(a) != hash(b) != hash(c) != hash(d) != hash(e)
|
||||
assert a is not b is not c is not d is not e
|
||||
|
||||
assert a == e
|
||||
assert hash(a) == hash(e)
|
|
@ -35,6 +35,11 @@ from .playlist.playlist_id import PlaylistId
|
|||
from .playlist.playlist_absence import PlaylistAbsence
|
||||
from .playlist.playlist import Playlist
|
||||
|
||||
from .shot.shot_type import ShotType
|
||||
from .shot.shot_data import ShotData
|
||||
from .shot.shot import Shot
|
||||
from .shot.shot_event import ShotEvent
|
||||
|
||||
from .tracks_list import TracksList
|
||||
from .track.major import Major
|
||||
from .track.normalization import Normalization
|
||||
|
@ -112,6 +117,6 @@ __all__ = ['YandexMusicObject', 'Client', 'Account', 'PassportPhone', 'Invocatio
|
|||
'Suggestions', 'MixLink', 'BlockEntity', 'Block', 'PlayContextsData', 'TrackId', 'TrackShortOld',
|
||||
'PersonalPlaylistsData', 'Promotion', 'Landing', 'Chart', 'ChartItem', 'PlayContext', 'Title', 'Genre',
|
||||
'Icon', 'Images', 'Id', 'Station', 'Dashboard', 'RotorSettings', 'AdParams', 'Restrictions', 'Value', 'Enum',
|
||||
'DiscreteScale', 'StationResult', 'Sequence', 'StationTracksResult', 'BriefInfo', 'Description', 'PlaylistId',
|
||||
'Vinyl', 'Supplement', 'Lyrics', 'VideoSupplement', 'ArtistTracks', 'Pager', 'ArtistAlbums',
|
||||
'PlaylistAbsence']
|
||||
'DiscreteScale', 'StationResult', 'Sequence', 'StationTracksResult', 'BriefInfo', 'Description',
|
||||
'PlaylistId', 'Vinyl', 'Supplement', 'Lyrics', 'VideoSupplement', 'ArtistTracks', 'Pager', 'ArtistAlbums',
|
||||
'PlaylistAbsence', 'Shot', 'ShotEvent', 'ShotType', 'ShotData']
|
||||
|
|
|
@ -6,7 +6,7 @@ from typing import Callable, Union, List, Optional
|
|||
from yandex_music import YandexMusicObject, Status, Settings, PermissionAlerts, Experiments, Artist, Album, Playlist, \
|
||||
TracksList, Track, AlbumsLikes, ArtistsLikes, PlaylistsLikes, Feed, PromoCodeStatus, DownloadInfo, Search, \
|
||||
Suggestions, Landing, Genre, Dashboard, StationResult, StationTracksResult, BriefInfo, Supplement, ArtistTracks, \
|
||||
ArtistAlbums
|
||||
ArtistAlbums, ShotEvent
|
||||
from yandex_music.utils.request import Request
|
||||
from yandex_music.utils.difference import Difference
|
||||
from yandex_music.exceptions import InvalidToken, Captcha
|
||||
|
@ -1443,6 +1443,57 @@ class Client(YandexMusicObject):
|
|||
timeout: Union[int, float] = None, *args, **kwargs) -> bool:
|
||||
return self._dislike_action(track_ids, True, user_id, timeout, *args, **kwargs)
|
||||
|
||||
@log
|
||||
def after_track(self, next_track_id: Union[str, int], context_item: str, prev_track_id: Union[str, int] = None,
|
||||
context: str = 'playlist', types: str = 'shot', from_: str = 'mobile-landing-origin-default',
|
||||
timeout: Union[int, float] = None, *args, **kwargs) -> Optional[ShotEvent]:
|
||||
"""Получение рекламы или шота от Алисы после трека.
|
||||
|
||||
При получения шота от Алисы `prev_track_id` можно не указывать.
|
||||
|
||||
Если `context = 'playlist'`, то в `context_item` необходимо передать `{OWNER_PLAYLIST}:{ID_PLAYLIST}`.
|
||||
Плейлист с Алисой имеет владельца с `id = 940441070`.
|
||||
|
||||
ID плейлиста можно получить из блоков landing'a. Получить шот чужого плейлиста нельзя.
|
||||
|
||||
Известные значения `context`: `playlist`.
|
||||
Известные значения `types`: `shot`, `ad`.
|
||||
|
||||
Args:
|
||||
prev_track_id (:obj:`str` | :obj:`int`): Уникальный идентификатор предыдущего трека.
|
||||
next_track_id (:obj:`str` | :obj:`int`): Уникальный идентификатор следующего трека.
|
||||
context_item (:obj:`str`): Уникальный идентификатор контекста.
|
||||
context (:obj:`str`, optional): Место, откуда было вызвано получение.
|
||||
types (:obj:`str`, optional): Тип того, что вернуть после трека.
|
||||
from_ (:obj:`str`, optional): Место, с которого попали в контекст.
|
||||
timeout (:obj:`int` | :obj:`float`, optional): Если это значение указано, используется как время ожидания
|
||||
ответа от сервера вместо указанного при создании пула.
|
||||
**kwargs (:obj:`dict`, optional): Произвольные аргументы (будут переданы в запрос).
|
||||
|
||||
Returns:
|
||||
:obj:`yandex_music.ShotEvent`: Объекта класса :class:`yandex_music.ShotEvent`
|
||||
представляющий шоты от Алисы, иначе :obj:`None`.
|
||||
|
||||
Raises:
|
||||
:class:`yandex_music.YandexMusicError`
|
||||
"""
|
||||
|
||||
url = f'{self.base_url}/after-track'
|
||||
|
||||
params = {
|
||||
'from': from_,
|
||||
'prevTrackId': prev_track_id,
|
||||
'nextTrackId': next_track_id,
|
||||
'context': context,
|
||||
'contextItem': context_item,
|
||||
'types': types,
|
||||
}
|
||||
|
||||
result = self._request.get(url, params=params, timeout=timeout, *args, **kwargs)
|
||||
|
||||
# TODO судя по всему эндпоинт ещё возвращает рекламу после треков для пользователей без подписки.
|
||||
return ShotEvent.de_json(result.get('shot_event'), self)
|
||||
|
||||
# camelCase псевдонимы
|
||||
|
||||
#: Псевдоним для :attr:`from_credentials`
|
||||
|
@ -1549,3 +1600,5 @@ class Client(YandexMusicObject):
|
|||
usersDislikesTracksAdd = users_dislikes_tracks_add
|
||||
#: Псевдоним для :attr:`users_dislikes_tracks_remove`
|
||||
usersDislikesTracksRemove = users_dislikes_tracks_remove
|
||||
#: Псевдоним для :attr:`after_track`
|
||||
afterTrack = after_track
|
||||
|
|
|
@ -0,0 +1,93 @@
|
|||
from typing import TYPE_CHECKING, Optional, List
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from yandex_music import Client, ShotData
|
||||
|
||||
from yandex_music import YandexMusicObject
|
||||
|
||||
|
||||
class Shot(YandexMusicObject):
|
||||
"""Класс, представляющий шот от Алисы.
|
||||
|
||||
Известные значения поля `status`: `ready`.
|
||||
|
||||
Attributes:
|
||||
order (:obj:`int`): Порядковый номер при воспроизведении.
|
||||
played (:obj:`bool`): Был ли проигран шот.
|
||||
shot_data (:obj:`yandex_music.ShotData`): Объект класса :class:`yandex_music.ShotData` представляющий
|
||||
основную информацию о шоте.
|
||||
shot_id (:obj:`str`): Уникальный идентификатор шота.
|
||||
status (:obj:`str`): Статус шота.
|
||||
client (:obj:`yandex_music.Client`): Объект класса :class:`yandex_music.Client` представляющий клиент Yandex
|
||||
Music.
|
||||
|
||||
Args:
|
||||
order (:obj:`int`): Порядковый номер при воспроизведении.
|
||||
played (:obj:`bool`): Был ли проигран шот.
|
||||
shot_data (:obj:`yandex_music.ShotData`): Объект класса :class:`yandex_music.ShotData` представляющий
|
||||
основную информацию о шоте.
|
||||
shot_id (:obj:`str`): Уникальный идентификатор шота.
|
||||
status (:obj:`str`): Статус шота.
|
||||
client (:obj:`yandex_music.Client`, optional): Объект класса :class:`yandex_music.Client` представляющий клиент
|
||||
Yandex Music.
|
||||
**kwargs: Произвольные ключевые аргументы полученные от API.
|
||||
"""
|
||||
|
||||
def __init__(self,
|
||||
order: int,
|
||||
played: bool,
|
||||
shot_data: 'ShotData',
|
||||
shot_id: str,
|
||||
status: str,
|
||||
client: Optional['Client'] = None,
|
||||
**kwargs):
|
||||
self.order = order
|
||||
self.played = played
|
||||
self.shot_data = shot_data
|
||||
self.shot_id = shot_id
|
||||
self.status = status
|
||||
|
||||
self.client = client
|
||||
self._id_attrs = (self.order, self.played, self.shot_data, self.shot_id, self.status)
|
||||
|
||||
@classmethod
|
||||
def de_json(cls, data: dict, client: 'Client') -> Optional['Shot']:
|
||||
"""Десериализация объекта.
|
||||
|
||||
Args:
|
||||
data (:obj:`dict`): Поля и значения десериализуемого объекта.
|
||||
client (:obj:`yandex_music.Client`): Объект класса :class:`yandex_music.Client` представляющий клиент Yandex
|
||||
Music.
|
||||
|
||||
Returns:
|
||||
:obj:`yandex_music.Shot`: Объект класса :class:`yandex_music.Shot`.
|
||||
"""
|
||||
if not data:
|
||||
return None
|
||||
|
||||
data = super(Shot, cls).de_json(data, client)
|
||||
from yandex_music import ShotData
|
||||
data['shot_data'] = ShotData.de_json(data.get('shot_data'), client)
|
||||
|
||||
return cls(client=client, **data)
|
||||
|
||||
@classmethod
|
||||
def de_list(cls, data: dict, client: 'Client') -> List['Shot']:
|
||||
"""Десериализация списка объектов.
|
||||
|
||||
Args:
|
||||
data (:obj:`list`): Список словарей с полями и значениями десериализуемого объекта.
|
||||
client (:obj:`yandex_music.Client`): Объект класса :class:`yandex_music.Client` представляющий клиент Yandex
|
||||
Music.
|
||||
|
||||
Returns:
|
||||
:obj:`list` из :obj:`yandex_music.Shot`: Список объектов класса :class:`yandex_music.Shot`.
|
||||
"""
|
||||
if not data:
|
||||
return []
|
||||
|
||||
shots = list()
|
||||
for shot in data:
|
||||
shots.append(cls.de_json(shot, client))
|
||||
|
||||
return shots
|
|
@ -0,0 +1,92 @@
|
|||
from typing import TYPE_CHECKING, Optional
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from yandex_music import Client, ShotType
|
||||
|
||||
from yandex_music import YandexMusicObject
|
||||
|
||||
|
||||
class ShotData(YandexMusicObject):
|
||||
"""Класс, представляющий основную информацию о шоте.
|
||||
|
||||
Attributes:
|
||||
cover_uri (:obj:`str`): Ссылка на обложку шота (иконка Алисы).
|
||||
mds_url (:obj:`str`): Ссылка на аудиоверсию шота в озвучке от Алисы.
|
||||
shot_text (:obj:`str`): Текстовая версия шота.
|
||||
shot_type (:obj:`yandex_music.ShotType`): Объект класса :class:`yandex_music.ShotType` представляющий тип
|
||||
шота.
|
||||
client (:obj:`yandex_music.Client`): Объект класса :class:`yandex_music.Client` представляющий клиент Yandex
|
||||
Music.
|
||||
|
||||
Args:
|
||||
cover_uri (:obj:`str`): Ссылка на обложку шота (иконка Алисы).
|
||||
mds_url (:obj:`str`): Ссылка на аудиоверсию шота в озвучке от Алисы.
|
||||
shot_text (:obj:`str`): Текстовая версия шота.
|
||||
shot_type (:obj:`yandex_music.ShotType`): Объект класса :class:`yandex_music.ShotType` представляющий тип
|
||||
шота.
|
||||
client (:obj:`yandex_music.Client`, optional): Объект класса :class:`yandex_music.Client` представляющий клиент
|
||||
Yandex Music.
|
||||
**kwargs: Произвольные ключевые аргументы полученные от API.
|
||||
"""
|
||||
|
||||
def __init__(self,
|
||||
cover_uri: str,
|
||||
mds_url: str,
|
||||
shot_text: str,
|
||||
shot_type: 'ShotType',
|
||||
client: Optional['Client'] = None,
|
||||
**kwargs):
|
||||
self.cover_uri = cover_uri
|
||||
self.mds_url = mds_url
|
||||
self.shot_text = shot_text
|
||||
self.shot_type = shot_type
|
||||
|
||||
self.client = client
|
||||
self._id_attrs = (self.cover_uri, self.mds_url, self.shot_text, self.shot_type)
|
||||
|
||||
def download_cover(self, filename: str, size: str = '200x200') -> None:
|
||||
"""Загрузка обложки.
|
||||
|
||||
Args:
|
||||
filename (:obj:`str`): Путь для сохранения файла с названием и расширением.
|
||||
size (:obj:`str`, optional): Размер обложки.
|
||||
"""
|
||||
|
||||
self.client.request.download(f'https://{self.cover_uri.replace("%%", size)}', filename)
|
||||
|
||||
def download_mds(self, filename: str) -> None:
|
||||
"""Загрузка аудиоверсии шота.
|
||||
|
||||
Args:
|
||||
filename (:obj:`str`): Путь для сохранения файла с названием и расширением.
|
||||
"""
|
||||
|
||||
self.client.request.download(self.mds_url, filename)
|
||||
|
||||
@classmethod
|
||||
def de_json(cls, data: dict, client: 'Client') -> Optional['ShotData']:
|
||||
"""Десериализация объекта.
|
||||
|
||||
Args:
|
||||
data (:obj:`dict`): Поля и значения десериализуемого объекта.
|
||||
client (:obj:`yandex_music.Client`): Объект класса :class:`yandex_music.Client` представляющий клиент Yandex
|
||||
Music.
|
||||
|
||||
Returns:
|
||||
:obj:`yandex_music.ShotData`: Объект класса :class:`yandex_music.ShotData`.
|
||||
"""
|
||||
if not data:
|
||||
return None
|
||||
|
||||
data = super(ShotData, cls).de_json(data, client)
|
||||
from yandex_music import ShotType
|
||||
data['shot_type'] = ShotType.de_json(data.get('shot_type'), client)
|
||||
|
||||
return cls(client=client, **data)
|
||||
|
||||
# camelCase псевдонимы
|
||||
|
||||
#: Псевдоним для :attr:`download_cover`
|
||||
downloadCover = download_cover
|
||||
#: Псевдоним для :attr:`download_mds`
|
||||
downloadMds = download_mds
|
|
@ -0,0 +1,58 @@
|
|||
from typing import TYPE_CHECKING, Optional, List
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from yandex_music import Client, Shot
|
||||
|
||||
from yandex_music import YandexMusicObject
|
||||
|
||||
|
||||
class ShotEvent(YandexMusicObject):
|
||||
"""Класс, представляющий событие-шот перед началом следующего трека.
|
||||
|
||||
Attributes:
|
||||
event_id (:obj:`str`): Уникальный идентификатор события.
|
||||
shots (:obj:`list` из :obj:`yandex_music.Shot`): Список объектов класса :class:`yandex_music.Shot`
|
||||
представляющие шоты от Алисы.
|
||||
client (:obj:`yandex_music.Client`): Объект класса :class:`yandex_music.Client` представляющий клиент Yandex
|
||||
Music.
|
||||
|
||||
Args:
|
||||
event_id (:obj:`str`): Уникальный идентификатор события.
|
||||
shots (:obj:`list` из :obj:`yandex_music.Shot`): Список объектов класса :class:`yandex_music.Shot`
|
||||
представляющие шоты от Алисы.
|
||||
client (:obj:`yandex_music.Client`, optional): Объект класса :class:`yandex_music.Client` представляющий клиент
|
||||
Yandex Music.
|
||||
**kwargs: Произвольные ключевые аргументы полученные от API.
|
||||
"""
|
||||
|
||||
def __init__(self,
|
||||
event_id: str,
|
||||
shots: List['Shot'],
|
||||
client: Optional['Client'] = None,
|
||||
**kwargs):
|
||||
self.event_id = event_id
|
||||
self.shots = shots
|
||||
|
||||
self.client = client
|
||||
self._id_attrs = (self.event_id, self.shots)
|
||||
|
||||
@classmethod
|
||||
def de_json(cls, data: dict, client: 'Client') -> Optional['ShotEvent']:
|
||||
"""Десериализация объекта.
|
||||
|
||||
Args:
|
||||
data (:obj:`dict`): Поля и значения десериализуемого объекта.
|
||||
client (:obj:`yandex_music.Client`): Объект класса :class:`yandex_music.Client` представляющий клиент Yandex
|
||||
Music.
|
||||
|
||||
Returns:
|
||||
:obj:`yandex_music.ShotEvent`: Объект класса :class:`yandex_music.ShotEvent`.
|
||||
"""
|
||||
if not data:
|
||||
return None
|
||||
|
||||
data = super(ShotEvent, cls).de_json(data, client)
|
||||
from yandex_music import Shot
|
||||
data['shots'] = Shot.de_list(data.get('shots'), client)
|
||||
|
||||
return cls(client=client, **data)
|
|
@ -0,0 +1,54 @@
|
|||
from typing import TYPE_CHECKING, Optional
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from yandex_music import Client
|
||||
|
||||
from yandex_music import YandexMusicObject
|
||||
|
||||
|
||||
class ShotType(YandexMusicObject):
|
||||
"""Класс, представляющий тип шота от Алисы.
|
||||
|
||||
Attributes:
|
||||
id_ (:obj:`str`): Уникальный идентификатор типа.
|
||||
title (:obj:`str`): Заголовок шота.
|
||||
client (:obj:`yandex_music.Client`): Объект класса :class:`yandex_music.Client` представляющий клиент Yandex
|
||||
Music.
|
||||
|
||||
Args:
|
||||
id_ (:obj:`str`): Уникальный идентификатор типа.
|
||||
title (:obj:`str`): Заголовок шота.
|
||||
client (:obj:`yandex_music.Client`, optional): Объект класса :class:`yandex_music.Client` представляющий клиент
|
||||
Yandex Music.
|
||||
**kwargs: Произвольные ключевые аргументы полученные от API.
|
||||
"""
|
||||
|
||||
def __init__(self,
|
||||
id_: str,
|
||||
title: str,
|
||||
client: Optional['Client'] = None,
|
||||
**kwargs):
|
||||
self.id_ = id_
|
||||
self.title = title
|
||||
|
||||
self.client = client
|
||||
self._id_attrs = (self.id_, self.title)
|
||||
|
||||
@classmethod
|
||||
def de_json(cls, data: dict, client: 'Client') -> Optional['ShotType']:
|
||||
"""Десериализация объекта.
|
||||
|
||||
Args:
|
||||
data (:obj:`dict`): Поля и значения десериализуемого объекта.
|
||||
client (:obj:`yandex_music.Client`): Объект класса :class:`yandex_music.Client` представляющий клиент Yandex
|
||||
Music.
|
||||
|
||||
Returns:
|
||||
:obj:`yandex_music.ShotType`: Объект класса :class:`yandex_music.ShotType`.
|
||||
"""
|
||||
if not data:
|
||||
return None
|
||||
|
||||
data = super(ShotType, cls).de_json(data, client)
|
||||
|
||||
return cls(client=client, **data)
|
|
@ -6,6 +6,7 @@ import builtins
|
|||
# Отправка вообще application/x-www-form-urlencoded, а не JSON'a
|
||||
# https://github.com/psf/requests/blob/master/requests/models.py#L508
|
||||
import json
|
||||
from typing import Optional
|
||||
|
||||
import requests
|
||||
|
||||
|
@ -16,7 +17,7 @@ from yandex_music.exceptions import Unauthorized, BadRequest, NetworkError, Yand
|
|||
|
||||
USER_AGENT = 'Yandex-Music-API'
|
||||
HEADERS = {
|
||||
'X-Yandex-Music-Client': 'WindowsPhone/3.20',
|
||||
'X-Yandex-Music-Client': 'YandexMusicAndroid/23020055',
|
||||
}
|
||||
|
||||
reserved_names = [name.lower() for name in dir(builtins)] + ['client']
|
||||
|
@ -76,7 +77,7 @@ class Request:
|
|||
|
||||
return cleaned_object
|
||||
|
||||
def _parse(self, json_data) -> Response:
|
||||
def _parse(self, json_data) -> Optional[Response]:
|
||||
try:
|
||||
decoded_s = json_data.decode('utf-8')
|
||||
data = json.loads(decoded_s, object_hook=Request._object_hook)
|
||||
|
|
読み込み中…
新しいイシューから参照