Auto-fill ČSFD links on import, rename in pool, multi-country tags, Filmotéka layout

This commit is contained in:
2026-06-15 17:31:52 +02:00
parent 86c689b9f1
commit b3a61f9e86
18 changed files with 1407 additions and 168 deletions
+121 -3
View File
@@ -17,6 +17,10 @@ from src.core.csfd import (
_extract_origin_info,
_check_dependencies,
_solve_anubis_pow,
_split_countries,
rating_band,
clean_filename_to_query,
find_csfd_url,
)
@@ -87,7 +91,7 @@ class TestCSFDMovie:
rating=85,
rating_count=1000,
duration=120,
country="Česko",
countries=["Česko"],
poster_url="https://image.example.com/poster.jpg",
plot="A test movie.",
csfd_id=123
@@ -96,6 +100,7 @@ class TestCSFDMovie:
assert movie.genres == ["Drama", "Thriller"]
assert movie.rating == 85
assert movie.duration == 120
assert movie.countries == ["Česko"]
assert movie.csfd_id == 123
def test_csfd_movie_str(self):
@@ -145,6 +150,38 @@ class TestHelperFunctions:
"""Test parsing invalid duration."""
assert _parse_duration("") is None
assert _parse_duration("invalid") is None
def test_split_countries_single(self):
"""A single country yields a one-item list."""
assert _split_countries("USA") == ["USA"]
def test_split_countries_multiple(self):
"""Slash-separated co-production countries are split and trimmed."""
assert _split_countries("USA / Velká Británie") == ["USA", "Velká Británie"]
assert _split_countries("Japonsko/USA") == ["Japonsko", "USA"]
def test_split_countries_empty(self):
"""None/empty yields an empty list."""
assert _split_countries(None) == []
assert _split_countries("") == []
def test_from_dict_migrates_legacy_country(self):
"""Legacy cache with a single 'country' string maps to countries list."""
movie = CSFDMovie.from_dict({"title": "X", "country": "USA / Kanada"})
assert movie.countries == ["USA", "Kanada"]
def test_from_dict_uses_countries_when_present(self):
"""New cache with 'countries' is used verbatim."""
movie = CSFDMovie.from_dict({"title": "X", "countries": ["Japonsko", "USA"]})
assert movie.countries == ["Japonsko", "USA"]
def test_rating_band_buckets(self):
"""Rating is bucketed into ten-point bands, top band spans 90100 %."""
assert rating_band(0) == "09 %"
assert rating_band(86) == "8089 %"
assert rating_band(90) == "90100 %"
assert rating_band(95) == "90100 %"
assert rating_band(100) == "90100 %"
assert _parse_duration("PT") is None
@@ -191,7 +228,7 @@ class TestHTMLExtraction:
def test_extract_origin_info(self, soup):
"""Test extracting origin info (comma-separated legacy format)."""
info = _extract_origin_info(soup)
assert info["country"] == "Česko"
assert info["countries"] == ["Česko"]
assert info["year"] == 2020
assert info["duration"] == 120
@@ -204,10 +241,23 @@ class TestHTMLExtraction:
'136 min (Alternativní 131 min)</div>'
)
info = _extract_origin_info(BeautifulSoup(html, "html.parser"))
assert info["country"] == "USA"
assert info["countries"] == ["USA"]
assert info["year"] == 1999
assert info["duration"] == 136
def test_extract_origin_info_multiple_countries(self):
"""A co-production lists several slash-separated countries."""
from bs4 import BeautifulSoup
html = (
'<div class="origin">USA / Velká Británie '
'<span class="bullet"></span><span>2009 </span>'
'<span class="bullet"></span> 166 min</div>'
)
info = _extract_origin_info(BeautifulSoup(html, "html.parser"))
assert info["countries"] == ["USA", "Velká Británie"]
assert info["year"] == 2009
assert info["duration"] == 166
def test_extract_json_ld_year_from_date_created(self):
"""Year is taken from JSON-LD dateCreated when present."""
from bs4 import BeautifulSoup
@@ -220,6 +270,49 @@ class TestHTMLExtraction:
assert data["year"] == 1999
class TestCleanFilenameToQuery:
"""Tests for turning a filename into a ČSFD search query."""
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):
# No real title words before the marker → fall back to the cleaned stem
assert clean_filename_to_query("1080p.mkv") == "1080p"
class TestFindCsfdUrl:
"""Tests for find_csfd_url (search is mocked)."""
def test_returns_first_result_url(self):
from unittest.mock import patch
movies = [
CSFDMovie(title="Matrix", url="https://www.csfd.cz/film/9499-matrix/"),
CSFDMovie(title="Matrix Reloaded", url="https://www.csfd.cz/film/9497-x/"),
]
with patch("src.core.csfd.search_movies", return_value=movies):
assert find_csfd_url("Matrix 1999") == "https://www.csfd.cz/film/9499-matrix/"
def test_returns_none_for_empty_query(self):
assert find_csfd_url(" ") is None
def test_returns_none_when_no_results(self):
from unittest.mock import patch
with patch("src.core.csfd.search_movies", return_value=[]):
assert find_csfd_url("nonexistent film") is None
class TestFetchMovie:
"""Tests for fetch_movie function."""
@@ -240,6 +333,31 @@ class TestFetchMovie:
assert "Drama" in movie.genres
session.get.assert_called_once()
@patch("src.core.csfd.requests")
def test_fetch_movie_caps_actors_at_ten(self, mock_requests):
"""Only the first MAX_ACTORS (10) of a long cast are kept."""
import json as _json
actors = [{"@type": "Person", "name": f"Actor {i}"} for i in range(25)]
json_ld = _json.dumps({
"@type": "Movie", "name": "Crowded", "actor": actors,
"director": [{"@type": "Person", "name": "Dir"}],
"aggregateRating": {"ratingValue": 70, "ratingCount": 5},
})
html = f'<html><head><script type="application/ld+json">{json_ld}</script></head></html>'
mock_response = MagicMock()
mock_response.text = html
mock_response.raise_for_status = MagicMock()
session = _mock_session(mock_requests)
session.get.return_value = mock_response
movie = fetch_movie("https://www.csfd.cz/film/1-crowded/")
assert movie.directors == ["Dir"]
assert movie.rating == 70
assert len(movie.actors) == 10
assert movie.actors[0] == "Actor 0"
assert movie.actors[-1] == "Actor 9"
@patch("src.core.csfd.requests")
def test_fetch_movie_network_error(self, mock_requests):
"""Test network error handling."""