Updated download options to take a media item; allowed for specifying custom output path template options

This commit is contained in:
Kieran Eglin 2024-03-02 11:32:42 -08:00
parent 1ad9e2c331
commit 8b2d7ec759
No known key found for this signature in database
GPG key ID: 193984967FCF432D
5 changed files with 117 additions and 96 deletions

View file

@ -11,7 +11,6 @@ defmodule Pinchflat.MediaClient.VideoDownloader do
alias Pinchflat.Repo alias Pinchflat.Repo
alias Pinchflat.Media alias Pinchflat.Media
alias Pinchflat.Media.MediaItem alias Pinchflat.Media.MediaItem
alias Pinchflat.Profiles.MediaProfile
alias Pinchflat.MediaClient.Backends.YtDlp.Video, as: YtDlpVideo alias Pinchflat.MediaClient.Backends.YtDlp.Video, as: YtDlpVideo
alias Pinchflat.Profiles.Options.YtDlp.DownloadOptionBuilder, as: YtDlpDownloadOptionBuilder alias Pinchflat.Profiles.Options.YtDlp.DownloadOptionBuilder, as: YtDlpDownloadOptionBuilder
@ -31,9 +30,8 @@ defmodule Pinchflat.MediaClient.VideoDownloader do
""" """
def download_for_media_item(%MediaItem{} = media_item, backend \\ :yt_dlp) do def download_for_media_item(%MediaItem{} = media_item, backend \\ :yt_dlp) do
item_with_preloads = Repo.preload(media_item, [:metadata, source: :media_profile]) item_with_preloads = Repo.preload(media_item, [:metadata, source: :media_profile])
media_profile = item_with_preloads.source.media_profile
case download_for_media_profile(media_item.original_url, media_profile, backend) do case download_with_options(media_item.original_url, item_with_preloads, backend) do
{:ok, parsed_json} -> {:ok, parsed_json} ->
{parser, helpers} = metadata_parsers(backend) {parser, helpers} = metadata_parsers(backend)
@ -57,10 +55,10 @@ defmodule Pinchflat.MediaClient.VideoDownloader do
end end
end end
defp download_for_media_profile(url, %MediaProfile{} = media_profile, backend) do defp download_with_options(url, item_with_preloads, backend) do
option_builder = option_builder(backend) option_builder = option_builder(backend)
video_backend = video_backend(backend) video_backend = video_backend(backend)
{:ok, options} = option_builder.build(media_profile) {:ok, options} = option_builder.build(item_with_preloads)
video_backend.download(url, options) video_backend.download(url, options)
end end

View file

@ -5,21 +5,17 @@ defmodule Pinchflat.Profiles.Options.YtDlp.DownloadOptionBuilder do
IDEA: consider making this a behaviour so I can add other backends later IDEA: consider making this a behaviour so I can add other backends later
""" """
alias Pinchflat.Profiles.MediaProfile alias Pinchflat.Media.MediaItem
alias Pinchflat.Profiles.Options.YtDlp.OutputPathBuilder alias Pinchflat.Profiles.Options.YtDlp.OutputPathBuilder
@doc """ @doc """
Builds the options for yt-dlp to download media based on the given media profile. Builds the options for yt-dlp to download media based on the given media's profile.
IDEA: consider adding the ability to pass in a second argument to override IDEA: consider adding the ability to pass in a second argument to override
these options these options
""" """
def build(%MediaProfile{} = media_profile) do def build(%MediaItem{} = media_item_with_preloads) do
# NOTE: I'll be hardcoding most things for now (esp. options to help me test) - media_profile = media_item_with_preloads.source.media_profile
# add more configuration later as I build out the models. Walk before you can run!
# NOTE: Looks like you can put different media types in different directories.
# see: https://github.com/yt-dlp/yt-dlp#output-template
built_options = built_options =
default_options() ++ default_options() ++
@ -27,12 +23,11 @@ defmodule Pinchflat.Profiles.Options.YtDlp.DownloadOptionBuilder do
thumbnail_options(media_profile) ++ thumbnail_options(media_profile) ++
metadata_options(media_profile) ++ metadata_options(media_profile) ++
quality_options(media_profile) ++ quality_options(media_profile) ++
output_options(media_profile) output_options(media_item_with_preloads)
{:ok, built_options} {:ok, built_options}
end end
# This will be updated a lot as I add new options to profiles
defp default_options do defp default_options do
[:no_progress, :windows_filenames] [:no_progress, :windows_filenames]
end end
@ -101,14 +96,25 @@ defmodule Pinchflat.Profiles.Options.YtDlp.DownloadOptionBuilder do
end end
end end
defp output_options(media_profile) do defp output_options(media_item_with_preloads) do
{:ok, output_path} = OutputPathBuilder.build(media_profile.output_path_template) media_profile = media_item_with_preloads.source.media_profile
additional_options_map = output_options_map(media_item_with_preloads)
{:ok, output_path} = OutputPathBuilder.build(media_profile.output_path_template, additional_options_map)
[ [
output: Path.join(base_directory(), output_path) output: Path.join(base_directory(), output_path)
] ]
end end
defp output_options_map(media_item_with_preloads) do
source = media_item_with_preloads.source
%{
"source_friendly_name" => source.friendly_name,
"source_collection_type" => source.collection_type
}
end
defp base_directory do defp base_directory do
Application.get_env(:pinchflat, :media_directory) Application.get_env(:pinchflat, :media_directory)
end end

View file

@ -8,13 +8,16 @@ defmodule Pinchflat.Profiles.Options.YtDlp.OutputPathBuilder do
alias Pinchflat.RenderedString.Parser, as: TemplateParser alias Pinchflat.RenderedString.Parser, as: TemplateParser
@doc """ @doc """
Builds the actual final filepath from a given template. Builds the actual final filepath from a given template. Optionally, you can pass in
a map of additional options to be used in the template.
Translates liquid-style templates into yt-dlp-style templates, Translates liquid-style templates into yt-dlp-style templates,
leaving yt-dlp syntax intact. leaving yt-dlp syntax intact.
""" """
def build(template_string) do def build(template_string, additional_template_options \\ %{}) do
TemplateParser.parse(template_string, custom_yt_dlp_option_map(), &identifier_fn/2) combined_options = Map.merge(custom_yt_dlp_option_map(), additional_template_options)
TemplateParser.parse(template_string, combined_options, &identifier_fn/2)
end end
# The `nil` case simply wraps the identifier in yt-dlp-style syntax. This assumes that # The `nil` case simply wraps the identifier in yt-dlp-style syntax. This assumes that

View file

@ -3,7 +3,7 @@ defmodule Pinchflat.MediaClient.Backends.YtDlp.OutputPathBuilderTest do
alias Pinchflat.Profiles.Options.YtDlp.OutputPathBuilder alias Pinchflat.Profiles.Options.YtDlp.OutputPathBuilder
describe "build/1" do describe "build/2" do
test "it expands 'standard' curly brace variables in the template" do test "it expands 'standard' curly brace variables in the template" do
assert {:ok, res} = OutputPathBuilder.build("/videos/{{ title }}.{{ ext }}") assert {:ok, res} = OutputPathBuilder.build("/videos/{{ title }}.{{ ext }}")
@ -16,6 +16,12 @@ defmodule Pinchflat.MediaClient.Backends.YtDlp.OutputPathBuilderTest do
assert res == "/videos/%(upload_date>%Y)S.%(ext)S" assert res == "/videos/%(upload_date>%Y)S.%(ext)S"
end end
test "it respects additional options" do
assert {:ok, res} = OutputPathBuilder.build("/videos/{{ custom }}.{{ ext }}", %{"custom" => "test"})
assert res == "/videos/test.%(ext)S"
end
test "it leaves yt-dlp variables alone" do test "it leaves yt-dlp variables alone" do
assert {:ok, res} = OutputPathBuilder.build("/videos/%(title)s.%(ext)s") assert {:ok, res} = OutputPathBuilder.build("/videos/%(title)s.%(ext)s")

View file

@ -1,24 +1,40 @@
defmodule Pinchflat.Profiles.Options.YtDlp.DownloadOptionBuilderTest do defmodule Pinchflat.Profiles.Options.YtDlp.DownloadOptionBuilderTest do
use ExUnit.Case, async: true use Pinchflat.DataCase
import Pinchflat.MediaFixtures
import Pinchflat.ProfilesFixtures
import Pinchflat.SourcesFixtures
alias Pinchflat.Profiles.MediaProfile alias Pinchflat.Profiles
alias Pinchflat.Profiles.Options.YtDlp.DownloadOptionBuilder alias Pinchflat.Profiles.Options.YtDlp.DownloadOptionBuilder
@media_profile %MediaProfile{ setup do
output_path_template: "{{ title }}.%(ext)s" media_profile = media_profile_fixture(%{output_path_template: "{{ title }}.%(ext)s"})
} source = source_fixture(%{media_profile_id: media_profile.id, friendly_name: "my source"})
media_item = Repo.preload(media_item_fixture(source_id: source.id), source: :media_profile)
describe "build/1" do {:ok, media_item: media_item}
test "it generates an expanded output path based on the given template" do end
assert {:ok, res} = DownloadOptionBuilder.build(@media_profile)
describe "build/1 when testing output options" do
test "it generates an expanded output path based on the given template", %{media_item: media_item} do
assert {:ok, res} = DownloadOptionBuilder.build(media_item)
assert {:output, "/tmp/test/videos/%(title)S.%(ext)s"} in res assert {:output, "/tmp/test/videos/%(title)S.%(ext)s"} in res
end end
test "it respects custom output path options", %{media_item: media_item} do
media_item =
update_media_profile_attribute(media_item, %{output_path_template: "{{ source_friendly_name }}.%(ext)s"})
assert {:ok, res} = DownloadOptionBuilder.build(media_item)
assert {:output, "/tmp/test/videos/#{media_item.source.friendly_name}.%(ext)s"} in res
end
end end
describe "build/1 when testing default options" do describe "build/1 when testing default options" do
test "it includes default options" do test "it includes default options", %{media_item: media_item} do
assert {:ok, res} = DownloadOptionBuilder.build(@media_profile) assert {:ok, res} = DownloadOptionBuilder.build(media_item)
assert :no_progress in res assert :no_progress in res
assert :windows_filenames in res assert :windows_filenames in res
@ -26,109 +42,93 @@ defmodule Pinchflat.Profiles.Options.YtDlp.DownloadOptionBuilderTest do
end end
describe "build/1 when testing subtitle options" do describe "build/1 when testing subtitle options" do
test "includes :write_subs option when specified" do test "includes :write_subs option when specified", %{media_item: media_item} do
media_profile = %MediaProfile{@media_profile | download_subs: true} media_item = update_media_profile_attribute(media_item, %{download_subs: true})
assert {:ok, res} = DownloadOptionBuilder.build(media_profile) assert {:ok, res} = DownloadOptionBuilder.build(media_item)
assert :write_subs in res assert :write_subs in res
end end
test "forces SRT format when download_subs is true" do test "forces SRT format when download_subs is true", %{media_item: media_item} do
media_profile = %MediaProfile{@media_profile | download_subs: true} media_item = update_media_profile_attribute(media_item, %{download_subs: true})
assert {:ok, res} = DownloadOptionBuilder.build(media_profile) assert {:ok, res} = DownloadOptionBuilder.build(media_item)
assert {:convert_subs, "srt"} in res assert {:convert_subs, "srt"} in res
end end
test "includes :write_auto_subs option when specified" do test "includes :write_auto_subs option when specified", %{media_item: media_item} do
media_profile = %MediaProfile{@media_profile | download_subs: true, download_auto_subs: true} media_item = update_media_profile_attribute(media_item, %{download_subs: true, download_auto_subs: true})
assert {:ok, res} = DownloadOptionBuilder.build(media_profile) assert {:ok, res} = DownloadOptionBuilder.build(media_item)
assert :write_auto_subs in res assert :write_auto_subs in res
end end
test "doesn't include :write_auto_subs option when download_subs is false" do test "doesn't include :write_auto_subs option when download_subs is false", %{media_item: media_item} do
media_profile = %MediaProfile{@media_profile | download_subs: false, download_auto_subs: true} media_item = update_media_profile_attribute(media_item, %{download_subs: false, download_auto_subs: true})
assert {:ok, res} = DownloadOptionBuilder.build(media_profile) assert {:ok, res} = DownloadOptionBuilder.build(media_item)
refute :write_auto_subs in res refute :write_auto_subs in res
end end
test "includes :embed_subs option when specified" do test "includes :embed_subs option when specified", %{media_item: media_item} do
media_profile = %MediaProfile{@media_profile | embed_subs: true} media_item = update_media_profile_attribute(media_item, %{embed_subs: true})
assert {:ok, res} = DownloadOptionBuilder.build(media_profile) assert {:ok, res} = DownloadOptionBuilder.build(media_item)
assert :embed_subs in res assert :embed_subs in res
end end
test "includes sub_langs option when download_subs is true" do test "includes sub_langs option when download_subs is true", %{media_item: media_item} do
media_profile = %MediaProfile{@media_profile | download_subs: true, sub_langs: "en"} media_item = update_media_profile_attribute(media_item, %{download_subs: true, sub_langs: "en"})
assert {:ok, res} = DownloadOptionBuilder.build(media_profile) assert {:ok, res} = DownloadOptionBuilder.build(media_item)
assert {:sub_langs, "en"} in res assert {:sub_langs, "en"} in res
end end
test "includes sub_langs option when embed_subs is true" do test "includes sub_langs option when embed_subs is true", %{media_item: media_item} do
media_profile = %MediaProfile{@media_profile | embed_subs: true, sub_langs: "en"} media_item = update_media_profile_attribute(media_item, %{embed_subs: true, sub_langs: "en"})
assert {:ok, res} = DownloadOptionBuilder.build(media_profile) assert {:ok, res} = DownloadOptionBuilder.build(media_item)
assert {:sub_langs, "en"} in res assert {:sub_langs, "en"} in res
end end
test "doesn't include sub_langs option when neither downloading nor embedding" do test "doesn't include sub_langs option when neither downloading nor embedding", %{media_item: media_item} do
media_profile = %MediaProfile{ media_item =
@media_profile update_media_profile_attribute(media_item, %{embed_subs: false, download_subs: false, sub_langs: "en"})
| embed_subs: false,
download_subs: false,
sub_langs: "en"
}
assert {:ok, res} = DownloadOptionBuilder.build(media_profile) assert {:ok, res} = DownloadOptionBuilder.build(media_item)
refute {:sub_langs, "en"} in res refute {:sub_langs, "en"} in res
end end
test "other struct attributes are ignored" do
media_profile = %MediaProfile{@media_profile | id: -1}
assert {:ok, res} = DownloadOptionBuilder.build(media_profile)
refute {:id, -1} in res
end
end end
describe "build/1 when testing thumbnail options" do describe "build/1 when testing thumbnail options" do
test "includes :write_thumbnail option when specified" do test "includes :write_thumbnail option when specified", %{media_item: media_item} do
media_profile = %MediaProfile{@media_profile | download_thumbnail: true} media_item = update_media_profile_attribute(media_item, %{download_thumbnail: true})
assert {:ok, res} = DownloadOptionBuilder.build(media_profile) assert {:ok, res} = DownloadOptionBuilder.build(media_item)
assert :write_thumbnail in res assert :write_thumbnail in res
end end
test "includes :embed_thumbnail option when specified" do test "includes :embed_thumbnail option when specified", %{media_item: media_item} do
media_profile = %MediaProfile{@media_profile | embed_thumbnail: true} media_item = update_media_profile_attribute(media_item, %{embed_thumbnail: true})
assert {:ok, res} = DownloadOptionBuilder.build(media_profile) assert {:ok, res} = DownloadOptionBuilder.build(media_item)
assert :embed_thumbnail in res assert :embed_thumbnail in res
end end
test "doesn't include these options when not specified" do test "doesn't include these options when not specified", %{media_item: media_item} do
media_profile = %MediaProfile{ media_item = update_media_profile_attribute(media_item, %{embed_thumbnail: false, download_thumbnail: false})
@media_profile
| embed_thumbnail: false,
download_thumbnail: false
}
assert {:ok, res} = DownloadOptionBuilder.build(media_profile) assert {:ok, res} = DownloadOptionBuilder.build(media_item)
refute :write_thumbnail in res refute :write_thumbnail in res
refute :embed_thumbnail in res refute :embed_thumbnail in res
@ -136,31 +136,27 @@ defmodule Pinchflat.Profiles.Options.YtDlp.DownloadOptionBuilderTest do
end end
describe "build/1 when testing metadata options" do describe "build/1 when testing metadata options" do
test "includes :write_info_json option when specified" do test "includes :write_info_json option when specified", %{media_item: media_item} do
media_profile = %MediaProfile{@media_profile | download_metadata: true} media_item = update_media_profile_attribute(media_item, %{download_metadata: true})
assert {:ok, res} = DownloadOptionBuilder.build(media_profile) assert {:ok, res} = DownloadOptionBuilder.build(media_item)
assert :write_info_json in res assert :write_info_json in res
assert :clean_info_json in res assert :clean_info_json in res
end end
test "includes :embed_metadata option when specified" do test "includes :embed_metadata option when specified", %{media_item: media_item} do
media_profile = %MediaProfile{@media_profile | embed_metadata: true} media_item = update_media_profile_attribute(media_item, %{embed_metadata: true})
assert {:ok, res} = DownloadOptionBuilder.build(media_profile) assert {:ok, res} = DownloadOptionBuilder.build(media_item)
assert :embed_metadata in res assert :embed_metadata in res
end end
test "doesn't include these options when not specified" do test "doesn't include these options when not specified", %{media_item: media_item} do
media_profile = %MediaProfile{ media_item = update_media_profile_attribute(media_item, %{embed_metadata: false, download_metadata: false})
@media_profile
| embed_metadata: false,
download_metadata: false
}
assert {:ok, res} = DownloadOptionBuilder.build(media_profile) assert {:ok, res} = DownloadOptionBuilder.build(media_item)
refute :write_info_json in res refute :write_info_json in res
refute :clean_info_json in res refute :clean_info_json in res
@ -169,10 +165,22 @@ defmodule Pinchflat.Profiles.Options.YtDlp.DownloadOptionBuilderTest do
end end
describe "build/1 when testing quality options" do describe "build/1 when testing quality options" do
test "it includes quality options" do test "it includes quality options", %{media_item: media_item} do
assert {:ok, res} = DownloadOptionBuilder.build(@media_profile) media_item = update_media_profile_attribute(media_item, %{preferred_resolution: :"1080p"})
assert {:ok, res} = DownloadOptionBuilder.build(media_item)
assert {:format_sort, "res:1080,+codec:avc:m4a"} in res assert {:format_sort, "res:1080,+codec:avc:m4a"} in res
end end
end end
defp update_media_profile_attribute(media_item_with_preloads, attrs) do
media_item_with_preloads.source.media_profile
|> Profiles.change_media_profile(attrs)
|> Repo.update!()
media_item_with_preloads
|> Repo.reload()
|> Repo.preload(source: :media_profile)
end
end end