Updated source image parser to work with playlists
This commit is contained in:
parent
b7684430bf
commit
bffc82c0d7
6 changed files with 141 additions and 35 deletions
|
|
@ -20,44 +20,51 @@ defmodule Pinchflat.Metadata.SourceImageParser do
|
||||||
def store_source_images(base_directory, source_metadata) do
|
def store_source_images(base_directory, source_metadata) do
|
||||||
(source_metadata["thumbnails"] || [])
|
(source_metadata["thumbnails"] || [])
|
||||||
|> Enum.filter(&(&1["filepath"] != nil))
|
|> Enum.filter(&(&1["filepath"] != nil))
|
||||||
|> select_useful_images()
|
|> select_useful_images(source_metadata)
|
||||||
|> Enum.map(&move_image(&1, base_directory))
|
|> Enum.map(&move_image(&1, base_directory))
|
||||||
|> Enum.into(%{})
|
|> Enum.into(%{})
|
||||||
end
|
end
|
||||||
|
|
||||||
defp select_useful_images(images) do
|
defp select_useful_images(images, source_metadata) do
|
||||||
labelled_images =
|
labelled_images =
|
||||||
Enum.reduce(images, %{}, fn image_map, acc ->
|
Enum.reduce(images, %{}, fn image_map, acc ->
|
||||||
case image_map do
|
case image_map do
|
||||||
%{"id" => "avatar_uncropped"} ->
|
%{"id" => "avatar_uncropped"} -> put_image_key(acc, :poster, image_map["filepath"])
|
||||||
Map.put(acc, :poster, %{
|
%{"id" => "banner_uncropped"} -> put_image_key(acc, :fanart, image_map["filepath"])
|
||||||
attribute_name: :poster_filepath,
|
_ -> acc
|
||||||
final_filename: "poster",
|
|
||||||
current_filepath: image_map["filepath"]
|
|
||||||
})
|
|
||||||
|
|
||||||
%{"id" => "banner_uncropped"} ->
|
|
||||||
Map.put(acc, :fanart, %{
|
|
||||||
attribute_name: :fanart_filepath,
|
|
||||||
final_filename: "fanart",
|
|
||||||
current_filepath: image_map["filepath"]
|
|
||||||
})
|
|
||||||
|
|
||||||
_ ->
|
|
||||||
acc
|
|
||||||
end
|
end
|
||||||
end)
|
end)
|
||||||
|
|
||||||
labelled_images
|
labelled_images
|
||||||
# |> add_fallback_poster()
|
|> add_fallback_poster(source_metadata)
|
||||||
|> Map.put(:banner, %{
|
|> put_image_key(:banner, determine_best_banner(images))
|
||||||
attribute_name: :banner_filepath,
|
|
||||||
final_filename: "banner",
|
|
||||||
current_filepath: determine_best_banner(images)
|
|
||||||
})
|
|
||||||
|> Enum.filter(fn {_key, attrs} -> attrs.current_filepath end)
|
|> Enum.filter(fn {_key, attrs} -> attrs.current_filepath end)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# If a poster is set, short-circuit and return the images as-is
|
||||||
|
defp add_fallback_poster(%{poster: _} = images, _), do: images
|
||||||
|
|
||||||
|
# If a poster is NOT set, see if we can find a suitable image to use as a fallback
|
||||||
|
defp add_fallback_poster(images, source_metadata) do
|
||||||
|
case source_metadata["entries"] do
|
||||||
|
nil -> images
|
||||||
|
[] -> images
|
||||||
|
[first_entry | _] -> add_poster_from_entry_thumbnail(images, first_entry)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp add_poster_from_entry_thumbnail(images, entry) do
|
||||||
|
thumbnail =
|
||||||
|
(entry["thumbnails"] || [])
|
||||||
|
|> Enum.reverse()
|
||||||
|
|> Enum.find(& &1["filepath"])
|
||||||
|
|
||||||
|
case thumbnail do
|
||||||
|
nil -> images
|
||||||
|
_ -> put_image_key(images, :poster, thumbnail["filepath"])
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
defp determine_best_banner(images) do
|
defp determine_best_banner(images) do
|
||||||
best_candidate =
|
best_candidate =
|
||||||
images
|
images
|
||||||
|
|
@ -80,5 +87,13 @@ defmodule Pinchflat.Metadata.SourceImageParser do
|
||||||
{attrs.attribute_name, final_filepath}
|
{attrs.attribute_name, final_filepath}
|
||||||
end
|
end
|
||||||
|
|
||||||
# defp add_fallback_poster()
|
defp put_image_key(map, key, image) do
|
||||||
|
attribute_atom = String.to_existing_atom("#{key}_filepath")
|
||||||
|
|
||||||
|
Map.put(map, key, %{
|
||||||
|
attribute_name: attribute_atom,
|
||||||
|
final_filename: to_string(key),
|
||||||
|
current_filepath: image
|
||||||
|
})
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -77,7 +77,7 @@ defmodule Pinchflat.Metadata.SourceMetadataStorageWorker do
|
||||||
|
|
||||||
defp fetch_source_metadata_and_images(series_directory, source) do
|
defp fetch_source_metadata_and_images(series_directory, source) do
|
||||||
metadata_directory = MetadataFileHelpers.metadata_directory_for(source)
|
metadata_directory = MetadataFileHelpers.metadata_directory_for(source)
|
||||||
# TODO: test
|
|
||||||
{:ok, metadata} = fetch_metadata_for_source(source)
|
{:ok, metadata} = fetch_metadata_for_source(source)
|
||||||
metadata_image_attrs = SourceImageParser.store_source_images(metadata_directory, metadata)
|
metadata_image_attrs = SourceImageParser.store_source_images(metadata_directory, metadata)
|
||||||
|
|
||||||
|
|
@ -109,7 +109,6 @@ defmodule Pinchflat.Metadata.SourceMetadataStorageWorker do
|
||||||
end
|
end
|
||||||
|
|
||||||
defp fetch_metadata_for_source(source) do
|
defp fetch_metadata_for_source(source) do
|
||||||
# TODO: test
|
|
||||||
tmp_output_path = "#{tmp_directory()}/#{StringUtils.random_string(16)}/source_image.%(ext)S"
|
tmp_output_path = "#{tmp_directory()}/#{StringUtils.random_string(16)}/source_image.%(ext)S"
|
||||||
base_opts = [convert_thumbnails: "jpg", output: tmp_output_path]
|
base_opts = [convert_thumbnails: "jpg", output: tmp_output_path]
|
||||||
|
|
||||||
|
|
@ -117,7 +116,7 @@ defmodule Pinchflat.Metadata.SourceMetadataStorageWorker do
|
||||||
if source.collection_type == :channel do
|
if source.collection_type == :channel do
|
||||||
base_opts ++ [:write_all_thumbnails, playlist_items: 0]
|
base_opts ++ [:write_all_thumbnails, playlist_items: 0]
|
||||||
else
|
else
|
||||||
base_opts ++ [:write_thumbnails, playlist_items: 1]
|
base_opts ++ [:write_thumbnail, playlist_items: 1]
|
||||||
end
|
end
|
||||||
|
|
||||||
MediaCollection.get_source_metadata(source.original_url, opts)
|
MediaCollection.get_source_metadata(source.original_url, opts)
|
||||||
|
|
|
||||||
|
|
@ -109,9 +109,10 @@ defmodule Pinchflat.YtDlp.MediaCollection do
|
||||||
|
|
||||||
Returns {:ok, map()} | {:error, any, ...}.
|
Returns {:ok, map()} | {:error, any, ...}.
|
||||||
"""
|
"""
|
||||||
def get_source_metadata(source_url, addl_opts \\ []) do
|
def get_source_metadata(source_url, addl_opts \\ [playlist_items: 0]) do
|
||||||
# TODO: test
|
# This only validates that the `playlist_items` key is present. It's otherwise unused
|
||||||
# NOTE: don't forget to set `playlist_items` when you're calling this method! See above.
|
_playlist_items = Keyword.fetch!(addl_opts, :playlist_items)
|
||||||
|
|
||||||
opts = [:skip_download] ++ addl_opts
|
opts = [:skip_download] ++ addl_opts
|
||||||
output_template = "playlist:%()j"
|
output_template = "playlist:%()j"
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -75,4 +75,51 @@ defmodule Pinchflat.Metadata.SourceImageParserTest do
|
||||||
assert SourceImageParser.store_source_images(@base_dir, metadata) == %{}
|
assert SourceImageParser.store_source_images(@base_dir, metadata) == %{}
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
describe "store_source_images/2 when testing fallbacks" do
|
||||||
|
test "uses the entries list for a fallback poster if needed" do
|
||||||
|
metadata = %{
|
||||||
|
"thumbnails" => [],
|
||||||
|
"entries" => [
|
||||||
|
%{
|
||||||
|
"thumbnails" => [%{"filepath" => "/app/test/support/files/channel_photos/a.0.jpg"}]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
expected = %{
|
||||||
|
poster_filepath: "#{@base_dir}/poster.jpg"
|
||||||
|
}
|
||||||
|
|
||||||
|
assert SourceImageParser.store_source_images(@base_dir, metadata) == expected
|
||||||
|
end
|
||||||
|
|
||||||
|
test "doesn't blow up if the entries list doesn't have any suitable thumbnails" do
|
||||||
|
metadata = %{
|
||||||
|
"thumbnails" => [],
|
||||||
|
"entries" => [
|
||||||
|
%{"thumbnails" => [%{"id" => "1"}]}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
assert SourceImageParser.store_source_images(@base_dir, metadata) == %{}
|
||||||
|
end
|
||||||
|
|
||||||
|
test "doesn't use the entries list if it's empty" do
|
||||||
|
metadata = %{
|
||||||
|
"thumbnails" => [],
|
||||||
|
"entries" => []
|
||||||
|
}
|
||||||
|
|
||||||
|
assert SourceImageParser.store_source_images(@base_dir, metadata) == %{}
|
||||||
|
end
|
||||||
|
|
||||||
|
test "doesn't use the entries list if it's not present" do
|
||||||
|
metadata = %{
|
||||||
|
"thumbnails" => []
|
||||||
|
}
|
||||||
|
|
||||||
|
assert SourceImageParser.store_source_images(@base_dir, metadata) == %{}
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -143,7 +143,9 @@ defmodule Pinchflat.Metadata.SourceMetadataStorageWorkerTest do
|
||||||
|
|
||||||
{:ok, source_details_return_fixture(%{filename: filename})}
|
{:ok, source_details_return_fixture(%{filename: filename})}
|
||||||
|
|
||||||
_url, _opts, ot when ot == @metadata_ot ->
|
_url, opts, ot when ot == @metadata_ot ->
|
||||||
|
assert {:convert_thumbnails, "jpg"} in opts
|
||||||
|
|
||||||
{:ok, render_metadata(:channel_source_metadata)}
|
{:ok, render_metadata(:channel_source_metadata)}
|
||||||
end)
|
end)
|
||||||
|
|
||||||
|
|
@ -164,6 +166,42 @@ defmodule Pinchflat.Metadata.SourceMetadataStorageWorkerTest do
|
||||||
Sources.delete_source(source, delete_files: true)
|
Sources.delete_source(source, delete_files: true)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
test "calls one set of yt-dlp metadata opts for channels" do
|
||||||
|
stub(YtDlpRunnerMock, :run, fn
|
||||||
|
_url, _opts, ot when ot == @source_details_ot ->
|
||||||
|
{:ok, source_details_return_fixture()}
|
||||||
|
|
||||||
|
_url, opts, ot when ot == @metadata_ot ->
|
||||||
|
assert {:playlist_items, 0} in opts
|
||||||
|
assert :write_all_thumbnails in opts
|
||||||
|
|
||||||
|
{:ok, render_metadata(:channel_source_metadata)}
|
||||||
|
end)
|
||||||
|
|
||||||
|
profile = media_profile_fixture(%{download_source_images: true})
|
||||||
|
source = source_fixture(media_profile_id: profile.id, collection_type: :channel)
|
||||||
|
|
||||||
|
perform_job(SourceMetadataStorageWorker, %{id: source.id})
|
||||||
|
end
|
||||||
|
|
||||||
|
test "calls another set of yt-dlp metadata opts for playlists" do
|
||||||
|
stub(YtDlpRunnerMock, :run, fn
|
||||||
|
_url, _opts, ot when ot == @source_details_ot ->
|
||||||
|
{:ok, source_details_return_fixture()}
|
||||||
|
|
||||||
|
_url, opts, ot when ot == @metadata_ot ->
|
||||||
|
assert {:playlist_items, 1} in opts
|
||||||
|
assert :write_thumbnail in opts
|
||||||
|
|
||||||
|
{:ok, render_metadata(:channel_source_metadata)}
|
||||||
|
end)
|
||||||
|
|
||||||
|
profile = media_profile_fixture(%{download_source_images: true})
|
||||||
|
source = source_fixture(media_profile_id: profile.id, collection_type: :playlist)
|
||||||
|
|
||||||
|
perform_job(SourceMetadataStorageWorker, %{id: source.id})
|
||||||
|
end
|
||||||
|
|
||||||
test "does not store source images if the profile is not set to" do
|
test "does not store source images if the profile is not set to" do
|
||||||
stub(YtDlpRunnerMock, :run, fn
|
stub(YtDlpRunnerMock, :run, fn
|
||||||
_url, _opts, ot when ot == @source_details_ot ->
|
_url, _opts, ot when ot == @source_details_ot ->
|
||||||
|
|
|
||||||
|
|
@ -129,7 +129,7 @@ defmodule Pinchflat.YtDlp.MediaCollectionTest do
|
||||||
|
|
||||||
test "it passes the expected args to the backend runner" do
|
test "it passes the expected args to the backend runner" do
|
||||||
expect(YtDlpRunnerMock, :run, fn @channel_url, opts, ot ->
|
expect(YtDlpRunnerMock, :run, fn @channel_url, opts, ot ->
|
||||||
assert opts == [playlist_items: 0]
|
assert opts == [:skip_download, playlist_items: 0]
|
||||||
assert ot == "playlist:%()j"
|
assert ot == "playlist:%()j"
|
||||||
|
|
||||||
{:ok, "{}"}
|
{:ok, "{}"}
|
||||||
|
|
@ -152,12 +152,18 @@ defmodule Pinchflat.YtDlp.MediaCollectionTest do
|
||||||
|
|
||||||
test "allows you to pass additional opts" do
|
test "allows you to pass additional opts" do
|
||||||
expect(YtDlpRunnerMock, :run, fn _url, opts, _ot ->
|
expect(YtDlpRunnerMock, :run, fn _url, opts, _ot ->
|
||||||
assert opts == [playlist_items: 0, real_opt: :yup]
|
assert opts == [:skip_download, playlist_items: 1, real_opt: :yup]
|
||||||
|
|
||||||
{:ok, "{}"}
|
{:ok, "{}"}
|
||||||
end)
|
end)
|
||||||
|
|
||||||
assert {:ok, _} = MediaCollection.get_source_metadata(@channel_url, real_opt: :yup)
|
assert {:ok, _} = MediaCollection.get_source_metadata(@channel_url, playlist_items: 1, real_opt: :yup)
|
||||||
|
end
|
||||||
|
|
||||||
|
test "blows up if you pass addl opts but don't pass playlist items" do
|
||||||
|
assert_raise KeyError, fn ->
|
||||||
|
MediaCollection.get_source_metadata(@channel_url, real_opt: :yup)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue