TanaRadio

たな

私立工科系大学教員(科学史)の日々の出来事や雑感。日記,メモ,ブログなど TanaRadio Blog ( https://tanakahi.hatenablog.com/ )

  1. -17 ч

    372 日記 | 複数RSSで過去3日分を古い順に再生

    複数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のエピソードを集めて、公開

    12 мин.
  2. -1 дн.

    371 日記 | 過去3日分を古い順に再生

    あるポッドキャストの過去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つが合っていれば、5

    13 мин.
  3. -2 дн.

    370 日記 | RSSからプレイリストを作る

    RSSからプレイリストを作って,それにより連続再生することができました(フェーズ5A完了)。 フェーズ5A 詳細マニュアル RSS 1本から playlist.m3u を作る 0. 今回の目標 今回やることはこれだけです。 LISTENなどのRSSを1本用意する ↓ PythonでRSSを読む ↓ 音声ファイルのURLを取り出す ↓ playlist.m3u を作る ↓ mpvで連続再生する 今回はまだ、以下はやりません。 複数RSS 棚選び 再生モード選び LED ロータリーエンコーダー 再生履歴 ここを欲張らないのが大事です。 まず「RSSから音声URLを取り出してmpvで鳴らす」だけ成功させます。 ステップ1:作業フォルダを作る Raspberry Piでターミナルを開きます。 以下を入力します。 mkdir -p ~/tanaradio5 cd ~/tanaradio5 確認します。 pwd 次のように表示されればOKです。 /home/ユーザー名/tanaradio5 たとえばユーザー名が pi なら、 /home/pi/tanaradio5 です。 ステップ2:mpvが使えるか確認する フェーズ4でmpvは使っているはずですが、念のため確認します。 mpv --version mpvのバージョン情報が出ればOKです。 もし、 command not found のように出たら、mpvを入れます。 sudo apt update sudo apt install -y mpv ステップ3:RSS URLを1本用意する まずは、TanaRadioなど、公開RSSを1本使います。 作業フォルダ内に feed.txt を作ります。 nano feed.txt 開いたら、RSS URLを1行だけ貼り付けます。 例: https://listen.style/p/xxxxx/rss ここは、実際のTanaRadioのRSS URLに置き換えてください。 保存します。 Ctrl + O Enter Ctrl + X 確認します。 cat feed.txt RSS URLが1行表示されればOKです。 ステップ4:Pythonスクリプトを作る 次に、RSSを読んで playlist.m3u を作るPythonファイルを作ります。 nano make_playlist_5a.py 次のコードをそのまま貼り付けます。 #!/usr/bin/env python3 import urllib.request import xml.etree.ElementTree as ET from pathlib import Path FEED_FILE = "feed.txt" PLAYLIST_FILE = "playlist.m3u" 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/5A" } ) 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 make_playlist(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が見つかりません。") playlist_lines = ["#EXTM3U"] count = 0 for item in items: title = get_text(item, "title") audio_url = find_audio_url(item) if not audio_url: continue if title: playlist_lines.append(f"# {title}") playlist_lines.append(audio_url) count += 1 if count == 0: raise ValueError("音声URLを1件も見つけられませんでした。") Path(PLAYLIST_FILE).write_text( "\n".join(playlist_lines) + "\n", encoding="utf-8" ) print(f"{PLAYLIST_FILE} を作成しました。") print(f"音声URL数: {count} 件") def main(): try: feed_url = read_feed_url() rss_data = download_rss(feed_url) make_playlist(rss_data) print("完了しました。") except Exception as e: print("エラーが発生しました。") print(e) if __name__ == "__main__": main() 保存します。 Ctrl + O Enter Ctrl + X ステップ5:Pythonファイルを実行できるようにする chmod +x make_playlist_5a.py ステップ6:プレイリストを作る 実行します。 ./make_playlist_5a.py うまくいくと、たとえば次のように出ます。 RSSを取得します: https://listen.style/p/xxxxx/rss playlist.m3u を作成しました。 音声URL数: 10 件 完了しました。 確認します。 ls 次の3つが見えればOKです。 feed.txt make_playlist_5a.py playlist.m3u ステップ7:playlist.m3u の中身を確認する head playlist.m3u 次のように、音声URLらしきものが出ていればOKです。 #EXTM3U # エピソードタイトル https://... # エピソードタイトル https://... この段階で、RSSから音声URLを取り出すところは成功です。 ステップ8:mpvで再生する いよいよ再生します。 mpv playlist.m3u 音が出れば成功です。 mpvの基本操作は次の通りです。 スペースキー:一時停止/再開 Enter:次へ q:終了 もし音が出ない場合は、まず別の音声ファイルやYouTubeなどでRaspberry Piから音が出るか確認してください。 ここでの問題は、RSSではなく音声出力側の可能性があります。 ステップ9:成功条件 フェーズ5Aの成功条件は、次の3つです。 1. feed.txt にRSS URLを1本書けた 2. make_playlist_5a.py で playlist.m3u を作れた 3. mpv playlist.m3u で音声が再生できた この3つができれば、フェーズ5Aは成功です。 よくあるエラー 1. feed.txt が見つかりません 原因: 作業フォルダが違う feed.txt を作っていない 確認: pwd ls ~/tanaradio5 にいて、feed.txt があるか見ます。 2. 音声URLを1件も見つけられませんでした 原因としては、 RSS URLが間違っている RSSが音声配信用ではない RSSの形式が少し特殊 限定配信で取得できない などが考えられます。 まずは公開RSSで試すのがよいです。 3. mpv playlist.m3u で音が出ない 原因は大きく3つです。 Raspberry Piの音声出力先が違う USBスピーカーやBluetoothスピーカーが未接続 音量が小さい、またはミュート 確認: mpv --volume=80 playlist.m3u これで少し大きめの音量で再生できます。 今回はここで止めてよいです 5Aでは、ここまでで十分です。 RSSを読む playlist.m3uを作る mpvで鳴らす これだけです。 次の5Bでは、このプレイリスト生成に、 過去3日分だけ 古い順 という条件を加えます。 でも今はまだ、そこへ行かなくて大丈夫です。 まずは「RSSがmpvで鳴る」。ここをきっちり成功させましょう。これはフェーズ5の土台です。#声日記 #TanaRadioPi LISTENで開く

    20 мин.
  4. -4 дн.

    368 日記 | TanaRadioの音声ファイル再生をボタンで制御

    TanaRadioの音声ファイル再生をボタンで制御できるようになり,フェーズ4が完了しました。 mpvで音声を再生し、3ボタンで制御する 今日の到達目標 今日のゴールはこれです。 Chromiumを使わずに mpvでTanaRadioの音声を再生し 3つの物理ボタンで操作する 3ボタンの役割は、これまでと同じです。 GPIO17:再生/一時停止 GPIO27:10秒戻る GPIO22:10秒進む 配線はすでに成功しているので、今日は基本的にソフトウェア側の作業です。 ステップ0:現在の成功版をバックアップする まず、今うまく動いているChromium版を保存しておきます。 cp ~/tanaradio/buttons3_playerctl.py ~/tanaradio/buttons3_playerctl_success.py 確認します。 ls ~/tanaradio buttons3_playerctl_success.py が見えればOKです。 これは保険です。 mpv版でつまずいても、Chromium版にはいつでも戻れます。 ステップ1:mpvと必要なPython部品を入れる ターミナルで次を実行します。 sudo apt update sudo apt install -y mpv python3-feedparser python3-gpiozero mpvが入ったか確認します。 mpv --version バージョン情報が出ればOKです。 mpvは、--input-ipc-server を使うことで外部プログラムからJSON IPCで操作できます。今日はPythonからこの仕組みを使います。 ステップ2:TanaRadioのRSS URLを用意する mpvは、LISTENのページそのものではなく、音声ファイルURLを再生します。 その音声ファイルURLを取得するために、今回はRSSを使います。LISTENのTanaRadioページには「RSSを開く」「LISTENのRSS URLをコピー」といった導線があります。 TanaRadioのページで、RSS URLをコピーしてください。 メモとして、ターミナルではなく紙やテキストに一度控えておくとよいです。 TanaRadio RSS URL = ここにコピーしたURL まだRSS URLがわからない場合は、ここで止まって大丈夫です。 今日の最大の山は、実は配線ではなく「mpvに渡す音声URLを取ること」です。 ステップ3:RSSから最新音声URLを取り出すスクリプトを作る 作業フォルダに移動します。 cd ~/tanaradio 新しいファイルを作ります。 nano ~/tanaradio/get_latest_audio.py 中身はこれです。 RSS_FEED_URL のところだけ、コピーしたTanaRadioのRSS URLに置き換えてください。 #!/usr/bin/env python3 import feedparser import sys RSS_FEED_URL = "ここにTanaRadioのRSS URLを貼る" feed = feedparser.parse(RSS_FEED_URL) if not feed.entries: print("RSSからエピソードを取得できませんでした。", file=sys.stderr) sys.exit(1) latest = feed.entries[0] if not latest.enclosures: print("最新エピソードに音声ファイルURLが見つかりませんでした。", file=sys.stderr) sys.exit(1) audio_url = latest.enclosures[0].href print(audio_url) 保存します。 Ctrl + O Enter Ctrl + X 実行します。 python3 ~/tanaradio/get_latest_audio.py 長いURLが表示されれば成功です。 https://...mp3 または、 https://...m4a のようなものが出るはずです。 ステップ4:mpvだけで音声を再生する まず、Pythonやボタンを使わず、mpv単体で再生できるか試します。 AUDIO_URL=$(python3 ~/tanaradio/get_latest_audio.py) mpv --no-video "$AUDIO_URL" 音声が出れば成功です。 停止するときは、 q を押します。 ここで音が出れば、Chromiumなし再生の入口を突破です。 ステップ5:mpvを外部制御できるか試す 次に、mpvを「外から操作できる状態」で起動します。 まず、もしmpvが残っていたら止めます。 pkill mpv 次に、mpvを待機状態で起動します。 mpv --no-video --idle=yes --input-ipc-server=/tmp/tanaradio-mpv.sock このターミナルはそのまま開いたままにします。 別のターミナルを開いて、次を実行します。 AUDIO_URL=$(python3 ~/tanaradio/get_latest_audio.py) python3 - EOF import socket, json sock = "/tmp/tanaradio-mpv.sock" audio_url = "$AUDIO_URL" message = json.dumps({"command": ["loadfile", audio_url, "replace"]}) + "\n" client = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) client.connect(sock) client.send(message.encode("utf-8")) client.close() EOF これでmpvから音声が流れれば成功です。 このテストは少し面倒ですが、意味は単純です。 mpvを起動して待機させる ↓ 別のPythonからmpvへ命令を送る ↓ 音声を読み込ませる ここまでできれば、ボタン制御の準備は整っています。 mpvを終了するときは、mpvを起動しているターミナルで、 q を押します。 ステップ6:3ボタンmpv制御スクリプトを作る いよいよ本番です。 nano ~/tanaradio/buttons3_mpv.py 中身はこれです。 ここでも RSS_FEED_URL のところだけ置き換えてください。 #!/usr/bin/env python3 from gpiozero import Button from signal import pause import subprocess import socket import json import os import time import sys import feedparser RSS_FEED_URL = "ここにTanaRadioのRSS URLを貼る" MPV_SOCKET = "/tmp/tanaradio-mpv.sock" def get_latest_audio_url(): feed = feedparser.parse(RSS_FEED_URL) if not feed.entries: print("RSSからエピソードを取得できませんでした。") sys.exit(1) latest = feed.entries[0] if not latest.enclosures: print("最新エピソードに音声ファイルURLが見つかりませんでした。") sys.exit(1) title = latest.get("title", "No title") audio_url = latest.enclosures[0].href print("Latest episode:", title) print("Audio URL:", audio_url) return audio_url def start_mpv(): if os.path.exists(MPV_SOCKET): os.remove(MPV_SOCKET) subprocess.Popen([ "mpv", "--no-video", "--idle=yes", f"--input-ipc-server={MPV_SOCKET}" ]) for _ in range(50): if os.path.exists(MPV_SOCKET): print("mpv is ready.") return time.sleep(0.1) print("mpv socket was not created.") sys.exit(1) def send_mpv(command): message = json.dumps({"command": command}) + "\n" try: with socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) as client: client.connect(MPV_SOCKET) client.send(message.encode("utf-8")) except FileNotFoundError: print("mpv socket not found. Is mpv running?") except ConnectionRefusedError: print("mpv connection refused.") def load_audio(audio_url): send_mpv(["loadfile", audio_url, "replace"]) def play_pause_pressed(): print("PLAY / PAUSE") send_mpv(["cycle", "pause"]) def back_10_pressed(): print("BACK 10 seconds") send_mpv(["seek", -10, "relative"]) def forward_10_pressed(): print("FORWARD 10 seconds") send_mpv(["seek", 10, "relative"]) play_pause = Button(17, pull_up=True, bounce_time=0.08) back_10 = Button(27, pull_up=True, bounce_time=0.08) forward_10 = Button(22, pull_up=True, bounce_time=0.08) play_pause.when_pressed = play_pause_pressed back_10.when_pressed = back_10_pressed forward_10.when_pressed = forward_10_pressed audio_url = get_latest_audio_url() start_mpv() load_audio(audio_url) print("TanaRadio mpv 3 buttons are ready.") print("GPIO17: play/pause") print("GPIO27: back 10 seconds") print("GPIO22: forward 10 seconds") pause() 保存します。 Ctrl + O Enter Ctrl + X gpiozero.Button は pull_up=True を指定すると、GPIOをHigh側へ引っ張り、ボタンの反対側をGNDへつなぐ使い方になります。これは今の配線方針と合っています。 ステップ7:3ボタンmpv版を実行する 念のため、mpvが残っていたら止めます。 pkill mpv 実行します。 python3 ~/tanaradio/buttons3_mpv.py うまくいくと、ターミナルに次のような表示が出ます。 Latest episode: ... Audio URL: ... mpv is ready. TanaRadio mpv 3 buttons are ready. GPIO17: play/pause GPIO27: back 10 seconds GPIO22: forward 10 seconds そして音声が流れます。 その状態でボタンを押します。 GPIO17 → 再生/一時停止 GPIO27 → 10秒戻る GPIO22 → 10秒進む 終了するときは、 Ctrl + C です。 そのあとmpvが残っていたら、 pkill mpv で止めます。 ステップ8:うまくいかないときの確認 1. RSSからURLが取れない まずこれを確認します。 python3 ~/tanaradio/get_latest_audio.py ここでURLが出なければ、RSS URLの貼り間違いの可能性があります。 確認点はこれです。 RSS_FEED_URL の引用符が消えていないか URLの前後に余計な空白がないか LISTENのページURLではなくRSS URLを入れているか 2. mpv単体で音が出ない これを確認します。 AUDIO_URL=$(python3 ~/tanaradio/get_latest_audio.py) echo "$AUDIO_URL" mpv --no-video "$AUDIO_URL" echo でURLが表示され、mpv で音が出なければ、音声出力設定の問題か、URLがmpvから直接再生できない可能性があります。 3. mpvは鳴るがボタンが効かない まず、前に成功したGPIOテストに戻ります。 python3 ~/tanaradio/buttons3_test.py 3ボタンが反応すれば、配線はOKです。 その場合は、mpv制御スクリプト側の問題です。 4. 1回押しただけなのに何回も反応する この値を少し増やします。 bounce_time=0.08 たとえば、 bounce_time=0.15 にします。 今日の進行順まとめ 今日は、この順番で進めるのがよいです。 1. Chromium版成功スクリプトをバックアップ 2. mpvとfeedparserをインストール 3. RSS URLを用意する 4. get_latest_audio.

    23 мин.
  5. 366 日記 | 3ボタンの実験

    25 июн.

    366 日記 | 3ボタンの実験

    フェーズ4の3ボタンの実験です。ボタンを押すことで,ブラウザのプレーヤーをコントロールできるところまでいきました。 9. ステップ5:3ボタンへ増やす 1ボタンが成功したら、いったんシャットダウンします。 sudo shutdown -h now 電源を抜いてから、3ボタンに増やします。 配線はこれです。 GPIO17 ─ 1kΩ抵抗 ─ ボタン ─ GND GPIO27 ─ 1kΩ抵抗 ─ ボタン ─ GND GPIO22 ─ 1kΩ抵抗 ─ ボタン ─ GND GNDは共通でかまいません。 ブレッドボード上では、GNDレールを作って、各ボタンをそこへ戻すとわかりやすいです。 GPIO17 → 抵抗 → ボタン → GNDレール GPIO27 → 抵抗 → ボタン → GNDレール GPIO22 → 抵抗 → ボタン → GNDレール GPIO23は、今回は配線しません。 10. ステップ6:Pythonで3ボタンをテストする 電源を入れ、ターミナルを開きます。 3ボタンテスト用スクリプトを作ります。 nano ~/tanaradio/buttons3_test.py 中身はこれです。 from gpiozero import Button from signal import pause play_pause = Button(17, pull_up=True, bounce_time=0.08) back_10 = Button(27, pull_up=True, bounce_time=0.08) forward_10 = Button(22, pull_up=True, bounce_time=0.08) def play_pause_pressed(): print("PLAY / PAUSE button pressed") def back_10_pressed(): print("BACK 10 seconds button pressed") def forward_10_pressed(): print("FORWARD 10 seconds button pressed") play_pause.when_pressed = play_pause_pressed back_10.when_pressed = back_10_pressed forward_10.when_pressed = forward_10_pressed print("3 button test is ready.") pause() 実行します。 python3 ~/tanaradio/buttons3_test.py それぞれのボタンを押して、次のように別々の表示が出れば成功です。 PLAY / PAUSE button pressed BACK 10 seconds button pressed FORWARD 10 seconds button pressed ここまでできれば、GPIO入力実験は成功です。 フェーズ4の核心はかなり越えています。 11. うまくいかないときの確認 何も表示されない場合 確認することはこれです。 GPIO番号を間違えていないか GNDにつながっているか ボタンの向きが合っているか 抵抗が同じ列に刺さっていてショートしていないか ブレッドボードの列を勘違いしていないか まずはGPIO17の1ボタンに戻って確認してください。 3つ同時に直そうとすると、原因が見えにくくなります。 1回押しただけなのに何回も表示される場合 bounce_time を少し大きくします。 bounce_time=0.15 たとえば、3か所すべてを 0.15 にして試します。 押していないのに反応する場合 タクトスイッチの向きが怪しいです。 ブレッドボードの中央の溝をまたいでいるかを確認してください。 12. ステップ7:playerctlでChromium再生を操作する 次に、Chromium上のTanaRadio再生をコマンドで操作できるか試します。 これは最終形ではありません。 Chromiumなしのmpv版へ行く前の足場です。 まずインストールします。 sudo apt install -y playerctl playerctl はMPRIS対応メディアプレイヤーをコマンドラインから制御するツールで、再生、一時停止、停止、前後トラックなどの操作ができます。 ChromiumでTanaRadioを開き、音声を再生します。 その状態でターミナルから試します。 playerctl status 再生中なら、 Playing のように表示されます。 次に、 playerctl play-pause これで再生/一時停止が切り替われば成功です。 10秒戻る/進むは、環境によって効き方が違う場合があります。まず試します。 playerctl position 10- playerctl position 10+ 効けば、そのまま使えます。 効かなければ、フェーズ4前半では「再生/一時停止だけ成功」と考えてよいです。 13. ステップ8:3ボタンでChromium再生を操作する 3ボタン操作用スクリプトを作ります。 nano ~/tanaradio/buttons3_playerctl.py 中身はこれです。 #!/usr/bin/env python3 from gpiozero import Button from signal import pause import subprocess def run(command): print(">", " ".join(command)) subprocess.run(command, check=False) play_pause = Button(17, pull_up=True, bounce_time=0.08) back_10 = Button(27, pull_up=True, bounce_time=0.08) forward_10 = Button(22, pull_up=True, bounce_time=0.08) def play_pause_pressed(): run(["playerctl", "play-pause"]) def back_10_pressed(): run(["playerctl", "position", "10-"]) def forward_10_pressed(): run(["playerctl", "position", "10+"]) play_pause.when_pressed = play_pause_pressed back_10.when_pressed = back_10_pressed forward_10.when_pressed = forward_10_pressed print("TanaRadio 3 physical buttons are ready.") pause() 実行します。 python3 ~/tanaradio/buttons3_playerctl.py この状態で、3つのボタンを押します。 期待する動作はこれです。 GPIO17:再生/一時停止 GPIO27:10秒戻る GPIO22:10秒進む ここまでできれば、フェーズ4前半は成功です。#声日記 #TanaRadioPi LISTENで開く

    38 мин.
  6. 365 日記 | 1ボタンの実験

    24 июн.

    365 日記 | 1ボタンの実験

    いよいよフェーズ4。ボタンやつまみでTanaRadio Piを操作する第一歩を踏み出します。まずはボタン1つから。 4. ステップ1:Raspberry Piをシャットダウンする 配線前に、必ずシャットダウンします。 ターミナルで次を実行します。 sudo shutdown -h now Raspberry Piの動作が止まったら、USB-C電源を抜きます。 ここで焦らないでください。 電源が入ったままGPIOを触るのは、初心者段階では避けた方がよいです。 5. ステップ2:ブレイクアウト基板を接続する 接続はこうです。 Raspberry Pi GPIO ↓ リボンケーブル ↓ ブレイクアウト基板 ↓ ブレッドボード 注意点は、1番ピンの向きです。 ブレイクアウト基板には、たいてい次のような表示があります。 3V3 5V GND GPIO17 GPIO27 GPIO22 最初に確認するのはこの3つです。 GPIO17 GND 5Vの位置 5Vは使いません。 「どこが5Vか」を確認するのは、使うためではなく、避けるためです。 6. ステップ3:GPIO17の1ボタンだけ配線する 最初は1個だけです。 GPIO17 ─ 1kΩ抵抗 ─ ボタン ─ GND ブレッドボード上では、次のような考え方です。 ブレイクアウト上のGPIO17 ↓ ジャンパー線 ↓ 1kΩ抵抗 ↓ ボタンの片側 ↓ ボタンの反対側 ↓ GND タクトスイッチは、ブレッドボードの中央の溝をまたぐように挿します。 ここは大事です。向きを間違えると、押していないのに常時ONのようになることがあります。 7. ステップ4:Pythonで1ボタンをテストする Raspberry Piの電源を入れます。 ターミナルを開いて、作業用フォルダを作ります。 mkdir -p ~/tanaradio cd ~/tanaradio 必要なものを入れます。 sudo apt update sudo apt install -y python3-gpiozero 1ボタンテスト用スクリプトを作ります。 nano ~/tanaradio/button17_test.py 中身はこれです。 from gpiozero import Button from signal import pause button = Button(17, pull_up=True, bounce_time=0.08) def pressed(): print("GPIO17 button pressed") button.when_pressed = pressed print("GPIO17 button test is ready.") pause() 保存して実行します。 python3 ~/tanaradio/button17_test.py ボタンを押して、次のように表示されれば成功です。 GPIO17 button pressed 終了するときは、 Ctrl + C です。 8. このスクリプトがしていること この1ボタンテストは、こういう意味です。 GPIO17をボタン入力として使う ↓ ボタンが押されたら ↓ 画面に文字を表示する ↓ プログラムは終了せず待ち続ける 重要なのはこの行です。 button = Button(17, pull_up=True, bounce_time=0.08) これは、 GPIO17にボタンがつながっている 内部プルアップを使う チャタリングを少し無視する という意味です。 bounce_time は、物理ボタンの接点が細かく震えて1回の押下が複数回として読まれる現象を抑えるための指定です。gpiozero の Button には bounce_time などの引数が用意されています。#声日記 #TanaRadioPi LISTENで開く

    32 мин.

Об этом подкасте

私立工科系大学教員(科学史)の日々の出来事や雑感。日記,メモ,ブログなど TanaRadio Blog ( https://tanakahi.hatenablog.com/ )

Вам может также понравиться