Finished first draft of methods to do with querying channels
This commit is contained in:
parent
cdd708f813
commit
94ca11001b
11 changed files with 171 additions and 36 deletions
2
ideas.md
2
ideas.md
|
|
@ -1,3 +1,3 @@
|
|||
- Write media datbase ID as metadata/to file/whatever so it gives us an option to retroactively match media to the DB down the line. Useful if someone moves the media without informing the UI
|
||||
- Use a UUID for the media database ID (or at least alongside it)
|
||||
- Use a UUID for the media database ID (or at least alongside it)
|
||||
- Look into this and its recommended plugins https://hexdocs.pm/ex_check/readme.html
|
||||
|
|
|
|||
|
|
@ -3,27 +3,26 @@ defmodule Pinchflat.Downloader.Backends.YtDlp.Channel do
|
|||
Contains utilities for working with a channel's videos
|
||||
"""
|
||||
|
||||
# TODO: convert to `use`
|
||||
import Pinchflat.Downloader.Backends.YtDlp.VideoCollection
|
||||
alias __MODULE__
|
||||
use Pinchflat.Downloader.Backends.YtDlp.VideoCollection
|
||||
alias Pinchflat.Downloader.ChannelDetails
|
||||
|
||||
defstruct [:id, :name]
|
||||
@doc """
|
||||
Gets a channel's ID and name from its URL.
|
||||
|
||||
def new(id, name) do
|
||||
%__MODULE__{id: id, name: name}
|
||||
end
|
||||
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, %ChannelDetails{}} | {:error, any, ...}.
|
||||
"""
|
||||
def get_channel_info(channel_url) do
|
||||
opts = [print: "%(.{channel,channel_id})j", playlist_end: 1]
|
||||
|
||||
case backend_runner().run(channel_url, opts) do
|
||||
{:ok, output} ->
|
||||
result = Phoenix.json_library().decode!(output)
|
||||
|
||||
{:ok, Channel.new(result["channel_id"], result["channel"])}
|
||||
|
||||
res ->
|
||||
res
|
||||
with {:ok, output} <- backend_runner().run(channel_url, opts),
|
||||
{:ok, parsed_json} <- Phoenix.json_library().decode(output) do
|
||||
{:ok, ChannelDetails.new(parsed_json["channel_id"], parsed_json["channel"])}
|
||||
else
|
||||
err -> err
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -11,6 +11,8 @@ defmodule Pinchflat.Downloader.Backends.YtDlp.CommandRunner do
|
|||
@doc """
|
||||
Runs a yt-dlp command and returns the string output
|
||||
|
||||
Returns {:ok, binary()} | {:error, output, status}.
|
||||
|
||||
# IDEA: deduplicate command opts, keeping the last one on conflict
|
||||
although possibly not needed (and a LOT easier) if yt-dlp
|
||||
just ignores duplicate options (ie: look into that)
|
||||
|
|
|
|||
|
|
@ -6,13 +6,16 @@ defmodule Pinchflat.Downloader.Backends.YtDlp.Video do
|
|||
@doc """
|
||||
Downloads a single video (and possible metadata) directly to its
|
||||
final destination. Returns the parsed JSON output from yt-dlp.
|
||||
|
||||
Returns {:ok, map()} | {:error, any, ...}.
|
||||
"""
|
||||
def download(url, command_opts \\ []) do
|
||||
opts = [:no_simulate, print: "%()j"] ++ command_opts
|
||||
|
||||
case backend_runner().run(url, opts) do
|
||||
# TODO: test that I changed this to a ! method
|
||||
{:ok, output} -> {:ok, Phoenix.json_library().decode!(output)}
|
||||
with {:ok, output} <- backend_runner().run(url, opts),
|
||||
{:ok, parsed_json} <- Phoenix.json_library().decode(output) do
|
||||
{:ok, parsed_json}
|
||||
else
|
||||
err -> err
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -2,22 +2,27 @@ defmodule Pinchflat.Downloader.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.
|
||||
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.
|
||||
"""
|
||||
|
||||
@doc """
|
||||
Returns a list of strings representing the video ids in the collection
|
||||
"""
|
||||
def get_video_ids(url, command_opts \\ []) do
|
||||
opts = command_opts ++ [:simulate, :skip_download, print: :id]
|
||||
defmacro __using__(_) do
|
||||
quote do
|
||||
@doc """
|
||||
Returns a list of strings representing the video ids in the collection.
|
||||
|
||||
case backend_runner().run(url, opts) do
|
||||
{:ok, output} -> {:ok, String.split(output, "\n", trim: true)}
|
||||
res -> res
|
||||
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, print: :id]
|
||||
|
||||
case runner.run(url, opts) do
|
||||
{:ok, output} -> {:ok, String.split(output, "\n", trim: true)}
|
||||
res -> res
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
defp backend_runner do
|
||||
Application.get_env(:pinchflat, :yt_dlp_runner)
|
||||
end
|
||||
end
|
||||
|
|
|
|||
32
lib/pinchflat/downloader/channel_details.ex
Normal file
32
lib/pinchflat/downloader/channel_details.ex
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
defmodule Pinchflat.Downloader.ChannelDetails do
|
||||
@moduledoc """
|
||||
This is the integration layer for actually working with channels.
|
||||
|
||||
Technically hardcodes the yt-dlp backend for now, but should leave
|
||||
it open-ish for future expansion (just in case).
|
||||
"""
|
||||
@enforce_keys [:id, :name]
|
||||
defstruct [:id, :name]
|
||||
|
||||
alias Pinchflat.Downloader.Backends.YtDlp.Channel, as: YtDlpChannel
|
||||
|
||||
@doc false
|
||||
def new(id, name) do
|
||||
%__MODULE__{id: id, name: name}
|
||||
end
|
||||
|
||||
@doc """
|
||||
Gets a channel's ID and name from its URL, using the given backend.
|
||||
|
||||
Returns {:ok, map()} | {:error, any, ...}.
|
||||
"""
|
||||
def get_channel_details(channel_url, backend \\ :yt_dlp) do
|
||||
channel_module(backend).get_channel_info(channel_url)
|
||||
end
|
||||
|
||||
defp channel_module(backend) do
|
||||
case backend do
|
||||
:yt_dlp -> YtDlpChannel
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -15,6 +15,8 @@ defmodule Pinchflat.Downloader.VideoDownloader do
|
|||
|
||||
@doc """
|
||||
Downloads a single video based on the settings in the given media profile.
|
||||
|
||||
Returns {:ok, %ChannelDetails{}} | {:error, any, ...}.
|
||||
"""
|
||||
def download_for_media_profile(url, %MediaProfile{} = media_profile, backend \\ :yt_dlp) do
|
||||
option_builder = option_builder(backend)
|
||||
|
|
|
|||
44
test/pinchflat/downloader/backends/yt_dlp/channel_test.exs
Normal file
44
test/pinchflat/downloader/backends/yt_dlp/channel_test.exs
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
defmodule Pinchflat.Downloader.Backends.YtDlp.ChannelTest do
|
||||
use ExUnit.Case, async: true
|
||||
import Mox
|
||||
|
||||
alias Pinchflat.Downloader.ChannelDetails
|
||||
alias Pinchflat.Downloader.Backends.YtDlp.Channel
|
||||
|
||||
@channel_url "https://www.youtube.com/c/TheUselessTrials"
|
||||
|
||||
setup :verify_on_exit!
|
||||
|
||||
describe "get_channel_info/1" do
|
||||
test "it returns a %ChannelDetails{} with data on success" do
|
||||
expect(CommandRunnerMock, :run, fn _url, _opts ->
|
||||
{:ok, "{\"channel\": \"TheUselessTrials\", \"channel_id\": \"UCQH2\"}"}
|
||||
end)
|
||||
|
||||
assert {:ok, res} = Channel.get_channel_info(@channel_url)
|
||||
assert %ChannelDetails{id: "UCQH2", name: "TheUselessTrials"} = res
|
||||
end
|
||||
|
||||
test "it passes the expected args to the backend runner" do
|
||||
expect(CommandRunnerMock, :run, fn @channel_url, opts ->
|
||||
assert opts == [{:print, "%(.{channel,channel_id})j"}, {:playlist_end, 1}]
|
||||
|
||||
{:ok, "{}"}
|
||||
end)
|
||||
|
||||
assert {:ok, _} = Channel.get_channel_info(@channel_url)
|
||||
end
|
||||
|
||||
test "it returns an error if the runner returns an error" do
|
||||
expect(CommandRunnerMock, :run, fn _url, _opts -> {:error, "Big issue", 1} end)
|
||||
|
||||
assert {:error, "Big issue", 1} = Channel.get_channel_info(@channel_url)
|
||||
end
|
||||
|
||||
test "it returns an error if the output is not JSON" do
|
||||
expect(CommandRunnerMock, :run, fn _url, _opts -> {:ok, "Not JSON"} end)
|
||||
|
||||
assert {:error, %Jason.DecodeError{}} = Channel.get_channel_info(@channel_url)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -6,13 +6,17 @@ defmodule Pinchflat.Downloader.Backends.YtDlp.VideoCollectionTest do
|
|||
|
||||
@channel_url "https://www.youtube.com/@TheUselessTrials"
|
||||
|
||||
defmodule VideoCollectionUser do
|
||||
use VideoCollection
|
||||
end
|
||||
|
||||
setup :verify_on_exit!
|
||||
|
||||
describe "get_video_ids/2" do
|
||||
test "returns a list of video ids with no blank elements" do
|
||||
expect(CommandRunnerMock, :run, fn _url, _opts -> {:ok, "id1\nid2\n\nid3\n"} end)
|
||||
|
||||
assert {:ok, ["id1", "id2", "id3"]} = VideoCollection.get_video_ids(@channel_url)
|
||||
assert {:ok, ["id1", "id2", "id3"]} = VideoCollectionUser.get_video_ids(@channel_url)
|
||||
end
|
||||
|
||||
test "it passes the expected default args" do
|
||||
|
|
@ -22,7 +26,7 @@ defmodule Pinchflat.Downloader.Backends.YtDlp.VideoCollectionTest do
|
|||
{:ok, ""}
|
||||
end)
|
||||
|
||||
assert {:ok, _} = VideoCollection.get_video_ids(@channel_url)
|
||||
assert {:ok, _} = VideoCollectionUser.get_video_ids(@channel_url)
|
||||
end
|
||||
|
||||
test "it passes the expected custom args" do
|
||||
|
|
@ -32,13 +36,13 @@ defmodule Pinchflat.Downloader.Backends.YtDlp.VideoCollectionTest do
|
|||
{:ok, ""}
|
||||
end)
|
||||
|
||||
assert {:ok, _} = VideoCollection.get_video_ids(@channel_url, [:custom_arg])
|
||||
assert {:ok, _} = VideoCollectionUser.get_video_ids(@channel_url, [:custom_arg])
|
||||
end
|
||||
|
||||
test "returns the error straight through when the command fails" do
|
||||
expect(CommandRunnerMock, :run, fn _url, _opts -> {:error, "Big issue", 1} end)
|
||||
|
||||
assert {:error, "Big issue", 1} = VideoCollection.get_video_ids(@channel_url)
|
||||
assert {:error, "Big issue", 1} = VideoCollectionUser.get_video_ids(@channel_url)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -35,6 +35,12 @@ defmodule Pinchflat.Downloader.Backends.YtDlp.VideoTest do
|
|||
assert {:ok, %{"title" => "Test"}} = Video.download(@video_url)
|
||||
end
|
||||
|
||||
test "it returns an error if the output is not JSON" do
|
||||
expect(CommandRunnerMock, :run, fn _url, _opts -> {:ok, "Not JSON"} end)
|
||||
|
||||
assert {:error, %Jason.DecodeError{}} = Video.download(@video_url)
|
||||
end
|
||||
|
||||
test "it directly passes along any errors" do
|
||||
expect(CommandRunnerMock, :run, fn _url, _opts -> {:error, "Big issue", 1} end)
|
||||
|
||||
|
|
|
|||
38
test/pinchflat/downloader/channel_details_test.exs
Normal file
38
test/pinchflat/downloader/channel_details_test.exs
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
defmodule Pinchflat.Downloader.ChannelDetailsTest do
|
||||
use ExUnit.Case, async: true
|
||||
import Mox
|
||||
|
||||
alias Pinchflat.Downloader.ChannelDetails
|
||||
|
||||
@channel_url "https://www.youtube.com/c/TheUselessTrials"
|
||||
|
||||
setup :verify_on_exit!
|
||||
|
||||
describe "new/2" do
|
||||
test "it returns a struct with the given values" do
|
||||
assert %ChannelDetails{id: "UCQH2", name: "TheUselessTrials"} =
|
||||
ChannelDetails.new("UCQH2", "TheUselessTrials")
|
||||
end
|
||||
end
|
||||
|
||||
describe "get_channel_details/2" do
|
||||
test "it passes the expected arguments to the backend" do
|
||||
expect(CommandRunnerMock, :run, fn @channel_url, opts ->
|
||||
assert opts == [{:print, "%(.{channel,channel_id})j"}, {:playlist_end, 1}]
|
||||
|
||||
{:ok, "{\"channel\": \"TheUselessTrials\", \"channel_id\": \"UCQH2\"}"}
|
||||
end)
|
||||
|
||||
assert {:ok, _} = ChannelDetails.get_channel_details(@channel_url)
|
||||
end
|
||||
|
||||
test "it returns a struct composed of the returned data" do
|
||||
expect(CommandRunnerMock, :run, fn _url, _opts ->
|
||||
{:ok, "{\"channel\": \"TheUselessTrials\", \"channel_id\": \"UCQH2\"}"}
|
||||
end)
|
||||
|
||||
assert {:ok, res} = ChannelDetails.get_channel_details(@channel_url)
|
||||
assert %ChannelDetails{id: "UCQH2", name: "TheUselessTrials"} = res
|
||||
end
|
||||
end
|
||||
end
|
||||
Loading…
Reference in a new issue