PodcastRSS
An Elixir library for generating podcast RSS feeds in RSS 2.0 format with iTunes and Podcast Index namespace support.
PodcastRSS provides a pipeline-friendly API for creating podcast feeds with sensible defaults and support for modern podcast features. It focuses on simplicity and readability while supporting all the core requirements for podcast distribution.
Features
- RSS 2.0 Compliant - Generates valid RSS feeds for podcast distribution
- iTunes Support - Built-in support for iTunes podcast tags
- Podcast Index - Support for modern Podcast Index namespace features
- Pipeline API - Clean, chainable functions for easy feed construction
- Fast XML Generation - Uses Saxy for efficient XML serialization
Installation
Add podcast_rss to your list of dependencies in mix.exs:
def deps do
[
{:podcast_rss, "~> 0.5.0"}
]
end
Quick Start
alias PodcastRSS.Episode
# Create a podcast feed
channel = PodcastRSS.new()
|> PodcastRSS.title("Retro Gaming Chronicles")
|> PodcastRSS.link("https://example.com")
|> PodcastRSS.description("Exploring the golden age of video games")
|> PodcastRSS.self_link("https://feeds.example.com/rss")
|> PodcastRSS.image("https://example.com/cover.png")
|> PodcastRSS.language("en-us")
|> PodcastRSS.category("Technology")
|> PodcastRSS.explicit(false)
# Create an episode (inherits channel namespaces)
episode = channel
|> PodcastRSS.new_episode()
|> Episode.title("The Birth of the NES")
|> Episode.description("How Nintendo revolutionized home gaming in 1985")
|> Episode.enclosure("https://example.com/episodes/nes-birth.mp3", 45678901, "audio/mpeg")
|> Episode.guid("retro-001-nes-birth", permalink: false)
|> Episode.pub_date(~U[2025-10-04 08:00:00Z])
|> Episode.chapter(0, "Introduction")
|> Episode.chapter(180, "Main Topic", url: "https://example.com")
|> Episode.chapter(365, "Bye")
feed_xml = channel
|> PodcastRSS.add_episode(episode)
|> PodcastRSS.to_xml()
IO.puts(feed_xml)
Usage
Creating a Channel
The PodcastRSS.new/0 function creates a channel with useful defaults:
- Pre-registers common namespaces (iTunes, Podcast Index, Atom)
- Sets language to "en-us"
- Adds iTunes block and complete tags with sensible defaults
channel = PodcastRSS.new()
|> PodcastRSS.title("My Podcast")
|> PodcastRSS.description("A fascinating podcast about interesting topics")
|> PodcastRSS.link("https://example.com")
|> PodcastRSS.self_link("https://feeds.example.com/rss")
|> PodcastRSS.image("https://example.com/cover.jpg")
Adding Episodes
Episodes are created using PodcastRSS.new_episode/1 which inherits the channel's namespaces:
alias PodcastRSS.Episode
episode = channel
|> PodcastRSS.new_episode()
|> Episode.title("Episode 1: Getting Started")
|> Episode.link("https://example.com/episodes/1")
|> Episode.description("Our first episode covers the basics")
|> Episode.enclosure("https://cdn.example.com/episode1.mp3", 98765432, "audio/mpeg")
|> Episode.guid("episode-001", permalink: false)
|> Episode.pub_date(~U[2025-10-04 08:00:00Z])
channel = PodcastRSS.add_episode(channel, episode)
Pass permalink: false when the GUID is not a URL. Without it, <guid> has
no isPermaLink attribute, which RSS reads as a permalink. pub_date/2
takes a DateTime and writes it in RFC 2822 format.
Episode.link/2 sets the episode's website URL as <link> on the item. The
element is omitted when unset.
Episodes created this way automatically inherit the channel's registered namespaces, allowing immediate use of namespace-prefixed custom fields like itunes:duration or podcast:soundbite.
The psc namespace for Podlove Simple Chapters is added to the feed during XML
generation when an episode has chapters. Feeds without chapters do not declare
it by default. This also works with the low-level PodcastRSS.Channel API.
Categories
Set iTunes categories and subcategories:
channel = PodcastRSS.category(channel, "Arts")
# or with subcategory
channel = PodcastRSS.category(channel, "Arts", "Design")
Explicit Content
Apple Podcasts requires itunes:explicit:
channel = PodcastRSS.explicit(channel, false)
Author and Copyright
channel = channel
|> PodcastRSS.author("Müller & Söhne")
|> PodcastRSS.copyright("© 2026 Müller & Söhne")
These write itunes:author and copyright on the channel when set. The matching
PodcastRSS.Channel.author/2 and PodcastRSS.Channel.copyright/2 functions are
also available.
Podcast Owner
channel = PodcastRSS.owner(channel, "Müller & Söhne", "owner@example.com")
This writes itunes:owner with itunes:name and itunes:email child elements.
The owner is omitted when unset. PodcastRSS.Channel.owner/3 is also available.
Moving a Feed
Set the new feed URL when moving your podcast:
channel = PodcastRSS.new_feed_url(channel, "https://example.com/new-feed.xml")
This writes itunes:new-feed-url on the channel. It is omitted when unset.
Text Escaping
Pass plain text everywhere. PodcastRSS escapes &, <, and > in element
text and attribute values, so do not escape them yourself.
Custom Fields and Namespaces
Add custom fields for extended functionality:
channel = PodcastRSS.new()
|> PodcastRSS.register_namespace("dc", "http://purl.org/dc/elements/1.1/")
|> PodcastRSS.custom_field("dc:creator", "John Doe")
Use add_custom_field/4 for elements that may repeat, such as podcast:person:
channel = PodcastRSS.new()
|> PodcastRSS.add_custom_field("podcast:person", "Eric Teubert",
attributes: %{"role" => "host", "href" => "https://ericteubert.de/"})
|> PodcastRSS.add_custom_field("podcast:person", "Martin Fischer",
attributes: %{"role" => "audio editor", "group" => "audio post-production"})
PodcastRSS.Channel.add_custom_field/4 and PodcastRSS.Episode.add_custom_field/4
work the same way. They accept the same attributes, cdata, and namespace_uri
options as custom_field/4. A namespace_uri registers the field's namespace
prefix for later fields, too.
custom_field/4 replaces all existing custom fields with the same name,
then appends the new field. For example, PodcastRSS.custom_field(channel, "itunes:block", "Yes") overrides the default set by PodcastRSS.new/0.
Starting with 0.5.0, custom_fields on channels and episodes is an ordered
list of {name, field} tuples, defaulting to [], instead of a map. If you
access this struct field directly, update your code to use the list. Custom
elements are emitted in insertion order instead of the previous map order.
Low-Level API
For more control, use the PodcastRSS.Channel module directly:
alias PodcastRSS.Channel
channel = Channel.new() # No defaults
|> Channel.title("My Podcast")
|> Channel.description("Custom channel")
Architecture
PodcastRSS follows a clear separation between high-level convenience and low-level control:
PodcastRSS- High-level "batteries included" API with sensible defaultsPodcastRSS.Channel- Low-level channel API for direct controlPodcastRSS.Episode- Episode/item managementPodcastRSS.XML- XML generation using Saxy
Testing
Run the test suite:
mix test
Documentation
Generate documentation:
mix docs
Contributing
- Fork the repository
- Create your feature branch (
git checkout -b my-new-feature) - Write tests for your changes
- Ensure all tests pass (
mix test) - Format your code (
mix format) - Commit your changes (
git commit -am 'Add some feature') - Push to the branch (
git push origin my-new-feature) - Create a new Pull Request
License
This project is licensed under the MIT License.
Acknowledgments
- Built with Saxy for fast XML generation
- Inspired by podcast RSS standards and iTunes podcast requirements
- Supports modern Podcast Index namespace features