TanaRadio

TanaRadio

Download on the App Store

TanaRadio episodes

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

    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で開く
    30 min
  • 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につながっているボタンを押します。
    ボタン: 戻る:前のエピソードへ
    前のエピソードへ戻ればOKです。
    ただし、プレイリストの最初のエピソードを再生しているときは、それ以上前へ戻れないので、動きがわかりにくい場合があります。
    その場合は、先に「次へ」を1〜2回押してから「戻る」を試してください。
    ステップ11:終了する
    終了するときは、ターミナルで次を押します。
    Ctrl + C
    次のように出ればOKです。
    終了します。
    mpvも一緒に終了します。
    ステップ12:今回の成功条件
    フェーズ5Dの成功条件は、次の4つです。
    1. フェーズ5Cの playlist.m3u が作れている
    2. tanaradio_5d.py を実行すると mpv が起動する
    3. 再生/一時停止ボタンで pause / resume ができる
    4. 戻る・次へボタンで playlist-prev / playlist-next ができる
    ここまでできれば、5Dは成功です。
    これはかなり大きいです。
    5Cまでは「RSSから音声を集める仕組み」でしたが、5Dでついに「物理ボタンで声の流れを操る装置」になります。ラジオ感が、ぐっと前に出てきます。

    #声日記 #TanaRadioPi

    LISTENで開く
    26 min
  • 373 講義 | 「ほどよい」技術

    この音源は、最近収録した今学期の「科学と技術の社会史」の講義(ビデオ教材)の最後の部分です。現在製作中のTanaRadio Piの話も出てきます。

    #講義 #TanaRadioPi

    LISTENで開く
    20 min
  • 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のエピソードを集めて、公開日時で並べ替えているわけです。
    ここが5Cの肝です。
    ステップ8:mpvで再生する
    mpv playlist.m3u
    音が出れば成功です。
    mpvの基本操作はこれです。
    スペースキー:一時停止/再開
    Enter:次のエピソードへ
    q:終了
    再生される順番が、過去3日分の古い順になっているか、耳でざっくり確認してください。
    厳密に確認したい場合は、playlist.m3u のコメント行を見ます。
    grep '^# 20' playlist.m3u
    次のように日時が古い順に並んでいればOKです。
    # 2026-06-29 09:00 / ...
    # 2026-06-30 14:20 / ...
    # 2026-07-01 08:30 / ...
    ステップ9:RSSを少しずつ増やす
    最初の2本でうまく行ったら、feeds.txt にRSSを追加します。
    nano feeds.txt
    例です。
    # TanaRadio
    https://listen.style/p/xxxxx/rss
    # 気になる番組1
    https://listen.style/p/yyyyy/rss
    # 気になる番組2
    https://listen.style/p/zzzzz/rss
    このスクリプトでは、空行と # で始まる行は無視されます。
    なので、コメントを書いておけます。
    保存したら再実行します。
    python3 make_playlist_5c.py
    mpv playlist.m3u
    RSSを増やすと、かなり「フォロー中ラジオ」感が出てきます。
    この段階でもう、TanaRadio Piは単なる1番組再生機ではなくなります。
    ステップ10:過去3日分が空だった場合
    次のように出ることがあります。
    過去3日分: 0 件
    エラーが発生しました。
    過去3日分のエピソードが見つかりませんでした。
    これは故障ではありません。
    原因は単純で、登録したRSSに最近のエピソードがないだけです。
    その場合は、テスト用に日数を一時的に増やします。
    nano make_playlist_5c.py
    この部分を探します。
    DAYS = 3
    一時的にこうします。
    DAYS = 30
    保存して、再実行します。
    python3 make_playlist_5c.py
    これでエピソードが出てくるなら、スクリプトは正常です。
    テストが終わったら、また戻します。
    DAYS = 3
    ステップ11:成功条件
    フェーズ5Cの成功条件は、次の4つです。
    1. feeds.txt にRSS URLを複数本書けた
    2. make_playlist_5c.py を実行できた
    3. 複数RSS由来のエピソードが playlist.m3u に入った
    4. mpv playlist.m3u で連続再生できた
    特に重要なのは3です。
    複数RSS
    ↓
    1つのプレイリスト
    ↓
    過去3日分
    ↓
    古い順
    これができれば、5Cは成功です。
    5Cでできるようになったこと
    5Cが成功すると、TanaRadio Piはこうなります。
    1つの番組を聴く装置
    から、
    複数番組をまとめて流す装置
    に変わります。
    これは地味ですが、かなり大きな変化です。
    もう「ポッドキャストを1本ずつ選んで聴く」のではなく、「登録してある声が時間順に流れてくる」状態になります。
    ここで、かなりラジオらしくなります。
    次のフェーズ5Dでやること
    次の5Dでは、フェーズ4で作った既存の3ボタンを、この新しい playlist.m3u 再生に結びつけます。
    予定としては、まず単純にこうします。
    再生/一時停止ボタン:pause切り替え
    戻るボタン:前のエピソードへ
    次へボタン:次のエピソードへ
    構想メモでは戻るボタンに「1回押し・2回押し・長押し」の区別も出ていますが、5Dの最初ではまだ入れなくてよいです。
    まずは「物理ボタンでプレイリストを前後できる」だけで十分です。
    今回の5Cは、ひとことで言えばこうです。
    複数の声を、1本の時間の流れにする。
    ここまで来ると、TanaRadio Piはかなり「声の本棚」の入口に立っています。これは順調です。

    #声日記 #TanaRadioPi

    LISTENで開く
    13 min
  • 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つが合っていれば、5Bの中心部分は成功です。
    ステップ7:playlist_5b.m3u の中身を確認する
    次を入力します。
    head playlist_5b.m3u
    次のように表示されればOKです。
    #EXTM3U
    # 2026-06-28 08:00 JST | エピソードタイトルA
    https://...
    # 2026-06-29 09:00 JST | エピソードタイトルB
    https://...
    件数も確認できます。
    grep -v '^#' playlist_5b.m3u | sed '/^$/d' | wc -l
    これで表示される数字が、playlist_5b_info.txt の「今回プレイリストに入れる数」と同じならOKです。
    ステップ8:mpvで再生する
    いよいよ再生します。
    mpv playlist_5b.m3u
    音が出れば成功です。
    mpvの基本操作は5Aと同じです。
    スペースキー:一時停止/再開
    Enter:次へ
    q:終了
    聴いてみて、古いエピソードから順に流れているか確認してください。
    ステップ9:うまくいったら通常名にコピーする
    5B版がうまく動いたら、これを通常の playlist.m3u として使えるようにしておきます。
    cp playlist_5b.m3u playlist.m3u
    その後は、いつものように次で再生できます。
    mpv playlist.m3u
    ただし、今後しばらくは確認しやすいので、
    mpv playlist_5b.m3u
    のまま使っても構いません。
    ステップ10:5Bの成功条件
    フェーズ5Bの成功条件は、次の4つです。
    1. make_playlist_5b.py を作れた
    2. playlist_5b.m3u が作成された
    3. playlist_5b_info.txt で「過去3日分・古い順」を確認できた
    4. mpv playlist_5b.m3u で再生できた
    この4つができれば、5Bは成功です。
    よくあるエラー
    1. 過去3日分のエピソードが見つかりませんでした
    これは、必ずしも失敗ではありません。
    原因は単純で、RSS内に本当に過去3日分のエピソードがない可能性があります。
    まず確認します。
    cat playlist_5b_info.txt
    もし「RSSから取得した音声付きエピソード数」はあるのに、「今回プレイリストに入れる数」が0なら、日付条件に合うエピソードがなかったということです。
    テストだけしたい場合は、スクリプト内のここを一時的に変えます。
    DAYS_BACK = 3
    たとえば30日にします。
    DAYS_BACK = 30
    保存して、もう一度実行します。
    python3 make_playlist_5b.py
    これで動けば、スクリプト自体は正常です。最後に DAYS_BACK = 3 に戻してください。
    2. 公開日時なし/日時解析失敗でスキップ が多い
    RSS内の pubDate がうまく読めていない可能性があります。
    LISTENの通常RSSなら、おそらく大丈夫だと思います。
    ただし、別サービスのRSSでは日付形式が少し違う場合があります。
    この段階では、まずLISTENの公開RSSで試すのが安全です。
    3. 音声URLなしでスキップ が多い
    これは、RSSに音声ファイルのURLが入っていない場合です。
    原因としては次が考えられます。
    RSS URLが番組配信用ではない
    限定配信で取得できない
    RSS形式が特殊
    5Bではまだ公開RSSだけで進めましょう。限定配信や購入済み音声は、ここに混ぜない方がいいです。沼に足を入れるのは、長靴を履いてからです。
    4. 実行するとまた import: コマンドが見つかりません と出る
    その場合は、次のように実行してください。
    python3 make_playlist_5b.py
    ./make_playlist_5b.py ではなく、python3 を前につけます。
    それでもダメなら、ファイルの先頭行を確認します。
    head -n 3 make_playlist_5b.py
    先頭がこうなっていればOKです。
    #!/usr/bin/env python3
    import urllib.request
    import xml.etree.ElementTree as ET
    もし #!/usr/bin/env python3 より前に余計な文字や空行がある場合は、nano で直してください。
    nano make_playlist_5b.py
    今回はここで止めてよいです
    5Bではここまでで十分です。
    RSSを読む
    ↓
    公開日時を見る
    ↓
    過去3日分だけ選ぶ
    ↓
    古い順に並べる
    ↓
    mpvで再生する
    これができれば、次の5Cで「RSSを複数本にする」準備が整います。
    ここまで来ると、TanaRadio Piはかなり「フォロー中ラジオ」の原型に近づいてきます。まだボタンもLEDも増えていませんが、中身は確実にラジオらしくなっています。

    #声日記 #TanaRadioPi

    LISTENで開く
    14 min
  • 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 min
  • 369 日記 | フェーズ5の計画を立てる

    フェーズ5の計画を立てました。

    5A:RSS 1本から playlist.m3u を作る
    5B:過去3日分・古い順にする
    5C:RSS複数本にする
    5D:既存3ボタンで mpv プレイリストを操作する
    5E:棚ファイルを作り、コマンドで棚を切り替える
    5F:棚選びボタン+LED1を追加する
    5G:再生モードボタン+LED2を追加する
    5H:必要になったら再生履歴を作る
    5I:最後に音量ロータリーエンコーダーを追加する

    #声日記 #TanaRadioPi

    LISTENで開く
    14 min
  • 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 - <

    #声日記 #TanaRadioPi

    LISTENで開く
    24 min
  • 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 min

About TanaRadio

From the publisher's feed

私立工科系大学教員(科学史)の日々の出来事や雑感。日記,メモ,ブログなど