Add movie-keyed pool index with multi-version movies, ČSFD title bridge, subtitles and miniDLNA library scoping
This commit is contained in:
+645
-9
@@ -208,7 +208,7 @@ class TestPoolManagement:
|
||||
assert movie.file_path == tmp_path / "pool" / "Filmy" / "Matrix.mkv"
|
||||
assert source.exists() # non-destructive copy
|
||||
assert movie.title == "Matrix"
|
||||
assert file_manager.index.get(movie.file_path) is not None
|
||||
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")
|
||||
@@ -274,7 +274,7 @@ class TestPoolManagement:
|
||||
# parseable ISO timestamp, persisted in the index
|
||||
from datetime import datetime
|
||||
datetime.fromisoformat(movie.added)
|
||||
assert file_manager.index.get(movie.file_path)["added"] == 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")
|
||||
@@ -325,7 +325,7 @@ class TestPoolManagement:
|
||||
assert second.file_path.read_bytes() == b"bb" # new content in place
|
||||
assert [f.file_path.name for f in file_manager.filelist] == ["Matrix.mkv"]
|
||||
# index record reflects the new import (fresh metadata, old tags dropped)
|
||||
record = file_manager.index.get(second.file_path)
|
||||
record = file_manager.index.record_for(second.file_path)
|
||||
assert record is not None
|
||||
assert record["tags"] == []
|
||||
assert second.tags == []
|
||||
@@ -367,8 +367,8 @@ class TestPoolManagement:
|
||||
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.get(new_path) is not None
|
||||
assert file_manager.index.get(old_path) is None
|
||||
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")
|
||||
@@ -427,6 +427,15 @@ class TestPoolManagement:
|
||||
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 == [
|
||||
@@ -529,15 +538,18 @@ class TestPoolManagement:
|
||||
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_key = movie.file_path.relative_to(tmp_path / "pool").as_posix()
|
||||
old_path = movie.file_path
|
||||
|
||||
file_manager.move_to_library(movie, "Kids")
|
||||
|
||||
assert file_manager.library_of(movie) == "Kids"
|
||||
assert movie.file_path.exists()
|
||||
assert file_manager.index.records.get(old_key) is None # old key dropped
|
||||
new_key = movie.file_path.relative_to(tmp_path / "pool").as_posix()
|
||||
assert file_manager.index.records.get(new_key) is not None
|
||||
# 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")
|
||||
@@ -613,6 +625,42 @@ class TestPoolManagement:
|
||||
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")
|
||||
@@ -634,6 +682,19 @@ class TestPoolManagement:
|
||||
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")
|
||||
@@ -734,3 +795,578 @@ class TestPoolManagement:
|
||||
assert file_manager.copyasis_folders == ["Seriály", "Filmy jinak"]
|
||||
assert "copyasis_folders" not in file_manager.global_config
|
||||
assert file_manager.index.get_setting("copyasis_folders") == ["Seriály", "Filmy jinak"]
|
||||
|
||||
|
||||
class TestCsfdBridge:
|
||||
"""ČSFD → TMDb title bridge (resolve_via_csfd, migration wiring)."""
|
||||
|
||||
@pytest.fixture
|
||||
def temp_global_config(self, tmp_path, monkeypatch):
|
||||
config_path = tmp_path / "test_config.json"
|
||||
import src.core.config as config_module
|
||||
monkeypatch.setattr(config_module, "GLOBAL_CONFIG_FILE", config_path)
|
||||
return config_path
|
||||
|
||||
@pytest.fixture
|
||||
def file_manager(self, temp_global_config):
|
||||
return FileManager(TagManager())
|
||||
|
||||
def test_resolve_via_csfd_bridges_original_title_to_tmdb(
|
||||
self, file_manager, monkeypatch
|
||||
):
|
||||
from src.core import csfd, tmdb
|
||||
film = csfd.CsfdFilm(
|
||||
url="https://www.csfd.cz/film/2294-x/", csfd_id=2294,
|
||||
czech_title="Vykoupení z věznice Shawshank",
|
||||
original_title="The Shawshank Redemption", year="1994")
|
||||
monkeypatch.setattr(csfd, "fetch_film", lambda url, session=None: film)
|
||||
monkeypatch.setattr(tmdb, "api_available", lambda: True)
|
||||
|
||||
captured: dict = {}
|
||||
|
||||
def fake_search(query, year=None, limit=5):
|
||||
captured["query"], captured["year"] = query, year
|
||||
return [tmdb.TmdbResult(278, "Vykoupení z věznice Shawshank", "1994", "movie")]
|
||||
|
||||
monkeypatch.setattr(tmdb, "search_movie", fake_search)
|
||||
|
||||
result = file_manager.resolve_via_csfd(
|
||||
csfd_url="https://www.csfd.cz/film/2294-x/")
|
||||
|
||||
assert captured == {"query": "The Shawshank Redemption", "year": "1994"}
|
||||
assert result["tmdb"].tmdb_id == 278
|
||||
assert result["film"] is film
|
||||
|
||||
def test_resolve_via_csfd_swallows_scrape_failure(self, file_manager, monkeypatch):
|
||||
from src.core import csfd
|
||||
|
||||
def boom(url, session=None):
|
||||
raise csfd.CsfdError("anti-bot")
|
||||
|
||||
monkeypatch.setattr(csfd, "fetch_film", boom)
|
||||
result = file_manager.resolve_via_csfd(csfd_url="https://www.csfd.cz/film/1/")
|
||||
assert result == {"csfd_url": "https://www.csfd.cz/film/1/",
|
||||
"film": None, "tmdb": None}
|
||||
|
||||
def test_match_pool_uses_csfd_url_for_the_search(
|
||||
self, file_manager, tmp_path, monkeypatch
|
||||
):
|
||||
from src.core import csfd, tmdb
|
||||
file_manager.set_pool_dir(tmp_path / "pool")
|
||||
(tmp_path / "a.mkv").write_bytes(b"x")
|
||||
movie = file_manager.import_movie(
|
||||
tmp_path / "a.mkv", "Vykoupení z věznice Shawshank", year=1994,
|
||||
csfd_url="https://www.csfd.cz/film/2294-x/")
|
||||
|
||||
film = csfd.CsfdFilm(
|
||||
url=movie.csfd_url, czech_title="Vykoupení z věznice Shawshank",
|
||||
original_title="The Shawshank Redemption", year="1994")
|
||||
monkeypatch.setattr(csfd, "fetch_film", lambda url, session=None: film)
|
||||
monkeypatch.setattr(tmdb, "api_available", lambda: True)
|
||||
|
||||
seen: list = []
|
||||
|
||||
def fake_search(query, year=None, limit=5):
|
||||
seen.append((query, year))
|
||||
return [tmdb.TmdbResult(278, "The Shawshank Redemption", "1994", "movie")]
|
||||
|
||||
monkeypatch.setattr(tmdb, "search_movie", fake_search)
|
||||
|
||||
proposals = file_manager.match_pool_to_tmdb([movie])
|
||||
|
||||
assert seen == [("The Shawshank Redemption", "1994")]
|
||||
assert len(proposals) == 1
|
||||
assert proposals[0].status == "auto"
|
||||
assert proposals[0].chosen.tmdb_id == 278
|
||||
assert "ČSFD" in proposals[0].note
|
||||
|
||||
|
||||
class TestSubtitleAttachment:
|
||||
"""External subtitle sidecars for movies (attach / list / remove / rename-along)."""
|
||||
|
||||
@pytest.fixture
|
||||
def temp_global_config(self, tmp_path, monkeypatch):
|
||||
config_path = tmp_path / "test_config.json"
|
||||
import src.core.config as config_module
|
||||
monkeypatch.setattr(config_module, "GLOBAL_CONFIG_FILE", config_path)
|
||||
return config_path
|
||||
|
||||
@pytest.fixture
|
||||
def file_manager(self, temp_global_config, tmp_path):
|
||||
fm = FileManager(TagManager())
|
||||
fm.set_pool_dir(tmp_path / "pool")
|
||||
return fm
|
||||
|
||||
def test_attach_subtitle_copies_next_to_movie(self, file_manager, tmp_path):
|
||||
(tmp_path / "raw.mkv").write_bytes(b"x")
|
||||
movie = file_manager.import_movie(tmp_path / "raw.mkv", "Matrix", year=1999)
|
||||
srt = tmp_path / "downloaded.srt"
|
||||
srt.write_bytes(b"1\n00:00:01,000 --> 00:00:02,000\nAhoj\n")
|
||||
|
||||
target = file_manager.attach_subtitle(movie, srt, language="cs")
|
||||
|
||||
assert target == movie.file_path.with_name("Matrix (1999).cs.srt")
|
||||
assert target.exists()
|
||||
assert srt.exists() # copied, not moved
|
||||
assert [s.path for s in file_manager.list_subtitles(movie)] == [target]
|
||||
|
||||
def test_remove_subtitle_deletes_file(self, file_manager, tmp_path):
|
||||
(tmp_path / "raw.mkv").write_bytes(b"x")
|
||||
movie = file_manager.import_movie(tmp_path / "raw.mkv", "Matrix", year=1999)
|
||||
srt = tmp_path / "s.srt"
|
||||
srt.write_bytes(b"x")
|
||||
target = file_manager.attach_subtitle(movie, srt, language="cs")
|
||||
|
||||
file_manager.remove_subtitle(target)
|
||||
|
||||
assert not target.exists()
|
||||
assert file_manager.list_subtitles(movie) == []
|
||||
|
||||
def test_rename_movie_carries_subtitle_along(self, file_manager, tmp_path):
|
||||
(tmp_path / "raw.mkv").write_bytes(b"x")
|
||||
movie = file_manager.import_movie(tmp_path / "raw.mkv", "Matrix", year=1999)
|
||||
srt = tmp_path / "s.srt"
|
||||
srt.write_bytes(b"x")
|
||||
old_sub = file_manager.attach_subtitle(movie, srt, language="cs")
|
||||
|
||||
file_manager.rename_movie(movie, "Matrix Reloaded")
|
||||
|
||||
assert not old_sub.exists()
|
||||
new_sub = movie.file_path.with_name("Matrix Reloaded.cs.srt")
|
||||
assert new_sub.exists()
|
||||
|
||||
def test_apply_movie_edits_carries_subtitle_along(self, file_manager, tmp_path):
|
||||
(tmp_path / "raw.mkv").write_bytes(b"x")
|
||||
movie = file_manager.import_movie(tmp_path / "raw.mkv", "Matrix", year=1999)
|
||||
srt = tmp_path / "s.srt"
|
||||
srt.write_bytes(b"x")
|
||||
file_manager.attach_subtitle(movie, srt, language="cs", flag="forced")
|
||||
|
||||
file_manager.apply_movie_edits(movie, title="Matrix", year=1999, tmdb_id=603)
|
||||
|
||||
assert movie.file_path.name == "Matrix (1999) [tmdbid-603].mkv"
|
||||
assert movie.file_path.with_name(
|
||||
"Matrix (1999) [tmdbid-603].cs.forced.srt").exists()
|
||||
|
||||
def test_move_to_library_carries_subtitle_along(self, file_manager, tmp_path):
|
||||
file_manager.set_movie_libraries(["Filmy", "Kids"])
|
||||
(tmp_path / "raw.mkv").write_bytes(b"x")
|
||||
movie = file_manager.import_movie(tmp_path / "raw.mkv", "Matrix", year=1999)
|
||||
srt = tmp_path / "s.srt"
|
||||
srt.write_bytes(b"x")
|
||||
file_manager.attach_subtitle(movie, srt, language="cs")
|
||||
|
||||
file_manager.move_to_library(movie, "Kids")
|
||||
|
||||
assert movie.file_path == tmp_path / "pool" / "Kids" / "Matrix (1999).mkv"
|
||||
assert (tmp_path / "pool" / "Kids" / "Matrix (1999).cs.srt").exists()
|
||||
assert not (tmp_path / "pool" / "Filmy" / "Matrix (1999).cs.srt").exists()
|
||||
|
||||
|
||||
class TestMinidlnaScope:
|
||||
"""Which libraries feed miniDLNA generation (minidlna_libraries / scope)."""
|
||||
|
||||
@pytest.fixture
|
||||
def temp_global_config(self, tmp_path, monkeypatch):
|
||||
config_path = tmp_path / "test_config.json"
|
||||
import src.core.config as config_module
|
||||
monkeypatch.setattr(config_module, "GLOBAL_CONFIG_FILE", config_path)
|
||||
return config_path
|
||||
|
||||
@pytest.fixture
|
||||
def file_manager(self, temp_global_config, tmp_path):
|
||||
fm = FileManager(TagManager())
|
||||
fm.set_pool_dir(tmp_path / "pool")
|
||||
return fm
|
||||
|
||||
def test_default_is_none_meaning_all(self, file_manager):
|
||||
assert file_manager.minidlna_libraries is None
|
||||
|
||||
def test_default_scope_is_the_whole_pool(self, file_manager, tmp_path):
|
||||
file_manager.set_libraries([
|
||||
{"name": "Filmy", "kind": "movie"}, {"name": "Seriály", "kind": "series"}])
|
||||
(tmp_path / "a.mkv").write_bytes(b"x")
|
||||
movie = file_manager.import_movie(tmp_path / "a.mkv", "Matrix", year=1999)
|
||||
|
||||
movies, mirrors, mixed_series = file_manager.minidlna_scope()
|
||||
|
||||
assert movies == [movie]
|
||||
assert mirrors == ["Seriály"]
|
||||
assert mixed_series == []
|
||||
|
||||
def test_scope_filters_to_selected_libraries(self, file_manager, tmp_path):
|
||||
file_manager.set_libraries([
|
||||
{"name": "Filmy", "kind": "movie"}, {"name": "Kids", "kind": "movie"},
|
||||
{"name": "Seriály", "kind": "series"}, {"name": "Dokumenty", "kind": "series"}])
|
||||
(tmp_path / "a.mkv").write_bytes(b"x")
|
||||
(tmp_path / "b.mkv").write_bytes(b"x")
|
||||
in_scope = file_manager.import_movie(
|
||||
tmp_path / "a.mkv", "Matrix", year=1999, library="Filmy")
|
||||
file_manager.import_movie(tmp_path / "b.mkv", "Kids Movie", year=2005, library="Kids")
|
||||
|
||||
file_manager.set_minidlna_libraries(["Filmy", "Seriály"])
|
||||
movies, mirrors, mixed_series = file_manager.minidlna_scope()
|
||||
|
||||
assert movies == [in_scope]
|
||||
assert mirrors == ["Seriály"]
|
||||
assert mixed_series == []
|
||||
|
||||
def test_homevideo_libraries_are_mirrored_like_series(self, file_manager):
|
||||
file_manager.set_libraries([
|
||||
{"name": "Seriály", "kind": "series"}, {"name": "Dokumenty", "kind": "homevideo"}])
|
||||
|
||||
assert file_manager.mirror_as_is_libraries == ["Seriály", "Dokumenty"]
|
||||
_, mirrors, _ = file_manager.minidlna_scope()
|
||||
assert mirrors == ["Seriály", "Dokumenty"]
|
||||
|
||||
def test_homevideo_library_excluded_when_not_selected(self, file_manager):
|
||||
file_manager.set_libraries([
|
||||
{"name": "Seriály", "kind": "series"}, {"name": "Dokumenty", "kind": "homevideo"}])
|
||||
file_manager.set_minidlna_libraries(["Seriály"])
|
||||
|
||||
_, mirrors, _ = file_manager.minidlna_scope()
|
||||
assert mirrors == ["Seriály"]
|
||||
|
||||
def test_copyasis_folders_stays_series_only(self, file_manager):
|
||||
"""The library-organizer "copy-as-is" concept doesn't widen to homevideo."""
|
||||
file_manager.set_libraries([
|
||||
{"name": "Seriály", "kind": "series"}, {"name": "Dokumenty", "kind": "homevideo"}])
|
||||
assert file_manager.copyasis_folders == ["Seriály"]
|
||||
|
||||
def test_set_none_resets_to_all(self, file_manager):
|
||||
file_manager.set_minidlna_libraries(["Filmy"])
|
||||
file_manager.set_minidlna_libraries(None)
|
||||
assert file_manager.minidlna_libraries is None
|
||||
|
||||
def test_drops_unknown_library_names_on_read(self, file_manager):
|
||||
file_manager.set_libraries([{"name": "Filmy", "kind": "movie"}])
|
||||
file_manager.set_minidlna_libraries(["Filmy", "Zaniklá knihovna"])
|
||||
assert file_manager.minidlna_libraries == ["Filmy"]
|
||||
|
||||
def test_persists_across_reload(self, file_manager, tmp_path):
|
||||
file_manager.set_libraries([{"name": "Filmy", "kind": "movie"}])
|
||||
file_manager.set_minidlna_libraries(["Filmy"])
|
||||
|
||||
reloaded = FileManager(TagManager())
|
||||
reloaded.set_pool_dir(tmp_path / "pool")
|
||||
assert reloaded.minidlna_libraries == ["Filmy"]
|
||||
|
||||
def test_mixed_library_series_returned_separately_from_movies(
|
||||
self, file_manager, tmp_path
|
||||
):
|
||||
file_manager.set_libraries([{"name": "Hlavní", "kind": "mixed"}])
|
||||
(tmp_path / "movie.mkv").write_bytes(b"x")
|
||||
movie = file_manager.import_movie(
|
||||
tmp_path / "movie.mkv", "Matrix", year=1999, library="Hlavní")
|
||||
(tmp_path / "bb.S01E01.mkv").write_bytes(b"x")
|
||||
file_manager.import_episodes(
|
||||
[tmp_path / "bb.S01E01.mkv"], "Breaking Bad", year=2008, tmdb_id=1396,
|
||||
library="Hlavní")
|
||||
|
||||
movies, mirrors, mixed_series = file_manager.minidlna_scope()
|
||||
|
||||
assert movies == [movie]
|
||||
assert mirrors == []
|
||||
assert len(mixed_series) == 1
|
||||
assert mixed_series[0].title == "Breaking Bad"
|
||||
assert mixed_series[0].library == "Hlavní"
|
||||
|
||||
def test_mixed_library_series_excluded_when_library_not_selected(
|
||||
self, file_manager, tmp_path
|
||||
):
|
||||
file_manager.set_libraries([
|
||||
{"name": "Hlavní", "kind": "mixed"}, {"name": "Filmy", "kind": "movie"}])
|
||||
(tmp_path / "bb.S01E01.mkv").write_bytes(b"x")
|
||||
file_manager.import_episodes(
|
||||
[tmp_path / "bb.S01E01.mkv"], "Breaking Bad", year=2008, library="Hlavní")
|
||||
|
||||
file_manager.set_minidlna_libraries(["Filmy"])
|
||||
_, _, mixed_series = file_manager.minidlna_scope()
|
||||
|
||||
assert mixed_series == []
|
||||
|
||||
|
||||
class TestMinidlnaPruneTargets:
|
||||
"""What to remove from a previous generation's output once deselected."""
|
||||
|
||||
@pytest.fixture
|
||||
def temp_global_config(self, tmp_path, monkeypatch):
|
||||
config_path = tmp_path / "test_config.json"
|
||||
import src.core.config as config_module
|
||||
monkeypatch.setattr(config_module, "GLOBAL_CONFIG_FILE", config_path)
|
||||
return config_path
|
||||
|
||||
@pytest.fixture
|
||||
def file_manager(self, temp_global_config, tmp_path):
|
||||
fm = FileManager(TagManager())
|
||||
fm.set_pool_dir(tmp_path / "pool")
|
||||
return fm
|
||||
|
||||
def test_deselected_series_library_is_stale(self, file_manager):
|
||||
file_manager.set_libraries([
|
||||
{"name": "Anime", "kind": "series"}, {"name": "Dokumenty", "kind": "homevideo"}])
|
||||
# both deselected — neither is "Seriály" so both go whole-folder
|
||||
stale, valid = file_manager.minidlna_prune_targets(
|
||||
mirror_libs=[], mixed_series=[], mixed_series_bucket="Seriály")
|
||||
assert set(stale) == {"Anime", "Dokumenty"}
|
||||
assert valid == set()
|
||||
|
||||
def test_selected_library_is_not_stale(self, file_manager):
|
||||
file_manager.set_libraries([{"name": "Anime", "kind": "series"}])
|
||||
stale, _ = file_manager.minidlna_prune_targets(
|
||||
mirror_libs=["Anime"], mixed_series=[], mixed_series_bucket="Seriály")
|
||||
assert stale == []
|
||||
|
||||
def test_bucket_named_library_never_whole_folder_removed(self, file_manager):
|
||||
"""A real 'Seriály' library, even deselected, is reconciled per-child —
|
||||
never nuked via the whole-folder path (that would also erase valid
|
||||
mixed-library series sharing the same bucket)."""
|
||||
file_manager.set_libraries([{"name": "Seriály", "kind": "series"}])
|
||||
stale, valid = file_manager.minidlna_prune_targets(
|
||||
mirror_libs=[], mixed_series=[], mixed_series_bucket="Seriály")
|
||||
assert "Seriály" not in stale
|
||||
assert valid == set()
|
||||
|
||||
def test_bucket_named_library_selected_contributes_its_children(
|
||||
self, file_manager, tmp_path
|
||||
):
|
||||
file_manager.set_libraries([{"name": "Seriály", "kind": "series"}])
|
||||
(tmp_path / "pool" / "Seriály" / "Show A").mkdir(parents=True)
|
||||
(tmp_path / "pool" / "Seriály" / "Show B").mkdir(parents=True)
|
||||
|
||||
_, valid = file_manager.minidlna_prune_targets(
|
||||
mirror_libs=["Seriály"], mixed_series=[], mixed_series_bucket="Seriály")
|
||||
|
||||
assert valid == {"Show A", "Show B"}
|
||||
|
||||
def test_mixed_series_folder_names_are_valid(self, file_manager, tmp_path):
|
||||
file_manager.set_libraries([{"name": "Hlavní", "kind": "mixed"}])
|
||||
(tmp_path / "bb.S01E01.mkv").write_bytes(b"x")
|
||||
file_manager.import_episodes(
|
||||
[tmp_path / "bb.S01E01.mkv"], "Breaking Bad", year=2008,
|
||||
tmdb_id=1396, library="Hlavní")
|
||||
_, _, mixed_series = file_manager.minidlna_scope()
|
||||
|
||||
_, valid = file_manager.minidlna_prune_targets(
|
||||
mirror_libs=[], mixed_series=mixed_series, mixed_series_bucket="Seriály")
|
||||
|
||||
assert valid == {"Breaking Bad (2008) [tmdbid-1396]"}
|
||||
|
||||
def test_deselected_mixed_series_drops_out_of_valid(self, file_manager, tmp_path):
|
||||
"""Simulates re-running after the mixed library is unchecked: the
|
||||
previously-mirrored series' folder name is no longer valid."""
|
||||
file_manager.set_libraries([{"name": "Hlavní", "kind": "mixed"}])
|
||||
(tmp_path / "bb.S01E01.mkv").write_bytes(b"x")
|
||||
file_manager.import_episodes(
|
||||
[tmp_path / "bb.S01E01.mkv"], "Breaking Bad", year=2008,
|
||||
tmdb_id=1396, library="Hlavní")
|
||||
|
||||
file_manager.set_minidlna_libraries([]) # deselect everything
|
||||
_, _, mixed_series = file_manager.minidlna_scope()
|
||||
_, valid = file_manager.minidlna_prune_targets(
|
||||
mirror_libs=[], mixed_series=mixed_series, mixed_series_bucket="Seriály")
|
||||
|
||||
assert mixed_series == []
|
||||
assert valid == set()
|
||||
|
||||
|
||||
class TestMovieVersions:
|
||||
"""Multiple versions of one movie (2D+3D, 4K+1080p, …) — flat until a
|
||||
second file shows up, then grouped into a per-movie folder."""
|
||||
|
||||
@pytest.fixture
|
||||
def temp_global_config(self, tmp_path, monkeypatch):
|
||||
config_path = tmp_path / "test_config.json"
|
||||
import src.core.config as config_module
|
||||
monkeypatch.setattr(config_module, "GLOBAL_CONFIG_FILE", config_path)
|
||||
return config_path
|
||||
|
||||
@pytest.fixture
|
||||
def file_manager(self, temp_global_config, tmp_path):
|
||||
fm = FileManager(TagManager())
|
||||
fm.set_pool_dir(tmp_path / "pool")
|
||||
return fm
|
||||
|
||||
def test_flat_movie_is_not_a_group_member(self, file_manager, tmp_path):
|
||||
(tmp_path / "a.mkv").write_bytes(b"x")
|
||||
movie = file_manager.import_movie(tmp_path / "a.mkv", "Avatar", year=2009, tmdb_id=19995)
|
||||
assert file_manager.is_movie_group_member(movie) is False
|
||||
assert file_manager.list_movie_versions(movie) == [movie]
|
||||
assert file_manager.visible_movies() == [movie]
|
||||
|
||||
def test_add_version_promotes_flat_movie_to_a_folder(self, file_manager, tmp_path):
|
||||
(tmp_path / "a.mkv").write_bytes(b"2d")
|
||||
movie = file_manager.import_movie(tmp_path / "a.mkv", "Avatar", year=2009, tmdb_id=19995)
|
||||
(tmp_path / "b.mkv").write_bytes(b"3d")
|
||||
|
||||
v2 = file_manager.add_movie_version(movie, tmp_path / "b.mkv", "3D HSBS")
|
||||
|
||||
base = tmp_path / "pool" / "Filmy" / "Avatar (2009) [tmdbid-19995]"
|
||||
assert movie.file_path == base / "Avatar (2009) [tmdbid-19995] - Výchozí.mkv"
|
||||
assert v2.file_path == base / "Avatar (2009) [tmdbid-19995] - 3D HSBS.mkv"
|
||||
assert file_manager.is_movie_group_member(movie)
|
||||
assert file_manager.is_movie_group_member(v2)
|
||||
assert (tmp_path / "b.mkv").exists() # copied, not moved (move=False default)
|
||||
|
||||
def test_add_version_carries_subtitle_along_on_promotion(self, file_manager, tmp_path):
|
||||
(tmp_path / "a.mkv").write_bytes(b"2d")
|
||||
movie = file_manager.import_movie(tmp_path / "a.mkv", "Avatar", year=2009, tmdb_id=19995)
|
||||
srt = tmp_path / "s.srt"
|
||||
srt.write_bytes(b"x")
|
||||
file_manager.attach_subtitle(movie, srt, language="cs")
|
||||
(tmp_path / "b.mkv").write_bytes(b"3d")
|
||||
|
||||
file_manager.add_movie_version(movie, tmp_path / "b.mkv", "3D HSBS")
|
||||
|
||||
base = tmp_path / "pool" / "Filmy" / "Avatar (2009) [tmdbid-19995]"
|
||||
assert (base / "Avatar (2009) [tmdbid-19995] - Výchozí.cs.srt").exists()
|
||||
|
||||
def test_second_version_added_to_existing_group(self, file_manager, tmp_path):
|
||||
(tmp_path / "a.mkv").write_bytes(b"2d")
|
||||
movie = file_manager.import_movie(tmp_path / "a.mkv", "Avatar", year=2009, tmdb_id=19995)
|
||||
(tmp_path / "b.mkv").write_bytes(b"3d")
|
||||
file_manager.add_movie_version(movie, tmp_path / "b.mkv", "3D HSBS")
|
||||
(tmp_path / "c.mkv").write_bytes(b"4k")
|
||||
|
||||
v3 = file_manager.add_movie_version(movie, tmp_path / "c.mkv", "4K")
|
||||
|
||||
assert len(file_manager.list_movie_versions(movie)) == 3
|
||||
assert v3.file_path.name == "Avatar (2009) [tmdbid-19995] - 4K.mkv"
|
||||
|
||||
def test_add_version_rejects_empty_label(self, file_manager, tmp_path):
|
||||
(tmp_path / "a.mkv").write_bytes(b"x")
|
||||
movie = file_manager.import_movie(tmp_path / "a.mkv", "Avatar", year=2009, tmdb_id=19995)
|
||||
with pytest.raises(ValueError):
|
||||
file_manager.add_movie_version(movie, tmp_path / "a.mkv", " ")
|
||||
|
||||
def test_add_version_rejects_duplicate_label(self, file_manager, tmp_path):
|
||||
(tmp_path / "a.mkv").write_bytes(b"2d")
|
||||
movie = file_manager.import_movie(tmp_path / "a.mkv", "Avatar", year=2009, tmdb_id=19995)
|
||||
(tmp_path / "b.mkv").write_bytes(b"3d")
|
||||
file_manager.add_movie_version(movie, tmp_path / "b.mkv", "3D")
|
||||
(tmp_path / "c.mkv").write_bytes(b"3d-2")
|
||||
with pytest.raises(FileExistsError):
|
||||
file_manager.add_movie_version(movie, tmp_path / "c.mkv", "3D")
|
||||
|
||||
def test_visible_movies_shows_only_the_primary_version(self, file_manager, tmp_path):
|
||||
(tmp_path / "a.mkv").write_bytes(b"2d")
|
||||
movie = file_manager.import_movie(tmp_path / "a.mkv", "Avatar", year=2009, tmdb_id=19995)
|
||||
movie.add_tag("Žánr/Sci-Fi")
|
||||
movie.csfd_url = "https://www.csfd.cz/film/avatar/"
|
||||
movie.save_metadata()
|
||||
(tmp_path / "b.mkv").write_bytes(b"3d")
|
||||
file_manager.add_movie_version(movie, tmp_path / "b.mkv", "3D HSBS")
|
||||
|
||||
visible = file_manager.visible_movies()
|
||||
|
||||
assert len(visible) == 1
|
||||
assert visible[0].file_path == movie.file_path
|
||||
assert visible[0].title == "Avatar"
|
||||
assert visible[0].csfd_url == "https://www.csfd.cz/film/avatar/"
|
||||
assert {t.full_path for t in visible[0].tags} == {"Žánr/Sci-Fi"}
|
||||
|
||||
def test_remove_version_keeps_group_when_more_than_one_remains(self, file_manager, tmp_path):
|
||||
(tmp_path / "a.mkv").write_bytes(b"2d")
|
||||
movie = file_manager.import_movie(tmp_path / "a.mkv", "Avatar", year=2009, tmdb_id=19995)
|
||||
(tmp_path / "b.mkv").write_bytes(b"3d")
|
||||
v2 = file_manager.add_movie_version(movie, tmp_path / "b.mkv", "3D")
|
||||
(tmp_path / "c.mkv").write_bytes(b"4k")
|
||||
v3 = file_manager.add_movie_version(movie, tmp_path / "c.mkv", "4K")
|
||||
|
||||
file_manager.remove_movie_version(v3)
|
||||
|
||||
assert not v3.file_path.exists()
|
||||
assert file_manager.is_movie_group_member(movie)
|
||||
assert file_manager.is_movie_group_member(v2)
|
||||
assert len(file_manager.list_movie_versions(movie)) == 2
|
||||
|
||||
def test_remove_last_extra_version_demotes_to_flat(self, file_manager, tmp_path):
|
||||
(tmp_path / "a.mkv").write_bytes(b"2d")
|
||||
movie = file_manager.import_movie(tmp_path / "a.mkv", "Avatar", year=2009, tmdb_id=19995)
|
||||
(tmp_path / "b.mkv").write_bytes(b"3d")
|
||||
v2 = file_manager.add_movie_version(movie, tmp_path / "b.mkv", "3D")
|
||||
|
||||
file_manager.remove_movie_version(v2)
|
||||
|
||||
assert not file_manager.is_movie_group_member(movie)
|
||||
assert movie.file_path == tmp_path / "pool" / "Filmy" / "Avatar (2009) [tmdbid-19995].mkv"
|
||||
assert movie.file_path.exists()
|
||||
assert not (tmp_path / "pool" / "Filmy" / "Avatar (2009) [tmdbid-19995]").exists()
|
||||
|
||||
def test_removing_primary_migrates_metadata_to_survivor(self, file_manager, tmp_path):
|
||||
(tmp_path / "a.mkv").write_bytes(b"2d")
|
||||
movie = file_manager.import_movie(tmp_path / "a.mkv", "Avatar", year=2009, tmdb_id=19995)
|
||||
movie.add_tag("Žánr/Sci-Fi")
|
||||
movie.csfd_url = "https://www.csfd.cz/film/avatar/"
|
||||
movie.attributes = {"collection_sort": "01"}
|
||||
movie.save_metadata()
|
||||
(tmp_path / "b.mkv").write_bytes(b"3d")
|
||||
v2 = file_manager.add_movie_version(movie, tmp_path / "b.mkv", "3D")
|
||||
|
||||
file_manager.remove_movie_version(movie) # remove the primary
|
||||
|
||||
assert v2.title == "Avatar"
|
||||
assert v2.csfd_url == "https://www.csfd.cz/film/avatar/"
|
||||
assert v2.attributes == {"collection_sort": "01"}
|
||||
assert {t.full_path for t in v2.tags} == {"Žánr/Sci-Fi"}
|
||||
assert not file_manager.is_movie_group_member(v2) # demoted, only one left
|
||||
|
||||
def test_remove_version_rejects_ungrouped_movie(self, file_manager, tmp_path):
|
||||
(tmp_path / "a.mkv").write_bytes(b"x")
|
||||
movie = file_manager.import_movie(tmp_path / "a.mkv", "Avatar", year=2009, tmdb_id=19995)
|
||||
with pytest.raises(ValueError):
|
||||
file_manager.remove_movie_version(movie)
|
||||
|
||||
def test_rename_movie_refuses_grouped_movie(self, file_manager, tmp_path):
|
||||
(tmp_path / "a.mkv").write_bytes(b"2d")
|
||||
movie = file_manager.import_movie(tmp_path / "a.mkv", "Avatar", year=2009, tmdb_id=19995)
|
||||
(tmp_path / "b.mkv").write_bytes(b"3d")
|
||||
file_manager.add_movie_version(movie, tmp_path / "b.mkv", "3D")
|
||||
|
||||
with pytest.raises(ValueError):
|
||||
file_manager.rename_movie(movie, "Avatar Reloaded")
|
||||
|
||||
def test_move_to_library_refuses_grouped_movie(self, file_manager, tmp_path):
|
||||
file_manager.set_movie_libraries(["Filmy", "Kids"])
|
||||
(tmp_path / "a.mkv").write_bytes(b"2d")
|
||||
movie = file_manager.import_movie(tmp_path / "a.mkv", "Avatar", year=2009, tmdb_id=19995)
|
||||
(tmp_path / "b.mkv").write_bytes(b"3d")
|
||||
file_manager.add_movie_version(movie, tmp_path / "b.mkv", "3D")
|
||||
|
||||
with pytest.raises(ValueError):
|
||||
file_manager.move_to_library(movie, "Kids")
|
||||
|
||||
def test_apply_movie_edits_refuses_rename_of_grouped_movie(self, file_manager, tmp_path):
|
||||
(tmp_path / "a.mkv").write_bytes(b"2d")
|
||||
movie = file_manager.import_movie(tmp_path / "a.mkv", "Avatar", year=2009, tmdb_id=19995)
|
||||
(tmp_path / "b.mkv").write_bytes(b"3d")
|
||||
file_manager.add_movie_version(movie, tmp_path / "b.mkv", "3D")
|
||||
|
||||
with pytest.raises(ValueError):
|
||||
file_manager.apply_movie_edits(movie, title="Avatar Reloaded", year=2009, tmdb_id=19995)
|
||||
|
||||
def test_apply_movie_edits_allows_metadata_only_edit_of_grouped_movie(
|
||||
self, file_manager, tmp_path
|
||||
):
|
||||
"""Same title/year/tmdb_id → no path change → date/tags/etc still work."""
|
||||
(tmp_path / "a.mkv").write_bytes(b"2d")
|
||||
movie = file_manager.import_movie(tmp_path / "a.mkv", "Avatar", year=2009, tmdb_id=19995)
|
||||
(tmp_path / "b.mkv").write_bytes(b"3d")
|
||||
file_manager.add_movie_version(movie, tmp_path / "b.mkv", "3D")
|
||||
|
||||
file_manager.apply_movie_edits(
|
||||
movie, title="Avatar", year=2009, tmdb_id=19995, date="2026-01-01")
|
||||
|
||||
assert movie.date == "2026-01-01"
|
||||
assert file_manager.is_movie_group_member(movie)
|
||||
|
||||
def test_rename_to_canonical_skips_grouped_movie(self, file_manager, tmp_path):
|
||||
(tmp_path / "a.mkv").write_bytes(b"2d")
|
||||
movie = file_manager.import_movie(tmp_path / "a.mkv", "Avatar", year=2009, tmdb_id=19995)
|
||||
(tmp_path / "b.mkv").write_bytes(b"3d")
|
||||
file_manager.add_movie_version(movie, tmp_path / "b.mkv", "3D")
|
||||
|
||||
movie.title = "Avatar Retitled"
|
||||
assert file_manager.rename_to_canonical(movie) is None
|
||||
assert movie.title == "Avatar Retitled" # metadata field unaffected
|
||||
assert file_manager.is_movie_group_member(movie) # file untouched
|
||||
|
||||
Reference in New Issue
Block a user