From 3e5faa0503e726395ffc4009650ba761276f3565 Mon Sep 17 00:00:00 2001 From: Kieran Eglin Date: Mon, 5 Feb 2024 12:22:08 -0800 Subject: [PATCH] Adds options + option builder + metadata parsing for media thumbnails --- lib/pinchflat/media/media_item.ex | 3 +- .../backends/yt_dlp/metadata_parser.ex | 18 +- lib/pinchflat/profiles/media_profile.ex | 6 + .../profiles/options/yt_dlp/option_builder.ex | 14 +- ...dd_thumbnail_options_to_media_profiles.exs | 14 ++ .../backends/yt_dlp/metadata_parser_test.exs | 24 ++ .../media_client/video_downloader_test.exs | 9 +- .../options/yt_dlp/option_builder_test.exs | 31 +++ test/support/files/media_metadata.json | 210 ++++++++++++++---- 9 files changed, 281 insertions(+), 48 deletions(-) create mode 100644 priv/repo/migrations/20240205194558_add_thumbnail_options_to_media_profiles.exs diff --git a/lib/pinchflat/media/media_item.ex b/lib/pinchflat/media/media_item.ex index 430c3e5..b7f31dd 100644 --- a/lib/pinchflat/media/media_item.ex +++ b/lib/pinchflat/media/media_item.ex @@ -11,12 +11,13 @@ defmodule Pinchflat.Media.MediaItem do alias Pinchflat.Media.MediaMetadata @required_fields ~w(media_id source_id)a - @allowed_fields ~w(title media_id media_filepath source_id subtitle_filepaths)a + @allowed_fields ~w(title media_id media_filepath source_id subtitle_filepaths thumbnail_filepath)a schema "media_items" do field :title, :string field :media_id, :string field :media_filepath, :string + field :thumbnail_filepath, :string # This is an array of [iso-2 language, filepath] pairs. Probably could # be an associated record, but I don't see the benefit right now. # Will very likely revisit because I can't leave well-enough alone. diff --git a/lib/pinchflat/media_client/backends/yt_dlp/metadata_parser.ex b/lib/pinchflat/media_client/backends/yt_dlp/metadata_parser.ex index e2cd2b3..7dbdf14 100644 --- a/lib/pinchflat/media_client/backends/yt_dlp/metadata_parser.ex +++ b/lib/pinchflat/media_client/backends/yt_dlp/metadata_parser.ex @@ -25,6 +25,7 @@ defmodule Pinchflat.MediaClient.Backends.YtDlp.MetadataParser do metadata_attrs |> Map.merge(parse_media_metadata(metadata)) |> Map.merge(parse_subtitle_metadata(metadata)) + |> Map.merge(parse_thumbnail_metadata(metadata)) end defp parse_media_metadata(metadata) do @@ -35,10 +36,9 @@ defmodule Pinchflat.MediaClient.Backends.YtDlp.MetadataParser do end defp parse_subtitle_metadata(metadata) do - subtitle_map = metadata["requested_subtitles"] || %{} # IDEA: if needed, consider filtering out subtitles that don't exist on-disk subtitle_filepaths = - subtitle_map + (metadata["requested_subtitles"] || %{}) |> Enum.map(fn {lang, attrs} -> [lang, attrs["filepath"]] end) |> Enum.sort(fn [lang_a, _], [lang_b, _] -> lang_a < lang_b end) @@ -46,4 +46,18 @@ defmodule Pinchflat.MediaClient.Backends.YtDlp.MetadataParser do subtitle_filepaths: subtitle_filepaths } end + + defp parse_thumbnail_metadata(metadata) do + thumbnail_filepath = + (metadata["thumbnails"] || %{}) + # Reverse so that higher resolution thumbnails come first. + # This _shouldn't_ matter yet, but I'd rather default to the best + # in case I'm wrong. + |> Enum.reverse() + |> Enum.find_value(fn attrs -> attrs["filepath"] end) + + %{ + thumbnail_filepath: thumbnail_filepath + } + end end diff --git a/lib/pinchflat/profiles/media_profile.ex b/lib/pinchflat/profiles/media_profile.ex index 10964c9..579aa9c 100644 --- a/lib/pinchflat/profiles/media_profile.ex +++ b/lib/pinchflat/profiles/media_profile.ex @@ -15,6 +15,8 @@ defmodule Pinchflat.Profiles.MediaProfile do download_auto_subs embed_subs sub_langs + download_thumbnail + embed_thumbnail )a @required_fields ~w(name output_path_template)a @@ -22,11 +24,15 @@ defmodule Pinchflat.Profiles.MediaProfile do schema "media_profiles" do field :name, :string field :output_path_template, :string + field :download_subs, :boolean, default: true field :download_auto_subs, :boolean, default: true field :embed_subs, :boolean, default: true field :sub_langs, :string, default: "en" + field :download_thumbnail, :boolean, default: true + field :embed_thumbnail, :boolean, default: true + has_many :sources, Source timestamps(type: :utc_datetime) diff --git a/lib/pinchflat/profiles/options/yt_dlp/option_builder.ex b/lib/pinchflat/profiles/options/yt_dlp/option_builder.ex index ffbf3ca..06aa9db 100644 --- a/lib/pinchflat/profiles/options/yt_dlp/option_builder.ex +++ b/lib/pinchflat/profiles/options/yt_dlp/option_builder.ex @@ -24,6 +24,7 @@ defmodule Pinchflat.Profiles.Options.YtDlp.OptionBuilder do built_options = default_options() ++ subtitle_options(media_profile) ++ + thumbnail_options(media_profile) ++ output_options(media_profile) {:ok, built_options} @@ -33,7 +34,6 @@ defmodule Pinchflat.Profiles.Options.YtDlp.OptionBuilder do defp default_options do [ :embed_metadata, - :embed_thumbnail, :no_progress ] end @@ -65,6 +65,18 @@ defmodule Pinchflat.Profiles.Options.YtDlp.OptionBuilder do end) end + defp thumbnail_options(media_profile) do + mapped_struct = Map.from_struct(media_profile) + + Enum.reduce(mapped_struct, [], fn attr, acc -> + case attr do + {:download_thumbnail, true} -> acc ++ [:write_thumbnail] + {:embed_thumbnail, true} -> acc ++ [:embed_thumbnail] + _ -> acc + end + end) + end + defp output_options(media_profile) do {:ok, output_path} = OutputPathBuilder.build(media_profile.output_path_template) diff --git a/priv/repo/migrations/20240205194558_add_thumbnail_options_to_media_profiles.exs b/priv/repo/migrations/20240205194558_add_thumbnail_options_to_media_profiles.exs new file mode 100644 index 0000000..979d640 --- /dev/null +++ b/priv/repo/migrations/20240205194558_add_thumbnail_options_to_media_profiles.exs @@ -0,0 +1,14 @@ +defmodule Pinchflat.Repo.Migrations.AddThumbnailOptionsToMediaProfiles do + use Ecto.Migration + + def change do + alter table(:media_profiles) do + add :download_thumbnail, :boolean, default: true, null: false + add :embed_thumbnail, :boolean, default: true, null: false + end + + alter table(:media_items) do + add :thumbnail_filepath, :string + end + end +end diff --git a/test/pinchflat/media_client/backends/yt_dlp/metadata_parser_test.exs b/test/pinchflat/media_client/backends/yt_dlp/metadata_parser_test.exs index d414aac..c2010ab 100644 --- a/test/pinchflat/media_client/backends/yt_dlp/metadata_parser_test.exs +++ b/test/pinchflat/media_client/backends/yt_dlp/metadata_parser_test.exs @@ -83,4 +83,28 @@ defmodule Pinchflat.MediaClient.Backends.YtDlp.MediaParserTest do assert result.subtitle_filepaths == [] end end + + describe "parse_for_media_item/1 when testing thumbnail metadata" do + test "extracts the thumbnail filepath", %{metadata: metadata} do + result = Parser.parse_for_media_item(metadata) + + assert String.ends_with?(result.thumbnail_filepath, ".webp") + end + + test "doesn't freak out if the video has no thumbnails", %{metadata: metadata} do + metadata = Map.put(metadata, "thumbnails", %{}) + + result = Parser.parse_for_media_item(metadata) + + assert result.thumbnail_filepath == nil + end + + test "doesn't freak out if the thumbnails key is missing", %{metadata: metadata} do + metadata = Map.delete(metadata, "thumbnails") + + result = Parser.parse_for_media_item(metadata) + + assert result.thumbnail_filepath == nil + end + end end diff --git a/test/pinchflat/media_client/video_downloader_test.exs b/test/pinchflat/media_client/video_downloader_test.exs index 3d31530..a05c957 100644 --- a/test/pinchflat/media_client/video_downloader_test.exs +++ b/test/pinchflat/media_client/video_downloader_test.exs @@ -33,11 +33,18 @@ defmodule Pinchflat.MediaClient.VideoDownloaderTest do {:ok, render_metadata(:media_metadata)} end) - assert %{media_filepath: nil, title: nil, subtitle_filepaths: []} = media_item + assert %{ + media_filepath: nil, + title: nil, + subtitle_filepaths: [], + thumbnail_filepath: nil + } = media_item + assert {:ok, updated_media_item} = VideoDownloader.download_for_media_item(media_item) assert updated_media_item.media_filepath assert updated_media_item.title assert length(updated_media_item.subtitle_filepaths) > 0 + assert updated_media_item.thumbnail_filepath end test "it saves the metadata to the database", %{media_item: media_item} do diff --git a/test/pinchflat/profiles/options/yt_dlp/option_builder_test.exs b/test/pinchflat/profiles/options/yt_dlp/option_builder_test.exs index 0753d2d..30982b9 100644 --- a/test/pinchflat/profiles/options/yt_dlp/option_builder_test.exs +++ b/test/pinchflat/profiles/options/yt_dlp/option_builder_test.exs @@ -94,4 +94,35 @@ defmodule Pinchflat.Profiles.Options.YtDlp.OptionBuilderTest do refute {:id, -1} in res end end + + describe "build/1 when testing thumbnail options" do + test "includes :write_thumbnail option when specified" do + media_profile = %MediaProfile{@media_profile | download_thumbnail: true} + + assert {:ok, res} = OptionBuilder.build(media_profile) + + assert :write_thumbnail in res + end + + test "includes :embed_thumbnail option when specified" do + media_profile = %MediaProfile{@media_profile | embed_thumbnail: true} + + assert {:ok, res} = OptionBuilder.build(media_profile) + + assert :embed_thumbnail in res + end + + test "doesn't include these options when not specified" do + media_profile = %MediaProfile{ + @media_profile + | embed_thumbnail: false, + download_thumbnail: false + } + + assert {:ok, res} = OptionBuilder.build(media_profile) + + refute :write_thumbnail in res + refute :embed_thumbnail in res + end + end end diff --git a/test/support/files/media_metadata.json b/test/support/files/media_metadata.json index e8653ce..f5ae2ed 100644 --- a/test/support/files/media_metadata.json +++ b/test/support/files/media_metadata.json @@ -1389,42 +1389,138 @@ } ], "thumbnails": [ - { "url": "https://i.ytimg.com/vi/bwRHIkYqYJo/3.jpg", "preference": -37, "id": "0" }, - { "url": "https://i.ytimg.com/vi_webp/bwRHIkYqYJo/3.webp", "preference": -36, "id": "1" }, - { "url": "https://i.ytimg.com/vi/bwRHIkYqYJo/2.jpg", "preference": -35, "id": "2" }, - { "url": "https://i.ytimg.com/vi_webp/bwRHIkYqYJo/2.webp", "preference": -34, "id": "3" }, - { "url": "https://i.ytimg.com/vi/bwRHIkYqYJo/1.jpg", "preference": -33, "id": "4" }, - { "url": "https://i.ytimg.com/vi_webp/bwRHIkYqYJo/1.webp", "preference": -32, "id": "5" }, - { "url": "https://i.ytimg.com/vi/bwRHIkYqYJo/mq3.jpg", "preference": -31, "id": "6" }, - { "url": "https://i.ytimg.com/vi_webp/bwRHIkYqYJo/mq3.webp", "preference": -30, "id": "7" }, - { "url": "https://i.ytimg.com/vi/bwRHIkYqYJo/mq2.jpg", "preference": -29, "id": "8" }, - { "url": "https://i.ytimg.com/vi_webp/bwRHIkYqYJo/mq2.webp", "preference": -28, "id": "9" }, - { "url": "https://i.ytimg.com/vi/bwRHIkYqYJo/mq1.jpg", "preference": -27, "id": "10" }, - { "url": "https://i.ytimg.com/vi_webp/bwRHIkYqYJo/mq1.webp", "preference": -26, "id": "11" }, - { "url": "https://i.ytimg.com/vi/bwRHIkYqYJo/hq3.jpg", "preference": -25, "id": "12" }, - { "url": "https://i.ytimg.com/vi_webp/bwRHIkYqYJo/hq3.webp", "preference": -24, "id": "13" }, - { "url": "https://i.ytimg.com/vi/bwRHIkYqYJo/hq2.jpg", "preference": -23, "id": "14" }, - { "url": "https://i.ytimg.com/vi_webp/bwRHIkYqYJo/hq2.webp", "preference": -22, "id": "15" }, - { "url": "https://i.ytimg.com/vi/bwRHIkYqYJo/hq1.jpg", "preference": -21, "id": "16" }, - { "url": "https://i.ytimg.com/vi_webp/bwRHIkYqYJo/hq1.webp", "preference": -20, "id": "17" }, - { "url": "https://i.ytimg.com/vi/bwRHIkYqYJo/sd3.jpg", "preference": -19, "id": "18" }, - { "url": "https://i.ytimg.com/vi_webp/bwRHIkYqYJo/sd3.webp", "preference": -18, "id": "19" }, - { "url": "https://i.ytimg.com/vi/bwRHIkYqYJo/sd2.jpg", "preference": -17, "id": "20" }, - { "url": "https://i.ytimg.com/vi_webp/bwRHIkYqYJo/sd2.webp", "preference": -16, "id": "21" }, - { "url": "https://i.ytimg.com/vi/bwRHIkYqYJo/sd1.jpg", "preference": -15, "id": "22" }, - { "url": "https://i.ytimg.com/vi_webp/bwRHIkYqYJo/sd1.webp", "preference": -14, "id": "23" }, + { + "url": "https://i.ytimg.com/vi/bwRHIkYqYJo/3.jpg", + "preference": -37, + "id": "0" + }, + { + "url": "https://i.ytimg.com/vi_webp/bwRHIkYqYJo/3.webp", + "preference": -36, + "id": "1" + }, + { + "url": "https://i.ytimg.com/vi/bwRHIkYqYJo/2.jpg", + "preference": -35, + "id": "2" + }, + { + "url": "https://i.ytimg.com/vi_webp/bwRHIkYqYJo/2.webp", + "preference": -34, + "id": "3" + }, + { + "url": "https://i.ytimg.com/vi/bwRHIkYqYJo/1.jpg", + "preference": -33, + "id": "4" + }, + { + "url": "https://i.ytimg.com/vi_webp/bwRHIkYqYJo/1.webp", + "preference": -32, + "id": "5" + }, + { + "url": "https://i.ytimg.com/vi/bwRHIkYqYJo/mq3.jpg", + "preference": -31, + "id": "6" + }, + { + "url": "https://i.ytimg.com/vi_webp/bwRHIkYqYJo/mq3.webp", + "preference": -30, + "id": "7" + }, + { + "url": "https://i.ytimg.com/vi/bwRHIkYqYJo/mq2.jpg", + "preference": -29, + "id": "8" + }, + { + "url": "https://i.ytimg.com/vi_webp/bwRHIkYqYJo/mq2.webp", + "preference": -28, + "id": "9" + }, + { + "url": "https://i.ytimg.com/vi/bwRHIkYqYJo/mq1.jpg", + "preference": -27, + "id": "10" + }, + { + "url": "https://i.ytimg.com/vi_webp/bwRHIkYqYJo/mq1.webp", + "preference": -26, + "id": "11" + }, + { + "url": "https://i.ytimg.com/vi/bwRHIkYqYJo/hq3.jpg", + "preference": -25, + "id": "12" + }, + { + "url": "https://i.ytimg.com/vi_webp/bwRHIkYqYJo/hq3.webp", + "preference": -24, + "id": "13" + }, + { + "url": "https://i.ytimg.com/vi/bwRHIkYqYJo/hq2.jpg", + "preference": -23, + "id": "14" + }, + { + "url": "https://i.ytimg.com/vi_webp/bwRHIkYqYJo/hq2.webp", + "preference": -22, + "id": "15" + }, + { + "url": "https://i.ytimg.com/vi/bwRHIkYqYJo/hq1.jpg", + "preference": -21, + "id": "16" + }, + { + "url": "https://i.ytimg.com/vi_webp/bwRHIkYqYJo/hq1.webp", + "preference": -20, + "id": "17" + }, + { + "url": "https://i.ytimg.com/vi/bwRHIkYqYJo/sd3.jpg", + "preference": -19, + "id": "18" + }, + { + "url": "https://i.ytimg.com/vi_webp/bwRHIkYqYJo/sd3.webp", + "preference": -18, + "id": "19" + }, + { + "url": "https://i.ytimg.com/vi/bwRHIkYqYJo/sd2.jpg", + "preference": -17, + "id": "20" + }, + { + "url": "https://i.ytimg.com/vi_webp/bwRHIkYqYJo/sd2.webp", + "preference": -16, + "id": "21" + }, + { + "url": "https://i.ytimg.com/vi/bwRHIkYqYJo/sd1.jpg", + "preference": -15, + "id": "22" + }, + { + "url": "https://i.ytimg.com/vi_webp/bwRHIkYqYJo/sd1.webp", + "preference": -14, + "id": "23" + }, { "url": "https://i.ytimg.com/vi/bwRHIkYqYJo/default.jpg", - "height": 90, - "width": 120, "preference": -13, - "id": "24", - "resolution": "120x90" + "id": "24" }, { "url": "https://i.ytimg.com/vi_webp/bwRHIkYqYJo/default.webp", + "height": 90, + "width": 120, "preference": -12, - "id": "25" + "id": "25", + "resolution": "120x90" }, { "url": "https://i.ytimg.com/vi/bwRHIkYqYJo/mqdefault.jpg", @@ -1436,13 +1532,24 @@ }, { "url": "https://i.ytimg.com/vi_webp/bwRHIkYqYJo/mqdefault.webp", + "height": 180, + "width": 320, "preference": -10, - "id": "27" + "id": "27", + "resolution": "320x180" }, - { "url": "https://i.ytimg.com/vi/bwRHIkYqYJo/0.jpg", "preference": -9, "id": "28" }, - { "url": "https://i.ytimg.com/vi_webp/bwRHIkYqYJo/0.webp", "preference": -8, "id": "29" }, { - "url": "https://i.ytimg.com/vi/bwRHIkYqYJo/hqdefault.jpg?sqp=-oaymwEbCKgBEF5IVfKriqkDDggBFQAAiEIYAXABwAEG&rs=AOn4CLCfnqZqzf3SpMWSBlr5eEkYrRbeOw", + "url": "https://i.ytimg.com/vi/bwRHIkYqYJo/0.jpg", + "preference": -9, + "id": "28" + }, + { + "url": "https://i.ytimg.com/vi_webp/bwRHIkYqYJo/0.webp", + "preference": -8, + "id": "29" + }, + { + "url": "https://i.ytimg.com/vi/bwRHIkYqYJo/hqdefault.jpg?sqp=-oaymwEbCKgBEF5IVfKriqkDDggBFQAAiEIYAXABwAEG&rs=AOn4CLDmfPp_bFp43fLeDiG7JmFAldoFLA", "height": 94, "width": 168, "preference": -7, @@ -1450,7 +1557,7 @@ "resolution": "168x94" }, { - "url": "https://i.ytimg.com/vi/bwRHIkYqYJo/hqdefault.jpg?sqp=-oaymwEbCMQBEG5IVfKriqkDDggBFQAAiEIYAXABwAEG&rs=AOn4CLAnj5wtnbYGzZX9gerMKQ-FS5sSeA", + "url": "https://i.ytimg.com/vi/bwRHIkYqYJo/hqdefault.jpg?sqp=-oaymwEbCMQBEG5IVfKriqkDDggBFQAAiEIYAXABwAEG&rs=AOn4CLCGAiCuOy3Tmz9U9iGo3YF5dM57bQ", "height": 110, "width": 196, "preference": -7, @@ -1458,7 +1565,7 @@ "resolution": "196x110" }, { - "url": "https://i.ytimg.com/vi/bwRHIkYqYJo/hqdefault.jpg?sqp=-oaymwEcCPYBEIoBSFXyq4qpAw4IARUAAIhCGAFwAcABBg==&rs=AOn4CLC5fmbFoduOVcifLAMu006pXlcicQ", + "url": "https://i.ytimg.com/vi/bwRHIkYqYJo/hqdefault.jpg?sqp=-oaymwEcCPYBEIoBSFXyq4qpAw4IARUAAIhCGAFwAcABBg==&rs=AOn4CLDcSQOIb-t1Aug8y0A-_M2jNehcRw", "height": 138, "width": 246, "preference": -7, @@ -1466,7 +1573,7 @@ "resolution": "246x138" }, { - "url": "https://i.ytimg.com/vi/bwRHIkYqYJo/hqdefault.jpg?sqp=-oaymwEcCNACELwBSFXyq4qpAw4IARUAAIhCGAFwAcABBg==&rs=AOn4CLDISoJqnSXFGgqsSCMRd9NCiV5Dyg", + "url": "https://i.ytimg.com/vi/bwRHIkYqYJo/hqdefault.jpg?sqp=-oaymwEcCNACELwBSFXyq4qpAw4IARUAAIhCGAFwAcABBg==&rs=AOn4CLAT65DjLaxTpghDUrIAQEclNUUJog", "height": 188, "width": 336, "preference": -7, @@ -1483,8 +1590,11 @@ }, { "url": "https://i.ytimg.com/vi_webp/bwRHIkYqYJo/hqdefault.webp", + "height": 360, + "width": 480, "preference": -6, - "id": "35" + "id": "35", + "resolution": "480x360" }, { "url": "https://i.ytimg.com/vi/bwRHIkYqYJo/sddefault.jpg", @@ -1496,23 +1606,37 @@ }, { "url": "https://i.ytimg.com/vi_webp/bwRHIkYqYJo/sddefault.webp", + "height": 480, + "width": 640, "preference": -4, - "id": "37" + "id": "37", + "resolution": "640x480" + }, + { + "url": "https://i.ytimg.com/vi/bwRHIkYqYJo/hq720.jpg", + "preference": -3, + "id": "38" + }, + { + "url": "https://i.ytimg.com/vi_webp/bwRHIkYqYJo/hq720.webp", + "preference": -2, + "id": "39" }, - { "url": "https://i.ytimg.com/vi/bwRHIkYqYJo/hq720.jpg", "preference": -3, "id": "38" }, - { "url": "https://i.ytimg.com/vi_webp/bwRHIkYqYJo/hq720.webp", "preference": -2, "id": "39" }, { "url": "https://i.ytimg.com/vi/bwRHIkYqYJo/maxresdefault.jpg", - "height": 1080, - "width": 1920, + "height": 720, + "width": 1280, "preference": -1, "id": "40", - "resolution": "1920x1080" + "resolution": "1280x720" }, { "url": "https://i.ytimg.com/vi_webp/bwRHIkYqYJo/maxresdefault.webp", + "height": 1080, + "width": 1920, "preference": 0, "id": "41", + "resolution": "1920x1080", "filepath": "/app/tmp/videos/bwRHIkYqYJo/Trying to Wheelie Without the Rear Brake-bwRHIkYqYJo.webp" } ],