1. TanaRadio
  2. 371 日記 | 過去3日分を古い順..
371 日記 | 過去3日分を古い順に再生
2026-07-01 13:10

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

spotify apple_podcasts

あるポッドキャストの過去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  +0900
    # Tue, 30 Jun 2026  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  JST
対象開始: 2026-06-27  JST

RSSから取得した音声付きエピソード数: 10 件
音声URLなしでスキップ: 0 件
公開日時なし/日時解析失敗でスキップ: 0 件
今回プレイリストに入れる数: 3 件

今回入れるエピソード:

1. 2026-06-28  JST | エピソードタイトルA
2. 2026-06-29  JST | エピソードタイトルB
3. 2026-06-30  JST | エピソードタイトルC

ここで見るべき点は2つです。

1. 対象開始が「約3日前」になっているか
2. エピソードが古い順に並んでいるか

この2つが合っていれば、5Bの中心部分は成功です。

ステップ7:playlist_5b.m3u の中身を確認する

次を入力します。

head playlist_5b.m3u

次のように表示されればOKです。

#EXTM3U
# 2026-06-28  JST | エピソードタイトルA
https://...
# 2026-06-29  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

サマリー

TanaRadio Pi製作のフェーズ5Bでは、過去3日分のポッドキャストエピソードを古い順に再生する機能が実装されました。これにより、ラジオのように時系列でエピソードを追いかける「追いつき再生」が可能になります。Pythonスクリプトを作成・実行し、生成されたプレイリストが期待通りに動作することを確認、無事成功を収めました。

フェーズ5Bの目標設定
はい、では、TanaRadio Pi制作ですけれども、フェーズ5Bですね。今日は5Bをやりたいと思います。
例によりまして、ChatGPTのマニュアル通りに行きますが、まずステップ1は作業フォルダに移動する。
その前にですね、忘れていました。
5Bは何を目標とするかと言いますと、こういうことです。
TanaRadioの過去3日分だけを古い順に並べるというものです。
5Aではですね、TanaRadioをともかく新しい方から順に再生するということが実現できましたが、
実際にラジオを聴くときには、それだと時間順が逆になりますので、
とりあえず過去3日分を古い方から新しい方へ向かって順に聴くという風にすればですね、
だいたい時系列で聴けるのではないかと。
もしすでに聴いたものがあれば、それをスキップするボタンを押して聴くということができますよね。
ということで、このフェーズ5B、過去3日分だけを古い順に並べるというのをやりたいのですが、
スクリプトの作成と保存
まずステップ1は作業用フォルダに移動するということで、
ターミナルを開きまして、cd ~/tanaradio5 で、tanaradio5というフォルダに移動しました。
確認ということで、pwdでフォルダへのパスが出まして、lsをやりますとフォルダ内のファイルが出てきます。
これはOKですね。
では次にステップの2、新しくPythonのファイルを作るということですね。
まず、エディターのnanoを使いまして、make_playlist_5b.pyというファイルを作り開きます。
そして、この中にPythonのコードを入れるわけですが、これはコピー&ペーストでいきたいと思います。
ChatGPTの今見ているマニュアルを今出していますけれども、どのコードを貼るんですかね。
これですね。
貼り付ける部分、一番最初の行を忘れないようにして、ずっと反転させてスクロールを今させていますが、結構長いコードですね。
全部反転させましたので、コピーをし、エディターの方でペーストです。
これをCtrl-O、Enter、Ctrl-Xで保存ですね。
保存ができましたので、次はこのファイルを実行できるようにします。
スクリプト実行と生成ファイルの確認
chmod +x ...
これでできたはずですが、このPythonを実行します。
python3というのを最初に入れて、これでRSSを取得して、プレイリストを作成と。
それから、infoというテキストファイルも作っています。
対象エピソードは2件となっていますね。
これを確認します。
lsコマンドでやりますと、ファイルが結構たくさんできてきています。
語尾というのがついているファイルが大事だということですね。
その内容をcatコマンドで見てみます。
playlist_5b_info.txtですね。
過去3日分で音声付きのエピソードが391、今回プレイリストに入れるものは2件ということで、
6月29日と6月30日のものが2件ということになっています。
6月28日が3日前なんですけど、
6月28日の今現在ですね、今日は7月の1日ですが、
8時半頃なんですね。
3日前ですと、ちょうど3日前の夜の8時半よりも後ですので、
それよりも前に配信したものはここに含まれないということで、
3日分なんですけども、2日分がここにリストされているということですね。
これで、期待通りの挙動をしているという感じです。
古い順に並んでいるかというと、並んでいますね。
次にプレイリストの中身を見ます。
headというコマンドで、
中身を見ますと、
6月29日の声日記と30日の声日記、
タイトルとMP3ファイルのURLが出ていますので、
これも期待通りです。
では次に件数も確認できますということで、
2件なので目で見れば分かりますが、
これをgrepでもって、多ければこのgrepが効くんだと思うんですけど、
とりあえず2件ですがやってみますね。
これはコピー&ペーストできます。
コピーして、グレップして実行。
2と出ましたので、これも想定通りです。
mpvでのプレイリスト再生検証
では最後いよいよ再生ですね。
MPVコマンドを使います。
playlist_5b.m3uで再生できるはずです。
スペースキーで一時停止再開、エンターキーで次へということで、
アルファベットのqのキーで終了という前と同じ操作ができます。
では再生してみます。
このコマンドを実行ですね。
今しゃべってましたけども、これ実家に来ているという話でしたね。
実家に来ているのは月曜日ですので29日ですので古い方が再生されているということが分かります。
もう一回再生させてエンターで次に行くかどうか。
実家から自宅に帰ってきたということで、これは火曜日の30日の配信だということが分かりましたので、
過去3日の古い方から順に再生するということが無事うまくいきました。
フェーズ5Bの成功とフェーズ5Cへの展望
この後は何をするかということですが、
特にもうこれでうまくいったのでやることはないですね。
では今日は順調に済みました。
この後、次のフェーズ5Cでは何をやるかというと、
RSSを複数本にするということなので、
今、RSSはTanaRadioのRSSだけ入れています。
一本ですね。
これを他のポッドキャストのRSSも加えて複数本にしまして、
それを束ねて過去3日分古い方から順に再生などということができるようにするというのが次の段階ですね。
これはいくつかのポッドキャストをフォローしていて、
フォローしているものを古い順から聞くということがリスのサイトですとできますけれども、
それをこのTanaRadio Piでも実現しようということなわけですね。
明日はこれに挑戦したいと思います。
それではまた。
13:10

コメント

スクロール