Sphinxと拡張だけでPodcastを配信するには…という検証
Sphinxはドキュメントビルダーであり、広義の静的サイトジェネレーターです。
実際にSphinxには統一されたデザインにするためのテーマやテンプレートがありますし、拡張の中にもブログと関連要素を生成するためのものがあります。 [1]
さて、同じような(?)静的サイトコンテンツとして「ポッドキャスト」があります。
Sphinxは既存のコア機能+拡張機能でポッドキャスト配信サイトを構築できるでしょうか?
話の前提
ポッドキャストの配信にはいくつかの方式がありますが、「Sphinxからビルドしたサイトでポッドキャストの提供コンテンツを全て配信できる」状態を目指します。
そのため、今回はSoundCloudのような音楽配信サイトを直接的には使用しません。
また、記事タイトルに「検証」とあるように、「体裁としてサイト配信は出来る状態」までゴールとしています。
もしかしたら、この記事の勢いのまま2025年に自分がポッドキャストを配信してみるかもしれません。
「ポッドキャスト配信サイト」のモデル
ここ最近で自分が聞いているポッドキャストを紹介します(あくまでURLとサイトだけ)。
今回は、これらのサイトをモデルケースとして話を進めます。
「ポッドキャスト配信サイト」に必要なもの
「ポッドキャスト配信サイト」をもう少し分解した表現にすると、次のことができている必要がありそうです。
- コンテンツとして、「音声(オーディオファイル)」や「動画(ビデオファイル)」を提供している。
- 以降、メディアファイルと表現する。
- 配信しているコンテンツ全体を、ある程度一覧化された状態で公開している。
- 各コンテンツごとの再生用ページを提供しており、下記のことができる。
- メディアプレイヤー形式でのメディアの再生。
- メディアファイルでは分かりづらい補足情報などの掲載。
- 再生用ページにアクセスしなくともポッドキャストプレイヤーで更新を確認できるような、RSSフィードを公開している。
パッとだけ見た感じでは、静的サイトジェネレーターとして要求しがちな項目が並んでいます。
Sphinxから見た「ポッドキャスト配信サイト」
先ほど挙げた仕様をひとまずSphinxに落とし込んでみましょう。
-
メディアファイル:
ただの静的ファイル。_static
にでも置いてしまえば、勝手に配信対象になる。 -
一覧化されたページ:
いわゆるindex
(トップページ)。Sphinxにはtoctree
ディレクティブもあるため、エピソード(後述)をまとめて登録するだけで完成する。
概要情報も書いてしまえば良い。 -
エピソードページ:
各エピソードごとにドキュメントを用意する形式。補足情報などはShownoteとしてドキュメント扱いで書けば平気。メディアファイルの埋め込みも大丈夫…なはず。 -
RSSフィード:
最大の課題。コア機能では備えていないため、拡張を探すなどをする必要がある。
自身の知識を元に落とし込んでいくと、やはりコア機能+αで大半は実現可能であることが分かります。
RSSフィードをどうするか
落とし込んだ時に言葉を濁さざるを得なかったのが「RSSフィード」の存在です。
Googleが掲載しているポッドキャストRSSのガイドラインやAppleが掲載しているポッドキャストRSSの要求仕様などを参照してもらうと分かるのですが、そこそこ多くの項目が必要となってきます。
- ポッドキャスト自体の「名前」「概要」「サイトURL」「画像」「所有者/作成者」
- 各エピソードごとの「タイトル」「メディアファイルの情報(URL,フォーマット,サイズ)」
これらの多くは、Sphinxの基本機能ではあまり管理しない印象です。
そのため、どうにかしてビルド時に抽出するなりメタデータとして管理していく必要があります。
「Sphinx製ポッドキャストサイト」を実現するために
ここからは、ポッドキャストの仕様を踏まえつつ、もう少し具体的にSphinx上でのサイト構築を進めてみましょう。
エピソードの配信
ポッドキャストとして重要なのは、やはり「各エピソードの再生」でしょうか。
エピソードごとの固有情報は基本的にこの単位で管理する必要があります。
しかし、Sphinxの本分が「ドキュメントビルダー」である弊害と言っていいものか、メディア配信に関わる機能はそんなに強くありません。
エピソード概要の管理も通常のreStructuredText/Markdownでは案外面倒だったりします。
メディアファイルの扱い
Sphinxは画像と動画に対するディレクティブは存在するのですが、オーディオに対するディレクティブが存在しません。
そこで、以前のZenn記事でも紹介した自作の拡張を使い、
オーディオファイルを直接<audio>
タグとして埋め込めるようにします。
+ conf.py
+ episodes/
+ test/
+ idex.rst
+ talk.mp3
このようなファイル配置をして…
Episode 0
=========
.. audio:: talk.mp3
このような記述をすることで、無事にエピソードページにオーディオを埋め込むことが出来るようになります。 [2]
概要欄の構成
ポッドキャストの成立には「空ではない文字列」であれば良さそうなので、最終手段としては「それっぽいテキストを置く」だけでも成立自体はします。
また、素朴な手法として「doctreeで検出した一番最初の段落を【概要】とみなす」方法もあります。
せっかくなので、ここでもSphinx拡張の手を借りるパターンも紹介しておきます。
Sphinx製サイトのURLをSNSなどに掲載することは多く、そういうシーンのためにOGP対応することもよくあります。
このライブラリは、OGPの要素として使われる情報をサイト全体やドキュメントで管理しやすくなる機能を提供します。
:og:description: テスト配信です。
Episode 0
=========
このような記述を通して、概要欄の管理を「ページ内の定義」として実現できます。
拡張の機能をより深く利用することで、「サムネイル画像の管理」あたりもできるでしょう。
今回は「最低限の配信」に注力するため、ひとまず割愛します。
一覧ページ
最初に分解した通り、ポッドキャスト配信サイト上で「エピソードを自由に探して閲覧できる」状態が望ましいです。
実質的にサイトインデックスがその役割を担うのですが、Sphinxにはtoctree
ディレクティブという非常に便利な機能があります。
具体的に、このようなフォルダ構成をイメージしてみてください。
+ conf.py
+ index.rst
+ episodes/
+ 20241211/
| + index.rst
| + talk.mp3
+ 20241212/
+ index.rst
+ talk.mp3
index.rst
に簡素なtoctree
ディレクティブを記述するだけで、episodes
配下に所定のルールで管理した全エピソードを一覧として掲載できます。 [3]
テスト用ポッドキャスト
======================
About
-----
Episodes
--------
.. toctre::
:glob:
:reversed:
:maxdepth: 1
episodes/*/index
再臨:RSSフィードをどうするか
ここまでの情報を中心に、ポッドキャストとして必要な要素はそれなりに揃った状態が進行しています。
最後にRSSフィードをどうするかを考えていきましょう。
まず、大事な点としてSphinx本体にRSSフィードを出力する機能は一切存在しません。
しかも、PyPI内で検索してもSphinxに関連するようなpodcast系ライブラリも無さそうです。
というわけで、基本的にここは自作する必要があります。
Sphinxのイベントフック
RSSフィードはその性質上、全てのコンテンツが出揃ったタイミングで初めて生成できます。
Sphinxには多くのタイミングで処理を差し込めるようになっているので、今回は「全ての処理が一通り終わったらRSSフィードを生成する」ようにしていきます。
def setup(app):
app.connect("build-finished", generate_feed)
app.connect
が処理を差し込むためのメソッドですが、build-finished
に登録することで「メインの処理が終わったら実行する」処理を指定できます。
フィードを生成する
Pythonのサードパーティライブラリの中にはそのままズバリと言えるfeedgen
というライブラリがあります。
RSSフィードに必要な【フィード本体】と【アイテム】をオブジェクトとして登録管理を行い、最終的にいくつかの形式を選択してファイル出力してくれる便利なものです。
from feedgen.feed import FeedGenerator
from sphinx.application import Sphinx
def generate_podcast(app: Sphinx, exc: Exception | None = None):
# ... 一部省略 ...
fg = FeedGenerator()
# load_extensionでポッドキャスト用の構成をロードできる
fg.load_extension("podcast")
# ここからはポッドキャスト全体のアイテムに関する設定
fg.language("ja")
fg.id(feed.link)
fg.title(feed.title)
fg.link(href=feed.link, rel="alternate")
fg.description("Demo")
# ここからはエピソード単位の処理
for entry in entries: # episodes分だけアイテムを収集しておく
fg_entry = fg.add_entry()
fg_entry.id(entry.link)
fg_entry.link(href=entry.link)
fg_entry.title(entry.title)
fg_entry.description(entry.summary)
fg_entry.pubDate(entry.updated.strftime("%a, %d %b %Y %H:%M:%S +0900"))
fg_entry.enclosure(
entry.media_url,
type="audio/mp3",
)
# ここからは出力処理(load_extensionのおかげで要素名などは適切なものが採用される)
out_path = Path(app.outdir) / "podcast.xml"
fg.rss_file(out_path, pretty=True)
上記のコードは、先ほどの例でapp.connect
の対象として登録できる出力処理を実装してみたものです。
見ての通り「とにかくRSSフィードの要素を追記して最終的にファイル出力する」という簡素な実装で目的が実現できます。
「エピソード」はどれなのか
さて、先程のコードで「一部省略」でスルーした箇所があります。
「entries
に格納するルール」を定めていません。
幸いSphinxの処理中に使われるapp
は、app.env.all_docs
というプロパティ経由で登録している全ての管理対象ページの情報を取得できます。
ドキュメント管理の仕方で思いつく範囲では、下記のようなルールが出せそうです。いろいろと考えてみるとよいでしょう。
- 単純に
audio
を含むものを全て抽出する - ドキュメントのパスが特定の名称で始まるものを拾う
- 専用のディレクティブを用意して、このディレクティブを含むもののみを抽出する
entries
に格納する時点でメディアファイルのサイズなども必要となるため、基本的に上記で挙げたルールは前者ほど後続作業が楽です。
実際に作ってみた
以上を踏まえて、試しに作ってみた「Sphinx製のポッドキャスト配信サイト」がこちらです。
サイト内の案内にしたがってポッドキャストURLをコピーしてアプリなどに登録すると、配信済みのメディアを再生できます。
結論としては、「RSSフィードさえ作れるなら、拡張を組み合わせてサイトを作れる」となります。
課題とか
検証としては以上なのですが、実際に作ったりするうえで当然ながら課題は残っています。
今回は大雑把に纏めますが、もし仮にSphinxでのポッドキャスト配信サイトをちゃんと運用するなら避けがたいポイントではあります。
- メディアを本当にGit管理するべきか。
- 外部URLを参照させる場合、どのように管理すべきか。(特にファイルサイズ)
- サイト上でのエピソード再生はちゃんとしたコンポーネントライブラリを使うほうが良い。
- エピソードと一覧以外にも管理したほうが良いページの存在。
- ブログや通常のドキュメントと統合管理したいとき。
- 配信サービスとの連携。
- アクセス情報の統計管理。
Discussion