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:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
+5
-4
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user