Download video(s) (first iteration) (#5)
* Updated package.json (and made an excuse to make a branch) * Video filepath parser (#6) * Restructured files; Added parser placeholder * More restructuring * Added basic parser for hydrating template strings * Improved docs * More docs * Initial implementation of media profiles (#7) * [WIP] Added basic video download method * [WIP] Very-WIP first steps at parsing options and downloading * Made my options safe by default and removed special safe versions * Ran html generator for mediaprofile model - leaving as-is for now * Addressed a bunch of TODO comments * Add "channel" type Media Source (#8) * [WIP] Working on fetching channel metadata in yt-dlp backend * Finished first draft of methods to do with querying channels * Renamed CommandRunnerMock to have a more descriptive name * Ran the phx generator for the channel model * Renamed Downloader namespace to MediaClient * [WIP] saving before attempting LiveView * LiveView did not work out but here's a working controller how about * Index a channel (#9) * Ran a MediaItem generator; Reformatted to my liking * [WIP] added basic index function * setup oban * Added basic Oban job for indexing * Added in workers for indexing; hooked them into record creation flow * Added a task model with a phx generator * Tied together tasks with jobs and channels * Download indexed videos (#10) * Clarified documentation * more comments * [WIP] hooked up basic video downloading; starting work on metadata * Added metadata model and parsing Adding the metadata model made me realize that, in many cases, yt-dlp returns undesired input in stdout, breaking parsing. In order to get the metadata model working, I had to change the way in which the app interacts with yt-dlp. Now, output is written as a file to disk which is immediately re-read and returned. * Added tests for video download worker * Hooked up video downloading to the channel indexing pipeline * Adds tasks for media items * Updated video metadata parser to extract the title * Ran linting
This commit is contained in:
@@ -1,44 +0,0 @@
|
||||
defmodule Pinchflat.DownloaderBackends.YtDlp.VideoCollectionTest do
|
||||
use ExUnit.Case, async: true
|
||||
import Mox
|
||||
|
||||
alias Pinchflat.DownloaderBackends.YtDlp.VideoCollection, as: VideoCollection
|
||||
|
||||
@channel_url "https://www.youtube.com/@TheUselessTrials"
|
||||
|
||||
setup :verify_on_exit!
|
||||
|
||||
describe "get_video_ids/2" do
|
||||
test "returns a list of video ids with no blank elements" do
|
||||
expect(CommandRunnerMock, :run, fn _url, _opts -> {:ok, "id1\nid2\n\nid3\n"} end)
|
||||
|
||||
assert {:ok, ["id1", "id2", "id3"]} = VideoCollection.get_video_ids(@channel_url)
|
||||
end
|
||||
|
||||
test "it passes the expected default args" do
|
||||
expect(CommandRunnerMock, :run, fn _url, opts ->
|
||||
assert opts == [:simulate, :skip_download, :get_id]
|
||||
|
||||
{:ok, ""}
|
||||
end)
|
||||
|
||||
assert {:ok, _} = VideoCollection.get_video_ids(@channel_url)
|
||||
end
|
||||
|
||||
test "it passes the expected custom args" do
|
||||
expect(CommandRunnerMock, :run, fn _url, opts ->
|
||||
assert opts == [:custom_arg, :simulate, :skip_download, :get_id]
|
||||
|
||||
{:ok, ""}
|
||||
end)
|
||||
|
||||
assert {:ok, _} = VideoCollection.get_video_ids(@channel_url, [:custom_arg])
|
||||
end
|
||||
|
||||
test "returns the error straight through when the command fails" do
|
||||
expect(CommandRunnerMock, :run, fn _url, _opts -> {:error, "Big issue", 1} end)
|
||||
|
||||
assert {:error, "Big issue", 1} = VideoCollection.get_video_ids(@channel_url)
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,45 @@
|
||||
defmodule Pinchflat.MediaClient.Backends.YtDlp.ChannelTest do
|
||||
use ExUnit.Case, async: true
|
||||
import Mox
|
||||
|
||||
alias Pinchflat.MediaClient.ChannelDetails
|
||||
alias Pinchflat.MediaClient.Backends.YtDlp.Channel
|
||||
|
||||
@channel_url "https://www.youtube.com/c/TheUselessTrials"
|
||||
|
||||
setup :verify_on_exit!
|
||||
|
||||
describe "get_channel_details/1" do
|
||||
test "it returns a %ChannelDetails{} with data on success" do
|
||||
expect(YtDlpRunnerMock, :run, fn _url, _opts, _ot ->
|
||||
{:ok, "{\"channel\": \"TheUselessTrials\", \"channel_id\": \"UCQH2\"}"}
|
||||
end)
|
||||
|
||||
assert {:ok, res} = Channel.get_channel_details(@channel_url)
|
||||
assert %ChannelDetails{id: "UCQH2", name: "TheUselessTrials"} = res
|
||||
end
|
||||
|
||||
test "it passes the expected args to the backend runner" do
|
||||
expect(YtDlpRunnerMock, :run, fn @channel_url, opts, ot ->
|
||||
assert opts == [playlist_end: 1]
|
||||
assert ot == "%(.{channel,channel_id})j"
|
||||
|
||||
{:ok, "{}"}
|
||||
end)
|
||||
|
||||
assert {:ok, _} = Channel.get_channel_details(@channel_url)
|
||||
end
|
||||
|
||||
test "it returns an error if the runner returns an error" do
|
||||
expect(YtDlpRunnerMock, :run, fn _url, _opts, _ot -> {:error, "Big issue", 1} end)
|
||||
|
||||
assert {:error, "Big issue", 1} = Channel.get_channel_details(@channel_url)
|
||||
end
|
||||
|
||||
test "it returns an error if the output is not JSON" do
|
||||
expect(YtDlpRunnerMock, :run, fn _url, _opts, _ot -> {:ok, "Not JSON"} end)
|
||||
|
||||
assert {:error, %Jason.DecodeError{}} = Channel.get_channel_details(@channel_url)
|
||||
end
|
||||
end
|
||||
end
|
||||
+15
-16
@@ -1,10 +1,10 @@
|
||||
defmodule Pinchflat.DownloaderBackends.YtDlp.CommandRunnerTest do
|
||||
defmodule Pinchflat.MediaClient.Backends.YtDlp.CommandRunnerTest do
|
||||
use ExUnit.Case, async: true
|
||||
|
||||
alias Pinchflat.DownloaderBackends.YtDlp.CommandRunner, as: Runner
|
||||
alias Pinchflat.MediaClient.Backends.YtDlp.CommandRunner, as: Runner
|
||||
|
||||
@original_executable Application.compile_env(:pinchflat, :yt_dlp_executable)
|
||||
@video_url "https://www.youtube.com/watch?v=9bZkp7q19f0"
|
||||
@video_url "https://www.youtube.com/watch?v=-LHXuyzpex0"
|
||||
|
||||
setup do
|
||||
on_exit(&reset_executable/0)
|
||||
@@ -12,50 +12,49 @@ defmodule Pinchflat.DownloaderBackends.YtDlp.CommandRunnerTest do
|
||||
|
||||
describe "run/2" do
|
||||
test "it returns the output and status when the command succeeds" do
|
||||
assert {:ok, _output} = Runner.run(@video_url, [])
|
||||
assert {:ok, _output} = Runner.run(@video_url, [], "")
|
||||
end
|
||||
|
||||
test "it converts symbol k-v arg keys to kebab case" do
|
||||
assert {:ok, output} = Runner.run(@video_url, buffer_size: 1024)
|
||||
assert {:ok, output} = Runner.run(@video_url, [buffer_size: 1024], "")
|
||||
|
||||
assert String.contains?(output, "--buffer-size 1024")
|
||||
end
|
||||
|
||||
test "it keeps string k-v arg keys untouched" do
|
||||
assert {:ok, output} = Runner.run(@video_url, [{"--under_score", 1024}])
|
||||
assert {:ok, output} = Runner.run(@video_url, [{"--under_score", 1024}], "")
|
||||
|
||||
assert String.contains?(output, "--under_score 1024")
|
||||
end
|
||||
|
||||
test "it converts symbol arg keys to kebab case" do
|
||||
assert {:ok, output} = Runner.run(@video_url, [:ignore_errors])
|
||||
assert {:ok, output} = Runner.run(@video_url, [:ignore_errors], "")
|
||||
|
||||
assert String.contains?(output, "--ignore-errors")
|
||||
end
|
||||
|
||||
test "it keeps string arg keys untouched" do
|
||||
assert {:ok, output} = Runner.run(@video_url, ["-v"])
|
||||
assert {:ok, output} = Runner.run(@video_url, ["-v"], "")
|
||||
|
||||
assert String.contains?(output, "-v")
|
||||
refute String.contains?(output, "--v")
|
||||
end
|
||||
|
||||
test "it places arg keys (flags) at the beginning of the command" do
|
||||
assert {:ok, output} =
|
||||
Runner.run(@video_url, [{"--under_score", 1024}, :ignore_errors])
|
||||
test "it includes the video url as the first argument" do
|
||||
assert {:ok, output} = Runner.run(@video_url, [:ignore_errors], "")
|
||||
|
||||
assert String.contains?(output, "--ignore-errors --under_score 1024")
|
||||
assert String.contains?(output, "#{@video_url} --ignore-errors")
|
||||
end
|
||||
|
||||
test "it includes the video url as the last argument" do
|
||||
assert {:ok, output} = Runner.run(@video_url, [:ignore_errors])
|
||||
test "it automatically includes the --print-to-file flag" do
|
||||
assert {:ok, output} = Runner.run(@video_url, [], "%(id)s")
|
||||
|
||||
assert String.contains?(output, "--ignore-errors #{@video_url}\n")
|
||||
assert String.contains?(output, "--print-to-file %(id)s /tmp/")
|
||||
end
|
||||
|
||||
test "it returns the output and status when the command fails" do
|
||||
wrap_executable("/bin/false", fn ->
|
||||
assert {:error, "", 1} = Runner.run(@video_url, [])
|
||||
assert {:error, "", 1} = Runner.run(@video_url, [], "")
|
||||
end)
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,45 @@
|
||||
defmodule Pinchflat.MediaClient.Backends.YtDlp.MediaParserTest do
|
||||
use ExUnit.Case, async: true
|
||||
|
||||
alias Pinchflat.MediaClient.Backends.YtDlp.MetadataParser, as: Parser
|
||||
|
||||
setup do
|
||||
json_filepath =
|
||||
Path.join([
|
||||
File.cwd!(),
|
||||
"test",
|
||||
"support",
|
||||
"files",
|
||||
"media_metadata.json"
|
||||
])
|
||||
|
||||
{:ok, file_body} = File.read(json_filepath)
|
||||
{:ok, parsed_json} = Phoenix.json_library().decode(file_body)
|
||||
|
||||
{:ok,
|
||||
%{
|
||||
metadata: parsed_json
|
||||
}}
|
||||
end
|
||||
|
||||
describe "parse_for_media_item/1" do
|
||||
test "it extracts the video filepath", %{metadata: metadata} do
|
||||
result = Parser.parse_for_media_item(metadata)
|
||||
|
||||
assert String.contains?(result.video_filepath, "bwRHIkYqYJo")
|
||||
assert String.ends_with?(result.video_filepath, ".mkv")
|
||||
end
|
||||
|
||||
test "it extracts the title", %{metadata: metadata} do
|
||||
result = Parser.parse_for_media_item(metadata)
|
||||
|
||||
assert result.title == "Trying to Wheelie Without the Rear Brake"
|
||||
end
|
||||
|
||||
test "it returns the metadata as a map", %{metadata: metadata} do
|
||||
result = Parser.parse_for_media_item(metadata)
|
||||
|
||||
assert result.metadata.client_response == metadata
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,25 @@
|
||||
defmodule Pinchflat.MediaClient.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
|
||||
@@ -0,0 +1,49 @@
|
||||
defmodule Pinchflat.MediaClient.Backends.YtDlp.VideoCollectionTest do
|
||||
use ExUnit.Case, async: true
|
||||
import Mox
|
||||
|
||||
alias Pinchflat.MediaClient.Backends.YtDlp.VideoCollection
|
||||
|
||||
@channel_url "https://www.youtube.com/@TheUselessTrials"
|
||||
|
||||
defmodule VideoCollectionUser do
|
||||
use VideoCollection
|
||||
end
|
||||
|
||||
setup :verify_on_exit!
|
||||
|
||||
describe "get_video_ids/2" do
|
||||
test "returns a list of video ids with no blank elements" do
|
||||
expect(YtDlpRunnerMock, :run, fn _url, _opts, _ot -> {:ok, "id1\nid2\n\nid3\n"} end)
|
||||
|
||||
assert {:ok, ["id1", "id2", "id3"]} = VideoCollectionUser.get_video_ids(@channel_url)
|
||||
end
|
||||
|
||||
test "it passes the expected default args" do
|
||||
expect(YtDlpRunnerMock, :run, fn _url, opts, ot ->
|
||||
assert opts == [:simulate, :skip_download]
|
||||
assert ot == "%(id)s"
|
||||
|
||||
{:ok, ""}
|
||||
end)
|
||||
|
||||
assert {:ok, _} = VideoCollectionUser.get_video_ids(@channel_url)
|
||||
end
|
||||
|
||||
test "it passes the expected custom args" do
|
||||
expect(YtDlpRunnerMock, :run, fn _url, opts, _ot ->
|
||||
assert opts == [:custom_arg, :simulate, :skip_download]
|
||||
|
||||
{:ok, ""}
|
||||
end)
|
||||
|
||||
assert {:ok, _} = VideoCollectionUser.get_video_ids(@channel_url, [:custom_arg])
|
||||
end
|
||||
|
||||
test "returns the error straight through when the command fails" do
|
||||
expect(YtDlpRunnerMock, :run, fn _url, _opts, _ot -> {:error, "Big issue", 1} end)
|
||||
|
||||
assert {:error, "Big issue", 1} = VideoCollectionUser.get_video_ids(@channel_url)
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,56 @@
|
||||
defmodule Pinchflat.MediaClient.Backends.YtDlp.VideoTest do
|
||||
use Pinchflat.DataCase
|
||||
import Mox
|
||||
|
||||
alias Pinchflat.MediaClient.Backends.YtDlp.Video
|
||||
|
||||
@video_url "https://www.youtube.com/watch?v=TiZPUDkDYbk"
|
||||
|
||||
setup :verify_on_exit!
|
||||
|
||||
# expect(YtDlpRunnerMock, :run, fn _url, [_, _, json_output_path | _] ->
|
||||
# copy_metadata(json_output_path)
|
||||
|
||||
# {:ok, ""}
|
||||
# end)
|
||||
|
||||
describe "download/2" do
|
||||
test "it calls the backend runner with the expected arguments" do
|
||||
expect(YtDlpRunnerMock, :run, fn @video_url, opts, ot ->
|
||||
assert [:no_simulate] = opts
|
||||
assert "after_move:%()j" = ot
|
||||
|
||||
{:ok, render_metadata(:media_metadata)}
|
||||
end)
|
||||
|
||||
assert {:ok, _} = Video.download(@video_url)
|
||||
end
|
||||
|
||||
test "it passes along additional options" do
|
||||
expect(YtDlpRunnerMock, :run, fn _url, opts, _ot ->
|
||||
assert [:no_simulate, :custom_arg] = opts
|
||||
|
||||
{:ok, "{}"}
|
||||
end)
|
||||
|
||||
assert {:ok, _} = Video.download(@video_url, [:custom_arg])
|
||||
end
|
||||
|
||||
test "it parses and returns the generated file as JSON" do
|
||||
expect(YtDlpRunnerMock, :run, fn _url, _opts, _ot ->
|
||||
{:ok, render_metadata(:media_metadata)}
|
||||
end)
|
||||
|
||||
assert {:ok, %{"title" => "Trying to Wheelie Without the Rear Brake"}} =
|
||||
Video.download(@video_url)
|
||||
end
|
||||
|
||||
test "it returns errors" do
|
||||
expect(YtDlpRunnerMock, :run, fn _url, _opt, _ot ->
|
||||
{:error, "something"}
|
||||
end)
|
||||
|
||||
assert {:error, "something"} = Video.download(@video_url)
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,60 @@
|
||||
defmodule Pinchflat.MediaClient.ChannelDetailsTest do
|
||||
use ExUnit.Case, async: true
|
||||
import Mox
|
||||
|
||||
alias Pinchflat.MediaClient.ChannelDetails
|
||||
|
||||
@channel_url "https://www.youtube.com/c/TheUselessTrials"
|
||||
|
||||
setup :verify_on_exit!
|
||||
|
||||
describe "new/2" do
|
||||
test "it returns a struct with the given values" do
|
||||
assert %ChannelDetails{id: "UCQH2", name: "TheUselessTrials"} =
|
||||
ChannelDetails.new("UCQH2", "TheUselessTrials")
|
||||
end
|
||||
end
|
||||
|
||||
describe "get_channel_details/2" do
|
||||
test "it passes the expected arguments to the backend" do
|
||||
expect(YtDlpRunnerMock, :run, fn @channel_url, opts, ot ->
|
||||
assert opts == [playlist_end: 1]
|
||||
assert ot == "%(.{channel,channel_id})j"
|
||||
|
||||
{:ok, "{\"channel\": \"TheUselessTrials\", \"channel_id\": \"UCQH2\"}"}
|
||||
end)
|
||||
|
||||
assert {:ok, _} = ChannelDetails.get_channel_details(@channel_url)
|
||||
end
|
||||
|
||||
test "it returns a struct composed of the returned data" do
|
||||
expect(YtDlpRunnerMock, :run, fn _url, _opts, _ot ->
|
||||
{:ok, "{\"channel\": \"TheUselessTrials\", \"channel_id\": \"UCQH2\"}"}
|
||||
end)
|
||||
|
||||
assert {:ok, res} = ChannelDetails.get_channel_details(@channel_url)
|
||||
assert %ChannelDetails{id: "UCQH2", name: "TheUselessTrials"} = res
|
||||
end
|
||||
end
|
||||
|
||||
describe "get_video_ids/2" do
|
||||
test "it passes the expected arguments to the backend" do
|
||||
expect(YtDlpRunnerMock, :run, fn @channel_url, opts, ot ->
|
||||
assert opts == [:simulate, :skip_download]
|
||||
assert ot == "%(id)s"
|
||||
|
||||
{:ok, ""}
|
||||
end)
|
||||
|
||||
assert {:ok, _} = ChannelDetails.get_video_ids(@channel_url)
|
||||
end
|
||||
|
||||
test "it returns a list of strings" do
|
||||
expect(YtDlpRunnerMock, :run, fn _url, _opts, _ot ->
|
||||
{:ok, "video1\nvideo2\nvideo3"}
|
||||
end)
|
||||
|
||||
assert {:ok, ["video1", "video2", "video3"]} = ChannelDetails.get_video_ids(@channel_url)
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,61 @@
|
||||
defmodule Pinchflat.MediaClient.VideoDownloaderTest do
|
||||
use Pinchflat.DataCase
|
||||
import Mox
|
||||
import Pinchflat.MediaFixtures
|
||||
|
||||
alias Pinchflat.MediaClient.VideoDownloader
|
||||
|
||||
setup :verify_on_exit!
|
||||
|
||||
setup do
|
||||
media_item =
|
||||
Repo.preload(
|
||||
media_item_fixture(%{title: nil, video_filepath: nil}),
|
||||
[:metadata, channel: :media_profile]
|
||||
)
|
||||
|
||||
{:ok, %{media_item: media_item}}
|
||||
end
|
||||
|
||||
describe "download_for_media_item/3" do
|
||||
test "it calls the backend runner", %{media_item: media_item} do
|
||||
expect(YtDlpRunnerMock, :run, fn _url, _opts, ot ->
|
||||
assert ot == "after_move:%()j"
|
||||
|
||||
{:ok, render_metadata(:media_metadata)}
|
||||
end)
|
||||
|
||||
assert {:ok, _} = VideoDownloader.download_for_media_item(media_item)
|
||||
end
|
||||
|
||||
test "it writes attributes to the media item", %{media_item: media_item} do
|
||||
expect(YtDlpRunnerMock, :run, fn _url, _opts, _ot ->
|
||||
{:ok, render_metadata(:media_metadata)}
|
||||
end)
|
||||
|
||||
assert %{video_filepath: nil, title: nil} = media_item
|
||||
assert {:ok, updated_media_item} = VideoDownloader.download_for_media_item(media_item)
|
||||
assert updated_media_item.video_filepath
|
||||
assert updated_media_item.title
|
||||
end
|
||||
|
||||
test "it saves the metadata to the database", %{media_item: media_item} do
|
||||
expect(YtDlpRunnerMock, :run, fn _url, _opts, _ot ->
|
||||
{:ok, render_metadata(:media_metadata)}
|
||||
end)
|
||||
|
||||
assert is_nil(media_item.metadata)
|
||||
assert {:ok, updated_media_item} = VideoDownloader.download_for_media_item(media_item)
|
||||
assert updated_media_item.metadata
|
||||
assert is_map(updated_media_item.metadata.client_response)
|
||||
end
|
||||
|
||||
test "errors are passed through", %{media_item: media_item} do
|
||||
expect(YtDlpRunnerMock, :run, fn _url, _opts, _ot ->
|
||||
{:error, :some_error}
|
||||
end)
|
||||
|
||||
assert {:error, :some_error} = VideoDownloader.download_for_media_item(media_item)
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,313 @@
|
||||
defmodule Pinchflat.MediaSourceTest do
|
||||
use Pinchflat.DataCase
|
||||
import Mox
|
||||
import Pinchflat.TasksFixtures
|
||||
import Pinchflat.ProfilesFixtures
|
||||
import Pinchflat.MediaSourceFixtures
|
||||
|
||||
alias Pinchflat.MediaSource
|
||||
alias Pinchflat.Media.MediaItem
|
||||
alias Pinchflat.MediaSource.Channel
|
||||
alias Pinchflat.Workers.MediaIndexingWorker
|
||||
|
||||
@invalid_channel_attrs %{name: nil, channel_id: nil}
|
||||
|
||||
setup :verify_on_exit!
|
||||
|
||||
describe "list_channels/0" do
|
||||
test "it returns all channels" do
|
||||
channel = channel_fixture()
|
||||
assert MediaSource.list_channels() == [channel]
|
||||
end
|
||||
end
|
||||
|
||||
describe "get_channel!/1" do
|
||||
test "it returns the channel with given id" do
|
||||
channel = channel_fixture()
|
||||
assert MediaSource.get_channel!(channel.id) == channel
|
||||
end
|
||||
end
|
||||
|
||||
describe "create_channel/1" do
|
||||
test "creates a channel and adds name + ID from runner response" do
|
||||
expect(YtDlpRunnerMock, :run, &runner_function_mock/3)
|
||||
|
||||
valid_attrs = %{
|
||||
media_profile_id: media_profile_fixture().id,
|
||||
original_url: "https://www.youtube.com/channel/abc123"
|
||||
}
|
||||
|
||||
assert {:ok, %Channel{} = channel} = MediaSource.create_channel(valid_attrs)
|
||||
assert channel.name == "some name"
|
||||
assert String.starts_with?(channel.channel_id, "some_channel_id_")
|
||||
end
|
||||
|
||||
test "creation with invalid data returns error changeset" do
|
||||
assert {:error, %Ecto.Changeset{}} = MediaSource.create_channel(@invalid_channel_attrs)
|
||||
end
|
||||
|
||||
test "creation enforces uniqueness of channel_id scoped to the media_profile" do
|
||||
expect(YtDlpRunnerMock, :run, 2, fn _url, _opts, _ot ->
|
||||
{:ok,
|
||||
Phoenix.json_library().encode!(%{
|
||||
channel: "some name",
|
||||
channel_id: "some_channel_id_12345678"
|
||||
})}
|
||||
end)
|
||||
|
||||
valid_once_attrs = %{
|
||||
media_profile_id: media_profile_fixture().id,
|
||||
original_url: "https://www.youtube.com/channel/abc123"
|
||||
}
|
||||
|
||||
assert {:ok, %Channel{}} = MediaSource.create_channel(valid_once_attrs)
|
||||
assert {:error, %Ecto.Changeset{}} = MediaSource.create_channel(valid_once_attrs)
|
||||
end
|
||||
|
||||
test "creation lets you duplicate channel_ids as long as the media profile is different" do
|
||||
expect(YtDlpRunnerMock, :run, 2, fn _url, _opts, _ot ->
|
||||
{:ok,
|
||||
Phoenix.json_library().encode!(%{
|
||||
channel: "some name",
|
||||
channel_id: "some_channel_id_12345678"
|
||||
})}
|
||||
end)
|
||||
|
||||
valid_attrs = %{
|
||||
name: "some name",
|
||||
original_url: "https://www.youtube.com/channel/abc123"
|
||||
}
|
||||
|
||||
channel_1_attrs = Map.merge(valid_attrs, %{media_profile_id: media_profile_fixture().id})
|
||||
channel_2_attrs = Map.merge(valid_attrs, %{media_profile_id: media_profile_fixture().id})
|
||||
|
||||
assert {:ok, %Channel{}} = MediaSource.create_channel(channel_1_attrs)
|
||||
assert {:ok, %Channel{}} = MediaSource.create_channel(channel_2_attrs)
|
||||
end
|
||||
|
||||
test "creation will schedule the indexing task" do
|
||||
expect(YtDlpRunnerMock, :run, &runner_function_mock/3)
|
||||
|
||||
valid_attrs = %{
|
||||
media_profile_id: media_profile_fixture().id,
|
||||
original_url: "https://www.youtube.com/channel/abc123"
|
||||
}
|
||||
|
||||
assert {:ok, %Channel{} = channel} = MediaSource.create_channel(valid_attrs)
|
||||
|
||||
assert_enqueued(worker: MediaIndexingWorker, args: %{"id" => channel.id})
|
||||
end
|
||||
end
|
||||
|
||||
describe "index_media_items/1" do
|
||||
setup do
|
||||
stub(YtDlpRunnerMock, :run, fn _url, _opts, _ot -> {:ok, "video1\nvideo2\nvideo3"} end)
|
||||
|
||||
{:ok, [channel: channel_fixture()]}
|
||||
end
|
||||
|
||||
test "it creates a media_item record for each media ID returned", %{channel: channel} do
|
||||
assert media_items = MediaSource.index_media_items(channel)
|
||||
|
||||
assert Enum.count(media_items) == 3
|
||||
assert ["video1", "video2", "video3"] == Enum.map(media_items, & &1.media_id)
|
||||
assert Enum.all?(media_items, fn %MediaItem{} -> true end)
|
||||
end
|
||||
|
||||
test "it attaches all media_items to the given channel", %{channel: channel} do
|
||||
channel_id = channel.id
|
||||
assert media_items = MediaSource.index_media_items(channel)
|
||||
|
||||
assert Enum.count(media_items) == 3
|
||||
assert Enum.all?(media_items, fn %MediaItem{channel_id: ^channel_id} -> true end)
|
||||
end
|
||||
|
||||
test "it won't duplicate media_items based on media_id and channel", %{channel: channel} do
|
||||
_first_run = MediaSource.index_media_items(channel)
|
||||
_duplicate_run = MediaSource.index_media_items(channel)
|
||||
|
||||
media_items = Repo.preload(channel, :media_items).media_items
|
||||
assert Enum.count(media_items) == 3
|
||||
end
|
||||
|
||||
test "it can duplicate media_ids for different channels", %{channel: channel} do
|
||||
other_channel = channel_fixture()
|
||||
|
||||
media_items = MediaSource.index_media_items(channel)
|
||||
media_items_other_channel = MediaSource.index_media_items(other_channel)
|
||||
|
||||
assert Enum.count(media_items) == 3
|
||||
assert Enum.count(media_items_other_channel) == 3
|
||||
|
||||
assert Enum.map(media_items, & &1.media_id) ==
|
||||
Enum.map(media_items_other_channel, & &1.media_id)
|
||||
end
|
||||
|
||||
test "it returns a list of media_items or changesets", %{channel: channel} do
|
||||
first_run = MediaSource.index_media_items(channel)
|
||||
duplicate_run = MediaSource.index_media_items(channel)
|
||||
|
||||
assert Enum.all?(first_run, fn %MediaItem{} -> true end)
|
||||
assert Enum.all?(duplicate_run, fn %Ecto.Changeset{} -> true end)
|
||||
end
|
||||
end
|
||||
|
||||
describe "update_channel/2" do
|
||||
test "updates with valid data updates the channel" do
|
||||
channel = channel_fixture()
|
||||
update_attrs = %{name: "some updated name"}
|
||||
|
||||
assert {:ok, %Channel{} = channel} = MediaSource.update_channel(channel, update_attrs)
|
||||
assert channel.name == "some updated name"
|
||||
end
|
||||
|
||||
test "updating the original_url will re-fetch the channel details" do
|
||||
expect(YtDlpRunnerMock, :run, &runner_function_mock/3)
|
||||
|
||||
channel = channel_fixture()
|
||||
update_attrs = %{original_url: "https://www.youtube.com/channel/abc123"}
|
||||
|
||||
assert {:ok, %Channel{} = channel} = MediaSource.update_channel(channel, update_attrs)
|
||||
assert channel.name == "some name"
|
||||
assert String.starts_with?(channel.channel_id, "some_channel_id_")
|
||||
end
|
||||
|
||||
test "not updating the original_url will not re-fetch the channel details" do
|
||||
expect(YtDlpRunnerMock, :run, 0, &runner_function_mock/3)
|
||||
|
||||
channel = channel_fixture()
|
||||
update_attrs = %{name: "some updated name"}
|
||||
|
||||
assert {:ok, %Channel{}} = MediaSource.update_channel(channel, update_attrs)
|
||||
end
|
||||
|
||||
test "updating the index frequency will re-schedule the indexing task" do
|
||||
channel = channel_fixture()
|
||||
update_attrs = %{index_frequency_minutes: 123}
|
||||
|
||||
assert {:ok, %Channel{} = channel} = MediaSource.update_channel(channel, update_attrs)
|
||||
assert channel.index_frequency_minutes == 123
|
||||
assert_enqueued(worker: MediaIndexingWorker, args: %{"id" => channel.id})
|
||||
end
|
||||
|
||||
test "not updating the index frequency will not re-schedule the indexing task" do
|
||||
channel = channel_fixture()
|
||||
update_attrs = %{name: "some updated name"}
|
||||
|
||||
assert {:ok, %Channel{}} = MediaSource.update_channel(channel, update_attrs)
|
||||
refute_enqueued(worker: MediaIndexingWorker, args: %{"id" => channel.id})
|
||||
end
|
||||
|
||||
test "updates with invalid data returns error changeset" do
|
||||
channel = channel_fixture()
|
||||
|
||||
assert {:error, %Ecto.Changeset{}} =
|
||||
MediaSource.update_channel(channel, @invalid_channel_attrs)
|
||||
|
||||
assert channel == MediaSource.get_channel!(channel.id)
|
||||
end
|
||||
end
|
||||
|
||||
describe "delete_channel/1" do
|
||||
test "it deletes the channel" do
|
||||
channel = channel_fixture()
|
||||
assert {:ok, %Channel{}} = MediaSource.delete_channel(channel)
|
||||
assert_raise Ecto.NoResultsError, fn -> MediaSource.get_channel!(channel.id) end
|
||||
end
|
||||
|
||||
test "it returns a channel changeset" do
|
||||
channel = channel_fixture()
|
||||
assert %Ecto.Changeset{} = MediaSource.change_channel(channel)
|
||||
end
|
||||
|
||||
test "deletion also deletes all associated tasks" do
|
||||
channel = channel_fixture()
|
||||
task = task_fixture(channel_id: channel.id)
|
||||
|
||||
assert {:ok, %Channel{}} = MediaSource.delete_channel(channel)
|
||||
assert_raise Ecto.NoResultsError, fn -> Repo.reload!(task) end
|
||||
end
|
||||
end
|
||||
|
||||
describe "change_channel/2" do
|
||||
test "it returns a changeset" do
|
||||
channel = channel_fixture()
|
||||
|
||||
assert %Ecto.Changeset{} = MediaSource.change_channel(channel)
|
||||
end
|
||||
end
|
||||
|
||||
describe "change_channel_from_url/2" do
|
||||
test "it returns a changeset" do
|
||||
stub(YtDlpRunnerMock, :run, &runner_function_mock/3)
|
||||
channel = channel_fixture()
|
||||
|
||||
assert %Ecto.Changeset{} = MediaSource.change_channel_from_url(channel, %{})
|
||||
end
|
||||
|
||||
test "it does not fetch channel details if the original_url isn't in the changeset" do
|
||||
expect(YtDlpRunnerMock, :run, 0, &runner_function_mock/3)
|
||||
|
||||
changeset = MediaSource.change_channel_from_url(%Channel{}, %{name: "some updated name"})
|
||||
|
||||
assert %Ecto.Changeset{} = changeset
|
||||
end
|
||||
|
||||
test "it fetches channel details if the original_url is in the changeset" do
|
||||
expect(YtDlpRunnerMock, :run, &runner_function_mock/3)
|
||||
|
||||
changeset =
|
||||
MediaSource.change_channel_from_url(%Channel{}, %{
|
||||
original_url: "https://www.youtube.com/channel/abc123"
|
||||
})
|
||||
|
||||
assert %Ecto.Changeset{} = changeset
|
||||
end
|
||||
|
||||
test "it adds channel details to the changeset, keeping the orignal details" do
|
||||
expect(YtDlpRunnerMock, :run, &runner_function_mock/3)
|
||||
|
||||
media_profile = media_profile_fixture()
|
||||
media_profile_id = media_profile.id
|
||||
|
||||
changeset =
|
||||
MediaSource.change_channel_from_url(%Channel{}, %{
|
||||
original_url: "https://www.youtube.com/channel/abc123",
|
||||
media_profile_id: media_profile.id
|
||||
})
|
||||
|
||||
assert %Ecto.Changeset{} = changeset
|
||||
assert String.starts_with?(changeset.changes.channel_id, "some_channel_id_")
|
||||
|
||||
assert %{
|
||||
name: "some name",
|
||||
media_profile_id: ^media_profile_id,
|
||||
original_url: "https://www.youtube.com/channel/abc123"
|
||||
} = changeset.changes
|
||||
end
|
||||
|
||||
test "it adds an error to the changeset if the runner fails" do
|
||||
expect(YtDlpRunnerMock, :run, 1, fn _url, _opts, _ot ->
|
||||
{:error, "some error", 1}
|
||||
end)
|
||||
|
||||
changeset =
|
||||
MediaSource.change_channel_from_url(%Channel{}, %{
|
||||
original_url: "https://www.youtube.com/channel/abc123"
|
||||
})
|
||||
|
||||
assert %Ecto.Changeset{} = changeset
|
||||
assert errors_on(changeset).original_url == ["could not fetch channel details from URL"]
|
||||
end
|
||||
end
|
||||
|
||||
defp runner_function_mock(_url, _opts, _ot) do
|
||||
{
|
||||
:ok,
|
||||
Phoenix.json_library().encode!(%{
|
||||
channel: "some name",
|
||||
channel_id: "some_channel_id_#{:rand.uniform(1_000_000)}"
|
||||
})
|
||||
}
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,126 @@
|
||||
defmodule Pinchflat.MediaTest do
|
||||
use Pinchflat.DataCase
|
||||
|
||||
import Pinchflat.TasksFixtures
|
||||
import Pinchflat.MediaFixtures
|
||||
import Pinchflat.MediaSourceFixtures
|
||||
|
||||
alias Pinchflat.Media
|
||||
alias Pinchflat.Media.MediaItem
|
||||
|
||||
@invalid_attrs %{title: nil, media_id: nil, video_filepath: nil}
|
||||
|
||||
describe "schema" do
|
||||
test "media_metadata is deleted when media_item is deleted" do
|
||||
media_item = media_item_fixture(%{metadata: %{client_response: %{foo: "bar"}}})
|
||||
metadata = media_item.metadata
|
||||
assert {:ok, %MediaItem{}} = Media.delete_media_item(media_item)
|
||||
|
||||
assert_raise Ecto.NoResultsError, fn ->
|
||||
Repo.reload!(metadata)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "list_media_items/0" do
|
||||
test "it returns all media_items" do
|
||||
media_item = media_item_fixture()
|
||||
assert Media.list_media_items() == [media_item]
|
||||
end
|
||||
end
|
||||
|
||||
describe "list_pending_media_items_for/1" do
|
||||
test "it returns pending media_items for a given channel" do
|
||||
channel = channel_fixture()
|
||||
media_item = media_item_fixture(%{channel_id: channel.id, video_filepath: nil})
|
||||
|
||||
assert Media.list_pending_media_items_for(channel) == [media_item]
|
||||
end
|
||||
|
||||
test "it does not return media_items with video_filepath" do
|
||||
channel = channel_fixture()
|
||||
|
||||
_media_item =
|
||||
media_item_fixture(%{
|
||||
channel_id: channel.id,
|
||||
video_filepath: "/video/#{Faker.File.file_name(:video)}"
|
||||
})
|
||||
|
||||
assert Media.list_pending_media_items_for(channel) == []
|
||||
end
|
||||
end
|
||||
|
||||
describe "get_media_item!/1" do
|
||||
test "it returns the media_item with given id" do
|
||||
media_item = media_item_fixture()
|
||||
assert Media.get_media_item!(media_item.id) == media_item
|
||||
end
|
||||
end
|
||||
|
||||
describe "create_media_item/1" do
|
||||
test "creating with valid data creates a media_item" do
|
||||
valid_attrs = %{
|
||||
media_id: Faker.String.base64(12),
|
||||
title: Faker.Commerce.product_name(),
|
||||
video_filepath: "/video/#{Faker.File.file_name(:video)}",
|
||||
channel_id: channel_fixture().id
|
||||
}
|
||||
|
||||
assert {:ok, %MediaItem{} = media_item} = Media.create_media_item(valid_attrs)
|
||||
assert media_item.title == valid_attrs.title
|
||||
assert media_item.media_id == valid_attrs.media_id
|
||||
assert media_item.video_filepath == valid_attrs.video_filepath
|
||||
end
|
||||
|
||||
test "creating with invalid data returns error changeset" do
|
||||
assert {:error, %Ecto.Changeset{}} = Media.create_media_item(@invalid_attrs)
|
||||
end
|
||||
end
|
||||
|
||||
describe "update_media_item/2" do
|
||||
test "updating with valid data updates the media_item" do
|
||||
media_item = media_item_fixture()
|
||||
|
||||
update_attrs = %{
|
||||
media_id: Faker.String.base64(12),
|
||||
title: Faker.Commerce.product_name(),
|
||||
video_filepath: "/video/#{Faker.File.file_name(:video)}",
|
||||
channel_id: channel_fixture().id
|
||||
}
|
||||
|
||||
assert {:ok, %MediaItem{} = media_item} = Media.update_media_item(media_item, update_attrs)
|
||||
assert media_item.title == update_attrs.title
|
||||
assert media_item.media_id == update_attrs.media_id
|
||||
assert media_item.video_filepath == update_attrs.video_filepath
|
||||
end
|
||||
|
||||
test "updating with invalid data returns error changeset" do
|
||||
media_item = media_item_fixture()
|
||||
assert {:error, %Ecto.Changeset{}} = Media.update_media_item(media_item, @invalid_attrs)
|
||||
assert media_item == Media.get_media_item!(media_item.id)
|
||||
end
|
||||
end
|
||||
|
||||
describe "delete_media_item/1" do
|
||||
test "deletion deletes the media_item" do
|
||||
media_item = media_item_fixture()
|
||||
assert {:ok, %MediaItem{}} = Media.delete_media_item(media_item)
|
||||
assert_raise Ecto.NoResultsError, fn -> Media.get_media_item!(media_item.id) end
|
||||
end
|
||||
|
||||
test "it also deletes attached tasks" do
|
||||
media_item = media_item_fixture()
|
||||
task = task_fixture(%{media_item_id: media_item.id})
|
||||
|
||||
assert {:ok, %MediaItem{}} = Media.delete_media_item(media_item)
|
||||
assert_raise Ecto.NoResultsError, fn -> Repo.reload!(task) end
|
||||
end
|
||||
end
|
||||
|
||||
describe "change_media_item/1" do
|
||||
test "change_media_item/1 returns a media_item changeset" do
|
||||
media_item = media_item_fixture()
|
||||
assert %Ecto.Changeset{} = Media.change_media_item(media_item)
|
||||
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: "{{ 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/videos/%(title)S.%(ext)s"} in res
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,78 @@
|
||||
defmodule Pinchflat.ProfilesTest do
|
||||
use Pinchflat.DataCase
|
||||
|
||||
alias Pinchflat.Profiles
|
||||
alias Pinchflat.Profiles.MediaProfile
|
||||
import Pinchflat.ProfilesFixtures
|
||||
|
||||
@invalid_attrs %{name: nil, output_path_template: nil}
|
||||
|
||||
describe "list_media_profiles/0" do
|
||||
test "it returns all media_profiles" do
|
||||
media_profile = media_profile_fixture()
|
||||
assert Profiles.list_media_profiles() == [media_profile]
|
||||
end
|
||||
end
|
||||
|
||||
describe "get_media_profile!/1" do
|
||||
test "it returns the media_profile with given id" do
|
||||
media_profile = media_profile_fixture()
|
||||
assert Profiles.get_media_profile!(media_profile.id) == media_profile
|
||||
end
|
||||
end
|
||||
|
||||
describe "create_media_profile/1" do
|
||||
test "creation with valid data creates a media_profile" do
|
||||
valid_attrs = %{name: "some name", output_path_template: "some output_path_template"}
|
||||
|
||||
assert {:ok, %MediaProfile{} = media_profile} = Profiles.create_media_profile(valid_attrs)
|
||||
assert media_profile.name == "some name"
|
||||
assert media_profile.output_path_template == "some output_path_template"
|
||||
end
|
||||
|
||||
test "creation with invalid data returns error changeset" do
|
||||
assert {:error, %Ecto.Changeset{}} = Profiles.create_media_profile(@invalid_attrs)
|
||||
end
|
||||
end
|
||||
|
||||
describe "update_media_profile/2" do
|
||||
test "updating with valid data updates the media_profile" do
|
||||
media_profile = media_profile_fixture()
|
||||
|
||||
update_attrs = %{
|
||||
name: "some updated name",
|
||||
output_path_template: "some updated output_path_template"
|
||||
}
|
||||
|
||||
assert {:ok, %MediaProfile{} = media_profile} =
|
||||
Profiles.update_media_profile(media_profile, update_attrs)
|
||||
|
||||
assert media_profile.name == "some updated name"
|
||||
assert media_profile.output_path_template == "some updated output_path_template"
|
||||
end
|
||||
|
||||
test "updating with invalid data returns error changeset" do
|
||||
media_profile = media_profile_fixture()
|
||||
|
||||
assert {:error, %Ecto.Changeset{}} =
|
||||
Profiles.update_media_profile(media_profile, @invalid_attrs)
|
||||
|
||||
assert media_profile == Profiles.get_media_profile!(media_profile.id)
|
||||
end
|
||||
end
|
||||
|
||||
describe "delete_media_profile/1" do
|
||||
test "deletion deletes the media_profile" do
|
||||
media_profile = media_profile_fixture()
|
||||
assert {:ok, %MediaProfile{}} = Profiles.delete_media_profile(media_profile)
|
||||
assert_raise Ecto.NoResultsError, fn -> Profiles.get_media_profile!(media_profile.id) end
|
||||
end
|
||||
end
|
||||
|
||||
describe "change_media_profile/1" do
|
||||
test "it returns a media_profile changeset" do
|
||||
media_profile = media_profile_fixture()
|
||||
assert %Ecto.Changeset{} = Profiles.change_media_profile(media_profile)
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -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
|
||||
@@ -0,0 +1,26 @@
|
||||
defmodule Pinchflat.RepoTest do
|
||||
use Pinchflat.DataCase
|
||||
|
||||
alias Pinchflat.JobFixtures.TestJobWorker
|
||||
|
||||
describe "insert_unique_job/1" do
|
||||
test "returns {:ok, job} if there is no conflict" do
|
||||
job = TestJobWorker.new(%{})
|
||||
|
||||
assert {:ok, %Oban.Job{}} = Pinchflat.Repo.insert_unique_job(job)
|
||||
end
|
||||
|
||||
test "returns {:duplicate, original_job} if there is a conflict" do
|
||||
job = TestJobWorker.new(%{foo: "bar"}, unique: [period: :infinity])
|
||||
|
||||
{:ok, saved_job_1} = Pinchflat.Repo.insert_unique_job(job)
|
||||
|
||||
assert {:duplicate, saved_job_2} = Pinchflat.Repo.insert_unique_job(job)
|
||||
assert saved_job_1.id == saved_job_2.id
|
||||
end
|
||||
|
||||
test "returns the error if there is an error" do
|
||||
assert {:error, _} = Pinchflat.Repo.insert_unique_job(%Ecto.Changeset{})
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,45 @@
|
||||
defmodule Pinchflat.Tasks.ChannelTasksTest do
|
||||
use Pinchflat.DataCase
|
||||
|
||||
import Pinchflat.TasksFixtures
|
||||
import Pinchflat.MediaSourceFixtures
|
||||
|
||||
alias Pinchflat.Tasks.Task
|
||||
alias Pinchflat.Tasks.ChannelTasks
|
||||
alias Pinchflat.Workers.MediaIndexingWorker
|
||||
|
||||
describe "kickoff_indexing_task/1" do
|
||||
test "it does not schedule a job if the interval is <= 0" do
|
||||
channel = channel_fixture(index_frequency_minutes: -1)
|
||||
|
||||
assert {:ok, :should_not_index} = ChannelTasks.kickoff_indexing_task(channel)
|
||||
|
||||
refute_enqueued(worker: MediaIndexingWorker, args: %{"id" => channel.id})
|
||||
end
|
||||
|
||||
test "it schedules a job if the interval is > 0" do
|
||||
channel = channel_fixture(index_frequency_minutes: 1)
|
||||
|
||||
assert {:ok, _} = ChannelTasks.kickoff_indexing_task(channel)
|
||||
|
||||
assert_enqueued(worker: MediaIndexingWorker, args: %{"id" => channel.id})
|
||||
end
|
||||
|
||||
test "it creates and attaches a task if the interval is > 0" do
|
||||
channel = channel_fixture(index_frequency_minutes: 1)
|
||||
|
||||
assert {:ok, %Task{} = task} = ChannelTasks.kickoff_indexing_task(channel)
|
||||
|
||||
assert task.channel_id == channel.id
|
||||
end
|
||||
|
||||
test "it deletes any pending tasks for the channel" do
|
||||
channel = channel_fixture()
|
||||
task = task_fixture(channel_id: channel.id)
|
||||
|
||||
assert {:ok, _} = ChannelTasks.kickoff_indexing_task(channel)
|
||||
|
||||
assert_raise Ecto.NoResultsError, fn -> Repo.reload!(task) end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,211 @@
|
||||
defmodule Pinchflat.TasksTest do
|
||||
use Pinchflat.DataCase
|
||||
import Pinchflat.JobFixtures
|
||||
import Pinchflat.TasksFixtures
|
||||
import Pinchflat.MediaFixtures
|
||||
import Pinchflat.MediaSourceFixtures
|
||||
|
||||
alias Pinchflat.Tasks
|
||||
alias Pinchflat.Tasks.Task
|
||||
alias Pinchflat.JobFixtures.TestJobWorker
|
||||
|
||||
@invalid_attrs %{job_id: nil}
|
||||
|
||||
describe "schema" do
|
||||
test "it deletes a task when the job gets deleted" do
|
||||
task = Repo.preload(task_fixture(), [:job])
|
||||
|
||||
{:ok, _} = Repo.delete(task.job)
|
||||
|
||||
assert_raise Ecto.NoResultsError, fn -> Repo.reload!(task) end
|
||||
end
|
||||
|
||||
test "it does not delete the other record when a job gets deleted" do
|
||||
task = Repo.preload(task_fixture(), [:channel, :job])
|
||||
|
||||
{:ok, _} = Repo.delete(task.job)
|
||||
|
||||
assert Repo.reload!(task.channel)
|
||||
end
|
||||
end
|
||||
|
||||
describe "list_tasks/0" do
|
||||
test "it returns all tasks" do
|
||||
task = task_fixture()
|
||||
assert Tasks.list_tasks() == [task]
|
||||
end
|
||||
end
|
||||
|
||||
describe "list_tasks_for/3" do
|
||||
test "it lets you specify which record type/ID to join on" do
|
||||
task = task_fixture()
|
||||
|
||||
assert Tasks.list_tasks_for(:channel_id, task.channel_id) == [task]
|
||||
end
|
||||
|
||||
test "it lets you specify which job states to include" do
|
||||
task = task_fixture()
|
||||
|
||||
assert Tasks.list_tasks_for(:channel_id, task.channel_id, [:available]) == [task]
|
||||
assert Tasks.list_tasks_for(:channel_id, task.channel_id, [:cancelled]) == []
|
||||
end
|
||||
end
|
||||
|
||||
describe "list_pending_tasks_for/2" do
|
||||
test "it lists pending tasks" do
|
||||
task = task_fixture()
|
||||
|
||||
assert Tasks.list_pending_tasks_for(:channel_id, task.channel_id) == [task]
|
||||
end
|
||||
|
||||
test "it does not list non-pending tasks" do
|
||||
task = Repo.preload(task_fixture(), :job)
|
||||
:ok = Oban.cancel_job(task.job)
|
||||
|
||||
assert Tasks.list_pending_tasks_for(:channel_id, task.channel_id) == []
|
||||
end
|
||||
end
|
||||
|
||||
describe "get_task!/1" do
|
||||
test "it returns the task with given id" do
|
||||
task = task_fixture()
|
||||
assert Tasks.get_task!(task.id) == task
|
||||
end
|
||||
end
|
||||
|
||||
describe "create_task/1" do
|
||||
test "creation with valid data creates a task" do
|
||||
valid_attrs = %{job_id: job_fixture().id}
|
||||
|
||||
assert {:ok, %Task{} = _task} = Tasks.create_task(valid_attrs)
|
||||
end
|
||||
|
||||
test "creation with invalid data returns error changeset" do
|
||||
assert {:error, %Ecto.Changeset{}} = Tasks.create_task(@invalid_attrs)
|
||||
end
|
||||
|
||||
test "accepts a job and channel" do
|
||||
job = job_fixture()
|
||||
channel = channel_fixture()
|
||||
|
||||
assert {:ok, %Task{} = task} = Tasks.create_task(job, channel)
|
||||
|
||||
assert task.job_id == job.id
|
||||
assert task.channel_id == channel.id
|
||||
end
|
||||
|
||||
test "accepts a job and media item" do
|
||||
job = job_fixture()
|
||||
media_item = media_item_fixture()
|
||||
|
||||
assert {:ok, %Task{} = task} = Tasks.create_task(job, media_item)
|
||||
|
||||
assert task.job_id == job.id
|
||||
assert task.media_item_id == media_item.id
|
||||
end
|
||||
end
|
||||
|
||||
describe "create_job_with_task/2" do
|
||||
test "it enqueues the given job" do
|
||||
media_item = media_item_fixture()
|
||||
|
||||
refute_enqueued(worker: TestJobWorker)
|
||||
assert {:ok, %Task{}} = Tasks.create_job_with_task(TestJobWorker.new(%{}), media_item)
|
||||
assert_enqueued(worker: TestJobWorker)
|
||||
end
|
||||
|
||||
test "it creates a task record if successful" do
|
||||
channel = channel_fixture()
|
||||
|
||||
assert {:ok, %Task{} = task} = Tasks.create_job_with_task(TestJobWorker.new(%{}), channel)
|
||||
|
||||
assert task.channel_id == channel.id
|
||||
end
|
||||
|
||||
test "it returns an error if the job already exists" do
|
||||
channel = channel_fixture()
|
||||
job = TestJobWorker.new(%{foo: "bar"}, unique: [period: :infinity])
|
||||
|
||||
assert {:ok, %Task{}} = Tasks.create_job_with_task(job, channel)
|
||||
assert {:error, :duplicate_job} = Tasks.create_job_with_task(job, channel)
|
||||
end
|
||||
|
||||
test "it returns an error if the job fails to enqueue" do
|
||||
channel = channel_fixture()
|
||||
|
||||
assert {:error, %Ecto.Changeset{}} = Tasks.create_job_with_task(%Ecto.Changeset{}, channel)
|
||||
end
|
||||
end
|
||||
|
||||
describe "delete_task/1" do
|
||||
test "deletion deletes the task" do
|
||||
task = task_fixture()
|
||||
assert {:ok, %Task{}} = Tasks.delete_task(task)
|
||||
assert_raise Ecto.NoResultsError, fn -> Tasks.get_task!(task.id) end
|
||||
end
|
||||
|
||||
test "deletion also cancels the attached job" do
|
||||
task = Repo.preload(task_fixture(), :job)
|
||||
|
||||
assert {:ok, %Task{}} = Tasks.delete_task(task)
|
||||
job = Repo.reload!(task.job)
|
||||
|
||||
assert job.state == "cancelled"
|
||||
end
|
||||
end
|
||||
|
||||
describe "delete_tasks_for/1" do
|
||||
test "it deletes tasks attached to a channel" do
|
||||
channel = channel_fixture()
|
||||
task = task_fixture(channel_id: channel.id)
|
||||
|
||||
assert :ok = Tasks.delete_tasks_for(channel)
|
||||
assert_raise Ecto.NoResultsError, fn -> Tasks.get_task!(task.id) end
|
||||
end
|
||||
|
||||
test "it deletes the tasks attached to a media_item" do
|
||||
media_item = media_item_fixture()
|
||||
task = task_fixture(media_item_id: media_item.id)
|
||||
|
||||
assert :ok = Tasks.delete_tasks_for(media_item)
|
||||
assert_raise Ecto.NoResultsError, fn -> Tasks.get_task!(task.id) end
|
||||
end
|
||||
end
|
||||
|
||||
describe "delete_pending_tasks_for/1" do
|
||||
test "it deletes pending tasks attached to a channel" do
|
||||
channel = channel_fixture()
|
||||
task = task_fixture(channel_id: channel.id)
|
||||
|
||||
assert :ok = Tasks.delete_pending_tasks_for(channel)
|
||||
assert_raise Ecto.NoResultsError, fn -> Tasks.get_task!(task.id) end
|
||||
end
|
||||
|
||||
test "it does not delete non-pending tasks" do
|
||||
channel = channel_fixture()
|
||||
task = Repo.preload(task_fixture(channel_id: channel.id), :job)
|
||||
:ok = Oban.cancel_job(task.job)
|
||||
|
||||
assert :ok = Tasks.delete_pending_tasks_for(channel)
|
||||
assert Tasks.get_task!(task.id)
|
||||
end
|
||||
|
||||
test "it works on media_items" do
|
||||
media_item = media_item_fixture()
|
||||
pending_task = task_fixture(media_item_id: media_item.id)
|
||||
cancelled_task = Repo.preload(task_fixture(media_item_id: media_item.id), :job)
|
||||
:ok = Oban.cancel_job(cancelled_task.job)
|
||||
|
||||
assert :ok = Tasks.delete_pending_tasks_for(media_item)
|
||||
assert Tasks.get_task!(cancelled_task.id)
|
||||
assert_raise Ecto.NoResultsError, fn -> Repo.reload!(pending_task) end
|
||||
end
|
||||
end
|
||||
|
||||
describe "change_task/1" do
|
||||
test "it returns a task changeset" do
|
||||
task = task_fixture()
|
||||
assert %Ecto.Changeset{} = Tasks.change_task(task)
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,7 +1,7 @@
|
||||
defmodule Pinchflat.Utils.StringUtilsTest do
|
||||
use ExUnit.Case, async: true
|
||||
|
||||
alias Pinchflat.Utils.StringUtils, as: StringUtils
|
||||
alias Pinchflat.Utils.StringUtils
|
||||
|
||||
describe "to_kebab_case/1" do
|
||||
test "converts a space-delimited string to kebab-case" do
|
||||
@@ -12,4 +12,19 @@ defmodule Pinchflat.Utils.StringUtilsTest do
|
||||
assert StringUtils.to_kebab_case("hello_world") == "hello-world"
|
||||
end
|
||||
end
|
||||
|
||||
describe "random_string/1" do
|
||||
test "generates a random string" do
|
||||
assert is_binary(StringUtils.random_string())
|
||||
assert StringUtils.random_string() != StringUtils.random_string()
|
||||
end
|
||||
|
||||
test "has a defined default length" do
|
||||
assert String.length(StringUtils.random_string()) == 32
|
||||
end
|
||||
|
||||
test "can generate a string of a given length" do
|
||||
assert String.length(StringUtils.random_string(64)) == 64
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -0,0 +1,110 @@
|
||||
defmodule Pinchflat.Workers.MediaIndexingWorkerTest do
|
||||
use Pinchflat.DataCase
|
||||
|
||||
import Mox
|
||||
import Pinchflat.MediaFixtures
|
||||
import Pinchflat.MediaSourceFixtures
|
||||
|
||||
alias Pinchflat.Tasks
|
||||
alias Pinchflat.Workers.MediaIndexingWorker
|
||||
alias Pinchflat.Workers.VideoDownloadWorker
|
||||
|
||||
setup :verify_on_exit!
|
||||
|
||||
describe "perform/1" do
|
||||
test "it does not do any indexing if the channel shouldn't be indexed" do
|
||||
expect(YtDlpRunnerMock, :run, 0, fn _url, _opts, _ot -> {:ok, ""} end)
|
||||
|
||||
channel = channel_fixture(index_frequency_minutes: -1)
|
||||
|
||||
perform_job(MediaIndexingWorker, %{id: channel.id})
|
||||
end
|
||||
|
||||
test "it does not reschedule if the channel shouldn't be indexed" do
|
||||
expect(YtDlpRunnerMock, :run, 0, fn _url, _opts, _ot -> {:ok, ""} end)
|
||||
|
||||
channel = channel_fixture(index_frequency_minutes: -1)
|
||||
perform_job(MediaIndexingWorker, %{id: channel.id})
|
||||
|
||||
refute_enqueued(worker: MediaIndexingWorker, args: %{"id" => channel.id})
|
||||
end
|
||||
|
||||
test "it indexes the channel if it should be indexed" do
|
||||
expect(YtDlpRunnerMock, :run, fn _url, _opts, _ot -> {:ok, ""} end)
|
||||
|
||||
channel = channel_fixture(index_frequency_minutes: 10)
|
||||
|
||||
perform_job(MediaIndexingWorker, %{id: channel.id})
|
||||
end
|
||||
|
||||
test "it kicks off a download job for each pending media item" do
|
||||
expect(YtDlpRunnerMock, :run, fn _url, _opts, _ot -> {:ok, "video1"} end)
|
||||
|
||||
channel = channel_fixture(index_frequency_minutes: 10)
|
||||
perform_job(MediaIndexingWorker, %{id: channel.id})
|
||||
|
||||
assert [_] = all_enqueued(worker: VideoDownloadWorker)
|
||||
end
|
||||
|
||||
test "it starts a job for any pending media item even if it's from another run" do
|
||||
expect(YtDlpRunnerMock, :run, fn _url, _opts, _ot -> {:ok, "video1"} end)
|
||||
|
||||
channel = channel_fixture(index_frequency_minutes: 10)
|
||||
media_item_fixture(%{channel_id: channel.id, video_filepath: nil})
|
||||
perform_job(MediaIndexingWorker, %{id: channel.id})
|
||||
|
||||
assert [_, _] = all_enqueued(worker: VideoDownloadWorker)
|
||||
end
|
||||
|
||||
test "it does not kick off a job for media items that could not be saved" do
|
||||
expect(YtDlpRunnerMock, :run, fn _url, _opts, _ot -> {:ok, "video1\nvideo1"} end)
|
||||
|
||||
channel = channel_fixture(index_frequency_minutes: 10)
|
||||
perform_job(MediaIndexingWorker, %{id: channel.id})
|
||||
|
||||
# Only one job should be enqueued, since the second video is a duplicate
|
||||
assert [_] = all_enqueued(worker: VideoDownloadWorker)
|
||||
end
|
||||
|
||||
test "it reschedules the job based on the index frequency" do
|
||||
expect(YtDlpRunnerMock, :run, fn _url, _opts, _ot -> {:ok, ""} end)
|
||||
|
||||
channel = channel_fixture(index_frequency_minutes: 10)
|
||||
perform_job(MediaIndexingWorker, %{id: channel.id})
|
||||
|
||||
assert_enqueued(
|
||||
worker: MediaIndexingWorker,
|
||||
args: %{"id" => channel.id},
|
||||
scheduled_at: now_plus(channel.index_frequency_minutes, :minutes)
|
||||
)
|
||||
end
|
||||
|
||||
test "it creates a task for the rescheduled job" do
|
||||
expect(YtDlpRunnerMock, :run, fn _url, _opts, _ot -> {:ok, ""} end)
|
||||
|
||||
channel = channel_fixture(index_frequency_minutes: 10)
|
||||
task_count_fetcher = fn -> Enum.count(Tasks.list_tasks()) end
|
||||
|
||||
assert_changed([from: 0, to: 1], task_count_fetcher, fn ->
|
||||
perform_job(MediaIndexingWorker, %{id: channel.id})
|
||||
end)
|
||||
end
|
||||
|
||||
test "it creates the basic media_item records" do
|
||||
expect(YtDlpRunnerMock, :run, fn _url, _opts, _ot -> {:ok, "video1\nvideo2"} end)
|
||||
|
||||
channel = channel_fixture(index_frequency_minutes: 10)
|
||||
|
||||
media_item_fetcher = fn ->
|
||||
channel
|
||||
|> Repo.preload(:media_items)
|
||||
|> Map.get(:media_items)
|
||||
|> Enum.map(fn media_item -> media_item.media_id end)
|
||||
end
|
||||
|
||||
assert_changed([from: [], to: ["video1", "video2"]], media_item_fetcher, fn ->
|
||||
perform_job(MediaIndexingWorker, %{id: channel.id})
|
||||
end)
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,59 @@
|
||||
defmodule Pinchflat.Workers.VideoDownloadWorkerTest do
|
||||
use Pinchflat.DataCase
|
||||
|
||||
import Mox
|
||||
import Pinchflat.MediaFixtures
|
||||
|
||||
alias Pinchflat.Workers.VideoDownloadWorker
|
||||
|
||||
setup :verify_on_exit!
|
||||
|
||||
setup do
|
||||
media_item =
|
||||
Repo.preload(
|
||||
media_item_fixture(%{video_filepath: nil}),
|
||||
[:metadata, channel: :media_profile]
|
||||
)
|
||||
|
||||
{:ok, %{media_item: media_item}}
|
||||
end
|
||||
|
||||
describe "perform/1" do
|
||||
test "it saves attributes to the media_item", %{media_item: media_item} do
|
||||
expect(YtDlpRunnerMock, :run, fn _url, _opts, _ot ->
|
||||
{:ok, render_metadata(:media_metadata)}
|
||||
end)
|
||||
|
||||
assert media_item.video_filepath == nil
|
||||
perform_job(VideoDownloadWorker, %{id: media_item.id})
|
||||
assert Repo.reload(media_item).video_filepath != nil
|
||||
end
|
||||
|
||||
test "it saves the metadata to the media_item", %{media_item: media_item} do
|
||||
expect(YtDlpRunnerMock, :run, fn _url, _opts, _ot ->
|
||||
{:ok, render_metadata(:media_metadata)}
|
||||
end)
|
||||
|
||||
assert media_item.metadata == nil
|
||||
perform_job(VideoDownloadWorker, %{id: media_item.id})
|
||||
assert Repo.reload(media_item).metadata != nil
|
||||
end
|
||||
|
||||
test "it won't double-schedule downloading jobs", %{media_item: media_item} do
|
||||
Oban.insert(VideoDownloadWorker.new(%{id: media_item.id}))
|
||||
Oban.insert(VideoDownloadWorker.new(%{id: media_item.id}))
|
||||
|
||||
assert [_] = all_enqueued(worker: VideoDownloadWorker)
|
||||
end
|
||||
|
||||
test "it sets the job to retryable if the download fails", %{media_item: media_item} do
|
||||
expect(YtDlpRunnerMock, :run, fn _url, _opts, _ot -> {:error, "error"} end)
|
||||
|
||||
Oban.Testing.with_testing_mode(:inline, fn ->
|
||||
{:ok, job} = Oban.insert(VideoDownloadWorker.new(%{id: media_item.id}))
|
||||
|
||||
assert job.state == "retryable"
|
||||
end)
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,119 @@
|
||||
defmodule PinchflatWeb.ChannelControllerTest do
|
||||
use PinchflatWeb.ConnCase
|
||||
import Mox
|
||||
|
||||
import Pinchflat.ProfilesFixtures
|
||||
import Pinchflat.MediaSourceFixtures
|
||||
|
||||
setup do
|
||||
media_profile = media_profile_fixture()
|
||||
|
||||
{
|
||||
:ok,
|
||||
%{
|
||||
create_attrs: %{
|
||||
media_profile_id: media_profile.id,
|
||||
original_url: "https://www.youtube.com/channel/abc123"
|
||||
},
|
||||
update_attrs: %{
|
||||
original_url: "https://www.youtube.com/channel/321xyz"
|
||||
},
|
||||
invalid_attrs: %{original_url: nil, media_profile_id: nil}
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
setup :verify_on_exit!
|
||||
|
||||
describe "index" do
|
||||
test "lists all channels", %{conn: conn} do
|
||||
conn = get(conn, ~p"/media_sources/channels")
|
||||
assert html_response(conn, 200) =~ "Listing Channels"
|
||||
end
|
||||
end
|
||||
|
||||
describe "new channel" do
|
||||
test "renders form", %{conn: conn} do
|
||||
conn = get(conn, ~p"/media_sources/channels/new")
|
||||
assert html_response(conn, 200) =~ "New Channel"
|
||||
end
|
||||
end
|
||||
|
||||
describe "create channel" do
|
||||
test "redirects to show when data is valid", %{conn: conn, create_attrs: create_attrs} do
|
||||
expect(YtDlpRunnerMock, :run, 1, &runner_function_mock/3)
|
||||
conn = post(conn, ~p"/media_sources/channels", channel: create_attrs)
|
||||
|
||||
assert %{id: id} = redirected_params(conn)
|
||||
assert redirected_to(conn) == ~p"/media_sources/channels/#{id}"
|
||||
|
||||
conn = get(conn, ~p"/media_sources/channels/#{id}")
|
||||
assert html_response(conn, 200) =~ "Channel #{id}"
|
||||
end
|
||||
|
||||
test "renders errors when data is invalid", %{conn: conn, invalid_attrs: invalid_attrs} do
|
||||
conn = post(conn, ~p"/media_sources/channels", channel: invalid_attrs)
|
||||
assert html_response(conn, 200) =~ "New Channel"
|
||||
end
|
||||
end
|
||||
|
||||
describe "edit channel" do
|
||||
setup [:create_channel]
|
||||
|
||||
test "renders form for editing chosen channel", %{conn: conn, channel: channel} do
|
||||
conn = get(conn, ~p"/media_sources/channels/#{channel}/edit")
|
||||
assert html_response(conn, 200) =~ "Edit Channel"
|
||||
end
|
||||
end
|
||||
|
||||
describe "update channel" do
|
||||
setup [:create_channel]
|
||||
|
||||
test "redirects when data is valid", %{conn: conn, channel: channel, update_attrs: update_attrs} do
|
||||
expect(YtDlpRunnerMock, :run, 1, &runner_function_mock/3)
|
||||
|
||||
conn = put(conn, ~p"/media_sources/channels/#{channel}", channel: update_attrs)
|
||||
assert redirected_to(conn) == ~p"/media_sources/channels/#{channel}"
|
||||
|
||||
conn = get(conn, ~p"/media_sources/channels/#{channel}")
|
||||
assert html_response(conn, 200) =~ "https://www.youtube.com/channel/321xyz"
|
||||
end
|
||||
|
||||
test "renders errors when data is invalid", %{
|
||||
conn: conn,
|
||||
channel: channel,
|
||||
invalid_attrs: invalid_attrs
|
||||
} do
|
||||
conn = put(conn, ~p"/media_sources/channels/#{channel}", channel: invalid_attrs)
|
||||
assert html_response(conn, 200) =~ "Edit Channel"
|
||||
end
|
||||
end
|
||||
|
||||
describe "delete channel" do
|
||||
setup [:create_channel]
|
||||
|
||||
test "deletes chosen channel", %{conn: conn, channel: channel} do
|
||||
conn = delete(conn, ~p"/media_sources/channels/#{channel}")
|
||||
assert redirected_to(conn) == ~p"/media_sources/channels"
|
||||
|
||||
assert_error_sent 404, fn ->
|
||||
get(conn, ~p"/media_sources/channels/#{channel}")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
defp create_channel(_) do
|
||||
channel = channel_fixture()
|
||||
%{channel: channel}
|
||||
end
|
||||
|
||||
defp runner_function_mock(_url, _opts, _ot) do
|
||||
{
|
||||
:ok,
|
||||
Phoenix.json_library().encode!(%{
|
||||
channel: "some name",
|
||||
channel_id: "some_channel_id_#{:rand.uniform(1_000_000)}"
|
||||
})
|
||||
}
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,90 @@
|
||||
defmodule PinchflatWeb.MediaProfileControllerTest do
|
||||
use PinchflatWeb.ConnCase
|
||||
|
||||
import Pinchflat.ProfilesFixtures
|
||||
|
||||
@create_attrs %{name: "some name", output_path_template: "some output_path_template"}
|
||||
@update_attrs %{
|
||||
name: "some updated name",
|
||||
output_path_template: "some updated output_path_template"
|
||||
}
|
||||
@invalid_attrs %{name: nil, output_path_template: nil}
|
||||
|
||||
describe "index" do
|
||||
test "lists all media_profiles", %{conn: conn} do
|
||||
conn = get(conn, ~p"/media_profiles")
|
||||
assert html_response(conn, 200) =~ "Listing Media profiles"
|
||||
end
|
||||
end
|
||||
|
||||
describe "new media_profile" do
|
||||
test "renders form", %{conn: conn} do
|
||||
conn = get(conn, ~p"/media_profiles/new")
|
||||
assert html_response(conn, 200) =~ "New Media profile"
|
||||
end
|
||||
end
|
||||
|
||||
describe "create media_profile" do
|
||||
test "redirects to show when data is valid", %{conn: conn} do
|
||||
conn = post(conn, ~p"/media_profiles", media_profile: @create_attrs)
|
||||
|
||||
assert %{id: id} = redirected_params(conn)
|
||||
assert redirected_to(conn) == ~p"/media_profiles/#{id}"
|
||||
|
||||
conn = get(conn, ~p"/media_profiles/#{id}")
|
||||
assert html_response(conn, 200) =~ "Media profile #{id}"
|
||||
end
|
||||
|
||||
test "renders errors when data is invalid", %{conn: conn} do
|
||||
conn = post(conn, ~p"/media_profiles", media_profile: @invalid_attrs)
|
||||
assert html_response(conn, 200) =~ "New Media profile"
|
||||
end
|
||||
end
|
||||
|
||||
describe "edit media_profile" do
|
||||
setup [:create_media_profile]
|
||||
|
||||
test "renders form for editing chosen media_profile", %{
|
||||
conn: conn,
|
||||
media_profile: media_profile
|
||||
} do
|
||||
conn = get(conn, ~p"/media_profiles/#{media_profile}/edit")
|
||||
assert html_response(conn, 200) =~ "Edit Media profile"
|
||||
end
|
||||
end
|
||||
|
||||
describe "update media_profile" do
|
||||
setup [:create_media_profile]
|
||||
|
||||
test "redirects when data is valid", %{conn: conn, media_profile: media_profile} do
|
||||
conn = put(conn, ~p"/media_profiles/#{media_profile}", media_profile: @update_attrs)
|
||||
assert redirected_to(conn) == ~p"/media_profiles/#{media_profile}"
|
||||
|
||||
conn = get(conn, ~p"/media_profiles/#{media_profile}")
|
||||
assert html_response(conn, 200) =~ "some updated name"
|
||||
end
|
||||
|
||||
test "renders errors when data is invalid", %{conn: conn, media_profile: media_profile} do
|
||||
conn = put(conn, ~p"/media_profiles/#{media_profile}", media_profile: @invalid_attrs)
|
||||
assert html_response(conn, 200) =~ "Edit Media profile"
|
||||
end
|
||||
end
|
||||
|
||||
describe "delete media_profile" do
|
||||
setup [:create_media_profile]
|
||||
|
||||
test "deletes chosen media_profile", %{conn: conn, media_profile: media_profile} do
|
||||
conn = delete(conn, ~p"/media_profiles/#{media_profile}")
|
||||
assert redirected_to(conn) == ~p"/media_profiles"
|
||||
|
||||
assert_error_sent 404, fn ->
|
||||
get(conn, ~p"/media_profiles/#{media_profile}")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
defp create_media_profile(_) do
|
||||
media_profile = media_profile_fixture()
|
||||
%{media_profile: media_profile}
|
||||
end
|
||||
end
|
||||
@@ -28,6 +28,7 @@ defmodule PinchflatWeb.ConnCase do
|
||||
import Plug.Conn
|
||||
import Phoenix.ConnTest
|
||||
import PinchflatWeb.ConnCase
|
||||
import Pinchflat.TestingHelperMethods
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -20,10 +20,13 @@ defmodule Pinchflat.DataCase do
|
||||
quote do
|
||||
alias Pinchflat.Repo
|
||||
|
||||
use Oban.Testing, repo: Repo
|
||||
|
||||
import Ecto
|
||||
import Ecto.Changeset
|
||||
import Ecto.Query
|
||||
import Pinchflat.DataCase
|
||||
import Pinchflat.TestingHelperMethods
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,19 @@
|
||||
defmodule Pinchflat.JobFixtures do
|
||||
@moduledoc false
|
||||
|
||||
defmodule TestJobWorker do
|
||||
@moduledoc false
|
||||
use Oban.Worker, queue: :default
|
||||
|
||||
@impl Oban.Worker
|
||||
def perform(%Oban.Job{}) do
|
||||
:ok
|
||||
end
|
||||
end
|
||||
|
||||
def job_fixture() do
|
||||
{:ok, job} = Oban.insert(TestJobWorker.new(%{}))
|
||||
|
||||
job
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,45 @@
|
||||
defmodule Pinchflat.MediaFixtures do
|
||||
@moduledoc """
|
||||
This module defines test helpers for creating
|
||||
entities via the `Pinchflat.Media` context.
|
||||
"""
|
||||
|
||||
alias Pinchflat.MediaSourceFixtures
|
||||
|
||||
@doc """
|
||||
Generate a media_item.
|
||||
"""
|
||||
def media_item_fixture(attrs \\ %{}) do
|
||||
{:ok, media_item} =
|
||||
attrs
|
||||
|> Enum.into(%{
|
||||
media_id: Faker.String.base64(12),
|
||||
title: Faker.Commerce.product_name(),
|
||||
video_filepath: "/video/#{Faker.File.file_name(:video)}",
|
||||
channel_id: MediaSourceFixtures.channel_fixture().id
|
||||
})
|
||||
|> Pinchflat.Media.create_media_item()
|
||||
|
||||
media_item
|
||||
end
|
||||
|
||||
@doc """
|
||||
Generate a media_item with metadata.
|
||||
"""
|
||||
def media_item_with_metadata(attrs \\ %{}) do
|
||||
json_filepath =
|
||||
Path.join([
|
||||
Path.dirname(__ENV__.file),
|
||||
"support",
|
||||
"fixtures",
|
||||
"files",
|
||||
"media_metadata.json"
|
||||
])
|
||||
|
||||
{:ok, file_body} = File.read(json_filepath)
|
||||
{:ok, parsed_json} = Phoenix.json_library().decode(file_body)
|
||||
merged_attrs = Map.merge(attrs, %{metadata: %{client_respinse: parsed_json}})
|
||||
|
||||
media_item_fixture(merged_attrs)
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,30 @@
|
||||
defmodule Pinchflat.MediaSourceFixtures do
|
||||
@moduledoc """
|
||||
This module defines test helpers for creating
|
||||
entities via the `Pinchflat.MediaSource` context.
|
||||
"""
|
||||
|
||||
alias Pinchflat.Repo
|
||||
alias Pinchflat.ProfilesFixtures
|
||||
alias Pinchflat.MediaSource.Channel
|
||||
|
||||
@doc """
|
||||
Generate a channel.
|
||||
"""
|
||||
def channel_fixture(attrs \\ %{}) do
|
||||
{:ok, channel} =
|
||||
%Channel{}
|
||||
|> Channel.changeset(
|
||||
Enum.into(attrs, %{
|
||||
name: "Channel ##{:rand.uniform(1_000_000)}",
|
||||
channel_id: Base.encode16(:crypto.hash(:md5, "#{:rand.uniform(1_000_000)}")),
|
||||
original_url: "https://www.youtube.com/channel/#{Faker.String.base64(12)}",
|
||||
media_profile_id: ProfilesFixtures.media_profile_fixture().id,
|
||||
index_frequency_minutes: 60
|
||||
})
|
||||
)
|
||||
|> Repo.insert()
|
||||
|
||||
channel
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,21 @@
|
||||
defmodule Pinchflat.ProfilesFixtures do
|
||||
@moduledoc """
|
||||
This module defines test helpers for creating
|
||||
entities via the `Pinchflat.Profiles` context.
|
||||
"""
|
||||
|
||||
@doc """
|
||||
Generate a media_profile.
|
||||
"""
|
||||
def media_profile_fixture(attrs \\ %{}) do
|
||||
{:ok, media_profile} =
|
||||
attrs
|
||||
|> Enum.into(%{
|
||||
name: "Media Profile ##{:rand.uniform(1_000_000)}",
|
||||
output_path_template: "{{title}}.{{ext}}"
|
||||
})
|
||||
|> Pinchflat.Profiles.create_media_profile()
|
||||
|
||||
media_profile
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,24 @@
|
||||
defmodule Pinchflat.TasksFixtures do
|
||||
@moduledoc """
|
||||
This module defines test helpers for creating
|
||||
entities via the `Pinchflat.Tasks` context.
|
||||
"""
|
||||
|
||||
alias Pinchflat.JobFixtures
|
||||
alias Pinchflat.MediaSourceFixtures
|
||||
|
||||
@doc """
|
||||
Generate a task.
|
||||
"""
|
||||
def task_fixture(attrs \\ %{}) do
|
||||
{:ok, task} =
|
||||
attrs
|
||||
|> Enum.into(%{
|
||||
channel_id: MediaSourceFixtures.channel_fixture().id,
|
||||
job_id: JobFixtures.job_fixture().id
|
||||
})
|
||||
|> Pinchflat.Tasks.create_task()
|
||||
|
||||
task
|
||||
end
|
||||
end
|
||||
@@ -1,7 +1,16 @@
|
||||
#!/bin/bash
|
||||
|
||||
if [[ "$@" == *"--dump-json"* ]]; then
|
||||
echo '{ "args": "'$@'"}'
|
||||
else
|
||||
echo $@
|
||||
fi
|
||||
# Args come in the format of "<unknown number of args> --print-to-file <output template> <file location> <unknown number of args>".
|
||||
# I need to extract <file location> and write all args to it.
|
||||
|
||||
# Extract the file location (in an unknown position BUT it's 2 args after --print-to-file).
|
||||
for ((i = 1; i <= $#; i++)); do
|
||||
if [ "${!i}" == "--print-to-file" ]; then
|
||||
# Extract the file location.
|
||||
file_location="${@:i+2:1}"
|
||||
break
|
||||
fi
|
||||
done
|
||||
|
||||
# Write all args to the file
|
||||
echo "$@" >"$file_location"
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
defmodule Pinchflat.TestingHelperMethods do
|
||||
@moduledoc false
|
||||
|
||||
use ExUnit.CaseTemplate
|
||||
|
||||
def now do
|
||||
DateTime.utc_now()
|
||||
end
|
||||
|
||||
def now_plus(offset, unit) when unit in [:minute, :minutes] do
|
||||
DateTime.add(now(), offset, :minute)
|
||||
end
|
||||
|
||||
def assert_changed(checker_fun, action_fn) do
|
||||
before_res = checker_fun.()
|
||||
action_fn.()
|
||||
after_res = checker_fun.()
|
||||
|
||||
assert before_res != after_res
|
||||
end
|
||||
|
||||
def assert_changed([from: from, to: to], checker_fun, action_fn) do
|
||||
before_res = checker_fun.()
|
||||
action_fn.()
|
||||
after_res = checker_fun.()
|
||||
|
||||
assert before_res == from
|
||||
assert after_res == to
|
||||
end
|
||||
|
||||
def render_metadata(metadata_name) do
|
||||
json_filepath =
|
||||
Path.join([
|
||||
File.cwd!(),
|
||||
"test",
|
||||
"support",
|
||||
"files",
|
||||
"#{metadata_name}.json"
|
||||
])
|
||||
|
||||
File.read!(json_filepath)
|
||||
end
|
||||
end
|
||||
@@ -1,5 +1,6 @@
|
||||
Mox.defmock(CommandRunnerMock, for: Pinchflat.DownloaderBackends.BackendCommandRunner)
|
||||
Application.put_env(:pinchflat, :yt_dlp_runner, CommandRunnerMock)
|
||||
Mox.defmock(YtDlpRunnerMock, for: Pinchflat.MediaClient.Backends.BackendCommandRunner)
|
||||
Application.put_env(:pinchflat, :yt_dlp_runner, YtDlpRunnerMock)
|
||||
|
||||
ExUnit.start()
|
||||
Ecto.Adapters.SQL.Sandbox.mode(Pinchflat.Repo, :manual)
|
||||
Faker.start()
|
||||
|
||||
Reference in New Issue
Block a user