314 lines
9.9 KiB
Python
314 lines
9.9 KiB
Python
"""Report CLI command implementations."""
|
|||
|
|
|
||
|
|
from __future__ import annotations
|
||
|
|
|
||
|
|
import json
|
||
|
|
import sys
|
||
|
|
from datetime import datetime, timezone
|
||
|
|
from pathlib import Path
|
||
|
|
from typing import Optional
|
||
|
|
|
||
|
|
import click
|
||
|
|
|
||
|
|
from vlm.cli_helpers import command_error, resolve_legacy_default_input_path
|
||
|
|
from vlm.context import CLIContext
|
||
|
|
from vlm.io import load_analysis_json, load_inventory_csv
|
||
|
|
from vlm.models import DuplicateGroup, MovieIdentity, SeasonCompleteness, SeriesIdentity, VideoFile
|
||
|
|
from vlm.plan_render import preferred_plan_summary
|
||
|
|
from vlm.planner import load_plan
|
||
|
|
from vlm.reports import (
|
||
|
|
generate_completeness_report,
|
||
|
|
generate_duplicate_report,
|
||
|
|
generate_inventory_report,
|
||
|
|
generate_summary_report,
|
||
|
|
)
|
||
|
|
|
||
|
|
|
||
|
|
def report_inventory_cmd(
|
||
|
|
ctx: CLIContext,
|
||
|
|
format: str,
|
||
|
|
input: Path,
|
||
|
|
output: Optional[Path],
|
||
|
|
) -> None:
|
||
|
|
"""Generate inventory report."""
|
||
|
|
config = ctx.config
|
||
|
|
logger = ctx.logger
|
||
|
|
|
||
|
|
try:
|
||
|
|
input = resolve_legacy_default_input_path(input, "input", "inventory.csv", "--input")
|
||
|
|
click.echo(f"Loading inventory from: {input}")
|
||
|
|
|
||
|
|
video_files = load_inventory_csv(input)
|
||
|
|
|
||
|
|
click.echo(f"Loaded {len(video_files)} files")
|
||
|
|
click.echo()
|
||
|
|
|
||
|
|
click.echo(f"Generating inventory report in {format} format...")
|
||
|
|
|
||
|
|
report_format = "csv" if format == "text" else format
|
||
|
|
report_content = generate_inventory_report(video_files, report_format, config.library_root)
|
||
|
|
|
||
|
|
if output:
|
||
|
|
output.parent.mkdir(parents=True, exist_ok=True)
|
||
|
|
with open(output, "w", encoding="utf-8") as f:
|
||
|
|
f.write(report_content)
|
||
|
|
click.echo(f"Report saved to: {output}")
|
||
|
|
else:
|
||
|
|
click.echo()
|
||
|
|
click.echo(report_content)
|
||
|
|
|
||
|
|
logger.info("Generated inventory report in %s format with %s files", format, len(video_files))
|
||
|
|
|
||
|
|
except FileNotFoundError:
|
||
|
|
command_error(ctx, f"Error: Input file not found: {input}", f"Input file not found: {input}")
|
||
|
|
|
||
|
|
except Exception as e:
|
||
|
|
command_error(
|
||
|
|
ctx,
|
||
|
|
f"Error generating inventory report: {e}",
|
||
|
|
f"Inventory report generation failed: {e}",
|
||
|
|
exc_info=True,
|
||
|
|
)
|
||
|
|
|
||
|
|
|
||
|
|
def report_completeness_cmd(
|
||
|
|
ctx: CLIContext,
|
||
|
|
format: str,
|
||
|
|
input: Path,
|
||
|
|
output: Optional[Path],
|
||
|
|
plan: Optional[Path],
|
||
|
|
) -> None:
|
||
|
|
"""Generate completeness report."""
|
||
|
|
config = ctx.config
|
||
|
|
logger = ctx.logger
|
||
|
|
|
||
|
|
input = resolve_legacy_default_input_path(input, "input", "analysis.json", "--input")
|
||
|
|
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 = preferred_plan_summary(execution_plan)
|
||
|
|
|
||
|
|
try:
|
||
|
|
click.echo(f"Loading analysis from: {input}")
|
||
|
|
|
||
|
|
analysis_data = load_analysis_json(input)
|
||
|
|
|
||
|
|
completeness_list = analysis_data.get("completeness", [])
|
||
|
|
|
||
|
|
season_completeness = []
|
||
|
|
for c in completeness_list:
|
||
|
|
season_completeness.append(
|
||
|
|
SeasonCompleteness(
|
||
|
|
series_title=c["series_title"],
|
||
|
|
season=c["season"],
|
||
|
|
episodes_found=c["episodes_found"],
|
||
|
|
episodes_missing=c["episodes_missing"],
|
||
|
|
)
|
||
|
|
)
|
||
|
|
|
||
|
|
click.echo(f"Loaded {len(season_completeness)} series with gaps")
|
||
|
|
click.echo()
|
||
|
|
|
||
|
|
click.echo(f"Generating completeness report in {format} format...")
|
||
|
|
report_content = generate_completeness_report(
|
||
|
|
season_completeness, format, config.library_root, plan_summary=plan_summary
|
||
|
|
)
|
||
|
|
|
||
|
|
if output:
|
||
|
|
output.parent.mkdir(parents=True, exist_ok=True)
|
||
|
|
with open(output, "w", encoding="utf-8") as f:
|
||
|
|
f.write(report_content)
|
||
|
|
click.echo(f"Report saved to: {output}")
|
||
|
|
else:
|
||
|
|
click.echo()
|
||
|
|
click.echo(report_content)
|
||
|
|
|
||
|
|
logger.info(
|
||
|
|
"Generated completeness report in %s format with %s series",
|
||
|
|
format,
|
||
|
|
len(season_completeness),
|
||
|
|
)
|
||
|
|
|
||
|
|
except FileNotFoundError:
|
||
|
|
command_error(ctx, f"Error: Input file not found: {input}", f"Input file not found: {input}")
|
||
|
|
|
||
|
|
except json.JSONDecodeError as e:
|
||
|
|
command_error(
|
||
|
|
ctx,
|
||
|
|
f"Error: Failed to parse JSON file: {e}",
|
||
|
|
f"JSON parsing failed: {e}",
|
||
|
|
exc_info=True,
|
||
|
|
)
|
||
|
|
|
||
|
|
except Exception as e:
|
||
|
|
command_error(
|
||
|
|
ctx,
|
||
|
|
f"Error generating completeness report: {e}",
|
||
|
|
f"Completeness report generation failed: {e}",
|
||
|
|
exc_info=True,
|
||
|
|
)
|
||
|
|
|
||
|
|
|
||
|
|
def report_duplicates_cmd(
|
||
|
|
ctx: CLIContext,
|
||
|
|
format: str,
|
||
|
|
input: Path,
|
||
|
|
output: Optional[Path],
|
||
|
|
plan: Optional[Path],
|
||
|
|
) -> None:
|
||
|
|
"""Generate duplicate report."""
|
||
|
|
config = ctx.config
|
||
|
|
logger = ctx.logger
|
||
|
|
|
||
|
|
input = resolve_legacy_default_input_path(input, "input", "analysis.json", "--input")
|
||
|
|
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 = preferred_plan_summary(execution_plan)
|
||
|
|
|
||
|
|
try:
|
||
|
|
click.echo(f"Loading analysis from: {input}")
|
||
|
|
|
||
|
|
analysis_data = load_analysis_json(input)
|
||
|
|
|
||
|
|
duplicates_list = analysis_data.get("duplicates", [])
|
||
|
|
|
||
|
|
duplicate_groups = []
|
||
|
|
for d in duplicates_list:
|
||
|
|
identity_data = d["identity"]
|
||
|
|
|
||
|
|
if identity_data["type"] == "movie":
|
||
|
|
identity = MovieIdentity(
|
||
|
|
title=identity_data["title"],
|
||
|
|
year=identity_data.get("year"),
|
||
|
|
confidence=1.0,
|
||
|
|
needs_review=False,
|
||
|
|
original_filename="",
|
||
|
|
)
|
||
|
|
else:
|
||
|
|
identity = SeriesIdentity(
|
||
|
|
title=identity_data["title"],
|
||
|
|
season=identity_data.get("season"),
|
||
|
|
episodes=identity_data.get("episodes", []),
|
||
|
|
confidence=1.0,
|
||
|
|
needs_review=False,
|
||
|
|
original_filename="",
|
||
|
|
)
|
||
|
|
|
||
|
|
quality_by_path = {
|
||
|
|
str(item.get("path", "")): item for item in d.get("quality_comparison", [])
|
||
|
|
}
|
||
|
|
|
||
|
|
files = []
|
||
|
|
for file_path in d["files"]:
|
||
|
|
quality = quality_by_path.get(str(file_path), {})
|
||
|
|
files.append(
|
||
|
|
VideoFile(
|
||
|
|
path=Path(file_path),
|
||
|
|
filename=Path(file_path).name,
|
||
|
|
size_bytes=int(quality.get("size_bytes", 0) or 0),
|
||
|
|
modified_timestamp=datetime.now(timezone.utc),
|
||
|
|
category="",
|
||
|
|
resolution=quality.get("resolution"),
|
||
|
|
codec=quality.get("codec"),
|
||
|
|
duration_seconds=quality.get("duration_seconds"),
|
||
|
|
bitrate_kbps=quality.get("bitrate_kbps"),
|
||
|
|
)
|
||
|
|
)
|
||
|
|
|
||
|
|
duplicate_groups.append(
|
||
|
|
DuplicateGroup(
|
||
|
|
identity=identity,
|
||
|
|
files=files,
|
||
|
|
quality_comparison=d["quality_comparison"],
|
||
|
|
)
|
||
|
|
)
|
||
|
|
|
||
|
|
click.echo(f"Loaded {len(duplicate_groups)} duplicate groups")
|
||
|
|
click.echo()
|
||
|
|
|
||
|
|
click.echo(f"Generating duplicate report in {format} format...")
|
||
|
|
report_content = generate_duplicate_report(
|
||
|
|
duplicate_groups, format, config.library_root, plan_summary=plan_summary
|
||
|
|
)
|
||
|
|
|
||
|
|
if output:
|
||
|
|
output.parent.mkdir(parents=True, exist_ok=True)
|
||
|
|
with open(output, "w", encoding="utf-8") as f:
|
||
|
|
f.write(report_content)
|
||
|
|
click.echo(f"Report saved to: {output}")
|
||
|
|
else:
|
||
|
|
click.echo()
|
||
|
|
click.echo(report_content)
|
||
|
|
|
||
|
|
logger.info(
|
||
|
|
"Generated duplicate report in %s format with %s groups",
|
||
|
|
format,
|
||
|
|
len(duplicate_groups),
|
||
|
|
)
|
||
|
|
|
||
|
|
except FileNotFoundError:
|
||
|
|
command_error(ctx, f"Error: Input file not found: {input}", f"Input file not found: {input}")
|
||
|
|
|
||
|
|
except json.JSONDecodeError as e:
|
||
|
|
command_error(
|
||
|
|
ctx,
|
||
|
|
f"Error: Failed to parse JSON file: {e}",
|
||
|
|
f"JSON parsing failed: {e}",
|
||
|
|
exc_info=True,
|
||
|
|
)
|
||
|
|
|
||
|
|
except Exception as e:
|
||
|
|
command_error(
|
||
|
|
ctx,
|
||
|
|
f"Error generating duplicate report: {e}",
|
||
|
|
f"Duplicate report generation failed: {e}",
|
||
|
|
exc_info=True,
|
||
|
|
)
|
||
|
|
|
||
|
|
|
||
|
|
def report_summary_cmd(ctx: CLIContext, input: Path, output: Optional[Path]) -> None:
|
||
|
|
"""Generate summary report."""
|
||
|
|
config = ctx.config
|
||
|
|
logger = ctx.logger
|
||
|
|
|
||
|
|
try:
|
||
|
|
input = resolve_legacy_default_input_path(input, "input", "inventory.csv", "--input")
|
||
|
|
click.echo(f"Loading inventory from: {input}")
|
||
|
|
|
||
|
|
video_files = load_inventory_csv(input)
|
||
|
|
|
||
|
|
click.echo(f"Loaded {len(video_files)} files")
|
||
|
|
click.echo()
|
||
|
|
|
||
|
|
click.echo("Generating summary report...")
|
||
|
|
report_content = generate_summary_report(video_files, config.library_root)
|
||
|
|
|
||
|
|
if output:
|
||
|
|
output.parent.mkdir(parents=True, exist_ok=True)
|
||
|
|
with open(output, "w", encoding="utf-8") as f:
|
||
|
|
f.write(report_content)
|
||
|
|
click.echo(f"Report saved to: {output}")
|
||
|
|
else:
|
||
|
|
click.echo()
|
||
|
|
click.echo(report_content)
|
||
|
|
|
||
|
|
logger.info("Generated summary report with %s files", len(video_files))
|
||
|
|
|
||
|
|
except FileNotFoundError:
|
||
|
|
command_error(ctx, f"Error: Input file not found: {input}", f"Input file not found: {input}")
|
||
|
|
|
||
|
|
except Exception as e:
|
||
|
|
command_error(
|
||
|
|
ctx,
|
||
|
|
f"Error generating summary report: {e}",
|
||
|
|
f"Summary report generation failed: {e}",
|
||
|
|
exc_info=True,
|
||
|
|
)
|