Added OPML Endpoint for podcast rss feeds

This commit is contained in:
robs 2024-12-13 16:45:53 +01:00
parent 53e106dac2
commit a1ae215dd4
5 changed files with 101 additions and 0 deletions

View file

@ -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,
&"""
<outline type="rss" text="#{safe(&1.custom_name)}" xmlUrl="#{safe(source_route(url_base, &1))}" />
"""
)
"""
<?xml version="1.0" encoding="UTF-8"?>
<opml version="2.0">
<head>
<title>All Sources</title>
</head>
<body>
#{Enum.join(sources_xml, "\n")}
</body>
</opml>
"""
end
defp source_route(url_base, source) do
Path.join(url_base, "#{Routes.podcast_path(PinchflatWeb.Endpoint, :rss_feed, source.uuid)}.xml")
end
end

View file

@ -5,11 +5,25 @@ defmodule Pinchflat.Podcasts.PodcastHelpers do
""" """
use Pinchflat.Media.MediaQuery use Pinchflat.Media.MediaQuery
use Pinchflat.Sources.SourcesQuery
alias Pinchflat.Repo alias Pinchflat.Repo
alias Pinchflat.Metadata.MediaMetadata alias Pinchflat.Metadata.MediaMetadata
alias Pinchflat.Metadata.SourceMetadata 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 """ @doc """
Returns a list of media items that have been downloaded to disk Returns a list of media items that have been downloaded to disk
and have been proven to still exist there. and have been proven to still exist there.

View file

@ -6,8 +6,19 @@ defmodule PinchflatWeb.Podcasts.PodcastController do
alias Pinchflat.Sources.Source alias Pinchflat.Sources.Source
alias Pinchflat.Media.MediaItem alias Pinchflat.Media.MediaItem
alias Pinchflat.Podcasts.RssFeedBuilder alias Pinchflat.Podcasts.RssFeedBuilder
alias Pinchflat.Podcasts.OpmlFeedBuilder
alias Pinchflat.Podcasts.PodcastHelpers alias Pinchflat.Podcasts.PodcastHelpers
def opml_feed(conn, %{}) 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 def rss_feed(conn, %{"uuid" => uuid}) do
source = Repo.get_by!(Source, uuid: uuid) source = Repo.get_by!(Source, uuid: uuid)
url_base = url(conn, ~p"/") url_base = url(conn, ~p"/")

View file

@ -53,6 +53,8 @@ defmodule PinchflatWeb.Router do
scope "/", PinchflatWeb do scope "/", PinchflatWeb do
pipe_through :feeds pipe_through :feeds
get "/opml", Podcasts.PodcastController, :opml_feed
get "/sources/:uuid/feed", Podcasts.PodcastController, :rss_feed get "/sources/:uuid/feed", Podcasts.PodcastController, :rss_feed
get "/sources/:uuid/feed_image", Podcasts.PodcastController, :feed_image get "/sources/:uuid/feed_image", Podcasts.PodcastController, :feed_image
get "/media/:uuid/episode_image", Podcasts.PodcastController, :episode_image get "/media/:uuid/episode_image", Podcasts.PodcastController, :episode_image

View file

@ -0,0 +1,34 @@
defmodule Pinchflat.Podcasts.OpmlFeedBuilderTest do
use Pinchflat.DataCase
import Pinchflat.SourcesFixtures
alias Pinchflat.Podcasts.OpmlFeedBuilder
setup do
source = source_fixture()
{:ok, source: source}
end
describe "build/2" do
test "returns an XML document", %{source: source} do
res = OpmlFeedBuilder.build("http://example.com", [source])
assert String.contains?(res, ~s(<?xml version="1.0" encoding="UTF-8"?>))
end
test "escapes illegal characters" do
source = source_fixture(%{custom_name: "A & B"})
res = OpmlFeedBuilder.build("http://example.com", [source])
assert String.contains?(res, ~s(A &amp; B))
end
test "build podcast link with URL base", %{source: source} do
res = OpmlFeedBuilder.build("http://example.com", [source])
assert String.contains?(res, ~s(http://example.com/sources/#{source.uuid}/feed.xml))
end
end
end