From e5fe2c79f9674c873c0b11d4bd5aab1c939a4001 Mon Sep 17 00:00:00 2001 From: Il`ya Date: Tue, 21 Jan 2020 15:13:44 +0300 Subject: [PATCH 1/5] =?UTF-8?q?=D0=98=D1=81=D0=BF=D1=80=D0=B0=D0=B2=D0=BB?= =?UTF-8?q?=D0=B5=D0=BD=D0=BE=20=D0=BF=D0=BE=D0=BB=D1=83=D1=87=D0=B5=D0=BD?= =?UTF-8?q?=D0=B8=D0=B5=20=D0=BF=D0=BB=D0=B5=D0=B9=D0=BB=D0=B8=D1=81=D1=82?= =?UTF-8?q?=D0=B0=20=D1=81=20=D0=B0=D0=BB=D0=B8=D1=81=D0=BE=D0=B9=20#185?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- yandex_music/utils/request.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/yandex_music/utils/request.py b/yandex_music/utils/request.py index 5df4471..2934db7 100644 --- a/yandex_music/utils/request.py +++ b/yandex_music/utils/request.py @@ -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) From 900b52b411d0f5c838c3d216fa0a52b0de85026d Mon Sep 17 00:00:00 2001 From: Il`ya Date: Tue, 21 Jan 2020 15:22:41 +0300 Subject: [PATCH 2/5] =?UTF-8?q?=D0=98=D1=81=D0=BF=D1=80=D0=B0=D0=B2=D0=BB?= =?UTF-8?q?=D0=B5=D0=BD=D0=B0=20=D1=83=D1=81=D1=82=D0=B0=D0=BD=D0=BE=D0=B2?= =?UTF-8?q?=D0=BA=D0=B0=20pip=20=D0=B2=20github=20actions=20#185?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/codecov.yml | 3 ++- .github/workflows/full_test.yml | 3 ++- .github/workflows/pypi.yml | 3 ++- .github/workflows/test.yml | 3 ++- 4 files changed, 8 insertions(+), 4 deletions(-) diff --git a/.github/workflows/codecov.yml b/.github/workflows/codecov.yml index b2be0a0..bebd63a 100644 --- a/.github/workflows/codecov.yml +++ b/.github/workflows/codecov.yml @@ -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 diff --git a/.github/workflows/full_test.yml b/.github/workflows/full_test.yml index 377e402..f318465 100644 --- a/.github/workflows/full_test.yml +++ b/.github/workflows/full_test.yml @@ -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 diff --git a/.github/workflows/pypi.yml b/.github/workflows/pypi.yml index 787deed..16694ba 100644 --- a/.github/workflows/pypi.yml +++ b/.github/workflows/pypi.yml @@ -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 diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index fcbac21..c6fca97 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -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 From 3dad8653d002e2dfd4788b914e87a980156d894c Mon Sep 17 00:00:00 2001 From: Il`ya Date: Fri, 24 Jan 2020 02:18:27 +0300 Subject: [PATCH 3/5] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB=D0=B5?= =?UTF-8?q?=D0=BD=D1=8B=20=D0=BA=D0=BB=D0=B0=D1=81=D1=81=D1=8B-=D0=BE?= =?UTF-8?q?=D0=B1=D1=91=D1=80=D1=82=D0=BA=D0=B8=20=D0=BD=D0=B0=D0=B4=20?= =?UTF-8?q?=D0=BE=D0=B1=D1=8A=D0=B5=D0=BA=D1=82=D0=B0=D0=BC=D0=B8=20=D0=B4?= =?UTF-8?q?=D0=BB=D1=8F=20=D1=88=D0=BE=D1=82=D0=BE=D0=B2=20=D1=81=20=D0=B4?= =?UTF-8?q?=D0=BE=D0=BA=D1=83=D0=BC=D0=B5=D0=BD=D1=82=D0=B0=D1=86=D0=B8?= =?UTF-8?q?=D0=B9=20=D0=B8=20=D1=82=D0=B0=D0=B9=D0=BF=D1=85=D0=B8=D0=BD?= =?UTF-8?q?=D1=82=D0=B0=D0=BC=D0=B8=20(Shot,=20ShotData,=20ShotEvent,=20Sh?= =?UTF-8?q?otType).=20=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB=D0=B5=D0=BD=20?= =?UTF-8?q?=D0=BC=D0=B5=D1=82=D0=BE=D0=B4=20after=5Ftrack=20=D0=B2=20?= =?UTF-8?q?=D0=BA=D0=BB=D0=B0=D1=81=D1=81=20Client=20=D0=B4=D0=BB=D1=8F=20?= =?UTF-8?q?=D0=BF=D0=BE=D0=BB=D1=83=D1=87=D0=B5=D0=BD=D0=B8=D1=8F=20=D0=BA?= =?UTF-8?q?=D0=BE=D0=BD=D1=82=D0=B5=D0=BD=D1=82=D0=B0=20=D0=B4=D0=BB=D1=8F?= =?UTF-8?q?=20=D0=B2=D0=BE=D1=81=D0=BF=D0=BE=D1=80=D0=B8=D0=B7=D0=B2=D0=B5?= =?UTF-8?q?=D0=B4=D0=B5=D0=BD=D0=B8=D1=8F=20=D0=BF=D0=BE=D1=81=D0=BB=D0=B5?= =?UTF-8?q?=20=D1=82=D1=80=D0=B5=D0=BA=D0=B0=20(=D1=80=D0=B5=D0=BA=D0=BB?= =?UTF-8?q?=D0=B0=D0=BC=D0=B0,=20=D1=88=D0=BE=D1=82).=20#185?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- yandex_music/__init__.py | 11 ++-- yandex_music/client.py | 48 ++++++++++++++++- yandex_music/shot/__init__.py | 0 yandex_music/shot/shot.py | 93 +++++++++++++++++++++++++++++++++ yandex_music/shot/shot_data.py | 66 +++++++++++++++++++++++ yandex_music/shot/shot_event.py | 58 ++++++++++++++++++++ yandex_music/shot/shot_type.py | 54 +++++++++++++++++++ 7 files changed, 326 insertions(+), 4 deletions(-) create mode 100644 yandex_music/shot/__init__.py create mode 100644 yandex_music/shot/shot.py create mode 100644 yandex_music/shot/shot_data.py create mode 100644 yandex_music/shot/shot_event.py create mode 100644 yandex_music/shot/shot_type.py diff --git a/yandex_music/__init__.py b/yandex_music/__init__.py index ea716eb..f5d2c01 100644 --- a/yandex_music/__init__.py +++ b/yandex_music/__init__.py @@ -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'] diff --git a/yandex_music/client.py b/yandex_music/client.py index 921f4d7..f2d262a 100644 --- a/yandex_music/client.py +++ b/yandex_music/client.py @@ -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 @@ -1228,6 +1228,50 @@ 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, prev_track_id: Union[str, int], next_track_id: Union[str, int], context_item: str, + context: str = 'playlist', types: str = 'shot', from_: str = 'mobile-landing-origin-default', + timeout: Union[int, float] = None, *args, **kwargs) -> Optional[ShotEvent]: + """Получение рекламы или шота от Алисы после трека. + + Известные значения `context`: `playlist`. + Известные значения `types`: `shot`, `ad`. + + Args: + prev_track_id (:obj:`str` | :obj:`int`): Уникальный идентификатор предыдущего трека. + next_track_id (:obj:`str` | :obj:`int`): Уникальный идентификатор следующего трека. + context_item (:obj:`str`): TODO (уникальный идентификатор контекста). + 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, self) + # camelCase псевдонимы #: Псевдоним для :attr:`from_credentials` @@ -1326,3 +1370,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 diff --git a/yandex_music/shot/__init__.py b/yandex_music/shot/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/yandex_music/shot/shot.py b/yandex_music/shot/shot.py new file mode 100644 index 0000000..db79d00 --- /dev/null +++ b/yandex_music/shot/shot.py @@ -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 diff --git a/yandex_music/shot/shot_data.py b/yandex_music/shot/shot_data.py new file mode 100644 index 0000000..1ca7526 --- /dev/null +++ b/yandex_music/shot/shot_data.py @@ -0,0 +1,66 @@ +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) + + @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) diff --git a/yandex_music/shot/shot_event.py b/yandex_music/shot/shot_event.py new file mode 100644 index 0000000..1087d3b --- /dev/null +++ b/yandex_music/shot/shot_event.py @@ -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) diff --git a/yandex_music/shot/shot_type.py b/yandex_music/shot/shot_type.py new file mode 100644 index 0000000..516b89c --- /dev/null +++ b/yandex_music/shot/shot_type.py @@ -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) From 9681487c9a813e14b24d5485b53dd2ac1714ecd2 Mon Sep 17 00:00:00 2001 From: Il`ya Date: Fri, 24 Jan 2020 17:47:50 +0300 Subject: [PATCH 4/5] =?UTF-8?q?=D0=98=D1=81=D0=BF=D1=80=D0=B0=D0=B2=D0=BB?= =?UTF-8?q?=D0=B5=D0=BD=D0=B0=20=D0=B4=D0=B5=D1=81=D0=B5=D1=80=D0=B8=D0=B0?= =?UTF-8?q?=D0=BB=D0=B8=D0=B7=D0=B0=D1=86=D0=B8=D1=8F=20=D1=88=D0=BE=D1=82?= =?UTF-8?q?=20=D0=B8=D0=B2=D0=B5=D0=BD=D1=82=D0=B0.=20=D0=94=D0=BE=D0=B1?= =?UTF-8?q?=D0=B0=D0=B2=D0=BB=D0=B5=D0=BD=D1=8B=20=D0=BC=D0=B5=D1=82=D0=BE?= =?UTF-8?q?=D0=B4=D1=8B=20=D0=B4=D0=BB=D1=8F=20=D0=B7=D0=B0=D0=B3=D1=80?= =?UTF-8?q?=D1=83=D0=B7=D0=BA=D0=B8=20=D0=BE=D0=B1=D0=BB=D0=BE=D0=B6=D0=BA?= =?UTF-8?q?=D0=B8=20=D0=B8=20=D0=B0=D1=83=D0=B4=D0=B8=D0=BE=D0=B2=D0=B5?= =?UTF-8?q?=D1=80=D1=81=D0=B8=D0=B8=20=D1=88=D0=BE=D1=82=D0=B0.=20=D0=94?= =?UTF-8?q?=D0=BE=D0=BF=D0=BE=D0=BB=D0=BD=D0=B5=D0=BD=D0=B0=20=D0=B4=D0=BE?= =?UTF-8?q?=D0=BA=D1=83=D0=BC=D0=B5=D0=BD=D1=82=D0=B0=D1=86=D0=B8=D1=8F.?= =?UTF-8?q?=20#185?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- yandex_music/client.py | 13 ++++++++++--- yandex_music/shot/shot_data.py | 26 ++++++++++++++++++++++++++ 2 files changed, 36 insertions(+), 3 deletions(-) diff --git a/yandex_music/client.py b/yandex_music/client.py index f2d262a..f6ead99 100644 --- a/yandex_music/client.py +++ b/yandex_music/client.py @@ -1229,18 +1229,25 @@ class Client(YandexMusicObject): return self._dislike_action(track_ids, True, user_id, timeout, *args, **kwargs) @log - def after_track(self, prev_track_id: Union[str, int], next_track_id: Union[str, int], context_item: str, + 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`): TODO (уникальный идентификатор контекста). + context_item (:obj:`str`): Уникальный идентификатор контекста. context (:obj:`str`, optional): Место, откуда было вызвано получение. types (:obj:`str`, optional): Тип того, что вернуть после трека. from_ (:obj:`str`, optional): Место, с которого попали в контекст. @@ -1270,7 +1277,7 @@ class Client(YandexMusicObject): result = self._request.get(url, params=params, timeout=timeout, *args, **kwargs) # TODO судя по всему эндпоинт ещё возвращает рекламу после треков для пользователей без подписки. - return ShotEvent.de_json(result, self) + return ShotEvent.de_json(result.get('shot_event'), self) # camelCase псевдонимы diff --git a/yandex_music/shot/shot_data.py b/yandex_music/shot/shot_data.py index 1ca7526..8123736 100644 --- a/yandex_music/shot/shot_data.py +++ b/yandex_music/shot/shot_data.py @@ -44,6 +44,25 @@ class ShotData(YandexMusicObject): 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']: """Десериализация объекта. @@ -64,3 +83,10 @@ class ShotData(YandexMusicObject): 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 From 86db74b664ba3deb662487bdd580058ebeff07a1 Mon Sep 17 00:00:00 2001 From: Il`ya Date: Fri, 24 Jan 2020 19:10:16 +0300 Subject: [PATCH 5/5] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB=D0=B5?= =?UTF-8?q?=D0=BD=D1=8B=20=D1=82=D0=B5=D1=81=D1=82=D1=8B=20=D0=B4=D0=BB?= =?UTF-8?q?=D1=8F=20=D1=88=D0=BE=D1=82=D0=BE=D0=B2.=20#185?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/__init__.py | 3 +++ tests/conftest.py | 19 ++++++++++++-- tests/test_shot.py | 53 ++++++++++++++++++++++++++++++++++++++++ tests/test_shot_data.py | 49 +++++++++++++++++++++++++++++++++++++ tests/test_shot_event.py | 46 ++++++++++++++++++++++++++++++++++ tests/test_shot_type.py | 41 +++++++++++++++++++++++++++++++ 6 files changed, 209 insertions(+), 2 deletions(-) create mode 100644 tests/test_shot.py create mode 100644 tests/test_shot_data.py create mode 100644 tests/test_shot_event.py create mode 100644 tests/test_shot_type.py diff --git a/tests/__init__.py b/tests/__init__.py index 4609f79..e44a428 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -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 diff --git a/tests/conftest.py b/tests/conftest.py index 79d7adf..ecd4ed5 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -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) diff --git a/tests/test_shot.py b/tests/test_shot.py new file mode 100644 index 0000000..6f43f74 --- /dev/null +++ b/tests/test_shot.py @@ -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) diff --git a/tests/test_shot_data.py b/tests/test_shot_data.py new file mode 100644 index 0000000..e6c7488 --- /dev/null +++ b/tests/test_shot_data.py @@ -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) diff --git a/tests/test_shot_event.py b/tests/test_shot_event.py new file mode 100644 index 0000000..005a8a4 --- /dev/null +++ b/tests/test_shot_event.py @@ -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) diff --git a/tests/test_shot_type.py b/tests/test_shot_type.py new file mode 100644 index 0000000..b150e85 --- /dev/null +++ b/tests/test_shot_type.py @@ -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)