1373 lines
61 KiB
Python
1373 lines
61 KiB
Python
import pytest
|
|
from src.core.file import File
|
|
from src.core.file_manager import FileManager
|
|
from src.core.tag_manager import TagManager
|
|
from src.core.tag import Tag
|
|
|
|
|
|
@pytest.fixture
|
|
def temp_global_config(tmp_path, monkeypatch):
|
|
"""Point the global config file at a throwaway path for each test."""
|
|
config_path = tmp_path / "test_config.json"
|
|
import src.core.config as config_module
|
|
monkeypatch.setattr(config_module, "GLOBAL_CONFIG_FILE", config_path)
|
|
return config_path
|
|
|
|
|
|
@pytest.fixture
|
|
def tag_manager():
|
|
return TagManager()
|
|
|
|
|
|
@pytest.fixture
|
|
def file_manager(tag_manager, temp_global_config):
|
|
return FileManager(tag_manager)
|
|
|
|
|
|
def _in_memory_files(tmp_path, tag_manager, *names):
|
|
"""Create real files and wrap them as index-less (in-memory) File objects."""
|
|
files = []
|
|
for name in names:
|
|
path = tmp_path / name
|
|
path.write_text("content")
|
|
files.append(File(path, tag_manager))
|
|
return files
|
|
|
|
|
|
class TestFileManager:
|
|
"""Basic FileManager state."""
|
|
|
|
def test_file_manager_creation(self, file_manager, tag_manager):
|
|
assert file_manager.filelist == []
|
|
assert file_manager.tagmanager == tag_manager
|
|
assert file_manager.global_config is not None
|
|
assert file_manager.index is None
|
|
|
|
|
|
class TestFileManagerTagOperations:
|
|
"""assign/remove tag operations over the loaded file list."""
|
|
|
|
def test_assign_tag_to_file_objects_tag_object(self, file_manager, tmp_path, tag_manager):
|
|
files = _in_memory_files(tmp_path, tag_manager, "f1.txt", "f2.txt")
|
|
file_manager.filelist = files
|
|
tag = Tag("Video", "HD")
|
|
|
|
file_manager.assign_tag_to_file_objects(files, tag)
|
|
|
|
for f in files:
|
|
assert tag in f.tags
|
|
|
|
def test_assign_tag_string_with_category(self, file_manager, tmp_path, tag_manager):
|
|
files = _in_memory_files(tmp_path, tag_manager, "f1.txt")
|
|
file_manager.filelist = files
|
|
|
|
file_manager.assign_tag_to_file_objects(files, "Video/4K")
|
|
|
|
tag_paths = {tag.full_path for tag in files[0].tags}
|
|
assert "Video/4K" in tag_paths
|
|
|
|
def test_assign_tag_string_without_category(self, file_manager, tmp_path, tag_manager):
|
|
files = _in_memory_files(tmp_path, tag_manager, "f1.txt")
|
|
file_manager.filelist = files
|
|
|
|
file_manager.assign_tag_to_file_objects(files, "SimpleTag")
|
|
|
|
tag_paths = {tag.full_path for tag in files[0].tags}
|
|
assert "default/SimpleTag" in tag_paths
|
|
|
|
def test_assign_tag_no_duplicate(self, file_manager, tmp_path, tag_manager):
|
|
files = _in_memory_files(tmp_path, tag_manager, "f1.txt")
|
|
file_manager.filelist = files
|
|
tag = Tag("Video", "HD")
|
|
|
|
file_manager.assign_tag_to_file_objects(files, tag)
|
|
file_manager.assign_tag_to_file_objects(files, tag)
|
|
|
|
count = sum(1 for t in files[0].tags if t == tag)
|
|
assert count == 1
|
|
|
|
def test_remove_tag_from_file_objects(self, file_manager, tmp_path, tag_manager):
|
|
files = _in_memory_files(tmp_path, tag_manager, "f1.txt", "f2.txt")
|
|
file_manager.filelist = files
|
|
tag = Tag("Video", "HD")
|
|
|
|
file_manager.assign_tag_to_file_objects(files, tag)
|
|
file_manager.remove_tag_from_file_objects(files, tag)
|
|
|
|
for f in files:
|
|
assert tag not in f.tags
|
|
|
|
def test_remove_tag_string(self, file_manager, tmp_path, tag_manager):
|
|
files = _in_memory_files(tmp_path, tag_manager, "f1.txt")
|
|
file_manager.filelist = files
|
|
|
|
file_manager.assign_tag_to_file_objects(files, "Video/HD")
|
|
file_manager.remove_tag_from_file_objects(files, "Video/HD")
|
|
|
|
tag_paths = {tag.full_path for tag in files[0].tags}
|
|
assert "Video/HD" not in tag_paths
|
|
|
|
def test_callback_on_tag_change(self, file_manager, tmp_path, tag_manager):
|
|
files = _in_memory_files(tmp_path, tag_manager, "f1.txt")
|
|
file_manager.filelist = files
|
|
callback_calls = []
|
|
file_manager.on_files_changed = lambda filelist: callback_calls.append(len(filelist))
|
|
|
|
file_manager.assign_tag_to_file_objects([files[0]], Tag("Test", "Tag"))
|
|
|
|
assert len(callback_calls) == 1
|
|
|
|
|
|
class TestFileManagerFiltering:
|
|
"""filter_files_by_tags over the loaded file list."""
|
|
|
|
@pytest.fixture
|
|
def loaded(self, file_manager, tmp_path, tag_manager):
|
|
files = _in_memory_files(tmp_path, tag_manager, "f1.txt", "f2.txt", "f3.txt")
|
|
file_manager.filelist = files
|
|
return file_manager
|
|
|
|
def test_filter_empty_tags_returns_all(self, loaded):
|
|
assert len(loaded.filter_files_by_tags([])) == len(loaded.filelist)
|
|
|
|
def test_filter_none_returns_all(self, loaded):
|
|
assert len(loaded.filter_files_by_tags(None)) == len(loaded.filelist)
|
|
|
|
def test_filter_by_single_tag(self, loaded):
|
|
tag = Tag("Video", "HD")
|
|
loaded.assign_tag_to_file_objects(loaded.filelist[:2], tag)
|
|
|
|
filtered = loaded.filter_files_by_tags([tag])
|
|
assert len(filtered) == 2
|
|
for f in filtered:
|
|
assert tag in f.tags
|
|
|
|
def test_filter_by_multiple_tags_and_logic(self, loaded):
|
|
tag1 = Tag("Video", "HD")
|
|
tag2 = Tag("Audio", "Stereo")
|
|
loaded.assign_tag_to_file_objects([loaded.filelist[0]], tag1)
|
|
loaded.assign_tag_to_file_objects([loaded.filelist[0]], tag2)
|
|
loaded.assign_tag_to_file_objects([loaded.filelist[1]], tag1)
|
|
|
|
filtered = loaded.filter_files_by_tags([tag1, tag2])
|
|
assert len(filtered) == 1
|
|
assert filtered[0] == loaded.filelist[0]
|
|
|
|
def test_filter_by_tag_strings(self, loaded):
|
|
loaded.assign_tag_to_file_objects([loaded.filelist[0]], "Video/HD")
|
|
assert len(loaded.filter_files_by_tags(["Video/HD"])) == 1
|
|
|
|
def test_filter_no_match(self, loaded):
|
|
assert len(loaded.filter_files_by_tags([Tag("NonExistent", "Tag")])) == 0
|
|
|
|
|
|
class TestFileManagerEdgeCases:
|
|
def test_empty_filelist_operations(self, file_manager):
|
|
assert file_manager.filter_files_by_tags([Tag("Video", "HD")]) == []
|
|
file_manager.assign_tag_to_file_objects([], Tag("Video", "HD"))
|
|
assert len(file_manager.filelist) == 0
|
|
|
|
def test_assign_tag_to_empty_list(self, file_manager):
|
|
file_manager.assign_tag_to_file_objects([], Tag("Test", "Tag")) # no exception
|
|
|
|
def test_remove_nonexistent_tag(self, file_manager, tmp_path, tag_manager):
|
|
file_manager.filelist = _in_memory_files(tmp_path, tag_manager, "file.txt")
|
|
file_manager.remove_tag_from_file_objects(
|
|
file_manager.filelist, Tag("NonExistent", "Tag")) # no exception
|
|
|
|
|
|
class TestPoolManagement:
|
|
"""Testy pro pool a copy-as-is složky"""
|
|
|
|
@pytest.fixture
|
|
def temp_global_config(self, tmp_path, monkeypatch):
|
|
config_path = tmp_path / "test_config.json"
|
|
import src.core.config as config_module
|
|
monkeypatch.setattr(config_module, 'GLOBAL_CONFIG_FILE', config_path)
|
|
return config_path
|
|
|
|
@pytest.fixture
|
|
def file_manager(self, temp_global_config):
|
|
return FileManager(TagManager())
|
|
|
|
def test_set_pool_creates_top_level_folders(self, file_manager, tmp_path):
|
|
pool = tmp_path / "pool"
|
|
file_manager.set_pool_dir(pool)
|
|
|
|
assert (pool / "Filmy").is_dir()
|
|
assert (pool / "Seriály").is_dir()
|
|
assert file_manager.pool_dir == pool
|
|
|
|
def test_import_movie_copies_and_indexes(self, file_manager, tmp_path):
|
|
file_manager.set_pool_dir(tmp_path / "pool")
|
|
source = tmp_path / "raw.mkv"
|
|
source.write_bytes(b"x" * 10)
|
|
|
|
movie = file_manager.import_movie(source, "Matrix")
|
|
|
|
assert movie.file_path == tmp_path / "pool" / "Filmy" / "Matrix.mkv"
|
|
assert source.exists() # non-destructive copy
|
|
assert movie.title == "Matrix"
|
|
assert file_manager.index.record_for(movie.file_path) is not None
|
|
|
|
def test_statistics_aggregates_pool(self, file_manager, tmp_path):
|
|
file_manager.set_pool_dir(tmp_path / "pool")
|
|
(tmp_path / "a.mkv").write_bytes(b"x" * 100)
|
|
(tmp_path / "b.mkv").write_bytes(b"y" * 50)
|
|
a = file_manager.import_movie(tmp_path / "a.mkv", "A")
|
|
a.add_tag("Žánr/Akční")
|
|
a.add_tag("Žánr/Sci-Fi")
|
|
a.meta = {"rating": 90}
|
|
a.save_metadata()
|
|
b = file_manager.import_movie(tmp_path / "b.mkv", "B")
|
|
b.add_tag("Žánr/Akční")
|
|
|
|
s = file_manager.statistics()
|
|
assert s["count"] == 2
|
|
assert s["total_size"] == 150
|
|
assert s["with_nfo"] == 1 and s["without_nfo"] == 1
|
|
assert s["untagged"] == 0
|
|
assert s["avg_rating"] == 90
|
|
# Akční on both movies → count 2, sorted first
|
|
assert dict(s["categories"]["Žánr"])["Akční"] == 2
|
|
assert s["categories"]["Žánr"][0] == ("Akční", 2)
|
|
|
|
def test_check_data_consistency_clean(self, file_manager, tmp_path):
|
|
file_manager.set_pool_dir(tmp_path / "pool")
|
|
(tmp_path / "a.mkv").write_bytes(b"a")
|
|
file_manager.import_movie(tmp_path / "a.mkv", "Matrix")
|
|
|
|
result = file_manager.check_data_consistency()
|
|
assert result["ok"]
|
|
assert result["missing"] == []
|
|
assert result["untracked"] == []
|
|
assert result["index_count"] == 1 and result["disk_count"] == 1
|
|
|
|
def test_check_data_consistency_detects_missing_file(self, file_manager, tmp_path):
|
|
file_manager.set_pool_dir(tmp_path / "pool")
|
|
(tmp_path / "a.mkv").write_bytes(b"a")
|
|
movie = file_manager.import_movie(tmp_path / "a.mkv", "Matrix")
|
|
movie.file_path.unlink() # someone deleted the file directly
|
|
|
|
result = file_manager.check_data_consistency()
|
|
assert result["missing"] == ["Filmy/Matrix.mkv"]
|
|
assert result["untracked"] == []
|
|
|
|
def test_check_data_consistency_detects_untracked_file(self, file_manager, tmp_path):
|
|
file_manager.set_pool_dir(tmp_path / "pool")
|
|
(tmp_path / "a.mkv").write_bytes(b"a")
|
|
file_manager.import_movie(tmp_path / "a.mkv", "Matrix")
|
|
# a file dropped into the pool without importing
|
|
(file_manager.movies_dir / "Sneaked.mkv").write_bytes(b"x")
|
|
|
|
result = file_manager.check_data_consistency()
|
|
assert result["missing"] == []
|
|
assert result["untracked"] == ["Filmy/Sneaked.mkv"]
|
|
|
|
def test_import_movie_records_added_timestamp(self, file_manager, tmp_path):
|
|
file_manager.set_pool_dir(tmp_path / "pool")
|
|
source = tmp_path / "raw.mkv"
|
|
source.write_bytes(b"x")
|
|
movie = file_manager.import_movie(source, "Matrix")
|
|
|
|
assert movie.added is not None
|
|
# parseable ISO timestamp, persisted in the index
|
|
from datetime import datetime
|
|
datetime.fromisoformat(movie.added)
|
|
assert file_manager.index.record_for(movie.file_path)["added"] == movie.added
|
|
|
|
def test_import_movie_move_removes_source(self, file_manager, tmp_path):
|
|
file_manager.set_pool_dir(tmp_path / "pool")
|
|
source = tmp_path / "raw.mkv"
|
|
source.write_bytes(b"x" * 10)
|
|
|
|
movie = file_manager.import_movie(source, "Matrix", move=True)
|
|
|
|
assert movie.file_path == tmp_path / "pool" / "Filmy" / "Matrix.mkv"
|
|
assert movie.file_path.exists()
|
|
assert not source.exists() # moved, not copied
|
|
|
|
def test_filmoteka_category_roots_from_schema(self, file_manager, tmp_path):
|
|
file_manager.set_pool_dir(tmp_path / "pool")
|
|
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", 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.record_for(second.file_path)
|
|
assert record is not None
|
|
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"
|
|
source.write_bytes(b"x" * 10)
|
|
movie = file_manager.import_movie(source, "Matrix")
|
|
movie.add_tag("Žánr/Sci-Fi")
|
|
old_path = movie.file_path
|
|
|
|
file_manager.rename_movie(movie, "Matrix Reloaded")
|
|
|
|
new_path = tmp_path / "pool" / "Filmy" / "Matrix Reloaded.mkv"
|
|
assert movie.file_path == new_path
|
|
assert new_path.exists()
|
|
assert not old_path.exists()
|
|
assert movie.title == "Matrix Reloaded"
|
|
# metadata moved to the new key, old key gone, tags preserved
|
|
assert file_manager.index.record_for(new_path) is not None
|
|
assert file_manager.index.record_for(old_path) is None
|
|
# a fresh manager reading the index sees the renamed file with its tags
|
|
reloaded = FileManager(TagManager())
|
|
reloaded.set_pool_dir(tmp_path / "pool")
|
|
reloaded.load_pool_movies()
|
|
assert [f.filename for f in reloaded.filelist] == ["Matrix Reloaded.mkv"]
|
|
assert "Žánr/Sci-Fi" in {t.full_path for t in reloaded.filelist[0].tags}
|
|
|
|
def test_rename_movie_preserves_extension(self, file_manager, tmp_path):
|
|
file_manager.set_pool_dir(tmp_path / "pool")
|
|
source = tmp_path / "raw.mp4"
|
|
source.write_bytes(b"x")
|
|
movie = file_manager.import_movie(source, "Film")
|
|
|
|
file_manager.rename_movie(movie, "Jiný název")
|
|
|
|
assert movie.file_path.name == "Jiný název.mp4"
|
|
|
|
def test_rename_movie_rejects_existing_name(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", "Already")
|
|
second = file_manager.import_movie(tmp_path / "b.mkv", "Other")
|
|
|
|
with pytest.raises(FileExistsError):
|
|
file_manager.rename_movie(second, "Already")
|
|
# second movie is left untouched
|
|
assert second.file_path.name == "Other.mkv"
|
|
assert first.file_path.exists()
|
|
|
|
def test_rename_movie_rejects_empty_name(self, file_manager, tmp_path):
|
|
file_manager.set_pool_dir(tmp_path / "pool")
|
|
(tmp_path / "a.mkv").write_bytes(b"a")
|
|
movie = file_manager.import_movie(tmp_path / "a.mkv", "Name")
|
|
|
|
with pytest.raises(ValueError):
|
|
file_manager.rename_movie(movie, " ")
|
|
|
|
def test_import_movie_with_year_uses_convention(self, file_manager, tmp_path):
|
|
file_manager.set_pool_dir(tmp_path / "pool")
|
|
source = tmp_path / "raw.mkv"
|
|
source.write_bytes(b"x" * 10)
|
|
|
|
movie = file_manager.import_movie(source, "Matrix", year=1999)
|
|
|
|
assert movie.file_path.name == "Matrix (1999).mkv"
|
|
assert movie.title == "Matrix" # clean title, no year
|
|
|
|
def test_import_movie_with_tmdb_id_names_for_jellyfin(self, file_manager, tmp_path):
|
|
file_manager.set_pool_dir(tmp_path / "pool")
|
|
(tmp_path / "raw.mkv").write_bytes(b"x")
|
|
|
|
movie = file_manager.import_movie(
|
|
tmp_path / "raw.mkv", "Matrix", year=1999, tmdb_id=603)
|
|
|
|
assert movie.file_path.name == "Matrix (1999) [tmdbid-603].mkv"
|
|
assert movie.title == "Matrix" # clean title, no year/id
|
|
|
|
def test_import_movie_with_3d_format(self, file_manager, tmp_path):
|
|
file_manager.set_pool_dir(tmp_path / "pool")
|
|
(tmp_path / "raw.mkv").write_bytes(b"x")
|
|
|
|
movie = file_manager.import_movie(
|
|
tmp_path / "raw.mkv", "Avatar", year=2009, tmdb_id=19995, format_3d="hsbs")
|
|
|
|
assert movie.file_path.name == "Avatar (2009) [tmdbid-19995].3D.HSBS.mkv"
|
|
|
|
def test_libraries_default_and_derivation(self, file_manager, tmp_path):
|
|
file_manager.set_pool_dir(tmp_path / "pool")
|
|
assert file_manager.libraries == [
|
|
{"name": "Filmy", "kind": "movie"},
|
|
{"name": "Seriály", "kind": "series"},
|
|
]
|
|
assert file_manager.movie_libraries == ["Filmy"]
|
|
assert file_manager.copyasis_folders == ["Seriály"]
|
|
|
|
def test_set_libraries_with_all_kinds(self, file_manager, tmp_path):
|
|
file_manager.set_pool_dir(tmp_path / "pool")
|
|
file_manager.set_libraries([
|
|
{"name": "Hlavní", "kind": "mixed"},
|
|
{"name": "Dokumenty", "kind": "mixed"},
|
|
{"name": "Anime", "kind": "series"},
|
|
{"name": "Chajda", "kind": "homevideo"},
|
|
])
|
|
assert file_manager.copyasis_folders == ["Anime"]
|
|
assert file_manager.movie_libraries == ["Filmy"] # none of kind movie → fallback
|
|
assert file_manager.libraries_of_kind("mixed") == ["Hlavní", "Dokumenty"]
|
|
assert file_manager.libraries_of_kind("homevideo") == ["Chajda"]
|
|
|
|
def test_set_pool_dir_creates_all_library_folders(self, file_manager, tmp_path):
|
|
pool = tmp_path / "pool"
|
|
file_manager.set_pool_dir(pool)
|
|
file_manager.set_libraries([
|
|
{"name": "Hlavní", "kind": "mixed"},
|
|
{"name": "Chajda", "kind": "homevideo"},
|
|
])
|
|
file_manager.set_pool_dir(pool) # re-run creates the newly defined folders
|
|
assert (pool / "Hlavní").is_dir()
|
|
assert (pool / "Chajda").is_dir()
|
|
|
|
def test_mixed_library_loads_movies_not_episodes(self, file_manager, tmp_path):
|
|
file_manager.set_pool_dir(tmp_path / "pool")
|
|
file_manager.set_libraries([{"name": "Hlavní", "kind": "mixed"}])
|
|
hlavni = tmp_path / "pool" / "Hlavní"
|
|
hlavni.mkdir(parents=True, exist_ok=True)
|
|
(hlavni / "Matrix (1999) [tmdbid-603].mkv").write_bytes(b"x") # movie (top level)
|
|
ep = hlavni / "Breaking Bad (2008) [tmdbid-1396]" / "Season 01"
|
|
ep.mkdir(parents=True)
|
|
(ep / "S01E01 - pilot.mkv").write_bytes(b"y") # series episode (subfolder)
|
|
|
|
file_manager.load_pool_movies()
|
|
names = [f.filename for f in file_manager.filelist]
|
|
assert names == ["Matrix (1999) [tmdbid-603].mkv"] # episode not loaded as movie
|
|
assert file_manager.library_of(file_manager.filelist[0]) == "Hlavní"
|
|
|
|
def test_libraries_accepting_movies(self, file_manager, tmp_path):
|
|
file_manager.set_pool_dir(tmp_path / "pool")
|
|
file_manager.set_libraries([
|
|
{"name": "Filmy", "kind": "movie"},
|
|
{"name": "Hlavní", "kind": "mixed"},
|
|
{"name": "Anime", "kind": "series"},
|
|
])
|
|
assert file_manager.libraries_accepting_movies == ["Filmy", "Hlavní"]
|
|
|
|
def test_orphan_folder_cleanup(self, file_manager, tmp_path):
|
|
pool = tmp_path / "pool"
|
|
file_manager.set_pool_dir(pool)
|
|
file_manager.set_libraries([{"name": "Filmy", "kind": "movie"}])
|
|
(pool / "OldKids").mkdir() # empty orphan
|
|
(pool / "OldStuff").mkdir()
|
|
(pool / "OldStuff" / "film.mkv").write_bytes(b"x") # non-empty orphan
|
|
|
|
orphans = dict(file_manager.orphan_pool_folders())
|
|
assert orphans.get("OldKids") is True
|
|
assert orphans.get("OldStuff") is False
|
|
assert "Filmy" not in orphans # a configured library is not an orphan
|
|
|
|
assert file_manager.remove_empty_pool_folder("OldKids") is True
|
|
assert not (pool / "OldKids").exists()
|
|
assert file_manager.remove_empty_pool_folder("OldStuff") is False # non-empty
|
|
assert file_manager.remove_empty_pool_folder("Filmy") is False # a library
|
|
|
|
def test_move_movie_into_mixed_library(self, file_manager, tmp_path):
|
|
file_manager.set_pool_dir(tmp_path / "pool")
|
|
file_manager.set_libraries([
|
|
{"name": "Filmy", "kind": "movie"},
|
|
{"name": "Hlavní", "kind": "mixed"},
|
|
])
|
|
(tmp_path / "a.mkv").write_bytes(b"x")
|
|
movie = file_manager.import_movie(tmp_path / "a.mkv", "Matrix", year=1999)
|
|
file_manager.move_to_library(movie, "Hlavní")
|
|
assert file_manager.library_of(movie) == "Hlavní"
|
|
|
|
def test_import_into_named_library(self, file_manager, tmp_path):
|
|
file_manager.set_pool_dir(tmp_path / "pool")
|
|
file_manager.set_movie_libraries(["Filmy", "Kids"])
|
|
(tmp_path / "a.mkv").write_bytes(b"x")
|
|
|
|
movie = file_manager.import_movie(
|
|
tmp_path / "a.mkv", "Auta", year=2006, tmdb_id=920, library="Kids")
|
|
|
|
assert movie.file_path == tmp_path / "pool" / "Kids" / "Auta (2006) [tmdbid-920].mkv"
|
|
assert file_manager.library_of(movie) == "Kids"
|
|
|
|
def test_move_to_library_reindexes(self, file_manager, tmp_path):
|
|
file_manager.set_pool_dir(tmp_path / "pool")
|
|
file_manager.set_movie_libraries(["Filmy", "Kids"])
|
|
(tmp_path / "a.mkv").write_bytes(b"x")
|
|
movie = file_manager.import_movie(tmp_path / "a.mkv", "Auta", year=2006)
|
|
old_path = movie.file_path
|
|
|
|
file_manager.move_to_library(movie, "Kids")
|
|
|
|
assert file_manager.library_of(movie) == "Kids"
|
|
assert movie.file_path.exists()
|
|
# The movie keeps its identity record; only its media path + library move.
|
|
assert file_manager.index.record_for(old_path) is None
|
|
record = file_manager.index.record_for(movie.file_path)
|
|
assert record is not None
|
|
assert record["library"] == "Kids"
|
|
assert [m["file"] for m in record["media"]] == ["Kids/Auta (2006).mkv"]
|
|
|
|
def test_move_to_unknown_library_rejected(self, file_manager, tmp_path):
|
|
file_manager.set_pool_dir(tmp_path / "pool")
|
|
(tmp_path / "a.mkv").write_bytes(b"x")
|
|
movie = file_manager.import_movie(tmp_path / "a.mkv", "Auta", year=2006)
|
|
with pytest.raises(ValueError):
|
|
file_manager.move_to_library(movie, "Neexistuje")
|
|
|
|
def test_load_scans_all_movie_libraries(self, file_manager, tmp_path):
|
|
file_manager.set_pool_dir(tmp_path / "pool")
|
|
file_manager.set_movie_libraries(["Filmy", "Kids"])
|
|
(tmp_path / "a.mkv").write_bytes(b"x")
|
|
(tmp_path / "b.mkv").write_bytes(b"y")
|
|
file_manager.import_movie(tmp_path / "a.mkv", "Matrix", year=1999)
|
|
file_manager.import_movie(tmp_path / "b.mkv", "Auta", year=2006, library="Kids")
|
|
|
|
reloaded = FileManager(TagManager())
|
|
reloaded.set_pool_dir(tmp_path / "pool")
|
|
reloaded.load_pool_movies()
|
|
|
|
libs = sorted(reloaded.library_of(f) for f in reloaded.filelist)
|
|
assert libs == ["Filmy", "Kids"]
|
|
|
|
def test_load_skips_jellyfin_sidecars(self, file_manager, tmp_path):
|
|
file_manager.set_pool_dir(tmp_path / "pool")
|
|
(tmp_path / "a.mkv").write_bytes(b"x")
|
|
movie = file_manager.import_movie(tmp_path / "a.mkv", "Matrix", year=1999)
|
|
# Jellyfin writes these next to the movie — must not count as movies
|
|
movie.file_path.with_suffix(".nfo").write_text("<movie/>")
|
|
(movie.file_path.parent / "poster.jpg").write_bytes(b"img")
|
|
|
|
reloaded = FileManager(TagManager())
|
|
reloaded.set_pool_dir(tmp_path / "pool")
|
|
reloaded.load_pool_movies()
|
|
|
|
assert len(reloaded.filelist) == 1
|
|
assert reloaded.filelist[0].file_path.suffix == ".mkv"
|
|
|
|
def test_apply_movie_edits_all_at_once(self, file_manager, tmp_path):
|
|
file_manager.set_pool_dir(tmp_path / "pool")
|
|
file_manager.set_movie_libraries(["Filmy", "Kids"])
|
|
(tmp_path / "a.mkv").write_bytes(b"x")
|
|
movie = file_manager.import_movie(tmp_path / "a.mkv", "Puvodni", year=2000)
|
|
|
|
file_manager.apply_movie_edits(
|
|
movie, title="Auta", year=2006, tmdb_id=920, library="Kids",
|
|
date="2026-01-02", attributes={"collection_sort": "01"},
|
|
tags=["Žánr/Animovaný", "Sbírka/Oblíbené"])
|
|
|
|
assert movie.file_path == tmp_path / "pool" / "Kids" / "Auta (2006) [tmdbid-920].mkv"
|
|
assert movie.title == "Auta"
|
|
assert file_manager.library_of(movie) == "Kids"
|
|
assert movie.date == "2026-01-02"
|
|
assert movie.attributes == {"collection_sort": "01"}
|
|
assert {t.full_path for t in movie.tags} == {"Žánr/Animovaný", "Sbírka/Oblíbené"}
|
|
|
|
def test_import_and_edit_store_csfd_url(self, file_manager, tmp_path):
|
|
file_manager.set_pool_dir(tmp_path / "pool")
|
|
(tmp_path / "a.mkv").write_bytes(b"x")
|
|
movie = file_manager.import_movie(
|
|
tmp_path / "a.mkv", "Matrix", year=1999, tmdb_id=603,
|
|
csfd_url="https://www.csfd.cz/film/9499/")
|
|
assert movie.csfd_url == "https://www.csfd.cz/film/9499/"
|
|
|
|
# persists across reload
|
|
reloaded = FileManager(TagManager())
|
|
reloaded.set_pool_dir(tmp_path / "pool")
|
|
reloaded.load_pool_movies()
|
|
assert reloaded.filelist[0].csfd_url == "https://www.csfd.cz/film/9499/"
|
|
|
|
# editable via apply_movie_edits
|
|
file_manager.apply_movie_edits(movie, title="Matrix", year=1999,
|
|
csfd_url="https://www.csfd.cz/film/9499-matrix/")
|
|
assert movie.csfd_url == "https://www.csfd.cz/film/9499-matrix/"
|
|
|
|
def test_apply_movie_edits_sets_3d_format(self, file_manager, tmp_path):
|
|
file_manager.set_pool_dir(tmp_path / "pool")
|
|
(tmp_path / "a.mkv").write_bytes(b"x")
|
|
movie = file_manager.import_movie(tmp_path / "a.mkv", "Avatar", year=2009, tmdb_id=19995)
|
|
|
|
file_manager.apply_movie_edits(
|
|
movie, title="Avatar", year=2009, tmdb_id=19995, format_3d="hsbs")
|
|
|
|
assert movie.file_path.name == "Avatar (2009) [tmdbid-19995].3D.HSBS.mkv"
|
|
|
|
def test_apply_movie_edits_omitted_3d_format_is_preserved(self, file_manager, tmp_path):
|
|
"""Editing an unrelated field (e.g. date) must not silently drop the tag."""
|
|
file_manager.set_pool_dir(tmp_path / "pool")
|
|
(tmp_path / "a.mkv").write_bytes(b"x")
|
|
movie = file_manager.import_movie(tmp_path / "a.mkv", "Avatar", year=2009, tmdb_id=19995)
|
|
file_manager.apply_movie_edits(
|
|
movie, title="Avatar", year=2009, tmdb_id=19995, format_3d="hsbs")
|
|
|
|
file_manager.apply_movie_edits(
|
|
movie, title="Avatar", year=2009, tmdb_id=19995, date="2026-01-01")
|
|
|
|
assert movie.file_path.name == "Avatar (2009) [tmdbid-19995].3D.HSBS.mkv"
|
|
assert movie.date == "2026-01-01"
|
|
|
|
def test_apply_movie_edits_empty_string_clears_3d_format(self, file_manager, tmp_path):
|
|
file_manager.set_pool_dir(tmp_path / "pool")
|
|
(tmp_path / "a.mkv").write_bytes(b"x")
|
|
movie = file_manager.import_movie(tmp_path / "a.mkv", "Avatar", year=2009, tmdb_id=19995)
|
|
file_manager.apply_movie_edits(
|
|
movie, title="Avatar", year=2009, tmdb_id=19995, format_3d="hsbs")
|
|
|
|
file_manager.apply_movie_edits(
|
|
movie, title="Avatar", year=2009, tmdb_id=19995, format_3d="")
|
|
|
|
assert movie.file_path.name == "Avatar (2009) [tmdbid-19995].mkv"
|
|
|
|
def test_apply_movie_edits_rejects_name_clash(self, file_manager, tmp_path):
|
|
file_manager.set_pool_dir(tmp_path / "pool")
|
|
(tmp_path / "a.mkv").write_bytes(b"x")
|
|
(tmp_path / "b.mkv").write_bytes(b"y")
|
|
file_manager.import_movie(tmp_path / "a.mkv", "Solaris", year=1972)
|
|
other = file_manager.import_movie(tmp_path / "b.mkv", "Jiny", year=2002)
|
|
with pytest.raises(FileExistsError):
|
|
file_manager.apply_movie_edits(other, title="Solaris", year=1972)
|
|
|
|
def test_rename_to_canonical_preserves_tmdb_id(self, file_manager, tmp_path):
|
|
file_manager.set_pool_dir(tmp_path / "pool")
|
|
(tmp_path / "raw.mkv").write_bytes(b"x")
|
|
movie = file_manager.import_movie(
|
|
tmp_path / "raw.mkv", "Matrix", year=1999, tmdb_id=603)
|
|
|
|
movie.title = "Matrix Reloaded" # metadata title changed
|
|
renamed = file_manager.rename_to_canonical(movie)
|
|
|
|
assert renamed is not None
|
|
assert movie.file_path.name == "Matrix Reloaded (1999) [tmdbid-603].mkv"
|
|
|
|
def test_rename_to_canonical_preserves_3d_format(self, file_manager, tmp_path):
|
|
file_manager.set_pool_dir(tmp_path / "pool")
|
|
(tmp_path / "raw.mkv").write_bytes(b"x")
|
|
movie = file_manager.import_movie(
|
|
tmp_path / "raw.mkv", "Avatar", year=2009, tmdb_id=19995)
|
|
file_manager.apply_movie_edits(
|
|
movie, title="Avatar", year=2009, tmdb_id=19995, format_3d="fsbs")
|
|
|
|
movie.title = "Avatar Retitled"
|
|
file_manager.rename_to_canonical(movie)
|
|
|
|
assert movie.file_path.name == "Avatar Retitled (2009) [tmdbid-19995].3D.FSBS.mkv"
|
|
|
|
def test_import_movie_rejects_invalid_year(self, file_manager, tmp_path):
|
|
file_manager.set_pool_dir(tmp_path / "pool")
|
|
(tmp_path / "a.mkv").write_bytes(b"a")
|
|
with pytest.raises(ValueError):
|
|
file_manager.import_movie(tmp_path / "a.mkv", "Matrix", year="99")
|
|
|
|
def test_import_same_title_different_year_coexist(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", "Solaris", year=1972)
|
|
second = file_manager.import_movie(tmp_path / "b.mkv", "Solaris", year=2002)
|
|
|
|
assert first.file_path.name == "Solaris (1972).mkv"
|
|
assert second.file_path.name == "Solaris (2002).mkv" # no collision/suffix
|
|
|
|
def test_rename_to_canonical_from_csfd_year(self, file_manager, tmp_path):
|
|
file_manager.set_pool_dir(tmp_path / "pool")
|
|
(tmp_path / "raw.mkv").write_bytes(b"x")
|
|
movie = file_manager.import_movie(tmp_path / "raw.mkv", "Matrix") # plain
|
|
assert movie.file_path.name == "Matrix.mkv"
|
|
|
|
movie.meta = {"year": 1999}
|
|
renamed = file_manager.rename_to_canonical(movie)
|
|
|
|
assert renamed is not None
|
|
assert movie.file_path.name == "Matrix (1999).mkv"
|
|
assert movie.title == "Matrix"
|
|
|
|
def test_rename_to_canonical_skips_without_year(self, file_manager, tmp_path):
|
|
file_manager.set_pool_dir(tmp_path / "pool")
|
|
(tmp_path / "raw.mkv").write_bytes(b"x")
|
|
movie = file_manager.import_movie(tmp_path / "raw.mkv", "Unknown")
|
|
|
|
assert file_manager.rename_to_canonical(movie) is None
|
|
assert movie.file_path.name == "Unknown.mkv" # untouched
|
|
|
|
def test_rename_all_to_canonical_reports(self, file_manager, tmp_path):
|
|
file_manager.set_pool_dir(tmp_path / "pool")
|
|
for n in ("a.mkv", "b.mkv", "c.mkv"):
|
|
(tmp_path / n).write_bytes(b"x")
|
|
with_year = file_manager.import_movie(tmp_path / "a.mkv", "Matrix")
|
|
with_year.meta = {"year": 1999}
|
|
already = file_manager.import_movie(tmp_path / "b.mkv", "Solaris", year=1972)
|
|
_no_year = file_manager.import_movie(tmp_path / "c.mkv", "Mystery")
|
|
|
|
result = file_manager.rename_all_to_canonical(file_manager.filelist)
|
|
|
|
assert result["renamed"] == [("Matrix.mkv", "Matrix (1999).mkv")]
|
|
assert result["unchanged"] == 1 # Solaris (1972) already canonical
|
|
assert result["skipped_no_year"] == ["Mystery.mkv"]
|
|
assert already.file_path.name == "Solaris (1972).mkv"
|
|
|
|
def test_load_pool_movies_reads_from_index(self, file_manager, tmp_path):
|
|
file_manager.set_pool_dir(tmp_path / "pool")
|
|
source = tmp_path / "raw.mkv"
|
|
source.write_bytes(b"x" * 10)
|
|
file_manager.import_movie(source, "Matrix")
|
|
|
|
reloaded = FileManager(TagManager())
|
|
reloaded.set_pool_dir(tmp_path / "pool")
|
|
reloaded.load_pool_movies()
|
|
|
|
assert len(reloaded.filelist) == 1
|
|
assert reloaded.filelist[0].title == "Matrix"
|
|
|
|
def test_copyasis_folders_default_and_set(self, file_manager, tmp_path):
|
|
assert file_manager.copyasis_folders == ["Seriály"]
|
|
|
|
file_manager.set_pool_dir(tmp_path / "pool")
|
|
file_manager.set_copyasis_folders(["Seriály", " Dokumenty ", ""])
|
|
assert file_manager.copyasis_folders == ["Seriály", "Dokumenty"]
|
|
|
|
def test_library_settings_stored_in_index_not_global(self, file_manager, tmp_path):
|
|
"""tag_schema / copyasis_folders persist in the pool index, not .!gtag."""
|
|
file_manager.set_pool_dir(tmp_path / "pool")
|
|
file_manager.set_copyasis_folders(["Seriály", "Dokumenty"])
|
|
|
|
assert "copyasis_folders" not in file_manager.global_config
|
|
# series libraries now persist in the unified "libraries" setting (in the index)
|
|
stored = file_manager.index.get_setting("libraries")
|
|
assert stored is not None
|
|
assert [lib["name"] for lib in stored if lib["kind"] == "series"] == \
|
|
["Seriály", "Dokumenty"]
|
|
|
|
# a fresh manager over the same pool reads the stored settings back
|
|
reloaded = FileManager(TagManager())
|
|
reloaded.set_pool_dir(tmp_path / "pool")
|
|
assert reloaded.copyasis_folders == ["Seriály", "Dokumenty"]
|
|
|
|
def test_migrates_legacy_global_settings_into_index(self, file_manager, tmp_path):
|
|
"""Old settings left in the global config move into the index on open."""
|
|
file_manager.set_pool_dir(tmp_path / "pool")
|
|
# simulate a pre-migration global config carrying library settings
|
|
file_manager.global_config["copyasis_folders"] = ["Seriály", "Filmy jinak"]
|
|
file_manager.index = None # force a fresh open + migration
|
|
|
|
assert file_manager.copyasis_folders == ["Seriály", "Filmy jinak"]
|
|
assert "copyasis_folders" not in file_manager.global_config
|
|
assert file_manager.index.get_setting("copyasis_folders") == ["Seriály", "Filmy jinak"]
|
|
|
|
|
|
class TestCsfdBridge:
|
|
"""ČSFD → TMDb title bridge (resolve_via_csfd, migration wiring)."""
|
|
|
|
@pytest.fixture
|
|
def temp_global_config(self, tmp_path, monkeypatch):
|
|
config_path = tmp_path / "test_config.json"
|
|
import src.core.config as config_module
|
|
monkeypatch.setattr(config_module, "GLOBAL_CONFIG_FILE", config_path)
|
|
return config_path
|
|
|
|
@pytest.fixture
|
|
def file_manager(self, temp_global_config):
|
|
return FileManager(TagManager())
|
|
|
|
def test_resolve_via_csfd_bridges_original_title_to_tmdb(
|
|
self, file_manager, monkeypatch
|
|
):
|
|
from src.core import csfd, tmdb
|
|
film = csfd.CsfdFilm(
|
|
url="https://www.csfd.cz/film/2294-x/", csfd_id=2294,
|
|
czech_title="Vykoupení z věznice Shawshank",
|
|
original_title="The Shawshank Redemption", year="1994")
|
|
monkeypatch.setattr(csfd, "fetch_film", lambda url, session=None: film)
|
|
monkeypatch.setattr(tmdb, "api_available", lambda: True)
|
|
|
|
captured: dict = {}
|
|
|
|
def fake_search(query, year=None, limit=5):
|
|
captured["query"], captured["year"] = query, year
|
|
return [tmdb.TmdbResult(278, "Vykoupení z věznice Shawshank", "1994", "movie")]
|
|
|
|
monkeypatch.setattr(tmdb, "search_movie", fake_search)
|
|
|
|
result = file_manager.resolve_via_csfd(
|
|
csfd_url="https://www.csfd.cz/film/2294-x/")
|
|
|
|
assert captured == {"query": "The Shawshank Redemption", "year": "1994"}
|
|
assert result["tmdb"].tmdb_id == 278
|
|
assert result["film"] is film
|
|
|
|
def test_resolve_via_csfd_swallows_scrape_failure(self, file_manager, monkeypatch):
|
|
from src.core import csfd
|
|
|
|
def boom(url, session=None):
|
|
raise csfd.CsfdError("anti-bot")
|
|
|
|
monkeypatch.setattr(csfd, "fetch_film", boom)
|
|
result = file_manager.resolve_via_csfd(csfd_url="https://www.csfd.cz/film/1/")
|
|
assert result == {"csfd_url": "https://www.csfd.cz/film/1/",
|
|
"film": None, "tmdb": None}
|
|
|
|
def test_match_pool_uses_csfd_url_for_the_search(
|
|
self, file_manager, tmp_path, monkeypatch
|
|
):
|
|
from src.core import csfd, tmdb
|
|
file_manager.set_pool_dir(tmp_path / "pool")
|
|
(tmp_path / "a.mkv").write_bytes(b"x")
|
|
movie = file_manager.import_movie(
|
|
tmp_path / "a.mkv", "Vykoupení z věznice Shawshank", year=1994,
|
|
csfd_url="https://www.csfd.cz/film/2294-x/")
|
|
|
|
film = csfd.CsfdFilm(
|
|
url=movie.csfd_url, czech_title="Vykoupení z věznice Shawshank",
|
|
original_title="The Shawshank Redemption", year="1994")
|
|
monkeypatch.setattr(csfd, "fetch_film", lambda url, session=None: film)
|
|
monkeypatch.setattr(tmdb, "api_available", lambda: True)
|
|
|
|
seen: list = []
|
|
|
|
def fake_search(query, year=None, limit=5):
|
|
seen.append((query, year))
|
|
return [tmdb.TmdbResult(278, "The Shawshank Redemption", "1994", "movie")]
|
|
|
|
monkeypatch.setattr(tmdb, "search_movie", fake_search)
|
|
|
|
proposals = file_manager.match_pool_to_tmdb([movie])
|
|
|
|
assert seen == [("The Shawshank Redemption", "1994")]
|
|
assert len(proposals) == 1
|
|
assert proposals[0].status == "auto"
|
|
assert proposals[0].chosen.tmdb_id == 278
|
|
assert "ČSFD" in proposals[0].note
|
|
|
|
|
|
class TestSubtitleAttachment:
|
|
"""External subtitle sidecars for movies (attach / list / remove / rename-along)."""
|
|
|
|
@pytest.fixture
|
|
def temp_global_config(self, tmp_path, monkeypatch):
|
|
config_path = tmp_path / "test_config.json"
|
|
import src.core.config as config_module
|
|
monkeypatch.setattr(config_module, "GLOBAL_CONFIG_FILE", config_path)
|
|
return config_path
|
|
|
|
@pytest.fixture
|
|
def file_manager(self, temp_global_config, tmp_path):
|
|
fm = FileManager(TagManager())
|
|
fm.set_pool_dir(tmp_path / "pool")
|
|
return fm
|
|
|
|
def test_attach_subtitle_copies_next_to_movie(self, file_manager, tmp_path):
|
|
(tmp_path / "raw.mkv").write_bytes(b"x")
|
|
movie = file_manager.import_movie(tmp_path / "raw.mkv", "Matrix", year=1999)
|
|
srt = tmp_path / "downloaded.srt"
|
|
srt.write_bytes(b"1\n00:00:01,000 --> 00:00:02,000\nAhoj\n")
|
|
|
|
target = file_manager.attach_subtitle(movie, srt, language="cs")
|
|
|
|
assert target == movie.file_path.with_name("Matrix (1999).cs.srt")
|
|
assert target.exists()
|
|
assert srt.exists() # copied, not moved
|
|
assert [s.path for s in file_manager.list_subtitles(movie)] == [target]
|
|
|
|
def test_remove_subtitle_deletes_file(self, file_manager, tmp_path):
|
|
(tmp_path / "raw.mkv").write_bytes(b"x")
|
|
movie = file_manager.import_movie(tmp_path / "raw.mkv", "Matrix", year=1999)
|
|
srt = tmp_path / "s.srt"
|
|
srt.write_bytes(b"x")
|
|
target = file_manager.attach_subtitle(movie, srt, language="cs")
|
|
|
|
file_manager.remove_subtitle(target)
|
|
|
|
assert not target.exists()
|
|
assert file_manager.list_subtitles(movie) == []
|
|
|
|
def test_rename_movie_carries_subtitle_along(self, file_manager, tmp_path):
|
|
(tmp_path / "raw.mkv").write_bytes(b"x")
|
|
movie = file_manager.import_movie(tmp_path / "raw.mkv", "Matrix", year=1999)
|
|
srt = tmp_path / "s.srt"
|
|
srt.write_bytes(b"x")
|
|
old_sub = file_manager.attach_subtitle(movie, srt, language="cs")
|
|
|
|
file_manager.rename_movie(movie, "Matrix Reloaded")
|
|
|
|
assert not old_sub.exists()
|
|
new_sub = movie.file_path.with_name("Matrix Reloaded.cs.srt")
|
|
assert new_sub.exists()
|
|
|
|
def test_apply_movie_edits_carries_subtitle_along(self, file_manager, tmp_path):
|
|
(tmp_path / "raw.mkv").write_bytes(b"x")
|
|
movie = file_manager.import_movie(tmp_path / "raw.mkv", "Matrix", year=1999)
|
|
srt = tmp_path / "s.srt"
|
|
srt.write_bytes(b"x")
|
|
file_manager.attach_subtitle(movie, srt, language="cs", flag="forced")
|
|
|
|
file_manager.apply_movie_edits(movie, title="Matrix", year=1999, tmdb_id=603)
|
|
|
|
assert movie.file_path.name == "Matrix (1999) [tmdbid-603].mkv"
|
|
assert movie.file_path.with_name(
|
|
"Matrix (1999) [tmdbid-603].cs.forced.srt").exists()
|
|
|
|
def test_move_to_library_carries_subtitle_along(self, file_manager, tmp_path):
|
|
file_manager.set_movie_libraries(["Filmy", "Kids"])
|
|
(tmp_path / "raw.mkv").write_bytes(b"x")
|
|
movie = file_manager.import_movie(tmp_path / "raw.mkv", "Matrix", year=1999)
|
|
srt = tmp_path / "s.srt"
|
|
srt.write_bytes(b"x")
|
|
file_manager.attach_subtitle(movie, srt, language="cs")
|
|
|
|
file_manager.move_to_library(movie, "Kids")
|
|
|
|
assert movie.file_path == tmp_path / "pool" / "Kids" / "Matrix (1999).mkv"
|
|
assert (tmp_path / "pool" / "Kids" / "Matrix (1999).cs.srt").exists()
|
|
assert not (tmp_path / "pool" / "Filmy" / "Matrix (1999).cs.srt").exists()
|
|
|
|
|
|
class TestMinidlnaScope:
|
|
"""Which libraries feed miniDLNA generation (minidlna_libraries / scope)."""
|
|
|
|
@pytest.fixture
|
|
def temp_global_config(self, tmp_path, monkeypatch):
|
|
config_path = tmp_path / "test_config.json"
|
|
import src.core.config as config_module
|
|
monkeypatch.setattr(config_module, "GLOBAL_CONFIG_FILE", config_path)
|
|
return config_path
|
|
|
|
@pytest.fixture
|
|
def file_manager(self, temp_global_config, tmp_path):
|
|
fm = FileManager(TagManager())
|
|
fm.set_pool_dir(tmp_path / "pool")
|
|
return fm
|
|
|
|
def test_default_is_none_meaning_all(self, file_manager):
|
|
assert file_manager.minidlna_libraries is None
|
|
|
|
def test_default_scope_is_the_whole_pool(self, file_manager, tmp_path):
|
|
file_manager.set_libraries([
|
|
{"name": "Filmy", "kind": "movie"}, {"name": "Seriály", "kind": "series"}])
|
|
(tmp_path / "a.mkv").write_bytes(b"x")
|
|
movie = file_manager.import_movie(tmp_path / "a.mkv", "Matrix", year=1999)
|
|
|
|
movies, mirrors, mixed_series = file_manager.minidlna_scope()
|
|
|
|
assert movies == [movie]
|
|
assert mirrors == ["Seriály"]
|
|
assert mixed_series == []
|
|
|
|
def test_scope_filters_to_selected_libraries(self, file_manager, tmp_path):
|
|
file_manager.set_libraries([
|
|
{"name": "Filmy", "kind": "movie"}, {"name": "Kids", "kind": "movie"},
|
|
{"name": "Seriály", "kind": "series"}, {"name": "Dokumenty", "kind": "series"}])
|
|
(tmp_path / "a.mkv").write_bytes(b"x")
|
|
(tmp_path / "b.mkv").write_bytes(b"x")
|
|
in_scope = file_manager.import_movie(
|
|
tmp_path / "a.mkv", "Matrix", year=1999, library="Filmy")
|
|
file_manager.import_movie(tmp_path / "b.mkv", "Kids Movie", year=2005, library="Kids")
|
|
|
|
file_manager.set_minidlna_libraries(["Filmy", "Seriály"])
|
|
movies, mirrors, mixed_series = file_manager.minidlna_scope()
|
|
|
|
assert movies == [in_scope]
|
|
assert mirrors == ["Seriály"]
|
|
assert mixed_series == []
|
|
|
|
def test_homevideo_libraries_are_mirrored_like_series(self, file_manager):
|
|
file_manager.set_libraries([
|
|
{"name": "Seriály", "kind": "series"}, {"name": "Dokumenty", "kind": "homevideo"}])
|
|
|
|
assert file_manager.mirror_as_is_libraries == ["Seriály", "Dokumenty"]
|
|
_, mirrors, _ = file_manager.minidlna_scope()
|
|
assert mirrors == ["Seriály", "Dokumenty"]
|
|
|
|
def test_homevideo_library_excluded_when_not_selected(self, file_manager):
|
|
file_manager.set_libraries([
|
|
{"name": "Seriály", "kind": "series"}, {"name": "Dokumenty", "kind": "homevideo"}])
|
|
file_manager.set_minidlna_libraries(["Seriály"])
|
|
|
|
_, mirrors, _ = file_manager.minidlna_scope()
|
|
assert mirrors == ["Seriály"]
|
|
|
|
def test_copyasis_folders_stays_series_only(self, file_manager):
|
|
"""The library-organizer "copy-as-is" concept doesn't widen to homevideo."""
|
|
file_manager.set_libraries([
|
|
{"name": "Seriály", "kind": "series"}, {"name": "Dokumenty", "kind": "homevideo"}])
|
|
assert file_manager.copyasis_folders == ["Seriály"]
|
|
|
|
def test_set_none_resets_to_all(self, file_manager):
|
|
file_manager.set_minidlna_libraries(["Filmy"])
|
|
file_manager.set_minidlna_libraries(None)
|
|
assert file_manager.minidlna_libraries is None
|
|
|
|
def test_drops_unknown_library_names_on_read(self, file_manager):
|
|
file_manager.set_libraries([{"name": "Filmy", "kind": "movie"}])
|
|
file_manager.set_minidlna_libraries(["Filmy", "Zaniklá knihovna"])
|
|
assert file_manager.minidlna_libraries == ["Filmy"]
|
|
|
|
def test_persists_across_reload(self, file_manager, tmp_path):
|
|
file_manager.set_libraries([{"name": "Filmy", "kind": "movie"}])
|
|
file_manager.set_minidlna_libraries(["Filmy"])
|
|
|
|
reloaded = FileManager(TagManager())
|
|
reloaded.set_pool_dir(tmp_path / "pool")
|
|
assert reloaded.minidlna_libraries == ["Filmy"]
|
|
|
|
def test_mixed_library_series_returned_separately_from_movies(
|
|
self, file_manager, tmp_path
|
|
):
|
|
file_manager.set_libraries([{"name": "Hlavní", "kind": "mixed"}])
|
|
(tmp_path / "movie.mkv").write_bytes(b"x")
|
|
movie = file_manager.import_movie(
|
|
tmp_path / "movie.mkv", "Matrix", year=1999, library="Hlavní")
|
|
(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,
|
|
library="Hlavní")
|
|
|
|
movies, mirrors, mixed_series = file_manager.minidlna_scope()
|
|
|
|
assert movies == [movie]
|
|
assert mirrors == []
|
|
assert len(mixed_series) == 1
|
|
assert mixed_series[0].title == "Breaking Bad"
|
|
assert mixed_series[0].library == "Hlavní"
|
|
|
|
def test_mixed_library_series_excluded_when_library_not_selected(
|
|
self, file_manager, tmp_path
|
|
):
|
|
file_manager.set_libraries([
|
|
{"name": "Hlavní", "kind": "mixed"}, {"name": "Filmy", "kind": "movie"}])
|
|
(tmp_path / "bb.S01E01.mkv").write_bytes(b"x")
|
|
file_manager.import_episodes(
|
|
[tmp_path / "bb.S01E01.mkv"], "Breaking Bad", year=2008, library="Hlavní")
|
|
|
|
file_manager.set_minidlna_libraries(["Filmy"])
|
|
_, _, mixed_series = file_manager.minidlna_scope()
|
|
|
|
assert mixed_series == []
|
|
|
|
|
|
class TestMinidlnaPruneTargets:
|
|
"""What to remove from a previous generation's output once deselected."""
|
|
|
|
@pytest.fixture
|
|
def temp_global_config(self, tmp_path, monkeypatch):
|
|
config_path = tmp_path / "test_config.json"
|
|
import src.core.config as config_module
|
|
monkeypatch.setattr(config_module, "GLOBAL_CONFIG_FILE", config_path)
|
|
return config_path
|
|
|
|
@pytest.fixture
|
|
def file_manager(self, temp_global_config, tmp_path):
|
|
fm = FileManager(TagManager())
|
|
fm.set_pool_dir(tmp_path / "pool")
|
|
return fm
|
|
|
|
def test_deselected_series_library_is_stale(self, file_manager):
|
|
file_manager.set_libraries([
|
|
{"name": "Anime", "kind": "series"}, {"name": "Dokumenty", "kind": "homevideo"}])
|
|
# both deselected — neither is "Seriály" so both go whole-folder
|
|
stale, valid = file_manager.minidlna_prune_targets(
|
|
mirror_libs=[], mixed_series=[], mixed_series_bucket="Seriály")
|
|
assert set(stale) == {"Anime", "Dokumenty"}
|
|
assert valid == set()
|
|
|
|
def test_selected_library_is_not_stale(self, file_manager):
|
|
file_manager.set_libraries([{"name": "Anime", "kind": "series"}])
|
|
stale, _ = file_manager.minidlna_prune_targets(
|
|
mirror_libs=["Anime"], mixed_series=[], mixed_series_bucket="Seriály")
|
|
assert stale == []
|
|
|
|
def test_bucket_named_library_never_whole_folder_removed(self, file_manager):
|
|
"""A real 'Seriály' library, even deselected, is reconciled per-child —
|
|
never nuked via the whole-folder path (that would also erase valid
|
|
mixed-library series sharing the same bucket)."""
|
|
file_manager.set_libraries([{"name": "Seriály", "kind": "series"}])
|
|
stale, valid = file_manager.minidlna_prune_targets(
|
|
mirror_libs=[], mixed_series=[], mixed_series_bucket="Seriály")
|
|
assert "Seriály" not in stale
|
|
assert valid == set()
|
|
|
|
def test_bucket_named_library_selected_contributes_its_children(
|
|
self, file_manager, tmp_path
|
|
):
|
|
file_manager.set_libraries([{"name": "Seriály", "kind": "series"}])
|
|
(tmp_path / "pool" / "Seriály" / "Show A").mkdir(parents=True)
|
|
(tmp_path / "pool" / "Seriály" / "Show B").mkdir(parents=True)
|
|
|
|
_, valid = file_manager.minidlna_prune_targets(
|
|
mirror_libs=["Seriály"], mixed_series=[], mixed_series_bucket="Seriály")
|
|
|
|
assert valid == {"Show A", "Show B"}
|
|
|
|
def test_mixed_series_folder_names_are_valid(self, file_manager, tmp_path):
|
|
file_manager.set_libraries([{"name": "Hlavní", "kind": "mixed"}])
|
|
(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, library="Hlavní")
|
|
_, _, mixed_series = file_manager.minidlna_scope()
|
|
|
|
_, valid = file_manager.minidlna_prune_targets(
|
|
mirror_libs=[], mixed_series=mixed_series, mixed_series_bucket="Seriály")
|
|
|
|
assert valid == {"Breaking Bad (2008) [tmdbid-1396]"}
|
|
|
|
def test_deselected_mixed_series_drops_out_of_valid(self, file_manager, tmp_path):
|
|
"""Simulates re-running after the mixed library is unchecked: the
|
|
previously-mirrored series' folder name is no longer valid."""
|
|
file_manager.set_libraries([{"name": "Hlavní", "kind": "mixed"}])
|
|
(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, library="Hlavní")
|
|
|
|
file_manager.set_minidlna_libraries([]) # deselect everything
|
|
_, _, mixed_series = file_manager.minidlna_scope()
|
|
_, valid = file_manager.minidlna_prune_targets(
|
|
mirror_libs=[], mixed_series=mixed_series, mixed_series_bucket="Seriály")
|
|
|
|
assert mixed_series == []
|
|
assert valid == set()
|
|
|
|
|
|
class TestMovieVersions:
|
|
"""Multiple versions of one movie (2D+3D, 4K+1080p, …) — flat until a
|
|
second file shows up, then grouped into a per-movie folder."""
|
|
|
|
@pytest.fixture
|
|
def temp_global_config(self, tmp_path, monkeypatch):
|
|
config_path = tmp_path / "test_config.json"
|
|
import src.core.config as config_module
|
|
monkeypatch.setattr(config_module, "GLOBAL_CONFIG_FILE", config_path)
|
|
return config_path
|
|
|
|
@pytest.fixture
|
|
def file_manager(self, temp_global_config, tmp_path):
|
|
fm = FileManager(TagManager())
|
|
fm.set_pool_dir(tmp_path / "pool")
|
|
return fm
|
|
|
|
def test_flat_movie_is_not_a_group_member(self, file_manager, tmp_path):
|
|
(tmp_path / "a.mkv").write_bytes(b"x")
|
|
movie = file_manager.import_movie(tmp_path / "a.mkv", "Avatar", year=2009, tmdb_id=19995)
|
|
assert file_manager.is_movie_group_member(movie) is False
|
|
assert file_manager.list_movie_versions(movie) == [movie]
|
|
assert file_manager.visible_movies() == [movie]
|
|
|
|
def test_add_version_promotes_flat_movie_to_a_folder(self, file_manager, tmp_path):
|
|
(tmp_path / "a.mkv").write_bytes(b"2d")
|
|
movie = file_manager.import_movie(tmp_path / "a.mkv", "Avatar", year=2009, tmdb_id=19995)
|
|
(tmp_path / "b.mkv").write_bytes(b"3d")
|
|
|
|
v2 = file_manager.add_movie_version(movie, tmp_path / "b.mkv", "3D HSBS")
|
|
|
|
base = tmp_path / "pool" / "Filmy" / "Avatar (2009) [tmdbid-19995]"
|
|
assert movie.file_path == base / "Avatar (2009) [tmdbid-19995] - Výchozí.mkv"
|
|
assert v2.file_path == base / "Avatar (2009) [tmdbid-19995] - 3D HSBS.mkv"
|
|
assert file_manager.is_movie_group_member(movie)
|
|
assert file_manager.is_movie_group_member(v2)
|
|
assert (tmp_path / "b.mkv").exists() # copied, not moved (move=False default)
|
|
|
|
def test_add_version_carries_subtitle_along_on_promotion(self, file_manager, tmp_path):
|
|
(tmp_path / "a.mkv").write_bytes(b"2d")
|
|
movie = file_manager.import_movie(tmp_path / "a.mkv", "Avatar", year=2009, tmdb_id=19995)
|
|
srt = tmp_path / "s.srt"
|
|
srt.write_bytes(b"x")
|
|
file_manager.attach_subtitle(movie, srt, language="cs")
|
|
(tmp_path / "b.mkv").write_bytes(b"3d")
|
|
|
|
file_manager.add_movie_version(movie, tmp_path / "b.mkv", "3D HSBS")
|
|
|
|
base = tmp_path / "pool" / "Filmy" / "Avatar (2009) [tmdbid-19995]"
|
|
assert (base / "Avatar (2009) [tmdbid-19995] - Výchozí.cs.srt").exists()
|
|
|
|
def test_second_version_added_to_existing_group(self, file_manager, tmp_path):
|
|
(tmp_path / "a.mkv").write_bytes(b"2d")
|
|
movie = file_manager.import_movie(tmp_path / "a.mkv", "Avatar", year=2009, tmdb_id=19995)
|
|
(tmp_path / "b.mkv").write_bytes(b"3d")
|
|
file_manager.add_movie_version(movie, tmp_path / "b.mkv", "3D HSBS")
|
|
(tmp_path / "c.mkv").write_bytes(b"4k")
|
|
|
|
v3 = file_manager.add_movie_version(movie, tmp_path / "c.mkv", "4K")
|
|
|
|
assert len(file_manager.list_movie_versions(movie)) == 3
|
|
assert v3.file_path.name == "Avatar (2009) [tmdbid-19995] - 4K.mkv"
|
|
|
|
def test_add_version_rejects_empty_label(self, file_manager, tmp_path):
|
|
(tmp_path / "a.mkv").write_bytes(b"x")
|
|
movie = file_manager.import_movie(tmp_path / "a.mkv", "Avatar", year=2009, tmdb_id=19995)
|
|
with pytest.raises(ValueError):
|
|
file_manager.add_movie_version(movie, tmp_path / "a.mkv", " ")
|
|
|
|
def test_add_version_rejects_duplicate_label(self, file_manager, tmp_path):
|
|
(tmp_path / "a.mkv").write_bytes(b"2d")
|
|
movie = file_manager.import_movie(tmp_path / "a.mkv", "Avatar", year=2009, tmdb_id=19995)
|
|
(tmp_path / "b.mkv").write_bytes(b"3d")
|
|
file_manager.add_movie_version(movie, tmp_path / "b.mkv", "3D")
|
|
(tmp_path / "c.mkv").write_bytes(b"3d-2")
|
|
with pytest.raises(FileExistsError):
|
|
file_manager.add_movie_version(movie, tmp_path / "c.mkv", "3D")
|
|
|
|
def test_visible_movies_shows_only_the_primary_version(self, file_manager, tmp_path):
|
|
(tmp_path / "a.mkv").write_bytes(b"2d")
|
|
movie = file_manager.import_movie(tmp_path / "a.mkv", "Avatar", year=2009, tmdb_id=19995)
|
|
movie.add_tag("Žánr/Sci-Fi")
|
|
movie.csfd_url = "https://www.csfd.cz/film/avatar/"
|
|
movie.save_metadata()
|
|
(tmp_path / "b.mkv").write_bytes(b"3d")
|
|
file_manager.add_movie_version(movie, tmp_path / "b.mkv", "3D HSBS")
|
|
|
|
visible = file_manager.visible_movies()
|
|
|
|
assert len(visible) == 1
|
|
assert visible[0].file_path == movie.file_path
|
|
assert visible[0].title == "Avatar"
|
|
assert visible[0].csfd_url == "https://www.csfd.cz/film/avatar/"
|
|
assert {t.full_path for t in visible[0].tags} == {"Žánr/Sci-Fi"}
|
|
|
|
def test_remove_version_keeps_group_when_more_than_one_remains(self, file_manager, tmp_path):
|
|
(tmp_path / "a.mkv").write_bytes(b"2d")
|
|
movie = file_manager.import_movie(tmp_path / "a.mkv", "Avatar", year=2009, tmdb_id=19995)
|
|
(tmp_path / "b.mkv").write_bytes(b"3d")
|
|
v2 = file_manager.add_movie_version(movie, tmp_path / "b.mkv", "3D")
|
|
(tmp_path / "c.mkv").write_bytes(b"4k")
|
|
v3 = file_manager.add_movie_version(movie, tmp_path / "c.mkv", "4K")
|
|
|
|
file_manager.remove_movie_version(v3)
|
|
|
|
assert not v3.file_path.exists()
|
|
assert file_manager.is_movie_group_member(movie)
|
|
assert file_manager.is_movie_group_member(v2)
|
|
assert len(file_manager.list_movie_versions(movie)) == 2
|
|
|
|
def test_remove_last_extra_version_demotes_to_flat(self, file_manager, tmp_path):
|
|
(tmp_path / "a.mkv").write_bytes(b"2d")
|
|
movie = file_manager.import_movie(tmp_path / "a.mkv", "Avatar", year=2009, tmdb_id=19995)
|
|
(tmp_path / "b.mkv").write_bytes(b"3d")
|
|
v2 = file_manager.add_movie_version(movie, tmp_path / "b.mkv", "3D")
|
|
|
|
file_manager.remove_movie_version(v2)
|
|
|
|
assert not file_manager.is_movie_group_member(movie)
|
|
assert movie.file_path == tmp_path / "pool" / "Filmy" / "Avatar (2009) [tmdbid-19995].mkv"
|
|
assert movie.file_path.exists()
|
|
assert not (tmp_path / "pool" / "Filmy" / "Avatar (2009) [tmdbid-19995]").exists()
|
|
|
|
def test_removing_primary_migrates_metadata_to_survivor(self, file_manager, tmp_path):
|
|
(tmp_path / "a.mkv").write_bytes(b"2d")
|
|
movie = file_manager.import_movie(tmp_path / "a.mkv", "Avatar", year=2009, tmdb_id=19995)
|
|
movie.add_tag("Žánr/Sci-Fi")
|
|
movie.csfd_url = "https://www.csfd.cz/film/avatar/"
|
|
movie.attributes = {"collection_sort": "01"}
|
|
movie.save_metadata()
|
|
(tmp_path / "b.mkv").write_bytes(b"3d")
|
|
v2 = file_manager.add_movie_version(movie, tmp_path / "b.mkv", "3D")
|
|
|
|
file_manager.remove_movie_version(movie) # remove the primary
|
|
|
|
assert v2.title == "Avatar"
|
|
assert v2.csfd_url == "https://www.csfd.cz/film/avatar/"
|
|
assert v2.attributes == {"collection_sort": "01"}
|
|
assert {t.full_path for t in v2.tags} == {"Žánr/Sci-Fi"}
|
|
assert not file_manager.is_movie_group_member(v2) # demoted, only one left
|
|
|
|
def test_remove_version_rejects_ungrouped_movie(self, file_manager, tmp_path):
|
|
(tmp_path / "a.mkv").write_bytes(b"x")
|
|
movie = file_manager.import_movie(tmp_path / "a.mkv", "Avatar", year=2009, tmdb_id=19995)
|
|
with pytest.raises(ValueError):
|
|
file_manager.remove_movie_version(movie)
|
|
|
|
def test_rename_movie_refuses_grouped_movie(self, file_manager, tmp_path):
|
|
(tmp_path / "a.mkv").write_bytes(b"2d")
|
|
movie = file_manager.import_movie(tmp_path / "a.mkv", "Avatar", year=2009, tmdb_id=19995)
|
|
(tmp_path / "b.mkv").write_bytes(b"3d")
|
|
file_manager.add_movie_version(movie, tmp_path / "b.mkv", "3D")
|
|
|
|
with pytest.raises(ValueError):
|
|
file_manager.rename_movie(movie, "Avatar Reloaded")
|
|
|
|
def test_move_to_library_refuses_grouped_movie(self, file_manager, tmp_path):
|
|
file_manager.set_movie_libraries(["Filmy", "Kids"])
|
|
(tmp_path / "a.mkv").write_bytes(b"2d")
|
|
movie = file_manager.import_movie(tmp_path / "a.mkv", "Avatar", year=2009, tmdb_id=19995)
|
|
(tmp_path / "b.mkv").write_bytes(b"3d")
|
|
file_manager.add_movie_version(movie, tmp_path / "b.mkv", "3D")
|
|
|
|
with pytest.raises(ValueError):
|
|
file_manager.move_to_library(movie, "Kids")
|
|
|
|
def test_apply_movie_edits_refuses_rename_of_grouped_movie(self, file_manager, tmp_path):
|
|
(tmp_path / "a.mkv").write_bytes(b"2d")
|
|
movie = file_manager.import_movie(tmp_path / "a.mkv", "Avatar", year=2009, tmdb_id=19995)
|
|
(tmp_path / "b.mkv").write_bytes(b"3d")
|
|
file_manager.add_movie_version(movie, tmp_path / "b.mkv", "3D")
|
|
|
|
with pytest.raises(ValueError):
|
|
file_manager.apply_movie_edits(movie, title="Avatar Reloaded", year=2009, tmdb_id=19995)
|
|
|
|
def test_apply_movie_edits_allows_metadata_only_edit_of_grouped_movie(
|
|
self, file_manager, tmp_path
|
|
):
|
|
"""Same title/year/tmdb_id → no path change → date/tags/etc still work."""
|
|
(tmp_path / "a.mkv").write_bytes(b"2d")
|
|
movie = file_manager.import_movie(tmp_path / "a.mkv", "Avatar", year=2009, tmdb_id=19995)
|
|
(tmp_path / "b.mkv").write_bytes(b"3d")
|
|
file_manager.add_movie_version(movie, tmp_path / "b.mkv", "3D")
|
|
|
|
file_manager.apply_movie_edits(
|
|
movie, title="Avatar", year=2009, tmdb_id=19995, date="2026-01-01")
|
|
|
|
assert movie.date == "2026-01-01"
|
|
assert file_manager.is_movie_group_member(movie)
|
|
|
|
def test_rename_to_canonical_skips_grouped_movie(self, file_manager, tmp_path):
|
|
(tmp_path / "a.mkv").write_bytes(b"2d")
|
|
movie = file_manager.import_movie(tmp_path / "a.mkv", "Avatar", year=2009, tmdb_id=19995)
|
|
(tmp_path / "b.mkv").write_bytes(b"3d")
|
|
file_manager.add_movie_version(movie, tmp_path / "b.mkv", "3D")
|
|
|
|
movie.title = "Avatar Retitled"
|
|
assert file_manager.rename_to_canonical(movie) is None
|
|
assert movie.title == "Avatar Retitled" # metadata field unaffected
|
|
assert file_manager.is_movie_group_member(movie) # file untouched
|