Module hdrezka.post.page

Any HDRezka catalog page.

Classes

class Page (url: str, *, client: HDRezkaClient)
Expand source code
class Page:
    """Paginated HDRezka catalog or listing page."""
    __slots__ = ('_page', '_page_format', '__yields', '__yields_page', 'client')

    def __init__(self, url: str, *, client: 'HDRezkaClient'):
        """
        :param url: Absolute page URL (or host root).
        :param client: Session used for HTTP requests.
        """
        self.client = client
        self.__yields: list[InlineItem] = []
        self.__yields_page = 0
        self.page = url

    @property
    def page(self) -> str:
        """Current page URL template base."""
        return self._page

    @page.setter
    def page(self, value):
        """Cast value to str and set the paginator format."""
        if not isinstance(value, str):
            value = str(value)
        # noinspection HttpUrlsUsage
        if not (value.startswith('https://') or value.startswith('http://')):
            value = f'https://{value}'
        self._page = value
        self._page_format = self._concat_paginator(value.removesuffix('/'))

    @staticmethod
    def _concat_paginator(url: str) -> Callable[[...], str]:
        def __concat_paginator(page, *args, **kwargs):
            if page is not None:
                return f'{url}/page/{{0}}/'.format(page, *args, **kwargs)
            return url.format(*args, **kwargs)

        return __concat_paginator

    @staticmethod
    def _inline_info(years: str, country: str, genre: str):
        year, *finals = years.split('-')
        if finals:
            final, = finals
            year_final = ... if final.strip() == '...' else int(final)
        else:
            year_final = None
        return InlineInfo(int(year), year_final, country.strip(), genre.strip())

    def _request_url(self, page: int | slice | Iterable[int] | Any, **query) -> str:
        """Build a paginated request URL with optional query parameters."""
        return merge_query(self._page_format(page), **query)

    def _parse_items(self, soup: BeautifulSoup) -> list[InlineItem]:
        items = soup.find_all(class_='b-content__inline_item')
        return [InlineItem(
            (a := (link := i.find(class_='b-content__inline_item-link')).find('a'))['href'],
            a.text,
            self._inline_info(*link.find('div').text.split(',', 3)),
            i.find(class_='b-content__inline_item-cover').find('img').get('src', ''),
            self.client,
        )
            for i in items]

    async def get_page_content(
            self,
            page: int | slice | Iterable[int] | None = None,
            *,
            filter: str | None = None,
            genre: str | int | None = None,
            **query,
    ) -> PageContent:
        """
        Fetch a catalog page and parse items, filters, series updates, and favorites cats.

        Extra keyword arguments are merged into the request query string
        (for example ``filter`` / ``genre`` from ``PageFilters``).
        Raises ``EmptyPage`` when no inline items are present.
        """
        if filter is not None:
            query['filter'] = filter
        if genre is not None:
            query['genre'] = genre
        html = (await self.client.get_response('GET', self._request_url(page, **query))).text
        soup = BeautifulSoup(html, builder=BUILDER)
        items = self._parse_items(soup)
        if not items:
            raise EmptyPage(page)
        return PageContent(
            items=items,
            filters=parse_page_filters(soup),
            series_updates=parse_series_updates(soup),
            favorites_cats=parse_favorites_cats(soup),
        )

    async def get_page(
            self,
            page: int | slice | Iterable[int] | None = None,
            *,
            filter: str | None = None,
            genre: str | int | None = None,
            **query,
    ) -> list[InlineItem]:
        """
        Fetch inline items for the given page number.

        Optional ``filter`` / ``genre`` (and other query kwargs) are appended
        to the request URL. Raises ``EmptyPage`` when the page has no items.
        """
        return (await self.get_page_content(page, filter=filter, genre=genre, **query)).items

    async def get_filters(
            self,
            page: int | slice | Iterable[int] = 1,
            *,
            filter: str | None = None,
            genre: str | int | None = None,
            **query,
    ) -> PageFilters:
        """
        Fetch the page and return available sort/type filters (empty if absent).

        Does not require inline items to be present.
        """
        if filter is not None:
            query['filter'] = filter
        if genre is not None:
            query['genre'] = genre
        html = (await self.client.get_response('GET', self._request_url(page, **query))).text
        return parse_page_filters(html)

    async def get_series_updates(self) -> tuple[SeriesUpdateBlock, ...]:
        """
        Fetch this page URL and parse sidebar series updates.

        Typically present on the site home page. Returns an empty tuple if missing.
        """
        html = (await self.client.get_response('GET', self._page)).text
        return parse_series_updates(html)

    async def get_navbar(self) -> Navbar:
        """Fetch this page URL and parse the top navigation menu."""
        html = (await self.client.get_response('GET', self._page)).text
        return parse_navbar(html)

    async def get_favorites_cats(self) -> tuple[FavoritesCat, ...]:
        """
        Fetch this page URL and parse favorites collection links.

        Present on ``/favorites/`` pages. Returns an empty tuple if missing.
        """
        html = (await self.client.get_response('GET', self._page)).text
        return parse_favorites_cats(html)

    def __aiter__(self):
        """Async iterator over items across successive pages."""
        self.__yields_page = 0
        self.__yields.clear()
        return self

    async def __anext__(self) -> InlineItem:
        """Return the next ``InlineItem`` across pages."""
        if not self.__yields:
            try:
                self.__yields_page += 1
                self.__yields += await self.get_page(self.__yields_page)
            except EmptyPage:
                raise StopAsyncIteration
        return self.__yields.pop(0)

    def __repr__(self):
        return f'{self.__class__.__qualname__}({self.page!r})'

Paginated HDRezka catalog or listing page.

:param url: Absolute page URL (or host root). :param client: Session used for HTTP requests.

Subclasses

Instance variables

var client
Expand source code
class Page:
    """Paginated HDRezka catalog or listing page."""
    __slots__ = ('_page', '_page_format', '__yields', '__yields_page', 'client')

    def __init__(self, url: str, *, client: 'HDRezkaClient'):
        """
        :param url: Absolute page URL (or host root).
        :param client: Session used for HTTP requests.
        """
        self.client = client
        self.__yields: list[InlineItem] = []
        self.__yields_page = 0
        self.page = url

    @property
    def page(self) -> str:
        """Current page URL template base."""
        return self._page

    @page.setter
    def page(self, value):
        """Cast value to str and set the paginator format."""
        if not isinstance(value, str):
            value = str(value)
        # noinspection HttpUrlsUsage
        if not (value.startswith('https://') or value.startswith('http://')):
            value = f'https://{value}'
        self._page = value
        self._page_format = self._concat_paginator(value.removesuffix('/'))

    @staticmethod
    def _concat_paginator(url: str) -> Callable[[...], str]:
        def __concat_paginator(page, *args, **kwargs):
            if page is not None:
                return f'{url}/page/{{0}}/'.format(page, *args, **kwargs)
            return url.format(*args, **kwargs)

        return __concat_paginator

    @staticmethod
    def _inline_info(years: str, country: str, genre: str):
        year, *finals = years.split('-')
        if finals:
            final, = finals
            year_final = ... if final.strip() == '...' else int(final)
        else:
            year_final = None
        return InlineInfo(int(year), year_final, country.strip(), genre.strip())

    def _request_url(self, page: int | slice | Iterable[int] | Any, **query) -> str:
        """Build a paginated request URL with optional query parameters."""
        return merge_query(self._page_format(page), **query)

    def _parse_items(self, soup: BeautifulSoup) -> list[InlineItem]:
        items = soup.find_all(class_='b-content__inline_item')
        return [InlineItem(
            (a := (link := i.find(class_='b-content__inline_item-link')).find('a'))['href'],
            a.text,
            self._inline_info(*link.find('div').text.split(',', 3)),
            i.find(class_='b-content__inline_item-cover').find('img').get('src', ''),
            self.client,
        )
            for i in items]

    async def get_page_content(
            self,
            page: int | slice | Iterable[int] | None = None,
            *,
            filter: str | None = None,
            genre: str | int | None = None,
            **query,
    ) -> PageContent:
        """
        Fetch a catalog page and parse items, filters, series updates, and favorites cats.

        Extra keyword arguments are merged into the request query string
        (for example ``filter`` / ``genre`` from ``PageFilters``).
        Raises ``EmptyPage`` when no inline items are present.
        """
        if filter is not None:
            query['filter'] = filter
        if genre is not None:
            query['genre'] = genre
        html = (await self.client.get_response('GET', self._request_url(page, **query))).text
        soup = BeautifulSoup(html, builder=BUILDER)
        items = self._parse_items(soup)
        if not items:
            raise EmptyPage(page)
        return PageContent(
            items=items,
            filters=parse_page_filters(soup),
            series_updates=parse_series_updates(soup),
            favorites_cats=parse_favorites_cats(soup),
        )

    async def get_page(
            self,
            page: int | slice | Iterable[int] | None = None,
            *,
            filter: str | None = None,
            genre: str | int | None = None,
            **query,
    ) -> list[InlineItem]:
        """
        Fetch inline items for the given page number.

        Optional ``filter`` / ``genre`` (and other query kwargs) are appended
        to the request URL. Raises ``EmptyPage`` when the page has no items.
        """
        return (await self.get_page_content(page, filter=filter, genre=genre, **query)).items

    async def get_filters(
            self,
            page: int | slice | Iterable[int] = 1,
            *,
            filter: str | None = None,
            genre: str | int | None = None,
            **query,
    ) -> PageFilters:
        """
        Fetch the page and return available sort/type filters (empty if absent).

        Does not require inline items to be present.
        """
        if filter is not None:
            query['filter'] = filter
        if genre is not None:
            query['genre'] = genre
        html = (await self.client.get_response('GET', self._request_url(page, **query))).text
        return parse_page_filters(html)

    async def get_series_updates(self) -> tuple[SeriesUpdateBlock, ...]:
        """
        Fetch this page URL and parse sidebar series updates.

        Typically present on the site home page. Returns an empty tuple if missing.
        """
        html = (await self.client.get_response('GET', self._page)).text
        return parse_series_updates(html)

    async def get_navbar(self) -> Navbar:
        """Fetch this page URL and parse the top navigation menu."""
        html = (await self.client.get_response('GET', self._page)).text
        return parse_navbar(html)

    async def get_favorites_cats(self) -> tuple[FavoritesCat, ...]:
        """
        Fetch this page URL and parse favorites collection links.

        Present on ``/favorites/`` pages. Returns an empty tuple if missing.
        """
        html = (await self.client.get_response('GET', self._page)).text
        return parse_favorites_cats(html)

    def __aiter__(self):
        """Async iterator over items across successive pages."""
        self.__yields_page = 0
        self.__yields.clear()
        return self

    async def __anext__(self) -> InlineItem:
        """Return the next ``InlineItem`` across pages."""
        if not self.__yields:
            try:
                self.__yields_page += 1
                self.__yields += await self.get_page(self.__yields_page)
            except EmptyPage:
                raise StopAsyncIteration
        return self.__yields.pop(0)

    def __repr__(self):
        return f'{self.__class__.__qualname__}({self.page!r})'
prop page : str
Expand source code
@property
def page(self) -> str:
    """Current page URL template base."""
    return self._page

Current page URL template base.

Methods

async def get_favorites_cats(self) ‑> tuple[FavoritesCat, ...]
Expand source code
async def get_favorites_cats(self) -> tuple[FavoritesCat, ...]:
    """
    Fetch this page URL and parse favorites collection links.

    Present on ``/favorites/`` pages. Returns an empty tuple if missing.
    """
    html = (await self.client.get_response('GET', self._page)).text
    return parse_favorites_cats(html)

Fetch this page URL and parse favorites collection links.

Present on /favorites/ pages. Returns an empty tuple if missing.

async def get_filters(self,
page: int | slice | Iterable[int] = 1,
*,
filter: str | None = None,
genre: int | str | None = None,
**query) ‑> PageFilters
Expand source code
async def get_filters(
        self,
        page: int | slice | Iterable[int] = 1,
        *,
        filter: str | None = None,
        genre: str | int | None = None,
        **query,
) -> PageFilters:
    """
    Fetch the page and return available sort/type filters (empty if absent).

    Does not require inline items to be present.
    """
    if filter is not None:
        query['filter'] = filter
    if genre is not None:
        query['genre'] = genre
    html = (await self.client.get_response('GET', self._request_url(page, **query))).text
    return parse_page_filters(html)

Fetch the page and return available sort/type filters (empty if absent).

Does not require inline items to be present.

async def get_navbar(self) ‑> Navbar
Expand source code
async def get_navbar(self) -> Navbar:
    """Fetch this page URL and parse the top navigation menu."""
    html = (await self.client.get_response('GET', self._page)).text
    return parse_navbar(html)

Fetch this page URL and parse the top navigation menu.

async def get_page(self,
page: int | slice | Iterable[int] | None = None,
*,
filter: str | None = None,
genre: int | str | None = None,
**query) ‑> list[InlineItem]
Expand source code
async def get_page(
        self,
        page: int | slice | Iterable[int] | None = None,
        *,
        filter: str | None = None,
        genre: str | int | None = None,
        **query,
) -> list[InlineItem]:
    """
    Fetch inline items for the given page number.

    Optional ``filter`` / ``genre`` (and other query kwargs) are appended
    to the request URL. Raises ``EmptyPage`` when the page has no items.
    """
    return (await self.get_page_content(page, filter=filter, genre=genre, **query)).items

Fetch inline items for the given page number.

Optional filter / genre (and other query kwargs) are appended to the request URL. Raises EmptyPage when the page has no items.

async def get_page_content(self,
page: int | slice | Iterable[int] | None = None,
*,
filter: str | None = None,
genre: int | str | None = None,
**query) ‑> PageContent
Expand source code
async def get_page_content(
        self,
        page: int | slice | Iterable[int] | None = None,
        *,
        filter: str | None = None,
        genre: str | int | None = None,
        **query,
) -> PageContent:
    """
    Fetch a catalog page and parse items, filters, series updates, and favorites cats.

    Extra keyword arguments are merged into the request query string
    (for example ``filter`` / ``genre`` from ``PageFilters``).
    Raises ``EmptyPage`` when no inline items are present.
    """
    if filter is not None:
        query['filter'] = filter
    if genre is not None:
        query['genre'] = genre
    html = (await self.client.get_response('GET', self._request_url(page, **query))).text
    soup = BeautifulSoup(html, builder=BUILDER)
    items = self._parse_items(soup)
    if not items:
        raise EmptyPage(page)
    return PageContent(
        items=items,
        filters=parse_page_filters(soup),
        series_updates=parse_series_updates(soup),
        favorites_cats=parse_favorites_cats(soup),
    )

Fetch a catalog page and parse items, filters, series updates, and favorites cats.

Extra keyword arguments are merged into the request query string (for example filter / genre from PageFilters). Raises EmptyPage when no inline items are present.

async def get_series_updates(self) ‑> tuple[SeriesUpdateBlock, ...]
Expand source code
async def get_series_updates(self) -> tuple[SeriesUpdateBlock, ...]:
    """
    Fetch this page URL and parse sidebar series updates.

    Typically present on the site home page. Returns an empty tuple if missing.
    """
    html = (await self.client.get_response('GET', self._page)).text
    return parse_series_updates(html)

Fetch this page URL and parse sidebar series updates.

Typically present on the site home page. Returns an empty tuple if missing.