diff --git a/lib/pinchflat/podcasts/opml_feed_builder.ex b/lib/pinchflat/podcasts/opml_feed_builder.ex
new file mode 100644
index 00000000..c764a661
--- /dev/null
+++ b/lib/pinchflat/podcasts/opml_feed_builder.ex
@@ -0,0 +1,40 @@
+defmodule Pinchflat.Podcasts.OpmlFeedBuilder do
+ @moduledoc """
+ Methods for building an OPML feed for a list of sources.
+ """
+
+ import Pinchflat.Utils.XmlUtils, only: [safe: 1]
+
+ alias PinchflatWeb.Router.Helpers, as: Routes
+
+ @doc """
+ Builds an OPML feed for a given list of sources.
+
+ Returns an XML document as a string.
+ """
+ def build(url_base, sources) do
+ sources_xml =
+ Enum.map(
+ sources,
+ &"""
+
+ """
+ )
+
+ """
+
+
+
+ All Sources
+
+
+ #{Enum.join(sources_xml, "\n")}
+
+
+ """
+ end
+
+ defp source_route(url_base, source) do
+ Path.join(url_base, "#{Routes.podcast_path(PinchflatWeb.Endpoint, :rss_feed, source.uuid)}.xml")
+ end
+end
diff --git a/lib/pinchflat/podcasts/podcast_helpers.ex b/lib/pinchflat/podcasts/podcast_helpers.ex
index 041107c5..30ba17ac 100644
--- a/lib/pinchflat/podcasts/podcast_helpers.ex
+++ b/lib/pinchflat/podcasts/podcast_helpers.ex
@@ -5,11 +5,25 @@ defmodule Pinchflat.Podcasts.PodcastHelpers do
"""
use Pinchflat.Media.MediaQuery
+ use Pinchflat.Sources.SourcesQuery
alias Pinchflat.Repo
alias Pinchflat.Metadata.MediaMetadata
alias Pinchflat.Metadata.SourceMetadata
+ @doc """
+ Returns a list of sources that are not marked for deletion.
+
+ Returns: [%Source{}]
+ """
+ def opml_sources() do
+ SourcesQuery.new()
+ |> select([s], %{custom_name: s.custom_name, uuid: s.uuid})
+ |> where([s], is_nil(s.marked_for_deletion_at))
+ |> order_by(asc: :custom_name)
+ |> Repo.all()
+ end
+
@doc """
Returns a list of media items that have been downloaded to disk
and have been proven to still exist there.
diff --git a/lib/pinchflat_web/controllers/podcasts/podcast_controller.ex b/lib/pinchflat_web/controllers/podcasts/podcast_controller.ex
index d69e4f6d..84401fe4 100644
--- a/lib/pinchflat_web/controllers/podcasts/podcast_controller.ex
+++ b/lib/pinchflat_web/controllers/podcasts/podcast_controller.ex
@@ -6,8 +6,19 @@ defmodule PinchflatWeb.Podcasts.PodcastController do
alias Pinchflat.Sources.Source
alias Pinchflat.Media.MediaItem
alias Pinchflat.Podcasts.RssFeedBuilder
+ alias Pinchflat.Podcasts.OpmlFeedBuilder
alias Pinchflat.Podcasts.PodcastHelpers
+ def opml_feed(conn, _params) do
+ url_base = url(conn, ~p"/")
+ xml = OpmlFeedBuilder.build(url_base, PodcastHelpers.opml_sources())
+
+ conn
+ |> put_resp_content_type("application/opml+xml")
+ |> put_resp_header("content-disposition", "inline")
+ |> send_resp(200, xml)
+ end
+
def rss_feed(conn, %{"uuid" => uuid}) do
source = Repo.get_by!(Source, uuid: uuid)
url_base = url(conn, ~p"/")
diff --git a/lib/pinchflat_web/controllers/sources/source_html.ex b/lib/pinchflat_web/controllers/sources/source_html.ex
index d9673ebc..4692a043 100644
--- a/lib/pinchflat_web/controllers/sources/source_html.ex
+++ b/lib/pinchflat_web/controllers/sources/source_html.ex
@@ -43,6 +43,10 @@ defmodule PinchflatWeb.Sources.SourceHTML do
url(conn, ~p"/sources/#{source.uuid}/feed") <> ".xml"
end
+ def opml_feed_url(conn) do
+ url(conn, ~p"/sources/opml") <> ".xml"
+ end
+
def output_path_template_override_placeholders(media_profiles) do
media_profiles
|> Enum.map(&{&1.id, &1.output_path_template})
diff --git a/lib/pinchflat_web/controllers/sources/source_html/index.html.heex b/lib/pinchflat_web/controllers/sources/source_html/index.html.heex
index 47615525..6ace2bc9 100644
--- a/lib/pinchflat_web/controllers/sources/source_html/index.html.heex
+++ b/lib/pinchflat_web/controllers/sources/source_html/index.html.heex
@@ -1,6 +1,16 @@