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:
Kieran
2024-03-19 13:34:01 -07:00
committed by GitHub
parent 8491f8b317
commit 70dd95211f
32 changed files with 584 additions and 18 deletions
@@ -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
+2
View File
@@ -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
+7 -1
View File
@@ -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
+2 -2
View File
@@ -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),
@@ -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>