Added apprise command runner
This commit is contained in:
parent
aed36acfaa
commit
b71105d7bf
13 changed files with 203 additions and 78 deletions
|
|
@ -3,6 +3,7 @@ import Config
|
|||
config :pinchflat,
|
||||
# Specifying backend data here makes mocking and local testing SUPER easy
|
||||
yt_dlp_executable: Path.join([File.cwd!(), "/test/support/scripts/yt-dlp-mocks/repeater.sh"]),
|
||||
apprise_executable: Path.join([File.cwd!(), "/test/support/scripts/yt-dlp-mocks/repeater.sh"]),
|
||||
media_directory: Path.join([System.tmp_dir!(), "test", "media"]),
|
||||
metadata_directory: Path.join([System.tmp_dir!(), "test", "metadata"]),
|
||||
tmpfile_directory: Path.join([System.tmp_dir!(), "test", "tmpfiles"]),
|
||||
|
|
|
|||
|
|
@ -14,7 +14,6 @@ defmodule Pinchflat.Boot.PreJobStartupTasks do
|
|||
|
||||
alias Pinchflat.Repo
|
||||
alias Pinchflat.Settings
|
||||
alias Pinchflat.YtDlp.CommandRunner
|
||||
alias Pinchflat.Filesystem.FilesystemHelpers
|
||||
|
||||
def start_link(opts \\ []) do
|
||||
|
|
@ -56,15 +55,25 @@ defmodule Pinchflat.Boot.PreJobStartupTasks do
|
|||
filepath = Path.join(base_dir, "cookies.txt")
|
||||
|
||||
if !File.exists?(filepath) do
|
||||
Logger.info("Cookies does not exist - creating it")
|
||||
Logger.info("yt-dlp cookie file does not exist - creating it")
|
||||
|
||||
FilesystemHelpers.write_p!(filepath, "")
|
||||
end
|
||||
end
|
||||
|
||||
defp apply_default_settings do
|
||||
{:ok, yt_dlp_version} = CommandRunner.version()
|
||||
{:ok, yt_dlp_version} = yt_dlp_runner().version()
|
||||
{:ok, apprise_version} = apprise_runner().version()
|
||||
|
||||
Settings.set(yt_dlp_version: yt_dlp_version)
|
||||
Settings.set(apprise_version: apprise_version)
|
||||
end
|
||||
|
||||
defp yt_dlp_runner do
|
||||
Application.get_env(:pinchflat, :yt_dlp_runner)
|
||||
end
|
||||
|
||||
defp apprise_runner do
|
||||
Application.get_env(:pinchflat, :apprise_runner)
|
||||
end
|
||||
end
|
||||
|
|
|
|||
12
lib/pinchflat/notifications/apprise_command_runner.ex
Normal file
12
lib/pinchflat/notifications/apprise_command_runner.ex
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
defmodule Pinchflat.Notifications.AppriseCommandRunner do
|
||||
@moduledoc """
|
||||
A behaviour for running CLI commands against a notification backend (apprise).
|
||||
|
||||
Used so we can implement Mox for testing without actually running the
|
||||
apprise command.
|
||||
"""
|
||||
|
||||
@callback run(binary(), keyword()) :: :ok | {:error, binary()}
|
||||
@callback run(List.t(), keyword()) :: :ok | {:error, binary()}
|
||||
@callback version() :: {:ok, binary()} | {:error, binary()}
|
||||
end
|
||||
|
|
@ -5,16 +5,39 @@ defmodule Pinchflat.Notifications.CommandRunner do
|
|||
|
||||
require Logger
|
||||
|
||||
alias Pinchflat.Utils.CliUtils
|
||||
alias Pinchflat.Utils.FunctionUtils
|
||||
alias Pinchflat.Notifications.AppriseCommandRunner
|
||||
|
||||
@behaviour AppriseCommandRunner
|
||||
|
||||
@doc """
|
||||
# TODO
|
||||
Runs an apprise command and returns the string output (often just "").
|
||||
Can take a single server string or a list of servers as well as additional
|
||||
arguments to pass to the command.
|
||||
|
||||
Returns {:ok, binary()} | {:error, binary()}.
|
||||
"""
|
||||
def run() do
|
||||
@impl AppriseCommandRunner
|
||||
def run(endpoints, args) do
|
||||
endpoints = List.wrap(endpoints)
|
||||
parsed_args = CliUtils.parse_options(args)
|
||||
|
||||
case System.cmd(backend_executable(), parsed_args ++ endpoints) do
|
||||
{output, 0} ->
|
||||
{:ok, String.trim(output)}
|
||||
|
||||
{output, _} ->
|
||||
{:error, String.trim(output)}
|
||||
end
|
||||
end
|
||||
|
||||
# TODO: test
|
||||
# TODO: add behaviour
|
||||
@doc """
|
||||
Returns the version of apprise as a string.
|
||||
|
||||
Returns {:ok, binary()} | {:error, binary()}
|
||||
"""
|
||||
@impl AppriseCommandRunner
|
||||
def version do
|
||||
case System.cmd(backend_executable(), ["--version"]) do
|
||||
{output, 0} ->
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ defmodule Pinchflat.Settings.Setting do
|
|||
:onboarding,
|
||||
:pro_enabled,
|
||||
:yt_dlp_version,
|
||||
:apprise_version,
|
||||
:apprise_server
|
||||
]
|
||||
|
||||
|
|
@ -22,6 +23,7 @@ defmodule Pinchflat.Settings.Setting do
|
|||
field :onboarding, :boolean, default: true
|
||||
field :pro_enabled, :boolean, default: false
|
||||
field :yt_dlp_version, :string
|
||||
field :apprise_version, :string
|
||||
field :apprise_server, :string
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -1,15 +1,28 @@
|
|||
defmodule Pinchflat.Utils.CliUtils do
|
||||
# TODO: test
|
||||
@moduledoc """
|
||||
Utility methods for working with CLI executables
|
||||
"""
|
||||
|
||||
alias Pinchflat.Utils.StringUtils
|
||||
|
||||
@doc """
|
||||
Parses a list of command options into a list of strings suitable for passing to
|
||||
`System.cmd/3`.
|
||||
|
||||
We want to satisfy the following behaviours:
|
||||
1. If the key is an atom, convert it to a string and convert it to kebab case (for convenience)
|
||||
2. If the key is a string, assume we want it as-is and don't convert it
|
||||
3. If the key is accompanied by a value, append the value to the list
|
||||
4. If the key is not accompanied by a value, assume it's a flag and PREpend it to the list
|
||||
|
||||
Returns [binary()]
|
||||
"""
|
||||
def parse_options(command_opts) do
|
||||
Enum.reduce(command_opts, [], &parse_option/2)
|
||||
command_opts
|
||||
|> List.wrap()
|
||||
|> Enum.reduce([], &parse_option/2)
|
||||
end
|
||||
|
||||
# We want to satisfy the following behaviours:
|
||||
#
|
||||
# 1. If the key is an atom, convert it to a string and convert it to kebab case (for convenience)
|
||||
# 2. If the key is a string, assume we want it as-is and don't convert it
|
||||
# 3. If the key is accompanied by a value, append the value to the list
|
||||
# 4. If the key is not accompanied by a value, assume it's a flag and PREpend it to the list
|
||||
defp parse_option({k, v}, acc) when is_atom(k) do
|
||||
stringified_key = StringUtils.to_kebab_case(Atom.to_string(k))
|
||||
|
||||
|
|
@ -21,7 +34,10 @@ defmodule Pinchflat.Utils.CliUtils do
|
|||
end
|
||||
|
||||
defp parse_option(arg, acc) when is_atom(arg) do
|
||||
stringified_arg = StringUtils.to_kebab_case(Atom.to_string(arg))
|
||||
stringified_arg =
|
||||
arg
|
||||
|> Atom.to_string()
|
||||
|> StringUtils.to_kebab_case()
|
||||
|
||||
parse_option("--#{stringified_arg}", acc)
|
||||
end
|
||||
|
|
|
|||
|
|
@ -5,9 +5,9 @@ defmodule Pinchflat.YtDlp.CommandRunner do
|
|||
|
||||
require Logger
|
||||
|
||||
alias Pinchflat.Utils.StringUtils
|
||||
alias Pinchflat.Filesystem.FilesystemHelpers, as: FSUtils
|
||||
alias Pinchflat.Utils.CliUtils
|
||||
alias Pinchflat.YtDlp.YtDlpCommandRunner
|
||||
alias Pinchflat.Filesystem.FilesystemHelpers, as: FSUtils
|
||||
|
||||
@behaviour YtDlpCommandRunner
|
||||
|
||||
|
|
@ -32,7 +32,7 @@ defmodule Pinchflat.YtDlp.CommandRunner do
|
|||
output_filepath = generate_output_filepath(addl_opts)
|
||||
print_to_file_opts = [{:print_to_file, output_template}, output_filepath]
|
||||
cookie_opts = build_cookie_options()
|
||||
formatted_command_opts = [url] ++ parse_options(command_opts ++ print_to_file_opts ++ cookie_opts)
|
||||
formatted_command_opts = [url] ++ CliUtils.parse_options(command_opts ++ print_to_file_opts ++ cookie_opts)
|
||||
|
||||
Logger.info("[yt-dlp] called with: #{Enum.join(formatted_command_opts, " ")}")
|
||||
|
||||
|
|
@ -48,6 +48,11 @@ defmodule Pinchflat.YtDlp.CommandRunner do
|
|||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns the version of yt-dlp as a string
|
||||
|
||||
Returns {:ok, binary()} | {:error, binary()}
|
||||
"""
|
||||
@impl YtDlpCommandRunner
|
||||
def version do
|
||||
command = backend_executable()
|
||||
|
|
@ -81,36 +86,6 @@ defmodule Pinchflat.YtDlp.CommandRunner do
|
|||
end
|
||||
end
|
||||
|
||||
# We want to satisfy the following behaviours:
|
||||
#
|
||||
# 1. If the key is an atom, convert it to a string and convert it to kebab case (for convenience)
|
||||
# 2. If the key is a string, assume we want it as-is and don't convert it
|
||||
# 3. If the key is accompanied by a value, append the value to the list
|
||||
# 4. If the key is not accompanied by a value, assume it's a flag and PREpend it to the list
|
||||
defp parse_options(command_opts) do
|
||||
Enum.reduce(command_opts, [], &parse_option/2)
|
||||
end
|
||||
|
||||
defp parse_option({k, v}, acc) when is_atom(k) do
|
||||
stringified_key = StringUtils.to_kebab_case(Atom.to_string(k))
|
||||
|
||||
parse_option({"--#{stringified_key}", v}, acc)
|
||||
end
|
||||
|
||||
defp parse_option({k, v}, acc) when is_binary(k) do
|
||||
acc ++ [k, to_string(v)]
|
||||
end
|
||||
|
||||
defp parse_option(arg, acc) when is_atom(arg) do
|
||||
stringified_arg = StringUtils.to_kebab_case(Atom.to_string(arg))
|
||||
|
||||
parse_option("--#{stringified_arg}", acc)
|
||||
end
|
||||
|
||||
defp parse_option(arg, acc) when is_binary(arg) do
|
||||
acc ++ [arg]
|
||||
end
|
||||
|
||||
defp backend_executable do
|
||||
Application.get_env(:pinchflat, :yt_dlp_executable)
|
||||
end
|
||||
|
|
|
|||
|
|
@ -0,0 +1,9 @@
|
|||
defmodule Pinchflat.Repo.Migrations.AddAppriseVersionToSettings do
|
||||
use Ecto.Migration
|
||||
|
||||
def change do
|
||||
alter table(:settings) do
|
||||
add :apprise_version, :string
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -1,11 +1,19 @@
|
|||
defmodule Pinchflat.Boot.PreJobStartupTasksTest do
|
||||
use Pinchflat.DataCase
|
||||
|
||||
import Mox
|
||||
import Pinchflat.JobFixtures
|
||||
|
||||
alias Pinchflat.Settings
|
||||
alias Pinchflat.Boot.PreJobStartupTasks
|
||||
|
||||
setup do
|
||||
stub(YtDlpRunnerMock, :version, fn -> {:ok, "1"} end)
|
||||
stub(AppriseRunnerMock, :version, fn -> {:ok, "2"} end)
|
||||
|
||||
:ok
|
||||
end
|
||||
|
||||
describe "reset_executing_jobs" do
|
||||
test "resets executing jobs" do
|
||||
job = job_fixture()
|
||||
|
|
@ -13,7 +21,7 @@ defmodule Pinchflat.Boot.PreJobStartupTasksTest do
|
|||
|
||||
assert Repo.reload!(job).state == "executing"
|
||||
|
||||
PreJobStartupTasks.start_link()
|
||||
PreJobStartupTasks.init(%{})
|
||||
|
||||
assert Repo.reload!(job).state == "retryable"
|
||||
end
|
||||
|
|
@ -27,21 +35,31 @@ defmodule Pinchflat.Boot.PreJobStartupTasksTest do
|
|||
|
||||
refute File.exists?(filepath)
|
||||
|
||||
PreJobStartupTasks.start_link()
|
||||
PreJobStartupTasks.init(%{})
|
||||
|
||||
assert File.exists?(filepath)
|
||||
end
|
||||
end
|
||||
|
||||
describe "apply_default_settings" do
|
||||
test "sets default settings" do
|
||||
test "sets yt_dlp version" do
|
||||
Settings.set(yt_dlp_version: nil)
|
||||
|
||||
refute Settings.get!(:yt_dlp_version)
|
||||
|
||||
PreJobStartupTasks.start_link()
|
||||
PreJobStartupTasks.init(%{})
|
||||
|
||||
assert Settings.get!(:yt_dlp_version)
|
||||
end
|
||||
|
||||
test "sets apprise version" do
|
||||
Settings.set(apprise_version: nil)
|
||||
|
||||
refute Settings.get!(:apprise_version)
|
||||
|
||||
PreJobStartupTasks.init(%{})
|
||||
|
||||
assert Settings.get!(:apprise_version)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
59
test/pinchflat/notifications/command_runner_test.exs
Normal file
59
test/pinchflat/notifications/command_runner_test.exs
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
defmodule Pinchflat.Notifications.CommandRunnerTest do
|
||||
use ExUnit.Case, async: true
|
||||
|
||||
alias Pinchflat.Notifications.CommandRunner, as: Runner
|
||||
|
||||
@original_executable Application.compile_env(:pinchflat, :apprise_executable)
|
||||
|
||||
setup do
|
||||
on_exit(&reset_executable/0)
|
||||
end
|
||||
|
||||
describe "run/2" do
|
||||
test "returns :ok when the command succeeds" do
|
||||
assert {:ok, _} = Runner.run("", [])
|
||||
end
|
||||
|
||||
test "includes the servers as the first argument" do
|
||||
assert {:ok, output} = Runner.run(["server_1", "server_2"], [])
|
||||
|
||||
assert String.contains?(output, "server_1 server_2")
|
||||
end
|
||||
|
||||
test "lets you pass a single server as a string" do
|
||||
assert {:ok, output} = Runner.run("server_1", [])
|
||||
|
||||
assert String.contains?(output, "server_1")
|
||||
end
|
||||
|
||||
test "passes all arguments to the command" do
|
||||
assert {:ok, output} = Runner.run("", ["--dry-run"])
|
||||
|
||||
assert String.contains?(output, "--dry-run")
|
||||
end
|
||||
|
||||
test "returns the output when the command fails" do
|
||||
wrap_executable("/bin/false", fn ->
|
||||
assert {:error, ""} = Runner.run("", [])
|
||||
end)
|
||||
end
|
||||
end
|
||||
|
||||
describe "version/0" do
|
||||
test "adds the version arg" do
|
||||
assert {:ok, output} = Runner.version()
|
||||
|
||||
assert String.contains?(output, "--version")
|
||||
end
|
||||
end
|
||||
|
||||
defp wrap_executable(new_executable, fun) do
|
||||
Application.put_env(:pinchflat, :apprise_executable, new_executable)
|
||||
fun.()
|
||||
reset_executable()
|
||||
end
|
||||
|
||||
def reset_executable do
|
||||
Application.put_env(:pinchflat, :apprise_executable, @original_executable)
|
||||
end
|
||||
end
|
||||
23
test/pinchflat/utils/cli_utils_test.exs
Normal file
23
test/pinchflat/utils/cli_utils_test.exs
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
defmodule Pinchflat.Utils.CliUtilsTest do
|
||||
use ExUnit.Case, async: true
|
||||
|
||||
alias Pinchflat.Utils.CliUtils
|
||||
|
||||
describe "parse_options/1" do
|
||||
test "it converts symbol k-v arg keys to kebab case" do
|
||||
assert ["--buffer-size", "1024"] = CliUtils.parse_options(buffer_size: 1024)
|
||||
end
|
||||
|
||||
test "it keeps string k-v arg keys untouched" do
|
||||
assert ["--under_score", "1024"] = CliUtils.parse_options({"--under_score", 1024})
|
||||
end
|
||||
|
||||
test "it converts symbol arg keys to kebab case" do
|
||||
assert ["--ignore-errors"] = CliUtils.parse_options(:ignore_errors)
|
||||
end
|
||||
|
||||
test "it keeps string arg keys untouched" do
|
||||
assert ["-v"] = CliUtils.parse_options("-v")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -17,31 +17,6 @@ defmodule Pinchflat.YtDlp.CommandRunnerTest do
|
|||
assert {:ok, _output} = Runner.run(@media_url, [], "")
|
||||
end
|
||||
|
||||
test "it converts symbol k-v arg keys to kebab case" do
|
||||
assert {:ok, output} = Runner.run(@media_url, [buffer_size: 1024], "")
|
||||
|
||||
assert String.contains?(output, "--buffer-size 1024")
|
||||
end
|
||||
|
||||
test "it keeps string k-v arg keys untouched" do
|
||||
assert {:ok, output} = Runner.run(@media_url, [{"--under_score", 1024}], "")
|
||||
|
||||
assert String.contains?(output, "--under_score 1024")
|
||||
end
|
||||
|
||||
test "it converts symbol arg keys to kebab case" do
|
||||
assert {:ok, output} = Runner.run(@media_url, [:ignore_errors], "")
|
||||
|
||||
assert String.contains?(output, "--ignore-errors")
|
||||
end
|
||||
|
||||
test "it keeps string arg keys untouched" do
|
||||
assert {:ok, output} = Runner.run(@media_url, ["-v"], "")
|
||||
|
||||
assert String.contains?(output, "-v")
|
||||
refute String.contains?(output, "--v")
|
||||
end
|
||||
|
||||
test "it includes the media url as the first argument" do
|
||||
assert {:ok, output} = Runner.run(@media_url, [:ignore_errors], "")
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,9 @@
|
|||
Mox.defmock(YtDlpRunnerMock, for: Pinchflat.YtDlp.YtDlpCommandRunner)
|
||||
Application.put_env(:pinchflat, :yt_dlp_runner, YtDlpRunnerMock)
|
||||
|
||||
Mox.defmock(AppriseRunnerMock, for: Pinchflat.Notifications.AppriseCommandRunner)
|
||||
Application.put_env(:pinchflat, :apprise_runner, AppriseRunnerMock)
|
||||
|
||||
Mox.defmock(HTTPClientMock, for: Pinchflat.HTTP.HTTPBehaviour)
|
||||
Application.put_env(:pinchflat, :http_client, HTTPClientMock)
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue