defmodule Pinchflat.Podcasts.RssFeedBuilder do @moduledoc """ Methods for building an RSS feed for a source and its media items. """ @datetime_format "%a, %d %b %Y %H:%M:%S %z" import Pinchflat.Utils.XmlUtils, only: [safe: 1] alias Pinchflat.Utils.DatetimeUtils alias Pinchflat.Podcasts.PodcastHelpers alias PinchflatWeb.Router.Helpers, as: Routes @doc """ Builds an RSS feed for a given source and its media items. Only MediaItems that have been persisted will be included in the feed. ## Options: - `:limit` - The maximum number of media items to include in the feed. Defaults to 2,000. Returns an XML document as a string. """ def build(source, opts \\ []) do limit = Keyword.get(opts, :limit, 2_000) url_base = Keyword.get(opts, :url_base, PinchflatWeb.Endpoint.url()) media_items = PodcastHelpers.persisted_media_items_for(source, limit: limit) build_source_xml(source, media_items, url_base) end defp build_source_xml(source, media_items, url_base) do media_item_xml = Enum.map(media_items, &build_media_item_xml(source, &1, url_base)) # "caching" the image path since it requires some DB calls and is used twice feed_image_path = feed_image_path(url_base, source, media_items) # Useful: resources: # - https://validator.w3.org/feed/#validate_by_input # - https://github.com/Podcastindex-org/podcast-namespace/blob/main/docs/1.0.md # - https://podba.se/validate """ #{safe(source.custom_name)} #{safe(source.original_url)} #{safe(source.description)} TV & Film Generated by Pinchflat en-us #{Calendar.strftime(source.updated_at, @datetime_format)} #{Calendar.strftime(source.inserted_at, @datetime_format)} yes #{source.uuid} #{safe(feed_image_path)} #{safe(source.custom_name)} #{safe(source.original_url)} #{safe(source.custom_name)} #{safe(source.custom_name)} yes false #{Enum.join(media_item_xml, "\n")} """ end defp build_media_item_xml(source, media_item, url_base) do item_image_path = item_image_path(url_base, media_item) """ #{media_item.uuid} #{safe(media_item.title)} #{safe(media_item.original_url)} #{safe(media_item.description)} #{generate_upload_date(media_item)} #{media_item.duration_seconds} #{safe(source.custom_name)} #{safe(media_item.title)} #{item_image_path && ~s()} #{item_image_path && ~s()} false """ end defp generate_self_link(url_base, source) do Path.join(url_base, "#{podcast_route(:rss_feed, source.uuid)}.xml") end defp media_stream_path(url_base, media_item) do extension = Path.extname(media_item.media_filepath) Path.join(url_base, "#{media_route(:stream, media_item.uuid)}#{extension}") end defp feed_image_path(url_base, source, media_items) do case PodcastHelpers.select_cover_image(source, media_items) do {:error, _} -> "" {:ok, filepath} -> extension = Path.extname(filepath) Path.join(url_base, "#{podcast_route(:feed_image, source.uuid)}#{extension}") end end def item_image_path(url_base, media_item) do if media_item.thumbnail_filepath && File.exists?(media_item.thumbnail_filepath) do extension = Path.extname(media_item.thumbnail_filepath) Path.join(url_base, "#{podcast_route(:episode_image, media_item.uuid)}#{extension}") else nil end end defp generate_upload_date(media_item) do media_item.upload_date |> DatetimeUtils.date_to_datetime() |> Calendar.strftime(@datetime_format) end defp podcast_route(action, params) do Routes.podcast_path(PinchflatWeb.Endpoint, action, params) end defp media_route(action, params) do Routes.media_item_path(PinchflatWeb.Endpoint, action, params) end end