mirror of
https://github.com/kieraneglin/pinchflat.git
synced 2026-01-23 02:24:24 +00:00
[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:
parent
bfb27427ce
commit
652fcccb4a
10 changed files with 210 additions and 66 deletions
|
|
@ -4,10 +4,10 @@ defmodule Pinchflat.Downloading.DownloadOptionBuilder do
|
|||
"""
|
||||
|
||||
alias Pinchflat.Sources
|
||||
alias Pinchflat.Settings
|
||||
alias Pinchflat.Sources.Source
|
||||
alias Pinchflat.Media.MediaItem
|
||||
alias Pinchflat.Downloading.OutputPathBuilder
|
||||
alias Pinchflat.Downloading.QualityOptionBuilder
|
||||
|
||||
alias Pinchflat.Utils.FilesystemUtils, as: FSUtils
|
||||
|
||||
|
|
@ -142,27 +142,7 @@ defmodule Pinchflat.Downloading.DownloadOptionBuilder do
|
|||
end
|
||||
|
||||
defp quality_options(media_profile) do
|
||||
vcodec = Settings.get!(:video_codec_preference)
|
||||
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
|
||||
QualityOptionBuilder.build(media_profile)
|
||||
end
|
||||
|
||||
defp sponsorblock_options(media_profile) do
|
||||
|
|
|
|||
66
lib/pinchflat/downloading/quality_option_builder.ex
Normal file
66
lib/pinchflat/downloading/quality_option_builder.ex
Normal file
|
|
@ -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
|
||||
shorts_behaviour
|
||||
livestream_behaviour
|
||||
audio_track
|
||||
preferred_resolution
|
||||
media_container
|
||||
redownload_delay_days
|
||||
|
|
@ -65,6 +66,7 @@ defmodule Pinchflat.Profiles.MediaProfile do
|
|||
# See `build_format_clauses` in the Media context for more.
|
||||
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 :audio_track, :string
|
||||
field :preferred_resolution, Ecto.Enum, values: ~w(4320p 2160p 1080p 720p 480p 360p audio)a, default: :"1080p"
|
||||
field :media_container, :string, default: nil
|
||||
|
||||
|
|
|
|||
|
|
@ -1,13 +1,13 @@
|
|||
<%= if media_type(@media_item) == :video do %>
|
||||
<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.
|
||||
</video>
|
||||
<% end %>
|
||||
|
||||
<%= if media_type(@media_item) == :audio do %>
|
||||
<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.
|
||||
</audio>
|
||||
<% end %>
|
||||
|
|
|
|||
|
|
@ -39,7 +39,10 @@
|
|||
<span class="mx-2">or</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
|
||||
</.subtle_link>
|
||||
</span>
|
||||
|
|
|
|||
|
|
@ -125,6 +125,16 @@
|
|||
/>
|
||||
</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">
|
||||
Thumbnail Options
|
||||
</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.Profiles
|
||||
alias Pinchflat.Settings
|
||||
alias Pinchflat.Utils.FilesystemUtils
|
||||
alias Pinchflat.Downloading.DownloadOptionBuilder
|
||||
|
||||
|
|
@ -253,21 +252,14 @@ defmodule Pinchflat.Downloading.DownloadOptionBuilderTest do
|
|||
end
|
||||
|
||||
describe "build/1 when testing media quality and format options" do
|
||||
test "includes quality options" do
|
||||
resolutions = ["360", "480", "720", "1080", "2160", "4320"]
|
||||
# There are more tests inside QualityOptionBuilderTest
|
||||
# This is essenitally just testing that we implement that module correctly
|
||||
|
||||
Enum.each(resolutions, fn resolution ->
|
||||
resolution_atom = String.to_existing_atom(resolution <> "p")
|
||||
test "includes video options for video profiles", %{media_item: media_item} do
|
||||
assert {:ok, res} = DownloadOptionBuilder.build(media_item)
|
||||
|
||||
media_profile = media_profile_fixture(%{preferred_resolution: resolution_atom})
|
||||
source = source_fixture(%{media_profile_id: media_profile.id})
|
||||
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)
|
||||
assert {:format_sort, "res:1080,+codec:avc:m4a"} in res
|
||||
assert {:remux_video, "mp4"} in res
|
||||
end
|
||||
|
||||
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
|
||||
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
|
||||
|
||||
describe "build/1 when testing sponsorblock options" do
|
||||
|
|
|
|||
109
test/pinchflat/downloading/quality_option_builder_test.exs
Normal file
109
test/pinchflat/downloading/quality_option_builder_test.exs
Normal file
|
|
@ -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
|
||||
Loading…
Add table
Add a link
Reference in a new issue