[Enhancement] Ignore media based on user scripts (#330)

* Integrated pre-download callback for media items

* Refactored existing tests

* Docs, etc
This commit is contained in:
Kieran
2024-07-22 10:47:49 -07:00
committed by GitHub
parent d392fc3818
commit 7a01db05dd
12 changed files with 108 additions and 48 deletions
@@ -39,17 +39,14 @@ defmodule Pinchflat.Downloading.MediaDownloadWorker do
- `quality_upgrade?`: re-downloads media, including the video. Does not force download - `quality_upgrade?`: re-downloads media, including the video. Does not force download
if the source is set to not download media if the source is set to not download media
Returns :ok | {:ok, %MediaItem{}} | {:error, any, ...any} Returns :ok | {:error, any, ...any}
""" """
@impl Oban.Worker @impl Oban.Worker
def perform(%Oban.Job{args: %{"id" => media_item_id} = args}) do def perform(%Oban.Job{args: %{"id" => media_item_id} = args}) do
should_force = Map.get(args, "force", false) should_force = Map.get(args, "force", false)
is_quality_upgrade = Map.get(args, "quality_upgrade?", false) is_quality_upgrade = Map.get(args, "quality_upgrade?", false)
media_item = media_item = fetch_and_run_prevent_download_user_script(media_item_id)
media_item_id
|> Media.get_media_item!()
|> Repo.preload(:source)
# If the source or media item is set to not download media, perform a no-op unless forced # If the source or media item is set to not download media, perform a no-op unless forced
if (media_item.source.download_media && !media_item.prevent_download) || should_force do if (media_item.source.download_media && !media_item.prevent_download) || should_force do
@@ -62,6 +59,20 @@ defmodule Pinchflat.Downloading.MediaDownloadWorker do
Ecto.StaleEntryError -> Logger.info("#{__MODULE__} discarded: media item #{media_item_id} stale") Ecto.StaleEntryError -> Logger.info("#{__MODULE__} discarded: media item #{media_item_id} stale")
end end
# If a user script exists and, when run, returns a non-zero exit code, prevent this and all future downloads
# of the media item.
defp fetch_and_run_prevent_download_user_script(media_item_id) do
media_item = Media.get_media_item!(media_item_id)
{:ok, media_item} =
case run_user_script(:media_pre_download, media_item) do
{:ok, _, exit_code} when exit_code != 0 -> Media.update_media_item(media_item, %{prevent_download: true})
_ -> {:ok, media_item}
end
Repo.preload(media_item, :source)
end
defp download_media_and_schedule_jobs(media_item, is_quality_upgrade, should_force) do defp download_media_and_schedule_jobs(media_item, is_quality_upgrade, should_force) do
overwrite_behaviour = if should_force || is_quality_upgrade, do: :force_overwrites, else: :no_force_overwrites overwrite_behaviour = if should_force || is_quality_upgrade, do: :force_overwrites, else: :no_force_overwrites
override_opts = [overwrite_behaviour: overwrite_behaviour] override_opts = [overwrite_behaviour: overwrite_behaviour]
@@ -74,9 +85,9 @@ defmodule Pinchflat.Downloading.MediaDownloadWorker do
media_redownloaded_at: get_redownloaded_at(is_quality_upgrade) media_redownloaded_at: get_redownloaded_at(is_quality_upgrade)
}) })
:ok = run_user_script(updated_media_item) run_user_script(:media_downloaded, updated_media_item)
{:ok, updated_media_item} :ok
{:recovered, _} -> {:recovered, _} ->
{:error, :retry} {:error, :retry}
@@ -112,9 +123,9 @@ defmodule Pinchflat.Downloading.MediaDownloadWorker do
# NOTE: I like this pattern of using the default value so that I don't have to # NOTE: I like this pattern of using the default value so that I don't have to
# define it in config.exs (and friends). Consider using this elsewhere. # define it in config.exs (and friends). Consider using this elsewhere.
defp run_user_script(media_item) do defp run_user_script(event, media_item) do
runner = Application.get_env(:pinchflat, :user_script_runner, UserScriptRunner) runner = Application.get_env(:pinchflat, :user_script_runner, UserScriptRunner)
runner.run(:media_downloaded, media_item) runner.run(event, media_item)
end end
end end
@@ -12,6 +12,7 @@ defmodule Pinchflat.Lifecycle.UserScripts.CommandRunner do
@behaviour UserScriptCommandRunner @behaviour UserScriptCommandRunner
@event_types [ @event_types [
:media_pre_download,
:media_downloaded, :media_downloaded,
:media_deleted :media_deleted
] ]
@@ -22,24 +23,25 @@ defmodule Pinchflat.Lifecycle.UserScripts.CommandRunner do
This function will succeed in almost all cases, even if the user script command This function will succeed in almost all cases, even if the user script command
failed - this is because I don't want bad scripts to stop the whole process. failed - this is because I don't want bad scripts to stop the whole process.
If something fails, it'll be logged. If something fails, it'll be logged and returned BUT the tuple will always
start with {:ok, ...}.
The only things that can cause a true failure are passing in an invalid event The only things that can cause a true failure are passing in an invalid event
type or if the passed data cannot be encoded into JSON - both indicative of type or if the passed data cannot be encoded into JSON - both indicative of
failures in the development process. failures in the development process.
Returns :ok Returns {:ok, :no_executable} | {:ok, output, exit_code}
""" """
@impl UserScriptCommandRunner @impl UserScriptCommandRunner
def run(event_type, encodable_data) when event_type in @event_types do def run(event_type, encodable_data) when event_type in @event_types do
case backend_executable() do case backend_executable() do
{:ok, :no_executable} -> {:ok, :no_executable} ->
:ok {:ok, :no_executable}
{:ok, executable_path} -> {:ok, executable_path} ->
{:ok, encoded_data} = Phoenix.json_library().encode(encodable_data) {:ok, encoded_data} = Phoenix.json_library().encode(encodable_data)
{_output, _exit_code} = {output, exit_code} =
CliUtils.wrap_cmd( CliUtils.wrap_cmd(
executable_path, executable_path,
[to_string(event_type), encoded_data], [to_string(event_type), encoded_data],
@@ -47,7 +49,7 @@ defmodule Pinchflat.Lifecycle.UserScripts.CommandRunner do
logging_arg_override: "[suppressed]" logging_arg_override: "[suppressed]"
) )
:ok {:ok, output, exit_code}
end end
end end
@@ -62,7 +64,7 @@ defmodule Pinchflat.Lifecycle.UserScripts.CommandRunner do
if FilesystemUtils.exists_and_nonempty?(filepath) do if FilesystemUtils.exists_and_nonempty?(filepath) do
{:ok, filepath} {:ok, filepath}
else else
Logger.warning("User scripts lifecyle file either not present or is empty. Skipping.") Logger.info("User scripts lifecyle file either not present or is empty. Skipping.")
{:ok, :no_executable} {:ok, :no_executable}
end end
+2 -2
View File
@@ -171,7 +171,7 @@ defmodule Pinchflat.Media do
if delete_files do if delete_files do
{:ok, _} = do_delete_media_files(media_item) {:ok, _} = do_delete_media_files(media_item)
:ok = run_user_script(:media_deleted, media_item) run_user_script(:media_deleted, media_item)
end end
# Should delete these no matter what # Should delete these no matter what
@@ -194,7 +194,7 @@ defmodule Pinchflat.Media do
Tasks.delete_tasks_for(media_item) Tasks.delete_tasks_for(media_item)
{:ok, _} = do_delete_media_files(media_item) {:ok, _} = do_delete_media_files(media_item)
:ok = run_user_script(:media_deleted, media_item) run_user_script(:media_deleted, media_item)
update_media_item(media_item, Map.merge(filepath_attrs, addl_attrs)) update_media_item(media_item, Map.merge(filepath_attrs, addl_attrs))
end end
@@ -10,7 +10,7 @@ defmodule Pinchflat.Downloading.MediaDownloadWorkerTest do
setup do setup do
stub(YtDlpRunnerMock, :run, fn _url, _opts, _ot -> {:ok, ""} end) stub(YtDlpRunnerMock, :run, fn _url, _opts, _ot -> {:ok, ""} end)
stub(UserScriptRunnerMock, :run, fn _event_type, _data -> :ok end) stub(UserScriptRunnerMock, :run, fn _event_type, _data -> {:ok, "", 0} end)
stub(HTTPClientMock, :get, fn _url, _headers, _opts -> {:ok, ""} end) stub(HTTPClientMock, :get, fn _url, _headers, _opts -> {:ok, ""} end)
media_item = media_item =
@@ -162,20 +162,6 @@ defmodule Pinchflat.Downloading.MediaDownloadWorkerTest do
assert media_item.media_redownloaded_at == nil assert media_item.media_redownloaded_at == nil
end 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 test "does not blow up if the record doesn't exist" do
assert :ok = perform_job(MediaDownloadWorker, %{id: 0}) assert :ok = perform_job(MediaDownloadWorker, %{id: 0})
end end
@@ -237,4 +223,59 @@ defmodule Pinchflat.Downloading.MediaDownloadWorkerTest do
perform_job(MediaDownloadWorker, %{id: media_item.id, force: true}) perform_job(MediaDownloadWorker, %{id: media_item.id, force: true})
end end
end end
describe "perform/1 when testing user script callbacks" do
setup do
stub(YtDlpRunnerMock, :run, fn _url, _opts, _ot, _addl ->
{:ok, render_metadata(:media_metadata)}
end)
:ok
end
test "calls the media_pre_download user script runner", %{media_item: media_item} do
expect(UserScriptRunnerMock, :run, fn :media_pre_download, data ->
assert data.id == media_item.id
{:ok, "", 0}
end)
expect(UserScriptRunnerMock, :run, fn :media_downloaded, _ -> {:ok, "", 0} end)
perform_job(MediaDownloadWorker, %{id: media_item.id})
end
test "does not download the media if the pre-download script returns an error", %{media_item: media_item} do
expect(UserScriptRunnerMock, :run, fn :media_pre_download, _ -> {:ok, "", 1} end)
assert :ok = perform_job(MediaDownloadWorker, %{id: media_item.id})
media_item = Repo.reload!(media_item)
refute media_item.media_filepath
assert media_item.prevent_download
end
test "downloads media if the pre-download script is not present", %{media_item: media_item} do
expect(UserScriptRunnerMock, :run, fn :media_pre_download, _ -> {:ok, :no_executable} end)
expect(UserScriptRunnerMock, :run, fn :media_downloaded, _ -> {:ok, :no_executable} end)
assert :ok = perform_job(MediaDownloadWorker, %{id: media_item.id})
media_item = Repo.reload!(media_item)
assert media_item.media_filepath
refute media_item.prevent_download
end
test "calls the media_downloaded user script runner", %{media_item: media_item} do
expect(UserScriptRunnerMock, :run, fn :media_pre_download, _ -> {:ok, "", 0} end)
expect(UserScriptRunnerMock, :run, fn :media_downloaded, data ->
assert data.id == media_item.id
{:ok, "", 0}
end)
perform_job(MediaDownloadWorker, %{id: media_item.id})
end
end
end end
@@ -8,7 +8,7 @@ defmodule Pinchflat.Downloading.MediaRetentionWorkerTest do
alias Pinchflat.Downloading.MediaRetentionWorker alias Pinchflat.Downloading.MediaRetentionWorker
setup do setup do
stub(UserScriptRunnerMock, :run, fn _event_type, _data -> :ok end) stub(UserScriptRunnerMock, :run, fn _event_type, _data -> {:ok, "", 0} end)
:ok :ok
end end
@@ -19,7 +19,7 @@ defmodule Pinchflat.Lifecycle.UserScripts.CommandRunnerTest do
File.write(filepath(), "#!/bin/bash\ntouch #{filename}\n") File.write(filepath(), "#!/bin/bash\ntouch #{filename}\n")
refute File.exists?(filename) refute File.exists?(filename)
assert :ok = Runner.run(:media_downloaded, %{}) assert {:ok, _, _} = Runner.run(:media_downloaded, %{})
assert File.exists?(filename) assert File.exists?(filename)
end end
@@ -27,7 +27,7 @@ defmodule Pinchflat.Lifecycle.UserScripts.CommandRunnerTest do
tmp_dir = Application.get_env(:pinchflat, :tmpfile_directory) tmp_dir = Application.get_env(:pinchflat, :tmpfile_directory)
File.write(filepath(), "#!/bin/bash\necho $1 > #{tmp_dir}/event_name\n") File.write(filepath(), "#!/bin/bash\necho $1 > #{tmp_dir}/event_name\n")
assert :ok = Runner.run(:media_downloaded, %{}) assert {:ok, _, _} = Runner.run(:media_downloaded, %{})
assert File.read!("#{tmp_dir}/event_name") == "media_downloaded\n" assert File.read!("#{tmp_dir}/event_name") == "media_downloaded\n"
end end
@@ -35,26 +35,32 @@ defmodule Pinchflat.Lifecycle.UserScripts.CommandRunnerTest do
tmp_dir = Application.get_env(:pinchflat, :tmpfile_directory) tmp_dir = Application.get_env(:pinchflat, :tmpfile_directory)
File.write(filepath(), "#!/bin/bash\necho $2 > #{tmp_dir}/encoded_data\n") File.write(filepath(), "#!/bin/bash\necho $2 > #{tmp_dir}/encoded_data\n")
assert :ok = Runner.run(:media_downloaded, %{foo: "bar"}) assert {:ok, _, _} = Runner.run(:media_downloaded, %{foo: "bar"})
assert File.read!("#{tmp_dir}/encoded_data") == "{\"foo\":\"bar\"}\n" assert File.read!("#{tmp_dir}/encoded_data") == "{\"foo\":\"bar\"}\n"
end end
test "does nothing if the lifecycle file is not present" do test "does nothing if the lifecycle file is not present" do
:ok = File.rm(filepath()) :ok = File.rm(filepath())
assert :ok = Runner.run(:media_downloaded, %{}) assert {:ok, :no_executable} = Runner.run(:media_downloaded, %{})
end end
test "does nothing if the lifecycle file is empty" do test "does nothing if the lifecycle file is empty" do
File.write(filepath(), "") File.write(filepath(), "")
assert :ok = Runner.run(:media_downloaded, %{}) assert {:ok, :no_executable} = Runner.run(:media_downloaded, %{})
end end
test "returns :ok if the command exits with a non-zero status" do test "returns :ok if the command exits with a non-zero status" do
File.write(filepath(), "#!/bin/bash\nexit 1\n") File.write(filepath(), "#!/bin/bash\nexit 1\n")
assert :ok = Runner.run(:media_downloaded, %{}) assert {:ok, _, 1} = Runner.run(:media_downloaded, %{})
end
test "returns the output of the command" do
File.write(filepath(), "#!/bin/bash\necho 'hello'\n")
assert {:ok, "hello\n", 0} = Runner.run(:media_downloaded, %{})
end end
test "gets upset if you pass an invalid event type" do test "gets upset if you pass an invalid event type" do
+4 -4
View File
@@ -684,7 +684,7 @@ defmodule Pinchflat.MediaTest do
describe "delete_media_item/2 when testing file deletion" do describe "delete_media_item/2 when testing file deletion" do
setup do setup do
stub(UserScriptRunnerMock, :run, fn _event_type, _data -> :ok end) stub(UserScriptRunnerMock, :run, fn _event_type, _data -> {:ok, "", 0} end)
:ok :ok
end end
@@ -745,7 +745,7 @@ defmodule Pinchflat.MediaTest do
expect(UserScriptRunnerMock, :run, fn :media_deleted, data -> expect(UserScriptRunnerMock, :run, fn :media_deleted, data ->
assert data.id == media_item.id assert data.id == media_item.id
:ok {:ok, "", 0}
end) end)
assert {:ok, _} = Media.delete_media_item(media_item, delete_files: true) assert {:ok, _} = Media.delete_media_item(media_item, delete_files: true)
@@ -754,7 +754,7 @@ defmodule Pinchflat.MediaTest do
describe "delete_media_files/2" do describe "delete_media_files/2" do
setup do setup do
stub(UserScriptRunnerMock, :run, fn _event_type, _data -> :ok end) stub(UserScriptRunnerMock, :run, fn _event_type, _data -> {:ok, "", 0} end)
:ok :ok
end end
@@ -817,7 +817,7 @@ defmodule Pinchflat.MediaTest do
expect(UserScriptRunnerMock, :run, fn :media_deleted, data -> expect(UserScriptRunnerMock, :run, fn :media_deleted, data ->
assert data.id == media_item.id assert data.id == media_item.id
:ok {:ok, "", 0}
end) end)
assert {:ok, _} = Media.delete_media_files(media_item) assert {:ok, _} = Media.delete_media_files(media_item)
+1 -1
View File
@@ -113,7 +113,7 @@ defmodule Pinchflat.ProfilesTest do
describe "delete_media_profile/2 when deleting files" do describe "delete_media_profile/2 when deleting files" do
setup do setup do
stub(UserScriptRunnerMock, :run, fn _event_type, _data -> :ok end) stub(UserScriptRunnerMock, :run, fn _event_type, _data -> {:ok, "", 0} end)
:ok :ok
end end
+1 -1
View File
@@ -617,7 +617,7 @@ defmodule Pinchflat.SourcesTest do
describe "delete_source/2 when deleting files" do describe "delete_source/2 when deleting files" do
setup do setup do
stub(UserScriptRunnerMock, :run, fn _event_type, _data -> :ok end) stub(UserScriptRunnerMock, :run, fn _event_type, _data -> {:ok, "", 0} end)
:ok :ok
end end
@@ -56,7 +56,7 @@ defmodule PinchflatWeb.MediaItemControllerTest do
describe "delete media" do describe "delete media" do
setup do setup do
media_item = media_item_with_attachments() media_item = media_item_with_attachments()
stub(UserScriptRunnerMock, :run, fn _event_type, _data -> :ok end) stub(UserScriptRunnerMock, :run, fn _event_type, _data -> {:ok, "", 0} end)
%{media_item: media_item} %{media_item: media_item}
end end
@@ -137,7 +137,7 @@ defmodule PinchflatWeb.MediaProfileControllerTest do
setup [:create_media_profile] setup [:create_media_profile]
setup do setup do
stub(UserScriptRunnerMock, :run, fn _event_type, _data -> :ok end) stub(UserScriptRunnerMock, :run, fn _event_type, _data -> {:ok, "", 0} end)
:ok :ok
end end
@@ -152,7 +152,7 @@ defmodule PinchflatWeb.SourceControllerTest do
setup [:create_source] setup [:create_source]
setup do setup do
stub(UserScriptRunnerMock, :run, fn _event_type, _data -> :ok end) stub(UserScriptRunnerMock, :run, fn _event_type, _data -> {:ok, "", 0} end)
:ok :ok
end end