Async Python client for HDRezka. Designed for scripts and for multi-user backends
(one HDRezkaClient instance per user / request scope).
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), 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.
pip install hdrezkaSocks proxy support:
pip install hdrezka[socks]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 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).
By default HDRezkaClient sends browser-like headers and uses
curl_cffi (via
httpx-curl-cffi) so the TLS/HTTP2
fingerprint looks closer to a real browser. Turn it off or pick a target:
HDRezkaClient() # impersonate=True → Chrome
HDRezkaClient(impersonate='safari') # specific curl_cffi target
HDRezkaClient(impersonate=False) # plain httpx + browser-like headersThis 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.
These parsers read whatever the HTML currently contains (no hardcoded genre lists). Missing blocks return empty structures.
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.
On category/genre pages, ul.b-content__main_filters exposes sort links (filter=)
and content-type links (genre=).
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)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)Many series pages include .b-post__schedule tables (air dates per season).
They are parsed when you await a Post:
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 require login. Mutations go through POST /ajax/favorites/; browsing
uses the normal Page flow on /favorites/[id].
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.
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:
client = HDRezkaClient(host='https://hdrezka.tv/')Many domains are behind Cloudflare; see 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):
from hdrezka.url import Request, DEFAULT_HOST, DEFAULT_REDIRECT_URLfrom hdrezka import HDRezkaClient
client = HDRezkaClient(proxy='socks5://localhost:9050')Or pass your own httpx.AsyncClient as http_client= (the library will not close it).
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.
- API reference: ndenissov.github.io/HDRezka
- Changelog: CHANGELOG.md