diff --git a/config/config.exs b/config/config.exs index 2551df6..3696960 100644 --- a/config/config.exs +++ b/config/config.exs @@ -39,7 +39,7 @@ config :pinchflat, Oban, # Keep old jobs for 30 days for display in the UI plugins: [{Oban.Plugins.Pruner, max_age: 30 * 24 * 60 * 60}], # TODO: consider making this an env var or something? - queues: [default: 10, media_indexing: 2, media_fetching: 2] + queues: [default: 10, media_indexing: 2, media_fetching: 2, media_local_metadata: 8] # Configures the mailer # diff --git a/lib/pinchflat/media/media_item.ex b/lib/pinchflat/media/media_item.ex index 2886b61..0023e93 100644 --- a/lib/pinchflat/media/media_item.ex +++ b/lib/pinchflat/media/media_item.ex @@ -22,6 +22,7 @@ defmodule Pinchflat.Media.MediaItem do # these fields are captured on download :media_downloaded_at, :media_filepath, + :media_size_bytes, :subtitle_filepaths, :thumbnail_filepath, :metadata_filepath @@ -37,6 +38,7 @@ defmodule Pinchflat.Media.MediaItem do field :media_downloaded_at, :utc_datetime field :media_filepath, :string + field :media_size_bytes, :integer field :thumbnail_filepath, :string field :metadata_filepath, :string # This is an array of [iso-2 language, filepath] pairs. Probably could diff --git a/lib/pinchflat/tasks/media_item_tasks.ex b/lib/pinchflat/tasks/media_item_tasks.ex new file mode 100644 index 0000000..c3cad23 --- /dev/null +++ b/lib/pinchflat/tasks/media_item_tasks.ex @@ -0,0 +1,22 @@ +defmodule Pinchflat.Tasks.MediaItemTasks do + @moduledoc """ + This module contains methods used by or used to control tasks (aka workers) + related to media items. + """ + alias Pinchflat.Media + + @doc """ + Fetches the file size of a media item and saves it to the database. + + Returns {:ok, media_item} | {:error, any()} + """ + def compute_and_save_media_filesize(media_item) do + case File.stat(media_item.media_filepath) do + {:ok, %{size: size}} -> + Media.update_media_item(media_item, %{media_size_bytes: size}) + + err -> + err + end + end +end diff --git a/lib/pinchflat/tasks/source_tasks.ex b/lib/pinchflat/tasks/source_tasks.ex index 51f3938..360345e 100644 --- a/lib/pinchflat/tasks/source_tasks.ex +++ b/lib/pinchflat/tasks/source_tasks.ex @@ -1,6 +1,7 @@ defmodule Pinchflat.Tasks.SourceTasks do @moduledoc """ - This module contains methods for managing tasks (workers) related to sources. + This module contains methods used by or used to control tasks (aka workers) + related to sources. """ alias Pinchflat.Media diff --git a/lib/pinchflat/workers/filesystem_data_worker.ex b/lib/pinchflat/workers/filesystem_data_worker.ex new file mode 100644 index 0000000..137a2c6 --- /dev/null +++ b/lib/pinchflat/workers/filesystem_data_worker.ex @@ -0,0 +1,27 @@ +defmodule Pinchflat.Workers.FilesystemDataWorker do + @moduledoc false + + use Oban.Worker, + queue: :media_local_metadata, + tags: ["media_item", "media_metadata", "local_metadata"], + max_attempts: 1 + + alias Pinchflat.Media + alias Pinchflat.Tasks.MediaItemTasks + + @impl Oban.Worker + @doc """ + For a given media item, compute and save metadata about the file on-disk. + + Returns :ok + """ + def perform(%Oban.Job{args: %{"id" => media_item_id}}) do + media_item = Media.get_media_item!(media_item_id) + + MediaItemTasks.compute_and_save_media_filesize(media_item) + + # Don't retry on failure - if it didn't work immediately there's no + # reason to believe it will work later. + :ok + end +end diff --git a/lib/pinchflat/workers/video_download_worker.ex b/lib/pinchflat/workers/video_download_worker.ex index 8a93a26..9afd336 100644 --- a/lib/pinchflat/workers/video_download_worker.ex +++ b/lib/pinchflat/workers/video_download_worker.ex @@ -8,7 +8,9 @@ defmodule Pinchflat.Workers.VideoDownloadWorker do alias Pinchflat.Repo alias Pinchflat.Media + alias Pinchflat.Tasks alias Pinchflat.MediaClient.VideoDownloader + alias Pinchflat.Workers.FilesystemDataWorker @impl Oban.Worker @doc """ @@ -25,16 +27,31 @@ defmodule Pinchflat.Workers.VideoDownloadWorker do # If the source is set to not download media, perform a no-op if media_item.source.download_media do - download_media(media_item) + download_media_and_schedule_jobs(media_item) else :ok end end - defp download_media(media_item) do + defp download_media_and_schedule_jobs(media_item) do case VideoDownloader.download_for_media_item(media_item) do - {:ok, _} -> {:ok, media_item} - err -> err + {:ok, _} -> + schedule_filesystem_data_worker(media_item) + {:ok, media_item} + + err -> + err + end + end + + defp schedule_filesystem_data_worker(media_item) do + media_item + |> Map.take([:id]) + |> FilesystemDataWorker.new() + |> Tasks.create_job_with_task(media_item) + |> case do + {:ok, task} -> {:ok, task} + {:error, :duplicate_job} -> {:ok, :job_exists} end end end diff --git a/priv/repo/migrations/20240303021902_add_media_size_to_media_item.exs b/priv/repo/migrations/20240303021902_add_media_size_to_media_item.exs new file mode 100644 index 0000000..3b99220 --- /dev/null +++ b/priv/repo/migrations/20240303021902_add_media_size_to_media_item.exs @@ -0,0 +1,9 @@ +defmodule Pinchflat.Repo.Migrations.AddMediaSizeToMediaItem do + use Ecto.Migration + + def change do + alter table(:media_items) do + add :media_size_bytes, :integer + end + end +end diff --git a/test/pinchflat/tasks/media_items_tasks_test.exs b/test/pinchflat/tasks/media_items_tasks_test.exs new file mode 100644 index 0000000..7e890c5 --- /dev/null +++ b/test/pinchflat/tasks/media_items_tasks_test.exs @@ -0,0 +1,25 @@ +defmodule Pinchflat.Tasks.MediaItemTasksTest do + use Pinchflat.DataCase + + import Pinchflat.MediaFixtures + + alias Pinchflat.Tasks.MediaItemTasks + + describe "compute_and_save_media_filesize/1" do + test "updates the media item with the file size" do + media_item = media_item_with_attachments() + + refute media_item.media_size_bytes + + assert {:ok, media_item} = MediaItemTasks.compute_and_save_media_filesize(media_item) + + assert Repo.reload!(media_item).media_size_bytes + end + + test "returns the error if operation fails" do + media_item = media_item_fixture(%{media_filepath: "/nonexistent/file.mkv"}) + + assert {:error, _} = MediaItemTasks.compute_and_save_media_filesize(media_item) + end + end +end diff --git a/test/pinchflat/workers/filesystem_data_worker_test.exs b/test/pinchflat/workers/filesystem_data_worker_test.exs new file mode 100644 index 0000000..44b0afd --- /dev/null +++ b/test/pinchflat/workers/filesystem_data_worker_test.exs @@ -0,0 +1,19 @@ +defmodule Pinchflat.Workers.FilesystemDataWorkerTest do + use Pinchflat.DataCase + + import Pinchflat.MediaFixtures + + alias Pinchflat.Workers.FilesystemDataWorker + + describe "perform/1" do + test "Computes and stores the media file size" do + media_item = media_item_with_attachments() + + refute media_item.media_size_bytes + + perform_job(FilesystemDataWorker, %{id: media_item.id}) + + assert Repo.reload!(media_item).media_size_bytes + end + end +end diff --git a/test/pinchflat/workers/video_download_worker_test.exs b/test/pinchflat/workers/video_download_worker_test.exs index 0e043f1..b2bebed 100644 --- a/test/pinchflat/workers/video_download_worker_test.exs +++ b/test/pinchflat/workers/video_download_worker_test.exs @@ -6,6 +6,7 @@ defmodule Pinchflat.Workers.VideoDownloadWorkerTest do alias Pinchflat.Sources alias Pinchflat.Workers.VideoDownloadWorker + alias Pinchflat.Workers.FilesystemDataWorker setup :verify_on_exit! @@ -68,5 +69,17 @@ defmodule Pinchflat.Workers.VideoDownloadWorkerTest do perform_job(VideoDownloadWorker, %{id: media_item.id}) end + + test "it schedules a filesystem data worker", %{media_item: media_item} do + expect(YtDlpRunnerMock, :run, fn _url, _opts, _ot -> + {:ok, render_metadata(:media_metadata)} + end) + + assert [] = all_enqueued(worker: FilesystemDataWorker) + + perform_job(VideoDownloadWorker, %{id: media_item.id}) + + assert [_] = all_enqueued(worker: FilesystemDataWorker) + end end end