Update analysis and plan files to enhance duplicate handling and reporting

- Updated `analysis.json` with a new generation timestamp.
- Modified `plan.json` to include a new plan ID and created timestamp, and changed operation types from "no-op" to "quarantine" for specific files needing manual review.
- Enhanced the README.md to document the new `--analysis` option for generating execution plans, which now includes a human-readable summary and duplicate handling strategies.
- Introduced a new `duplicate_resolve.py` module to manage duplicate file resolution strategies.
- Improved the execution engine to support quarantine operations and added rollback functionality for quarantined files.

These changes improve the functionality of the Video Library Manager by providing better duplicate management and clearer reporting capabilities.
This commit is contained in:
windyboy
2026-02-10 18:07:38 +08:00
parent dcd87754cf
commit 79f5ddf1f5
14 changed files with 882 additions and 456 deletions
+51
View File
@@ -0,0 +1,51 @@
"""Duplicate group resolution: choose which file to keep when consuming analysis."""
from pathlib import Path
from typing import Union
from vlm.models import MovieIdentity, SeriesIdentity
def choose_keep_index(
items: list[tuple[Path, Union[MovieIdentity, SeriesIdentity]]],
strategy: str,
) -> int | None:
"""Choose the index of the item to keep in a duplicate group.
Strategies:
- by_reputation: Prefer items with reputation_score; then sort by score desc,
then reputation_votes desc, then first_seen (input order). Keep index 0 after sort.
- first_seen: Keep the first item (index 0).
- manual: Return None; caller should not generate quarantine ops, only record in metadata.
Args:
items: List of (path, identity) for the duplicate group.
strategy: One of "by_reputation", "first_seen", "manual".
Returns:
Index in [0, len(items)) to keep, or None for manual.
"""
if strategy == "manual":
return None
if strategy == "first_seen":
return 0
if strategy == "by_reputation":
return _by_reputation_index(items)
return 0
def _by_reputation_index(
items: list[tuple[Path, Union[MovieIdentity, SeriesIdentity]]],
) -> int:
"""Sort by: has reputation > no reputation; then score desc; then votes desc; then order. Return 0."""
def key(idx_reason: tuple[int, tuple[Path, Union[MovieIdentity, SeriesIdentity]]]) -> tuple:
idx, (path, identity) = idx_reason
has_rep = identity.reputation_score is not None
score = identity.reputation_score if identity.reputation_score is not None else -1.0
votes = identity.reputation_votes if identity.reputation_votes is not None else -1
# Prefer has reputation (True > False), then higher score, then higher votes, then lower index
return (not has_rep, -score, -votes, idx)
indexed = list(enumerate(items))
indexed.sort(key=key)
return indexed[0][0]