diff --git a/config/test.exs b/config/test.exs
index f6a7182..94e1919 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([File.cwd!(), "tmp", "test", "videos"]),
- metadata_directory: Path.join([File.cwd!(), "tmp", "test", "metadata"])
+ media_directory: Path.join([System.tmp_dir!(), "test", "videos"]),
+ metadata_directory: Path.join([System.tmp_dir!(), "test", "metadata"])
config :pinchflat, Oban, testing: :manual
diff --git a/lib/pinchflat/media.ex b/lib/pinchflat/media.ex
index efedf25..14ae494 100644
--- a/lib/pinchflat/media.ex
+++ b/lib/pinchflat/media.ex
@@ -94,10 +94,9 @@ defmodule Pinchflat.Media do
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
+ MediaItem.filepath_attributes()
|> Enum.map(fn
:subtitle_filepaths = field -> Enum.map(mapped_struct[field], fn [_, filepath] -> filepath end)
field -> List.wrap(mapped_struct[field])
@@ -124,7 +123,7 @@ defmodule Pinchflat.Media do
end
@doc """
- Deletes a media_item and its associated tasks.
+ Deletes a media_item and its associated tasks. Will leave files on disk.
Returns {:ok, %MediaItem{}} | {:error, %Ecto.Changeset{}}.
"""
@@ -133,6 +132,23 @@ defmodule Pinchflat.Media do
Repo.delete(media_item)
end
+ @doc """
+ Deletes the media_item's associated files. Will leave the media_item in the database.
+
+ Returns {:ok, %MediaItem{}}
+ """
+ def delete_attachments(media_item) do
+ media_item
+ |> media_filepaths()
+ |> Enum.each(&File.rm/1)
+
+ # Fails if the directory is not empty
+ case File.rmdir(Path.dirname(media_item.media_filepath)) do
+ :ok -> {:ok, media_item}
+ {:error, :eexist} -> {:ok, media_item}
+ end
+ end
+
@doc """
Deletes the media_item and all associated files. Attempts to delete the root directory
but only if it is empty.
@@ -140,19 +156,9 @@ defmodule Pinchflat.Media do
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)
+ {:ok, _} = delete_attachments(media_item)
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 """
diff --git a/lib/pinchflat/media/media_item.ex b/lib/pinchflat/media/media_item.ex
index 838cc14..b43a1a2 100644
--- a/lib/pinchflat/media/media_item.ex
+++ b/lib/pinchflat/media/media_item.ex
@@ -60,4 +60,9 @@ defmodule Pinchflat.Media.MediaItem do
|> validate_required(@required_fields)
|> unique_constraint([:media_id, :source_id])
end
+
+ @doc false
+ def filepath_attributes do
+ ~w(media_filepath thumbnail_filepath metadata_filepath subtitle_filepaths)a
+ end
end
diff --git a/lib/pinchflat/media_source.ex b/lib/pinchflat/media_source.ex
index e7c8abb..46fab79 100644
--- a/lib/pinchflat/media_source.ex
+++ b/lib/pinchflat/media_source.ex
@@ -56,6 +56,8 @@ defmodule Pinchflat.MediaSource do
@doc """
Deletes a source and it's associated tasks (of any state).
+ NOTE: will fail if the source has associated media items. Intended
+ for now, will almost certainly change in the future.
Returns {:ok, %Source{}} | {:error, %Ecto.Changeset{}}
"""
diff --git a/lib/pinchflat_web/controllers/media/media_item_controller.ex b/lib/pinchflat_web/controllers/media/media_item_controller.ex
index 3db81d4..398bd32 100644
--- a/lib/pinchflat_web/controllers/media/media_item_controller.ex
+++ b/lib/pinchflat_web/controllers/media/media_item_controller.ex
@@ -8,4 +8,23 @@ defmodule PinchflatWeb.Media.MediaItemController do
render(conn, :show, media_item: media_item)
end
+
+ def delete(conn, %{"id" => id} = params) do
+ delete_files = Map.get(params, "delete_files", false)
+ media_item = Media.get_media_item!(id)
+
+ if delete_files do
+ {:ok, _} = Media.delete_media_item_and_attachments(media_item)
+
+ conn
+ |> put_flash(:info, "Record and files deleted successfully.")
+ |> redirect(to: ~p"/sources/#{media_item.source_id}")
+ else
+ {:ok, _} = Media.delete_media_item(media_item)
+
+ conn
+ |> put_flash(:info, "Record deleted successfully. Files were not deleted.")
+ |> redirect(to: ~p"/sources/#{media_item.source_id}")
+ end
+ end
end
diff --git a/lib/pinchflat_web/controllers/media/media_item_html/show.html.heex b/lib/pinchflat_web/controllers/media/media_item_html/show.html.heex
index ac65a28..48db176 100644
--- a/lib/pinchflat_web/controllers/media/media_item_html/show.html.heex
+++ b/lib/pinchflat_web/controllers/media/media_item_html/show.html.heex
@@ -1,6 +1,6 @@
- <.link :if={@conn.params["source_id"]} navigate={~p"/sources/#{@media_item.source_id}"}>
+ <.link navigate={~p"/sources/#{@media_item.source_id}"}>
<.icon name="hero-arrow-left" class="w-10 h-10 hover:dark:text-white" />
diff --git a/lib/pinchflat_web/controllers/searches/search_html/show.html.heex b/lib/pinchflat_web/controllers/searches/search_html/show.html.heex
index 996701e..4d2bfd5 100644
--- a/lib/pinchflat_web/controllers/searches/search_html/show.html.heex
+++ b/lib/pinchflat_web/controllers/searches/search_html/show.html.heex
@@ -16,7 +16,10 @@
<.highlight_search_terms text={result.matching_search_term} />
<:col :let={result} label="" class="flex place-content-evenly">
- <.link navigate={~p"/media/#{result.id}"} class="hover:text-secondary duration-200 ease-in-out mx-0.5">
+ <.link
+ navigate={~p"/sources/#{result.source_id}/media/#{result.id}"}
+ class="hover:text-secondary duration-200 ease-in-out mx-0.5"
+ >
<.icon name="hero-eye" />
diff --git a/lib/pinchflat_web/router.ex b/lib/pinchflat_web/router.ex
index 55c9fcb..0470029 100644
--- a/lib/pinchflat_web/router.ex
+++ b/lib/pinchflat_web/router.ex
@@ -20,11 +20,10 @@ defmodule PinchflatWeb.Router do
get "/", PageController, :home
resources "/media_profiles", MediaProfiles.MediaProfileController
- resources "/media", Media.MediaItemController, only: [:show]
resources "/search", Searches.SearchController, only: [:show], singleton: true
resources "/sources", MediaSources.SourceController do
- resources "/media", Media.MediaItemController, only: [:show]
+ resources "/media", Media.MediaItemController, only: [:show, :delete]
end
end
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 9050343..d63bfcc 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 /app/tmp/")
+ assert String.contains?(output, "--print-to-file %(id)s /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 391b126..77d267d 100644
--- a/test/pinchflat/media_test.exs
+++ b/test/pinchflat/media_test.exs
@@ -300,6 +300,43 @@ defmodule Pinchflat.MediaTest do
end
end
+ describe "delete_attachments/1" do
+ test "deletes the media item's files" do
+ media_item = media_item_with_attachments()
+
+ assert {:ok, _} = Media.delete_attachments(media_item)
+ refute File.exists?(media_item.media_filepath)
+ end
+
+ test "does not delete the media item" do
+ media_item = media_item_with_attachments()
+
+ assert {:ok, _} = Media.delete_attachments(media_item)
+
+ assert Repo.reload!(media_item)
+ end
+
+ test "deletes the parent folder if it is empty" do
+ media_item = media_item_with_attachments()
+ root_directory = Path.dirname(media_item.media_filepath)
+
+ assert {:ok, _} = Media.delete_attachments(media_item)
+ refute File.exists?(root_directory)
+ end
+
+ test "does not delete the parent folder if it is not empty" do
+ media_item = media_item_with_attachments()
+ root_directory = Path.dirname(media_item.media_filepath)
+ File.touch(Path.join([root_directory, "test.txt"]))
+
+ assert {:ok, _} = Media.delete_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 "delete_media_item_and_attachments/1" do
setup do
media_item = media_item_with_attachments()
@@ -316,23 +353,6 @@ defmodule Pinchflat.MediaTest do
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
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 f168657..56942ce 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, "/app/tmp/test/videos/%(title)S.%(ext)s"} in res
+ assert {:output, "/tmp/test/videos/%(title)S.%(ext)s"} in res
end
end
diff --git a/test/pinchflat_web/controllers/media_item_controller_test.exs b/test/pinchflat_web/controllers/media_item_controller_test.exs
index eb0eab2..002f3bd 100644
--- a/test/pinchflat_web/controllers/media_item_controller_test.exs
+++ b/test/pinchflat_web/controllers/media_item_controller_test.exs
@@ -3,15 +3,74 @@ defmodule PinchflatWeb.MediaItemControllerTest do
import Pinchflat.MediaFixtures
+ alias Pinchflat.Repo
+ alias Pinchflat.Media
+
describe "show media" do
setup [:create_media_item]
test "renders the page", %{conn: conn, media_item: media_item} do
- conn = get(conn, ~p"/media/#{media_item}")
+ conn = get(conn, ~p"/sources/#{media_item.source_id}/media/#{media_item}")
assert html_response(conn, 200) =~ "Media Item ##{media_item.id}"
end
end
+ describe "delete media when just deleting the records" do
+ setup do
+ media_item = media_item_with_attachments()
+
+ on_exit(fn ->
+ Media.delete_attachments(media_item)
+ end)
+
+ %{media_item: media_item}
+ end
+
+ test "the media item is deleted", %{conn: conn, media_item: media_item} do
+ delete(conn, ~p"/sources/#{media_item.source_id}/media/#{media_item.id}")
+
+ assert_raise Ecto.NoResultsError, fn -> Repo.reload!(media_item) end
+ end
+
+ test "the files are not deleted", %{conn: conn, media_item: media_item} do
+ delete(conn, ~p"/sources/#{media_item.source_id}/media/#{media_item.id}")
+
+ assert File.exists?(media_item.media_filepath)
+ end
+
+ test "redirects to the source page", %{conn: conn, media_item: media_item} do
+ conn = delete(conn, ~p"/sources/#{media_item.source_id}/media/#{media_item.id}")
+
+ assert redirected_to(conn) == ~p"/sources/#{media_item.source_id}"
+ end
+ end
+
+ describe "delete media when deleting the records and files" do
+ setup do
+ media_item = media_item_with_attachments()
+
+ %{media_item: media_item}
+ end
+
+ test "the media item is deleted", %{conn: conn, media_item: media_item} do
+ delete(conn, ~p"/sources/#{media_item.source_id}/media/#{media_item.id}?delete_files=true")
+
+ assert_raise Ecto.NoResultsError, fn -> Repo.reload!(media_item) end
+ end
+
+ test "the files are deleted", %{conn: conn, media_item: media_item} do
+ delete(conn, ~p"/sources/#{media_item.source_id}/media/#{media_item.id}?delete_files=true")
+
+ refute File.exists?(media_item.media_filepath)
+ end
+
+ test "redirects to the source page", %{conn: conn, media_item: media_item} do
+ conn = delete(conn, ~p"/sources/#{media_item.source_id}/media/#{media_item.id}?delete_files=true")
+
+ assert redirected_to(conn) == ~p"/sources/#{media_item.source_id}"
+ end
+ end
+
defp create_media_item(_) do
media_item = media_item_fixture()
%{media_item: media_item}
diff --git a/test/test_helper.exs b/test/test_helper.exs
index 40d1202..56f7ccf 100644
--- a/test/test_helper.exs
+++ b/test/test_helper.exs
@@ -4,3 +4,8 @@ Application.put_env(:pinchflat, :yt_dlp_runner, YtDlpRunnerMock)
ExUnit.start()
Ecto.Adapters.SQL.Sandbox.mode(Pinchflat.Repo, :manual)
Faker.start()
+
+setup_all do
+ File.rm_rf(Application.get_env(:pinchflat, :media_directory))
+ File.rm_rf(Application.get_env(:pinchflat, :metadata_directory))
+end