1. TanaRadio
  2. 372 日記 | 複数RSSで過去3日..
372 日記 | 複数RSSで過去3日分を古い順に再生
2026-07-02 12:13

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

spotify apple_podcasts

複数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  / 番組A / エピソードタイトル
https://...
# 2026-07-01  / 番組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  / ...
# 2026-06-30  / ...
# 2026-07-01  / ...

ステップ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

12:13

コメント

スクロール