| ファイル | 役割 |
|---|---|
| encode.sh | EPGStation録画完了コールバック。リネーム・ジョブ登録 |
| encode_host.sh | ホスト上でエンコードを実行。succeeded移動も担当 |
| encode_jls.sh | QSVEncCでエンコード。チャプター付与 |
| jls_process.sh | CM検出メイン。chapter_exe→logoframe→join_logo_scp |
| disk_cleanup.sh | TS/MP4の古いファイル自動削除 |
| /etc/systemd/system/encode-watcher.service | encode_queueを監視するsystemdサービス |
#!/bin/bash
# encode.sh - EPGStation録画完了コールバック
# JLSでCM検出+VAAPIエンコード、TSはAmatsukaze比較用に残す
set -uo pipefail
export LANG=ja_JP.UTF-8
export LC_ALL=ja_JP.UTF-8
LOG_BASE="/mnt/data/PT2/log"
INPUT="${RECPATH:-}"
NAME="${NAME:-unknown}"
CHANNELNAME="${CHANNELNAME:-unknown}"
CLEANUP_SCRIPT="/mnt/data/backup/scripts/disk_cleanup.sh"
ENCODE_JLS="/mnt/data/backup/scripts/encode_jls.sh"
if [ -z "${INPUT}" ]; then
echo "ERROR: RECPATH is not set" >&2
exit 0
fi
BASENAME=$(basename "${INPUT}" .ts)
INPUT_DIR=$(dirname "${INPUT}")
DATEDIR=$(date +%Y%m)
LOG_DIR="${LOG_BASE}/${DATEDIR}"
mkdir -p "${LOG_DIR}"
umask 0002
LOG_FILE="${LOG_DIR}/${BASENAME}.log"
log() { echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*" | tee -a "${LOG_FILE}"; }
log "=== 録画完了 ==="
log "ファイル: ${INPUT}"
log "番組名: ${NAME}"
log "チャンネル: ${CHANNELNAME}"
# --- スキップ判定 ---
SKIP="false"
if echo "${CHANNELNAME}" | grep -qiP '[JJ].{0,6}[SS][PP][OO][RR][TT][SS]'; then
log "JSPORTS → スキップ"
SKIP="true"
elif echo "${NAME}" | grep -q '連続テレビ小説'; then
log "連続テレビ小説 → スキップ"
SKIP="true"
elif echo "${BASENAME}" | grep -qE 'PR|PR|ダイジェスト|「ドジャース」'; then
log "スキップ対象番組 → スキップ"
SKIP="true"
fi
# --- ファイル名クリーニング ---
DATEPART="${BASENAME%% *}"
RAW_TITLE=$(echo "${BASENAME}" | sed -e 's/^[0-9]* //')
CLEAN_TITLE=$(echo "${RAW_TITLE}" | sed \
-e 's/^\[[^]]*\][[:space:]]*//' \
-e 's/\[解\]//g' -e 's/\[初\]//g' -e 's/\[デ\]//g' \
-e 's/\[無\]//g' -e 's/\[無料\]//g' -e 's/\[二\]//g' \
-e 's/\[字\]//g' -e 's/\[新\]//g' -e 's/\[再\]//g' \
-e 's/\[SS\]//g' -e 's/\[多\]//g' -e 's/(字幕版)//g' \
-e 's/\[R15+\]//g' -e 's/\[PG12相当\]//g' -e 's/\[PG12\]//g' \
-e 's/‼//g' -e 's/\//-/g' -e 's/|/ /g' \
-e 's/[[:space:]]\+/ /g' -e 's/[[:space:]]*$//' -e 's/^[[:space:]]*//' \
-e 's/"//g' \
-e "s/'//g" \
-e 's/`//g' \
-e 's/\$//g' \
-e 's/\*//g')
NEW_BASENAME="${DATEPART} ${CLEAN_TITLE}"
NEW_INPUT="${INPUT_DIR}/${NEW_BASENAME}.ts"
if [ "${NEW_BASENAME}" != "${BASENAME}" ]; then
if [ ! -f "${NEW_INPUT}" ]; then
mv "${INPUT}" "${NEW_INPUT}"
log "リネーム: ${BASENAME} → ${NEW_BASENAME}"
NEW_LOG_FILE="${LOG_DIR}/${NEW_BASENAME}.log"
mv "${LOG_FILE}" "${NEW_LOG_FILE}"
LOG_FILE="${NEW_LOG_FILE}"
INPUT="${NEW_INPUT}"
BASENAME="${NEW_BASENAME}"
else
log "WARN: リネーム先が既に存在: ${NEW_BASENAME}"
fi
fi
# --- JLS+エンコード ---
if [ "${SKIP}" = "false" ]; then
log "エンコードジョブ登録..."
QUEUE_DIR="/mnt/data/PT2/encode_queue"
mkdir -p "${QUEUE_DIR}"
JOB_FILE="${QUEUE_DIR}/${BASENAME}.job"
echo "JOB_INPUT=\"${INPUT}\"" > "${JOB_FILE}"
echo "JOB_CHANNEL=\"${CHANNELNAME}\"" >> "${JOB_FILE}"
log "ジョブ登録完了: ${JOB_FILE}"
else
log "エンコードスキップ(TSを保持)"
fi
# --- ディスク容量チェック ---
if [ -x "${CLEANUP_SCRIPT}" ]; then
bash "${CLEANUP_SCRIPT}" >> "${LOG_FILE}" 2>&1
fi
exit 0
/mnt/data/backup/scripts/encode_host.sh
#!/bin/bash
# encode_host.sh - ホスト上でエンコードを実行
export LANG=ja_JP.UTF-8
export LC_ALL=ja_JP.UTF-8
QUEUE_DIR="/mnt/data/PT2/encode_queue"
ENCODE_JLS="/mnt/data/backup/scripts/encode_jls.sh"
LOG_BASE="/mnt/data/PT2/log"
log() { echo "[$(date '+%Y-%m-%d %H:%M:%S')] [HOST] $*"; }
# キューファイルを処理
for job in "${QUEUE_DIR}"/*.job; do
[ -f "${job}" ] || continue
# ジョブファイル読み込み
source "${job}"
log "エンコード開始: ${JOB_INPUT}"
DATEDIR=$(date +%Y%m)
BASENAME=$(basename "${JOB_INPUT}" .ts)
LOG_FILE="${LOG_BASE}/${DATEDIR}/${BASENAME}.log"
# フォアグラウンドで実行(完了を待つ)
bash "${ENCODE_JLS}" "${JOB_INPUT}" "${JOB_CHANNEL}" \
>> "${LOG_FILE}" 2>&1
if [ $? -eq 0 ]; then
mv "${JOB_INPUT}" "/mnt/data/PT2/succeeded/"
log "TSをsucceededに移動: $(basename ${JOB_INPUT})"
else
log "WARN: エンコード失敗 → TSをPT2に保持(Amatsukaze処理待ち)"
fi
# 処理済みジョブを削除
rm -f "${job}"
log "ジョブ完了: ${job}"
done
/mnt/data/backup/scripts/encode_jls.sh
#!/bin/bash # encode_jls.sh - JLS CM検出+QSVEncCエンコード # 使用法: encode_jls.sh<チャンネル名> set -uo pipefail export LANG=ja_JP.UTF-8 export LC_ALL=ja_JP.UTF-8 INPUT="$1" CHANNELNAME="$2" JLS_SCRIPT="/mnt/data/backup/scripts/jls_process.sh" OUTPUT_DIR="/mnt/data/TV" WORK_BASE="/mnt/data/PT2/work_jls" BASENAME=$(basename "${INPUT}" .ts) WORK_DIR="${WORK_BASE}/${BASENAME}" TRIM_FILE="${WORK_DIR}/${BASENAME}.trim.txt" OUTPUT="${OUTPUT_DIR}/${BASENAME}.mp4" LOG_BASE="/mnt/data/PT2/log" DATEDIR=$(date +%Y%m) LOG_FILE="${LOG_BASE}/${DATEDIR}/${BASENAME}.log" log() { echo "[$(date '+%Y-%m-%d %H:%M:%S')] [ENC] $*" >> "${LOG_FILE}"; } log "=== encode_jls.sh 開始 ===" log "入力: ${INPUT}" log "チャンネル: ${CHANNELNAME}" mkdir -p "${OUTPUT_DIR}" # --- JLS CM検出 --- log "JLS CM検出開始..." bash "${JLS_SCRIPT}" "${INPUT}" "${WORK_DIR}" "${CHANNELNAME}" \ >> "${LOG_FILE}" 2>&1 # --- QSVEncCエンコード --- if [ ! -f "${TRIM_FILE}" ]; then log "Trimなし → 全編エンコード" QSVENCC_TRIM="" else TRIM_LINE=$(cat "${TRIM_FILE}") log "Trim: ${TRIM_LINE}" QSVENCC_TRIM=$(echo "${TRIM_LINE}" | \ python3 -c " import re, sys line = sys.stdin.read().strip() trims = re.findall(r'Trim\((\d+),(\d+)\)', line) print(','.join(f'{s}:{e}' for s,e in trims)) ") log "QSVEncC trim: ${QSVENCC_TRIM}" fi log "エンコード開始(QSVEncC)..." if [ -n "${QSVENCC_TRIM}" ]; then CHAPTER_FILE="${WORK_DIR}/${BASENAME}.chapter.txt" if [ -f "${CHAPTER_FILE}" ]; then qsvencc \ -i "${INPUT}" \ --input-analyze 5 \ --trim "${QSVENCC_TRIM}" \ -c h264 --icq 23 \ --audio-codec aac --audio-bitrate 192 \ --chapter "${CHAPTER_FILE}" \ -o "${OUTPUT}" >> "${LOG_FILE}" 2>&1 else qsvencc \ -i "${INPUT}" \ --input-analyze 5 \ --trim "${QSVENCC_TRIM}" \ -c h264 --icq 23 \ --audio-codec aac --audio-bitrate 192 \ -o "${OUTPUT}" >> "${LOG_FILE}" 2>&1 fi else qsvencc \ -i "${INPUT}" \ --input-analyze 5 \ -c h264 --icq 23 \ --audio-codec aac --audio-bitrate 192 \ -o "${OUTPUT}" >> "${LOG_FILE}" 2>&1 fi if [ $? -eq 0 ]; then SIZE=$(du -sh "${OUTPUT}" | cut -f1) log "エンコード完了: ${OUTPUT} (${SIZE})" else log "ERROR: エンコード失敗" exit 1 fi # 作業フォルダ削除 rm -rf "${WORK_DIR}" log "作業フォルダ削除: ${WORK_DIR}" log "=== encode_jls.sh 完了 ===" exit 0
/mnt/data/backup/scripts/jls_process.sh
#!/bin/bash
# jls_process.sh - JLS CM検出スクリプト(ホスト直接実行版)
# 使用法: jls_process.sh <TSファイルパス> <作業ディレクトリ> [チャンネル名]
set -uo pipefail
export LANG=ja_JP.UTF-8
export LC_ALL=ja_JP.UTF-8
INPUT="$1"
WORK_DIR="$2"
CHANNELNAME="${3:-}"
LOGO_DIR="/mnt/data/docker/jls/logo"
JL_DIR="/mnt/data/docker/jls/JL"
FFMPEG="/usr/bin/ffmpeg"
CHAPTER_EXE="/usr/local/bin/chapter_exe"
LOGOFRAME="/usr/local/bin/logoframe"
JLS="/usr/local/bin/join_logo_scp"
BASENAME=$(basename "${INPUT}" .ts)
FPS="29.97"
log() { echo "[$(date '+%Y-%m-%d %H:%M:%S')] [JLS] $*"; }
mkdir -p "${WORK_DIR}"
# --- チャンネル別早期スキップ判定 ---
SKIP_LOGOFRAME="false"
WOWOW_MODE="false"
case "${CHANNELNAME}" in
*"WOWOW"*|*"WOWOW"*)
log "WOWOW → logoframeスキップ・無音検出のみ"
SKIP_LOGOFRAME="true"
WOWOW_MODE="true" ;;
*"NHK"*|*"NHK"*)
log "NHK → logoframeスキップ"
SKIP_LOGOFRAME="true" ;;
esac
# --- チャンネル名→ロゴファイル名マッピング(全角対応)---
get_logo_file() {
local ch="$1"
local logo=""
case "${ch}" in
*"NHK総合"*|*"NHK総合"*|*"NHK-G"*) logo="NHK-G" ;;
*"NHK BS"*|*"NHK BS"*|*"NHK BS"*) logo="NHK-BS1" ;;
*"NHKEテレ"*|*"NHKEテレ"*|*"NHK-E"*) logo="NHK-E" ;;
*"日テレ"*|*"日本テレビ"*|*"NTV"*) logo="NTV" ;;
*"BS-TBS"*|*"BS-TBS"*) logo="BS-TBS" ;;
*"TBS"*|*"TBS"*) logo="TBS" ;;
*"フジテレビ"*|*"CX"*) logo="CX" ;;
*"テレビ朝日"*|*"EX"*) logo="EX" ;;
*"テレ東"*|*"テレビ東京"*|*"TX"*) logo="TX" ;;
*"BSテレ東"*|*"BSテレ東"*|*"BS-TX"*) logo="BS-TX" ;;
*"BS日テレ"*|*"BS日テレ"*|*"BS-NTV"*) logo="BS-NTV" ;;
*"BS朝日"*|*"BS朝日"*|*"BS-ASA"*) logo="BS-ASA" ;;
*"BS11"*|*"BS11"*) logo="BS11" ;;
esac
if [ -n "${logo}" ] && [ -f "${LOGO_DIR}/${logo}.lgd" ]; then
echo "${LOGO_DIR}/${logo}.lgd"
fi
}
# --- ffmpegで音声をWAVに分離 ---
WAV_FILE="${WORK_DIR}/${BASENAME}.wav"
log "音声抽出中..."
"${FFMPEG}" -nostdin -i "${INPUT}" -vn -acodec pcm_s16le -ar 48000 \
"${WAV_FILE}" -y 2>&1 | tail -1
if [ ! -f "${WAV_FILE}" ]; then
log "ERROR: 音声抽出失敗"
exit 1
fi
log "音声抽出完了"
# --- AVSファイル生成 ---
AVS_FILE="${WORK_DIR}/${BASENAME}.avs"
cat > "${AVS_FILE}" << AVSEOF
v = LWLibavVideoSource("${INPUT}", stream_index=0, cache=false)
a = LWLibavAudioSource("${WAV_FILE}", cache=false)
AudioDub(v, a)
AVSEOF
# --- chapter_exe: 無音+シーンチェンジ検出 ---
SCP_FILE="${WORK_DIR}/${BASENAME}.scp"
log "chapter_exe 実行中..."
"${CHAPTER_EXE}" -v "${AVS_FILE}" -o "${SCP_FILE}" < /dev/null 2>&1
if [ ! -f "${SCP_FILE}" ]; then
log "ERROR: chapter_exe 失敗"
exit 1
fi
log "chapter_exe 完了"
# --- ロゴファイル選択 ---
LOGO_FILE=""
if [ "${SKIP_LOGOFRAME}" = "false" ]; then
LOGO_FILE=$(get_logo_file "${CHANNELNAME}")
if [ -n "${LOGO_FILE}" ]; then
log "ロゴ使用: $(basename ${LOGO_FILE})"
else
log "ロゴなし(チャンネル: ${CHANNELNAME})"
fi
else
log "logoframeスキップ(${CHANNELNAME})"
fi
# --- logoframe: ロゴ検出 ---
LOGO_TXT="${WORK_DIR}/${BASENAME}.logo.txt"
if [ "${SKIP_LOGOFRAME}" = "false" ] && [ -n "${LOGO_FILE}" ]; then
AVS_VIDEO="${WORK_DIR}/${BASENAME}_video.avs"
cat > "${AVS_VIDEO}" << AVSEOF
LWLibavVideoSource("${INPUT}", stream_index=0, cache=false)
AVSEOF
log "logoframe 実行中..."
"${LOGOFRAME}" "${AVS_VIDEO}" -logo "${LOGO_FILE}" -oa "${LOGO_TXT}" 2>&1
log "logoframe 完了"
if [ ! -s "${LOGO_TXT}" ]; then
log "WARN: ロゴ未検出(ロゴなしで処理継続)"
LOGO_TXT=""
fi
fi
TRIM_FILE="${WORK_DIR}/${BASENAME}.trim.txt"
# --- WOWOW: 無音検出だけでTrim生成 ---
if [ "${WOWOW_MODE}" = "true" ]; then
log "WOWOW用 無音Trim生成中..."
# freezedetectで先頭2分の静止画区間を検出
FREEZE_END=$(ffmpeg -nostdin -t 120 -i "${INPUT}" -vf "freezedetect=n=0.01:d=1" -f null - 2>&1 | awk -F: '/freeze_end/{t=$2+0; if(t>10 && t<120){print t; exit}}')
if [ -n "${FREEZE_END}" ]; then
FREEZE_FRAME=$(python3 -c "print(int(${FREEZE_END} * 29.97) + 30)")
log "WOWOW: freezedetect検出 ${FREEZE_END}秒 → フレーム${FREEZE_FRAME}"
# SCPファイルの最後のフレーム番号から取得
TOTAL_FRAMES=$(awk '/SCPos:/{n=$NF} END{print n+0}' "${SCP_FILE}")
if [ -z "${TOTAL_FRAMES}" ] || [ "${TOTAL_FRAMES}" -eq 0 ]; then
TOTAL_FRAMES=999999
fi
END_FRAME=$((TOTAL_FRAMES - 30))
echo "Trim(${FREEZE_FRAME},${END_FRAME})" > "${TRIM_FILE}"
log "WOWOW Trim生成完了(freezedetect)"
WOWOW_DONE="true"
fi
if [ "${WOWOW_DONE:-false}" != "true" ]; then
python3 << WOWPYEOF
import re
scp_file = "${SCP_FILE}"
input_file = "${INPUT}"
trim_file = "${TRIM_FILE}"
fps = 29.97
total_frames = None
with open(scp_file) as f:
content_scp = f.read()
lines = content_scp.splitlines()
# 最後のフレーム数取得
total_frames = None
for line in reversed(lines):
m = re.search(r'SCPos:\d+ (\d+)', line)
if m:
total_frames = int(m.group(1))
break
# ○マークを優先、なければ先頭2分以内の50フレーム以上の無音を境界とする
boundaries = []
for line in lines:
if '○' in line:
m = re.search(r'SCPos:(\d+)', line)
if m:
boundaries.append(('circle', int(m.group(1))))
if not boundaries:
# ○マークなし: 先頭120秒(3594フレーム)以内の@マークを探す
for line in lines:
if '@' in line:
m = re.search(r'SCPos:(\d+)', line)
if m and int(m.group(1)) < 3594:
boundaries.append(('at', int(m.group(1))))
break
if not boundaries:
# @マークもなし: 先頭120秒以内の長い無音区間を探す
mutes = re.findall(r'mute\s*\d+:\s*(\d+)\s*-\s*(\d+)フレーム', content_scp)
for start_f, dur_f in mutes:
start_f, dur_f = int(start_f), int(dur_f)
if start_f < 3594 and dur_f >= 30:
boundaries.append(('mute', start_f + dur_f))
break
# freezedetectで静止画区間を検出(先頭2分のみ)
freeze_boundary = None
try:
import subprocess
result = subprocess.run(
["ffmpeg", "-t", "120", "-i", input_file,
"-vf", "freezedetect=n=0.01:d=1", "-f", "null", "-"],
capture_output=True, text=True, timeout=60
)
freeze_ends = []
for line in result.stderr.splitlines():
if "freeze_end" in line:
t = float(line.split(":")[-1].strip())
if 10 < t < 120: # 10秒〜2分の間
freeze_ends.append(t)
if freeze_ends:
# 最初のfreeze_endをCM境界とする
t = freeze_ends[0]
freeze_boundary = int(t * fps) + 30 # 1秒マージン
print(f"WOWOW: freezedetect検出 {t:.1f}秒 → フレーム{freeze_boundary}")
except Exception as e:
print(f"WOWOW: freezedetect失敗 ({e})")
if not boundaries or not total_frames:
if freeze_boundary:
end = total_frames - 30 if total_frames else 999999
with open(trim_file, 'w') as f:
f.write(f"Trim({freeze_boundary},{end})")
print(f"WOWOW: freezedetect Trim({freeze_boundary},{end})")
else:
with open(trim_file, 'w') as f:
f.write(f"Trim(0,{total_frames or 999999})")
print("WOWOW: 境界なし → 全編")
else:
marker_type, boundary = boundaries[0]
start = freeze_boundary if freeze_boundary and freeze_boundary > boundary else boundary + 30
end = total_frames - 30
with open(trim_file, 'w') as f:
f.write(f"Trim({start},{end})")
print(f"WOWOW: Trim({start},{end}) 先頭{start/fps:.1f}秒カット ({marker_type}マーク)")
WOWPYEOF
fi
log "WOWOW Trim生成完了"
fi
if [ "${WOWOW_DONE:-false}" = "true" ]; then
exit 0
fi
# --- join_logo_scp: CM位置推定 ---
TRIM_FILE="${WORK_DIR}/${BASENAME}.trim.txt"
if echo "${CHANNELNAME}" | grep -qE 'NHK総合|NHK総合|NHKEテレ|NHK-E'; then
JL_SCRIPT="${JL_DIR}/JL_NHK.txt"
elif echo "${CHANNELNAME}" | grep -qiE 'MBS|毎日放送'; then
JL_SCRIPT="${JL_DIR}/JL_MBS.txt"
elif echo "${CHANNELNAME}" | grep -qiE 'AT-X|アニマックス'; then
JL_SCRIPT="${JL_DIR}/JL_ATX.txt"
elif echo "${CHANNELNAME}" | grep -qiE 'BS-TBS'; then
JL_SCRIPT="${JL_DIR}/JL_BS-TBS.txt"
else
JL_SCRIPT="${JL_DIR}/JL_標準.txt"
fi
log "JLスクリプト: $(basename ${JL_SCRIPT})"
log "join_logo_scp 実行中..."
JLS_ARGS=(-inscp "${SCP_FILE}" -incmd "${JL_SCRIPT}" -o "${TRIM_FILE}")
if [ -n "${LOGO_TXT:-}" ] && [ -f "${LOGO_TXT}" ]; then
JLS_ARGS=(-inlogo "${LOGO_TXT}" "${JLS_ARGS[@]}")
fi
"${JLS}" "${JLS_ARGS[@]}" 2>&1
if [ ! -f "${TRIM_FILE}" ]; then
log "ERROR: Trim生成失敗"
exit 1
fi
TRIM_LINE=$(cat "${TRIM_FILE}")
log "Trim出力: ${TRIM_LINE}"
# --- Trim形式 → ffmpegチャプターメタデータ変換 ---
FFMETA="${WORK_DIR}/${BASENAME}.ffmeta"
log "ffmetaデータ変換中..."
python3 << PYEOF
import re, sys
trim_line = """${TRIM_LINE}"""
fps = ${FPS}
trims = re.findall(r'Trim\((\d+),(\d+)\)', trim_line)
if not trims:
print("ERROR: Trimが見つかりません")
sys.exit(1)
def frame_to_us(frame, fps):
return int(frame / fps * 1_000_000)
lines = [";FFMETADATA1"]
chapter_num = 1
prev_end = None
TRIM_MARGIN = 30 # 1秒分(29.97fps)内側にトリム
for i, (s, e) in enumerate(trims):
s, e = int(s), int(e)
# 本編の開始・終了を0.5秒内側にトリム
s_trim = s + TRIM_MARGIN
e_trim = e - TRIM_MARGIN
if s_trim >= e_trim:
s_trim, e_trim = s, e # 短すぎる場合はそのまま
if prev_end is not None and s > prev_end + 1:
cm_start = frame_to_us(prev_end + 1, fps)
cm_end = frame_to_us(s - 1, fps)
lines += ["[CHAPTER]", "TIMEBASE=1/1000000",
f"START={cm_start}", f"END={cm_end}",
f"title=CM{chapter_num - 1}"]
start_us = frame_to_us(s_trim, fps)
end_us = frame_to_us(e_trim, fps)
lines += ["[CHAPTER]", "TIMEBASE=1/1000000",
f"START={start_us}", f"END={end_us}",
f"title=本編{chapter_num}"]
chapter_num += 1
prev_end = e
with open("${FFMETA}", "w") as f:
f.write("\n".join(lines) + "\n")
print(f"チャプター生成: 本編{chapter_num-1}個")
# OGM形式チャプターファイル生成(qsvencc用)
chapter_ogm = "${WORK_DIR}/${BASENAME}.chapter.txt"
ogm_lines = []
chap_num = 1
chap_time = 0.0
for i, (s, e) in enumerate(trims):
s_trim = int(s) + TRIM_MARGIN
e_trim = int(e) - TRIM_MARGIN
if s_trim >= e_trim:
s_trim, e_trim = int(s), int(e)
if i > 0:
prev_e = int(trims[i-1][1]) - TRIM_MARGIN
if prev_e < int(trims[i-1][1]):
pass
cm_dur = (s_trim - (int(trims[i-1][1]) - TRIM_MARGIN)) / fps
chap_time += cm_dur
t = chap_time
h = int(t // 3600)
m = int((t % 3600) // 60)
sec = t % 60
ogm_lines.append(f"CHAPTER{chap_num:02d}={h:02d}:{m:02d}:{sec:06.3f}")
ogm_lines.append(f"CHAPTER{chap_num:02d}NAME=本編{chap_num}")
dur = (e_trim - s_trim) / fps
chap_time += dur
chap_num += 1
with open(chapter_ogm, "w") as f:
f.write("\n".join(ogm_lines) + "\n")
print(f"OGMチャプター生成完了: {chapter_ogm}")
PYEOF
if [ -f "${FFMETA}" ]; then
log "ffmeta生成完了: ${FFMETA}"
CHAPTER_COUNT=$(grep -c "^title=" "${FFMETA}" 2>/dev/null || echo 0)
log "総チャプター数: ${CHAPTER_COUNT}"
rm -f "${WAV_FILE}"
exit 0
else
log "ERROR: ffmeta生成失敗"
exit 1
fi
/etc/systemd/system/encode-watcher.service
[Unit]
Description=Encode Queue Watcher
After=network.target
[Service]
Type=simple
User=vafee
ExecStart=/bin/bash -c 'inotifywait -m -e close_write \
/mnt/data/PT2/encode_queue/ | \
while read dir event file; do \
bash /mnt/data/backup/scripts/encode_host.sh; \
done'
Restart=always
RestartSec=5
[Install]
WantedBy=multi-user.target
# 有効化・起動 sudo systemctl daemon-reload sudo systemctl enable encode-watcher sudo systemctl start encode-watcher # 動作確認 sudo systemctl status encode-watcher