diff --git a/config/test.exs b/config/test.exs index b2d8614..f6a7182 100644 --- a/config/test.exs +++ b/config/test.exs @@ -3,8 +3,8 @@ import Config config :pinchflat, # Specifying backend data here makes mocking and local testing SUPER easy yt_dlp_executable: Path.join([File.cwd!(), "/test/support/scripts/yt-dlp-mocks/repeater.sh"]), - media_directory: Path.join([System.tmp_dir!(), "videos"]), - metadata_directory: Path.join([System.tmp_dir!(), "metadata"]) + media_directory: Path.join([File.cwd!(), "tmp", "test", "videos"]), + metadata_directory: Path.join([File.cwd!(), "tmp", "test", "metadata"]) config :pinchflat, Oban, testing: :manual diff --git a/lib/pinchflat/media.ex b/lib/pinchflat/media.ex index 6bd21ea..efedf25 100644 --- a/lib/pinchflat/media.ex +++ b/lib/pinchflat/media.ex @@ -88,6 +88,23 @@ defmodule Pinchflat.Media do """ def get_media_item!(id), do: Repo.get!(MediaItem, id) + @doc """ + Produces a flat list of the filesystem paths for a media_item's downloaded files + + Returns [binary()] + """ + def media_filepaths(media_item) do + filesystem_fields = ~w(media_filepath thumbnail_filepath metadata_filepath subtitle_filepaths)a + mapped_struct = Map.from_struct(media_item) + + filesystem_fields + |> Enum.map(fn + :subtitle_filepaths = field -> Enum.map(mapped_struct[field], fn [_, filepath] -> filepath end) + field -> List.wrap(mapped_struct[field]) + end) + |> List.flatten() + end + @doc """ Creates a media_item. Returns {:ok, %MediaItem{}} | {:error, %Ecto.Changeset{}}. """ @@ -116,6 +133,28 @@ defmodule Pinchflat.Media do Repo.delete(media_item) end + @doc """ + Deletes the media_item and all associated files. Attempts to delete the root directory + but only if it is empty. + + Returns {:ok, %MediaItem{}} + """ + def delete_media_item_and_attachments(media_item) do + root_directory = Path.dirname(media_item.media_filepath) + + media_item + |> media_filepaths() + |> Enum.each(&File.rm/1) + + delete_media_item(media_item) + + # Fails if the directory is not empty + case File.rmdir(root_directory) do + :ok -> {:ok, media_item} + {:error, :eexist} -> {:ok, media_item} + end + end + @doc """ Returns an `%Ecto.Changeset{}` for tracking media_item changes. """ diff --git a/test/pinchflat/media_client/backends/yt_dlp/command_runner_test.exs b/test/pinchflat/media_client/backends/yt_dlp/command_runner_test.exs index d63bfcc..9050343 100644 --- a/test/pinchflat/media_client/backends/yt_dlp/command_runner_test.exs +++ b/test/pinchflat/media_client/backends/yt_dlp/command_runner_test.exs @@ -49,7 +49,7 @@ defmodule Pinchflat.MediaClient.Backends.YtDlp.CommandRunnerTest do test "it automatically includes the --print-to-file flag" do assert {:ok, output} = Runner.run(@video_url, [], "%(id)s") - assert String.contains?(output, "--print-to-file %(id)s /tmp/") + assert String.contains?(output, "--print-to-file %(id)s /app/tmp/") end test "it returns the output and status when the command fails" do diff --git a/test/pinchflat/media_test.exs b/test/pinchflat/media_test.exs index 8fbe8d7..391b126 100644 --- a/test/pinchflat/media_test.exs +++ b/test/pinchflat/media_test.exs @@ -221,6 +221,24 @@ defmodule Pinchflat.MediaTest do end end + describe "media_filepaths/1" do + test "returns filepaths in a flat list" do + filepaths = %{ + media_filepath: "/video/test.mp4", + thumbnail_filepath: "/video/test.jpg", + subtitle_filepaths: [["en", "video/test.srt"]] + } + + media_item = media_item_fixture(filepaths) + + assert Media.media_filepaths(media_item) == [ + "/video/test.mp4", + "/video/test.jpg", + "video/test.srt" + ] + end + end + describe "create_media_item/1" do test "creating with valid data creates a media_item" do valid_attrs = %{ @@ -282,6 +300,41 @@ defmodule Pinchflat.MediaTest do end end + describe "delete_media_item_and_attachments/1" do + setup do + media_item = media_item_with_attachments() + {:ok, media_item: media_item} + end + + test "deletes the media item", %{media_item: media_item} do + assert {:ok, _} = Media.delete_media_item_and_attachments(media_item) + assert_raise Ecto.NoResultsError, fn -> Media.get_media_item!(media_item.id) end + end + + test "deletes associated files", %{media_item: media_item} do + assert File.exists?(media_item.media_filepath) + assert {:ok, _} = Media.delete_media_item_and_attachments(media_item) + refute File.exists?(media_item.media_filepath) + end + + test "deletes the parent folder if it is empty", %{media_item: media_item} do + root_directory = Path.dirname(media_item.media_filepath) + assert File.exists?(root_directory) + assert {:ok, _} = Media.delete_media_item_and_attachments(media_item) + refute File.exists?(root_directory) + end + + test "does not delete the parent folder if it is not empty", %{media_item: media_item} do + root_directory = Path.dirname(media_item.media_filepath) + File.touch(Path.join([root_directory, "test.txt"])) + assert {:ok, _} = Media.delete_media_item_and_attachments(media_item) + assert File.exists?(root_directory) + + :ok = File.rm(Path.join([root_directory, "test.txt"])) + :ok = File.rmdir(root_directory) + end + end + describe "change_media_item/1" do test "change_media_item/1 returns a media_item changeset" do media_item = media_item_fixture() diff --git a/test/pinchflat/profiles/options/yt_dlp/download_option_builder_test.exs b/test/pinchflat/profiles/options/yt_dlp/download_option_builder_test.exs index 32d3ffa..f168657 100644 --- a/test/pinchflat/profiles/options/yt_dlp/download_option_builder_test.exs +++ b/test/pinchflat/profiles/options/yt_dlp/download_option_builder_test.exs @@ -12,7 +12,7 @@ defmodule Pinchflat.Profiles.Options.YtDlp.DownloadOptionBuilderTest do test "it generates an expanded output path based on the given template" do assert {:ok, res} = DownloadOptionBuilder.build(@media_profile) - assert {:output, "/tmp/videos/%(title)S.%(ext)s"} in res + assert {:output, "/app/tmp/test/videos/%(title)S.%(ext)s"} in res end end diff --git a/test/support/files/media.mkv b/test/support/files/media.mkv new file mode 100644 index 0000000..b0b02c1 Binary files /dev/null and b/test/support/files/media.mkv differ diff --git a/test/support/fixtures/media_fixtures.ex b/test/support/fixtures/media_fixtures.ex index 66bb1da..f998824 100644 --- a/test/support/fixtures/media_fixtures.ex +++ b/test/support/fixtures/media_fixtures.ex @@ -31,9 +31,9 @@ defmodule Pinchflat.MediaFixtures do def media_item_with_metadata(attrs \\ %{}) do json_filepath = Path.join([ - Path.dirname(__ENV__.file), + File.cwd!(), + "test", "support", - "fixtures", "files", "media_metadata.json" ]) @@ -44,4 +44,28 @@ defmodule Pinchflat.MediaFixtures do media_item_fixture(merged_attrs) end + + def media_item_with_attachments(attrs \\ %{}) do + stored_media_filepath = + Path.join([ + Application.get_env(:pinchflat, :media_directory), + "#{:rand.uniform(1_000_000)}", + "#{:rand.uniform(1_000_000)}_media.mkv" + ]) + + fixture_media_filepath = + Path.join([ + File.cwd!(), + "test", + "support", + "files", + "media.mkv" + ]) + + :ok = File.mkdir_p(Path.dirname(stored_media_filepath)) + :ok = File.cp(fixture_media_filepath, stored_media_filepath) + + merged_attrs = Map.merge(attrs, %{media_filepath: stored_media_filepath}) + media_item_fixture(merged_attrs) + end end