Anime files with SxxEyy format are now parsed and organized into anime-specific directories using configurable templates. Files with absolute episode numbering (no season info) are marked needs_review for manual handling. Also removes unused imports flagged by ruff. Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
199 lines
6.9 KiB
Python
199 lines
6.9 KiB
Python
"""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)
|