From 829ff8c05c9dedffb69f5726085156bceec9302e Mon Sep 17 00:00:00 2001 From: Gleb Liutsko Date: Fri, 8 May 2020 15:47:16 +0400 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=B0=20=D0=BE=D1=88=D0=B8=D0=B1=D0=BA=D0=B0=20?= =?UTF-8?q?=D0=B2=20=D0=B4=D0=BE=D0=BA=D1=83=D0=BC=D0=B5=D0=BD=D1=82=D0=B0?= =?UTF-8?q?=D1=86=D0=B8=D0=B8.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit В документации некоторых методов был написан не правльный формат ID плейлиста. --- yandex_music/client.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/yandex_music/client.py b/yandex_music/client.py index 404a81c..21c3020 100644 --- a/yandex_music/client.py +++ b/yandex_music/client.py @@ -1421,7 +1421,7 @@ class Client(YandexMusicObject): Note: Типы объектов: `track` - трек, `artist` - исполнитель, `playlist` - плейлист, `album` - альбом. - Идентификатор плейлиста указывается в формате `playlist_id:owner_id`. Где `playlist_id` - идентификатор + Идентификатор плейлиста указывается в формате `owner_id:playlist_id`. Где `playlist_id` - идентификатор плейлиста, `owner_id` - уникальный идентификатор владельца плейлиста. Args: @@ -1554,7 +1554,7 @@ class Client(YandexMusicObject): """Поставить отметку "Мне нравится" плейлисту/плейлистам. Note: - Идентификатор плейлиста указывается в формате `playlist_id:owner_id`. Где `playlist_id` - идентификатор + Идентификатор плейлиста указывается в формате `owner_id:playlist_id`. Где `playlist_id` - идентификатор плейлиста, `owner_id` - уникальный идентификатор владельца плейлиста. Args: @@ -1581,7 +1581,7 @@ class Client(YandexMusicObject): """Снять отметку "Мне нравится" у плейлиста/плейлистов. Note: - Идентификатор плейлиста указывается в формате `playlist_id:owner_id`. Где `playlist_id` - идентификатор + Идентификатор плейлиста указывается в формате `owner_id:playlist_id`. Где `playlist_id` - идентификатор плейлиста, `owner_id` - уникальный идентификатор владельца плейлиста. Args: @@ -1745,7 +1745,7 @@ class Client(YandexMusicObject): """Получение плейлиста/плейлистов. Note: - Идентификатор плейлиста указывается в формате `playlist_id:owner_id`. Где `playlist_id` - идентификатор + Идентификатор плейлиста указывается в формате `owner_id:playlist_id`. Где `playlist_id` - идентификатор плейлиста, `owner_id` - уникальный идентификатор владельца плейлиста. Args: From f4edd6348d2e7cb40a6dc1b0ad588a98d309e883 Mon Sep 17 00:00:00 2001 From: Il'ya Date: Thu, 14 May 2020 21:02:34 +0300 Subject: [PATCH 2/5] =?UTF-8?q?=D0=9F=D0=BE=D0=BB=D1=83=D1=87=D0=B5=D0=BD?= =?UTF-8?q?=D0=B8=D0=B5=20=D1=87=D0=B0=D1=80=D1=82=D0=BE=D0=B2=20(#328)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Добавлен метод получения чарта - `chart()`. Добавлены новые классы: `ChartInfo`, `ChartInfoMenu`, `ChartInfoMenuItem` Co-authored-by: Angel --- .../yandex_music.landing.chart_info.rst | 7 ++ .../yandex_music.landing.chart_info_menu.rst | 7 ++ ...dex_music.landing.chart_info_menu_item.rst | 7 ++ docs/source/yandex_music.landing.rst | 3 + tests/__init__.py | 3 + tests/conftest.py | 20 ++++- tests/test_chart_info.py | 74 +++++++++++++++++++ tests/test_chart_info_menu.py | 29 ++++++++ tests/test_chart_info_menu_item.py | 50 +++++++++++++ yandex_music/__init__.py | 6 +- yandex_music/client.py | 32 +++++++- yandex_music/landing/chart_info.py | 72 ++++++++++++++++++ yandex_music/landing/chart_info_menu.py | 44 +++++++++++ yandex_music/landing/chart_info_menu_item.py | 65 ++++++++++++++++ 14 files changed, 415 insertions(+), 4 deletions(-) create mode 100644 docs/source/yandex_music.landing.chart_info.rst create mode 100644 docs/source/yandex_music.landing.chart_info_menu.rst create mode 100644 docs/source/yandex_music.landing.chart_info_menu_item.rst create mode 100644 tests/test_chart_info.py create mode 100644 tests/test_chart_info_menu.py create mode 100644 tests/test_chart_info_menu_item.py create mode 100644 yandex_music/landing/chart_info.py create mode 100644 yandex_music/landing/chart_info_menu.py create mode 100644 yandex_music/landing/chart_info_menu_item.py diff --git a/docs/source/yandex_music.landing.chart_info.rst b/docs/source/yandex_music.landing.chart_info.rst new file mode 100644 index 0000000..5b1ca6f --- /dev/null +++ b/docs/source/yandex_music.landing.chart_info.rst @@ -0,0 +1,7 @@ +yandex_music.ChartInfo +====================== + +.. autoclass:: yandex_music.ChartInfo + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/source/yandex_music.landing.chart_info_menu.rst b/docs/source/yandex_music.landing.chart_info_menu.rst new file mode 100644 index 0000000..a007244 --- /dev/null +++ b/docs/source/yandex_music.landing.chart_info_menu.rst @@ -0,0 +1,7 @@ +yandex_music.ChartInfoMenu +========================== + +.. autoclass:: yandex_music.ChartInfoMenu + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/source/yandex_music.landing.chart_info_menu_item.rst b/docs/source/yandex_music.landing.chart_info_menu_item.rst new file mode 100644 index 0000000..34a24fe --- /dev/null +++ b/docs/source/yandex_music.landing.chart_info_menu_item.rst @@ -0,0 +1,7 @@ +yandex_music.ChartInfoMenuItem +============================== + +.. autoclass:: yandex_music.ChartInfoMenuItem + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/source/yandex_music.landing.rst b/docs/source/yandex_music.landing.rst index fe8938c..8cfb6ff 100644 --- a/docs/source/yandex_music.landing.rst +++ b/docs/source/yandex_music.landing.rst @@ -15,3 +15,6 @@ yandex_music.landing.personal_playlists_data yandex_music.landing.promotion yandex_music.landing.block + yandex_music.landing.chart_info + yandex_music.landing.chart_info_menu + yandex_music.landing.chart_info_menu_item diff --git a/tests/__init__.py b/tests/__init__.py index b4d1cf8..b4e77e7 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -17,6 +17,9 @@ from .test_description import TestDescription from .test_discrete_scale import TestDiscreteScale from .test_enum import TestEnum from .test_event import TestEvent +from .test_chart_info_menu_item import TestChartInfoMenuItem +from .test_chart_info_menu import TestChartInfoMenu +from .test_chart_info import TestChartInfo from .test_generated_playlist import TestGeneratedPlaylist from .test_genre import TestGenre from .test_icon import TestIcon diff --git a/tests/conftest.py b/tests/conftest.py index fb2db5c..7610646 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -7,7 +7,7 @@ from yandex_music import Counts, TrackId, CaseForms, Ratings, Icon, Album, Lyric 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, ShotType, ShotData, Shot, \ - RenewableRemainder + RenewableRemainder, ChartInfoMenuItem, ChartInfoMenu, ChartInfo from . import TestCounts, TestTrackId, TestCaseForms, TestRatings, TestIcon, TestAlbum, TestLyrics, \ TestTrack, TestInvocationInfo, TestPlaylist, TestAutoRenewable, TestStation, TestNormalization, TestMajor, \ TestTrackPosition, TestBest, TestChart, TestPermissions, TestPlus, TestProduct, TestCover, TestPlayCounter, \ @@ -17,7 +17,7 @@ from . import TestCounts, TestTrackId, TestCaseForms, TestRatings, TestIcon, Tes TestTrackShortOld, TestPager, TestStatus, TestSettings, TestStationResult, TestLabel, TestTrackWithAds, \ TestVideoSupplement, TestEvent, TestDay, TestPlayContext, TestGeneratedPlaylist, TestVideo, TestVinyl, \ TestSearchResult, TestBlockEntity, TestBlock, TestPlaylistAbsence, TestShot, TestShotData, TestShotType, \ - TestRenewableRemainder + TestRenewableRemainder, TestChartInfoMenuItem, TestChartInfo @pytest.fixture(scope='session') @@ -423,6 +423,22 @@ def event(track, artist_event, album_event): [album_event], TestEvent.message, TestEvent.device, TestEvent.tracks_count) +@pytest.fixture(scope='session') +def chart_info_menu_item(): + return ChartInfoMenuItem(TestChartInfoMenuItem.title, TestChartInfoMenuItem.url, TestChartInfoMenuItem.selected) + + +@pytest.fixture(scope='session') +def chart_info_menu(chart_info_menu_item): + return ChartInfoMenu([chart_info_menu_item]) + + +@pytest.fixture(scope='session') +def chart_info(playlist, chart_info_menu): + return ChartInfo(TestChartInfo.id, TestChartInfo.type, TestChartInfo.type_for_from, TestChartInfo.title, + chart_info_menu, playlist, TestChartInfo.chart_description) + + @pytest.fixture(scope='session') def track_id(): return TrackId(TestTrackId.id, TestTrackId.album_id) diff --git a/tests/test_chart_info.py b/tests/test_chart_info.py new file mode 100644 index 0000000..7f19472 --- /dev/null +++ b/tests/test_chart_info.py @@ -0,0 +1,74 @@ +from yandex_music import ChartInfo + + +class TestChartInfo: + id = 'KpXst7X4' + type = 'chart' + type_for_from = 'chart' + title = 'Треки, популярные на Яндекс.Музыке прямо сейчас' + chart_description = 'Слушателей за день' + + def test_expected_values(self, chart_info, chart_info_menu, playlist): + assert chart_info.id == self.id + assert chart_info.type == self.type + assert chart_info.type_for_from == self.type_for_from + assert chart_info.title == self.title + assert chart_info.chart == playlist + assert chart_info.menu == chart_info_menu + + def test_de_json_none(self, client): + assert ChartInfo.de_json({}, client) is None + + def test_de_json_required(self, playlist, chart_info_menu, client): + json_dict = { + 'id_': self.id, + 'type_': self.type, + 'type_for_from': self.type_for_from, + 'title': self.title, + 'chart_description': self.chart_description, + 'menu': chart_info_menu.to_dict(), + 'chart': playlist.to_dict() + } + + chart_info = ChartInfo.de_json(json_dict, client) + + assert chart_info.id == self.id + assert chart_info.type == self.type + assert chart_info.type_for_from == self.type_for_from + assert chart_info.title == self.title + assert chart_info.chart_description == self.chart_description + + def test_de_json_all(self, client, playlist, chart_info_menu): + json_dict = { + 'id_': self.id, + 'type_': self.type, + 'type_for_from': self.type_for_from, + 'title': self.title, + 'chart_description': self.chart_description, + 'menu': chart_info_menu.to_dict(), + 'chart': playlist.to_dict() + } + + chart_info = ChartInfo.de_json(json_dict, client) + + assert chart_info.id == self.id + assert chart_info.type == self.type + assert chart_info.type_for_from == self.type_for_from + assert chart_info.title == self.title + assert chart_info.chart_description == self.chart_description + assert chart_info.menu == chart_info_menu + assert chart_info.chart == playlist + + def test_equality(self, playlist, chart_info_menu): + a = ChartInfo(self.id, self.type, self.type_for_from, self.title, self.chart_description, chart_info_menu, + playlist) + b = ChartInfo("no_id", self.type, self.type_for_from, self.title, self.chart_description, chart_info_menu, + playlist) + c = ChartInfo(self.id, self.type, self.type_for_from, self.title, self.chart_description, chart_info_menu, + playlist) + + assert a != b + assert hash(a) != hash(b) + assert a is not b + + assert a == c diff --git a/tests/test_chart_info_menu.py b/tests/test_chart_info_menu.py new file mode 100644 index 0000000..eee86c5 --- /dev/null +++ b/tests/test_chart_info_menu.py @@ -0,0 +1,29 @@ +from yandex_music import ChartInfoMenu, ChartInfoMenuItem + + +class TestChartInfoMenu: + def test_expected_values(self, chart_info_menu, chart_info_menu_item): + assert chart_info_menu.items == [chart_info_menu_item] + + def test_de_json_none(self, client): + assert ChartInfoMenu.de_json({}, client) is None + + def test_de_json_required(self, chart_info_menu_item, client): + json_dict = { + 'items': [chart_info_menu_item.to_dict()], + } + + chart_info_menu = ChartInfoMenu.de_json(json_dict, client) + + assert chart_info_menu.items == [chart_info_menu_item] + + def test_equality(self, chart_info_menu_item): + a = ChartInfoMenu([chart_info_menu_item]) + b = ChartInfoMenu([ChartInfoMenuItem("tt", "no_url")]) + c = ChartInfoMenu([chart_info_menu_item]) + + assert a != b + assert hash(a) != hash(b) + assert a is not b + + assert a == c diff --git a/tests/test_chart_info_menu_item.py b/tests/test_chart_info_menu_item.py new file mode 100644 index 0000000..4240782 --- /dev/null +++ b/tests/test_chart_info_menu_item.py @@ -0,0 +1,50 @@ +from yandex_music import ChartInfoMenuItem + + +class TestChartInfoMenuItem: + title = 'Россия' + url = 'russia' + selected = True + + def test_expected_values(self, chart_info_menu_item): + assert chart_info_menu_item.title == self.title + assert chart_info_menu_item.url == self.url + assert chart_info_menu_item.selected == self.selected + + def test_de_json_none(self, client): + assert ChartInfoMenuItem.de_json({}, client) is None + + def test_de_json_required(self, client): + json_dict = { + 'title': self.title, + 'url': self.url, + } + + chart_info_menu_item = ChartInfoMenuItem.de_json(json_dict, client) + + assert chart_info_menu_item.title == self.title + assert chart_info_menu_item.url == self.url + + def test_de_json_all(self, client): + json_dict = { + 'title': self.title, + 'url': self.url, + 'selected': self.selected, + } + + chart_info_menu_item = ChartInfoMenuItem.de_json(json_dict, client) + + assert chart_info_menu_item.title == self.title + assert chart_info_menu_item.url == self.url + assert chart_info_menu_item.selected == self.selected + + def test_equality(self): + a = ChartInfoMenuItem(self.title, self.url, self.selected) + b = ChartInfoMenuItem(self.title, "no_url", self.selected) + c = ChartInfoMenuItem(self.title, self.url, self.selected) + + assert a != b + assert hash(a) != hash(b) + assert a is not b + + assert a == c diff --git a/yandex_music/__init__.py b/yandex_music/__init__.py index 1416049..a2711de 100644 --- a/yandex_music/__init__.py +++ b/yandex_music/__init__.py @@ -73,6 +73,9 @@ from .landing.promotion import Promotion from .landing.block_entity import BlockEntity from .landing.landing import Landing from .landing.block import Block +from .landing.chart_info_menu_item import ChartInfoMenuItem +from .landing.chart_info_menu import ChartInfoMenu +from .landing.chart_info import ChartInfo from .landing.track_id import TrackId from .landing.chart import Chart from .landing.play_contexts_data import PlayContextsData @@ -119,4 +122,5 @@ __all__ = ['YandexMusicObject', 'Client', 'Account', 'PassportPhone', 'Invocatio 'Dashboard', 'RotorSettings', 'AdParams', 'Restrictions', 'Value', 'Enum', 'DiscreteScale', 'StationResult', 'Sequence', 'StationTracksResult', 'BriefInfo', 'Description', 'PlaylistId', 'Vinyl', 'Supplement', 'Lyrics', 'VideoSupplement', 'ArtistTracks', 'Pager', 'ArtistAlbums', 'PlaylistAbsence', 'Shot', 'ShotEvent', - 'ShotType', 'ShotData', 'SimilarTracks', 'UserSettings', 'RenewableRemainder'] + 'ShotType', 'ShotData', 'SimilarTracks', 'UserSettings', 'RenewableRemainder', 'ChartInfo', 'ChartInfoMenu', + 'ChartInfoMenuItem'] diff --git a/yandex_music/client.py b/yandex_music/client.py index 21c3020..5d303a6 100644 --- a/yandex_music/client.py +++ b/yandex_music/client.py @@ -6,7 +6,7 @@ from typing import Callable, Dict, List, Optional, Union from yandex_music import Album, Artist, ArtistAlbums, ArtistTracks, BriefInfo, Dashboard, DownloadInfo, Experiments, \ Feed, Genre, Landing, Like, PermissionAlerts, Playlist, PromoCodeStatus, Search, Settings, ShotEvent, SimilarTracks, \ StationResult, StationTracksResult, Status, Suggestions, Supplement, Track, TracksList, UserSettings, \ - YandexMusicObject + YandexMusicObject, ChartInfo from yandex_music.exceptions import Captcha, InvalidToken from yandex_music.utils.difference import Difference from yandex_music.utils.request import Request @@ -439,6 +439,36 @@ class Client(YandexMusicObject): return Landing.de_json(result, self) + @log + def chart(self, chart_option: str = '', timeout: Union[int, float] = None, *args, **kwargs) -> Optional[ChartInfo]: + """Получение чарта. + + Note: + `chart_option` - это постфикс к запросу из поля `menu` чарта. + Например, на сайте можно выбрать глобальный (world) чарт или российский (russia). + + Args: + chart_option (:obj:`str` optional): Параметры чарта. + timeout (:obj:`int` | :obj:`float`, optional): Если это значение указано, используется как время ожидания + ответа от сервера вместо указанного при создании пула. + **kwargs (:obj:`dict`, optional): Произвольные аргументы (будут переданы в запрос). + + Returns: + :obj:`yandex_music.ChartInfo`: Чарт. + + Raises: + :class:`yandex_music.exceptions.YandexMusicError`: Базовое исключение библиотеки. + """ + + url = f'{self.base_url}/landing3/chart' + + if chart_option: + url = f'{url}/{chart_option}' + + result = self._request.get(url, timeout=timeout, *args, **kwargs) + + return ChartInfo.de_json(result, self) + @log def genres(self, timeout: Union[int, float] = None, *args, **kwargs) -> List[Genre]: """Получение жанров музыки. diff --git a/yandex_music/landing/chart_info.py b/yandex_music/landing/chart_info.py new file mode 100644 index 0000000..2300bb4 --- /dev/null +++ b/yandex_music/landing/chart_info.py @@ -0,0 +1,72 @@ +from typing import TYPE_CHECKING, Optional + +from yandex_music import YandexMusicObject, Playlist, ChartInfoMenu + +if TYPE_CHECKING: + from yandex_music import Client + + +class ChartInfo(YandexMusicObject): + """Класс, представляющий чарт. + + Attributes: + id (:obj:`str`): Уникальный идентификатор блока. + type (:obj:`str`): Тип блока. + type_for_from (:obj:`str`): Откуда получен блок (как к нему пришли). + title (:obj:`str`): Заголовок. + menu (:obj:`yandex_music.ChartInfoMenu` | :obj:`None`): Меню TODO. + chart (:obj:`yandex_music.Playlist` | :obj:`None`): Плейлист. + chart_description (:obj:`str`): Описание. + client (:obj:`yandex_music.Client`): Клиент Yandex Music. + + Args: + id_ (:obj:`str`): Уникальный идентификатор блока. + type_ (:obj:`str`): Тип блока. + type_for_from (:obj:`str`): Откуда получен блок (как к нему пришли). + title (:obj:`str`): Заголовок. + menu (:obj:`yandex_music.ChartInfoMenu`, optional): Меню TODO. + chart (:obj:`yandex_music.Playlist`, optional): Плейлист. + chart_description (:obj:`str`, optional): Описание. + client (:obj:`yandex_music.Client`, optional): Клиент Yandex Music. + """ + + def __init__(self, + id_: str, + type_: str, + type_for_from: str, + title: str, + menu: Optional['ChartInfoMenu'], + chart: Optional['Playlist'], + chart_description: Optional[str] = None, + client: Optional['Client'] = None): + self.id = id_ + self.type = type_ + self.type_for_from = type_for_from + self.title = title + + self.menu = menu + self.chart = chart + self.chart_description = chart_description + + self.client = client + self._id_attrs = (id_,) + + @classmethod + def de_json(cls, data: dict, client: 'Client') -> Optional['ChartInfo']: + """Десериализация объекта. + + Args: + data (:obj:`dict`): Поля и значения десериализуемого объекта. + client (:obj:`yandex_music.Client`, optional): Клиент Yandex Music. + + Returns: + :obj:`yandex_music.ChartInfo`: Чарт. + """ + if not data: + return None + + data = super(ChartInfo, cls).de_json(data, client) + data['chart'] = Playlist.de_json(data.get('chart'), client) + data['menu'] = ChartInfoMenu.de_json(data.get('menu'), client) + + return cls(client=client, **data) diff --git a/yandex_music/landing/chart_info_menu.py b/yandex_music/landing/chart_info_menu.py new file mode 100644 index 0000000..f10c4f4 --- /dev/null +++ b/yandex_music/landing/chart_info_menu.py @@ -0,0 +1,44 @@ +from typing import TYPE_CHECKING, List, Optional + +from yandex_music import YandexMusicObject, ChartInfoMenuItem + +if TYPE_CHECKING: + from yandex_music import Client + + +class ChartInfoMenu(YandexMusicObject): + """Класс, представляющий меню чарта. + + Attributes: + items (:obj:`list` из :obj:`yandex_music.ChartInfoMenuItem`): Список элементов меню. + client (:obj:`yandex_music.Client`): Клиент Yandex Music. + + Args: + items (:obj:`list` из :obj:`yandex_music.ChartInfoMenuItem`): Список элементов меню. + client (:obj:`yandex_music.Client`, optional): Клиент Yandex Music. + """ + + def __init__(self, items: List['ChartInfoMenuItem'], client: Optional['Client'] = None): + self.items = items + + self.client = client + self._id_attrs = (self.items, ) + + @classmethod + def de_json(cls, data: dict, client: 'Client') -> Optional['ChartInfoMenu']: + """Десериализация объекта. + + Args: + data (:obj:`dict`): Поля и значения десериализуемого объекта. + client (:obj:`yandex_music.Client`, optional): Клиент Yandex Music. + + Returns: + :obj:`yandex_music.ChartInfoMenu`: Меню чарта. + """ + if not data: + return None + + data = super(ChartInfoMenu, cls).de_json(data, client) + data['items'] = ChartInfoMenuItem.de_list(data.get('items'), client) + + return cls(client=client, **data) diff --git a/yandex_music/landing/chart_info_menu_item.py b/yandex_music/landing/chart_info_menu_item.py new file mode 100644 index 0000000..cbc11f6 --- /dev/null +++ b/yandex_music/landing/chart_info_menu_item.py @@ -0,0 +1,65 @@ +from typing import TYPE_CHECKING, Optional, List + +from yandex_music import YandexMusicObject + +if TYPE_CHECKING: + from yandex_music import Client + + +class ChartInfoMenuItem(YandexMusicObject): + """Класс, представляющий элемент меню чарта. + + Attributes: + title (:obj:`str`): Заголовок. + url (:obj:`str`): Постфикс для запроса чарта. + selected (:obj:`bool` | :obj:`None`): Текущий ли элемент. + client (:obj:`yandex_music.Client`): Клиент Yandex Music. + + Args: + title (:obj:`str`): Заголовок. + url (:obj:`str`): Постфикс для запроса чарта. + selected (:obj:`bool`, optional): Текущий ли элемент. + client (:obj:`yandex_music.Client`, optional): Клиент Yandex Music. + """ + + def __init__(self, title: str, url: str, selected: Optional[bool] = None, client: Optional['Client'] = None): + self.title = title + self.url = url + self.selected = selected + + self.client = client + self._id_attrs = (url, selected) + + @classmethod + def de_json(cls, data: dict, client: 'Client') -> Optional['ChartInfoMenuItem']: + """Десериализация объекта. + + Args: + data (:obj:`dict`): Поля и значения десериализуемого объекта. + client (:obj:`yandex_music.Client`, optional): Клиент Yandex Music. + + Returns: + :obj:`yandex_music.ChartInfoMenuItem`: Элемент меню. + """ + if not data: + return None + + data = super(ChartInfoMenuItem, cls).de_json(data, client) + + return cls(client=client, **data) + + @classmethod + def de_list(cls, data: list, client: 'Client') -> List['ChartInfoMenuItem']: + """Десериализация списка объектов. + + Args: + data (:obj:`list`): Список словарей с полями и значениями десериализуемого объекта. + client (:obj:`yandex_music.Client`, optional): Клиент Yandex Music. + + Returns: + :obj:`list` из :obj:`yandex_music.ChartInfoMenuItem`: Список элементов меню чарта. + """ + if not data: + return [] + + return [cls.de_json(item, client) for item in data] From 995f82430a00a3369a40b480471c1f1e776ce618 Mon Sep 17 00:00:00 2001 From: Il`ya Semyonov Date: Thu, 14 May 2020 22:44:57 +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=D0=BE=20=D0=BF=D0=BE=D0=BB=D1=83=D1=87=D0=B5=D0=BD=D0=B8?= =?UTF-8?q?=D0=B5=20=D1=82=D0=B5=D0=B3=D0=BE=D0=B2.=20=D0=9C=D0=B5=D1=82?= =?UTF-8?q?=D0=BE=D0=B4=20tags()=20=D1=83=20Client.=20=D0=9D=D0=BE=D0=B2?= =?UTF-8?q?=D1=8B=D0=B5=20=D0=BA=D0=BB=D0=B0=D1=81=D1=81=D1=8B:=20TagResul?= =?UTF-8?q?t,=20Tag.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/source/yandex_music.playlist.rst | 2 + docs/source/yandex_music.playlist.tag.rst | 7 +++ .../yandex_music.playlist.tag_result.rst | 7 +++ tests/__init__.py | 1 + tests/conftest.py | 9 ++- tests/test_tag.py | 47 ++++++++++++++ tests/test_tag_result.py | 42 +++++++++++++ yandex_music/__init__.py | 4 +- yandex_music/client.py | 35 ++++++++++- yandex_music/playlist/tag.py | 61 +++++++++++++++++++ yandex_music/playlist/tag_result.py | 56 +++++++++++++++++ 11 files changed, 265 insertions(+), 6 deletions(-) create mode 100644 docs/source/yandex_music.playlist.tag.rst create mode 100644 docs/source/yandex_music.playlist.tag_result.rst create mode 100644 tests/test_tag.py create mode 100644 tests/test_tag_result.py create mode 100644 yandex_music/playlist/tag.py create mode 100644 yandex_music/playlist/tag_result.py diff --git a/docs/source/yandex_music.playlist.rst b/docs/source/yandex_music.playlist.rst index 29a8455..10aa8aa 100644 --- a/docs/source/yandex_music.playlist.rst +++ b/docs/source/yandex_music.playlist.rst @@ -10,3 +10,5 @@ yandex_music.playlist.playlist yandex_music.playlist.case_forms yandex_music.playlist.playlist_id + yandex_music.playlist.tag_result + yandex_music.playlist.tag diff --git a/docs/source/yandex_music.playlist.tag.rst b/docs/source/yandex_music.playlist.tag.rst new file mode 100644 index 0000000..06b2534 --- /dev/null +++ b/docs/source/yandex_music.playlist.tag.rst @@ -0,0 +1,7 @@ +yandex_music.Tag +================ + +.. autoclass:: yandex_music.Tag + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/source/yandex_music.playlist.tag_result.rst b/docs/source/yandex_music.playlist.tag_result.rst new file mode 100644 index 0000000..97aae18 --- /dev/null +++ b/docs/source/yandex_music.playlist.tag_result.rst @@ -0,0 +1,7 @@ +yandex_music.TagResult +====================== + +.. autoclass:: yandex_music.TagResult + :members: + :undoc-members: + :show-inheritance: diff --git a/tests/__init__.py b/tests/__init__.py index b4e77e7..b3bb0b7 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -77,3 +77,4 @@ from .test_shot_type import TestShotType from .test_shot_data import TestShotData from .test_shot import TestShot from .test_renewable_remainder import TestRenewableRemainder +from .test_tag import TestTag diff --git a/tests/conftest.py b/tests/conftest.py index 7610646..1269ed3 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -7,7 +7,7 @@ from yandex_music import Counts, TrackId, CaseForms, Ratings, Icon, Album, Lyric 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, ShotType, ShotData, Shot, \ - RenewableRemainder, ChartInfoMenuItem, ChartInfoMenu, ChartInfo + RenewableRemainder, ChartInfoMenuItem, ChartInfoMenu, ChartInfo, Tag from . import TestCounts, TestTrackId, TestCaseForms, TestRatings, TestIcon, TestAlbum, TestLyrics, \ TestTrack, TestInvocationInfo, TestPlaylist, TestAutoRenewable, TestStation, TestNormalization, TestMajor, \ TestTrackPosition, TestBest, TestChart, TestPermissions, TestPlus, TestProduct, TestCover, TestPlayCounter, \ @@ -17,7 +17,7 @@ from . import TestCounts, TestTrackId, TestCaseForms, TestRatings, TestIcon, Tes TestTrackShortOld, TestPager, TestStatus, TestSettings, TestStationResult, TestLabel, TestTrackWithAds, \ TestVideoSupplement, TestEvent, TestDay, TestPlayContext, TestGeneratedPlaylist, TestVideo, TestVinyl, \ TestSearchResult, TestBlockEntity, TestBlock, TestPlaylistAbsence, TestShot, TestShotData, TestShotType, \ - TestRenewableRemainder, TestChartInfoMenuItem, TestChartInfo + TestRenewableRemainder, TestChartInfoMenuItem, TestChartInfo, TestTag @pytest.fixture(scope='session') @@ -137,6 +137,11 @@ def client(): return Client() +@pytest.fixture(scope='session') +def tag(): + return Tag(TestTag.id_, TestTag.value, TestTag.name, TestTag.og_description) + + @pytest.fixture(scope='session') def track_with_ads(track): return TrackWithAds(TestTrackWithAds.type, track) diff --git a/tests/test_tag.py b/tests/test_tag.py new file mode 100644 index 0000000..6477a98 --- /dev/null +++ b/tests/test_tag.py @@ -0,0 +1,47 @@ +from yandex_music import Tag + + +class TestTag: + id_ = '5795ce8f77d30f7fda41bca0' + value = 'вечные хиты' + name = 'Вечные хиты' + og_description = '' + + def test_expected_values(self, tag): + assert tag.id == self.id_ + assert tag.value == self.value + assert tag.name == self.name + assert tag.og_description == self.og_description + + def test_de_json_none(self, client): + assert Tag.de_json({}, client) is None + + def test_de_json_required(self, client): + json_dict = {'id_': self.id_, 'value': self.value, 'name': self.name, 'og_description': self.og_description} + tag = Tag.de_json(json_dict, client) + + assert tag.id == self.id_ + assert tag.value == self.value + assert tag.name == self.name + assert tag.og_description == self.og_description + + def test_de_json_all(self, client): + json_dict = {'id_': self.id_, 'value': self.value, 'name': self.name, 'og_description': self.og_description} + tag = Tag.de_json(json_dict, client) + + assert tag.id == self.id_ + assert tag.value == self.value + assert tag.name == self.name + assert tag.og_description == self.og_description + + def test_equality(self): + a = Tag(self.id_, self.value, self.name, self.og_description) + b = Tag('10b300', self.value, self.name, self.og_description) + c = Tag(self.id_, self.value, '', self.og_description) + d = Tag(self.id_, self.value, self.name, self.og_description) + + assert a != b != c + assert hash(a) != hash(b) != hash(c) + assert a is not b is not c + + assert a == d diff --git a/tests/test_tag_result.py b/tests/test_tag_result.py new file mode 100644 index 0000000..12826e1 --- /dev/null +++ b/tests/test_tag_result.py @@ -0,0 +1,42 @@ +import pytest + +from yandex_music import TagResult + + +@pytest.fixture(scope='class') +def tag_result(tag, playlist_id): + return TagResult(tag, [playlist_id]) + + +class TestTagResult: + def test_expected_values(self, tag_result, tag, playlist_id): + assert tag_result.tag == tag + assert tag_result.ids == [playlist_id] + + def test_de_json_none(self, client): + assert TagResult.de_json({}, client) is None + + def test_de_json_required(self, client, tag, playlist_id): + json_dict = {'tag': tag.to_dict(), 'ids': [playlist_id.to_dict()]} + tag_result = TagResult.de_json(json_dict, client) + + assert tag_result.tag == tag + assert tag_result.ids == [playlist_id] + + def test_de_json_all(self, client, tag, playlist_id): + json_dict = {'tag': tag.to_dict(), 'ids': [playlist_id.to_dict()]} + tag_result = TagResult.de_json(json_dict, client) + + assert tag_result.tag == tag + assert tag_result.ids == [playlist_id] + + def test_equality(self, tag, playlist_id): + a = TagResult(tag, [playlist_id]) + b = TagResult(tag, []) + c = TagResult(tag, [playlist_id]) + + assert a != b + assert hash(a) != hash(b) + assert a is not b + + assert a == c diff --git a/yandex_music/__init__.py b/yandex_music/__init__.py index a2711de..4fbba79 100644 --- a/yandex_music/__init__.py +++ b/yandex_music/__init__.py @@ -34,6 +34,8 @@ from .playlist.made_for import MadeFor from .playlist.user import User from .playlist.play_counter import PlayCounter from .playlist.playlist_id import PlaylistId +from .playlist.tag import Tag +from .playlist.tag_result import TagResult from .playlist.playlist_absence import PlaylistAbsence from .playlist.playlist import Playlist @@ -123,4 +125,4 @@ __all__ = ['YandexMusicObject', 'Client', 'Account', 'PassportPhone', 'Invocatio 'Sequence', 'StationTracksResult', 'BriefInfo', 'Description', 'PlaylistId', 'Vinyl', 'Supplement', 'Lyrics', 'VideoSupplement', 'ArtistTracks', 'Pager', 'ArtistAlbums', 'PlaylistAbsence', 'Shot', 'ShotEvent', 'ShotType', 'ShotData', 'SimilarTracks', 'UserSettings', 'RenewableRemainder', 'ChartInfo', 'ChartInfoMenu', - 'ChartInfoMenuItem'] + 'ChartInfoMenuItem', 'Tag', 'TagResult'] diff --git a/yandex_music/client.py b/yandex_music/client.py index 5d303a6..766d312 100644 --- a/yandex_music/client.py +++ b/yandex_music/client.py @@ -4,9 +4,9 @@ from datetime import datetime from typing import Callable, Dict, List, Optional, Union from yandex_music import Album, Artist, ArtistAlbums, ArtistTracks, BriefInfo, Dashboard, DownloadInfo, Experiments, \ - Feed, Genre, Landing, Like, PermissionAlerts, Playlist, PromoCodeStatus, Search, Settings, ShotEvent, SimilarTracks, \ - StationResult, StationTracksResult, Status, Suggestions, Supplement, Track, TracksList, UserSettings, \ - YandexMusicObject, ChartInfo + Feed, Genre, Landing, Like, PermissionAlerts, Playlist, PromoCodeStatus, Search, Settings, ShotEvent, Supplement, \ + StationResult, StationTracksResult, Status, Suggestions, SimilarTracks, Track, TracksList, UserSettings, \ + YandexMusicObject, ChartInfo, TagResult from yandex_music.exceptions import Captcha, InvalidToken from yandex_music.utils.difference import Difference from yandex_music.utils.request import Request @@ -491,6 +491,35 @@ class Client(YandexMusicObject): return Genre.de_list(result, self) + @log + def tags(self, tag_id: str, timeout: Union[int, float] = None, *args, **kwargs) -> Optional[TagResult]: + """Получение тега (подборки). + + Note: + Теги есть в `MixLink` у `Landing`, а также плейлистов в `.tags`. + + У `MixLink` есть `URL`, но `tag_id` только его последняя часть. + Например, `/tag/belarus/`. `Tag` - `belarus`. + + Args: + tag_id (:obj:`str`): Уникальный идентификатор тега. + timeout (:obj:`int` | :obj:`float`, optional): Если это значение указано, используется как время ожидания + ответа от сервера вместо указанного при создании пула. + **kwargs (:obj:`dict`, optional): Произвольные аргументы (будут переданы в запрос). + + Returns: + :obj:`list` из :obj:`yandex_music.Genre` | :obj:`None`: Жанры музыки или :obj:`None`. + + Raises: + :class:`yandex_music.exceptions.YandexMusicError`: Базовое исключение библиотеки. + """ + + url = f'{self.base_url}/tags/{tag_id}/playlist-ids' + + result = self._request.get(url, timeout=timeout, *args, **kwargs) + + return TagResult.de_json(result, self) + @log def tracks_download_info(self, track_id: Union[str, int], get_direct_links: bool = False, timeout: Union[int, float] = None, *args, **kwargs) -> List[DownloadInfo]: diff --git a/yandex_music/playlist/tag.py b/yandex_music/playlist/tag.py new file mode 100644 index 0000000..fdc798d --- /dev/null +++ b/yandex_music/playlist/tag.py @@ -0,0 +1,61 @@ +from typing import TYPE_CHECKING, Optional + +from yandex_music import YandexMusicObject + +if TYPE_CHECKING: + from yandex_music import Client + + +class Tag(YandexMusicObject): + """Класс, представляющий тег (подборку). + + Attributes: + id (:obj:`str`): Идентификатор тега. + value (:obj:`str`): Значение тега (название в lower case). + name (:obj:`str`): Название тега (отображаемое). + og_description (:obj:`str`): Описание тега для OpenGraph. + client (:obj:`yandex_music.Client`): Клиент Yandex Music. + + Args: + id (:obj:`str`): Идентификатор тега. + value (:obj:`str`): Значение тега (название в lower case). + name (:obj:`str`): Название тега (отображаемое). + og_description (:obj:`str`): Описание тега для OpenGraph. + client (:obj:`yandex_music.Client`, optional): Клиент Yandex Music. + **kwargs: Произвольные ключевые аргументы полученные от API. + """ + + def __init__(self, + id_: str, + value: str, + name: str, + og_description: str, + client: Optional['Client'] = None, + **kwargs) -> None: + super().handle_unknown_kwargs(self, **kwargs) + + self.id = id_ + self.value = value + self.name = name + self.og_description = og_description + + self.client = client + self._id_attrs = (self.id, ) + + @classmethod + def de_json(cls, data: dict, client: 'Client') -> Optional['Tag']: + """Десериализация объекта. + + Args: + data (:obj:`dict`): Поля и значения десериализуемого объекта. + client (:obj:`yandex_music.Client`, optional): Клиент Yandex Music. + + Returns: + :obj:`yandex_music.Tag`: Тег. + """ + if not data: + return None + + data = super(Tag, cls).de_json(data, client) + + return cls(client=client, **data) diff --git a/yandex_music/playlist/tag_result.py b/yandex_music/playlist/tag_result.py new file mode 100644 index 0000000..7295f75 --- /dev/null +++ b/yandex_music/playlist/tag_result.py @@ -0,0 +1,56 @@ +from typing import TYPE_CHECKING, Optional, List + +from yandex_music import YandexMusicObject + +if TYPE_CHECKING: + from yandex_music import Client, PlaylistId + + +class TagResult(YandexMusicObject): + """Класс, представляющий тег и его плейлисты. + + Attributes: + tag (:obj:`yandex_music.Tag`): Тег. + ids (:obj:`list` из :obj:`yandex_music.PlaylistId`): Уникальные идентификаторы плейлистов тега. + client (:obj:`yandex_music.Client`): Клиент Yandex Music. + + Args: + tag (:obj:`yandex_music.Tag`): Тег. + ids (:obj:`list` из :obj:`yandex_music.PlaylistId`): Уникальные идентификаторы плейлистов тега. + client (:obj:`yandex_music.Client`, optional): Клиент Yandex Music. + **kwargs: Произвольные ключевые аргументы полученные от API. + """ + + def __init__(self, + tag: str, + ids: List['PlaylistId'], + client: Optional['Client'] = None, + **kwargs) -> None: + super().handle_unknown_kwargs(self, **kwargs) + + self.tag = tag + self.ids = ids + + self.client = client + self._id_attrs = (self.tag, self.ids) + + @classmethod + def de_json(cls, data: dict, client: 'Client') -> Optional['TagResult']: + """Десериализация объекта. + + Args: + data (:obj:`dict`): Поля и значения десериализуемого объекта. + client (:obj:`yandex_music.Client`, optional): Клиент Yandex Music. + + Returns: + :obj:`yandex_music.Tag`: Тег и его плейлисты. + """ + if not data: + return None + + data = super(TagResult, cls).de_json(data, client) + from yandex_music import Tag, PlaylistId + data['tag'] = Tag.de_json(data.get('tag'), client) + data['ids'] = PlaylistId.de_list(data.get('ids'), client) + + return cls(client=client, **data) From 8b81942a10195dbf7d07c08cb53e19e1c1672dd5 Mon Sep 17 00:00:00 2001 From: Il`ya Semyonov Date: Thu, 14 May 2020 22:52:09 +0300 Subject: [PATCH 4/5] =?UTF-8?q?=D0=9F=D0=BE=D0=B4=D0=BF=D1=80=D0=B0=D0=B2?= =?UTF-8?q?=D0=BB=D0=B5=D0=BD=D0=B0=20=D0=B4=D0=BE=D0=BA=D1=83=D0=BC=D0=B5?= =?UTF-8?q?=D0=BD=D1=82=D0=B0=D1=86=D0=B8=D1=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- yandex_music/playlist/tag.py | 4 ++-- yandex_music/playlist/tag_result.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/yandex_music/playlist/tag.py b/yandex_music/playlist/tag.py index fdc798d..63a3f5b 100644 --- a/yandex_music/playlist/tag.py +++ b/yandex_music/playlist/tag.py @@ -10,14 +10,14 @@ class Tag(YandexMusicObject): """Класс, представляющий тег (подборку). Attributes: - id (:obj:`str`): Идентификатор тега. + id (:obj:`str`): Уникальный идентификатор тега. value (:obj:`str`): Значение тега (название в lower case). name (:obj:`str`): Название тега (отображаемое). og_description (:obj:`str`): Описание тега для OpenGraph. client (:obj:`yandex_music.Client`): Клиент Yandex Music. Args: - id (:obj:`str`): Идентификатор тега. + id (:obj:`str`): Уникальный идентификатор тега. value (:obj:`str`): Значение тега (название в lower case). name (:obj:`str`): Название тега (отображаемое). og_description (:obj:`str`): Описание тега для OpenGraph. diff --git a/yandex_music/playlist/tag_result.py b/yandex_music/playlist/tag_result.py index 7295f75..a2ed833 100644 --- a/yandex_music/playlist/tag_result.py +++ b/yandex_music/playlist/tag_result.py @@ -43,7 +43,7 @@ class TagResult(YandexMusicObject): client (:obj:`yandex_music.Client`, optional): Клиент Yandex Music. Returns: - :obj:`yandex_music.Tag`: Тег и его плейлисты. + :obj:`yandex_music.TagResult`: Тег и его плейлисты. """ if not data: return None From 97cf5b5d139c042aa44f8b5cf237ded9e8234023 Mon Sep 17 00:00:00 2001 From: Il`ya Semyonov Date: Thu, 14 May 2020 22:59:42 +0300 Subject: [PATCH 5/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=BE=D0=BF=D0=B5=D1=87=D0=B0=D1=82=D0=BA?= =?UTF-8?q?=D0=B0=20#312?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- yandex_music/client.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/yandex_music/client.py b/yandex_music/client.py index 766d312..5541eb9 100644 --- a/yandex_music/client.py +++ b/yandex_music/client.py @@ -1947,7 +1947,7 @@ class Client(YandexMusicObject): @log def users_likes_playlists(self, user_id: Union[str, int] = None, timeout: Union[int, float] = None, *args, **kwargs) -> List[Like]: - """Получение артистов с отметкой "Мне нравится". + """Получение плейлистов с отметкой "Мне нравится". Args: user_id (:obj:`str` | :obj:`int`, optional): Уникальный идентификатор пользователя. Если не указан