Download images for sources (#96)
* [WIP] hooked up photo downloading to source metadata worker * Added new attributes for source images * Added option to profile form
This commit is contained in:
@@ -34,6 +34,19 @@ defmodule Pinchflat.Filesystem.FilesystemHelpers do
|
||||
File.write!(filepath, content, modes)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Copies a file from source to destination, creating directories as needed.
|
||||
|
||||
Returns :ok | raises on error
|
||||
"""
|
||||
def cp_p!(source, destination) do
|
||||
destination
|
||||
|> Path.dirname()
|
||||
|> File.mkdir_p!()
|
||||
|
||||
File.cp!(source, destination)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Fetches the file size of a media item and saves it to the database.
|
||||
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
defmodule Pinchflat.Metadata.SourceImageParser do
|
||||
@moduledoc """
|
||||
Functions for parsing and storing source images.
|
||||
"""
|
||||
alias Pinchflat.Filesystem.FilesystemHelpers
|
||||
|
||||
@doc """
|
||||
Given a base directory and source metadata, look for the appropriate images
|
||||
and move them to the base directory. Returns a map of image types and their
|
||||
filepaths (specifically for a source). If a given image type is not found,
|
||||
the key will not be present in the map.
|
||||
|
||||
The metadata is expected to contain the `filepath` key for images, implying
|
||||
that the images have already been downloaded by yt-dlp. This method will NOT
|
||||
download images from the internet - it just copys them around.
|
||||
|
||||
Returns a map with the possible keys :poster_filepath, :fanart_filepath, and
|
||||
:banner_filepath.
|
||||
"""
|
||||
def store_source_images(base_directory, source_metadata) do
|
||||
(source_metadata["thumbnails"] || [])
|
||||
|> Enum.filter(&(&1["filepath"] != nil))
|
||||
|> select_useful_images()
|
||||
|> Enum.map(&move_image(&1, base_directory))
|
||||
|> Enum.into(%{})
|
||||
end
|
||||
|
||||
defp select_useful_images(images) do
|
||||
labelled_images =
|
||||
Enum.reduce(images, [], fn image_map, acc ->
|
||||
case image_map do
|
||||
%{"id" => "avatar_uncropped"} ->
|
||||
acc ++ [{:poster, :poster_filepath, image_map["filepath"]}]
|
||||
|
||||
%{"id" => "banner_uncropped"} ->
|
||||
acc ++ [{:fanart, :fanart_filepath, image_map["filepath"]}]
|
||||
|
||||
_ ->
|
||||
acc
|
||||
end
|
||||
end)
|
||||
|
||||
labelled_images
|
||||
|> Enum.concat([{:banner, :banner_filepath, determine_best_banner(images)}])
|
||||
|> Enum.filter(fn {_, _, tmp_filepath} -> tmp_filepath end)
|
||||
end
|
||||
|
||||
defp determine_best_banner(images) do
|
||||
best_candidate =
|
||||
images
|
||||
# Filter out images that don't have a width and height attribute
|
||||
|> Enum.filter(&(&1["width"] && &1["height"]))
|
||||
# Sort images with the highest width first
|
||||
|> Enum.sort(&(&1["width"] >= &2["width"]))
|
||||
# Find the first image where the ratio of width to height is greater than 3
|
||||
|> Enum.find(&(&1["width"] / &1["height"] > 3))
|
||||
|
||||
Map.get(best_candidate || %{}, "filepath")
|
||||
end
|
||||
|
||||
defp move_image({filename, source_attr_name, tmp_filepath}, base_directory) do
|
||||
extension = Path.extname(tmp_filepath)
|
||||
final_filepath = Path.join([base_directory, "#{filename}#{extension}"])
|
||||
|
||||
FilesystemHelpers.cp_p!(tmp_filepath, final_filepath)
|
||||
|
||||
{source_attr_name, final_filepath}
|
||||
end
|
||||
end
|
||||
@@ -12,8 +12,10 @@ defmodule Pinchflat.Metadata.SourceMetadataStorageWorker do
|
||||
alias Pinchflat.Repo
|
||||
alias Pinchflat.Tasks
|
||||
alias Pinchflat.Sources
|
||||
alias Pinchflat.Utils.StringUtils
|
||||
alias Pinchflat.Metadata.NfoBuilder
|
||||
alias Pinchflat.YtDlp.MediaCollection
|
||||
alias Pinchflat.Metadata.SourceImageParser
|
||||
alias Pinchflat.Metadata.MetadataFileHelpers
|
||||
alias Pinchflat.Downloading.DownloadOptionBuilder
|
||||
|
||||
@@ -33,6 +35,7 @@ defmodule Pinchflat.Metadata.SourceMetadataStorageWorker do
|
||||
- JSON metadata for internal use
|
||||
- The series directory for the source
|
||||
- The NFO file for the source (if specified)
|
||||
- Downloads and stores source images (if specified)
|
||||
|
||||
The worker is kicked off after a source is inserted/updated - this can
|
||||
take an unknown amount of time so don't rely on this data being here
|
||||
@@ -43,19 +46,22 @@ defmodule Pinchflat.Metadata.SourceMetadataStorageWorker do
|
||||
@impl Oban.Worker
|
||||
def perform(%Oban.Job{args: %{"id" => source_id}}) do
|
||||
source = Repo.preload(Sources.get_source!(source_id), [:metadata, :media_profile])
|
||||
source_metadata = fetch_source_metadata(source)
|
||||
series_directory = determine_series_directory(source)
|
||||
{source_metadata, image_filepath_attrs} = fetch_source_metadata_and_images(series_directory, source)
|
||||
|
||||
# `run_post_commit_tasks: false` prevents this from running in an infinite loop
|
||||
Sources.update_source(
|
||||
source,
|
||||
%{
|
||||
series_directory: series_directory,
|
||||
nfo_filepath: store_source_nfo(source, series_directory, source_metadata),
|
||||
metadata: %{
|
||||
metadata_filepath: store_source_metadata(source, source_metadata)
|
||||
}
|
||||
},
|
||||
Map.merge(
|
||||
%{
|
||||
series_directory: series_directory,
|
||||
nfo_filepath: store_source_nfo(source, series_directory, source_metadata),
|
||||
metadata: %{
|
||||
metadata_filepath: MetadataFileHelpers.compress_and_store_metadata_for(source, source_metadata)
|
||||
}
|
||||
},
|
||||
image_filepath_attrs
|
||||
),
|
||||
run_post_commit_tasks: false
|
||||
)
|
||||
|
||||
@@ -65,14 +71,20 @@ defmodule Pinchflat.Metadata.SourceMetadataStorageWorker do
|
||||
Ecto.StaleEntryError -> Logger.info("#{__MODULE__} discarded: source #{source_id} stale")
|
||||
end
|
||||
|
||||
defp fetch_source_metadata(source) do
|
||||
{:ok, metadata} = MediaCollection.get_source_metadata(source.original_url)
|
||||
defp fetch_source_metadata_and_images(series_directory, source) do
|
||||
if source.media_profile.download_source_images && series_directory do
|
||||
output_path = "#{tmp_directory()}/#{StringUtils.random_string(16)}/source_image.%(ext)S"
|
||||
opts = [:write_all_thumbnails, convert_thumbnails: "jpg", output: output_path]
|
||||
|
||||
metadata
|
||||
end
|
||||
{:ok, metadata} = MediaCollection.get_source_metadata(source.original_url, opts)
|
||||
image_attrs = SourceImageParser.store_source_images(series_directory, metadata)
|
||||
|
||||
defp store_source_metadata(source, metadata) do
|
||||
MetadataFileHelpers.compress_and_store_metadata_for(source, metadata)
|
||||
{metadata, image_attrs}
|
||||
else
|
||||
{:ok, metadata} = MediaCollection.get_source_metadata(source.original_url, [])
|
||||
|
||||
{metadata, %{}}
|
||||
end
|
||||
end
|
||||
|
||||
defp determine_series_directory(source) do
|
||||
@@ -92,4 +104,8 @@ defmodule Pinchflat.Metadata.SourceMetadataStorageWorker do
|
||||
NfoBuilder.build_and_store_for_source(nfo_filepath, metadata)
|
||||
end
|
||||
end
|
||||
|
||||
defp tmp_directory do
|
||||
Application.get_env(:pinchflat, :tmpfile_directory)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -17,6 +17,7 @@ defmodule Pinchflat.Profiles.MediaProfile do
|
||||
sub_langs
|
||||
download_thumbnail
|
||||
embed_thumbnail
|
||||
download_source_images
|
||||
download_metadata
|
||||
embed_metadata
|
||||
download_nfo
|
||||
@@ -40,6 +41,7 @@ defmodule Pinchflat.Profiles.MediaProfile do
|
||||
|
||||
field :download_thumbnail, :boolean, default: false
|
||||
field :embed_thumbnail, :boolean, default: false
|
||||
field :download_source_images, :boolean, default: false
|
||||
|
||||
field :download_metadata, :boolean, default: false
|
||||
field :embed_metadata, :boolean, default: false
|
||||
|
||||
@@ -18,6 +18,9 @@ defmodule Pinchflat.Sources.Source do
|
||||
collection_type
|
||||
custom_name
|
||||
nfo_filepath
|
||||
poster_filepath
|
||||
fanart_filepath
|
||||
banner_filepath
|
||||
series_directory
|
||||
index_frequency_minutes
|
||||
fast_index
|
||||
@@ -55,6 +58,9 @@ defmodule Pinchflat.Sources.Source do
|
||||
field :collection_id, :string
|
||||
field :collection_type, Ecto.Enum, values: [:channel, :playlist]
|
||||
field :nfo_filepath, :string
|
||||
field :poster_filepath, :string
|
||||
field :fanart_filepath, :string
|
||||
field :banner_filepath, :string
|
||||
field :series_directory, :string
|
||||
field :index_frequency_minutes, :integer, default: 60 * 24
|
||||
field :fast_index, :boolean, default: false
|
||||
@@ -106,6 +112,6 @@ defmodule Pinchflat.Sources.Source do
|
||||
|
||||
@doc false
|
||||
def filepath_attributes do
|
||||
~w(nfo_filepath)a
|
||||
~w(nfo_filepath fanart_filepath poster_filepath banner_filepath)a
|
||||
end
|
||||
end
|
||||
|
||||
@@ -94,8 +94,8 @@ defmodule Pinchflat.YtDlp.MediaCollection do
|
||||
|
||||
Returns {:ok, map()} | {:error, any, ...}.
|
||||
"""
|
||||
def get_source_metadata(source_url) do
|
||||
opts = [playlist_items: 0]
|
||||
def get_source_metadata(source_url, addl_opts \\ []) do
|
||||
opts = [playlist_items: 0] ++ addl_opts
|
||||
output_template = "playlist:%()j"
|
||||
|
||||
with {:ok, output} <- backend_runner().run(source_url, opts, output_template),
|
||||
|
||||
+13
-1
@@ -208,7 +208,9 @@
|
||||
</h3>
|
||||
<p class="text-sm mt-2 max-w-prose">
|
||||
Everything in this section is experimental - please open a GitHub issue if you see something odd.
|
||||
These options only work if this Media Profile's output template is set to split media into seasons.
|
||||
<strong>
|
||||
These options only work if this Media Profile's output template is set to split media into seasons.
|
||||
</strong>
|
||||
Try the "Media Center" preset if you're not sure.
|
||||
</p>
|
||||
|
||||
@@ -226,6 +228,16 @@
|
||||
/>
|
||||
</section>
|
||||
|
||||
<section x-data="{ presets: { default: false, media_center: true, audio: false, archiving: true } }">
|
||||
<.input
|
||||
field={f[:download_source_images]}
|
||||
type="toggle"
|
||||
label="Download Series Images"
|
||||
help="Downloads poster and banner images for use with Jellyfin, Kodi, etc. Only works for full channels (not playlists)"
|
||||
x-init="$watch('selectedPreset', p => p && (enabled = presets[p]))"
|
||||
/>
|
||||
</section>
|
||||
|
||||
<.button class="my-10 sm:mb-7.5 w-full sm:w-auto">Save Media profile</.button>
|
||||
</section>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user