Adds options + option builder + metadata parsing for media thumbnails

This commit is contained in:
Kieran Eglin 2024-02-05 12:22:08 -08:00
parent ca01f17b58
commit 3e5faa0503
No known key found for this signature in database
GPG key ID: 193984967FCF432D
9 changed files with 281 additions and 48 deletions

View file

@ -11,12 +11,13 @@ defmodule Pinchflat.Media.MediaItem do
alias Pinchflat.Media.MediaMetadata alias Pinchflat.Media.MediaMetadata
@required_fields ~w(media_id source_id)a @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 schema "media_items" do
field :title, :string field :title, :string
field :media_id, :string field :media_id, :string
field :media_filepath, :string field :media_filepath, :string
field :thumbnail_filepath, :string
# This is an array of [iso-2 language, filepath] pairs. Probably could # 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. # 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. # Will very likely revisit because I can't leave well-enough alone.

View file

@ -25,6 +25,7 @@ defmodule Pinchflat.MediaClient.Backends.YtDlp.MetadataParser do
metadata_attrs metadata_attrs
|> Map.merge(parse_media_metadata(metadata)) |> Map.merge(parse_media_metadata(metadata))
|> Map.merge(parse_subtitle_metadata(metadata)) |> Map.merge(parse_subtitle_metadata(metadata))
|> Map.merge(parse_thumbnail_metadata(metadata))
end end
defp parse_media_metadata(metadata) do defp parse_media_metadata(metadata) do
@ -35,10 +36,9 @@ defmodule Pinchflat.MediaClient.Backends.YtDlp.MetadataParser do
end end
defp parse_subtitle_metadata(metadata) do defp parse_subtitle_metadata(metadata) do
subtitle_map = metadata["requested_subtitles"] || %{}
# IDEA: if needed, consider filtering out subtitles that don't exist on-disk # IDEA: if needed, consider filtering out subtitles that don't exist on-disk
subtitle_filepaths = subtitle_filepaths =
subtitle_map (metadata["requested_subtitles"] || %{})
|> Enum.map(fn {lang, attrs} -> [lang, attrs["filepath"]] end) |> Enum.map(fn {lang, attrs} -> [lang, attrs["filepath"]] end)
|> Enum.sort(fn [lang_a, _], [lang_b, _] -> lang_a < lang_b 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 subtitle_filepaths: subtitle_filepaths
} }
end 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 end

View file

@ -15,6 +15,8 @@ defmodule Pinchflat.Profiles.MediaProfile do
download_auto_subs download_auto_subs
embed_subs embed_subs
sub_langs sub_langs
download_thumbnail
embed_thumbnail
)a )a
@required_fields ~w(name output_path_template)a @required_fields ~w(name output_path_template)a
@ -22,11 +24,15 @@ defmodule Pinchflat.Profiles.MediaProfile do
schema "media_profiles" do schema "media_profiles" do
field :name, :string field :name, :string
field :output_path_template, :string field :output_path_template, :string
field :download_subs, :boolean, default: true field :download_subs, :boolean, default: true
field :download_auto_subs, :boolean, default: true field :download_auto_subs, :boolean, default: true
field :embed_subs, :boolean, default: true field :embed_subs, :boolean, default: true
field :sub_langs, :string, default: "en" field :sub_langs, :string, default: "en"
field :download_thumbnail, :boolean, default: true
field :embed_thumbnail, :boolean, default: true
has_many :sources, Source has_many :sources, Source
timestamps(type: :utc_datetime) timestamps(type: :utc_datetime)

View file

@ -24,6 +24,7 @@ defmodule Pinchflat.Profiles.Options.YtDlp.OptionBuilder do
built_options = built_options =
default_options() ++ default_options() ++
subtitle_options(media_profile) ++ subtitle_options(media_profile) ++
thumbnail_options(media_profile) ++
output_options(media_profile) output_options(media_profile)
{:ok, built_options} {:ok, built_options}
@ -33,7 +34,6 @@ defmodule Pinchflat.Profiles.Options.YtDlp.OptionBuilder do
defp default_options do defp default_options do
[ [
:embed_metadata, :embed_metadata,
:embed_thumbnail,
:no_progress :no_progress
] ]
end end
@ -65,6 +65,18 @@ defmodule Pinchflat.Profiles.Options.YtDlp.OptionBuilder do
end) end)
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 defp output_options(media_profile) do
{:ok, output_path} = OutputPathBuilder.build(media_profile.output_path_template) {:ok, output_path} = OutputPathBuilder.build(media_profile.output_path_template)

View file

@ -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

View file

@ -83,4 +83,28 @@ defmodule Pinchflat.MediaClient.Backends.YtDlp.MediaParserTest do
assert result.subtitle_filepaths == [] assert result.subtitle_filepaths == []
end end
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 end

View file

@ -33,11 +33,18 @@ defmodule Pinchflat.MediaClient.VideoDownloaderTest do
{:ok, render_metadata(:media_metadata)} {:ok, render_metadata(:media_metadata)}
end) 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 {:ok, updated_media_item} = VideoDownloader.download_for_media_item(media_item)
assert updated_media_item.media_filepath assert updated_media_item.media_filepath
assert updated_media_item.title assert updated_media_item.title
assert length(updated_media_item.subtitle_filepaths) > 0 assert length(updated_media_item.subtitle_filepaths) > 0
assert updated_media_item.thumbnail_filepath
end end
test "it saves the metadata to the database", %{media_item: media_item} do test "it saves the metadata to the database", %{media_item: media_item} do

View file

@ -94,4 +94,35 @@ defmodule Pinchflat.Profiles.Options.YtDlp.OptionBuilderTest do
refute {:id, -1} in res refute {:id, -1} in res
end end
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 end

View file

@ -1389,42 +1389,138 @@
} }
], ],
"thumbnails": [ "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/3.jpg",
{ "url": "https://i.ytimg.com/vi/bwRHIkYqYJo/2.jpg", "preference": -35, "id": "2" }, "preference": -37,
{ "url": "https://i.ytimg.com/vi_webp/bwRHIkYqYJo/2.webp", "preference": -34, "id": "3" }, "id": "0"
{ "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/3.webp",
{ "url": "https://i.ytimg.com/vi_webp/bwRHIkYqYJo/mq3.webp", "preference": -30, "id": "7" }, "preference": -36,
{ "url": "https://i.ytimg.com/vi/bwRHIkYqYJo/mq2.jpg", "preference": -29, "id": "8" }, "id": "1"
{ "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/2.jpg",
{ "url": "https://i.ytimg.com/vi/bwRHIkYqYJo/hq3.jpg", "preference": -25, "id": "12" }, "preference": -35,
{ "url": "https://i.ytimg.com/vi_webp/bwRHIkYqYJo/hq3.webp", "preference": -24, "id": "13" }, "id": "2"
{ "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/2.webp",
{ "url": "https://i.ytimg.com/vi_webp/bwRHIkYqYJo/hq1.webp", "preference": -20, "id": "17" }, "preference": -34,
{ "url": "https://i.ytimg.com/vi/bwRHIkYqYJo/sd3.jpg", "preference": -19, "id": "18" }, "id": "3"
{ "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/1.jpg",
{ "url": "https://i.ytimg.com/vi/bwRHIkYqYJo/sd1.jpg", "preference": -15, "id": "22" }, "preference": -33,
{ "url": "https://i.ytimg.com/vi_webp/bwRHIkYqYJo/sd1.webp", "preference": -14, "id": "23" }, "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", "url": "https://i.ytimg.com/vi/bwRHIkYqYJo/default.jpg",
"height": 90,
"width": 120,
"preference": -13, "preference": -13,
"id": "24", "id": "24"
"resolution": "120x90"
}, },
{ {
"url": "https://i.ytimg.com/vi_webp/bwRHIkYqYJo/default.webp", "url": "https://i.ytimg.com/vi_webp/bwRHIkYqYJo/default.webp",
"height": 90,
"width": 120,
"preference": -12, "preference": -12,
"id": "25" "id": "25",
"resolution": "120x90"
}, },
{ {
"url": "https://i.ytimg.com/vi/bwRHIkYqYJo/mqdefault.jpg", "url": "https://i.ytimg.com/vi/bwRHIkYqYJo/mqdefault.jpg",
@ -1436,13 +1532,24 @@
}, },
{ {
"url": "https://i.ytimg.com/vi_webp/bwRHIkYqYJo/mqdefault.webp", "url": "https://i.ytimg.com/vi_webp/bwRHIkYqYJo/mqdefault.webp",
"height": 180,
"width": 320,
"preference": -10, "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, "height": 94,
"width": 168, "width": 168,
"preference": -7, "preference": -7,
@ -1450,7 +1557,7 @@
"resolution": "168x94" "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, "height": 110,
"width": 196, "width": 196,
"preference": -7, "preference": -7,
@ -1458,7 +1565,7 @@
"resolution": "196x110" "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, "height": 138,
"width": 246, "width": 246,
"preference": -7, "preference": -7,
@ -1466,7 +1573,7 @@
"resolution": "246x138" "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, "height": 188,
"width": 336, "width": 336,
"preference": -7, "preference": -7,
@ -1483,8 +1590,11 @@
}, },
{ {
"url": "https://i.ytimg.com/vi_webp/bwRHIkYqYJo/hqdefault.webp", "url": "https://i.ytimg.com/vi_webp/bwRHIkYqYJo/hqdefault.webp",
"height": 360,
"width": 480,
"preference": -6, "preference": -6,
"id": "35" "id": "35",
"resolution": "480x360"
}, },
{ {
"url": "https://i.ytimg.com/vi/bwRHIkYqYJo/sddefault.jpg", "url": "https://i.ytimg.com/vi/bwRHIkYqYJo/sddefault.jpg",
@ -1496,23 +1606,37 @@
}, },
{ {
"url": "https://i.ytimg.com/vi_webp/bwRHIkYqYJo/sddefault.webp", "url": "https://i.ytimg.com/vi_webp/bwRHIkYqYJo/sddefault.webp",
"height": 480,
"width": 640,
"preference": -4, "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", "url": "https://i.ytimg.com/vi/bwRHIkYqYJo/maxresdefault.jpg",
"height": 1080, "height": 720,
"width": 1920, "width": 1280,
"preference": -1, "preference": -1,
"id": "40", "id": "40",
"resolution": "1920x1080" "resolution": "1280x720"
}, },
{ {
"url": "https://i.ytimg.com/vi_webp/bwRHIkYqYJo/maxresdefault.webp", "url": "https://i.ytimg.com/vi_webp/bwRHIkYqYJo/maxresdefault.webp",
"height": 1080,
"width": 1920,
"preference": 0, "preference": 0,
"id": "41", "id": "41",
"resolution": "1920x1080",
"filepath": "/app/tmp/videos/bwRHIkYqYJo/Trying to Wheelie Without the Rear Brake-bwRHIkYqYJo.webp" "filepath": "/app/tmp/videos/bwRHIkYqYJo/Trying to Wheelie Without the Rear Brake-bwRHIkYqYJo.webp"
} }
], ],