188 lines
6.9 KiB
Python
188 lines
6.9 KiB
Python
"""CLI entry point and main orchestration logic."""
|
|
|
|
import argparse
|
|
import os
|
|
import sys
|
|
|
|
from rich.panel import Panel
|
|
|
|
from .config import INPUT_PATH, OUTPUT_BASE, CONFIG_FILE, init_config, console
|
|
from .scanner import detect_encoder, scan_disc, select_title
|
|
from .tracks import best_tracks_per_language, print_track_tables
|
|
from .naming import build_scene_name, get_source_tag, get_volume_label
|
|
from .encode import build_handbrake_cmd, run_encode
|
|
from .metadata import lookup_imdb, generate_nfo
|
|
from .library import list_library
|
|
from .disc import check_disc_readable, unstuck_disc
|
|
|
|
|
|
def main():
|
|
parser = argparse.ArgumentParser(
|
|
description="Rip DVD/Blu-ray to MKV using HandBrakeCLI (H.265 10-bit).",
|
|
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
epilog=(
|
|
"Examples:\n"
|
|
" %(prog)s --imdb tt6977338\n"
|
|
" %(prog)s --name 'Game Night' --year 2018\n"
|
|
" %(prog)s --scan-only\n"
|
|
" %(prog)s --list\n"
|
|
" %(prog)s --init-config\n"
|
|
),
|
|
)
|
|
parser.add_argument(
|
|
"--name", "-n",
|
|
help="Movie name (spaces OK, will be dotted). "
|
|
"If omitted, the disc volume label is used.",
|
|
)
|
|
parser.add_argument("--year", "-y", help="Release year for the filename.")
|
|
parser.add_argument(
|
|
"--imdb",
|
|
help="IMDB ID (e.g. tt6977338) — fetches name and year from TMDB. "
|
|
"Requires TMDB_API_KEY env var or config.",
|
|
)
|
|
parser.add_argument(
|
|
"--input", "-i",
|
|
default=INPUT_PATH,
|
|
help=f"Input path (default: {INPUT_PATH}).",
|
|
)
|
|
parser.add_argument(
|
|
"--output-base",
|
|
default=OUTPUT_BASE,
|
|
help=f"Base output directory (default: {OUTPUT_BASE}).",
|
|
)
|
|
parser.add_argument(
|
|
"--scan-only",
|
|
action="store_true",
|
|
help="Only scan the disc and print track info, don't encode.",
|
|
)
|
|
parser.add_argument(
|
|
"--list", "-l",
|
|
action="store_true",
|
|
help="List all ripped movies in the output directory.",
|
|
)
|
|
parser.add_argument(
|
|
"--check-disc",
|
|
action="store_true",
|
|
help="Check disc readability and attempt recovery if stuck.",
|
|
)
|
|
parser.add_argument(
|
|
"--init-config",
|
|
action="store_true",
|
|
help="Create a default config.toml in the config directory.",
|
|
)
|
|
args = parser.parse_args()
|
|
|
|
# ── Generate config file ─────────────────────────────────────────────
|
|
if args.init_config:
|
|
init_config()
|
|
return
|
|
|
|
# ── Library listing (no disc needed) ─────────────────────────────────
|
|
if args.list:
|
|
list_library(args.output_base)
|
|
return
|
|
|
|
# ── Disc health check ────────────────────────────────────────────────
|
|
if args.check_disc:
|
|
if not unstuck_disc(args.input):
|
|
sys.exit(1)
|
|
return
|
|
|
|
# ── Banner ───────────────────────────────────────────────────────────
|
|
console.print()
|
|
console.print(
|
|
Panel(
|
|
"[bold white]DVD / Blu-ray Ripper[/]\n"
|
|
"[dim]H.265 10-bit · HandBrakeCLI · Scene Naming[/]",
|
|
border_style="cyan",
|
|
padding=(0, 2),
|
|
)
|
|
)
|
|
console.print()
|
|
|
|
# ── 1. Detect encoder ────────────────────────────────────────────────
|
|
encoder = detect_encoder()
|
|
|
|
# ── 2. Scan disc ─────────────────────────────────────────────────────
|
|
scan = scan_disc(args.input)
|
|
title = select_title(scan)
|
|
|
|
# ── 3. Select tracks ─────────────────────────────────────────────────
|
|
audio_all = title.get("AudioList", [])
|
|
subtitle_all = title.get("SubtitleList", [])
|
|
|
|
audio_sel = best_tracks_per_language(audio_all, "audio")
|
|
subtitle_sel = best_tracks_per_language(subtitle_all, "subtitle")
|
|
|
|
print_track_tables(audio_sel, subtitle_sel)
|
|
|
|
if args.scan_only:
|
|
console.print("\n [dim](scan-only mode, exiting)[/]\n")
|
|
return
|
|
|
|
# ── 4. Build filename ────────────────────────────────────────────────
|
|
movie_name = args.name
|
|
year = args.year
|
|
|
|
# IMDB lookup overrides --name and --year
|
|
if args.imdb:
|
|
movie_name, imdb_year = lookup_imdb(args.imdb)
|
|
if not year:
|
|
year = imdb_year
|
|
|
|
if not movie_name:
|
|
label = get_volume_label(args.input)
|
|
if label:
|
|
movie_name = label.replace("_", " ").title()
|
|
console.print(f" [green]✓[/] Using volume label: [bold]{movie_name}[/]")
|
|
else:
|
|
movie_name = console.input(" [cyan]Enter movie name:[/] ").strip()
|
|
if not movie_name:
|
|
console.print("[bold red]ERROR:[/] No movie name provided.")
|
|
sys.exit(1)
|
|
|
|
source_tag = get_source_tag(args.input)
|
|
scene = build_scene_name(movie_name, year, title, audio_sel, source_tag)
|
|
|
|
# Create output directory
|
|
out_dir = os.path.join(args.output_base, scene)
|
|
os.makedirs(out_dir, exist_ok=True)
|
|
output_file = os.path.join(out_dir, f"{scene}.mkv")
|
|
|
|
console.print()
|
|
console.print(
|
|
Panel(
|
|
f"[bold]{scene}.mkv[/]\n"
|
|
f"[dim]{out_dir}[/]",
|
|
title="[bold cyan]Output[/]",
|
|
border_style="dim",
|
|
padding=(0, 2),
|
|
)
|
|
)
|
|
console.print()
|
|
|
|
# ── 5. Encode ────────────────────────────────────────────────────────
|
|
cmd = build_handbrake_cmd(
|
|
args.input, output_file, title, audio_sel, subtitle_sel, encoder,
|
|
)
|
|
|
|
returncode = run_encode(cmd, input_path=args.input)
|
|
if returncode != 0:
|
|
console.print(f"\n[bold red]ERROR:[/] HandBrakeCLI exited with code {returncode}")
|
|
sys.exit(returncode)
|
|
|
|
console.print(f"\n [green]✓[/] Encode complete")
|
|
|
|
# ── 6. Generate NFO ──────────────────────────────────────────────────
|
|
generate_nfo(output_file)
|
|
|
|
console.print()
|
|
console.print(
|
|
Panel(
|
|
f"[green]✓ All done![/]\n[dim]{out_dir}[/]",
|
|
border_style="green",
|
|
padding=(0, 2),
|
|
)
|
|
)
|
|
console.print()
|