Skip to main content

rss2podcast

Turn an RSS feed into a listenable podcast. Fetches each entry, extracts the article body, sends it to gopipertts for Piper TTS synthesis, and publishes the resulting MP3s as a standards-compliant podcast RSS feed.

How it works

  1. Parse the source RSS feed.
  2. For each entry not yet processed (tracked in state.json):
    • Fetch the linked article and extract the body with trafilatura (falls back to RSS summary).
    • Strip HTML to plain speakable text.
    • POST to gopipertts /api/tts, save the MP3 to disk.
    • Append the entry to state.json immediately (crash-safe).
  3. Rewrite feed.xml from the full state with feedgen (iTunes namespace).
  4. Write style.xsl to the output root so browsers render feeds as a readable HTML page.

Install

uv sync

Or install as a tool:

uv tool install .

Usage

Single feed (CLI)

EFF Updates (equivalent to the config.sample.yaml entry):

uv run rss2podcast --feed-url https://www.eff.org/rss/updates.xml --feed-name "EFF Updates" --output-dir podcasts --url-root https://podcasts.example.com --tts-endpoint http://localhost:8080/ --voice en_US-amy-medium --description "EFF updates, narrated by Piper TTS" --author EFF --limit 5

Ars Technica:

uv run rss2podcast --feed-url https://feeds.arstechnica.com/arstechnica/index --feed-name "Ars Technica" --output-dir podcasts --url-root https://podcasts.example.com --tts-endpoint http://localhost:8080/ --voice en_US-amy-medium --description "Ars Technica articles, narrated by Piper TTS" --author "Ars Technica" --limit 1 --prune-xpath '//div[contains(@class,"author-bio")]' --merge-xpath '//div[contains(@class,"post-content")]'

Hackaday:

uv run rss2podcast --feed-url https://hackaday.com/blog/feed/ --feed-name Hackaday --output-dir podcasts --url-root https://podcasts.example.com --tts-endpoint http://localhost:8080/ --voice en_US-amy-medium --description "Hackaday articles, narrated by Piper TTS" --author Hackaday --prune-xpath '//div[contains(@class,"author-bio")]' --prune-xpath '//section[contains(@class,"related")]'

Multi-feed (YAML)

uv run rss2podcast --config config.yaml

See config.sample.yaml for a fully annotated example.

Output layout

{output_dir}/
  style.xsl
  {feed-slug}/
    state.json
    feed.xml
    2026-04-16-some-post-title-abc12345.mp3
    ...

Serve {output_dir} over HTTP at {url_root} and subscribe to {url_root}/{feed-slug}/feed.xml in a podcast app. When style_rss_feed is enabled (the default), opening feed.xml directly in a browser renders it as a human-readable HTML page with an inline audio player.

Scheduling

Designed to run as a cron / scheduled job. Re-runs are idempotent — entries already in state.json are skipped. Long runs are fine (no time limits, state is committed after each entry).

CLI reference

Feed selection

Flag Default Description
--config PATH — YAML config file; enables multi-feed mode (--limit may still be used to override the YAML value)
--feed-url URL — Source RSS feed URL (single-feed mode)
--feed-name NAME — Feed display name; also determines the output subdirectory slug
--output-dir PATH — Directory to write state.json, feed.xml, and MP3s
--url-root URL — Public base URL where output-dir is served

TTS

Flag Default Description
--tts-endpoint URL http://localhost:8080 gopipertts base URL
--voice MODEL en_US-amy-medium Piper voice model name

Feed metadata

Flag Default Description
--description TEXT — Channel description
--author TEXT — Channel author
--image-url URL — Channel artwork URL

Processing

Flag Default Description
--limit N — Keep only the N newest articles per feed; entries that roll out of the window are evicted from state and removed from the podcast feed
--save-text off Persist raw/clean text in state.json (useful for debugging)
--no-fetch off Skip external crawling; use only RSS content/description
--no-style-rss-feed style on Disable XSLT styling; skip style.xsl and omit the processing instruction from feed.xml
--no-github-ribbon ribbon on Hide the "Fork me on GitHub" ribbon in the styled feed page

Extraction tuning

These control how trafilatura extracts article text from fetched pages. Defaults are tuned for broad recall; tighten them for noisy feeds.

Flag Default Description
--no-favor-recall recall on Disable recall-biased extraction; fall back to trafilatura's default balanced mode
--favor-precision off Prefer fewer, higher-confidence text blocks; reduces sidebar/bio bleed-through at the cost of occasionally truncating real content
--include-comments off Include comment sections in extracted text
--include-tables off Include table content
--deduplicate off Remove duplicate text blocks (useful for feeds that repeat headlines or teasers)
--fast-extraction off Skip fallback extractors; faster but may miss content on harder pages
--prune-xpath XPATH — XPath expression to remove from the DOM before extraction; repeatable. Use this to surgically excise author bios, related-article widgets, cookie banners, etc.
--merge-xpath XPATH — XPath matching split article containers; children of all matches are concatenated into the first match before extraction. Repeatable. Use this when a site breaks the article body across sibling containers around mid-article ads (e.g. Ars Technica's split post-content divs), which otherwise causes trafilatura to truncate at the first ad break.

--prune-xpath examples:

# Remove Ars Technica author bio
--prune-xpath '//div[contains(@class,"author-bio")]'

# Remove multiple sections
--prune-xpath '//aside' --prune-xpath '//div[@id="related"]'

--merge-xpath example:

# Ars Technica splits the article body across multiple <div class="post-content">
# siblings separated by ad wrappers; merging them avoids mid-article truncation.
--merge-xpath '//div[contains(@class,"post-content")]'

YAML config reference

Top-level keys:

output_dir: /var/www/podcasts        # required
url_root: https://podcasts.example.com  # required
tts_endpoint: http://gopipertts:8080 # default: http://localhost:8080
limit: 5                             # optional: process only N newest per feed
save_text: false                     # optional: persist text in state.json
no_fetch: false                      # optional: skip external crawling globally
style_rss_feed: true                 # default: true; set to false to disable XSLT browser rendering
show_github_ribbon: true             # default: true; set to false to hide the "Fork me on GitHub" ribbon

Per-feed keys:

feeds:
  - name: My Feed        # required
    url: https://...     # required

    # TTS
    voice: en_US-amy-medium   # default: en_US-amy-medium

    # Feed metadata
    description: "..."
    author: "..."
    image_url: "https://..."

    # Processing (override top-level defaults)
    limit: 5             # optional: overrides top-level limit for this feed

    # Extraction tuning
    favor_recall: true         # default: true
    favor_precision: false     # default: false
    include_comments: false    # default: false
    include_tables: false      # default: false
    deduplicate: false         # default: false
    fast_extraction: false     # default: false
    prune_xpath:               # default: null
      - '//div[contains(@class,"author-bio")]'
      - '//section[@id="related"]'
    merge_xpath:               # default: null
      - '//div[contains(@class,"post-content")]'

favor_recall and favor_precision are independent trafilatura flags. Setting both to true is valid; trafilatura will apply both biases simultaneously.

Tests

uv sync --extra test
uv run pytest

Metadata

Release files for rss2podcast 0.19.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for rss2podcast 0.19.0
File Size Uploaded
rss2podcast-0.19.0.tar.gz 13.3 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for rss2podcast 0.19.0
File Interpreter ABI Platform
rss2podcast-0.19.0-py3-none-any.whl Python 3 none any Details

Total release size: 30.8 kB

Release files / rss2podcast-0.19.0.tar.gz

Download URL rss2podcast-0.19.0.tar.gz
Size 13.3 kB
Tags Source
SHA-256 checksum
How to use checksums
89dac6c2bcce9b9843eb41a3cbe2838d7fecbfbe4a744900a4239260500c1283
BLAKE2b-256 checksum
How to use checksums
21ca5b7e1a16912034bbd21443c532c43d50c50ecf895b2e7dc4ea2c0241f7d2
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/6.1.0 CPython/3.13.12

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Apr 21, 2026.

Transparency log

Release files / rss2podcast-0.19.0-py3-none-any.whl

Download URL rss2podcast-0.19.0-py3-none-any.whl
Size 17.5 kB
Tags Python 3
SHA-256 checksum
How to use checksums
1916e502af8ebdf716712ad9e230aff55f685c9486b9d5a9057bd00e727693ab
BLAKE2b-256 checksum
How to use checksums
6544adadd33294d9d45384db14c617d9e8047dd5d103162a31e0540bb2ecb6e0
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/6.1.0 CPython/3.13.12

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Apr 21, 2026.

Transparency log

Release history Release notifications | RSS feed

This release

0.19.0 This release

2 release files

0.18.0

2 release files

0.17.0

2 release files

0.16.0

2 release files

0.15.0

2 release files

0.14.0

2 release files

0.13.0

2 release files

0.12.0

2 release files

0.11.0

2 release files

0.10.0

2 release files

0.9.0

2 release files

0.8.0

2 release files

0.7.0

2 release files

0.6.0

2 release files

0.5.0

2 release files

0.4.0

2 release files

0.3.0

2 release files

0.2.0

2 release files

0.1.1

2 release files

0.1.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page