[Enhancement] Add audio track language selection to Media Profile (#487)
* Moved quality options to their own module * Added language and format selection to quality option builder * [WIP] migrating tests * Added audio_lang to media_profile table * Renamed column; added format options and tests * Adds UI for audio_track to the media profile form * Adds a version string to in-app streams to help with cache busting
This commit is contained in:
@@ -4,10 +4,10 @@ defmodule Pinchflat.Downloading.DownloadOptionBuilder do
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
alias Pinchflat.Sources
|
alias Pinchflat.Sources
|
||||||
alias Pinchflat.Settings
|
|
||||||
alias Pinchflat.Sources.Source
|
alias Pinchflat.Sources.Source
|
||||||
alias Pinchflat.Media.MediaItem
|
alias Pinchflat.Media.MediaItem
|
||||||
alias Pinchflat.Downloading.OutputPathBuilder
|
alias Pinchflat.Downloading.OutputPathBuilder
|
||||||
|
alias Pinchflat.Downloading.QualityOptionBuilder
|
||||||
|
|
||||||
alias Pinchflat.Utils.FilesystemUtils, as: FSUtils
|
alias Pinchflat.Utils.FilesystemUtils, as: FSUtils
|
||||||
|
|
||||||
@@ -142,27 +142,7 @@ defmodule Pinchflat.Downloading.DownloadOptionBuilder do
|
|||||||
end
|
end
|
||||||
|
|
||||||
defp quality_options(media_profile) do
|
defp quality_options(media_profile) do
|
||||||
vcodec = Settings.get!(:video_codec_preference)
|
QualityOptionBuilder.build(media_profile)
|
||||||
acodec = Settings.get!(:audio_codec_preference)
|
|
||||||
container = media_profile.media_container
|
|
||||||
|
|
||||||
case media_profile.preferred_resolution do
|
|
||||||
# Also be aware that :audio disabled all embedding options for subtitles
|
|
||||||
:audio ->
|
|
||||||
[:extract_audio, format_sort: "+acodec:#{acodec}", audio_format: container || "best"]
|
|
||||||
|
|
||||||
resolution_atom ->
|
|
||||||
{resolution_string, _} =
|
|
||||||
resolution_atom
|
|
||||||
|> Atom.to_string()
|
|
||||||
|> Integer.parse()
|
|
||||||
|
|
||||||
[
|
|
||||||
# Since Plex doesn't support reading metadata from MKV
|
|
||||||
remux_video: container || "mp4",
|
|
||||||
format_sort: "res:#{resolution_string},+codec:#{vcodec}:#{acodec}"
|
|
||||||
]
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
defp sponsorblock_options(media_profile) do
|
defp sponsorblock_options(media_profile) do
|
||||||
|
|||||||
@@ -0,0 +1,66 @@
|
|||||||
|
defmodule Pinchflat.Downloading.QualityOptionBuilder do
|
||||||
|
@moduledoc """
|
||||||
|
A standalone builder module for building quality-related options for yt-dlp to download media.
|
||||||
|
|
||||||
|
Currently exclusively used in DownloadOptionBuilder since this logic is too complex to just
|
||||||
|
place in the main module.
|
||||||
|
"""
|
||||||
|
|
||||||
|
alias Pinchflat.Settings
|
||||||
|
alias Pinchflat.Profiles.MediaProfile
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Builds the quality-related options for yt-dlp to download media based on the given media profile
|
||||||
|
|
||||||
|
Includes things like container, preferred format/codec, and audio track options.
|
||||||
|
"""
|
||||||
|
def build(%MediaProfile{preferred_resolution: :audio, media_container: container} = media_profile) do
|
||||||
|
acodec = Settings.get!(:audio_codec_preference)
|
||||||
|
|
||||||
|
[
|
||||||
|
:extract_audio,
|
||||||
|
format_sort: "+acodec:#{acodec}",
|
||||||
|
audio_format: container || "best",
|
||||||
|
format: build_format_string(media_profile)
|
||||||
|
]
|
||||||
|
end
|
||||||
|
|
||||||
|
def build(%MediaProfile{preferred_resolution: resolution_atom, media_container: container} = media_profile) do
|
||||||
|
vcodec = Settings.get!(:video_codec_preference)
|
||||||
|
acodec = Settings.get!(:audio_codec_preference)
|
||||||
|
{resolution_string, _} = resolution_atom |> Atom.to_string() |> Integer.parse()
|
||||||
|
|
||||||
|
[
|
||||||
|
# Since Plex doesn't support reading metadata from MKV
|
||||||
|
remux_video: container || "mp4",
|
||||||
|
format_sort: "res:#{resolution_string},+codec:#{vcodec}:#{acodec}",
|
||||||
|
format: build_format_string(media_profile)
|
||||||
|
]
|
||||||
|
end
|
||||||
|
|
||||||
|
defp build_format_string(%MediaProfile{preferred_resolution: :audio, audio_track: audio_track}) do
|
||||||
|
if audio_track do
|
||||||
|
"bestaudio[#{build_format_modifier(audio_track)}]/bestaudio/best"
|
||||||
|
else
|
||||||
|
"bestaudio/best"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp build_format_string(%MediaProfile{audio_track: audio_track}) do
|
||||||
|
if audio_track do
|
||||||
|
"bestvideo+bestaudio[#{build_format_modifier(audio_track)}]/bestvideo*+bestaudio/best"
|
||||||
|
else
|
||||||
|
"bestvideo*+bestaudio/best"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# Reminder to self: this conflicts with `--extractor-args "youtube:lang=<LANG>"`
|
||||||
|
# since that will translate the format_notes as well, which means they may not match.
|
||||||
|
# At least that's what happens now - worth a re-check if I have to come back to this
|
||||||
|
defp build_format_modifier("original"), do: "format_note*=original"
|
||||||
|
defp build_format_modifier("default"), do: "format_note*='(default)'"
|
||||||
|
# This uses the carat to anchor the language to the beginning of the string
|
||||||
|
# since that's what's needed to match `en` to `en-US` and `en-GB`, etc. The user
|
||||||
|
# can always specify the full language code if they want.
|
||||||
|
defp build_format_modifier(language_code), do: "language^=#{language_code}"
|
||||||
|
end
|
||||||
@@ -26,6 +26,7 @@ defmodule Pinchflat.Profiles.MediaProfile do
|
|||||||
sponsorblock_categories
|
sponsorblock_categories
|
||||||
shorts_behaviour
|
shorts_behaviour
|
||||||
livestream_behaviour
|
livestream_behaviour
|
||||||
|
audio_track
|
||||||
preferred_resolution
|
preferred_resolution
|
||||||
media_container
|
media_container
|
||||||
redownload_delay_days
|
redownload_delay_days
|
||||||
@@ -65,6 +66,7 @@ defmodule Pinchflat.Profiles.MediaProfile do
|
|||||||
# See `build_format_clauses` in the Media context for more.
|
# See `build_format_clauses` in the Media context for more.
|
||||||
field :shorts_behaviour, Ecto.Enum, values: ~w(include exclude only)a, default: :include
|
field :shorts_behaviour, Ecto.Enum, values: ~w(include exclude only)a, default: :include
|
||||||
field :livestream_behaviour, Ecto.Enum, values: ~w(include exclude only)a, default: :include
|
field :livestream_behaviour, Ecto.Enum, values: ~w(include exclude only)a, default: :include
|
||||||
|
field :audio_track, :string
|
||||||
field :preferred_resolution, Ecto.Enum, values: ~w(4320p 2160p 1080p 720p 480p 360p audio)a, default: :"1080p"
|
field :preferred_resolution, Ecto.Enum, values: ~w(4320p 2160p 1080p 720p 480p 360p audio)a, default: :"1080p"
|
||||||
field :media_container, :string, default: nil
|
field :media_container, :string, default: nil
|
||||||
|
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
<%= if media_type(@media_item) == :video do %>
|
<%= if media_type(@media_item) == :video do %>
|
||||||
<video controls class="max-h-128 w-full">
|
<video controls class="max-h-128 w-full">
|
||||||
<source src={~p"/media/#{@media_item.uuid}/stream"} type="video/mp4" />
|
<source src={~p"/media/#{@media_item.uuid}/stream?v=#{DateTime.to_unix(@media_item.updated_at)}"} type="video/mp4" />
|
||||||
Your browser does not support the video element.
|
Your browser does not support the video element.
|
||||||
</video>
|
</video>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|
||||||
<%= if media_type(@media_item) == :audio do %>
|
<%= if media_type(@media_item) == :audio do %>
|
||||||
<audio controls class="w-full">
|
<audio controls class="w-full">
|
||||||
<source src={~p"/media/#{@media_item.uuid}/stream"} type="audio/mpeg" />
|
<source src={~p"/media/#{@media_item.uuid}/stream?v=#{DateTime.to_unix(@media_item.updated_at)}"} type="audio/mpeg" />
|
||||||
Your browser does not support the audio element.
|
Your browser does not support the audio element.
|
||||||
</audio>
|
</audio>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|||||||
@@ -39,7 +39,10 @@
|
|||||||
<span class="mx-2">or</span>
|
<span class="mx-2">or</span>
|
||||||
</span>
|
</span>
|
||||||
<span>
|
<span>
|
||||||
<.subtle_link href={~p"/media/#{@media_item.uuid}/stream"} target="_blank">
|
<.subtle_link
|
||||||
|
href={~p"/media/#{@media_item.uuid}/stream?v=#{DateTime.to_unix(@media_item.updated_at)}"}
|
||||||
|
target="_blank"
|
||||||
|
>
|
||||||
Open Local Stream
|
Open Local Stream
|
||||||
</.subtle_link>
|
</.subtle_link>
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
+10
@@ -125,6 +125,16 @@
|
|||||||
/>
|
/>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
<section x-show="advancedMode">
|
||||||
|
<.input
|
||||||
|
field={f[:audio_track]}
|
||||||
|
placeholder="de"
|
||||||
|
type="text"
|
||||||
|
label="Audio Track Language"
|
||||||
|
help="Only works if there are multiple audio tracks. Use either a language code, 'original' for the original audio track, or 'default' for YouTube's preference. Or just leave it blank"
|
||||||
|
/>
|
||||||
|
</section>
|
||||||
|
|
||||||
<h3 class="mt-10 text-2xl text-black dark:text-white">
|
<h3 class="mt-10 text-2xl text-black dark:text-white">
|
||||||
Thumbnail Options
|
Thumbnail Options
|
||||||
</h3>
|
</h3>
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 434 KiB After Width: | Height: | Size: 438 KiB |
@@ -0,0 +1,9 @@
|
|||||||
|
defmodule Pinchflat.Repo.Migrations.AddAudioLangToMediaProfiles do
|
||||||
|
use Ecto.Migration
|
||||||
|
|
||||||
|
def change do
|
||||||
|
alter table(:media_profiles) do
|
||||||
|
add :audio_track, :string
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -6,7 +6,6 @@ defmodule Pinchflat.Downloading.DownloadOptionBuilderTest do
|
|||||||
|
|
||||||
alias Pinchflat.Sources
|
alias Pinchflat.Sources
|
||||||
alias Pinchflat.Profiles
|
alias Pinchflat.Profiles
|
||||||
alias Pinchflat.Settings
|
|
||||||
alias Pinchflat.Utils.FilesystemUtils
|
alias Pinchflat.Utils.FilesystemUtils
|
||||||
alias Pinchflat.Downloading.DownloadOptionBuilder
|
alias Pinchflat.Downloading.DownloadOptionBuilder
|
||||||
|
|
||||||
@@ -253,21 +252,14 @@ defmodule Pinchflat.Downloading.DownloadOptionBuilderTest do
|
|||||||
end
|
end
|
||||||
|
|
||||||
describe "build/1 when testing media quality and format options" do
|
describe "build/1 when testing media quality and format options" do
|
||||||
test "includes quality options" do
|
# There are more tests inside QualityOptionBuilderTest
|
||||||
resolutions = ["360", "480", "720", "1080", "2160", "4320"]
|
# This is essenitally just testing that we implement that module correctly
|
||||||
|
|
||||||
Enum.each(resolutions, fn resolution ->
|
test "includes video options for video profiles", %{media_item: media_item} do
|
||||||
resolution_atom = String.to_existing_atom(resolution <> "p")
|
assert {:ok, res} = DownloadOptionBuilder.build(media_item)
|
||||||
|
|
||||||
media_profile = media_profile_fixture(%{preferred_resolution: resolution_atom})
|
assert {:format_sort, "res:1080,+codec:avc:m4a"} in res
|
||||||
source = source_fixture(%{media_profile_id: media_profile.id})
|
assert {:remux_video, "mp4"} in res
|
||||||
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 {:remux_video, "mp4"} in res
|
|
||||||
end)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
test "includes quality options for audio only", %{media_item: media_item} do
|
test "includes quality options for audio only", %{media_item: media_item} do
|
||||||
@@ -280,33 +272,6 @@ defmodule Pinchflat.Downloading.DownloadOptionBuilderTest do
|
|||||||
|
|
||||||
refute {:remux_video, "mp4"} in res
|
refute {:remux_video, "mp4"} in res
|
||||||
end
|
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_sort, "res:1080,+codec:av01:aac"} in res
|
|
||||||
end
|
|
||||||
|
|
||||||
test "includes custom remux target for videos if specified", %{media_item: media_item} do
|
|
||||||
media_item = update_media_profile_attribute(media_item, %{media_container: "mkv"})
|
|
||||||
|
|
||||||
assert {:ok, res} = DownloadOptionBuilder.build(media_item)
|
|
||||||
|
|
||||||
assert {:remux_video, "mkv"} in res
|
|
||||||
end
|
|
||||||
|
|
||||||
test "includes custom format target for audio if specified", %{media_item: media_item} do
|
|
||||||
media_item = update_media_profile_attribute(media_item, %{media_container: "flac", preferred_resolution: :audio})
|
|
||||||
|
|
||||||
assert {:ok, res} = DownloadOptionBuilder.build(media_item)
|
|
||||||
|
|
||||||
assert {:audio_format, "flac"} in res
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
describe "build/1 when testing sponsorblock options" do
|
describe "build/1 when testing sponsorblock options" do
|
||||||
|
|||||||
@@ -0,0 +1,109 @@
|
|||||||
|
defmodule Pinchflat.Downloading.QualityOptionBuilderTest do
|
||||||
|
use Pinchflat.DataCase
|
||||||
|
import Pinchflat.ProfilesFixtures
|
||||||
|
|
||||||
|
alias Pinchflat.Profiles
|
||||||
|
alias Pinchflat.Settings
|
||||||
|
alias Pinchflat.Downloading.QualityOptionBuilder
|
||||||
|
|
||||||
|
describe "build/1" do
|
||||||
|
test "includes format options if audio_track is set to original" do
|
||||||
|
media_profile = media_profile_fixture(%{audio_track: "original"})
|
||||||
|
|
||||||
|
assert res = QualityOptionBuilder.build(media_profile)
|
||||||
|
|
||||||
|
assert {:format, "bestvideo+bestaudio[format_note*=original]/bestvideo*+bestaudio/best"} in res
|
||||||
|
end
|
||||||
|
|
||||||
|
test "includes format options if audio_track is set to default" do
|
||||||
|
media_profile = media_profile_fixture(%{audio_track: "default"})
|
||||||
|
|
||||||
|
assert res = QualityOptionBuilder.build(media_profile)
|
||||||
|
|
||||||
|
assert {:format, "bestvideo+bestaudio[format_note*='(default)']/bestvideo*+bestaudio/best"} in res
|
||||||
|
end
|
||||||
|
|
||||||
|
test "includes format options if audio_track is set to a language code" do
|
||||||
|
media_profile = media_profile_fixture(%{audio_track: "en"})
|
||||||
|
|
||||||
|
assert res = QualityOptionBuilder.build(media_profile)
|
||||||
|
|
||||||
|
assert {:format, "bestvideo+bestaudio[language^=en]/bestvideo*+bestaudio/best"} in res
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "build/1 when testing audio profiles" do
|
||||||
|
setup do
|
||||||
|
{:ok, media_profile: media_profile_fixture(%{preferred_resolution: :audio})}
|
||||||
|
end
|
||||||
|
|
||||||
|
test "includes quality options for audio only", %{media_profile: media_profile} do
|
||||||
|
assert res = QualityOptionBuilder.build(media_profile)
|
||||||
|
|
||||||
|
assert :extract_audio in res
|
||||||
|
assert {:format_sort, "+acodec:m4a"} in res
|
||||||
|
|
||||||
|
refute {:remux_video, "mp4"} in res
|
||||||
|
end
|
||||||
|
|
||||||
|
test "includes custom format target for audio if specified", %{media_profile: media_profile} do
|
||||||
|
{:ok, media_profile} =
|
||||||
|
Profiles.update_media_profile(media_profile, %{media_container: "flac", preferred_resolution: :audio})
|
||||||
|
|
||||||
|
assert res = QualityOptionBuilder.build(media_profile)
|
||||||
|
|
||||||
|
assert {:audio_format, "flac"} in res
|
||||||
|
end
|
||||||
|
|
||||||
|
test "includes custom format options", %{media_profile: media_profile} do
|
||||||
|
assert res = QualityOptionBuilder.build(media_profile)
|
||||||
|
|
||||||
|
assert {:format, "bestaudio/best"} in res
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "build/1 when testing non-audio profiles" do
|
||||||
|
setup do
|
||||||
|
{:ok, media_profile: media_profile_fixture(%{preferred_resolution: :"480p"})}
|
||||||
|
end
|
||||||
|
|
||||||
|
test "includes quality options" do
|
||||||
|
resolutions = ["360", "480", "720", "1080", "2160", "4320"]
|
||||||
|
|
||||||
|
Enum.each(resolutions, fn resolution ->
|
||||||
|
resolution_atom = String.to_existing_atom(resolution <> "p")
|
||||||
|
media_profile = media_profile_fixture(%{preferred_resolution: resolution_atom})
|
||||||
|
|
||||||
|
assert res = QualityOptionBuilder.build(media_profile)
|
||||||
|
|
||||||
|
assert {:format_sort, "res:#{resolution},+codec:avc:m4a"} in res
|
||||||
|
assert {:remux_video, "mp4"} in res
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
|
test "includes custom quality options if specified", %{media_profile: media_profile} do
|
||||||
|
Settings.set(video_codec_preference: "av01")
|
||||||
|
Settings.set(audio_codec_preference: "aac")
|
||||||
|
|
||||||
|
{:ok, media_profile} = Profiles.update_media_profile(media_profile, %{preferred_resolution: :"1080p"})
|
||||||
|
|
||||||
|
assert res = QualityOptionBuilder.build(media_profile)
|
||||||
|
|
||||||
|
assert {:format_sort, "res:1080,+codec:av01:aac"} in res
|
||||||
|
end
|
||||||
|
|
||||||
|
test "includes custom remux target for videos if specified", %{media_profile: media_profile} do
|
||||||
|
{:ok, media_profile} = Profiles.update_media_profile(media_profile, %{media_container: "mkv"})
|
||||||
|
|
||||||
|
assert res = QualityOptionBuilder.build(media_profile)
|
||||||
|
|
||||||
|
assert {:remux_video, "mkv"} in res
|
||||||
|
end
|
||||||
|
|
||||||
|
test "includes custom format options", %{media_profile: media_profile} do
|
||||||
|
assert res = QualityOptionBuilder.build(media_profile)
|
||||||
|
|
||||||
|
assert {:format, "bestvideo*+bestaudio/best"} in res
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
Reference in New Issue
Block a user