revise extract episode from dvd script
This commit is contained in:
1 parent
47cfca3c13
commit
58baa5f011
1 file changed
+72
-139
+72
-139
@@ -15,26 +15,20 @@ def check_command(command):
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def scan_dvd(dvd):
|
||||
print("=" * 50)
|
||||
def scan_dvd(dvd_name: Path):
|
||||
print("Scanning DVD")
|
||||
print("=" * 50)
|
||||
print(dvd)
|
||||
print()
|
||||
print(dvd_name)
|
||||
|
||||
result = subprocess.run(
|
||||
["lsdvd", dvd],
|
||||
["lsdvd", dvd_name],
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.STDOUT,
|
||||
text=True,
|
||||
check=False,
|
||||
check=True,
|
||||
)
|
||||
|
||||
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+)?)"
|
||||
)
|
||||
@@ -43,11 +37,12 @@ def scan_dvd(dvd):
|
||||
|
||||
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 = (
|
||||
duration = (
|
||||
hours * 3600
|
||||
+ minutes * 60
|
||||
+ seconds
|
||||
@@ -55,20 +50,15 @@ def scan_dvd(dvd):
|
||||
|
||||
titles.append({
|
||||
"title": title,
|
||||
"duration": total_seconds,
|
||||
"duration_string": (
|
||||
f"{hours:02d}:{minutes:02d}:{seconds:06.3f}"
|
||||
),
|
||||
"duration": duration,
|
||||
})
|
||||
|
||||
if not titles:
|
||||
print("ERROR: No DVD titles found.", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
raise RuntimeError("lsdvd found no DVD titles")
|
||||
|
||||
# Longest title is assumed to be the combined episode track.
|
||||
combined = max(titles, key=lambda x: x["duration"])
|
||||
|
||||
return combined, titles
|
||||
|
||||
return titles
|
||||
|
||||
def get_audio_tracks(dvd, title):
|
||||
"""Return HandBrake audio track information for a DVD title."""
|
||||
@@ -109,10 +99,13 @@ def get_audio_tracks(dvd, title):
|
||||
"number": track_number,
|
||||
"description": description,
|
||||
})
|
||||
|
||||
if not tracks:
|
||||
tracks.append({
|
||||
"number": 0,
|
||||
"description": "",
|
||||
})
|
||||
return tracks
|
||||
|
||||
|
||||
def choose_audio_tracks(tracks):
|
||||
"""
|
||||
Select German first, English second.
|
||||
@@ -122,10 +115,12 @@ def choose_audio_tracks(tracks):
|
||||
|
||||
german = None
|
||||
english = None
|
||||
|
||||
unkown = None
|
||||
for track in tracks:
|
||||
text = track["description"].lower()
|
||||
|
||||
if not text:
|
||||
track["description"] = "Unknown"
|
||||
unkown = track["number"]
|
||||
# German
|
||||
if any(x in text for x in (
|
||||
"german",
|
||||
@@ -152,8 +147,12 @@ def choose_audio_tracks(tracks):
|
||||
if english is not None and english != german:
|
||||
selected.append(english)
|
||||
|
||||
if english is None and german is None:
|
||||
selected.append(unkown)
|
||||
|
||||
return selected
|
||||
def find_episode_count(dvd):
|
||||
|
||||
def find_episode_count(dvd_name: Path, max_dvd_length: int=70):
|
||||
"""Find the number of episodes using lsdvd.
|
||||
|
||||
Assumes:
|
||||
@@ -162,48 +161,13 @@ def find_episode_count(dvd):
|
||||
- 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"]
|
||||
titles = scan_dvd(dvd_name)
|
||||
sorted_items = sorted(titles, key=lambda x: x["duration"], reverse=True)
|
||||
combined_title = 0
|
||||
if sorted_items[0]["duration"] > sorted_items[1]["duration"] * 2:
|
||||
# Longest title = combined episode track
|
||||
combined = max(titles, key=lambda x: x["duration"])
|
||||
combined_title = combined["title"]
|
||||
|
||||
# Episode titles must immediately follow the combined title.
|
||||
title_map = {
|
||||
@@ -223,56 +187,38 @@ def find_episode_count(dvd):
|
||||
|
||||
# Typical episode is ~20-35 minutes.
|
||||
# Adjust these limits if your DVDs contain longer/shorter episodes.
|
||||
if 15 * 60 <= duration <= 40 * 60:
|
||||
if 15 * 60 <= duration <= max_dvd_length * 60:
|
||||
episode_titles.append(title)
|
||||
else:
|
||||
break
|
||||
continue
|
||||
|
||||
if not episode_titles:
|
||||
raise RuntimeError(
|
||||
f"Could not find episodes after combined title "
|
||||
f"{combined_title}"
|
||||
f"{first_episode}"
|
||||
)
|
||||
|
||||
print(f"Combined title: {combined_title}")
|
||||
print(f"Combined duration: {combined_duration:.1f}s")
|
||||
first_episode = episode_titles[0]
|
||||
if combined_title > 0:
|
||||
print(f"Combined title: {titles[combined_title]["title"]}")
|
||||
print(f"Combined duration: {titles[combined_title]["duration"]:.1f}s")
|
||||
print(f"Found {len(episode_titles)} episodes:")
|
||||
print(f" Titles: {episode_titles}")
|
||||
|
||||
return len(episode_titles), episode_titles
|
||||
return first_episode, len(episode_titles), episode_titles
|
||||
|
||||
def process_dvd():
|
||||
dvd_path = Path(dvd)
|
||||
def process_dvd(dvd_source: Path,dvd_title: str,num_of_episodes: int, first_episode: int, args):
|
||||
|
||||
dvd_name = dvd_path.name
|
||||
parent = dvd_path.parent
|
||||
output_dir = dvd.parent / f"{dvd.parent.name} - MP4"
|
||||
output_dir = Path("~/Downloads").expanduser() / dvd_source.parent.name
|
||||
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
|
||||
titles = scan_dvd(dvd_source)
|
||||
last_episode = first_episode + num_of_episodes
|
||||
|
||||
available_titles = {t["title"] for t in titles}
|
||||
|
||||
missing = [
|
||||
title
|
||||
for title in range(first_episode, last_episode + 1)
|
||||
for title in range(first_episode, last_episode )
|
||||
if title not in available_titles
|
||||
]
|
||||
|
||||
@@ -285,38 +231,30 @@ def process_dvd():
|
||||
sys.exit(1)
|
||||
|
||||
print("Episodes to extract:")
|
||||
for number in range(1, args.episodes + 1):
|
||||
for number in range(1, num_of_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
|
||||
for episode in range(first_episode, first_episode + num_of_episodes ):
|
||||
title = episode
|
||||
title_from_zero = episode - first_episode +1
|
||||
if title_from_zero in args.skip_episodes:
|
||||
continue
|
||||
|
||||
output_file = os.path.join(
|
||||
output_dir,
|
||||
f"Pippi Langstrumpf - {episode:02d}.mp4",
|
||||
)
|
||||
audio_tracks = choose_audio_tracks(get_audio_tracks(dvd, title))
|
||||
output_file = output_dir / f"{dvd_title} - {title_from_zero:02d}.mp4"
|
||||
|
||||
if output_file.exists():
|
||||
size_mb = output_file.stat().st_size / (1024 * 1024)
|
||||
print(f"{output_file} ({size_mb:.1f} MB)")
|
||||
audio_tracks = choose_audio_tracks(get_audio_tracks(dvd_source, title))
|
||||
|
||||
if not audio_tracks:
|
||||
raise RuntimeError(
|
||||
@@ -325,14 +263,14 @@ def process_dvd():
|
||||
|
||||
|
||||
print("=" * 50)
|
||||
print(f"Episode {episode} / {args.episodes}")
|
||||
print(f"Episode {episode-first_episode +1} / {num_of_episodes}")
|
||||
print(f"DVD title: {title}")
|
||||
print(f"Output: {output_file}")
|
||||
print("=" * 50)
|
||||
|
||||
command = [
|
||||
"HandBrakeCLI",
|
||||
"-i", dvd,
|
||||
"-i", dvd_source,
|
||||
"-t", str(title),
|
||||
"-o", output_file,
|
||||
"-e", "x264",
|
||||
@@ -364,18 +302,6 @@ def process_dvd():
|
||||
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."
|
||||
@@ -392,15 +318,17 @@ def main():
|
||||
default=0,
|
||||
help="Number of episodes to extract (0 = auto-detect)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--skip-episodes",
|
||||
type=int,nargs="+",
|
||||
default=[],
|
||||
help="List of episodes to skip",
|
||||
)
|
||||
|
||||
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")
|
||||
|
||||
@@ -423,7 +351,7 @@ def main():
|
||||
|
||||
# Process VIDEO_TS directories
|
||||
for video_ts in video_ts_dirs:
|
||||
dvd_source = str(video_ts)
|
||||
dvd_source = video_ts
|
||||
|
||||
# Parent of VIDEO_TS, e.g.
|
||||
# /movies/Pippi DVD 1/VIDEO_TS
|
||||
@@ -432,11 +360,13 @@ def main():
|
||||
|
||||
print(f"Found DVD: {dvd_title}")
|
||||
print(f" Source: {dvd_source}")
|
||||
episode_count, episode_titles = find_episode_count(dvd_name)
|
||||
first_episode, episode_count, episode_titles = find_episode_count(dvd_source)
|
||||
process_dvd(
|
||||
dvd_source,
|
||||
dvd_title,
|
||||
episode_count,
|
||||
first_episode,
|
||||
args
|
||||
)
|
||||
|
||||
# Process ISO files
|
||||
@@ -448,12 +378,15 @@ def main():
|
||||
|
||||
print(f"Found ISO: {dvd_title}")
|
||||
print(f" Source: {dvd_source}")
|
||||
episode_count, episode_titles = find_episode_count(dvd_name)
|
||||
first_episode, episode_count, episode_titles = find_episode_count(dvd_source)
|
||||
process_dvd(
|
||||
dvd_source,
|
||||
dvd_title,
|
||||
episode_count,
|
||||
first_episode,
|
||||
args
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in new issue
Block a user