diff --git a/lib/pinchflat/media_client/backends/yt_dlp/channel.ex b/lib/pinchflat/media_client/backends/yt_dlp/channel.ex deleted file mode 100644 index 783979e..0000000 --- a/lib/pinchflat/media_client/backends/yt_dlp/channel.ex +++ /dev/null @@ -1,32 +0,0 @@ -defmodule Pinchflat.MediaClient.Backends.YtDlp.Channel do - @moduledoc """ - Contains utilities for working with a channel's videos - """ - - use Pinchflat.MediaClient.Backends.YtDlp.VideoCollection - alias Pinchflat.MediaClient.SourceDetails - - @doc """ - Gets a channel's ID and name from its URL. - - yt-dlp does not _really_ have channel-specific functions, so - instead we're fetching just the first video (using playlist_end: 1) - and parsing the channel ID and name from _its_ metadata - - Returns {:ok, %SourceDetails{}} | {:error, any, ...}. - """ - def get_source_details(channel_url) do - opts = [:skip_download, playlist_end: 1] - - with {:ok, output} <- backend_runner().run(channel_url, opts, "%(.{channel,channel_id})j"), - {:ok, parsed_json} <- Phoenix.json_library().decode(output) do - {:ok, SourceDetails.new(parsed_json["channel_id"], parsed_json["channel"])} - else - err -> err - end - end - - defp backend_runner do - Application.get_env(:pinchflat, :yt_dlp_runner) - end -end diff --git a/lib/pinchflat/media_client/backends/yt_dlp/video_collection.ex b/lib/pinchflat/media_client/backends/yt_dlp/video_collection.ex index d89c064..624689b 100644 --- a/lib/pinchflat/media_client/backends/yt_dlp/video_collection.ex +++ b/lib/pinchflat/media_client/backends/yt_dlp/video_collection.ex @@ -1,28 +1,47 @@ defmodule Pinchflat.MediaClient.Backends.YtDlp.VideoCollection do @moduledoc """ - Contains utilities for working with collections of videos (ie: channels, playlists). - - Meant to be included in other modules but can be used on its own. Channels and playlists - will have many of their own methods, but also share a lot of methods. This module is for - those shared methods. + Contains utilities for working with collections of + videos (aka: a source [ie: channels, playlists]). """ - defmacro __using__(_) do - quote do - @doc """ - Returns a list of strings representing the video ids in the collection. + alias Pinchflat.MediaClient.SourceDetails - Returns {:ok, [binary()]} | {:error, any, ...}. - """ - def get_video_ids(url, command_opts \\ []) do - runner = Application.get_env(:pinchflat, :yt_dlp_runner) - opts = command_opts ++ [:simulate, :skip_download] + @doc """ + Returns a list of strings representing the video ids in the collection. - case runner.run(url, opts, "%(id)s") do - {:ok, output} -> {:ok, String.split(output, "\n", trim: true)} - res -> res - end - end + Returns {:ok, [binary()]} | {:error, any, ...}. + """ + def get_video_ids(url, command_opts \\ []) do + runner = Application.get_env(:pinchflat, :yt_dlp_runner) + opts = command_opts ++ [:simulate, :skip_download] + + case runner.run(url, opts, "%(id)s") do + {:ok, output} -> {:ok, String.split(output, "\n", trim: true)} + res -> res end end + + @doc """ + Gets a source's ID and name from its URL. + + yt-dlp does not _really_ have source-specific functions, so + instead we're fetching just the first video (using playlist_end: 1) + and parsing the source ID and name from _its_ metadata + + Returns {:ok, %SourceDetails{}} | {:error, any, ...}. + """ + def get_source_details(source_url) do + opts = [:skip_download, playlist_end: 1] + + with {:ok, output} <- backend_runner().run(source_url, opts, "%(.{channel,channel_id})j"), + {:ok, parsed_json} <- Phoenix.json_library().decode(output) do + {:ok, SourceDetails.new(parsed_json["channel_id"], parsed_json["channel"])} + else + err -> err + end + end + + defp backend_runner do + Application.get_env(:pinchflat, :yt_dlp_runner) + end end diff --git a/lib/pinchflat/media_client/source_details.ex b/lib/pinchflat/media_client/source_details.ex index e8796c2..75c7077 100644 --- a/lib/pinchflat/media_client/source_details.ex +++ b/lib/pinchflat/media_client/source_details.ex @@ -8,7 +8,7 @@ defmodule Pinchflat.MediaClient.SourceDetails do @enforce_keys [:id, :name] defstruct [:id, :name] - alias Pinchflat.MediaClient.Backends.YtDlp.Channel, as: YtDlpChannel + alias Pinchflat.MediaClient.Backends.YtDlp.VideoCollection, as: YtDlpSource @doc false def new(id, name) do @@ -35,7 +35,7 @@ defmodule Pinchflat.MediaClient.SourceDetails do defp source_module(backend) do case backend do - :yt_dlp -> YtDlpChannel + :yt_dlp -> YtDlpSource end end end diff --git a/test/pinchflat/media_client/backends/yt_dlp/channel_test.exs b/test/pinchflat/media_client/backends/yt_dlp/channel_test.exs deleted file mode 100644 index 724e6e3..0000000 --- a/test/pinchflat/media_client/backends/yt_dlp/channel_test.exs +++ /dev/null @@ -1,45 +0,0 @@ -defmodule Pinchflat.MediaClient.Backends.YtDlp.ChannelTest do - use ExUnit.Case, async: true - import Mox - - alias Pinchflat.MediaClient.SourceDetails - alias Pinchflat.MediaClient.Backends.YtDlp.Channel - - @channel_url "https://www.youtube.com/c/TheUselessTrials" - - setup :verify_on_exit! - - describe "get_source_details/1" do - test "it returns a %SourceDetails{} with data on success" do - expect(YtDlpRunnerMock, :run, fn _url, _opts, _ot -> - {:ok, "{\"channel\": \"TheUselessTrials\", \"channel_id\": \"UCQH2\"}"} - end) - - assert {:ok, res} = Channel.get_source_details(@channel_url) - assert %SourceDetails{id: "UCQH2", name: "TheUselessTrials"} = res - end - - test "it passes the expected args to the backend runner" do - expect(YtDlpRunnerMock, :run, fn @channel_url, opts, ot -> - assert opts == [:skip_download, playlist_end: 1] - assert ot == "%(.{channel,channel_id})j" - - {:ok, "{}"} - end) - - assert {:ok, _} = Channel.get_source_details(@channel_url) - end - - test "it returns an error if the runner returns an error" do - expect(YtDlpRunnerMock, :run, fn _url, _opts, _ot -> {:error, "Big issue", 1} end) - - assert {:error, "Big issue", 1} = Channel.get_source_details(@channel_url) - end - - test "it returns an error if the output is not JSON" do - expect(YtDlpRunnerMock, :run, fn _url, _opts, _ot -> {:ok, "Not JSON"} end) - - assert {:error, %Jason.DecodeError{}} = Channel.get_source_details(@channel_url) - end - end -end diff --git a/test/pinchflat/media_client/backends/yt_dlp/video_collection_test.exs b/test/pinchflat/media_client/backends/yt_dlp/video_collection_test.exs index 3acae20..3c8bfe7 100644 --- a/test/pinchflat/media_client/backends/yt_dlp/video_collection_test.exs +++ b/test/pinchflat/media_client/backends/yt_dlp/video_collection_test.exs @@ -2,13 +2,10 @@ defmodule Pinchflat.MediaClient.Backends.YtDlp.VideoCollectionTest do use ExUnit.Case, async: true import Mox + alias Pinchflat.MediaClient.SourceDetails alias Pinchflat.MediaClient.Backends.YtDlp.VideoCollection - @channel_url "https://www.youtube.com/@TheUselessTrials" - - defmodule VideoCollectionUser do - use VideoCollection - end + @channel_url "https://www.youtube.com/c/TheUselessTrials" setup :verify_on_exit! @@ -16,7 +13,7 @@ defmodule Pinchflat.MediaClient.Backends.YtDlp.VideoCollectionTest do test "returns a list of video ids with no blank elements" do expect(YtDlpRunnerMock, :run, fn _url, _opts, _ot -> {:ok, "id1\nid2\n\nid3\n"} end) - assert {:ok, ["id1", "id2", "id3"]} = VideoCollectionUser.get_video_ids(@channel_url) + assert {:ok, ["id1", "id2", "id3"]} = VideoCollection.get_video_ids(@channel_url) end test "it passes the expected default args" do @@ -27,7 +24,7 @@ defmodule Pinchflat.MediaClient.Backends.YtDlp.VideoCollectionTest do {:ok, ""} end) - assert {:ok, _} = VideoCollectionUser.get_video_ids(@channel_url) + assert {:ok, _} = VideoCollection.get_video_ids(@channel_url) end test "it passes the expected custom args" do @@ -37,13 +34,47 @@ defmodule Pinchflat.MediaClient.Backends.YtDlp.VideoCollectionTest do {:ok, ""} end) - assert {:ok, _} = VideoCollectionUser.get_video_ids(@channel_url, [:custom_arg]) + assert {:ok, _} = VideoCollection.get_video_ids(@channel_url, [:custom_arg]) end test "returns the error straight through when the command fails" do expect(YtDlpRunnerMock, :run, fn _url, _opts, _ot -> {:error, "Big issue", 1} end) - assert {:error, "Big issue", 1} = VideoCollectionUser.get_video_ids(@channel_url) + assert {:error, "Big issue", 1} = VideoCollection.get_video_ids(@channel_url) + end + end + + describe "get_source_details/1" do + test "it returns a %SourceDetails{} with data on success" do + expect(YtDlpRunnerMock, :run, fn _url, _opts, _ot -> + {:ok, "{\"channel\": \"TheUselessTrials\", \"channel_id\": \"UCQH2\"}"} + end) + + assert {:ok, res} = VideoCollection.get_source_details(@channel_url) + assert %SourceDetails{id: "UCQH2", name: "TheUselessTrials"} = res + end + + test "it passes the expected args to the backend runner" do + expect(YtDlpRunnerMock, :run, fn @channel_url, opts, ot -> + assert opts == [:skip_download, playlist_end: 1] + assert ot == "%(.{channel,channel_id})j" + + {:ok, "{}"} + end) + + assert {:ok, _} = VideoCollection.get_source_details(@channel_url) + end + + test "it returns an error if the runner returns an error" do + expect(YtDlpRunnerMock, :run, fn _url, _opts, _ot -> {:error, "Big issue", 1} end) + + assert {:error, "Big issue", 1} = VideoCollection.get_source_details(@channel_url) + end + + test "it returns an error if the output is not JSON" do + expect(YtDlpRunnerMock, :run, fn _url, _opts, _ot -> {:ok, "Not JSON"} end) + + assert {:error, %Jason.DecodeError{}} = VideoCollection.get_source_details(@channel_url) end end end