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:
parent
330f1edf42
commit
98b757a9b1
@ -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.
|
||||
|
||||
139
scripts/video_repackage_mkv.py
Executable file
139
scripts/video_repackage_mkv.py
Executable 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()
|
||||
Loading…
Reference in New Issue
Block a user