Source attributes in output template (#45)

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

* Fixed bug where indexing job wouldn't run for the first time

* Renamed friendly_name to custom_name

* Added new options to default template and UI

* Improved explainer UI
This commit is contained in:
Kieran 2024-03-02 12:15:10 -08:00 committed by GitHub
parent 9e75092876
commit 09448e3fcc
18 changed files with 200 additions and 139 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

@ -15,7 +15,7 @@ defmodule Pinchflat.Sources.Source do
collection_name collection_name
collection_id collection_id
collection_type collection_type
friendly_name custom_name
index_frequency_minutes index_frequency_minutes
download_media download_media
last_indexed_at last_indexed_at
@ -27,7 +27,7 @@ defmodule Pinchflat.Sources.Source do
collection_name collection_name
collection_id collection_id
collection_type collection_type
friendly_name custom_name
index_frequency_minutes index_frequency_minutes
download_media download_media
original_url original_url
@ -35,7 +35,7 @@ defmodule Pinchflat.Sources.Source do
)a )a
schema "sources" do schema "sources" do
field :friendly_name, :string field :custom_name, :string
field :collection_name, :string field :collection_name, :string
field :collection_id, :string field :collection_id, :string
field :collection_type, Ecto.Enum, values: [:channel, :playlist] field :collection_type, Ecto.Enum, values: [:channel, :playlist]
@ -58,7 +58,7 @@ defmodule Pinchflat.Sources.Source do
def changeset(source, attrs) do def changeset(source, attrs) do
source source
|> cast(attrs, @allowed_fields) |> cast(attrs, @allowed_fields)
|> dynamic_default(:friendly_name, fn cs -> get_field(cs, :collection_name) end) |> dynamic_default(:custom_name, fn cs -> get_field(cs, :collection_name) end)
|> validate_required(@required_fields) |> validate_required(@required_fields)
|> unique_constraint([:collection_id, :media_profile_id]) |> unique_constraint([:collection_id, :media_profile_id])
end end

View file

@ -29,7 +29,8 @@ defmodule Pinchflat.Profiles.MediaProfile do
schema "media_profiles" do schema "media_profiles" do
field :name, :string field :name, :string
field :output_path_template, :string, default: "/{{ channel }}/{{ title }}/{{ title }} [{{ id }}].{{ ext }}" field :output_path_template, :string,
default: "/{{ source_custom_name }}/{{ title }}/{{ title }} [{{ id }}].{{ ext }}"
field :download_subs, :boolean, default: true field :download_subs, :boolean, default: true
field :download_auto_subs, :boolean, default: true field :download_auto_subs, :boolean, default: true

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_custom_name" => source.custom_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
@ -30,6 +33,9 @@ defmodule Pinchflat.Profiles.Options.YtDlp.OutputPathBuilder do
end end
end end
# This isn't the only source for custom options, since they can be passed in my the caller.
# `download_option_builder` is the most likely place for other custom options to be added,
# but if in doubt just search the codebase for `OutputPathBuilder.build`.
defp custom_yt_dlp_option_map do defp custom_yt_dlp_option_map do
%{ %{
# Individual parts of the upload date # Individual parts of the upload date

View file

@ -15,8 +15,9 @@ defmodule Pinchflat.Workers.MediaIndexingWorker do
@doc """ @doc """
The ID is that of a source _record_, not a YouTube channel/playlist ID. Indexes The ID is that of a source _record_, not a YouTube channel/playlist ID. Indexes
the provided source, kicks off downloads for each new MediaItem, and the provided source, kicks off downloads for each new MediaItem, and
reschedules the job to run again in the future (as determined by the reschedules the job to run again in the future. It will ALWAYS index a source
souce's `index_frequency_minutes` field). if it's never been indexed before, but rescheduling is determined by the
`index_frequency_minutes` field.
README: Re-scheduling here works a little different than you may expect. README: Re-scheduling here works a little different than you may expect.
The reschedule time is relative to the time the job has actually _completed_. The reschedule time is relative to the time the job has actually _completed_.
@ -39,18 +40,31 @@ defmodule Pinchflat.Workers.MediaIndexingWorker do
def perform(%Oban.Job{args: %{"id" => source_id}}) do def perform(%Oban.Job{args: %{"id" => source_id}}) do
source = Sources.get_source!(source_id) source = Sources.get_source!(source_id)
if source.index_frequency_minutes <= 0 do case {source.index_frequency_minutes, source.last_indexed_at} do
:ok {index_freq, _} when index_freq > 0 ->
else # If the indexing is on a schedule simply run indexing and reschedule
index_media_and_reschedule(source) index_media(source)
reschedule_indexing(source)
{_, nil} ->
# If the source has never been indexed, index it once
# even if it's not meant to reschedule
index_media(source)
_ ->
# If the source HAS been indexed and is not meant to reschedule,
# perform a no-op
:ok
end end
end end
defp index_media_and_reschedule(source) do defp index_media(source) do
SourceTasks.index_media_items(source) SourceTasks.index_media_items(source)
# This method handles the case where a source is set to not download media # This method handles the case where a source is set to not download media
SourceTasks.enqueue_pending_media_tasks(source) SourceTasks.enqueue_pending_media_tasks(source)
end
defp reschedule_indexing(source) do
source source
|> Map.take([:id]) |> Map.take([:id])
|> MediaIndexingWorker.new(schedule_in: source.index_frequency_minutes * 60) |> MediaIndexingWorker.new(schedule_in: source.index_frequency_minutes * 60)

View file

@ -17,7 +17,7 @@
<section> <section>
<strong>Source:</strong> <strong>Source:</strong>
<.inline_link href={~p"/sources/#{@media_item.source_id}"}> <.inline_link href={~p"/sources/#{@media_item.source_id}"}>
<%= @media_item.source.friendly_name %> <%= @media_item.source.custom_name %>
</.inline_link> </.inline_link>
</section> </section>

View file

@ -30,7 +30,13 @@ defmodule PinchflatWeb.MediaProfiles.MediaProfileHTML do
end end
def custom_output_template_options do def custom_output_template_options do
~w(upload_day upload_month upload_year)a %{
upload_day: nil,
upload_month: nil,
upload_year: nil,
source_custom_name: "the name of the sources that use this profile",
source_collection_type: "the collection type of the sources that use this profile. Either 'channel' or 'playlist'"
}
end end
def common_output_template_options do def common_output_template_options do

View file

@ -46,8 +46,8 @@
</section> </section>
<h2 class="text-xl font-bold mb-2">Template Options</h2> <h2 class="text-xl font-bold mb-2">Template Options</h2>
<section class="ml-2 md:ml-4 mb-4 max-w-prose"> <section class="ml-2 md:ml-4 mb-4">
<p> <p class="max-w-prose">
Any single-word <code class="text-sm">yt-dlp</code> Any single-word <code class="text-sm">yt-dlp</code>
option option
<.inline_link href="https://github.com/yt-dlp/yt-dlp?tab=readme-ov-file#output-template"> <.inline_link href="https://github.com/yt-dlp/yt-dlp?tab=readme-ov-file#output-template">
@ -58,8 +58,9 @@
</p> </p>
<h3 class="text-lg font-bold mb-2">Custom Aliases</h3> <h3 class="text-lg font-bold mb-2">Custom Aliases</h3>
<ul class="list-disc list-inside ml-2 md:ml-5"> <ul class="list-disc list-inside ml-2 md:ml-5">
<li :for={opt <- custom_output_template_options()}> <li :for={{k, v} <- custom_output_template_options()}>
<.inline_code>{{ <%= opt %> }}</.inline_code> <.inline_code>{{ <%= k %> }}</.inline_code>
<span :if={v}>- <%= v %></span>
</li> </li>
</ul> </ul>
<h3 class="text-lg font-bold mb-2">Common Options</h3> <h3 class="text-lg font-bold mb-2">Common Options</h3>

View file

@ -50,7 +50,7 @@
<:tab title="Sources"> <:tab title="Sources">
<.table rows={@media_profile.sources} table_class="text-black dark:text-white"> <.table rows={@media_profile.sources} table_class="text-black dark:text-white">
<:col :let={source} label="Name"> <:col :let={source} label="Name">
<%= source.friendly_name || source.collection_name %> <%= source.custom_name || source.collection_name %>
</:col> </:col>
<:col :let={source} label="Type"><%= source.collection_type %></:col> <:col :let={source} label="Type"><%= source.collection_type %></:col>
<:col :let={source} label="Should Download?"> <:col :let={source} label="Should Download?">

View file

@ -14,7 +14,7 @@
<div class="flex flex-col gap-10 min-w-max"> <div class="flex flex-col gap-10 min-w-max">
<.table rows={@sources} table_class="text-black dark:text-white"> <.table rows={@sources} table_class="text-black dark:text-white">
<:col :let={source} label="Name"> <:col :let={source} label="Name">
<%= source.friendly_name || source.collection_name %> <%= source.custom_name || source.collection_name %>
</:col> </:col>
<:col :let={source} label="Type"><%= source.collection_type %></:col> <:col :let={source} label="Type"><%= source.collection_type %></:col>
<:col :let={source} label="Should Download?"> <:col :let={source} label="Should Download?">

View file

@ -4,7 +4,7 @@
</.error> </.error>
<.input <.input
field={f[:friendly_name]} field={f[:custom_name]}
type="text" type="text"
label="Custom Name" label="Custom Name"
help="Something descriptive. Does not impact indexing or downloading" help="Something descriptive. Does not impact indexing or downloading"

View file

@ -0,0 +1,7 @@
defmodule Pinchflat.Repo.Migrations.RenameFriendlyNameToCustomName do
use Ecto.Migration
def change do
rename table(:sources), :friendly_name, to: :custom_name
end
end

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, custom_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_custom_name }}.%(ext)s"})
assert {:ok, res} = DownloadOptionBuilder.build(media_item)
assert {:output, "/tmp/test/videos/#{media_item.source.custom_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

View file

@ -66,18 +66,18 @@ defmodule Pinchflat.SourcesTest do
assert String.starts_with?(source.collection_id, "some_playlist_id_") assert String.starts_with?(source.collection_id, "some_playlist_id_")
end end
test "you can specify a custom friendly_name" do test "you can specify a custom custom_name" do
expect(YtDlpRunnerMock, :run, &channel_mock/3) expect(YtDlpRunnerMock, :run, &channel_mock/3)
valid_attrs = %{ valid_attrs = %{
media_profile_id: media_profile_fixture().id, media_profile_id: media_profile_fixture().id,
original_url: "https://www.youtube.com/channel/abc123", original_url: "https://www.youtube.com/channel/abc123",
friendly_name: "some custom name" custom_name: "some custom name"
} }
assert {:ok, %Source{} = source} = Sources.create_source(valid_attrs) assert {:ok, %Source{} = source} = Sources.create_source(valid_attrs)
assert source.friendly_name == "some custom name" assert source.custom_name == "some custom name"
end end
test "friendly name is pulled from collection_name if not specified" do test "friendly name is pulled from collection_name if not specified" do
@ -90,7 +90,7 @@ defmodule Pinchflat.SourcesTest do
assert {:ok, %Source{} = source} = Sources.create_source(valid_attrs) assert {:ok, %Source{} = source} = Sources.create_source(valid_attrs)
assert source.friendly_name == "some channel name" assert source.custom_name == "some channel name"
end end
test "collection_type is inferred from source details" do test "collection_type is inferred from source details" do

View file

@ -12,23 +12,6 @@ defmodule Pinchflat.Workers.MediaIndexingWorkerTest do
setup :verify_on_exit! setup :verify_on_exit!
describe "perform/1" do describe "perform/1" do
test "it does not do any indexing if the source shouldn't be indexed" do
expect(YtDlpRunnerMock, :run, 0, fn _url, _opts, _ot -> {:ok, ""} end)
source = source_fixture(index_frequency_minutes: -1)
perform_job(MediaIndexingWorker, %{id: source.id})
end
test "it does not reschedule if the source shouldn't be indexed" do
expect(YtDlpRunnerMock, :run, 0, fn _url, _opts, _ot -> {:ok, ""} end)
source = source_fixture(index_frequency_minutes: -1)
perform_job(MediaIndexingWorker, %{id: source.id})
refute_enqueued(worker: MediaIndexingWorker, args: %{"id" => source.id})
end
test "it indexes the source if it should be indexed" do test "it indexes the source if it should be indexed" do
expect(YtDlpRunnerMock, :run, fn _url, _opts, _ot -> {:ok, ""} end) expect(YtDlpRunnerMock, :run, fn _url, _opts, _ot -> {:ok, ""} end)
@ -37,6 +20,31 @@ defmodule Pinchflat.Workers.MediaIndexingWorkerTest do
perform_job(MediaIndexingWorker, %{id: source.id}) perform_job(MediaIndexingWorker, %{id: source.id})
end end
test "it indexes the source no matter what if the source has never been indexed before" do
expect(YtDlpRunnerMock, :run, fn _url, _opts, _ot -> {:ok, ""} end)
source = source_fixture(index_frequency_minutes: 0, last_indexed_at: nil)
perform_job(MediaIndexingWorker, %{id: source.id})
end
test "it does not do any indexing if the source has been indexed and shouldn't be rescheduled" do
expect(YtDlpRunnerMock, :run, 0, fn _url, _opts, _ot -> {:ok, ""} end)
source = source_fixture(index_frequency_minutes: -1, last_indexed_at: DateTime.utc_now())
perform_job(MediaIndexingWorker, %{id: source.id})
end
test "it does not reschedule if the source shouldn't be indexed" do
stub(YtDlpRunnerMock, :run, fn _url, _opts, _ot -> {:ok, ""} end)
source = source_fixture(index_frequency_minutes: -1)
perform_job(MediaIndexingWorker, %{id: source.id})
refute_enqueued(worker: MediaIndexingWorker, args: %{"id" => source.id})
end
test "it kicks off a download job for each pending media item" do test "it kicks off a download job for each pending media item" do
expect(YtDlpRunnerMock, :run, fn _url, _opts, _ot -> {:ok, source_attributes_return_fixture()} end) expect(YtDlpRunnerMock, :run, fn _url, _opts, _ot -> {:ok, source_attributes_return_fixture()} end)

View file

@ -19,7 +19,7 @@ defmodule Pinchflat.SourcesFixtures do
collection_name: "Source ##{:rand.uniform(1_000_000)}", collection_name: "Source ##{:rand.uniform(1_000_000)}",
collection_id: Base.encode16(:crypto.hash(:md5, "#{:rand.uniform(1_000_000)}")), collection_id: Base.encode16(:crypto.hash(:md5, "#{:rand.uniform(1_000_000)}")),
collection_type: "channel", collection_type: "channel",
friendly_name: "Cool and good internal name!", custom_name: "Cool and good internal name!",
original_url: "https://www.youtube.com/channel/#{Faker.String.base64(12)}", original_url: "https://www.youtube.com/channel/#{Faker.String.base64(12)}",
media_profile_id: ProfilesFixtures.media_profile_fixture().id, media_profile_id: ProfilesFixtures.media_profile_fixture().id,
index_frequency_minutes: 60 index_frequency_minutes: 60