Add movie-keyed pool index with multi-version movies, ČSFD title bridge, subtitles and miniDLNA library scoping

This commit is contained in:
2026-09-13 17:16:48 +02:00
parent c0ad1c1553
commit c399a6aed3
26 changed files with 4635 additions and 221 deletions
+255
View File
@@ -0,0 +1,255 @@
"""Tests for the lean ČSFD title-bridge scraper (`src.core.csfd`)."""
import hashlib
from unittest.mock import patch
import pytest
from src.core import csfd
from src.core.csfd import (
CsfdError,
CsfdFilm,
_extract_csfd_id,
_is_film_href,
_parse_origin,
_parse_original_title,
_solve_anubis_pow,
clean_filename_to_query,
fetch_film,
find_csfd_url,
search_films,
)
# A trimmed ČSFD film page: Czech title in JSON-LD, USA origin, and a
# ``.film-names`` list whose USA-flagged entry is the original title.
FILM_HTML = """
<html><head>
<script type="application/ld+json">
{"@context":"https://schema.org/","@type":"Movie","name":"Vykoupení z věznice Shawshank",
"dateCreated":"1994","duration":"PT142M"}
</script>
</head><body>
<div class="film-header-name">
<h1>Vykoupení z věznice Shawshank</h1>
<ul class="film-names">
<li><img src="//x/us.svg" class="flag" title="USA" alt="USA"/> The Shawshank Redemption
<span class="normal more-name-link"><a class="more">více</a></span></li>
<li class="more-names hidden"><img src="//x/sk.svg" class="flag" title="Slovensko"/>
Vykúpenie z väznice Shawshank</li>
</ul>
</div>
<div class="origin">USA <span class="bullet"></span>
<span>1994 <span class="bullet"></span> </span>142 min</div>
</body></html>
"""
# A Czech film: ČSFD omits the Czech title from ``.film-names`` (it equals the
# <h1>) and lists only a foreign festival title — the original title must fall
# back to the <h1>, not "Cosy Dens".
CZECH_FILM_HTML = """
<html><head>
<script type="application/ld+json">
{"@type":"Movie","name":"Pelíšky","datePublished":"1999-04-01"}
</script>
</head><body>
<div class="film-header-name"><h1>Pelíšky</h1>
<ul class="film-names">
<li><img class="flag" title="angličtina"/> Cosy Dens</li>
<li><img class="flag" title="Slovensko"/> Pelíšky</li>
</ul></div>
<div class="origin">Česko <span class="bullet"></span><span>1999</span></div>
</body></html>
"""
# The real search page groups hits into <section> blocks; a.film-title-name also
# matches series episodes, so search_films must scope to "Filmy" and drop the
# two-segment episode hrefs.
SEARCH_HTML = """
<html><body>
<section><div class="box-header"><h2>Filmy</h2></div>
<a href="/film/2294-vykoupeni-z-veznice-shawshank/prehled/" class="film-title-name">Vykoupení z věznice Shawshank</a>
<a href="/film/248803-film-o-filmu/prehled/" class="film-title-name">Film o filmu</a>
</section>
<section><div class="box-header"><h2>Seriály</h2></div>
<a href="/film/318290-arrow/620181-x/prehled/" class="film-title-name">Arrow - Vykoupení</a>
</section>
</body></html>
"""
class TestCleanFilenameToQuery:
def test_strips_release_tags_and_keeps_year(self):
assert clean_filename_to_query(
"Matrix.1999.1080p.BluRay.x264-GROUP.mkv") == "Matrix 1999"
def test_handles_spaces_and_parens_year(self):
assert clean_filename_to_query(
"Forrest Gump (1994) 2160p HDR.mkv") == "Forrest Gump 1994"
def test_no_year_no_markers(self):
assert clean_filename_to_query("Amelie.mkv") == "Amelie"
def test_underscores_and_resolution(self):
assert clean_filename_to_query("Sam_doma_720p.mkv") == "Sam doma"
def test_falls_back_to_stem_when_starting_with_marker(self):
assert clean_filename_to_query("1080p.mkv") == "1080p"
class TestExtractCsfdId:
def test_extract_from_various_urls(self):
assert _extract_csfd_id(
"https://www.csfd.cz/film/2294-vykoupeni/prehled/") == 2294
assert _extract_csfd_id("/film/999/") == 999
def test_none_when_absent(self):
assert _extract_csfd_id("https://www.csfd.cz/") is None
class TestAnubisPoW:
def test_solve_difficulty_one_gives_one_leading_zero_nibble(self):
hash_hex, nonce, _ = _solve_anubis_pow("abc123", difficulty=1)
assert hash_hex[0] == "0"
assert hashlib.sha256(f"abc123{nonce}".encode()).hexdigest() == hash_hex
def test_solve_difficulty_two_gives_leading_zero_byte(self):
hash_hex, _, _ = _solve_anubis_pow("seed", difficulty=2)
assert hash_hex[:2] == "00"
class TestParseOrigin:
def test_country_and_year_from_bullet_format(self):
from bs4 import BeautifulSoup
soup = BeautifulSoup(FILM_HTML, "html.parser")
assert _parse_origin(soup) == ("USA", "1994")
def test_first_country_of_coproduction(self):
from bs4 import BeautifulSoup
html = ('<div class="origin">Francie / Německo <span class="bullet"></span>'
'<span>2001</span> 120 min</div>')
soup = BeautifulSoup(html, "html.parser")
assert _parse_origin(soup) == ("Francie", "2001")
class TestParseOriginalTitle:
def test_picks_name_matching_origin_country(self):
from bs4 import BeautifulSoup
soup = BeautifulSoup(FILM_HTML, "html.parser")
assert _parse_original_title(soup, "USA") == "The Shawshank Redemption"
def test_none_without_country_match_so_caller_uses_czech_title(self):
# A Czech film lists only foreign/festival alt-titles here — none match
# the origin country, so we must NOT return one of them.
from bs4 import BeautifulSoup
html = ('<ul class="film-names">'
'<li><img class="flag" title="angličtina"/> Loners</li>'
'<li><img class="flag" title="Slovinsko"/> Samotarji</li></ul>')
soup = BeautifulSoup(html, "html.parser")
assert _parse_original_title(soup, "Česko") is None
def test_cesko_matches_ceskoslovensko_flag(self):
from bs4 import BeautifulSoup
html = ('<ul class="film-names">'
'<li><img class="flag" title="Československo"/> Ostře sledované vlaky</li>'
'</ul>')
soup = BeautifulSoup(html, "html.parser")
assert _parse_original_title(soup, "Česko") == "Ostře sledované vlaky"
def test_none_when_no_names(self):
from bs4 import BeautifulSoup
soup = BeautifulSoup("<div>x</div>", "html.parser")
assert _parse_original_title(soup, "USA") is None
class TestFetchFilm:
def test_parses_czech_and_original_title_and_year(self):
class _Resp:
text = FILM_HTML
with patch("src.core.csfd._get_page", return_value=_Resp()):
film = fetch_film("https://www.csfd.cz/film/2294-x/")
assert film.czech_title == "Vykoupení z věznice Shawshank"
assert film.original_title == "The Shawshank Redemption"
assert film.year == "1994"
assert film.csfd_id == 2294
assert film.tmdb_query == ("The Shawshank Redemption", "1994")
def test_czech_film_original_title_falls_back_to_czech(self):
class _Resp:
text = CZECH_FILM_HTML
with patch("src.core.csfd._get_page", return_value=_Resp()):
film = fetch_film("https://www.csfd.cz/film/15387-pelisky/")
assert film.czech_title == "Pelíšky"
assert film.original_title == "Pelíšky"
assert film.year == "1999"
assert film.tmdb_query == ("Pelíšky", "1999")
def test_wraps_network_error_in_csfd_error(self):
import requests
with patch("src.core.csfd._get_page",
side_effect=requests.RequestException("boom")):
with pytest.raises(CsfdError):
fetch_film("https://www.csfd.cz/film/1/")
class TestIsFilmHref:
def test_accepts_single_segment_film(self):
assert _is_film_href("/film/2294-vykoupeni/prehled/") is True
assert _is_film_href("/film/2294-vykoupeni/") is True
def test_rejects_series_episode(self):
assert _is_film_href("/film/318290-arrow/620181-vykoupeni/prehled/") is False
def test_rejects_non_film(self):
assert _is_film_href("/tvurce/123-nekdo/") is False
class TestSearchFilms:
def test_scopes_to_films_section_and_drops_episodes(self):
class _Resp:
text = SEARCH_HTML
with patch("src.core.csfd._get_page", return_value=_Resp()):
films = search_films("shawshank", limit=5)
# the Seriály-section episode link is excluded
assert [f.csfd_id for f in films] == [2294, 248803]
assert films[0].url == (
"https://www.csfd.cz/film/2294-vykoupeni-z-veznice-shawshank/prehled/")
def test_empty_query_returns_empty(self):
assert search_films(" ") == []
class TestFindCsfdUrl:
def test_returns_first_hit_url(self):
hit = CsfdFilm(url="https://www.csfd.cz/film/2294-x/", csfd_id=2294)
with patch("src.core.csfd.search_films", return_value=[hit]):
assert find_csfd_url("shawshank 1994") == "https://www.csfd.cz/film/2294-x/"
def test_retries_without_trailing_year_when_first_query_misses(self):
hit = CsfdFilm(url="https://www.csfd.cz/film/221006-vratne-lahve/", csfd_id=221006)
calls: list[str] = []
def fake_search(query, limit=1, session=None):
calls.append(query)
return [] if query.endswith("2006") else [hit]
with patch("src.core.csfd.search_films", side_effect=fake_search):
assert find_csfd_url("Vratne lahve 2006") == hit.url
assert calls == ["Vratne lahve 2006", "Vratne lahve"]
def test_none_for_blank_query(self):
assert find_csfd_url(" ") is None
def test_none_when_no_results(self):
with patch("src.core.csfd.search_films", return_value=[]):
assert find_csfd_url("nic") is None
class TestDependencies:
def test_dependencies_available_in_test_env(self):
assert csfd.dependencies_available() is True