From a3d3ff91bacd46ff6e17963774c97de74bec12f6 Mon Sep 17 00:00:00 2001 From: Kieran Eglin Date: Thu, 26 Sep 2024 11:50:09 -0700 Subject: [PATCH] Added function for updating a media item's filepaths on-disk --- .../downloading/media_download_worker.ex | 4 +- .../{file_deletion.ex => file_syncing.ex} | 43 +++++++++++++++- ...eletion_test.exs => file_syncing_test.exs} | 50 ++++++++++++++++--- 3 files changed, 85 insertions(+), 12 deletions(-) rename lib/pinchflat/media/{file_deletion.ex => file_syncing.ex} (55%) rename test/pinchflat/media/{file_deletion_test.exs => file_syncing_test.exs} (53%) diff --git a/lib/pinchflat/downloading/media_download_worker.ex b/lib/pinchflat/downloading/media_download_worker.ex index 0605bcd..989668e 100644 --- a/lib/pinchflat/downloading/media_download_worker.ex +++ b/lib/pinchflat/downloading/media_download_worker.ex @@ -12,7 +12,7 @@ defmodule Pinchflat.Downloading.MediaDownloadWorker do alias Pinchflat.Tasks alias Pinchflat.Repo alias Pinchflat.Media - alias Pinchflat.Media.FileDeletion + alias Pinchflat.Media.FileSyncing alias Pinchflat.Downloading.MediaDownloader alias Pinchflat.Lifecycle.UserScripts.CommandRunner, as: UserScriptRunner @@ -86,7 +86,7 @@ defmodule Pinchflat.Downloading.MediaDownloadWorker do media_redownloaded_at: get_redownloaded_at(is_quality_upgrade) }) - :ok = FileDeletion.delete_outdated_files(media_item, updated_media_item) + :ok = FileSyncing.delete_outdated_files(media_item, updated_media_item) run_user_script(:media_downloaded, updated_media_item) :ok diff --git a/lib/pinchflat/media/file_deletion.ex b/lib/pinchflat/media/file_syncing.ex similarity index 55% rename from lib/pinchflat/media/file_deletion.ex rename to lib/pinchflat/media/file_syncing.ex index 7ed01a8..7fb5b65 100644 --- a/lib/pinchflat/media/file_deletion.ex +++ b/lib/pinchflat/media/file_syncing.ex @@ -1,8 +1,9 @@ -defmodule Pinchflat.Media.FileDeletion do +defmodule Pinchflat.Media.FileSyncing do @moduledoc """ - Functions for deleting files that are no longer needed by media items. + Functions for ensuring file state is accurately reflected in the database. """ + alias Pinchflat.Media alias Pinchflat.Utils.MapUtils alias Pinchflat.Media.MediaItem alias Pinchflat.Utils.FilesystemUtils, as: FSUtils @@ -34,6 +35,22 @@ defmodule Pinchflat.Media.FileDeletion do :ok end + @doc """ + Nillifies any media item filepaths that don't exist on disk for a list of media items + + returns [%MediaItem{}] + """ + def sync_file_presence_on_disk(media_items) do + Enum.map(media_items, fn media_item -> + new_attributes = sync_media_item_files(media_item) + # Doing this one-by-one instead of batching since this process + # can take time and a batch could let MediaItem state get out of sync + {:ok, updated_media_item} = Media.update_media_item(media_item, new_attributes) + + updated_media_item + end) + end + defp handle_file_deletion(old_attributes, new_attributes) do # The logic: # - A file should only be deleted if it exists and the new file is different @@ -51,4 +68,26 @@ defmodule Pinchflat.Media.FileDeletion do end end) end + + defp sync_media_item_files(media_item) do + non_subtitle_keys = MediaItem.filepath_attributes() -- [:subtitle_filepaths] + subtitle_keys = MapUtils.from_nested_list(media_item.subtitle_filepaths) + non_subtitles = Map.take(media_item, non_subtitle_keys) + + # This one is checking for the negative (ie: only update if the file doesn't exist) + new_non_subtitle_attrs = + Enum.reduce(non_subtitles, %{}, fn {key, filepath}, acc -> + if filepath && File.exists?(filepath), do: acc, else: Map.put(acc, key, nil) + end) + + # This one is checking for the positive (ie: only update if the file exists) + # This is because subtitles, being an array type in the DB, are most easily updated + # by a full replacement rather than finding the actual diff + new_subtitle_attrs = + Enum.reduce(subtitle_keys, [], fn {key, filepath}, acc -> + if filepath && File.exists?(filepath), do: acc ++ [[key, filepath]], else: acc + end) + + Map.put(new_non_subtitle_attrs, :subtitle_filepaths, new_subtitle_attrs) + end end diff --git a/test/pinchflat/media/file_deletion_test.exs b/test/pinchflat/media/file_syncing_test.exs similarity index 53% rename from test/pinchflat/media/file_deletion_test.exs rename to test/pinchflat/media/file_syncing_test.exs index 6494f21..da354bd 100644 --- a/test/pinchflat/media/file_deletion_test.exs +++ b/test/pinchflat/media/file_syncing_test.exs @@ -1,16 +1,16 @@ -defmodule Pinchflat.Media.FileDeletionTest do +defmodule Pinchflat.Media.FileSyncingTest do use Pinchflat.DataCase import Pinchflat.MediaFixtures - alias Pinchflat.Media.FileDeletion + alias Pinchflat.Media.FileSyncing describe "delete_outdated_files/2" do test "deletes outdated non-subtitle files" do new_media_item = media_item_with_attachments() old_media_item = media_item_with_attachments() - assert :ok = FileDeletion.delete_outdated_files(old_media_item, new_media_item) + assert :ok = FileSyncing.delete_outdated_files(old_media_item, new_media_item) assert File.exists?(new_media_item.media_filepath) refute File.exists?(old_media_item.media_filepath) @@ -20,7 +20,7 @@ defmodule Pinchflat.Media.FileDeletionTest do new_media_item = media_item_with_attachments() old_media_item = media_item_fixture(%{media_filepath: new_media_item.media_filepath}) - assert :ok = FileDeletion.delete_outdated_files(old_media_item, new_media_item) + assert :ok = FileSyncing.delete_outdated_files(old_media_item, new_media_item) assert File.exists?(new_media_item.media_filepath) assert File.exists?(old_media_item.media_filepath) @@ -30,7 +30,7 @@ defmodule Pinchflat.Media.FileDeletionTest do new_media_item = media_item_fixture(%{media_filepath: nil}) old_media_item = media_item_with_attachments() - assert :ok = FileDeletion.delete_outdated_files(old_media_item, new_media_item) + assert :ok = FileSyncing.delete_outdated_files(old_media_item, new_media_item) assert File.exists?(old_media_item.media_filepath) end @@ -39,7 +39,7 @@ defmodule Pinchflat.Media.FileDeletionTest do new_media_item = media_item_with_attachments() old_media_item = media_item_with_attachments() - assert :ok = FileDeletion.delete_outdated_files(old_media_item, new_media_item) + assert :ok = FileSyncing.delete_outdated_files(old_media_item, new_media_item) assert File.exists?(get_subtitle_filepath(new_media_item, "en")) refute File.exists?(get_subtitle_filepath(old_media_item, "en")) @@ -49,7 +49,7 @@ defmodule Pinchflat.Media.FileDeletionTest do new_media_item = media_item_with_attachments() old_media_item = media_item_fixture(%{subtitle_filepaths: new_media_item.subtitle_filepaths}) - assert :ok = FileDeletion.delete_outdated_files(old_media_item, new_media_item) + assert :ok = FileSyncing.delete_outdated_files(old_media_item, new_media_item) assert File.exists?(get_subtitle_filepath(new_media_item, "en")) assert File.exists?(get_subtitle_filepath(old_media_item, "en")) @@ -59,12 +59,46 @@ defmodule Pinchflat.Media.FileDeletionTest do new_media_item = media_item_fixture(%{subtitle_filepaths: []}) old_media_item = media_item_with_attachments() - assert :ok = FileDeletion.delete_outdated_files(old_media_item, new_media_item) + assert :ok = FileSyncing.delete_outdated_files(old_media_item, new_media_item) assert File.exists?(get_subtitle_filepath(old_media_item, "en")) end end + describe "sync_file_presence_on_disk/1" do + test "removes attributes whose files are missing" do + media_item = media_item_fixture(%{media_filepath: "/tmp/missing_file.mp4"}) + + assert media_item.media_filepath + assert [updated_media_item] = FileSyncing.sync_file_presence_on_disk([media_item]) + refute updated_media_item.media_filepath + end + + test "doesn't remove attributes where the files still exist" do + media_item = media_item_with_attachments() + + assert media_item.media_filepath + assert [updated_media_item] = FileSyncing.sync_file_presence_on_disk([media_item]) + assert updated_media_item.media_filepath + end + + test "removes subtitle files that are missing" do + media_item = media_item_fixture(%{subtitle_filepaths: [["en", "/tmp/missing_file.srt"]]}) + + assert get_subtitle_filepath(media_item, "en") + assert [updated_media_item] = FileSyncing.sync_file_presence_on_disk([media_item]) + refute get_subtitle_filepath(updated_media_item, "en") + end + + test "doesn't remove subtitle files that still exist" do + media_item = media_item_with_attachments() + + assert get_subtitle_filepath(media_item, "en") + assert [updated_media_item] = FileSyncing.sync_file_presence_on_disk([media_item]) + assert get_subtitle_filepath(updated_media_item, "en") + end + end + defp get_subtitle_filepath(media_item, language) do Enum.reduce_while(media_item.subtitle_filepaths, nil, fn [lang, filepath], acc -> if lang == language do