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

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:

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)
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:

Testing

Run the test suite:

mix test

Documentation

Generate documentation:

mix docs

Contributing

  1. Fork the repository
  2. Create your feature branch (git checkout -b my-new-feature)
  3. Write tests for your changes
  4. Ensure all tests pass (mix test)
  5. Format your code (mix format)
  6. Commit your changes (git commit -am 'Add some feature')
  7. Push to the branch (git push origin my-new-feature)
  8. Create a new Pull Request

License

This project is licensed under the MIT License.

Acknowledgments