diff --git a/lib/pinchflat/downloader/backends/yt_dlp/command_runner.ex b/lib/pinchflat/downloader/backends/yt_dlp/command_runner.ex index 545e0948..522c0432 100644 --- a/lib/pinchflat/downloader/backends/yt_dlp/command_runner.ex +++ b/lib/pinchflat/downloader/backends/yt_dlp/command_runner.ex @@ -10,6 +10,10 @@ defmodule Pinchflat.Downloader.Backends.YtDlp.CommandRunner do @doc """ Runs a yt-dlp command and returns the string output + + # TODO: deduplicate command opts, keeping the last one on conflict + although possibly not needed (and a LOT easier) if yt-dlp + just ignores duplicate options (ie: look into that) """ @impl BackendCommandRunner def run(url, command_opts) do diff --git a/lib/pinchflat/downloader/backends/yt_dlp/video.ex b/lib/pinchflat/downloader/backends/yt_dlp/video.ex index fe6895d5..e00bec75 100644 --- a/lib/pinchflat/downloader/backends/yt_dlp/video.ex +++ b/lib/pinchflat/downloader/backends/yt_dlp/video.ex @@ -10,6 +10,7 @@ defmodule Pinchflat.Downloader.Backends.YtDlp.Video do The video will be moved to its final destination... elsewhere # TODO: update these docs when I figure out a module to move videos + # TODO: test # NOTE: maybe instead of moving it to the tempdir, I can just download it to the final destination by using the `output` option. The parser could be updated to generate a value for the output option. @@ -17,15 +18,9 @@ defmodule Pinchflat.Downloader.Backends.YtDlp.Video do newer users can use the easier liquid-like syntax. """ def download(url, command_opts \\ []) do - default_output_path = Path.join([base_directory(), "%(id)s", "%(id)s.%(ext)s"]) - default_opts = [output: default_output_path] - opts = Keyword.merge(default_opts, command_opts) - - backend_runner().run(url, opts) - end - - defp base_directory do - Application.get_env(:pinchflat, :media_directory) + # TODO: if this stays this simple, consider not abstracting it + # HOWEVER - this module does provide clarity of intent so maybe keep? + backend_runner().run(url, command_opts) end defp backend_runner do diff --git a/lib/pinchflat/downloader/video_downloader.ex b/lib/pinchflat/downloader/video_downloader.ex new file mode 100644 index 00000000..aeda97a3 --- /dev/null +++ b/lib/pinchflat/downloader/video_downloader.ex @@ -0,0 +1,39 @@ +defmodule Pinchflat.Downloader.VideoDownloader do + @moduledoc """ + This is the integration layer for actually downloading videos. + It takes into account the media profile's settings in order + to download the video with the desired options. + + Technically hardcodes the yt-dlp backend for now, but should leave + it open-ish for future expansion (just in case). + """ + + alias Pinchflat.Downloader.Backends.YtDlp.Video, as: YtDlpVideo + alias Pinchflat.Profiles.Options.YtDlp.OptionBuilder, as: YtDlpOptionBuilder + + @doc """ + Downloads a single video based on the settings in the given media profile. + + # TODO: implement media profiles - so far this is a glorified mock + # TODO: test + """ + def download_for_media_profile(url, media_profile, backend \\ :yt_dlp) do + option_builder = option_builder(backend) + video_backend = video_backend(backend) + {:ok, options} = option_builder.build(media_profile) + + video_backend.download(url, options) + end + + def option_builder(backend) do + case backend do + :yt_dlp -> YtDlpOptionBuilder + end + end + + def video_backend(backend) do + case backend do + :yt_dlp -> YtDlpVideo + end + end +end diff --git a/lib/pinchflat/profiles/options/yt_dlp/option_builder.ex b/lib/pinchflat/profiles/options/yt_dlp/option_builder.ex new file mode 100644 index 00000000..02ca39f1 --- /dev/null +++ b/lib/pinchflat/profiles/options/yt_dlp/option_builder.ex @@ -0,0 +1,43 @@ +defmodule Pinchflat.Profiles.Options.YtDlp.OptionBuilder do + @moduledoc """ + Builds the options for yt-dlp based on the given media profile. + + TODO: probably make this a behaviour so I can add other backends later + """ + + alias Pinchflat.Profiles.Options.YtDlp.OutputPathBuilder + + @doc """ + Builds the options for yt-dlp based on the given media profile. + + TODO: add a guard to ensure argument is a media profile + TODO: consider adding the ability to pass in a second argument to override + these options + TODO: test + """ + def build(media_profile) do + {:ok, output_path} = OutputPathBuilder.build(media_profile.output_path_template) + + # NOTE: I'll be hardcoding most things for now (esp. options to help me test) - + # add more configuration later as I build out the models. Walk before you can run! + + {:ok, + [ + :write_thumbnail, + :write_subs, + :embed_metadata, + :embed_thumbnail, + :embed_subs, + :write_info_json, + :write_auto_subs, + :no_progress, + convert_thumbnails: "jpg", + sub_langs: "en.*", + output: Path.join(base_directory(), output_path) + ]} + end + + defp base_directory do + Application.get_env(:pinchflat, :media_directory) + end +end diff --git a/lib/pinchflat/profiles/options/yt_dlp/output_path_builder.ex b/lib/pinchflat/profiles/options/yt_dlp/output_path_builder.ex new file mode 100644 index 00000000..cd3bcb36 --- /dev/null +++ b/lib/pinchflat/profiles/options/yt_dlp/output_path_builder.ex @@ -0,0 +1,71 @@ +defmodule Pinchflat.Profiles.Options.YtDlp.OutputPathBuilder do + @moduledoc """ + Builds yt-dlp-friendly output paths for downloaded media + + TODO: probably make this a behaviour so I can add other backends later + """ + + alias Pinchflat.RenderedString.Parser, as: TemplateParser + + @doc """ + Builds the actual final filepath from a given template. + + Translates liquid-style templates into yt-dlp-style templates, + leaving yt-dlp syntax intact. + + TODO: test + """ + def build(template_string) do + TemplateParser.parse(template_string, full_yt_dlp_options_map()) + end + + defp full_yt_dlp_options_map do + Map.merge( + standard_yt_dlp_option_map(), + custom_yt_dlp_option_map() + ) + end + + defp standard_yt_dlp_option_map do + %{ + "id" => "%(id)s", + "ext" => "%(ext)s", + "title" => "%(title)s", + "fulltitle" => "%(fulltitle)s", + "uploader" => "%(uploader)s", + "creator" => "%(creator)s", + "upload_date" => "%(upload_date)s", + "release_date" => "%(release_date)s", + "duration" => "%(duration)s", + # For videos classified as an episode of a series: + "series" => "%(series)s", + "season" => "%(season)s", + "season_number" => "%(season_number)s", + "episode" => "%(episode)s", + "episode_number" => "%(episode_number)s", + "episode_id" => "%(episode_id)s", + # For videos classified as music: + "track" => "%(track)s", + "track_number" => "%(track_number)s", + "artist" => "%(artist)s", + "album" => "%(album)s", + "album_type" => "%(album_type)s", + "genre" => "%(genre)s" + } + end + + defp custom_yt_dlp_option_map do + %{ + # Filepath-safe versions of some standard options + "safe_id" => "%(id)S", + "safe_title" => "%(title)S", + "safe_fulltitle" => "%(fulltitle)S", + "safe_uploader" => "%(uploader)S", + "safe_creator" => "%(creator)S", + # Individual parts of the upload date + "upload_year" => "%(upload_date>%Y)s", + "upload_month" => "%(upload_date>%m)s", + "upload_day" => "%(upload_date>%d)s" + } + end +end