Finished first draft of methods to do with querying channels

This commit is contained in:
Kieran Eglin 2024-01-23 09:41:39 -08:00
parent cdd708f813
commit 94ca11001b
No known key found for this signature in database
GPG key ID: 193984967FCF432D
11 changed files with 171 additions and 36 deletions

View file

@ -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 - 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 - Look into this and its recommended plugins https://hexdocs.pm/ex_check/readme.html

View file

@ -3,27 +3,26 @@ defmodule Pinchflat.Downloader.Backends.YtDlp.Channel do
Contains utilities for working with a channel's videos Contains utilities for working with a channel's videos
""" """
# TODO: convert to `use` use Pinchflat.Downloader.Backends.YtDlp.VideoCollection
import Pinchflat.Downloader.Backends.YtDlp.VideoCollection alias Pinchflat.Downloader.ChannelDetails
alias __MODULE__
defstruct [:id, :name] @doc """
Gets a channel's ID and name from its URL.
def new(id, name) do yt-dlp does not _really_ have channel-specific functions, so
%__MODULE__{id: id, name: name} instead we're fetching just the first video (using playlist_end: 1)
end and parsing the channel ID and name from _its_ metadata
Returns {:ok, %ChannelDetails{}} | {:error, any, ...}.
"""
def get_channel_info(channel_url) do def get_channel_info(channel_url) do
opts = [print: "%(.{channel,channel_id})j", playlist_end: 1] opts = [print: "%(.{channel,channel_id})j", playlist_end: 1]
case backend_runner().run(channel_url, opts) do with {:ok, output} <- backend_runner().run(channel_url, opts),
{:ok, output} -> {:ok, parsed_json} <- Phoenix.json_library().decode(output) do
result = Phoenix.json_library().decode!(output) {:ok, ChannelDetails.new(parsed_json["channel_id"], parsed_json["channel"])}
else
{:ok, Channel.new(result["channel_id"], result["channel"])} err -> err
res ->
res
end end
end end

View file

@ -11,6 +11,8 @@ defmodule Pinchflat.Downloader.Backends.YtDlp.CommandRunner do
@doc """ @doc """
Runs a yt-dlp command and returns the string output 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 # IDEA: deduplicate command opts, keeping the last one on conflict
although possibly not needed (and a LOT easier) if yt-dlp although possibly not needed (and a LOT easier) if yt-dlp
just ignores duplicate options (ie: look into that) just ignores duplicate options (ie: look into that)

View file

@ -6,13 +6,16 @@ defmodule Pinchflat.Downloader.Backends.YtDlp.Video do
@doc """ @doc """
Downloads a single video (and possible metadata) directly to its Downloads a single video (and possible metadata) directly to its
final destination. Returns the parsed JSON output from yt-dlp. final destination. Returns the parsed JSON output from yt-dlp.
Returns {:ok, map()} | {:error, any, ...}.
""" """
def download(url, command_opts \\ []) do def download(url, command_opts \\ []) do
opts = [:no_simulate, print: "%()j"] ++ command_opts opts = [:no_simulate, print: "%()j"] ++ command_opts
case backend_runner().run(url, opts) do with {:ok, output} <- backend_runner().run(url, opts),
# TODO: test that I changed this to a ! method {:ok, parsed_json} <- Phoenix.json_library().decode(output) do
{:ok, output} -> {:ok, Phoenix.json_library().decode!(output)} {:ok, parsed_json}
else
err -> err err -> err
end end
end end

View file

@ -2,22 +2,27 @@ defmodule Pinchflat.Downloader.Backends.YtDlp.VideoCollection do
@moduledoc """ @moduledoc """
Contains utilities for working with collections of videos (ie: channels, playlists). 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 """ defmacro __using__(_) do
Returns a list of strings representing the video ids in the collection quote do
""" @doc """
def get_video_ids(url, command_opts \\ []) do Returns a list of strings representing the video ids in the collection.
opts = command_opts ++ [:simulate, :skip_download, print: :id]
case backend_runner().run(url, opts) do Returns {:ok, [binary()]} | {:error, any, ...}.
{:ok, output} -> {:ok, String.split(output, "\n", trim: true)} """
res -> res 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
end end
defp backend_runner do
Application.get_env(:pinchflat, :yt_dlp_runner)
end
end end

View 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

View file

@ -15,6 +15,8 @@ defmodule Pinchflat.Downloader.VideoDownloader do
@doc """ @doc """
Downloads a single video based on the settings in the given media profile. 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 def download_for_media_profile(url, %MediaProfile{} = media_profile, backend \\ :yt_dlp) do
option_builder = option_builder(backend) option_builder = option_builder(backend)

View 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

View file

@ -6,13 +6,17 @@ defmodule Pinchflat.Downloader.Backends.YtDlp.VideoCollectionTest do
@channel_url "https://www.youtube.com/@TheUselessTrials" @channel_url "https://www.youtube.com/@TheUselessTrials"
defmodule VideoCollectionUser do
use VideoCollection
end
setup :verify_on_exit! setup :verify_on_exit!
describe "get_video_ids/2" do describe "get_video_ids/2" do
test "returns a list of video ids with no blank elements" 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) 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 end
test "it passes the expected default args" do test "it passes the expected default args" do
@ -22,7 +26,7 @@ defmodule Pinchflat.Downloader.Backends.YtDlp.VideoCollectionTest do
{:ok, ""} {:ok, ""}
end) end)
assert {:ok, _} = VideoCollection.get_video_ids(@channel_url) assert {:ok, _} = VideoCollectionUser.get_video_ids(@channel_url)
end end
test "it passes the expected custom args" do test "it passes the expected custom args" do
@ -32,13 +36,13 @@ defmodule Pinchflat.Downloader.Backends.YtDlp.VideoCollectionTest do
{:ok, ""} {:ok, ""}
end) end)
assert {:ok, _} = VideoCollection.get_video_ids(@channel_url, [:custom_arg]) assert {:ok, _} = VideoCollectionUser.get_video_ids(@channel_url, [:custom_arg])
end end
test "returns the error straight through when the command fails" do test "returns the error straight through when the command fails" do
expect(CommandRunnerMock, :run, fn _url, _opts -> {:error, "Big issue", 1} end) 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 end
end end

View file

@ -35,6 +35,12 @@ defmodule Pinchflat.Downloader.Backends.YtDlp.VideoTest do
assert {:ok, %{"title" => "Test"}} = Video.download(@video_url) assert {:ok, %{"title" => "Test"}} = Video.download(@video_url)
end 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 test "it directly passes along any errors" do
expect(CommandRunnerMock, :run, fn _url, _opts -> {:error, "Big issue", 1} end) expect(CommandRunnerMock, :run, fn _url, _opts -> {:error, "Big issue", 1} end)

View 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