Skip to content

Instantly share code, notes, and snippets.

@tatesuke
Created July 18, 2024 04:07
Show Gist options
  • Save tatesuke/246f6039b149ca5d7cc91544813502eb to your computer and use it in GitHub Desktop.
Save tatesuke/246f6039b149ca5d7cc91544813502eb to your computer and use it in GitHub Desktop.
FFmpegで連結
@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