From 98b757a9b19b1a76b28386735c7a2583e80ea4a4 Mon Sep 17 00:00:00 2001 From: Fabrice Quenneville Date: Sun, 17 May 2026 05:12:50 -0400 Subject: [PATCH] feat: add video_repackage_mkv.py script Add a Python utility to recursively repackage non-MKV video files into Matroska containers using FFmpeg, preserving all original streams and metadata without re-encoding. --- README.md | 1 + scripts/video_repackage_mkv.py | 139 +++++++++++++++++++++++++++++++++ 2 files changed, 140 insertions(+) create mode 100755 scripts/video_repackage_mkv.py diff --git a/README.md b/README.md index 77a728f..f757536 100644 --- a/README.md +++ b/README.md @@ -15,6 +15,7 @@ This repository is structured into several key directories: - `video_autoreduce_rename.py`: A script for automated renaming of video files post resolution reduction. - `video_manage_audio.py`: A script for removing audio from video files. - `video_manage_subtitles.py`: A script for removing subtitles from video files. + - `video_repackage_mkv.py`: Recursively repackage non-MKV video files into Matroska containers. - **notes/**: A collection of markdown files containing notes on various topics, including: - `brother.md`: Brother printer setup and troubleshooting on Linux. diff --git a/scripts/video_repackage_mkv.py b/scripts/video_repackage_mkv.py new file mode 100755 index 0000000..fa5851f --- /dev/null +++ b/scripts/video_repackage_mkv.py @@ -0,0 +1,139 @@ +#!/usr/bin/env python3 +"""A utility script to repackage video files into MKV containers using FFmpeg. + +This script recursively scans a specified directory for non-MKV video files +and uses FFmpeg to repackage them into '.mkv' containers. It explicitly maps +all streams (video, audio, subtitles, data) and preserves all metadata without +re-encoding. It includes options to automatically delete the source files upon +successful conversion. +""" + +import argparse +import subprocess +from pathlib import Path +from typing import Sequence + +# Define the targeted non-MKV extensions (case-insensitive handling below) +TARGET_EXTENSIONS = {".mp4", ".m4v", ".flv", ".avi", ".mov", ".wmv"} + + +def parse_args(args: Sequence[str] | None = None) -> argparse.Namespace: + """Parse command-line arguments for the repackaging utility. + + Args: + args: Optional sequence of strings to parse. If None, sys.argv is used. + + Returns: + An argparse.Namespace object containing the parsed arguments. + """ + parser = argparse.ArgumentParser( + description="Recursively repackage non-MKV video files into MKV containers, preserving all streams and metadata." + ) + + parser.add_argument( + "-i", + "--input", + type=Path, + default=Path.cwd(), + help="Path to the root directory to scan for video files (default: current working directory).", + ) + + parser.add_argument( + "-d", + "--delete", + action="store_true", + default=False, + help="Delete the original video files upon successful conversion.", + ) + + # Enable argcomplete support if the module is available + try: + import argcomplete + + argcomplete.autocomplete(parser) + except ImportError: + pass + + return parser.parse_args(args) + + +def repackage_to_mkv(input_dir: Path, delete_source: bool) -> None: + """Recursively scan the input directory and repackage non-MKV videos to MKV. + + Args: + input_dir: The root Path directory to scan. + delete_source: If True, deletes the original video file after a + successful FFmpeg operation. + """ + if not input_dir.is_dir(): + print(f"Error: The directory '{input_dir}' does not exist.") + return + + print(f"Scanning '{input_dir}' recursively for video files...") + + video_files = [ + p + for p in input_dir.rglob("*") + if p.is_file() and p.suffix.lower() in TARGET_EXTENSIONS + ] + + if not video_files: + print("No matching video files found.") + return + + print(f"Found {len(video_files)} files to process.\n") + + for file_path in video_files: + output_path = file_path.with_suffix(".mkv") + + if output_path.exists(): + print( + f"Warning: Destination {output_path.name} already exists. Skipping to avoid accidental overwrite." + ) + print("-" * 40) + continue + + cmd = [ + "ffmpeg", + "-i", + str(file_path), + "-map", + "0", + "-c", + "copy", + "-map_metadata", + "0", + str(output_path), + "-loglevel", + "error", + "-y", + ] + + # Display the relative path from the input directory so it's easier to track progress + relative_display = file_path.relative_to(input_dir) + print(f"Processing: {relative_display} -> {output_path.name}...") + + try: + subprocess.run(cmd, check=True) + print(f"Successfully converted: {file_path.name}") + + if delete_source: + file_path.unlink() + print(f"Deleted original file: {file_path.name}") + + except subprocess.CalledProcessError: + print(f"Failed to convert: {file_path.name}") + except OSError as e: + print(f"System error processing {file_path.name}: {e}") + + print("-" * 40) + + +def main() -> None: + """Main execution entry point.""" + args = parse_args() + repackage_to_mkv(input_dir=args.input, delete_source=args.delete) + + +if __name__ == "__main__": + main()