Added function for updating a media item's filepaths on-disk

This commit is contained in:
Kieran Eglin 2024-09-26 11:50:09 -07:00
parent 0163e85e76
commit a3d3ff91ba
No known key found for this signature in database
GPG key ID: 193984967FCF432D
3 changed files with 85 additions and 12 deletions

View file

@ -12,7 +12,7 @@ defmodule Pinchflat.Downloading.MediaDownloadWorker do
alias Pinchflat.Tasks alias Pinchflat.Tasks
alias Pinchflat.Repo alias Pinchflat.Repo
alias Pinchflat.Media alias Pinchflat.Media
alias Pinchflat.Media.FileDeletion alias Pinchflat.Media.FileSyncing
alias Pinchflat.Downloading.MediaDownloader alias Pinchflat.Downloading.MediaDownloader
alias Pinchflat.Lifecycle.UserScripts.CommandRunner, as: UserScriptRunner 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) 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) run_user_script(:media_downloaded, updated_media_item)
:ok :ok

View file

@ -1,8 +1,9 @@
defmodule Pinchflat.Media.FileDeletion do defmodule Pinchflat.Media.FileSyncing do
@moduledoc """ @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.Utils.MapUtils
alias Pinchflat.Media.MediaItem alias Pinchflat.Media.MediaItem
alias Pinchflat.Utils.FilesystemUtils, as: FSUtils alias Pinchflat.Utils.FilesystemUtils, as: FSUtils
@ -34,6 +35,22 @@ defmodule Pinchflat.Media.FileDeletion do
:ok :ok
end 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 defp handle_file_deletion(old_attributes, new_attributes) do
# The logic: # The logic:
# - A file should only be deleted if it exists and the new file is different # - 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) 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 end

View file

@ -1,16 +1,16 @@
defmodule Pinchflat.Media.FileDeletionTest do defmodule Pinchflat.Media.FileSyncingTest do
use Pinchflat.DataCase use Pinchflat.DataCase
import Pinchflat.MediaFixtures import Pinchflat.MediaFixtures
alias Pinchflat.Media.FileDeletion alias Pinchflat.Media.FileSyncing
describe "delete_outdated_files/2" do describe "delete_outdated_files/2" do
test "deletes outdated non-subtitle files" do test "deletes outdated non-subtitle files" do
new_media_item = media_item_with_attachments() new_media_item = media_item_with_attachments()
old_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) assert File.exists?(new_media_item.media_filepath)
refute File.exists?(old_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() new_media_item = media_item_with_attachments()
old_media_item = media_item_fixture(%{media_filepath: new_media_item.media_filepath}) 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?(new_media_item.media_filepath)
assert File.exists?(old_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}) new_media_item = media_item_fixture(%{media_filepath: nil})
old_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?(old_media_item.media_filepath) assert File.exists?(old_media_item.media_filepath)
end end
@ -39,7 +39,7 @@ defmodule Pinchflat.Media.FileDeletionTest do
new_media_item = media_item_with_attachments() new_media_item = media_item_with_attachments()
old_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")) assert File.exists?(get_subtitle_filepath(new_media_item, "en"))
refute File.exists?(get_subtitle_filepath(old_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() new_media_item = media_item_with_attachments()
old_media_item = media_item_fixture(%{subtitle_filepaths: new_media_item.subtitle_filepaths}) 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(new_media_item, "en"))
assert File.exists?(get_subtitle_filepath(old_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: []}) new_media_item = media_item_fixture(%{subtitle_filepaths: []})
old_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(old_media_item, "en")) assert File.exists?(get_subtitle_filepath(old_media_item, "en"))
end end
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 defp get_subtitle_filepath(media_item, language) do
Enum.reduce_while(media_item.subtitle_filepaths, nil, fn [lang, filepath], acc -> Enum.reduce_while(media_item.subtitle_filepaths, nil, fn [lang, filepath], acc ->
if lang == language do if lang == language do