見出し画像

テレビ録画メディアサーバー構築入門(第9回)

使用法

ここで構築した「録画メディアサーバー」の特徴は、EPGStation で予約録画して(中間作業を完全自動化して)Jellyfin で視聴するというものです。

予約設定はEPGStationの「ルール」で細かく指定します。

ルール設定

最初に「ルール」に複数のスロットを作っておくと便利です。

まずは「検索」

http://localhost:8888/#/search

から適当に「検索」して、最下部の「追加」を押すと、「ルール」に追加されます。

http://localhost:8888/#/rule

追加された項目の、左にあるスイッチをオフにする。

おなじ作業を5回繰り返して、ルールのスロットを5つ作成する。

一番上のスロットには、最優先の番組を登録する。時間的に重複しないことが確実であれば、複数の番組をまとめて登録が可能です。これには、「正規表現」を選択して、「キーワード」に番組名を OR記号 | でつないで並べれば良い。

例:番組名1|番組名2|番組名3

ジャンルや放送局、最小の長さ(分)などで指定することも可能です。

あるいは、キーワードを設定せずに、ジャンルを選び、除外キーワードを設定する。例:名前に [再] を含む番組を除外
キーワードが複数あるなら、上のようにOR記号 |で項目を加えていく。

同様にして、優先度の低い番組を下のスロットに追加していきます。

たとえば、「映画」や「ドラマ」は、「ジャンル」よりまとめて登録できますが、時間的に一部重複するかたち(裏番組)で、複数の放送局が同時放送される場合があります。
こうした場合、「キーワード」や「放送局」で区別して、別のスロットに登録することで、録画の優先順位を指定できます。

画像


個人的には、キーワード(芸人)やジャンル(アニメ)で「ざっくり」検索して、ヒットした結果から、不要な番組を手動でシラミツブシに「除外」していく方法を、便利に使っています。

サムネ画像

第7回で動画ファイル名から番組名を抽出したことで、同じ番組がフォルダにまとめられることになりました。

しかし、フォルダのイメージ画像が設定されていないため、Jellyfinの番組一覧表示が殺風景なものになっています。この問題は、動画からサムネ画像folder.jpgを作成して、各フォルダに配置すれば解決します。対象としては、最新の動画を使用することにします。さらに、親フォルダである、メディアルート media の画像もあわせてアップデートします。

まずその前に、Jellyfinはファイル名に基づき、登録されたオンラインデータベースを検索しに行きます。とりあえず、オンラインアクセスしないように設定します。

ダッシュボード
ライブラリ
ライブラリ
番組
ライブラリを管理
メタデータダウンローダーの項目すべてチェックを外す
インターネットからメタデータを自動的に再取得:しない
TheMovieDb, The Open Movie Database のチェックを外す

update_thumbs.sh

作業フォルダ~/workに作成
(追記:実はこれ、後で使わなくなる。第12回・13回参照)

#!/bin/bash
MEDIA_ROOT="$HOME/media"

latest_file=""
latest_time=0
latest_dir=""
for show_dir in "$MEDIA_ROOT"/*; do
    [ -d "$show_dir" ] || continue

    # そのディレクトリ内で最新の動画ファイルを探す
    latest_episode=$(find "$show_dir" -type f \( -iname "*.mkv" -o -iname "*.mp4" -o -iname "*.avi" \) \
                     -printf "%T@ %p\n" | sort -nr | head -n1 | cut -d' ' -f2-)

    if [ -n "$latest_episode" ]; then
        if [ ! -f "$show_dir/folder.jpg" ]; then
            echo "Generating thumbnail in $show_dir from: $latest_episode"
            ffmpeg -y -ss 00:00:05 -i "$latest_episode" -frames:v 1 -q:v 2 "$show_dir/folder.jpg"
        fi
        # 全体で最も新しいファイルかをチェック
        file_time=$(stat -c %Y "$latest_episode")
        if [ "$file_time" -ge "$latest_time" ]; then
            latest_time=$file_time
            latest_file="$latest_episode"
        fi
    else
        echo "No video files found in: $show_dir"
    fi
done

# 全体で最新の動画から生成したサムネールを MEDIA_ROOT にコピー
if [ -n "$latest_file" ]; then
    echo "Also copying thumbnail of latest video globally: $latest_file"
    temp_thumb="/tmp/latest_folder.jpg"
    ffmpeg -y -ss 00:00:05 -i "$latest_file" -frames:v 1 -q:v 2 "$temp_thumb"
    cp "$temp_thumb" "$MEDIA_ROOT/folder.jpg"
    rm -f "$temp_thumb"
else
    echo "No video files found in any show directories."
fi
chmod +x update_thumbs.sh
./update_thumbs.sh

空フォルダの削除

視聴後の動画を手動で削除することがあります。動画がなくとも、親フォルダは残るため、番組一覧には、クリックしても再生エラーとなる残骸だけが残されます。

過去に作成したものの、もはや動画mp4を含まないフォルダを自動で削除するスクリプトを用意します。

remove_empty_dirs.sh

作業フォルダ~/workに作成

#!/bin/bash

MEDIA_ROOT="$HOME/media"

# 再帰的にディレクトリを検査する関数
check_and_remove_dir() {
    local dir="$1"
    
    # ディレクトリ内のサブディレクトリについて再帰的にチェック
    for sub_dir in "$dir"/*/; do
        if [ -d "$sub_dir" ]; then
            check_and_remove_dir "$sub_dir"
        fi
    done
    
    # サブディレクトリのチェックが完了したら、現在のディレクトリに .mp4 ファイルがないか確認
    mp4_files=$(find "$dir" -type f -name "*.mp4")
    
    if [ -z "$mp4_files" ]; then
        # サブディレクトリにも .mp4 ファイルが存在しない場合のみ削除
        sub_dirs_with_mp4=false
        for sub_dir in "$dir"/*/; do
            if [ -d "$sub_dir" ]; then
                mp4_in_sub_dir=$(find "$sub_dir" -type f -name "*.mp4")
                if [ -n "$mp4_in_sub_dir" ]; then
                    sub_dirs_with_mp4=true
                    break
                fi
            fi
        done
        
        if ! $sub_dirs_with_mp4; then
            echo "Deleting directory: $dir"
            rm -rf "$dir"
        fi
    fi
}

# 再帰的にチェックと削除実行
check_and_remove_dir "$MEDIA_ROOT"

処理済み動画の削除

これまで EPGStation の録画済み動画には、手を加えない方針できました。しかし、ある程度の期間、問題なく運用できてくると、サイズが大きい元のm2tsファイルを削除する必要が出てきます。できれば、自動でやりたい。

1週間以上前の古い動画をデータベースから削除するスクリプトが次のとおりです。

deleteold.py

#!/usr/bin/env python3
import requests
import json
from datetime import datetime, timedelta

# 録画サーバのURLとAPIエンドポイント
BASE_URL = 'http://localhost:8888/api'
RECORDED_ENDPOINT = '/recorded?isHalfWidth=false&offset=0&limit=1000'

# APIヘッダー
headers = {
    'accept': 'application/json',
}

def get_recorded_programs():
    """録画済みの番組情報を取得"""
    response = requests.get(BASE_URL + RECORDED_ENDPOINT, headers=headers)
    if response.status_code == 200:
        return response.json()['records']
    else:
        print(f"Failed to fetch recorded programs: {response.status_code}")
        return []

def delete_recorded_program(program_id):
    """指定したIDの録画済み番組を削除"""
    url = f"{BASE_URL}/recorded/{program_id}"
    response = requests.delete(url, headers=headers)
    if response.status_code == 200:
        print(f"Program {program_id} deleted successfully.")
    else:
        print(f"Failed to delete program {program_id}: {response.status_code}")

def main():
    recorded_programs = get_recorded_programs()
    
    # 現在の日時と最古保存日時を取得
    now = datetime.now()
#    datetime_delete = now - timedelta(days=30) #1ヶ月前
    datetime_delete = now - timedelta(days=7) #1週間 
    
    for record in recorded_programs:
        program_id = record['id']
        start_at = record.get('startAt', None)
        
        if start_at is not None:
            # UNIXタイムスタンプをdatetimeオブジェクトに変換
            start_datetime = datetime.fromtimestamp(start_at / 1000)
            
            # 古いもののみ削除する
            if start_datetime < datetime_delete:
                delete_recorded_program(program_id)

if __name__ == '__main__':
    main()

保存領域圧迫の最大の要因は、m2ts動画ファイルです。正常に処理した後は、適切に削除(または移動)していかないとシステムが崩壊します。ディスク容量が枯渇してきたら、削除間隔を1週間 timedelta(days=7) から1日 days=1 に変更する必要が生じるでしょう。

ファイル削除は、細心の注意を要する作業です。確実に動作するか手動操作で確認しましょう。

enc01.sh

以上3つを 適切なタイミングで実行します。update_thumbs.shは、録画m2tsをmp4に編集するスクリプトenc01.sh の最後で実行します。(つまり、また、enc01.shを置き換える。)

#!/bin/bash

SOURCEDIR="$HOME/docker-mirakurun-epgstation/recorded/"
WORKDIR="$HOME/work/"
OUTDIR="$HOME/media/"
NHK1="GR20"
NHK2="GR13"

encoded=false

while IFS= read -r FILE; 
do
    cd $SOURCEDIR
    if [ -f "$FILE" ]; then
	FILENAME=${FILE%.*}
	GRSTRING=`echo $FILENAME | sed -n 's/.*\(GR[0-9][0-9]\).*/\1/p'`

	cd $WORKDIR
	if [ -z $GRSTRING ] || [ $GRSTRING = $NHK1 ] || [ $GRSTRING = $NHK2 ]; then
	    # NHKならば
	    ./enc0.sh $SOURCEDIR/"$FILE" ${FILENAME}.mp4
	else
	    # NHK以外
	    ./enc1.sh $SOURCEDIR/"$FILE" ${FILENAME}.mp4
	fi

	if [ -f ${FILENAME}.mp4 ] ; then
	    ./mvjf.sh ${FILENAME}.mp4 $OUTDIR
	else
	    echo ERROR: mp4 not created 
	fi

	./processed.py "$FILE"
	encoded=true
    fi
done < <($WORKDIR/toprocess.py)

$encoded && $WORKDIR/update_thumbs.sh

enc01.sh はencode.sh から呼び出されます。encode.shは、録画状態を監視する意味でも、頻繁に実行します。

一方で、remove_empty_dirs.shとdeleteold.py は、そこまで頻繁に実行する必要はありませんので、それぞれ定時(毎時0分と朝5時0分)に実行します。(追記:remove_empty_dirs.shは必須ではありません。Jellyfinの「番組」タブから手動で「シリーズを削除」できます。)

crontab -e

#0 5 * * * $HOME/work/enc01.sh
*/4 * * * * $HOME/work/encode.sh >> $HOME/work/encode.log 2>&1
0 * * * * $HOME/work/remove_empty_dirs.sh >> $HOME/work/encode.log 2>&1 #不要?
0 5 * * * $HOME/work/deleteold.py >> $HOME/work/encode.log 2>&1

ここまで自動化をすすめると、録画予約から視聴まで、すべての操作をスマホだけで完結させることができるようになりました。

自家版ネットフリックスのような、自分だけのテレビ録画メディアサーバー環境が完成しました。


http://100.x.y.z:8888/ から録画予約し、http://100.x.y.z:8096/ から視聴します。100.x.y.zはサーバーのアドレスです(第2回第3回第6回)。


いいなと思ったら応援しよう!

ピックアップされています

全自動TV録画 視聴環境構築

  • 26本

コメント

コメントするには、 ログイン または 会員登録 をお願いします。
テレビ録画メディアサーバー構築入門(第9回)|takya
word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word

mmMwWLliI0fiflO&1
mmMwWLliI0fiflO&1
mmMwWLliI0fiflO&1
mmMwWLliI0fiflO&1
mmMwWLliI0fiflO&1
mmMwWLliI0fiflO&1
mmMwWLliI0fiflO&1