Migrate from Poetry and pyenv to uv
This commit is contained in:
+6
-5
@@ -1,11 +1,14 @@
|
||||
"""
|
||||
Konfigurace pytest - sdílené fixtures a nastavení pro všechny testy
|
||||
"""
|
||||
import pytest
|
||||
import tempfile
|
||||
|
||||
import contextlib
|
||||
import shutil
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def session_temp_dir():
|
||||
@@ -22,7 +25,5 @@ def cleanup_config_files():
|
||||
# Cleanup po testu
|
||||
config_file = Path("config.json")
|
||||
if config_file.exists():
|
||||
try:
|
||||
with contextlib.suppress(Exception):
|
||||
config_file.unlink()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
+16
-11
@@ -1,8 +1,13 @@
|
||||
import pytest
|
||||
import json
|
||||
|
||||
import pytest
|
||||
|
||||
from src.core.config import (
|
||||
load_global_config, save_global_config, DEFAULT_GLOBAL_CONFIG,
|
||||
load_config, save_config # Legacy functions
|
||||
DEFAULT_GLOBAL_CONFIG,
|
||||
load_config, # Legacy functions
|
||||
load_global_config,
|
||||
save_config,
|
||||
save_global_config,
|
||||
)
|
||||
|
||||
|
||||
@@ -14,7 +19,8 @@ class TestGlobalConfig:
|
||||
"""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)
|
||||
|
||||
monkeypatch.setattr(config_module, "GLOBAL_CONFIG_FILE", config_path)
|
||||
return config_path
|
||||
|
||||
def test_default_global_config_structure(self):
|
||||
@@ -136,7 +142,8 @@ class TestLegacyFunctions:
|
||||
"""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)
|
||||
|
||||
monkeypatch.setattr(config_module, "GLOBAL_CONFIG_FILE", config_path)
|
||||
return config_path
|
||||
|
||||
def test_load_config_legacy(self, temp_global_config):
|
||||
@@ -166,14 +173,15 @@ class TestConfigEdgeCases:
|
||||
"""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)
|
||||
|
||||
monkeypatch.setattr(config_module, "GLOBAL_CONFIG_FILE", config_path)
|
||||
return config_path
|
||||
|
||||
def test_config_path_with_spaces(self, temp_global_config):
|
||||
"""Test s cestou obsahující mezery"""
|
||||
test_config = {
|
||||
**DEFAULT_GLOBAL_CONFIG,
|
||||
"last_folder": "/path/with spaces/in name"
|
||||
"last_folder": "/path/with spaces/in name",
|
||||
}
|
||||
|
||||
save_global_config(test_config)
|
||||
@@ -215,10 +223,7 @@ class TestConfigEdgeCases:
|
||||
|
||||
def test_config_ensure_ascii_false(self, temp_global_config):
|
||||
"""Test že ensure_ascii=False funguje správně"""
|
||||
test_config = {
|
||||
**DEFAULT_GLOBAL_CONFIG,
|
||||
"last_folder": "/cesta/čeština"
|
||||
}
|
||||
test_config = {**DEFAULT_GLOBAL_CONFIG, "last_folder": "/cesta/čeština"}
|
||||
|
||||
save_global_config(test_config)
|
||||
|
||||
|
||||
+11
-6
@@ -15,7 +15,6 @@ from src.constants import (
|
||||
get_version,
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# get_version()
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -42,15 +41,19 @@ def test_get_version_fallback_when_toml_missing(tmp_path: Path) -> None:
|
||||
assert len(result) > 0
|
||||
|
||||
|
||||
def test_get_version_unknown_fallback(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
def test_get_version_unknown_fallback(
|
||||
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
||||
) -> None:
|
||||
"""get_version() returns '0.0.0-unknown' when all sources are unavailable."""
|
||||
missing = tmp_path / "nonexistent.toml"
|
||||
monkeypatch.setattr("src.constants._PYPROJECT_PATH", missing)
|
||||
|
||||
# Patch _version import to also fail
|
||||
with patch("src.constants.Path.write_text", side_effect=OSError):
|
||||
with patch.dict("sys.modules", {"src._version": None}):
|
||||
result = get_version()
|
||||
with (
|
||||
patch("src.constants.Path.write_text", side_effect=OSError),
|
||||
patch.dict("sys.modules", {"src._version": None}),
|
||||
):
|
||||
result = get_version()
|
||||
|
||||
assert isinstance(result, str)
|
||||
|
||||
@@ -131,7 +134,9 @@ def test_app_title_dev_suffix_when_debug(monkeypatch: pytest.MonkeyPatch) -> Non
|
||||
assert title.endswith("-DEV")
|
||||
|
||||
|
||||
def test_app_title_no_dev_suffix_when_not_debug(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
def test_app_title_no_dev_suffix_when_not_debug(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
"""APP_TITLE does not end with '-DEV' when ENV_DEBUG is False."""
|
||||
import src.constants as consts
|
||||
|
||||
|
||||
+44
-20
@@ -1,4 +1,5 @@
|
||||
"""Tests for the lean ČSFD title-bridge scraper (`src.core.csfd`)."""
|
||||
|
||||
import hashlib
|
||||
from unittest.mock import patch
|
||||
|
||||
@@ -78,12 +79,16 @@ SEARCH_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"
|
||||
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"
|
||||
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"
|
||||
@@ -97,8 +102,9 @@ class TestCleanFilenameToQuery:
|
||||
|
||||
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("https://www.csfd.cz/film/2294-vykoupeni/prehled/") == 2294
|
||||
)
|
||||
assert _extract_csfd_id("/film/999/") == 999
|
||||
|
||||
def test_none_when_absent(self):
|
||||
@@ -119,13 +125,17 @@ class TestAnubisPoW:
|
||||
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>')
|
||||
|
||||
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")
|
||||
|
||||
@@ -133,6 +143,7 @@ class TestParseOrigin:
|
||||
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"
|
||||
|
||||
@@ -140,22 +151,29 @@ class TestParseOriginalTitle:
|
||||
# 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>')
|
||||
|
||||
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>')
|
||||
|
||||
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
|
||||
|
||||
@@ -189,10 +207,13 @@ class TestFetchFilm:
|
||||
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/")
|
||||
with (
|
||||
patch(
|
||||
"src.core.csfd._get_page", side_effect=requests.RequestException("boom")
|
||||
),
|
||||
pytest.raises(CsfdError),
|
||||
):
|
||||
fetch_film("https://www.csfd.cz/film/1/")
|
||||
|
||||
|
||||
class TestIsFilmHref:
|
||||
@@ -218,7 +239,8 @@ class TestSearchFilms:
|
||||
# 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/")
|
||||
"https://www.csfd.cz/film/2294-vykoupeni-z-veznice-shawshank/prehled/"
|
||||
)
|
||||
|
||||
def test_empty_query_returns_empty(self):
|
||||
assert search_films(" ") == []
|
||||
@@ -231,7 +253,9 @@ class TestFindCsfdUrl:
|
||||
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)
|
||||
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):
|
||||
|
||||
+36
-15
@@ -1,4 +1,5 @@
|
||||
import pytest
|
||||
|
||||
from src.core.file import File
|
||||
from src.core.pool_index import PoolIndex
|
||||
from src.core.tag import Tag
|
||||
@@ -130,7 +131,9 @@ class TestFile:
|
||||
file_obj.remove_tag("Video/HD")
|
||||
assert "Video/HD" not in {t.full_path for t in file_obj.tags}
|
||||
|
||||
def test_file_remove_tag_string_without_category(self, test_file, tag_manager, index):
|
||||
def test_file_remove_tag_string_without_category(
|
||||
self, test_file, tag_manager, index
|
||||
):
|
||||
file_obj = File(test_file, tag_manager, index=index)
|
||||
file_obj.add_tag("SimpleTag")
|
||||
file_obj.remove_tag("SimpleTag")
|
||||
@@ -227,8 +230,13 @@ class TestApplyNfoTags:
|
||||
def test_apply_nfo_assigns_expected_categories(self, movie_file):
|
||||
from src.core.nfo import NfoData
|
||||
|
||||
nfo = NfoData(title="Matrix", year="1999", genres=["Akční", "Sci-Fi"],
|
||||
countries=["USA"], rating=90)
|
||||
nfo = NfoData(
|
||||
title="Matrix",
|
||||
year="1999",
|
||||
genres=["Akční", "Sci-Fi"],
|
||||
countries=["USA"],
|
||||
rating=90,
|
||||
)
|
||||
result = movie_file.apply_nfo(nfo)
|
||||
|
||||
assert result["success"]
|
||||
@@ -263,12 +271,16 @@ class TestApplyNfoTags:
|
||||
def test_added_timestamp_uses_stored_added(self, movie_file):
|
||||
movie_file.added = "2026-06-16T12:00:00"
|
||||
from datetime import datetime
|
||||
assert movie_file.added_timestamp() == datetime.fromisoformat(
|
||||
"2026-06-16T12:00:00").timestamp()
|
||||
|
||||
assert (
|
||||
movie_file.added_timestamp()
|
||||
== datetime.fromisoformat("2026-06-16T12:00:00").timestamp()
|
||||
)
|
||||
|
||||
def test_added_timestamp_falls_back_to_mtime(self, movie_file):
|
||||
movie_file.added = None
|
||||
import os
|
||||
|
||||
expected = os.stat(movie_file.file_path).st_mtime
|
||||
assert movie_file.added_timestamp() == expected
|
||||
|
||||
@@ -277,8 +289,9 @@ class TestApplyNfoTags:
|
||||
assert movie_file.attributes["collection_sort"] == "03"
|
||||
assert movie_file.name_context()["collection_sort"] == "03"
|
||||
# reload (from index) keeps it
|
||||
reloaded = File(movie_file.file_path, movie_file.tagmanager,
|
||||
index=movie_file.index)
|
||||
reloaded = File(
|
||||
movie_file.file_path, movie_file.tagmanager, index=movie_file.index
|
||||
)
|
||||
assert reloaded.attributes["collection_sort"] == "03"
|
||||
|
||||
def test_set_attribute_empty_removes_it(self, movie_file):
|
||||
@@ -290,15 +303,23 @@ class TestApplyNfoTags:
|
||||
movie_file.title = "Dr. No"
|
||||
movie_file.set_attribute("collection_sort", "01")
|
||||
ctx = movie_file.name_context()
|
||||
assert "{collection_sort} - {title}{ext}".format(**ctx) == \
|
||||
f"01 - Dr. No{ctx['ext']}"
|
||||
assert (
|
||||
"{collection_sort} - {title}{ext}".format(**ctx)
|
||||
== f"01 - Dr. No{ctx['ext']}"
|
||||
)
|
||||
|
||||
def test_apply_nfo_honors_custom_schema(self, movie_file):
|
||||
"""A schema without the rating entry produces no Hodnocení tags."""
|
||||
from src.core.nfo import NfoData
|
||||
|
||||
schema = [{"category": "Žánr", "csfd_field": "genres",
|
||||
"transform": None, "filmoteka_root": ""}]
|
||||
schema = [
|
||||
{
|
||||
"category": "Žánr",
|
||||
"csfd_field": "genres",
|
||||
"transform": None,
|
||||
"filmoteka_root": "",
|
||||
}
|
||||
]
|
||||
movie_file.apply_nfo(NfoData(genres=["Drama"], rating=90), schema)
|
||||
|
||||
paths = {t.full_path for t in movie_file.tags}
|
||||
@@ -314,9 +335,9 @@ class TestApplyNfoTags:
|
||||
movie_file.apply_nfo(NfoData(year="2009", genres=["Drama"]))
|
||||
|
||||
paths = {t.full_path for t in movie_file.tags}
|
||||
assert "Sbírka/Oblíbené" in paths # user tag kept
|
||||
assert "Žánr/Drama" in paths # new NFO tag
|
||||
assert "Sbírka/Oblíbené" in paths # user tag kept
|
||||
assert "Žánr/Drama" in paths # new NFO tag
|
||||
assert "Rok/2009" in paths
|
||||
assert "Žánr/Akční" not in paths # old NFO tag regenerated away
|
||||
assert "Žánr/Akční" not in paths # stale ČSFD tag dropped
|
||||
assert "Žánr/Akční" not in paths # old NFO tag regenerated away
|
||||
assert "Žánr/Akční" not in paths # stale ČSFD tag dropped
|
||||
assert "Rok/1999" not in paths
|
||||
|
||||
+1313
-139
File diff suppressed because it is too large
Load Diff
@@ -1,7 +1,9 @@
|
||||
import pytest
|
||||
import os
|
||||
from src.core.hardlink_manager import HardlinkManager, create_hardlink_structure
|
||||
|
||||
import pytest
|
||||
|
||||
from src.core.file import File
|
||||
from src.core.hardlink_manager import HardlinkManager, create_hardlink_structure
|
||||
from src.core.tag import Tag
|
||||
from src.core.tag_manager import TagManager
|
||||
|
||||
@@ -84,7 +86,9 @@ class TestHardlinkManager:
|
||||
assert (temp_output_dir / "rok" / "1988" / "file1.txt").exists()
|
||||
assert (temp_output_dir / "žánr" / "Drama" / "file2.txt").exists()
|
||||
|
||||
def test_hardlinks_are_same_inode(self, files_with_tags, temp_output_dir, temp_source_dir):
|
||||
def test_hardlinks_are_same_inode(
|
||||
self, files_with_tags, temp_output_dir, temp_source_dir
|
||||
):
|
||||
"""Test že vytvořené soubory jsou opravdu hardlinky (stejný inode)"""
|
||||
manager = HardlinkManager(temp_output_dir)
|
||||
manager.create_structure_for_files(files_with_tags)
|
||||
@@ -95,10 +99,14 @@ class TestHardlinkManager:
|
||||
# Same inode = hardlink
|
||||
assert original.stat().st_ino == hardlink.stat().st_ino
|
||||
|
||||
def test_create_structure_with_category_filter(self, files_with_tags, temp_output_dir):
|
||||
def test_create_structure_with_category_filter(
|
||||
self, files_with_tags, temp_output_dir
|
||||
):
|
||||
"""Test vytvoření struktury jen pro vybrané kategorie"""
|
||||
manager = HardlinkManager(temp_output_dir)
|
||||
success, fail = manager.create_structure_for_files(files_with_tags, categories=["žánr"])
|
||||
success, fail = manager.create_structure_for_files(
|
||||
files_with_tags, categories=["žánr"]
|
||||
)
|
||||
|
||||
# Only "žánr" tags should be processed (3 links)
|
||||
assert success == 3
|
||||
@@ -107,7 +115,9 @@ class TestHardlinkManager:
|
||||
assert (temp_output_dir / "žánr" / "Komedie" / "file1.txt").exists()
|
||||
assert not (temp_output_dir / "rok").exists()
|
||||
|
||||
def test_create_structure_with_category_roots(self, files_with_tags, temp_output_dir):
|
||||
def test_create_structure_with_category_roots(
|
||||
self, files_with_tags, temp_output_dir
|
||||
):
|
||||
"""category_roots: genres sit at the output root, rok under 'Dle roku'."""
|
||||
manager = HardlinkManager(temp_output_dir)
|
||||
roots = {"žánr": "", "rok": "Dle roku"}
|
||||
@@ -137,8 +147,8 @@ class TestHardlinkManager:
|
||||
os.link(temp_source_dir / "file1.txt", mirror_link)
|
||||
|
||||
manager.sync_structure(
|
||||
files_with_tags, category_roots=roots,
|
||||
reserved_subfolders={"Seriály"})
|
||||
files_with_tags, category_roots=roots, reserved_subfolders={"Seriály"}
|
||||
)
|
||||
|
||||
# The mirror (reserved folder) is left alone
|
||||
assert mirror_link.exists()
|
||||
@@ -208,9 +218,7 @@ class TestHardlinkManager:
|
||||
created2, _, removed2, _ = manager.sync_structure([f], category_roots=roots)
|
||||
assert (created2, removed2) == (0, 0)
|
||||
|
||||
def test_link_name_strips_year_by_default(
|
||||
self, tmp_path, tag_manager
|
||||
):
|
||||
def test_link_name_strips_year_by_default(self, tmp_path, tag_manager):
|
||||
"""A pooled 'Title (YYYY).ext' hardlinks into the tag tree as 'Title.ext'."""
|
||||
source = tmp_path / "source"
|
||||
source.mkdir()
|
||||
@@ -242,9 +250,7 @@ class TestHardlinkManager:
|
||||
|
||||
assert (output / "Sci-Fi" / "Matrix.mkv").exists() # year + id stripped
|
||||
|
||||
def test_link_name_keeps_year_on_collision(
|
||||
self, tmp_path, tag_manager
|
||||
):
|
||||
def test_link_name_keeps_year_on_collision(self, tmp_path, tag_manager):
|
||||
"""Two same-title films in one folder keep the year to disambiguate."""
|
||||
source = tmp_path / "source"
|
||||
source.mkdir()
|
||||
@@ -276,9 +282,7 @@ class TestHardlinkManager:
|
||||
assert (output / "Dle roku" / "1972" / "Solaris.mkv").exists()
|
||||
assert (output / "Dle roku" / "2002" / "Solaris.mkv").exists()
|
||||
|
||||
def test_clean_naming_cleanup_is_consistent(
|
||||
self, tmp_path, tag_manager
|
||||
):
|
||||
def test_clean_naming_cleanup_is_consistent(self, tmp_path, tag_manager):
|
||||
"""Clean-named links are stable: a second sync removes/creates nothing."""
|
||||
source = tmp_path / "source"
|
||||
source.mkdir()
|
||||
@@ -349,7 +353,9 @@ class TestHardlinkManager:
|
||||
)
|
||||
|
||||
# Templated name inside the collection folder
|
||||
assert (temp_output_dir / "Dle kolekce" / "James Bond" / "1962 - Dr. No.txt").exists()
|
||||
assert (
|
||||
temp_output_dir / "Dle kolekce" / "James Bond" / "1962 - Dr. No.txt"
|
||||
).exists()
|
||||
# Other categories use the default clean title (no year, not the pool name)
|
||||
assert (temp_output_dir / "Akční" / "Dr. No.txt").exists()
|
||||
|
||||
@@ -365,9 +371,12 @@ class TestHardlinkManager:
|
||||
roots = {"Kolekce": "Dle kolekce"}
|
||||
templates = {"Kolekce": "{year} - {title}{ext}"}
|
||||
manager = HardlinkManager(temp_output_dir)
|
||||
manager.sync_structure([f], category_roots=roots, category_filename_templates=templates)
|
||||
created, _, removed, _ = manager.sync_structure(
|
||||
[f], category_roots=roots, category_filename_templates=templates)
|
||||
manager.sync_structure(
|
||||
[f], category_roots=roots, category_filename_templates=templates
|
||||
)
|
||||
_created, _, removed, _ = manager.sync_structure(
|
||||
[f], category_roots=roots, category_filename_templates=templates
|
||||
)
|
||||
|
||||
folder = temp_output_dir / "Dle kolekce" / "James Bond"
|
||||
assert [p.name for p in folder.iterdir()] == ["1962 - Dr. No.txt"]
|
||||
@@ -417,7 +426,8 @@ class TestHardlinkManager:
|
||||
files_with_tags[2].added = "2026-01-01T00:00:00"
|
||||
manager = HardlinkManager(temp_output_dir)
|
||||
created, fail = manager.generate_recently_added(
|
||||
files_with_tags, count=2, subfolder="- Nově přidané")
|
||||
files_with_tags, count=2, subfolder="- Nově přidané"
|
||||
)
|
||||
|
||||
recent = temp_output_dir / "- Nově přidané"
|
||||
assert created == 2 and fail == 0
|
||||
@@ -427,7 +437,9 @@ class TestHardlinkManager:
|
||||
def test_dry_run(self, files_with_tags, temp_output_dir):
|
||||
"""Test dry run (bez skutečného vytváření)"""
|
||||
manager = HardlinkManager(temp_output_dir)
|
||||
success, fail = manager.create_structure_for_files(files_with_tags, dry_run=True)
|
||||
success, fail = manager.create_structure_for_files(
|
||||
files_with_tags, dry_run=True
|
||||
)
|
||||
|
||||
assert success == 4
|
||||
assert fail == 0
|
||||
@@ -497,7 +509,7 @@ class TestHardlinkManager:
|
||||
manager = HardlinkManager(temp_output_dir)
|
||||
|
||||
# Create first time
|
||||
success1, _ = manager.create_structure_for_files(files_with_tags)
|
||||
_success1, _ = manager.create_structure_for_files(files_with_tags)
|
||||
|
||||
# Create second time - should skip existing
|
||||
manager2 = HardlinkManager(temp_output_dir)
|
||||
@@ -507,7 +519,9 @@ class TestHardlinkManager:
|
||||
assert success2 == 0
|
||||
assert fail2 == 0
|
||||
|
||||
def test_unique_name_on_conflict(self, temp_source_dir, temp_output_dir, tag_manager):
|
||||
def test_unique_name_on_conflict(
|
||||
self, temp_source_dir, temp_output_dir, tag_manager
|
||||
):
|
||||
"""Two different movies with the same clean name (no year to tell them
|
||||
apart) in one folder get distinct links via a numeric suffix."""
|
||||
f1 = File(temp_source_dir / "file1.txt", tag_manager)
|
||||
@@ -530,7 +544,9 @@ class TestHardlinkManager:
|
||||
assert (folder / "file1.txt").exists()
|
||||
assert (folder / "file1_2.txt").exists()
|
||||
|
||||
def test_czech_characters_in_tags(self, temp_source_dir, temp_output_dir, tag_manager):
|
||||
def test_czech_characters_in_tags(
|
||||
self, temp_source_dir, temp_output_dir, tag_manager
|
||||
):
|
||||
"""Test českých znaků v názvech tagů"""
|
||||
f1 = File(temp_source_dir / "file1.txt", tag_manager)
|
||||
f1.tags.clear()
|
||||
@@ -593,7 +609,9 @@ class TestConvenienceFunction:
|
||||
output = tmp_path / "output"
|
||||
output.mkdir()
|
||||
|
||||
success, fail, errors = create_hardlink_structure([f], output, categories=["include"])
|
||||
success, _fail, _errors = create_hardlink_structure(
|
||||
[f], output, categories=["include"]
|
||||
)
|
||||
|
||||
assert success == 1
|
||||
assert (output / "include" / "yes" / "file.txt").exists()
|
||||
@@ -677,7 +695,7 @@ class TestSyncStructure:
|
||||
f.add_tag(Tag("cat", "tag1"))
|
||||
|
||||
# Remove obsolete links
|
||||
removed, paths = manager.remove_obsolete_links([f])
|
||||
removed, _paths = manager.remove_obsolete_links([f])
|
||||
|
||||
assert removed == 1
|
||||
assert not (output / "cat" / "tag2" / "file.txt").exists()
|
||||
@@ -699,7 +717,7 @@ class TestSyncStructure:
|
||||
f.tags.clear()
|
||||
f.add_tag(Tag("cat", "tag1"))
|
||||
|
||||
removed, paths = manager.remove_obsolete_links([f], dry_run=True)
|
||||
removed, _paths = manager.remove_obsolete_links([f], dry_run=True)
|
||||
|
||||
assert removed == 1
|
||||
# File should still exist (dry run)
|
||||
@@ -747,7 +765,7 @@ class TestSyncStructure:
|
||||
manager.create_structure_for_files([f])
|
||||
|
||||
# Sync again without changes
|
||||
created, c_fail, removed, r_fail = manager.sync_structure([f])
|
||||
_created, _c_fail, removed, _r_fail = manager.sync_structure([f])
|
||||
|
||||
# Nothing should change (existing links are skipped)
|
||||
assert removed == 0
|
||||
@@ -821,7 +839,7 @@ class TestEdgeCases:
|
||||
# output doesn't exist
|
||||
|
||||
manager = HardlinkManager(output)
|
||||
success, fail = manager.create_structure_for_files([f])
|
||||
success, _fail = manager.create_structure_for_files([f])
|
||||
|
||||
assert success == 1
|
||||
assert (output / "cat" / "tag" / "file.txt").exists()
|
||||
@@ -842,7 +860,7 @@ class TestEdgeCases:
|
||||
output.mkdir()
|
||||
|
||||
manager = HardlinkManager(output)
|
||||
success, fail = manager.create_structure_for_files([f])
|
||||
success, _fail = manager.create_structure_for_files([f])
|
||||
|
||||
assert success == 1
|
||||
assert (output / "test" / "tag" / "file with spaces (part 2).txt").exists()
|
||||
@@ -862,7 +880,7 @@ class TestEdgeCases:
|
||||
|
||||
manager = HardlinkManager(output)
|
||||
# Empty list = no categories = no links
|
||||
success, fail = manager.create_structure_for_files([f], categories=[])
|
||||
success, _fail = manager.create_structure_for_files([f], categories=[])
|
||||
|
||||
assert success == 0
|
||||
|
||||
|
||||
+16
-11
@@ -3,6 +3,7 @@
|
||||
The FFmpeg subprocess call is monkeypatched so the tests don't need ffmpeg
|
||||
installed and don't touch real video data.
|
||||
"""
|
||||
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
from types import SimpleNamespace
|
||||
@@ -23,6 +24,7 @@ def video_file(tmp_path) -> Path:
|
||||
def _fake_run(returncode=0, stdout="", stderr=""):
|
||||
def run(cmd, **kwargs):
|
||||
return SimpleNamespace(returncode=returncode, stdout=stdout, stderr=stderr)
|
||||
|
||||
return run
|
||||
|
||||
|
||||
@@ -36,8 +38,10 @@ def test_deep_clean_file_is_ok(monkeypatch, video_file):
|
||||
def test_deep_reports_decode_errors(monkeypatch, video_file):
|
||||
monkeypatch.setattr(integrity.shutil, "which", lambda _: "/usr/bin/ffmpeg")
|
||||
monkeypatch.setattr(
|
||||
integrity.subprocess, "run",
|
||||
_fake_run(1, "", "[matroska @ 0x..] Read error\nmore noise"))
|
||||
integrity.subprocess,
|
||||
"run",
|
||||
_fake_run(1, "", "[matroska @ 0x..] Read error\nmore noise"),
|
||||
)
|
||||
result = check_video_integrity(video_file, deep=True)
|
||||
assert not result.ok and result.checked
|
||||
assert result.error == "[matroska @ 0x..] Read error" # first line only
|
||||
@@ -46,8 +50,7 @@ def test_deep_reports_decode_errors(monkeypatch, video_file):
|
||||
def test_deep_stderr_without_nonzero_rc_still_fails(monkeypatch, video_file):
|
||||
"""ffmpeg can print decode errors while returning rc 0 — must still fail."""
|
||||
monkeypatch.setattr(integrity.shutil, "which", lambda _: "/usr/bin/ffmpeg")
|
||||
monkeypatch.setattr(
|
||||
integrity.subprocess, "run", _fake_run(0, "", "corrupt packet"))
|
||||
monkeypatch.setattr(integrity.subprocess, "run", _fake_run(0, "", "corrupt packet"))
|
||||
result = check_video_integrity(video_file, deep=True)
|
||||
assert not result.ok
|
||||
|
||||
@@ -99,9 +102,9 @@ def test_ffmpeg_available_reflects_which(monkeypatch):
|
||||
|
||||
def test_scan_collects_problems_and_progress(monkeypatch, tmp_path):
|
||||
"""FileManager.scan_video_integrity aggregates per-file results."""
|
||||
from src.core.tag_manager import TagManager
|
||||
from src.core.file_manager import FileManager
|
||||
from src.core.file import File
|
||||
from src.core.file_manager import FileManager
|
||||
from src.core.tag_manager import TagManager
|
||||
|
||||
good = tmp_path / "good.mkv"
|
||||
bad = tmp_path / "bad.mkv"
|
||||
@@ -121,7 +124,8 @@ def test_scan_collects_problems_and_progress(monkeypatch, tmp_path):
|
||||
|
||||
seen: list[int] = []
|
||||
result = fm.scan_video_integrity(
|
||||
files, deep=True, on_progress=lambda d, t, f: seen.append(d))
|
||||
files, deep=True, on_progress=lambda d, t, f: seen.append(d)
|
||||
)
|
||||
|
||||
assert result["ok"] and result["checked"] == 2
|
||||
assert [r.path.name for r in result["problems"]] == ["bad.mkv"]
|
||||
@@ -129,9 +133,9 @@ def test_scan_collects_problems_and_progress(monkeypatch, tmp_path):
|
||||
|
||||
|
||||
def test_scan_stops_on_cancel(monkeypatch, tmp_path):
|
||||
from src.core.tag_manager import TagManager
|
||||
from src.core.file_manager import FileManager
|
||||
from src.core.file import File
|
||||
from src.core.file_manager import FileManager
|
||||
from src.core.tag_manager import TagManager
|
||||
|
||||
files = []
|
||||
for i in range(3):
|
||||
@@ -142,7 +146,8 @@ def test_scan_stops_on_cancel(monkeypatch, tmp_path):
|
||||
monkeypatch.setattr("src.core.file_manager.ffmpeg_available", lambda deep: True)
|
||||
monkeypatch.setattr(
|
||||
"src.core.file_manager.check_video_integrity",
|
||||
lambda path, deep=True: IntegrityResult(Path(path), ok=True))
|
||||
lambda path, deep=True: IntegrityResult(Path(path), ok=True),
|
||||
)
|
||||
|
||||
fm = FileManager(TagManager())
|
||||
result = fm.scan_video_integrity(files, should_cancel=lambda: True)
|
||||
@@ -150,8 +155,8 @@ def test_scan_stops_on_cancel(monkeypatch, tmp_path):
|
||||
|
||||
|
||||
def test_scan_reports_missing_ffmpeg(monkeypatch, tmp_path):
|
||||
from src.core.tag_manager import TagManager
|
||||
from src.core.file_manager import FileManager
|
||||
from src.core.tag_manager import TagManager
|
||||
|
||||
monkeypatch.setattr("src.core.file_manager.ffmpeg_available", lambda deep: False)
|
||||
fm = FileManager(TagManager())
|
||||
|
||||
+30
-14
@@ -1,4 +1,5 @@
|
||||
"""Tests for the Jellyfin client + pool verification. Network is monkeypatched."""
|
||||
|
||||
from types import SimpleNamespace
|
||||
|
||||
import pytest
|
||||
@@ -14,8 +15,9 @@ def _configured(monkeypatch):
|
||||
|
||||
|
||||
def _resp(status=200, payload=None):
|
||||
return SimpleNamespace(status_code=status, ok=(200 <= status < 300),
|
||||
json=lambda: (payload or {}))
|
||||
return SimpleNamespace(
|
||||
status_code=status, ok=(200 <= status < 300), json=lambda: payload or {}
|
||||
)
|
||||
|
||||
|
||||
def test_base_url_strips_trailing_slash():
|
||||
@@ -29,8 +31,9 @@ def test_api_available(monkeypatch):
|
||||
|
||||
|
||||
def test_server_version(monkeypatch):
|
||||
monkeypatch.setattr(jellyfin.requests, "request",
|
||||
lambda *a, **k: _resp(200, {"Version": "10.9.11"}))
|
||||
monkeypatch.setattr(
|
||||
jellyfin.requests, "request", lambda *a, **k: _resp(200, {"Version": "10.9.11"})
|
||||
)
|
||||
assert jellyfin.server_version() == "10.9.11"
|
||||
|
||||
|
||||
@@ -40,21 +43,32 @@ def test_scan_all_posts_refresh(monkeypatch):
|
||||
def fake(method, url, **k):
|
||||
seen["method"], seen["url"] = method, url
|
||||
return _resp(200, {})
|
||||
|
||||
monkeypatch.setattr(jellyfin.requests, "request", fake)
|
||||
jellyfin.scan_all()
|
||||
assert seen["method"] == "POST" and seen["url"].endswith("/Library/Refresh")
|
||||
|
||||
|
||||
def test_fetch_movies_maps_tmdb_id(monkeypatch):
|
||||
payload = {"Items": [
|
||||
{"Id": "a1", "Name": "Matrix", "ProductionYear": 1999,
|
||||
"ProviderIds": {"Tmdb": "603"}, "Path": "/m/Matrix (1999) [tmdbid-603].mkv"},
|
||||
{"Id": "a2", "Name": "Bez ID", "ProductionYear": 2000, "ProviderIds": {}},
|
||||
]}
|
||||
monkeypatch.setattr(jellyfin.requests, "request", lambda *a, **k: _resp(200, payload))
|
||||
payload = {
|
||||
"Items": [
|
||||
{
|
||||
"Id": "a1",
|
||||
"Name": "Matrix",
|
||||
"ProductionYear": 1999,
|
||||
"ProviderIds": {"Tmdb": "603"},
|
||||
"Path": "/m/Matrix (1999) [tmdbid-603].mkv",
|
||||
},
|
||||
{"Id": "a2", "Name": "Bez ID", "ProductionYear": 2000, "ProviderIds": {}},
|
||||
]
|
||||
}
|
||||
monkeypatch.setattr(
|
||||
jellyfin.requests, "request", lambda *a, **k: _resp(200, payload)
|
||||
)
|
||||
movies = jellyfin.fetch_movies()
|
||||
assert movies[0] == JellyfinMovie("a1", "Matrix", 1999, 603,
|
||||
"/m/Matrix (1999) [tmdbid-603].mkv")
|
||||
assert movies[0] == JellyfinMovie(
|
||||
"a1", "Matrix", 1999, 603, "/m/Matrix (1999) [tmdbid-603].mkv"
|
||||
)
|
||||
assert movies[1].tmdb_id is None
|
||||
|
||||
|
||||
@@ -74,9 +88,11 @@ class TestVerifyAgainstJellyfin:
|
||||
@pytest.fixture
|
||||
def file_manager(self, tmp_path, monkeypatch):
|
||||
import src.core.config as config_module
|
||||
|
||||
monkeypatch.setattr(config_module, "GLOBAL_CONFIG_FILE", tmp_path / ".gtag")
|
||||
from src.core.tag_manager import TagManager
|
||||
from src.core.file_manager import FileManager
|
||||
from src.core.tag_manager import TagManager
|
||||
|
||||
fm = FileManager(TagManager())
|
||||
fm.set_pool_dir(tmp_path / "pool")
|
||||
return fm
|
||||
@@ -88,7 +104,7 @@ class TestVerifyAgainstJellyfin:
|
||||
file_manager.import_movie(tmp_path / "b.mkv", "Neznámý", year=2020) # no id
|
||||
|
||||
jf = [
|
||||
JellyfinMovie("j1", "Matrix", 1999, 603, None), # matches a by tmdb
|
||||
JellyfinMovie("j1", "Matrix", 1999, 603, None), # matches a by tmdb
|
||||
JellyfinMovie("j2", "Cizí film", 2011, 999, None), # jellyfin-only
|
||||
]
|
||||
result = file_manager.verify_against_jellyfin(jf)
|
||||
|
||||
+67
-29
@@ -1,4 +1,5 @@
|
||||
"""Tests for recovering ČSFD link / title / year from pre-index layouts."""
|
||||
|
||||
import json
|
||||
|
||||
from src.core.legacy_meta import LegacyMeta, read_legacy_meta
|
||||
@@ -12,25 +13,38 @@ def _video(tmp_path, name="Matrix (1999).mkv"):
|
||||
|
||||
def test_reads_tagger_era_sidecar(tmp_path):
|
||||
video = _video(tmp_path)
|
||||
(tmp_path / f".{video.name}.!tag").write_text(json.dumps({
|
||||
"title": "Matrix",
|
||||
"csfd_link": "https://www.csfd.cz/film/9499-matrix/",
|
||||
"csfd_cache": {"year": 1999, "rating": 90},
|
||||
}), encoding="utf-8")
|
||||
(tmp_path / f".{video.name}.!tag").write_text(
|
||||
json.dumps(
|
||||
{
|
||||
"title": "Matrix",
|
||||
"csfd_link": "https://www.csfd.cz/film/9499-matrix/",
|
||||
"csfd_cache": {"year": 1999, "rating": 90},
|
||||
}
|
||||
),
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
meta = read_legacy_meta(video)
|
||||
assert meta == LegacyMeta(
|
||||
csfd_url="https://www.csfd.cz/film/9499-matrix/",
|
||||
title="Matrix", year="1999", source="sidecar")
|
||||
title="Matrix",
|
||||
year="1999",
|
||||
source="sidecar",
|
||||
)
|
||||
|
||||
|
||||
def test_sidecar_new_key_and_meta_block(tmp_path):
|
||||
video = _video(tmp_path)
|
||||
(tmp_path / f".{video.name}.!tag").write_text(json.dumps({
|
||||
"title": "Matrix",
|
||||
"csfd_url": "https://www.csfd.cz/film/9499/",
|
||||
"meta": {"year": "1999"},
|
||||
}), encoding="utf-8")
|
||||
(tmp_path / f".{video.name}.!tag").write_text(
|
||||
json.dumps(
|
||||
{
|
||||
"title": "Matrix",
|
||||
"csfd_url": "https://www.csfd.cz/film/9499/",
|
||||
"meta": {"year": "1999"},
|
||||
}
|
||||
),
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
meta = read_legacy_meta(video)
|
||||
assert meta.csfd_url == "https://www.csfd.cz/film/9499/"
|
||||
@@ -42,16 +56,21 @@ def test_reads_old_pool_index_by_relative_key(tmp_path):
|
||||
movies_dir = pool / "Filmy"
|
||||
movies_dir.mkdir(parents=True)
|
||||
video = _video(movies_dir)
|
||||
(pool / ".Curator.!index").write_text(json.dumps({
|
||||
"settings": {},
|
||||
"movies": {
|
||||
"Filmy/Matrix (1999).mkv": {
|
||||
"title": "Matrix",
|
||||
"csfd_link": "https://www.csfd.cz/film/9499-matrix/",
|
||||
"csfd_cache": {"year": 1999},
|
||||
(pool / ".Curator.!index").write_text(
|
||||
json.dumps(
|
||||
{
|
||||
"settings": {},
|
||||
"movies": {
|
||||
"Filmy/Matrix (1999).mkv": {
|
||||
"title": "Matrix",
|
||||
"csfd_link": "https://www.csfd.cz/film/9499-matrix/",
|
||||
"csfd_cache": {"year": 1999},
|
||||
}
|
||||
},
|
||||
}
|
||||
},
|
||||
}), encoding="utf-8")
|
||||
),
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
meta = read_legacy_meta(video)
|
||||
assert meta.csfd_url == "https://www.csfd.cz/film/9499-matrix/"
|
||||
@@ -66,10 +85,18 @@ def test_index_fallback_to_unique_basename_match(tmp_path):
|
||||
elsewhere = tmp_path / "loose"
|
||||
elsewhere.mkdir()
|
||||
video = _video(elsewhere)
|
||||
(pool.parent / ".Curator.!index").write_text(json.dumps({
|
||||
"movies": {"whatever/Matrix (1999).mkv": {
|
||||
"csfd_url": "https://www.csfd.cz/film/9499/"}},
|
||||
}), encoding="utf-8")
|
||||
(pool.parent / ".Curator.!index").write_text(
|
||||
json.dumps(
|
||||
{
|
||||
"movies": {
|
||||
"whatever/Matrix (1999).mkv": {
|
||||
"csfd_url": "https://www.csfd.cz/film/9499/"
|
||||
}
|
||||
},
|
||||
}
|
||||
),
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
meta = read_legacy_meta(video)
|
||||
assert meta is not None
|
||||
@@ -79,10 +106,20 @@ def test_index_fallback_to_unique_basename_match(tmp_path):
|
||||
def test_sidecar_wins_over_index(tmp_path):
|
||||
video = _video(tmp_path)
|
||||
(tmp_path / f".{video.name}.!tag").write_text(
|
||||
json.dumps({"csfd_link": "https://www.csfd.cz/film/SIDECAR/"}), encoding="utf-8")
|
||||
(tmp_path / ".Curator.!index").write_text(json.dumps({
|
||||
"movies": {"Matrix (1999).mkv": {"csfd_link": "https://www.csfd.cz/film/INDEX/"}},
|
||||
}), encoding="utf-8")
|
||||
json.dumps({"csfd_link": "https://www.csfd.cz/film/SIDECAR/"}), encoding="utf-8"
|
||||
)
|
||||
(tmp_path / ".Curator.!index").write_text(
|
||||
json.dumps(
|
||||
{
|
||||
"movies": {
|
||||
"Matrix (1999).mkv": {
|
||||
"csfd_link": "https://www.csfd.cz/film/INDEX/"
|
||||
}
|
||||
},
|
||||
}
|
||||
),
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
assert read_legacy_meta(video).csfd_url == "https://www.csfd.cz/film/SIDECAR/"
|
||||
|
||||
@@ -100,5 +137,6 @@ def test_none_on_broken_sidecar(tmp_path):
|
||||
def test_record_without_useful_fields_is_none(tmp_path):
|
||||
video = _video(tmp_path)
|
||||
(tmp_path / f".{video.name}.!tag").write_text(
|
||||
json.dumps({"new": True, "ignored": False, "tags": []}), encoding="utf-8")
|
||||
json.dumps({"new": True, "ignored": False, "tags": []}), encoding="utf-8"
|
||||
)
|
||||
assert read_legacy_meta(video) is None
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import tempfile
|
||||
import tkinter as tk
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
from PIL import Image, ImageTk
|
||||
|
||||
from src.core.media_utils import load_icon
|
||||
from PIL import Image, ImageTk
|
||||
import tkinter as tk
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
|
||||
+14
-7
@@ -1,4 +1,5 @@
|
||||
"""Tests for pool → TMDb migration (src.core.migrate + FileManager)."""
|
||||
|
||||
import pytest
|
||||
|
||||
from src.core.migrate import classify, normalize
|
||||
@@ -49,9 +50,11 @@ class TestFileManagerMigrate:
|
||||
@pytest.fixture
|
||||
def file_manager(self, tmp_path, monkeypatch):
|
||||
import src.core.config as config_module
|
||||
|
||||
monkeypatch.setattr(config_module, "GLOBAL_CONFIG_FILE", tmp_path / ".gtag")
|
||||
from src.core.tag_manager import TagManager
|
||||
from src.core.file_manager import FileManager
|
||||
from src.core.tag_manager import TagManager
|
||||
|
||||
fm = FileManager(TagManager())
|
||||
fm.set_pool_dir(tmp_path / "pool")
|
||||
return fm
|
||||
@@ -61,10 +64,13 @@ class TestFileManagerMigrate:
|
||||
movie = file_manager.import_movie(tmp_path / "a.mkv", "Matrix", year=1999)
|
||||
assert "[tmdbid" not in movie.file_path.name # not migrated yet
|
||||
|
||||
import src.core.tmdb as tmdb
|
||||
from src.core import tmdb
|
||||
|
||||
monkeypatch.setattr(
|
||||
tmdb, "search_movie",
|
||||
lambda q, year=None, limit=5: [TmdbResult(603, "Matrix", "1999", "movie")])
|
||||
tmdb,
|
||||
"search_movie",
|
||||
lambda q, year=None, limit=5: [TmdbResult(603, "Matrix", "1999", "movie")],
|
||||
)
|
||||
|
||||
proposals = file_manager.match_pool_to_tmdb()
|
||||
assert len(proposals) == 1
|
||||
@@ -78,11 +84,12 @@ class TestFileManagerMigrate:
|
||||
(tmp_path / "a.mkv").write_bytes(b"x")
|
||||
file_manager.import_movie(tmp_path / "a.mkv", "Matrix", year=1999, tmdb_id=603)
|
||||
|
||||
import src.core.tmdb as tmdb
|
||||
from src.core import tmdb
|
||||
|
||||
called = []
|
||||
monkeypatch.setattr(
|
||||
tmdb, "search_movie",
|
||||
lambda q, year=None, limit=5: called.append(q) or [])
|
||||
tmdb, "search_movie", lambda q, year=None, limit=5: called.append(q) or []
|
||||
)
|
||||
proposals = file_manager.match_pool_to_tmdb()
|
||||
assert proposals == [] and called == [] # already has id → not searched
|
||||
|
||||
|
||||
+104
-35
@@ -1,4 +1,5 @@
|
||||
"""Tests for the movie object and the pool layout it derives (src.core.movie)."""
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
@@ -13,34 +14,54 @@ from src.core.movie import (
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("label,expected", [
|
||||
("HSBS", "HSBS"), ("hsbs", "HSBS"), ("3D HSBS", "HSBS"),
|
||||
("3d.hsbs", "HSBS"), ("3D_FTAB", "FTAB"), ("3d-mvc", "MVC"),
|
||||
("4K", None), ("Director's Cut", None), ("", None), (None, None),
|
||||
])
|
||||
@pytest.mark.parametrize(
|
||||
"label,expected",
|
||||
[
|
||||
("HSBS", "HSBS"),
|
||||
("hsbs", "HSBS"),
|
||||
("3D HSBS", "HSBS"),
|
||||
("3d.hsbs", "HSBS"),
|
||||
("3D_FTAB", "FTAB"),
|
||||
("3d-mvc", "MVC"),
|
||||
("4K", None),
|
||||
("Director's Cut", None),
|
||||
("", None),
|
||||
(None, None),
|
||||
],
|
||||
)
|
||||
def test_normalize_3d_label(label, expected):
|
||||
assert normalize_3d_label(label) == expected
|
||||
|
||||
|
||||
class TestMediaFilename:
|
||||
def test_no_label_is_the_bare_stem(self):
|
||||
assert media_filename("Matrix (1999)", "", ".mkv", grouped=False) == \
|
||||
"Matrix (1999).mkv"
|
||||
assert (
|
||||
media_filename("Matrix (1999)", "", ".mkv", grouped=False)
|
||||
== "Matrix (1999).mkv"
|
||||
)
|
||||
|
||||
def test_3d_label_uses_jellyfins_documented_form(self):
|
||||
assert media_filename("Avatar (2009)", "3D HSBS", ".mkv", grouped=False) == \
|
||||
"Avatar (2009).3D.HSBS.mkv"
|
||||
assert (
|
||||
media_filename("Avatar (2009)", "3D HSBS", ".mkv", grouped=False)
|
||||
== "Avatar (2009).3D.HSBS.mkv"
|
||||
)
|
||||
# same spelling grouped — "." is a valid separator after the folder name
|
||||
assert media_filename("Avatar (2009)", "hsbs", ".mkv", grouped=True) == \
|
||||
"Avatar (2009).3D.HSBS.mkv"
|
||||
assert (
|
||||
media_filename("Avatar (2009)", "hsbs", ".mkv", grouped=True)
|
||||
== "Avatar (2009).3D.HSBS.mkv"
|
||||
)
|
||||
|
||||
def test_plain_label_uses_dash_form(self):
|
||||
assert media_filename("Avatar (2009)", "4K", ".mkv", grouped=True) == \
|
||||
"Avatar (2009) - 4K.mkv"
|
||||
assert (
|
||||
media_filename("Avatar (2009)", "4K", ".mkv", grouped=True)
|
||||
== "Avatar (2009) - 4K.mkv"
|
||||
)
|
||||
|
||||
def test_grouped_empty_label_gets_a_default(self):
|
||||
assert media_filename("Avatar (2009)", "", ".mkv", grouped=True) == \
|
||||
f"Avatar (2009) - {DEFAULT_VERSION_LABEL}.mkv"
|
||||
assert (
|
||||
media_filename("Avatar (2009)", "", ".mkv", grouped=True)
|
||||
== f"Avatar (2009) - {DEFAULT_VERSION_LABEL}.mkv"
|
||||
)
|
||||
|
||||
|
||||
class TestIdentityKey:
|
||||
@@ -59,8 +80,13 @@ class TestIdentityKey:
|
||||
|
||||
|
||||
def _movie(**kwargs) -> Movie:
|
||||
base = dict(key="tmdb-19995", title="Avatar", year="2009", tmdb_id=19995,
|
||||
library="Filmy")
|
||||
base = {
|
||||
"key": "tmdb-19995",
|
||||
"title": "Avatar",
|
||||
"year": "2009",
|
||||
"tmdb_id": 19995,
|
||||
"library": "Filmy",
|
||||
}
|
||||
base.update(kwargs)
|
||||
return Movie(**base)
|
||||
|
||||
@@ -77,50 +103,66 @@ class TestMovieShape:
|
||||
assert m.is_grouped is False
|
||||
|
||||
def test_two_videos_are_grouped(self):
|
||||
m = _movie(media=[MediaFile(Path("/a.mkv"), "3D HSBS"),
|
||||
MediaFile(Path("/b.mkv"), "Výchozí")])
|
||||
m = _movie(
|
||||
media=[
|
||||
MediaFile(Path("/a.mkv"), "3D HSBS"),
|
||||
MediaFile(Path("/b.mkv"), "Výchozí"),
|
||||
]
|
||||
)
|
||||
assert m.is_grouped is True
|
||||
|
||||
def test_primary_prefers_the_plain_version(self):
|
||||
m = _movie(media=[MediaFile(Path("/a.mkv"), "3D HSBS"),
|
||||
MediaFile(Path("/b.mkv"), "")])
|
||||
m = _movie(
|
||||
media=[MediaFile(Path("/a.mkv"), "3D HSBS"), MediaFile(Path("/b.mkv"), "")]
|
||||
)
|
||||
assert m.primary.label == ""
|
||||
|
||||
|
||||
class TestDesiredLayout:
|
||||
def test_one_video_stays_flat_in_the_library(self, tmp_path):
|
||||
m = _movie(media=[MediaFile(tmp_path / "kdekoli.mkv", "")])
|
||||
(media, target), = m.desired_layout(tmp_path)
|
||||
((_media, target),) = m.desired_layout(tmp_path)
|
||||
assert target == tmp_path / "Filmy" / "Avatar (2009) [tmdbid-19995].mkv"
|
||||
|
||||
def test_one_3d_video_keeps_the_3d_tag_flat(self, tmp_path):
|
||||
m = _movie(media=[MediaFile(tmp_path / "x.mkv", "3D HSBS")])
|
||||
(_m, target), = m.desired_layout(tmp_path)
|
||||
((_m, target),) = m.desired_layout(tmp_path)
|
||||
assert target.name == "Avatar (2009) [tmdbid-19995].3D.HSBS.mkv"
|
||||
assert target.parent == tmp_path / "Filmy"
|
||||
|
||||
def test_two_videos_move_into_the_movie_folder(self, tmp_path):
|
||||
m = _movie(media=[MediaFile(tmp_path / "a.mkv", ""),
|
||||
MediaFile(tmp_path / "b.mkv", "3D HSBS")])
|
||||
m = _movie(
|
||||
media=[
|
||||
MediaFile(tmp_path / "a.mkv", ""),
|
||||
MediaFile(tmp_path / "b.mkv", "3D HSBS"),
|
||||
]
|
||||
)
|
||||
targets = {mf.label: path for mf, path in m.desired_layout(tmp_path)}
|
||||
folder = tmp_path / "Filmy" / "Avatar (2009) [tmdbid-19995]"
|
||||
assert targets[""] == folder / \
|
||||
f"Avatar (2009) [tmdbid-19995] - {DEFAULT_VERSION_LABEL}.mkv"
|
||||
assert (
|
||||
targets[""]
|
||||
== folder / f"Avatar (2009) [tmdbid-19995] - {DEFAULT_VERSION_LABEL}.mkv"
|
||||
)
|
||||
assert targets["3D HSBS"] == folder / "Avatar (2009) [tmdbid-19995].3D.HSBS.mkv"
|
||||
|
||||
def test_layout_follows_a_retitle(self, tmp_path):
|
||||
"""Renaming the movie is just a different desired layout — the thing
|
||||
that used to be impossible for a grouped movie."""
|
||||
m = _movie(title="Avatar: Nová verze",
|
||||
media=[MediaFile(tmp_path / "a.mkv", "A"),
|
||||
MediaFile(tmp_path / "b.mkv", "B")])
|
||||
m = _movie(
|
||||
title="Avatar: Nová verze",
|
||||
media=[
|
||||
MediaFile(tmp_path / "a.mkv", "A"),
|
||||
MediaFile(tmp_path / "b.mkv", "B"),
|
||||
],
|
||||
)
|
||||
folders = {path.parent for _mf, path in m.desired_layout(tmp_path)}
|
||||
assert folders == {
|
||||
tmp_path / "Filmy" / "Avatar: Nová verze (2009) [tmdbid-19995]"}
|
||||
tmp_path / "Filmy" / "Avatar: Nová verze (2009) [tmdbid-19995]"
|
||||
}
|
||||
|
||||
def test_layout_follows_a_library_move(self, tmp_path):
|
||||
m = _movie(library="Kids", media=[MediaFile(tmp_path / "a.mkv", "")])
|
||||
(_m, target), = m.desired_layout(tmp_path)
|
||||
((_m, target),) = m.desired_layout(tmp_path)
|
||||
assert target.parent == tmp_path / "Kids"
|
||||
|
||||
|
||||
@@ -128,12 +170,16 @@ class TestRecordRoundTrip:
|
||||
def test_to_and_from_record(self, tmp_path):
|
||||
m = _movie(
|
||||
media=[MediaFile(tmp_path / "Filmy" / "a.mkv", "3D HSBS")],
|
||||
csfd_url="https://www.csfd.cz/film/1/", date="2026-01-01",
|
||||
attributes={"collection_sort": "01"}, meta={"year": "2009"})
|
||||
csfd_url="https://www.csfd.cz/film/1/",
|
||||
date="2026-01-01",
|
||||
attributes={"collection_sort": "01"},
|
||||
meta={"year": "2009"},
|
||||
)
|
||||
|
||||
record = m.to_record(tmp_path)
|
||||
assert record["media"] == [
|
||||
{"role": "video", "label": "3D HSBS", "file": "Filmy/a.mkv"}]
|
||||
{"role": "video", "label": "3D HSBS", "file": "Filmy/a.mkv"}
|
||||
]
|
||||
|
||||
back = Movie.from_record("tmdb-19995", record, tmp_path)
|
||||
assert back.title == "Avatar"
|
||||
@@ -148,8 +194,31 @@ class TestRecordRoundTrip:
|
||||
|
||||
def test_from_record_builds_tags_via_tagmanager(self, tmp_path):
|
||||
from src.core.tag_manager import TagManager
|
||||
|
||||
record = {"title": "Avatar", "media": [], "tags": ["Žánr/Sci-Fi"]}
|
||||
|
||||
back = Movie.from_record("tmdb-19995", record, tmp_path, TagManager())
|
||||
|
||||
assert {t.full_path for t in back.tags} == {"Žánr/Sci-Fi"}
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"label,expected",
|
||||
[
|
||||
("part1", 1),
|
||||
("Part 2", 2),
|
||||
("pt3", 3),
|
||||
("CD1", 1),
|
||||
("disc-2", 2),
|
||||
("dvd_4", 4),
|
||||
("disk12", 12),
|
||||
("4K", None),
|
||||
("3D HSBS", None),
|
||||
("", None),
|
||||
("partition", None),
|
||||
],
|
||||
)
|
||||
def test_normalize_part_label(label, expected):
|
||||
from src.core.movie import normalize_part_label
|
||||
|
||||
assert normalize_part_label(label) == expected
|
||||
|
||||
+44
-13
@@ -1,4 +1,5 @@
|
||||
"""Tests for the pool naming convention helpers (src.core.naming)."""
|
||||
|
||||
import pytest
|
||||
|
||||
from src.core.naming import (
|
||||
@@ -9,14 +10,23 @@ from src.core.naming import (
|
||||
parse_3d_format,
|
||||
parse_pool_stem,
|
||||
parse_tmdb_id,
|
||||
three_d_format_from_filename,
|
||||
year_from_filename,
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("year,ok", [
|
||||
("1968", True), (1968, True), ("968", False), ("19688", False),
|
||||
("abcd", False), ("", False), (" 1999 ", True),
|
||||
])
|
||||
@pytest.mark.parametrize(
|
||||
"year,ok",
|
||||
[
|
||||
("1968", True),
|
||||
(1968, True),
|
||||
("968", False),
|
||||
("19688", False),
|
||||
("abcd", False),
|
||||
("", False),
|
||||
(" 1999 ", True),
|
||||
],
|
||||
)
|
||||
def test_is_valid_year(year, ok):
|
||||
assert is_valid_year(year) is ok
|
||||
|
||||
@@ -82,8 +92,10 @@ def test_is_canonical_pool_stem():
|
||||
|
||||
|
||||
def test_canonical_pool_stem_with_3d_format():
|
||||
assert canonical_pool_stem("Avatar", 2009, 19995, "hsbs") == \
|
||||
"Avatar (2009) [tmdbid-19995].3D.HSBS"
|
||||
assert (
|
||||
canonical_pool_stem("Avatar", 2009, 19995, "hsbs")
|
||||
== "Avatar (2009) [tmdbid-19995].3D.HSBS"
|
||||
)
|
||||
# upper-cased regardless of input case
|
||||
assert canonical_pool_stem("Avatar", 2009, None, "fsbs") == "Avatar (2009).3D.FSBS"
|
||||
# empty/None → no tag at all
|
||||
@@ -118,12 +130,31 @@ def test_canonical_pool_stem_3d_without_tmdb_id():
|
||||
assert parse_3d_format("Avatar (2009).3D.MVC") == "MVC"
|
||||
|
||||
|
||||
@pytest.mark.parametrize("name,year", [
|
||||
("Matrix.1999.1080p.BluRay.mkv", "1999"),
|
||||
("Šíleně smutná princezna (1968).mkv", "1968"),
|
||||
("Movie 2001 2010 remux.mkv", "2010"), # last standalone year wins
|
||||
("No Year Here.mkv", None),
|
||||
("Film 12345.mkv", None), # not a bare 4-digit year
|
||||
])
|
||||
@pytest.mark.parametrize(
|
||||
"name,year",
|
||||
[
|
||||
("Matrix.1999.1080p.BluRay.mkv", "1999"),
|
||||
("Šíleně smutná princezna (1968).mkv", "1968"),
|
||||
("Movie 2001 2010 remux.mkv", "2010"), # last standalone year wins
|
||||
("No Year Here.mkv", None),
|
||||
("Film 12345.mkv", None), # not a bare 4-digit year
|
||||
],
|
||||
)
|
||||
def test_year_from_filename(name, year):
|
||||
assert year_from_filename(name) == year
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"name,fmt",
|
||||
[
|
||||
("Avatar.2009.3D.HSBS.BluRay.mkv", "HSBS"),
|
||||
("Avatar.2009.3d.hsbs.mkv", "HSBS"),
|
||||
("Avatar (2009) 3D_FTAB 1080p.mkv", "FTAB"),
|
||||
("Avatar (2009) - 3D-MVC.mkv", "MVC"),
|
||||
("Avatar (2009).mkv", None),
|
||||
("Premium3D.HSBS.mkv", None), # "3D" glued to a word doesn't count
|
||||
("Avatar.2009.HSBS.mkv", None), # no "3D" token at all
|
||||
],
|
||||
)
|
||||
def test_three_d_format_from_filename(name, fmt):
|
||||
assert three_d_format_from_filename(name) == fmt
|
||||
|
||||
+26
-10
@@ -1,4 +1,5 @@
|
||||
"""Tests for the NFO reader (src.core.nfo)."""
|
||||
|
||||
from src.core.nfo import NfoData, find_nfo, read_nfo
|
||||
|
||||
MOVIE_NFO = """<?xml version="1.0" encoding="utf-8"?>
|
||||
@@ -26,14 +27,20 @@ def test_read_nfo_sibling(tmp_path):
|
||||
|
||||
nfo = read_nfo(video)
|
||||
assert nfo == NfoData(
|
||||
title="Matrix", year="1999", genres=["Akční", "Sci-Fi"],
|
||||
countries=["USA"], rating=87, tmdb_id=603)
|
||||
title="Matrix",
|
||||
year="1999",
|
||||
genres=["Akční", "Sci-Fi"],
|
||||
countries=["USA"],
|
||||
rating=87,
|
||||
tmdb_id=603,
|
||||
)
|
||||
|
||||
|
||||
def test_rating_scaled_to_percent(tmp_path):
|
||||
video = _movie(tmp_path)
|
||||
video.with_suffix(".nfo").write_text(
|
||||
"<movie><rating>7.3</rating></movie>", encoding="utf-8")
|
||||
"<movie><rating>7.3</rating></movie>", encoding="utf-8"
|
||||
)
|
||||
assert read_nfo(video).rating == 73
|
||||
|
||||
|
||||
@@ -41,21 +48,25 @@ def test_rating_from_ratings_block(tmp_path):
|
||||
video = _movie(tmp_path)
|
||||
video.with_suffix(".nfo").write_text(
|
||||
"<movie><ratings><rating name='tmdb'><value>9.1</value></rating>"
|
||||
"</ratings></movie>", encoding="utf-8")
|
||||
"</ratings></movie>",
|
||||
encoding="utf-8",
|
||||
)
|
||||
assert read_nfo(video).rating == 91
|
||||
|
||||
|
||||
def test_tmdbid_from_tmdbid_tag(tmp_path):
|
||||
video = _movie(tmp_path)
|
||||
video.with_suffix(".nfo").write_text(
|
||||
"<movie><tmdbid>603</tmdbid></movie>", encoding="utf-8")
|
||||
"<movie><tmdbid>603</tmdbid></movie>", encoding="utf-8"
|
||||
)
|
||||
assert read_nfo(video).tmdb_id == 603
|
||||
|
||||
|
||||
def test_csfd_url_from_uniqueid_bare_id(tmp_path):
|
||||
video = _movie(tmp_path)
|
||||
video.with_suffix(".nfo").write_text(
|
||||
'<movie><uniqueid type="csfd">9499</uniqueid></movie>', encoding="utf-8")
|
||||
'<movie><uniqueid type="csfd">9499</uniqueid></movie>', encoding="utf-8"
|
||||
)
|
||||
assert read_nfo(video).csfd_url == "https://www.csfd.cz/film/9499/"
|
||||
|
||||
|
||||
@@ -63,21 +74,25 @@ def test_csfd_url_from_uniqueid_full_url(tmp_path):
|
||||
video = _movie(tmp_path)
|
||||
video.with_suffix(".nfo").write_text(
|
||||
'<movie><uniqueid type="csfd">https://www.csfd.cz/film/9499-matrix/'
|
||||
'</uniqueid></movie>', encoding="utf-8")
|
||||
"</uniqueid></movie>",
|
||||
encoding="utf-8",
|
||||
)
|
||||
assert read_nfo(video).csfd_url == "https://www.csfd.cz/film/9499-matrix/"
|
||||
|
||||
|
||||
def test_csfd_url_from_legacy_csfdid_tag(tmp_path):
|
||||
video = _movie(tmp_path)
|
||||
video.with_suffix(".nfo").write_text(
|
||||
"<movie><csfdid>12345</csfdid></movie>", encoding="utf-8")
|
||||
"<movie><csfdid>12345</csfdid></movie>", encoding="utf-8"
|
||||
)
|
||||
assert read_nfo(video).csfd_url == "https://www.csfd.cz/film/12345/"
|
||||
|
||||
|
||||
def test_csfd_url_absent_is_none(tmp_path):
|
||||
video = _movie(tmp_path)
|
||||
video.with_suffix(".nfo").write_text(
|
||||
"<movie><title>Matrix</title></movie>", encoding="utf-8")
|
||||
"<movie><title>Matrix</title></movie>", encoding="utf-8"
|
||||
)
|
||||
assert read_nfo(video).csfd_url is None
|
||||
|
||||
|
||||
@@ -115,9 +130,10 @@ def test_field_values():
|
||||
|
||||
def test_refresh_tags_from_nfo(tmp_path, monkeypatch):
|
||||
import src.core.config as config_module
|
||||
|
||||
monkeypatch.setattr(config_module, "GLOBAL_CONFIG_FILE", tmp_path / ".gtag")
|
||||
from src.core.tag_manager import TagManager
|
||||
from src.core.file_manager import FileManager
|
||||
from src.core.tag_manager import TagManager
|
||||
|
||||
fm = FileManager(TagManager())
|
||||
fm.set_pool_dir(tmp_path / "pool")
|
||||
|
||||
+55
-24
@@ -1,7 +1,7 @@
|
||||
import json
|
||||
|
||||
from src.core.pool_index import PoolIndex, INDEX_FILENAME, INDEX_VERSION
|
||||
from src.core.file import File
|
||||
from src.core.pool_index import INDEX_FILENAME, INDEX_VERSION, PoolIndex
|
||||
from src.core.tag_manager import TagManager
|
||||
|
||||
|
||||
@@ -53,9 +53,13 @@ class TestPoolIndex:
|
||||
|
||||
def test_find_movie_by_media(self, tmp_path):
|
||||
index = PoolIndex(tmp_path)
|
||||
index.set_movie("tmdb-603", {
|
||||
"title": "Matrix",
|
||||
"media": [{"role": "video", "label": "", "file": "Filmy/Matrix.mkv"}]})
|
||||
index.set_movie(
|
||||
"tmdb-603",
|
||||
{
|
||||
"title": "Matrix",
|
||||
"media": [{"role": "video", "label": "", "file": "Filmy/Matrix.mkv"}],
|
||||
},
|
||||
)
|
||||
|
||||
found = index.find_movie_by_media(tmp_path / "Filmy" / "Matrix.mkv")
|
||||
assert found is not None
|
||||
@@ -76,16 +80,23 @@ class TestV1Migration:
|
||||
def _write_v1(self, tmp_path, movies: dict) -> None:
|
||||
(tmp_path / INDEX_FILENAME).write_text(
|
||||
json.dumps({"settings": {"tag_schema": []}, "movies": movies}),
|
||||
encoding="utf-8")
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
def test_flat_movie_becomes_identity_keyed_record(self, tmp_path):
|
||||
self._write_v1(tmp_path, {
|
||||
"Filmy/Matrix (1999) [tmdbid-603].mkv": {
|
||||
"title": "Matrix", "tags": ["Žánr/Akční"],
|
||||
"csfd_url": "https://www.csfd.cz/film/9499/",
|
||||
"date": "1999-03-31", "added": "2026-01-01T00:00:00",
|
||||
"attributes": {"collection_sort": "01"},
|
||||
}})
|
||||
self._write_v1(
|
||||
tmp_path,
|
||||
{
|
||||
"Filmy/Matrix (1999) [tmdbid-603].mkv": {
|
||||
"title": "Matrix",
|
||||
"tags": ["Žánr/Akční"],
|
||||
"csfd_url": "https://www.csfd.cz/film/9499/",
|
||||
"date": "1999-03-31",
|
||||
"added": "2026-01-01T00:00:00",
|
||||
"attributes": {"collection_sort": "01"},
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
index = PoolIndex(tmp_path)
|
||||
|
||||
@@ -96,18 +107,29 @@ class TestV1Migration:
|
||||
assert record["tmdb_id"] == 603
|
||||
assert record["library"] == "Filmy"
|
||||
assert record["media"] == [
|
||||
{"role": "video", "label": "",
|
||||
"file": "Filmy/Matrix (1999) [tmdbid-603].mkv"}]
|
||||
{
|
||||
"role": "video",
|
||||
"label": "",
|
||||
"file": "Filmy/Matrix (1999) [tmdbid-603].mkv",
|
||||
}
|
||||
]
|
||||
assert record["tags"] == ["Žánr/Akční"]
|
||||
assert record["csfd_url"] == "https://www.csfd.cz/film/9499/"
|
||||
assert record["attributes"] == {"collection_sort": "01"}
|
||||
|
||||
def test_grouped_versions_merge_into_one_record(self, tmp_path):
|
||||
base = "Filmy/Avatar (2009) [tmdbid-19995]"
|
||||
self._write_v1(tmp_path, {
|
||||
f"{base}/Avatar (2009) [tmdbid-19995] - Výchozí.mkv": {"title": "Avatar"},
|
||||
f"{base}/Avatar (2009) [tmdbid-19995] - 3D HSBS.mkv": {"title": "Avatar"},
|
||||
})
|
||||
self._write_v1(
|
||||
tmp_path,
|
||||
{
|
||||
f"{base}/Avatar (2009) [tmdbid-19995] - Výchozí.mkv": {
|
||||
"title": "Avatar"
|
||||
},
|
||||
f"{base}/Avatar (2009) [tmdbid-19995] - 3D HSBS.mkv": {
|
||||
"title": "Avatar"
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
index = PoolIndex(tmp_path)
|
||||
|
||||
@@ -117,8 +139,7 @@ class TestV1Migration:
|
||||
assert record["library"] == "Filmy"
|
||||
|
||||
def test_movie_without_tmdbid_gets_slug_key(self, tmp_path):
|
||||
self._write_v1(tmp_path, {
|
||||
"Filmy/Pelíšky (1999).mkv": {"title": "Pelíšky"}})
|
||||
self._write_v1(tmp_path, {"Filmy/Pelíšky (1999).mkv": {"title": "Pelíšky"}})
|
||||
|
||||
index = PoolIndex(tmp_path)
|
||||
|
||||
@@ -131,7 +152,10 @@ class TestV1Migration:
|
||||
|
||||
backup = tmp_path / (INDEX_FILENAME + ".v1.bak")
|
||||
assert backup.exists()
|
||||
assert "Filmy/A (2000).mkv" in json.loads(backup.read_text(encoding="utf-8"))["movies"]
|
||||
assert (
|
||||
"Filmy/A (2000).mkv"
|
||||
in json.loads(backup.read_text(encoding="utf-8"))["movies"]
|
||||
)
|
||||
on_disk = json.loads((tmp_path / INDEX_FILENAME).read_text(encoding="utf-8"))
|
||||
assert on_disk["version"] == INDEX_VERSION
|
||||
|
||||
@@ -141,9 +165,16 @@ class TestV1Migration:
|
||||
assert index.get_setting("tag_schema") == []
|
||||
|
||||
def test_v2_index_is_not_migrated_again(self, tmp_path):
|
||||
(tmp_path / INDEX_FILENAME).write_text(json.dumps({
|
||||
"version": INDEX_VERSION, "settings": {},
|
||||
"movies": {"tmdb-1": {"title": "X", "media": []}}}), encoding="utf-8")
|
||||
(tmp_path / INDEX_FILENAME).write_text(
|
||||
json.dumps(
|
||||
{
|
||||
"version": INDEX_VERSION,
|
||||
"settings": {},
|
||||
"movies": {"tmdb-1": {"title": "X", "media": []}},
|
||||
}
|
||||
),
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
index = PoolIndex(tmp_path)
|
||||
|
||||
|
||||
+304
-49
@@ -1,12 +1,16 @@
|
||||
"""Tests for series naming/scanning (src.core.series) + FileManager episodes."""
|
||||
|
||||
import pytest
|
||||
|
||||
from src.core.series import (
|
||||
Episode,
|
||||
collect_episode_files,
|
||||
episode_filename,
|
||||
is_canonical_episode,
|
||||
is_season_folder,
|
||||
parse_episode,
|
||||
parse_episode_code,
|
||||
parse_episode_part,
|
||||
sanitize_title,
|
||||
scan_series,
|
||||
season_folder_name,
|
||||
@@ -14,14 +18,17 @@ from src.core.series import (
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("name,expected", [
|
||||
("Breaking Bad S01E02.mkv", (1, 2)),
|
||||
("bb.s1e2.mkv", (1, 2)),
|
||||
("Show 01x02 x264.mkv", (1, 2)),
|
||||
("Series.S01E02E03.mkv", (1, 2)), # multi-ep → first
|
||||
("Series S12E145.mkv", (12, 145)),
|
||||
("No code here.mkv", None),
|
||||
])
|
||||
@pytest.mark.parametrize(
|
||||
"name,expected",
|
||||
[
|
||||
("Breaking Bad S01E02.mkv", (1, 2)),
|
||||
("bb.s1e2.mkv", (1, 2)),
|
||||
("Show 01x02 x264.mkv", (1, 2)),
|
||||
("Series.S01E02E03.mkv", (1, 2)), # multi-ep → first
|
||||
("Series S12E145.mkv", (12, 145)),
|
||||
("No code here.mkv", None),
|
||||
],
|
||||
)
|
||||
def test_parse_episode(name, expected):
|
||||
assert parse_episode(name) == expected
|
||||
|
||||
@@ -36,8 +43,10 @@ def test_episode_filename_is_code_plus_series_title():
|
||||
assert episode_filename(1, 2, "Most!", ".mkv") == "S01E02 - Most!.mkv"
|
||||
assert episode_filename(0, 3, "Most!", ".mkv") == "S00E03 - Most!.mkv"
|
||||
# release string is NOT kept — only the code + series title
|
||||
assert episode_filename(12, 145, "Breaking Bad", ".mkv") == \
|
||||
"S12E145 - Breaking Bad.mkv"
|
||||
assert (
|
||||
episode_filename(12, 145, "Breaking Bad", ".mkv")
|
||||
== "S12E145 - Breaking Bad.mkv"
|
||||
)
|
||||
# empty / unusable title → just the code
|
||||
assert episode_filename(3, 4, "", ".mkv") == "S03E04.mkv"
|
||||
assert episode_filename(3, 4, "/", ".mkv") == "S03E04.mkv"
|
||||
@@ -45,7 +54,7 @@ def test_episode_filename_is_code_plus_series_title():
|
||||
|
||||
def test_sanitize_title():
|
||||
assert sanitize_title("Most!") == "Most!"
|
||||
assert sanitize_title('A: B / C D') == "A B C D"
|
||||
assert sanitize_title("A: B / C D") == "A B C D"
|
||||
assert sanitize_title(" trailing. ") == "trailing"
|
||||
|
||||
|
||||
@@ -58,11 +67,61 @@ def test_is_season_folder():
|
||||
|
||||
|
||||
def test_series_folder_stem():
|
||||
assert series_folder_stem("Breaking Bad", 2008, 1396) == "Breaking Bad (2008) [tmdbid-1396]"
|
||||
assert (
|
||||
series_folder_stem("Breaking Bad", 2008, 1396)
|
||||
== "Breaking Bad (2008) [tmdbid-1396]"
|
||||
)
|
||||
assert series_folder_stem("Neznámý", None, 42) == "Neznámý [tmdbid-42]"
|
||||
assert series_folder_stem("Jen název", None, None) == "Jen název"
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"name,expected",
|
||||
[
|
||||
("Show S01E02.pt2.mkv", 2),
|
||||
("Show S01E01-part-1.mkv", 1),
|
||||
("Show S01E01 part 1.mkv", 1),
|
||||
("Show S01E01_cd1.mkv", 1),
|
||||
("Show S01E01.disc2.mkv", 2),
|
||||
("Show S01E01.mkv", None),
|
||||
("Show Abcd2.mkv", None), # "cd2" mid-word — not a stacking suffix
|
||||
("Show Part of Something.mkv", None), # no digit right after → not a match
|
||||
],
|
||||
)
|
||||
def test_parse_episode_part(name, expected):
|
||||
assert parse_episode_part(name) == expected
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"text,expected",
|
||||
[
|
||||
("S01E02P3", (1, 2, 3)),
|
||||
("s1e2p3", (1, 2, 3)),
|
||||
("S01E02 pt3", (1, 2, 3)),
|
||||
("S01E02.part.3", (1, 2, 3)),
|
||||
("S01E02 cd2", (1, 2, 2)),
|
||||
("S01E02", (1, 2, None)),
|
||||
("1x02", (1, 2, None)),
|
||||
("no code here", None),
|
||||
],
|
||||
)
|
||||
def test_parse_episode_code(text, expected):
|
||||
assert parse_episode_code(text) == expected
|
||||
|
||||
|
||||
def test_episode_filename_with_part():
|
||||
assert episode_filename(1, 2, "Most!", ".mkv", part=3) == "S01E02 - Most!.pt3.mkv"
|
||||
assert episode_filename(1, 2, "", ".mkv", part=2) == "S01E02.pt2.mkv"
|
||||
# no part → unchanged from the plain form
|
||||
assert episode_filename(1, 2, "Most!", ".mkv", part=None) == "S01E02 - Most!.mkv"
|
||||
|
||||
|
||||
def test_episode_code_property_includes_part():
|
||||
assert Episode(None, 1, 2, part=3).code == "S01E02.pt3"
|
||||
assert Episode(None, 1, 2).code == "S01E02"
|
||||
assert Episode(None, None, None).code == "?"
|
||||
|
||||
|
||||
def test_is_canonical_episode(tmp_path):
|
||||
folder = tmp_path / "Breaking Bad (2008) [tmdbid-1396]" / "Season 01"
|
||||
folder.mkdir(parents=True)
|
||||
@@ -77,6 +136,17 @@ def test_is_canonical_episode(tmp_path):
|
||||
assert not is_canonical_episode(wrong_body, "Breaking Bad")
|
||||
|
||||
|
||||
def test_is_canonical_episode_with_part(tmp_path):
|
||||
folder = tmp_path / "Show (2008) [tmdbid-1]" / "Season 01"
|
||||
folder.mkdir(parents=True)
|
||||
good = folder / "S01E02 - Show.pt1.mkv"
|
||||
good.write_bytes(b"x")
|
||||
old_style = folder / "S01E02-part-1.mkv" # recognised, but not our own form
|
||||
old_style.write_bytes(b"x")
|
||||
assert is_canonical_episode(good, "Show")
|
||||
assert not is_canonical_episode(old_style, "Show")
|
||||
|
||||
|
||||
def test_scan_series(tmp_path):
|
||||
lib = tmp_path / "Seriály"
|
||||
s1 = lib / "Breaking Bad (2008) [tmdbid-1396]" / "Season 01"
|
||||
@@ -95,20 +165,36 @@ def test_scan_series(tmp_path):
|
||||
assert list(seasons.keys()) == [1] and len(seasons[1]) == 2
|
||||
|
||||
|
||||
def test_scan_series_reads_episode_parts(tmp_path):
|
||||
lib = tmp_path / "Seriály"
|
||||
s1 = lib / "Show (2008) [tmdbid-1]" / "Season 01"
|
||||
s1.mkdir(parents=True)
|
||||
(s1 / "Show S01E02.pt1.mkv").write_bytes(b"x")
|
||||
(s1 / "Show S01E02.pt2.mkv").write_bytes(b"x")
|
||||
|
||||
ser = scan_series(lib, "Seriály")[0]
|
||||
parts = sorted(ep.part for ep in ser.episodes)
|
||||
assert parts == [1, 2]
|
||||
assert {ep.code for ep in ser.episodes} == {"S01E02.pt1", "S01E02.pt2"}
|
||||
|
||||
|
||||
def test_collect_episode_files_walks_subfolders(tmp_path):
|
||||
root = tmp_path / "Breaking.Bad.COMPLETE"
|
||||
(root / "Season 01").mkdir(parents=True)
|
||||
(root / "Season 02").mkdir(parents=True)
|
||||
(root / "Season 01" / "BB S01E01.mkv").write_bytes(b"x")
|
||||
(root / "Season 01" / "BB S01E02.mkv").write_bytes(b"x")
|
||||
(root / "Season 01" / "BB S01E02.srt").write_bytes(b"x") # sidecar → skipped
|
||||
(root / "Season 01" / "BB S01E02.srt").write_bytes(b"x") # sidecar → skipped
|
||||
(root / "Season 02" / "BB S02E01.avi").write_bytes(b"x")
|
||||
(root / "readme.nfo").write_bytes(b"x") # sidecar → skipped
|
||||
(root / "readme.nfo").write_bytes(b"x") # sidecar → skipped
|
||||
|
||||
files = collect_episode_files(root)
|
||||
|
||||
assert [f.name for f in files] == [
|
||||
"BB S01E01.mkv", "BB S01E02.mkv", "BB S02E01.avi"]
|
||||
"BB S01E01.mkv",
|
||||
"BB S01E02.mkv",
|
||||
"BB S02E01.avi",
|
||||
]
|
||||
|
||||
|
||||
def test_collect_episode_files_missing_folder_is_empty(tmp_path):
|
||||
@@ -119,9 +205,11 @@ class TestFileManagerSeries:
|
||||
@pytest.fixture
|
||||
def file_manager(self, tmp_path, monkeypatch):
|
||||
import src.core.config as config_module
|
||||
|
||||
monkeypatch.setattr(config_module, "GLOBAL_CONFIG_FILE", tmp_path / ".gtag")
|
||||
from src.core.tag_manager import TagManager
|
||||
from src.core.file_manager import FileManager
|
||||
from src.core.tag_manager import TagManager
|
||||
|
||||
fm = FileManager(TagManager())
|
||||
fm.set_pool_dir(tmp_path / "pool")
|
||||
return fm
|
||||
@@ -131,11 +219,19 @@ class TestFileManagerSeries:
|
||||
(tmp_path / n).write_bytes(b"x")
|
||||
result = file_manager.import_episodes(
|
||||
[tmp_path / "bb.S01E01.mkv", tmp_path / "bb.S01E02.mkv"],
|
||||
"Breaking Bad", year=2008, tmdb_id=1396)
|
||||
"Breaking Bad",
|
||||
year=2008,
|
||||
tmdb_id=1396,
|
||||
)
|
||||
|
||||
assert result["imported"] == 2
|
||||
base = (tmp_path / "pool" / "Seriály"
|
||||
/ "Breaking Bad (2008) [tmdbid-1396]" / "Season 01")
|
||||
base = (
|
||||
tmp_path
|
||||
/ "pool"
|
||||
/ "Seriály"
|
||||
/ "Breaking Bad (2008) [tmdbid-1396]"
|
||||
/ "Season 01"
|
||||
)
|
||||
assert (base / "S01E01 - Breaking Bad.mkv").exists()
|
||||
assert (base / "S01E02 - Breaking Bad.mkv").exists()
|
||||
|
||||
@@ -143,12 +239,22 @@ class TestFileManagerSeries:
|
||||
file_manager.set_libraries([{"name": "Hlavní", "kind": "mixed"}])
|
||||
(tmp_path / "bb.S01E01.mkv").write_bytes(b"x")
|
||||
result = file_manager.import_episodes(
|
||||
[tmp_path / "bb.S01E01.mkv"], "Breaking Bad", year=2008, tmdb_id=1396,
|
||||
library="Hlavní")
|
||||
[tmp_path / "bb.S01E01.mkv"],
|
||||
"Breaking Bad",
|
||||
year=2008,
|
||||
tmdb_id=1396,
|
||||
library="Hlavní",
|
||||
)
|
||||
|
||||
assert result["imported"] == 1
|
||||
assert (tmp_path / "pool" / "Hlavní" / "Breaking Bad (2008) [tmdbid-1396]"
|
||||
/ "Season 01" / "S01E01 - Breaking Bad.mkv").exists()
|
||||
assert (
|
||||
tmp_path
|
||||
/ "pool"
|
||||
/ "Hlavní"
|
||||
/ "Breaking Bad (2008) [tmdbid-1396]"
|
||||
/ "Season 01"
|
||||
/ "S01E01 - Breaking Bad.mkv"
|
||||
).exists()
|
||||
# a mixed library is scanned for series too
|
||||
found = file_manager.load_series()
|
||||
assert [s.title for s in found] == ["Breaking Bad"]
|
||||
@@ -156,7 +262,8 @@ class TestFileManagerSeries:
|
||||
def test_import_episodes_skips_uncoded(self, file_manager, tmp_path):
|
||||
(tmp_path / "random.mkv").write_bytes(b"x")
|
||||
result = file_manager.import_episodes(
|
||||
[tmp_path / "random.mkv"], "Breaking Bad", year=2008)
|
||||
[tmp_path / "random.mkv"], "Breaking Bad", year=2008
|
||||
)
|
||||
assert result["imported"] == 0
|
||||
assert result["skipped_no_code"] == ["random.mkv"]
|
||||
|
||||
@@ -165,7 +272,10 @@ class TestFileManagerSeries:
|
||||
(tmp_path / "bb.S02E01.mkv").write_bytes(b"x")
|
||||
file_manager.import_episodes(
|
||||
[tmp_path / "bb.S01E01.mkv", tmp_path / "bb.S02E01.mkv"],
|
||||
"Breaking Bad", year=2008, tmdb_id=1396)
|
||||
"Breaking Bad",
|
||||
year=2008,
|
||||
tmdb_id=1396,
|
||||
)
|
||||
(ser,) = file_manager.load_series()
|
||||
assert ser.folder.is_dir()
|
||||
|
||||
@@ -176,6 +286,7 @@ class TestFileManagerSeries:
|
||||
|
||||
def test_remove_series_refuses_folder_outside_pool(self, file_manager, tmp_path):
|
||||
from src.core.series import Series
|
||||
|
||||
outside = tmp_path / "not-the-pool" / "Show (2020)"
|
||||
outside.mkdir(parents=True)
|
||||
bogus = Series(outside, "Show", "2020", None, "Seriály")
|
||||
@@ -183,26 +294,141 @@ class TestFileManagerSeries:
|
||||
file_manager.remove_series(bogus)
|
||||
assert outside.is_dir() # untouched
|
||||
|
||||
def test_remove_episodes_deletes_files_and_subtitles(self, file_manager, tmp_path):
|
||||
(tmp_path / "bb.S01E01.mkv").write_bytes(b"x")
|
||||
(tmp_path / "bb.S01E02.mkv").write_bytes(b"x")
|
||||
file_manager.import_episodes(
|
||||
[tmp_path / "bb.S01E01.mkv", tmp_path / "bb.S01E02.mkv"],
|
||||
"Breaking Bad",
|
||||
year=2008,
|
||||
tmdb_id=1396,
|
||||
)
|
||||
(ser,) = file_manager.load_series()
|
||||
season = ser.seasons()[1]
|
||||
e1 = next(e for e in season if e.episode == 1)
|
||||
e2 = next(e for e in season if e.episode == 2)
|
||||
srt = e1.path.with_suffix(".srt")
|
||||
srt.write_bytes(b"x")
|
||||
|
||||
file_manager.remove_episodes([e1])
|
||||
|
||||
assert not e1.path.exists()
|
||||
assert not srt.exists()
|
||||
assert e2.path.exists() # untouched
|
||||
assert e1.path.parent.is_dir() # not empty — e2 is still there
|
||||
|
||||
def test_remove_episodes_cleans_up_the_now_empty_season_folder(
|
||||
self, file_manager, tmp_path
|
||||
):
|
||||
(tmp_path / "bb.S01E01.mkv").write_bytes(b"x")
|
||||
file_manager.import_episodes(
|
||||
[tmp_path / "bb.S01E01.mkv"], "Breaking Bad", year=2008, tmdb_id=1396
|
||||
)
|
||||
(ser,) = file_manager.load_series()
|
||||
(ep,) = ser.episodes
|
||||
season_folder = ep.path.parent
|
||||
|
||||
file_manager.remove_episodes([ep])
|
||||
|
||||
assert not ep.path.exists()
|
||||
assert not season_folder.exists()
|
||||
|
||||
def test_remove_season_deletes_every_episode_in_it(self, file_manager, tmp_path):
|
||||
(tmp_path / "bb.S01E01.mkv").write_bytes(b"x")
|
||||
(tmp_path / "bb.S01E02.mkv").write_bytes(b"x")
|
||||
(tmp_path / "bb.S02E01.mkv").write_bytes(b"x")
|
||||
file_manager.import_episodes(
|
||||
[
|
||||
tmp_path / "bb.S01E01.mkv",
|
||||
tmp_path / "bb.S01E02.mkv",
|
||||
tmp_path / "bb.S02E01.mkv",
|
||||
],
|
||||
"Breaking Bad",
|
||||
year=2008,
|
||||
tmdb_id=1396,
|
||||
)
|
||||
(ser,) = file_manager.load_series()
|
||||
season1_folder = ser.seasons()[1][0].path.parent
|
||||
|
||||
file_manager.remove_season(ser, 1)
|
||||
|
||||
assert not season1_folder.exists()
|
||||
(ser_after,) = file_manager.load_series()
|
||||
assert [ep.season for ep in ser_after.episodes] == [2]
|
||||
|
||||
def test_remove_season_raises_for_a_season_with_no_episodes(
|
||||
self, file_manager, tmp_path
|
||||
):
|
||||
(tmp_path / "bb.S01E01.mkv").write_bytes(b"x")
|
||||
file_manager.import_episodes(
|
||||
[tmp_path / "bb.S01E01.mkv"], "Breaking Bad", year=2008, tmdb_id=1396
|
||||
)
|
||||
(ser,) = file_manager.load_series()
|
||||
with pytest.raises(ValueError):
|
||||
file_manager.remove_season(ser, 9)
|
||||
|
||||
def test_import_episodes_uses_explicit_codes_override(self, file_manager, tmp_path):
|
||||
uncoded = tmp_path / "pilot.mkv"
|
||||
uncoded.write_bytes(b"x")
|
||||
misparsed = tmp_path / "bb 2008 e07.mkv" # "2008" would parse as 20x08
|
||||
misparsed.write_bytes(b"x")
|
||||
result = file_manager.import_episodes(
|
||||
[uncoded, misparsed], "Breaking Bad", year=2008, tmdb_id=1396,
|
||||
codes={uncoded: (1, 1), misparsed: (1, 7)})
|
||||
[uncoded, misparsed],
|
||||
"Breaking Bad",
|
||||
year=2008,
|
||||
tmdb_id=1396,
|
||||
codes={uncoded: (1, 1), misparsed: (1, 7)},
|
||||
)
|
||||
|
||||
assert result["imported"] == 2
|
||||
base = (tmp_path / "pool" / "Seriály"
|
||||
/ "Breaking Bad (2008) [tmdbid-1396]" / "Season 01")
|
||||
base = (
|
||||
tmp_path
|
||||
/ "pool"
|
||||
/ "Seriály"
|
||||
/ "Breaking Bad (2008) [tmdbid-1396]"
|
||||
/ "Season 01"
|
||||
)
|
||||
assert (base / "S01E01 - Breaking Bad.mkv").exists()
|
||||
assert (base / "S01E07 - Breaking Bad.mkv").exists()
|
||||
assert result["skipped_no_code"] == []
|
||||
|
||||
def test_import_episodes_uses_explicit_part_override(self, file_manager, tmp_path):
|
||||
tape1 = tmp_path / "tape1.mkv"
|
||||
tape1.write_bytes(b"x")
|
||||
tape2 = tmp_path / "tape2.mkv"
|
||||
tape2.write_bytes(b"x")
|
||||
result = file_manager.import_episodes(
|
||||
[tape1, tape2],
|
||||
"Breaking Bad",
|
||||
year=2008,
|
||||
tmdb_id=1396,
|
||||
codes={tape1: (1, 1, 1), tape2: (1, 1, 2)},
|
||||
)
|
||||
|
||||
assert result["imported"] == 2
|
||||
base = (
|
||||
tmp_path
|
||||
/ "pool"
|
||||
/ "Seriály"
|
||||
/ "Breaking Bad (2008) [tmdbid-1396]"
|
||||
/ "Season 01"
|
||||
)
|
||||
assert (base / "S01E01 - Breaking Bad.pt1.mkv").exists()
|
||||
assert (base / "S01E01 - Breaking Bad.pt2.mkv").exists()
|
||||
|
||||
def test_import_episodes_reads_part_from_filename(self, file_manager, tmp_path):
|
||||
tape = tmp_path / "Show.S01E01.pt2.mkv"
|
||||
tape.write_bytes(b"x")
|
||||
|
||||
result = file_manager.import_episodes([tape], "Show", year=2020, tmdb_id=5)
|
||||
|
||||
base = tmp_path / "pool" / "Seriály" / "Show (2020) [tmdbid-5]" / "Season 01"
|
||||
assert (base / "S01E01 - Show.pt2.mkv").exists()
|
||||
assert result["imported"] == 1
|
||||
|
||||
def test_rename_episodes_to_canonical(self, file_manager, tmp_path):
|
||||
# a mis-named episode sitting in the wrong place
|
||||
folder = (tmp_path / "pool" / "Seriály"
|
||||
/ "Breaking Bad (2008) [tmdbid-1396]")
|
||||
folder = tmp_path / "pool" / "Seriály" / "Breaking Bad (2008) [tmdbid-1396]"
|
||||
folder.mkdir(parents=True)
|
||||
(folder / "bb.1x03.HDTV.mkv").write_bytes(b"x")
|
||||
|
||||
@@ -213,16 +439,33 @@ class TestFileManagerSeries:
|
||||
assert result["renamed"] == [("bb.1x03.HDTV.mkv", "S01E03 - Breaking Bad.mkv")]
|
||||
assert (folder / "Season 01" / "S01E03 - Breaking Bad.mkv").exists()
|
||||
|
||||
def test_rename_episodes_to_canonical_keeps_the_part_tag(
|
||||
self, file_manager, tmp_path
|
||||
):
|
||||
folder = tmp_path / "pool" / "Seriály" / "Breaking Bad (2008) [tmdbid-1396]"
|
||||
folder.mkdir(parents=True)
|
||||
(folder / "bb.1x03.HDTV-part2.mkv").write_bytes(b"x")
|
||||
|
||||
result = file_manager.rename_episodes_to_canonical(
|
||||
file_manager.load_series()[0]
|
||||
)
|
||||
|
||||
assert result["renamed"] == [
|
||||
("bb.1x03.HDTV-part2.mkv", "S01E03 - Breaking Bad.pt2.mkv")
|
||||
]
|
||||
assert (folder / "Season 01" / "S01E03 - Breaking Bad.pt2.mkv").exists()
|
||||
|
||||
def test_rename_episodes_to_canonical_carries_subtitle_along(
|
||||
self, file_manager, tmp_path
|
||||
):
|
||||
folder = (tmp_path / "pool" / "Seriály"
|
||||
/ "Breaking Bad (2008) [tmdbid-1396]")
|
||||
folder = tmp_path / "pool" / "Seriály" / "Breaking Bad (2008) [tmdbid-1396]"
|
||||
folder.mkdir(parents=True)
|
||||
(folder / "bb.1x03.HDTV.mkv").write_bytes(b"x")
|
||||
(folder / "bb.1x03.HDTV.cs.srt").write_bytes(b"x")
|
||||
|
||||
result = file_manager.rename_episodes_to_canonical(file_manager.load_series()[0])
|
||||
result = file_manager.rename_episodes_to_canonical(
|
||||
file_manager.load_series()[0]
|
||||
)
|
||||
|
||||
assert result["renamed"] == [("bb.1x03.HDTV.mkv", "S01E03 - Breaking Bad.mkv")]
|
||||
assert (folder / "Season 01" / "S01E03 - Breaking Bad.cs.srt").exists()
|
||||
@@ -232,39 +475,51 @@ class TestFileManagerSeries:
|
||||
seri = tmp_path / "pool" / "Seriály"
|
||||
# series A: mis-named episodes in a non-padded season folder
|
||||
(seri / "Fargo (2014) [tmdbid-60622]" / "Season 1").mkdir(parents=True)
|
||||
(seri / "Fargo (2014) [tmdbid-60622]" / "Season 1"
|
||||
/ "fargo.1x01.1080p.WEB.mkv").write_bytes(b"x")
|
||||
(
|
||||
seri
|
||||
/ "Fargo (2014) [tmdbid-60622]"
|
||||
/ "Season 1"
|
||||
/ "fargo.1x01.1080p.WEB.mkv"
|
||||
).write_bytes(b"x")
|
||||
# series B: one good, one without a code
|
||||
(seri / "Most! (2019) [tmdbid-90000]" / "Season 01").mkdir(parents=True)
|
||||
(seri / "Most! (2019) [tmdbid-90000]" / "Season 01"
|
||||
/ "S01E02 - Most!.mkv").write_bytes(b"x")
|
||||
(seri / "Most! (2019) [tmdbid-90000]" / "Season 01"
|
||||
/ "necoZvlastniho.mkv").write_bytes(b"x")
|
||||
(
|
||||
seri / "Most! (2019) [tmdbid-90000]" / "Season 01" / "S01E02 - Most!.mkv"
|
||||
).write_bytes(b"x")
|
||||
(
|
||||
seri / "Most! (2019) [tmdbid-90000]" / "Season 01" / "necoZvlastniho.mkv"
|
||||
).write_bytes(b"x")
|
||||
|
||||
result = file_manager.rename_all_series_to_canonical()
|
||||
|
||||
assert result["series"] == 2
|
||||
assert ("Fargo", "fargo.1x01.1080p.WEB.mkv", "S01E01 - Fargo.mkv") in result["renamed"]
|
||||
assert (seri / "Fargo (2014) [tmdbid-60622]" / "Season 01"
|
||||
/ "S01E01 - Fargo.mkv").exists()
|
||||
assert ("Fargo", "fargo.1x01.1080p.WEB.mkv", "S01E01 - Fargo.mkv") in result[
|
||||
"renamed"
|
||||
]
|
||||
assert (
|
||||
seri / "Fargo (2014) [tmdbid-60622]" / "Season 01" / "S01E01 - Fargo.mkv"
|
||||
).exists()
|
||||
# emptied non-padded season folder swept
|
||||
assert not (seri / "Fargo (2014) [tmdbid-60622]" / "Season 1").exists()
|
||||
# the already-canonical one is untouched, the code-less one is reported
|
||||
assert ("Most!", "necoZvlastniho.mkv") in result["skipped_no_code"]
|
||||
assert (seri / "Most! (2019) [tmdbid-90000]" / "Season 01"
|
||||
/ "S01E02 - Most!.mkv").exists()
|
||||
assert (
|
||||
seri / "Most! (2019) [tmdbid-90000]" / "Season 01" / "S01E02 - Most!.mkv"
|
||||
).exists()
|
||||
|
||||
def test_apply_series_edits_renames_folder_and_episodes(self, file_manager, tmp_path):
|
||||
def test_apply_series_edits_renames_folder_and_episodes(
|
||||
self, file_manager, tmp_path
|
||||
):
|
||||
folder = tmp_path / "pool" / "Seriály" / "Wrong Name"
|
||||
(folder / "Season 01").mkdir(parents=True)
|
||||
(folder / "Season 01" / "old S01E01.mkv").write_bytes(b"x")
|
||||
|
||||
ser = file_manager.load_series()[0]
|
||||
result = file_manager.apply_series_edits(
|
||||
ser, title="Breaking Bad", year=2008, tmdb_id=1396)
|
||||
ser, title="Breaking Bad", year=2008, tmdb_id=1396
|
||||
)
|
||||
|
||||
new_folder = (tmp_path / "pool" / "Seriály"
|
||||
/ "Breaking Bad (2008) [tmdbid-1396]")
|
||||
new_folder = tmp_path / "pool" / "Seriály" / "Breaking Bad (2008) [tmdbid-1396]"
|
||||
assert result["folder"] == new_folder
|
||||
assert (new_folder / "Season 01" / "S01E01 - Breaking Bad.mkv").exists()
|
||||
assert not folder.exists()
|
||||
|
||||
+16
-7
@@ -1,4 +1,5 @@
|
||||
"""Tests for external subtitle sidecars (src.core.subtitles)."""
|
||||
|
||||
import pytest
|
||||
|
||||
from src.core.subtitles import (
|
||||
@@ -21,15 +22,21 @@ class TestSubtitleFilename:
|
||||
assert subtitle_filename("Matrix (1999)", ".srt") == "Matrix (1999).srt"
|
||||
|
||||
def test_with_language(self):
|
||||
assert subtitle_filename("Matrix (1999)", ".srt", "cs") == "Matrix (1999).cs.srt"
|
||||
assert (
|
||||
subtitle_filename("Matrix (1999)", ".srt", "cs") == "Matrix (1999).cs.srt"
|
||||
)
|
||||
|
||||
def test_with_language_and_flag(self):
|
||||
assert subtitle_filename("Matrix (1999)", ".srt", "cs", "forced") == \
|
||||
"Matrix (1999).cs.forced.srt"
|
||||
assert (
|
||||
subtitle_filename("Matrix (1999)", ".srt", "cs", "forced")
|
||||
== "Matrix (1999).cs.forced.srt"
|
||||
)
|
||||
|
||||
def test_uppercase_input_is_lowered(self):
|
||||
assert subtitle_filename("Matrix (1999)", ".SRT", "CS", "FORCED") == \
|
||||
"Matrix (1999).cs.forced.srt"
|
||||
assert (
|
||||
subtitle_filename("Matrix (1999)", ".SRT", "CS", "FORCED")
|
||||
== "Matrix (1999).cs.forced.srt"
|
||||
)
|
||||
|
||||
|
||||
class TestFindSubtitles:
|
||||
@@ -53,7 +60,7 @@ class TestFindSubtitles:
|
||||
def test_ignores_unrelated_and_overlapping_stem(self, tmp_path):
|
||||
video = _video(tmp_path, "Matrix.mkv")
|
||||
(tmp_path / "Matrix Reloaded.srt").write_bytes(b"x") # overlapping prefix
|
||||
(tmp_path / "Matrix.nfo").write_bytes(b"x") # not a subtitle ext
|
||||
(tmp_path / "Matrix.nfo").write_bytes(b"x") # not a subtitle ext
|
||||
(tmp_path / "poster.jpg").write_bytes(b"x")
|
||||
assert find_subtitles(video) == []
|
||||
|
||||
@@ -67,7 +74,9 @@ class TestFindSubtitles:
|
||||
|
||||
def test_label_property(self):
|
||||
assert SubtitleFile(path=None, language="cs").label == "cs"
|
||||
assert SubtitleFile(path=None, language="cs", flag="forced").label == "cs (forced)"
|
||||
assert (
|
||||
SubtitleFile(path=None, language="cs", flag="forced").label == "cs (forced)"
|
||||
)
|
||||
assert SubtitleFile(path=None).label == "?"
|
||||
|
||||
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import pytest
|
||||
from src.core.tag import Tag
|
||||
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import pytest
|
||||
from src.core.tag_manager import TagManager, DEFAULT_TAGS
|
||||
|
||||
from src.core.tag import Tag
|
||||
from src.core.tag_manager import DEFAULT_TAGS, TagManager
|
||||
|
||||
|
||||
class TestTagManager:
|
||||
@@ -55,7 +56,7 @@ class TestTagManager:
|
||||
|
||||
def test_add_tag_creates_category(self, tag_manager):
|
||||
"""Test že add_tag vytvoří kategorii pokud neexistuje"""
|
||||
tag = tag_manager.add_tag("NovaKategorie", "Tag")
|
||||
tag_manager.add_tag("NovaKategorie", "Tag")
|
||||
assert "NovaKategorie" in tag_manager.tags_by_category
|
||||
|
||||
def test_add_multiple_tags_same_category(self, tag_manager):
|
||||
|
||||
+57
-20
@@ -1,4 +1,5 @@
|
||||
"""Tests for the TMDb client (src.core.tmdb). Network is monkeypatched."""
|
||||
|
||||
from types import SimpleNamespace
|
||||
|
||||
import pytest
|
||||
@@ -13,8 +14,9 @@ def _with_key(monkeypatch):
|
||||
|
||||
|
||||
def _resp(status=200, payload=None):
|
||||
return SimpleNamespace(status_code=status, ok=(200 <= status < 300),
|
||||
json=lambda: (payload or {}))
|
||||
return SimpleNamespace(
|
||||
status_code=status, ok=(200 <= status < 300), json=lambda: payload or {}
|
||||
)
|
||||
|
||||
|
||||
def test_api_available_reflects_key(monkeypatch):
|
||||
@@ -24,36 +26,56 @@ def test_api_available_reflects_key(monkeypatch):
|
||||
|
||||
|
||||
def test_lookup_movie_returns_title_and_year(monkeypatch):
|
||||
monkeypatch.setattr(tmdb.requests, "get", lambda *a, **k: _resp(
|
||||
200, {"id": 603, "title": "Matrix", "release_date": "1999-03-31"}))
|
||||
monkeypatch.setattr(
|
||||
tmdb.requests,
|
||||
"get",
|
||||
lambda *a, **k: _resp(
|
||||
200, {"id": 603, "title": "Matrix", "release_date": "1999-03-31"}
|
||||
),
|
||||
)
|
||||
result = tmdb.lookup_movie(603)
|
||||
assert result == TmdbResult(603, "Matrix", "1999", "movie")
|
||||
|
||||
|
||||
def test_lookup_movie_falls_back_to_original_title(monkeypatch):
|
||||
monkeypatch.setattr(tmdb.requests, "get", lambda *a, **k: _resp(
|
||||
200, {"id": 1, "original_title": "Solyaris", "release_date": "1972-01-01"}))
|
||||
monkeypatch.setattr(
|
||||
tmdb.requests,
|
||||
"get",
|
||||
lambda *a, **k: _resp(
|
||||
200, {"id": 1, "original_title": "Solyaris", "release_date": "1972-01-01"}
|
||||
),
|
||||
)
|
||||
assert tmdb.lookup_movie(1).title == "Solyaris"
|
||||
|
||||
|
||||
def test_lookup_movie_missing_date_gives_no_year(monkeypatch):
|
||||
monkeypatch.setattr(tmdb.requests, "get", lambda *a, **k: _resp(
|
||||
200, {"id": 2, "title": "Bez data", "release_date": ""}))
|
||||
monkeypatch.setattr(
|
||||
tmdb.requests,
|
||||
"get",
|
||||
lambda *a, **k: _resp(200, {"id": 2, "title": "Bez data", "release_date": ""}),
|
||||
)
|
||||
assert tmdb.lookup_movie(2).year is None
|
||||
|
||||
|
||||
def test_lookup_tv_uses_name_and_first_air(monkeypatch):
|
||||
monkeypatch.setattr(tmdb.requests, "get", lambda *a, **k: _resp(
|
||||
200, {"id": 1396, "name": "Breaking Bad", "first_air_date": "2008-01-20"}))
|
||||
monkeypatch.setattr(
|
||||
tmdb.requests,
|
||||
"get",
|
||||
lambda *a, **k: _resp(
|
||||
200, {"id": 1396, "name": "Breaking Bad", "first_air_date": "2008-01-20"}
|
||||
),
|
||||
)
|
||||
result = tmdb.lookup(1396, kind="tv")
|
||||
assert result == TmdbResult(1396, "Breaking Bad", "2008", "tv")
|
||||
|
||||
|
||||
def test_search_movie_maps_results(monkeypatch):
|
||||
payload = {"results": [
|
||||
{"id": 603, "title": "Matrix", "release_date": "1999-03-31"},
|
||||
{"id": 605, "title": "Matrix Revolutions", "release_date": "2003-11-05"},
|
||||
]}
|
||||
payload = {
|
||||
"results": [
|
||||
{"id": 603, "title": "Matrix", "release_date": "1999-03-31"},
|
||||
{"id": 605, "title": "Matrix Revolutions", "release_date": "2003-11-05"},
|
||||
]
|
||||
}
|
||||
monkeypatch.setattr(tmdb.requests, "get", lambda *a, **k: _resp(200, payload))
|
||||
results = tmdb.search_movie("Matrix", year="1999")
|
||||
assert [r.tmdb_id for r in results] == [603, 605]
|
||||
@@ -63,16 +85,24 @@ def test_search_movie_maps_results(monkeypatch):
|
||||
def test_search_movie_empty_query_skips_network(monkeypatch):
|
||||
def boom(*a, **k):
|
||||
raise AssertionError("should not hit network")
|
||||
|
||||
monkeypatch.setattr(tmdb.requests, "get", boom)
|
||||
assert tmdb.search_movie(" ") == []
|
||||
|
||||
|
||||
def test_movie_details_maps_fields(monkeypatch):
|
||||
payload = {
|
||||
"id": 603, "title": "Matrix", "release_date": "1999-03-31",
|
||||
"id": 603,
|
||||
"title": "Matrix",
|
||||
"release_date": "1999-03-31",
|
||||
"vote_average": 8.3,
|
||||
"genres": [{"id": 28, "name": "Akční"}, {"id": 878, "name": "Vědeckofantastický"}],
|
||||
"production_countries": [{"iso_3166_1": "US", "name": "United States of America"}],
|
||||
"genres": [
|
||||
{"id": 28, "name": "Akční"},
|
||||
{"id": 878, "name": "Vědeckofantastický"},
|
||||
],
|
||||
"production_countries": [
|
||||
{"iso_3166_1": "US", "name": "United States of America"}
|
||||
],
|
||||
}
|
||||
monkeypatch.setattr(tmdb.requests, "get", lambda *a, **k: _resp(200, payload))
|
||||
d = tmdb.movie_details(603)
|
||||
@@ -85,8 +115,8 @@ def test_movie_details_maps_fields(monkeypatch):
|
||||
|
||||
def test_refresh_tags_from_tmdb(tmp_path, monkeypatch):
|
||||
monkeypatch.setattr("src.core.config.GLOBAL_CONFIG_FILE", tmp_path / ".gtag")
|
||||
from src.core.tag_manager import TagManager
|
||||
from src.core.file_manager import FileManager
|
||||
from src.core.tag_manager import TagManager
|
||||
from src.core.tmdb import TmdbDetails
|
||||
|
||||
fm = FileManager(TagManager())
|
||||
@@ -97,8 +127,14 @@ def test_refresh_tags_from_tmdb(tmp_path, monkeypatch):
|
||||
fm.import_movie(tmp_path / "b.mkv", "Bez ID", year=2000) # no tmdbid
|
||||
|
||||
import src.core.tmdb as tmdb_mod
|
||||
monkeypatch.setattr(tmdb_mod, "movie_details", lambda i: TmdbDetails(
|
||||
int(i), "Matrix", "1999", ["Akční", "Sci-Fi"], ["USA"], 83))
|
||||
|
||||
monkeypatch.setattr(
|
||||
tmdb_mod,
|
||||
"movie_details",
|
||||
lambda i: TmdbDetails(
|
||||
int(i), "Matrix", "1999", ["Akční", "Sci-Fi"], ["USA"], 83
|
||||
),
|
||||
)
|
||||
|
||||
result = fm.refresh_tags_from_tmdb()
|
||||
assert result == {"ok": True, "updated": 1, "without_id": 1, "errors": []}
|
||||
@@ -124,6 +160,7 @@ def test_network_error_raises(monkeypatch):
|
||||
|
||||
def boom(*a, **k):
|
||||
raise requests.ConnectionError("down")
|
||||
|
||||
monkeypatch.setattr(tmdb.requests, "get", boom)
|
||||
with pytest.raises(TmdbError):
|
||||
tmdb.lookup_movie(603)
|
||||
|
||||
+4
-2
@@ -1,5 +1,7 @@
|
||||
import pytest
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from src.core.utils import list_files
|
||||
|
||||
|
||||
@@ -171,7 +173,7 @@ class TestUtils:
|
||||
files = list_files(temp_dir)
|
||||
|
||||
# Najdeme soubor v subdir2
|
||||
file5 = [f for f in files if f.name == "file5.txt"][0]
|
||||
file5 = next(f for f in files if f.name == "file5.txt")
|
||||
|
||||
# Cesta by měla obsahovat obě složky
|
||||
assert "subdir1" in str(file5)
|
||||
|
||||
Reference in New Issue
Block a user