Add movie-keyed pool index with multi-version movies, ČSFD title bridge, subtitles and miniDLNA library scoping
This commit is contained in:
@@ -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
|
||||
Reference in New Issue
Block a user