EPISODE · Jul 2, 2026 · 12 MIN
372 日記 | 複数RSSで過去3日分を古い順に再生
from TanaRadio · host たな
複数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 08:30 / 番組A / エピソードタイトル https://... # 2026-07-01 10:15 / 番組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 09:00 / ... # 2026-06-30 14:20 / ... # 2026-07-01 08:30 / ... ステップ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はかなり「声の本棚」の入口に立っています。これは順調です。#声日記 #TanaRadioPi LISTENで開く
Embed this episode
NOW PLAYING
372 日記 | 複数RSSで過去3日分を古い順に再生
No transcript for this episode yet
Similar Episodes
No similar episodes found.
Similar Podcasts
No similar podcasts found.