複数RSSで過去3日分を古い順に再生できるようになりまいした(フェーズ5C完了)。
*NHKの番組を再生している部分はカットしました。
フェーズ5C 詳細マニュアル
複数RSSから「過去3日分・古い順」のプレイリストを作る
0. 今回の目標
5Cでやることは、これです。
複数のRSS URLを feeds.txt に書く
↓
PythonでRSSを順番に読む
↓
各RSSからエピソードを集める
↓
過去3日分だけに絞る
↓
公開日時の古い順に並べる
↓
playlist.m3u を作る
↓
mpvで連続再生する
今回も、まだやらないことがあります。
棚選び
再生モード選び
LED表示
追加ボタン
ロータリーエンコーダー
再生履歴
ここ、大事です。
5Cは「複数RSS化だけ」です。まだラジオの操作部品は増やしません。焦らないのが勝ち筋です。
ステップ1:作業フォルダへ移動する
Raspberry Piでターミナルを開きます。
cd ~/tanaradio5
確認します。
pwd
次のような表示ならOKです。
/home/pi/tanaradio5
ユーザー名が違う場合は、pi の部分は別名になります。
ステップ2:5Bまでのファイルを確認する
ls
おそらく、次のようなファイルがあるはずです。
feed.txt
make_playlist_5a.py
make_playlist_5b.py
playlist.m3u
5Cでは、5Bのファイルを壊さず、新しく次の2つを作ります。
feeds.txt
make_playlist_5c.py
5Bはそのまま残します。
動いたものは残す。これは電子工作でもプログラムでも鉄則です。動いたものを消すと、あとで泣きます。小さく泣くならまだしも、Linux相手だとわりと本気で泣きます。
ステップ3:複数RSS用の feeds.txt を作る
まず、RSS URLを複数書くためのファイルを作ります。
nano feeds.txt
中には、RSS URLを1行に1本ずつ書きます。
例です。
https://listen.style/p/xxxxx/rss
https://listen.style/p/yyyyy/rss
https://listen.style/p/zzzzz/rss
最初は欲張らず、まずは2本でよいです。
https://listen.style/p/xxxxx/rss
https://listen.style/p/yyyyy/rss
保存します。
Ctrl + O
Enter
Ctrl + X
確認します。
cat feeds.txt
RSS URLが複数行で表示されればOKです。
ステップ4:5C用のPythonスクリプトを作る
新しいファイルを作ります。
nano make_playlist_5c.py
次のコードをそのまま貼り付けてください。
#!/usr/bin/env python3
import urllib.request
import xml.etree.ElementTree as ET
from pathlib import Path
from datetime import datetime, timedelta, timezone
from email.utils import parsedate_to_datetime
from zoneinfo import ZoneInfo
FEEDS_FILE = "feeds.txt"
PLAYLIST_FILE = "playlist.m3u"
# 5Bと同じく「過去3日分」
DAYS = 3
JST = ZoneInfo("Asia/Tokyo")
def read_feed_urls():
path = Path(FEEDS_FILE)
if not path.exists():
raise FileNotFoundError(f"{FEEDS_FILE} が見つかりません。")
urls = []
for line in path.read_text(encoding="utf-8").splitlines():
line = line.strip()
# 空行と # で始まるコメント行は無視する
if not line:
continue
if line.startswith("#"):
continue
urls.append(line)
if not urls:
raise ValueError(f"{FEEDS_FILE} にRSS URLが書かれていません。")
return urls
def download_rss(url):
print(f"RSSを取得します: {url}")
request = urllib.request.Request(
url,
headers={
"User-Agent": "TanaRadio-Pi/5C"
}
)
with urllib.request.urlopen(request, timeout=20) as response:
return response.read()
def get_text(element, tag_name):
child = element.find(tag_name)
if child is not None and child.text:
return child.text.strip()
return ""
def find_audio_url(item):
# 通常のPodcast RSSでは enclosure に音声URLが入る
enclosure = item.find("enclosure")
if enclosure is not None:
audio_url = enclosure.attrib.get("url")
if audio_url:
return audio_url
# 念のため media:content 的な形式にも軽く対応する
for child in item:
if child.tag.endswith("content"):
audio_url = child.attrib.get("url")
if audio_url:
return audio_url
return None
def parse_pub_date(item):
pub_date_text = get_text(item, "pubDate")
if not pub_date_text:
return None
try:
dt = parsedate_to_datetime(pub_date_text)
# タイムゾーン情報がない場合はUTC扱いにする
if dt.tzinfo is None:
dt = dt.replace(tzinfo=timezone.utc)
return dt.astimezone(JST)
except Exception:
return None
def parse_rss(rss_data, feed_url):
root = ET.fromstring(rss_data)
channel = root.find("channel")
if channel is None:
raise ValueError("RSSのchannelが見つかりません。")
program_title = get_text(channel, "title")
if not program_title:
program_title = "番組名不明"
items = channel.findall("item")
episodes = []
for item in items:
episode_title = get_text(item, "title")
audio_url = find_audio_url(item)
pub_dt = parse_pub_date(item)
if not audio_url:
continue
if pub_dt is None:
continue
episodes.append({
"program_title": program_title,
"episode_title": episode_title,
"audio_url": audio_url,
"pub_dt": pub_dt,
"feed_url": feed_url,
})
return episodes
def collect_episodes(feed_urls):
all_episodes = []
for url in feed_urls:
try:
rss_data = download_rss(url)
episodes = parse_rss(rss_data, url)
all_episodes.extend(episodes)
print(f" 取得できたエピソード数: {len(episodes)} 件")
except Exception as e:
print(" このRSSの取得または解析でエラーが発生しました。")
print(f" {e}")
print(" 次のRSSへ進みます。")
return all_episodes
def filter_recent_episodes(episodes):
now = datetime.now(JST)
cutoff = now - timedelta(days=DAYS)
recent = []
for ep in episodes:
if ep["pub_dt"] >= cutoff:
recent.append(ep)
return recent
def remove_duplicates(episodes):
seen = set()
unique = []
for ep in episodes:
audio_url = ep["audio_url"]
if audio_url in seen:
continue
seen.add(audio_url)
unique.append(ep)
return unique
def make_playlist(episodes):
if not episodes:
raise ValueError(
f"過去{DAYS}日分のエピソードが見つかりませんでした。"
)
# 古い順に並べる
episodes.sort(key=lambda ep: ep["pub_dt"])
playlist_lines = ["#EXTM3U"]
for ep in episodes:
pub_text = ep["pub_dt"].strftime("%Y-%m-%d %H:%M")
program = ep["program_title"]
title = ep["episode_title"]
url = ep["audio_url"]
playlist_lines.append(f"# {pub_text} / {program} / {title}")
playlist_lines.append(url)
Path(PLAYLIST_FILE).write_text(
"\n".join(playlist_lines) + "\n",
encoding="utf-8"
)
print(f"{PLAYLIST_FILE} を作成しました。")
print(f"プレイリスト内のエピソード数: {len(episodes)} 件")
def main():
try:
feed_urls = read_feed_urls()
print(f"RSS URL数: {len(feed_urls)} 本")
episodes = collect_episodes(feed_urls)
print(f"全取得エピソード数: {len(episodes)} 件")
episodes = remove_duplicates(episodes)
print(f"重複除去後: {len(episodes)} 件")
episodes = filter_recent_episodes(episodes)
print(f"過去{DAYS}日分: {len(episodes)} 件")
make_playlist(episodes)
print("完了しました。")
except Exception as e:
print("エラーが発生しました。")
print(e)
if __name__ == "__main__":
main()
保存します。
Ctrl + O
Enter
Ctrl + X
ステップ5:実行権限をつける
chmod +x make_playlist_5c.py
ただし、実行はまず次の形でやるのがおすすめです。
python3 make_playlist_5c.py
以前のように、Pythonファイルをシェルとして実行してしまう事故を避けるためです。
python3 を前につければ、確実にPythonとして実行されます。
ステップ6:プレイリストを作る
実行します。
python3 make_playlist_5c.py
うまくいくと、次のような表示になります。
RSS URL数: 2 本
RSSを取得します: https://listen.style/p/xxxxx/rss
取得できたエピソード数: 10 件
RSSを取得します: https://listen.style/p/yyyyy/rss
取得できたエピソード数: 10 件
全取得エピソード数: 20 件
重複除去後: 20 件
過去3日分: 5 件
playlist.m3u を作成しました。
プレイリスト内のエピソード数: 5 件
完了しました。
ここで見るポイントは、次の3つです。
RSS URL数
過去3日分
プレイリスト内のエピソード数
プレイリスト内のエピソード数 が1件以上なら成功です。
ステップ7:playlist.m3u の中身を確認する
head -n 30 playlist.m3u
次のように表示されればOKです。
#EXTM3U
# 2026-07-01 / 番組A / エピソードタイトル
https://...
# 2026-07-01 / 番組B / エピソードタイトル
https://...
ここで確認したいのは、複数番組が混ざっているかどうかです。
たとえば、
番組A
番組B
番組A
番組C
のように並んでいれば、5Cらしい動きになっています。
単にRSSを順番に再生しているのではなく、複数RSSのエピソードを集めて、公開日時で並べ替えているわけです。
ここが5Cの肝です。
ステップ8:mpvで再生する
mpv playlist.m3u
音が出れば成功です。
mpvの基本操作はこれです。
スペースキー:一時停止/再開
Enter:次のエピソードへ
q:終了
再生される順番が、過去3日分の古い順になっているか、耳でざっくり確認してください。
厳密に確認したい場合は、playlist.m3u のコメント行を見ます。
grep '^# 20' playlist.m3u
次のように日時が古い順に並んでいればOKです。
# 2026-06-29 / ...
# 2026-06-30 / ...
# 2026-07-01 / ...
ステップ9:RSSを少しずつ増やす
最初の2本でうまく行ったら、feeds.txt にRSSを追加します。
nano feeds.txt
例です。
# TanaRadio
https://listen.style/p/xxxxx/rss
# 気になる番組1
https://listen.style/p/yyyyy/rss
# 気になる番組2
https://listen.style/p/zzzzz/rss
このスクリプトでは、空行と # で始まる行は無視されます。
なので、コメントを書いておけます。
保存したら再実行します。
python3 make_playlist_5c.py
mpv playlist.m3u
RSSを増やすと、かなり「フォロー中ラジオ」感が出てきます。
この段階でもう、TanaRadio Piは単なる1番組再生機ではなくなります。
ステップ10:過去3日分が空だった場合
次のように出ることがあります。
過去3日分: 0 件
エラーが発生しました。
過去3日分のエピソードが見つかりませんでした。
これは故障ではありません。
原因は単純で、登録したRSSに最近のエピソードがないだけです。
その場合は、テスト用に日数を一時的に増やします。
nano make_playlist_5c.py
この部分を探します。
DAYS = 3
一時的にこうします。
DAYS = 30
保存して、再実行します。
python3 make_playlist_5c.py
これでエピソードが出てくるなら、スクリプトは正常です。
テストが終わったら、また戻します。
DAYS = 3
ステップ11:成功条件
フェーズ5Cの成功条件は、次の4つです。
1. feeds.txt にRSS URLを複数本書けた
2. make_playlist_5c.py を実行できた
3. 複数RSS由来のエピソードが playlist.m3u に入った
4. mpv playlist.m3u で連続再生できた
特に重要なのは3です。
複数RSS
↓
1つのプレイリスト
↓
過去3日分
↓
古い順
これができれば、5Cは成功です。
5Cでできるようになったこと
5Cが成功すると、TanaRadio Piはこうなります。
1つの番組を聴く装置
から、
複数番組をまとめて流す装置
に変わります。
これは地味ですが、かなり大きな変化です。
もう「ポッドキャストを1本ずつ選んで聴く」のではなく、「登録してある声が時間順に流れてくる」状態になります。
ここで、かなりラジオらしくなります。
次のフェーズ5Dでやること
次の5Dでは、フェーズ4で作った既存の3ボタンを、この新しい playlist.m3u 再生に結びつけます。
予定としては、まず単純にこうします。
再生/一時停止ボタン:pause切り替え
戻るボタン:前のエピソードへ
次へボタン:次のエピソードへ
構想メモでは戻るボタンに「1回押し・2回押し・長押し」の区別も出ていますが、5Dの最初ではまだ入れなくてよいです。
まずは「物理ボタンでプレイリストを前後できる」だけで十分です。
今回の5Cは、ひとことで言えばこうです。
複数の声を、1本の時間の流れにする。
ここまで来ると、TanaRadio Piはかなり「声の本棚」の入口に立っています。これは順調です。
12:13
コメント
スクロール