diff --git a/ripper.py b/ripper.py old mode 100644 new mode 100755 index e61a264..28e03ef --- a/ripper.py +++ b/ripper.py @@ -672,6 +672,147 @@ def generate_nfo(mkv_path: str) -> str: return nfo_path +def parse_scene_name(scene: str) -> dict: + """ + Parse a scene-style directory/file name into components. + + Example: Game.Night.2018.1080p.BluRay.10bit.x265.DTS-HD.MA.5.1.MULTI-6.Audio + """ + info: dict = {"raw": scene} + + # Extract known tokens by pattern + res_match = re.search(r"\b(2160p|1080p|720p|480p)\b", scene, re.I) + info["resolution"] = res_match.group(1) if res_match else "?" + + source_match = re.search(r"\b(BluRay|DVD|BDRip|WEB-DL|WEBRip|HDTV)\b", scene, re.I) + info["source"] = source_match.group(1) if source_match else "?" + + codec_match = re.search(r"\b(x265|x264|HEVC|AVC|h\.?265|h\.?264)\b", scene, re.I) + info["codec"] = codec_match.group(1) if codec_match else "?" + + # Audio: find codec tags like DTS-HD.MA.5.1, DD.5.1, TrueHD.7.1, etc. + audio_match = re.search( + r"\b(DTS-HD\.MA|DTS-HD\.HR|DTS|TrueHD|DDP|DD|AAC|FLAC|LPCM|MP3|OPUS)" + r"(?:\.(\d\.\d))?", + scene, re.I, + ) + if audio_match: + info["audio"] = audio_match.group(0) + else: + info["audio"] = "?" + + multi_match = re.search(r"MULTI-(\d+)", scene, re.I) + info["languages"] = int(multi_match.group(1)) if multi_match else 1 + + # Year: 4-digit number that looks like a year (1900-2099) + year_match = re.search(r"\.((?:19|20)\d{2})\.", scene) + info["year"] = year_match.group(1) if year_match else "?" + + # Title: everything before the year (or before the resolution if no year) + if year_match: + info["title"] = scene[:year_match.start()].replace(".", " ") + elif res_match: + info["title"] = scene[:res_match.start()].rstrip(".").replace(".", " ") + else: + info["title"] = scene.replace(".", " ") + + return info + + +def format_size(size_bytes: int) -> str: + """Format bytes into human-readable size.""" + for unit in ("B", "KB", "MB", "GB", "TB"): + if size_bytes < 1024: + return f"{size_bytes:.1f} {unit}" + size_bytes /= 1024 + return f"{size_bytes:.1f} PB" + + +def list_library(output_base: str) -> None: + """ + Scan the output directory for ripped movies and display them in a table. + """ + base = Path(output_base) + if not base.is_dir(): + console.print(f"[bold red]ERROR:[/] Output directory not found: [dim]{output_base}[/]") + sys.exit(1) + + movies: list[dict] = [] + + for entry in sorted(base.iterdir()): + if not entry.is_dir(): + continue + + # Look for MKV files in the directory + mkv_files = list(entry.glob("*.mkv")) + if not mkv_files: + continue + + mkv = mkv_files[0] # primary MKV + nfo = entry / (mkv.stem + ".nfo") + info = parse_scene_name(entry.name) + try: + info["mkv_size"] = mkv.stat().st_size + except OSError: + info["mkv_size"] = 0 + info["has_nfo"] = nfo.exists() + info["path"] = str(entry) + movies.append(info) + + if not movies: + console.print() + console.print( + Panel( + "[dim]No ripped movies found.[/]\n" + f"Output directory: [dim]{output_base}[/]", + title="[bold cyan]Library[/]", + border_style="dim", + padding=(0, 2), + ) + ) + console.print() + return + + # Build table + table = Table( + title=f"Library · {len(movies)} movies", + box=box.ROUNDED, + title_style="bold cyan", + header_style="bold", + show_lines=True, + padding=(0, 1), + ) + table.add_column("#", style="dim", width=3, justify="right") + table.add_column("Title", style="bold white", min_width=20) + table.add_column("Year", style="cyan", justify="center", width=6) + table.add_column("Res", style="green", justify="center", width=6) + table.add_column("Source", style="yellow", width=7) + table.add_column("Audio", style="magenta") + table.add_column("Lang", justify="center", width=5) + table.add_column("Size", style="dim", justify="right", width=9) + table.add_column("NFO", justify="center", width=3) + + total_size = 0 + for i, m in enumerate(movies, 1): + total_size += m["mkv_size"] + table.add_row( + str(i), + m["title"], + m["year"], + m["resolution"], + m["source"], + m["audio"], + str(m["languages"]), + format_size(m["mkv_size"]), + "[green]✓[/]" if m["has_nfo"] else "[dim]✗[/]", + ) + + console.print() + console.print(table) + console.print(f" [dim]Total: {format_size(total_size)} across {len(movies)} movies · {output_base}[/]") + console.print() + + # ── Main ───────────────────────────────────────────────────────────────────── def main(): @@ -683,6 +824,7 @@ def main(): " %(prog)s --imdb tt6977338\n" " %(prog)s --name 'Game Night' --year 2018\n" " %(prog)s --scan-only\n" + " %(prog)s --list\n" " %(prog)s --name Inception --year 2010 --input /dev/sr0\n" ), ) @@ -712,8 +854,18 @@ def main(): 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.", + ) args = parser.parse_args() + # ── Library listing (no disc needed) ───────────────────────────────── + if args.list: + list_library(args.output_base) + return + # ── Banner ─────────────────────────────────────────────────────────── console.print() console.print(