diff --git a/lib/pinchflat/metadata/nfo_builder.ex b/lib/pinchflat/metadata/nfo_builder.ex index d14fb0b..fb5ba7b 100644 --- a/lib/pinchflat/metadata/nfo_builder.ex +++ b/lib/pinchflat/metadata/nfo_builder.ex @@ -21,6 +21,20 @@ defmodule Pinchflat.Metadata.NfoBuilder do filepath end + @doc """ + Builds an NFO file for a souce and stores it at the specified location. + Technically works for playlists, but it's really made for channels. + + Returns the filepath of the NFO file. + """ + def build_and_store_for_source(filepath, metadata) do + nfo = build_for_source(metadata) + + FilesystemHelpers.write_p!(filepath, nfo) + + filepath + end + defp build_for_media_item(metadata) do upload_date = MetadataFileHelpers.parse_upload_date(metadata["upload_date"]) # Cribbed from a combination of the Kodi wiki, ytdl-nfo, and ytdl-sub. @@ -39,4 +53,16 @@ defmodule Pinchflat.Metadata.NfoBuilder do """ end + + defp build_for_source(metadata) do + """ + + + #{metadata["title"]} + #{metadata["description"]} + #{metadata["id"]} + YouTube + + """ + end end diff --git a/lib/pinchflat/metadata/source_metadata_storage_worker.ex b/lib/pinchflat/metadata/source_metadata_storage_worker.ex index c430213..257b0f0 100644 --- a/lib/pinchflat/metadata/source_metadata_storage_worker.ex +++ b/lib/pinchflat/metadata/source_metadata_storage_worker.ex @@ -4,7 +4,7 @@ defmodule Pinchflat.Metadata.SourceMetadataStorageWorker do use Oban.Worker, queue: :remote_metadata, tags: ["media_source", "source_metadata", "remote_metadata"], - max_attempts: 1, + max_attempts: 3, # This is the only thing stopping this job from calling itself # in an infinite loop. Time is in seconds unique: [period: 120] @@ -15,6 +15,7 @@ defmodule Pinchflat.Metadata.SourceMetadataStorageWorker do alias Pinchflat.Repo alias Pinchflat.Tasks alias Pinchflat.Sources + alias Pinchflat.Metadata.NfoBuilder alias Pinchflat.YtDlp.MediaCollection alias Pinchflat.Metadata.MetadataFileHelpers alias Pinchflat.Downloading.DownloadOptionBuilder @@ -41,6 +42,7 @@ defmodule Pinchflat.Metadata.SourceMetadataStorageWorker do @impl Oban.Worker def perform(%Oban.Job{args: %{"id" => source_id}}) do source = Repo.preload(Sources.get_source!(source_id), [:metadata, :media_profile]) + source_metadata = fetch_source_metadata(source) series_directory = determine_series_directory(source) # Since updating a source kicks this job off again, we enforce job uniqueness (above) @@ -48,8 +50,9 @@ defmodule Pinchflat.Metadata.SourceMetadataStorageWorker do # in an infinite loop. Sources.update_source(source, %{ series_directory: series_directory, + nfo_filepath: store_source_nfo(source, series_directory, source_metadata), metadata: %{ - metadata_filepath: store_source_metadata(source) + metadata_filepath: store_source_metadata(source, source_metadata) } }) @@ -59,9 +62,13 @@ defmodule Pinchflat.Metadata.SourceMetadataStorageWorker do Ecto.StaleEntryError -> Logger.info("#{__MODULE__} discarded: source #{source_id} stale") end - defp store_source_metadata(source) do + defp fetch_source_metadata(source) do {:ok, metadata} = MediaCollection.get_source_metadata(source.original_url) + metadata + end + + defp store_source_metadata(source, metadata) do MetadataFileHelpers.compress_and_store_metadata_for(source, metadata) end @@ -74,4 +81,12 @@ defmodule Pinchflat.Metadata.SourceMetadataStorageWorker do {:error, _} -> nil end end + + defp store_source_nfo(source, series_directory, metadata) do + if source.download_nfo && series_directory do + nfo_filepath = Path.join(series_directory, "tvshow.nfo") + + NfoBuilder.build_and_store_for_source(nfo_filepath, metadata) + end + end end diff --git a/lib/pinchflat/sources/source.ex b/lib/pinchflat/sources/source.ex index 1d393c2..baab932 100644 --- a/lib/pinchflat/sources/source.ex +++ b/lib/pinchflat/sources/source.ex @@ -106,4 +106,9 @@ defmodule Pinchflat.Sources.Source do # minutes 15 end + + @doc false + def filepath_attributes do + ~w(nfo_filepath)a + end end diff --git a/lib/pinchflat/sources/sources.ex b/lib/pinchflat/sources/sources.ex index 64f1e61..8f9ca71 100644 --- a/lib/pinchflat/sources/sources.ex +++ b/lib/pinchflat/sources/sources.ex @@ -102,7 +102,6 @@ defmodule Pinchflat.Sources do """ def delete_source(%Source{} = source, opts \\ []) do delete_files = Keyword.get(opts, :delete_files, false) - Tasks.delete_tasks_for(source) source @@ -111,7 +110,11 @@ defmodule Pinchflat.Sources do Media.delete_media_item(media_item, delete_files: delete_files) end) - delete_source_metadata_files(source) + if delete_files do + delete_source_files(source) + end + + delete_internal_metadata_files(source) Repo.delete(source) end @@ -134,16 +137,23 @@ defmodule Pinchflat.Sources do end end - defp delete_source_metadata_files(source) do + defp delete_source_files(source) do + mapped_struct = Map.from_struct(source) + + Source.filepath_attributes() + |> Enum.map(fn field -> mapped_struct[field] end) + |> Enum.filter(&is_binary/1) + |> Enum.each(&FilesystemHelpers.delete_file_and_remove_empty_directories/1) + end + + defp delete_internal_metadata_files(source) do metadata = Repo.preload(source, :metadata).metadata || %SourceMetadata{} mapped_struct = Map.from_struct(metadata) - filepaths = - SourceMetadata.filepath_attributes() - |> Enum.map(fn field -> mapped_struct[field] end) - |> Enum.filter(&is_binary/1) - - Enum.each(filepaths, &FilesystemHelpers.delete_file_and_remove_empty_directories/1) + SourceMetadata.filepath_attributes() + |> Enum.map(fn field -> mapped_struct[field] end) + |> Enum.filter(&is_binary/1) + |> Enum.each(&FilesystemHelpers.delete_file_and_remove_empty_directories/1) end defp add_source_details_to_changeset(source, changeset) do diff --git a/test/pinchflat/metadata/nfo_builder_test.exs b/test/pinchflat/metadata/nfo_builder_test.exs index 0985170..c6119a4 100644 --- a/test/pinchflat/metadata/nfo_builder_test.exs +++ b/test/pinchflat/metadata/nfo_builder_test.exs @@ -31,4 +31,20 @@ defmodule Pinchflat.Metadata.NfoBuilderTest do assert String.contains?(nfo, "#{metadata["title"]}") end end + + describe "build_and_store_for_source/2" do + test "returns the filepath", %{metadata: metadata, filepath: filepath} do + result = NfoBuilder.build_and_store_for_source(filepath, metadata) + + assert File.exists?(result) + end + + test "builds an NFO file", %{metadata: metadata, filepath: filepath} do + result = NfoBuilder.build_and_store_for_source(filepath, metadata) + nfo = File.read!(result) + + assert String.contains?(nfo, ~S()) + assert String.contains?(nfo, "#{metadata["title"]}") + end + end end diff --git a/test/pinchflat/metadata/source_metadata_storage_worker_test.exs b/test/pinchflat/metadata/source_metadata_storage_worker_test.exs index 0a24e49..2981f0c 100644 --- a/test/pinchflat/metadata/source_metadata_storage_worker_test.exs +++ b/test/pinchflat/metadata/source_metadata_storage_worker_test.exs @@ -138,4 +138,64 @@ defmodule Pinchflat.Metadata.SourceMetadataStorageWorkerTest do refute source.series_directory end end + + describe "perform/1 when storing the series NFO" do + test "stores the NFO if specified" do + stub(YtDlpRunnerMock, :run, fn + _url, _opts, ot when ot == @source_details_ot -> + filename = Path.join([Application.get_env(:pinchflat, :media_directory), "Season 1", "bar.mp4"]) + + {:ok, source_details_return_fixture(%{filename: filename})} + + _url, _opts, ot when ot == @metadata_ot -> + {:ok, "{}"} + end) + + source = source_fixture(%{download_nfo: true, nfo_filepath: nil}) + perform_job(SourceMetadataStorageWorker, %{id: source.id}) + source = Repo.reload(source) + + assert source.nfo_filepath + assert source.nfo_filepath == Path.join([source.series_directory, "tvshow.nfo"]) + assert File.exists?(source.nfo_filepath) + + File.rm!(source.nfo_filepath) + end + + test "does not store the NFO if not specified" do + stub(YtDlpRunnerMock, :run, fn + _url, _opts, ot when ot == @source_details_ot -> + filename = Path.join([Application.get_env(:pinchflat, :media_directory), "Season 1", "bar.mp4"]) + + {:ok, source_details_return_fixture(%{filename: filename})} + + _url, _opts, ot when ot == @metadata_ot -> + {:ok, "{}"} + end) + + source = source_fixture(%{download_nfo: false, nfo_filepath: nil}) + perform_job(SourceMetadataStorageWorker, %{id: source.id}) + source = Repo.reload(source) + + refute source.nfo_filepath + end + + test "does not store the NFO if the series directory cannot be determined" do + stub(YtDlpRunnerMock, :run, fn + _url, _opts, ot when ot == @source_details_ot -> + filename = Path.join([Application.get_env(:pinchflat, :media_directory), "foo", "bar.mp4"]) + + {:ok, source_details_return_fixture(%{filename: filename})} + + _url, _opts, ot when ot == @metadata_ot -> + {:ok, "{}"} + end) + + source = source_fixture(%{download_nfo: true, nfo_filepath: nil}) + perform_job(SourceMetadataStorageWorker, %{id: source.id}) + source = Repo.reload(source) + + refute source.nfo_filepath + end + end end diff --git a/test/pinchflat/sources_test.exs b/test/pinchflat/sources_test.exs index 103a33f..788fd7c 100644 --- a/test/pinchflat/sources_test.exs +++ b/test/pinchflat/sources_test.exs @@ -8,6 +8,7 @@ defmodule Pinchflat.SourcesTest do alias Pinchflat.Sources alias Pinchflat.Sources.Source + alias Pinchflat.Filesystem.FilesystemHelpers alias Pinchflat.Metadata.MetadataFileHelpers alias Pinchflat.Downloading.DownloadingHelpers alias Pinchflat.FastIndexing.FastIndexingWorker @@ -474,9 +475,19 @@ defmodule Pinchflat.SourcesTest do {:ok, updated_source} = Sources.update_source(source, update_attrs) - assert {:ok, _} = Sources.delete_source(updated_source, delete_files: true) + assert {:ok, _} = Sources.delete_source(updated_source) refute File.exists?(updated_source.metadata.metadata_filepath) end + + test "does not delete the source's non-metadata files" do + filepath = FilesystemHelpers.generate_metadata_tmpfile(:nfo) + source = source_fixture(%{nfo_filepath: filepath}) + + assert {:ok, _} = Sources.delete_source(source) + assert File.exists?(filepath) + + File.rm!(filepath) + end end describe "delete_source/2 when deleting files" do @@ -498,6 +509,15 @@ defmodule Pinchflat.SourcesTest do refute File.exists?(media_item.media_filepath) end + + test "deletes the source's non-metadata files" do + filepath = FilesystemHelpers.generate_metadata_tmpfile(:nfo) + source = source_fixture(%{nfo_filepath: filepath}) + + assert {:ok, _} = Sources.delete_source(source, delete_files: true) + + refute File.exists?(filepath) + end end describe "change_source/3" do