5つの棚(ポッドキャストのグループ)を作成し,それを選んでプレイリストを作成できるようにしました(フェーズ5E完了)。
フェーズ5E 詳細マニュアル
棚ファイルを作り、コマンドで棚を切り替える
0. 今回の目標
フェーズ5Eでやることは、これです。
棚ごとのRSSリストを作る
↓
コマンドで棚を指定する
↓
指定した棚のRSSだけを読む
↓
過去3日分・古い順で playlist.m3u を作る
↓
mpv または tanaradio_5d.py で再生する
今回はまだ、以下はやりません。
棚選びボタン
LED点滅表示
再生モード選び
ローカル再生履歴
ロータリーエンコーダー
ここは焦らなくてよいです。
5Eの目的は、「棚」という考え方を、まずファイルで実現することです。
ステップ1:作業フォルダへ移動する
Raspberry Piでターミナルを開きます。
cd ~/tanaradio5
確認します。
pwd
次のように表示されればOKです。
/home/ユーザー名/tanaradio5
ファイルも確認します。
ls
少なくとも、次のようなファイルがあるはずです。
feeds.txt
make_playlist_5c.py
playlist.m3u
tanaradio_5d.py
ステップ2:今の状態をバックアップする
5Dまで動いているので、念のためバックアップします。
cp make_playlist_5c.py make_playlist_5c_backup.py
cp tanaradio_5d.py tanaradio_5d_backup.py
cp playlist.m3u playlist_backup_5d.m3u
確認します。
ls
バックアップファイルが見えればOKです。
make_playlist_5c_backup.py
tanaradio_5d_backup.py
playlist_backup_5d.m3u
5Eでは tanaradio_5d.py は基本的に変更しません。
ここ、大事です。5Dで動いたものはそのまま残します。動いたものは宝です。Linux作業では、動いたものを消すと、静かに深い後悔が来ます。
ステップ3:棚ファイルを作る
まず、今回決めた5つの棚に対応するファイルを作ります。
touch shelf_01_voice_life.txt
touch shelf_02_tech_ai_mono.txt
touch shelf_03_education_research.txt
touch shelf_04_books_culture.txt
touch shelf_05_tanaradio.txt
確認します。
ls shelf_*.txt
次のように表示されればOKです。
shelf_01_voice_life.txt
shelf_02_tech_ai_mono.txt
shelf_03_education_research.txt
shelf_04_books_culture.txt
shelf_05_tanaradio.txt
棚の意味は次の通りです。
1:声日記・生活
2:技術・AI・ものづくり
3:教育・研究・知の実践
4:本・文化・物語
5:TanaRadio
ステップ4:まず1つの棚にRSSを入れる
最初から全部きれいに分類しなくてよいです。
まずは、動作確認のために、これまで使っていた feeds.txt を1番の棚にコピーします。
cp feeds.txt shelf_01_voice_life.txt
確認します。
cat shelf_01_voice_life.txt
RSS URLが表示されればOKです。
あとで、実際の分類に合わせて中身を編集します。
たとえば、1番の棚を編集するには、
nano shelf_01_voice_life.txt
次のように、コメントを入れながらRSS URLを書けます。
# 声日記・生活
# で始まる行は、あとで作るプログラムでは無視されます。
つまり、番組名メモを書いておいて大丈夫です。
ステップ5:5E用スクリプトを作る
新しいPythonファイルを作ります。
nano make_playlist_5e.py
次のコードをそのまま貼り付けてください。
#!/usr/bin/env python3
import sys
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
PLAYLIST_FILE = "playlist.m3u"
# 5Eでは、まだ再生モード選びはしない。
# 5Cと同じく「過去3日分・古い順」で固定する。
DAYS = 3
JST = ZoneInfo("Asia/Tokyo")
SHELVES = [
{
"id": "1",
"key": "voice",
"name": "声日記・生活",
"file": "shelf_01_voice_life.txt",
"aliases": ["life", "koe", "nikki"],
},
{
"id": "2",
"key": "tech",
"name": "技術・AI・ものづくり",
"file": "shelf_02_tech_ai_mono.txt",
"aliases": ["ai", "mono", "monozukuri"],
},
{
"id": "3",
"key": "edu",
"name": "教育・研究・知の実践",
"file": "shelf_03_education_research.txt",
"aliases": ["education", "research", "study"],
},
{
"id": "4",
"key": "books",
"name": "本・文化・物語",
"file": "shelf_04_books_culture.txt",
"aliases": ["book", "culture", "story"],
},
{
"id": "5",
"key": "tana",
"name": "TanaRadio",
"file": "shelf_05_tanaradio.txt",
"aliases": ["tanaradio"],
},
]
def show_shelves():
print("使える棚:")
for shelf in SHELVES:
print(
f" {shelf['id']}: {shelf['name']} "
f"({shelf['key']}) -> {shelf['file']}"
)
print()
print("例:")
print(" python3 make_playlist_5e.py 1")
print(" python3 make_playlist_5e.py tech")
print(" python3 make_playlist_5e.py shelf_05_tanaradio.txt")
def resolve_shelf(argument):
if argument is None:
# 引数なしの場合は1番棚を使う
return SHELVES[0]
arg = argument.strip()
for shelf in SHELVES:
candidates = [shelf["id"], shelf["key"]] + shelf["aliases"]
if arg in candidates:
return shelf
# .txt が指定された場合は、任意の棚ファイルとして扱う
path = Path(arg)
if path.suffix == ".txt" or path.exists():
return {
"id": "?",
"key": "custom",
"name": path.stem,
"file": str(path),
"aliases": [],
}
raise ValueError(f"棚を見つけられませんでした: {argument}")
def read_feed_urls(shelf_file):
path = Path(shelf_file)
if not path.exists():
raise FileNotFoundError(f"{shelf_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"{shelf_file} にRSS URLが書かれていません。")
return urls
def download_rss(url):
print(f"RSSを取得します: {url}")
request = urllib.request.Request(
url,
headers={
"User-Agent": "TanaRadio-Pi/5E"
}
)
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 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 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 make_playlist(episodes, shelf):
if not episodes:
raise ValueError(
f"過去{DAYS}日分のエピソードが見つかりませんでした。"
)
# 古い順に並べる
episodes.sort(key=lambda ep: ep["pub_dt"])
playlist_lines = [
"#EXTM3U",
f"# Shelf: {shelf['name']}",
f"# Shelf file: {shelf['file']}",
f"# Mode: 過去{DAYS}日分・古い順",
]
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:
if len(sys.argv) >= 2 and sys.argv[1] in ["--list", "-l"]:
show_shelves()
return
argument = sys.argv[1] if len(sys.argv) >= 2 else None
shelf = resolve_shelf(argument)
print("選択された棚:")
print(f" 番号: {shelf['id']}")
print(f" 名前: {shelf['name']}")
print(f" ファイル: {shelf['file']}")
print()
feed_urls = read_feed_urls(shelf["file"])
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, shelf)
print("完了しました。")
except Exception as e:
print("エラーが発生しました。")
print(e)
print()
print("使い方を確認するには、次を実行してください。")
print(" python3 make_playlist_5e.py --list")
if __name__ == "__main__":
main()
保存します。
Ctrl + O
Enter
Ctrl + X
ステップ6:実行権限をつける
chmod +x make_playlist_5e.py
ただし、最初の実行は今回も python3 をつけるのがおすすめです。
python3 make_playlist_5e.py --list
次のように表示されればOKです。
使える棚:
1: 声日記・生活 (voice) -> shelf_01_voice_life.txt
2: 技術・AI・ものづくり (tech) -> shelf_02_tech_ai_mono.txt
3: 教育・研究・知の実践 (edu) -> shelf_03_education_research.txt
4: 本・文化・物語 (books) -> shelf_04_books_culture.txt
5: TanaRadio (tana) -> shelf_05_tanaradio.txt
ステップ7:1番棚でプレイリストを作る
まず、1番棚を使います。
python3 make_playlist_5e.py 1
または、
python3 make_playlist_5e.py voice
うまくいくと、次のような表示になります。
選択された棚:
番号: 1
名前: 声日記・生活
ファイル: shelf_01_voice_life.txt
RSS URL数: 5 本
RSSを取得します: https://...
取得できたエピソード数: 10 件
...
全取得エピソード数: 50 件
重複除去後: 50 件
過去3日分: 12 件
playlist.m3u を作成しました。
プレイリスト内のエピソード数: 12 件
完了しました。
確認します。
head -n 20 playlist.m3u
次のように、棚名が入っていればOKです。
#EXTM3U
# Shelf: 声日記・生活
# Shelf file: shelf_01_voice_life.txt
# Mode: 過去3日分・古い順
# 2026-07-03 18:21 / 1分声日記 / 2026/07/03 18:21
https://...
ここで、5Eの基本は成功です。
ステップ8:mpvで再生確認する
まずは単純にmpvで再生します。
mpv playlist.m3u
音が出ればOKです。
mpvの操作はこれまで通りです。
スペースキー:一時停止/再開
Enter:次へ
q:終了
確認できたら、q で終了します。
ステップ9:5Dの物理ボタン操作につなげる
5Eで作った playlist.m3u は、5Dの tanaradio_5d.py でそのまま使えます。
つまり、次の流れです。
python3 make_playlist_5e.py 1
./tanaradio_5d.py
これで、
1番棚のRSSから playlist.m3u を作る
↓
tanaradio_5d.py が playlist.m3u を再生する
↓
3つの物理ボタンで操作する
という形になります。
ボタン操作は5Dと同じです。
再生/一時停止ボタン:再生・一時停止
戻るボタン:前のエピソードへ
次へボタン:次のエピソードへ
ここまでできると、TanaRadio Piはもうかなり「棚を持ったラジオ」になっています。
ステップ10:別の棚を試す
2番棚にRSSを入れてから試します。
nano shelf_02_tech_ai_mono.txt
たとえば、次のように書きます。
# 技術・AI・ものづくり
# ・・・
https://listen.style/p/xxxxx/rss
# ・・・
https://listen.style/p/yyyyy/rss
# ・・・
https://listen.style/p/zzzzz/rss
保存します。
Ctrl + O
Enter
Ctrl + X
2番棚でプレイリストを作ります。
python3 make_playlist_5e.py 2
または、
python3 make_playlist_5e.py tech
確認します。
head -n 20 playlist.m3u
Shelf: 技術・AI・ものづくり と表示されればOKです。
再生します。
mpv playlist.m3u
または、物理ボタンつきで再生します。
./tanaradio_5d.py
ステップ11:5つの棚を順番に整える
最初の棚構成は、次の形でよいと思います。
1:声日記・生活
nano shelf_01_voice_life.txt
入れる候補:
2:技術・AI・ものづくり
nano shelf_02_tech_ai_mono.txt
入れる候補:
3:教育・研究・知の実践
nano shelf_03_education_research.txt
入れる候補:
4:本・文化・物語
nano shelf_04_books_culture.txt
入れる候補:
5:TanaRadio
nano shelf_05_tanaradio.txt
入れる候補:
TanaRadio
同じ番組を複数の棚に入れても大丈夫です。
棚は図書館の分類ではありません。
聴くときの気分で使う分類です。ここはゆるくて大丈夫です。
ステップ12:棚ごとの動作確認
それぞれの棚で試します。
python3 make_playlist_5e.py 1
head -n 10 playlist.m3u
python3 make_playlist_5e.py 2
head -n 10 playlist.m3u
python3 make_playlist_5e.py 3
head -n 10 playlist.m3u
python3 make_playlist_5e.py 4
head -n 10 playlist.m3u
python3 make_playlist_5e.py 5
head -n 10 playlist.m3u
それぞれで、
# Shelf: ...
の部分が変わればOKです。
たとえば、
# Shelf: 声日記・生活
# Shelf: 技術・AI・ものづくり
# Shelf: TanaRadio
のように変われば成功です。
ステップ13:実際の使い方
フェーズ5E段階での日常運用は、こうなります。
声日記・生活を聴く
cd ~/tanaradio5
python3 make_playlist_5e.py 1
./tanaradio_5d.py
技術・AI・ものづくりを聴く
cd ~/tanaradio5
python3 make_playlist_5e.py 2
./tanaradio_5d.py
TanaRadioを聴く
cd ~/tanaradio5
python3 make_playlist_5e.py 5
./tanaradio_5d.py
ここではまだ「棚を選んでから再生」ではなく、**「棚を指定してプレイリストを作ってから再生」**です。
次の5Fで、これを棚選びボタンに接続します。
フェーズ5Eの成功条件
フェーズ5Eの成功条件は、次の5つです。
1. shelf_01〜shelf_05 の棚ファイルを作れた
2. 棚ファイルにRSS URLを書けた
3. make_playlist_5e.py で棚を指定できた
4. 指定した棚に応じて playlist.m3u が作り直された
5. tanaradio_5d.py で、その playlist.m3u を物理ボタン操作できた
ここまでできれば、5Eは成功です。
今回できるようになること
5Eが成功すると、TanaRadio Piはこう変わります。
フォロー中をまとめて流す装置
から、
声の棚を選んで流す装置
になります。
これはかなり大きいです。
まだ棚選びボタンはありませんが、考え方としてはもう「本棚」です。
今回の5Eを一言で言えば、こうです。
RSS一覧を、意味のある棚に分ける。
ここからTanaRadio Piは、単なるポッドキャスト再生機ではなく、たなさんの生活・研究・制作に合わせて育つ「声の本棚」になっていきます。次の5Fでは、この棚切り替えを物理ボタンに接続します。
#声日記 #TanaRadioPi
LISTENで開く