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