diff --git a/config/config.exs b/config/config.exs index 49dde36..8e3cd83 100644 --- a/config/config.exs +++ b/config/config.exs @@ -50,7 +50,7 @@ config :pinchflat, Oban, {Oban.Plugins.Pruner, max_age: 30 * 24 * 60 * 60}, {Oban.Plugins.Cron, crontab: [ - {"@midnight", Pinchflat.Downloading.MediaRetentionWorker} + {"@daily", Pinchflat.Downloading.MediaRetentionWorker} ]} ], # TODO: consider making this an env var or something? diff --git a/lib/pinchflat/downloading/media_retention_worker.ex b/lib/pinchflat/downloading/media_retention_worker.ex index dd560d3..128d61d 100644 --- a/lib/pinchflat/downloading/media_retention_worker.ex +++ b/lib/pinchflat/downloading/media_retention_worker.ex @@ -8,14 +8,30 @@ defmodule Pinchflat.Downloading.MediaRetentionWorker do require Logger - # TODO: docs - # TODO: test - # TODO: set to a 1 min interval to make sure that cron works + alias Pinchflat.Media + # TODO: update wiki after this is merged # TODO: remove data backfill worker # TODO: (other PR - not this one) add way to manually trigger an index of a source AND a pending media download # of a source + @doc """ + Deletes media items that are past their retention date and prevents + them from being re-downloaded. + + This worker is scheduled to run daily via the Oban Cron plugin. + + Returns :ok + """ @impl Oban.Worker def perform(%Oban.Job{}) do + cullable_media = Media.list_cullable_media_items() + Logger.info("Culling #{length(cullable_media)} media items past their retention date") + + Enum.each(cullable_media, fn media_item -> + Media.delete_media_files(media_item, %{ + prevent_download: true, + culled_at: DateTime.utc_now() + }) + end) end end diff --git a/lib/pinchflat/media/media.ex b/lib/pinchflat/media/media.ex index 1caf266..eb0f9c1 100644 --- a/lib/pinchflat/media/media.ex +++ b/lib/pinchflat/media/media.ex @@ -22,7 +22,6 @@ defmodule Pinchflat.Media do Repo.all(MediaItem) end - # TODO: docs # TODO: add attr for excluding media items from this list (ie: keep forever) @doc """ Returns a list of media_items that are cullable based on the retention period @@ -189,20 +188,19 @@ defmodule Pinchflat.Media do Deletes the tasks and media files associated with a media_item but leaves the media_item in the database. Does not delete anything to do with associated metadata. - ## Options: - - `:prevent_download` - If `true`, the media_item will be marked to prevent being redownloaded + Optionally accepts a second argument `addl_attrs` which will be merged into the + media_item before it is updated. Useful for setting things like `prevent_download` + and `culled_at`, if wanted Returns {:ok, %MediaItem{}} | {:error, %Ecto.Changeset{}} """ - def delete_media_files(%MediaItem{} = media_item, opts \\ []) do - prevent_download = Keyword.get(opts, :prevent_download, false) + def delete_media_files(%MediaItem{} = media_item, addl_attrs \\ %{}) do filepath_attrs = MediaItem.filepath_attribute_defaults() - opt_attrs = %{prevent_download: prevent_download} Tasks.delete_tasks_for(media_item) {:ok, _} = do_delete_media_files(media_item) - update_media_item(media_item, Map.merge(filepath_attrs, opt_attrs)) + update_media_item(media_item, Map.merge(filepath_attrs, addl_attrs)) end @doc """ diff --git a/lib/pinchflat/media/media_item.ex b/lib/pinchflat/media/media_item.ex index 05ab537..595d893 100644 --- a/lib/pinchflat/media/media_item.ex +++ b/lib/pinchflat/media/media_item.ex @@ -32,7 +32,8 @@ defmodule Pinchflat.Media.MediaItem do :metadata_filepath, :nfo_filepath, # These are user or system controlled fields - :prevent_download + :prevent_download, + :culled_at ] # Pretty much all the fields captured at index are required. @required_fields ~w( @@ -73,6 +74,7 @@ defmodule Pinchflat.Media.MediaItem do field :subtitle_filepaths, {:array, {:array, :string}}, default: [] field :prevent_download, :boolean, default: false + field :culled_at, :utc_datetime field :matching_search_term, :string, virtual: true diff --git a/lib/pinchflat_web/controllers/media_items/media_item_controller.ex b/lib/pinchflat_web/controllers/media_items/media_item_controller.ex index d054564..bfcb3d2 100644 --- a/lib/pinchflat_web/controllers/media_items/media_item_controller.ex +++ b/lib/pinchflat_web/controllers/media_items/media_item_controller.ex @@ -40,7 +40,7 @@ defmodule PinchflatWeb.MediaItems.MediaItemController do def delete(conn, %{"id" => id} = params) do prevent_download = Map.get(params, "prevent_download", false) media_item = Media.get_media_item!(id) - {:ok, _} = Media.delete_media_files(media_item, prevent_download: prevent_download) + {:ok, _} = Media.delete_media_files(media_item, %{prevent_download: prevent_download}) conn |> put_flash(:info, "Files deleted successfully.") diff --git a/lib/pinchflat_web/controllers/sources/source_html/index.html.heex b/lib/pinchflat_web/controllers/sources/source_html/index.html.heex index 3f66720..c912cfb 100644 --- a/lib/pinchflat_web/controllers/sources/source_html/index.html.heex +++ b/lib/pinchflat_web/controllers/sources/source_html/index.html.heex @@ -22,6 +22,13 @@ <:col :let={source} label="Should Download?"> <.icon name={if source.download_media, do: "hero-check", else: "hero-x-mark"} /> + <:col :let={source} label="Retention"> + <%= if source.retention_period_days && source.retention_period_days > 0 do %> + <%= source.retention_period_days %> day(s) + <% else %> + + <% end %> + <:col :let={source} label="Media Profile"> <.subtle_link href={~p"/media_profiles/#{source.media_profile_id}"}> <%= source.media_profile.name %> diff --git a/lib/pinchflat_web/controllers/sources/source_html/source_form.html.heex b/lib/pinchflat_web/controllers/sources/source_html/source_form.html.heex index cd16370..11374ef 100644 --- a/lib/pinchflat_web/controllers/sources/source_html/source_form.html.heex +++ b/lib/pinchflat_web/controllers/sources/source_html/source_form.html.heex @@ -93,7 +93,7 @@ type="number" label="Retention Period (days)" min="0" - help="Days between when media is *downloaded* and when it's deleted. Set to 0 or delete to keep media indefinitely" + help="Days between when media is *downloaded* and when it's deleted. Delete to keep media forever" />
diff --git a/priv/repo/migrations/20240403164943_add_culled_at_to_media_items.exs b/priv/repo/migrations/20240403164943_add_culled_at_to_media_items.exs new file mode 100644 index 0000000..8ac2d6f --- /dev/null +++ b/priv/repo/migrations/20240403164943_add_culled_at_to_media_items.exs @@ -0,0 +1,9 @@ +defmodule Pinchflat.Repo.Migrations.AddCulledAtToMediaItems do + use Ecto.Migration + + def change do + alter table(:media_items) do + add :culled_at, :utc_datetime + end + end +end diff --git a/test/pinchflat/downloading/media_retention_worker_test.exs b/test/pinchflat/downloading/media_retention_worker_test.exs new file mode 100644 index 0000000..e0aa5c2 --- /dev/null +++ b/test/pinchflat/downloading/media_retention_worker_test.exs @@ -0,0 +1,58 @@ +defmodule Pinchflat.Downloading.MediaRetentionWorkerTest do + use Pinchflat.DataCase + + import Pinchflat.MediaFixtures + import Pinchflat.SourcesFixtures + + alias Pinchflat.Downloading.MediaRetentionWorker + + describe "perform/1" do + test "deletes media files that are past their retention date" do + {_source, old_media_item, new_media_item} = prepare_records() + + perform_job(MediaRetentionWorker, %{}) + + assert File.exists?(new_media_item.media_filepath) + refute File.exists?(old_media_item.media_filepath) + assert Repo.reload!(new_media_item).media_filepath + refute Repo.reload!(old_media_item).media_filepath + end + + test "sets deleted media to not re-download" do + {_source, old_media_item, new_media_item} = prepare_records() + + perform_job(MediaRetentionWorker, %{}) + + refute Repo.reload!(new_media_item).prevent_download + assert Repo.reload!(old_media_item).prevent_download + end + + test "sets culled_at timestamp on deleted media" do + {_source, old_media_item, new_media_item} = prepare_records() + + perform_job(MediaRetentionWorker, %{}) + + refute Repo.reload!(new_media_item).culled_at + assert Repo.reload!(old_media_item).culled_at + assert DateTime.diff(now(), Repo.reload!(old_media_item).culled_at) < 1 + end + end + + defp prepare_records do + source = source_fixture(%{retention_period_days: 2}) + + old_media_item = + media_item_with_attachments(%{ + source_id: source.id, + media_downloaded_at: now_minus(3, :days) + }) + + new_media_item = + media_item_with_attachments(%{ + source_id: source.id, + media_downloaded_at: now_minus(1, :day) + }) + + {source, old_media_item, new_media_item} + end +end diff --git a/test/pinchflat/media_test.exs b/test/pinchflat/media_test.exs index 9a17ca9..35bd3dd 100644 --- a/test/pinchflat/media_test.exs +++ b/test/pinchflat/media_test.exs @@ -699,6 +699,7 @@ defmodule Pinchflat.MediaTest do test "deletes the media_item's files" do media_item = media_item_with_attachments() + assert File.exists?(media_item.media_filepath) assert {:ok, _} = Media.delete_media_files(media_item) refute File.exists?(media_item.media_filepath) end @@ -728,10 +729,10 @@ defmodule Pinchflat.MediaTest do Media.delete_media_item(updated_media_item, delete_files: true) end - test "can prevent the media item from being downloaded" do + test "can take additional attributes update media item" do media_item = media_item_with_attachments() - assert {:ok, updated_media_item} = Media.delete_media_files(media_item, prevent_download: true) + assert {:ok, updated_media_item} = Media.delete_media_files(media_item, %{prevent_download: true}) assert updated_media_item.prevent_download end end