CSFD integration

This commit is contained in:
2025-12-30 07:54:30 +01:00
parent 028c6606e0
commit 47b39aadfe
20 changed files with 2597 additions and 129 deletions

View File

@@ -4,8 +4,7 @@ from pathlib import Path
from src.core.config import (
load_global_config, save_global_config, DEFAULT_GLOBAL_CONFIG,
load_folder_config, save_folder_config, DEFAULT_FOLDER_CONFIG,
get_folder_config_path, folder_has_config, FOLDER_CONFIG_NAME,
load_config, save_config # Legacy functions
get_folder_config_path, folder_has_config, FOLDER_CONFIG_NAME
)
@@ -266,36 +265,6 @@ class TestFolderConfig:
assert loaded2["ignore_patterns"] == ["*.jpg"]
class TestLegacyFunctions:
"""Testy pro zpětnou kompatibilitu"""
@pytest.fixture
def temp_global_config(self, tmp_path, monkeypatch):
"""Fixture pro dočasný globální config soubor"""
config_path = tmp_path / "config.json"
import src.core.config as config_module
monkeypatch.setattr(config_module, 'GLOBAL_CONFIG_FILE', config_path)
return config_path
def test_load_config_legacy(self, temp_global_config):
"""Test že load_config funguje jako alias pro load_global_config"""
test_config = {**DEFAULT_GLOBAL_CONFIG, "last_folder": "/test"}
save_global_config(test_config)
loaded = load_config()
assert loaded["last_folder"] == "/test"
def test_save_config_legacy(self, temp_global_config):
"""Test že save_config funguje jako alias pro save_global_config"""
test_config = {**DEFAULT_GLOBAL_CONFIG, "last_folder": "/legacy"}
save_config(test_config)
loaded = load_global_config()
assert loaded["last_folder"] == "/legacy"
class TestConfigEdgeCases:
"""Testy pro edge cases"""

262
tests/test_csfd.py Normal file
View File

@@ -0,0 +1,262 @@
"""Tests for CSFD.cz scraper module."""
import pytest
from unittest.mock import patch, MagicMock
from src.core.csfd import (
CSFDMovie,
fetch_movie,
search_movies,
fetch_movie_by_id,
_extract_csfd_id,
_parse_duration,
_extract_json_ld,
_extract_rating,
_extract_poster,
_extract_plot,
_extract_genres,
_extract_origin_info,
_check_dependencies,
)
# Sample HTML for testing
SAMPLE_JSON_LD = """
{
"@type": "Movie",
"name": "Test Movie",
"director": [{"@type": "Person", "name": "Test Director"}],
"actor": [{"@type": "Person", "name": "Actor 1"}, {"@type": "Person", "name": "Actor 2"}],
"aggregateRating": {"ratingValue": 85.5, "ratingCount": 1000},
"duration": "PT120M",
"description": "A test movie description."
}
"""
SAMPLE_HTML = """
<html>
<head>
<script type="application/ld+json">%s</script>
</head>
<body>
<div class="film-rating-average">85%%</div>
<div class="genres">
<a href="/zanry/1/">Drama</a> /
<a href="/zanry/2/">Thriller</a>
</div>
<div class="origin">Česko, 2020, 120 min</div>
<div class="film-poster">
<img src="//image.example.com/poster.jpg">
</div>
<div class="plot-full"><p>Full plot description.</p></div>
</body>
</html>
""" % SAMPLE_JSON_LD
class TestCSFDMovie:
"""Tests for CSFDMovie dataclass."""
def test_csfd_movie_basic(self):
"""Test basic CSFDMovie creation."""
movie = CSFDMovie(title="Test", url="https://csfd.cz/film/123/")
assert movie.title == "Test"
assert movie.url == "https://csfd.cz/film/123/"
assert movie.year is None
assert movie.genres == []
assert movie.rating is None
def test_csfd_movie_full(self):
"""Test CSFDMovie with all fields."""
movie = CSFDMovie(
title="Test Movie",
url="https://csfd.cz/film/123/",
year=2020,
genres=["Drama", "Thriller"],
directors=["Director 1"],
actors=["Actor 1", "Actor 2"],
rating=85,
rating_count=1000,
duration=120,
country="Česko",
poster_url="https://image.example.com/poster.jpg",
plot="A test movie.",
csfd_id=123
)
assert movie.year == 2020
assert movie.genres == ["Drama", "Thriller"]
assert movie.rating == 85
assert movie.duration == 120
assert movie.csfd_id == 123
def test_csfd_movie_str(self):
"""Test CSFDMovie string representation."""
movie = CSFDMovie(
title="Test Movie",
url="https://csfd.cz/film/123/",
year=2020,
genres=["Drama"],
directors=["Director 1"],
rating=85
)
s = str(movie)
assert "Test Movie (2020)" in s
assert "85%" in s
assert "Drama" in s
assert "Director 1" in s
def test_csfd_movie_str_minimal(self):
"""Test CSFDMovie string with minimal data."""
movie = CSFDMovie(title="Test", url="https://csfd.cz/film/123/")
s = str(movie)
assert "Test" in s
class TestHelperFunctions:
"""Tests for helper functions."""
def test_extract_csfd_id_valid(self):
"""Test extracting CSFD ID from valid URL."""
assert _extract_csfd_id("https://www.csfd.cz/film/9423-pane-vy-jste-vdova/") == 9423
assert _extract_csfd_id("https://www.csfd.cz/film/123456/") == 123456
assert _extract_csfd_id("/film/999/prehled/") == 999
def test_extract_csfd_id_invalid(self):
"""Test extracting CSFD ID from invalid URL."""
assert _extract_csfd_id("https://www.csfd.cz/") is None
assert _extract_csfd_id("not-a-url") is None
def test_parse_duration_valid(self):
"""Test parsing ISO 8601 duration."""
assert _parse_duration("PT97M") == 97
assert _parse_duration("PT120M") == 120
assert _parse_duration("PT60M") == 60
def test_parse_duration_invalid(self):
"""Test parsing invalid duration."""
assert _parse_duration("") is None
assert _parse_duration("invalid") is None
assert _parse_duration("PT") is None
class TestHTMLExtraction:
"""Tests for HTML extraction functions."""
@pytest.fixture
def soup(self):
"""Create BeautifulSoup object from sample HTML."""
from bs4 import BeautifulSoup
return BeautifulSoup(SAMPLE_HTML, "html.parser")
def test_extract_json_ld(self, soup):
"""Test extracting data from JSON-LD."""
data = _extract_json_ld(soup)
assert data["title"] == "Test Movie"
assert data["directors"] == ["Test Director"]
assert data["actors"] == ["Actor 1", "Actor 2"]
assert data["rating"] == 86 # Rounded from 85.5
assert data["rating_count"] == 1000
assert data["duration"] == 120
def test_extract_rating(self, soup):
"""Test extracting rating from HTML."""
rating = _extract_rating(soup)
assert rating == 85
def test_extract_genres(self, soup):
"""Test extracting genres from HTML."""
genres = _extract_genres(soup)
assert "Drama" in genres
assert "Thriller" in genres
def test_extract_poster(self, soup):
"""Test extracting poster URL."""
poster = _extract_poster(soup)
assert poster == "https://image.example.com/poster.jpg"
def test_extract_plot(self, soup):
"""Test extracting plot."""
plot = _extract_plot(soup)
assert plot == "Full plot description."
def test_extract_origin_info(self, soup):
"""Test extracting origin info."""
info = _extract_origin_info(soup)
assert info["country"] == "Česko"
assert info["year"] == 2020
assert info["duration"] == 120
class TestFetchMovie:
"""Tests for fetch_movie function."""
@patch("src.core.csfd.requests")
def test_fetch_movie_success(self, mock_requests):
"""Test successful movie fetch."""
mock_response = MagicMock()
mock_response.text = SAMPLE_HTML
mock_response.raise_for_status = MagicMock()
mock_requests.get.return_value = mock_response
movie = fetch_movie("https://www.csfd.cz/film/123-test/")
assert movie.title == "Test Movie"
assert movie.csfd_id == 123
assert movie.rating == 86
assert "Drama" in movie.genres
mock_requests.get.assert_called_once()
@patch("src.core.csfd.requests")
def test_fetch_movie_network_error(self, mock_requests):
"""Test network error handling."""
import requests as real_requests
mock_requests.get.side_effect = real_requests.RequestException("Network error")
with pytest.raises(real_requests.RequestException):
fetch_movie("https://www.csfd.cz/film/123/")
class TestSearchMovies:
"""Tests for search_movies function."""
@patch("src.core.csfd.requests")
def test_search_movies(self, mock_requests):
"""Test movie search."""
search_html = """
<html><body>
<a href="/film/123-test/" class="film-title-name">Test Movie</a>
<a href="/film/456-another/" class="film-title-name">Another Movie</a>
</body></html>
"""
mock_response = MagicMock()
mock_response.text = search_html
mock_response.raise_for_status = MagicMock()
mock_requests.get.return_value = mock_response
mock_requests.utils.quote = lambda x: x
results = search_movies("test", limit=10)
assert len(results) >= 1
assert any(m.csfd_id == 123 for m in results)
class TestFetchMovieById:
"""Tests for fetch_movie_by_id function."""
@patch("src.core.csfd.fetch_movie")
def test_fetch_by_id(self, mock_fetch):
"""Test fetching movie by ID."""
mock_fetch.return_value = CSFDMovie(title="Test", url="https://csfd.cz/film/9423/")
movie = fetch_movie_by_id(9423)
mock_fetch.assert_called_once_with("https://www.csfd.cz/film/9423/")
assert movie.title == "Test"
class TestDependencyCheck:
"""Tests for dependency checking."""
def test_dependencies_available(self):
"""Test that dependencies are available (they should be in test env)."""
# Should not raise
_check_dependencies()

View File

@@ -263,3 +263,155 @@ class TestFile:
tag_paths2 = {tag.full_path for tag in file_obj2.tags}
assert tag_paths == tag_paths2
assert file_obj2.date == "2025-01-01"
class TestFileCSFDIntegration:
"""Testy pro CSFD integraci v File"""
@pytest.fixture
def temp_dir(self, tmp_path):
return tmp_path
@pytest.fixture
def tag_manager(self):
return TagManager()
@pytest.fixture
def test_file(self, temp_dir):
test_file = temp_dir / "film.mkv"
test_file.write_text("video content")
return test_file
def test_file_csfd_url_initial(self, test_file, tag_manager):
"""Test že csfd_url je None při vytvoření"""
file_obj = File(test_file, tag_manager)
assert file_obj.csfd_url is None
def test_file_set_csfd_url(self, test_file, tag_manager):
"""Test nastavení CSFD URL"""
file_obj = File(test_file, tag_manager)
file_obj.set_csfd_url("https://www.csfd.cz/film/9423-pane-vy-jste-vdova/")
assert file_obj.csfd_url == "https://www.csfd.cz/film/9423-pane-vy-jste-vdova/"
def test_file_set_csfd_url_persistence(self, test_file, tag_manager):
"""Test že CSFD URL přežije reload"""
file_obj = File(test_file, tag_manager)
file_obj.set_csfd_url("https://www.csfd.cz/film/123/")
file_obj2 = File(test_file, tag_manager)
assert file_obj2.csfd_url == "https://www.csfd.cz/film/123/"
def test_file_set_csfd_url_none(self, test_file, tag_manager):
"""Test smazání CSFD URL"""
file_obj = File(test_file, tag_manager)
file_obj.set_csfd_url("https://www.csfd.cz/film/123/")
file_obj.set_csfd_url(None)
assert file_obj.csfd_url is None
def test_file_set_csfd_url_empty(self, test_file, tag_manager):
"""Test nastavení prázdného řetězce jako CSFD URL"""
file_obj = File(test_file, tag_manager)
file_obj.set_csfd_url("https://www.csfd.cz/film/123/")
file_obj.set_csfd_url("")
assert file_obj.csfd_url is None
def test_file_csfd_url_in_metadata(self, test_file, tag_manager):
"""Test že CSFD URL je uložena v metadatech"""
file_obj = File(test_file, tag_manager)
file_obj.set_csfd_url("https://www.csfd.cz/film/999/")
import json
with open(file_obj.metadata_filename, "r", encoding="utf-8") as f:
data = json.load(f)
assert data["csfd_url"] == "https://www.csfd.cz/film/999/"
def test_apply_csfd_tags_no_url(self, test_file, tag_manager):
"""Test apply_csfd_tags bez nastaveného URL"""
file_obj = File(test_file, tag_manager)
result = file_obj.apply_csfd_tags()
assert result["success"] is False
assert "URL není nastavena" in result["error"]
assert result["tags_added"] == []
@pytest.fixture
def mock_csfd_movie(self):
"""Mock CSFDMovie pro testování"""
from unittest.mock import MagicMock
movie = MagicMock()
movie.title = "Test Film"
movie.year = 2020
movie.genres = ["Komedie", "Drama"]
movie.country = "Česko"
movie.rating = 85
return movie
def test_apply_csfd_tags_success(self, test_file, tag_manager, mock_csfd_movie):
"""Test úspěšného načtení tagů z CSFD"""
from unittest.mock import patch
file_obj = File(test_file, tag_manager)
file_obj.set_csfd_url("https://www.csfd.cz/film/123/")
with patch("src.core.csfd.fetch_movie", return_value=mock_csfd_movie):
result = file_obj.apply_csfd_tags()
assert result["success"] is True
assert "Žánr/Komedie" in result["tags_added"]
assert "Žánr/Drama" in result["tags_added"]
assert "Rok/2020" in result["tags_added"]
assert "Země/Česko" in result["tags_added"]
# Kontrola že tagy jsou opravdu přidány
tag_paths = {tag.full_path for tag in file_obj.tags}
assert "Žánr/Komedie" in tag_paths
assert "Žánr/Drama" in tag_paths
assert "Rok/2020" in tag_paths
assert "Země/Česko" in tag_paths
def test_apply_csfd_tags_genres_only(self, test_file, tag_manager, mock_csfd_movie):
"""Test načtení pouze žánrů"""
from unittest.mock import patch
file_obj = File(test_file, tag_manager)
file_obj.set_csfd_url("https://www.csfd.cz/film/123/")
with patch("src.core.csfd.fetch_movie", return_value=mock_csfd_movie):
result = file_obj.apply_csfd_tags(add_genres=True, add_year=False, add_country=False)
assert result["success"] is True
assert "Žánr/Komedie" in result["tags_added"]
assert "Rok/2020" not in result["tags_added"]
assert "Země/Česko" not in result["tags_added"]
def test_apply_csfd_tags_no_duplicate(self, test_file, tag_manager, mock_csfd_movie):
"""Test že duplicitní tagy nejsou přidány"""
from unittest.mock import patch
file_obj = File(test_file, tag_manager)
file_obj.set_csfd_url("https://www.csfd.cz/film/123/")
# Přidáme tag ručně
file_obj.add_tag("Žánr/Komedie")
with patch("src.core.csfd.fetch_movie", return_value=mock_csfd_movie):
result = file_obj.apply_csfd_tags()
# Komedie by neměla být v tags_added, protože už existuje
assert "Žánr/Komedie" not in result["tags_added"]
assert "Žánr/Drama" in result["tags_added"]
def test_apply_csfd_tags_network_error(self, test_file, tag_manager):
"""Test chyby při načítání z CSFD"""
from unittest.mock import patch
file_obj = File(test_file, tag_manager)
file_obj.set_csfd_url("https://www.csfd.cz/film/123/")
with patch("src.core.csfd.fetch_movie", side_effect=Exception("Network error")):
result = file_obj.apply_csfd_tags()
assert result["success"] is False
assert "error" in result
assert result["tags_added"] == []

View File

@@ -556,3 +556,433 @@ class TestFileManagerEdgeCases:
filenames = {f.filename for f in file_manager.filelist}
assert "soubor s mezerami.txt" in filenames
assert "čeština.txt" in filenames
class TestFileManagerCloseFolder:
"""Testy pro close_folder metodu"""
@pytest.fixture
def tag_manager(self):
return TagManager()
@pytest.fixture
def temp_global_config(self, tmp_path, monkeypatch):
config_dir = tmp_path / "config"
config_dir.mkdir()
config_path = config_dir / "test_config.json"
import src.core.config as config_module
monkeypatch.setattr(config_module, 'GLOBAL_CONFIG_FILE', config_path)
return config_path
@pytest.fixture
def file_manager(self, tag_manager, temp_global_config):
return FileManager(tag_manager)
@pytest.fixture
def temp_dir(self, tmp_path):
data_dir = tmp_path / "data"
data_dir.mkdir()
(data_dir / "file1.txt").write_text("content1")
(data_dir / "file2.txt").write_text("content2")
return data_dir
def test_close_folder_clears_state(self, file_manager, temp_dir):
"""Test že close_folder vymaže stav"""
file_manager.append(temp_dir)
assert len(file_manager.filelist) == 2
assert file_manager.current_folder == temp_dir
file_manager.close_folder()
assert len(file_manager.filelist) == 0
assert len(file_manager.folders) == 0
assert file_manager.current_folder is None
assert len(file_manager.folder_configs) == 0
def test_close_folder_saves_metadata(self, file_manager, temp_dir):
"""Test že close_folder uloží metadata"""
file_manager.append(temp_dir)
# Find file1.txt specifically
file = next(f for f in file_manager.filelist if f.filename == "file1.txt")
file.add_tag("Test/CloseTag")
file_manager.close_folder()
# Reload file and check tag persists
from src.core.file import File
reloaded = File(temp_dir / "file1.txt", file_manager.tagmanager)
tag_paths = {t.full_path for t in reloaded.tags}
assert "Test/CloseTag" in tag_paths
def test_close_folder_callback(self, file_manager, temp_dir):
"""Test že close_folder volá callback"""
file_manager.append(temp_dir)
callback_calls = []
def callback(filelist):
callback_calls.append(len(filelist))
file_manager.on_files_changed = callback
file_manager.close_folder()
assert len(callback_calls) == 1
assert callback_calls[0] == 0 # Empty list after close
def test_close_folder_no_folder_open(self, file_manager):
"""Test close_folder bez otevřené složky"""
# Should not raise
file_manager.close_folder()
assert file_manager.current_folder is None
def test_close_folder_preserves_global_config(self, file_manager, temp_dir):
"""Test že close_folder zachová global config"""
file_manager.append(temp_dir)
file_manager.global_config["test_key"] = "test_value"
file_manager.close_folder()
assert file_manager.global_config.get("test_key") == "test_value"
class TestFileManagerRenameTag:
"""Testy pro přejmenování tagů v souborech"""
@pytest.fixture
def tag_manager(self):
return TagManager()
@pytest.fixture
def temp_global_config(self, tmp_path, monkeypatch):
config_dir = tmp_path / "config"
config_dir.mkdir()
config_path = config_dir / "test_config.json"
import src.core.config as config_module
monkeypatch.setattr(config_module, 'GLOBAL_CONFIG_FILE', config_path)
return config_path
@pytest.fixture
def file_manager(self, tag_manager, temp_global_config):
return FileManager(tag_manager)
@pytest.fixture
def temp_dir(self, tmp_path):
data_dir = tmp_path / "data"
data_dir.mkdir()
(data_dir / "file1.txt").write_text("content1")
(data_dir / "file2.txt").write_text("content2")
(data_dir / "file3.txt").write_text("content3")
return data_dir
def test_rename_tag_in_files_success(self, file_manager, temp_dir):
"""Test úspěšného přejmenování tagu v souborech"""
file_manager.append(temp_dir)
# Přidat tag dvěma souborům
files_to_tag = file_manager.filelist[:2]
file_manager.assign_tag_to_file_objects(files_to_tag, "Video/HD")
# Přejmenovat tag
updated_count = file_manager.rename_tag_in_files("Video", "HD", "FullHD")
assert updated_count == 2
# Zkontrolovat že tagy jsou přejmenovány
for f in files_to_tag:
tag_paths = {t.full_path for t in f.tags}
assert "Video/FullHD" in tag_paths
assert "Video/HD" not in tag_paths
def test_rename_tag_in_files_persistence(self, file_manager, temp_dir):
"""Test že přejmenovaný tag přežije reload"""
file_manager.append(temp_dir)
file = file_manager.filelist[0]
file_manager.assign_tag_to_file_objects([file], "Video/HD")
file_manager.rename_tag_in_files("Video", "HD", "FullHD")
# Reload soubor
from src.core.file import File
reloaded = File(file.file_path, file_manager.tagmanager)
tag_paths = {t.full_path for t in reloaded.tags}
assert "Video/FullHD" in tag_paths
assert "Video/HD" not in tag_paths
def test_rename_tag_in_files_no_match(self, file_manager, temp_dir):
"""Test přejmenování tagu který žádný soubor nemá"""
file_manager.append(temp_dir)
file_manager.assign_tag_to_file_objects(file_manager.filelist[:1], "Video/HD")
updated_count = file_manager.rename_tag_in_files("Video", "4K", "UHD")
assert updated_count == 0
def test_rename_tag_in_files_nonexistent_category(self, file_manager, temp_dir):
"""Test přejmenování tagu v neexistující kategorii"""
file_manager.append(temp_dir)
updated_count = file_manager.rename_tag_in_files("NonExistent", "Tag", "NewTag")
assert updated_count == 0
def test_rename_tag_in_files_callback(self, file_manager, temp_dir):
"""Test že přejmenování tagu volá callback"""
file_manager.append(temp_dir)
file_manager.assign_tag_to_file_objects(file_manager.filelist[:1], "Video/HD")
callback_calls = []
def callback(filelist):
callback_calls.append(len(filelist))
file_manager.on_files_changed = callback
file_manager.rename_tag_in_files("Video", "HD", "FullHD")
assert len(callback_calls) == 1
def test_rename_tag_preserves_other_tags(self, file_manager, temp_dir):
"""Test že přejmenování jednoho tagu neovlivní ostatní tagy souboru"""
file_manager.append(temp_dir)
file = file_manager.filelist[0]
file_manager.assign_tag_to_file_objects([file], "Video/HD")
file_manager.assign_tag_to_file_objects([file], "Audio/Stereo")
file_manager.assign_tag_to_file_objects([file], "Quality/High")
file_manager.rename_tag_in_files("Video", "HD", "FullHD")
tag_paths = {t.full_path for t in file.tags}
assert "Video/FullHD" in tag_paths
assert "Audio/Stereo" in tag_paths
assert "Quality/High" in tag_paths
def test_rename_category_in_files_success(self, file_manager, temp_dir):
"""Test úspěšného přejmenování kategorie v souborech"""
file_manager.append(temp_dir)
# Přidat tagy ze stejné kategorie
file_manager.assign_tag_to_file_objects(file_manager.filelist[:2], "Video/HD")
file_manager.assign_tag_to_file_objects([file_manager.filelist[0]], "Video/4K")
# Přejmenovat kategorii
updated_count = file_manager.rename_category_in_files("Video", "Rozlišení")
assert updated_count == 2
# Zkontrolovat že tagy mají novou kategorii
file1 = file_manager.filelist[0]
tag_paths = {t.full_path for t in file1.tags}
assert "Rozlišení/HD" in tag_paths
assert "Rozlišení/4K" in tag_paths
assert "Video/HD" not in tag_paths
assert "Video/4K" not in tag_paths
def test_rename_category_in_files_persistence(self, file_manager, temp_dir):
"""Test že přejmenovaná kategorie přežije reload"""
file_manager.append(temp_dir)
file = file_manager.filelist[0]
file_manager.assign_tag_to_file_objects([file], "Video/HD")
file_manager.rename_category_in_files("Video", "Rozlišení")
# Reload soubor
from src.core.file import File
reloaded = File(file.file_path, file_manager.tagmanager)
tag_paths = {t.full_path for t in reloaded.tags}
assert "Rozlišení/HD" in tag_paths
assert "Video/HD" not in tag_paths
def test_rename_category_in_files_no_match(self, file_manager, temp_dir):
"""Test přejmenování kategorie kterou žádný soubor nemá"""
file_manager.append(temp_dir)
file_manager.assign_tag_to_file_objects(file_manager.filelist[:1], "Video/HD")
updated_count = file_manager.rename_category_in_files("Audio", "Sound")
assert updated_count == 0
def test_rename_category_in_files_nonexistent(self, file_manager, temp_dir):
"""Test přejmenování neexistující kategorie"""
file_manager.append(temp_dir)
updated_count = file_manager.rename_category_in_files("NonExistent", "NewName")
assert updated_count == 0
def test_rename_category_preserves_other_categories(self, file_manager, temp_dir):
"""Test že přejmenování kategorie neovlivní jiné kategorie"""
file_manager.append(temp_dir)
file = file_manager.filelist[0]
file_manager.assign_tag_to_file_objects([file], "Video/HD")
file_manager.assign_tag_to_file_objects([file], "Audio/Stereo")
file_manager.rename_category_in_files("Video", "Rozlišení")
tag_paths = {t.full_path for t in file.tags}
assert "Rozlišení/HD" in tag_paths
assert "Audio/Stereo" in tag_paths
class TestFileManagerMergeTag:
"""Testy pro slučování tagů v souborech"""
@pytest.fixture
def tag_manager(self):
return TagManager()
@pytest.fixture
def temp_global_config(self, tmp_path, monkeypatch):
config_dir = tmp_path / "config"
config_dir.mkdir()
config_path = config_dir / "test_config.json"
import src.core.config as config_module
monkeypatch.setattr(config_module, 'GLOBAL_CONFIG_FILE', config_path)
return config_path
@pytest.fixture
def file_manager(self, tag_manager, temp_global_config):
return FileManager(tag_manager)
@pytest.fixture
def temp_dir(self, tmp_path):
data_dir = tmp_path / "data"
data_dir.mkdir()
(data_dir / "file1.txt").write_text("content1")
(data_dir / "file2.txt").write_text("content2")
(data_dir / "file3.txt").write_text("content3")
return data_dir
def test_merge_tag_in_files_success(self, file_manager, temp_dir):
"""Test úspěšného sloučení tagů v souborech"""
file_manager.append(temp_dir)
# Přidat oba tagy - jeden soubor má HD, druhý má FullHD
file_manager.assign_tag_to_file_objects([file_manager.filelist[0]], "Video/HD")
file_manager.assign_tag_to_file_objects([file_manager.filelist[1]], "Video/FullHD")
# Sloučit HD do FullHD
updated_count = file_manager.merge_tag_in_files("Video", "HD", "FullHD")
assert updated_count == 1
# Soubor 0 by měl mít FullHD místo HD
tag_paths = {t.full_path for t in file_manager.filelist[0].tags}
assert "Video/FullHD" in tag_paths
assert "Video/HD" not in tag_paths
def test_merge_tag_in_files_file_has_both(self, file_manager, temp_dir):
"""Test sloučení když soubor má oba tagy"""
file_manager.append(temp_dir)
# Soubor má oba tagy
file = file_manager.filelist[0]
file_manager.assign_tag_to_file_objects([file], "Video/HD")
file_manager.assign_tag_to_file_objects([file], "Video/FullHD")
# Sloučit HD do FullHD - HD by měl být odstraněn
updated_count = file_manager.merge_tag_in_files("Video", "HD", "FullHD")
assert updated_count == 1
tag_paths = {t.full_path for t in file.tags}
assert "Video/FullHD" in tag_paths
assert "Video/HD" not in tag_paths
# FullHD by měl být jen jednou
fullhd_count = sum(1 for t in file.tags if t.full_path == "Video/FullHD")
assert fullhd_count == 1
def test_merge_tag_in_files_persistence(self, file_manager, temp_dir):
"""Test že sloučený tag přežije reload"""
file_manager.append(temp_dir)
file = file_manager.filelist[0]
file_manager.assign_tag_to_file_objects([file], "Video/HD")
file_manager.assign_tag_to_file_objects([file_manager.filelist[1]], "Video/FullHD")
file_manager.merge_tag_in_files("Video", "HD", "FullHD")
# Reload soubor
from src.core.file import File
reloaded = File(file.file_path, file_manager.tagmanager)
tag_paths = {t.full_path for t in reloaded.tags}
assert "Video/FullHD" in tag_paths
assert "Video/HD" not in tag_paths
def test_merge_tag_in_files_no_source(self, file_manager, temp_dir):
"""Test sloučení když žádný soubor nemá zdrojový tag"""
file_manager.append(temp_dir)
file_manager.assign_tag_to_file_objects([file_manager.filelist[0]], "Video/FullHD")
updated_count = file_manager.merge_tag_in_files("Video", "HD", "FullHD")
assert updated_count == 0
def test_merge_tag_preserves_other_tags(self, file_manager, temp_dir):
"""Test že sloučení neovlivní ostatní tagy"""
file_manager.append(temp_dir)
file = file_manager.filelist[0]
file_manager.assign_tag_to_file_objects([file], "Video/HD")
file_manager.assign_tag_to_file_objects([file], "Video/FullHD")
file_manager.assign_tag_to_file_objects([file], "Audio/Stereo")
file_manager.merge_tag_in_files("Video", "HD", "FullHD")
tag_paths = {t.full_path for t in file.tags}
assert "Video/FullHD" in tag_paths
assert "Audio/Stereo" in tag_paths
def test_merge_category_in_files_success(self, file_manager, temp_dir):
"""Test úspěšného sloučení kategorií v souborech"""
file_manager.append(temp_dir)
# Přidat tagy z různých kategorií
file_manager.assign_tag_to_file_objects([file_manager.filelist[0]], "Video/HD")
file_manager.assign_tag_to_file_objects([file_manager.filelist[1]], "Rozlišení/4K")
# Sloučit Video do Rozlišení
updated_count = file_manager.merge_category_in_files("Video", "Rozlišení")
assert updated_count == 1
# Soubor 0 by měl mít Rozlišení/HD místo Video/HD
tag_paths = {t.full_path for t in file_manager.filelist[0].tags}
assert "Rozlišení/HD" in tag_paths
assert "Video/HD" not in tag_paths
def test_merge_category_in_files_persistence(self, file_manager, temp_dir):
"""Test že sloučená kategorie přežije reload"""
file_manager.append(temp_dir)
file = file_manager.filelist[0]
file_manager.assign_tag_to_file_objects([file], "Video/HD")
file_manager.assign_tag_to_file_objects([file_manager.filelist[1]], "Rozlišení/4K")
file_manager.merge_category_in_files("Video", "Rozlišení")
# Reload soubor
from src.core.file import File
reloaded = File(file.file_path, file_manager.tagmanager)
tag_paths = {t.full_path for t in reloaded.tags}
assert "Rozlišení/HD" in tag_paths
assert "Video/HD" not in tag_paths
def test_merge_category_no_source_files(self, file_manager, temp_dir):
"""Test sloučení když žádný soubor nemá zdrojovou kategorii"""
file_manager.append(temp_dir)
file_manager.assign_tag_to_file_objects([file_manager.filelist[0]], "Rozlišení/4K")
updated_count = file_manager.merge_category_in_files("Video", "Rozlišení")
assert updated_count == 0
def test_merge_category_preserves_other_categories(self, file_manager, temp_dir):
"""Test že sloučení kategorie neovlivní jiné kategorie"""
file_manager.append(temp_dir)
file = file_manager.filelist[0]
file_manager.assign_tag_to_file_objects([file], "Video/HD")
file_manager.assign_tag_to_file_objects([file], "Rozlišení/4K")
file_manager.assign_tag_to_file_objects([file], "Audio/Stereo")
file_manager.merge_category_in_files("Video", "Rozlišení")
tag_paths = {t.full_path for t in file.tags}
assert "Rozlišení/HD" in tag_paths
assert "Rozlišení/4K" in tag_paths
assert "Audio/Stereo" in tag_paths
assert "Video/HD" not in tag_paths

View File

@@ -2,7 +2,7 @@ import tempfile
from pathlib import Path
import pytest
from src.core.media_utils import load_icon
from src.ui.utils import load_icon
from PIL import Image, ImageTk
import tkinter as tk

View File

@@ -104,3 +104,44 @@ class TestTag:
assert tag.category == "Kategorie"
assert tag.name == "Čeština"
assert tag.full_path == "Kategorie/Čeština"
class TestTagFromString:
"""Testy pro Tag.from_string() class method"""
def test_from_string_with_category(self):
"""Test parsování stringu s kategorií"""
tag = Tag.from_string("Stav/Nové")
assert tag.category == "Stav"
assert tag.name == "Nové"
def test_from_string_without_category(self):
"""Test parsování stringu bez kategorie - použije default"""
tag = Tag.from_string("simple")
assert tag.category == "default"
assert tag.name == "simple"
def test_from_string_custom_default_category(self):
"""Test parsování s vlastní default kategorií"""
tag = Tag.from_string("simple", default_category="Custom")
assert tag.category == "Custom"
assert tag.name == "simple"
def test_from_string_multiple_slashes(self):
"""Test parsování stringu s více lomítky"""
tag = Tag.from_string("Kategorie/Název/s/lomítky")
assert tag.category == "Kategorie"
assert tag.name == "Název/s/lomítky"
def test_from_string_unicode(self):
"""Test parsování unicode stringu"""
tag = Tag.from_string("Žánr/Komedie")
assert tag.category == "Žánr"
assert tag.name == "Komedie"
def test_from_string_equality(self):
"""Test že from_string vytváří ekvivalentní tag"""
tag1 = Tag("Stav", "Nové")
tag2 = Tag.from_string("Stav/Nové")
assert tag1 == tag2
assert hash(tag1) == hash(tag2)

View File

@@ -325,3 +325,287 @@ class TestDefaultTags:
tm.add_tag("Hodnocení", "Custom Rating")
assert len(tm.get_tags_in_category("Hodnocení")) == initial_count + 1
class TestRenameTag:
"""Testy pro přejmenování tagů a kategorií"""
@pytest.fixture
def tag_manager(self):
return TagManager()
@pytest.fixture
def empty_tag_manager(self):
tm = TagManager()
for category in list(tm.tags_by_category.keys()):
tm.remove_category(category)
return tm
def test_rename_tag_success(self, empty_tag_manager):
"""Test úspěšného přejmenování tagu"""
tm = empty_tag_manager
tm.add_tag("Video", "HD")
new_tag = tm.rename_tag("Video", "HD", "FullHD")
assert new_tag is not None
assert new_tag.name == "FullHD"
assert new_tag.category == "Video"
# Old tag should not exist
tags = tm.get_tags_in_category("Video")
tag_names = [t.name for t in tags]
assert "HD" not in tag_names
assert "FullHD" in tag_names
def test_rename_tag_nonexistent_category(self, empty_tag_manager):
"""Test přejmenování tagu v neexistující kategorii"""
result = empty_tag_manager.rename_tag("Nonexistent", "Tag", "NewTag")
assert result is None
def test_rename_tag_nonexistent_tag(self, empty_tag_manager):
"""Test přejmenování neexistujícího tagu"""
tm = empty_tag_manager
tm.add_tag("Video", "HD")
result = tm.rename_tag("Video", "Nonexistent", "NewTag")
assert result is None
def test_rename_tag_to_existing_name(self, empty_tag_manager):
"""Test přejmenování tagu na existující název"""
tm = empty_tag_manager
tm.add_tag("Video", "HD")
tm.add_tag("Video", "4K")
result = tm.rename_tag("Video", "HD", "4K")
assert result is None
# Original tags should still exist
tags = tm.get_tags_in_category("Video")
tag_names = [t.name for t in tags]
assert "HD" in tag_names
assert "4K" in tag_names
def test_rename_tag_same_name(self, empty_tag_manager):
"""Test přejmenování tagu na stejný název (no-op)"""
tm = empty_tag_manager
tm.add_tag("Video", "HD")
new_tag = tm.rename_tag("Video", "HD", "HD")
# Should succeed but effectively be a no-op
assert new_tag is not None
assert new_tag.name == "HD"
def test_rename_category_success(self, empty_tag_manager):
"""Test úspěšného přejmenování kategorie"""
tm = empty_tag_manager
tm.add_tag("Video", "HD")
tm.add_tag("Video", "4K")
result = tm.rename_category("Video", "Rozlišení")
assert result is True
assert "Video" not in tm.get_categories()
assert "Rozlišení" in tm.get_categories()
# Tags should be moved to new category
tags = tm.get_tags_in_category("Rozlišení")
tag_names = [t.name for t in tags]
assert "HD" in tag_names
assert "4K" in tag_names
def test_rename_category_nonexistent(self, empty_tag_manager):
"""Test přejmenování neexistující kategorie"""
result = empty_tag_manager.rename_category("Nonexistent", "NewName")
assert result is False
def test_rename_category_to_existing_name(self, empty_tag_manager):
"""Test přejmenování kategorie na existující název"""
tm = empty_tag_manager
tm.add_tag("Video", "HD")
tm.add_tag("Audio", "MP3")
result = tm.rename_category("Video", "Audio")
assert result is False
# Original categories should still exist
assert "Video" in tm.get_categories()
assert "Audio" in tm.get_categories()
def test_rename_category_same_name(self, empty_tag_manager):
"""Test přejmenování kategorie na stejný název"""
tm = empty_tag_manager
tm.add_tag("Video", "HD")
result = tm.rename_category("Video", "Video")
# Should succeed but effectively be a no-op
assert result is True
assert "Video" in tm.get_categories()
def test_rename_tag_preserves_other_tags(self, empty_tag_manager):
"""Test že přejmenování jednoho tagu neovlivní ostatní"""
tm = empty_tag_manager
tm.add_tag("Video", "HD")
tm.add_tag("Video", "4K")
tm.add_tag("Video", "SD")
tm.rename_tag("Video", "HD", "FullHD")
tags = tm.get_tags_in_category("Video")
tag_names = [t.name for t in tags]
assert len(tag_names) == 3
assert "FullHD" in tag_names
assert "4K" in tag_names
assert "SD" in tag_names
assert "HD" not in tag_names
def test_rename_category_preserves_tags(self, empty_tag_manager):
"""Test že přejmenování kategorie zachová všechny tagy"""
tm = empty_tag_manager
tm.add_tag("Video", "HD")
tm.add_tag("Video", "4K")
tm.add_tag("Video", "SD")
tm.rename_category("Video", "Rozlišení")
tags = tm.get_tags_in_category("Rozlišení")
assert len(tags) == 3
tag_names = [t.name for t in tags]
assert "HD" in tag_names
assert "4K" in tag_names
assert "SD" in tag_names
class TestMergeTag:
"""Testy pro slučování tagů a kategorií"""
@pytest.fixture
def empty_tag_manager(self):
tm = TagManager()
for category in list(tm.tags_by_category.keys()):
tm.remove_category(category)
return tm
def test_merge_tag_success(self, empty_tag_manager):
"""Test úspěšného sloučení tagů"""
tm = empty_tag_manager
tm.add_tag("Video", "HD")
tm.add_tag("Video", "FullHD")
result = tm.merge_tag("Video", "HD", "FullHD")
assert result is not None
assert result.name == "FullHD"
tags = tm.get_tags_in_category("Video")
tag_names = [t.name for t in tags]
assert "FullHD" in tag_names
assert "HD" not in tag_names
assert len(tag_names) == 1
def test_merge_tag_nonexistent_source(self, empty_tag_manager):
"""Test sloučení neexistujícího zdrojového tagu"""
tm = empty_tag_manager
tm.add_tag("Video", "FullHD")
result = tm.merge_tag("Video", "HD", "FullHD")
assert result is None
def test_merge_tag_nonexistent_target(self, empty_tag_manager):
"""Test sloučení do neexistujícího cílového tagu"""
tm = empty_tag_manager
tm.add_tag("Video", "HD")
result = tm.merge_tag("Video", "HD", "FullHD")
assert result is None
def test_merge_tag_nonexistent_category(self, empty_tag_manager):
"""Test sloučení v neexistující kategorii"""
result = empty_tag_manager.merge_tag("Nonexistent", "HD", "FullHD")
assert result is None
def test_merge_tag_preserves_other_tags(self, empty_tag_manager):
"""Test že sloučení jednoho tagu neovlivní ostatní"""
tm = empty_tag_manager
tm.add_tag("Video", "HD")
tm.add_tag("Video", "4K")
tm.add_tag("Video", "SD")
tm.merge_tag("Video", "HD", "4K")
tags = tm.get_tags_in_category("Video")
tag_names = [t.name for t in tags]
assert len(tag_names) == 2
assert "4K" in tag_names
assert "SD" in tag_names
assert "HD" not in tag_names
def test_merge_category_success(self, empty_tag_manager):
"""Test úspěšného sloučení kategorií"""
tm = empty_tag_manager
tm.add_tag("Video", "HD")
tm.add_tag("Video", "4K")
tm.add_tag("Rozlišení", "SD")
result = tm.merge_category("Video", "Rozlišení")
assert result is True
assert "Video" not in tm.get_categories()
assert "Rozlišení" in tm.get_categories()
tags = tm.get_tags_in_category("Rozlišení")
tag_names = [t.name for t in tags]
assert "HD" in tag_names
assert "4K" in tag_names
assert "SD" in tag_names
def test_merge_category_nonexistent_source(self, empty_tag_manager):
"""Test sloučení neexistující zdrojové kategorie"""
tm = empty_tag_manager
tm.add_tag("Rozlišení", "HD")
result = tm.merge_category("Video", "Rozlišení")
assert result is False
def test_merge_category_nonexistent_target(self, empty_tag_manager):
"""Test sloučení do neexistující cílové kategorie"""
tm = empty_tag_manager
tm.add_tag("Video", "HD")
result = tm.merge_category("Video", "Rozlišení")
assert result is False
def test_merge_category_same_category(self, empty_tag_manager):
"""Test sloučení kategorie se sebou samou"""
tm = empty_tag_manager
tm.add_tag("Video", "HD")
result = tm.merge_category("Video", "Video")
assert result is True # No-op, should succeed
def test_merge_category_duplicate_tags(self, empty_tag_manager):
"""Test sloučení kategorií s duplicitními tagy"""
tm = empty_tag_manager
tm.add_tag("Video", "HD")
tm.add_tag("Video", "4K")
tm.add_tag("Rozlišení", "HD") # Same tag name in target
result = tm.merge_category("Video", "Rozlišení")
assert result is True
tags = tm.get_tags_in_category("Rozlišení")
tag_names = [t.name for t in tags]
# HD should appear only once (set deduplication)
assert tag_names.count("HD") == 1
assert "4K" in tag_names
def test_tag_exists(self, empty_tag_manager):
"""Test kontroly existence tagu"""
tm = empty_tag_manager
tm.add_tag("Video", "HD")
assert tm.tag_exists("Video", "HD") is True
assert tm.tag_exists("Video", "4K") is False
assert tm.tag_exists("Nonexistent", "HD") is False
def test_category_exists(self, empty_tag_manager):
"""Test kontroly existence kategorie"""
tm = empty_tag_manager
tm.add_tag("Video", "HD")
assert tm.category_exists("Video") is True
assert tm.category_exists("Nonexistent") is False