IPTV - もう一度

(2025-07-20)

今日は参院選挙。投票はしますが、あまりにも暑くてまだ投票していません。

涼しい場所で涼みながらすることもないので、chatGPT と IPTV をもう一度挑戦してみました。

プレイリスト

あるサイトのプレイリストは、

=== 2025-07-20 03:10:16 UTC ===
2025/07/20/03/09/56-01001.ts
2025/07/20/03/09/57-01001.ts
2025/07/20/03/09/58-01001.ts
2025/07/20/03/09/59-01001.ts

4 つしか表示されていません。

これまでは、56-01001.ts をダウンロードする python を作成して cron で 10 秒ごとに実行していましたが、それだとある瞬間から保存が止まってしまうことに気づきました。

ログを確認すると

🟡 Skip (already exists): 03-01001.ts
🟡 Skip (already exists): 04-01001.ts
🟡 Skip (already exists): 05-01001.ts
🟡 Skip (already exists): 06-01001.ts

すでに ts ファイルが存在しているのでスキップされているようです。 ちょっと調べて、ファイル名を使いまわしていることがわかりました。

そうして、数秒間隔でプレイリストを更新してるのだと思います。

ファイル名変更

ts ファイル名そのものは重複していても、その前の日付・時間と思われる文字列まで含めると、おそらくユニークになると思いました。

つまり、

2025/07/20/03/09/56-01001.ts

この文字列はユニークだと思うので、この文字列を使って以下のようなファイル名で temp フォルダに保存します。

2025_07_20_03_09_56-01001.ts

cron で数秒ごとに python を実行するのでプレイリストに同じファイルがあった場合スキップするようにして、なかった場合は保存するようにしました。

さらに、ts ファイルをタイムスタンプで rename して ts フォルダに保存しました。

python

python は以下のようになっています。

import os
import requests
from urllib.parse import urljoin
from datetime import datetime
import time
# 保存先フォルダ
base_dir = "/iptv/BS4K"
temp_dir = os.path.join(base_dir, "temp")
os.makedirs(temp_dir, exist_ok=True)
# マスタープレイリストURL
master_url = "http://heno/index.m3u8"
try:
master_res = requests.get(master_url, timeout=5)
master_res.raise_for_status()
master_lines = master_res.text.strip().splitlines()
media_relative = next(line for line in master_lines if line and not line.startswith("#"))
media_url = urljoin(master_url, media_relative)
media_res = requests.get(media_url, timeout=5)
media_res.raise_for_status()
media_lines = media_res.text.strip().splitlines()
for line in media_lines:
if line.startswith("#") or not line.strip():
continue
ts_path_relative = line.strip()
ts_url = urljoin(media_url, ts_path_relative)
# 例: 2025/07/20/00/25/52-05005.ts → 2025_07_20_00_25_52-05005.ts
parts = ts_path_relative.replace(".ts", "").split("/")
renamed = "_".join(parts) + ".ts"
temp_path = os.path.join(temp_dir, renamed)
# tempにすでにあればスキップ
if os.path.exists(temp_path):
print(f" Skip: {renamed}")
continue
# tsファイルダウンロード
print(f" Downloading: {renamed}")
ts_res = requests.get(ts_url, timeout=5)
ts_res.raise_for_status()
# tempに保存
with open(temp_path, "wb") as f:
f.write(ts_res.content)
# 保存用ファイル名(実行時刻ベース)
timestamp_name = datetime.utcnow().strftime("%Y%m%d_%H%M%S") + ".ts"
save_path = os.path.join(ts_dir, timestamp_name)
print(f" saved: {timestamp_name}")
except Exception as e:
print(f" Error: {e}")

cron で実行

サイトによっては、ts ファイルは 1 秒間・プレイリストは 4 つなので、4 秒以下で python を実行する必要があります。

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

ffmpeg で merge

ffmpeg で結合します。

#!/bin/bash
TS_DIR="./ts"
LIST_FILE="./list.txt"
# list.txt を作成(ファイル名の昇順で)
ls -1v ./ts/*.ts | awk '{printf "file '\''%s'\''\n", $0}' > ./list.txt
# ffmpeg で結合して mp4 に変換
ffmpeg -f concat -safe 0 -i list.txt -c copy ./mp4/`date +%Y%m%d%H%M%S_`.mp4

一応それなりに見られますが、通信障害でフリーズするとその分のコマ落ちが発生します。

改善

かなりの部分が脱落してしまう動画はほとんど無意味なので、もう一度 chatGPT と相談して以下のようにしてみました。

import requests
import os
from datetime import datetime, timezone
M3U8_URL = "http://heno/mono.m3u8"
BASE_URL = "http://moheno/"
SAVE_DIR = "iptv/SunArt/ts_files"
LOG_FILE = "failed_downloads.log"
os.makedirs(SAVE_DIR, exist_ok=True)
def log_failure(ts_name):
with open(LOG_FILE, "a") as log:
now = datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M:%S")
log.write(f"[{now}] Failed: {ts_name}\n")
def fetch_and_save_ts(ts_path):
renamed = ts_path.replace("/", "_")
save_path = os.path.join(SAVE_DIR, renamed)
# 既に保存済みならスキップ
if os.path.exists(save_path):
return
url = BASE_URL + ts_path
try:
response = requests.get(url, timeout=10)
response.raise_for_status()
with open(save_path, "wb") as f:
f.write(response.content)
print(f"Saved: {renamed}")
except Exception as e:
print(f"Error downloading {renamed}: {e}")
log_failure(ts_path)
def main():
try:
response = requests.get(M3U8_URL, timeout=5)
response.raise_for_status()
lines = response.text.strip().splitlines()
ts_list = [line for line in lines if line.endswith(".ts")]
for ts in ts_list:
fetch_and_save_ts(ts)
except Exception as e:
print(f"Failed to fetch m3u8: {e}")
if __name__ == "__main__":
main()