[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:
parent
8051107d32
commit
112c6a4f14
47 changed files with 498 additions and 70 deletions
|
|
@ -66,6 +66,7 @@ If it doesn't work for your use case, please make a feature request! You can als
|
||||||
- Can pass cookies to YouTube to download your private playlists ([docs](https://github.com/kieraneglin/pinchflat/wiki/YouTube-Cookies))
|
- Can pass cookies to YouTube to download your private playlists ([docs](https://github.com/kieraneglin/pinchflat/wiki/YouTube-Cookies))
|
||||||
- Sponsorblock integration
|
- Sponsorblock integration
|
||||||
- \[Advanced\] allows custom `yt-dlp` options ([docs](https://github.com/kieraneglin/pinchflat/wiki/%5BAdvanced%5D-Custom-yt%E2%80%90dlp-options))
|
- \[Advanced\] allows custom `yt-dlp` options ([docs](https://github.com/kieraneglin/pinchflat/wiki/%5BAdvanced%5D-Custom-yt%E2%80%90dlp-options))
|
||||||
|
- \[Advanced\] supports running custom scripts when after downloading/deleting media (alpha - [docs](https://github.com/kieraneglin/pinchflat/wiki/%5BAdvanced%5D-Custom-lifecycle-scripts))
|
||||||
|
|
||||||
## Screenshots
|
## Screenshots
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,7 @@ config :pinchflat,
|
||||||
yt_dlp_executable: System.find_executable("yt-dlp"),
|
yt_dlp_executable: System.find_executable("yt-dlp"),
|
||||||
apprise_executable: System.find_executable("apprise"),
|
apprise_executable: System.find_executable("apprise"),
|
||||||
yt_dlp_runner: Pinchflat.YtDlp.CommandRunner,
|
yt_dlp_runner: Pinchflat.YtDlp.CommandRunner,
|
||||||
apprise_runner: Pinchflat.Notifications.CommandRunner,
|
apprise_runner: Pinchflat.Lifecycle.Notifications.CommandRunner,
|
||||||
media_directory: "/downloads",
|
media_directory: "/downloads",
|
||||||
# The user may or may not store metadata for their needs, but the app will always store its copy
|
# The user may or may not store metadata for their needs, but the app will always store its copy
|
||||||
metadata_directory: "/config/metadata",
|
metadata_directory: "/config/metadata",
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ FROM ${DEV_IMAGE}
|
||||||
|
|
||||||
# Install debian packages
|
# Install debian packages
|
||||||
RUN apt-get update -qq
|
RUN apt-get update -qq
|
||||||
RUN apt-get install -y inotify-tools ffmpeg curl git openssh-client \
|
RUN apt-get install -y inotify-tools ffmpeg curl git openssh-client jq \
|
||||||
python3 python3-pip python3-setuptools python3-wheel python3-dev locales procps
|
python3 python3-pip python3-setuptools python3-wheel python3-dev locales procps
|
||||||
|
|
||||||
# Install nodejs
|
# Install nodejs
|
||||||
|
|
|
||||||
|
|
@ -33,6 +33,7 @@ defmodule Pinchflat.Boot.PreJobStartupTasks do
|
||||||
def init(state) do
|
def init(state) do
|
||||||
reset_executing_jobs()
|
reset_executing_jobs()
|
||||||
create_blank_yt_dlp_files()
|
create_blank_yt_dlp_files()
|
||||||
|
create_blank_user_script_file()
|
||||||
apply_default_settings()
|
apply_default_settings()
|
||||||
|
|
||||||
{:ok, state}
|
{:ok, state}
|
||||||
|
|
@ -65,6 +66,18 @@ defmodule Pinchflat.Boot.PreJobStartupTasks do
|
||||||
end)
|
end)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
defp create_blank_user_script_file do
|
||||||
|
base_dir = Application.get_env(:pinchflat, :extras_directory)
|
||||||
|
filepath = Path.join([base_dir, "user-scripts", "lifecycle"])
|
||||||
|
|
||||||
|
if !File.exists?(filepath) do
|
||||||
|
Logger.info("Creating blank file and making it executable: #{filepath}")
|
||||||
|
|
||||||
|
FilesystemUtils.write_p!(filepath, "")
|
||||||
|
File.chmod(filepath, 0o755)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
defp apply_default_settings do
|
defp apply_default_settings do
|
||||||
{:ok, yt_dlp_version} = yt_dlp_runner().version()
|
{:ok, yt_dlp_version} = yt_dlp_runner().version()
|
||||||
{:ok, apprise_version} = apprise_runner().version()
|
{:ok, apprise_version} = apprise_runner().version()
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,8 @@ defmodule Pinchflat.Downloading.DownloadOptionBuilder do
|
||||||
alias Pinchflat.Media.MediaItem
|
alias Pinchflat.Media.MediaItem
|
||||||
alias Pinchflat.Downloading.OutputPathBuilder
|
alias Pinchflat.Downloading.OutputPathBuilder
|
||||||
|
|
||||||
|
alias Pinchflat.Utils.FilesystemUtils, as: FSUtils
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
Builds the options for yt-dlp to download media based on the given media's profile.
|
Builds the options for yt-dlp to download media based on the given media's profile.
|
||||||
|
|
||||||
|
|
@ -154,12 +156,10 @@ defmodule Pinchflat.Downloading.DownloadOptionBuilder do
|
||||||
Enum.reduce(filenames, [], fn filename, acc ->
|
Enum.reduce(filenames, [], fn filename, acc ->
|
||||||
filepath = Path.join(base_dir, filename)
|
filepath = Path.join(base_dir, filename)
|
||||||
|
|
||||||
case File.read(filepath) do
|
if FSUtils.exists_and_nonempty?(filepath) do
|
||||||
{:ok, file_data} ->
|
[filepath | acc]
|
||||||
if String.trim(file_data) != "", do: [filepath | acc], else: acc
|
else
|
||||||
|
acc
|
||||||
{:error, _} ->
|
|
||||||
acc
|
|
||||||
end
|
end
|
||||||
end)
|
end)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,8 @@ defmodule Pinchflat.Downloading.MediaDownloadWorker do
|
||||||
alias Pinchflat.Media
|
alias Pinchflat.Media
|
||||||
alias Pinchflat.Downloading.MediaDownloader
|
alias Pinchflat.Downloading.MediaDownloader
|
||||||
|
|
||||||
|
alias Pinchflat.Lifecycle.UserScripts.CommandRunner, as: UserScriptRunner
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
Starts the media_item media download worker and creates a task for the media_item.
|
Starts the media_item media download worker and creates a task for the media_item.
|
||||||
|
|
||||||
|
|
@ -56,11 +58,14 @@ defmodule Pinchflat.Downloading.MediaDownloadWorker do
|
||||||
|
|
||||||
defp download_media_and_schedule_jobs(media_item, is_redownload) do
|
defp download_media_and_schedule_jobs(media_item, is_redownload) do
|
||||||
case MediaDownloader.download_for_media_item(media_item) do
|
case MediaDownloader.download_for_media_item(media_item) do
|
||||||
{:ok, updated_media_item} ->
|
{:ok, downloaded_media_item} ->
|
||||||
Media.update_media_item(updated_media_item, %{
|
{:ok, updated_media_item} =
|
||||||
media_size_bytes: compute_media_filesize(updated_media_item),
|
Media.update_media_item(downloaded_media_item, %{
|
||||||
media_redownloaded_at: get_redownloaded_at(is_redownload)
|
media_size_bytes: compute_media_filesize(downloaded_media_item),
|
||||||
})
|
media_redownloaded_at: get_redownloaded_at(is_redownload)
|
||||||
|
})
|
||||||
|
|
||||||
|
:ok = run_user_script(updated_media_item)
|
||||||
|
|
||||||
{:ok, updated_media_item}
|
{:ok, updated_media_item}
|
||||||
|
|
||||||
|
|
@ -74,21 +79,13 @@ defmodule Pinchflat.Downloading.MediaDownloadWorker do
|
||||||
|
|
||||||
defp compute_media_filesize(media_item) do
|
defp compute_media_filesize(media_item) do
|
||||||
case File.stat(media_item.media_filepath) do
|
case File.stat(media_item.media_filepath) do
|
||||||
{:ok, %{size: size}} ->
|
{:ok, %{size: size}} -> size
|
||||||
size
|
_ -> nil
|
||||||
|
|
||||||
_ ->
|
|
||||||
nil
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
defp get_redownloaded_at(is_redownload) do
|
defp get_redownloaded_at(true), do: DateTime.utc_now()
|
||||||
if is_redownload do
|
defp get_redownloaded_at(_), do: nil
|
||||||
DateTime.utc_now()
|
|
||||||
else
|
|
||||||
nil
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
defp action_on_error(message) do
|
defp action_on_error(message) do
|
||||||
# This will attempt re-download at the next indexing, but it won't be retried
|
# This will attempt re-download at the next indexing, but it won't be retried
|
||||||
|
|
@ -103,4 +100,12 @@ defmodule Pinchflat.Downloading.MediaDownloadWorker do
|
||||||
{:error, :download_failed}
|
{:error, :download_failed}
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# 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.
|
||||||
|
defp run_user_script(media_item) do
|
||||||
|
runner = Application.get_env(:pinchflat, :user_script_runner, UserScriptRunner)
|
||||||
|
|
||||||
|
runner.run(:media_downloaded, media_item)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,7 @@ defmodule Pinchflat.FastIndexing.FastIndexingWorker do
|
||||||
alias Pinchflat.Settings
|
alias Pinchflat.Settings
|
||||||
alias Pinchflat.Sources.Source
|
alias Pinchflat.Sources.Source
|
||||||
alias Pinchflat.FastIndexing.FastIndexingHelpers
|
alias Pinchflat.FastIndexing.FastIndexingHelpers
|
||||||
alias Pinchflat.Notifications.SourceNotifications
|
alias Pinchflat.Lifecycle.Notifications.SourceNotifications
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
Starts the source fast indexing worker and creates a task for the source.
|
Starts the source fast indexing worker and creates a task for the source.
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
defmodule Pinchflat.Notifications.AppriseCommandRunner do
|
defmodule Pinchflat.Lifecycle.Notifications.AppriseCommandRunner do
|
||||||
@moduledoc """
|
@moduledoc """
|
||||||
A behaviour for running CLI commands against a notification backend (apprise).
|
A behaviour for running CLI commands against a notification backend (apprise).
|
||||||
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
defmodule Pinchflat.Notifications.CommandRunner do
|
defmodule Pinchflat.Lifecycle.Notifications.CommandRunner do
|
||||||
@moduledoc """
|
@moduledoc """
|
||||||
Runs apprise commands using the `System.cmd/3` function
|
Runs apprise commands using the `System.cmd/3` function
|
||||||
"""
|
"""
|
||||||
|
|
@ -7,7 +7,7 @@ defmodule Pinchflat.Notifications.CommandRunner do
|
||||||
|
|
||||||
alias Pinchflat.Utils.CliUtils
|
alias Pinchflat.Utils.CliUtils
|
||||||
alias Pinchflat.Utils.FunctionUtils
|
alias Pinchflat.Utils.FunctionUtils
|
||||||
alias Pinchflat.Notifications.AppriseCommandRunner
|
alias Pinchflat.Lifecycle.Notifications.AppriseCommandRunner
|
||||||
|
|
||||||
@behaviour AppriseCommandRunner
|
@behaviour AppriseCommandRunner
|
||||||
|
|
||||||
|
|
@ -28,10 +28,10 @@ defmodule Pinchflat.Notifications.CommandRunner do
|
||||||
default_opts = [:verbose]
|
default_opts = [:verbose]
|
||||||
parsed_opts = CliUtils.parse_options(default_opts ++ command_opts)
|
parsed_opts = CliUtils.parse_options(default_opts ++ command_opts)
|
||||||
|
|
||||||
{output, return_code} = CliUtils.wrap_cmd(backend_executable(), parsed_opts ++ endpoints)
|
{output, exit_code} = CliUtils.wrap_cmd(backend_executable(), parsed_opts ++ endpoints)
|
||||||
Logger.info("[apprise] response: #{output}")
|
Logger.info("[apprise] response: #{output}")
|
||||||
|
|
||||||
case return_code do
|
case exit_code do
|
||||||
0 -> {:ok, String.trim(output)}
|
0 -> {:ok, String.trim(output)}
|
||||||
_ -> {:error, String.trim(output)}
|
_ -> {:error, String.trim(output)}
|
||||||
end
|
end
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
defmodule Pinchflat.Notifications.SourceNotifications do
|
defmodule Pinchflat.Lifecycle.Notifications.SourceNotifications do
|
||||||
@moduledoc """
|
@moduledoc """
|
||||||
Contains utilities for sending notifications about sources
|
Contains utilities for sending notifications about sources
|
||||||
"""
|
"""
|
||||||
76
lib/pinchflat/lifecycle/user_scripts/command_runner.ex
Normal file
76
lib/pinchflat/lifecycle/user_scripts/command_runner.ex
Normal file
|
|
@ -0,0 +1,76 @@
|
||||||
|
defmodule Pinchflat.Lifecycle.UserScripts.CommandRunner do
|
||||||
|
@moduledoc """
|
||||||
|
Runs custom user commands commands using the `System.cmd/3` function
|
||||||
|
"""
|
||||||
|
|
||||||
|
require Logger
|
||||||
|
|
||||||
|
alias Pinchflat.Utils.CliUtils
|
||||||
|
alias Pinchflat.Utils.FilesystemUtils
|
||||||
|
alias Pinchflat.Lifecycle.UserScripts.UserScriptCommandRunner
|
||||||
|
|
||||||
|
@behaviour UserScriptCommandRunner
|
||||||
|
|
||||||
|
@event_types [
|
||||||
|
:media_downloaded,
|
||||||
|
:media_deleted
|
||||||
|
]
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Runs the user script command for the given event type. Passes the event
|
||||||
|
and the encoded data to 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.
|
||||||
|
If something fails, it'll be logged.
|
||||||
|
|
||||||
|
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
|
||||||
|
failures in the development process.
|
||||||
|
|
||||||
|
Returns :ok
|
||||||
|
"""
|
||||||
|
@impl UserScriptCommandRunner
|
||||||
|
def run(event_type, encodable_data) when event_type in @event_types do
|
||||||
|
case backend_executable() do
|
||||||
|
{:ok, :no_executable} ->
|
||||||
|
:ok
|
||||||
|
|
||||||
|
{:ok, executable_path} ->
|
||||||
|
{:ok, encoded_data} = Phoenix.json_library().encode(encodable_data)
|
||||||
|
|
||||||
|
{output, exit_code} =
|
||||||
|
CliUtils.wrap_cmd(
|
||||||
|
executable_path,
|
||||||
|
[to_string(event_type), encoded_data],
|
||||||
|
[],
|
||||||
|
logging_arg_override: "[suppressed]"
|
||||||
|
)
|
||||||
|
|
||||||
|
handle_output(output, exit_code)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def run(event_type, _encodable_data) do
|
||||||
|
raise ArgumentError, "Invalid event type: #{inspect(event_type)}"
|
||||||
|
end
|
||||||
|
|
||||||
|
defp handle_output(output, exit_code) do
|
||||||
|
Logger.debug("Custom lifecycle script exit code: #{exit_code} with output: #{output}")
|
||||||
|
|
||||||
|
:ok
|
||||||
|
end
|
||||||
|
|
||||||
|
defp backend_executable do
|
||||||
|
base_dir = Application.get_env(:pinchflat, :extras_directory)
|
||||||
|
filepath = Path.join([base_dir, "user-scripts", "lifecycle"])
|
||||||
|
|
||||||
|
if FilesystemUtils.exists_and_nonempty?(filepath) do
|
||||||
|
{:ok, filepath}
|
||||||
|
else
|
||||||
|
Logger.warning("User scripts lifecyle file either not present or is empty. Skipping.")
|
||||||
|
|
||||||
|
{:ok, :no_executable}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
@ -0,0 +1,10 @@
|
||||||
|
defmodule Pinchflat.Lifecycle.UserScripts.UserScriptCommandRunner do
|
||||||
|
@moduledoc """
|
||||||
|
A behaviour for running custom user scripts on certain events.
|
||||||
|
|
||||||
|
Used so we can implement Mox for testing without actually running the
|
||||||
|
user's command.
|
||||||
|
"""
|
||||||
|
|
||||||
|
@callback run(atom(), map()) :: :ok | {:error, binary()}
|
||||||
|
end
|
||||||
|
|
@ -10,8 +10,10 @@ defmodule Pinchflat.Media do
|
||||||
alias Pinchflat.Sources.Source
|
alias Pinchflat.Sources.Source
|
||||||
alias Pinchflat.Media.MediaItem
|
alias Pinchflat.Media.MediaItem
|
||||||
alias Pinchflat.Media.MediaQuery
|
alias Pinchflat.Media.MediaQuery
|
||||||
alias Pinchflat.Metadata.MediaMetadata
|
|
||||||
alias Pinchflat.Utils.FilesystemUtils
|
alias Pinchflat.Utils.FilesystemUtils
|
||||||
|
alias Pinchflat.Metadata.MediaMetadata
|
||||||
|
|
||||||
|
alias Pinchflat.Lifecycle.UserScripts.CommandRunner, as: UserScriptRunner
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
Returns the list of media_items.
|
Returns the list of media_items.
|
||||||
|
|
@ -180,6 +182,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)
|
||||||
end
|
end
|
||||||
|
|
||||||
# Should delete these no matter what
|
# Should delete these no matter what
|
||||||
|
|
@ -202,6 +205,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)
|
||||||
|
|
||||||
update_media_item(media_item, Map.merge(filepath_attrs, addl_attrs))
|
update_media_item(media_item, Map.merge(filepath_attrs, addl_attrs))
|
||||||
end
|
end
|
||||||
|
|
@ -237,4 +241,10 @@ defmodule Pinchflat.Media do
|
||||||
|> Enum.filter(&is_binary/1)
|
|> Enum.filter(&is_binary/1)
|
||||||
|> Enum.each(&FilesystemUtils.delete_file_and_remove_empty_directories/1)
|
|> Enum.each(&FilesystemUtils.delete_file_and_remove_empty_directories/1)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
defp run_user_script(event, media_item) do
|
||||||
|
runner = Application.get_env(:pinchflat, :user_script_runner, UserScriptRunner)
|
||||||
|
|
||||||
|
runner.run(event, media_item)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,8 @@ defmodule Pinchflat.Media.MediaItem do
|
||||||
import Ecto.Changeset
|
import Ecto.Changeset
|
||||||
import Pinchflat.Utils.ChangesetUtils
|
import Pinchflat.Utils.ChangesetUtils
|
||||||
|
|
||||||
|
alias __MODULE__
|
||||||
|
alias Pinchflat.Repo
|
||||||
alias Pinchflat.Tasks.Task
|
alias Pinchflat.Tasks.Task
|
||||||
alias Pinchflat.Sources.Source
|
alias Pinchflat.Sources.Source
|
||||||
alias Pinchflat.Metadata.MediaMetadata
|
alias Pinchflat.Metadata.MediaMetadata
|
||||||
|
|
@ -116,4 +118,18 @@ defmodule Pinchflat.Media.MediaItem do
|
||||||
end)
|
end)
|
||||||
|> Enum.into(%{})
|
|> Enum.into(%{})
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@doc false
|
||||||
|
def json_exluded_fields do
|
||||||
|
~w(__meta__ __struct__ metadata tasks media_items_search_index)a
|
||||||
|
end
|
||||||
|
|
||||||
|
defimpl Jason.Encoder, for: MediaItem do
|
||||||
|
def encode(value, opts) do
|
||||||
|
value
|
||||||
|
|> Repo.preload(:source)
|
||||||
|
|> Map.drop(MediaItem.json_exluded_fields())
|
||||||
|
|> Jason.Encode.map(opts)
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ defmodule Pinchflat.Profiles.MediaProfile do
|
||||||
use Ecto.Schema
|
use Ecto.Schema
|
||||||
import Ecto.Changeset
|
import Ecto.Changeset
|
||||||
|
|
||||||
|
alias __MODULE__
|
||||||
alias Pinchflat.Sources.Source
|
alias Pinchflat.Sources.Source
|
||||||
|
|
||||||
@allowed_fields ~w(
|
@allowed_fields ~w(
|
||||||
|
|
@ -84,4 +85,17 @@ defmodule Pinchflat.Profiles.MediaProfile do
|
||||||
def ext_regex do
|
def ext_regex do
|
||||||
~r/\.({{ ?ext ?}}|%\( ?ext ?\)[sS])$/
|
~r/\.({{ ?ext ?}}|%\( ?ext ?\)[sS])$/
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@doc false
|
||||||
|
def json_exluded_fields do
|
||||||
|
~w(__meta__ __struct__ sources)a
|
||||||
|
end
|
||||||
|
|
||||||
|
defimpl Jason.Encoder, for: MediaProfile do
|
||||||
|
def encode(value, opts) do
|
||||||
|
value
|
||||||
|
|> Map.drop(MediaProfile.json_exluded_fields())
|
||||||
|
|> Jason.Encode.map(opts)
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,7 @@ defmodule Pinchflat.SlowIndexing.MediaCollectionIndexingWorker do
|
||||||
alias Pinchflat.Sources.Source
|
alias Pinchflat.Sources.Source
|
||||||
alias Pinchflat.FastIndexing.FastIndexingWorker
|
alias Pinchflat.FastIndexing.FastIndexingWorker
|
||||||
alias Pinchflat.SlowIndexing.SlowIndexingHelpers
|
alias Pinchflat.SlowIndexing.SlowIndexingHelpers
|
||||||
alias Pinchflat.Notifications.SourceNotifications
|
alias Pinchflat.Lifecycle.Notifications.SourceNotifications
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
Starts the source slow indexing worker and creates a task for the source.
|
Starts the source slow indexing worker and creates a task for the source.
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,8 @@ defmodule Pinchflat.Sources.Source do
|
||||||
import Ecto.Changeset
|
import Ecto.Changeset
|
||||||
import Pinchflat.Utils.ChangesetUtils
|
import Pinchflat.Utils.ChangesetUtils
|
||||||
|
|
||||||
|
alias __MODULE__
|
||||||
|
alias Pinchflat.Repo
|
||||||
alias Pinchflat.Tasks.Task
|
alias Pinchflat.Tasks.Task
|
||||||
alias Pinchflat.Media.MediaItem
|
alias Pinchflat.Media.MediaItem
|
||||||
alias Pinchflat.Profiles.MediaProfile
|
alias Pinchflat.Profiles.MediaProfile
|
||||||
|
|
@ -133,4 +135,18 @@ defmodule Pinchflat.Sources.Source do
|
||||||
def filepath_attributes do
|
def filepath_attributes do
|
||||||
~w(nfo_filepath fanart_filepath poster_filepath banner_filepath)a
|
~w(nfo_filepath fanart_filepath poster_filepath banner_filepath)a
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@doc false
|
||||||
|
def json_exluded_fields do
|
||||||
|
~w(__meta__ __struct__ metadata tasks media_items)a
|
||||||
|
end
|
||||||
|
|
||||||
|
defimpl Jason.Encoder, for: Source do
|
||||||
|
def encode(value, opts) do
|
||||||
|
value
|
||||||
|
|> Repo.preload(:media_profile)
|
||||||
|
|> Map.drop(Source.json_exluded_fields())
|
||||||
|
|> Jason.Encode.map(opts)
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -13,17 +13,22 @@ defmodule Pinchflat.Utils.CliUtils do
|
||||||
commands if the job runner is cancelled.
|
commands if the job runner is cancelled.
|
||||||
|
|
||||||
Delegates to `System.cmd/3` and any options/output
|
Delegates to `System.cmd/3` and any options/output
|
||||||
are passed through.
|
are passed through. Custom options can be passed in.
|
||||||
|
|
||||||
|
Custom options:
|
||||||
|
- logging_arg_override: if set, the passed value will be logged in place of
|
||||||
|
the actual arguments passed to the command
|
||||||
|
|
||||||
Returns {binary(), integer()}
|
Returns {binary(), integer()}
|
||||||
"""
|
"""
|
||||||
def wrap_cmd(command, args, opts \\ []) do
|
def wrap_cmd(command, args, passthrough_opts \\ [], opts \\ []) do
|
||||||
wrapper_command = Path.join(:code.priv_dir(:pinchflat), "cmd_wrapper.sh")
|
wrapper_command = Path.join(:code.priv_dir(:pinchflat), "cmd_wrapper.sh")
|
||||||
actual_command = [command] ++ args
|
actual_command = [command] ++ args
|
||||||
|
logging_arg_override = Keyword.get(opts, :logging_arg_override, Enum.join(args, " "))
|
||||||
|
|
||||||
Logger.info("[command_wrapper]: #{command} called with: #{Enum.join(args, " ")}")
|
Logger.info("[command_wrapper]: #{command} called with: #{logging_arg_override}")
|
||||||
|
|
||||||
System.cmd(wrapper_command, actual_command, opts)
|
System.cmd(wrapper_command, actual_command, passthrough_opts)
|
||||||
end
|
end
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,21 @@ defmodule Pinchflat.Utils.FilesystemUtils do
|
||||||
alias Pinchflat.Media
|
alias Pinchflat.Media
|
||||||
alias Pinchflat.Utils.StringUtils
|
alias Pinchflat.Utils.StringUtils
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Checks if a file exists and has non-whitespace contents.
|
||||||
|
|
||||||
|
Returns boolean()
|
||||||
|
"""
|
||||||
|
def exists_and_nonempty?(filepath) do
|
||||||
|
case File.read(filepath) do
|
||||||
|
{:ok, contents} ->
|
||||||
|
String.trim(contents) != ""
|
||||||
|
|
||||||
|
_ ->
|
||||||
|
false
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
Generates a temporary file and returns its path. The file is empty and has the given type.
|
Generates a temporary file and returns its path. The file is empty and has the given type.
|
||||||
Generates all the directories in the path if they don't exist.
|
Generates all the directories in the path if they don't exist.
|
||||||
|
|
|
||||||
|
|
@ -81,16 +81,10 @@ defmodule Pinchflat.YtDlp.CommandRunner do
|
||||||
Enum.reduce(filename_options_map, [], fn {opt_name, filename}, acc ->
|
Enum.reduce(filename_options_map, [], fn {opt_name, filename}, acc ->
|
||||||
filepath = Path.join(base_dir, filename)
|
filepath = Path.join(base_dir, filename)
|
||||||
|
|
||||||
case File.read(filepath) do
|
if FSUtils.exists_and_nonempty?(filepath) do
|
||||||
{:ok, file_data} ->
|
[{opt_name, filepath} | acc]
|
||||||
if String.trim(file_data) != "" do
|
else
|
||||||
[{opt_name, filepath} | acc]
|
acc
|
||||||
else
|
|
||||||
acc
|
|
||||||
end
|
|
||||||
|
|
||||||
{:error, _} ->
|
|
||||||
acc
|
|
||||||
end
|
end
|
||||||
end)
|
end)
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -79,7 +79,7 @@ ARG PORT=8945
|
||||||
|
|
||||||
RUN apt-get update -y
|
RUN apt-get update -y
|
||||||
RUN apt-get install -y libstdc++6 openssl libncurses5 locales ca-certificates \
|
RUN apt-get install -y libstdc++6 openssl libncurses5 locales ca-certificates \
|
||||||
ffmpeg curl git openssh-client nano python3 python3-pip
|
ffmpeg curl git openssh-client nano python3 python3-pip jq procps
|
||||||
RUN apt-get clean && rm -f /var/lib/apt/lists/*_*
|
RUN apt-get clean && rm -f /var/lib/apt/lists/*_*
|
||||||
|
|
||||||
# Download and update YT-DLP
|
# Download and update YT-DLP
|
||||||
|
|
|
||||||
|
|
@ -53,6 +53,30 @@ defmodule Pinchflat.Boot.PreJobStartupTasksTest do
|
||||||
end
|
end
|
||||||
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
|
describe "apply_default_settings" do
|
||||||
test "sets yt_dlp version" do
|
test "sets yt_dlp version" do
|
||||||
Settings.set(yt_dlp_version: nil)
|
Settings.set(yt_dlp_version: nil)
|
||||||
|
|
|
||||||
|
|
@ -12,9 +12,8 @@ defmodule Pinchflat.Downloading.MediaDownloadWorkerTest do
|
||||||
setup :verify_on_exit!
|
setup :verify_on_exit!
|
||||||
|
|
||||||
setup do
|
setup do
|
||||||
stub(HTTPClientMock, :get, fn _url, _headers, _opts ->
|
stub(UserScriptRunnerMock, :run, fn _event_type, _data -> :ok end)
|
||||||
{:ok, ""}
|
stub(HTTPClientMock, :get, fn _url, _headers, _opts -> {:ok, ""} end)
|
||||||
end)
|
|
||||||
|
|
||||||
media_item =
|
media_item =
|
||||||
%{media_filepath: nil}
|
%{media_filepath: nil}
|
||||||
|
|
@ -185,6 +184,20 @@ 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
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,22 @@
|
||||||
defmodule Pinchflat.Downloading.MediaRetentionWorkerTest do
|
defmodule Pinchflat.Downloading.MediaRetentionWorkerTest do
|
||||||
use Pinchflat.DataCase
|
use Pinchflat.DataCase
|
||||||
|
|
||||||
|
import Mox
|
||||||
import Pinchflat.MediaFixtures
|
import Pinchflat.MediaFixtures
|
||||||
import Pinchflat.SourcesFixtures
|
import Pinchflat.SourcesFixtures
|
||||||
|
|
||||||
alias Pinchflat.Media
|
alias Pinchflat.Media
|
||||||
alias Pinchflat.Downloading.MediaRetentionWorker
|
alias Pinchflat.Downloading.MediaRetentionWorker
|
||||||
|
|
||||||
|
setup :verify_on_exit!
|
||||||
|
|
||||||
describe "perform/1" do
|
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
|
test "deletes media files that are past their retention date" do
|
||||||
{_source, old_media_item, new_media_item} = prepare_records()
|
{_source, old_media_item, new_media_item} = prepare_records()
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
defmodule Pinchflat.Downloading.OutputPath.ParserTest do
|
defmodule Pinchflat.Downloading.OutputPath.ParserTest do
|
||||||
use ExUnit.Case, async: true
|
use ExUnit.Case, async: false
|
||||||
|
|
||||||
alias Pinchflat.Downloading.OutputPath.Parser
|
alias Pinchflat.Downloading.OutputPath.Parser
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
defmodule Pinchflat.Notifications.CommandRunnerTest do
|
defmodule Pinchflat.Lifecycle.Notifications.CommandRunnerTest do
|
||||||
use ExUnit.Case, async: true
|
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)
|
@original_executable Application.compile_env(:pinchflat, :apprise_executable)
|
||||||
|
|
||||||
|
|
@ -1,11 +1,11 @@
|
||||||
defmodule Pinchflat.Notifications.SourceNotificationsTest do
|
defmodule Pinchflat.Lifecycle.Notifications.SourceNotificationsTest do
|
||||||
use Pinchflat.DataCase
|
use Pinchflat.DataCase
|
||||||
|
|
||||||
import Mox
|
import Mox
|
||||||
import Pinchflat.MediaFixtures
|
import Pinchflat.MediaFixtures
|
||||||
import Pinchflat.SourcesFixtures
|
import Pinchflat.SourcesFixtures
|
||||||
|
|
||||||
alias Pinchflat.Notifications.SourceNotifications
|
alias Pinchflat.Lifecycle.Notifications.SourceNotifications
|
||||||
|
|
||||||
@apprise_servers ["server_1", "server_2"]
|
@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)
|
Repo.reload!(metadata)
|
||||||
end
|
end
|
||||||
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
|
end
|
||||||
|
|
||||||
describe "list_media_items/0" do
|
describe "list_media_items/0" do
|
||||||
|
|
@ -743,6 +749,12 @@ defmodule Pinchflat.MediaTest do
|
||||||
end
|
end
|
||||||
|
|
||||||
describe "delete_media_item/2 when testing file deletion" do
|
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
|
test "deletes the media item's files" do
|
||||||
media_item = media_item_with_attachments()
|
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.rm(Path.join([root_directory, "test.txt"]))
|
||||||
:ok = File.rmdir(root_directory)
|
:ok = File.rmdir(root_directory)
|
||||||
end
|
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
|
end
|
||||||
|
|
||||||
describe "delete_media_files/2" do
|
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
|
test "does not delete the media_item" do
|
||||||
media_item = media_item_fixture()
|
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 {:ok, updated_media_item} = Media.delete_media_files(media_item, %{prevent_download: true})
|
||||||
assert updated_media_item.prevent_download
|
assert updated_media_item.prevent_download
|
||||||
end
|
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
|
end
|
||||||
|
|
||||||
describe "change_media_item/1" do
|
describe "change_media_item/1" do
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
defmodule Pinchflat.ProfilesTest do
|
defmodule Pinchflat.ProfilesTest do
|
||||||
use Pinchflat.DataCase
|
use Pinchflat.DataCase
|
||||||
|
|
||||||
|
import Mox
|
||||||
import Pinchflat.MediaFixtures
|
import Pinchflat.MediaFixtures
|
||||||
import Pinchflat.SourcesFixtures
|
import Pinchflat.SourcesFixtures
|
||||||
import Pinchflat.ProfilesFixtures
|
import Pinchflat.ProfilesFixtures
|
||||||
|
|
@ -10,6 +11,16 @@ defmodule Pinchflat.ProfilesTest do
|
||||||
|
|
||||||
@invalid_attrs %{name: nil, output_path_template: nil}
|
@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
|
describe "list_media_profiles/0" do
|
||||||
test "it returns all media_profiles" do
|
test "it returns all media_profiles" do
|
||||||
media_profile = media_profile_fixture()
|
media_profile = media_profile_fixture()
|
||||||
|
|
@ -104,6 +115,12 @@ defmodule Pinchflat.ProfilesTest do
|
||||||
end
|
end
|
||||||
|
|
||||||
describe "delete_media_profile/2 when deleting files" do
|
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
|
test "still deletes all the needful records" do
|
||||||
media_profile = media_profile_fixture()
|
media_profile = media_profile_fixture()
|
||||||
source = source_fixture(media_profile_id: media_profile.id)
|
source = source_fixture(media_profile_id: media_profile.id)
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
defmodule Pinchflat.SlowIndexing.FileFollowerServerTest do
|
defmodule Pinchflat.SlowIndexing.FileFollowerServerTest do
|
||||||
use ExUnit.Case, async: true
|
use ExUnit.Case, async: false
|
||||||
|
|
||||||
alias alias Pinchflat.Utils.FilesystemUtils
|
alias alias Pinchflat.Utils.FilesystemUtils
|
||||||
alias Pinchflat.SlowIndexing.FileFollowerServer
|
alias Pinchflat.SlowIndexing.FileFollowerServer
|
||||||
|
|
|
||||||
|
|
@ -32,6 +32,12 @@ defmodule Pinchflat.SourcesTest do
|
||||||
Repo.reload!(metadata)
|
Repo.reload!(metadata)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
test "can be JSON encoded without error" do
|
||||||
|
source = source_fixture()
|
||||||
|
|
||||||
|
assert {:ok, _} = Phoenix.json_library().encode(source)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
describe "output_path_template/1" do
|
describe "output_path_template/1" do
|
||||||
|
|
@ -612,6 +618,12 @@ defmodule Pinchflat.SourcesTest do
|
||||||
end
|
end
|
||||||
|
|
||||||
describe "delete_source/2 when deleting files" do
|
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
|
test "deletes source and media_items" do
|
||||||
source = source_fixture()
|
source = source_fixture()
|
||||||
media_item = media_item_with_attachments(%{source_id: source.id})
|
media_item = media_item_with_attachments(%{source_id: source.id})
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
defmodule Pinchflat.Utils.ChangesetUtilsTest do
|
defmodule Pinchflat.Utils.ChangesetUtilsTest do
|
||||||
use ExUnit.Case, async: true
|
use ExUnit.Case, async: false
|
||||||
|
|
||||||
defmodule MockSchema do
|
defmodule MockSchema do
|
||||||
use Ecto.Schema
|
use Ecto.Schema
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
defmodule Pinchflat.Utils.CliUtilsTest do
|
defmodule Pinchflat.Utils.CliUtilsTest do
|
||||||
use ExUnit.Case, async: true
|
use ExUnit.Case, async: false
|
||||||
|
|
||||||
alias Pinchflat.Utils.CliUtils
|
alias Pinchflat.Utils.CliUtils
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
defmodule Pinchflat.Utils.DatetimeUtilsTest do
|
defmodule Pinchflat.Utils.DatetimeUtilsTest do
|
||||||
use ExUnit.Case, async: true
|
use ExUnit.Case, async: false
|
||||||
|
|
||||||
alias Pinchflat.Utils.DatetimeUtils
|
alias Pinchflat.Utils.DatetimeUtils
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,38 @@ defmodule Pinchflat.Utils.FilesystemUtilsTest do
|
||||||
|
|
||||||
alias Pinchflat.Utils.FilesystemUtils
|
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
|
describe "generate_metadata_tmpfile/1" do
|
||||||
test "creates a tmpfile and returns its path" do
|
test "creates a tmpfile and returns its path" do
|
||||||
res = FilesystemUtils.generate_metadata_tmpfile(:json)
|
res = FilesystemUtils.generate_metadata_tmpfile(:json)
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
defmodule Pinchflat.Utils.FunctionUtilsTest do
|
defmodule Pinchflat.Utils.FunctionUtilsTest do
|
||||||
use ExUnit.Case, async: true
|
use ExUnit.Case, async: false
|
||||||
|
|
||||||
alias Pinchflat.Utils.FunctionUtils
|
alias Pinchflat.Utils.FunctionUtils
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
defmodule Pinchflat.Utils.NumberUtilsTest do
|
defmodule Pinchflat.Utils.NumberUtilsTest do
|
||||||
use ExUnit.Case, async: true
|
use ExUnit.Case, async: false
|
||||||
|
|
||||||
alias Pinchflat.Utils.NumberUtils
|
alias Pinchflat.Utils.NumberUtils
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
defmodule Pinchflat.Utils.StringUtilsTest do
|
defmodule Pinchflat.Utils.StringUtilsTest do
|
||||||
use ExUnit.Case, async: true
|
use ExUnit.Case, async: false
|
||||||
|
|
||||||
alias Pinchflat.Utils.StringUtils
|
alias Pinchflat.Utils.StringUtils
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
defmodule Pinchflat.Utils.XmlUtilsTest do
|
defmodule Pinchflat.Utils.XmlUtilsTest do
|
||||||
use ExUnit.Case, async: true
|
use ExUnit.Case, async: false
|
||||||
|
|
||||||
alias Pinchflat.Utils.XmlUtils
|
alias Pinchflat.Utils.XmlUtils
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
defmodule Pinchflat.YtDlp.CommandRunnerTest do
|
defmodule Pinchflat.YtDlp.CommandRunnerTest do
|
||||||
use ExUnit.Case, async: true
|
use ExUnit.Case, async: false
|
||||||
|
|
||||||
alias Pinchflat.Utils.FilesystemUtils
|
alias Pinchflat.Utils.FilesystemUtils
|
||||||
|
|
||||||
|
|
@ -75,6 +75,9 @@ defmodule Pinchflat.YtDlp.CommandRunnerTest do
|
||||||
|
|
||||||
refute String.contains?(output, "--cookies")
|
refute String.contains?(output, "--cookies")
|
||||||
refute String.contains?(output, cookie_file)
|
refute String.contains?(output, cookie_file)
|
||||||
|
|
||||||
|
# Cleanup
|
||||||
|
FilesystemUtils.write_p!(cookie_file, "")
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
defmodule PinchflatWeb.ErrorHTMLTest do
|
defmodule PinchflatWeb.ErrorHTMLTest do
|
||||||
use PinchflatWeb.ConnCase, async: true
|
use PinchflatWeb.ConnCase, async: false
|
||||||
|
|
||||||
# Bring render_to_string/4 for testing custom views
|
# Bring render_to_string/4 for testing custom views
|
||||||
import Phoenix.Template
|
import Phoenix.Template
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
defmodule PinchflatWeb.ErrorJSONTest do
|
defmodule PinchflatWeb.ErrorJSONTest do
|
||||||
use PinchflatWeb.ConnCase, async: true
|
use PinchflatWeb.ConnCase, async: false
|
||||||
|
|
||||||
test "renders 404" do
|
test "renders 404" do
|
||||||
assert PinchflatWeb.ErrorJSON.render("404.json", %{}) == %{errors: %{detail: "Not Found"}}
|
assert PinchflatWeb.ErrorJSON.render("404.json", %{}) == %{errors: %{detail: "Not Found"}}
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,14 @@
|
||||||
defmodule PinchflatWeb.MediaItemControllerTest do
|
defmodule PinchflatWeb.MediaItemControllerTest do
|
||||||
use PinchflatWeb.ConnCase
|
use PinchflatWeb.ConnCase
|
||||||
|
|
||||||
|
import Mox
|
||||||
import Pinchflat.MediaFixtures
|
import Pinchflat.MediaFixtures
|
||||||
|
|
||||||
alias Pinchflat.Repo
|
alias Pinchflat.Repo
|
||||||
alias Pinchflat.Downloading.MediaDownloadWorker
|
alias Pinchflat.Downloading.MediaDownloadWorker
|
||||||
|
|
||||||
|
setup :verify_on_exit!
|
||||||
|
|
||||||
describe "show media" do
|
describe "show media" do
|
||||||
setup [:create_media_item]
|
setup [:create_media_item]
|
||||||
|
|
||||||
|
|
@ -49,6 +52,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)
|
||||||
|
|
||||||
%{media_item: media_item}
|
%{media_item: media_item}
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
defmodule PinchflatWeb.MediaProfileControllerTest do
|
defmodule PinchflatWeb.MediaProfileControllerTest do
|
||||||
use PinchflatWeb.ConnCase
|
use PinchflatWeb.ConnCase
|
||||||
|
|
||||||
|
import Mox
|
||||||
import Pinchflat.MediaFixtures
|
import Pinchflat.MediaFixtures
|
||||||
import Pinchflat.SourcesFixtures
|
import Pinchflat.SourcesFixtures
|
||||||
import Pinchflat.ProfilesFixtures
|
import Pinchflat.ProfilesFixtures
|
||||||
|
|
@ -15,6 +16,8 @@ defmodule PinchflatWeb.MediaProfileControllerTest do
|
||||||
}
|
}
|
||||||
@invalid_attrs %{name: nil, output_path_template: nil}
|
@invalid_attrs %{name: nil, output_path_template: nil}
|
||||||
|
|
||||||
|
setup :verify_on_exit!
|
||||||
|
|
||||||
setup do
|
setup do
|
||||||
Settings.set(onboarding: false)
|
Settings.set(onboarding: false)
|
||||||
|
|
||||||
|
|
@ -136,6 +139,12 @@ defmodule PinchflatWeb.MediaProfileControllerTest do
|
||||||
describe "delete media_profile when deleting the records and files" do
|
describe "delete media_profile when deleting the records and files" do
|
||||||
setup [:create_media_profile]
|
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
|
test "deletes chosen media_profile and its associations", %{conn: conn, media_profile: media_profile} do
|
||||||
source = source_fixture(media_profile_id: media_profile.id)
|
source = source_fixture(media_profile_id: media_profile.id)
|
||||||
media_item = media_item_with_attachments(%{source_id: source.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
|
describe "delete source when deleting the records and files" do
|
||||||
setup [:create_source]
|
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
|
test "deletes chosen source and media_items", %{conn: conn, source: source, media_item: media_item} do
|
||||||
delete(conn, ~p"/sources/#{source}?delete_files=true")
|
delete(conn, ~p"/sources/#{source}?delete_files=true")
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,15 @@
|
||||||
Mox.defmock(YtDlpRunnerMock, for: Pinchflat.YtDlp.YtDlpCommandRunner)
|
Mox.defmock(YtDlpRunnerMock, for: Pinchflat.YtDlp.YtDlpCommandRunner)
|
||||||
Application.put_env(:pinchflat, :yt_dlp_runner, YtDlpRunnerMock)
|
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)
|
Application.put_env(:pinchflat, :apprise_runner, AppriseRunnerMock)
|
||||||
|
|
||||||
Mox.defmock(HTTPClientMock, for: Pinchflat.HTTP.HTTPBehaviour)
|
Mox.defmock(HTTPClientMock, for: Pinchflat.HTTP.HTTPBehaviour)
|
||||||
Application.put_env(:pinchflat, :http_client, HTTPClientMock)
|
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()
|
ExUnit.start()
|
||||||
Ecto.Adapters.SQL.Sandbox.mode(Pinchflat.Repo, :manual)
|
Ecto.Adapters.SQL.Sandbox.mode(Pinchflat.Repo, :manual)
|
||||||
Faker.start()
|
Faker.start()
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue