Added in workers for indexing; hooked them into record creation flow
This commit is contained in:
parent
da1fd98e83
commit
cdd1926049
19 changed files with 387 additions and 25 deletions
|
|
@ -29,7 +29,8 @@ config :pinchflat, PinchflatWeb.Endpoint,
|
||||||
|
|
||||||
config :pinchflat, Oban,
|
config :pinchflat, Oban,
|
||||||
repo: Pinchflat.Repo,
|
repo: Pinchflat.Repo,
|
||||||
plugins: [Oban.Plugins.Pruner],
|
# Keep old jobs for 30 days for display in the UI
|
||||||
|
plugins: [{Oban.Plugins.Pruner, max_age: 30 * 24 * 60 * 60}],
|
||||||
# TODO: consider making this an env var or something?
|
# TODO: consider making this an env var or something?
|
||||||
queues: [default: 10, media_indexing: 2, media_fetching: 2]
|
queues: [default: 10, media_indexing: 2, media_fetching: 2]
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ config :pinchflat,
|
||||||
yt_dlp_executable: Path.join([File.cwd!(), "/test/support/scripts/yt-dlp-mocks/repeater.sh"]),
|
yt_dlp_executable: Path.join([File.cwd!(), "/test/support/scripts/yt-dlp-mocks/repeater.sh"]),
|
||||||
media_directory: Path.join([System.tmp_dir!(), "yt-dlp"])
|
media_directory: Path.join([System.tmp_dir!(), "yt-dlp"])
|
||||||
|
|
||||||
config :pinchflat, Oban, testing: :inline
|
config :pinchflat, Oban, testing: :manual
|
||||||
|
|
||||||
# Configure your database
|
# Configure your database
|
||||||
#
|
#
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,8 @@
|
||||||
defmodule Pinchflat.Media.MediaItem do
|
defmodule Pinchflat.Media.MediaItem do
|
||||||
|
@moduledoc """
|
||||||
|
The MediaItem schema.
|
||||||
|
"""
|
||||||
|
|
||||||
use Ecto.Schema
|
use Ecto.Schema
|
||||||
import Ecto.Changeset
|
import Ecto.Changeset
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -25,7 +25,9 @@ defmodule Pinchflat.MediaClient.ChannelDetails do
|
||||||
end
|
end
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
TODO: test
|
Returns a list of video IDs for the given channel URL, using the given backend.
|
||||||
|
|
||||||
|
Returns {:ok, list(binary())} | {:error, any, ...}.
|
||||||
"""
|
"""
|
||||||
def get_video_ids(channel_url, backend \\ :yt_dlp) do
|
def get_video_ids(channel_url, backend \\ :yt_dlp) do
|
||||||
channel_module(backend).get_video_ids(channel_url)
|
channel_module(backend).get_video_ids(channel_url)
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ defmodule Pinchflat.MediaSource do
|
||||||
alias Pinchflat.Repo
|
alias Pinchflat.Repo
|
||||||
|
|
||||||
alias Pinchflat.Media
|
alias Pinchflat.Media
|
||||||
|
alias Pinchflat.Tasks.ChannelTasks
|
||||||
alias Pinchflat.MediaSource.Channel
|
alias Pinchflat.MediaSource.Channel
|
||||||
alias Pinchflat.MediaClient.ChannelDetails
|
alias Pinchflat.MediaClient.ChannelDetails
|
||||||
|
|
||||||
|
|
@ -25,12 +26,16 @@ defmodule Pinchflat.MediaSource do
|
||||||
def get_channel!(id), do: Repo.get!(Channel, id)
|
def get_channel!(id), do: Repo.get!(Channel, id)
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
Creates a channel. Returns {:ok, %Channel{}} | {:error, %Ecto.Changeset{}}
|
Creates a channel. May attempt to pull additional channel details from the
|
||||||
|
original_url (if provided). Will attempt to start indexing the channel's
|
||||||
|
media if successfully inserted.
|
||||||
|
|
||||||
|
Returns {:ok, %Channel{}} | {:error, %Ecto.Changeset{}}
|
||||||
"""
|
"""
|
||||||
def create_channel(attrs \\ %{}) do
|
def create_channel(attrs \\ %{}) do
|
||||||
%Channel{}
|
%Channel{}
|
||||||
|> change_channel_from_url(attrs)
|
|> change_channel_from_url(attrs)
|
||||||
|> Repo.insert()
|
|> commit_and_start_indexing()
|
||||||
end
|
end
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
|
|
@ -38,8 +43,6 @@ defmodule Pinchflat.MediaSource do
|
||||||
media ID in the source.
|
media ID in the source.
|
||||||
|
|
||||||
Returns [%MediaItem{}, ...] | [%Ecto.Changeset{}, ...]
|
Returns [%MediaItem{}, ...] | [%Ecto.Changeset{}, ...]
|
||||||
|
|
||||||
TODO: test
|
|
||||||
"""
|
"""
|
||||||
def index_media_items(%Channel{} = channel) do
|
def index_media_items(%Channel{} = channel) do
|
||||||
{:ok, media_ids} = ChannelDetails.get_video_ids(channel.original_url)
|
{:ok, media_ids} = ChannelDetails.get_video_ids(channel.original_url)
|
||||||
|
|
@ -56,12 +59,19 @@ defmodule Pinchflat.MediaSource do
|
||||||
end
|
end
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
Updates a channel. Returns {:ok, %Channel{}} | {:error, %Ecto.Changeset{}}
|
Updates a channel. May attempt to pull additional channel details from the
|
||||||
|
original_url (if changed). May attempt to start indexing the channel's
|
||||||
|
media if the indexing frequency has been changed.
|
||||||
|
|
||||||
|
TODO: ensure that indexing is cancelled/rescheduled if the indexing frequency
|
||||||
|
has been changed.
|
||||||
|
|
||||||
|
Returns {:ok, %Channel{}} | {:error, %Ecto.Changeset{}}
|
||||||
"""
|
"""
|
||||||
def update_channel(%Channel{} = channel, attrs) do
|
def update_channel(%Channel{} = channel, attrs) do
|
||||||
channel
|
channel
|
||||||
|> change_channel_from_url(attrs)
|
|> change_channel_from_url(attrs)
|
||||||
|> Repo.update()
|
|> commit_and_start_indexing()
|
||||||
end
|
end
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
|
|
@ -119,4 +129,31 @@ defmodule Pinchflat.MediaSource do
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
defp commit_and_start_indexing(changeset) do
|
||||||
|
case Repo.insert_or_update(changeset) do
|
||||||
|
{:ok, %Channel{} = channel} ->
|
||||||
|
maybe_run_indexing_task(changeset, channel)
|
||||||
|
|
||||||
|
{:ok, channel}
|
||||||
|
|
||||||
|
err ->
|
||||||
|
err
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp maybe_run_indexing_task(changeset, channel) do
|
||||||
|
case changeset.data do
|
||||||
|
# If the changeset is new (not persisted), start indexing no matter what
|
||||||
|
%{__meta__: %{state: :built}} ->
|
||||||
|
ChannelTasks.kickoff_indexing_task(channel)
|
||||||
|
|
||||||
|
# If the record has been persisted, only run indexing if the
|
||||||
|
# indexing frequency has been changed
|
||||||
|
%{__meta__: %{state: :loaded}} ->
|
||||||
|
if Map.has_key?(changeset.changes, :index_frequency_minutes) do
|
||||||
|
ChannelTasks.kickoff_indexing_task(channel)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -9,12 +9,13 @@ defmodule Pinchflat.MediaSource.Channel do
|
||||||
alias Pinchflat.Media.MediaItem
|
alias Pinchflat.Media.MediaItem
|
||||||
alias Pinchflat.Profiles.MediaProfile
|
alias Pinchflat.Profiles.MediaProfile
|
||||||
|
|
||||||
@required_fields ~w(name channel_id original_url media_profile_id)a
|
@allowed_fields ~w(name channel_id index_frequency_minutes original_url media_profile_id)a
|
||||||
@allowed_fields @required_fields
|
@required_fields @allowed_fields -- ~w(index_frequency_minutes)a
|
||||||
|
|
||||||
schema "channels" do
|
schema "channels" do
|
||||||
field :name, :string
|
field :name, :string
|
||||||
field :channel_id, :string
|
field :channel_id, :string
|
||||||
|
field :index_frequency_minutes, :integer
|
||||||
# This should only be used for user reference going forward
|
# This should only be used for user reference going forward
|
||||||
# as the channel_id should be used for all API calls
|
# as the channel_id should be used for all API calls
|
||||||
field :original_url, :string
|
field :original_url, :string
|
||||||
|
|
|
||||||
27
lib/pinchflat/tasks/channel_tasks.ex
Normal file
27
lib/pinchflat/tasks/channel_tasks.ex
Normal file
|
|
@ -0,0 +1,27 @@
|
||||||
|
defmodule Pinchflat.Tasks.ChannelTasks do
|
||||||
|
@moduledoc """
|
||||||
|
This module contains methods for managing tasks (workers) related to channels.
|
||||||
|
"""
|
||||||
|
|
||||||
|
alias Pinchflat.MediaSource.Channel
|
||||||
|
alias Pinchflat.Workers.MediaIndexingWorker
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Starts tasks for indexing a channel's media.
|
||||||
|
|
||||||
|
TODO: modify so that updates cancel/reschedule existing tasks as-needed
|
||||||
|
TODO: modify so that deletion cancels existing tasks (or maybe can do from Postgres?)
|
||||||
|
TODO: modify so that starting a worker adds a Task record (not implemented yet)
|
||||||
|
"""
|
||||||
|
def kickoff_indexing_task(%Channel{} = channel) do
|
||||||
|
if channel.index_frequency_minutes <= 0 do
|
||||||
|
{:ok, :should_not_index}
|
||||||
|
else
|
||||||
|
channel
|
||||||
|
|> Map.take([:id])
|
||||||
|
# Schedule this one immediately, but future ones will be on an interval
|
||||||
|
|> MediaIndexingWorker.new()
|
||||||
|
|> Oban.insert()
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
@ -1,17 +1,46 @@
|
||||||
defmodule Pinchflat.Workers.MediaIndexingWorker do
|
defmodule Pinchflat.Workers.MediaIndexingWorker do
|
||||||
use Oban.Worker, queue: :media_indexing
|
@moduledoc false
|
||||||
|
|
||||||
|
use Oban.Worker,
|
||||||
|
queue: :media_indexing,
|
||||||
|
unique: [period: :infinity, states: [:available, :scheduled]]
|
||||||
|
|
||||||
|
alias __MODULE__
|
||||||
alias Pinchflat.MediaSource
|
alias Pinchflat.MediaSource
|
||||||
|
|
||||||
@impl Oban.Worker
|
@impl Oban.Worker
|
||||||
# This `channel_id` is the ID of the channel _record_ in
|
@doc """
|
||||||
# the database, not the ID of the channel on YouTube.
|
The ID is that of a channel _record_, not a YouTube channel ID.
|
||||||
# TODO: test
|
|
||||||
def perform(%Oban.Job{args: %{"channel_id" => channel_id}}) do
|
|
||||||
channel_id
|
|
||||||
|> MediaSource.get_channel!()
|
|
||||||
|> MediaSource.index_media_items()
|
|
||||||
|
|
||||||
|
NOTE: Re-scheduling here works a little different than you may expect.
|
||||||
|
The reschedule time is relative to the time the job has actually _completed_.
|
||||||
|
This has some benefits but also side effects to be aware of:
|
||||||
|
|
||||||
|
- Benefit: No chance for jobs to overlap if a job takes longer than the
|
||||||
|
scheduled interval. Less likely to hit API rate limits.
|
||||||
|
- Side effect: Intervals are "soft" and _always_ walk forward. This may cause
|
||||||
|
user confusion since a 30-minute job scheduled for every hour will
|
||||||
|
actually run every 1 hour and 30 minutes. The tradeoff of not inundating
|
||||||
|
the API with requests and also not overlapping jobs is worth it, IMO.
|
||||||
|
|
||||||
|
Returns :ok | {:ok, %Oban.Job{}}. Not that it matters.
|
||||||
|
"""
|
||||||
|
def perform(%Oban.Job{args: %{"id" => channel_id}}) do
|
||||||
|
channel = MediaSource.get_channel!(channel_id)
|
||||||
|
|
||||||
|
if channel.index_frequency_minutes <= 0 do
|
||||||
:ok
|
:ok
|
||||||
|
else
|
||||||
|
index_media_and_reschedule(channel)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp index_media_and_reschedule(channel) do
|
||||||
|
MediaSource.index_media_items(channel)
|
||||||
|
|
||||||
|
channel
|
||||||
|
|> Map.take([:id])
|
||||||
|
|> MediaIndexingWorker.new(schedule_in: channel.index_frequency_minutes * 60)
|
||||||
|
|> Oban.insert()
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -11,4 +11,17 @@ defmodule PinchflatWeb.MediaSources.ChannelHTML do
|
||||||
attr :media_profiles, :list, required: true
|
attr :media_profiles, :list, required: true
|
||||||
|
|
||||||
def channel_form(assigns)
|
def channel_form(assigns)
|
||||||
|
|
||||||
|
def friendly_index_frequencies do
|
||||||
|
[
|
||||||
|
{"Never", -1},
|
||||||
|
{"1 Hour", 60},
|
||||||
|
{"3 Hours", 3 * 60},
|
||||||
|
{"6 Hours", 6 * 60},
|
||||||
|
{"12 Hours", 12 * 60},
|
||||||
|
{"Daily (recommended)", 24 * 60},
|
||||||
|
{"Weekly", 7 * 24 * 60},
|
||||||
|
{"Monthly", 30 * 24 * 60}
|
||||||
|
]
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,13 @@
|
||||||
|
|
||||||
<.input field={f[:original_url]} type="text" label="Channel URL" />
|
<.input field={f[:original_url]} type="text" label="Channel URL" />
|
||||||
|
|
||||||
|
<.input
|
||||||
|
field={f[:index_frequency_minutes]}
|
||||||
|
options={friendly_index_frequencies()}
|
||||||
|
type="select"
|
||||||
|
label="Index Frequency"
|
||||||
|
/>
|
||||||
|
|
||||||
<:actions>
|
<:actions>
|
||||||
<.button>Save Channel</.button>
|
<.button>Save Channel</.button>
|
||||||
</:actions>
|
</:actions>
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,9 @@
|
||||||
|
defmodule Pinchflat.Repo.Migrations.AddIndexFrequencyToChannels do
|
||||||
|
use Ecto.Migration
|
||||||
|
|
||||||
|
def change do
|
||||||
|
alter table(:channels) do
|
||||||
|
add :index_frequency_minutes, :integer, default: 60 * 24, null: false
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
@ -35,4 +35,24 @@ defmodule Pinchflat.MediaClient.ChannelDetailsTest do
|
||||||
assert %ChannelDetails{id: "UCQH2", name: "TheUselessTrials"} = res
|
assert %ChannelDetails{id: "UCQH2", name: "TheUselessTrials"} = res
|
||||||
end
|
end
|
||||||
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 ->
|
||||||
|
assert opts == [:simulate, :skip_download, {:print, :id}]
|
||||||
|
|
||||||
|
{: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 ->
|
||||||
|
{:ok, "video1\nvideo2\nvideo3"}
|
||||||
|
end)
|
||||||
|
|
||||||
|
assert {:ok, ["video1", "video2", "video3"]} = ChannelDetails.get_video_ids(@channel_url)
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,14 @@
|
||||||
defmodule Pinchflat.MediaSourceTest do
|
defmodule Pinchflat.MediaSourceTest do
|
||||||
use Pinchflat.DataCase
|
use Pinchflat.DataCase
|
||||||
import Mox
|
import Mox
|
||||||
|
|
||||||
alias Pinchflat.MediaSource
|
|
||||||
alias Pinchflat.MediaSource.Channel
|
|
||||||
|
|
||||||
import Pinchflat.ProfilesFixtures
|
import Pinchflat.ProfilesFixtures
|
||||||
import Pinchflat.MediaSourceFixtures
|
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}
|
@invalid_channel_attrs %{name: nil, channel_id: nil}
|
||||||
|
|
||||||
setup :verify_on_exit!
|
setup :verify_on_exit!
|
||||||
|
|
@ -82,6 +83,72 @@ defmodule Pinchflat.MediaSourceTest do
|
||||||
assert {:ok, %Channel{}} = MediaSource.create_channel(channel_1_attrs)
|
assert {:ok, %Channel{}} = MediaSource.create_channel(channel_1_attrs)
|
||||||
assert {:ok, %Channel{}} = MediaSource.create_channel(channel_2_attrs)
|
assert {:ok, %Channel{}} = MediaSource.create_channel(channel_2_attrs)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
test "creation will schedule the indexing task" do
|
||||||
|
expect(YtDlpRunnerMock, :run, &runner_function_mock/2)
|
||||||
|
|
||||||
|
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 -> {: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
|
end
|
||||||
|
|
||||||
describe "update_channel/2" do
|
describe "update_channel/2" do
|
||||||
|
|
@ -113,6 +180,23 @@ defmodule Pinchflat.MediaSourceTest do
|
||||||
assert {:ok, %Channel{}} = MediaSource.update_channel(channel, update_attrs)
|
assert {:ok, %Channel{}} = MediaSource.update_channel(channel, update_attrs)
|
||||||
end
|
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
|
test "updates with invalid data returns error changeset" do
|
||||||
channel = channel_fixture()
|
channel = channel_fixture()
|
||||||
|
|
||||||
|
|
|
||||||
26
test/pinchflat/tasks/channel_tasks_test.exs
Normal file
26
test/pinchflat/tasks/channel_tasks_test.exs
Normal file
|
|
@ -0,0 +1,26 @@
|
||||||
|
defmodule Pinchflat.Tasks.ChannelTasksTest do
|
||||||
|
use Pinchflat.DataCase
|
||||||
|
|
||||||
|
import Pinchflat.MediaSourceFixtures
|
||||||
|
|
||||||
|
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
|
||||||
|
end
|
||||||
|
end
|
||||||
67
test/pinchflat/workers/media_indexing_worker_test.exs
Normal file
67
test/pinchflat/workers/media_indexing_worker_test.exs
Normal file
|
|
@ -0,0 +1,67 @@
|
||||||
|
defmodule Pinchflat.Workers.MediaIndexingWorkerTest do
|
||||||
|
use Pinchflat.DataCase
|
||||||
|
|
||||||
|
import Mox
|
||||||
|
import Pinchflat.MediaSourceFixtures
|
||||||
|
|
||||||
|
alias Pinchflat.Workers.MediaIndexingWorker
|
||||||
|
|
||||||
|
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 -> {: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 -> {: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, 1, fn _url, _opts -> {:ok, ""} end)
|
||||||
|
|
||||||
|
channel = channel_fixture(index_frequency_minutes: 10)
|
||||||
|
|
||||||
|
perform_job(MediaIndexingWorker, %{id: channel.id})
|
||||||
|
end
|
||||||
|
|
||||||
|
test "it reschedules the job based on the index frequency" do
|
||||||
|
expect(YtDlpRunnerMock, :run, 1, fn _url, _opts -> {: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 the basic media_item records" do
|
||||||
|
expect(YtDlpRunnerMock, :run, 1, fn _url, _opts -> {: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
|
||||||
|
|
@ -28,6 +28,7 @@ defmodule PinchflatWeb.ConnCase do
|
||||||
import Plug.Conn
|
import Plug.Conn
|
||||||
import Phoenix.ConnTest
|
import Phoenix.ConnTest
|
||||||
import PinchflatWeb.ConnCase
|
import PinchflatWeb.ConnCase
|
||||||
|
import Pinchflat.TestingHelperMethods
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -20,10 +20,13 @@ defmodule Pinchflat.DataCase do
|
||||||
quote do
|
quote do
|
||||||
alias Pinchflat.Repo
|
alias Pinchflat.Repo
|
||||||
|
|
||||||
|
use Oban.Testing, repo: Repo
|
||||||
|
|
||||||
import Ecto
|
import Ecto
|
||||||
import Ecto.Changeset
|
import Ecto.Changeset
|
||||||
import Ecto.Query
|
import Ecto.Query
|
||||||
import Pinchflat.DataCase
|
import Pinchflat.DataCase
|
||||||
|
import Pinchflat.TestingHelperMethods
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,8 @@ defmodule Pinchflat.MediaSourceFixtures do
|
||||||
name: "Channel ##{:rand.uniform(1_000_000)}",
|
name: "Channel ##{:rand.uniform(1_000_000)}",
|
||||||
channel_id: Base.encode16(:crypto.hash(:md5, "#{: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)}",
|
original_url: "https://www.youtube.com/channel/#{Faker.String.base64(12)}",
|
||||||
media_profile_id: ProfilesFixtures.media_profile_fixture().id
|
media_profile_id: ProfilesFixtures.media_profile_fixture().id,
|
||||||
|
index_frequency_minutes: 60
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
|> Repo.insert()
|
|> Repo.insert()
|
||||||
|
|
|
||||||
30
test/support/testing_helper_methods.ex
Normal file
30
test/support/testing_helper_methods.ex
Normal file
|
|
@ -0,0 +1,30 @@
|
||||||
|
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
|
||||||
|
end
|
||||||
Loading…
Reference in a new issue