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
- 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

View file

@ -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

View file

@ -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)

View file

@ -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

View file

@ -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

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 """
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)

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"
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

View file

@ -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)

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