TanaRadio

たな

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

  1. 9 giờ trước

    374 日記 | 物理ボタンでプレイリスト再生を制御

    物理ボタンでプレイリスト再生の制御ができるようになりました(フェーズ5D完了)。 *以下のtanaradio_5d.pyのコードは,修正済みのものです(この収録後も1回修正しました)。 フェーズ5D 詳細マニュアル 既存3ボタンでRSSプレイリストを操作する 0. 今回の目標 今回やることは、これです。 フェーズ5Cで playlist.m3u を作る ↓ mpvで playlist.m3u を再生する ↓ GPIOの3ボタンでmpvを操作する 3つのボタンの役割は、今回は次のようにします。 再生/一時停止ボタン:再生・一時停止 戻るボタン:前のエピソードへ 次へボタン:次のエピソードへ mpvへの命令は次の3つです。 再生/一時停止:cycle pause 戻る:playlist-prev 次へ:playlist-next 構想メモでは、戻るボタンに「1回押し=先頭へ、2回押し=前へ、長押し=10秒戻る」という発展案があります。 ただし、5Dではまだ入れません。ここで欲張ると、ボタン判定の沼に入ります。沼は楽しいですが、今日はまだ岸辺でお茶を飲みます。 ステップ1:作業フォルダへ移動する Raspberry Piでターミナルを開きます。 cd ~/tanaradio5 確認します。 pwd 次のような表示になればOKです。 /home/ユーザー名/tanaradio5 ステップ2:5Cまでのファイルを確認する ls 少なくとも、次のようなファイルがあるはずです。 feeds.txt make_playlist_5c.py playlist.m3u スクリプト名は、たなさんが5Cで作った名前に合わせてください。 以下では、5Cのスクリプト名を make_playlist_5c.py として進めます。 ステップ3:5Cのプレイリストをもう一度作る まず、5Cのスクリプトを実行して、最新の playlist.m3u を作ります。 ./make_playlist_5c.py または、実行権限を付けていない場合は、 python3 make_playlist_5c.py うまくいけば、たとえば次のような表示になります。 playlist.m3u を作成しました。 音声URL数: 12 件 完了しました。 確認します。 ls -l playlist.m3u playlist.m3u が存在し、サイズが0でなければOKです。 中身も少し見ます。 head playlist.m3u 次のように、音声URLが入っていればOKです。 #EXTM3U # エピソードタイトル https://... ステップ4:mpvだけで再生できるか確認する ボタン操作に入る前に、まず普通に再生します。 mpv playlist.m3u 音が出ればOKです。 この状態では、キーボードで操作できます。 スペースキー:一時停止/再開 Enter:次へ q:終了 確認できたら、q を押してmpvを終了します。 ここで音が出ない場合は、5Dには進まず、音声出力側を先に確認してください。 ステップ5:GPIOライブラリを確認する 今回はPythonからGPIOボタンを読むために gpiozero を使います。 まず確認します。 python3 -c "import gpiozero; print('gpiozero OK')" 次のように出ればOKです。 gpiozero OK もしエラーが出たら、次を実行します。 sudo apt update sudo apt install -y python3-gpiozero python3-lgpio インストール後、もう一度確認します。 python3 -c "import gpiozero; print('gpiozero OK')" ステップ6:GPIO番号を確認する 今回のコードでは、フェーズ4で使ってきた3つのGPIOを次のように使います。 GPIO17:再生/一時停止 GPIO27:戻る、つまり前のエピソードへ GPIO22:次へ、つまり次のエピソードへ 配線はこれまで通り、基本形は次です。構想メモでも、ボタン入力には保険として抵抗を残す方針になっています。 GPIO ─ 1kΩ程度の抵抗 ─ ボタン ─ GND つまり、ボタンを押すとGPIOがGNDにつながる形です。 Python側では pull_up=True にします。 ステップ7:5D用スクリプトを作る 新しいファイルを作ります。 nano tanaradio_5d.py 次のコードをそのまま貼り付けます。 #!/usr/bin/env python3 from gpiozero import Button from signal import pause import json import os import signal import socket import subprocess import sys import time from pathlib import Path # ===== 設定ここから ===== BASE_DIR = Path(__file__).resolve().parent PLAYLIST_FILE = BASE_DIR / "playlist.m3u" SOCKET_PATH = "/tmp/tanaradio-mpv.sock" # フェーズ4で使った既存3ボタンのGPIO番号 PIN_PLAY_PAUSE = 17 # 再生/一時停止 PIN_BACK = 27 # 戻る:前のエピソードへ PIN_NEXT = 22 # 次へ:次のエピソードへ # ===== 設定ここまで ===== mpv_process = None def check_playlist(): if not PLAYLIST_FILE.exists(): print(f"{PLAYLIST_FILE} が見つかりません。") print("先にフェーズ5Cのスクリプトを実行して playlist.m3u を作ってください。") sys.exit(1) text = PLAYLIST_FILE.read_text(encoding="utf-8", errors="ignore").strip() if not text: print(f"{PLAYLIST_FILE} は空です。") sys.exit(1) def start_mpv(): global mpv_process if os.path.exists(SOCKET_PATH): os.remove(SOCKET_PATH) print("mpvを起動します。") print(f"プレイリスト: {PLAYLIST_FILE}") mpv_process = subprocess.Popen( [ "mpv", "--no-video", f"--input-ipc-server={SOCKET_PATH}", str(PLAYLIST_FILE), ], cwd=str(BASE_DIR), ) # mpvのIPCソケットができるまで少し待つ for _ in range(50): if os.path.exists(SOCKET_PATH): print("mpv操作用ソケットを確認しました。") return time.sleep(0.1) print("mpv操作用ソケットが見つかりません。") print("mpvが起動できていない可能性があります。") def send_mpv_command(command): payload = json.dumps({"command": command}).encode("utf-8") + b"\n" try: with socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) as client: client.settimeout(1.0) client.connect(SOCKET_PATH) client.sendall(payload) # mpvからの返事を少し読む。 # これをしないと、mpv側に Broken pipe が出ることがある。 try: client.recv(4096) except socket.timeout: pass except FileNotFoundError: print("mpvのソケットが見つかりません。mpvが終了している可能性があります。") except ConnectionRefusedError: print("mpvに接続できませんでした。") except Exception as e: print("mpvへの命令送信でエラーが出ました。") print(e) def play_pause(): print("ボタン: 再生/一時停止") send_mpv_command(["cycle", "pause"]) def back_episode(): print("ボタン: 戻る:前のエピソードへ") send_mpv_command(["playlist-prev"]) def next_episode(): print("ボタン: 次へ") send_mpv_command(["playlist-next"]) def setup_buttons(): print("GPIOボタンを準備します。") print(f"再生/一時停止: GPIO{PIN_PLAY_PAUSE}") print(f"戻る: GPIO{PIN_BACK}") print(f"次へ: GPIO{PIN_NEXT}") play_button = Button(PIN_PLAY_PAUSE, pull_up=True, bounce_time=0.08) back_button = Button(PIN_BACK, pull_up=True, bounce_time=0.08) next_button = Button(PIN_NEXT, pull_up=True, bounce_time=0.08) play_button.when_pressed = play_pause back_button.when_pressed = back_episode next_button.when_pressed = next_episode # main側で保持するために返す return play_button, back_button, next_button def cleanup(signum=None, frame=None): global mpv_process print("\n終了します。") if mpv_process is not None and mpv_process.poll() is None: mpv_process.terminate() try: mpv_process.wait(timeout=5) except subprocess.TimeoutExpired: mpv_process.kill() if os.path.exists(SOCKET_PATH): try: os.remove(SOCKET_PATH) except OSError: pass sys.exit(0) def main(): signal.signal(signal.SIGINT, cleanup) signal.signal(signal.SIGTERM, cleanup) check_playlist() start_mpv() # 重要: # setup_buttons() の戻り値を変数に入れて保持する。 # これをしないと、環境によってはボタンが反応しなくなることがある。 buttons = setup_buttons() print("準備完了です。") print("3つの物理ボタンで操作できます。終了は Ctrl + C です。") pause() if __name__ == "__main__": main() 保存します。 Ctrl + O Enter Ctrl + X ステップ8:実行権限を付ける chmod +x tanaradio_5d.py ステップ9:5Dを実行する ./tanaradio_5d.py うまくいくと、次のような表示になります。 mpvを起動します。 プレイリスト: /home/ユーザー名/tanaradio5/playlist.m3u mpv操作用ソケットを確認しました。 GPIOボタンを準備します。 再生/一時停止: GPIO17 戻る: GPIO27 次へ: GPIO22 準備完了です。 3つの物理ボタンで操作できます。終了は Ctrl + C です。 この状態で、音声が流れ始めます。 ステップ10:3つのボタンを試す 10-1. 再生/一時停止ボタン GPIO17につながっているボタンを押します。 ターミナルに次のように出ればOKです。 ボタン: 再生/一時停止 音声が一時停止します。 もう一度押すと再開します。 10-2. 次へボタン GPIO22につながっているボタンを押します。 ボタン: 次へ 次のエピソードへ進めばOKです。 10-3. 戻るボタン GPIO27につながっているボ

    26 phút
  2. 2 ngày trước

    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 phút
  3. 3 ngày trước

    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 phút
  4. 4 ngày trước

    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 phút
  5. 6 ngày trước

    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 phút

Giới Thiệu

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

Có Thể Bạn Cũng Thích