diff --git a/.iex.exs b/.iex.exs index 620b05e..8a0ac79 100644 --- a/.iex.exs +++ b/.iex.exs @@ -1,10 +1,10 @@ alias Pinchflat.Repo alias Pinchflat.Tasks.Task +alias Pinchflat.Sources.Source alias Pinchflat.Media.MediaItem alias Pinchflat.Tasks.SourceTasks alias Pinchflat.Media.MediaMetadata -alias Pinchflat.Sources.Source alias Pinchflat.Profiles.MediaProfile alias Pinchflat.Tasks @@ -13,7 +13,11 @@ alias Pinchflat.Profiles alias Pinchflat.Sources alias Pinchflat.Settings -alias Pinchflat.MediaClient.{SourceDetails, MediaDownloader} +alias Pinchflat.MediaClient.MediaDownloader +alias Pinchflat.YtDlp.Backend.Media, as: YtDlpMedia +alias Pinchflat.YtDlp.Backend.MediaCollection, as: YtDlpCollection + +alias Pinchflat.Api.YoutubeRss alias Pinchflat.Metadata.MetadataFileHelpers alias Pinchflat.Utils.FilesystemUtils.FileFollowerServer @@ -38,7 +42,7 @@ defmodule IexHelpers do :channel -> channel_url() end - SourceDetails.get_source_details(source) + YtDlpCollection.get_source_details(source) end def ids(type) do @@ -48,7 +52,7 @@ defmodule IexHelpers do :channel -> channel_url() end - SourceDetails.get_media_attributes_for_collection(source) + YtDlpCollection.get_media_attributes_for_collection(source) end end diff --git a/lib/pinchflat/api/youtube_rss.ex b/lib/pinchflat/api/youtube_rss.ex new file mode 100644 index 0000000..d9fa7d9 --- /dev/null +++ b/lib/pinchflat/api/youtube_rss.ex @@ -0,0 +1,44 @@ +defmodule Pinchflat.Api.YoutubeRss do + @moduledoc """ + Methods for interacting with YouTube RSS feeds + """ + + alias Pinchflat.Sources.Source + + @doc """ + Fetches the recent media IDs from a YouTube RSS feed for a given source. + + Returns {:ok, [binary()]} | {:error, binary()} + """ + def get_recent_media_ids_from_rss(%Source{} = source) do + case http_client().get(rss_url_for_source(source)) do + {:ok, response} -> + response = to_string(response) + media_id_regex = ~r/(.*?)<\/yt:videoId>/ + + # Don't get on me about using regex to search XML. + # The content is known, well-formed, and simple. + media_ids = + media_id_regex + |> Regex.scan(response) + |> Enum.map(fn [_, id] -> String.trim(id) end) + |> Enum.filter(&(String.length(&1) > 0)) + + {:ok, media_ids} + + {:error, _reason} -> + {:error, "Failed to fetch RSS feed"} + end + end + + defp rss_url_for_source(source) do + case source.collection_type do + :channel -> "https://www.youtube.com/feeds/videos.xml?channel_id=#{source.collection_id}" + :playlist -> "https://www.youtube.com/feeds/videos.xml?playlist_id=#{source.collection_id}" + end + end + + defp http_client do + Application.get_env(:pinchflat, :http_client, Pinchflat.HTTP.HTTPClient) + end +end diff --git a/lib/pinchflat/http/http_behaviour.ex b/lib/pinchflat/http/http_behaviour.ex index 2fe639e..b9c3703 100644 --- a/lib/pinchflat/http/http_behaviour.ex +++ b/lib/pinchflat/http/http_behaviour.ex @@ -4,5 +4,7 @@ defmodule Pinchflat.HTTP.HTTPBehaviour do so I can use Mox to create an HTTP mock """ + @callback get(String.t()) :: {:ok, String.t()} | {:error, String.t()} + @callback get(String.t(), Keyword.t()) :: {:ok, String.t()} | {:error, String.t()} @callback get(String.t(), Keyword.t(), Keyword.t()) :: {:ok, String.t()} | {:error, String.t()} end diff --git a/lib/pinchflat/media_source/source.ex b/lib/pinchflat/sources/source.ex similarity index 100% rename from lib/pinchflat/media_source/source.ex rename to lib/pinchflat/sources/source.ex diff --git a/test/pinchflat/api/youtube_rss_test.exs b/test/pinchflat/api/youtube_rss_test.exs new file mode 100644 index 0000000..eb9acb6 --- /dev/null +++ b/test/pinchflat/api/youtube_rss_test.exs @@ -0,0 +1,71 @@ +defmodule Pinchflat.Api.YoutubeRssTest do + use Pinchflat.DataCase + import Mox + import Pinchflat.SourcesFixtures + + alias Pinchflat.Api.YoutubeRss + + setup :verify_on_exit! + + setup do + source = source_fixture() + + {:ok, source: source} + end + + describe "get_recent_media_ids_from_rss/1" do + test "calls the expected URL for channel sources" do + source = source_fixture(collection_type: :channel, collection_id: "channel_id") + + expect(HTTPClientMock, :get, fn url -> + assert url =~ "https://www.youtube.com/feeds/videos.xml?channel_id=#{source.collection_id}" + + {:ok, ""} + end) + + assert {:ok, _} = YoutubeRss.get_recent_media_ids_from_rss(source) + end + + test "calls the expected URL for playlist sources" do + source = source_fixture(collection_type: :playlist, collection_id: "playlist_id") + + expect(HTTPClientMock, :get, fn url -> + assert url =~ "https://www.youtube.com/feeds/videos.xml?playlist_id=#{source.collection_id}" + + {:ok, ""} + end) + + assert {:ok, _} = YoutubeRss.get_recent_media_ids_from_rss(source) + end + + test "returns an error if the HTTP request fails", %{source: source} do + expect(HTTPClientMock, :get, fn _url -> {:error, ""} end) + + assert {:error, "Failed to fetch RSS feed"} = YoutubeRss.get_recent_media_ids_from_rss(source) + end + + test "returns the media IDs from the RSS feed", %{source: source} do + expect(HTTPClientMock, :get, fn _url -> + {:ok, "test_1test_2"} + end) + + assert {:ok, ["test_1", "test_2"]} = YoutubeRss.get_recent_media_ids_from_rss(source) + end + + test "strips whitespace from media IDs", %{source: source} do + expect(HTTPClientMock, :get, fn _url -> + {:ok, " test_1 test_2 "} + end) + + assert {:ok, ["test_1", "test_2"]} = YoutubeRss.get_recent_media_ids_from_rss(source) + end + + test "removes empty media IDs", %{source: source} do + expect(HTTPClientMock, :get, fn _url -> + {:ok, "test_1"} + end) + + assert {:ok, ["test_1"]} = YoutubeRss.get_recent_media_ids_from_rss(source) + end + end +end