From a1ae215dd4c242a1ffa51e17b29bbd587ec5c7b5 Mon Sep 17 00:00:00 2001 From: robs Date: Fri, 13 Dec 2024 16:45:53 +0100 Subject: [PATCH 1/9] Added OPML Endpoint for podcast rss feeds --- lib/pinchflat/podcasts/opml_feed_builder.ex | 40 +++++++++++++++++++ lib/pinchflat/podcasts/podcast_helpers.ex | 14 +++++++ .../podcasts/podcast_controller.ex | 11 +++++ lib/pinchflat_web/router.ex | 2 + .../podcasts/opml_feed_builder_test.exs | 34 ++++++++++++++++ 5 files changed, 101 insertions(+) create mode 100644 lib/pinchflat/podcasts/opml_feed_builder.ex create mode 100644 test/pinchflat/podcasts/opml_feed_builder_test.exs diff --git a/lib/pinchflat/podcasts/opml_feed_builder.ex b/lib/pinchflat/podcasts/opml_feed_builder.ex new file mode 100644 index 00000000..de5c7000 --- /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..e6675035 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, %{}) 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/router.ex b/lib/pinchflat_web/router.ex index 9ba7068f..e9fb6f3b 100644 --- a/lib/pinchflat_web/router.ex +++ b/lib/pinchflat_web/router.ex @@ -53,6 +53,8 @@ defmodule PinchflatWeb.Router do scope "/", PinchflatWeb do pipe_through :feeds + get "/opml", Podcasts.PodcastController, :opml_feed + get "/sources/:uuid/feed", Podcasts.PodcastController, :rss_feed get "/sources/:uuid/feed_image", Podcasts.PodcastController, :feed_image get "/media/:uuid/episode_image", Podcasts.PodcastController, :episode_image diff --git a/test/pinchflat/podcasts/opml_feed_builder_test.exs b/test/pinchflat/podcasts/opml_feed_builder_test.exs new file mode 100644 index 00000000..460de762 --- /dev/null +++ b/test/pinchflat/podcasts/opml_feed_builder_test.exs @@ -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()) + 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 & 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 From f972081b1b2a23573de85101a3deb8f367189649 Mon Sep 17 00:00:00 2001 From: robs Date: Fri, 13 Dec 2024 16:59:23 +0100 Subject: [PATCH 2/9] changed opml route and added controller test for opml endpoint --- lib/pinchflat_web/router.ex | 2 +- .../controllers/podcast_controller_test.exs | 14 ++++++++++++++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/lib/pinchflat_web/router.ex b/lib/pinchflat_web/router.ex index e9fb6f3b..afcbe24d 100644 --- a/lib/pinchflat_web/router.ex +++ b/lib/pinchflat_web/router.ex @@ -53,7 +53,7 @@ defmodule PinchflatWeb.Router do scope "/", PinchflatWeb do pipe_through :feeds - get "/opml", Podcasts.PodcastController, :opml_feed + get "/podcasts/opml", Podcasts.PodcastController, :opml_feed get "/sources/:uuid/feed", Podcasts.PodcastController, :rss_feed get "/sources/:uuid/feed_image", Podcasts.PodcastController, :feed_image diff --git a/test/pinchflat_web/controllers/podcast_controller_test.exs b/test/pinchflat_web/controllers/podcast_controller_test.exs index 676ab54b..ec79b761 100644 --- a/test/pinchflat_web/controllers/podcast_controller_test.exs +++ b/test/pinchflat_web/controllers/podcast_controller_test.exs @@ -4,6 +4,20 @@ defmodule PinchflatWeb.PodcastControllerTest do import Pinchflat.MediaFixtures import Pinchflat.SourcesFixtures + describe "opml_feed" do + test "renders the XML document", %{conn: conn} do + source = source_fixture() + + conn = get(conn, ~p"/podcasts/opml" <> ".xml") + + assert conn.status == 200 + assert {"content-type", "application/opml+xml; charset=utf-8"} in conn.resp_headers + assert {"content-disposition", "inline"} in conn.resp_headers + assert conn.resp_body =~ ~s"http://www.example.com/sources/#{source.uuid}/feed.xml" + assert conn.resp_body =~ "text=\"Cool and good internal name!\"" + end + end + describe "rss_feed" do test "renders the XML document", %{conn: conn} do source = source_fixture() From 08b77e942f1915f88fee9f54920367804d19d0b6 Mon Sep 17 00:00:00 2001 From: robs Date: Fri, 13 Dec 2024 17:19:01 +0100 Subject: [PATCH 3/9] add copy opml feed button --- .../controllers/sources/source_html/index.html.heex | 10 ++++++++++ 1 file changed, 10 insertions(+) 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..7add60dd 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 @@

Sources