Video filepath parser (#6)
* Restructured files; Added parser placeholder * More restructuring * Added basic parser for hydrating template strings * Improved docs * More docs
This commit is contained in:
parent
bad3e10fea
commit
a4f5024d8f
13 changed files with 140 additions and 11 deletions
|
|
@ -12,7 +12,7 @@ config :pinchflat,
|
|||
generators: [timestamp_type: :utc_datetime],
|
||||
# Specifying backend data here makes mocking and local testing SUPER easy
|
||||
yt_dlp_executable: System.find_executable("yt-dlp"),
|
||||
yt_dlp_runner: Pinchflat.DownloaderBackends.YtDlp.CommandRunner
|
||||
yt_dlp_runner: Pinchflat.Downloader.Backends.YtDlp.CommandRunner
|
||||
|
||||
# Configures the endpoint
|
||||
config :pinchflat, PinchflatWeb.Endpoint,
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
defmodule Pinchflat.DownloaderBackends.BackendCommandRunner do
|
||||
defmodule Pinchflat.Downloader.Backends.BackendCommandRunner do
|
||||
@moduledoc """
|
||||
A behaviour for running CLI commands against a downloader backend
|
||||
"""
|
||||
|
|
@ -1,10 +1,10 @@
|
|||
defmodule Pinchflat.DownloaderBackends.YtDlp.CommandRunner do
|
||||
defmodule Pinchflat.Downloader.Backends.YtDlp.CommandRunner do
|
||||
@moduledoc """
|
||||
Runs yt-dlp commands using the `System.cmd/3` function
|
||||
"""
|
||||
|
||||
alias Pinchflat.Utils.StringUtils
|
||||
alias Pinchflat.DownloaderBackends.BackendCommandRunner
|
||||
alias Pinchflat.Downloader.Backends.BackendCommandRunner
|
||||
|
||||
@behaviour BackendCommandRunner
|
||||
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
defmodule Pinchflat.DownloaderBackends.YtDlp.VideoCollection do
|
||||
defmodule Pinchflat.Downloader.Backends.YtDlp.VideoCollection do
|
||||
@moduledoc """
|
||||
Contains utilities for working with collections of videos (ie: channels, playlists)
|
||||
"""
|
||||
52
lib/pinchflat/rendered_string/base.ex
Normal file
52
lib/pinchflat/rendered_string/base.ex
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
defmodule Pinchflat.RenderedString.Base do
|
||||
@moduledoc """
|
||||
A base module for parsing rendered strings, designed as a macro to be used
|
||||
in other modules. See https://elixirforum.com/t/help-to-parse-a-template-with-nimbleparsec/47980
|
||||
|
||||
NOTE: if the needs here get any more complicated, look into using a Liquid
|
||||
template parser. No need to reinvent the wheel any more than I already have.
|
||||
|
||||
NOTE: this is effectively tested by the `Pinchflat.RenderedString.Parser`'s tests
|
||||
"""
|
||||
|
||||
defmacro __using__(_opts) do
|
||||
quote location: :keep do
|
||||
import NimbleParsec
|
||||
|
||||
opening_tag = string("{{")
|
||||
closing_tag = string("}}")
|
||||
optional_whitespaces = ascii_string(~c[ \t\n\r], min: 0)
|
||||
|
||||
# Capture everything up to the opening object
|
||||
text =
|
||||
lookahead_not(opening_tag)
|
||||
# ... as long as it's a character
|
||||
|> utf8_char([])
|
||||
# ... and there's at least one character
|
||||
|> times(min: 1)
|
||||
# ... and then convert it to a string
|
||||
|> reduce({List, :to_string, []})
|
||||
# ... finally bag it and tag it
|
||||
|> unwrap_and_tag(:text)
|
||||
|
||||
identifier =
|
||||
utf8_string([?a..?z, ?A..?Z, ?_, ?0..?9], min: 1)
|
||||
|> reduce({Enum, :join, []})
|
||||
|> unwrap_and_tag(:identifier)
|
||||
|
||||
defparsecp(:expression, identifier)
|
||||
|
||||
# when spotting interpolation, ignore the opening tag and any whitespace
|
||||
interpolation =
|
||||
ignore(concat(opening_tag, optional_whitespaces))
|
||||
# ... then parse the expression (identifier)
|
||||
|> parsec(:expression)
|
||||
# ... then ignore any whitespace and the closing tags after the expression
|
||||
|> ignore(concat(optional_whitespaces, closing_tag))
|
||||
# ... once again we bag it and tag it
|
||||
|> unwrap_and_tag(:interpolation)
|
||||
|
||||
defparsec(:do_parse, choice([interpolation, text]) |> repeat() |> eos())
|
||||
end
|
||||
end
|
||||
end
|
||||
35
lib/pinchflat/rendered_string/parser.ex
Normal file
35
lib/pinchflat/rendered_string/parser.ex
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
defmodule Pinchflat.RenderedString.Parser do
|
||||
@moduledoc """
|
||||
Parses liquid-ish-style strings into a rendered string
|
||||
|
||||
Used for turning filepath templates into real filepaths
|
||||
"""
|
||||
|
||||
use Pinchflat.RenderedString.Base
|
||||
|
||||
@doc """
|
||||
Parses a string into a rendered string, using the provided variables.
|
||||
|
||||
Variable identifiers are surrounded by {{ and }}. The variable keys MUST be strings.
|
||||
If an identifier is not found in the provided variables, it will be removed from the string.
|
||||
"""
|
||||
def parse(string, variables) do
|
||||
# `do_parse` comes from `RenderedString.Base`
|
||||
case do_parse(string) do
|
||||
{:ok, parsed, _, _, _, _} ->
|
||||
{:ok, build_string(parsed, variables)}
|
||||
|
||||
{:error, message, _, _, _, _} ->
|
||||
{:error, message}
|
||||
end
|
||||
end
|
||||
|
||||
defp build_string(parsed, variables) do
|
||||
Enum.reduce(parsed, "", fn element, acc ->
|
||||
case element do
|
||||
{:text, text} -> acc <> text
|
||||
{:interpolation, {:identifier, identifier}} -> acc <> to_string(variables[identifier])
|
||||
end
|
||||
end)
|
||||
end
|
||||
end
|
||||
3
mix.exs
3
mix.exs
|
|
@ -52,7 +52,8 @@ defmodule Pinchflat.MixProject do
|
|||
{:dns_cluster, "~> 0.1.1"},
|
||||
{:plug_cowboy, "~> 2.5"},
|
||||
{:mox, "~> 1.0", only: :test},
|
||||
{:credo, "~> 1.7", only: [:dev, :test], runtime: false}
|
||||
{:credo, "~> 1.7", only: [:dev, :test], runtime: false},
|
||||
{:nimble_parsec, "~> 1.4"}
|
||||
]
|
||||
end
|
||||
|
||||
|
|
|
|||
1
mix.lock
1
mix.lock
|
|
@ -22,6 +22,7 @@
|
|||
"mint": {:hex, :mint, "1.5.2", "4805e059f96028948870d23d7783613b7e6b0e2fb4e98d720383852a760067fd", [:mix], [{:castore, "~> 0.1.0 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:hpax, "~> 0.1.1", [hex: :hpax, repo: "hexpm", optional: false]}], "hexpm", "d77d9e9ce4eb35941907f1d3df38d8f750c357865353e21d335bdcdf6d892a02"},
|
||||
"mox": {:hex, :mox, "1.1.0", "0f5e399649ce9ab7602f72e718305c0f9cdc351190f72844599545e4996af73c", [:mix], [], "hexpm", "d44474c50be02d5b72131070281a5d3895c0e7a95c780e90bc0cfe712f633a13"},
|
||||
"nimble_options": {:hex, :nimble_options, "1.1.0", "3b31a57ede9cb1502071fade751ab0c7b8dbe75a9a4c2b5bbb0943a690b63172", [:mix], [], "hexpm", "8bbbb3941af3ca9acc7835f5655ea062111c9c27bcac53e004460dfd19008a99"},
|
||||
"nimble_parsec": {:hex, :nimble_parsec, "1.4.0", "51f9b613ea62cfa97b25ccc2c1b4216e81df970acd8e16e8d1bdc58fef21370d", [:mix], [], "hexpm", "9c565862810fb383e9838c1dd2d7d2c437b3d13b267414ba6af33e50d2d1cf28"},
|
||||
"nimble_pool": {:hex, :nimble_pool, "1.0.0", "5eb82705d138f4dd4423f69ceb19ac667b3b492ae570c9f5c900bb3d2f50a847", [:mix], [], "hexpm", "80be3b882d2d351882256087078e1b1952a28bf98d0a287be87e4a24a710b67a"},
|
||||
"phoenix": {:hex, :phoenix, "1.7.10", "02189140a61b2ce85bb633a9b6fd02dff705a5f1596869547aeb2b2b95edd729", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 2.1", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.6", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:plug_crypto, "~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:websock_adapter, "~> 0.5.3", [hex: :websock_adapter, repo: "hexpm", optional: false]}], "hexpm", "cf784932e010fd736d656d7fead6a584a4498efefe5b8227e9f383bf15bb79d0"},
|
||||
"phoenix_ecto": {:hex, :phoenix_ecto, "4.4.3", "86e9878f833829c3f66da03d75254c155d91d72a201eb56ae83482328dc7ca93", [:mix], [{:ecto, "~> 3.5", [hex: :ecto, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 2.14.2 or ~> 3.0 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:plug, "~> 1.9", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "d36c401206f3011fefd63d04e8ef626ec8791975d9d107f9a0817d426f61ac07"},
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
defmodule Pinchflat.DownloaderBackends.YtDlp.CommandRunnerTest do
|
||||
defmodule Pinchflat.Downloader.Backends.YtDlp.CommandRunnerTest do
|
||||
use ExUnit.Case, async: true
|
||||
|
||||
alias Pinchflat.DownloaderBackends.YtDlp.CommandRunner, as: Runner
|
||||
alias Pinchflat.Downloader.Backends.YtDlp.CommandRunner, as: Runner
|
||||
|
||||
@original_executable Application.compile_env(:pinchflat, :yt_dlp_executable)
|
||||
@video_url "https://www.youtube.com/watch?v=9bZkp7q19f0"
|
||||
|
|
@ -1,8 +1,8 @@
|
|||
defmodule Pinchflat.DownloaderBackends.YtDlp.VideoCollectionTest do
|
||||
defmodule Pinchflat.Downloader.Backends.YtDlp.VideoCollectionTest do
|
||||
use ExUnit.Case, async: true
|
||||
import Mox
|
||||
|
||||
alias Pinchflat.DownloaderBackends.YtDlp.VideoCollection, as: VideoCollection
|
||||
alias Pinchflat.Downloader.Backends.YtDlp.VideoCollection, as: VideoCollection
|
||||
|
||||
@channel_url "https://www.youtube.com/@TheUselessTrials"
|
||||
|
||||
40
test/pinchflat/rendered_string/parser_test.exs
Normal file
40
test/pinchflat/rendered_string/parser_test.exs
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
defmodule Pinchflat.RenderedString.ParserTest do
|
||||
use ExUnit.Case, async: true
|
||||
|
||||
alias Pinchflat.RenderedString.Parser
|
||||
|
||||
describe "parse/2" do
|
||||
test "it returns the rendered string when the string is valid" do
|
||||
assert {:ok, "bar"} = Parser.parse("{{ foo }}", %{"foo" => "bar"})
|
||||
end
|
||||
|
||||
test "it works with filepath-like strings" do
|
||||
assert {:ok, "bar/baz"} =
|
||||
Parser.parse("{{ foo }}/{{ bar }}", %{"foo" => "bar", "bar" => "baz"})
|
||||
end
|
||||
|
||||
test "it works when mixing text and variables" do
|
||||
assert {:ok, "bar text baz"} =
|
||||
Parser.parse("{{ foo }} text {{ bar }}", %{"foo" => "bar", "bar" => "baz"})
|
||||
end
|
||||
|
||||
test "it removes the placeholder but doesn't blow up when the variable isn't provided" do
|
||||
assert {:ok, ""} = Parser.parse("{{ foo }}", %{})
|
||||
end
|
||||
|
||||
test "it accepts any number of spaces between open and closing tags" do
|
||||
assert {:ok, "bar"} = Parser.parse("{{foo}}", %{"foo" => "bar"})
|
||||
assert {:ok, "bar"} = Parser.parse("{{ foo}}", %{"foo" => "bar"})
|
||||
assert {:ok, "bar"} = Parser.parse("{{foo }}", %{"foo" => "bar"})
|
||||
assert {:ok, "bar"} = Parser.parse("{{ foo }}", %{"foo" => "bar"})
|
||||
end
|
||||
|
||||
test "it doesn't interpret single braces as variables" do
|
||||
assert {:ok, "{foo}"} = Parser.parse("{foo}", %{})
|
||||
end
|
||||
|
||||
test "it returns an error when the string is invalid" do
|
||||
assert {:error, "expected end of string"} = Parser.parse("{{ 1-1 }", %{})
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
Mox.defmock(CommandRunnerMock, for: Pinchflat.DownloaderBackends.BackendCommandRunner)
|
||||
Mox.defmock(CommandRunnerMock, for: Pinchflat.Downloader.Backends.BackendCommandRunner)
|
||||
Application.put_env(:pinchflat, :yt_dlp_runner, CommandRunnerMock)
|
||||
|
||||
ExUnit.start()
|
||||
|
|
|
|||
Loading…
Reference in a new issue