"""Command-line interface for opentranscode. Provides three flags: - ``--version`` — print the package version and exit (0). - ``--dry-run`` — probe the environment, run the av1an VSScript smoke test if av1an is available, print a report, and exit. Does NOT launch the GUI and does NOT encode anything. - ``--verify-only PATH`` — re-verify an existing output file's size, resolution, and duration via ffprobe, without re-encoding. With no flag, ``main()`` defers to ``ui_window.launch_gui()``. Heavy imports (``env_probe``, ``ffprobe_utils``, ``ui_window``) are deferred into the bodies of ``run_dry_run`` / ``run_verify_only`` / the no-flag branch so that ``--version`` does not pull in PySide6. Extracted from ``open-transcode.v3.py`` (QA item v4-03 — package split). This is a pure code-organization refactor; behavior is identical to v3. """ from __future__ import annotations import argparse import sys from pathlib import Path def build_parser() -> argparse.ArgumentParser: """Build the CLI argument parser.""" parser = argparse.ArgumentParser( prog="opentranscode", description="Open-source batch video transcoder (av1an + ffmpeg)", ) parser.add_argument( "--version", action="store_true", help="Print version and exit", ) parser.add_argument( "--dry-run", action="store_true", help="Probe environment, run smoke test, print report — but do " "NOT launch GUI or encode anything", ) parser.add_argument( "--verify-only", metavar="PATH", help="Re-verify an existing output file (size, resolution, " "duration checks) without re-encoding", ) # v5-01: --force pre-checks the "Force (skip validation)" checkbox in # the GUI. This is a convenience flag — the checkbox can also be toggled # manually in the UI. parser.add_argument( "--force", action="store_true", help="Pre-check the 'Force (skip validation)' checkbox in the GUI. " "Skips ffprobe pre-validation and attempts encode even for " "files ffprobe cannot read. WARNING: invalid files will waste " "the full per-file timeout before failing.", ) return parser def run_dry_run() -> int: """Run the dry-run: probe env + smoke test, print report, return exit code.""" # Deferred imports so --version never pulls in PySide6 or runs the # environment probe. from . import __version__ from .env_probe import _av1an_vsscript_smoke_test, probe_environment print(f"opentranscode {__version__} — dry-run environment probe") print("=" * 60) env = probe_environment() print(f"Distro: {env.distro.name} (family={env.distro.family}, " f"v{env.distro.version_id})") print(f"CPU: {env.cpu.model_name} — " f"{env.cpu.physical_cores} physical / {env.cpu.logical_threads} logical") print(f"av1an: {env.av1an_path or 'NOT FOUND'}" + (f" (v{env.av1an_version})" if env.av1an_version else "")) print(f"ffmpeg: {env.ffmpeg_path or 'NOT FOUND'}" + (f" (v{env.ffmpeg_version})" if env.ffmpeg_version else "")) print(f"ffprobe: {env.ffprobe_path or 'NOT FOUND'}") print(f"VapourSynth: {env.vs_version or 'NOT FOUND'}" + (f" ({env.vs_script_lib})" if env.vs_script_lib else "")) print("ffmpeg libs: " + ", ".join( f"{k}={'yes' if v else 'no'}" for k, v in sorted(env.ffmpeg_libs.items()) )) if env.errors: print("\nERRORS:") for e in env.errors: print(f" - {e}") if env.warnings: print("\nWARNINGS:") for w in env.warnings: print(f" - {w}") # Smoke test only if av1an + ffmpeg are both present. if env.av1an_path and env.ffmpeg_path: print("\n--- av1an VSScript smoke test ---") svt_name = (env.av1an_flags or {}).get("svt_name", "svt_av1") ok, detail = _av1an_vsscript_smoke_test( env.av1an_path, env.ffmpeg_path, env.av1an_flags, svt_name, ) print(f" result: {'OK' if ok else 'FAIL'}") print(f" detail: {detail}") if not ok: print("\nDry-run complete — smoke test FAILED.") return 1 else: print("\nSmoke test skipped (av1an or ffmpeg not found).") print("\nDry-run complete.") return 0 if not env.errors else 1 def run_verify_only(path: str) -> int: """Re-verify an existing output file via ffprobe (no re-encode).""" import os from .ffprobe_utils import ffprobe_duration, ffprobe_validate target = Path(path) if not target.is_file(): print(f"verify-only: file not found: {target}", file=sys.stderr) return 1 ffprobe_bin = os.environ.get("FFPROBE_BIN", "ffprobe") info = ffprobe_validate(target, ffprobe_bin) if info is None: print(f"verify-only: ffprobe could not read {target}", file=sys.stderr) return 1 size = target.stat().st_size duration = ffprobe_duration(target, ffprobe_bin) streams = info.get("streams", []) vstream = next((s for s in streams if s.get("codec_type") == "video"), {}) width = vstream.get("width", "?") height = vstream.get("height", "?") print(f"file: {target}") print(f"size: {size} bytes ({size / 1024 / 1024:.2f} MiB)") print(f"duration: {duration if duration is not None else '?'} s" if duration is not None else "duration: ?") print(f"resolution: {width}x{height}") print("\nverify-only: OK" if size > 0 else "\nverify-only: FAIL (empty file)") return 0 if size > 0 else 1 def main(argv: list[str] | None = None) -> int: """CLI entry point. Returns the process exit code.""" args = build_parser().parse_args(argv) if args.version: from . import __version__ print(f"opentranscode {__version__}") return 0 if args.dry_run: return run_dry_run() if args.verify_only: return run_verify_only(args.verify_only) # No flag (or --force) — launch GUI. --force pre-checks the Force # checkbox; the user can still toggle it in the UI. from .ui_window import launch_gui return launch_gui(force=args.force)