EPISODE · Jul 5, 2026 · 29 MIN
375 日記 | 棚を選択してプレイリストを作成
from TanaRadio · host たな
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で開く
Embed this episode
NOW PLAYING
375 日記 | 棚を選択してプレイリストを作成
No transcript for this episode yet
Similar Episodes
No similar episodes found.
Similar Podcasts
No similar podcasts found.