Add statistics and data-consistency menus, recently-added folder
This commit is contained in:
@@ -274,32 +274,20 @@ class HardlinkManager:
|
||||
|
||||
return success_count, fail_count
|
||||
|
||||
def generate_random_tips(
|
||||
self,
|
||||
files: List[File],
|
||||
count: int = 10,
|
||||
subfolder: str = "Tipy dne",
|
||||
dry_run: bool = False,
|
||||
def _fill_special_folder(
|
||||
self, chosen: List[File], subfolder: str, dry_run: bool
|
||||
) -> Tuple[int, int]:
|
||||
"""(Re)fill ``subfolder`` with ``count`` random files as hardlinks.
|
||||
|
||||
The folder is emptied first so each run picks a fresh random selection.
|
||||
``files`` should be the pool movies only (copy-as-is mirrors excluded).
|
||||
|
||||
Returns:
|
||||
Tuple of (created_links, failed_links)
|
||||
"""
|
||||
"""Empty ``subfolder`` and hardlink ``chosen`` files into it."""
|
||||
base = self.output_dir / subfolder
|
||||
|
||||
if not dry_run and base.exists():
|
||||
for child in base.iterdir():
|
||||
if child.is_file():
|
||||
if child.is_file() or child.is_symlink():
|
||||
try:
|
||||
child.unlink()
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
chosen = random.sample(files, min(count, len(files)))
|
||||
created = 0
|
||||
fail = 0
|
||||
for file_obj in chosen:
|
||||
@@ -318,9 +306,41 @@ class HardlinkManager:
|
||||
except OSError as e:
|
||||
self.errors.append((file_obj.file_path, str(e)))
|
||||
fail += 1
|
||||
|
||||
return created, fail
|
||||
|
||||
def generate_recently_added(
|
||||
self,
|
||||
files: List[File],
|
||||
count: int = 10,
|
||||
subfolder: str = "- Nově přidané",
|
||||
dry_run: bool = False,
|
||||
) -> Tuple[int, int]:
|
||||
"""(Re)fill ``subfolder`` with the ``count`` most recently added movies.
|
||||
|
||||
Ordering is by ``File.added_timestamp()`` (import time, ctime fallback),
|
||||
newest first. Returns (created_links, failed_links).
|
||||
"""
|
||||
chosen = sorted(files, key=lambda f: f.added_timestamp(), reverse=True)[:count]
|
||||
return self._fill_special_folder(chosen, subfolder, dry_run)
|
||||
|
||||
def generate_random_tips(
|
||||
self,
|
||||
files: List[File],
|
||||
count: int = 10,
|
||||
subfolder: str = "Tipy dne",
|
||||
dry_run: bool = False,
|
||||
) -> Tuple[int, int]:
|
||||
"""(Re)fill ``subfolder`` with ``count`` random files as hardlinks.
|
||||
|
||||
The folder is emptied first so each run picks a fresh random selection.
|
||||
``files`` should be the pool movies only (copy-as-is mirrors excluded).
|
||||
|
||||
Returns:
|
||||
Tuple of (created_links, failed_links)
|
||||
"""
|
||||
chosen = random.sample(files, min(count, len(files)))
|
||||
return self._fill_special_folder(chosen, subfolder, dry_run)
|
||||
|
||||
def _is_same_file(self, path1: Path, path2: Path) -> bool:
|
||||
"""Check if two paths point to the same file (same inode)."""
|
||||
try:
|
||||
@@ -458,23 +478,20 @@ class HardlinkManager:
|
||||
except OSError:
|
||||
continue
|
||||
|
||||
# Build expected paths for each file based on current tags
|
||||
expected_paths: dict[int, set[Path]] = {}
|
||||
# Every link the tag tree *should* contain, given current files+tags.
|
||||
expected_all: set[Path] = set()
|
||||
for file_obj in files:
|
||||
try:
|
||||
inode = file_obj.file_path.stat().st_ino
|
||||
expected_paths[inode] = set()
|
||||
for tag in file_obj.tags:
|
||||
target_dir = self._target_dir(tag, roots, category_transforms)
|
||||
if target_dir is None:
|
||||
continue
|
||||
expected_all.add(target_dir / self._link_name(
|
||||
file_obj, tag, category_filename_templates))
|
||||
|
||||
for tag in file_obj.tags:
|
||||
target_dir = self._target_dir(tag, roots, category_transforms)
|
||||
if target_dir is None:
|
||||
continue
|
||||
expected_paths[inode].add(target_dir / self._link_name(
|
||||
file_obj, tag, category_filename_templates))
|
||||
except OSError:
|
||||
continue
|
||||
|
||||
# Scan only the tag-tree's own top-level folders (skip copy-as-is mirrors)
|
||||
# Scan only the tag-tree's own top-level folders (skip copy-as-is mirrors).
|
||||
# Inside them, any file that isn't an expected link is obsolete — whether
|
||||
# a tag was removed, the movie was replaced (orphan inode), or the folder
|
||||
# itself is a leftover from a renamed layout.
|
||||
top_dirs = self._managed_top_dirs(
|
||||
files, roots, category_transforms, reserved_subfolders)
|
||||
for top in self.output_dir.iterdir():
|
||||
@@ -483,18 +500,16 @@ class HardlinkManager:
|
||||
if top_dirs is not None and top.name not in top_dirs:
|
||||
continue
|
||||
|
||||
# Depth-agnostic: genres sit one level deep, "Dle roku"/"Dle země
|
||||
# původu" two levels deep — walk all files under the managed folder.
|
||||
for link_file in top.rglob("*"):
|
||||
if not link_file.is_file():
|
||||
continue
|
||||
try:
|
||||
link_inode = link_file.stat().st_ino
|
||||
if link_inode in expected_paths:
|
||||
if link_file not in expected_paths[link_inode]:
|
||||
obsolete.append((link_file, inode_to_file[link_inode].file_path))
|
||||
except OSError:
|
||||
if link_file in expected_all:
|
||||
continue
|
||||
try:
|
||||
source = inode_to_file[link_file.stat().st_ino].file_path
|
||||
except (OSError, KeyError):
|
||||
source = link_file # orphan (no current pool movie)
|
||||
obsolete.append((link_file, source))
|
||||
|
||||
return obsolete
|
||||
|
||||
|
||||
Reference in New Issue
Block a user