feat: add preflight
This commit is contained in:
6
requirements.txt
Normal file
6
requirements.txt
Normal file
@@ -0,0 +1,6 @@
|
||||
# ripper — DVD/Blu-ray ripper
|
||||
# Required
|
||||
rich>=13.0
|
||||
|
||||
# Optional (for NFO generation with media metadata)
|
||||
pymediainfo>=6.0
|
||||
@@ -91,6 +91,11 @@ def main():
|
||||
action="store_true",
|
||||
help="Check disc readability and attempt recovery if stuck.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--check-deps",
|
||||
action="store_true",
|
||||
help="Verify all dependencies are installed and show install hints.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--init-config",
|
||||
action="store_true",
|
||||
@@ -101,6 +106,12 @@ def main():
|
||||
# Quality: CLI flag > config default
|
||||
quality = args.quality if args.quality is not None else QUALITY
|
||||
|
||||
# ── Dependency check ─────────────────────────────────────────────────
|
||||
if args.check_deps:
|
||||
from .preflight import check_all
|
||||
ok = check_all(verbose=True)
|
||||
sys.exit(0 if ok else 1)
|
||||
|
||||
# ── Generate config file ─────────────────────────────────────────────
|
||||
if args.init_config:
|
||||
init_config()
|
||||
@@ -123,6 +134,15 @@ def main():
|
||||
sys.exit(1)
|
||||
return
|
||||
|
||||
# ── Quick dependency check (HandBrakeCLI must be available) ──────────
|
||||
import shutil
|
||||
if not shutil.which("HandBrakeCLI"):
|
||||
console.print(
|
||||
"\n [bold red]✗ HandBrakeCLI not found on PATH[/]\n"
|
||||
" [dim]Run with --check-deps for install instructions.[/]\n"
|
||||
)
|
||||
sys.exit(1)
|
||||
|
||||
# ── Banner ───────────────────────────────────────────────────────────
|
||||
codec_display = CODEC_DISPLAY.get(args.codec, args.codec.upper())
|
||||
console.print()
|
||||
|
||||
231
ripper/preflight.py
Normal file
231
ripper/preflight.py
Normal file
@@ -0,0 +1,231 @@
|
||||
"""Pre-flight dependency and environment checks.
|
||||
|
||||
Verifies that all required and optional tools are available before
|
||||
starting a rip, with platform-specific install instructions.
|
||||
"""
|
||||
|
||||
import importlib
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
|
||||
def _check_python_version() -> tuple[bool, str]:
|
||||
"""Check Python >= 3.11 (needed for tomllib)."""
|
||||
v = sys.version_info
|
||||
ok = v >= (3, 11)
|
||||
detail = f"Python {v.major}.{v.minor}.{v.micro}"
|
||||
if not ok:
|
||||
detail += " — Python 3.11+ required (for tomllib)"
|
||||
return ok, detail
|
||||
|
||||
|
||||
def _check_package(name: str, pip_name: str | None = None) -> tuple[bool, str]:
|
||||
"""Check if a Python package is importable."""
|
||||
try:
|
||||
mod = importlib.import_module(name)
|
||||
version = getattr(mod, "__version__", "installed")
|
||||
return True, f"{name} ({version})"
|
||||
except ImportError:
|
||||
install = pip_name or name
|
||||
return False, f"{name} — install with: pip install {install}"
|
||||
|
||||
|
||||
def _check_binary(name: str, version_flag: str = "--version") -> tuple[bool, str]:
|
||||
"""Check if a binary is on PATH and get its version."""
|
||||
path = shutil.which(name)
|
||||
if not path:
|
||||
return False, f"{name} — not found on PATH"
|
||||
try:
|
||||
result = subprocess.run(
|
||||
[path, version_flag],
|
||||
capture_output=True, text=True, timeout=10,
|
||||
)
|
||||
# HandBrakeCLI outputs version on stderr
|
||||
output = (result.stdout + result.stderr).strip().splitlines()
|
||||
version_line = ""
|
||||
for line in output:
|
||||
if "version" in line.lower() or "handbrake" in line.lower():
|
||||
version_line = line.strip()
|
||||
break
|
||||
if not version_line and output:
|
||||
version_line = output[0].strip()
|
||||
return True, f"{name} — {version_line}" if version_line else f"{name} ({path})"
|
||||
except (subprocess.TimeoutExpired, OSError):
|
||||
return True, f"{name} ({path})"
|
||||
|
||||
|
||||
def _windows_install_hint(tool: str) -> str:
|
||||
"""Return a Windows-specific install hint."""
|
||||
hints = {
|
||||
"HandBrakeCLI": (
|
||||
" Download from https://handbrake.fr/downloads2.php\n"
|
||||
" Or install via: winget install HandBrake.HandBrakeCLI\n"
|
||||
" Or: choco install handbrake-cli"
|
||||
),
|
||||
"rich": " pip install rich",
|
||||
"pymediainfo": (
|
||||
" pip install pymediainfo\n"
|
||||
" Also requires MediaInfo DLL: https://mediaarea.net/en/MediaInfo/Download/Windows"
|
||||
),
|
||||
}
|
||||
return hints.get(tool, f" pip install {tool}")
|
||||
|
||||
|
||||
def _linux_install_hint(tool: str) -> str:
|
||||
"""Return a Linux-specific install hint."""
|
||||
hints = {
|
||||
"HandBrakeCLI": (
|
||||
" Ubuntu/Debian: sudo add-apt-repository ppa:stebbins/handbrake-releases && "
|
||||
"sudo apt install handbrake-cli\n"
|
||||
" Fedora: sudo dnf install HandBrakeCLI\n"
|
||||
" Arch: sudo pacman -S handbrake-cli\n"
|
||||
" Flatpak: flatpak install fr.handbrake.ghb"
|
||||
),
|
||||
"rich": " pip install rich",
|
||||
"pymediainfo": (
|
||||
" pip install pymediainfo\n"
|
||||
" Also install: sudo apt install libmediainfo0v5 (or equivalent)"
|
||||
),
|
||||
"eject": " Usually pre-installed. Ubuntu/Debian: sudo apt install eject",
|
||||
}
|
||||
return hints.get(tool, f" pip install {tool}")
|
||||
|
||||
|
||||
def _macos_install_hint(tool: str) -> str:
|
||||
"""Return a macOS-specific install hint."""
|
||||
hints = {
|
||||
"HandBrakeCLI": (
|
||||
" brew install --cask handbrake\n"
|
||||
" Or download from https://handbrake.fr/downloads2.php"
|
||||
),
|
||||
"rich": " pip install rich",
|
||||
"pymediainfo": (
|
||||
" pip install pymediainfo\n"
|
||||
" Also install: brew install media-info"
|
||||
),
|
||||
}
|
||||
return hints.get(tool, f" pip install {tool}")
|
||||
|
||||
|
||||
def _get_install_hint(tool: str) -> str:
|
||||
"""Return a platform-appropriate install hint."""
|
||||
if sys.platform == "win32":
|
||||
return _windows_install_hint(tool)
|
||||
elif sys.platform == "darwin":
|
||||
return _macos_install_hint(tool)
|
||||
return _linux_install_hint(tool)
|
||||
|
||||
|
||||
def check_all(verbose: bool = True) -> bool:
|
||||
"""
|
||||
Run all pre-flight checks and optionally print results.
|
||||
|
||||
Returns True if all required dependencies are met.
|
||||
"""
|
||||
# Lazy import to avoid circular deps — console may not be available
|
||||
# if rich itself is missing
|
||||
try:
|
||||
from rich.console import Console
|
||||
from rich.table import Table
|
||||
from rich import box
|
||||
console = Console(stderr=True)
|
||||
has_rich = True
|
||||
except ImportError:
|
||||
has_rich = False
|
||||
|
||||
checks: list[tuple[str, bool, str, bool]] = [] # (label, ok, detail, required)
|
||||
|
||||
# Python version
|
||||
ok, detail = _check_python_version()
|
||||
checks.append(("Python ≥ 3.11", ok, detail, True))
|
||||
|
||||
# Required Python packages
|
||||
ok, detail = _check_package("rich")
|
||||
checks.append(("rich", ok, detail, True))
|
||||
|
||||
# Optional Python packages
|
||||
ok, detail = _check_package("pymediainfo")
|
||||
checks.append(("pymediainfo", ok, detail, False))
|
||||
|
||||
# Required binaries
|
||||
ok, detail = _check_binary("HandBrakeCLI", "--version")
|
||||
checks.append(("HandBrakeCLI", ok, detail, True))
|
||||
|
||||
# Optional binaries (platform-specific)
|
||||
if sys.platform == "win32":
|
||||
# PowerShell is always available on Windows
|
||||
ok, detail = _check_binary("powershell", "-Command \"echo ok\"")
|
||||
checks.append(("PowerShell", ok, detail, False))
|
||||
elif sys.platform == "darwin":
|
||||
ok, detail = _check_binary("drutil", "status")
|
||||
checks.append(("drutil", ok, detail, False))
|
||||
else:
|
||||
ok, detail = _check_binary("eject", "--version")
|
||||
checks.append(("eject", ok, detail, False))
|
||||
|
||||
# Print results
|
||||
all_required_ok = all(ok for _, ok, _, req in checks if req)
|
||||
|
||||
if verbose and has_rich:
|
||||
from rich.text import Text
|
||||
|
||||
table = Table(
|
||||
title="Dependency Check",
|
||||
box=box.ROUNDED,
|
||||
title_style="bold cyan",
|
||||
header_style="bold",
|
||||
padding=(0, 1),
|
||||
)
|
||||
table.add_column("", width=3, justify="center")
|
||||
table.add_column("Dependency", style="white")
|
||||
table.add_column("Status")
|
||||
table.add_column("Required", justify="center", width=8)
|
||||
|
||||
for label, ok, detail, required in checks:
|
||||
icon = "[green]✓[/]" if ok else ("[red]✗[/]" if required else "[yellow]–[/]")
|
||||
status_style = "" if ok else ("red" if required else "yellow")
|
||||
req_tag = "[bold]yes[/]" if required else "[dim]no[/]"
|
||||
# Use Text() to avoid rich markup interpretation of version strings
|
||||
status_text = Text(detail, style=status_style)
|
||||
table.add_row(icon, label, status_text, req_tag)
|
||||
|
||||
console.print()
|
||||
console.print(table)
|
||||
console.print()
|
||||
|
||||
# Print install hints for missing items
|
||||
missing = [(label, req) for label, ok, _, req in checks if not ok]
|
||||
if missing:
|
||||
console.print("[bold]Install instructions:[/]\n")
|
||||
for label, required in missing:
|
||||
tag = "[red]REQUIRED[/]" if required else "[yellow]optional[/]"
|
||||
console.print(f" {tag} [bold]{label}[/]")
|
||||
console.print(_get_install_hint(label))
|
||||
console.print()
|
||||
|
||||
if all_required_ok:
|
||||
console.print(" [green]✓ All required dependencies are installed.[/]\n")
|
||||
else:
|
||||
console.print(
|
||||
" [bold red]✗ Some required dependencies are missing.[/]\n"
|
||||
" [dim]Install them and run this check again.[/]\n"
|
||||
)
|
||||
|
||||
elif verbose:
|
||||
# Fallback if rich not available
|
||||
print("\n=== Dependency Check ===\n")
|
||||
for label, ok, detail, required in checks:
|
||||
icon = "✓" if ok else ("✗" if required else "–")
|
||||
req_tag = "(required)" if required else "(optional)"
|
||||
print(f" {icon} {label}: {detail} {req_tag}")
|
||||
print()
|
||||
if not all_required_ok:
|
||||
print("Some required dependencies are missing.")
|
||||
for label, ok, _, req in checks:
|
||||
if not ok:
|
||||
print(f"\n {label}:")
|
||||
print(_get_install_hint(label))
|
||||
print()
|
||||
|
||||
return all_required_ok
|
||||
Reference in New Issue
Block a user