"""Parse command implementation.""" from __future__ import annotations from pathlib import Path from typing import Optional import click from vlm.cli_helpers import default_artifact_path, resolve_legacy_default_input_path, run_command from vlm.context import CLIContext, pass_context from vlm.io import load_inventory_csv, save_identities_json from vlm.models import ( IdentityRecord, MovieIdentityRecord, ParsedIdentitiesJSON, SeriesIdentityRecord, VideoFile, ) from vlm.parser import parse_anime, parse_movie, parse_series from vlm.utils import utc_now def parse_cmd(ctx: CLIContext, input: Path, output: Path, inventory: Optional[Path]) -> None: """Parse identities from scanned inventory.""" config = ctx.config logger = ctx.logger click.echo(f"Parsing identities from: {input}") path_to_metadata: dict[str, VideoFile] = {} if inventory: click.echo(f"Loading video metadata from: {inventory}") inventory_files = load_inventory_csv(inventory) path_to_metadata = {str(vf.path): vf for vf in inventory_files} click.echo(f"Loaded metadata for {len(path_to_metadata)} files") click.echo() inventory_files = load_inventory_csv(input) video_files: list[dict[str, str]] = [ { "path": str(vf.path), "filename": vf.filename, "category": vf.category, } for vf in inventory_files ] click.echo(f"Loaded {len(video_files)} files from inventory") click.echo() movie_identities: list[MovieIdentityRecord] = [] series_identities: list[SeriesIdentityRecord] = [] anime_identities: list[SeriesIdentityRecord] = [] other_files: list[IdentityRecord] = [] def get_video_metadata(file_path: str) -> dict: """Extract video metadata from inventory if available.""" if not path_to_metadata: return {} vf = path_to_metadata.get(file_path) if not vf: return {} return { "size_bytes": vf.size_bytes, "modified_timestamp": vf.modified_timestamp.isoformat(), "resolution": vf.resolution, "codec": vf.codec, "duration_seconds": vf.duration_seconds, "bitrate_kbps": vf.bitrate_kbps, } for vf in video_files: filename = vf["filename"] category = vf["category"] file_path = vf["path"] video_metadata = get_video_metadata(file_path) if category == "movie": identity = parse_movie(filename, extensions=config.video_extensions) record = { "path": file_path, "filename": filename, "category": category, "title": identity.title, "year": identity.year, "confidence": identity.confidence, "needs_review": identity.needs_review, } if video_metadata: record["video_metadata"] = video_metadata movie_identities.append(record) elif category == "series": identity = parse_series(filename, extensions=config.video_extensions) record = { "path": file_path, "filename": filename, "category": category, "title": identity.title, "season": identity.season, "episodes": identity.episodes, "confidence": identity.confidence, "needs_review": identity.needs_review, } if video_metadata: record["video_metadata"] = video_metadata series_identities.append(record) elif category == "anime": identity = parse_anime(filename, extensions=config.video_extensions) record = { "path": file_path, "filename": filename, "category": category, "title": identity.title, "season": identity.season, "episodes": identity.episodes, "confidence": identity.confidence, "needs_review": identity.needs_review, } if video_metadata: record["video_metadata"] = video_metadata anime_identities.append(record) else: other_files.append( { "path": vf["path"], "filename": filename, "category": category, "note": "Not categorized for parsing", } ) click.echo("Parsing complete!") click.echo() click.echo("Results by category:") click.echo(f" Movies: {len(movie_identities)}") movies_need_review = sum(1 for m in movie_identities if m["needs_review"]) if movies_need_review > 0: click.echo(f" - Need review: {movies_need_review}") click.echo(f" Series: {len(series_identities)}") series_need_review = sum(1 for s in series_identities if s["needs_review"]) if series_need_review > 0: click.echo(f" - Need review: {series_need_review}") click.echo(f" Anime: {len(anime_identities)}") anime_need_review = sum(1 for a in anime_identities if a["needs_review"]) if anime_need_review > 0: click.echo(f" - Need review: {anime_need_review}") click.echo(f" Other: {len(other_files)} (not parsed)") click.echo() click.echo(f"Saving parsed identities to: {output}") generation_timestamp = utc_now().strftime("%Y-%m-%dT%H:%M:%S") schema_version = "2.0" if path_to_metadata else "1.0" identities_data: ParsedIdentitiesJSON = { "vlm_schema_version": schema_version, "metadata": { "generated": generation_timestamp, "source_inventory": str(input), "total_files": len(video_files), }, "movies": movie_identities, "series": series_identities, "anime": anime_identities, "other": other_files, } save_identities_json(identities_data, output) click.echo("Parsed identities saved successfully!") logger.info( "Parse completed: %s movies, %s series, %s anime, saved to %s", len(movie_identities), len(series_identities), len(anime_identities), output, ) @click.command() @click.option("--input", type=click.Path(path_type=Path), default=lambda: default_artifact_path("inventory.csv")) @click.option("--output", type=click.Path(path_type=Path), default=lambda: default_artifact_path("identities.json")) @click.option("--inventory", type=click.Path(exists=True, path_type=Path), default=None) @pass_context def parse(ctx: CLIContext, input: Path, output: Path, inventory: Optional[Path]): """Parse identities from filenames.""" def _run(): input_resolved = resolve_legacy_default_input_path(input, "input", "inventory.csv", "--input") parse_cmd(ctx, input_resolved, output, inventory) run_command(ctx, _run, stage="parse", json_errors=True)