2547 lines
102 KiB
Python
2547 lines
102 KiB
Python
import pytest
|
|
|
|
from src.core.file import File
|
|
from src.core.file_manager import FileManager
|
|
from src.core.tag import Tag
|
|
from src.core.tag_manager import TagManager
|
|
|
|
|
|
@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].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].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_with_empty_label_becomes_vychozi(self, file_manager, tmp_path):
|
|
"""A blank popisek is a plain "2D" version with nothing else to call
|
|
it — it becomes DEFAULT_VERSION_LABEL instead of being rejected."""
|
|
(tmp_path / "a.mkv").write_bytes(b"3d")
|
|
movie = file_manager.import_movie(
|
|
tmp_path / "a.mkv", "Avatar", year=2009, tmdb_id=19995, format_3d="HSBS"
|
|
)
|
|
(tmp_path / "b.mkv").write_bytes(b"2d")
|
|
|
|
v2 = file_manager.add_movie_version(movie, tmp_path / "b.mkv", " ")
|
|
|
|
base = tmp_path / "pool" / "Filmy" / "Avatar (2009) [tmdbid-19995]"
|
|
assert v2.file_path == base / "Avatar (2009) [tmdbid-19995].mkv"
|
|
# the pre-existing 3D file keeps its own 3D formát automatically —
|
|
# promotion never silently drops it just because no one asked for it
|
|
assert movie.file_path == base / "Avatar (2009) [tmdbid-19995] - 3D HSBS.mkv"
|
|
|
|
def test_add_version_rejects_a_blank_label_that_collides_with_vychozi(
|
|
self, file_manager, tmp_path
|
|
):
|
|
(tmp_path / "a.mkv").write_bytes(b"x") # no 3D formát → promotes as "Výchozí"
|
|
movie = file_manager.import_movie(
|
|
tmp_path / "a.mkv", "Avatar", year=2009, tmdb_id=19995
|
|
)
|
|
with pytest.raises(FileExistsError):
|
|
file_manager.add_movie_version(movie, tmp_path / "a.mkv", " ")
|
|
|
|
def test_add_version_promotes_a_movie_in_a_mixed_library_correctly(
|
|
self, file_manager, tmp_path
|
|
):
|
|
"""A movie's version folder and a series folder are both
|
|
|
|
``Title (rok) [tmdbid]/`` — indistinguishable by name alone. Told
|
|
apart by shape instead: a series always nests episodes in a
|
|
``Season NN``/``Specials`` folder, a movie's version folder never
|
|
does. Promoting here must keep the movie a movie, not turn it into a
|
|
mis-parsed "series" on reload.
|
|
"""
|
|
file_manager.set_libraries([{"name": "Hlavní", "kind": "mixed"}])
|
|
(tmp_path / "a.mkv").write_bytes(b"2d")
|
|
movie = file_manager.import_movie(
|
|
tmp_path / "a.mkv", "Avatar", year=2009, tmdb_id=19995, library="Hlavní"
|
|
)
|
|
(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" / "Hlavní" / "Avatar (2009) [tmdbid-19995]"
|
|
assert movie.file_path == base / "Avatar (2009) [tmdbid-19995].mkv"
|
|
assert file_manager.is_movie_group_member(movie)
|
|
assert file_manager.is_movie_group_member(v2)
|
|
|
|
# survives a fresh reload from disk too — still a movie, not a series
|
|
file_manager.load_pool_movies()
|
|
assert {f.file_path for f in file_manager.filelist} == {
|
|
movie.file_path,
|
|
v2.file_path,
|
|
}
|
|
assert file_manager.load_series() == []
|
|
|
|
def test_a_real_series_in_a_mixed_library_stays_a_series(
|
|
self, file_manager, tmp_path
|
|
):
|
|
"""A movie folder and a series folder can sit side by side in the same
|
|
|
|
mixed library without either one leaking into the other's listing.
|
|
"""
|
|
file_manager.set_libraries([{"name": "Hlavní", "kind": "mixed"}])
|
|
(tmp_path / "a.mkv").write_bytes(b"2d")
|
|
movie = file_manager.import_movie(
|
|
tmp_path / "a.mkv", "Avatar", year=2009, tmdb_id=19995, library="Hlavní"
|
|
)
|
|
(tmp_path / "b.mkv").write_bytes(b"3d")
|
|
file_manager.add_movie_version(movie, tmp_path / "b.mkv", "3D HSBS")
|
|
|
|
ep = (
|
|
tmp_path
|
|
/ "pool"
|
|
/ "Hlavní"
|
|
/ "Breaking Bad (2008) [tmdbid-1396]"
|
|
/ "Season 01"
|
|
/ "S01E01 - Breaking Bad.mkv"
|
|
)
|
|
ep.parent.mkdir(parents=True)
|
|
ep.write_bytes(b"x")
|
|
|
|
file_manager.load_pool_movies()
|
|
assert {f.file_path.parent.name for f in file_manager.filelist} == {
|
|
"Avatar (2009) [tmdbid-19995]"
|
|
}
|
|
series_list = file_manager.load_series()
|
|
assert [s.title for s in series_list] == ["Breaking Bad"]
|
|
assert len(series_list[0].episodes) == 1
|
|
|
|
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_rename_movie_version_changes_the_label_and_filename(
|
|
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")
|
|
old_path = v2.file_path
|
|
|
|
renamed = file_manager.rename_movie_version(v2, "4K")
|
|
|
|
base = tmp_path / "pool" / "Filmy" / "Avatar (2009) [tmdbid-19995]"
|
|
assert renamed.file_path == base / "Avatar (2009) [tmdbid-19995] - 4K.mkv"
|
|
assert not old_path.exists()
|
|
assert renamed.file_path.exists()
|
|
|
|
def test_rename_movie_version_carries_subtitle_along(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")
|
|
srt = tmp_path / "s.srt"
|
|
srt.write_bytes(b"x")
|
|
file_manager.attach_subtitle(v2, srt, language="cs")
|
|
|
|
file_manager.rename_movie_version(v2, "4K")
|
|
|
|
base = tmp_path / "pool" / "Filmy" / "Avatar (2009) [tmdbid-19995]"
|
|
assert (base / "Avatar (2009) [tmdbid-19995] - 4K.cs.srt").exists()
|
|
|
|
def test_rename_movie_version_keeps_the_shared_metadata(
|
|
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.save_metadata()
|
|
(tmp_path / "b.mkv").write_bytes(b"3d")
|
|
v2 = file_manager.add_movie_version(movie, tmp_path / "b.mkv", "3D HSBS")
|
|
|
|
renamed = file_manager.rename_movie_version(movie, "Výchozí kvalita")
|
|
|
|
base = tmp_path / "pool" / "Filmy" / "Avatar (2009) [tmdbid-19995]"
|
|
assert (
|
|
renamed.file_path
|
|
== base / "Avatar (2009) [tmdbid-19995] - Výchozí kvalita.mkv"
|
|
)
|
|
assert {t.full_path for t in renamed.tags} == {"Žánr/Sci-Fi"}
|
|
assert {t.full_path for t in v2.tags} == {"Žánr/Sci-Fi"} # shared, untouched
|
|
|
|
def test_rename_movie_version_rejects_a_flat_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.rename_movie_version(movie, "4K")
|
|
|
|
def test_rename_movie_version_with_empty_label_becomes_vychozi(
|
|
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.rename_movie_version(movie, "Jiná") # free up "Výchozí" first
|
|
|
|
renamed = file_manager.rename_movie_version(v2, " ")
|
|
|
|
base = tmp_path / "pool" / "Filmy" / "Avatar (2009) [tmdbid-19995]"
|
|
assert renamed.file_path == base / "Avatar (2009) [tmdbid-19995].mkv"
|
|
|
|
def test_rename_movie_version_rejects_a_blank_label_that_collides(
|
|
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")
|
|
with pytest.raises(FileExistsError):
|
|
file_manager.rename_movie_version(v2, " ") # movie already owns "Výchozí"
|
|
|
|
def test_rename_movie_version_rejects_a_label_already_taken(
|
|
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")
|
|
with pytest.raises(FileExistsError):
|
|
file_manager.rename_movie_version(v2, "Výchozí") # movie's own label
|
|
|
|
def test_rename_movie_version_to_the_same_label_is_a_noop(
|
|
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")
|
|
old_path = v2.file_path
|
|
|
|
result = file_manager.rename_movie_version(v2, "3D")
|
|
|
|
assert result.file_path == old_path
|
|
assert old_path.exists()
|
|
|
|
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_demoting_to_a_3d_survivor_restores_its_3d_tag(
|
|
self, file_manager, tmp_path
|
|
):
|
|
"""The sole survivor's own popisek was a 3D formát — the flat name it
|
|
|
|
demotes back to must say so too, not silently become a plain file.
|
|
"""
|
|
(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")
|
|
|
|
file_manager.remove_movie_version(movie) # keep only the 3D version
|
|
|
|
assert not file_manager.is_movie_group_member(v2)
|
|
assert (
|
|
v2.file_path
|
|
== tmp_path / "pool" / "Filmy" / "Avatar (2009) [tmdbid-19995].3D.HSBS.mkv"
|
|
)
|
|
assert v2.file_path.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
|
|
|
|
|
|
class TestDuplicates:
|
|
"""Duplicate detection (Nástroje → Kontrola duplicit) and merging copies
|
|
of one movie into a single multi-version movie."""
|
|
|
|
@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 _import(self, fm, tmp_path, name, content, title="Avatar", **kwargs):
|
|
src = tmp_path / name
|
|
src.write_bytes(content)
|
|
kwargs.setdefault("year", 2009)
|
|
return fm.import_movie(src, title, **kwargs)
|
|
|
|
def test_find_duplicates_empty_for_distinct_movies(self, file_manager, tmp_path):
|
|
self._import(file_manager, tmp_path, "a.mkv", b"a", tmdb_id=19995)
|
|
self._import(
|
|
file_manager,
|
|
tmp_path,
|
|
"b.mkv",
|
|
b"bb",
|
|
title="Matrix",
|
|
year=1999,
|
|
tmdb_id=603,
|
|
)
|
|
|
|
assert file_manager.find_duplicates() == []
|
|
|
|
def test_find_duplicates_flags_flat_copies_with_same_tmdb_id(
|
|
self, file_manager, tmp_path
|
|
):
|
|
plain = self._import(file_manager, tmp_path, "a.mkv", b"2d", tmdb_id=19995)
|
|
three_d = self._import(
|
|
file_manager, tmp_path, "b.mkv", b"3d-x", tmdb_id=19995, format_3d="HSBS"
|
|
)
|
|
|
|
groups = file_manager.find_duplicates()
|
|
|
|
assert len(groups) == 1
|
|
assert {f.file_path for f in groups[0]["movies"]} == {
|
|
plain.file_path,
|
|
three_d.file_path,
|
|
}
|
|
assert groups[0]["reasons"] == ["stejné TMDb ID 19995"]
|
|
|
|
def test_find_duplicates_flags_same_title_and_year_without_id(
|
|
self, file_manager, tmp_path
|
|
):
|
|
self._import(file_manager, tmp_path, "a.mkv", b"a", tmdb_id=19995)
|
|
self._import(
|
|
file_manager,
|
|
tmp_path,
|
|
"b.mkv",
|
|
b"bb",
|
|
library="Filmy",
|
|
on_conflict="suffix",
|
|
)
|
|
|
|
groups = file_manager.find_duplicates()
|
|
|
|
assert len(groups) == 1
|
|
assert "stejný název a rok" in groups[0]["reasons"]
|
|
|
|
def test_find_duplicates_flags_identical_size(self, file_manager, tmp_path):
|
|
self._import(file_manager, tmp_path, "a.mkv", b"same", tmdb_id=19995)
|
|
self._import(
|
|
file_manager,
|
|
tmp_path,
|
|
"b.mkv",
|
|
b"same",
|
|
title="Matrix",
|
|
year=1999,
|
|
tmdb_id=603,
|
|
)
|
|
|
|
groups = file_manager.find_duplicates()
|
|
|
|
assert len(groups) == 1
|
|
assert groups[0]["reasons"] == ["shodná velikost souboru"]
|
|
|
|
def test_versions_of_one_movie_are_not_duplicates(self, file_manager, tmp_path):
|
|
movie = self._import(file_manager, tmp_path, "a.mkv", b"same", tmdb_id=19995)
|
|
(tmp_path / "b.mkv").write_bytes(b"same")
|
|
file_manager.add_movie_version(movie, tmp_path / "b.mkv", "3D HSBS")
|
|
|
|
assert file_manager.find_duplicates() == []
|
|
|
|
def test_merge_as_version_groups_the_two_copies(self, file_manager, tmp_path):
|
|
plain = self._import(file_manager, tmp_path, "a.mkv", b"2d", tmdb_id=19995)
|
|
plain.csfd_url = None
|
|
plain.save_metadata()
|
|
three_d = self._import(
|
|
file_manager, tmp_path, "b.mkv", b"3d-x", tmdb_id=19995, format_3d="HSBS"
|
|
)
|
|
old_3d_path = three_d.file_path
|
|
|
|
new_version = file_manager.merge_as_version(plain, three_d, "3D HSBS")
|
|
|
|
folder = tmp_path / "pool" / "Filmy" / "Avatar (2009) [tmdbid-19995]"
|
|
assert not old_3d_path.exists()
|
|
assert (
|
|
new_version.file_path
|
|
== folder / "Avatar (2009) [tmdbid-19995] - 3D HSBS.mkv"
|
|
)
|
|
assert plain.file_path == folder / "Avatar (2009) [tmdbid-19995].mkv"
|
|
assert three_d not in file_manager.filelist
|
|
assert file_manager.find_duplicates() == []
|
|
assert len(file_manager.visible_movies()) == 1
|
|
record = file_manager.index.get_movie("tmdb-19995")
|
|
assert sorted(m["label"] for m in record["media"]) == [
|
|
"",
|
|
"3D HSBS",
|
|
] # "" = default
|
|
|
|
def test_merge_as_version_preserves_keeps_own_3d_tag_automatically(
|
|
self, file_manager, tmp_path
|
|
):
|
|
"""``keep`` is itself a flat, 3D-only movie — promoting it during the
|
|
|
|
merge must carry its own 3D formát over on its own, without the
|
|
caller having to compute/pass ``keep_label`` explicitly.
|
|
"""
|
|
keep = self._import(
|
|
file_manager, tmp_path, "a.mkv", b"3d-a", tmdb_id=19995, format_3d="FTAB"
|
|
)
|
|
other = self._import(
|
|
file_manager,
|
|
tmp_path,
|
|
"b.mkv",
|
|
b"2d-b",
|
|
title="Avatar",
|
|
year=2009,
|
|
on_conflict="suffix",
|
|
)
|
|
|
|
new_version = file_manager.merge_as_version(keep, other, "")
|
|
|
|
folder = tmp_path / "pool" / "Filmy" / "Avatar (2009) [tmdbid-19995]"
|
|
assert keep.file_path == folder / "Avatar (2009) [tmdbid-19995] - 3D FTAB.mkv"
|
|
assert new_version.file_path == folder / "Avatar (2009) [tmdbid-19995].mkv"
|
|
|
|
def test_merge_as_version_carries_tags_csfd_and_subtitles(
|
|
self, file_manager, tmp_path
|
|
):
|
|
keep = self._import(file_manager, tmp_path, "a.mkv", b"a", tmdb_id=19995)
|
|
other = self._import(
|
|
file_manager, tmp_path, "b.mkv", b"bb", on_conflict="suffix"
|
|
)
|
|
other.add_tag("Žánr/Sci-Fi")
|
|
other.csfd_url = "https://www.csfd.cz/film/avatar/"
|
|
other.save_metadata()
|
|
srt = tmp_path / "s.srt"
|
|
srt.write_bytes(b"x")
|
|
file_manager.attach_subtitle(other, srt, language="cs")
|
|
|
|
new_version = file_manager.merge_as_version(keep, other, "4K")
|
|
|
|
assert keep.csfd_url == "https://www.csfd.cz/film/avatar/"
|
|
assert {t.full_path for t in keep.tags} == {"Žánr/Sci-Fi"}
|
|
assert (
|
|
new_version.file_path.parent / f"{new_version.file_path.stem}.cs.srt"
|
|
).exists()
|
|
# the id-less duplicate's own record is gone — no phantom movie left
|
|
assert [k for k in file_manager.index.all_movies()] == ["tmdb-19995"]
|
|
|
|
def test_merge_as_version_rejects_same_file(self, file_manager, tmp_path):
|
|
movie = self._import(file_manager, tmp_path, "a.mkv", b"a", tmdb_id=19995)
|
|
with pytest.raises(ValueError):
|
|
file_manager.merge_as_version(movie, movie, "3D")
|
|
|
|
def test_remove_movie_deletes_every_version(self, file_manager, tmp_path):
|
|
movie = self._import(file_manager, tmp_path, "a.mkv", b"2d", tmdb_id=19995)
|
|
(tmp_path / "b.mkv").write_bytes(b"3d")
|
|
file_manager.add_movie_version(movie, tmp_path / "b.mkv", "3D HSBS")
|
|
folder = movie.file_path.parent
|
|
|
|
file_manager.remove_movie(movie)
|
|
|
|
assert not folder.exists()
|
|
assert file_manager.filelist == []
|
|
assert file_manager.index.all_movies() == {}
|
|
|
|
|
|
class TestDefaultVersionNaming:
|
|
"""The plain/default version of a grouped movie carries no label at all —
|
|
``Folder/Folder.ext`` — and importing a film that's already in the pool
|
|
(even if it's grouped) must add a version, never a root-level duplicate."""
|
|
|
|
@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 _grouped_hobbit(self, fm, tmp_path):
|
|
(tmp_path / "h1.mkv").write_bytes(b"hobit-2d")
|
|
movie = fm.import_movie(tmp_path / "h1.mkv", "Hobit", year=2013, tmdb_id=49051)
|
|
(tmp_path / "h2.mkv").write_bytes(b"hobit-3d")
|
|
v2 = fm.add_movie_version(movie, tmp_path / "h2.mkv", "3D HSBS")
|
|
return movie, v2
|
|
|
|
def test_promoted_default_version_has_bare_name(self, file_manager, tmp_path):
|
|
movie, _v2 = self._grouped_hobbit(file_manager, tmp_path)
|
|
folder = tmp_path / "pool" / "Filmy" / "Hobit (2013) [tmdbid-49051]"
|
|
assert movie.file_path == folder / "Hobit (2013) [tmdbid-49051].mkv"
|
|
|
|
def test_new_default_version_has_bare_name(self, file_manager, tmp_path):
|
|
movie, _v2 = self._grouped_hobbit(file_manager, tmp_path)
|
|
(tmp_path / "h3.mkv").write_bytes(b"hobit-4k")
|
|
v3 = file_manager.add_movie_version(movie, tmp_path / "h3.mkv", "4K")
|
|
(tmp_path / "h4.mkv").write_bytes(b"hobit-plain")
|
|
with pytest.raises(FileExistsError):
|
|
file_manager.add_movie_version(
|
|
movie, tmp_path / "h4.mkv", ""
|
|
) # bare exists
|
|
assert v3.file_path.name == "Hobit (2013) [tmdbid-49051] - 4K.mkv"
|
|
|
|
def test_pooled_with_stem_finds_grouped_members_by_identity(
|
|
self, file_manager, tmp_path
|
|
):
|
|
self._grouped_hobbit(file_manager, tmp_path)
|
|
found = file_manager.pooled_with_stem("Hobit (2013) [tmdbid-49051]")
|
|
assert len(found) == 2
|
|
|
|
def test_import_of_a_film_already_in_the_pool_adds_a_version_not_a_duplicate(
|
|
self, file_manager, tmp_path
|
|
):
|
|
movie, _v2 = self._grouped_hobbit(file_manager, tmp_path)
|
|
(tmp_path / "h5.mkv").write_bytes(b"hobit-4k")
|
|
|
|
result = file_manager.import_movie(
|
|
tmp_path / "h5.mkv",
|
|
"Hobit",
|
|
year=2013,
|
|
tmdb_id=49051,
|
|
on_conflict="version",
|
|
version_label="4K",
|
|
)
|
|
|
|
folder = tmp_path / "pool" / "Filmy" / "Hobit (2013) [tmdbid-49051]"
|
|
assert result.file_path == folder / "Hobit (2013) [tmdbid-49051] - 4K.mkv"
|
|
assert len(file_manager.list_movie_versions(movie)) == 3
|
|
root_files = [
|
|
p.name for p in (tmp_path / "pool" / "Filmy").iterdir() if p.is_file()
|
|
]
|
|
assert root_files == [] # nothing loose at the library root
|
|
|
|
def test_plain_reimport_of_an_existing_plain_version_is_refused_not_duplicated(
|
|
self, file_manager, tmp_path
|
|
):
|
|
movie, _v2 = self._grouped_hobbit(file_manager, tmp_path)
|
|
(tmp_path / "h8.mkv").write_bytes(b"hobit-plain-again")
|
|
|
|
with pytest.raises(FileExistsError):
|
|
file_manager.import_movie(
|
|
tmp_path / "h8.mkv",
|
|
"Hobit",
|
|
year=2013,
|
|
tmdb_id=49051,
|
|
on_conflict="version",
|
|
)
|
|
|
|
assert len(file_manager.list_movie_versions(movie)) == 2
|
|
root_files = [
|
|
p.name for p in (tmp_path / "pool" / "Filmy").iterdir() if p.is_file()
|
|
]
|
|
assert root_files == []
|
|
|
|
def test_import_of_a_3d_file_adds_it_as_a_3d_version(self, file_manager, tmp_path):
|
|
_movie, _v2 = self._grouped_hobbit(file_manager, tmp_path)
|
|
(tmp_path / "h6.mkv").write_bytes(b"hobit-ftab")
|
|
|
|
result = file_manager.import_movie(
|
|
tmp_path / "h6.mkv",
|
|
"Hobit",
|
|
year=2013,
|
|
tmdb_id=49051,
|
|
on_conflict="version",
|
|
format_3d="FTAB",
|
|
)
|
|
|
|
assert result.file_path.name == "Hobit (2013) [tmdbid-49051] - 3D FTAB.mkv"
|
|
|
|
def test_import_replace_removes_the_whole_group_not_just_one_file(
|
|
self, file_manager, tmp_path
|
|
):
|
|
_movie, _v2 = self._grouped_hobbit(file_manager, tmp_path)
|
|
(tmp_path / "h7.mkv").write_bytes(b"hobit-new")
|
|
|
|
file_manager.import_movie(
|
|
tmp_path / "h7.mkv",
|
|
"Hobit",
|
|
year=2013,
|
|
tmdb_id=49051,
|
|
on_conflict="replace",
|
|
)
|
|
|
|
folder = tmp_path / "pool" / "Filmy" / "Hobit (2013) [tmdbid-49051]"
|
|
assert not folder.exists() # the old group's folder is gone entirely
|
|
assert len(file_manager.load_pool_movies() or file_manager.filelist) == 1
|
|
|
|
def test_normalize_renames_existing_vychozi_files_to_bare_names(
|
|
self, file_manager, tmp_path
|
|
):
|
|
"""Pool files written before this change still carry " - Výchozí"."""
|
|
folder = tmp_path / "pool" / "Filmy" / "Hobit (2013) [tmdbid-49051]"
|
|
folder.mkdir(parents=True)
|
|
(folder / "Hobit (2013) [tmdbid-49051] - Výchozí.mkv").write_bytes(b"x")
|
|
(folder / "Hobit (2013) [tmdbid-49051] - 3D HSBS.mkv").write_bytes(b"y")
|
|
file_manager.load_pool_movies()
|
|
|
|
renamed = file_manager.normalize_default_version_names()
|
|
|
|
assert renamed == [
|
|
(
|
|
"Hobit (2013) [tmdbid-49051] - Výchozí.mkv",
|
|
"Hobit (2013) [tmdbid-49051].mkv",
|
|
)
|
|
]
|
|
assert (folder / "Hobit (2013) [tmdbid-49051].mkv").exists()
|
|
assert (folder / "Hobit (2013) [tmdbid-49051] - 3D HSBS.mkv").exists()
|
|
|
|
|
|
class TestMovieParts:
|
|
"""One film split into several files (Jellyfin multi-part / stacking):
|
|
``Folder/Folder-part1.ext``, ``Folder-part2.ext`` — played back to back as
|
|
one movie, unlike versions, which are alternatives to choose from."""
|
|
|
|
@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
|
|
|
|
FOLDER = "Film (2010) [tmdbid-12345]"
|
|
|
|
def _flat(self, fm, tmp_path):
|
|
(tmp_path / "a.mkv").write_bytes(b"part-one")
|
|
return fm.import_movie(tmp_path / "a.mkv", "Film", year=2010, tmdb_id=12345)
|
|
|
|
def test_add_part_promotes_flat_movie_to_part1_and_part2(
|
|
self, file_manager, tmp_path
|
|
):
|
|
movie = self._flat(file_manager, tmp_path)
|
|
(tmp_path / "b.mkv").write_bytes(b"part-two")
|
|
|
|
p2 = file_manager.add_movie_part(movie, tmp_path / "b.mkv")
|
|
|
|
folder = tmp_path / "pool" / "Filmy" / self.FOLDER
|
|
assert movie.file_path == folder / f"{self.FOLDER}-part1.mkv"
|
|
assert p2.file_path == folder / f"{self.FOLDER}-part2.mkv"
|
|
assert file_manager.visible_movies() == [movie] # still one movie
|
|
|
|
def test_add_part_appends_the_next_number(self, file_manager, tmp_path):
|
|
movie = self._flat(file_manager, tmp_path)
|
|
(tmp_path / "b.mkv").write_bytes(b"part-two")
|
|
file_manager.add_movie_part(movie, tmp_path / "b.mkv")
|
|
(tmp_path / "c.mkv").write_bytes(b"part-three")
|
|
|
|
p3 = file_manager.add_movie_part(movie, tmp_path / "c.mkv")
|
|
|
|
assert p3.file_path.name == f"{self.FOLDER}-part3.mkv"
|
|
|
|
def test_part_labels_survive_a_reload(self, file_manager, tmp_path):
|
|
movie = self._flat(file_manager, tmp_path)
|
|
(tmp_path / "b.mkv").write_bytes(b"part-two")
|
|
file_manager.add_movie_part(movie, tmp_path / "b.mkv")
|
|
|
|
file_manager.load_pool_movies()
|
|
|
|
labels = sorted(f._identity_from_path()[4] for f in file_manager.filelist)
|
|
assert labels == ["part1", "part2"]
|
|
assert len(file_manager.visible_movies()) == 1
|
|
|
|
def test_part_subtitles_follow_their_part(self, file_manager, tmp_path):
|
|
movie = self._flat(file_manager, tmp_path)
|
|
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"part-two")
|
|
|
|
file_manager.add_movie_part(movie, tmp_path / "b.mkv")
|
|
|
|
folder = tmp_path / "pool" / "Filmy" / self.FOLDER
|
|
assert (folder / f"{self.FOLDER}-part1.cs.srt").exists()
|
|
|
|
def test_removing_down_to_one_part_demotes_to_flat(self, file_manager, tmp_path):
|
|
movie = self._flat(file_manager, tmp_path)
|
|
(tmp_path / "b.mkv").write_bytes(b"part-two")
|
|
p2 = file_manager.add_movie_part(movie, tmp_path / "b.mkv")
|
|
|
|
file_manager.remove_movie_version(p2)
|
|
|
|
assert movie.file_path == tmp_path / "pool" / "Filmy" / f"{self.FOLDER}.mkv"
|
|
|
|
def test_add_part_refuses_a_movie_that_already_has_versions(
|
|
self, file_manager, tmp_path
|
|
):
|
|
movie = self._flat(file_manager, tmp_path)
|
|
(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"part")
|
|
|
|
with pytest.raises(ValueError):
|
|
file_manager.add_movie_part(movie, tmp_path / "c.mkv")
|
|
|
|
def test_part_label_through_add_movie_version_names_both_files(
|
|
self, file_manager, tmp_path
|
|
):
|
|
"""Typing "part2" as a popisek (e.g. in the import collision dialog)
|
|
is the same as "Přidat díl": the existing file becomes part1."""
|
|
movie = self._flat(file_manager, tmp_path)
|
|
(tmp_path / "b.mkv").write_bytes(b"part-two")
|
|
|
|
p2 = file_manager.import_movie(
|
|
tmp_path / "b.mkv",
|
|
"Film",
|
|
year=2010,
|
|
tmdb_id=12345,
|
|
on_conflict="version",
|
|
version_label="CD2",
|
|
)
|
|
|
|
assert movie.file_path.name == f"{self.FOLDER}-part1.mkv"
|
|
assert p2.file_path.name == f"{self.FOLDER}-part2.mkv"
|
|
|
|
def test_rename_part_label_keeps_the_part_form(self, file_manager, tmp_path):
|
|
movie = self._flat(file_manager, tmp_path)
|
|
(tmp_path / "b.mkv").write_bytes(b"part-two")
|
|
p2 = file_manager.add_movie_part(movie, tmp_path / "b.mkv")
|
|
|
|
file_manager.rename_movie_version(p2, "pt3")
|
|
|
|
assert p2.file_path.name == f"{self.FOLDER}-part3.mkv"
|
|
|
|
|
|
class TestPartsAndVersionsDontMix:
|
|
"""A folder holds either parts (played back to back) or versions
|
|
(alternatives) — never both, or Jellyfin and the UI can't tell which file
|
|
is which (the "Císařův pekař" case: part1, part2 and a version "díl 1")."""
|
|
|
|
@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 _flat(self, fm, tmp_path):
|
|
(tmp_path / "a.mkv").write_bytes(b"one")
|
|
return fm.import_movie(tmp_path / "a.mkv", "Film", year=2010, tmdb_id=12345)
|
|
|
|
def test_part_label_on_a_versioned_movie_is_refused(self, file_manager, tmp_path):
|
|
movie = self._flat(file_manager, tmp_path)
|
|
(tmp_path / "b.mkv").write_bytes(b"3d")
|
|
file_manager.add_movie_version(movie, tmp_path / "b.mkv", "díl 1")
|
|
(tmp_path / "c.mkv").write_bytes(b"part")
|
|
|
|
with pytest.raises(ValueError):
|
|
file_manager.add_movie_version(movie, tmp_path / "c.mkv", "part2")
|
|
assert not (
|
|
tmp_path
|
|
/ "pool"
|
|
/ "Filmy"
|
|
/ "Film (2010) [tmdbid-12345]"
|
|
/ "Film (2010) [tmdbid-12345]-part2.mkv"
|
|
).exists()
|
|
|
|
def test_version_label_on_a_movie_with_parts_is_refused(
|
|
self, file_manager, tmp_path
|
|
):
|
|
movie = self._flat(file_manager, tmp_path)
|
|
(tmp_path / "b.mkv").write_bytes(b"two")
|
|
file_manager.add_movie_part(movie, tmp_path / "b.mkv")
|
|
(tmp_path / "c.mkv").write_bytes(b"4k")
|
|
|
|
with pytest.raises(ValueError):
|
|
file_manager.add_movie_version(movie, tmp_path / "c.mkv", "4K")
|
|
|
|
def test_renaming_a_part_to_a_version_label_is_refused(
|
|
self, file_manager, tmp_path
|
|
):
|
|
movie = self._flat(file_manager, tmp_path)
|
|
(tmp_path / "b.mkv").write_bytes(b"two")
|
|
p2 = file_manager.add_movie_part(movie, tmp_path / "b.mkv")
|
|
|
|
with pytest.raises(ValueError):
|
|
file_manager.rename_movie_version(p2, "díl 2")
|
|
assert p2.file_path.name.endswith("-part2.mkv")
|
|
|
|
def test_renaming_within_parts_is_allowed(self, file_manager, tmp_path):
|
|
movie = self._flat(file_manager, tmp_path)
|
|
(tmp_path / "b.mkv").write_bytes(b"two")
|
|
p2 = file_manager.add_movie_part(movie, tmp_path / "b.mkv")
|
|
|
|
file_manager.rename_movie_version(p2, "CD3")
|
|
|
|
assert p2.file_path.name.endswith("-part3.mkv")
|
|
|
|
|
|
class TestMovieGroupModes:
|
|
"""A movie is flat (one file) or a folder; a folder holds either versions
|
|
(alternatives, each with a popisek) or parts (ordered, played back to
|
|
back). Switching the mode and reordering parts relabel the whole 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
|
|
|
|
F = "Film (2010) [tmdbid-12345]"
|
|
|
|
def _folder(self, tmp_path):
|
|
return tmp_path / "pool" / "Filmy" / self.F
|
|
|
|
def _parts(self, fm, tmp_path, n=3):
|
|
(tmp_path / "a.mkv").write_bytes(b"p1")
|
|
movie = fm.import_movie(tmp_path / "a.mkv", "Film", year=2010, tmdb_id=12345)
|
|
for i in range(2, n + 1):
|
|
(tmp_path / f"p{i}.mkv").write_bytes(f"p{i}".encode())
|
|
fm.add_movie_part(movie, tmp_path / f"p{i}.mkv")
|
|
return movie
|
|
|
|
def _content(self, tmp_path, name):
|
|
return (self._folder(tmp_path) / name).read_bytes()
|
|
|
|
def test_mode_of_flat_versions_and_parts(self, file_manager, tmp_path):
|
|
(tmp_path / "a.mkv").write_bytes(b"x")
|
|
movie = file_manager.import_movie(
|
|
tmp_path / "a.mkv", "Film", year=2010, tmdb_id=12345
|
|
)
|
|
assert file_manager.movie_group_mode(movie) == "flat"
|
|
(tmp_path / "b.mkv").write_bytes(b"3d")
|
|
file_manager.add_movie_version(movie, tmp_path / "b.mkv", "3D HSBS")
|
|
assert file_manager.movie_group_mode(movie) == "versions"
|
|
|
|
def test_mode_of_parts(self, file_manager, tmp_path):
|
|
movie = self._parts(file_manager, tmp_path, n=2)
|
|
assert file_manager.movie_group_mode(movie) == "parts"
|
|
|
|
def test_ordered_media_sorts_parts_numerically(self, file_manager, tmp_path):
|
|
movie = self._parts(file_manager, tmp_path, n=11)
|
|
names = [f.file_path.name for f in file_manager.ordered_media(movie)]
|
|
assert names[1] == f"{self.F}-part2.mkv" # not part10
|
|
assert names[-1] == f"{self.F}-part11.mkv"
|
|
|
|
def test_move_part_down_swaps_the_numbers(self, file_manager, tmp_path):
|
|
movie = self._parts(file_manager, tmp_path, n=3)
|
|
first = file_manager.ordered_media(movie)[0] # content b"p1"
|
|
|
|
file_manager.move_movie_part(first, +1)
|
|
|
|
assert self._content(tmp_path, f"{self.F}-part1.mkv") == b"p2"
|
|
assert self._content(tmp_path, f"{self.F}-part2.mkv") == b"p1"
|
|
assert self._content(tmp_path, f"{self.F}-part3.mkv") == b"p3"
|
|
assert first.file_path.name == f"{self.F}-part2.mkv"
|
|
|
|
def test_move_part_carries_its_subtitles(self, file_manager, tmp_path):
|
|
movie = self._parts(file_manager, tmp_path, n=2)
|
|
first = file_manager.ordered_media(movie)[0]
|
|
srt = tmp_path / "s.srt"
|
|
srt.write_bytes(b"sub-of-p1")
|
|
file_manager.attach_subtitle(first, srt, language="cs")
|
|
|
|
file_manager.move_movie_part(first, +1)
|
|
|
|
assert self._content(tmp_path, f"{self.F}-part2.cs.srt") == b"sub-of-p1"
|
|
assert not (self._folder(tmp_path) / f"{self.F}-part1.cs.srt").exists()
|
|
|
|
def test_move_part_beyond_the_ends_is_a_noop(self, file_manager, tmp_path):
|
|
movie = self._parts(file_manager, tmp_path, n=2)
|
|
first = file_manager.ordered_media(movie)[0]
|
|
file_manager.move_movie_part(first, -1)
|
|
assert self._content(tmp_path, f"{self.F}-part1.mkv") == b"p1"
|
|
|
|
def test_switch_versions_to_parts_numbers_them_in_order(
|
|
self, file_manager, tmp_path
|
|
):
|
|
(tmp_path / "a.mkv").write_bytes(b"plain")
|
|
movie = file_manager.import_movie(
|
|
tmp_path / "a.mkv", "Film", year=2010, tmdb_id=12345
|
|
)
|
|
(tmp_path / "b.mkv").write_bytes(b"cut")
|
|
file_manager.add_movie_version(movie, tmp_path / "b.mkv", "zkrácená verze")
|
|
|
|
file_manager.set_movie_group_mode(movie, "parts")
|
|
|
|
assert file_manager.movie_group_mode(movie) == "parts"
|
|
assert (
|
|
self._content(tmp_path, f"{self.F}-part1.mkv") == b"plain"
|
|
) # default first
|
|
assert self._content(tmp_path, f"{self.F}-part2.mkv") == b"cut"
|
|
|
|
def test_switch_parts_to_versions_uses_given_labels(self, file_manager, tmp_path):
|
|
movie = self._parts(file_manager, tmp_path, n=3)
|
|
|
|
file_manager.set_movie_group_mode(
|
|
movie, "versions", labels=["kinoverze", "3D HSBS"]
|
|
)
|
|
|
|
assert file_manager.movie_group_mode(movie) == "versions"
|
|
assert self._content(tmp_path, f"{self.F}.mkv") == b"p1" # default
|
|
assert self._content(tmp_path, f"{self.F} - kinoverze.mkv") == b"p2"
|
|
assert self._content(tmp_path, f"{self.F} - 3D HSBS.mkv") == b"p3"
|
|
|
|
def test_switch_parts_to_versions_needs_a_label_per_extra_file(
|
|
self, file_manager, tmp_path
|
|
):
|
|
movie = self._parts(file_manager, tmp_path, n=3)
|
|
with pytest.raises(ValueError):
|
|
file_manager.set_movie_group_mode(movie, "versions", labels=["jen jeden"])
|
|
assert file_manager.movie_group_mode(movie) == "parts" # untouched
|
|
|
|
def test_switch_parts_to_versions_refuses_duplicate_labels(
|
|
self, file_manager, tmp_path
|
|
):
|
|
movie = self._parts(file_manager, tmp_path, n=3)
|
|
with pytest.raises(ValueError):
|
|
file_manager.set_movie_group_mode(movie, "versions", labels=["x", "X"])
|
|
assert file_manager.movie_group_mode(movie) == "parts"
|
|
|
|
def test_switch_to_parts_refuses_part_labels_as_versions(
|
|
self, file_manager, tmp_path
|
|
):
|
|
movie = self._parts(file_manager, tmp_path, n=2)
|
|
with pytest.raises(ValueError):
|
|
file_manager.set_movie_group_mode(movie, "versions", labels=["part5"])
|
|
|
|
def test_metadata_survives_a_mode_switch(self, file_manager, tmp_path):
|
|
movie = self._parts(file_manager, tmp_path, n=2)
|
|
movie.add_tag("Žánr/Komedie")
|
|
movie.save_metadata()
|
|
|
|
file_manager.set_movie_group_mode(movie, "versions", labels=["kinoverze"])
|
|
file_manager.load_pool_movies()
|
|
|
|
assert all(
|
|
{t.full_path for t in f.tags} == {"Žánr/Komedie"}
|
|
for f in file_manager.filelist
|
|
)
|
|
assert len(file_manager.index.get_movie("tmdb-12345")["media"]) == 2
|
|
|
|
|
|
class TestStaleSiblingDoesNotClobberMetadata:
|
|
"""A version object loaded before the shared metadata changed must not
|
|
write its stale copy back when it's renamed or demoted."""
|
|
|
|
@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 _pair(self, fm, tmp_path):
|
|
(tmp_path / "a.mkv").write_bytes(b"a")
|
|
movie = fm.import_movie(tmp_path / "a.mkv", "Film", year=2010, tmdb_id=12345)
|
|
(tmp_path / "b.mkv").write_bytes(b"b")
|
|
v2 = fm.add_movie_version(movie, tmp_path / "b.mkv", "kinoverze")
|
|
movie.add_tag("Žánr/Komedie") # v2 still holds the old, tag-less copy
|
|
movie.save_metadata()
|
|
return movie, v2
|
|
|
|
def _tags(self, fm):
|
|
return set(fm.index.get_movie("tmdb-12345")["tags"])
|
|
|
|
def test_renaming_a_stale_version_keeps_the_tags(self, file_manager, tmp_path):
|
|
_movie, v2 = self._pair(file_manager, tmp_path)
|
|
file_manager.rename_movie_version(v2, "zkrácená verze")
|
|
assert self._tags(file_manager) == {"Žánr/Komedie"}
|
|
|
|
def test_demoting_to_a_stale_survivor_keeps_the_tags(self, file_manager, tmp_path):
|
|
movie, _v2 = self._pair(file_manager, tmp_path)
|
|
file_manager.remove_movie_version(movie) # v2 (stale) survives, demoted
|
|
assert self._tags(file_manager) == {"Žánr/Komedie"}
|