Files
Curator/docs/modules/core.csfd.md
T

6.1 KiB

src/core/csfd.py

ČSFD.cz scraper — a title bridge to TMDb.

Curator identifies movies by TMDb id. A direct TMDb search on a Czech distribution title is unreliable (remakes, localized names, "weird" hits). ČSFD, on the other hand, indexes Czech release names well and lists a film's original title. So the bridge is::

filename → ČSFD search → ČSFD film page → original title + year
         → TMDb search → tmdbid

This module only scrapes ČSFD; the TMDb half lives in src.core.file_manager (FileManager.resolve_via_csfd). Nothing here feeds tags/genres — those still come from TMDb via Jellyfin/NFO.

ČSFD serves an Anubis proof-of-work wall in front of every page: a plain request gets a 200 with a JS challenge instead of the movie. We detect it, solve the SHA-256 PoW the way the bundled worker JS does, and replay the request through a shared requests.Session (so a batch pays the PoW cost only once).

Source: src/core/csfd.py

Classes

CsfdError

class · line 62

class CsfdError(RuntimeError):

A ČSFD lookup could not be completed (deps missing, network, anti-bot).

CsfdFilm

class · line 67

class CsfdFilm:

The slice of a ČSFD film page Curator needs to reach a TMDb id.

CsfdFilm.tmdb_query

property · line 77

tmdb_query -> tuple[str, Optional[str]]

(title, year) best suited for a TMDb search — original title first.

Functions

dependencies_available

function · line 82

dependencies_available() -> bool

True when requests + beautifulsoup4 are importable.

_check_dependencies

function · line 87

_check_dependencies() -> None

Raise CsfdError when the optional requests / beautifulsoup4 packages are missing (they are needed only for ČSFD scraping).

_extract_csfd_id

function · line 95

_extract_csfd_id(url: str) -> Optional[int]

Pull the numeric film id out of a ČSFD URL, or None.

_extract_json_blob

function · line 105

_extract_json_blob(html: str, element_id: str)

Return the parsed JSON from an Anubis <script id=...> blob, or None.

_solve_anubis_pow

function · line 120

_solve_anubis_pow(random_data: str, difficulty: int) -> tuple[str, int, int]

Brute-force the Anubis proof-of-work.

Mirrors the bundled sha256-purejs worker: find the smallest nonce such that sha256(random_data + str(nonce)) has difficulty leading zero nibbles. Returns (hash_hex, nonce, elapsed_ms).

_solve_anubis_challenge

function · line 143

_solve_anubis_challenge(session, html: str, url: str)

Solve the Anubis challenge in html and return the real page response.

_get_page

function · line 180

_get_page(session, url: str)

GET url through session, transparently clearing an Anubis wall.

_parse_json_ld

function · line 193

_parse_json_ld(soup) -> tuple[str, Optional[str]]

(name, year) from the film page's JSON-LD (name is the Czech title).

_parse_origin

function · line 215

_parse_origin(soup) -> tuple[Optional[str], Optional[str]]

(origin_country, year) from the .origin line.

ČSFD separates values with inline bullet <span> elements (no commas), e.g. USA • 1999 • 136 min. For a co-production only the first country is returned — enough to match it against a name's flag.

_parse_original_title

function · line 245

_parse_original_title(soup, origin_country: Optional[str]) -> Optional[str]

The film's original title from .film-names — or None to use the CZ title.

Each <li> carries a country/language flag (img.flag title) and a title. The original title is the entry whose flag matches the country of origin (USA → "The Shawshank Redemption"). ČSFD does not repeat the Czech title here when it equals the <h1>, so a Czech film has only foreign/festival alt-titles in this list ([angličtina] Cosy Dens, [Slovinsko] Samotarji) — picking one of those would be wrong, so we return None and let the caller fall back to the Czech <h1> title.

fetch_film

function · line 276

fetch_film(url: str, session=None) -> CsfdFilm

Fetch a ČSFD film page → CsfdFilm (Czech + original title, year).

Pass a shared requests.Session to reuse the Anubis auth cookie across a batch. Raises CsfdError on a network / anti-bot / parse failure.

_films_section

function · line 311

_films_section(soup)

The <section> holding the Filmy results, or the whole document.

ČSFD's search page groups hits into Filmy / Seriály / Tvůrci / Uživatelé sections; a.film-title-name matches in all of them, so without scoping a bare-title search happily returns a series episode.

_is_film_href

function · line 325

_is_film_href(href: str) -> bool

True for a film URL /film/<id>-slug/ — a series episode has two segments (/film/<series>/<episode>/) and is rejected.

search_films

function · line 332

search_films(query: str, limit: int=5, session=None) -> list[CsfdFilm]

Search ČSFD for query; film hits only (no series/episodes), best first.

find_csfd_url

function · line 365

find_csfd_url(query: str, session=None) -> Optional[str]

Return the first ČSFD film URL matching query, or None.

ČSFD's full-text search treats a trailing year as a search term (not a filter) and can miss on "Title 2006" while "Title" hits — so a yearful query with no result is retried without the year.

clean_filename_to_query

function · line 397

clean_filename_to_query(filename: str) -> str

Turn a (possibly release-named) filename into a ČSFD search query.

Strips the path/extension, splits on common separators and keeps the words before the first release marker (year, resolution, codec, source, …). The detected year is appended back as a disambiguator::

"Matrix.1999.1080p.BluRay.x264-GROUP.mkv" -> "Matrix 1999"