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:
+77
-9
@@ -600,8 +600,14 @@ def analyze(ctx: CLIContext, input: Path, output: Path, inventory: Optional[Path
|
||||
default=Path('plan.json'),
|
||||
help='Path to save execution plan (default: plan.json)'
|
||||
)
|
||||
@click.option(
|
||||
'--analysis',
|
||||
type=click.Path(path_type=Path),
|
||||
default=None,
|
||||
help='Path to analysis JSON (optional); when provided, duplicate groups are applied to the plan'
|
||||
)
|
||||
@pass_context
|
||||
def plan(ctx: CLIContext, input: Path, output: Path):
|
||||
def plan(ctx: CLIContext, input: Path, output: Path, analysis: Path | None):
|
||||
"""Generate execution plan.
|
||||
|
||||
Creates a structured, reviewable plan of all file operations to be performed.
|
||||
@@ -611,11 +617,12 @@ def plan(ctx: CLIContext, input: Path, output: Path):
|
||||
|
||||
vlm plan # Use default files
|
||||
vlm plan --input my_identities.json # Custom input
|
||||
vlm plan --analysis analysis.json # Use analysis for duplicate handling
|
||||
vlm plan --output my_plan.json # Custom output
|
||||
"""
|
||||
try:
|
||||
from vlm.commands.plan import plan_cmd
|
||||
plan_cmd(ctx, input, output)
|
||||
plan_cmd(ctx, input, output, analysis)
|
||||
except FileNotFoundError:
|
||||
click.echo(f"Error: Input file not found: {input}", err=True)
|
||||
ctx.logger.error(f"Input file not found: {input}")
|
||||
@@ -678,6 +685,17 @@ def execute(ctx: CLIContext, plan: Path, confirm: bool):
|
||||
click.echo(f"Execution plan loaded: {execution_plan.plan_id}")
|
||||
click.echo(f"Created at: {execution_plan.created_at}")
|
||||
click.echo(f"Total operations: {len(execution_plan.operations)}")
|
||||
if execution_plan.human_summary:
|
||||
click.echo()
|
||||
click.echo(execution_plan.human_summary)
|
||||
elif execution_plan.summary or execution_plan.summary_by_reason:
|
||||
s = execution_plan.summary or {}
|
||||
by_r = execution_plan.summary_by_reason or {}
|
||||
parts = [f"操作统计:共 {s.get('total', len(execution_plan.operations))} 条(move {s.get('move', 0)},rename {s.get('rename', 0)},quarantine {s.get('quarantine', 0)},no-op {s.get('no-op', 0)})"]
|
||||
if by_r:
|
||||
parts.append("原因分布:" + ";".join(f"{r}: {c}" for r, c in list(by_r.items())[:5]))
|
||||
click.echo()
|
||||
click.echo("\n".join(parts))
|
||||
click.echo()
|
||||
|
||||
# Display mode warning
|
||||
@@ -697,7 +715,7 @@ def execute(ctx: CLIContext, plan: Path, confirm: bool):
|
||||
click.echo()
|
||||
|
||||
# Create execution engine and execute plan
|
||||
engine = ExecutionEngine(logger=logger)
|
||||
engine = ExecutionEngine(logger=logger, config=config)
|
||||
results, summary, rollback_log = engine.execute_plan(
|
||||
execution_plan,
|
||||
mode=mode,
|
||||
@@ -1036,7 +1054,7 @@ def rollback(ctx: CLIContext, log: Optional[Path]):
|
||||
click.echo()
|
||||
|
||||
# Create execution engine
|
||||
engine = ExecutionEngine(logger=logger)
|
||||
engine = ExecutionEngine(logger=logger, config=config)
|
||||
|
||||
# Load rollback log
|
||||
rollback_log = engine.load_rollback_log(log)
|
||||
@@ -1115,6 +1133,20 @@ def rollback(ctx: CLIContext, log: Optional[Path]):
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def _fallback_plan_summary(execution_plan) -> str:
|
||||
"""Build a short plan summary from summary and summary_by_reason when human_summary is empty."""
|
||||
s = execution_plan.summary or {}
|
||||
by_r = execution_plan.summary_by_reason or {}
|
||||
total = s.get("total", len(execution_plan.operations))
|
||||
parts = [
|
||||
f"计划操作统计:共 {total} 条(move {s.get('move', 0)},rename {s.get('rename', 0)},"
|
||||
f"quarantine {s.get('quarantine', 0)},no-op {s.get('no-op', 0)})"
|
||||
]
|
||||
if by_r:
|
||||
parts.append("原因分布:" + ";".join(f"{r}: {c}" for r, c in list(by_r.items())[:8]))
|
||||
return "\n".join(parts)
|
||||
|
||||
|
||||
@main.group()
|
||||
@pass_context
|
||||
def report(ctx: CLIContext):
|
||||
@@ -1263,8 +1295,14 @@ def report_inventory(ctx: CLIContext, format: str, input: Path, output: Optional
|
||||
default=None,
|
||||
help='Output file (default: print to console)'
|
||||
)
|
||||
@click.option(
|
||||
'--plan',
|
||||
type=click.Path(path_type=Path),
|
||||
default=None,
|
||||
help='Optional plan JSON; when provided, report includes plan content summary'
|
||||
)
|
||||
@pass_context
|
||||
def report_completeness(ctx: CLIContext, format: str, input: Path, output: Optional[Path]):
|
||||
def report_completeness(ctx: CLIContext, format: str, input: Path, output: Optional[Path], plan: Optional[Path]):
|
||||
"""Generate completeness report.
|
||||
|
||||
Shows series with episode gaps detected through heuristic analysis.
|
||||
@@ -1273,14 +1311,24 @@ def report_completeness(ctx: CLIContext, format: str, input: Path, output: Optio
|
||||
|
||||
vlm report completeness # Text format to console
|
||||
vlm report completeness --format json # JSON format to console
|
||||
vlm report completeness --plan plan.json # Include plan content summary
|
||||
vlm report completeness --format text --output completeness.txt
|
||||
"""
|
||||
import json
|
||||
from vlm.reports import generate_completeness_report
|
||||
from vlm.models import SeasonCompleteness
|
||||
|
||||
from vlm.planner import load_plan
|
||||
|
||||
config = ctx.config
|
||||
logger = ctx.logger
|
||||
|
||||
plan_summary = None
|
||||
if plan:
|
||||
if not plan.exists():
|
||||
click.echo(f"Error: Plan file not found: {plan}", err=True)
|
||||
sys.exit(1)
|
||||
execution_plan = load_plan(plan)
|
||||
plan_summary = execution_plan.human_summary or _fallback_plan_summary(execution_plan)
|
||||
|
||||
try:
|
||||
# Load analysis from JSON
|
||||
@@ -1307,7 +1355,9 @@ def report_completeness(ctx: CLIContext, format: str, input: Path, output: Optio
|
||||
|
||||
# Generate report
|
||||
click.echo(f"Generating completeness report in {format} format...")
|
||||
report_content = generate_completeness_report(season_completeness, format, config.library_root)
|
||||
report_content = generate_completeness_report(
|
||||
season_completeness, format, config.library_root, plan_summary=plan_summary
|
||||
)
|
||||
|
||||
# Output report
|
||||
if output:
|
||||
@@ -1358,8 +1408,14 @@ def report_completeness(ctx: CLIContext, format: str, input: Path, output: Optio
|
||||
default=None,
|
||||
help='Output file (default: print to console)'
|
||||
)
|
||||
@click.option(
|
||||
'--plan',
|
||||
type=click.Path(path_type=Path),
|
||||
default=None,
|
||||
help='Optional plan JSON; when provided, report includes plan content summary'
|
||||
)
|
||||
@pass_context
|
||||
def report_duplicates(ctx: CLIContext, format: str, input: Path, output: Optional[Path]):
|
||||
def report_duplicates(ctx: CLIContext, format: str, input: Path, output: Optional[Path], plan: Optional[Path]):
|
||||
"""Generate duplicate report.
|
||||
|
||||
Shows duplicate files with quality comparison data to help decide which
|
||||
@@ -1369,15 +1425,25 @@ def report_duplicates(ctx: CLIContext, format: str, input: Path, output: Optiona
|
||||
|
||||
vlm report duplicates # Text format to console
|
||||
vlm report duplicates --format json # JSON format to console
|
||||
vlm report duplicates --plan plan.json # Include plan content summary
|
||||
vlm report duplicates --format text --output duplicates.txt
|
||||
"""
|
||||
import json
|
||||
from vlm.reports import generate_duplicate_report
|
||||
from vlm.models import DuplicateGroup, MovieIdentity, SeriesIdentity, VideoFile
|
||||
from vlm.planner import load_plan
|
||||
from datetime import datetime, timezone
|
||||
|
||||
config = ctx.config
|
||||
logger = ctx.logger
|
||||
|
||||
plan_summary = None
|
||||
if plan:
|
||||
if not plan.exists():
|
||||
click.echo(f"Error: Plan file not found: {plan}", err=True)
|
||||
sys.exit(1)
|
||||
execution_plan = load_plan(plan)
|
||||
plan_summary = execution_plan.human_summary or _fallback_plan_summary(execution_plan)
|
||||
|
||||
try:
|
||||
# Load analysis from JSON
|
||||
@@ -1439,7 +1505,9 @@ def report_duplicates(ctx: CLIContext, format: str, input: Path, output: Optiona
|
||||
|
||||
# Generate report
|
||||
click.echo(f"Generating duplicate report in {format} format...")
|
||||
report_content = generate_duplicate_report(duplicate_groups, format, config.library_root)
|
||||
report_content = generate_duplicate_report(
|
||||
duplicate_groups, format, config.library_root, plan_summary=plan_summary
|
||||
)
|
||||
|
||||
# Output report
|
||||
if output:
|
||||
|
||||
Reference in New Issue
Block a user