diff --git a/lib/pinchflat/downloading/download_option_builder.ex b/lib/pinchflat/downloading/download_option_builder.ex index dec34a7..180c4bc 100644 --- a/lib/pinchflat/downloading/download_option_builder.ex +++ b/lib/pinchflat/downloading/download_option_builder.ex @@ -3,6 +3,7 @@ defmodule Pinchflat.Downloading.DownloadOptionBuilder do Builds the options for yt-dlp to download media based on the given media profile. """ + alias Pinchflat.Sources alias Pinchflat.Sources.Source alias Pinchflat.Media.MediaItem alias Pinchflat.Downloading.OutputPathBuilder @@ -30,12 +31,12 @@ defmodule Pinchflat.Downloading.DownloadOptionBuilder do @doc """ Builds the output path for yt-dlp to download media based on the given source's - media profile. + media profile. Uses the source's override output path template if it exists. Returns binary() """ def build_output_path_for(%Source{} = source_with_preloads) do - output_path_template = source_with_preloads.media_profile.output_path_template + output_path_template = Sources.output_path_template(source_with_preloads) build_output_path(output_path_template, source_with_preloads) end @@ -184,7 +185,7 @@ defmodule Pinchflat.Downloading.DownloadOptionBuilder do # It's dependent on the output_path_template being a string ending `.{{ ext }}` # (or equivalent), but that's validated by the MediaProfile schema. defp determine_thumbnail_location(media_item_with_preloads) do - output_path_template = media_item_with_preloads.source.media_profile.output_path_template + output_path_template = Sources.output_path_template(media_item_with_preloads.source) output_path_template |> String.split(~r{\.}, include_captures: true) diff --git a/lib/pinchflat/profiles/media_profile.ex b/lib/pinchflat/profiles/media_profile.ex index 0b351a9..be6c6a0 100644 --- a/lib/pinchflat/profiles/media_profile.ex +++ b/lib/pinchflat/profiles/media_profile.ex @@ -80,7 +80,8 @@ defmodule Pinchflat.Profiles.MediaProfile do |> unique_constraint(:name) end - defp ext_regex do + @doc false + def ext_regex do ~r/\.({{ ?ext ?}}|%\( ?ext ?\)[sS])$/ end end diff --git a/lib/pinchflat/sources/source.ex b/lib/pinchflat/sources/source.ex index c2ef216..a02c8c0 100644 --- a/lib/pinchflat/sources/source.ex +++ b/lib/pinchflat/sources/source.ex @@ -32,6 +32,7 @@ defmodule Pinchflat.Sources.Source do retention_period_days title_filter_regex media_profile_id + output_path_template_override )a # Expensive API calls are made when a source is inserted/updated so @@ -76,6 +77,7 @@ defmodule Pinchflat.Sources.Source do field :retention_period_days, :integer field :original_url, :string field :title_filter_regex, :string + field :output_path_template_override, :string field :series_directory, :string field :nfo_filepath, :string @@ -109,6 +111,8 @@ defmodule Pinchflat.Sources.Source do |> dynamic_default(:uuid, fn _ -> Ecto.UUID.generate() end) |> validate_required(required_fields) |> validate_number(:retention_period_days, greater_than_or_equal_to: 0) + # Ensures it ends with `.{{ ext }}` or `.%(ext)s` or similar (with a little wiggle room) + |> validate_format(:output_path_template_override, MediaProfile.ext_regex(), message: "must end with .{{ ext }}") |> cast_assoc(:metadata, with: &SourceMetadata.changeset/2, required: false) |> unique_constraint([:collection_id, :media_profile_id, :title_filter_regex], error_key: :original_url) end diff --git a/lib/pinchflat/sources/sources.ex b/lib/pinchflat/sources/sources.ex index 27d16b8..b57a628 100644 --- a/lib/pinchflat/sources/sources.ex +++ b/lib/pinchflat/sources/sources.ex @@ -19,6 +19,19 @@ defmodule Pinchflat.Sources do alias Pinchflat.SlowIndexing.SlowIndexingHelpers alias Pinchflat.Metadata.SourceMetadataStorageWorker + @doc """ + Returns the relevant output path template for a source. + Pulls from the source's override if present, otherwise uses the media profile's. + + Returns binary() + """ + def output_path_template(source) do + source = Repo.preload(source, :media_profile) + media_profile = source.media_profile + + source.output_path_template_override || media_profile.output_path_template + end + @doc """ Returns the list of sources. Returns [%Source{}, ...] """ diff --git a/priv/repo/migrations/20240411154245_add_output_template_to_sources.exs b/priv/repo/migrations/20240411154245_add_output_template_to_sources.exs new file mode 100644 index 0000000..8237996 --- /dev/null +++ b/priv/repo/migrations/20240411154245_add_output_template_to_sources.exs @@ -0,0 +1,9 @@ +defmodule Pinchflat.Repo.Migrations.AddOutputTemplateToSources do + use Ecto.Migration + + def change do + alter table(:sources) do + add :output_path_template_override, :string + end + end +end diff --git a/test/pinchflat/downloading/download_option_builder_test.exs b/test/pinchflat/downloading/download_option_builder_test.exs index a7a30cb..5314f89 100644 --- a/test/pinchflat/downloading/download_option_builder_test.exs +++ b/test/pinchflat/downloading/download_option_builder_test.exs @@ -4,6 +4,7 @@ defmodule Pinchflat.Downloading.DownloadOptionBuilderTest do import Pinchflat.SourcesFixtures import Pinchflat.ProfilesFixtures + alias Pinchflat.Sources alias Pinchflat.Profiles alias Pinchflat.Utils.FilesystemUtils alias Pinchflat.Downloading.DownloadOptionBuilder @@ -31,6 +32,20 @@ defmodule Pinchflat.Downloading.DownloadOptionBuilderTest do assert {:output, "/tmp/test/media/#{media_item.source.custom_name}.%(ext)s"} in res end + + test "uses source's output override if present", %{media_item: media_item} do + source = media_item.source + {:ok, _} = Sources.update_source(source, %{output_path_template_override: "override.%(ext)s"}) + + media_item = + media_item + |> Repo.reload() + |> Repo.preload(source: :media_profile) + + assert {:ok, res} = DownloadOptionBuilder.build(media_item) + + assert {:output, "/tmp/test/media/override.%(ext)s"} in res + end end describe "build/1 when testing default options" do @@ -135,6 +150,20 @@ defmodule Pinchflat.Downloading.DownloadOptionBuilderTest do assert {:output, "thumbnail:/tmp/test/media/%(title)S-thumb.%(ext)s"} in res end + test "appends -thumb to source's output path override, if present", %{media_item: media_item} do + media_item = update_media_profile_attribute(media_item, %{download_thumbnail: true}) + {:ok, _} = Sources.update_source(media_item.source, %{output_path_template_override: "override.%(ext)s"}) + + media_item = + media_item + |> Repo.reload() + |> Repo.preload(source: :media_profile) + + assert {:ok, res} = DownloadOptionBuilder.build(media_item) + + assert {:output, "thumbnail:/tmp/test/media/override-thumb.%(ext)s"} in res + end + test "converts thumbnail to jpg when download_thumbnail is true", %{media_item: media_item} do media_item = update_media_profile_attribute(media_item, %{download_thumbnail: true}) @@ -359,6 +388,15 @@ defmodule Pinchflat.Downloading.DownloadOptionBuilderTest do assert path == "/tmp/test/media/%(title)S.%(ext)s" end + + test "uses source's output override if present", %{media_item: media_item} do + source = media_item.source + {:ok, source} = Sources.update_source(source, %{output_path_template_override: "override.%(ext)s"}) + + path = DownloadOptionBuilder.build_output_path_for(source) + + assert path == "/tmp/test/media/override.%(ext)s" + end end defp update_media_profile_attribute(media_item_with_preloads, attrs) do diff --git a/test/pinchflat/sources_test.exs b/test/pinchflat/sources_test.exs index 175a945..3b8091d 100644 --- a/test/pinchflat/sources_test.exs +++ b/test/pinchflat/sources_test.exs @@ -35,6 +35,28 @@ defmodule Pinchflat.SourcesTest do end end + describe "output_path_template/1" do + test "returns the source's override if present" do + source = source_fixture(%{output_path_template_override: "/override/{{ title }}.{{ ext }}"}) + + assert Sources.output_path_template(source) == "/override/{{ title }}.{{ ext }}" + end + + test "returns the media profile's template if no override is present" do + media_profile = media_profile_fixture(%{output_path_template: "/profile/{{ title }}.{{ ext }}"}) + source = source_fixture(%{media_profile_id: media_profile.id}) + + assert Sources.output_path_template(source) == "/profile/{{ title }}.{{ ext }}" + end + + test "Treats empty strings as being blank" do + media_profile = media_profile_fixture(%{output_path_template: "/profile/{{ title }}.{{ ext }}"}) + source = source_fixture(%{media_profile_id: media_profile.id, output_path_template_override: " "}) + + assert Sources.output_path_template(source) == "/profile/{{ title }}.{{ ext }}" + end + end + describe "list_sources/0" do test "it returns all sources" do source = source_fixture()