UX improvements v1 (#39)

* Removed collection_type user input instead inferring from yt-dlp response

* Updated docs

* Added delete buttons for source; Refactored the way deletion methods work

* Update source to always run an initial index

* Added deletion to the last models

* Improved clarity around deletion operation

* Improved task fetching and deletion methods

* More tests
This commit is contained in:
Kieran
2024-02-28 18:54:01 -08:00
committed by GitHub
parent 0020e5083b
commit 1f4823bda9
22 changed files with 621 additions and 236 deletions
+29 -31
View File
@@ -12,12 +12,25 @@ defmodule Pinchflat.Media do
alias Pinchflat.Media.MediaMetadata alias Pinchflat.Media.MediaMetadata
@doc """ @doc """
Returns the list of media_items. Returns [%MediaItem{}, ...]. Returns the list of media_items.
Returns [%MediaItem{}, ...].
""" """
def list_media_items do def list_media_items do
Repo.all(MediaItem) Repo.all(MediaItem)
end end
@doc """
Returns a list of media_items for a given source.
Returns [%MediaItem{}, ...].
"""
def list_media_items_for(%Source{} = source) do
MediaItem
|> where([mi], mi.source_id == ^source.id)
|> Repo.all()
end
@doc """ @doc """
Returns a list of pending media_items for a given source, where Returns a list of pending media_items for a given source, where
pending means the `media_filepath` is `nil` AND the media_item pending means the `media_filepath` is `nil` AND the media_item
@@ -138,26 +151,30 @@ defmodule Pinchflat.Media do
end end
@doc """ @doc """
Deletes a media_item and its associated tasks. Will leave files on disk. Deletes a media_item and its associated tasks.
Can optionally delete the media_item's files.
Returns {:ok, %MediaItem{}} | {:error, %Ecto.Changeset{}}. Returns {:ok, %MediaItem{}} | {:error, %Ecto.Changeset{}}.
""" """
def delete_media_item(%MediaItem{} = media_item) do def delete_media_item(%MediaItem{} = media_item, opts \\ []) do
delete_files = Keyword.get(opts, :delete_files, false)
if delete_files do
{:ok, _} = delete_all_attachments(media_item)
end
Tasks.delete_tasks_for(media_item) Tasks.delete_tasks_for(media_item)
Repo.delete(media_item) Repo.delete(media_item)
end end
@doc """ @doc """
Deletes the media_item's associated files. Will leave the media_item in the database. Returns an `%Ecto.Changeset{}` for tracking media_item changes.
NOTE: this deletes the metadata files as well, but maybe it shouldn't? I'm wondering if
the metadata is more a concern of the DB record itself and should be lumped in with those
delete operations. But the metadata does come from the download operation of the file.
Food for thought but not a priority at the moment.
Returns {:ok, %MediaItem{}}
""" """
def delete_attachments(media_item) do def change_media_item(%MediaItem{} = media_item, attrs \\ %{}) do
MediaItem.changeset(media_item, attrs)
end
defp delete_all_attachments(media_item) do
media_item = Repo.preload(media_item, :metadata) media_item = Repo.preload(media_item, :metadata)
media_item media_item
@@ -177,25 +194,6 @@ defmodule Pinchflat.Media do
{:ok, media_item} {: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.
Returns {:ok, %MediaItem{}}
"""
def delete_media_item_and_attachments(media_item) do
{:ok, _} = delete_attachments(media_item)
delete_media_item(media_item)
end
@doc """
Returns an `%Ecto.Changeset{}` for tracking media_item changes.
"""
def change_media_item(%MediaItem{} = media_item, attrs \\ %{}) do
MediaItem.changeset(media_item, attrs)
end
defp build_format_clauses(media_profile) do defp build_format_clauses(media_profile) do
mapped_struct = Map.from_struct(media_profile) mapped_struct = Map.from_struct(media_profile)
+1
View File
@@ -27,6 +27,7 @@ defmodule Pinchflat.Sources.Source do
collection_id collection_id
collection_type collection_type
friendly_name friendly_name
index_frequency_minutes
download_media download_media
original_url original_url
media_profile_id media_profile_id
+25 -7
View File
@@ -4,12 +4,15 @@ defmodule Pinchflat.Profiles do
""" """
import Ecto.Query, warn: false import Ecto.Query, warn: false
alias Pinchflat.Repo
alias Pinchflat.Repo
alias Pinchflat.Sources
alias Pinchflat.Profiles.MediaProfile alias Pinchflat.Profiles.MediaProfile
@doc """ @doc """
Returns the list of media_profiles. Returns [%MediaProfile{}, ...] Returns the list of media_profiles.
Returns [%MediaProfile{}, ...]
""" """
def list_media_profiles do def list_media_profiles do
Repo.all(MediaProfile) Repo.all(MediaProfile)
@@ -23,7 +26,9 @@ defmodule Pinchflat.Profiles do
def get_media_profile!(id), do: Repo.get!(MediaProfile, id) def get_media_profile!(id), do: Repo.get!(MediaProfile, id)
@doc """ @doc """
Creates a media_profile. Returns {:ok, %MediaProfile{}} | {:error, %Ecto.Changeset{}} Creates a media_profile.
Returns {:ok, %MediaProfile{}} | {:error, %Ecto.Changeset{}}
""" """
def create_media_profile(attrs) do def create_media_profile(attrs) do
%MediaProfile{} %MediaProfile{}
@@ -32,7 +37,9 @@ defmodule Pinchflat.Profiles do
end end
@doc """ @doc """
Updates a media_profile. Returns {:ok, %MediaProfile{}} | {:error, %Ecto.Changeset{}} Updates a media_profile.
Returns {:ok, %MediaProfile{}} | {:error, %Ecto.Changeset{}}
""" """
def update_media_profile(%MediaProfile{} = media_profile, attrs) do def update_media_profile(%MediaProfile{} = media_profile, attrs) do
media_profile media_profile
@@ -41,14 +48,25 @@ defmodule Pinchflat.Profiles do
end end
@doc """ @doc """
Deletes a media_profile. Returns {:ok, %MediaProfile{}} | {:error, %Ecto.Changeset{}} Deletes a media_profile, all its sources, and all their media items.
Can optionally delete the media files.
Returns {:ok, %MediaProfile{}} | {:error, %Ecto.Changeset{}}
""" """
def delete_media_profile(%MediaProfile{} = media_profile) do def delete_media_profile(%MediaProfile{} = media_profile, opts \\ []) do
delete_files = Keyword.get(opts, :delete_files, false)
media_profile
|> Sources.list_sources_for()
|> Enum.each(fn source ->
Sources.delete_source(source, delete_files: delete_files)
end)
Repo.delete(media_profile) Repo.delete(media_profile)
end end
@doc """ @doc """
Returns an `%Ecto.Changeset{}` for tracking media_profile changes. Returns `%Ecto.Changeset{}`
""" """
def change_media_profile(%MediaProfile{} = media_profile, attrs \\ %{}) do def change_media_profile(%MediaProfile{} = media_profile, attrs \\ %{}) do
MediaProfile.changeset(media_profile, attrs) MediaProfile.changeset(media_profile, attrs)
+41 -33
View File
@@ -6,9 +6,11 @@ defmodule Pinchflat.Sources do
import Ecto.Query, warn: false import Ecto.Query, warn: false
alias Pinchflat.Repo alias Pinchflat.Repo
alias Pinchflat.Media
alias Pinchflat.Tasks alias Pinchflat.Tasks
alias Pinchflat.Tasks.SourceTasks
alias Pinchflat.Sources.Source alias Pinchflat.Sources.Source
alias Pinchflat.Tasks.SourceTasks
alias Pinchflat.Profiles.MediaProfile
alias Pinchflat.MediaClient.SourceDetails alias Pinchflat.MediaClient.SourceDetails
@doc """ @doc """
@@ -18,6 +20,15 @@ defmodule Pinchflat.Sources do
Repo.all(Source) Repo.all(Source)
end end
@doc """
Returns the list of sources for a media_profile.
Returns [%Source{}, ...]
"""
def list_sources_for(%MediaProfile{} = media_profile) do
Repo.all(from s in Source, where: s.media_profile_id == ^media_profile.id)
end
@doc """ @doc """
Gets a single source. Gets a single source.
@@ -55,13 +66,20 @@ defmodule Pinchflat.Sources do
end end
@doc """ @doc """
Deletes a source and it's associated tasks (of any state). Deletes a source, its media items, and its associated tasks (of any state).
NOTE: will fail if the source has associated media items. Intended Can optionally delete the source's media files.
for now, will almost certainly change in the future.
Returns {:ok, %Source{}} | {:error, %Ecto.Changeset{}} Returns {:ok, %Source{}} | {:error, %Ecto.Changeset{}}
""" """
def delete_source(%Source{} = source) do def delete_source(%Source{} = source, opts \\ []) do
delete_files = Keyword.get(opts, :delete_files, false)
source
|> Media.list_media_items_for()
|> Enum.each(fn media_item ->
Media.delete_media_item(media_item, delete_files: delete_files)
end)
Tasks.delete_tasks_for(source) Tasks.delete_tasks_for(source)
Repo.delete(source) Repo.delete(source)
end end
@@ -84,10 +102,6 @@ defmodule Pinchflat.Sources do
NOTE: When operating in the ideal path, this effectively adds an API call NOTE: When operating in the ideal path, this effectively adds an API call
to the source creation/update process. Should be used only when needed. to the source creation/update process. Should be used only when needed.
IDEA: Maybe I could discern `collection_type` based on the original URL?
It also seems like it's a channel when the returned yt-dlp channel_id is the
same as the playlist_id - maybe could use that?
""" """
def change_source_from_url(%Source{} = source, attrs) do def change_source_from_url(%Source{} = source, attrs) do
case change_source(source, attrs) do case change_source(source, attrs) do
@@ -118,24 +132,20 @@ defmodule Pinchflat.Sources do
defp add_source_details_by_collection_type(source, changeset, source_details) do defp add_source_details_by_collection_type(source, changeset, source_details) do
%Ecto.Changeset{changes: changes} = changeset %Ecto.Changeset{changes: changes} = changeset
collection_type = Ecto.Changeset.get_field(changeset, :collection_type)
collection_changes = collection_changes =
case collection_type do if source_details.playlist_id == source_details.channel_id do
:channel -> %{
%{ collection_type: :channel,
collection_id: source_details.channel_id, collection_id: source_details.channel_id,
collection_name: source_details.channel_name collection_name: source_details.channel_name
} }
else
:playlist -> %{
%{ collection_type: :playlist,
collection_id: source_details.playlist_id, collection_id: source_details.playlist_id,
collection_name: source_details.playlist_name collection_name: source_details.playlist_name
} }
_ ->
%{}
end end
change_source(source, Map.merge(changes, collection_changes)) change_source(source, Map.merge(changes, collection_changes))
@@ -169,21 +179,19 @@ defmodule Pinchflat.Sources do
{:ok, source} {:ok, source}
end end
# IDEA: this uses a pattern where `kickoff_indexing_task` controls whether
# it should run based on the source, but `maybe_handle_media_tasks` handles that
# logic itself. Consider updating one or the other to be consistent (once I've
# decided which I like more)
defp maybe_run_indexing_task(changeset, source) do defp maybe_run_indexing_task(changeset, source) do
case changeset.data do case changeset.data do
# If the changeset is new (not persisted), attempt indexing no matter what # If the changeset is new (not persisted), attempt indexing no matter what
%{__meta__: %{state: :built}} -> %{__meta__: %{state: :built}} ->
SourceTasks.kickoff_indexing_task(source) SourceTasks.kickoff_indexing_task(source)
# If the record has been persisted, only attempt indexing if the # If the record has been persisted, only run indexing if the
# indexing frequency has been changed # indexing frequency has been changed and is now greater than 0
%{__meta__: %{state: :loaded}} -> %{__meta__: %{state: :loaded}} ->
if Map.has_key?(changeset.changes, :index_frequency_minutes) do case changeset.changes do
SourceTasks.kickoff_indexing_task(source) %{index_frequency_minutes: mins} when mins > 0 -> SourceTasks.kickoff_indexing_task(source)
%{index_frequency_minutes: _} -> Tasks.delete_pending_tasks_for(source, "MediaIndexingWorker")
_ -> :ok
end end
end end
+28 -10
View File
@@ -19,30 +19,46 @@ defmodule Pinchflat.Tasks do
@doc """ @doc """
Returns the list of tasks for a given record type and ID. Optionally allows you to specify Returns the list of tasks for a given record type and ID. Optionally allows you to specify
which job states to include. which worker or job states to include.
Returns [%Task{}, ...] Returns [%Task{}, ...]
""" """
def list_tasks_for(attached_record_type, attached_record_id, job_states \\ Oban.Job.states()) do def list_tasks_for(attached_record_type, attached_record_id, worker_name \\ nil, job_states \\ Oban.Job.states()) do
stringified_states = Enum.map(job_states, &to_string/1) stringified_states = Enum.map(job_states, &to_string/1)
worker_name_finder =
if worker_name do
# Workers are the full module name - we want to match on the string ENDING with
# the passed worker name and it should be preceeded with a . so we aren't matching
# on a substring. You can pass in more fragments of the worker name if you need
# to disambiguate. eg: "TestWorker" or "FooBar.TestWorker"
worker_finder = "%.#{worker_name}"
dynamic([_t, j], fragment("? LIKE ?", j.worker, ^worker_finder))
else
true
end
Repo.all( Repo.all(
from t in Task, from t in Task,
join: j in assoc(t, :job), join: j in assoc(t, :job),
where: field(t, ^attached_record_type) == ^attached_record_id, where: field(t, ^attached_record_type) == ^attached_record_id,
where: ^worker_name_finder,
where: j.state in ^stringified_states where: j.state in ^stringified_states
) )
end end
@doc """ @doc """
Returns the list of pending tasks for a given record type and ID. Returns the list of pending tasks for a given record type and ID. Optionally allows you to specify
which worker to include.
Returns [%Task{}, ...] Returns [%Task{}, ...]
""" """
def list_pending_tasks_for(attached_record_type, attached_record_id) do def list_pending_tasks_for(attached_record_type, attached_record_id, worker_name \\ nil) do
list_tasks_for( list_tasks_for(
attached_record_type, attached_record_type,
attached_record_id, attached_record_id,
worker_name,
[:available, :scheduled, :retryable] [:available, :scheduled, :retryable]
) )
end end
@@ -107,14 +123,15 @@ defmodule Pinchflat.Tasks do
@doc """ @doc """
Deletes all tasks attached to a given record, cancelling any attached jobs. Deletes all tasks attached to a given record, cancelling any attached jobs.
Optionally allows you to specify which worker to include.
Returns :ok Returns :ok
""" """
def delete_tasks_for(attached_record) do def delete_tasks_for(attached_record, worker_name \\ nil) do
tasks = tasks =
case attached_record do case attached_record do
%Source{} = source -> list_tasks_for(:source_id, source.id) %Source{} = source -> list_tasks_for(:source_id, source.id, worker_name)
%MediaItem{} = media_item -> list_tasks_for(:media_item_id, media_item.id) %MediaItem{} = media_item -> list_tasks_for(:media_item_id, media_item.id, worker_name)
end end
Enum.each(tasks, &delete_task/1) Enum.each(tasks, &delete_task/1)
@@ -122,14 +139,15 @@ defmodule Pinchflat.Tasks do
@doc """ @doc """
Deletes all _pending_ tasks attached to a given record, cancelling any attached jobs. Deletes all _pending_ tasks attached to a given record, cancelling any attached jobs.
Optionally allows you to specify which worker to include.
Returns :ok Returns :ok
""" """
def delete_pending_tasks_for(attached_record) do def delete_pending_tasks_for(attached_record, worker_name \\ nil) do
tasks = tasks =
case attached_record do case attached_record do
%Source{} = source -> list_pending_tasks_for(:source_id, source.id) %Source{} = source -> list_pending_tasks_for(:source_id, source.id, worker_name)
%MediaItem{} = media_item -> list_pending_tasks_for(:media_item_id, media_item.id) %MediaItem{} = media_item -> list_pending_tasks_for(:media_item_id, media_item.id, worker_name)
end end
Enum.each(tasks, &delete_task/1) Enum.each(tasks, &delete_task/1)
+13 -16
View File
@@ -12,26 +12,23 @@ defmodule Pinchflat.Tasks.SourceTasks do
alias Pinchflat.Workers.VideoDownloadWorker alias Pinchflat.Workers.VideoDownloadWorker
@doc """ @doc """
Starts tasks for indexing a source's media. Starts tasks for indexing a source's media regardless of the source's indexing
frequency. It's assumed the caller will check for that.
Returns {:ok, :should_not_index} | {:ok, %Task{}}. Returns {:ok, %Task{}}.
""" """
def kickoff_indexing_task(%Source{} = source) do def kickoff_indexing_task(%Source{} = source) do
Tasks.delete_pending_tasks_for(source) Tasks.delete_pending_tasks_for(source, "MediaIndexingWorker")
if source.index_frequency_minutes <= 0 do source
{:ok, :should_not_index} |> Map.take([:id])
else # Schedule this one immediately, but future ones will be on an interval
source |> MediaIndexingWorker.new()
|> Map.take([:id]) |> Tasks.create_job_with_task(source)
# Schedule this one immediately, but future ones will be on an interval |> case do
|> MediaIndexingWorker.new() # This should never return {:error, :duplicate_job} since we just deleted
|> Tasks.create_job_with_task(source) # any pending tasks. I'm being assertive about it so it's obvious if I'm wrong
|> case do {:ok, task} -> {:ok, task}
# This should never return {:error, :duplicate_job} since we just deleted
# any pending tasks. I'm being assertive about it so it's obvious if I'm wrong
{:ok, task} -> {:ok, task}
end
end end
end end
@@ -12,19 +12,17 @@ defmodule PinchflatWeb.MediaItems.MediaItemController do
def delete(conn, %{"id" => id} = params) do def delete(conn, %{"id" => id} = params) do
delete_files = Map.get(params, "delete_files", false) delete_files = Map.get(params, "delete_files", false)
media_item = Media.get_media_item!(id) media_item = Media.get_media_item!(id)
{:ok, _} = Media.delete_media_item(media_item, delete_files: delete_files)
if delete_files do flash_message =
{:ok, _} = Media.delete_media_item_and_attachments(media_item) if delete_files do
"Record and files deleted successfully."
else
"Record deleted successfully. Files were not deleted."
end
conn conn
|> put_flash(:info, "Record and files deleted successfully.") |> put_flash(:info, flash_message)
|> redirect(to: ~p"/sources/#{media_item.source_id}") |> 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 end
@@ -7,17 +7,6 @@
Media Item #<%= @media_item.id %> Media Item #<%= @media_item.id %>
</h2> </h2>
</div> </div>
<nav>
<.link
href={~p"/sources/#{@media_item.source_id}/media/#{@media_item}?delete_files=true"}
method="delete"
data-confirm="Are you sure?"
>
<.button color="bg-meta-1" rounding="rounded-full">
Delete Record and Files
</.button>
</.link>
</nav>
</div> </div>
<div class="rounded-sm border border-stroke bg-white px-5 pb-2.5 pt-6 shadow-default dark:border-strokedark dark:bg-boxdark sm:px-7.5 xl:pb-1"> <div class="rounded-sm border border-stroke bg-white px-5 pb-2.5 pt-6 shadow-default dark:border-strokedark dark:bg-boxdark sm:px-7.5 xl:pb-1">
<div class="max-w-full overflow-x-auto"> <div class="max-w-full overflow-x-auto">
@@ -25,5 +14,17 @@
<h3 class="font-bold text-xl">Attributes</h3> <h3 class="font-bold text-xl">Attributes</h3>
<.list_items_from_map map={Map.from_struct(@media_item)} /> <.list_items_from_map map={Map.from_struct(@media_item)} />
</div> </div>
<section class="flex justify-center my-10">
<.link
href={~p"/sources/#{@media_item.source_id}/media/#{@media_item}?delete_files=true"}
method="delete"
data-confirm="Are you sure you want to delete this record and all associated files on disk? This cannot be undone."
>
<.button color="bg-meta-1" rounding="rounded-full">
Delete Files
</.button>
</.link>
</section>
</div> </div>
</div> </div>
@@ -46,6 +46,7 @@ defmodule PinchflatWeb.MediaProfiles.MediaProfileController do
def edit(conn, %{"id" => id}) do def edit(conn, %{"id" => id}) do
media_profile = Profiles.get_media_profile!(id) media_profile = Profiles.get_media_profile!(id)
changeset = Profiles.change_media_profile(media_profile) changeset = Profiles.change_media_profile(media_profile)
render(conn, :edit, media_profile: media_profile, changeset: changeset) render(conn, :edit, media_profile: media_profile, changeset: changeset)
end end
@@ -63,12 +64,20 @@ defmodule PinchflatWeb.MediaProfiles.MediaProfileController do
end end
end end
def delete(conn, %{"id" => id}) do def delete(conn, %{"id" => id} = params) do
delete_files = Map.get(params, "delete_files", false)
media_profile = Profiles.get_media_profile!(id) media_profile = Profiles.get_media_profile!(id)
{:ok, _media_profile} = Profiles.delete_media_profile(media_profile) {:ok, _media_profile} = Profiles.delete_media_profile(media_profile, delete_files: delete_files)
flash_message =
if delete_files do
"Media profile, its sources, and its files deleted successfully."
else
"Media profile and its sources deleted successfully. Files were not deleted."
end
conn conn
|> put_flash(:info, "Media profile deleted successfully.") |> put_flash(:info, flash_message)
|> redirect(to: ~p"/media_profiles") |> redirect(to: ~p"/media_profiles")
end end
end end
@@ -22,5 +22,27 @@
<h3 class="font-bold text-xl">Attributes</h3> <h3 class="font-bold text-xl">Attributes</h3>
<.list_items_from_map map={Map.from_struct(@media_profile)} /> <.list_items_from_map map={Map.from_struct(@media_profile)} />
</div> </div>
<section class="flex flex-col md:flex-row items-center md:justify-around my-10">
<.link
href={~p"/media_profiles/#{@media_profile}"}
method="delete"
data-confirm="Are you sure you want to delete this profile and all its sources (leaving files in place)? This cannot be undone."
>
<.button color="bg-meta-1" rounding="rounded-full">
Delete Profile and its Sources
</.button>
</.link>
<.link
href={~p"/media_profiles/#{@media_profile}?delete_files=true"}
method="delete"
data-confirm="Are you sure you want to delete this profile, all its sources, and its files on disk? This cannot be undone."
class="mt-5 md:mt-0"
>
<.button color="bg-meta-1" rounding="rounded-full">
Delete Profile, Sources and Files
</.button>
</.link>
</section>
</div> </div>
</div> </div>
@@ -87,12 +87,20 @@ defmodule PinchflatWeb.Sources.SourceController do
end end
end end
def delete(conn, %{"id" => id}) do def delete(conn, %{"id" => id} = params) do
delete_files = Map.get(params, "delete_files", false)
source = Sources.get_source!(id) source = Sources.get_source!(id)
{:ok, _source} = Sources.delete_source(source) {:ok, _source} = Sources.delete_source(source, delete_files: delete_files)
flash_message =
if delete_files do
"Source and files deleted successfully."
else
"Source deleted successfully. Files were not deleted."
end
conn conn
|> put_flash(:info, "Source deleted successfully.") |> put_flash(:info, flash_message)
|> redirect(to: ~p"/sources") |> redirect(to: ~p"/sources")
end end
@@ -24,11 +24,4 @@ defmodule PinchflatWeb.Sources.SourceHTML do
{"Monthly", 30 * 24 * 60} {"Monthly", 30 * 24 * 60}
] ]
end end
def friendly_collection_types do
[
{"Channel", "channel"},
{"Playlist", "playlist"}
]
end
end end
@@ -72,5 +72,27 @@
<p class="text-black dark:text-white">Nothing Here!</p> <p class="text-black dark:text-white">Nothing Here!</p>
<% end %> <% end %>
</div> </div>
<section class="flex flex-col md:flex-row items-center md:justify-around mt-10">
<.link
href={~p"/sources/#{@source}"}
method="delete"
data-confirm="Are you sure you want to delete this source (leaving files in place)? This cannot be undone."
>
<.button color="bg-meta-1" rounding="rounded-full">
Delete Source
</.button>
</.link>
<.link
href={~p"/sources/#{@source}?delete_files=true"}
method="delete"
data-confirm="Are you sure you want to delete this source and it's files on disk? This cannot be undone."
class="mt-5 md:mt-0"
>
<.button color="bg-meta-1" rounding="rounded-full">
Delete Source and Files
</.button>
</.link>
</section>
</div> </div>
</div> </div>
@@ -19,14 +19,12 @@
label="Media Profile" label="Media Profile"
/> />
<.input field={f[:collection_type]} options={friendly_collection_types()} type="select" label="Source Type" />
<.input <.input
field={f[:index_frequency_minutes]} field={f[:index_frequency_minutes]}
options={friendly_index_frequencies()} options={friendly_index_frequencies()}
type="select" type="select"
label="Index Frequency" label="Index Frequency"
help="The time between one index of this source finishing and the next one starting" help="Time between one index of this source finishing and the next one starting. Setting to 'Never' will still run an initial index but no subsequent ones"
/> />
<.input <.input
+26 -30
View File
@@ -36,6 +36,15 @@ defmodule Pinchflat.MediaTest do
end end
end end
describe "list_media_items_for/1" do
test "it returns media_items for a given source" do
source = source_fixture()
media_item = media_item_fixture(%{source_id: source.id})
assert Media.list_media_items_for(source) == [media_item]
end
end
describe "list_pending_media_items_for/1" do describe "list_pending_media_items_for/1" do
test "it returns pending without a filepath for a given source" do test "it returns pending without a filepath for a given source" do
source = source_fixture() source = source_fixture()
@@ -331,7 +340,7 @@ defmodule Pinchflat.MediaTest do
end end
end end
describe "delete_media_item/1" do describe "delete_media_item/2" do
test "deletion deletes the media_item" do test "deletion deletes the media_item" do
media_item = media_item_fixture() media_item = media_item_fixture()
assert {:ok, %MediaItem{}} = Media.delete_media_item(media_item) assert {:ok, %MediaItem{}} = Media.delete_media_item(media_item)
@@ -345,13 +354,20 @@ defmodule Pinchflat.MediaTest do
assert {:ok, %MediaItem{}} = Media.delete_media_item(media_item) assert {:ok, %MediaItem{}} = Media.delete_media_item(media_item)
assert_raise Ecto.NoResultsError, fn -> Repo.reload!(task) end assert_raise Ecto.NoResultsError, fn -> Repo.reload!(task) end
end end
test "does not delete the media_item's files by default" do
media_item = media_item_with_attachments()
assert {:ok, _} = Media.delete_media_item(media_item)
assert File.exists?(media_item.media_filepath)
end
end end
describe "delete_attachments/1" do describe "delete_media_item/2 when testing file deletion" do
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()
assert {:ok, _} = Media.delete_attachments(media_item) assert {:ok, _} = Media.delete_media_item(media_item, delete_files: true)
refute File.exists?(media_item.media_filepath) refute File.exists?(media_item.media_filepath)
end end
@@ -371,23 +387,21 @@ defmodule Pinchflat.MediaTest do
{:ok, updated_media_item} = Media.update_media_item(media_item, update_attrs) {:ok, updated_media_item} = Media.update_media_item(media_item, update_attrs)
assert {:ok, _} = Media.delete_attachments(updated_media_item) assert {:ok, _} = Media.delete_media_item(updated_media_item, delete_files: true)
refute File.exists?(updated_media_item.metadata.metadata_filepath) refute File.exists?(updated_media_item.metadata.metadata_filepath)
end end
test "does not delete the media item" do test "deletion deletes the media_item" do
media_item = media_item_with_attachments() media_item = media_item_fixture()
assert {:ok, %MediaItem{}} = Media.delete_media_item(media_item, delete_files: true)
assert {:ok, _} = Media.delete_attachments(media_item) assert_raise Ecto.NoResultsError, fn -> Media.get_media_item!(media_item.id) end
assert Repo.reload!(media_item)
end end
test "deletes the parent folder if it is empty" do test "deletes the parent folder if it is empty" do
media_item = media_item_with_attachments() media_item = media_item_with_attachments()
root_directory = Path.dirname(media_item.media_filepath) root_directory = Path.dirname(media_item.media_filepath)
assert {:ok, _} = Media.delete_attachments(media_item) assert {:ok, _} = Media.delete_media_item(media_item, delete_files: true)
refute File.exists?(root_directory) refute File.exists?(root_directory)
end end
@@ -396,7 +410,7 @@ defmodule Pinchflat.MediaTest do
root_directory = Path.dirname(media_item.media_filepath) root_directory = Path.dirname(media_item.media_filepath)
File.touch(Path.join([root_directory, "test.txt"])) File.touch(Path.join([root_directory, "test.txt"]))
assert {:ok, _} = Media.delete_attachments(media_item) assert {:ok, _} = Media.delete_media_item(media_item, delete_files: true)
assert File.exists?(root_directory) assert File.exists?(root_directory)
:ok = File.rm(Path.join([root_directory, "test.txt"])) :ok = File.rm(Path.join([root_directory, "test.txt"]))
@@ -404,24 +418,6 @@ defmodule Pinchflat.MediaTest do
end end
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
end
describe "change_media_item/1" do describe "change_media_item/1" do
test "change_media_item/1 returns a media_item changeset" do test "change_media_item/1 returns a media_item changeset" do
media_item = media_item_fixture() media_item = media_item_fixture()
+61 -3
View File
@@ -1,9 +1,12 @@
defmodule Pinchflat.ProfilesTest do defmodule Pinchflat.ProfilesTest do
use Pinchflat.DataCase use Pinchflat.DataCase
import Pinchflat.MediaFixtures
import Pinchflat.SourcesFixtures
import Pinchflat.ProfilesFixtures
alias Pinchflat.Profiles alias Pinchflat.Profiles
alias Pinchflat.Profiles.MediaProfile alias Pinchflat.Profiles.MediaProfile
import Pinchflat.ProfilesFixtures
@invalid_attrs %{name: nil, output_path_template: nil} @invalid_attrs %{name: nil, output_path_template: nil}
@@ -61,11 +64,66 @@ defmodule Pinchflat.ProfilesTest do
end end
end end
describe "delete_media_profile/1" do describe "delete_media_profile/2" do
test "deletion deletes the media_profile" do test "deletion deletes the media_profile" do
media_profile = media_profile_fixture() media_profile = media_profile_fixture()
assert {:ok, %MediaProfile{}} = Profiles.delete_media_profile(media_profile) assert {:ok, %MediaProfile{}} = Profiles.delete_media_profile(media_profile)
assert_raise Ecto.NoResultsError, fn -> Profiles.get_media_profile!(media_profile.id) end
assert_raise Ecto.NoResultsError, fn -> Repo.reload!(media_profile) end
end
test "deletion deletes all sources" do
media_profile = media_profile_fixture()
source = source_fixture(media_profile_id: media_profile.id)
assert {:ok, %MediaProfile{}} = Profiles.delete_media_profile(media_profile)
assert_raise Ecto.NoResultsError, fn -> Repo.reload!(source) end
end
test "deletion deletes all media items" do
media_profile = media_profile_fixture()
source = source_fixture(media_profile_id: media_profile.id)
media_item = media_item_fixture(source_id: source.id)
assert {:ok, %MediaProfile{}} = Profiles.delete_media_profile(media_profile)
assert_raise Ecto.NoResultsError, fn -> Repo.reload!(media_item) end
end
test "deletion does not delete files by default" do
media_profile = media_profile_fixture()
source = source_fixture(media_profile_id: media_profile.id)
media_item = media_item_with_attachments(%{source_id: source.id})
assert {:ok, %MediaProfile{}} = Profiles.delete_media_profile(media_profile)
assert File.exists?(media_item.media_filepath)
end
end
describe "delete_media_profile/2 when deleting files" do
test "still deletes all the needful records" do
media_profile = media_profile_fixture()
source = source_fixture(media_profile_id: media_profile.id)
media_item = media_item_fixture(source_id: source.id)
assert {:ok, %MediaProfile{}} = Profiles.delete_media_profile(media_profile, delete_files: true)
assert_raise Ecto.NoResultsError, fn -> Repo.reload!(media_profile) end
assert_raise Ecto.NoResultsError, fn -> Repo.reload!(source) end
assert_raise Ecto.NoResultsError, fn -> Repo.reload!(media_item) end
end
test "deletes files" do
media_profile = media_profile_fixture()
source = source_fixture(media_profile_id: media_profile.id)
media_item = media_item_with_attachments(%{source_id: source.id})
assert {:ok, %MediaProfile{}} = Profiles.delete_media_profile(media_profile, delete_files: true)
refute File.exists?(media_item.media_filepath)
end end
end end
+144 -34
View File
@@ -23,6 +23,15 @@ defmodule Pinchflat.SourcesTest do
end end
end end
describe "list_sources_for/1" do
test "returns all sources for a given media profile" do
media_profile = media_profile_fixture()
source = source_fixture(media_profile_id: media_profile.id)
assert Sources.list_sources_for(media_profile) == [source]
end
end
describe "get_source!/1" do describe "get_source!/1" do
test "it returns the source with given id" do test "it returns the source with given id" do
source = source_fixture() source = source_fixture()
@@ -32,12 +41,11 @@ defmodule Pinchflat.SourcesTest do
describe "create_source/1" do describe "create_source/1" do
test "creates a source and adds name + ID from runner response for channels" do test "creates a source and adds name + ID from runner response for channels" do
expect(YtDlpRunnerMock, :run, &runner_function_mock/3) expect(YtDlpRunnerMock, :run, &channel_mock/3)
valid_attrs = %{ valid_attrs = %{
media_profile_id: media_profile_fixture().id, media_profile_id: media_profile_fixture().id,
original_url: "https://www.youtube.com/channel/abc123", original_url: "https://www.youtube.com/channel/abc123"
collection_type: "channel"
} }
assert {:ok, %Source{} = source} = Sources.create_source(valid_attrs) assert {:ok, %Source{} = source} = Sources.create_source(valid_attrs)
@@ -46,12 +54,11 @@ defmodule Pinchflat.SourcesTest do
end end
test "creates a source and adds name + ID for playlists" do test "creates a source and adds name + ID for playlists" do
expect(YtDlpRunnerMock, :run, &runner_function_mock/3) expect(YtDlpRunnerMock, :run, &playlist_mock/3)
valid_attrs = %{ valid_attrs = %{
media_profile_id: media_profile_fixture().id, media_profile_id: media_profile_fixture().id,
original_url: "https://www.youtube.com/playlist?list=abc123", original_url: "https://www.youtube.com/playlist?list=abc123"
collection_type: "playlist"
} }
assert {:ok, %Source{} = source} = Sources.create_source(valid_attrs) assert {:ok, %Source{} = source} = Sources.create_source(valid_attrs)
@@ -60,12 +67,11 @@ defmodule Pinchflat.SourcesTest do
end end
test "you can specify a custom friendly_name" do test "you can specify a custom friendly_name" do
expect(YtDlpRunnerMock, :run, &runner_function_mock/3) expect(YtDlpRunnerMock, :run, &channel_mock/3)
valid_attrs = %{ valid_attrs = %{
media_profile_id: media_profile_fixture().id, media_profile_id: media_profile_fixture().id,
original_url: "https://www.youtube.com/channel/abc123", original_url: "https://www.youtube.com/channel/abc123",
collection_type: "channel",
friendly_name: "some custom name" friendly_name: "some custom name"
} }
@@ -75,12 +81,11 @@ defmodule Pinchflat.SourcesTest do
end end
test "friendly name is pulled from collection_name if not specified" do test "friendly name is pulled from collection_name if not specified" do
expect(YtDlpRunnerMock, :run, &runner_function_mock/3) expect(YtDlpRunnerMock, :run, &channel_mock/3)
valid_attrs = %{ valid_attrs = %{
media_profile_id: media_profile_fixture().id, media_profile_id: media_profile_fixture().id,
original_url: "https://www.youtube.com/channel/abc123", original_url: "https://www.youtube.com/channel/abc123"
collection_type: "channel"
} }
assert {:ok, %Source{} = source} = Sources.create_source(valid_attrs) assert {:ok, %Source{} = source} = Sources.create_source(valid_attrs)
@@ -88,6 +93,22 @@ defmodule Pinchflat.SourcesTest do
assert source.friendly_name == "some channel name" assert source.friendly_name == "some channel name"
end end
test "collection_type is inferred from source details" do
expect(YtDlpRunnerMock, :run, &channel_mock/3)
expect(YtDlpRunnerMock, :run, &playlist_mock/3)
valid_attrs = %{
media_profile_id: media_profile_fixture().id,
original_url: "https://www.youtube.com/channel/abc123"
}
assert {:ok, %Source{} = source_1} = Sources.create_source(valid_attrs)
assert {:ok, %Source{} = source_2} = Sources.create_source(valid_attrs)
assert source_1.collection_type == :channel
assert source_2.collection_type == :playlist
end
test "creation with invalid data returns error changeset" do test "creation with invalid data returns error changeset" do
assert {:error, %Ecto.Changeset{}} = Sources.create_source(@invalid_source_attrs) assert {:error, %Ecto.Changeset{}} = Sources.create_source(@invalid_source_attrs)
end end
@@ -97,14 +118,15 @@ defmodule Pinchflat.SourcesTest do
{:ok, {:ok,
Phoenix.json_library().encode!(%{ Phoenix.json_library().encode!(%{
channel: "some channel name", channel: "some channel name",
channel_id: "some_channel_id_12345678" channel_id: "some_channel_id_12345678",
playlist_id: "some_channel_id_12345678",
playlist_title: "some channel name - videos"
})} })}
end) end)
valid_once_attrs = %{ valid_once_attrs = %{
media_profile_id: media_profile_fixture().id, media_profile_id: media_profile_fixture().id,
original_url: "https://www.youtube.com/channel/abc123", original_url: "https://www.youtube.com/channel/abc123"
collection_type: "channel"
} }
assert {:ok, %Source{}} = Sources.create_source(valid_once_attrs) assert {:ok, %Source{}} = Sources.create_source(valid_once_attrs)
@@ -116,14 +138,15 @@ defmodule Pinchflat.SourcesTest do
{:ok, {:ok,
Phoenix.json_library().encode!(%{ Phoenix.json_library().encode!(%{
channel: "some channel name", channel: "some channel name",
channel_id: "some_channel_id_12345678" channel_id: "some_channel_id_12345678",
playlist_id: "some_channel_id_12345678",
playlist_title: "some channel name - videos"
})} })}
end) end)
valid_attrs = %{ valid_attrs = %{
name: "some name", name: "some name",
original_url: "https://www.youtube.com/channel/abc123", original_url: "https://www.youtube.com/channel/abc123"
collection_type: "channel"
} }
source_1_attrs = Map.merge(valid_attrs, %{media_profile_id: media_profile_fixture().id}) source_1_attrs = Map.merge(valid_attrs, %{media_profile_id: media_profile_fixture().id})
@@ -134,12 +157,25 @@ defmodule Pinchflat.SourcesTest do
end end
test "creation will schedule the indexing task" do test "creation will schedule the indexing task" do
expect(YtDlpRunnerMock, :run, &runner_function_mock/3) expect(YtDlpRunnerMock, :run, &channel_mock/3)
valid_attrs = %{
media_profile_id: media_profile_fixture().id,
original_url: "https://www.youtube.com/channel/abc123"
}
assert {:ok, %Source{} = source} = Sources.create_source(valid_attrs)
assert_enqueued(worker: MediaIndexingWorker, args: %{"id" => source.id})
end
test "creation schedules an index test even if the index frequency is 0" do
expect(YtDlpRunnerMock, :run, &channel_mock/3)
valid_attrs = %{ valid_attrs = %{
media_profile_id: media_profile_fixture().id, media_profile_id: media_profile_fixture().id,
original_url: "https://www.youtube.com/channel/abc123", original_url: "https://www.youtube.com/channel/abc123",
collection_type: "channel" index_frequency_minutes: 0
} }
assert {:ok, %Source{} = source} = Sources.create_source(valid_attrs) assert {:ok, %Source{} = source} = Sources.create_source(valid_attrs)
@@ -158,7 +194,7 @@ defmodule Pinchflat.SourcesTest do
end end
test "updating the original_url will re-fetch the source details for channels" do test "updating the original_url will re-fetch the source details for channels" do
expect(YtDlpRunnerMock, :run, &runner_function_mock/3) expect(YtDlpRunnerMock, :run, &channel_mock/3)
source = source_fixture() source = source_fixture()
update_attrs = %{original_url: "https://www.youtube.com/channel/abc123"} update_attrs = %{original_url: "https://www.youtube.com/channel/abc123"}
@@ -169,9 +205,9 @@ defmodule Pinchflat.SourcesTest do
end end
test "updating the original_url will re-fetch the source details for playlists" do test "updating the original_url will re-fetch the source details for playlists" do
expect(YtDlpRunnerMock, :run, &runner_function_mock/3) expect(YtDlpRunnerMock, :run, &playlist_mock/3)
source = source_fixture(collection_type: "playlist") source = source_fixture()
update_attrs = %{original_url: "https://www.youtube.com/playlist?list=abc123"} update_attrs = %{original_url: "https://www.youtube.com/playlist?list=abc123"}
assert {:ok, %Source{} = source} = Sources.update_source(source, update_attrs) assert {:ok, %Source{} = source} = Sources.update_source(source, update_attrs)
@@ -180,7 +216,7 @@ defmodule Pinchflat.SourcesTest do
end end
test "not updating the original_url will not re-fetch the source details" do test "not updating the original_url will not re-fetch the source details" do
expect(YtDlpRunnerMock, :run, 0, &runner_function_mock/3) expect(YtDlpRunnerMock, :run, 0, &channel_mock/3)
source = source_fixture() source = source_fixture()
update_attrs = %{name: "some updated name"} update_attrs = %{name: "some updated name"}
@@ -188,7 +224,7 @@ defmodule Pinchflat.SourcesTest do
assert {:ok, %Source{}} = Sources.update_source(source, update_attrs) assert {:ok, %Source{}} = Sources.update_source(source, update_attrs)
end end
test "updating the index frequency will re-schedule the indexing task" do test "updating the index frequency to >0 will re-schedule the indexing task" do
source = source_fixture() source = source_fixture()
update_attrs = %{index_frequency_minutes: 123} update_attrs = %{index_frequency_minutes: 123}
@@ -197,11 +233,34 @@ defmodule Pinchflat.SourcesTest do
assert_enqueued(worker: MediaIndexingWorker, args: %{"id" => source.id}) assert_enqueued(worker: MediaIndexingWorker, args: %{"id" => source.id})
end end
test "not updating the index frequency will not re-schedule the indexing task" do test "updating the index frequency to 0 will not re-schedule the indexing task" do
source = source_fixture() source = source_fixture()
update_attrs = %{index_frequency_minutes: 0}
assert {:ok, %Source{}} = Sources.update_source(source, update_attrs)
refute_enqueued(worker: MediaIndexingWorker, args: %{"id" => source.id})
end
test "updating the index frequency to 0 will delete any pending tasks" do
source = source_fixture()
{:ok, job} = Oban.insert(MediaIndexingWorker.new(%{"id" => source.id}))
task = task_fixture(source_id: source.id, job_id: job.id)
update_attrs = %{index_frequency_minutes: 0}
assert {:ok, %Source{}} = Sources.update_source(source, update_attrs)
assert_raise Ecto.NoResultsError, fn -> Repo.reload!(task) end
end
test "not updating the index frequency will not re-schedule the indexing task or delete tasks" do
source = source_fixture()
task = task_fixture(source_id: source.id)
update_attrs = %{name: "some updated name"} update_attrs = %{name: "some updated name"}
assert {:ok, %Source{}} = Sources.update_source(source, update_attrs) assert {:ok, %Source{}} = Sources.update_source(source, update_attrs)
assert Repo.reload!(task)
refute_enqueued(worker: MediaIndexingWorker, args: %{"id" => source.id}) refute_enqueued(worker: MediaIndexingWorker, args: %{"id" => source.id})
end end
@@ -236,7 +295,7 @@ defmodule Pinchflat.SourcesTest do
end end
end end
describe "delete_source/1" do describe "delete_source/2" do
test "it deletes the source" do test "it deletes the source" do
source = source_fixture() source = source_fixture()
assert {:ok, %Source{}} = Sources.delete_source(source) assert {:ok, %Source{}} = Sources.delete_source(source)
@@ -255,6 +314,43 @@ defmodule Pinchflat.SourcesTest do
assert {:ok, %Source{}} = Sources.delete_source(source) assert {:ok, %Source{}} = Sources.delete_source(source)
assert_raise Ecto.NoResultsError, fn -> Repo.reload!(task) end assert_raise Ecto.NoResultsError, fn -> Repo.reload!(task) end
end end
test "deletion also deletes all associated media items" do
source = source_fixture()
media_item = media_item_fixture(source_id: source.id)
assert {:ok, %Source{}} = Sources.delete_source(source)
assert_raise Ecto.NoResultsError, fn -> Repo.reload!(media_item) end
end
test "deletion does not delete media files by default" do
source = source_fixture()
media_item = media_item_with_attachments(%{source_id: source.id})
assert {:ok, %Source{}} = Sources.delete_source(source)
assert File.exists?(media_item.media_filepath)
end
end
describe "delete_source/2 when deleting files" do
test "deletes source and media_items" do
source = source_fixture()
media_item = media_item_with_attachments(%{source_id: source.id})
assert {:ok, %Source{}} = Sources.delete_source(source, delete_files: true)
assert_raise Ecto.NoResultsError, fn -> Repo.reload!(media_item) end
assert_raise Ecto.NoResultsError, fn -> Repo.reload!(source) end
end
test "also deletes media files" do
source = source_fixture()
media_item = media_item_with_attachments(%{source_id: source.id})
assert {:ok, %Source{}} = Sources.delete_source(source, delete_files: true)
refute File.exists?(media_item.media_filepath)
end
end end
describe "change_source/2" do describe "change_source/2" do
@@ -267,14 +363,14 @@ defmodule Pinchflat.SourcesTest do
describe "change_source_from_url/2" do describe "change_source_from_url/2" do
test "it returns a changeset" do test "it returns a changeset" do
stub(YtDlpRunnerMock, :run, &runner_function_mock/3) stub(YtDlpRunnerMock, :run, &channel_mock/3)
source = source_fixture() source = source_fixture()
assert %Ecto.Changeset{} = Sources.change_source_from_url(source, %{}) assert %Ecto.Changeset{} = Sources.change_source_from_url(source, %{})
end end
test "it does not fetch source details if the original_url isn't in the changeset" do test "it does not fetch source details if the original_url isn't in the changeset" do
expect(YtDlpRunnerMock, :run, 0, &runner_function_mock/3) expect(YtDlpRunnerMock, :run, 0, &channel_mock/3)
changeset = Sources.change_source_from_url(%Source{}, %{name: "some updated name"}) changeset = Sources.change_source_from_url(%Source{}, %{name: "some updated name"})
@@ -282,7 +378,7 @@ defmodule Pinchflat.SourcesTest do
end end
test "it fetches source details if the original_url is in the changeset" do test "it fetches source details if the original_url is in the changeset" do
expect(YtDlpRunnerMock, :run, &runner_function_mock/3) expect(YtDlpRunnerMock, :run, &channel_mock/3)
changeset = changeset =
Sources.change_source_from_url(%Source{}, %{ Sources.change_source_from_url(%Source{}, %{
@@ -293,13 +389,13 @@ defmodule Pinchflat.SourcesTest do
end end
test "it adds source details to the changeset, keeping the orignal details" do test "it adds source details to the changeset, keeping the orignal details" do
expect(YtDlpRunnerMock, :run, &runner_function_mock/3) expect(YtDlpRunnerMock, :run, &channel_mock/3)
media_profile = media_profile_fixture() media_profile = media_profile_fixture()
media_profile_id = media_profile.id media_profile_id = media_profile.id
changeset = changeset =
Sources.change_source_from_url(%Source{collection_type: :channel}, %{ Sources.change_source_from_url(%Source{}, %{
original_url: "https://www.youtube.com/channel/abc123", original_url: "https://www.youtube.com/channel/abc123",
media_profile_id: media_profile.id media_profile_id: media_profile.id
}) })
@@ -329,15 +425,29 @@ defmodule Pinchflat.SourcesTest do
end end
end end
defp runner_function_mock(_url, _opts, _ot) do defp playlist_mock(_url, _opts, _ot) do
{ {
:ok, :ok,
Phoenix.json_library().encode!(%{ Phoenix.json_library().encode!(%{
channel: "some channel name", channel: nil,
channel_id: "some_channel_id_#{:rand.uniform(1_000_000)}", channel_id: nil,
playlist_id: "some_playlist_id_#{:rand.uniform(1_000_000)}", playlist_id: "some_playlist_id_#{:rand.uniform(1_000_000)}",
playlist_title: "some playlist name" playlist_title: "some playlist name"
}) })
} }
end end
defp channel_mock(_url, _opts, _ot) do
channel_id = "some_channel_id_#{:rand.uniform(1_000_000)}"
{
:ok,
Phoenix.json_library().encode!(%{
channel: "some channel name",
channel_id: channel_id,
playlist_id: channel_id,
playlist_title: "some channel name - videos"
})
}
end
end end
+4 -11
View File
@@ -16,15 +16,7 @@ defmodule Pinchflat.Tasks.SourceTasksTest do
setup :verify_on_exit! setup :verify_on_exit!
describe "kickoff_indexing_task/1" do describe "kickoff_indexing_task/1" do
test "it does not schedule a job if the interval is <= 0" do test "it schedules a job" do
source = source_fixture(index_frequency_minutes: -1)
assert {:ok, :should_not_index} = SourceTasks.kickoff_indexing_task(source)
refute_enqueued(worker: MediaIndexingWorker, args: %{"id" => source.id})
end
test "it schedules a job if the interval is > 0" do
source = source_fixture(index_frequency_minutes: 1) source = source_fixture(index_frequency_minutes: 1)
assert {:ok, _} = SourceTasks.kickoff_indexing_task(source) assert {:ok, _} = SourceTasks.kickoff_indexing_task(source)
@@ -32,7 +24,7 @@ defmodule Pinchflat.Tasks.SourceTasksTest do
assert_enqueued(worker: MediaIndexingWorker, args: %{"id" => source.id}) assert_enqueued(worker: MediaIndexingWorker, args: %{"id" => source.id})
end end
test "it creates and attaches a task if the interval is > 0" do test "it creates and attaches a task" do
source = source_fixture(index_frequency_minutes: 1) source = source_fixture(index_frequency_minutes: 1)
assert {:ok, %Task{} = task} = SourceTasks.kickoff_indexing_task(source) assert {:ok, %Task{} = task} = SourceTasks.kickoff_indexing_task(source)
@@ -42,7 +34,8 @@ defmodule Pinchflat.Tasks.SourceTasksTest do
test "it deletes any pending tasks for the source" do test "it deletes any pending tasks for the source" do
source = source_fixture() source = source_fixture()
task = task_fixture(source_id: source.id) {:ok, job} = Oban.insert(MediaIndexingWorker.new(%{"id" => source.id}))
task = task_fixture(source_id: source.id, job_id: job.id)
assert {:ok, _} = SourceTasks.kickoff_indexing_task(source) assert {:ok, _} = SourceTasks.kickoff_indexing_task(source)
+58 -5
View File
@@ -36,7 +36,7 @@ defmodule Pinchflat.TasksTest do
end end
end end
describe "list_tasks_for/3" do describe "list_tasks_for/4" do
test "it lets you specify which record type/ID to join on" do test "it lets you specify which record type/ID to join on" do
task = task_fixture() task = task_fixture()
@@ -46,12 +46,25 @@ defmodule Pinchflat.TasksTest do
test "it lets you specify which job states to include" do test "it lets you specify which job states to include" do
task = task_fixture() task = task_fixture()
assert Tasks.list_tasks_for(:source_id, task.source_id, [:available]) == [task] assert Tasks.list_tasks_for(:source_id, task.source_id, nil, [:available]) == [task]
assert Tasks.list_tasks_for(:source_id, task.source_id, [:cancelled]) == [] assert Tasks.list_tasks_for(:source_id, task.source_id, nil, [:cancelled]) == []
end
test "it lets you specify which worker to include" do
task = task_fixture()
assert Tasks.list_tasks_for(:source_id, task.source_id, "TestJobWorker") == [task]
assert Tasks.list_tasks_for(:source_id, task.source_id, "FooBarWorker") == []
end
test "it includes all workers if no worker is specified" do
task = task_fixture()
assert Tasks.list_tasks_for(:source_id, task.source_id, nil) == [task]
end end
end end
describe "list_pending_tasks_for/2" do describe "list_pending_tasks_for/3" do
test "it lists pending tasks" do test "it lists pending tasks" do
task = task_fixture() task = task_fixture()
@@ -64,6 +77,13 @@ defmodule Pinchflat.TasksTest do
assert Tasks.list_pending_tasks_for(:source_id, task.source_id) == [] assert Tasks.list_pending_tasks_for(:source_id, task.source_id) == []
end end
test "it lets you specify which worker to include" do
task = task_fixture()
assert Tasks.list_pending_tasks_for(:source_id, task.source_id, "TestJobWorker") == [task]
assert Tasks.list_pending_tasks_for(:source_id, task.source_id, "FooBarWorker") == []
end
end end
describe "get_task!/1" do describe "get_task!/1" do
@@ -154,7 +174,7 @@ defmodule Pinchflat.TasksTest do
end end
end end
describe "delete_tasks_for/1" do describe "delete_tasks_for/2" do
test "it deletes tasks attached to a source" do test "it deletes tasks attached to a source" do
source = source_fixture() source = source_fixture()
task = task_fixture(source_id: source.id) task = task_fixture(source_id: source.id)
@@ -170,6 +190,28 @@ defmodule Pinchflat.TasksTest do
assert :ok = Tasks.delete_tasks_for(media_item) assert :ok = Tasks.delete_tasks_for(media_item)
assert_raise Ecto.NoResultsError, fn -> Tasks.get_task!(task.id) end assert_raise Ecto.NoResultsError, fn -> Tasks.get_task!(task.id) end
end end
test "deletion can specify which worker to include" do
media_item = media_item_fixture()
task = task_fixture(media_item_id: media_item.id)
assert :ok = Tasks.delete_tasks_for(media_item, "FooBarWorker")
assert Repo.reload!(task)
assert :ok = Tasks.delete_tasks_for(media_item, "TestJobWorker")
assert_raise Ecto.NoResultsError, fn -> Repo.reload!(task) end
end
test "deletion does not impact unintended records" do
source = source_fixture()
task = task_fixture(source_id: source.id)
assert :ok = Tasks.delete_tasks_for(source_fixture())
assert :ok = Tasks.delete_tasks_for(source_fixture(), "FooBarWorker")
assert :ok = Tasks.delete_tasks_for(source_fixture(), "TestJobWorker")
assert Repo.reload!(task)
end
end end
describe "delete_pending_tasks_for/1" do describe "delete_pending_tasks_for/1" do
@@ -200,6 +242,17 @@ defmodule Pinchflat.TasksTest do
assert Tasks.get_task!(cancelled_task.id) assert Tasks.get_task!(cancelled_task.id)
assert_raise Ecto.NoResultsError, fn -> Repo.reload!(pending_task) end assert_raise Ecto.NoResultsError, fn -> Repo.reload!(pending_task) end
end end
test "deletion can specify which worker to include" do
media_item = media_item_fixture()
task = task_fixture(media_item_id: media_item.id)
assert :ok = Tasks.delete_pending_tasks_for(media_item, "FooBarWorker")
assert Repo.reload!(task)
assert :ok = Tasks.delete_pending_tasks_for(media_item, "TestJobWorker")
assert_raise Ecto.NoResultsError, fn -> Repo.reload!(task) end
end
end end
describe "change_task/1" do describe "change_task/1" do
@@ -4,7 +4,6 @@ defmodule PinchflatWeb.MediaItemControllerTest do
import Pinchflat.MediaFixtures import Pinchflat.MediaFixtures
alias Pinchflat.Repo alias Pinchflat.Repo
alias Pinchflat.Media
describe "show media" do describe "show media" do
setup [:create_media_item] setup [:create_media_item]
@@ -19,10 +18,6 @@ defmodule PinchflatWeb.MediaItemControllerTest do
setup do setup do
media_item = media_item_with_attachments() media_item = media_item_with_attachments()
on_exit(fn ->
Media.delete_attachments(media_item)
end)
%{media_item: media_item} %{media_item: media_item}
end end
@@ -1,8 +1,12 @@
defmodule PinchflatWeb.MediaProfileControllerTest do defmodule PinchflatWeb.MediaProfileControllerTest do
use PinchflatWeb.ConnCase use PinchflatWeb.ConnCase
import Pinchflat.MediaFixtures
import Pinchflat.SourcesFixtures
import Pinchflat.ProfilesFixtures import Pinchflat.ProfilesFixtures
alias Pinchflat.Repo
@create_attrs %{name: "some name", output_path_template: "some output_path_template"} @create_attrs %{name: "some name", output_path_template: "some output_path_template"}
@update_attrs %{ @update_attrs %{
name: "some updated name", name: "some updated name",
@@ -97,21 +101,71 @@ defmodule PinchflatWeb.MediaProfileControllerTest do
end end
end end
describe "delete media_profile" do describe "delete media_profile when just deleting the records" do
setup [:create_media_profile] setup [:create_media_profile]
test "deletes chosen media_profile", %{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)
media_item = media_item_with_attachments(%{source_id: source.id})
conn = delete(conn, ~p"/media_profiles/#{media_profile}") conn = delete(conn, ~p"/media_profiles/#{media_profile}")
assert redirected_to(conn) == ~p"/media_profiles" assert redirected_to(conn) == ~p"/media_profiles"
assert_error_sent 404, fn -> assert_raise Ecto.NoResultsError, fn -> Repo.reload!(media_profile) end
get(conn, ~p"/media_profiles/#{media_profile}") assert_raise Ecto.NoResultsError, fn -> Repo.reload!(source) end
end assert_raise Ecto.NoResultsError, fn -> Repo.reload!(media_item) end
end
test "redirects to the media_profiles page", %{conn: conn, media_profile: media_profile} do
conn = delete(conn, ~p"/media_profiles/#{media_profile}")
assert redirected_to(conn) == ~p"/media_profiles"
end
test "doesn't delete any files", %{conn: conn, media_profile: media_profile} do
source = source_fixture(media_profile_id: media_profile.id)
media_item = media_item_with_attachments(%{source_id: source.id})
delete(conn, ~p"/media_profiles/#{media_profile}")
assert File.exists?(media_item.media_filepath)
end
end
describe "delete media_profile when deleting the records and files" do
setup [:create_media_profile]
test "deletes chosen media_profile and its associations", %{conn: conn, media_profile: media_profile} do
source = source_fixture(media_profile_id: media_profile.id)
media_item = media_item_with_attachments(%{source_id: source.id})
conn = delete(conn, ~p"/media_profiles/#{media_profile}?delete_files=true")
assert redirected_to(conn) == ~p"/media_profiles"
assert_raise Ecto.NoResultsError, fn -> Repo.reload!(media_profile) end
assert_raise Ecto.NoResultsError, fn -> Repo.reload!(source) end
assert_raise Ecto.NoResultsError, fn -> Repo.reload!(media_item) end
end
test "redirects to the media_profiles page", %{conn: conn, media_profile: media_profile} do
conn = delete(conn, ~p"/media_profiles/#{media_profile}?delete_files=true")
assert redirected_to(conn) == ~p"/media_profiles"
end
test "deletes the files", %{conn: conn, media_profile: media_profile} do
source = source_fixture(media_profile_id: media_profile.id)
media_item = media_item_with_attachments(%{source_id: source.id})
delete(conn, ~p"/media_profiles/#{media_profile}?delete_files=true")
refute File.exists?(media_item.media_filepath)
end end
end end
defp create_media_profile(_) do defp create_media_profile(_) do
media_profile = media_profile_fixture() media_profile = media_profile_fixture()
%{media_profile: media_profile} %{media_profile: media_profile}
end end
end end
@@ -2,8 +2,11 @@ defmodule PinchflatWeb.SourceControllerTest do
use PinchflatWeb.ConnCase use PinchflatWeb.ConnCase
import Mox import Mox
import Pinchflat.ProfilesFixtures import Pinchflat.MediaFixtures
import Pinchflat.SourcesFixtures import Pinchflat.SourcesFixtures
import Pinchflat.ProfilesFixtures
alias Pinchflat.Repo
setup do setup do
media_profile = media_profile_fixture() media_profile = media_profile_fixture()
@@ -119,21 +122,53 @@ defmodule PinchflatWeb.SourceControllerTest do
end end
end end
describe "delete source" do describe "delete source when just deleting the records" do
setup [:create_source] setup [:create_source]
test "deletes chosen source", %{conn: conn, source: source} do test "deletes chosen source and media_items", %{conn: conn, source: source, media_item: media_item} do
delete(conn, ~p"/sources/#{source}")
assert_raise Ecto.NoResultsError, fn -> Repo.reload!(source) end
assert_raise Ecto.NoResultsError, fn -> Repo.reload!(media_item) end
end
test "redirects to the sources page", %{conn: conn, source: source} do
conn = delete(conn, ~p"/sources/#{source}") conn = delete(conn, ~p"/sources/#{source}")
assert redirected_to(conn) == ~p"/sources" assert redirected_to(conn) == ~p"/sources"
end
assert_error_sent 404, fn -> test "does not delete the files", %{conn: conn, source: source, media_item: media_item} do
get(conn, ~p"/sources/#{source}") delete(conn, ~p"/sources/#{source}")
end assert File.exists?(media_item.media_filepath)
end
end
describe "delete source when deleting the records and files" do
setup [:create_source]
test "deletes chosen source and media_items", %{conn: conn, source: source, media_item: media_item} do
delete(conn, ~p"/sources/#{source}?delete_files=true")
assert_raise Ecto.NoResultsError, fn -> Repo.reload!(source) end
assert_raise Ecto.NoResultsError, fn -> Repo.reload!(media_item) end
end
test "redirects to the sources page", %{conn: conn, source: source} do
conn = delete(conn, ~p"/sources/#{source}?delete_files=true")
assert redirected_to(conn) == ~p"/sources"
end
test "deletes the files", %{conn: conn, source: source, media_item: media_item} do
delete(conn, ~p"/sources/#{source}?delete_files=true")
refute File.exists?(media_item.media_filepath)
end end
end end
defp create_source(_) do defp create_source(_) do
%{source: source_fixture()} source = source_fixture()
media_item = media_item_with_attachments(%{source_id: source.id})
%{source: source, media_item: media_item}
end end
defp runner_function_mock(_url, _opts, _ot) do defp runner_function_mock(_url, _opts, _ot) do