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.
This commit is contained in:
Fabrice Quenneville 2026-05-17 05:12:50 -04:00
parent 330f1edf42
commit 98b757a9b1
2 changed files with 140 additions and 0 deletions

View File

@ -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_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_audio.py`: A script for removing audio from video files.
- `video_manage_subtitles.py`: A script for removing subtitles 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: - **notes/**: A collection of markdown files containing notes on various topics, including:
- `brother.md`: Brother printer setup and troubleshooting on Linux. - `brother.md`: Brother printer setup and troubleshooting on Linux.

139
scripts/video_repackage_mkv.py Executable file
View File

@ -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()