NAS cron 設定の自動化

(2025-10-17)

NAS で iptv 録画をしてますが、cron 設定はかなり面倒です。

ローカルのシェルスクリプトで NAS の cron 設定をしてますが、手動で行っています。

これはちょっと面倒で、時々混乱しておかしな設定をしてしまうことがあります。

以前から時間があったら python などを使って自動化したいと思っていましたが、chatGPT が優秀なので依頼してみました。

cron

cron はいわばタイムスケジューラで、とても便利なものです。

windows にもこの機能はありますが linux の方が遥かに細かい設定が可能です。

例えば、現在 NAS に実行させている cron は、

* 19 * * 0 moheno for i in `seq 0 3 59`;do (sleep ${i}; python /volume1/bak/iptv/python/moheno.py >> /volume1/bak/iptv/moheno/log/cron.log 2>&1 ) & done;

こんな感じです。

これは、日曜日の 19 時台に、ある python プログラムを 3 秒おきに実行して、そのログをあるディレクトリに記録するものです。

このような内容のテキストファイルを作成して、NAS へアップロードして cron 設定していました。

アップするのはシェルスクリプトで、

#!/bin/sh
scp -O -P 12345 ./crontab_backup.txt moheno@192.168.0.20:/volume1/bak/iptv
# SSH接続
ssh -p 12345 moheno@192.168.0.20 <<EOF
cd /volume1/bak/iptv
sudo sh ./editcron.sh
EOF

こんな感じでした。

これは、ローカルで作成した crontab_backup.txt を NAS に送って、その情報を元にして NAS の cron 設定をローカルからおこなうものです。

editcron.sh は NAS 上にあり、

#!/bin/bash
cat /volume1/bak/iptv/crontab_backup.txt | sudo tee /etc/crontab > /dev/null

これ、ほとんど理解できませんが、ローカルからこのシェルスクリプトを動かすと、NAS の cron 設定がおこなわれます。

でも、これらの処理を手動でおこなうのは面倒でもあり、また複雑なのでエラーが混入する可能性もあります。

そこで、これらの処理を python で自動化できないかと chatGPT に質問してみました。

python による自動化

chatGPT はとても優秀で、自分で作ると最低数日はかかるプログラムをあっという間に作ってくれます。

まずは、出来上がりのイメージから。

あるシェルスクリプトを実行すると、tkinter による入力フォームが表示されます。

チャンネルを選んで開始時刻と終了時刻を入力すると、cron_backup.txt の temporary 領域に cron 設定文が追記されます。

例えば、10 月 17 日の 10:00 から 13:30 まで設定すると、以下のような cron 設定文が追記されます。

# ----------------------------------- temporary
# Italy
0-59 10 17 10 * moheno for i in `seq 0 3 59`;do (sleep ${i}; python /volume1/bak/iptv/python/Italy.py >> /volume1/bak/iptv/Italy/log/cron.log 2>&1 ) & done;
* 11 17 10 * moheno for i in `seq 0 3 59`;do (sleep ${i}; python /volume1/bak/iptv/python/Italy.py >> /volume1/bak/iptv/Italy/log/cron.log 2>&1 ) & done;
* 12 17 10 * moheno for i in `seq 0 3 59`;do (sleep ${i}; python /volume1/bak/iptv/python/Italy.py >> /volume1/bak/iptv/Italy/log/cron.log 2>&1 ) & done;
0-30 13 17 10 * moheno for i in `seq 0 3 59`;do (sleep ${i}; python /volume1/bak/iptv/python/Italy.py >> /volume1/bak/iptv/Italy/log/cron.log 2>&1 ) & done;
#----------------------------------------------

これは、これまでは以下のようなテキストを手動で書いていました。

# ----------------------------------- temporary
# Italy
* 10-12 17 10 * moheno for i in `seq 0 3 59`;do (sleep ${i}; python /volume1/bak/iptv/python/Italy.py >> /volume1/bak/iptv/Italy/log/cron.log 2>&1 ) &
0-30 13 17 10 * moheno for i in `seq 0 3 59`;do (sleep ${i}; python /volume1/bak/iptv/python/Italy.py >> /volume1/bak/iptv/Italy/log/cron.log 2>&1 ) & done;
#----------------------------------------------

でも、最初の cron 設定は間違いではないのできちんと実行されます。

python を実行すると cron_backup.txt を編集して、NAS にアップロードして、NAS 上のシェルスクリプトを実行するまで完全自動化することができます。

いきなりですが、chatGPT が作ってくれた python です。

#!/usr/bin/env python
import tkinter as tk
from tkinter import ttk, messagebox
from tkcalendar import DateEntry
from pathlib import Path
from datetime import datetime, timedelta
import paramiko
from scp import SCPClient
import tkinter.font as tkFont
# ---------------- Cron管理 ----------------
class CronFileManager:
def __init__(self, base_file="/home/moheno/iptv/AAA/NAS720/crontab_backup.txt",
interval=3, user="moheno"):
self.base_file = Path(base_file)
self.interval = interval
self.user = user
self.start_marker = "# ----------------------------------- temporary"
self.end_marker = "#----------------------------------------------"
def generate_lines(self, channel_name, script_name, start_dt, end_dt):
base_cmd = (
f"{self.user} for i in `seq 0 {self.interval} 59`;do (sleep ${{i}}; "
f"python /volume1/bak/iptv/python/{script_name} >> "
f"/volume1/bak/iptv/{channel_name}/log/cron.log 2>&1 ) & done;"
)
lines = [f"# {channel_name}"]
current_dt = start_dt
while True:
if current_dt.date() == end_dt.date():
end_hour = end_dt.hour
end_min = end_dt.minute
else:
end_hour = 23
end_min = 59
start_hour = current_dt.hour
start_min = current_dt.minute
day = current_dt.day
month = current_dt.month
if start_hour == end_hour:
lines.append(f"{start_min}-{end_min} {start_hour} {day} {month} * {base_cmd}")
else:
lines.append(f"{start_min}-59 {start_hour} {day} {month} * {base_cmd}")
for hour in range(start_hour + 1, end_hour):
lines.append(f"* {hour} {day} {month} * {base_cmd}")
lines.append(f"0-{end_min} {end_hour} {day} {month} * {base_cmd}")
if current_dt.date() == end_dt.date():
break
current_dt += timedelta(days=1)
current_dt = current_dt.replace(hour=0, minute=0)
return lines
def append_to_temporary(self, channel_name, script_name, start_dt, end_dt):
if not self.base_file.exists():
raise FileNotFoundError(f"ベースファイルが存在しません: {self.base_file}")
lines = self.base_file.read_text().splitlines()
start_index = end_index = None
temp_buffer = []
for idx, line in enumerate(lines):
if self.start_marker in line:
start_index = idx
elif self.end_marker in line:
end_index = idx
if start_index is not None and end_index is None:
temp_buffer.append(line)
if start_index is None or end_index is None:
raise ValueError("temporary セクションがベースファイル内に見つかりません。")
temp_buffer.append("")
temp_buffer += self.generate_lines(channel_name, script_name, start_dt, end_dt)
new_lines = lines[:start_index] + temp_buffer + lines[end_index:]
self.base_file.write_text("\n".join(new_lines) + "\n")
return self.base_file
# ---------------- NAS転送 & SSH ----------------
class NASUploader:
def __init__(self, host, port, username, password=None, key_filename=None):
self.host = host
self.port = port
self.username = username
self.password = password
self.key_filename = key_filename
def upload_and_execute(self, local_file, remote_path, remote_command):
ssh = paramiko.SSHClient()
ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
ssh.connect(hostname=self.host, port=self.port, username=self.username,
password=self.password, key_filename=self.key_filename)
with SCPClient(ssh.get_transport()) as scp:
scp.put(local_file, remote_path)
stdin, stdout, stderr = ssh.exec_command(remote_command)
out = stdout.read().decode()
err = stderr.read().decode()
ssh.close()
return out, err
# ---------------- CSV読み込み(1カラム) ----------------
def load_channels_from_csv(csv_path):
channels = []
with open(csv_path, newline='', encoding='utf-8') as f:
for line in f:
name = line.strip()
if name:
channels.append((name, f"{name}.py"))
return channels
# ---------------- GUI ----------------
class CronGUI:
def __init__(self, root):
# フォント設定
default_font = tkFont.Font(family="Helvetica", size=14)
root.option_add("*Font", default_font)
# CSVファイルパス固定
csv_path = "/home/moheno/iptv/AAA/channels.csv"
self.channels = load_channels_from_csv(csv_path)
self.manager = CronFileManager()
self.nas = NASUploader(
host="192.168.0.20",
port=12345,
username="moheno",
password="YOUR_PASSWORD"
)
self.remote_path = "/volume1/bak/iptv/crontab_backup.txt"
self.remote_command = "cd /volume1/bak/iptv && sudo sh ./editcron.sh"
self.root = root
self.root.title("NAS720 Cron Updater + NAS転送")
self.root.geometry("800x450")
# root全体を縦横中央寄せ
self.root.grid_rowconfigure(0, weight=1)
self.root.grid_columnconfigure(0, weight=1)
self.create_widgets()
def create_widgets(self):
padx_label = 10
pady_label = 8
padx_entry = 5
pady_entry = 5
ipady_val = 3 # 垂直中央寄せ用の内部パディング
# メインフレームで全体中央に配置
main_frame = tk.Frame(self.root)
main_frame.grid(row=0, column=0, sticky="nsew")
main_frame.grid_columnconfigure(0, weight=1)
for i in range(5): # 5行想定で均等に伸ばす
main_frame.grid_rowconfigure(i, weight=1)
# チャンネル
tk.Label(main_frame, text="チャンネル:").grid(row=0, column=0, padx=padx_label, pady=pady_label, sticky="e")
self.channel_var = tk.StringVar()
channel_names = [c[0] for c in self.channels]
self.channel_box = ttk.Combobox(main_frame, textvariable=self.channel_var,
values=channel_names, width=30, justify="center")
self.channel_box.current(0)
self.channel_box.grid(row=0, column=1, columnspan=4, padx=padx_entry, pady=pady_entry, ipady=ipady_val)
# 開始フレーム
frame_start = tk.LabelFrame(main_frame, text="開始日時", padx=10, pady=5)
frame_start.grid(row=1, column=0, columnspan=5, padx=10, pady=5, sticky="nsew")
for i in range(5):
frame_start.grid_columnconfigure(i, weight=1)
tk.Label(frame_start, text="日付:").grid(row=0, column=0, padx=5, pady=5)
self.start_date = DateEntry(frame_start, width=12, justify="center", background='darkblue',
foreground='white', borderwidth=2, date_pattern='yyyy-mm-dd')
self.start_date.grid(row=0, column=1, padx=5, pady=5, ipady=ipady_val)
tk.Label(frame_start, text="時刻:").grid(row=0, column=2, padx=5, pady=5)
self.start_hour = ttk.Combobox(frame_start, values=[f"{i:02d}" for i in range(24)], width=5, justify="center")
self.start_hour.set("10")
self.start_hour.grid(row=0, column=3, padx=2, pady=5, ipady=ipady_val)
self.start_min = ttk.Combobox(frame_start, values=[f"{i:02d}" for i in range(60)], width=5, justify="center")
self.start_min.set("00")
self.start_min.grid(row=0, column=4, padx=2, pady=5, ipady=ipady_val)
# 終了フレーム
frame_end = tk.LabelFrame(main_frame, text="終了日時", padx=10, pady=5)
frame_end.grid(row=2, column=0, columnspan=5, padx=10, pady=5, sticky="nsew")
for i in range(5):
frame_end.grid_columnconfigure(i, weight=1)
tk.Label(frame_end, text="日付:").grid(row=0, column=0, padx=5, pady=5)
self.end_date = DateEntry(frame_end, width=12, justify="center", background='darkblue',
foreground='white', borderwidth=2, date_pattern='yyyy-mm-dd')
self.end_date.grid(row=0, column=1, padx=5, pady=5, ipady=ipady_val)
tk.Label(frame_end, text="時刻:").grid(row=0, column=2, padx=5, pady=5)
self.end_hour = ttk.Combobox(frame_end, values=[f"{i:02d}" for i in range(24)], width=5, justify="center")
self.end_hour.set("11")
self.end_hour.grid(row=0, column=3, padx=2, pady=5, ipady=ipady_val)
self.end_min = ttk.Combobox(frame_end, values=[f"{i:02d}" for i in range(60)], width=5, justify="center")
self.end_min.set("30")
self.end_min.grid(row=0, column=4, padx=2, pady=5, ipady=ipady_val)
# 実行ボタン
tk.Button(
main_frame,
text="temporaryに追記 + NAS転送実行",
command=self.on_append_and_upload,
bg="#4CAF50",
fg="white",
font=("Helvetica", 13, "bold")
).grid(row=3, column=0, columnspan=5, pady=20, padx=10, sticky="ew")
tk.Label(main_frame, text=f"ベースファイル: {self.manager.base_file}").grid(
row=4, column=0, columnspan=5, pady=5, sticky="w"
)
def on_append_and_upload(self):
try:
channel_name = self.channel_var.get()
script_name = next(s[1] for s in self.channels if s[0] == channel_name)
start_date_val = self.start_date.get_date()
end_date_val = self.end_date.get_date()
start_hour = int(self.start_hour.get())
start_min = int(self.start_min.get())
end_hour = int(self.end_hour.get())
end_min = int(self.end_min.get())
start_dt = datetime(year=start_date_val.year, month=start_date_val.month, day=start_date_val.day,
hour=start_hour, minute=start_min)
end_dt = datetime(year=end_date_val.year, month=end_date_val.month, day=end_date_val.day,
hour=end_hour, minute=end_min)
updated_path = self.manager.append_to_temporary(channel_name, script_name, start_dt, end_dt)
out, err = self.nas.upload_and_execute(
local_file=str(updated_path),
remote_path=self.remote_path,
remote_command=self.remote_command
)
messagebox.showinfo("完了",
f"temporary部分に追記しました。\nベースファイル更新済み。\n\n"
f"SCP + editcron.sh 実行完了\n\nSTDOUT:\n{out}\nSTDERR:\n{err}")
except Exception as e:
messagebox.showerror("エラー", str(e))
if __name__ == "__main__":
root = tk.Tk()
app = CronGUI(root)
root.mainloop()

このプログラムは 360 行を超えるもので、これを自分で作成しようと思うと最低 1 週間はかかると思います。
いや、今となってはもうできないかもしれません。

chatGPT にプログラム作成する際のコツ

chatGPT と一緒にいろいろとプログラムを作ってきましたが、挫折する場合とうまく行く場合の違いがやっと少しつかめてきたように思います。

今回、ほとんど大きな混乱もなく比較的短時間でかなり大きなプログラムができたのは、簡単なものから始めて少しずつ追加してきたためだと思います。

しかも、chatGPT に依頼する内容はかなり具体的にお願いしました。そのおかげで短時間で大きなプログラムをうまく作って貰えたと思います。

今回のプロセスは、

  1. まず、cron のテキストを編集できるか
  2. cron を temporary の中に追記できるか
  3. cron 編集を GUI で
  4. GUI に日付カレンダーを導入
  5. 数時間単位の場合、開始と終了の間の設定
  6. 開始と終了は日付をまたぐことができるか
  7. NAS への scp とシェルスクリプトの実行
  8. チャンネルは csv から読み込む
  9. GUI のアラインメントの改善

テーマを限定して具体的に依頼すると、とても迅速に答えを返してくれます。

AI に依頼することの危うさ

素晴らしいプログラムを作ってもらいましたが、私がいろいろチャットしているのはフリーの chatGPT 5 です。

有料の方は chat GPT 5 plus。

chatGPT に依頼すると非常に便利なのですが、自分で作成していないので編集が少し面倒ではあります。

それと、やはり自分で考えることが少なくなってきたように思います。
効率はいいのですが、やはり自分で考えることは大切だと感じています。

それと、現在はフリーの chatGPT とチャットしてますが、あまりにも便利なのである日突然に有料化しますと言われたら断れないようにも思います。