defmodule Pinchflat.Metadata.SourceMetadataStorageWorker do @moduledoc false use Oban.Worker, queue: :remote_metadata, tags: ["media_source", "source_metadata", "remote_metadata", "show_in_dashboard"], max_attempts: 3 require Logger alias __MODULE__ alias Pinchflat.Repo alias Pinchflat.Tasks alias Pinchflat.Sources alias Pinchflat.Utils.StringUtils alias Pinchflat.Metadata.NfoBuilder alias Pinchflat.YtDlp.MediaCollection alias Pinchflat.Metadata.SourceImageParser alias Pinchflat.Metadata.MetadataFileHelpers alias Pinchflat.Downloading.DownloadOptionBuilder @doc """ Starts the source metadata storage worker and creates a task for the source. Returns {:ok, %Task{}} | {:error, :duplicate_job} | {:error, %Ecto.Changeset{}} """ def kickoff_with_task(source, opts \\ []) do %{id: source.id} |> SourceMetadataStorageWorker.new(opts) |> Tasks.create_job_with_task(source) end @doc """ Fetches and stores various forms of metadata for a source: - Attributes like `description` - JSON metadata for internal use - The series directory for the source - The NFO file for the source (if specified) - Downloads and stores source images (if specified) The worker is kicked off after a source is inserted or it's original_url is updated - this can take an unknown amount of time so don't rely on this data being here before, say, the first indexing or downloading task is complete. Returns :ok """ @impl Oban.Worker def perform(%Oban.Job{args: %{"id" => source_id}}) do source = Repo.preload(Sources.get_source!(source_id), [:metadata, :media_profile]) series_directory = determine_series_directory(source) {source_metadata, source_image_attrs, metadata_image_attrs} = fetch_source_metadata_and_images(series_directory, source) source_metadata_filepath = MetadataFileHelpers.compress_and_store_metadata_for(source, source_metadata) Sources.update_source( source, Map.merge( %{ series_directory: series_directory, nfo_filepath: store_source_nfo(source, series_directory, source_metadata), description: source_metadata["description"], metadata: Map.merge(%{metadata_filepath: source_metadata_filepath}, metadata_image_attrs) }, source_image_attrs ), # `run_post_commit_tasks: false` prevents this from running in an infinite loop run_post_commit_tasks: false ) :ok rescue Ecto.NoResultsError -> Logger.info("#{__MODULE__} discarded: source #{source_id} not found") Ecto.StaleEntryError -> Logger.info("#{__MODULE__} discarded: source #{source_id} stale") end defp fetch_source_metadata_and_images(series_directory, source) do metadata_directory = MetadataFileHelpers.metadata_directory_for(source) {:ok, metadata} = fetch_metadata_for_source(source) metadata_image_attrs = SourceImageParser.store_source_images(metadata_directory, metadata) if source.media_profile.download_source_images && series_directory do source_image_attrs = SourceImageParser.store_source_images(series_directory, metadata) {metadata, source_image_attrs, metadata_image_attrs} else {metadata, %{}, metadata_image_attrs} end end defp determine_series_directory(source) do output_path = DownloadOptionBuilder.build_output_path_for(source) runner_opts = [output: output_path] addl_opts = [use_cookies: Sources.use_cookies?(source, :metadata)] {:ok, %{filepath: filepath}} = MediaCollection.get_source_details(source.original_url, runner_opts, addl_opts) case MetadataFileHelpers.series_directory_from_media_filepath(filepath) do {:ok, series_directory} -> series_directory {:error, _} -> nil end end defp store_source_nfo(source, series_directory, metadata) do if source.media_profile.download_nfo && series_directory do nfo_filepath = Path.join(series_directory, "tvshow.nfo") NfoBuilder.build_and_store_for_source(nfo_filepath, metadata) end end defp fetch_metadata_for_source(source) do tmp_output_path = "#{tmp_directory()}/#{StringUtils.random_string(16)}/source_image.%(ext)S" base_opts = [convert_thumbnails: "jpg", output: tmp_output_path] should_use_cookies = Sources.use_cookies?(source, :metadata) opts = if source.collection_type == :channel do base_opts ++ [:write_all_thumbnails, playlist_items: 0] else base_opts ++ [:write_thumbnail, playlist_items: 1] end MediaCollection.get_source_metadata(source.original_url, opts, use_cookies: should_use_cookies) end defp tmp_directory do Application.get_env(:pinchflat, :tmpfile_directory) end end