[Enhancement] Add Apprise support (#170)

* [WIP] add settings sidebar entry and placeholder page

* [WIP] added placeholder UI and logic for settings form

* Added column and UI for apprise server

* Add some tests

* Added placeholder command runner for apprise

* [WIP] Adding apprise package

* Added apprise command runner

* Hooked up apprise notification module

* Ensured apprise was running in verbose mode

* Updated wording of apprise notification

* Added apprise to README
This commit is contained in:
Kieran
2024-04-09 17:45:39 -07:00
committed by GitHub
parent a9f40ed843
commit 26d457e656
38 changed files with 730 additions and 107 deletions
@@ -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
@@ -24,7 +24,7 @@ defmodule Pinchflat.FastIndexing.FastIndexingHelpersTest do
test "enqueues a new worker for each new media_id in the source's RSS feed", %{source: source} do
expect(HTTPClientMock, :get, fn _url -> {:ok, "<yt:videoId>test_1</yt:videoId>"} end)
assert :ok = FastIndexingHelpers.kickoff_indexing_tasks_from_youtube_rss_feed(source)
assert [_] = FastIndexingHelpers.kickoff_indexing_tasks_from_youtube_rss_feed(source)
assert [worker] = all_enqueued(worker: MediaIndexingWorker)
assert worker.args["id"] == source.id
@@ -35,10 +35,16 @@ defmodule Pinchflat.FastIndexing.FastIndexingHelpersTest do
expect(HTTPClientMock, :get, fn _url -> {:ok, "<yt:videoId>test_1</yt:videoId>"} end)
media_item_fixture(source_id: source.id, media_id: "test_1")
assert :ok = FastIndexingHelpers.kickoff_indexing_tasks_from_youtube_rss_feed(source)
assert [] = FastIndexingHelpers.kickoff_indexing_tasks_from_youtube_rss_feed(source)
refute_enqueued(worker: MediaIndexingWorker)
end
test "returns the IDs of the found media items", %{source: source} do
expect(HTTPClientMock, :get, fn _url -> {:ok, "<yt:videoId>test_1</yt:videoId>"} end)
assert ["test_1"] = FastIndexingHelpers.kickoff_indexing_tasks_from_youtube_rss_feed(source)
end
end
describe "index_and_enqueue_download_for_media_item/2" do
@@ -4,6 +4,7 @@ defmodule Pinchflat.FastIndexing.FastIndexingWorkerTest do
import Mox
import Pinchflat.SourcesFixtures
alias Pinchflat.Settings
alias Pinchflat.Sources.Source
alias Pinchflat.FastIndexing.FastIndexingWorker
@@ -74,4 +75,28 @@ defmodule Pinchflat.FastIndexing.FastIndexingWorkerTest do
assert :ok = perform_job(FastIndexingWorker, %{id: 0})
end
end
describe "perform/1 when testing notifications" do
setup do
Settings.set(apprise_server: "server_1")
:ok
end
test "sends a notification if new media was found" do
source = source_fixture(fast_index: true)
expect(HTTPClientMock, :get, fn _url -> {:ok, "<yt:videoId>test_1</yt:videoId>"} end)
expect(AppriseRunnerMock, :run, fn servers, opts ->
assert "server_1" = servers
assert is_binary(Keyword.get(opts, :title))
assert is_binary(Keyword.get(opts, :body))
{:ok, ""}
end)
perform_job(FastIndexingWorker, %{id: source.id})
end
end
end
@@ -0,0 +1,65 @@
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("server_1", [])
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("server_1", ["--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("server_1", [])
end)
end
test "returns a relevant error if no servers are provided" do
assert {:error, :no_servers} = Runner.run(nil, [])
assert {:error, :no_servers} = Runner.run("", [])
assert {:error, :no_servers} = Runner.run([], [])
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
@@ -0,0 +1,100 @@
defmodule Pinchflat.Notifications.SourceNotificationsTest do
use Pinchflat.DataCase
import Mox
import Pinchflat.MediaFixtures
import Pinchflat.SourcesFixtures
alias Pinchflat.Notifications.SourceNotifications
@apprise_servers ["server_1", "server_2"]
setup :verify_on_exit!
describe "wrap_new_media_notification/3" do
test "sends a notification when the pending count changes" do
source = source_fixture()
expect(AppriseRunnerMock, :run, fn servers, opts ->
assert servers == @apprise_servers
assert opts == [
title: "[Pinchflat] New media found",
body: "Found 1 new media item(s) for #{source.custom_name}. Downloading them now"
]
{:ok, ""}
end)
SourceNotifications.wrap_new_media_notification(@apprise_servers, source, fn ->
media_item_fixture(%{source_id: source.id, media_filepath: nil})
end)
end
test "sends a notification when the downloaded count changes" do
source = source_fixture()
expect(AppriseRunnerMock, :run, fn servers, opts ->
assert servers == @apprise_servers
assert opts == [
title: "[Pinchflat] New media found",
body: "Found 1 new media item(s) for #{source.custom_name}. Downloading them now"
]
{:ok, ""}
end)
SourceNotifications.wrap_new_media_notification(@apprise_servers, source, fn ->
media_item_fixture(%{source_id: source.id, media_filepath: "file.mp4"})
end)
end
test "does not send a notification when the count does not change" do
source = source_fixture()
expect(AppriseRunnerMock, :run, 0, fn _, _ -> {:ok, ""} end)
SourceNotifications.wrap_new_media_notification(@apprise_servers, source, fn ->
media_item_fixture(%{source_id: source.id, prevent_download: true, media_filepath: nil})
end)
end
test "returns the value of the function" do
source = source_fixture()
expect(AppriseRunnerMock, :run, 0, fn _, _ -> {:ok, ""} end)
retval = SourceNotifications.wrap_new_media_notification(@apprise_servers, source, fn -> "value" end)
assert retval == "value"
end
end
describe "send_new_media_notification/3" do
test "sends a notification when count is positive" do
source = source_fixture()
expect(AppriseRunnerMock, :run, fn servers, opts ->
assert servers == @apprise_servers
assert opts == [
title: "[Pinchflat] New media found",
body: "Found 1 new media item(s) for #{source.custom_name}. Downloading them now"
]
{:ok, ""}
end)
:ok = SourceNotifications.send_new_media_notification(@apprise_servers, source, 1)
end
test "does not send a notification when count not positive" do
source = source_fixture()
expect(AppriseRunnerMock, :run, 0, fn _, _ -> {:ok, ""} end)
:ok = SourceNotifications.send_new_media_notification(@apprise_servers, source, 0)
:ok = SourceNotifications.send_new_media_notification(@apprise_servers, source, -1)
end
end
end
+18
View File
@@ -24,6 +24,16 @@ defmodule Pinchflat.SettingsTest do
end
end
describe "update_setting/2" do
test "updates the setting" do
setting = Settings.record()
assert {:ok, false} = Settings.get(:onboarding)
assert {:ok, %Setting{}} = Settings.update_setting(setting, %{onboarding: true})
assert {:ok, true} = Settings.get(:onboarding)
end
end
describe "set/1" do
test "updates the setting" do
assert {:ok, true} = Settings.set(onboarding: true)
@@ -60,4 +70,12 @@ defmodule Pinchflat.SettingsTest do
end
end
end
describe "change_setting/2" do
test "returns a changeset" do
setting = Settings.record()
assert %Ecto.Changeset{} = Settings.change_setting(setting, %{onboarding: true})
end
end
end
@@ -7,6 +7,7 @@ defmodule Pinchflat.SlowIndexing.MediaCollectionIndexingWorkerTest do
import Pinchflat.SourcesFixtures
alias Pinchflat.Tasks
alias Pinchflat.Settings
alias Pinchflat.Sources.Source
alias Pinchflat.FastIndexing.FastIndexingWorker
alias Pinchflat.Downloading.MediaDownloadWorker
@@ -51,6 +52,12 @@ defmodule Pinchflat.SlowIndexing.MediaCollectionIndexingWorkerTest do
end
describe "perform/1" do
setup do
stub(AppriseRunnerMock, :run, fn _, _ -> {:ok, ""} end)
:ok
end
test "it indexes the source if it should be indexed" do
expect(YtDlpRunnerMock, :run, fn _url, _opts, _ot, _addl_opts -> {:ok, ""} end)
@@ -210,4 +217,30 @@ defmodule Pinchflat.SlowIndexing.MediaCollectionIndexingWorkerTest do
assert :ok = perform_job(MediaCollectionIndexingWorker, %{id: 0})
end
end
describe "perform/1 when testing apprise notifications" do
setup do
Settings.set(apprise_server: "server_1")
:ok
end
test "sends a notification if new media was found" do
source = source_fixture()
expect(YtDlpRunnerMock, :run, fn _url, _opts, _ot, _addl_opts ->
{:ok, source_attributes_return_fixture()}
end)
expect(AppriseRunnerMock, :run, fn servers, opts ->
assert "server_1" = servers
assert is_binary(Keyword.get(opts, :title))
assert is_binary(Keyword.get(opts, :body))
{:ok, ""}
end)
perform_job(MediaCollectionIndexingWorker, %{id: source.id})
end
end
end
+23
View 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], "")
@@ -0,0 +1,23 @@
defmodule PinchflatWeb.SettingControllerTest do
use PinchflatWeb.ConnCase
describe "show settings" do
test "renders the page", %{conn: conn} do
conn = get(conn, ~p"/settings")
assert html_response(conn, 200) =~ "Settings"
end
end
describe "update settings" do
test "saves and redirects when data is valid", %{conn: conn} do
update_attrs = %{apprise_server: "test://server"}
conn = put(conn, ~p"/settings", setting: update_attrs)
assert redirected_to(conn) == ~p"/settings"
conn = get(conn, ~p"/settings")
assert html_response(conn, 200) =~ update_attrs[:apprise_server]
end
end
end
+4 -1
View File
@@ -1,6 +1,9 @@
Mox.defmock(YtDlpRunnerMock, for: Pinchflat.YtDlp.BackendCommandRunner)
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)