From df62c54d6169e39857aed7c1b126aa43166b8ec4 Mon Sep 17 00:00:00 2001 From: Kieran Eglin Date: Tue, 21 May 2024 15:04:26 -0700 Subject: [PATCH] Hooked up the backend portion of the codec preference work --- lib/pinchflat/downloading/codec_parser.ex | 53 +++++++++++--- .../downloading/download_option_builder.ex | 45 +++++------- lib/pinchflat/settings/setting.ex | 1 - .../media_items/media_item_html.ex | 3 +- .../settings/setting_controller.ex | 1 - .../downloading/codec_parser_test.exs | 70 +++++++++++++++++++ .../download_option_builder_test.exs | 23 ++++-- test/pinchflat/settings_test.exs | 28 ++++++++ 8 files changed, 177 insertions(+), 47 deletions(-) create mode 100644 test/pinchflat/downloading/codec_parser_test.exs diff --git a/lib/pinchflat/downloading/codec_parser.ex b/lib/pinchflat/downloading/codec_parser.ex index ff4d68b..a1a19bf 100644 --- a/lib/pinchflat/downloading/codec_parser.ex +++ b/lib/pinchflat/downloading/codec_parser.ex @@ -1,5 +1,35 @@ defmodule Pinchflat.Downloading.CodecParser do - # TODO: test + @moduledoc """ + Functions for generating yt-dlp codec strings + """ + + alias Pinchflat.Settings + + @doc """ + Generate a video codec string based on the value of the video_codec_preference setting. + + Returns binary() + """ + def generate_vcodec_string_from_settings do + generate_vcodec_string(Settings.get!(:video_codec_preference)) + end + + @doc """ + Generate an audio codec string based on the value of the audio_codec_preference setting. + + Returns binary() + """ + def generate_acodec_string_from_settings do + generate_acodec_string(Settings.get!(:audio_codec_preference)) + end + + @doc """ + Generate a video codec string from a list of video codecs. + + If the list is nil or empty, the default video codec is AVC. + + Returns binary() + """ def generate_vcodec_string(nil), do: "bestvideo[vcodec~='^avc']/bestvideo" def generate_vcodec_string([]), do: generate_vcodec_string(nil) @@ -8,12 +38,18 @@ defmodule Pinchflat.Downloading.CodecParser do |> Enum.map(&video_codec_map()[&1]) |> Enum.reject(&is_nil/1) |> Enum.map(&"bestvideo[vcodec~='^#{&1}']") - |> Enum.concat(["bestvideo", "best"]) + |> Enum.concat(["bestvideo"]) |> Enum.join("/") end - # TODO: test - def generate_acodec_string(nil), do: "bestaudio[acodec~='^mp4a']/bestaudio[acodec~='^mp3']/bestaudio" + @doc """ + Generate an audio codec string from a list of audio codecs. + + If the list is nil or empty, the default audio codec is MP4A. + + Returns binary() + """ + def generate_acodec_string(nil), do: "bestaudio[acodec~='^mp4a']/bestaudio" def generate_acodec_string([]), do: generate_acodec_string(nil) def generate_acodec_string(audio_codecs) do @@ -21,7 +57,7 @@ defmodule Pinchflat.Downloading.CodecParser do |> Enum.map(&audio_codec_map()[&1]) |> Enum.reject(&is_nil/1) |> Enum.map(&"bestaudio[acodec~='^#{&1}']") - |> Enum.concat(["bestaudio", "best"]) + |> Enum.concat(["bestaudio"]) |> Enum.join("/") end @@ -35,15 +71,10 @@ defmodule Pinchflat.Downloading.CodecParser do defp audio_codec_map do %{ - "flac" => "flac", - "alac" => "alac", - "wav" => "wav", - "aiff" => "aiff", "aac" => "aac", "mp4a" => "mp4a", "mp3" => "mp3", - "opus" => "opus", - "vorbis" => "vorbis" + "opus" => "opus" } end end diff --git a/lib/pinchflat/downloading/download_option_builder.ex b/lib/pinchflat/downloading/download_option_builder.ex index ec00616..d508b26 100644 --- a/lib/pinchflat/downloading/download_option_builder.ex +++ b/lib/pinchflat/downloading/download_option_builder.ex @@ -121,38 +121,27 @@ defmodule Pinchflat.Downloading.DownloadOptionBuilder do end) end - # TODO: test defp quality_options(media_profile) do - vcodec_string = CodecParser.generate_vcodec_string(["vp9", "av01"]) - acodec_string = CodecParser.generate_acodec_string(["opus", "aac", "mp4a"]) - - video_codec_option = fn res -> - [ - format_sort: "res:#{res}", - # Since Plex doesn't support reading metadata from MKV - remux_video: "mp4", - format: "((#{vcodec_string})+(#{acodec_string}))/best" - ] - end - - # audio_format_precedence = [ - # "bestaudio[ext=m4a]", - # "bestaudio[ext=mp3]", - # "bestaudio", - # "best[ext=m4a]", - # "best[ext=mp3]", - # "best" - # ] + vcodec_string = CodecParser.generate_vcodec_string_from_settings() + acodec_string = CodecParser.generate_acodec_string_from_settings() case media_profile.preferred_resolution do # Also be aware that :audio disabled all embedding options for subtitles - :audio -> [:extract_audio, format: "#{acodec_string}/best"] - :"360p" -> video_codec_option.("360") - :"480p" -> video_codec_option.("480") - :"720p" -> video_codec_option.("720") - :"1080p" -> video_codec_option.("1080") - :"2160p" -> video_codec_option.("2160") - :"4320p" -> video_codec_option.("4320") + :audio -> + [:extract_audio, format: "#{acodec_string}/best"] + + resolution_atom -> + {resolution_string, _} = + resolution_atom + |> Atom.to_string() + |> Integer.parse() + + [ + format_sort: "res:#{resolution_string}", + # Since Plex doesn't support reading metadata from MKV + remux_video: "mp4", + format: "((#{vcodec_string})+(#{acodec_string}))/best" + ] end end diff --git a/lib/pinchflat/settings/setting.ex b/lib/pinchflat/settings/setting.ex index 13b0427..25ac94b 100644 --- a/lib/pinchflat/settings/setting.ex +++ b/lib/pinchflat/settings/setting.ex @@ -44,7 +44,6 @@ defmodule Pinchflat.Settings.Setting do |> validate_required(@required_fields) end - # TODO: test defp convert_codec_preference_strings(changeset) do fields = [ video_codec_preference_string: :video_codec_preference, diff --git a/lib/pinchflat_web/controllers/media_items/media_item_html.ex b/lib/pinchflat_web/controllers/media_items/media_item_html.ex index 672a454..d0639e6 100644 --- a/lib/pinchflat_web/controllers/media_items/media_item_html.ex +++ b/lib/pinchflat_web/controllers/media_items/media_item_html.ex @@ -15,11 +15,10 @@ defmodule PinchflatWeb.MediaItems.MediaItemHTML do !!media_item.media_filepath and File.exists?(media_item.media_filepath) end - # TODO: update for new format types def media_type(media_item) do case Path.extname(media_item.media_filepath) do ext when ext in [".mp4", ".webm", ".mkv"] -> :video - ext when ext in [".mp3", ".m4a"] -> :audio + ext when ext in [".mp3", ".m4a", ".opus"] -> :audio _ -> :unknown end end diff --git a/lib/pinchflat_web/controllers/settings/setting_controller.ex b/lib/pinchflat_web/controllers/settings/setting_controller.ex index 6ae80f1..5e009c1 100644 --- a/lib/pinchflat_web/controllers/settings/setting_controller.ex +++ b/lib/pinchflat_web/controllers/settings/setting_controller.ex @@ -12,7 +12,6 @@ defmodule PinchflatWeb.Settings.SettingController do def update(conn, %{"setting" => setting_params}) do setting = Settings.record() - IO.inspect(setting_params) case Settings.update_setting(setting, setting_params) do {:ok, _} -> diff --git a/test/pinchflat/downloading/codec_parser_test.exs b/test/pinchflat/downloading/codec_parser_test.exs new file mode 100644 index 0000000..c7b856a --- /dev/null +++ b/test/pinchflat/downloading/codec_parser_test.exs @@ -0,0 +1,70 @@ +defmodule Pinchflat.Downloading.CodecParserTest do + use Pinchflat.DataCase + + alias Pinchflat.Settings + alias Pinchflat.Downloading.CodecParser + + describe "generate_vcodec_string_from_settings/1" do + test "returns a default vcodec string when setting isn't set" do + Settings.set(video_codec_preference: []) + + assert "bestvideo[vcodec~='^avc']/bestvideo" == CodecParser.generate_vcodec_string_from_settings() + end + + test "generates a vcodec string" do + Settings.set(video_codec_preference: ["av01"]) + + assert "bestvideo[vcodec~='^av01']/bestvideo" == CodecParser.generate_vcodec_string_from_settings() + end + end + + describe "generate_acodec_string_from_settings/1" do + test "returns a default acodec string when setting isn't set" do + Settings.set(audio_codec_preference: []) + + assert "bestaudio[acodec~='^mp4a']/bestaudio" == CodecParser.generate_acodec_string_from_settings() + end + + test "generates an acodec string" do + Settings.set(audio_codec_preference: ["mp3"]) + + assert "bestaudio[acodec~='^mp3']/bestaudio" == CodecParser.generate_acodec_string_from_settings() + end + end + + describe "generate_vcodec_string/1" do + test "returns a default vcodec string when nil" do + assert "bestvideo[vcodec~='^avc']/bestvideo" == CodecParser.generate_vcodec_string(nil) + end + + test "returns a default vcodec string when empty" do + assert "bestvideo[vcodec~='^avc']/bestvideo" == CodecParser.generate_vcodec_string([]) + end + + test "generates a vcodec string" do + assert "bestvideo[vcodec~='^av01']/bestvideo" == CodecParser.generate_vcodec_string(["av01"]) + end + + test "ignores options that don't exist" do + assert "bestvideo[vcodec~='^av01']/bestvideo" == CodecParser.generate_vcodec_string(["av01", "foo"]) + end + end + + describe "generate_acodec_string/1" do + test "returns a default acodec string when nil" do + assert "bestaudio[acodec~='^mp4a']/bestaudio" == CodecParser.generate_acodec_string(nil) + end + + test "returns a default acodec string when empty" do + assert "bestaudio[acodec~='^mp4a']/bestaudio" == CodecParser.generate_acodec_string([]) + end + + test "generates an acodec string" do + assert "bestaudio[acodec~='^mp3']/bestaudio" == CodecParser.generate_acodec_string(["mp3"]) + end + + test "ignores options that don't exist" do + assert "bestaudio[acodec~='^mp3']/bestaudio" == CodecParser.generate_acodec_string(["mp3", "foo"]) + end + end +end diff --git a/test/pinchflat/downloading/download_option_builder_test.exs b/test/pinchflat/downloading/download_option_builder_test.exs index 735e72d..a22e11c 100644 --- a/test/pinchflat/downloading/download_option_builder_test.exs +++ b/test/pinchflat/downloading/download_option_builder_test.exs @@ -6,6 +6,7 @@ defmodule Pinchflat.Downloading.DownloadOptionBuilderTest do alias Pinchflat.Sources alias Pinchflat.Profiles + alias Pinchflat.Settings alias Pinchflat.Utils.FilesystemUtils alias Pinchflat.Downloading.DownloadOptionBuilder @@ -244,7 +245,7 @@ defmodule Pinchflat.Downloading.DownloadOptionBuilderTest do end describe "build/1 when testing quality options" do - test "it includes quality options" do + test "includes quality options" do resolutions = ["360", "480", "720", "1080", "2160", "4320"] Enum.each(resolutions, fn resolution -> @@ -255,21 +256,35 @@ defmodule Pinchflat.Downloading.DownloadOptionBuilderTest do media_item = Repo.preload(media_item_fixture(source_id: source.id), source: :media_profile) assert {:ok, res} = DownloadOptionBuilder.build(media_item) - assert {:format_sort, "res:#{resolution},+codec:avc:m4a"} in res + assert {:format_sort, "res:#{resolution}"} in res + + assert {:format, "((bestvideo[vcodec~='^avc']/bestvideo)+(bestaudio[acodec~='^mp4a']/bestaudio))/best"} in res + assert {:remux_video, "mp4"} in res end) end - test "it includes quality options for audio only", %{media_item: media_item} do + test "includes quality options for audio only", %{media_item: media_item} do media_item = update_media_profile_attribute(media_item, %{preferred_resolution: :audio}) assert {:ok, res} = DownloadOptionBuilder.build(media_item) assert :extract_audio in res - assert {:format, "bestaudio[ext=m4a]/bestaudio[ext=mp3]/bestaudio/best[ext=m4a]/best[ext=mp3]/best"} in res + assert {:format, "bestaudio[acodec~='^mp4a']/bestaudio/best"} in res refute {:remux_video, "mp4"} in res end + + test "includes custom quality options if specified", %{media_item: media_item} do + Settings.set(video_codec_preference: ["av01"]) + Settings.set(audio_codec_preference: ["aac"]) + + media_item = update_media_profile_attribute(media_item, %{preferred_resolution: :"1080p"}) + + assert {:ok, res} = DownloadOptionBuilder.build(media_item) + + assert {:format, "((bestvideo[vcodec~='^av01']/bestvideo)+(bestaudio[acodec~='^aac']/bestaudio))/best"} in res + end end describe "build/1 when testing sponsorblock options" do diff --git a/test/pinchflat/settings_test.exs b/test/pinchflat/settings_test.exs index 944eaf1..ceacd1f 100644 --- a/test/pinchflat/settings_test.exs +++ b/test/pinchflat/settings_test.exs @@ -77,5 +77,33 @@ defmodule Pinchflat.SettingsTest do assert %Ecto.Changeset{} = Settings.change_setting(setting, %{onboarding: true}) end + + test "converts (video|audio)_codec_preference_string to an array" do + setting = Settings.record() + + new_setting = %{ + video_codec_preference_string: "avc>vp9", + audio_codec_preference_string: "aac>opus" + } + + changeset = Settings.change_setting(setting, new_setting) + + assert ["avc", "vp9"] = changeset.changes.video_codec_preference + assert ["aac", "opus"] = changeset.changes.audio_codec_preference + end + end + + test "removes whitespace from (video|audio)_codec_preference" do + setting = Settings.record() + + new_setting = %{ + video_codec_preference_string: " avc > > vp9 ", + audio_codec_preference_string: "aac> opus " + } + + changeset = Settings.change_setting(setting, new_setting) + + assert ["avc", "vp9"] = changeset.changes.video_codec_preference + assert ["aac", "opus"] = changeset.changes.audio_codec_preference end end