Merge pull request #329 from MarshalX/issue-192

Добавлено получение тегов.
このコミットが含まれているのは:
Il'ya 2020-05-14 22:53:21 +03:00 committed by GitHub
コミット 2ef2e5d533
この署名に対応する既知のキーがデータベースに存在しません
GPGキーID: 4AEE18F83AFDEB23
11個のファイルの変更265行の追加6行の削除

ファイルの表示

@ -10,3 +10,5 @@
yandex_music.playlist.playlist yandex_music.playlist.playlist
yandex_music.playlist.case_forms yandex_music.playlist.case_forms
yandex_music.playlist.playlist_id yandex_music.playlist.playlist_id
yandex_music.playlist.tag_result
yandex_music.playlist.tag

7
docs/source/yandex_music.playlist.tag.rst ノーマルファイル
ファイルの表示

@ -0,0 +1,7 @@
yandex_music.Tag
================
.. autoclass:: yandex_music.Tag
:members:
:undoc-members:
:show-inheritance:

ファイルの表示

@ -0,0 +1,7 @@
yandex_music.TagResult
======================
.. autoclass:: yandex_music.TagResult
:members:
:undoc-members:
:show-inheritance:

ファイルの表示

@ -77,3 +77,4 @@ from .test_shot_type import TestShotType
from .test_shot_data import TestShotData from .test_shot_data import TestShotData
from .test_shot import TestShot from .test_shot import TestShot
from .test_renewable_remainder import TestRenewableRemainder from .test_renewable_remainder import TestRenewableRemainder
from .test_tag import TestTag

ファイルの表示

@ -7,7 +7,7 @@ from yandex_music import Counts, TrackId, CaseForms, Ratings, Icon, Album, Lyric
PersonalPlaylistsData, RotorSettings, TrackShortOld, PlayContextsData, Status, Settings, StationResult, Enum, \ PersonalPlaylistsData, RotorSettings, TrackShortOld, PlayContextsData, Status, Settings, StationResult, Enum, \
TrackWithAds, VideoSupplement, ArtistEvent, ChartItem, Event, AlbumEvent, Day, PlayContext, Plus, Title, Label, \ TrackWithAds, VideoSupplement, ArtistEvent, ChartItem, Event, AlbumEvent, Day, PlayContext, Plus, Title, Label, \
GeneratedPlaylist, Video, Vinyl, SearchResult, BlockEntity, Block, PlaylistAbsence, ShotType, ShotData, Shot, \ 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, \ from . import TestCounts, TestTrackId, TestCaseForms, TestRatings, TestIcon, TestAlbum, TestLyrics, \
TestTrack, TestInvocationInfo, TestPlaylist, TestAutoRenewable, TestStation, TestNormalization, TestMajor, \ TestTrack, TestInvocationInfo, TestPlaylist, TestAutoRenewable, TestStation, TestNormalization, TestMajor, \
TestTrackPosition, TestBest, TestChart, TestPermissions, TestPlus, TestProduct, TestCover, TestPlayCounter, \ 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, \ TestTrackShortOld, TestPager, TestStatus, TestSettings, TestStationResult, TestLabel, TestTrackWithAds, \
TestVideoSupplement, TestEvent, TestDay, TestPlayContext, TestGeneratedPlaylist, TestVideo, TestVinyl, \ TestVideoSupplement, TestEvent, TestDay, TestPlayContext, TestGeneratedPlaylist, TestVideo, TestVinyl, \
TestSearchResult, TestBlockEntity, TestBlock, TestPlaylistAbsence, TestShot, TestShotData, TestShotType, \ TestSearchResult, TestBlockEntity, TestBlock, TestPlaylistAbsence, TestShot, TestShotData, TestShotType, \
TestRenewableRemainder, TestChartInfoMenuItem, TestChartInfo TestRenewableRemainder, TestChartInfoMenuItem, TestChartInfo, TestTag
@pytest.fixture(scope='session') @pytest.fixture(scope='session')
@ -137,6 +137,11 @@ def client():
return Client() return Client()
@pytest.fixture(scope='session')
def tag():
return Tag(TestTag.id_, TestTag.value, TestTag.name, TestTag.og_description)
@pytest.fixture(scope='session') @pytest.fixture(scope='session')
def track_with_ads(track): def track_with_ads(track):
return TrackWithAds(TestTrackWithAds.type, track) return TrackWithAds(TestTrackWithAds.type, track)

47
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

42
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

ファイルの表示

@ -34,6 +34,8 @@ from .playlist.made_for import MadeFor
from .playlist.user import User from .playlist.user import User
from .playlist.play_counter import PlayCounter from .playlist.play_counter import PlayCounter
from .playlist.playlist_id import PlaylistId 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_absence import PlaylistAbsence
from .playlist.playlist import Playlist from .playlist.playlist import Playlist
@ -123,4 +125,4 @@ __all__ = ['YandexMusicObject', 'Client', 'Account', 'PassportPhone', 'Invocatio
'Sequence', 'StationTracksResult', 'BriefInfo', 'Description', 'PlaylistId', 'Vinyl', 'Supplement', 'Lyrics', 'Sequence', 'StationTracksResult', 'BriefInfo', 'Description', 'PlaylistId', 'Vinyl', 'Supplement', 'Lyrics',
'VideoSupplement', 'ArtistTracks', 'Pager', 'ArtistAlbums', 'PlaylistAbsence', 'Shot', 'ShotEvent', 'VideoSupplement', 'ArtistTracks', 'Pager', 'ArtistAlbums', 'PlaylistAbsence', 'Shot', 'ShotEvent',
'ShotType', 'ShotData', 'SimilarTracks', 'UserSettings', 'RenewableRemainder', 'ChartInfo', 'ChartInfoMenu', 'ShotType', 'ShotData', 'SimilarTracks', 'UserSettings', 'RenewableRemainder', 'ChartInfo', 'ChartInfoMenu',
'ChartInfoMenuItem'] 'ChartInfoMenuItem', 'Tag', 'TagResult']

ファイルの表示

@ -4,9 +4,9 @@ from datetime import datetime
from typing import Callable, Dict, List, Optional, Union from typing import Callable, Dict, List, Optional, Union
from yandex_music import Album, Artist, ArtistAlbums, ArtistTracks, BriefInfo, Dashboard, DownloadInfo, Experiments, \ from yandex_music import Album, Artist, ArtistAlbums, ArtistTracks, BriefInfo, Dashboard, DownloadInfo, Experiments, \
Feed, Genre, Landing, Like, PermissionAlerts, Playlist, PromoCodeStatus, Search, Settings, ShotEvent, SimilarTracks, \ Feed, Genre, Landing, Like, PermissionAlerts, Playlist, PromoCodeStatus, Search, Settings, ShotEvent, Supplement, \
StationResult, StationTracksResult, Status, Suggestions, Supplement, Track, TracksList, UserSettings, \ StationResult, StationTracksResult, Status, Suggestions, SimilarTracks, Track, TracksList, UserSettings, \
YandexMusicObject, ChartInfo YandexMusicObject, ChartInfo, TagResult
from yandex_music.exceptions import Captcha, InvalidToken from yandex_music.exceptions import Captcha, InvalidToken
from yandex_music.utils.difference import Difference from yandex_music.utils.difference import Difference
from yandex_music.utils.request import Request from yandex_music.utils.request import Request
@ -491,6 +491,35 @@ class Client(YandexMusicObject):
return Genre.de_list(result, self) 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 @log
def tracks_download_info(self, track_id: Union[str, int], get_direct_links: bool = False, def tracks_download_info(self, track_id: Union[str, int], get_direct_links: bool = False,
timeout: Union[int, float] = None, *args, **kwargs) -> List[DownloadInfo]: timeout: Union[int, float] = None, *args, **kwargs) -> List[DownloadInfo]:

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

56
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.TagResult`: Тег и его плейлисты.
"""
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)