first version of old dvd extraction script
This commit is contained in:
1 parent
0c0920d3df
commit
0d059bc1c0
1 file changed
+459
@@ -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()
|
||||||
Reference in new issue
Block a user