first version of old dvd extraction script

This commit is contained in:
Lucas Faber authored and Lucas Faber committed 2026-10-01 14:07:20 +00:00
1 parent 0c0920d3df
commit 0d059bc1c0
1 file changed
+459
+459
View File
@@ -0,0 +1,459 @@
#!/usr/bin/env python3
import argparse
import os
import re
import shutil
import subprocess
import sys
from pathlib import Path
def check_command(command):
if shutil.which(command) is None:
print(f"ERROR: '{command}' is not installed.", file=sys.stderr)
sys.exit(1)
def scan_dvd(dvd):
print("=" * 50)
print("Scanning DVD")
print("=" * 50)
print(dvd)
print()
result = subprocess.run(
["lsdvd", dvd],
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
text=True,
check=False,
)
output = result.stdout
print(output)
# Example:
# Title: 07, Length: 01:50:34.560 Chapters: 17, ...
pattern = re.compile(
r"Title:\s*(\d+),\s*Length:\s*(\d+):(\d+):(\d+(?:\.\d+)?)"
)
titles = []
for match in pattern.finditer(output):
title = int(match.group(1))
hours = int(match.group(2))
minutes = int(match.group(3))
seconds = float(match.group(4))
total_seconds = (
hours * 3600
+ minutes * 60
+ seconds
)
titles.append({
"title": title,
"duration": total_seconds,
"duration_string": (
f"{hours:02d}:{minutes:02d}:{seconds:06.3f}"
),
})
if not titles:
print("ERROR: No DVD titles found.", file=sys.stderr)
sys.exit(1)
# Longest title is assumed to be the combined episode track.
combined = max(titles, key=lambda x: x["duration"])
return combined, titles
def get_audio_tracks(dvd, title):
"""Return HandBrake audio track information for a DVD title."""
result = subprocess.run(
[
"HandBrakeCLI",
"-i", dvd,
"-t", str(title),
"--scan",
],
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
text=True,
check=True,
)
output = result.stdout
# Examples HandBrake may produce:
#
# + 1, Deutsch (AC3, 2.0 ch, 192 kbps)
# + 2, English (AC3, 2.0 ch, 224 kbps)
#
# Also handles German/Englisch/deu/eng where present.
tracks = []
pattern = re.compile(
r"\+\s+(\d+),\s+(.+?)\s+\((?:AC3|AAC|MP3|E-AC-3|DTS)"
)
for match in pattern.finditer(output):
track_number = int(match.group(1))
description = match.group(2).strip()
tracks.append({
"number": track_number,
"description": description,
})
return tracks
def choose_audio_tracks(tracks):
"""
Select German first, English second.
Returns a list of HandBrake track numbers.
"""
german = None
english = None
for track in tracks:
text = track["description"].lower()
# German
if any(x in text for x in (
"german",
"deutsch",
"deu",
)):
german = track["number"]
# English
if any(x in text for x in (
"english",
"englisch",
"eng",
)):
english = track["number"]
selected = []
# German FIRST -> becomes default audio
if german is not None:
selected.append(german)
# English SECOND
if english is not None and english != german:
selected.append(english)
return selected
def find_episode_count(dvd):
"""Find the number of episodes using lsdvd.
Assumes:
- The longest title is a combined track containing all episodes.
- Individual episode titles immediately follow the longest title.
- Episode titles have approximately similar durations.
"""
result = subprocess.run(
["lsdvd", dvd],
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
text=True,
check=True,
)
output = result.stdout
pattern = re.compile(
r"Title:\s*(\d+),\s*Length:\s*(\d+):(\d+):(\d+(?:\.\d+)?)"
)
titles = []
for match in pattern.finditer(output):
title = int(match.group(1))
hours = int(match.group(2))
minutes = int(match.group(3))
seconds = float(match.group(4))
duration = (
hours * 3600
+ minutes * 60
+ seconds
)
titles.append({
"title": title,
"duration": duration,
})
if not titles:
raise RuntimeError("lsdvd found no DVD titles")
# Longest title = combined episode track
combined = max(titles, key=lambda x: x["duration"])
combined_title = combined["title"]
combined_duration = combined["duration"]
# Episode titles must immediately follow the combined title.
title_map = {
t["title"]: t["duration"]
for t in titles
}
first_episode = combined_title + 1
episode_titles = []
for title in range(first_episode, max(title_map) + 1):
if title not in title_map:
break
duration = title_map[title]
# Typical episode is ~20-35 minutes.
# Adjust these limits if your DVDs contain longer/shorter episodes.
if 15 * 60 <= duration <= 40 * 60:
episode_titles.append(title)
else:
break
if not episode_titles:
raise RuntimeError(
f"Could not find episodes after combined title "
f"{combined_title}"
)
print(f"Combined title: {combined_title}")
print(f"Combined duration: {combined_duration:.1f}s")
print(f"Found {len(episode_titles)} episodes:")
print(f" Titles: {episode_titles}")
return len(episode_titles), episode_titles
def process_dvd():
dvd_path = Path(dvd)
dvd_name = dvd_path.name
parent = dvd_path.parent
output_dir = dvd.parent / f"{dvd.parent.name} - MP4"
output_dir.mkdir(parents=True, exist_ok=True)
output_file = output_dir / f"{dvd_title} - {episode:02d}.mp4"
output_file = output_dir / f"{dvd_title} - {episode:02d}.mp4"
if output_file.exists():
size_mb = output_file.stat().st_size / (1024 * 1024)
print(f"{output_file} ({size_mb:.1f} MB)")
combined, titles = scan_dvd(dvd)
combined_title = combined["title"]
print("=" * 50)
print("Combined title")
print("=" * 50)
print(f"Title: {combined_title}")
print(f"Duration: {combined['duration_string']}")
print()
first_episode = combined_title + 1
last_episode = combined_title + args.episodes
available_titles = {t["title"] for t in titles}
missing = [
title
for title in range(first_episode, last_episode + 1)
if title not in available_titles
]
if missing:
print(
"ERROR: Expected episode titles were not found:",
", ".join(map(str, missing)),
file=sys.stderr,
)
sys.exit(1)
print("Episodes to extract:")
for number in range(1, args.episodes + 1):
title = first_episode + number - 1
print(f" Episode {number}: DVD title {title}")
print()
# Output directory:
#
# Pippi L... DVD - 1/
# Pippi L... DVD - 1 - MP4/
dvd_name = os.path.basename(os.path.normpath(dvd))
parent = os.path.dirname(dvd)
output_dir = os.path.join(
parent,
f"{dvd_name} - MP4",
)
os.makedirs(output_dir, exist_ok=True)
print("Output directory:")
print(f" {output_dir}")
print()
for episode in range(1, args.episodes + 1):
title = first_episode + episode - 1
output_file = os.path.join(
output_dir,
f"Pippi Langstrumpf - {episode:02d}.mp4",
)
audio_tracks = choose_audio_tracks(get_audio_tracks(dvd, title))
if not audio_tracks:
raise RuntimeError(
f"No German or English audio track found for title {title}"
)
print("=" * 50)
print(f"Episode {episode} / {args.episodes}")
print(f"DVD title: {title}")
print(f"Output: {output_file}")
print("=" * 50)
command = [
"HandBrakeCLI",
"-i", dvd,
"-t", str(title),
"-o", output_file,
"-e", "x264",
"-q", "18",
"-a", ",".join(map(str, audio_tracks)),
"-E", "copy:ac3",
]
with open("handbrake.log", "w") as logfile:
try:
subprocess.run(
command,
stdout=logfile,
stderr=subprocess.STDOUT,
text=True,
check=True,
)
except subprocess.CalledProcessError as exc:
print(
f"ERROR: HandBrakeCLI failed for episode {episode}.",
file=sys.stderr,
)
sys.exit(exc.returncode)
print()
print("=" * 50)
print("Finished!")
print("=" * 50)
for episode in range(1, args.episodes + 1):
output_file = os.path.join(
output_dir,
f"Pippi Langstrumpf - {episode:02d}.mp4",
)
if os.path.exists(output_file):
size_mb = os.path.getsize(output_file) / (1024 * 1024)
print(f"{output_file} ({size_mb:.1f} MB)")
def main():
parser = argparse.ArgumentParser(
description="Extract episodes from a DVD using lsdvd and HandBrakeCLI."
)
parser.add_argument(
"dvd",
help="Path to DVD directory containing VIDEO_TS",
)
parser.add_argument(
"--episodes",
type=int,
default=0,
help="Number of episodes to extract (0 = auto-detect)",
)
args = parser.parse_args()
dvd_name = os.path.abspath(args.dvd)
if args.episodes < 1:
parser.error("Number of episodes must be a positive integer.")
check_command("lsdvd")
check_command("HandBrakeCLI")
root = Path(dvd_name).resolve()
check_command("lsdvd")
check_command("HandBrakeCLI")
# Find all VIDEO_TS directories
video_ts_dirs = [
p for p in root.rglob("VIDEO_TS")
if p.is_dir()
]
# Find all ISO files
iso_files = [
p for p in root.rglob("*")
if p.is_file() and p.suffix.lower() == ".iso"
]
# Process VIDEO_TS directories
for video_ts in video_ts_dirs:
dvd_source = str(video_ts)
# Parent of VIDEO_TS, e.g.
# /movies/Pippi DVD 1/VIDEO_TS
# -> Pippi DVD 1
dvd_title = video_ts.parent.name
print(f"Found DVD: {dvd_title}")
print(f" Source: {dvd_source}")
episode_count, episode_titles = find_episode_count(dvd_name)
process_dvd(
dvd_source,
dvd_title,
episode_count,
)
# Process ISO files
for iso in iso_files:
dvd_source = str(iso)
# Filename without .iso
dvd_title = iso.stem
print(f"Found ISO: {dvd_title}")
print(f" Source: {dvd_source}")
episode_count, episode_titles = find_episode_count(dvd_name)
process_dvd(
dvd_source,
dvd_title,
episode_count,
)
if __name__ == "__main__":
main()