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