ubuntu
カテゴリ
TV録画後エンコード&join_logo_scp 3
2026/06/20 17時tv
スクリプト一覧
ファイル役割
encode.shEPGStation録画完了コールバック。リネーム・ジョブ登録
encode_host.shホスト上でエンコードを実行。succeeded移動も担当
encode_jls.shQSVEncCでエンコード。チャプター付与
jls_process.shCM検出メイン。chapter_exe→logoframe→join_logo_scp
disk_cleanup.shTS/MP4の古いファイル自動削除
/etc/systemd/system/encode-watcher.serviceencode_queueを監視するsystemdサービス
/mnt/data/backup/scripts/encode.sh
#!/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  

記事一覧