diff --git a/lib/pinchflat/metadata/source_image_parser.ex b/lib/pinchflat/metadata/source_image_parser.ex index c853966..26bacd7 100644 --- a/lib/pinchflat/metadata/source_image_parser.ex +++ b/lib/pinchflat/metadata/source_image_parser.ex @@ -20,44 +20,51 @@ defmodule Pinchflat.Metadata.SourceImageParser do def store_source_images(base_directory, source_metadata) do (source_metadata["thumbnails"] || []) |> Enum.filter(&(&1["filepath"] != nil)) - |> select_useful_images() + |> select_useful_images(source_metadata) |> Enum.map(&move_image(&1, base_directory)) |> Enum.into(%{}) end - defp select_useful_images(images) do + defp select_useful_images(images, source_metadata) do labelled_images = Enum.reduce(images, %{}, fn image_map, acc -> case image_map do - %{"id" => "avatar_uncropped"} -> - Map.put(acc, :poster, %{ - attribute_name: :poster_filepath, - 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 + %{"id" => "avatar_uncropped"} -> put_image_key(acc, :poster, image_map["filepath"]) + %{"id" => "banner_uncropped"} -> put_image_key(acc, :fanart, image_map["filepath"]) + _ -> acc end end) labelled_images - # |> add_fallback_poster() - |> Map.put(:banner, %{ - attribute_name: :banner_filepath, - final_filename: "banner", - current_filepath: determine_best_banner(images) - }) + |> add_fallback_poster(source_metadata) + |> put_image_key(:banner, determine_best_banner(images)) |> Enum.filter(fn {_key, attrs} -> attrs.current_filepath 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 best_candidate = images @@ -80,5 +87,13 @@ defmodule Pinchflat.Metadata.SourceImageParser do {attrs.attribute_name, final_filepath} 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 diff --git a/lib/pinchflat/metadata/source_metadata_storage_worker.ex b/lib/pinchflat/metadata/source_metadata_storage_worker.ex index b31d6ac..8ccfdf9 100644 --- a/lib/pinchflat/metadata/source_metadata_storage_worker.ex +++ b/lib/pinchflat/metadata/source_metadata_storage_worker.ex @@ -77,7 +77,7 @@ defmodule Pinchflat.Metadata.SourceMetadataStorageWorker do defp fetch_source_metadata_and_images(series_directory, source) do metadata_directory = MetadataFileHelpers.metadata_directory_for(source) - # TODO: test + {:ok, metadata} = fetch_metadata_for_source(source) metadata_image_attrs = SourceImageParser.store_source_images(metadata_directory, metadata) @@ -109,7 +109,6 @@ defmodule Pinchflat.Metadata.SourceMetadataStorageWorker do end defp fetch_metadata_for_source(source) do - # TODO: test tmp_output_path = "#{tmp_directory()}/#{StringUtils.random_string(16)}/source_image.%(ext)S" base_opts = [convert_thumbnails: "jpg", output: tmp_output_path] @@ -117,7 +116,7 @@ defmodule Pinchflat.Metadata.SourceMetadataStorageWorker do if source.collection_type == :channel do base_opts ++ [:write_all_thumbnails, playlist_items: 0] else - base_opts ++ [:write_thumbnails, playlist_items: 1] + base_opts ++ [:write_thumbnail, playlist_items: 1] end MediaCollection.get_source_metadata(source.original_url, opts) diff --git a/lib/pinchflat/yt_dlp/media_collection.ex b/lib/pinchflat/yt_dlp/media_collection.ex index 48c029f..f657195 100644 --- a/lib/pinchflat/yt_dlp/media_collection.ex +++ b/lib/pinchflat/yt_dlp/media_collection.ex @@ -109,9 +109,10 @@ defmodule Pinchflat.YtDlp.MediaCollection do Returns {:ok, map()} | {:error, any, ...}. """ - def get_source_metadata(source_url, addl_opts \\ []) do - # TODO: test - # NOTE: don't forget to set `playlist_items` when you're calling this method! See above. + def get_source_metadata(source_url, addl_opts \\ [playlist_items: 0]) do + # This only validates that the `playlist_items` key is present. It's otherwise unused + _playlist_items = Keyword.fetch!(addl_opts, :playlist_items) + opts = [:skip_download] ++ addl_opts output_template = "playlist:%()j" diff --git a/test/pinchflat/metadata/source_image_parser_test.exs b/test/pinchflat/metadata/source_image_parser_test.exs index 8ec3c6c..3f815ae 100644 --- a/test/pinchflat/metadata/source_image_parser_test.exs +++ b/test/pinchflat/metadata/source_image_parser_test.exs @@ -75,4 +75,51 @@ defmodule Pinchflat.Metadata.SourceImageParserTest do assert SourceImageParser.store_source_images(@base_dir, metadata) == %{} 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 diff --git a/test/pinchflat/metadata/source_metadata_storage_worker_test.exs b/test/pinchflat/metadata/source_metadata_storage_worker_test.exs index 503d0f3..d8b0324 100644 --- a/test/pinchflat/metadata/source_metadata_storage_worker_test.exs +++ b/test/pinchflat/metadata/source_metadata_storage_worker_test.exs @@ -143,7 +143,9 @@ defmodule Pinchflat.Metadata.SourceMetadataStorageWorkerTest do {: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)} end) @@ -164,6 +166,42 @@ defmodule Pinchflat.Metadata.SourceMetadataStorageWorkerTest do Sources.delete_source(source, delete_files: true) 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 stub(YtDlpRunnerMock, :run, fn _url, _opts, ot when ot == @source_details_ot -> diff --git a/test/pinchflat/yt_dlp/media_collection_test.exs b/test/pinchflat/yt_dlp/media_collection_test.exs index fa69e58..f08c649 100644 --- a/test/pinchflat/yt_dlp/media_collection_test.exs +++ b/test/pinchflat/yt_dlp/media_collection_test.exs @@ -129,7 +129,7 @@ defmodule Pinchflat.YtDlp.MediaCollectionTest do test "it passes the expected args to the backend runner" do expect(YtDlpRunnerMock, :run, fn @channel_url, opts, ot -> - assert opts == [playlist_items: 0] + assert opts == [:skip_download, playlist_items: 0] assert ot == "playlist:%()j" {:ok, "{}"} @@ -152,12 +152,18 @@ defmodule Pinchflat.YtDlp.MediaCollectionTest do test "allows you to pass additional opts" do 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, "{}"} 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