[Enhancement] Custom media lifecycle scripts (#219)
* Namespaced notification modules under lifecycle * Added a JSON encoder for all the main model types * Added startup task to create user script file * Hook up user script event to media download * Hooked up media deletion user script * Added jq to docker deps * Updated README
This commit is contained in:
@@ -53,6 +53,30 @@ defmodule Pinchflat.Boot.PreJobStartupTasksTest do
|
||||
end
|
||||
end
|
||||
|
||||
describe "create_blank_user_script_file" do
|
||||
test "creates a blank script file" do
|
||||
base_dir = Application.get_env(:pinchflat, :extras_directory)
|
||||
filepath = Path.join([base_dir, "user-scripts", "lifecycle"])
|
||||
File.rm(filepath)
|
||||
|
||||
refute File.exists?(filepath)
|
||||
|
||||
PreJobStartupTasks.init(%{})
|
||||
|
||||
assert File.exists?(filepath)
|
||||
end
|
||||
|
||||
test "gives it 755 permissions" do
|
||||
base_dir = Application.get_env(:pinchflat, :extras_directory)
|
||||
filepath = Path.join([base_dir, "user-scripts", "lifecycle"])
|
||||
File.rm(filepath)
|
||||
|
||||
PreJobStartupTasks.init(%{})
|
||||
|
||||
assert File.stat!(filepath).mode == 0o100755
|
||||
end
|
||||
end
|
||||
|
||||
describe "apply_default_settings" do
|
||||
test "sets yt_dlp version" do
|
||||
Settings.set(yt_dlp_version: nil)
|
||||
|
||||
@@ -12,9 +12,8 @@ defmodule Pinchflat.Downloading.MediaDownloadWorkerTest do
|
||||
setup :verify_on_exit!
|
||||
|
||||
setup do
|
||||
stub(HTTPClientMock, :get, fn _url, _headers, _opts ->
|
||||
{:ok, ""}
|
||||
end)
|
||||
stub(UserScriptRunnerMock, :run, fn _event_type, _data -> :ok end)
|
||||
stub(HTTPClientMock, :get, fn _url, _headers, _opts -> {:ok, ""} end)
|
||||
|
||||
media_item =
|
||||
%{media_filepath: nil}
|
||||
@@ -185,6 +184,20 @@ defmodule Pinchflat.Downloading.MediaDownloadWorkerTest do
|
||||
assert media_item.media_redownloaded_at == nil
|
||||
end
|
||||
|
||||
test "calls the user script runner", %{media_item: media_item} do
|
||||
expect(YtDlpRunnerMock, :run, fn _url, _opts, _ot, _addl ->
|
||||
{:ok, render_metadata(:media_metadata)}
|
||||
end)
|
||||
|
||||
expect(UserScriptRunnerMock, :run, fn :media_downloaded, data ->
|
||||
assert data.id == media_item.id
|
||||
|
||||
:ok
|
||||
end)
|
||||
|
||||
perform_job(MediaDownloadWorker, %{id: media_item.id})
|
||||
end
|
||||
|
||||
test "does not blow up if the record doesn't exist" do
|
||||
assert :ok = perform_job(MediaDownloadWorker, %{id: 0})
|
||||
end
|
||||
|
||||
@@ -1,13 +1,22 @@
|
||||
defmodule Pinchflat.Downloading.MediaRetentionWorkerTest do
|
||||
use Pinchflat.DataCase
|
||||
|
||||
import Mox
|
||||
import Pinchflat.MediaFixtures
|
||||
import Pinchflat.SourcesFixtures
|
||||
|
||||
alias Pinchflat.Media
|
||||
alias Pinchflat.Downloading.MediaRetentionWorker
|
||||
|
||||
setup :verify_on_exit!
|
||||
|
||||
describe "perform/1" do
|
||||
setup do
|
||||
stub(UserScriptRunnerMock, :run, fn _event_type, _data -> :ok end)
|
||||
|
||||
:ok
|
||||
end
|
||||
|
||||
test "deletes media files that are past their retention date" do
|
||||
{_source, old_media_item, new_media_item} = prepare_records()
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
defmodule Pinchflat.Downloading.OutputPath.ParserTest do
|
||||
use ExUnit.Case, async: true
|
||||
use ExUnit.Case, async: false
|
||||
|
||||
alias Pinchflat.Downloading.OutputPath.Parser
|
||||
|
||||
|
||||
+3
-3
@@ -1,7 +1,7 @@
|
||||
defmodule Pinchflat.Notifications.CommandRunnerTest do
|
||||
use ExUnit.Case, async: true
|
||||
defmodule Pinchflat.Lifecycle.Notifications.CommandRunnerTest do
|
||||
use ExUnit.Case, async: false
|
||||
|
||||
alias Pinchflat.Notifications.CommandRunner, as: Runner
|
||||
alias Pinchflat.Lifecycle.Notifications.CommandRunner, as: Runner
|
||||
|
||||
@original_executable Application.compile_env(:pinchflat, :apprise_executable)
|
||||
|
||||
+2
-2
@@ -1,11 +1,11 @@
|
||||
defmodule Pinchflat.Notifications.SourceNotificationsTest do
|
||||
defmodule Pinchflat.Lifecycle.Notifications.SourceNotificationsTest do
|
||||
use Pinchflat.DataCase
|
||||
|
||||
import Mox
|
||||
import Pinchflat.MediaFixtures
|
||||
import Pinchflat.SourcesFixtures
|
||||
|
||||
alias Pinchflat.Notifications.SourceNotifications
|
||||
alias Pinchflat.Lifecycle.Notifications.SourceNotifications
|
||||
|
||||
@apprise_servers ["server_1", "server_2"]
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
defmodule Pinchflat.Lifecycle.UserScripts.CommandRunnerTest do
|
||||
use ExUnit.Case, async: false
|
||||
|
||||
alias Pinchflat.Utils.FilesystemUtils
|
||||
alias Pinchflat.Lifecycle.UserScripts.CommandRunner, as: Runner
|
||||
|
||||
setup do
|
||||
FilesystemUtils.write_p!(filepath(), "")
|
||||
File.chmod(filepath(), 0o755)
|
||||
|
||||
:ok
|
||||
end
|
||||
|
||||
describe "run/2" do
|
||||
test "runs the provided lifecycle file if present" do
|
||||
# We *love* indirectly testing side effects
|
||||
tmp_dir = Application.get_env(:pinchflat, :tmpfile_directory)
|
||||
File.write(filepath(), "#!/bin/bash\ntouch #{tmp_dir}/test_file\n")
|
||||
|
||||
refute File.exists?("#{tmp_dir}/test_file")
|
||||
assert :ok = Runner.run(:media_downloaded, %{})
|
||||
assert File.exists?("#{tmp_dir}/test_file")
|
||||
end
|
||||
|
||||
test "passes the event name to the script" do
|
||||
tmp_dir = Application.get_env(:pinchflat, :tmpfile_directory)
|
||||
File.write(filepath(), "#!/bin/bash\necho $1 > #{tmp_dir}/event_name\n")
|
||||
|
||||
assert :ok = Runner.run(:media_downloaded, %{})
|
||||
assert File.read!("#{tmp_dir}/event_name") == "media_downloaded\n"
|
||||
end
|
||||
|
||||
test "passes the encoded data to the script" do
|
||||
tmp_dir = Application.get_env(:pinchflat, :tmpfile_directory)
|
||||
File.write(filepath(), "#!/bin/bash\necho $2 > #{tmp_dir}/encoded_data\n")
|
||||
|
||||
assert :ok = Runner.run(:media_downloaded, %{foo: "bar"})
|
||||
assert File.read!("#{tmp_dir}/encoded_data") == "{\"foo\":\"bar\"}\n"
|
||||
end
|
||||
|
||||
test "does nothing if the lifecycle file is not present" do
|
||||
:ok = File.rm(filepath())
|
||||
|
||||
assert :ok = Runner.run(:media_downloaded, %{})
|
||||
end
|
||||
|
||||
test "does nothing if the lifecycle file is empty" do
|
||||
File.write(filepath(), "")
|
||||
|
||||
assert :ok = Runner.run(:media_downloaded, %{})
|
||||
end
|
||||
|
||||
test "returns :ok if the command exits with a non-zero status" do
|
||||
File.write(filepath(), "#!/bin/bash\nexit 1\n")
|
||||
|
||||
assert :ok = Runner.run(:media_downloaded, %{})
|
||||
end
|
||||
|
||||
test "gets upset if you pass an invalid event type" do
|
||||
assert_raise ArgumentError, "Invalid event type: :invalid_event", fn ->
|
||||
Runner.run(:invalid_event, %{})
|
||||
end
|
||||
end
|
||||
|
||||
test "gets upset if the record cannot be decoded" do
|
||||
File.write(filepath(), "#!/bin/bash")
|
||||
|
||||
assert_raise MatchError, fn ->
|
||||
Runner.run(:media_downloaded, %Ecto.Changeset{})
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
defp filepath do
|
||||
base_dir = Application.get_env(:pinchflat, :extras_directory)
|
||||
|
||||
Path.join([base_dir, "user-scripts", "lifecycle"])
|
||||
end
|
||||
end
|
||||
@@ -29,6 +29,12 @@ defmodule Pinchflat.MediaTest do
|
||||
Repo.reload!(metadata)
|
||||
end
|
||||
end
|
||||
|
||||
test "can be JSON encoded without error" do
|
||||
media_item = media_item_fixture()
|
||||
|
||||
assert {:ok, _} = Phoenix.json_library().encode(media_item)
|
||||
end
|
||||
end
|
||||
|
||||
describe "list_media_items/0" do
|
||||
@@ -743,6 +749,12 @@ defmodule Pinchflat.MediaTest do
|
||||
end
|
||||
|
||||
describe "delete_media_item/2 when testing file deletion" do
|
||||
setup do
|
||||
stub(UserScriptRunnerMock, :run, fn _event_type, _data -> :ok end)
|
||||
|
||||
:ok
|
||||
end
|
||||
|
||||
test "deletes the media item's files" do
|
||||
media_item = media_item_with_attachments()
|
||||
|
||||
@@ -793,9 +805,27 @@ defmodule Pinchflat.MediaTest do
|
||||
:ok = File.rm(Path.join([root_directory, "test.txt"]))
|
||||
:ok = File.rmdir(root_directory)
|
||||
end
|
||||
|
||||
test "calls the user script runner" do
|
||||
media_item = media_item_with_attachments()
|
||||
|
||||
expect(UserScriptRunnerMock, :run, fn :media_deleted, data ->
|
||||
assert data.id == media_item.id
|
||||
|
||||
:ok
|
||||
end)
|
||||
|
||||
assert {:ok, _} = Media.delete_media_item(media_item, delete_files: true)
|
||||
end
|
||||
end
|
||||
|
||||
describe "delete_media_files/2" do
|
||||
setup do
|
||||
stub(UserScriptRunnerMock, :run, fn _event_type, _data -> :ok end)
|
||||
|
||||
:ok
|
||||
end
|
||||
|
||||
test "does not delete the media_item" do
|
||||
media_item = media_item_fixture()
|
||||
|
||||
@@ -848,6 +878,18 @@ defmodule Pinchflat.MediaTest do
|
||||
assert {:ok, updated_media_item} = Media.delete_media_files(media_item, %{prevent_download: true})
|
||||
assert updated_media_item.prevent_download
|
||||
end
|
||||
|
||||
test "calls the user script runner" do
|
||||
media_item = media_item_with_attachments()
|
||||
|
||||
expect(UserScriptRunnerMock, :run, fn :media_deleted, data ->
|
||||
assert data.id == media_item.id
|
||||
|
||||
:ok
|
||||
end)
|
||||
|
||||
assert {:ok, _} = Media.delete_media_files(media_item)
|
||||
end
|
||||
end
|
||||
|
||||
describe "change_media_item/1" do
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
defmodule Pinchflat.ProfilesTest do
|
||||
use Pinchflat.DataCase
|
||||
|
||||
import Mox
|
||||
import Pinchflat.MediaFixtures
|
||||
import Pinchflat.SourcesFixtures
|
||||
import Pinchflat.ProfilesFixtures
|
||||
@@ -10,6 +11,16 @@ defmodule Pinchflat.ProfilesTest do
|
||||
|
||||
@invalid_attrs %{name: nil, output_path_template: nil}
|
||||
|
||||
setup :verify_on_exit!
|
||||
|
||||
describe "schema" do
|
||||
test "can be JSON encoded without error" do
|
||||
profile = media_profile_fixture()
|
||||
|
||||
assert {:ok, _} = Phoenix.json_library().encode(profile)
|
||||
end
|
||||
end
|
||||
|
||||
describe "list_media_profiles/0" do
|
||||
test "it returns all media_profiles" do
|
||||
media_profile = media_profile_fixture()
|
||||
@@ -104,6 +115,12 @@ defmodule Pinchflat.ProfilesTest do
|
||||
end
|
||||
|
||||
describe "delete_media_profile/2 when deleting files" do
|
||||
setup do
|
||||
stub(UserScriptRunnerMock, :run, fn _event_type, _data -> :ok end)
|
||||
|
||||
:ok
|
||||
end
|
||||
|
||||
test "still deletes all the needful records" do
|
||||
media_profile = media_profile_fixture()
|
||||
source = source_fixture(media_profile_id: media_profile.id)
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
defmodule Pinchflat.SlowIndexing.FileFollowerServerTest do
|
||||
use ExUnit.Case, async: true
|
||||
use ExUnit.Case, async: false
|
||||
|
||||
alias alias Pinchflat.Utils.FilesystemUtils
|
||||
alias Pinchflat.SlowIndexing.FileFollowerServer
|
||||
|
||||
@@ -32,6 +32,12 @@ defmodule Pinchflat.SourcesTest do
|
||||
Repo.reload!(metadata)
|
||||
end
|
||||
end
|
||||
|
||||
test "can be JSON encoded without error" do
|
||||
source = source_fixture()
|
||||
|
||||
assert {:ok, _} = Phoenix.json_library().encode(source)
|
||||
end
|
||||
end
|
||||
|
||||
describe "output_path_template/1" do
|
||||
@@ -612,6 +618,12 @@ defmodule Pinchflat.SourcesTest do
|
||||
end
|
||||
|
||||
describe "delete_source/2 when deleting files" do
|
||||
setup do
|
||||
stub(UserScriptRunnerMock, :run, fn _event_type, _data -> :ok end)
|
||||
|
||||
:ok
|
||||
end
|
||||
|
||||
test "deletes source and media_items" do
|
||||
source = source_fixture()
|
||||
media_item = media_item_with_attachments(%{source_id: source.id})
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
defmodule Pinchflat.Utils.ChangesetUtilsTest do
|
||||
use ExUnit.Case, async: true
|
||||
use ExUnit.Case, async: false
|
||||
|
||||
defmodule MockSchema do
|
||||
use Ecto.Schema
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
defmodule Pinchflat.Utils.CliUtilsTest do
|
||||
use ExUnit.Case, async: true
|
||||
use ExUnit.Case, async: false
|
||||
|
||||
alias Pinchflat.Utils.CliUtils
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
defmodule Pinchflat.Utils.DatetimeUtilsTest do
|
||||
use ExUnit.Case, async: true
|
||||
use ExUnit.Case, async: false
|
||||
|
||||
alias Pinchflat.Utils.DatetimeUtils
|
||||
|
||||
|
||||
@@ -5,6 +5,38 @@ defmodule Pinchflat.Utils.FilesystemUtilsTest do
|
||||
|
||||
alias Pinchflat.Utils.FilesystemUtils
|
||||
|
||||
describe "exists_and_nonempty?" do
|
||||
test "returns true if a file exists and has contents" do
|
||||
filepath = FilesystemUtils.generate_metadata_tmpfile(:json)
|
||||
File.write(filepath, "{}")
|
||||
|
||||
assert FilesystemUtils.exists_and_nonempty?(filepath)
|
||||
|
||||
File.rm!(filepath)
|
||||
end
|
||||
|
||||
test "returns false if a file doesn't exist" do
|
||||
refute FilesystemUtils.exists_and_nonempty?("/nonexistent/file.json")
|
||||
end
|
||||
|
||||
test "returns false if a file exists but is empty" do
|
||||
filepath = FilesystemUtils.generate_metadata_tmpfile(:json)
|
||||
|
||||
refute FilesystemUtils.exists_and_nonempty?(filepath)
|
||||
|
||||
File.rm!(filepath)
|
||||
end
|
||||
|
||||
test "trims the contents before checking" do
|
||||
filepath = FilesystemUtils.generate_metadata_tmpfile(:json)
|
||||
File.write(filepath, " \n\n \r\n ")
|
||||
|
||||
refute FilesystemUtils.exists_and_nonempty?(filepath)
|
||||
|
||||
File.rm!(filepath)
|
||||
end
|
||||
end
|
||||
|
||||
describe "generate_metadata_tmpfile/1" do
|
||||
test "creates a tmpfile and returns its path" do
|
||||
res = FilesystemUtils.generate_metadata_tmpfile(:json)
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
defmodule Pinchflat.Utils.FunctionUtilsTest do
|
||||
use ExUnit.Case, async: true
|
||||
use ExUnit.Case, async: false
|
||||
|
||||
alias Pinchflat.Utils.FunctionUtils
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
defmodule Pinchflat.Utils.NumberUtilsTest do
|
||||
use ExUnit.Case, async: true
|
||||
use ExUnit.Case, async: false
|
||||
|
||||
alias Pinchflat.Utils.NumberUtils
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
defmodule Pinchflat.Utils.StringUtilsTest do
|
||||
use ExUnit.Case, async: true
|
||||
use ExUnit.Case, async: false
|
||||
|
||||
alias Pinchflat.Utils.StringUtils
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
defmodule Pinchflat.Utils.XmlUtilsTest do
|
||||
use ExUnit.Case, async: true
|
||||
use ExUnit.Case, async: false
|
||||
|
||||
alias Pinchflat.Utils.XmlUtils
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
defmodule Pinchflat.YtDlp.CommandRunnerTest do
|
||||
use ExUnit.Case, async: true
|
||||
use ExUnit.Case, async: false
|
||||
|
||||
alias Pinchflat.Utils.FilesystemUtils
|
||||
|
||||
@@ -75,6 +75,9 @@ defmodule Pinchflat.YtDlp.CommandRunnerTest do
|
||||
|
||||
refute String.contains?(output, "--cookies")
|
||||
refute String.contains?(output, cookie_file)
|
||||
|
||||
# Cleanup
|
||||
FilesystemUtils.write_p!(cookie_file, "")
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
defmodule PinchflatWeb.ErrorHTMLTest do
|
||||
use PinchflatWeb.ConnCase, async: true
|
||||
use PinchflatWeb.ConnCase, async: false
|
||||
|
||||
# Bring render_to_string/4 for testing custom views
|
||||
import Phoenix.Template
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
defmodule PinchflatWeb.ErrorJSONTest do
|
||||
use PinchflatWeb.ConnCase, async: true
|
||||
use PinchflatWeb.ConnCase, async: false
|
||||
|
||||
test "renders 404" do
|
||||
assert PinchflatWeb.ErrorJSON.render("404.json", %{}) == %{errors: %{detail: "Not Found"}}
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
defmodule PinchflatWeb.MediaItemControllerTest do
|
||||
use PinchflatWeb.ConnCase
|
||||
|
||||
import Mox
|
||||
import Pinchflat.MediaFixtures
|
||||
|
||||
alias Pinchflat.Repo
|
||||
alias Pinchflat.Downloading.MediaDownloadWorker
|
||||
|
||||
setup :verify_on_exit!
|
||||
|
||||
describe "show media" do
|
||||
setup [:create_media_item]
|
||||
|
||||
@@ -49,6 +52,7 @@ defmodule PinchflatWeb.MediaItemControllerTest do
|
||||
describe "delete media" do
|
||||
setup do
|
||||
media_item = media_item_with_attachments()
|
||||
stub(UserScriptRunnerMock, :run, fn _event_type, _data -> :ok end)
|
||||
|
||||
%{media_item: media_item}
|
||||
end
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
defmodule PinchflatWeb.MediaProfileControllerTest do
|
||||
use PinchflatWeb.ConnCase
|
||||
|
||||
import Mox
|
||||
import Pinchflat.MediaFixtures
|
||||
import Pinchflat.SourcesFixtures
|
||||
import Pinchflat.ProfilesFixtures
|
||||
@@ -15,6 +16,8 @@ defmodule PinchflatWeb.MediaProfileControllerTest do
|
||||
}
|
||||
@invalid_attrs %{name: nil, output_path_template: nil}
|
||||
|
||||
setup :verify_on_exit!
|
||||
|
||||
setup do
|
||||
Settings.set(onboarding: false)
|
||||
|
||||
@@ -136,6 +139,12 @@ defmodule PinchflatWeb.MediaProfileControllerTest do
|
||||
describe "delete media_profile when deleting the records and files" do
|
||||
setup [:create_media_profile]
|
||||
|
||||
setup do
|
||||
stub(UserScriptRunnerMock, :run, fn _event_type, _data -> :ok end)
|
||||
|
||||
:ok
|
||||
end
|
||||
|
||||
test "deletes chosen media_profile and its associations", %{conn: conn, media_profile: media_profile} do
|
||||
source = source_fixture(media_profile_id: media_profile.id)
|
||||
media_item = media_item_with_attachments(%{source_id: source.id})
|
||||
|
||||
@@ -145,6 +145,12 @@ defmodule PinchflatWeb.SourceControllerTest do
|
||||
describe "delete source when deleting the records and files" do
|
||||
setup [:create_source]
|
||||
|
||||
setup do
|
||||
stub(UserScriptRunnerMock, :run, fn _event_type, _data -> :ok end)
|
||||
|
||||
:ok
|
||||
end
|
||||
|
||||
test "deletes chosen source and media_items", %{conn: conn, source: source, media_item: media_item} do
|
||||
delete(conn, ~p"/sources/#{source}?delete_files=true")
|
||||
|
||||
|
||||
@@ -1,12 +1,15 @@
|
||||
Mox.defmock(YtDlpRunnerMock, for: Pinchflat.YtDlp.YtDlpCommandRunner)
|
||||
Application.put_env(:pinchflat, :yt_dlp_runner, YtDlpRunnerMock)
|
||||
|
||||
Mox.defmock(AppriseRunnerMock, for: Pinchflat.Notifications.AppriseCommandRunner)
|
||||
Mox.defmock(AppriseRunnerMock, for: Pinchflat.Lifecycle.Notifications.AppriseCommandRunner)
|
||||
Application.put_env(:pinchflat, :apprise_runner, AppriseRunnerMock)
|
||||
|
||||
Mox.defmock(HTTPClientMock, for: Pinchflat.HTTP.HTTPBehaviour)
|
||||
Application.put_env(:pinchflat, :http_client, HTTPClientMock)
|
||||
|
||||
Mox.defmock(UserScriptRunnerMock, for: Pinchflat.Lifecycle.UserScripts.UserScriptCommandRunner)
|
||||
Application.put_env(:pinchflat, :user_script_runner, UserScriptRunnerMock)
|
||||
|
||||
ExUnit.start()
|
||||
Ecto.Adapters.SQL.Sandbox.mode(Pinchflat.Repo, :manual)
|
||||
Faker.start()
|
||||
|
||||
Reference in New Issue
Block a user