feat: add preflight

This commit is contained in:
Jan Meyer
2026-02-10 20:36:32 +01:00
parent f9a4af1c94
commit e6b0191b26
3 changed files with 257 additions and 0 deletions

6
requirements.txt Normal file
View File

@@ -0,0 +1,6 @@
# ripper — DVD/Blu-ray ripper
# Required
rich>=13.0
# Optional (for NFO generation with media metadata)
pymediainfo>=6.0

View File

@@ -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
View 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