PodcastRSS.Episode (Podcast RSS v0.3.0)

View Source

Represents a podcast episode with required and optional fields.

The Episode struct contains the core RSS 2.0 fields needed for podcast episodes. At least one of title or description must be present for a valid episode.

Summary

Functions

Add a chapter to the episode.

Add a custom field to the episode.

Set the episode description.

Set the episode duration.

Set the episode enclosure (media file).

Set the episode GUID (globally unique identifier).

Create a new empty episode.

Register a namespace for use in custom fields.

Set the episode title.

Types

chapter()

@type chapter() :: %{
  start_time: number(),
  title: String.t(),
  image: String.t() | nil,
  url: String.t() | nil
}

custom_field()

@type custom_field() :: %{content: String.t(), attributes: map(), cdata: boolean()}

enclosure()

@type enclosure() :: %{url: String.t(), length: non_neg_integer(), type: String.t()}

t()

@type t() :: %PodcastRSS.Episode{
  chapters: [chapter()],
  custom_fields: %{required(String.t()) => custom_field()},
  description: String.t() | nil,
  duration: non_neg_integer() | nil,
  enclosure: enclosure() | nil,
  guid: String.t() | nil,
  namespaces: %{required(String.t()) => String.t()},
  title: String.t() | nil
}

Functions

chapter(episode, start_time, title, opts \\ [])

@spec chapter(t(), String.t() | number(), String.t(), keyword()) :: t()

Add a chapter to the episode.

Chapters are used for Podlove Simple Chapters (PSC) format. They will be automatically sorted by start_time when generating the XML output.

The start_time accepts various input formats:

  • Integer: 1234 (seconds)
  • Float: 123.456 (seconds with millisecond precision)
  • String seconds: "1234" or "123.456"
  • MM:SS format: "12:34"
  • MM:SS.ms format: "12:34.567"
  • HH:MM:SS format: "01:23:45"
  • HH:MM:SS.ms format: "01:23:45.678"

Options

  • :image - URL to chapter-specific image (optional)
  • :url - Related url for the chapter (optional)

Examples

episode = Episode.new()
  |> Episode.chapter(0, "Introduction")
  |> Episode.chapter("03:00", "Main Topic", url: "https://example.com")
  |> Episode.chapter(60.5, "Background")  # Will be auto-sorted between first and second
  |> Episode.chapter("01:23:45.678", "Chapter with precise timing", image: "https://example.com/img.jpg")

custom_field(episode, name, content, opts \\ [])

@spec custom_field(t(), String.t(), String.t(), keyword()) :: t()

Add a custom field to the episode.

Namespace Handling

If the field name contains a colon (e.g., "itunes:duration"), the namespace prefix must be registered first using register_namespace/3, or you can use the :namespace_uri option for one-shot registration.

Options

  • :attributes - A map of XML attributes for the field (default: %{})
  • :cdata - Whether to wrap content in CDATA (default: false)
  • :namespace_uri - URI for one-shot namespace registration (extracts prefix from field name)

Examples

# Method 1: Explicit registration (recommended for multiple fields)
episode = Episode.new()
  |> Episode.register_namespace("itunes", "http://www.itunes.com/dtds/podcast-1.0.dtd")
  |> Episode.custom_field("itunes:duration", "00:15:30")
  |> Episode.custom_field("itunes:explicit", "no")

# Method 2: One-shot registration (convenient for single usage)
episode = Episode.new()
  |> Episode.custom_field("itunes:duration", "00:15:30",
       namespace_uri: "http://www.itunes.com/dtds/podcast-1.0.dtd")
  |> Episode.custom_field("podcast:soundbite", "",
       namespace_uri: "https://podcastindex.org/namespace/1.0",
       attributes: %{"startTime" => "73.0", "duration" => "60.0"})
  |> Episode.custom_field("itunes:explicit", "no")  # Uses registered namespace

# Non-namespaced fields
episode
  |> Episode.custom_field("author", "John Doe")
  |> Episode.custom_field("season", "1", attributes: %{"number" => "1"})

description(episode, description)

@spec description(t(), String.t()) :: t()

Set the episode description.

duration(episode, duration_input)

@spec duration(t(), String.t() | integer()) :: t()

Set the episode duration.

Accepts various input formats:

  • Integer: 1234 (seconds)
  • String seconds: "1234" (seconds)
  • MM:SS format: "12:34"
  • HH:MM:SS format: "01:23:45"
  • HH:MM:SS.ms format: "01:23:45.678" (rounds up fractional seconds)

Returns the episode with the duration set, or raises an ArgumentError for invalid input.

Examples

iex> Episode.new() |> Episode.duration(1234)
%Episode{duration: 1234, ...}

iex> Episode.new() |> Episode.duration("01:23:45")
%Episode{duration: 5025, ...}

iex> Episode.new() |> Episode.duration("invalid")
** (ArgumentError) Invalid duration format: "invalid"

enclosure(episode, url, length, type)

@spec enclosure(t(), String.t(), non_neg_integer(), String.t()) :: t()

Set the episode enclosure (media file).

guid(episode, guid)

@spec guid(t(), String.t()) :: t()

Set the episode GUID (globally unique identifier).

new()

@spec new() :: t()

Create a new empty episode.

register_namespace(episode, namespace, uri)

@spec register_namespace(t(), String.t(), String.t()) :: t()

Register a namespace for use in custom fields.

This allows you to use namespaced field names (e.g., "itunes:duration") without having to specify the namespace URI every time.

Examples

episode = Episode.new()
  |> Episode.register_namespace("itunes", "http://www.itunes.com/dtds/podcast-1.0.dtd")
  |> Episode.custom_field("itunes:duration", "00:15:30")   # Uses registered namespace
  |> Episode.custom_field("itunes:explicit", "no")         # Uses registered namespace

title(episode, title)

@spec title(t(), String.t()) :: t()

Set the episode title.