feat: add crossplattform implementation
This commit is contained in:
@@ -6,7 +6,7 @@ import sys
|
||||
|
||||
from rich.panel import Panel
|
||||
|
||||
from .config import INPUT_PATH, OUTPUT_BASE, console
|
||||
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
|
||||
@@ -25,7 +25,7 @@ def main():
|
||||
" %(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"
|
||||
" %(prog)s --init-config\n"
|
||||
),
|
||||
)
|
||||
parser.add_argument(
|
||||
@@ -37,7 +37,7 @@ def main():
|
||||
parser.add_argument(
|
||||
"--imdb",
|
||||
help="IMDB ID (e.g. tt6977338) — fetches name and year from TMDB. "
|
||||
"Requires TMDB_API_KEY env var.",
|
||||
"Requires TMDB_API_KEY env var or config.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--input", "-i",
|
||||
@@ -59,8 +59,18 @@ def main():
|
||||
action="store_true",
|
||||
help="List all ripped movies in the output directory.",
|
||||
)
|
||||
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)
|
||||
|
||||
137
ripper/config.py
137
ripper/config.py
@@ -1,30 +1,147 @@
|
||||
"""Shared configuration, constants, and rich Console."""
|
||||
"""Shared configuration, constants, and rich Console.
|
||||
|
||||
Loads settings from a TOML config file:
|
||||
Linux/macOS: $XDG_CONFIG_HOME/ripper/config.toml (default ~/.config/ripper/config.toml)
|
||||
Windows: %APPDATA%\\ripper\\config.toml
|
||||
|
||||
Missing keys fall back to platform-aware defaults.
|
||||
Environment variables override config values where noted.
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import tomllib
|
||||
from pathlib import Path
|
||||
|
||||
from rich.console import Console
|
||||
|
||||
console = Console(stderr=True)
|
||||
|
||||
|
||||
# ── Paths ────────────────────────────────────────────────────────────────────
|
||||
# ── Config directory ─────────────────────────────────────────────────────────
|
||||
|
||||
INPUT_PATH = "/mnt/dvd"
|
||||
OUTPUT_BASE = "/mnt/shared/ripped"
|
||||
def _config_dir() -> Path:
|
||||
"""Return the platform-appropriate config directory for ripper."""
|
||||
if sys.platform == "win32":
|
||||
base = Path(os.environ.get("APPDATA", Path.home() / "AppData" / "Roaming"))
|
||||
else:
|
||||
base = Path(os.environ.get("XDG_CONFIG_HOME", Path.home() / ".config"))
|
||||
return base / "ripper"
|
||||
|
||||
|
||||
# ── Encoders ─────────────────────────────────────────────────────────────────
|
||||
CONFIG_DIR = _config_dir()
|
||||
CONFIG_FILE = CONFIG_DIR / "config.toml"
|
||||
|
||||
ENCODER_PRIMARY = "vce_h265_10bit"
|
||||
ENCODER_FALLBACK = "x265_10bit"
|
||||
# Default TOML template (written when user runs --init-config)
|
||||
DEFAULT_CONFIG = """\
|
||||
# ripper configuration
|
||||
# Generated automatically — edit as needed.
|
||||
|
||||
[paths]
|
||||
# Default disc input path
|
||||
# Linux: "/mnt/dvd" or "/dev/sr0"
|
||||
# Windows: "D:\\\\" (your optical drive letter)
|
||||
# input = "/mnt/dvd"
|
||||
|
||||
# Where ripped files are saved
|
||||
# output_base = "/mnt/shared/ripped"
|
||||
|
||||
[encoder]
|
||||
# Primary hardware encoder (set to your GPU's encoder)
|
||||
# AMD: "vce_h265_10bit" NVIDIA: "nvenc_h265_10bit" Intel: "qsv_h265_10bit"
|
||||
# primary = "vce_h265_10bit"
|
||||
|
||||
# CPU fallback encoder
|
||||
# fallback = "x265_10bit"
|
||||
|
||||
# Encoder preset: "speed", "balanced", or "quality"
|
||||
# preset = "speed"
|
||||
|
||||
# Constant quality (lower = better quality, bigger file)
|
||||
# quality = 22
|
||||
|
||||
[api]
|
||||
# TMDB API key for --imdb lookups
|
||||
# Get a free key at https://www.themoviedb.org/settings/api
|
||||
# Can also be set via TMDB_API_KEY environment variable
|
||||
# tmdb_api_key = ""
|
||||
"""
|
||||
|
||||
|
||||
# ── API keys ─────────────────────────────────────────────────────────────────
|
||||
def _platform_defaults() -> dict:
|
||||
"""Return sensible defaults for the current platform."""
|
||||
if sys.platform == "win32":
|
||||
input_path = "D:\\"
|
||||
output_base = str(Path.home() / "Videos" / "Ripped")
|
||||
elif sys.platform == "darwin":
|
||||
input_path = "/dev/disk1"
|
||||
output_base = str(Path.home() / "Movies" / "Ripped")
|
||||
else:
|
||||
input_path = "/mnt/dvd"
|
||||
output_base = "/mnt/shared/ripped"
|
||||
|
||||
TMDB_API_KEY = os.environ.get("TMDB_API_KEY", "")
|
||||
return {
|
||||
"input_path": input_path,
|
||||
"output_base": output_base,
|
||||
"encoder_primary": "vce_h265_10bit",
|
||||
"encoder_fallback": "x265_10bit",
|
||||
"encoder_preset": "speed",
|
||||
"quality": 22,
|
||||
"tmdb_api_key": "",
|
||||
}
|
||||
|
||||
|
||||
# ── Scene-naming maps ────────────────────────────────────────────────────────
|
||||
def _load_config() -> dict:
|
||||
"""Load config from TOML file, falling back to platform defaults."""
|
||||
defaults = _platform_defaults()
|
||||
|
||||
if CONFIG_FILE.is_file():
|
||||
with open(CONFIG_FILE, "rb") as f:
|
||||
toml = tomllib.load(f)
|
||||
paths = toml.get("paths", {})
|
||||
encoder = toml.get("encoder", {})
|
||||
api = toml.get("api", {})
|
||||
|
||||
defaults["input_path"] = paths.get("input", defaults["input_path"])
|
||||
defaults["output_base"] = paths.get("output_base", defaults["output_base"])
|
||||
defaults["encoder_primary"] = encoder.get("primary", defaults["encoder_primary"])
|
||||
defaults["encoder_fallback"] = encoder.get("fallback", defaults["encoder_fallback"])
|
||||
defaults["encoder_preset"] = encoder.get("preset", defaults["encoder_preset"])
|
||||
defaults["quality"] = encoder.get("quality", defaults["quality"])
|
||||
defaults["tmdb_api_key"] = api.get("tmdb_api_key", defaults["tmdb_api_key"])
|
||||
|
||||
# Environment variables override config file
|
||||
if env_key := os.environ.get("TMDB_API_KEY"):
|
||||
defaults["tmdb_api_key"] = env_key
|
||||
|
||||
return defaults
|
||||
|
||||
|
||||
def init_config() -> None:
|
||||
"""Create a default config file if one doesn't exist."""
|
||||
CONFIG_DIR.mkdir(parents=True, exist_ok=True)
|
||||
if CONFIG_FILE.exists():
|
||||
console.print(f" [yellow]⚠[/] Config already exists: [dim]{CONFIG_FILE}[/]")
|
||||
else:
|
||||
CONFIG_FILE.write_text(DEFAULT_CONFIG, encoding="utf-8")
|
||||
console.print(f" [green]✓[/] Created config: [bold]{CONFIG_FILE}[/]")
|
||||
console.print(f" [dim]Edit this file to set your paths, encoder, and API key.[/]")
|
||||
|
||||
|
||||
# ── Load config at import time ───────────────────────────────────────────────
|
||||
|
||||
_cfg = _load_config()
|
||||
|
||||
INPUT_PATH: str = _cfg["input_path"]
|
||||
OUTPUT_BASE: str = _cfg["output_base"]
|
||||
ENCODER_PRIMARY: str = _cfg["encoder_primary"]
|
||||
ENCODER_FALLBACK: str = _cfg["encoder_fallback"]
|
||||
ENCODER_PRESET: str = _cfg["encoder_preset"]
|
||||
QUALITY: int = _cfg["quality"]
|
||||
TMDB_API_KEY: str = _cfg["tmdb_api_key"]
|
||||
|
||||
|
||||
# ── Scene-naming maps (not platform-specific) ───────────────────────────────
|
||||
|
||||
AUDIO_CODEC_SCENE = {
|
||||
"truehd": "TrueHD",
|
||||
|
||||
@@ -13,7 +13,7 @@ from rich.progress import (
|
||||
TimeRemainingColumn,
|
||||
)
|
||||
|
||||
from .config import console
|
||||
from .config import ENCODER_PRESET, QUALITY, console
|
||||
|
||||
|
||||
def build_handbrake_cmd(
|
||||
@@ -36,11 +36,11 @@ def build_handbrake_cmd(
|
||||
# Video
|
||||
"--encoder", encoder,
|
||||
"--enable-hw-decoding",
|
||||
"--quality", "22",
|
||||
"--quality", str(QUALITY),
|
||||
"--rate", "30",
|
||||
"--pfr",
|
||||
"--color-range", "limited",
|
||||
"--encoder-preset", "speed",
|
||||
"--encoder-preset", ENCODER_PRESET,
|
||||
"--encoder-profile", "main10",
|
||||
"--encoder-level", "auto",
|
||||
]
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
import os
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
from .config import AUDIO_CODEC_SCENE, CHANNEL_SCENE
|
||||
|
||||
@@ -28,8 +29,8 @@ def get_source_tag(input_path: str) -> str:
|
||||
return "DVD"
|
||||
|
||||
|
||||
def get_volume_label(input_path: str) -> str | None:
|
||||
"""Try to read the disc volume label."""
|
||||
def _get_volume_label_linux(input_path: str) -> str | None:
|
||||
"""Read volume label on Linux using blkid/findmnt."""
|
||||
# Try blkid first
|
||||
try:
|
||||
result = subprocess.run(
|
||||
@@ -63,6 +64,51 @@ def get_volume_label(input_path: str) -> str | None:
|
||||
return None
|
||||
|
||||
|
||||
def _get_volume_label_windows(input_path: str) -> str | None:
|
||||
"""Read volume label on Windows using ctypes."""
|
||||
import ctypes
|
||||
|
||||
# Normalise to drive root (e.g. "D:\\")
|
||||
drive = os.path.splitdrive(input_path)[0]
|
||||
if not drive:
|
||||
return None
|
||||
root = drive + "\\"
|
||||
|
||||
label_buf = ctypes.create_unicode_buffer(256)
|
||||
ok = ctypes.windll.kernel32.GetVolumeInformationW(
|
||||
root, label_buf, 256, None, None, None, None, 0,
|
||||
)
|
||||
if ok and label_buf.value:
|
||||
return label_buf.value
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def _get_volume_label_macos(input_path: str) -> str | None:
|
||||
"""Read volume label on macOS using diskutil."""
|
||||
try:
|
||||
result = subprocess.run(
|
||||
["diskutil", "info", "-plist", input_path],
|
||||
capture_output=True, text=True, timeout=5,
|
||||
)
|
||||
# Quick extraction from plist XML
|
||||
import plistlib
|
||||
info = plistlib.loads(result.stdout.encode())
|
||||
return info.get("VolumeName") or None
|
||||
except (FileNotFoundError, subprocess.TimeoutExpired, Exception):
|
||||
return None
|
||||
|
||||
|
||||
def get_volume_label(input_path: str) -> str | None:
|
||||
"""Read the disc volume label (cross-platform)."""
|
||||
if sys.platform == "win32":
|
||||
return _get_volume_label_windows(input_path)
|
||||
elif sys.platform == "darwin":
|
||||
return _get_volume_label_macos(input_path)
|
||||
else:
|
||||
return _get_volume_label_linux(input_path)
|
||||
|
||||
|
||||
def scene_audio_tag(audio_track: dict) -> str:
|
||||
"""Build the primary audio scene tag like DTS-HD.MA.5.1"""
|
||||
codec = audio_track.get("CodecName", "unknown").lower()
|
||||
|
||||
Reference in New Issue
Block a user