[WIP] hooked up photo downloading to source metadata worker
|
|
@ -34,6 +34,19 @@ defmodule Pinchflat.Filesystem.FilesystemHelpers do
|
||||||
File.write!(filepath, content, modes)
|
File.write!(filepath, content, modes)
|
||||||
end
|
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 """
|
@doc """
|
||||||
Fetches the file size of a media item and saves it to the database.
|
Fetches the file size of a media item and saves it to the database.
|
||||||
|
|
||||||
|
|
|
||||||
69
lib/pinchflat/metadata/source_image_parser.ex
Normal file
|
|
@ -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.Repo
|
||||||
alias Pinchflat.Tasks
|
alias Pinchflat.Tasks
|
||||||
alias Pinchflat.Sources
|
alias Pinchflat.Sources
|
||||||
|
alias Pinchflat.Utils.StringUtils
|
||||||
alias Pinchflat.Metadata.NfoBuilder
|
alias Pinchflat.Metadata.NfoBuilder
|
||||||
alias Pinchflat.YtDlp.MediaCollection
|
alias Pinchflat.YtDlp.MediaCollection
|
||||||
|
alias Pinchflat.Metadata.SourceImageParser
|
||||||
alias Pinchflat.Metadata.MetadataFileHelpers
|
alias Pinchflat.Metadata.MetadataFileHelpers
|
||||||
alias Pinchflat.Downloading.DownloadOptionBuilder
|
alias Pinchflat.Downloading.DownloadOptionBuilder
|
||||||
|
|
||||||
|
|
@ -43,19 +45,22 @@ defmodule Pinchflat.Metadata.SourceMetadataStorageWorker do
|
||||||
@impl Oban.Worker
|
@impl Oban.Worker
|
||||||
def perform(%Oban.Job{args: %{"id" => source_id}}) do
|
def perform(%Oban.Job{args: %{"id" => source_id}}) do
|
||||||
source = Repo.preload(Sources.get_source!(source_id), [:metadata, :media_profile])
|
source = Repo.preload(Sources.get_source!(source_id), [:metadata, :media_profile])
|
||||||
source_metadata = fetch_source_metadata(source)
|
|
||||||
series_directory = determine_series_directory(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
|
# `run_post_commit_tasks: false` prevents this from running in an infinite loop
|
||||||
Sources.update_source(
|
Sources.update_source(
|
||||||
source,
|
source,
|
||||||
%{
|
Map.merge(
|
||||||
series_directory: series_directory,
|
%{
|
||||||
nfo_filepath: store_source_nfo(source, series_directory, source_metadata),
|
series_directory: series_directory,
|
||||||
metadata: %{
|
nfo_filepath: store_source_nfo(source, series_directory, source_metadata),
|
||||||
metadata_filepath: store_source_metadata(source, source_metadata)
|
metadata: %{
|
||||||
}
|
metadata_filepath: MetadataFileHelpers.compress_and_store_metadata_for(source, source_metadata)
|
||||||
},
|
}
|
||||||
|
},
|
||||||
|
image_filepath_attrs
|
||||||
|
),
|
||||||
run_post_commit_tasks: false
|
run_post_commit_tasks: false
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -65,14 +70,20 @@ defmodule Pinchflat.Metadata.SourceMetadataStorageWorker do
|
||||||
Ecto.StaleEntryError -> Logger.info("#{__MODULE__} discarded: source #{source_id} stale")
|
Ecto.StaleEntryError -> Logger.info("#{__MODULE__} discarded: source #{source_id} stale")
|
||||||
end
|
end
|
||||||
|
|
||||||
defp fetch_source_metadata(source) do
|
defp fetch_source_metadata_and_images(series_directory, source) do
|
||||||
{:ok, metadata} = MediaCollection.get_source_metadata(source.original_url)
|
# This is a proxy for checking whether we should download images
|
||||||
|
if :rand.uniform() > 0 do
|
||||||
|
output_path = "#{tmp_directory()}/#{StringUtils.random_string(16)}/source_image.%(ext)S"
|
||||||
|
opts = [:write_all_thumbnails, convert_thumbnails: "jpg", output: output_path]
|
||||||
|
{:ok, metadata} = MediaCollection.get_source_metadata(source.original_url, opts)
|
||||||
|
image_attrs = SourceImageParser.store_source_images(series_directory, metadata)
|
||||||
|
|
||||||
metadata
|
{metadata, image_attrs}
|
||||||
end
|
else
|
||||||
|
{:ok, metadata} = MediaCollection.get_source_metadata(source.original_url, [])
|
||||||
|
|
||||||
defp store_source_metadata(source, metadata) do
|
{metadata, %{}}
|
||||||
MetadataFileHelpers.compress_and_store_metadata_for(source, metadata)
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
defp determine_series_directory(source) do
|
defp determine_series_directory(source) do
|
||||||
|
|
@ -92,4 +103,8 @@ defmodule Pinchflat.Metadata.SourceMetadataStorageWorker do
|
||||||
NfoBuilder.build_and_store_for_source(nfo_filepath, metadata)
|
NfoBuilder.build_and_store_for_source(nfo_filepath, metadata)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
defp tmp_directory do
|
||||||
|
Application.get_env(:pinchflat, :tmpfile_directory)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -94,8 +94,8 @@ defmodule Pinchflat.YtDlp.MediaCollection do
|
||||||
|
|
||||||
Returns {:ok, map()} | {:error, any, ...}.
|
Returns {:ok, map()} | {:error, any, ...}.
|
||||||
"""
|
"""
|
||||||
def get_source_metadata(source_url) do
|
def get_source_metadata(source_url, addl_opts \\ []) do
|
||||||
opts = [playlist_items: 0]
|
opts = [playlist_items: 0] ++ addl_opts
|
||||||
output_template = "playlist:%()j"
|
output_template = "playlist:%()j"
|
||||||
|
|
||||||
with {:ok, output} <- backend_runner().run(source_url, opts, output_template),
|
with {:ok, output} <- backend_runner().run(source_url, opts, output_template),
|
||||||
|
|
|
||||||
|
|
@ -107,4 +107,37 @@ defmodule Pinchflat.Filesystem.FilesystemHelpersTest do
|
||||||
assert {:error, _} = FilesystemHelpers.delete_file_and_remove_empty_directories(filepath)
|
assert {:error, _} = FilesystemHelpers.delete_file_and_remove_empty_directories(filepath)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
describe "cp_p!/2" do
|
||||||
|
test "copies a file from source to destination" do
|
||||||
|
source = "#{tmpfile_directory()}/source.json"
|
||||||
|
FilesystemHelpers.write_p!(source, "TEST")
|
||||||
|
destination = "#{tmpfile_directory()}/destination.json"
|
||||||
|
|
||||||
|
refute File.exists?(destination)
|
||||||
|
FilesystemHelpers.cp_p!(source, destination)
|
||||||
|
assert File.exists?(destination)
|
||||||
|
assert File.read!(destination) == "TEST"
|
||||||
|
|
||||||
|
File.rm!(source)
|
||||||
|
File.rm!(destination)
|
||||||
|
end
|
||||||
|
|
||||||
|
test "creates directories as needed" do
|
||||||
|
source = "#{tmpfile_directory()}/source.json"
|
||||||
|
FilesystemHelpers.write_p!(source, "TEST")
|
||||||
|
destination = "#{tmpfile_directory()}/foo/bar/destination.json"
|
||||||
|
|
||||||
|
refute File.exists?(destination)
|
||||||
|
FilesystemHelpers.cp_p!(source, destination)
|
||||||
|
assert File.exists?(destination)
|
||||||
|
|
||||||
|
File.rm!(source)
|
||||||
|
File.rm!(destination)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp tmpfile_directory do
|
||||||
|
Application.get_env(:pinchflat, :tmpfile_directory)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
78
test/pinchflat/metadata/source_image_parser_test.exs
Normal file
|
|
@ -0,0 +1,78 @@
|
||||||
|
defmodule Pinchflat.Metadata.SourceImageParserTest do
|
||||||
|
use Pinchflat.DataCase
|
||||||
|
|
||||||
|
alias Pinchflat.Metadata.SourceImageParser
|
||||||
|
|
||||||
|
@base_dir Application.compile_env(:pinchflat, :tmpfile_directory)
|
||||||
|
|
||||||
|
describe "store_source_images/2" do
|
||||||
|
test "returns a map of image types and locations" do
|
||||||
|
metadata = render_parsed_metadata(:channel_source_metadata)
|
||||||
|
|
||||||
|
expected = %{
|
||||||
|
banner_filepath: "#{@base_dir}/banner.jpg",
|
||||||
|
fanart_filepath: "#{@base_dir}/fanart.jpg",
|
||||||
|
poster_filepath: "#{@base_dir}/poster.jpg"
|
||||||
|
}
|
||||||
|
|
||||||
|
assert SourceImageParser.store_source_images(@base_dir, metadata) == expected
|
||||||
|
end
|
||||||
|
|
||||||
|
test "returns the avatar_uncropped as the poster" do
|
||||||
|
metadata = %{
|
||||||
|
"thumbnails" => [
|
||||||
|
%{"id" => "avatar_uncropped", "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 "returns the banner_uncropped as the fanart" do
|
||||||
|
metadata = %{
|
||||||
|
"thumbnails" => [
|
||||||
|
%{"id" => "banner_uncropped", "filepath" => "/app/test/support/files/channel_photos/a.0.jpg"}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
expected = %{
|
||||||
|
fanart_filepath: "#{@base_dir}/fanart.jpg"
|
||||||
|
}
|
||||||
|
|
||||||
|
assert SourceImageParser.store_source_images(@base_dir, metadata) == expected
|
||||||
|
end
|
||||||
|
|
||||||
|
test "doesn't return a banner if no suitable images are found" do
|
||||||
|
metadata = %{
|
||||||
|
"thumbnails" => [
|
||||||
|
%{
|
||||||
|
"id" => "1",
|
||||||
|
"filepath" => "foo.jpg",
|
||||||
|
"width" => 100,
|
||||||
|
"height" => 100
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
expected = %{}
|
||||||
|
|
||||||
|
assert SourceImageParser.store_source_images(@base_dir, metadata) == expected
|
||||||
|
end
|
||||||
|
|
||||||
|
test "doesn't blow up if empty metadata is passed" do
|
||||||
|
metadata = %{}
|
||||||
|
|
||||||
|
assert SourceImageParser.store_source_images(@base_dir, metadata) == %{}
|
||||||
|
end
|
||||||
|
|
||||||
|
test "doesn't blow up if no thumbnails are passed" do
|
||||||
|
metadata = %{"thumbnails" => []}
|
||||||
|
|
||||||
|
assert SourceImageParser.store_source_images(@base_dir, metadata) == %{}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
@ -151,5 +151,15 @@ defmodule Pinchflat.YtDlp.MediaCollectionTest do
|
||||||
|
|
||||||
assert {:error, %Jason.DecodeError{}} = MediaCollection.get_source_metadata(@channel_url)
|
assert {:error, %Jason.DecodeError{}} = MediaCollection.get_source_metadata(@channel_url)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
test "allows you to pass additional opts" do
|
||||||
|
expect(YtDlpRunnerMock, :run, fn _url, opts, _ot ->
|
||||||
|
assert opts == [playlist_items: 0, real_opt: :yup]
|
||||||
|
|
||||||
|
{:ok, "{}"}
|
||||||
|
end)
|
||||||
|
|
||||||
|
assert {:ok, _} = MediaCollection.get_source_metadata(@channel_url, real_opt: :yup)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
BIN
test/support/files/channel_photos/a.0.jpg
Normal file
|
After Width: | Height: | Size: 9.6 KiB |
BIN
test/support/files/channel_photos/a.1.jpg
Normal file
|
After Width: | Height: | Size: 16 KiB |
BIN
test/support/files/channel_photos/a.10.jpg
Normal file
|
After Width: | Height: | Size: 62 KiB |
BIN
test/support/files/channel_photos/a.11.jpg
Normal file
|
After Width: | Height: | Size: 182 KiB |
BIN
test/support/files/channel_photos/a.12.jpg
Normal file
|
After Width: | Height: | Size: 82 KiB |
BIN
test/support/files/channel_photos/a.13.jpg
Normal file
|
After Width: | Height: | Size: 207 KiB |
BIN
test/support/files/channel_photos/a.14.jpg
Normal file
|
After Width: | Height: | Size: 82 KiB |
BIN
test/support/files/channel_photos/a.15.jpg
Normal file
|
After Width: | Height: | Size: 82 KiB |
BIN
test/support/files/channel_photos/a.17.jpg
Normal file
|
After Width: | Height: | Size: 28 KiB |
BIN
test/support/files/channel_photos/a.2.jpg
Normal file
|
After Width: | Height: | Size: 24 KiB |
BIN
test/support/files/channel_photos/a.3.jpg
Normal file
|
After Width: | Height: | Size: 68 KiB |
BIN
test/support/files/channel_photos/a.4.jpg
Normal file
|
After Width: | Height: | Size: 39 KiB |
BIN
test/support/files/channel_photos/a.5.jpg
Normal file
|
After Width: | Height: | Size: 33 KiB |
BIN
test/support/files/channel_photos/a.6.jpg
Normal file
|
After Width: | Height: | Size: 37 KiB |
BIN
test/support/files/channel_photos/a.7.jpg
Normal file
|
After Width: | Height: | Size: 59 KiB |
BIN
test/support/files/channel_photos/a.8.jpg
Normal file
|
After Width: | Height: | Size: 110 KiB |
BIN
test/support/files/channel_photos/a.9.jpg
Normal file
|
After Width: | Height: | Size: 59 KiB |
BIN
test/support/files/channel_photos/a.avatar_uncropped.jpg
Normal file
|
After Width: | Height: | Size: 17 KiB |
BIN
test/support/files/channel_photos/a.banner_uncropped.jpg
Normal file
|
After Width: | Height: | Size: 1.8 MiB |
237
test/support/files/channel_source_metadata.json
Normal file
|
|
@ -0,0 +1,237 @@
|
||||||
|
{
|
||||||
|
"id": "TheUselessTrials",
|
||||||
|
"channel": "TheUselessTrials",
|
||||||
|
"channel_id": "UCEi9yL4vhQlLafWRsAgAr3w",
|
||||||
|
"title": "TheUselessTrials",
|
||||||
|
"availability": null,
|
||||||
|
"channel_follower_count": 35000,
|
||||||
|
"description": "My name is Max and I'm a Street Trials bike rider with a passion for making videos. My channel features everything from learning and experiments all the way to just plain bike action. The first video series I started is called Quick New Trick and it featured me trying to learn new bike skills as quickly as possible, to show you how you can learn them as well.\n\nHope you enjoy the videos! See you in the comments section.",
|
||||||
|
"tags": [
|
||||||
|
"Useless Trials",
|
||||||
|
"TUT",
|
||||||
|
"UT",
|
||||||
|
"Max Fiergolla",
|
||||||
|
"maxfiergolla",
|
||||||
|
"Street Trial Bikes",
|
||||||
|
"Bike Tricks",
|
||||||
|
"How To",
|
||||||
|
"bike trial",
|
||||||
|
"TRIAL BIKE",
|
||||||
|
"quick new trick",
|
||||||
|
"bike hacks",
|
||||||
|
"trials bike",
|
||||||
|
"bike trials",
|
||||||
|
"TRIALS",
|
||||||
|
"street trial",
|
||||||
|
"trial street",
|
||||||
|
"trials biking",
|
||||||
|
"bike tricks",
|
||||||
|
"bmx tricks",
|
||||||
|
"mtb",
|
||||||
|
"mtb tricks",
|
||||||
|
"danny macaskill",
|
||||||
|
"martyn ashton",
|
||||||
|
"macaskill",
|
||||||
|
"bike stunts",
|
||||||
|
"mountainbike stunts",
|
||||||
|
"mountainbiking",
|
||||||
|
"inspired bicycles",
|
||||||
|
"imaginate",
|
||||||
|
"fahrrad tricks",
|
||||||
|
"fahrradtricks",
|
||||||
|
"fahrrad stunts"
|
||||||
|
],
|
||||||
|
"thumbnails": [
|
||||||
|
{
|
||||||
|
"url": "https://yt3.googleusercontent.com/_yUVmZqHvZZhNOFq2iMUPA82LylawAvYknCrwTE_9_IUQ0htiLPet2FKxE1ArvLAKf0lVsLccQ=w320-fcrop64=1,32b75a57cd48a5a8-k-c0xffffffff-no-nd-rj",
|
||||||
|
"height": 88,
|
||||||
|
"width": 320,
|
||||||
|
"preference": -10,
|
||||||
|
"id": "0",
|
||||||
|
"resolution": "320x88",
|
||||||
|
"filepath": "/app/test/support/files/channel_photos/a.0.jpg"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "https://yt3.googleusercontent.com/_yUVmZqHvZZhNOFq2iMUPA82LylawAvYknCrwTE_9_IUQ0htiLPet2FKxE1ArvLAKf0lVsLccQ=w320-fcrop64=1,00000000ffffffff-k-c0xffffffff-no-nd-rj",
|
||||||
|
"height": 180,
|
||||||
|
"width": 320,
|
||||||
|
"preference": -10,
|
||||||
|
"id": "1",
|
||||||
|
"resolution": "320x180",
|
||||||
|
"filepath": "/app/test/support/files/channel_photos/a.1.jpg"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "https://yt3.googleusercontent.com/_yUVmZqHvZZhNOFq2iMUPA82LylawAvYknCrwTE_9_IUQ0htiLPet2FKxE1ArvLAKf0lVsLccQ=w640-fcrop64=1,32b75a57cd48a5a8-k-c0xffffffff-no-nd-rj",
|
||||||
|
"height": 175,
|
||||||
|
"width": 640,
|
||||||
|
"preference": -10,
|
||||||
|
"id": "2",
|
||||||
|
"resolution": "640x175",
|
||||||
|
"filepath": "/app/test/support/files/channel_photos/a.2.jpg"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "https://yt3.googleusercontent.com/_yUVmZqHvZZhNOFq2iMUPA82LylawAvYknCrwTE_9_IUQ0htiLPet2FKxE1ArvLAKf0lVsLccQ=w854-fcrop64=1,00000000ffffffff-k-c0xffffffff-no-nd-rj",
|
||||||
|
"height": 480,
|
||||||
|
"width": 854,
|
||||||
|
"preference": -10,
|
||||||
|
"id": "3",
|
||||||
|
"resolution": "854x480",
|
||||||
|
"filepath": "/app/test/support/files/channel_photos/a.3.jpg"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "https://yt3.googleusercontent.com/_yUVmZqHvZZhNOFq2iMUPA82LylawAvYknCrwTE_9_IUQ0htiLPet2FKxE1ArvLAKf0lVsLccQ=w960-fcrop64=1,32b75a57cd48a5a8-k-c0xffffffff-no-nd-rj",
|
||||||
|
"height": 263,
|
||||||
|
"width": 960,
|
||||||
|
"preference": -10,
|
||||||
|
"id": "4",
|
||||||
|
"resolution": "960x263",
|
||||||
|
"filepath": "/app/test/support/files/channel_photos/a.4.jpg"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "https://yt3.googleusercontent.com/_yUVmZqHvZZhNOFq2iMUPA82LylawAvYknCrwTE_9_IUQ0htiLPet2FKxE1ArvLAKf0lVsLccQ=w1060-fcrop64=1,00005a57ffffa5a8-k-c0xffffffff-no-nd-rj",
|
||||||
|
"height": 175,
|
||||||
|
"width": 1060,
|
||||||
|
"preference": -10,
|
||||||
|
"id": "5",
|
||||||
|
"resolution": "1060x175",
|
||||||
|
"filepath": "/app/test/support/files/channel_photos/a.5.jpg"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "https://yt3.googleusercontent.com/_yUVmZqHvZZhNOFq2iMUPA82LylawAvYknCrwTE_9_IUQ0htiLPet2FKxE1ArvLAKf0lVsLccQ=w1138-fcrop64=1,00005a57ffffa5a8-k-c0xffffffff-no-nd-rj",
|
||||||
|
"height": 188,
|
||||||
|
"width": 1138,
|
||||||
|
"preference": -10,
|
||||||
|
"id": "6",
|
||||||
|
"resolution": "1138x188",
|
||||||
|
"filepath": "/app/test/support/files/channel_photos/a.6.jpg"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "https://yt3.googleusercontent.com/_yUVmZqHvZZhNOFq2iMUPA82LylawAvYknCrwTE_9_IUQ0htiLPet2FKxE1ArvLAKf0lVsLccQ=w1280-fcrop64=1,32b75a57cd48a5a8-k-c0xffffffff-no-nd-rj",
|
||||||
|
"height": 351,
|
||||||
|
"width": 1280,
|
||||||
|
"preference": -10,
|
||||||
|
"id": "7",
|
||||||
|
"resolution": "1280x351",
|
||||||
|
"filepath": "/app/test/support/files/channel_photos/a.7.jpg"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "https://yt3.googleusercontent.com/_yUVmZqHvZZhNOFq2iMUPA82LylawAvYknCrwTE_9_IUQ0htiLPet2FKxE1ArvLAKf0lVsLccQ=w1280-fcrop64=1,00000000ffffffff-k-c0xffffffff-no-nd-rj",
|
||||||
|
"height": 720,
|
||||||
|
"width": 1280,
|
||||||
|
"preference": -10,
|
||||||
|
"id": "8",
|
||||||
|
"resolution": "1280x720",
|
||||||
|
"filepath": "/app/test/support/files/channel_photos/a.8.jpg"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "https://yt3.googleusercontent.com/_yUVmZqHvZZhNOFq2iMUPA82LylawAvYknCrwTE_9_IUQ0htiLPet2FKxE1ArvLAKf0lVsLccQ=w1440-fcrop64=1,32b75a57cd48a5a8-k-c0xffffffff-no-nd-rj",
|
||||||
|
"height": 395,
|
||||||
|
"width": 1440,
|
||||||
|
"preference": -10,
|
||||||
|
"id": "9",
|
||||||
|
"resolution": "1440x395",
|
||||||
|
"filepath": "/app/test/support/files/channel_photos/a.9.jpg"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "https://yt3.googleusercontent.com/_yUVmZqHvZZhNOFq2iMUPA82LylawAvYknCrwTE_9_IUQ0htiLPet2FKxE1ArvLAKf0lVsLccQ=w1707-fcrop64=1,00005a57ffffa5a8-k-c0xffffffff-no-nd-rj",
|
||||||
|
"height": 283,
|
||||||
|
"width": 1707,
|
||||||
|
"preference": -10,
|
||||||
|
"id": "10",
|
||||||
|
"resolution": "1707x283",
|
||||||
|
"filepath": "/app/test/support/files/channel_photos/a.10.jpg"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "https://yt3.googleusercontent.com/_yUVmZqHvZZhNOFq2iMUPA82LylawAvYknCrwTE_9_IUQ0htiLPet2FKxE1ArvLAKf0lVsLccQ=w1920-fcrop64=1,00000000ffffffff-k-c0xffffffff-no-nd-rj",
|
||||||
|
"height": 1080,
|
||||||
|
"width": 1920,
|
||||||
|
"preference": -10,
|
||||||
|
"id": "11",
|
||||||
|
"resolution": "1920x1080",
|
||||||
|
"filepath": "/app/test/support/files/channel_photos/a.11.jpg"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "https://yt3.googleusercontent.com/_yUVmZqHvZZhNOFq2iMUPA82LylawAvYknCrwTE_9_IUQ0htiLPet2FKxE1ArvLAKf0lVsLccQ=w2120-fcrop64=1,00005a57ffffa5a8-k-c0xffffffff-no-nd-rj",
|
||||||
|
"height": 351,
|
||||||
|
"width": 2120,
|
||||||
|
"preference": -10,
|
||||||
|
"id": "12",
|
||||||
|
"resolution": "2120x351",
|
||||||
|
"filepath": "/app/test/support/files/channel_photos/a.12.jpg"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "https://yt3.googleusercontent.com/_yUVmZqHvZZhNOFq2iMUPA82LylawAvYknCrwTE_9_IUQ0htiLPet2FKxE1ArvLAKf0lVsLccQ=w2120-fcrop64=1,00000000ffffffff-k-c0xffffffff-no-nd-rj",
|
||||||
|
"height": 1192,
|
||||||
|
"width": 2120,
|
||||||
|
"preference": -10,
|
||||||
|
"id": "13",
|
||||||
|
"resolution": "2120x1192",
|
||||||
|
"filepath": "/app/test/support/files/channel_photos/a.13.jpg"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "https://yt3.googleusercontent.com/_yUVmZqHvZZhNOFq2iMUPA82LylawAvYknCrwTE_9_IUQ0htiLPet2FKxE1ArvLAKf0lVsLccQ=w2276-fcrop64=1,00005a57ffffa5a8-k-c0xffffffff-no-nd-rj",
|
||||||
|
"height": 377,
|
||||||
|
"width": 2276,
|
||||||
|
"preference": -10,
|
||||||
|
"id": "14",
|
||||||
|
"resolution": "2276x377",
|
||||||
|
"filepath": "/app/test/support/files/channel_photos/a.14.jpg"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "https://yt3.googleusercontent.com/_yUVmZqHvZZhNOFq2iMUPA82LylawAvYknCrwTE_9_IUQ0htiLPet2FKxE1ArvLAKf0lVsLccQ=w2560-fcrop64=1,00005a57ffffa5a8-k-c0xffffffff-no-nd-rj",
|
||||||
|
"height": 424,
|
||||||
|
"width": 2560,
|
||||||
|
"preference": -10,
|
||||||
|
"id": "15",
|
||||||
|
"resolution": "2560x424",
|
||||||
|
"filepath": "/app/test/support/files/channel_photos/a.15.jpg"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "https://yt3.googleusercontent.com/_yUVmZqHvZZhNOFq2iMUPA82LylawAvYknCrwTE_9_IUQ0htiLPet2FKxE1ArvLAKf0lVsLccQ=s0",
|
||||||
|
"id": "banner_uncropped",
|
||||||
|
"preference": -5,
|
||||||
|
"filepath": "/app/test/support/files/channel_photos/a.banner_uncropped.jpg"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "https://yt3.googleusercontent.com/ytc/AIdro_kw1KKWzbFmvq6u8WljTbo6QQ318WoAwnjX3AYrjA=s900-c-k-c0x00ffffff-no-rj",
|
||||||
|
"height": 900,
|
||||||
|
"width": 900,
|
||||||
|
"id": "17",
|
||||||
|
"resolution": "900x900",
|
||||||
|
"filepath": "/app/test/support/files/channel_photos/a.17.jpg"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "https://yt3.googleusercontent.com/ytc/AIdro_kw1KKWzbFmvq6u8WljTbo6QQ318WoAwnjX3AYrjA=s0",
|
||||||
|
"id": "avatar_uncropped",
|
||||||
|
"preference": 1,
|
||||||
|
"filepath": "/app/test/support/files/channel_photos/a.avatar_uncropped.jpg"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"uploader_id": "@TheUselessTrials",
|
||||||
|
"uploader_url": "https://www.youtube.com/@TheUselessTrials",
|
||||||
|
"modified_date": null,
|
||||||
|
"view_count": null,
|
||||||
|
"playlist_count": 2,
|
||||||
|
"uploader": "TheUselessTrials",
|
||||||
|
"channel_url": "https://www.youtube.com/channel/UCEi9yL4vhQlLafWRsAgAr3w",
|
||||||
|
"_type": "playlist",
|
||||||
|
"entries": [],
|
||||||
|
"webpage_url": "https://www.youtube.com/c/TheUselessTrials",
|
||||||
|
"original_url": "https://www.youtube.com/c/TheUselessTrials",
|
||||||
|
"webpage_url_basename": "TheUselessTrials",
|
||||||
|
"webpage_url_domain": "youtube.com",
|
||||||
|
"extractor": "youtube:tab",
|
||||||
|
"extractor_key": "YoutubeTab",
|
||||||
|
"release_year": null,
|
||||||
|
"requested_entries": [],
|
||||||
|
"epoch": 1710800380,
|
||||||
|
"filename": "/app/test/support/files/channel_photos/a.NA",
|
||||||
|
"formats_table": null,
|
||||||
|
"thumbnails_table": "ID Width Height URL\n0 320 88 https://yt3.googleusercontent.com/_yUVmZqHvZZhNOFq2iMUPA82LylawAvYknCrwTE_9_IUQ0htiLPet2FKxE1ArvLAKf0lVsLccQ=w320-fcrop64=1,32b75a57cd48a5a8-k-c0xffffffff-no-nd-rj\n1 320 180 https://yt3.googleusercontent.com/_yUVmZqHvZZhNOFq2iMUPA82LylawAvYknCrwTE_9_IUQ0htiLPet2FKxE1ArvLAKf0lVsLccQ=w320-fcrop64=1,00000000ffffffff-k-c0xffffffff-no-nd-rj\n2 640 175 https://yt3.googleusercontent.com/_yUVmZqHvZZhNOFq2iMUPA82LylawAvYknCrwTE_9_IUQ0htiLPet2FKxE1ArvLAKf0lVsLccQ=w640-fcrop64=1,32b75a57cd48a5a8-k-c0xffffffff-no-nd-rj\n3 854 480 https://yt3.googleusercontent.com/_yUVmZqHvZZhNOFq2iMUPA82LylawAvYknCrwTE_9_IUQ0htiLPet2FKxE1ArvLAKf0lVsLccQ=w854-fcrop64=1,00000000ffffffff-k-c0xffffffff-no-nd-rj\n4 960 263 https://yt3.googleusercontent.com/_yUVmZqHvZZhNOFq2iMUPA82LylawAvYknCrwTE_9_IUQ0htiLPet2FKxE1ArvLAKf0lVsLccQ=w960-fcrop64=1,32b75a57cd48a5a8-k-c0xffffffff-no-nd-rj\n5 1060 175 https://yt3.googleusercontent.com/_yUVmZqHvZZhNOFq2iMUPA82LylawAvYknCrwTE_9_IUQ0htiLPet2FKxE1ArvLAKf0lVsLccQ=w1060-fcrop64=1,00005a57ffffa5a8-k-c0xffffffff-no-nd-rj\n6 1138 188 https://yt3.googleusercontent.com/_yUVmZqHvZZhNOFq2iMUPA82LylawAvYknCrwTE_9_IUQ0htiLPet2FKxE1ArvLAKf0lVsLccQ=w1138-fcrop64=1,00005a57ffffa5a8-k-c0xffffffff-no-nd-rj\n7 1280 351 https://yt3.googleusercontent.com/_yUVmZqHvZZhNOFq2iMUPA82LylawAvYknCrwTE_9_IUQ0htiLPet2FKxE1ArvLAKf0lVsLccQ=w1280-fcrop64=1,32b75a57cd48a5a8-k-c0xffffffff-no-nd-rj\n8 1280 720 https://yt3.googleusercontent.com/_yUVmZqHvZZhNOFq2iMUPA82LylawAvYknCrwTE_9_IUQ0htiLPet2FKxE1ArvLAKf0lVsLccQ=w1280-fcrop64=1,00000000ffffffff-k-c0xffffffff-no-nd-rj\n9 1440 395 https://yt3.googleusercontent.com/_yUVmZqHvZZhNOFq2iMUPA82LylawAvYknCrwTE_9_IUQ0htiLPet2FKxE1ArvLAKf0lVsLccQ=w1440-fcrop64=1,32b75a57cd48a5a8-k-c0xffffffff-no-nd-rj\n10 1707 283 https://yt3.googleusercontent.com/_yUVmZqHvZZhNOFq2iMUPA82LylawAvYknCrwTE_9_IUQ0htiLPet2FKxE1ArvLAKf0lVsLccQ=w1707-fcrop64=1,00005a57ffffa5a8-k-c0xffffffff-no-nd-rj\n11 1920 1080 https://yt3.googleusercontent.com/_yUVmZqHvZZhNOFq2iMUPA82LylawAvYknCrwTE_9_IUQ0htiLPet2FKxE1ArvLAKf0lVsLccQ=w1920-fcrop64=1,00000000ffffffff-k-c0xffffffff-no-nd-rj\n12 2120 351 https://yt3.googleusercontent.com/_yUVmZqHvZZhNOFq2iMUPA82LylawAvYknCrwTE_9_IUQ0htiLPet2FKxE1ArvLAKf0lVsLccQ=w2120-fcrop64=1,00005a57ffffa5a8-k-c0xffffffff-no-nd-rj\n13 2120 1192 https://yt3.googleusercontent.com/_yUVmZqHvZZhNOFq2iMUPA82LylawAvYknCrwTE_9_IUQ0htiLPet2FKxE1ArvLAKf0lVsLccQ=w2120-fcrop64=1,00000000ffffffff-k-c0xffffffff-no-nd-rj\n14 2276 377 https://yt3.googleusercontent.com/_yUVmZqHvZZhNOFq2iMUPA82LylawAvYknCrwTE_9_IUQ0htiLPet2FKxE1ArvLAKf0lVsLccQ=w2276-fcrop64=1,00005a57ffffa5a8-k-c0xffffffff-no-nd-rj\n15 2560 424 https://yt3.googleusercontent.com/_yUVmZqHvZZhNOFq2iMUPA82LylawAvYknCrwTE_9_IUQ0htiLPet2FKxE1ArvLAKf0lVsLccQ=w2560-fcrop64=1,00005a57ffffa5a8-k-c0xffffffff-no-nd-rj\nbanner_uncropped unknown unknown https://yt3.googleusercontent.com/_yUVmZqHvZZhNOFq2iMUPA82LylawAvYknCrwTE_9_IUQ0htiLPet2FKxE1ArvLAKf0lVsLccQ=s0\n17 900 900 https://yt3.googleusercontent.com/ytc/AIdro_kw1KKWzbFmvq6u8WljTbo6QQ318WoAwnjX3AYrjA=s900-c-k-c0x00ffffff-no-rj\navatar_uncropped unknown unknown https://yt3.googleusercontent.com/ytc/AIdro_kw1KKWzbFmvq6u8WljTbo6QQ318WoAwnjX3AYrjA=s0",
|
||||||
|
"subtitles_table": null,
|
||||||
|
"automatic_captions_table": null,
|
||||||
|
"duration_string": null,
|
||||||
|
"autonumber": 0,
|
||||||
|
"video_autonumber": 0,
|
||||||
|
"resolution": null
|
||||||
|
}
|
||||||