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 1ad9e2c331
commit 8cc36928f1
18 changed files with 200 additions and 139 deletions
@@ -11,7 +11,6 @@ defmodule Pinchflat.MediaClient.VideoDownloader do
alias Pinchflat.Repo
alias Pinchflat.Media
alias Pinchflat.Media.MediaItem
alias Pinchflat.Profiles.MediaProfile
alias Pinchflat.MediaClient.Backends.YtDlp.Video, as: YtDlpVideo
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
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} ->
{parser, helpers} = metadata_parsers(backend)
@@ -57,10 +55,10 @@ defmodule Pinchflat.MediaClient.VideoDownloader do
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)
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)
end
+4 -4
View File
@@ -15,7 +15,7 @@ defmodule Pinchflat.Sources.Source do
collection_name
collection_id
collection_type
friendly_name
custom_name
index_frequency_minutes
download_media
last_indexed_at
@@ -27,7 +27,7 @@ defmodule Pinchflat.Sources.Source do
collection_name
collection_id
collection_type
friendly_name
custom_name
index_frequency_minutes
download_media
original_url
@@ -35,7 +35,7 @@ defmodule Pinchflat.Sources.Source do
)a
schema "sources" do
field :friendly_name, :string
field :custom_name, :string
field :collection_name, :string
field :collection_id, :string
field :collection_type, Ecto.Enum, values: [:channel, :playlist]
@@ -58,7 +58,7 @@ defmodule Pinchflat.Sources.Source do
def changeset(source, attrs) do
source
|> 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)
|> unique_constraint([:collection_id, :media_profile_id])
end
+2 -1
View File
@@ -29,7 +29,8 @@ defmodule Pinchflat.Profiles.MediaProfile do
schema "media_profiles" do
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_auto_subs, :boolean, default: true
@@ -5,21 +5,17 @@ defmodule Pinchflat.Profiles.Options.YtDlp.DownloadOptionBuilder do
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
@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
these options
"""
def build(%MediaProfile{} = media_profile) do
# 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!
# NOTE: Looks like you can put different media types in different directories.
# see: https://github.com/yt-dlp/yt-dlp#output-template
def build(%MediaItem{} = media_item_with_preloads) do
media_profile = media_item_with_preloads.source.media_profile
built_options =
default_options() ++
@@ -27,12 +23,11 @@ defmodule Pinchflat.Profiles.Options.YtDlp.DownloadOptionBuilder do
thumbnail_options(media_profile) ++
metadata_options(media_profile) ++
quality_options(media_profile) ++
output_options(media_profile)
output_options(media_item_with_preloads)
{:ok, built_options}
end
# This will be updated a lot as I add new options to profiles
defp default_options do
[:no_progress, :windows_filenames]
end
@@ -101,14 +96,25 @@ defmodule Pinchflat.Profiles.Options.YtDlp.DownloadOptionBuilder do
end
end
defp output_options(media_profile) do
{:ok, output_path} = OutputPathBuilder.build(media_profile.output_path_template)
defp output_options(media_item_with_preloads) do
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)
]
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
Application.get_env(:pinchflat, :media_directory)
end
@@ -8,13 +8,16 @@ defmodule Pinchflat.Profiles.Options.YtDlp.OutputPathBuilder do
alias Pinchflat.RenderedString.Parser, as: TemplateParser
@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,
leaving yt-dlp syntax intact.
"""
def build(template_string) do
TemplateParser.parse(template_string, custom_yt_dlp_option_map(), &identifier_fn/2)
def build(template_string, additional_template_options \\ %{}) do
combined_options = Map.merge(custom_yt_dlp_option_map(), additional_template_options)
TemplateParser.parse(template_string, combined_options, &identifier_fn/2)
end
# 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
# 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
%{
# Individual parts of the upload date
+21 -7
View File
@@ -15,8 +15,9 @@ defmodule Pinchflat.Workers.MediaIndexingWorker do
@doc """
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
reschedules the job to run again in the future (as determined by the
souce's `index_frequency_minutes` field).
reschedules the job to run again in the future. It will ALWAYS index a source
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.
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
source = Sources.get_source!(source_id)
if source.index_frequency_minutes <= 0 do
:ok
else
index_media_and_reschedule(source)
case {source.index_frequency_minutes, source.last_indexed_at} do
{index_freq, _} when index_freq > 0 ->
# If the indexing is on a schedule simply run indexing and reschedule
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
defp index_media_and_reschedule(source) do
defp index_media(source) do
SourceTasks.index_media_items(source)
# This method handles the case where a source is set to not download media
SourceTasks.enqueue_pending_media_tasks(source)
end
defp reschedule_indexing(source) do
source
|> Map.take([:id])
|> MediaIndexingWorker.new(schedule_in: source.index_frequency_minutes * 60)
@@ -17,7 +17,7 @@
<section>
<strong>Source:</strong>
<.inline_link href={~p"/sources/#{@media_item.source_id}"}>
<%= @media_item.source.friendly_name %>
<%= @media_item.source.custom_name %>
</.inline_link>
</section>
@@ -30,7 +30,13 @@ defmodule PinchflatWeb.MediaProfiles.MediaProfileHTML do
end
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
def common_output_template_options do
@@ -46,8 +46,8 @@
</section>
<h2 class="text-xl font-bold mb-2">Template Options</h2>
<section class="ml-2 md:ml-4 mb-4 max-w-prose">
<p>
<section class="ml-2 md:ml-4 mb-4">
<p class="max-w-prose">
Any single-word <code class="text-sm">yt-dlp</code>
option
<.inline_link href="https://github.com/yt-dlp/yt-dlp?tab=readme-ov-file#output-template">
@@ -58,8 +58,9 @@
</p>
<h3 class="text-lg font-bold mb-2">Custom Aliases</h3>
<ul class="list-disc list-inside ml-2 md:ml-5">
<li :for={opt <- custom_output_template_options()}>
<.inline_code>{{ <%= opt %> }}</.inline_code>
<li :for={{k, v} <- custom_output_template_options()}>
<.inline_code>{{ <%= k %> }}</.inline_code>
<span :if={v}>- <%= v %></span>
</li>
</ul>
<h3 class="text-lg font-bold mb-2">Common Options</h3>
@@ -50,7 +50,7 @@
<:tab title="Sources">
<.table rows={@media_profile.sources} table_class="text-black dark:text-white">
<:col :let={source} label="Name">
<%= source.friendly_name || source.collection_name %>
<%= source.custom_name || source.collection_name %>
</:col>
<:col :let={source} label="Type"><%= source.collection_type %></:col>
<:col :let={source} label="Should Download?">
@@ -14,7 +14,7 @@
<div class="flex flex-col gap-10 min-w-max">
<.table rows={@sources} table_class="text-black dark:text-white">
<:col :let={source} label="Name">
<%= source.friendly_name || source.collection_name %>
<%= source.custom_name || source.collection_name %>
</:col>
<:col :let={source} label="Type"><%= source.collection_type %></:col>
<:col :let={source} label="Should Download?">
@@ -4,7 +4,7 @@
</.error>
<.input
field={f[:friendly_name]}
field={f[:custom_name]}
type="text"
label="Custom Name"
help="Something descriptive. Does not impact indexing or downloading"