あるポッドキャストの過去3日分を古い順に再生できるようになりました(フェーズ5B完了)。
フェーズ5B 詳細マニュアル
過去3日分だけを古い順に並べる
0. 今回の目標
5Aでは、RSSから取得したエピソードをそのまま playlist.m3u にしました。
5Bでは、そこに次の条件を加えます。
過去3日分だけを対象にする
↓
古いエピソードから新しいエピソードへ並べる
↓
mpvで連続再生する
つまり、こういう再生になります。
3日前のエピソード
↓
2日前のエピソード
↓
昨日のエピソード
↓
今日のエピソード
これは「最新からつまみ食い」ではなく、「数日分を朝のラジオのように順番に追いつく」ためのモードです。構想メモでも、この「3日前から古い順」は追いつき再生として位置づけられています。
今回はまだ、以下はやりません。
複数RSS
棚選び
ボタン追加
LED表示
ロータリーエンコーダー
再生履歴
ここ、欲張らなくて正解です。5Bは地味ですが、ここで「時間順に声を流す」感覚が出てきます。ラジオらしさが一段増します。
ステップ1:作業フォルダに移動する
Raspberry Piでターミナルを開きます。
cd ~/tanaradio5
確認します。
pwd
ls
次のようなファイルが見えればOKです。
feed.txt
make_playlist_5a.py
playlist.m3u
5Aが成功しているので、たぶんこの状態になっているはずです。
ステップ2:5Aのファイルは残しておく
5Aのスクリプトは成功版として残します。
今回は新しく、
make_playlist_5b.py
を作ります。
5Aを上書きしない方が安全です。工作でもプログラムでも、成功した状態を残すのは大事です。ここを雑にすると、あとで「昨日の自分、何をした?」となります。昨日の自分はだいたいメモを残していません。困ったものです。
ステップ3:5B用スクリプトを作る
次を入力します。
nano make_playlist_5b.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
FEED_FILE = "feed.txt"
PLAYLIST_FILE = "playlist_5b.m3u"
INFO_FILE = "playlist_5b_info.txt"
# 5Bでは「過去3日分」を対象にする
DAYS_BACK = 3
# 表示用。日本時間で確認できるようにする
JST = timezone(timedelta(hours=9))
def read_feed_url():
path = Path(FEED_FILE)
if not path.exists():
raise FileNotFoundError(f"{FEED_FILE} が見つかりません。")
url = path.read_text(encoding="utf-8").strip()
if not url:
raise ValueError(f"{FEED_FILE} にRSS URLが書かれていません。")
return url
def download_rss(url):
print(f"RSSを取得します: {url}")
request = urllib.request.Request(
url,
headers={
"User-Agent": "TanaRadio-Pi/5B"
}
)
with urllib.request.urlopen(request, timeout=20) as response:
return response.read()
def find_audio_url(item):
# 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 get_text(item, tag_name):
element = item.find(tag_name)
if element is not None and element.text:
return element.text.strip()
return ""
def parse_pub_date(item):
text = get_text(item, "pubDate")
if not text:
return None
# RSSのpubDateは多くの場合、次のような形式
# Tue, 30 Jun 2026 08:00:00 +0900
# Tue, 30 Jun 2026 00:00:00 GMT
try:
dt = parsedate_to_datetime(text)
except Exception:
return None
# タイムゾーン情報がない場合はUTCとして扱う
if dt.tzinfo is None:
dt = dt.replace(tzinfo=timezone.utc)
# 比較しやすいようにUTCへ変換
return dt.astimezone(timezone.utc)
def format_dt_jst(dt):
return dt.astimezone(JST).strftime("%Y-%m-%d %H:%M")
def collect_episodes(rss_data):
root = ET.fromstring(rss_data)
channel = root.find("channel")
if channel is None:
raise ValueError("RSSのchannelが見つかりません。")
items = channel.findall("item")
if not items:
raise ValueError("RSS内にitemが見つかりません。")
episodes = []
skipped_no_audio = 0
skipped_no_date = 0
for item in items:
title = get_text(item, "title")
audio_url = find_audio_url(item)
pub_date = parse_pub_date(item)
if not audio_url:
skipped_no_audio += 1
continue
if pub_date is None:
skipped_no_date += 1
continue
episodes.append({
"title": title if title else "タイトルなし",
"audio_url": audio_url,
"pub_date": pub_date,
})
return episodes, skipped_no_audio, skipped_no_date
def make_playlist(episodes, skipped_no_audio, skipped_no_date):
now = datetime.now(timezone.utc)
cutoff = now - timedelta(days=DAYS_BACK)
selected = [
episode for episode in episodes
if episode["pub_date"] >= cutoff
]
# 古い順に並べる
selected.sort(key=lambda episode: episode["pub_date"])
info_lines = []
info_lines.append("TanaRadio Pi フェーズ5B プレイリスト情報")
info_lines.append("")
info_lines.append(f"対象期間: 過去{DAYS_BACK}日分")
info_lines.append(f"現在時刻: {format_dt_jst(now)} JST")
info_lines.append(f"対象開始: {format_dt_jst(cutoff)} JST")
info_lines.append("")
info_lines.append(f"RSSから取得した音声付きエピソード数: {len(episodes)} 件")
info_lines.append(f"音声URLなしでスキップ: {skipped_no_audio} 件")
info_lines.append(f"公開日時なし/日時解析失敗でスキップ: {skipped_no_date} 件")
info_lines.append(f"今回プレイリストに入れる数: {len(selected)} 件")
info_lines.append("")
info_lines.append("今回入れるエピソード:")
info_lines.append("")
for i, episode in enumerate(selected, start=1):
info_lines.append(
f"{i}. {format_dt_jst(episode['pub_date'])} JST | {episode['title']}"
)
Path(INFO_FILE).write_text(
"\n".join(info_lines) + "\n",
encoding="utf-8"
)
if not selected:
raise ValueError(
f"過去{DAYS_BACK}日分のエピソードが見つかりませんでした。"
f"確認用に {INFO_FILE} を見てください。"
)
playlist_lines = ["#EXTM3U"]
for episode in selected:
playlist_lines.append(
f"# {format_dt_jst(episode['pub_date'])} JST | {episode['title']}"
)
playlist_lines.append(episode["audio_url"])
Path(PLAYLIST_FILE).write_text(
"\n".join(playlist_lines) + "\n",
encoding="utf-8"
)
print(f"{PLAYLIST_FILE} を作成しました。")
print(f"{INFO_FILE} を作成しました。")
print(f"対象エピソード数: {len(selected)} 件")
def main():
try:
feed_url = read_feed_url()
rss_data = download_rss(feed_url)
episodes, skipped_no_audio, skipped_no_date = collect_episodes(rss_data)
make_playlist(episodes, skipped_no_audio, skipped_no_date)
print("完了しました。")
except Exception as e:
print("エラーが発生しました。")
print(e)
if __name__ == "__main__":
main()
保存します。
Ctrl + O
Enter
Ctrl + X
ステップ4:実行できるようにする
次を入力します。
chmod +x make_playlist_5b.py
ただし、前回のような「import: コマンドが見つかりません」系の事故を避けるため、今回はまず python3 で実行します。
python3 make_playlist_5b.py
うまくいくと、次のように表示されます。
RSSを取得します: https://listen.style/p/xxxxx/rss
playlist_5b.m3u を作成しました。
playlist_5b_info.txt を作成しました。
対象エピソード数: 3 件
完了しました。
ステップ5:作成されたファイルを確認する
次を入力します。
ls
次のようなファイルが見えればOKです。
feed.txt
make_playlist_5a.py
make_playlist_5b.py
playlist.m3u
playlist_5b.m3u
playlist_5b_info.txt
今回重要なのはこの2つです。
playlist_5b.m3u
playlist_5b_info.txt
ステップ6:プレイリスト情報を確認する
まず、確認用ファイルを開きます。
cat playlist_5b_info.txt
たとえば次のような表示になります。
TanaRadio Pi フェーズ5B プレイリスト情報
対象期間: 過去3日分
現在時刻: 2026-06-30 10:20 JST
対象開始: 2026-06-27 10:20 JST
RSSから取得した音声付きエピソード数: 10 件
音声URLなしでスキップ: 0 件
公開日時なし/日時解析失敗でスキップ: 0 件
今回プレイリストに入れる数: 3 件
今回入れるエピソード:
1. 2026-06-28 08:00 JST | エピソードタイトルA
2. 2026-06-29 09:00 JST | エピソードタイトルB
3. 2026-06-30 07:30 JST | エピソードタイトルC
ここで見るべき点は2つです。
1. 対象開始が「約3日前」になっているか
2. エピソードが古い順に並んでいるか
この2つが合っていれば、5Bの中心部分は成功です。
ステップ7:playlist_5b.m3u の中身を確認する
次を入力します。
head playlist_5b.m3u
次のように表示されればOKです。
#EXTM3U
# 2026-06-28 08:00 JST | エピソードタイトルA
https://...
# 2026-06-29 09:00 JST | エピソードタイトルB
https://...
件数も確認できます。
grep -v '^#' playlist_5b.m3u | sed '/^$/d' | wc -l
これで表示される数字が、playlist_5b_info.txt の「今回プレイリストに入れる数」と同じならOKです。
ステップ8:mpvで再生する
いよいよ再生します。
mpv playlist_5b.m3u
音が出れば成功です。
mpvの基本操作は5Aと同じです。
スペースキー:一時停止/再開
Enter:次へ
q:終了
聴いてみて、古いエピソードから順に流れているか確認してください。
ステップ9:うまくいったら通常名にコピーする
5B版がうまく動いたら、これを通常の playlist.m3u として使えるようにしておきます。
cp playlist_5b.m3u playlist.m3u
その後は、いつものように次で再生できます。
mpv playlist.m3u
ただし、今後しばらくは確認しやすいので、
mpv playlist_5b.m3u
のまま使っても構いません。
ステップ10:5Bの成功条件
フェーズ5Bの成功条件は、次の4つです。
1. make_playlist_5b.py を作れた
2. playlist_5b.m3u が作成された
3. playlist_5b_info.txt で「過去3日分・古い順」を確認できた
4. mpv playlist_5b.m3u で再生できた
この4つができれば、5Bは成功です。
よくあるエラー
1. 過去3日分のエピソードが見つかりませんでした
これは、必ずしも失敗ではありません。
原因は単純で、RSS内に本当に過去3日分のエピソードがない可能性があります。
まず確認します。
cat playlist_5b_info.txt
もし「RSSから取得した音声付きエピソード数」はあるのに、「今回プレイリストに入れる数」が0なら、日付条件に合うエピソードがなかったということです。
テストだけしたい場合は、スクリプト内のここを一時的に変えます。
DAYS_BACK = 3
たとえば30日にします。
DAYS_BACK = 30
保存して、もう一度実行します。
python3 make_playlist_5b.py
これで動けば、スクリプト自体は正常です。最後に DAYS_BACK = 3 に戻してください。
2. 公開日時なし/日時解析失敗でスキップ が多い
RSS内の pubDate がうまく読めていない可能性があります。
LISTENの通常RSSなら、おそらく大丈夫だと思います。
ただし、別サービスのRSSでは日付形式が少し違う場合があります。
この段階では、まずLISTENの公開RSSで試すのが安全です。
3. 音声URLなしでスキップ が多い
これは、RSSに音声ファイルのURLが入っていない場合です。
原因としては次が考えられます。
RSS URLが番組配信用ではない
限定配信で取得できない
RSS形式が特殊
5Bではまだ公開RSSだけで進めましょう。限定配信や購入済み音声は、ここに混ぜない方がいいです。沼に足を入れるのは、長靴を履いてからです。
4. 実行するとまた import: コマンドが見つかりません と出る
その場合は、次のように実行してください。
python3 make_playlist_5b.py
./make_playlist_5b.py ではなく、python3 を前につけます。
それでもダメなら、ファイルの先頭行を確認します。
head -n 3 make_playlist_5b.py
先頭がこうなっていればOKです。
#!/usr/bin/env python3
import urllib.request
import xml.etree.ElementTree as ET
もし #!/usr/bin/env python3 より前に余計な文字や空行がある場合は、nano で直してください。
nano make_playlist_5b.py
今回はここで止めてよいです
5Bではここまでで十分です。
RSSを読む
↓
公開日時を見る
↓
過去3日分だけ選ぶ
↓
古い順に並べる
↓
mpvで再生する
これができれば、次の5Cで「RSSを複数本にする」準備が整います。
ここまで来ると、TanaRadio Piはかなり「フォロー中ラジオ」の原型に近づいてきます。まだボタンもLEDも増えていませんが、中身は確実にラジオらしくなっています。
#声日記 #TanaRadioPi
LISTENで開く