diff --git a/media handling/extract_dvds.py b/media handling/extract_dvds.py index b233b86..765b7a9 100644 --- a/media handling/extract_dvds.py +++ b/media handling/extract_dvds.py @@ -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() \ No newline at end of file