375 日記 | 棚を選択してプレイリストを作成 episode artwork

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で開く

Episode metadata supplied by the publisher feed · Published Jul 5, 2026

Embed this episode

NOW PLAYING

375 日記 | 棚を選択してプレイリストを作成

0:00 29:54

No transcript for this episode yet

We transcribe on demand. Request one and we'll notify you when it's ready — usually under 10 minutes.

No similar episodes found.

No similar podcasts found.

Frequently Asked Questions

How long is this episode of TanaRadio?

This episode is 29 minutes long.

When was this TanaRadio episode published?

This episode was published on July 5, 2026.

Can I download this TanaRadio episode?

Yes. Use the download control on the episode player to save the publisher-provided media file.
URL copied to clipboard!