163 lines
6.1 KiB
Python
163 lines
6.1 KiB
Python
"""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)
|