"""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, QUALITY, CODEC, CONFIG_FILE, init_config, console from .scanner import ( discover_encoders, print_encoder_table, select_encoder, scan_disc, select_title, CODEC_SCENE_TAG, CODEC_DISPLAY, ) 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 CODEC_CHOICES = ("hevc", "h264", "av1") def main(): parser = argparse.ArgumentParser( description="Rip DVD/Blu-ray to MKV using HandBrakeCLI.", formatter_class=argparse.RawDescriptionHelpFormatter, epilog=( "Examples:\n" " %(prog)s --imdb tt6977338\n" " %(prog)s --name 'Game Night' --year 2018\n" " %(prog)s --codec av1 --quality 30\n" " %(prog)s --scan-only\n" " %(prog)s --list\n" " %(prog)s --encoders\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( "--codec", "-c", choices=CODEC_CHOICES, default=CODEC, help=f"Video codec family (default: {CODEC}).", ) parser.add_argument( "--quality", "-q", type=int, default=None, help=f"Constant quality / rate factor (default: {QUALITY}). " "Lower = better quality, bigger file.", ) 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( "--encoders", action="store_true", help="Show all available encoders and exit.", ) 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() # Quality: CLI flag > config default quality = args.quality if args.quality is not None else QUALITY # ── Generate config file ───────────────────────────────────────────── if args.init_config: init_config() return # ── Library listing (no disc needed) ───────────────────────────────── if args.list: list_library(args.output_base) return # ── Show available encoders ────────────────────────────────────────── if args.encoders: available = discover_encoders() print_encoder_table(available) return # ── Disc health check ──────────────────────────────────────────────── if args.check_disc: if not unstuck_disc(args.input): sys.exit(1) return # ── Banner ─────────────────────────────────────────────────────────── codec_display = CODEC_DISPLAY.get(args.codec, args.codec.upper()) console.print() console.print( Panel( f"[bold white]DVD / Blu-ray Ripper[/]\n" f"[dim]{codec_display} · HandBrakeCLI · Scene Naming[/]", border_style="cyan", padding=(0, 2), ) ) console.print() try: _run_pipeline(args, quality) except KeyboardInterrupt: console.print("\n [bold yellow]⚠ Interrupted[/] — exiting.\n") sys.exit(130) def _run_pipeline(args, quality: int) -> None: """Core ripping pipeline, separated for clean interrupt handling.""" # ── 1. Detect encoder ──────────────────────────────────────────────── available = discover_encoders() print_encoder_table(available) encoder = select_encoder(available, codec=args.codec) # ── 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) # Determine codec tag and bit depth from the selected encoder codec_tag = CODEC_SCENE_TAG.get(args.codec, args.codec.upper()) is_10bit = "10bit" in encoder or "10" in encoder scene = build_scene_name( movie_name, year, title, audio_sel, source_tag, codec_tag=codec_tag, is_10bit=is_10bit, ) # 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( input_path=args.input, output_path=output_file, title=title, audio_tracks=audio_sel, subtitle_tracks=subtitle_sel, encoder=encoder, quality=quality, ) returncode = run_encode(cmd, input_path=args.input) if returncode == 130: # User cancelled — re-raise so outer handler prints message raise KeyboardInterrupt 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()