Adds controllers and methods for deleting media and files
This commit is contained in:
parent
a2b2d9e150
commit
fecea9cc2f
13 changed files with 158 additions and 40 deletions
|
|
@ -3,8 +3,8 @@ import Config
|
||||||
config :pinchflat,
|
config :pinchflat,
|
||||||
# Specifying backend data here makes mocking and local testing SUPER easy
|
# 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"]),
|
yt_dlp_executable: Path.join([File.cwd!(), "/test/support/scripts/yt-dlp-mocks/repeater.sh"]),
|
||||||
media_directory: Path.join([File.cwd!(), "tmp", "test", "videos"]),
|
media_directory: Path.join([System.tmp_dir!(), "test", "videos"]),
|
||||||
metadata_directory: Path.join([File.cwd!(), "tmp", "test", "metadata"])
|
metadata_directory: Path.join([System.tmp_dir!(), "test", "metadata"])
|
||||||
|
|
||||||
config :pinchflat, Oban, testing: :manual
|
config :pinchflat, Oban, testing: :manual
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -94,10 +94,9 @@ defmodule Pinchflat.Media do
|
||||||
Returns [binary()]
|
Returns [binary()]
|
||||||
"""
|
"""
|
||||||
def media_filepaths(media_item) do
|
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)
|
mapped_struct = Map.from_struct(media_item)
|
||||||
|
|
||||||
filesystem_fields
|
MediaItem.filepath_attributes()
|
||||||
|> Enum.map(fn
|
|> Enum.map(fn
|
||||||
:subtitle_filepaths = field -> Enum.map(mapped_struct[field], fn [_, filepath] -> filepath end)
|
:subtitle_filepaths = field -> Enum.map(mapped_struct[field], fn [_, filepath] -> filepath end)
|
||||||
field -> List.wrap(mapped_struct[field])
|
field -> List.wrap(mapped_struct[field])
|
||||||
|
|
@ -124,7 +123,7 @@ defmodule Pinchflat.Media do
|
||||||
end
|
end
|
||||||
|
|
||||||
@doc """
|
@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{}}.
|
Returns {:ok, %MediaItem{}} | {:error, %Ecto.Changeset{}}.
|
||||||
"""
|
"""
|
||||||
|
|
@ -133,6 +132,23 @@ defmodule Pinchflat.Media do
|
||||||
Repo.delete(media_item)
|
Repo.delete(media_item)
|
||||||
end
|
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 """
|
@doc """
|
||||||
Deletes the media_item and all associated files. Attempts to delete the root directory
|
Deletes the media_item and all associated files. Attempts to delete the root directory
|
||||||
but only if it is empty.
|
but only if it is empty.
|
||||||
|
|
@ -140,19 +156,9 @@ defmodule Pinchflat.Media do
|
||||||
Returns {:ok, %MediaItem{}}
|
Returns {:ok, %MediaItem{}}
|
||||||
"""
|
"""
|
||||||
def delete_media_item_and_attachments(media_item) do
|
def delete_media_item_and_attachments(media_item) do
|
||||||
root_directory = Path.dirname(media_item.media_filepath)
|
{:ok, _} = delete_attachments(media_item)
|
||||||
|
|
||||||
media_item
|
|
||||||
|> media_filepaths()
|
|
||||||
|> Enum.each(&File.rm/1)
|
|
||||||
|
|
||||||
delete_media_item(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
|
end
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
|
|
|
||||||
|
|
@ -60,4 +60,9 @@ defmodule Pinchflat.Media.MediaItem do
|
||||||
|> validate_required(@required_fields)
|
|> validate_required(@required_fields)
|
||||||
|> unique_constraint([:media_id, :source_id])
|
|> unique_constraint([:media_id, :source_id])
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@doc false
|
||||||
|
def filepath_attributes do
|
||||||
|
~w(media_filepath thumbnail_filepath metadata_filepath subtitle_filepaths)a
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -56,6 +56,8 @@ defmodule Pinchflat.MediaSource do
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
Deletes a source and it's associated tasks (of any state).
|
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{}}
|
Returns {:ok, %Source{}} | {:error, %Ecto.Changeset{}}
|
||||||
"""
|
"""
|
||||||
|
|
|
||||||
|
|
@ -8,4 +8,23 @@ defmodule PinchflatWeb.Media.MediaItemController do
|
||||||
|
|
||||||
render(conn, :show, media_item: media_item)
|
render(conn, :show, media_item: media_item)
|
||||||
end
|
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
|
end
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
<div class="mb-6 flex gap-3 flex-row items-center justify-between">
|
<div class="mb-6 flex gap-3 flex-row items-center justify-between">
|
||||||
<div class="flex gap-3 items-center">
|
<div class="flex gap-3 items-center">
|
||||||
<.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" />
|
<.icon name="hero-arrow-left" class="w-10 h-10 hover:dark:text-white" />
|
||||||
</.link>
|
</.link>
|
||||||
<h2 class="text-title-md2 font-bold text-black dark:text-white ml-4">
|
<h2 class="text-title-md2 font-bold text-black dark:text-white ml-4">
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,10 @@
|
||||||
<.highlight_search_terms text={result.matching_search_term} />
|
<.highlight_search_terms text={result.matching_search_term} />
|
||||||
</:col>
|
</:col>
|
||||||
<:col :let={result} label="" class="flex place-content-evenly">
|
<: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" />
|
<.icon name="hero-eye" />
|
||||||
</.link>
|
</.link>
|
||||||
</:col>
|
</:col>
|
||||||
|
|
|
||||||
|
|
@ -20,11 +20,10 @@ defmodule PinchflatWeb.Router do
|
||||||
get "/", PageController, :home
|
get "/", PageController, :home
|
||||||
|
|
||||||
resources "/media_profiles", MediaProfiles.MediaProfileController
|
resources "/media_profiles", MediaProfiles.MediaProfileController
|
||||||
resources "/media", Media.MediaItemController, only: [:show]
|
|
||||||
resources "/search", Searches.SearchController, only: [:show], singleton: true
|
resources "/search", Searches.SearchController, only: [:show], singleton: true
|
||||||
|
|
||||||
resources "/sources", MediaSources.SourceController do
|
resources "/sources", MediaSources.SourceController do
|
||||||
resources "/media", Media.MediaItemController, only: [:show]
|
resources "/media", Media.MediaItemController, only: [:show, :delete]
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -49,7 +49,7 @@ defmodule Pinchflat.MediaClient.Backends.YtDlp.CommandRunnerTest do
|
||||||
test "it automatically includes the --print-to-file flag" do
|
test "it automatically includes the --print-to-file flag" do
|
||||||
assert {:ok, output} = Runner.run(@video_url, [], "%(id)s")
|
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
|
end
|
||||||
|
|
||||||
test "it returns the output and status when the command fails" do
|
test "it returns the output and status when the command fails" do
|
||||||
|
|
|
||||||
|
|
@ -300,6 +300,43 @@ defmodule Pinchflat.MediaTest do
|
||||||
end
|
end
|
||||||
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
|
describe "delete_media_item_and_attachments/1" do
|
||||||
setup do
|
setup do
|
||||||
media_item = media_item_with_attachments()
|
media_item = media_item_with_attachments()
|
||||||
|
|
@ -316,23 +353,6 @@ defmodule Pinchflat.MediaTest do
|
||||||
assert {:ok, _} = Media.delete_media_item_and_attachments(media_item)
|
assert {:ok, _} = Media.delete_media_item_and_attachments(media_item)
|
||||||
refute File.exists?(media_item.media_filepath)
|
refute File.exists?(media_item.media_filepath)
|
||||||
end
|
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
|
end
|
||||||
|
|
||||||
describe "change_media_item/1" do
|
describe "change_media_item/1" do
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,7 @@ defmodule Pinchflat.Profiles.Options.YtDlp.DownloadOptionBuilderTest do
|
||||||
test "it generates an expanded output path based on the given template" do
|
test "it generates an expanded output path based on the given template" do
|
||||||
assert {:ok, res} = DownloadOptionBuilder.build(@media_profile)
|
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
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,15 +3,74 @@ defmodule PinchflatWeb.MediaItemControllerTest do
|
||||||
|
|
||||||
import Pinchflat.MediaFixtures
|
import Pinchflat.MediaFixtures
|
||||||
|
|
||||||
|
alias Pinchflat.Repo
|
||||||
|
alias Pinchflat.Media
|
||||||
|
|
||||||
describe "show media" do
|
describe "show media" do
|
||||||
setup [:create_media_item]
|
setup [:create_media_item]
|
||||||
|
|
||||||
test "renders the page", %{conn: conn, media_item: media_item} do
|
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}"
|
assert html_response(conn, 200) =~ "Media Item ##{media_item.id}"
|
||||||
end
|
end
|
||||||
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
|
defp create_media_item(_) do
|
||||||
media_item = media_item_fixture()
|
media_item = media_item_fixture()
|
||||||
%{media_item: media_item}
|
%{media_item: media_item}
|
||||||
|
|
|
||||||
|
|
@ -4,3 +4,8 @@ Application.put_env(:pinchflat, :yt_dlp_runner, YtDlpRunnerMock)
|
||||||
ExUnit.start()
|
ExUnit.start()
|
||||||
Ecto.Adapters.SQL.Sandbox.mode(Pinchflat.Repo, :manual)
|
Ecto.Adapters.SQL.Sandbox.mode(Pinchflat.Repo, :manual)
|
||||||
Faker.start()
|
Faker.start()
|
||||||
|
|
||||||
|
setup_all do
|
||||||
|
File.rm_rf(Application.get_env(:pinchflat, :media_directory))
|
||||||
|
File.rm_rf(Application.get_env(:pinchflat, :metadata_directory))
|
||||||
|
end
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue