[WIP] hooked up basic video downloading; starting work on metadata
This commit is contained in:
parent
a354155203
commit
d8e9f8ce57
10 changed files with 110 additions and 14 deletions
|
|
@ -14,7 +14,8 @@ config :pinchflat,
|
||||||
yt_dlp_executable: System.find_executable("yt-dlp"),
|
yt_dlp_executable: System.find_executable("yt-dlp"),
|
||||||
yt_dlp_runner: Pinchflat.MediaClient.Backends.YtDlp.CommandRunner,
|
yt_dlp_runner: Pinchflat.MediaClient.Backends.YtDlp.CommandRunner,
|
||||||
# TODO: figure this out
|
# TODO: figure this out
|
||||||
media_directory: :not_implemented
|
media_directory: :not_implemented,
|
||||||
|
metadata_directory: Path.join([System.tmp_dir!(), "pinchflat", "metadata"])
|
||||||
|
|
||||||
# Configures the endpoint
|
# Configures the endpoint
|
||||||
config :pinchflat, PinchflatWeb.Endpoint,
|
config :pinchflat, PinchflatWeb.Endpoint,
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,8 @@
|
||||||
import Config
|
import Config
|
||||||
|
|
||||||
config :pinchflat,
|
config :pinchflat,
|
||||||
media_directory: Path.join([System.tmp_dir!(), "yt-dlp"])
|
media_directory: Path.join([File.cwd!(), "tmp", "videos"]),
|
||||||
|
metadata_directory: Path.join([File.cwd!(), "tmp", "metadata"])
|
||||||
|
|
||||||
# Configure your database
|
# Configure your database
|
||||||
config :pinchflat, Pinchflat.Repo,
|
config :pinchflat, Pinchflat.Repo,
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,8 @@ import Config
|
||||||
config :pinchflat,
|
config :pinchflat,
|
||||||
# Specifying backend data here makes mocking and local testing SUPER easy
|
# Specifying backend data here makes mocking and local testing SUPER easy
|
||||||
yt_dlp_executable: Path.join([File.cwd!(), "/test/support/scripts/yt-dlp-mocks/repeater.sh"]),
|
yt_dlp_executable: Path.join([File.cwd!(), "/test/support/scripts/yt-dlp-mocks/repeater.sh"]),
|
||||||
media_directory: Path.join([System.tmp_dir!(), "yt-dlp"])
|
media_directory: Path.join([File.cwd!(), "tmp", "videos"]),
|
||||||
|
metadata_directory: Path.join([File.cwd!(), "tmp", "metadata"])
|
||||||
|
|
||||||
config :pinchflat, Oban, testing: :manual
|
config :pinchflat, Oban, testing: :manual
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,8 @@ defmodule Pinchflat.MediaClient.Backends.YtDlp.CommandRunner do
|
||||||
Runs yt-dlp commands using the `System.cmd/3` function
|
Runs yt-dlp commands using the `System.cmd/3` function
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
require Logger
|
||||||
|
|
||||||
alias Pinchflat.Utils.StringUtils
|
alias Pinchflat.Utils.StringUtils
|
||||||
alias Pinchflat.MediaClient.Backends.BackendCommandRunner
|
alias Pinchflat.MediaClient.Backends.BackendCommandRunner
|
||||||
|
|
||||||
|
|
@ -20,7 +22,9 @@ defmodule Pinchflat.MediaClient.Backends.YtDlp.CommandRunner do
|
||||||
@impl BackendCommandRunner
|
@impl BackendCommandRunner
|
||||||
def run(url, command_opts) do
|
def run(url, command_opts) do
|
||||||
command = backend_executable()
|
command = backend_executable()
|
||||||
formatted_command_opts = parse_options(command_opts) ++ [url]
|
formatted_command_opts = [url] ++ parse_options(command_opts)
|
||||||
|
|
||||||
|
Logger.debug("[yt-dlp] called with: #{Enum.join(formatted_command_opts, " ")}")
|
||||||
|
|
||||||
case System.cmd(command, formatted_command_opts, stderr_to_stdout: true) do
|
case System.cmd(command, formatted_command_opts, stderr_to_stdout: true) do
|
||||||
{output, 0} -> {:ok, output}
|
{output, 0} -> {:ok, output}
|
||||||
|
|
@ -55,7 +59,7 @@ defmodule Pinchflat.MediaClient.Backends.YtDlp.CommandRunner do
|
||||||
end
|
end
|
||||||
|
|
||||||
defp parse_option(arg, acc) when is_binary(arg) do
|
defp parse_option(arg, acc) when is_binary(arg) do
|
||||||
[arg | acc]
|
acc ++ [arg]
|
||||||
end
|
end
|
||||||
|
|
||||||
defp backend_executable do
|
defp backend_executable do
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,7 @@
|
||||||
|
defmodule Pinchflat.MediaClient.Backends.YtDlp.MetadataParser do
|
||||||
|
def parse_for_media_item(metadata) do
|
||||||
|
%{
|
||||||
|
video_filepath: metadata["filepath"]
|
||||||
|
}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
@ -3,17 +3,31 @@ defmodule Pinchflat.MediaClient.Backends.YtDlp.Video do
|
||||||
Contains utilities for working with singular videos
|
Contains utilities for working with singular videos
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
alias Pinchflat.Utils.StringUtils
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
Downloads a single video (and possible metadata) directly to its
|
Downloads a single video (and possible metadata) directly to its
|
||||||
final destination. Returns the parsed JSON output from yt-dlp.
|
final destination. Returns the parsed JSON output from yt-dlp.
|
||||||
|
|
||||||
|
It writes to file and then immediately reads from it since printing
|
||||||
|
to stdout also contains any warnings/errors that may have occurred,
|
||||||
|
even if the command is otherwise successful (which creates invalid
|
||||||
|
JSON).
|
||||||
|
|
||||||
Returns {:ok, map()} | {:error, any, ...}.
|
Returns {:ok, map()} | {:error, any, ...}.
|
||||||
|
|
||||||
|
TODO: test changes (writing to then reading from a file)
|
||||||
"""
|
"""
|
||||||
def download(url, command_opts \\ []) do
|
def download(url, command_opts \\ []) do
|
||||||
opts = [:no_simulate, print: "%()j"] ++ command_opts
|
json_output_path = Path.join([metadata_directory(), "#{StringUtils.random_string()}.json"])
|
||||||
|
# These must stay in exactly this order, hence why I'm giving it its own variable.
|
||||||
|
# Also, can't use RAM file since yt-dlp needs a concrete filepath.
|
||||||
|
print_to_file_opts = [{:print_to_file, "after_move:%()j"}, json_output_path]
|
||||||
|
opts = [:no_simulate] ++ print_to_file_opts ++ command_opts
|
||||||
|
|
||||||
with {:ok, output} <- backend_runner().run(url, opts),
|
with {:ok, _} <- backend_runner().run(url, opts),
|
||||||
{:ok, parsed_json} <- Phoenix.json_library().decode(output) do
|
{:ok, file_body} <- File.read(json_output_path),
|
||||||
|
{:ok, parsed_json} <- Phoenix.json_library().decode(file_body) do
|
||||||
{:ok, parsed_json}
|
{:ok, parsed_json}
|
||||||
else
|
else
|
||||||
err -> err
|
err -> err
|
||||||
|
|
@ -23,4 +37,8 @@ defmodule Pinchflat.MediaClient.Backends.YtDlp.Video do
|
||||||
defp backend_runner do
|
defp backend_runner do
|
||||||
Application.get_env(:pinchflat, :yt_dlp_runner)
|
Application.get_env(:pinchflat, :yt_dlp_runner)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
defp metadata_directory do
|
||||||
|
Application.get_env(:pinchflat, :metadata_directory)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -8,15 +8,19 @@ defmodule Pinchflat.MediaClient.VideoDownloader do
|
||||||
it open-ish for future expansion (just in case).
|
it open-ish for future expansion (just in case).
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
alias Pinchflat.Repo
|
||||||
|
alias Pinchflat.Media
|
||||||
|
alias Pinchflat.Media.MediaItem
|
||||||
alias Pinchflat.Profiles.MediaProfile
|
alias Pinchflat.Profiles.MediaProfile
|
||||||
|
|
||||||
alias Pinchflat.MediaClient.Backends.YtDlp.Video, as: YtDlpVideo
|
alias Pinchflat.MediaClient.Backends.YtDlp.Video, as: YtDlpVideo
|
||||||
alias Pinchflat.Profiles.Options.YtDlp.OptionBuilder, as: YtDlpOptionBuilder
|
alias Pinchflat.Profiles.Options.YtDlp.OptionBuilder, as: YtDlpOptionBuilder
|
||||||
|
alias Pinchflat.MediaClient.Backends.YtDlp.MetadataParser, as: YtDlpMetadataParser
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
Downloads a single video based on the settings in the given media profile.
|
Downloads a single video based on the settings in the given media profile.
|
||||||
|
|
||||||
Returns {:ok, %ChannelDetails{}} | {:error, any, ...}.
|
Returns {:ok, map()} | {:error, any, ...}.
|
||||||
"""
|
"""
|
||||||
def download_for_media_profile(url, %MediaProfile{} = media_profile, backend \\ :yt_dlp) do
|
def download_for_media_profile(url, %MediaProfile{} = media_profile, backend \\ :yt_dlp) do
|
||||||
option_builder = option_builder(backend)
|
option_builder = option_builder(backend)
|
||||||
|
|
@ -26,6 +30,29 @@ defmodule Pinchflat.MediaClient.VideoDownloader do
|
||||||
video_backend.download(url, options)
|
video_backend.download(url, options)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
TODO: test
|
||||||
|
TODO: consider removing the above function? I don't know if it's actually useful
|
||||||
|
TODO: save metadata filepath to media item record
|
||||||
|
TODO: consider saving the output JSON to the database instead of the filesystem.
|
||||||
|
reason: would make updating metadata easier (no orphans). Also queryable.
|
||||||
|
"""
|
||||||
|
def download_for_media_item(%MediaItem{} = media_item, backend \\ :yt_dlp) do
|
||||||
|
item_with_preloads = Repo.preload(media_item, channel: :media_profile)
|
||||||
|
media_profile = item_with_preloads.channel.media_profile
|
||||||
|
|
||||||
|
case download_for_media_profile(media_item.media_id, media_profile, backend) do
|
||||||
|
{:ok, parsed_json} ->
|
||||||
|
parser = metadata_parser(backend)
|
||||||
|
parsed_attrs = parser.parse_for_media_item(parsed_json)
|
||||||
|
|
||||||
|
Media.update_media_item(media_item, parsed_attrs)
|
||||||
|
|
||||||
|
err ->
|
||||||
|
err
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
defp option_builder(backend) do
|
defp option_builder(backend) do
|
||||||
case backend do
|
case backend do
|
||||||
:yt_dlp -> YtDlpOptionBuilder
|
:yt_dlp -> YtDlpOptionBuilder
|
||||||
|
|
@ -37,4 +64,10 @@ defmodule Pinchflat.MediaClient.VideoDownloader do
|
||||||
:yt_dlp -> YtDlpVideo
|
:yt_dlp -> YtDlpVideo
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
defp metadata_parser(backend) do
|
||||||
|
case backend do
|
||||||
|
:yt_dlp -> YtDlpMetadataParser
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -24,15 +24,10 @@ defmodule Pinchflat.Profiles.Options.YtDlp.OptionBuilder do
|
||||||
# see: https://github.com/yt-dlp/yt-dlp#output-template
|
# see: https://github.com/yt-dlp/yt-dlp#output-template
|
||||||
{:ok,
|
{:ok,
|
||||||
[
|
[
|
||||||
:write_thumbnail,
|
|
||||||
:write_subs,
|
|
||||||
:embed_metadata,
|
:embed_metadata,
|
||||||
:embed_thumbnail,
|
:embed_thumbnail,
|
||||||
:embed_subs,
|
:embed_subs,
|
||||||
:write_info_json,
|
|
||||||
:write_auto_subs,
|
|
||||||
:no_progress,
|
:no_progress,
|
||||||
convert_thumbnails: "jpg",
|
|
||||||
sub_langs: "en.*",
|
sub_langs: "en.*",
|
||||||
output: Path.join(base_directory(), output_path)
|
output: Path.join(base_directory(), output_path)
|
||||||
]}
|
]}
|
||||||
|
|
|
||||||
|
|
@ -11,4 +11,13 @@ defmodule Pinchflat.Utils.StringUtils do
|
||||||
|> String.replace(~r/[\s_]/, "-")
|
|> String.replace(~r/[\s_]/, "-")
|
||||||
|> String.downcase()
|
|> String.downcase()
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
TODO: test
|
||||||
|
"""
|
||||||
|
def random_string(length \\ 32) do
|
||||||
|
:crypto.strong_rand_bytes(length)
|
||||||
|
|> Base.encode16(case: :lower)
|
||||||
|
|> String.slice(0..(length - 1))
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
27
lib/pinchflat/workers/video_download_worker.ex
Normal file
27
lib/pinchflat/workers/video_download_worker.ex
Normal file
|
|
@ -0,0 +1,27 @@
|
||||||
|
defmodule Pinchflat.Workers.VideoDownloadWorker do
|
||||||
|
@moduledoc false
|
||||||
|
|
||||||
|
use Oban.Worker,
|
||||||
|
queue: :media_fetching,
|
||||||
|
unique: [period: :infinity, states: [:available, :scheduled, :retryable]],
|
||||||
|
tags: ["media_itwm", "media_fetching"]
|
||||||
|
|
||||||
|
alias Pinchflat.Media
|
||||||
|
alias Pinchflat.MediaClient.VideoDownloader
|
||||||
|
|
||||||
|
@impl Oban.Worker
|
||||||
|
@doc """
|
||||||
|
TODO: test
|
||||||
|
"""
|
||||||
|
def perform(%Oban.Job{args: %{"id" => media_item_id}}) do
|
||||||
|
media_item = Media.get_media_item!(media_item_id)
|
||||||
|
|
||||||
|
case VideoDownloader.download_for_media_item(media_item) do
|
||||||
|
{:ok, _} ->
|
||||||
|
{:ok, media_item}
|
||||||
|
|
||||||
|
err ->
|
||||||
|
err
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
Loading…
Reference in a new issue