Metadata-Version: 2.4
Name: hdrezka
Version: 5.2.0
Summary: Async HDRezka client library for scripts and multi-user APIs
License-Expression: MIT
License-File: LICENSE
Keywords: HDRezka,watch online,api,stream,m3u8,hls,async,client
Author: Nikita Denissov
Author-email: n.denissov@proton.me
Requires-Python: >=3.10
Classifier: Development Status :: 5 - Production/Stable
Classifier: Environment :: Console
Classifier: Intended Audience :: Developers
Classifier: Intended Audience :: End Users/Desktop
Classifier: Intended Audience :: Information Technology
Classifier: Intended Audience :: Other Audience
Classifier: Intended Audience :: Telecommunications Industry
Classifier: Natural Language :: English
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: 3.14
Classifier: Topic :: Internet
Classifier: Topic :: Internet :: WWW/HTTP
Classifier: Topic :: Internet :: WWW/HTTP :: Dynamic Content
Classifier: Topic :: Multimedia :: Video
Classifier: Typing :: Typed
Provides-Extra: socks
Requires-Dist: beautifulsoup4 (>=4.13.4,<5.0.0)
Requires-Dist: httpx (>=0.28.1,<0.29.0)
Requires-Dist: httpx-curl-cffi (>=0.1.5,<0.2.0)
Requires-Dist: httpx[socks] (>=0.28.1,<0.29.0) ; extra == "socks"
Requires-Dist: lxml (>=6.0.0,<7.0.0) ; python_version < "3.15"
Project-URL: Bug Tracker, https://github.com/ndenissov/HDRezka/issues
Project-URL: Documentation, https://ndenissov.github.io/HDRezka/hdrezka
Project-URL: Homepage, https://github.com/ndenissov/HDRezka
Project-URL: Repository, https://github.com/ndenissov/HDRezka
Description-Content-Type: text/markdown

# HDRezka

[![PyPI version](https://img.shields.io/pypi/v/hdrezka.svg)](https://pypi.org/project/hdrezka/)
[![Downloads](https://static.pepy.tech/badge/hdrezka)](https://pepy.tech/project/hdrezka)
[![Python versions](https://img.shields.io/pypi/pyversions/hdrezka.svg)](https://pypi.org/project/hdrezka/)
[![License](https://img.shields.io/pypi/l/hdrezka.svg)](https://github.com/ndenissov/hdrezka/blob/main/LICENSE)
[![GitHub stars](https://img.shields.io/github/stars/ndenissov/hdrezka)](https://github.com/ndenissov/hdrezka/stargazers)

Async Python client for HDRezka. Designed for scripts and for multi-user backends
(one `HDRezkaClient` instance per user / request scope).

## Disclaimer

This is an **unofficial** library. It is meant for personal tooling and backends
built by people who already have the right to use HDRezka under the service's own
rules and under applicable law. Please read and follow the terms published by
HDRezka (they change over time). Among other things, bots, downloaders, parsers,
scraping, and many third-party clients may be restricted or may require Premium;
access can be limited without notice. Use only what you are allowed to use, and
only within those limits. This project does not endorse misuse of the service,
account sharing, or anything that harms the platform or other users.

Default host is `https://rezka.ag/` (free viewing is described there in the
service rules). In practice most public mirrors sit behind Cloudflare, and a
plain scripted client often gets blocked. You can point `host=` at another mirror
(see [mirrors.txt](mirrors.txt)), use a suitable network path, and keep
browser-like TLS impersonation enabled (default). That may help reliability; it
is not a guarantee, and it is not permission to ignore the service rules.

## Install

```bash
pip install hdrezka
```

Socks proxy support:

```bash
pip install hdrezka[socks]
```

## Quick start

```python
import asyncio
import os

from hdrezka import HDRezkaClient


async def main():
    async with HDRezkaClient() as client:
        await client.login(os.environ['LOGIN_NAME'], os.environ['LOGIN_PASSWORD'])

        items = await client.search('Breaking Bad').get_page(1)
        player = await client.player(items[0].url)
        # or: player = await Player(items[0].url, client=client)

        print(player.post.info)

        translator_id = None
        for name, id_ in player.post.translators.name_id.items():
            if 'субтитры' in name.casefold():
                translator_id = id_
                break

        stream = await player.get_stream(1, 1, translator_id)
        video = stream.video
        print(await video.last_url[-1])
        print(await video[video.min].last_url[0].mp4)

        subtitles = stream.subtitles
        if subtitles.has_subtitles:
            print(subtitles.default.url)


if __name__ == '__main__':
    asyncio.run(main())
```

## HDRezkaClient

`HDRezkaClient` owns the HTTP session, active mirror (`host`), cookies, and a small
player cache. Create a separate client for each user when building an API.

| Method / property                                  | Purpose                                                      |
|----------------------------------------------------|--------------------------------------------------------------|
| `await client.login(email, password)`              | Discover an active mirror and authenticate                   |
| `client.search(query)`                             | Bound `Search`                                               |
| `client.page(url=None)`                            | Bound catalog `Page`                                         |
| `client.favorites(cat_id=None)`                    | Bound `Favorites` page (`/favorites/` or `/favorites/{id}/`) |
| `await client.player(url)`                         | `PlayerMovie` or `PlayerSeries`                              |
| `await client.post(url)`                           | Initialized `Post`                                           |
| `await client.navbar()`                            | Site top navigation map                                      |
| `await client.series_updates()`                    | Sidebar series update feed (home page)                       |
| `await client.add_favorites_cat(name)`             | Create a favorites collection                                |
| `await client.rename_favorites_cat(cat_id, name)`  | Rename a collection                                          |
| `await client.add_favorites_post(post_id, cat_id)` | Add a title to a collection                                  |
| `await client.remove_favorites_cat(cat_id)`        | Delete a collection                                          |
| `client.ajax`                                      | Bound `AJAX` helpers (streams, trailers, favorites, …)       |
| `client.host`                                      | Current mirror base URL                                      |
| `await client.aclose()`                            | Close the HTTP client (also via `async with`)                |

Constructor options: `host`, `proxy`, `http_client`, `request_kwargs`, `headers`,
`redirect_url`, `impersonate`.

Domain types (`Search`, `Player`, `Post`, `Page`, `Favorites`, `AJAX`) require an
explicit `client=` (or are created through the client factories above).

## Cloudflare and browser impersonation

By default `HDRezkaClient` sends browser-like headers and uses
[`curl_cffi`](https://github.com/lexiforest/curl_cffi) (via
[`httpx-curl-cffi`](https://github.com/vgavro/httpx-curl-cffi)) so the TLS/HTTP2
fingerprint looks closer to a real browser. Turn it off or pick a target:

```python
HDRezkaClient()                          # impersonate=True → Chrome
HDRezkaClient(impersonate='safari')      # specific curl_cffi target
HDRezkaClient(impersonate=False)         # plain httpx + browser-like headers
```

This only helps with fingerprint checks. JavaScript challenges (the “Just a
moment…” interstitial), bad IPs, and account/policy limits are out of scope.
Prefer a clean residential / personal path over free shared VPNs when the
service blocks you. You can also paste browser cookies into the client after
solving a challenge manually.

## Navigation, filters, and sidebar

These parsers read whatever the HTML currently contains (no hardcoded genre lists).
Missing blocks return empty structures.

### Navbar

```python
nav = await client.navbar()
for item in nav.items:
    print(item.name, item.url, item.single)
    if item.submenu:
        for genre in item.submenu.genres:
            print(' ', genre.name, genre.url)
        for collection in item.submenu.collections:
            print(' ', collection.name, collection.url, collection.classes)
        if item.submenu.find_best:
            print(' ', item.submenu.find_best.categories, item.submenu.find_best.years)
```

You can also parse from any page: `await client.page().get_navbar()`, or call
`parse_navbar(html)` on a saved HTML string / BeautifulSoup tree.

### Catalog filters and sorting

On category/genre pages, `ul.b-content__main_filters` exposes sort links (`filter=`)
and content-type links (`genre=`).

```python
page = client.page('/films/')
filters = await page.get_filters()
for link in filters.sorts:
    print(link.name, link.param, link.value, link.active)
for link in filters.types:
    print(link.name, link.param, link.value, link.active)

# Apply a filter when fetching titles
items = await page.get_page(1, filter='popular', genre=1)

# Or get titles + filters + updates in one response
content = await page.get_page_content(1, filter='watching')
print(content.filters, content.series_updates)
```

### Series updates (sidebar)

```python
for block in await client.series_updates():
    print(block.date)
    for row in block.items:
        print(row.name, row.url, row.season, row.episode, row.translation)
```

### Post schedule

Many series pages include `.b-post__schedule` tables (air dates per season).
They are parsed when you await a `Post`:

```python
post = await client.post(url)
for block in post.schedule:
    print(block.title)
    for row in block.items:
        print(row.season, row.episode, row.title, row.date, row.exists)
```

Or call `parse_schedule(html)` on saved HTML / a BeautifulSoup tree.

## Favorites (collections)

Favorites require `login`. Mutations go through `POST /ajax/favorites/`; browsing
uses the normal `Page` flow on `/favorites/[id]`.

```python
async with HDRezkaClient() as client:
    await client.login(email, password)

    # Create a collection in the profile
    created = await client.add_favorites_cat('Смотрю')
    cat_id = created['id']

    # Rename
    await client.rename_favorites_cat(cat_id, 'Не буду смотреть')

    # Add a title (post id from Post / Player)
    post = await client.post('series/thriller/646-vo-vse-tyazhkie-2008.html')
    await client.add_favorites_post(post.id, cat_id)

    # Browse the collection (same pagination / InlineItem API as other pages)
    fav = client.favorites(cat_id)
    titles = await fav.get_page(1)
    cats = await fav.get_cats()  # a.b-favorites_content__cats_list_link
    for cat in cats:
        print(cat.id, cat.name, cat.count, cat.url)

    # Remove the collection
    await client.remove_favorites_cat(cat_id)
```

Equivalent low-level calls: `client.ajax.add_favorites_cat`,
`rename_favorites_cat`, `add_favorites_post`, `remove_favorites_cat`.

## Login and mirrors

Create an HDRezka account only if the service allows it for your case. If
registration is disabled, try the flows they document (for example social login
and setting a password). Premium and third-party clients are governed by their
rules, not by this library.

`login` requests the standby URL (default `https://standby-rezka.tv/`), follows the
redirect to an active mirror, posts credentials to `/ajax/login/`, updates
`client.host`, and stores cookies on that client only.

You can set a mirror without login:

```python
client = HDRezkaClient(host='https://hdrezka.tv/')
```

Many domains are behind Cloudflare; see
[mirrors.txt](https://github.com/ndenissov/HDRezka/blob/main/mirrors.txt).
Working access depends on mirror choice, IP reputation, and impersonation — not
on the library claiming any special relationship with the site.

Default constants (not shared session state):

```python
from hdrezka.url import Request, DEFAULT_HOST, DEFAULT_REDIRECT_URL
```

## Proxies

```python
from hdrezka import HDRezkaClient

client = HDRezkaClient(proxy='socks5://localhost:9050')
```

Or pass your own `httpx.AsyncClient` as `http_client=` (the library will not close it).

## Migration from 4.x

Version 5 removes process-global session state.

| 4.x                                         | 5.x                                                              |
|---------------------------------------------|------------------------------------------------------------------|
| `await login_global(email, password)`       | `await client.login(email, password)`                            |
| `DEFAULT_CLIENT` / `DEFAULT_REQUEST_KWARGS` | `HDRezkaClient(...)` / `request_kwargs=`                         |
| Mutating `Request.HOST` for the process     | `client.host` or `HDRezkaClient(host=...)`                       |
| `Search('query')`                           | `client.search('query')` or `Search('query', client=client)`     |
| `await Player(url)`                         | `await client.player(url)` or `await Player(url, client=client)` |
| `AJAX.get_stream(...)` (classmethods)       | `client.ajax.get_stream(...)`                                    |

Parsing and stream logic are unchanged; wire everything through a client.

## Documentation

- API reference: [ndenissov.github.io/HDRezka](https://ndenissov.github.io/HDRezka/)
- Changelog: [CHANGELOG.md](CHANGELOG.md)

