Created
July 18, 2024 04:07
-
-
Save tatesuke/246f6039b149ca5d7cc91544813502eb to your computer and use it in GitHub Desktop.
FFmpegで連結
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
@python -x "%~f0" %* & exit /b %errorlevel% | |
########################################################### | |
# FFmpegを使って動画ファイルを連結する | |
# | |
# このスクリプトはWindows向けで、`%AppData%\Microsoft\Windows\SendTo`に配置します。 | |
# ファイル右クリックのコンテキストメニュー`送る`内にメニューが追加されるので、そこから起動します。 | |
# | |
# pythonとFFmpegがインストールされ、パスが通っている必要があります。 | |
# | |
import os | |
import sys | |
import subprocess | |
import shutil | |
import re | |
import functools | |
import uuid | |
# 退避ファイルを残すならTrueに設定する | |
KEEP_ARCHIVES = True | |
# 退避フォルダの名前の接頭辞 | |
ARCHIVE_DIR_SUFFIX = "_" | |
# リストファイルの名前 | |
LIST_FILE_NAME = "list.txt" | |
# ファイル名を名前、番号、拡張子に分割する。 | |
# 【例】 | |
# 動画.mp4 → 動画, None, .mp4 | |
# 動画 (1).mp4 → 動画,1,.mp4 | |
def split_file_name(file_path): | |
file_name = os.path.basename(file_path) | |
match = re.match(r'^(?P<name>.+?)( \((?P<number>\d+)\))?(?P<ext>\.[a-zA-Z0-9]+)?$', file_name) | |
if not match: | |
raise ValueError(f"Invalid file name format: {file_name}") | |
name = match.group("name") | |
number = match.group("number") | |
ext = match.group("ext") | |
return name, number, ext | |
# ファイル名を比較する | |
# ファイル名、拡張子が同じなら番号で比較し(※)、そうでなければ文字列として比較する。 | |
# ※: このとき、番号が振られていない場合は0とみなす。例えば`動画.mp4`と`動画 (1).mp4`は`0`と`1`での比較となる。 | |
def compare_file_name(f1, f2): | |
name1, num1, ext1 = split_file_name(f1) | |
name2, num2, ext2 = split_file_name(f2) | |
if (name1 == name2) and (ext1 == ext2): | |
num1 = int(num1) if num1 is not None else 0 | |
num2 = int(num2) if num2 is not None else 0 | |
return num1 - num2 | |
return (f1 > f2) - (f1 < f2) | |
# 退避用ディレクトリを作成 | |
def create_archive_directory(base_name): | |
archive_dir = ARCHIVE_DIR_SUFFIX + base_name | |
os.makedirs(archive_dir, exist_ok=True) | |
return archive_dir | |
# ffmpeg用の連結ファイルを作成 | |
def create_list_file(sorted_input_path_list, archive_dir): | |
list_file_path = os.path.join(archive_dir, LIST_FILE_NAME) | |
with open(list_file_path, "w", encoding="utf-8") as f: | |
for file_path in sorted_input_path_list: | |
escaped_path = file_path.replace("\\", "\\\\") | |
f.write(f"file '{escaped_path}'\n") | |
return list_file_path | |
# 動画を連結する。ffmpegの機能を使う | |
def concat_videos(list_file_path, temp_video_path): | |
command = f"ffmpeg -safe 0 -f concat -i \"{list_file_path}\" -c copy \"{temp_video_path}\"" | |
subprocess.check_call(command, shell=True) | |
# main関数 | |
def main(): | |
# 入力検証 | |
input_path_list = sys.argv[1:] # 2番目以降の引数にファイルパスが渡る | |
if not input_path_list: | |
print("Usage: python script.py <file1> <file2> ...") | |
sys.exit(1) | |
sorted_input_path_list = sorted(input_path_list, key=functools.cmp_to_key(compare_file_name)) | |
print("以下の通りファイルを連結します。") | |
print("") | |
print("順番:") | |
for input_path in sorted_input_path_list: | |
print(f" {os.path.basename(input_path)}") | |
print("") | |
print("連結後ファイル名:") | |
print(f" {os.path.basename(sorted_input_path_list[0])}") | |
print("") | |
while True: | |
user_input = input("実行しますか? [y/n]: ").strip().lower() | |
if user_input in ('y', 'yes'): | |
break | |
elif user_input in ('n', 'no'): | |
sys.exit(0) | |
# 連結準備 | |
archive_dir = create_archive_directory(os.path.basename(sorted_input_path_list[0])) | |
list_file_path = create_list_file(sorted_input_path_list, archive_dir) | |
temp_video_path = os.path.join(archive_dir, f"{uuid.uuid4()}.mp4") | |
# 連結実行 | |
try: | |
concat_videos(list_file_path, temp_video_path) | |
except subprocess.CalledProcessError as e: | |
print(f"連結に失敗しました: {e}") | |
input() | |
sys.exit(1) | |
# 後処理 | |
for file_path in sorted_input_path_list: | |
shutil.move(file_path, archive_dir) | |
shutil.move(temp_video_path, sorted_input_path_list[0]) | |
print("") | |
print("連結完了。") | |
if KEEP_ARCHIVES: | |
print(f"元ファイルは以下に移動しました。") | |
print(archive_dir) | |
else: | |
shutil.rmtree(archive_dir) | |
print("エンターキーで終了します。") | |
input() | |
if __name__ == "__main__": | |
main() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment