Add per-movie attributes and per-category filename templates
This commit is contained in:
@@ -63,6 +63,7 @@ class TestGlobalConfig:
|
||||
"pool_dir": None,
|
||||
"filmoteka_dir": None,
|
||||
"copyasis_folders": ["Seriály"],
|
||||
"tag_schema": DEFAULT_GLOBAL_CONFIG["tag_schema"],
|
||||
}
|
||||
|
||||
save_global_config(test_config)
|
||||
|
||||
@@ -182,6 +182,26 @@ class TestHelperFunctions:
|
||||
assert rating_band(90) == "90–100 %"
|
||||
assert rating_band(95) == "90–100 %"
|
||||
assert rating_band(100) == "90–100 %"
|
||||
|
||||
def test_csfd_field_values_are_exact_no_transform(self):
|
||||
from src.core.csfd import csfd_field_values
|
||||
movie = CSFDMovie(title="X", url="u", year=1999, rating=86,
|
||||
genres=["Akční", "Sci-Fi"], countries=["USA", "Kanada"])
|
||||
assert csfd_field_values(movie, "genres") == ["Akční", "Sci-Fi"]
|
||||
assert csfd_field_values(movie, "countries") == ["USA", "Kanada"]
|
||||
assert csfd_field_values(movie, "year") == ["1999"]
|
||||
# rating tag carries the EXACT value (transform happens only for folders)
|
||||
assert csfd_field_values(movie, "rating") == ["86"]
|
||||
# missing field / value → empty
|
||||
assert csfd_field_values(CSFDMovie(title="X", url="u"), "rating") == []
|
||||
assert csfd_field_values(movie, "genres") == csfd_field_values(movie, "genres")
|
||||
|
||||
def test_apply_transform_decade_band(self):
|
||||
from src.core.csfd import apply_transform
|
||||
assert apply_transform("86", "decade_band") == "80–89 %"
|
||||
assert apply_transform("90", "decade_band") == "90–100 %"
|
||||
assert apply_transform("Akční", None) == "Akční" # identity for non-rating
|
||||
assert apply_transform("USA", "identity") == "USA"
|
||||
assert _parse_duration("PT") is None
|
||||
|
||||
|
||||
|
||||
+64
-3
@@ -297,7 +297,7 @@ class TestApplyCsfdTags:
|
||||
assert "Žánr/Sci-Fi" in paths
|
||||
assert "Rok/1999" in paths
|
||||
assert "Země původu/USA" in paths
|
||||
assert "Hodnocení/90–100 %" in paths
|
||||
assert "Hodnocení/90" in paths # exact value; folder grouping happens later
|
||||
|
||||
def test_apply_csfd_tags_does_not_tag_directors_or_actors(self, movie_file):
|
||||
"""Režie/herci se jen cachují, netvoří se z nich tagy (bylo by jich moc)."""
|
||||
@@ -319,14 +319,75 @@ class TestApplyCsfdTags:
|
||||
assert cached.directors == ["Lana Wachowski"]
|
||||
assert cached.actors == ["Keanu Reeves", "Laurence Fishburne"]
|
||||
|
||||
def test_apply_csfd_tags_can_skip_rating(self, movie_file):
|
||||
def test_name_context_fields(self, movie_file):
|
||||
movie_file.title = "Dr. No"
|
||||
movie_file.csfd_cache = {"year": 1962, "rating": 75}
|
||||
ctx = movie_file.name_context()
|
||||
assert ctx["title"] == "Dr. No"
|
||||
assert ctx["year"] == 1962
|
||||
assert ctx["rating"] == 75
|
||||
assert ctx["ext"] == movie_file.file_path.suffix
|
||||
assert ctx["filename"] == movie_file.filename
|
||||
assert "{year} - {title}{ext}".format(**ctx) == f"1962 - Dr. No{ctx['ext']}"
|
||||
|
||||
def test_name_context_year_from_tag_when_no_cache(self, movie_file):
|
||||
movie_file.csfd_cache = None
|
||||
movie_file.add_tag("Rok/1999")
|
||||
assert movie_file.name_context()["year"] == "1999"
|
||||
|
||||
def test_set_attribute_persists_and_in_context(self, movie_file):
|
||||
movie_file.set_attribute("collection_sort", "03")
|
||||
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)
|
||||
assert reloaded.attributes["collection_sort"] == "03"
|
||||
|
||||
def test_set_attribute_empty_removes_it(self, movie_file):
|
||||
movie_file.set_attribute("collection_sort", "03")
|
||||
movie_file.set_attribute("collection_sort", None)
|
||||
assert "collection_sort" not in movie_file.attributes
|
||||
|
||||
def test_attribute_usable_in_filename_template(self, movie_file):
|
||||
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']}"
|
||||
|
||||
def test_apply_csfd_tags_honors_custom_schema(self, movie_file):
|
||||
"""A schema without the rating entry produces no Hodnocení tags."""
|
||||
from unittest.mock import patch
|
||||
from src.core.csfd import CSFDMovie
|
||||
|
||||
schema = [{"category": "Žánr", "csfd_field": "genres",
|
||||
"transform": None, "filmoteka_root": ""}]
|
||||
movie = CSFDMovie(title="Matrix", url="u", rating=90, genres=["Drama"])
|
||||
with patch("src.core.csfd.fetch_movie", return_value=movie):
|
||||
movie_file.apply_csfd_tags(add_rating=False)
|
||||
movie_file.apply_csfd_tags(schema)
|
||||
|
||||
paths = {t.full_path for t in movie_file.tags}
|
||||
assert "Žánr/Drama" in paths
|
||||
assert not any(p.startswith("Hodnocení/") for p in paths)
|
||||
|
||||
def test_apply_csfd_tags_preserves_user_tags_on_refetch(self, movie_file):
|
||||
"""Re-fetching regenerates only ČSFD tags; user-added tags survive."""
|
||||
from unittest.mock import patch
|
||||
from src.core.csfd import CSFDMovie
|
||||
|
||||
movie_file.add_tag("Sbírka/Oblíbené") # user tag
|
||||
first = CSFDMovie(title="A", url="u", year=1999, genres=["Akční"])
|
||||
with patch("src.core.csfd.fetch_movie", return_value=first):
|
||||
movie_file.apply_csfd_tags()
|
||||
# different movie on re-fetch
|
||||
second = CSFDMovie(title="B", url="u", year=2009, genres=["Drama"])
|
||||
with patch("src.core.csfd.fetch_movie", return_value=second):
|
||||
movie_file.apply_csfd_tags()
|
||||
|
||||
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 ČSFD tag
|
||||
assert "Rok/2009" in paths
|
||||
assert "Žánr/Akční" not in paths # stale ČSFD tag dropped
|
||||
assert "Rok/1999" not in paths
|
||||
|
||||
@@ -603,6 +603,70 @@ class TestPoolManagement:
|
||||
assert movie.file_path.exists()
|
||||
assert not source.exists() # moved, not copied
|
||||
|
||||
def test_filmoteka_category_roots_from_schema(self, file_manager):
|
||||
file_manager.set_tag_schema([
|
||||
{"category": "Žánr", "csfd_field": "genres", "transform": None, "filmoteka_root": ""},
|
||||
{"category": "Rok", "csfd_field": "year", "transform": None, "filmoteka_root": "Dle roku"},
|
||||
{"category": "Herec", "csfd_field": "actors", "transform": None, "filmoteka_root": None},
|
||||
])
|
||||
roots = file_manager.filmoteka_category_roots()
|
||||
assert roots == {"Žánr": "", "Rok": "Dle roku"} # None-root excluded
|
||||
assert "Herec" not in roots
|
||||
|
||||
def test_import_movie_suffix_keeps_both(self, file_manager, tmp_path):
|
||||
file_manager.set_pool_dir(tmp_path / "pool")
|
||||
(tmp_path / "a.mkv").write_bytes(b"a")
|
||||
(tmp_path / "b.mkv").write_bytes(b"b")
|
||||
first = file_manager.import_movie(tmp_path / "a.mkv", "Matrix")
|
||||
second = file_manager.import_movie(tmp_path / "b.mkv", "Matrix") # default suffix
|
||||
|
||||
assert first.file_path.name == "Matrix.mkv"
|
||||
assert second.file_path.name == "Matrix_1.mkv"
|
||||
assert len(file_manager.filelist) == 2
|
||||
|
||||
def test_import_movie_replace_evicts_existing(self, file_manager, tmp_path):
|
||||
file_manager.set_pool_dir(tmp_path / "pool")
|
||||
(tmp_path / "a.mkv").write_bytes(b"a")
|
||||
(tmp_path / "b.mkv").write_bytes(b"bb")
|
||||
first = file_manager.import_movie(tmp_path / "a.mkv", "Matrix")
|
||||
first.add_tag("Žánr/Akční")
|
||||
old_path = first.file_path
|
||||
|
||||
second = file_manager.import_movie(
|
||||
tmp_path / "b.mkv", "Matrix", csfd_link="x", on_conflict="replace")
|
||||
|
||||
assert second.file_path.name == "Matrix.mkv"
|
||||
assert second.file_path == old_path # same name reused
|
||||
assert second.file_path.read_bytes() == b"bb" # new content in place
|
||||
assert [f.file_path.name for f in file_manager.filelist] == ["Matrix.mkv"]
|
||||
# index record reflects the new import (fresh metadata, old tags dropped)
|
||||
record = file_manager.index.get(second.file_path)
|
||||
assert record is not None
|
||||
assert record["csfd_link"] == "x"
|
||||
assert record["tags"] == []
|
||||
assert second.tags == []
|
||||
|
||||
def test_import_movie_replace_across_extensions(self, file_manager, tmp_path):
|
||||
file_manager.set_pool_dir(tmp_path / "pool")
|
||||
(tmp_path / "a.mkv").write_bytes(b"a")
|
||||
(tmp_path / "b.mp4").write_bytes(b"b")
|
||||
file_manager.import_movie(tmp_path / "a.mkv", "Matrix")
|
||||
file_manager.import_movie(tmp_path / "b.mp4", "Matrix", on_conflict="replace")
|
||||
|
||||
names = [f.file_path.name for f in file_manager.filelist]
|
||||
assert names == ["Matrix.mp4"]
|
||||
assert not (tmp_path / "pool" / "Filmy" / "Matrix.mkv").exists()
|
||||
|
||||
def test_import_movie_skip_returns_none(self, file_manager, tmp_path):
|
||||
file_manager.set_pool_dir(tmp_path / "pool")
|
||||
(tmp_path / "a.mkv").write_bytes(b"a")
|
||||
(tmp_path / "b.mkv").write_bytes(b"b")
|
||||
file_manager.import_movie(tmp_path / "a.mkv", "Matrix")
|
||||
result = file_manager.import_movie(tmp_path / "b.mkv", "Matrix", on_conflict="skip")
|
||||
|
||||
assert result is None
|
||||
assert len(file_manager.filelist) == 1
|
||||
|
||||
def test_rename_movie_renames_file_and_reindexes(self, file_manager, tmp_path):
|
||||
file_manager.set_pool_dir(tmp_path / "pool")
|
||||
source = tmp_path / "raw.mkv"
|
||||
|
||||
@@ -141,6 +141,67 @@ class TestHardlinkManager:
|
||||
# The mirror (not a managed tag folder) is left alone
|
||||
assert mirror_link.exists()
|
||||
|
||||
def test_category_transform_groups_folder_by_band(
|
||||
self, temp_source_dir, temp_output_dir, tag_manager
|
||||
):
|
||||
"""Exact tag value, but the folder name goes through the transform."""
|
||||
f = File(temp_source_dir / "file1.txt", tag_manager)
|
||||
f.tags.clear()
|
||||
f.add_tag(Tag("Hodnocení", "90")) # exact rating tag
|
||||
manager = HardlinkManager(temp_output_dir)
|
||||
manager.create_structure_for_files(
|
||||
[f],
|
||||
category_roots={"Hodnocení": "Dle hodnocení"},
|
||||
category_transforms={"Hodnocení": "decade_band"},
|
||||
)
|
||||
|
||||
# folder is the band; the file lands inside it
|
||||
assert (temp_output_dir / "Dle hodnocení" / "90–100 %" / "file1.txt").exists()
|
||||
# not a per-exact-value folder
|
||||
assert not (temp_output_dir / "Dle hodnocení" / "90").exists()
|
||||
|
||||
def test_filename_template_applies_only_in_that_category(
|
||||
self, temp_source_dir, temp_output_dir, tag_manager
|
||||
):
|
||||
"""A per-category template renames the hardlink only inside its folder."""
|
||||
f = File(temp_source_dir / "file1.txt", tag_manager)
|
||||
f.tags.clear()
|
||||
f.title = "Dr. No"
|
||||
f.csfd_cache = {"year": 1962}
|
||||
f.add_tag(Tag("Kolekce", "James Bond"))
|
||||
f.add_tag(Tag("žánr", "Akční"))
|
||||
manager = HardlinkManager(temp_output_dir)
|
||||
manager.create_structure_for_files(
|
||||
[f],
|
||||
category_roots={"Kolekce": "Dle kolekce", "žánr": ""},
|
||||
category_filename_templates={"Kolekce": "{year} - {title}{ext}"},
|
||||
)
|
||||
|
||||
# Templated name inside the collection folder
|
||||
assert (temp_output_dir / "Dle kolekce" / "James Bond" / "1962 - Dr. No.txt").exists()
|
||||
# Other categories keep the pool filename
|
||||
assert (temp_output_dir / "Akční" / "file1.txt").exists()
|
||||
|
||||
def test_filename_template_cleanup_is_consistent(
|
||||
self, temp_source_dir, temp_output_dir, tag_manager
|
||||
):
|
||||
"""sync twice with a template leaves no stale/duplicate templated link."""
|
||||
f = File(temp_source_dir / "file1.txt", tag_manager)
|
||||
f.tags.clear()
|
||||
f.title = "Dr. No"
|
||||
f.csfd_cache = {"year": 1962}
|
||||
f.add_tag(Tag("Kolekce", "James Bond"))
|
||||
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)
|
||||
|
||||
folder = temp_output_dir / "Dle kolekce" / "James Bond"
|
||||
assert [p.name for p in folder.iterdir()] == ["1962 - Dr. No.txt"]
|
||||
assert removed == 0 # nothing treated as obsolete on the second run
|
||||
|
||||
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)
|
||||
|
||||
Reference in New Issue
Block a user