Add statistics and data-consistency menus, recently-added folder
This commit is contained in:
@@ -335,6 +335,18 @@ class TestApplyCsfdTags:
|
||||
movie_file.add_tag("Rok/1999")
|
||||
assert movie_file.name_context()["year"] == "1999"
|
||||
|
||||
def test_added_timestamp_uses_stored_added(self, movie_file):
|
||||
movie_file.added = "2026-06-16T12:00:00"
|
||||
from datetime import datetime
|
||||
assert movie_file.added_timestamp() == datetime.fromisoformat(
|
||||
"2026-06-16T12:00:00").timestamp()
|
||||
|
||||
def test_added_timestamp_falls_back_to_mtime(self, movie_file):
|
||||
movie_file.added = None
|
||||
import os
|
||||
expected = os.stat(movie_file.file_path).st_mtime
|
||||
assert movie_file.added_timestamp() == expected
|
||||
|
||||
def test_set_attribute_persists_and_in_context(self, movie_file):
|
||||
movie_file.set_attribute("collection_sort", "03")
|
||||
assert movie_file.attributes["collection_sort"] == "03"
|
||||
|
||||
@@ -592,6 +592,72 @@ class TestPoolManagement:
|
||||
assert movie.csfd_link == "https://csfd.cz/film/1"
|
||||
assert file_manager.index.get(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", "https://csfd/1")
|
||||
a.add_tag("Žánr/Akční")
|
||||
a.add_tag("Žánr/Sci-Fi")
|
||||
a.csfd_cache = {"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_csfd"] == 1 and s["without_csfd"] == 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.get(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"
|
||||
|
||||
@@ -143,6 +143,32 @@ class TestHardlinkManager:
|
||||
# The mirror (reserved folder) is left alone
|
||||
assert mirror_link.exists()
|
||||
|
||||
def test_sync_removes_orphan_link_from_replaced_movie(
|
||||
self, temp_source_dir, temp_output_dir, tag_manager
|
||||
):
|
||||
"""A stale link pointing at a no-longer-pooled inode is cleaned up."""
|
||||
f = File(temp_source_dir / "file1.txt", tag_manager)
|
||||
f.tags.clear()
|
||||
f.add_tag(Tag("žánr", "Drama")) # a root-level category enables the sweep
|
||||
f.add_tag(Tag("rok", "1968"))
|
||||
roots = {"žánr": "", "rok": "- Dle roku"}
|
||||
manager = HardlinkManager(temp_output_dir)
|
||||
manager.sync_structure([f], category_roots=roots)
|
||||
|
||||
# Simulate a leftover from an OLD layout: a different file (different
|
||||
# inode, not in the pool) hardlinked under an old unprefixed folder.
|
||||
orphan_src = temp_source_dir / "orphan.txt"
|
||||
orphan_src.write_text("old data")
|
||||
old_dir = temp_output_dir / "Dle roku" / "1968"
|
||||
old_dir.mkdir(parents=True)
|
||||
os.link(orphan_src, old_dir / "file1.txt")
|
||||
|
||||
manager.sync_structure([f], category_roots=roots)
|
||||
|
||||
# new prefixed folder kept, stale unprefixed one (orphan) removed
|
||||
assert (temp_output_dir / "- Dle roku" / "1968" / "file1.txt").exists()
|
||||
assert not (temp_output_dir / "Dle roku").exists()
|
||||
|
||||
def test_sync_removes_root_genre_folder_when_last_movie_drops_tag(
|
||||
self, temp_source_dir, temp_output_dir, tag_manager
|
||||
):
|
||||
@@ -255,6 +281,23 @@ class TestHardlinkManager:
|
||||
tips = temp_output_dir / "Tipy dne"
|
||||
assert len(list(tips.iterdir())) == 1 # not accumulated to 2
|
||||
|
||||
def test_generate_recently_added_orders_by_added(
|
||||
self, files_with_tags, temp_output_dir
|
||||
):
|
||||
"""The newest-added movies (by added_timestamp) fill the folder."""
|
||||
# explicit added timestamps: file2 newest, file1 middle, file3 oldest
|
||||
files_with_tags[0].added = "2026-01-02T00:00:00"
|
||||
files_with_tags[1].added = "2026-01-03T00:00:00"
|
||||
files_with_tags[2].added = "2026-01-01T00:00:00"
|
||||
manager = HardlinkManager(temp_output_dir)
|
||||
created, fail = manager.generate_recently_added(
|
||||
files_with_tags, count=2, subfolder="- Nově přidané")
|
||||
|
||||
recent = temp_output_dir / "- Nově přidané"
|
||||
assert created == 2 and fail == 0
|
||||
names = {p.name for p in recent.iterdir()}
|
||||
assert names == {"file2.txt", "file1.txt"} # newest two, file3 excluded
|
||||
|
||||
def test_dry_run(self, files_with_tags, temp_output_dir):
|
||||
"""Test dry run (bez skutečného vytváření)"""
|
||||
manager = HardlinkManager(temp_output_dir)
|
||||
|
||||
Reference in New Issue
Block a user