Addressed a bunch of TODO comments
This commit is contained in:
parent
b29dec7294
commit
5c76d9a9d6
14 changed files with 158 additions and 88 deletions
3
ideas.md
Normal file
3
ideas.md
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
- Write media datbase ID as metadata/to file/whatever so it gives us an option to retroactively match media to the DB down the line. Useful if someone moves the media without informing the UI
|
||||||
|
- Use a UUID for the media database ID (or at least alongside it)
|
||||||
|
- Look into this and its recommended plugins https://hexdocs.pm/ex_check/readme.html
|
||||||
|
|
@ -11,7 +11,7 @@ defmodule Pinchflat.Downloader.Backends.YtDlp.CommandRunner do
|
||||||
@doc """
|
@doc """
|
||||||
Runs a yt-dlp command and returns the string output
|
Runs a yt-dlp command and returns the string output
|
||||||
|
|
||||||
# TODO: deduplicate command opts, keeping the last one on conflict
|
# IDEA: deduplicate command opts, keeping the last one on conflict
|
||||||
although possibly not needed (and a LOT easier) if yt-dlp
|
although possibly not needed (and a LOT easier) if yt-dlp
|
||||||
just ignores duplicate options (ie: look into that)
|
just ignores duplicate options (ie: look into that)
|
||||||
"""
|
"""
|
||||||
|
|
|
||||||
|
|
@ -4,23 +4,16 @@ defmodule Pinchflat.Downloader.Backends.YtDlp.Video do
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
Downloads a single video (and possible metadata) to the tmp directory.
|
Downloads a single video (and possible metadata) directly to its
|
||||||
Videos are downloaded in the following format:
|
final destination. Returns the parsed JSON output from yt-dlp.
|
||||||
`tmp/yt-dlp/<video_id>/<video_id>.<ext>`
|
|
||||||
|
|
||||||
The video will be moved to its final destination... elsewhere
|
|
||||||
# TODO: update these docs when I figure out a module to move videos
|
|
||||||
# TODO: test
|
|
||||||
# NOTE: maybe instead of moving it to the tempdir, I can just download it
|
|
||||||
to the final destination by using the `output` option. The
|
|
||||||
parser could be updated to generate a value for the output option.
|
|
||||||
This way, advanced users can just the yt-dlp output syntax and
|
|
||||||
newer users can use the easier liquid-like syntax.
|
|
||||||
"""
|
"""
|
||||||
def download(url, command_opts \\ []) do
|
def download(url, command_opts \\ []) do
|
||||||
# TODO: if this stays this simple, consider not abstracting it
|
opts = [:no_simulate, :dump_json] ++ command_opts
|
||||||
# HOWEVER - this module does provide clarity of intent so maybe keep?
|
|
||||||
backend_runner().run(url, command_opts)
|
case backend_runner().run(url, opts) do
|
||||||
|
{:ok, output} -> Phoenix.json_library().decode(output)
|
||||||
|
err -> err
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
defp backend_runner do
|
defp backend_runner do
|
||||||
|
|
|
||||||
|
|
@ -8,16 +8,15 @@ defmodule Pinchflat.Downloader.VideoDownloader do
|
||||||
it open-ish for future expansion (just in case).
|
it open-ish for future expansion (just in case).
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
alias Pinchflat.Profiles.MediaProfile
|
||||||
|
|
||||||
alias Pinchflat.Downloader.Backends.YtDlp.Video, as: YtDlpVideo
|
alias Pinchflat.Downloader.Backends.YtDlp.Video, as: YtDlpVideo
|
||||||
alias Pinchflat.Profiles.Options.YtDlp.OptionBuilder, as: YtDlpOptionBuilder
|
alias Pinchflat.Profiles.Options.YtDlp.OptionBuilder, as: YtDlpOptionBuilder
|
||||||
|
|
||||||
@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.
|
||||||
|
|
||||||
# TODO: implement media profiles - so far this is a glorified mock
|
|
||||||
# TODO: test
|
|
||||||
"""
|
"""
|
||||||
def download_for_media_profile(url, 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)
|
||||||
video_backend = video_backend(backend)
|
video_backend = video_backend(backend)
|
||||||
{:ok, options} = option_builder.build(media_profile)
|
{:ok, options} = option_builder.build(media_profile)
|
||||||
|
|
@ -25,13 +24,13 @@ defmodule Pinchflat.Downloader.VideoDownloader do
|
||||||
video_backend.download(url, options)
|
video_backend.download(url, options)
|
||||||
end
|
end
|
||||||
|
|
||||||
def option_builder(backend) do
|
defp option_builder(backend) do
|
||||||
case backend do
|
case backend do
|
||||||
:yt_dlp -> YtDlpOptionBuilder
|
:yt_dlp -> YtDlpOptionBuilder
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def video_backend(backend) do
|
defp video_backend(backend) do
|
||||||
case backend do
|
case backend do
|
||||||
:yt_dlp -> YtDlpVideo
|
:yt_dlp -> YtDlpVideo
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -9,13 +9,7 @@ defmodule Pinchflat.Profiles do
|
||||||
alias Pinchflat.Profiles.MediaProfile
|
alias Pinchflat.Profiles.MediaProfile
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
Returns the list of media_profiles.
|
Returns the list of media_profiles. Returns [%MediaProfile{}, ...]
|
||||||
|
|
||||||
## Examples
|
|
||||||
|
|
||||||
iex> list_media_profiles()
|
|
||||||
[%MediaProfile{}, ...]
|
|
||||||
|
|
||||||
"""
|
"""
|
||||||
def list_media_profiles do
|
def list_media_profiles do
|
||||||
Repo.all(MediaProfile)
|
Repo.all(MediaProfile)
|
||||||
|
|
@ -24,30 +18,12 @@ defmodule Pinchflat.Profiles do
|
||||||
@doc """
|
@doc """
|
||||||
Gets a single media_profile.
|
Gets a single media_profile.
|
||||||
|
|
||||||
Raises `Ecto.NoResultsError` if the Media profile does not exist.
|
Returns %MediaProfile{}. Raises `Ecto.NoResultsError` if the Media profile does not exist.
|
||||||
|
|
||||||
## Examples
|
|
||||||
|
|
||||||
iex> get_media_profile!(123)
|
|
||||||
%MediaProfile{}
|
|
||||||
|
|
||||||
iex> get_media_profile!(456)
|
|
||||||
** (Ecto.NoResultsError)
|
|
||||||
|
|
||||||
"""
|
"""
|
||||||
def get_media_profile!(id), do: Repo.get!(MediaProfile, id)
|
def get_media_profile!(id), do: Repo.get!(MediaProfile, id)
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
Creates a media_profile.
|
Creates a media_profile. Returns {:ok, %MediaProfile{}} | {:error, %Ecto.Changeset{}}
|
||||||
|
|
||||||
## Examples
|
|
||||||
|
|
||||||
iex> create_media_profile(%{field: value})
|
|
||||||
{:ok, %MediaProfile{}}
|
|
||||||
|
|
||||||
iex> create_media_profile(%{field: bad_value})
|
|
||||||
{:error, %Ecto.Changeset{}}
|
|
||||||
|
|
||||||
"""
|
"""
|
||||||
def create_media_profile(attrs \\ %{}) do
|
def create_media_profile(attrs \\ %{}) do
|
||||||
%MediaProfile{}
|
%MediaProfile{}
|
||||||
|
|
@ -56,16 +32,7 @@ defmodule Pinchflat.Profiles do
|
||||||
end
|
end
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
Updates a media_profile.
|
Updates a media_profile. Returns {:ok, %MediaProfile{}} | {:error, %Ecto.Changeset{}}
|
||||||
|
|
||||||
## Examples
|
|
||||||
|
|
||||||
iex> update_media_profile(media_profile, %{field: new_value})
|
|
||||||
{:ok, %MediaProfile{}}
|
|
||||||
|
|
||||||
iex> update_media_profile(media_profile, %{field: bad_value})
|
|
||||||
{:error, %Ecto.Changeset{}}
|
|
||||||
|
|
||||||
"""
|
"""
|
||||||
def update_media_profile(%MediaProfile{} = media_profile, attrs) do
|
def update_media_profile(%MediaProfile{} = media_profile, attrs) do
|
||||||
media_profile
|
media_profile
|
||||||
|
|
@ -74,16 +41,7 @@ defmodule Pinchflat.Profiles do
|
||||||
end
|
end
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
Deletes a media_profile.
|
Deletes a media_profile. Returns {:ok, %MediaProfile{}} | {:error, %Ecto.Changeset{}}
|
||||||
|
|
||||||
## Examples
|
|
||||||
|
|
||||||
iex> delete_media_profile(media_profile)
|
|
||||||
{:ok, %MediaProfile{}}
|
|
||||||
|
|
||||||
iex> delete_media_profile(media_profile)
|
|
||||||
{:error, %Ecto.Changeset{}}
|
|
||||||
|
|
||||||
"""
|
"""
|
||||||
def delete_media_profile(%MediaProfile{} = media_profile) do
|
def delete_media_profile(%MediaProfile{} = media_profile) do
|
||||||
Repo.delete(media_profile)
|
Repo.delete(media_profile)
|
||||||
|
|
@ -91,12 +49,6 @@ defmodule Pinchflat.Profiles do
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
Returns an `%Ecto.Changeset{}` for tracking media_profile changes.
|
Returns an `%Ecto.Changeset{}` for tracking media_profile changes.
|
||||||
|
|
||||||
## Examples
|
|
||||||
|
|
||||||
iex> change_media_profile(media_profile)
|
|
||||||
%Ecto.Changeset{data: %MediaProfile{}}
|
|
||||||
|
|
||||||
"""
|
"""
|
||||||
def change_media_profile(%MediaProfile{} = media_profile, attrs \\ %{}) do
|
def change_media_profile(%MediaProfile{} = media_profile, attrs \\ %{}) do
|
||||||
MediaProfile.changeset(media_profile, attrs)
|
MediaProfile.changeset(media_profile, attrs)
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,8 @@
|
||||||
defmodule Pinchflat.Profiles.MediaProfile do
|
defmodule Pinchflat.Profiles.MediaProfile do
|
||||||
|
@moduledoc """
|
||||||
|
A media profile is a set of settings that can be applied to many media sources
|
||||||
|
"""
|
||||||
|
|
||||||
use Ecto.Schema
|
use Ecto.Schema
|
||||||
import Ecto.Changeset
|
import Ecto.Changeset
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,20 +2,19 @@ defmodule Pinchflat.Profiles.Options.YtDlp.OptionBuilder do
|
||||||
@moduledoc """
|
@moduledoc """
|
||||||
Builds the options for yt-dlp based on the given media profile.
|
Builds the options for yt-dlp based on the given media profile.
|
||||||
|
|
||||||
TODO: probably make this a behaviour so I can add other backends later
|
IDEA: consider making this a behaviour so I can add other backends later
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
alias Pinchflat.Profiles.MediaProfile
|
||||||
alias Pinchflat.Profiles.Options.YtDlp.OutputPathBuilder
|
alias Pinchflat.Profiles.Options.YtDlp.OutputPathBuilder
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
Builds the options for yt-dlp based on the given media profile.
|
Builds the options for yt-dlp based on the given media profile.
|
||||||
|
|
||||||
TODO: add a guard to ensure argument is a media profile
|
IDEA: consider adding the ability to pass in a second argument to override
|
||||||
TODO: consider adding the ability to pass in a second argument to override
|
|
||||||
these options
|
these options
|
||||||
TODO: test
|
|
||||||
"""
|
"""
|
||||||
def build(media_profile) do
|
def build(%MediaProfile{} = media_profile) do
|
||||||
{:ok, output_path} = OutputPathBuilder.build(media_profile.output_path_template)
|
{:ok, output_path} = OutputPathBuilder.build(media_profile.output_path_template)
|
||||||
|
|
||||||
# NOTE: I'll be hardcoding most things for now (esp. options to help me test) -
|
# NOTE: I'll be hardcoding most things for now (esp. options to help me test) -
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@ defmodule Pinchflat.Profiles.Options.YtDlp.OutputPathBuilder do
|
||||||
@moduledoc """
|
@moduledoc """
|
||||||
Builds yt-dlp-friendly output paths for downloaded media
|
Builds yt-dlp-friendly output paths for downloaded media
|
||||||
|
|
||||||
TODO: probably make this a behaviour so I can add other backends later
|
IDEA: consider making this a behaviour so I can add other backends later
|
||||||
"""
|
"""
|
||||||
|
|
||||||
alias Pinchflat.RenderedString.Parser, as: TemplateParser
|
alias Pinchflat.RenderedString.Parser, as: TemplateParser
|
||||||
|
|
@ -12,8 +12,6 @@ defmodule Pinchflat.Profiles.Options.YtDlp.OutputPathBuilder do
|
||||||
|
|
||||||
Translates liquid-style templates into yt-dlp-style templates,
|
Translates liquid-style templates into yt-dlp-style templates,
|
||||||
leaving yt-dlp syntax intact.
|
leaving yt-dlp syntax intact.
|
||||||
|
|
||||||
TODO: test
|
|
||||||
"""
|
"""
|
||||||
def build(template_string) do
|
def build(template_string) do
|
||||||
TemplateParser.parse(template_string, full_yt_dlp_options_map())
|
TemplateParser.parse(template_string, full_yt_dlp_options_map())
|
||||||
|
|
@ -58,9 +56,9 @@ defmodule Pinchflat.Profiles.Options.YtDlp.OutputPathBuilder do
|
||||||
defp custom_yt_dlp_option_map do
|
defp custom_yt_dlp_option_map do
|
||||||
%{
|
%{
|
||||||
# Individual parts of the upload date
|
# Individual parts of the upload date
|
||||||
"upload_year" => "%(upload_date>%Y)s",
|
"upload_year" => "%(upload_date>%Y)S",
|
||||||
"upload_month" => "%(upload_date>%m)s",
|
"upload_month" => "%(upload_date>%m)S",
|
||||||
"upload_day" => "%(upload_date>%d)s"
|
"upload_day" => "%(upload_date>%d)S"
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,25 @@
|
||||||
|
defmodule Pinchflat.Downloader.Backends.YtDlp.OutputPathBuilderTest do
|
||||||
|
use ExUnit.Case, async: true
|
||||||
|
|
||||||
|
alias Pinchflat.Profiles.Options.YtDlp.OutputPathBuilder
|
||||||
|
|
||||||
|
describe "build/1" do
|
||||||
|
test "it expands 'standard' curly brace variables in the template" do
|
||||||
|
assert {:ok, res} = OutputPathBuilder.build("/videos/{{ title }}.{{ ext }}")
|
||||||
|
|
||||||
|
assert res == "/videos/%(title)S.%(ext)S"
|
||||||
|
end
|
||||||
|
|
||||||
|
test "it expands 'custom' curly brace variables in the template" do
|
||||||
|
assert {:ok, res} = OutputPathBuilder.build("/videos/{{ upload_year }}.{{ ext }}")
|
||||||
|
|
||||||
|
assert res == "/videos/%(upload_date>%Y)S.%(ext)S"
|
||||||
|
end
|
||||||
|
|
||||||
|
test "it leaves yt-dlp variables alone" do
|
||||||
|
assert {:ok, res} = OutputPathBuilder.build("/videos/%(title)s.%(ext)s")
|
||||||
|
|
||||||
|
assert res == "/videos/%(title)s.%(ext)s"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
@ -2,7 +2,7 @@ defmodule Pinchflat.Downloader.Backends.YtDlp.VideoCollectionTest do
|
||||||
use ExUnit.Case, async: true
|
use ExUnit.Case, async: true
|
||||||
import Mox
|
import Mox
|
||||||
|
|
||||||
alias Pinchflat.Downloader.Backends.YtDlp.VideoCollection, as: VideoCollection
|
alias Pinchflat.Downloader.Backends.YtDlp.VideoCollection
|
||||||
|
|
||||||
@channel_url "https://www.youtube.com/@TheUselessTrials"
|
@channel_url "https://www.youtube.com/@TheUselessTrials"
|
||||||
|
|
||||||
|
|
|
||||||
44
test/pinchflat/downloader/backends/yt_dlp/video_test.exs
Normal file
44
test/pinchflat/downloader/backends/yt_dlp/video_test.exs
Normal file
|
|
@ -0,0 +1,44 @@
|
||||||
|
defmodule Pinchflat.Downloader.Backends.YtDlp.VideoTest do
|
||||||
|
use ExUnit.Case, async: true
|
||||||
|
import Mox
|
||||||
|
|
||||||
|
alias Pinchflat.Downloader.Backends.YtDlp.Video
|
||||||
|
|
||||||
|
@video_url "https://www.youtube.com/watch?v=TiZPUDkDYbk"
|
||||||
|
|
||||||
|
setup :verify_on_exit!
|
||||||
|
|
||||||
|
describe "download/2" do
|
||||||
|
test "it calls the backend runner with the expected arguments" do
|
||||||
|
expect(CommandRunnerMock, :run, fn @video_url, opts ->
|
||||||
|
assert opts == [:no_simulate, :dump_json]
|
||||||
|
|
||||||
|
{:ok, "{}"}
|
||||||
|
end)
|
||||||
|
|
||||||
|
assert {:ok, _} = Video.download(@video_url)
|
||||||
|
end
|
||||||
|
|
||||||
|
test "it passes along additional options" do
|
||||||
|
expect(CommandRunnerMock, :run, fn _url, opts ->
|
||||||
|
assert opts == [:no_simulate, :dump_json, :custom_arg]
|
||||||
|
|
||||||
|
{:ok, "{}"}
|
||||||
|
end)
|
||||||
|
|
||||||
|
assert {:ok, _} = Video.download(@video_url, [:custom_arg])
|
||||||
|
end
|
||||||
|
|
||||||
|
test "it parses the output as JSON" do
|
||||||
|
expect(CommandRunnerMock, :run, fn _url, _opts -> {:ok, "{\"title\": \"Test\"}"} end)
|
||||||
|
|
||||||
|
assert {:ok, %{"title" => "Test"}} = Video.download(@video_url)
|
||||||
|
end
|
||||||
|
|
||||||
|
test "it directly passes along any errors" do
|
||||||
|
expect(CommandRunnerMock, :run, fn _url, _opts -> {:error, "Big issue", 1} end)
|
||||||
|
|
||||||
|
assert {:error, "Big issue", 1} = Video.download(@video_url)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
35
test/pinchflat/downloader/video_downloader_test.exs
Normal file
35
test/pinchflat/downloader/video_downloader_test.exs
Normal file
|
|
@ -0,0 +1,35 @@
|
||||||
|
defmodule Pinchflat.Downloader.VideoDownloaderTest do
|
||||||
|
use ExUnit.Case, async: true
|
||||||
|
import Mox
|
||||||
|
|
||||||
|
alias Pinchflat.Profiles.MediaProfile
|
||||||
|
alias Pinchflat.Downloader.VideoDownloader
|
||||||
|
|
||||||
|
@video_url "https://www.youtube.com/watch?v=TiZPUDkDYbk"
|
||||||
|
@media_profile %MediaProfile{
|
||||||
|
output_path_template: "videos/{{ title }}.%(ext)s"
|
||||||
|
}
|
||||||
|
|
||||||
|
setup :verify_on_exit!
|
||||||
|
|
||||||
|
describe "download_for_media_profile/3" do
|
||||||
|
test "it calls the backend runner with the arguments built from the media profile" do
|
||||||
|
expect(CommandRunnerMock, :run, fn @video_url, opts ->
|
||||||
|
assert :no_simulate in opts
|
||||||
|
assert :dump_json in opts
|
||||||
|
assert {:output, "/tmp/yt-dlp/videos/%(title)S.%(ext)s"} in opts
|
||||||
|
|
||||||
|
{:ok, "{}"}
|
||||||
|
end)
|
||||||
|
|
||||||
|
assert {:ok, _} = VideoDownloader.download_for_media_profile(@video_url, @media_profile)
|
||||||
|
end
|
||||||
|
|
||||||
|
test "it returns the parsed JSON output" do
|
||||||
|
expect(CommandRunnerMock, :run, fn _url, _opts -> {:ok, "{\"title\": \"Test\"}"} end)
|
||||||
|
|
||||||
|
assert {:ok, %{"title" => "Test"}} =
|
||||||
|
VideoDownloader.download_for_media_profile(@video_url, @media_profile)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
@ -0,0 +1,18 @@
|
||||||
|
defmodule Pinchflat.Profiles.Options.YtDlp.OptionBuilderTest do
|
||||||
|
use ExUnit.Case, async: true
|
||||||
|
|
||||||
|
alias Pinchflat.Profiles.MediaProfile
|
||||||
|
alias Pinchflat.Profiles.Options.YtDlp.OptionBuilder
|
||||||
|
|
||||||
|
@media_profile %MediaProfile{
|
||||||
|
output_path_template: "videos/{{ title }}.%(ext)s"
|
||||||
|
}
|
||||||
|
|
||||||
|
describe "build/1" do
|
||||||
|
test "it generates an expanded output path based on the given template" do
|
||||||
|
assert {:ok, res} = OptionBuilder.build(@media_profile)
|
||||||
|
|
||||||
|
assert {:output, "/tmp/yt-dlp/videos/%(title)S.%(ext)s"} in res
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
defmodule Pinchflat.Utils.StringUtilsTest do
|
defmodule Pinchflat.Utils.StringUtilsTest do
|
||||||
use ExUnit.Case, async: true
|
use ExUnit.Case, async: true
|
||||||
|
|
||||||
alias Pinchflat.Utils.StringUtils, as: StringUtils
|
alias Pinchflat.Utils.StringUtils
|
||||||
|
|
||||||
describe "to_kebab_case/1" do
|
describe "to_kebab_case/1" do
|
||||||
test "converts a space-delimited string to kebab-case" do
|
test "converts a space-delimited string to kebab-case" do
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue