Skip to content

Commit

Permalink
Support for new blocks from landing page (#3)
Browse files Browse the repository at this point in the history
Поддержка блоков с лендинга (подборки для вас, в тренде)
  • Loading branch information
KatantDev authored Dec 28, 2023
2 parents 5728578 + 9e76126 commit c4d245f
Show file tree
Hide file tree
Showing 53 changed files with 1,011 additions and 66 deletions.
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,12 @@ poetry add ymdantic

## Пример использования
Получаем список чартов и выводим треки

```python
import asyncio
from ymdantic import YMClient


async def get_chart(client: YMClient) -> None:
chart_block = await client.get_chart()
print(chart_block.title, end="\n\n")
Expand Down
2 changes: 1 addition & 1 deletion poetry.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 3 additions & 3 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ python = "^3.8"
aiohttp = "^3.9.0"
pydantic = "^2.4"
dataclass-rest = "^0.4"
certifi = "^2023.11.17"
certifi = "^2023.7.22"


[tool.poetry.group.dev.dependencies]
Expand All @@ -20,7 +20,7 @@ ruff = "^0.1"
pre-commit = "^3"

[tool.black]
target-version = ['py310', 'py311', 'py312']
target-version = ['py38', 'py39', 'py310', 'py311', 'py312']
exclude = '''
(
\.eggs
Expand Down Expand Up @@ -55,7 +55,7 @@ exclude = [
"scripts",
"*.egg-info",
]
target-version = "py310"
target-version = "py38"

[tool.ruff.isort]
known-first-party = [
Expand Down
17 changes: 13 additions & 4 deletions ymdantic/client/session/aiohttp_client.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import asyncio
import ssl
import urllib.parse
from contextlib import asynccontextmanager
from typing import Optional, Any, Dict, AsyncIterator
from typing import Optional, Any, Type, Dict, AsyncIterator

from aiohttp import ClientSession, FormData, ClientError, ClientTimeout
import certifi
from aiohttp import ClientSession, FormData, ClientError, ClientTimeout, TCPConnector
from dataclass_rest.base_client import BaseClient
from dataclass_rest.exceptions import ClientLibraryError
from dataclass_rest.http_request import HttpRequest
Expand All @@ -23,9 +25,13 @@ def __init__(
super().__init__()
self.base_url = base_url
self.headers = headers or {}

self.timeout: ClientTimeout = timeout or ClientTimeout(total=0)

self._session: Optional[ClientSession] = None
self._connector_type: Type[TCPConnector] = TCPConnector
self._connector_init: Dict[str, Any] = {
"ssl": ssl.create_default_context(cafile=certifi.where()),
}

@asynccontextmanager
async def context(self, auto_close: bool = True) -> AsyncIterator["AiohttpClient"]:
Expand All @@ -52,7 +58,10 @@ async def get_session(self) -> ClientSession:
новую сессию.
"""
if self._session is None or self._session.closed:
self._session = ClientSession(headers=self.headers)
self._session = ClientSession(
connector=self._connector_type(**self._connector_init),
headers=self.headers,
)

return self._session

Expand Down
4 changes: 3 additions & 1 deletion ymdantic/client/session/aiohttp_method.py
Original file line number Diff line number Diff line change
Expand Up @@ -83,9 +83,11 @@ async def _on_error_default(self, response: ClientResponse) -> None:
"""
response_json = await response.json()
if 400 <= response.status <= 500:
if "error" in response_json:
response_json = response_json["error"]
raise YandexMusicError(
error=YandexMusicErrorModel.model_validate(
response_json.get("error"),
response_json,
),
)

Expand Down
191 changes: 182 additions & 9 deletions ymdantic/client/yandex_music.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,15 @@
from dataclass_rest.client_protocol import FactoryProtocol
from pydantic import HttpUrl

from ymdantic import enums
from ymdantic.adapters.pydantic_factory import PydanticFactory
from ymdantic.client.session import AiohttpClient
from ymdantic.exceptions import UndefinedUser
from ymdantic.models import (
Response,
ShortAlbum,
Album,
ChartBlock,
OldChartBlock,
Playlist,
TrackType,
DownloadInfo,
Expand All @@ -21,7 +22,29 @@
NewReleasesBlock,
S3FileUrl,
DownloadInfoDirect,
SkeletonResponse,
EditorialResponse,
LandingArtist,
LandingArtistItem,
LandingAlbumItemData,
LandingAlbumItem,
LandingLikedPlaylistItem,
LandingLikedPlaylistItemData,
LandingPromotion,
LandingPromotionResponse,
LandingOpenPlaylist,
LandingSpecial,
LandingPersonalPlaylistItemData,
LandingPersonalPlaylistItem,
LandingPlaylistItem,
LandingPlaylistItemData,
ChartBlock,
InStyle,
InStyleResponse,
LandingWaves,
LandingWavesResponse,
)
from ymdantic.models.landing.artist import LandingArtistItemData


class YMClient(AiohttpClient):
Expand Down Expand Up @@ -110,15 +133,15 @@ async def __get_direct_url_info_request(
) -> S3FileUrl:
...

async def get_chart(self, limit: Optional[int] = None) -> ChartBlock:
response = await self.get_chart_request(limit=limit)
async def get_old_chart(self, limit: Optional[int] = None) -> OldChartBlock:
response = await self.get_old_chart_request(limit=limit)
return response.result

@get("landing3/chart")
async def get_chart_request(
async def get_old_chart_request(
self,
limit: Optional[int] = None,
) -> Response[ChartBlock]:
) -> Response[OldChartBlock]:
...

async def get_new_releases_old(self) -> NewReleasesBlock:
Expand Down Expand Up @@ -187,12 +210,18 @@ async def get_albums_request(
) -> Response[List[ShortAlbum]]:
...

async def get_editorial_new_releases(self) -> List[NewRelease]:
response = await self.get_editorial_new_releases_request()
async def get_editorial_new_releases(
self,
block_type: enums.EditorialNewReleasesEnum,
) -> List[NewRelease]:
response = await self.get_editorial_new_releases_request(block_type=block_type)
return response.new_releases

@get("landing/block/editorial/new-releases/ALL_albums_of_the_month")
async def get_editorial_new_releases_request(self) -> NewReleasesResponse:
@get("landing/block/editorial/new-releases/{block_type}")
async def get_editorial_new_releases_request(
self,
block_type: enums.EditorialNewReleasesEnum,
) -> NewReleasesResponse:
...

async def get_recommended_new_releases(self) -> List[NewRelease]:
Expand All @@ -202,3 +231,147 @@ async def get_recommended_new_releases(self) -> List[NewRelease]:
@get("landing/block/new-releases")
async def get_recommended_new_releases_request(self) -> NewReleasesResponse:
...

# Не нужен отдельный метод, так как возвращается прямой результат.
@get("landing/skeleton/main")
async def get_skeleton_main(self) -> SkeletonResponse:
...

async def get_editorial_artists(
self,
block_type: enums.EditorialArtistsEnum,
) -> List[LandingArtist]:
response = await self.get_editorial_artists_request(
block_type=block_type,
)
return [item.data.artist for item in response.items]

@get("landing/block/editorial/artists/{block_type}")
async def get_editorial_artists_request(
self,
block_type: enums.EditorialArtistsEnum,
) -> EditorialResponse[LandingArtistItem]:
...

async def get_editorial_compilation(
self,
block_type: enums.EditorialCompilationEnum,
) -> Union[List[LandingAlbumItemData], List[LandingLikedPlaylistItemData]]:
response = await self.get_editorial_compilation_request(
block_type=block_type,
)
return [item.data for item in response.items]

@get("landing/block/editorial/compilation/{block_type}")
async def get_editorial_compilation_request(
self,
block_type: enums.EditorialCompilationEnum,
) -> EditorialResponse[Union[LandingAlbumItem, LandingLikedPlaylistItem]]:
...

async def get_editorial_promotions(
self,
block_type: enums.EditorialPromotionEnum,
) -> List[LandingPromotion]:
response = await self.get_editorial_promotions_request(block_type=block_type)
return response.promotions

@get("landing/block/editorial-promotion/{block_type}")
async def get_editorial_promotions_request(
self,
block_type: enums.EditorialPromotionEnum,
) -> LandingPromotionResponse:
...

# Не нужен отдельный метод, так как возвращается прямой результат.
# Может быть smart-open-playlist или open-playlist.
@get("landing/block/{block_type}")
async def get_open_playlist(
self,
block_type: enums.OpenPlaylistEnum,
) -> LandingOpenPlaylist:
...

# Не нужен отдельный метод, так как возвращается прямой результат.
@get("landing/block/special/{block_type}")
async def get_special_blocks(
self,
block_type: enums.SpecialEnum,
) -> LandingSpecial:
...

async def get_personal_playlists(self) -> List[LandingPersonalPlaylistItemData]:
response = await self.get_personal_playlists_request()
return [item.data for item in response.items]

@get("landing/block/personal-playlists")
async def get_personal_playlists_request(
self,
) -> EditorialResponse[LandingPersonalPlaylistItem]:
...

async def get_new_playlists(self) -> List[LandingLikedPlaylistItemData]:
response = await self.get_new_playlists_request()
return [item.data for item in response.items]

@get("landing/block/new-playlists")
async def get_new_playlists_request(
self,
) -> EditorialResponse[LandingLikedPlaylistItem]:
...

async def get_personal_artists(self) -> List[LandingArtist]:
response = await self.get_personal_artists_request()
return [item.data.artist for item in response.items]

@get("landing/block/personal-artists")
async def get_personal_artists_request(
self,
) -> EditorialResponse[LandingArtistItem]:
...

async def get_recently_played(
self,
) -> List[
Union[LandingPlaylistItemData, LandingArtistItemData, LandingAlbumItemData]
]:
response = await self.get_recently_played_request()
return [item.data for item in response.items]

@get("landing/block/recently-played")
async def get_recently_played_request(
self,
) -> EditorialResponse[
Union[LandingPlaylistItem, LandingArtistItem, LandingAlbumItem]
]:
...

# Не нужен отдельный метод, так как возвращается прямой результат.
@get("landing/block/chart")
async def get_chart(
self,
limit: Optional[int] = None,
) -> ChartBlock:
...

async def get_in_style(self) -> List[InStyle]:
response = await self.get_in_style_request()
return response.in_style_tabs

@get("landing/block/in-style")
async def get_in_style_request(
self,
limit: Optional[int] = None,
) -> InStyleResponse:
...

async def get_waves(self) -> List[LandingWaves]:
response = await self.get_waves_request()
return response.waves

@get("landing/block/waves")
async def get_waves_request(
self,
limit: Optional[int] = None,
) -> LandingWavesResponse:
...
15 changes: 15 additions & 0 deletions ymdantic/enums/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
from .editorial_artists import EditorialArtistsEnum
from .editorial_compilation import EditorialCompilationEnum
from .editorial_new_releases import EditorialNewReleasesEnum
from .editorial_promotion import EditorialPromotionEnum
from .open_playlist import OpenPlaylistEnum
from .special import SpecialEnum

__all__ = (
"EditorialArtistsEnum",
"EditorialCompilationEnum",
"EditorialNewReleasesEnum",
"EditorialPromotionEnum",
"OpenPlaylistEnum",
"SpecialEnum",
)
9 changes: 9 additions & 0 deletions ymdantic/enums/editorial_artists.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
from enum import Enum


class EditorialArtistsEnum(str, Enum):
"""Перечисление, представляющее типы блоков с артистами на главной странице."""

ALL_ISRKA = "ALL_isrka"
RU_ARTISTS = "RU_artists"
RU_ISKRA = "RU_iskra"
26 changes: 26 additions & 0 deletions ymdantic/enums/editorial_compilation.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
from enum import Enum


class EditorialCompilationEnum(str, Enum):
"""Перечисление, представляющее типы блоков с подборками на главной странице."""

ALL_NEWYEAR = "ALL_newyear"
# Плейлисты.
ALL_WINTER = "ALL_winter"
# Плейлисты.
RUSSIA_HITS = "RUSSIA_hits"
# Плейлисты.
RUSSIA_NEWCOMERS = "RUSSIA_newcomers"
# Плейлисты.
RUSSIA_EDITORIAL_COMPILATION = "RUSSIA_editorial_compilation"
# Альбомы.
ALL_ALBUMS_WITH_VIDEOSHOTS = "ALL_albums_with_videoshots"
# Альбомы.
ALL_ALBUMS_WITH_COMMENTARY = "ALL_albums_with_commentary"
# Альбомы.
RU_GENRES = "RU_genres"
# Плейлисты.
RU_TRANDS = "RU_trands"
# Плейлисты.
RU_EDITORS = "RU_editors"
# Альбомы.
8 changes: 8 additions & 0 deletions ymdantic/enums/editorial_new_releases.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
from enum import Enum


class EditorialNewReleasesEnum(str, Enum):
"""Перечисление, представляющее типы блоков с новинками на главной странице."""

ALL_ALBUMS_OF_THE_MONTH = "ALL_albums_of_the_month"
RU_ALBUMS = "RU_albums"
Loading

0 comments on commit c4d245f

Please sign in to comment.