[Enhancement] Adds ability to enable/disable sources (#481)
* [Unrelated] updated module name for existing liveview module * Updated toggle component and moved MP index table to a liveview * [WIP] reverted MP index table; added source count to MP index * Moved new live table to sources index * Added 'enabled' boolean to sources * Got 'enabled' logic working re: downloading pending media * Updated sources context to do the right thing when a source is updated * Docs and tests * Updated slow indexing to maintain its old schedule if re-enabled * Hooked up the enabled toggle to the sources page * [Unrelated] added direct links to various tabs on the sources table * More tests * Removed unneeded guard in * Removed outdated comment
This commit is contained in:
@@ -35,3 +35,10 @@ window.markVersionAsSeen = (versionString) => {
|
|||||||
window.isVersionSeen = (versionString) => {
|
window.isVersionSeen = (versionString) => {
|
||||||
return localStorage.getItem('seenVersion') === versionString
|
return localStorage.getItem('seenVersion') === versionString
|
||||||
}
|
}
|
||||||
|
|
||||||
|
window.dispatchFor = (elementOrId, eventName, detail = {}) => {
|
||||||
|
const element =
|
||||||
|
typeof elementOrId === 'string' ? document.getElementById(elementOrId) : elementOrId
|
||||||
|
|
||||||
|
element.dispatchEvent(new CustomEvent(eventName, { detail }))
|
||||||
|
}
|
||||||
|
|||||||
+24
-1
@@ -39,7 +39,7 @@ let liveSocket = new LiveSocket(document.body.dataset.socketPath, Socket, {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
hooks: {
|
hooks: {
|
||||||
supressEnterSubmission: {
|
'supress-enter-submission': {
|
||||||
mounted() {
|
mounted() {
|
||||||
this.el.addEventListener('keypress', (event) => {
|
this.el.addEventListener('keypress', (event) => {
|
||||||
if (event.key === 'Enter') {
|
if (event.key === 'Enter') {
|
||||||
@@ -47,6 +47,29 @@ let liveSocket = new LiveSocket(document.body.dataset.socketPath, Socket, {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
'formless-input': {
|
||||||
|
mounted() {
|
||||||
|
const subscribedEvents = this.el.dataset.subscribe.split(' ')
|
||||||
|
const eventName = this.el.dataset.eventName || ''
|
||||||
|
const identifier = this.el.dataset.identifier || ''
|
||||||
|
|
||||||
|
subscribedEvents.forEach((domEvent) => {
|
||||||
|
this.el.addEventListener(domEvent, () => {
|
||||||
|
// This ensures that the event is pushed to the server after the input value has been updated
|
||||||
|
// so that the server has the most up-to-date value
|
||||||
|
setTimeout(() => {
|
||||||
|
this.pushEvent('formless-input', {
|
||||||
|
value: this.el.value,
|
||||||
|
id: identifier,
|
||||||
|
event: eventName,
|
||||||
|
dom_id: this.el.id,
|
||||||
|
dom_event: domEvent
|
||||||
|
})
|
||||||
|
}, 0)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -11,14 +11,27 @@ defmodule Pinchflat.FastIndexing.FastIndexingHelpers do
|
|||||||
|
|
||||||
alias Pinchflat.Repo
|
alias Pinchflat.Repo
|
||||||
alias Pinchflat.Media
|
alias Pinchflat.Media
|
||||||
|
alias Pinchflat.Tasks
|
||||||
alias Pinchflat.Sources.Source
|
alias Pinchflat.Sources.Source
|
||||||
alias Pinchflat.FastIndexing.YoutubeRss
|
alias Pinchflat.FastIndexing.YoutubeRss
|
||||||
alias Pinchflat.FastIndexing.YoutubeApi
|
alias Pinchflat.FastIndexing.YoutubeApi
|
||||||
alias Pinchflat.Downloading.DownloadingHelpers
|
alias Pinchflat.Downloading.DownloadingHelpers
|
||||||
|
alias Pinchflat.FastIndexing.FastIndexingWorker
|
||||||
alias Pinchflat.Downloading.DownloadOptionBuilder
|
alias Pinchflat.Downloading.DownloadOptionBuilder
|
||||||
|
|
||||||
alias Pinchflat.YtDlp.Media, as: YtDlpMedia
|
alias Pinchflat.YtDlp.Media, as: YtDlpMedia
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Kicks off a new fast indexing task for a source. This will delete any existing fast indexing
|
||||||
|
tasks for the source before starting a new one.
|
||||||
|
|
||||||
|
Returns {:ok, %Task{}}
|
||||||
|
"""
|
||||||
|
def kickoff_indexing_task(%Source{} = source) do
|
||||||
|
Tasks.delete_pending_tasks_for(source, "FastIndexingWorker", include_executing: true)
|
||||||
|
FastIndexingWorker.kickoff_with_task(source)
|
||||||
|
end
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
Fetches new media IDs for a source from YT's API or RSS, indexes them, and kicks off downloading
|
Fetches new media IDs for a source from YT's API or RSS, indexes them, and kicks off downloading
|
||||||
tasks for any pending media items. See comments in `FastIndexingWorker` for more info on the
|
tasks for any pending media items. See comments in `FastIndexingWorker` for more info on the
|
||||||
|
|||||||
@@ -0,0 +1,29 @@
|
|||||||
|
defmodule Pinchflat.Profiles.ProfilesQuery do
|
||||||
|
@moduledoc """
|
||||||
|
Query helpers for the Profiles context.
|
||||||
|
|
||||||
|
These methods are made to be one-ish liners used
|
||||||
|
to compose queries. Each method should strive to do
|
||||||
|
_one_ thing. These don't need to be tested as
|
||||||
|
they are just building blocks for other functionality
|
||||||
|
which, itself, will be tested.
|
||||||
|
"""
|
||||||
|
import Ecto.Query, warn: false
|
||||||
|
|
||||||
|
alias Pinchflat.Profiles.MediaProfile
|
||||||
|
|
||||||
|
# This allows the module to be aliased and query methods to be used
|
||||||
|
# all in one go
|
||||||
|
# usage: use Pinchflat.Profiles.ProfilesQuery
|
||||||
|
defmacro __using__(_opts) do
|
||||||
|
quote do
|
||||||
|
import Ecto.Query, warn: false
|
||||||
|
|
||||||
|
alias unquote(__MODULE__)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def new do
|
||||||
|
MediaProfile
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -25,13 +25,28 @@ defmodule Pinchflat.SlowIndexing.SlowIndexingHelpers do
|
|||||||
Starts tasks for indexing a source's media regardless of the source's indexing
|
Starts tasks for indexing a source's media regardless of the source's indexing
|
||||||
frequency. It's assumed the caller will check for indexing frequency.
|
frequency. It's assumed the caller will check for indexing frequency.
|
||||||
|
|
||||||
Returns {:ok, %Task{}}.
|
Returns {:ok, %Task{}}
|
||||||
"""
|
"""
|
||||||
def kickoff_indexing_task(%Source{} = source, job_args \\ %{}, job_opts \\ []) do
|
def kickoff_indexing_task(%Source{} = source, job_args \\ %{}, job_opts \\ []) do
|
||||||
|
job_offset_seconds = calculate_job_offset_seconds(source)
|
||||||
|
|
||||||
Tasks.delete_pending_tasks_for(source, "FastIndexingWorker")
|
Tasks.delete_pending_tasks_for(source, "FastIndexingWorker")
|
||||||
Tasks.delete_pending_tasks_for(source, "MediaCollectionIndexingWorker", include_executing: true)
|
Tasks.delete_pending_tasks_for(source, "MediaCollectionIndexingWorker", include_executing: true)
|
||||||
|
|
||||||
MediaCollectionIndexingWorker.kickoff_with_task(source, job_args, job_opts)
|
MediaCollectionIndexingWorker.kickoff_with_task(source, job_args, job_opts ++ [schedule_in: job_offset_seconds])
|
||||||
|
end
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
A helper method to delete all indexing-related tasks for a source.
|
||||||
|
Optionally, you can include executing tasks in the deletion process.
|
||||||
|
|
||||||
|
Returns :ok
|
||||||
|
"""
|
||||||
|
def delete_indexing_tasks(%Source{} = source, opts \\ []) do
|
||||||
|
include_executing = Keyword.get(opts, :include_executing, false)
|
||||||
|
|
||||||
|
Tasks.delete_pending_tasks_for(source, "FastIndexingWorker", include_executing: include_executing)
|
||||||
|
Tasks.delete_pending_tasks_for(source, "MediaCollectionIndexingWorker", include_executing: include_executing)
|
||||||
end
|
end
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
@@ -141,4 +156,14 @@ defmodule Pinchflat.SlowIndexing.SlowIndexingHelpers do
|
|||||||
changeset
|
changeset
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Find the difference between the current time and the last time the source was indexed
|
||||||
|
defp calculate_job_offset_seconds(%Source{last_indexed_at: nil}), do: 0
|
||||||
|
|
||||||
|
defp calculate_job_offset_seconds(source) do
|
||||||
|
offset_seconds = DateTime.diff(DateTime.utc_now(), source.last_indexed_at, :second)
|
||||||
|
index_frequency_seconds = source.index_frequency_minutes * 60
|
||||||
|
|
||||||
|
max(0, index_frequency_seconds - offset_seconds)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ defmodule Pinchflat.Sources.Source do
|
|||||||
alias Pinchflat.Metadata.SourceMetadata
|
alias Pinchflat.Metadata.SourceMetadata
|
||||||
|
|
||||||
@allowed_fields ~w(
|
@allowed_fields ~w(
|
||||||
|
enabled
|
||||||
collection_name
|
collection_name
|
||||||
collection_id
|
collection_id
|
||||||
collection_type
|
collection_type
|
||||||
@@ -64,6 +65,7 @@ defmodule Pinchflat.Sources.Source do
|
|||||||
)a
|
)a
|
||||||
|
|
||||||
schema "sources" do
|
schema "sources" do
|
||||||
|
field :enabled, :boolean, default: true
|
||||||
# This is _not_ used as the primary key or internally in the database
|
# This is _not_ used as the primary key or internally in the database
|
||||||
# relations. This is only used to prevent an enumeration attack on the streaming
|
# relations. This is only used to prevent an enumeration attack on the streaming
|
||||||
# and RSS feed endpoints since those _must_ be public (ie: no basic auth)
|
# and RSS feed endpoints since those _must_ be public (ie: no basic auth)
|
||||||
|
|||||||
@@ -15,8 +15,8 @@ defmodule Pinchflat.Sources do
|
|||||||
alias Pinchflat.Metadata.SourceMetadata
|
alias Pinchflat.Metadata.SourceMetadata
|
||||||
alias Pinchflat.Utils.FilesystemUtils
|
alias Pinchflat.Utils.FilesystemUtils
|
||||||
alias Pinchflat.Downloading.DownloadingHelpers
|
alias Pinchflat.Downloading.DownloadingHelpers
|
||||||
alias Pinchflat.FastIndexing.FastIndexingWorker
|
|
||||||
alias Pinchflat.SlowIndexing.SlowIndexingHelpers
|
alias Pinchflat.SlowIndexing.SlowIndexingHelpers
|
||||||
|
alias Pinchflat.FastIndexing.FastIndexingHelpers
|
||||||
alias Pinchflat.Metadata.SourceMetadataStorageWorker
|
alias Pinchflat.Metadata.SourceMetadataStorageWorker
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
@@ -255,19 +255,40 @@ defmodule Pinchflat.Sources do
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
# If the source is NOT new (ie: updated) and the download_media flag has changed,
|
# If the source is new (ie: not persisted), do nothing
|
||||||
|
defp maybe_handle_media_tasks(%{data: %{__meta__: %{state: state}}}, _source) when state != :loaded do
|
||||||
|
:ok
|
||||||
|
end
|
||||||
|
|
||||||
|
# If the source is NOT new (ie: updated),
|
||||||
# enqueue or dequeue media download tasks as necessary.
|
# enqueue or dequeue media download tasks as necessary.
|
||||||
defp maybe_handle_media_tasks(changeset, source) do
|
defp maybe_handle_media_tasks(changeset, source) do
|
||||||
case {changeset.data, changeset.changes} do
|
current_changes = changeset.changes
|
||||||
{%{__meta__: %{state: :loaded}}, %{download_media: true}} ->
|
applied_changes = Ecto.Changeset.apply_changes(changeset)
|
||||||
|
|
||||||
|
# We need both current_changes and applied_changes to determine
|
||||||
|
# the course of action to take. For example, we only care if a source is supposed
|
||||||
|
# to be `enabled` or not - we don't care if that information comes from the
|
||||||
|
# current changes or if that's how it already was in the database.
|
||||||
|
# Rephrased, we're essentially using it in place of `get_field/2`
|
||||||
|
case {current_changes, applied_changes} do
|
||||||
|
{%{download_media: true}, %{enabled: true}} ->
|
||||||
DownloadingHelpers.enqueue_pending_download_tasks(source)
|
DownloadingHelpers.enqueue_pending_download_tasks(source)
|
||||||
|
|
||||||
{%{__meta__: %{state: :loaded}}, %{download_media: false}} ->
|
{%{enabled: true}, %{download_media: true}} ->
|
||||||
|
DownloadingHelpers.enqueue_pending_download_tasks(source)
|
||||||
|
|
||||||
|
{%{download_media: false}, _} ->
|
||||||
|
DownloadingHelpers.dequeue_pending_download_tasks(source)
|
||||||
|
|
||||||
|
{%{enabled: false}, _} ->
|
||||||
DownloadingHelpers.dequeue_pending_download_tasks(source)
|
DownloadingHelpers.dequeue_pending_download_tasks(source)
|
||||||
|
|
||||||
_ ->
|
_ ->
|
||||||
:ok
|
nil
|
||||||
end
|
end
|
||||||
|
|
||||||
|
:ok
|
||||||
end
|
end
|
||||||
|
|
||||||
defp maybe_run_indexing_task(changeset, source) do
|
defp maybe_run_indexing_task(changeset, source) do
|
||||||
@@ -301,13 +322,22 @@ defmodule Pinchflat.Sources do
|
|||||||
end
|
end
|
||||||
|
|
||||||
defp maybe_update_slow_indexing_task(changeset, source) do
|
defp maybe_update_slow_indexing_task(changeset, source) do
|
||||||
case changeset.changes do
|
# See comment in `maybe_handle_media_tasks` as to why we need these
|
||||||
%{index_frequency_minutes: mins} when mins > 0 ->
|
current_changes = changeset.changes
|
||||||
|
applied_changes = Ecto.Changeset.apply_changes(changeset)
|
||||||
|
|
||||||
|
case {current_changes, applied_changes} do
|
||||||
|
{%{index_frequency_minutes: mins}, %{enabled: true}} when mins > 0 ->
|
||||||
SlowIndexingHelpers.kickoff_indexing_task(source)
|
SlowIndexingHelpers.kickoff_indexing_task(source)
|
||||||
|
|
||||||
%{index_frequency_minutes: _} ->
|
{%{enabled: true}, %{index_frequency_minutes: mins}} when mins > 0 ->
|
||||||
Tasks.delete_pending_tasks_for(source, "FastIndexingWorker")
|
SlowIndexingHelpers.kickoff_indexing_task(source)
|
||||||
Tasks.delete_pending_tasks_for(source, "MediaCollectionIndexingWorker")
|
|
||||||
|
{%{index_frequency_minutes: _}, _} ->
|
||||||
|
SlowIndexingHelpers.delete_indexing_tasks(source, include_executing: true)
|
||||||
|
|
||||||
|
{%{enabled: false}, _} ->
|
||||||
|
SlowIndexingHelpers.delete_indexing_tasks(source, include_executing: true)
|
||||||
|
|
||||||
_ ->
|
_ ->
|
||||||
:ok
|
:ok
|
||||||
@@ -315,13 +345,25 @@ defmodule Pinchflat.Sources do
|
|||||||
end
|
end
|
||||||
|
|
||||||
defp maybe_update_fast_indexing_task(changeset, source) do
|
defp maybe_update_fast_indexing_task(changeset, source) do
|
||||||
case changeset.changes do
|
# See comment in `maybe_handle_media_tasks` as to why we need these
|
||||||
%{fast_index: true} ->
|
current_changes = changeset.changes
|
||||||
Tasks.delete_pending_tasks_for(source, "FastIndexingWorker")
|
applied_changes = Ecto.Changeset.apply_changes(changeset)
|
||||||
FastIndexingWorker.kickoff_with_task(source)
|
|
||||||
|
|
||||||
%{fast_index: false} ->
|
# This technically could be simplified since `maybe_update_slow_indexing_task`
|
||||||
Tasks.delete_pending_tasks_for(source, "FastIndexingWorker")
|
# has some overlap re: deleting pending tasks, but I'm keeping it separate
|
||||||
|
# for clarity and explicitness.
|
||||||
|
case {current_changes, applied_changes} do
|
||||||
|
{%{fast_index: true}, %{enabled: true}} ->
|
||||||
|
FastIndexingHelpers.kickoff_indexing_task(source)
|
||||||
|
|
||||||
|
{%{enabled: true}, %{fast_index: true}} ->
|
||||||
|
FastIndexingHelpers.kickoff_indexing_task(source)
|
||||||
|
|
||||||
|
{%{fast_index: false}, _} ->
|
||||||
|
Tasks.delete_pending_tasks_for(source, "FastIndexingWorker", include_executing: true)
|
||||||
|
|
||||||
|
{%{enabled: false}, _} ->
|
||||||
|
Tasks.delete_pending_tasks_for(source, "FastIndexingWorker", include_executing: true)
|
||||||
|
|
||||||
_ ->
|
_ ->
|
||||||
:ok
|
:ok
|
||||||
|
|||||||
@@ -340,14 +340,15 @@ defmodule PinchflatWeb.CoreComponents do
|
|||||||
end)
|
end)
|
||||||
|
|
||||||
~H"""
|
~H"""
|
||||||
<div x-data={"{ enabled: #{@checked}}"}>
|
<div x-data={"{ enabled: #{@checked} }"} class="" phx-update="ignore" id={"#{@id}-wrapper"}>
|
||||||
<.label for={@id}>
|
<.label :if={@label} for={@id}>
|
||||||
<%= @label %>
|
<%= @label %>
|
||||||
<span :if={@label_suffix} class="text-xs text-bodydark"><%= @label_suffix %></span>
|
<span :if={@label_suffix} class="text-xs text-bodydark"><%= @label_suffix %></span>
|
||||||
</.label>
|
</.label>
|
||||||
<div class="relative">
|
<div class="relative flex flex-col">
|
||||||
<input type="hidden" id={@id} name={@name} x-bind:value="enabled" {@rest} />
|
<input type="hidden" id={@id} name={@name} x-bind:value="enabled" {@rest} />
|
||||||
<div class="inline-block cursor-pointer" @click="enabled = !enabled">
|
<%!-- This triggers a `change` event on the hidden input when the toggle is clicked --%>
|
||||||
|
<div class="inline-block cursor-pointer" @click={"enabled = !enabled; dispatchFor('#{@id}', 'change')"}>
|
||||||
<div x-bind:class="enabled && '!bg-primary'" class="block h-8 w-14 rounded-full bg-black"></div>
|
<div x-bind:class="enabled && '!bg-primary'" class="block h-8 w-14 rounded-full bg-black"></div>
|
||||||
<div
|
<div
|
||||||
x-bind:class="enabled && '!right-1 !translate-x-full'"
|
x-bind:class="enabled && '!right-1 !translate-x-full'"
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ defmodule Pinchflat.UpgradeButtonLive do
|
|||||||
|
|
||||||
def render(assigns) do
|
def render(assigns) do
|
||||||
~H"""
|
~H"""
|
||||||
<form id="upgradeForm" phx-change="check_matching_text" phx-hook="supressEnterSubmission">
|
<form id="upgradeForm" phx-change="check_matching_text" phx-hook="supress-enter-submission">
|
||||||
<.input type="text" name="unlock-pro-textbox" value="" />
|
<.input type="text" name="unlock-pro-textbox" value="" />
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
|
|||||||
@@ -1,20 +1,31 @@
|
|||||||
defmodule PinchflatWeb.MediaProfiles.MediaProfileController do
|
defmodule PinchflatWeb.MediaProfiles.MediaProfileController do
|
||||||
use PinchflatWeb, :controller
|
use PinchflatWeb, :controller
|
||||||
use Pinchflat.Sources.SourcesQuery
|
use Pinchflat.Sources.SourcesQuery
|
||||||
|
use Pinchflat.Profiles.ProfilesQuery
|
||||||
|
|
||||||
alias Pinchflat.Repo
|
alias Pinchflat.Repo
|
||||||
alias Pinchflat.Profiles
|
alias Pinchflat.Profiles
|
||||||
|
alias Pinchflat.Sources.Source
|
||||||
alias Pinchflat.Profiles.MediaProfile
|
alias Pinchflat.Profiles.MediaProfile
|
||||||
alias Pinchflat.Profiles.MediaProfileDeletionWorker
|
alias Pinchflat.Profiles.MediaProfileDeletionWorker
|
||||||
|
|
||||||
def index(conn, _params) do
|
def index(conn, _params) do
|
||||||
media_profiles =
|
media_profiles_query =
|
||||||
MediaProfile
|
from mp in MediaProfile,
|
||||||
|> where([mp], is_nil(mp.marked_for_deletion_at))
|
as: :media_profile,
|
||||||
|> order_by(asc: :name)
|
where: is_nil(mp.marked_for_deletion_at),
|
||||||
|> Repo.all()
|
order_by: [asc: mp.name],
|
||||||
|
select: map(mp, ^MediaProfile.__schema__(:fields)),
|
||||||
|
select_merge: %{
|
||||||
|
source_count:
|
||||||
|
subquery(
|
||||||
|
from s in Source,
|
||||||
|
where: s.media_profile_id == parent_as(:media_profile).id,
|
||||||
|
select: count(s.id)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
render(conn, :index, media_profiles: media_profiles)
|
render(conn, :index, media_profiles: Repo.all(media_profiles_query))
|
||||||
end
|
end
|
||||||
|
|
||||||
def new(conn, params) do
|
def new(conn, params) do
|
||||||
|
|||||||
@@ -10,7 +10,6 @@
|
|||||||
</.link>
|
</.link>
|
||||||
</nav>
|
</nav>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="rounded-sm border border-stroke bg-white shadow-default dark:border-strokedark dark:bg-boxdark">
|
<div class="rounded-sm border border-stroke bg-white shadow-default dark:border-strokedark dark:bg-boxdark">
|
||||||
<div class="max-w-full overflow-x-auto">
|
<div class="max-w-full overflow-x-auto">
|
||||||
<div class="flex flex-col gap-10 min-w-max">
|
<div class="flex flex-col gap-10 min-w-max">
|
||||||
@@ -23,6 +22,11 @@
|
|||||||
<:col :let={media_profile} label="Preferred Resolution">
|
<:col :let={media_profile} label="Preferred Resolution">
|
||||||
<%= media_profile.preferred_resolution %>
|
<%= media_profile.preferred_resolution %>
|
||||||
</:col>
|
</:col>
|
||||||
|
<:col :let={media_profile} label="Sources">
|
||||||
|
<.subtle_link href={~p"/media_profiles/#{media_profile.id}/#tab-sources"}>
|
||||||
|
<.localized_number number={media_profile.source_count} />
|
||||||
|
</.subtle_link>
|
||||||
|
</:col>
|
||||||
<:col :let={media_profile} label="" class="flex justify-end">
|
<:col :let={media_profile} label="" class="flex justify-end">
|
||||||
<.icon_link href={~p"/media_profiles/#{media_profile.id}/edit"} icon="hero-pencil-square" class="mr-4" />
|
<.icon_link href={~p"/media_profiles/#{media_profile.id}/edit"} icon="hero-pencil-square" class="mr-4" />
|
||||||
</:col>
|
</:col>
|
||||||
|
|||||||
@@ -1,12 +1,11 @@
|
|||||||
defmodule PinchflatWeb.Sources.SourceController do
|
defmodule PinchflatWeb.Sources.SourceController do
|
||||||
use PinchflatWeb, :controller
|
use PinchflatWeb, :controller
|
||||||
use Pinchflat.Media.MediaQuery
|
use Pinchflat.Sources.SourcesQuery
|
||||||
|
|
||||||
alias Pinchflat.Repo
|
alias Pinchflat.Repo
|
||||||
alias Pinchflat.Tasks
|
alias Pinchflat.Tasks
|
||||||
alias Pinchflat.Sources
|
alias Pinchflat.Sources
|
||||||
alias Pinchflat.Sources.Source
|
alias Pinchflat.Sources.Source
|
||||||
alias Pinchflat.Media.MediaItem
|
|
||||||
alias Pinchflat.Profiles.MediaProfile
|
alias Pinchflat.Profiles.MediaProfile
|
||||||
alias Pinchflat.Media.FileSyncingWorker
|
alias Pinchflat.Media.FileSyncingWorker
|
||||||
alias Pinchflat.Sources.SourceDeletionWorker
|
alias Pinchflat.Sources.SourceDeletionWorker
|
||||||
@@ -15,33 +14,7 @@ defmodule PinchflatWeb.Sources.SourceController do
|
|||||||
alias Pinchflat.Metadata.SourceMetadataStorageWorker
|
alias Pinchflat.Metadata.SourceMetadataStorageWorker
|
||||||
|
|
||||||
def index(conn, _params) do
|
def index(conn, _params) do
|
||||||
source_query =
|
render(conn, :index)
|
||||||
from s in Source,
|
|
||||||
as: :source,
|
|
||||||
inner_join: mp in assoc(s, :media_profile),
|
|
||||||
where: is_nil(s.marked_for_deletion_at) and is_nil(mp.marked_for_deletion_at),
|
|
||||||
preload: [media_profile: mp],
|
|
||||||
order_by: [asc: s.custom_name],
|
|
||||||
select: map(s, ^Source.__schema__(:fields)),
|
|
||||||
select_merge: %{
|
|
||||||
downloaded_count:
|
|
||||||
subquery(
|
|
||||||
from m in MediaItem,
|
|
||||||
where: m.source_id == parent_as(:source).id,
|
|
||||||
where: ^MediaQuery.downloaded(),
|
|
||||||
select: count(m.id)
|
|
||||||
),
|
|
||||||
pending_count:
|
|
||||||
subquery(
|
|
||||||
from m in MediaItem,
|
|
||||||
join: s in assoc(m, :source),
|
|
||||||
where: m.source_id == parent_as(:source).id,
|
|
||||||
where: ^MediaQuery.pending(),
|
|
||||||
select: count(m.id)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
render(conn, :index, sources: Repo.all(source_query))
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def new(conn, params) do
|
def new(conn, params) do
|
||||||
|
|||||||
@@ -12,32 +12,7 @@
|
|||||||
<div class="rounded-sm border border-stroke bg-white shadow-default dark:border-strokedark dark:bg-boxdark">
|
<div class="rounded-sm border border-stroke bg-white shadow-default dark:border-strokedark dark:bg-boxdark">
|
||||||
<div class="max-w-full overflow-x-auto">
|
<div class="max-w-full overflow-x-auto">
|
||||||
<div class="flex flex-col gap-10 min-w-max">
|
<div class="flex flex-col gap-10 min-w-max">
|
||||||
<.table rows={@sources} table_class="text-black dark:text-white">
|
<%= live_render(@conn, PinchflatWeb.Sources.IndexTableLive) %>
|
||||||
<:col :let={source} label="Name">
|
|
||||||
<.subtle_link href={~p"/sources/#{source.id}"}>
|
|
||||||
<%= StringUtils.truncate(source.custom_name || source.collection_name, 35) %>
|
|
||||||
</.subtle_link>
|
|
||||||
</:col>
|
|
||||||
<:col :let={source} label="Type"><%= source.collection_type %></:col>
|
|
||||||
<:col :let={source} label="Pending"><.localized_number number={source.pending_count} /></:col>
|
|
||||||
<:col :let={source} label="Downloaded"><.localized_number number={source.downloaded_count} /></:col>
|
|
||||||
<:col :let={source} label="Retention">
|
|
||||||
<%= if source.retention_period_days && source.retention_period_days > 0 do %>
|
|
||||||
<.localized_number number={source.retention_period_days} />
|
|
||||||
<.pluralize count={source.retention_period_days} word="day" />
|
|
||||||
<% else %>
|
|
||||||
<span class="text-lg">∞</span>
|
|
||||||
<% end %>
|
|
||||||
</:col>
|
|
||||||
<:col :let={source} label="Media Profile">
|
|
||||||
<.subtle_link href={~p"/media_profiles/#{source.media_profile_id}"}>
|
|
||||||
<%= source.media_profile.name %>
|
|
||||||
</.subtle_link>
|
|
||||||
</:col>
|
|
||||||
<:col :let={source} label="" class="flex place-content-evenly">
|
|
||||||
<.icon_link href={~p"/sources/#{source.id}/edit"} icon="hero-pencil-square" class="mx-1" />
|
|
||||||
</:col>
|
|
||||||
</.table>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -0,0 +1,103 @@
|
|||||||
|
defmodule PinchflatWeb.Sources.IndexTableLive do
|
||||||
|
use PinchflatWeb, :live_view
|
||||||
|
use Pinchflat.Media.MediaQuery
|
||||||
|
use Pinchflat.Sources.SourcesQuery
|
||||||
|
|
||||||
|
alias Pinchflat.Repo
|
||||||
|
alias Pinchflat.Sources
|
||||||
|
alias Pinchflat.Sources.Source
|
||||||
|
alias Pinchflat.Media.MediaItem
|
||||||
|
|
||||||
|
def render(assigns) do
|
||||||
|
~H"""
|
||||||
|
<.table rows={@sources} table_class="text-white">
|
||||||
|
<:col :let={source} label="Name">
|
||||||
|
<.subtle_link href={~p"/sources/#{source.id}"}>
|
||||||
|
<%= StringUtils.truncate(source.custom_name || source.collection_name, 35) %>
|
||||||
|
</.subtle_link>
|
||||||
|
</:col>
|
||||||
|
<:col :let={source} label="Pending">
|
||||||
|
<.subtle_link href={~p"/sources/#{source.id}/#tab-pending"}>
|
||||||
|
<.localized_number number={source.pending_count} />
|
||||||
|
</.subtle_link>
|
||||||
|
</:col>
|
||||||
|
<:col :let={source} label="Downloaded">
|
||||||
|
<.subtle_link href={~p"/sources/#{source.id}/#tab-downloaded"}>
|
||||||
|
<.localized_number number={source.downloaded_count} />
|
||||||
|
</.subtle_link>
|
||||||
|
</:col>
|
||||||
|
<:col :let={source} label="Retention">
|
||||||
|
<%= if source.retention_period_days && source.retention_period_days > 0 do %>
|
||||||
|
<.localized_number number={source.retention_period_days} />
|
||||||
|
<.pluralize count={source.retention_period_days} word="day" />
|
||||||
|
<% else %>
|
||||||
|
<span class="text-lg">∞</span>
|
||||||
|
<% end %>
|
||||||
|
</:col>
|
||||||
|
<:col :let={source} label="Media Profile">
|
||||||
|
<.subtle_link href={~p"/media_profiles/#{source.media_profile_id}"}>
|
||||||
|
<%= source.media_profile.name %>
|
||||||
|
</.subtle_link>
|
||||||
|
</:col>
|
||||||
|
<:col :let={source} label="Enabled?">
|
||||||
|
<.input
|
||||||
|
name={"source[#{source.id}][enabled]"}
|
||||||
|
value={source.enabled}
|
||||||
|
id={"source_#{source.id}_enabled"}
|
||||||
|
phx-hook="formless-input"
|
||||||
|
data-subscribe="change"
|
||||||
|
data-event-name="toggle_enabled"
|
||||||
|
data-identifier={source.id}
|
||||||
|
type="toggle"
|
||||||
|
/>
|
||||||
|
</:col>
|
||||||
|
<:col :let={source} label="" class="flex place-content-evenly">
|
||||||
|
<.icon_link href={~p"/sources/#{source.id}/edit"} icon="hero-pencil-square" class="mx-1" />
|
||||||
|
</:col>
|
||||||
|
</.table>
|
||||||
|
"""
|
||||||
|
end
|
||||||
|
|
||||||
|
def mount(_params, _session, socket) do
|
||||||
|
{:ok, assign(socket, %{sources: get_sources()})}
|
||||||
|
end
|
||||||
|
|
||||||
|
def handle_event("formless-input", %{"event" => "toggle_enabled"} = params, socket) do
|
||||||
|
source = Sources.get_source!(params["id"])
|
||||||
|
should_enable = params["value"] == "true"
|
||||||
|
|
||||||
|
{:ok, _} = Sources.update_source(source, %{enabled: should_enable})
|
||||||
|
|
||||||
|
{:noreply, assign(socket, %{sources: get_sources()})}
|
||||||
|
end
|
||||||
|
|
||||||
|
defp get_sources do
|
||||||
|
query =
|
||||||
|
from s in Source,
|
||||||
|
as: :source,
|
||||||
|
inner_join: mp in assoc(s, :media_profile),
|
||||||
|
where: is_nil(s.marked_for_deletion_at) and is_nil(mp.marked_for_deletion_at),
|
||||||
|
preload: [media_profile: mp],
|
||||||
|
order_by: [asc: s.custom_name],
|
||||||
|
select: map(s, ^Source.__schema__(:fields)),
|
||||||
|
select_merge: %{
|
||||||
|
downloaded_count:
|
||||||
|
subquery(
|
||||||
|
from m in MediaItem,
|
||||||
|
where: m.source_id == parent_as(:source).id,
|
||||||
|
where: ^MediaQuery.downloaded(),
|
||||||
|
select: count(m.id)
|
||||||
|
),
|
||||||
|
pending_count:
|
||||||
|
subquery(
|
||||||
|
from m in MediaItem,
|
||||||
|
join: s in assoc(m, :source),
|
||||||
|
where: m.source_id == parent_as(:source).id,
|
||||||
|
where: ^MediaQuery.pending(),
|
||||||
|
select: count(m.id)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Repo.all(query)
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
defmodule Pinchflat.Sources.MediaItemTableLive do
|
defmodule PinchflatWeb.Sources.MediaItemTableLive do
|
||||||
use PinchflatWeb, :live_view
|
use PinchflatWeb, :live_view
|
||||||
use Pinchflat.Media.MediaQuery
|
use Pinchflat.Media.MediaQuery
|
||||||
|
|
||||||
|
|||||||
@@ -39,21 +39,21 @@
|
|||||||
<:tab title="Pending" id="pending">
|
<:tab title="Pending" id="pending">
|
||||||
<%= live_render(
|
<%= live_render(
|
||||||
@conn,
|
@conn,
|
||||||
Pinchflat.Sources.MediaItemTableLive,
|
PinchflatWeb.Sources.MediaItemTableLive,
|
||||||
session: %{"source_id" => @source.id, "media_state" => "pending"}
|
session: %{"source_id" => @source.id, "media_state" => "pending"}
|
||||||
) %>
|
) %>
|
||||||
</:tab>
|
</:tab>
|
||||||
<:tab title="Downloaded" id="downloaded">
|
<:tab title="Downloaded" id="downloaded">
|
||||||
<%= live_render(
|
<%= live_render(
|
||||||
@conn,
|
@conn,
|
||||||
Pinchflat.Sources.MediaItemTableLive,
|
PinchflatWeb.Sources.MediaItemTableLive,
|
||||||
session: %{"source_id" => @source.id, "media_state" => "downloaded"}
|
session: %{"source_id" => @source.id, "media_state" => "downloaded"}
|
||||||
) %>
|
) %>
|
||||||
</:tab>
|
</:tab>
|
||||||
<:tab title="Other" id="other">
|
<:tab title="Other" id="other">
|
||||||
<%= live_render(
|
<%= live_render(
|
||||||
@conn,
|
@conn,
|
||||||
Pinchflat.Sources.MediaItemTableLive,
|
PinchflatWeb.Sources.MediaItemTableLive,
|
||||||
session: %{"source_id" => @source.id, "media_state" => "other"}
|
session: %{"source_id" => @source.id, "media_state" => "other"}
|
||||||
) %>
|
) %>
|
||||||
</:tab>
|
</:tab>
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 449 KiB After Width: | Height: | Size: 434 KiB |
@@ -0,0 +1,9 @@
|
|||||||
|
defmodule Pinchflat.Repo.Migrations.AddEnabledToSources do
|
||||||
|
use Ecto.Migration
|
||||||
|
|
||||||
|
def change do
|
||||||
|
alter table(:sources) do
|
||||||
|
add :enabled, :boolean, default: true, null: false
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
defmodule Pinchflat.FastIndexing.FastIndexingHelpersTest do
|
defmodule Pinchflat.FastIndexing.FastIndexingHelpersTest do
|
||||||
use Pinchflat.DataCase
|
use Pinchflat.DataCase
|
||||||
|
|
||||||
|
import Pinchflat.TasksFixtures
|
||||||
import Pinchflat.MediaFixtures
|
import Pinchflat.MediaFixtures
|
||||||
import Pinchflat.SourcesFixtures
|
import Pinchflat.SourcesFixtures
|
||||||
import Pinchflat.ProfilesFixtures
|
import Pinchflat.ProfilesFixtures
|
||||||
@@ -8,6 +9,7 @@ defmodule Pinchflat.FastIndexing.FastIndexingHelpersTest do
|
|||||||
alias Pinchflat.Tasks
|
alias Pinchflat.Tasks
|
||||||
alias Pinchflat.Settings
|
alias Pinchflat.Settings
|
||||||
alias Pinchflat.Media.MediaItem
|
alias Pinchflat.Media.MediaItem
|
||||||
|
alias Pinchflat.FastIndexing.FastIndexingWorker
|
||||||
alias Pinchflat.Downloading.MediaDownloadWorker
|
alias Pinchflat.Downloading.MediaDownloadWorker
|
||||||
alias Pinchflat.FastIndexing.FastIndexingHelpers
|
alias Pinchflat.FastIndexing.FastIndexingHelpers
|
||||||
|
|
||||||
@@ -19,6 +21,23 @@ defmodule Pinchflat.FastIndexing.FastIndexingHelpersTest do
|
|||||||
{:ok, [source: source_fixture()]}
|
{:ok, [source: source_fixture()]}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
describe "kickoff_indexing_task/1" do
|
||||||
|
test "deletes any existing fast indexing tasks", %{source: source} do
|
||||||
|
{:ok, job} = Oban.insert(FastIndexingWorker.new(%{"id" => source.id}))
|
||||||
|
task = task_fixture(source_id: source.id, job_id: job.id)
|
||||||
|
|
||||||
|
assert Repo.reload!(task)
|
||||||
|
assert {:ok, _} = FastIndexingHelpers.kickoff_indexing_task(source)
|
||||||
|
assert_raise Ecto.NoResultsError, fn -> Repo.reload!(task) end
|
||||||
|
end
|
||||||
|
|
||||||
|
test "kicks off a new fast indexing task", %{source: source} do
|
||||||
|
assert {:ok, _} = FastIndexingHelpers.kickoff_indexing_task(source)
|
||||||
|
assert [worker] = all_enqueued(worker: FastIndexingWorker)
|
||||||
|
assert worker.args["id"] == source.id
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
describe "kickoff_download_tasks_from_youtube_rss_feed/1" do
|
describe "kickoff_download_tasks_from_youtube_rss_feed/1" do
|
||||||
test "enqueues a new worker for each new media_id in the source's RSS feed", %{source: source} do
|
test "enqueues a new worker for each new media_id in the source's RSS feed", %{source: source} do
|
||||||
expect(HTTPClientMock, :get, fn _url -> {:ok, "<yt:videoId>test_1</yt:videoId>"} end)
|
expect(HTTPClientMock, :get, fn _url -> {:ok, "<yt:videoId>test_1</yt:videoId>"} end)
|
||||||
|
|||||||
@@ -23,6 +23,36 @@ defmodule Pinchflat.SlowIndexing.SlowIndexingHelpersTest do
|
|||||||
assert_enqueued(worker: MediaCollectionIndexingWorker, args: %{"id" => source.id})
|
assert_enqueued(worker: MediaCollectionIndexingWorker, args: %{"id" => source.id})
|
||||||
end
|
end
|
||||||
|
|
||||||
|
test "schedules a job for the future based on when the source was last indexed" do
|
||||||
|
source = source_fixture(index_frequency_minutes: 30, last_indexed_at: now_minus(5, :minutes))
|
||||||
|
|
||||||
|
assert {:ok, _} = SlowIndexingHelpers.kickoff_indexing_task(source)
|
||||||
|
|
||||||
|
[job] = all_enqueued(worker: MediaCollectionIndexingWorker, args: %{"id" => source.id})
|
||||||
|
|
||||||
|
assert_in_delta DateTime.diff(job.scheduled_at, DateTime.utc_now(), :minute), 25, 1
|
||||||
|
end
|
||||||
|
|
||||||
|
test "schedules a job immediately if the source was indexed far in the past" do
|
||||||
|
source = source_fixture(index_frequency_minutes: 30, last_indexed_at: now_minus(60, :minutes))
|
||||||
|
|
||||||
|
assert {:ok, _} = SlowIndexingHelpers.kickoff_indexing_task(source)
|
||||||
|
|
||||||
|
[job] = all_enqueued(worker: MediaCollectionIndexingWorker, args: %{"id" => source.id})
|
||||||
|
|
||||||
|
assert_in_delta DateTime.diff(job.scheduled_at, DateTime.utc_now(), :second), 0, 1
|
||||||
|
end
|
||||||
|
|
||||||
|
test "schedules a job immediately if the source has never been indexed" do
|
||||||
|
source = source_fixture(index_frequency_minutes: 30, last_indexed_at: nil)
|
||||||
|
|
||||||
|
assert {:ok, _} = SlowIndexingHelpers.kickoff_indexing_task(source)
|
||||||
|
|
||||||
|
[job] = all_enqueued(worker: MediaCollectionIndexingWorker, args: %{"id" => source.id})
|
||||||
|
|
||||||
|
assert_in_delta DateTime.diff(job.scheduled_at, DateTime.utc_now(), :second), 0, 1
|
||||||
|
end
|
||||||
|
|
||||||
test "creates and attaches a task" do
|
test "creates and attaches a task" do
|
||||||
source = source_fixture(index_frequency_minutes: 1)
|
source = source_fixture(index_frequency_minutes: 1)
|
||||||
|
|
||||||
@@ -92,6 +122,56 @@ defmodule Pinchflat.SlowIndexing.SlowIndexingHelpersTest do
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
describe "delete_indexing_tasks/2" do
|
||||||
|
setup do
|
||||||
|
source = source_fixture()
|
||||||
|
|
||||||
|
{:ok, %{source: source}}
|
||||||
|
end
|
||||||
|
|
||||||
|
test "deletes slow indexing tasks for the source", %{source: source} do
|
||||||
|
{:ok, job} = Oban.insert(MediaCollectionIndexingWorker.new(%{"id" => source.id}))
|
||||||
|
_task = task_fixture(source_id: source.id, job_id: job.id)
|
||||||
|
|
||||||
|
assert_enqueued(worker: MediaCollectionIndexingWorker, args: %{"id" => source.id})
|
||||||
|
assert :ok = SlowIndexingHelpers.delete_indexing_tasks(source)
|
||||||
|
refute_enqueued(worker: MediaCollectionIndexingWorker)
|
||||||
|
end
|
||||||
|
|
||||||
|
test "deletes fast indexing tasks for the source", %{source: source} do
|
||||||
|
{:ok, job} = Oban.insert(FastIndexingWorker.new(%{"id" => source.id}))
|
||||||
|
_task = task_fixture(source_id: source.id, job_id: job.id)
|
||||||
|
|
||||||
|
assert_enqueued(worker: FastIndexingWorker, args: %{"id" => source.id})
|
||||||
|
assert :ok = SlowIndexingHelpers.delete_indexing_tasks(source)
|
||||||
|
refute_enqueued(worker: FastIndexingWorker)
|
||||||
|
end
|
||||||
|
|
||||||
|
test "doesn't normally delete currently executing tasks", %{source: source} do
|
||||||
|
{:ok, job} = Oban.insert(MediaCollectionIndexingWorker.new(%{"id" => source.id}))
|
||||||
|
task = task_fixture(source_id: source.id, job_id: job.id)
|
||||||
|
|
||||||
|
from(Oban.Job, where: [id: ^job.id], update: [set: [state: "executing"]])
|
||||||
|
|> Repo.update_all([])
|
||||||
|
|
||||||
|
assert Repo.reload!(task)
|
||||||
|
assert :ok = SlowIndexingHelpers.delete_indexing_tasks(source)
|
||||||
|
assert Repo.reload!(task)
|
||||||
|
end
|
||||||
|
|
||||||
|
test "can optionally delete currently executing tasks", %{source: source} do
|
||||||
|
{:ok, job} = Oban.insert(MediaCollectionIndexingWorker.new(%{"id" => source.id}))
|
||||||
|
task = task_fixture(source_id: source.id, job_id: job.id)
|
||||||
|
|
||||||
|
from(Oban.Job, where: [id: ^job.id], update: [set: [state: "executing"]])
|
||||||
|
|> Repo.update_all([])
|
||||||
|
|
||||||
|
assert Repo.reload!(task)
|
||||||
|
assert :ok = SlowIndexingHelpers.delete_indexing_tasks(source, include_executing: true)
|
||||||
|
assert_raise Ecto.NoResultsError, fn -> Repo.reload!(task) end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
describe "index_and_enqueue_download_for_media_items/1" do
|
describe "index_and_enqueue_download_for_media_items/1" do
|
||||||
setup do
|
setup do
|
||||||
stub(YtDlpRunnerMock, :run, fn _url, _opts, _ot, _addl_opts ->
|
stub(YtDlpRunnerMock, :run, fn _url, _opts, _ot, _addl_opts ->
|
||||||
|
|||||||
+159
-34
@@ -418,6 +418,100 @@ defmodule Pinchflat.SourcesTest do
|
|||||||
assert {:ok, %Source{}} = Sources.update_source(source, update_attrs)
|
assert {:ok, %Source{}} = Sources.update_source(source, update_attrs)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
test "updates with invalid data returns error changeset" do
|
||||||
|
source = source_fixture()
|
||||||
|
|
||||||
|
assert {:error, %Ecto.Changeset{}} =
|
||||||
|
Sources.update_source(source, @invalid_source_attrs)
|
||||||
|
|
||||||
|
assert source == Sources.get_source!(source.id)
|
||||||
|
end
|
||||||
|
|
||||||
|
test "updating will kickoff a metadata storage worker if the original_url changes" do
|
||||||
|
expect(YtDlpRunnerMock, :run, &playlist_mock/4)
|
||||||
|
source = source_fixture()
|
||||||
|
update_attrs = %{original_url: "https://www.youtube.com/channel/cba321"}
|
||||||
|
|
||||||
|
assert {:ok, %Source{} = source} = Sources.update_source(source, update_attrs)
|
||||||
|
|
||||||
|
assert_enqueued(worker: SourceMetadataStorageWorker, args: %{"id" => source.id})
|
||||||
|
end
|
||||||
|
|
||||||
|
test "updating will not kickoff a metadata storage worker other attrs change" do
|
||||||
|
source = source_fixture()
|
||||||
|
update_attrs = %{name: "some new name"}
|
||||||
|
|
||||||
|
assert {:ok, %Source{}} = Sources.update_source(source, update_attrs)
|
||||||
|
|
||||||
|
refute_enqueued(worker: SourceMetadataStorageWorker)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "update_source/3 when testing media download tasks" do
|
||||||
|
test "enabling the download_media attribute will schedule a download task" do
|
||||||
|
source = source_fixture(download_media: false)
|
||||||
|
media_item = media_item_fixture(source_id: source.id, media_filepath: nil)
|
||||||
|
update_attrs = %{download_media: true}
|
||||||
|
|
||||||
|
refute_enqueued(worker: MediaDownloadWorker)
|
||||||
|
assert {:ok, %Source{}} = Sources.update_source(source, update_attrs)
|
||||||
|
assert_enqueued(worker: MediaDownloadWorker, args: %{"id" => media_item.id})
|
||||||
|
end
|
||||||
|
|
||||||
|
test "disabling the download_media attribute will cancel the download task" do
|
||||||
|
source = source_fixture(download_media: true, enabled: true)
|
||||||
|
media_item = media_item_fixture(source_id: source.id, media_filepath: nil)
|
||||||
|
update_attrs = %{download_media: false}
|
||||||
|
DownloadingHelpers.enqueue_pending_download_tasks(source)
|
||||||
|
|
||||||
|
assert_enqueued(worker: MediaDownloadWorker, args: %{"id" => media_item.id})
|
||||||
|
assert {:ok, %Source{}} = Sources.update_source(source, update_attrs)
|
||||||
|
refute_enqueued(worker: MediaDownloadWorker)
|
||||||
|
end
|
||||||
|
|
||||||
|
test "enabling download_media will not schedule a task if the source is disabled" do
|
||||||
|
source = source_fixture(download_media: false, enabled: false)
|
||||||
|
_media_item = media_item_fixture(source_id: source.id, media_filepath: nil)
|
||||||
|
update_attrs = %{download_media: true}
|
||||||
|
|
||||||
|
refute_enqueued(worker: MediaDownloadWorker)
|
||||||
|
assert {:ok, %Source{}} = Sources.update_source(source, update_attrs)
|
||||||
|
refute_enqueued(worker: MediaDownloadWorker)
|
||||||
|
end
|
||||||
|
|
||||||
|
test "disabling a source will cancel any pending download tasks" do
|
||||||
|
source = source_fixture(download_media: true, enabled: true)
|
||||||
|
media_item = media_item_fixture(source_id: source.id, media_filepath: nil)
|
||||||
|
update_attrs = %{enabled: false}
|
||||||
|
DownloadingHelpers.enqueue_pending_download_tasks(source)
|
||||||
|
|
||||||
|
assert_enqueued(worker: MediaDownloadWorker, args: %{"id" => media_item.id})
|
||||||
|
assert {:ok, %Source{}} = Sources.update_source(source, update_attrs)
|
||||||
|
refute_enqueued(worker: MediaDownloadWorker)
|
||||||
|
end
|
||||||
|
|
||||||
|
test "enabling a source will schedule a download task if download_media is true" do
|
||||||
|
source = source_fixture(download_media: true, enabled: false)
|
||||||
|
media_item = media_item_fixture(source_id: source.id, media_filepath: nil)
|
||||||
|
update_attrs = %{enabled: true}
|
||||||
|
|
||||||
|
refute_enqueued(worker: MediaDownloadWorker)
|
||||||
|
assert {:ok, %Source{}} = Sources.update_source(source, update_attrs)
|
||||||
|
assert_enqueued(worker: MediaDownloadWorker, args: %{"id" => media_item.id})
|
||||||
|
end
|
||||||
|
|
||||||
|
test "enabling a source will not schedule a download task if download_media is false" do
|
||||||
|
source = source_fixture(download_media: false, enabled: false)
|
||||||
|
_media_item = media_item_fixture(source_id: source.id, media_filepath: nil)
|
||||||
|
update_attrs = %{enabled: true}
|
||||||
|
|
||||||
|
refute_enqueued(worker: MediaDownloadWorker)
|
||||||
|
assert {:ok, %Source{}} = Sources.update_source(source, update_attrs)
|
||||||
|
refute_enqueued(worker: MediaDownloadWorker)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "update_source/3 when testing slow indexing" do
|
||||||
test "updating the index frequency to >0 will re-schedule the indexing task" do
|
test "updating the index frequency to >0 will re-schedule the indexing task" do
|
||||||
source = source_fixture()
|
source = source_fixture()
|
||||||
update_attrs = %{index_frequency_minutes: 123}
|
update_attrs = %{index_frequency_minutes: 123}
|
||||||
@@ -462,27 +556,47 @@ defmodule Pinchflat.SourcesTest do
|
|||||||
refute_enqueued(worker: MediaCollectionIndexingWorker, args: %{"id" => source.id})
|
refute_enqueued(worker: MediaCollectionIndexingWorker, args: %{"id" => source.id})
|
||||||
end
|
end
|
||||||
|
|
||||||
test "enabling the download_media attribute will schedule a download task" do
|
test "disabling a source will delete any pending tasks" do
|
||||||
source = source_fixture(download_media: false)
|
source = source_fixture()
|
||||||
media_item = media_item_fixture(source_id: source.id, media_filepath: nil)
|
update_attrs = %{enabled: false}
|
||||||
update_attrs = %{download_media: true}
|
|
||||||
|
{:ok, job} = Oban.insert(MediaCollectionIndexingWorker.new(%{"id" => source.id}))
|
||||||
|
task = task_fixture(source_id: source.id, job_id: job.id)
|
||||||
|
|
||||||
refute_enqueued(worker: MediaDownloadWorker)
|
|
||||||
assert {:ok, %Source{}} = Sources.update_source(source, update_attrs)
|
assert {:ok, %Source{}} = Sources.update_source(source, update_attrs)
|
||||||
assert_enqueued(worker: MediaDownloadWorker, args: %{"id" => media_item.id})
|
|
||||||
|
assert_raise Ecto.NoResultsError, fn -> Repo.reload!(task) end
|
||||||
end
|
end
|
||||||
|
|
||||||
test "disabling the download_media attribute will cancel the download task" do
|
test "updating the index frequency will not create a task if the source is disabled" do
|
||||||
source = source_fixture(download_media: true)
|
source = source_fixture(enabled: false)
|
||||||
media_item = media_item_fixture(source_id: source.id, media_filepath: nil)
|
update_attrs = %{index_frequency_minutes: 123}
|
||||||
update_attrs = %{download_media: false}
|
|
||||||
DownloadingHelpers.enqueue_pending_download_tasks(source)
|
|
||||||
|
|
||||||
assert_enqueued(worker: MediaDownloadWorker, args: %{"id" => media_item.id})
|
refute_enqueued(worker: MediaCollectionIndexingWorker)
|
||||||
assert {:ok, %Source{}} = Sources.update_source(source, update_attrs)
|
assert {:ok, %Source{}} = Sources.update_source(source, update_attrs)
|
||||||
refute_enqueued(worker: MediaDownloadWorker)
|
refute_enqueued(worker: MediaCollectionIndexingWorker)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
test "enabling a source will create a task if the index frequency is >0" do
|
||||||
|
source = source_fixture(enabled: false, index_frequency_minutes: 123)
|
||||||
|
update_attrs = %{enabled: true}
|
||||||
|
|
||||||
|
refute_enqueued(worker: MediaCollectionIndexingWorker)
|
||||||
|
assert {:ok, %Source{}} = Sources.update_source(source, update_attrs)
|
||||||
|
assert_enqueued(worker: MediaCollectionIndexingWorker, args: %{"id" => source.id})
|
||||||
|
end
|
||||||
|
|
||||||
|
test "enabling a source will not create a task if the index frequency is 0" do
|
||||||
|
source = source_fixture(enabled: false, index_frequency_minutes: 0)
|
||||||
|
update_attrs = %{enabled: true}
|
||||||
|
|
||||||
|
refute_enqueued(worker: MediaCollectionIndexingWorker)
|
||||||
|
assert {:ok, %Source{}} = Sources.update_source(source, update_attrs)
|
||||||
|
refute_enqueued(worker: MediaCollectionIndexingWorker)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "update_source/3 when testing fast indexing" do
|
||||||
test "enabling fast_index will schedule a fast indexing task" do
|
test "enabling fast_index will schedule a fast indexing task" do
|
||||||
source = source_fixture(fast_index: false)
|
source = source_fixture(fast_index: false)
|
||||||
update_attrs = %{fast_index: true}
|
update_attrs = %{fast_index: true}
|
||||||
@@ -503,15 +617,6 @@ defmodule Pinchflat.SourcesTest do
|
|||||||
refute_enqueued(worker: FastIndexingWorker)
|
refute_enqueued(worker: FastIndexingWorker)
|
||||||
end
|
end
|
||||||
|
|
||||||
test "updates with invalid data returns error changeset" do
|
|
||||||
source = source_fixture()
|
|
||||||
|
|
||||||
assert {:error, %Ecto.Changeset{}} =
|
|
||||||
Sources.update_source(source, @invalid_source_attrs)
|
|
||||||
|
|
||||||
assert source == Sources.get_source!(source.id)
|
|
||||||
end
|
|
||||||
|
|
||||||
test "fast_index forces the index frequency to be a default value" do
|
test "fast_index forces the index frequency to be a default value" do
|
||||||
source = source_fixture(%{fast_index: true})
|
source = source_fixture(%{fast_index: true})
|
||||||
update_attrs = %{index_frequency_minutes: 0}
|
update_attrs = %{index_frequency_minutes: 0}
|
||||||
@@ -530,23 +635,43 @@ defmodule Pinchflat.SourcesTest do
|
|||||||
assert source.index_frequency_minutes == 0
|
assert source.index_frequency_minutes == 0
|
||||||
end
|
end
|
||||||
|
|
||||||
test "updating will kickoff a metadata storage worker if the original_url changes" do
|
test "disabling a source will delete any pending tasks" do
|
||||||
expect(YtDlpRunnerMock, :run, &playlist_mock/4)
|
|
||||||
source = source_fixture()
|
source = source_fixture()
|
||||||
update_attrs = %{original_url: "https://www.youtube.com/channel/cba321"}
|
update_attrs = %{enabled: false}
|
||||||
|
|
||||||
assert {:ok, %Source{} = source} = Sources.update_source(source, update_attrs)
|
{:ok, job} = Oban.insert(FastIndexingWorker.new(%{"id" => source.id}))
|
||||||
|
task = task_fixture(source_id: source.id, job_id: job.id)
|
||||||
assert_enqueued(worker: SourceMetadataStorageWorker, args: %{"id" => source.id})
|
|
||||||
end
|
|
||||||
|
|
||||||
test "updating will not kickoff a metadata storage worker other attrs change" do
|
|
||||||
source = source_fixture()
|
|
||||||
update_attrs = %{name: "some new name"}
|
|
||||||
|
|
||||||
assert {:ok, %Source{}} = Sources.update_source(source, update_attrs)
|
assert {:ok, %Source{}} = Sources.update_source(source, update_attrs)
|
||||||
|
|
||||||
refute_enqueued(worker: SourceMetadataStorageWorker)
|
assert_raise Ecto.NoResultsError, fn -> Repo.reload!(task) end
|
||||||
|
end
|
||||||
|
|
||||||
|
test "updating fast indexing will not create a task if the source is disabled" do
|
||||||
|
source = source_fixture(enabled: false, fast_index: false)
|
||||||
|
update_attrs = %{fast_index: true}
|
||||||
|
|
||||||
|
refute_enqueued(worker: FastIndexingWorker)
|
||||||
|
assert {:ok, %Source{}} = Sources.update_source(source, update_attrs)
|
||||||
|
refute_enqueued(worker: FastIndexingWorker)
|
||||||
|
end
|
||||||
|
|
||||||
|
test "enabling a source will create a task if fast_index is true" do
|
||||||
|
source = source_fixture(enabled: false, fast_index: true)
|
||||||
|
update_attrs = %{enabled: true}
|
||||||
|
|
||||||
|
refute_enqueued(worker: FastIndexingWorker)
|
||||||
|
assert {:ok, %Source{}} = Sources.update_source(source, update_attrs)
|
||||||
|
assert_enqueued(worker: FastIndexingWorker, args: %{"id" => source.id})
|
||||||
|
end
|
||||||
|
|
||||||
|
test "enabling a source will not create a task if fast_index is false" do
|
||||||
|
source = source_fixture(enabled: false, fast_index: false)
|
||||||
|
update_attrs = %{enabled: true}
|
||||||
|
|
||||||
|
refute_enqueued(worker: FastIndexingWorker)
|
||||||
|
assert {:ok, %Source{}} = Sources.update_source(source, update_attrs)
|
||||||
|
refute_enqueued(worker: FastIndexingWorker)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -247,7 +247,7 @@ defmodule Pinchflat.TasksTest do
|
|||||||
assert_raise Ecto.NoResultsError, fn -> Repo.reload!(task) end
|
assert_raise Ecto.NoResultsError, fn -> Repo.reload!(task) end
|
||||||
end
|
end
|
||||||
|
|
||||||
test "deletion can optionall include executing tasks" do
|
test "deletion can optionally include executing tasks" do
|
||||||
source = source_fixture()
|
source = source_fixture()
|
||||||
task = task_fixture(source_id: source.id)
|
task = task_fixture(source_id: source.id)
|
||||||
|
|
||||||
|
|||||||
@@ -34,27 +34,10 @@ defmodule PinchflatWeb.SourceControllerTest do
|
|||||||
end
|
end
|
||||||
|
|
||||||
describe "index" do
|
describe "index" do
|
||||||
test "lists all sources", %{conn: conn} do
|
# Most of the tests are in `index_table_list_test.exs`
|
||||||
source = source_fixture()
|
test "returns 200", %{conn: conn} do
|
||||||
conn = get(conn, ~p"/sources")
|
conn = get(conn, ~p"/sources")
|
||||||
|
|
||||||
assert html_response(conn, 200) =~ "Sources"
|
assert html_response(conn, 200) =~ "Sources"
|
||||||
assert html_response(conn, 200) =~ source.custom_name
|
|
||||||
end
|
|
||||||
|
|
||||||
test "omits sources that have marked_for_deletion_at set", %{conn: conn} do
|
|
||||||
source = source_fixture(marked_for_deletion_at: DateTime.utc_now())
|
|
||||||
conn = get(conn, ~p"/sources")
|
|
||||||
|
|
||||||
refute html_response(conn, 200) =~ source.custom_name
|
|
||||||
end
|
|
||||||
|
|
||||||
test "omits sources who's media profile has marked_for_deletion_at set", %{conn: conn} do
|
|
||||||
media_profile = media_profile_fixture(marked_for_deletion_at: DateTime.utc_now())
|
|
||||||
source = source_fixture(media_profile_id: media_profile.id)
|
|
||||||
conn = get(conn, ~p"/sources")
|
|
||||||
|
|
||||||
refute html_response(conn, 200) =~ source.custom_name
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,55 @@
|
|||||||
|
defmodule PinchflatWeb.Sources.IndexTableLiveTest do
|
||||||
|
use PinchflatWeb.ConnCase
|
||||||
|
|
||||||
|
import Phoenix.LiveViewTest
|
||||||
|
import Pinchflat.SourcesFixtures
|
||||||
|
import Pinchflat.ProfilesFixtures
|
||||||
|
|
||||||
|
alias Pinchflat.Sources.Source
|
||||||
|
alias PinchflatWeb.Sources.IndexTableLive
|
||||||
|
|
||||||
|
describe "initial rendering" do
|
||||||
|
test "lists all sources", %{conn: conn} do
|
||||||
|
source = source_fixture()
|
||||||
|
|
||||||
|
{:ok, _view, html} = live_isolated(conn, IndexTableLive)
|
||||||
|
|
||||||
|
assert html =~ source.custom_name
|
||||||
|
end
|
||||||
|
|
||||||
|
test "omits sources that have marked_for_deletion_at set", %{conn: conn} do
|
||||||
|
source = source_fixture(marked_for_deletion_at: DateTime.utc_now())
|
||||||
|
|
||||||
|
{:ok, _view, html} = live_isolated(conn, IndexTableLive)
|
||||||
|
|
||||||
|
refute html =~ source.custom_name
|
||||||
|
end
|
||||||
|
|
||||||
|
test "omits sources who's media profile has marked_for_deletion_at set", %{conn: conn} do
|
||||||
|
media_profile = media_profile_fixture(marked_for_deletion_at: DateTime.utc_now())
|
||||||
|
source = source_fixture(media_profile_id: media_profile.id)
|
||||||
|
|
||||||
|
{:ok, _view, html} = live_isolated(conn, IndexTableLive)
|
||||||
|
|
||||||
|
refute html =~ source.custom_name
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "when a source is enabled or disabled" do
|
||||||
|
test "updates the source's enabled status", %{conn: conn} do
|
||||||
|
source = source_fixture(enabled: true)
|
||||||
|
{:ok, view, _html} = live_isolated(conn, IndexTableLive)
|
||||||
|
|
||||||
|
params = %{
|
||||||
|
"event" => "toggle_enabled",
|
||||||
|
"id" => source.id,
|
||||||
|
"value" => "false"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Send an event to the server directly
|
||||||
|
render_change(view, "formless-input", params)
|
||||||
|
|
||||||
|
assert %{enabled: false} = Repo.get!(Source, source.id)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -6,7 +6,7 @@ defmodule PinchflatWeb.Sources.MediaItemTableLiveTest do
|
|||||||
import Pinchflat.SourcesFixtures
|
import Pinchflat.SourcesFixtures
|
||||||
import Pinchflat.ProfilesFixtures
|
import Pinchflat.ProfilesFixtures
|
||||||
|
|
||||||
alias Pinchflat.Sources.MediaItemTableLive
|
alias PinchflatWeb.Sources.MediaItemTableLive
|
||||||
|
|
||||||
setup do
|
setup do
|
||||||
source = source_fixture()
|
source = source_fixture()
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ defmodule Pinchflat.SourcesFixtures do
|
|||||||
Enum.into(
|
Enum.into(
|
||||||
attrs,
|
attrs,
|
||||||
%{
|
%{
|
||||||
|
enabled: true,
|
||||||
collection_name: "Source ##{:rand.uniform(1_000_000)}",
|
collection_name: "Source ##{:rand.uniform(1_000_000)}",
|
||||||
collection_id: Base.encode16(:crypto.hash(:md5, "#{:rand.uniform(1_000_000)}")),
|
collection_id: Base.encode16(:crypto.hash(:md5, "#{:rand.uniform(1_000_000)}")),
|
||||||
collection_type: "channel",
|
collection_type: "channel",
|
||||||
|
|||||||
Reference in New Issue
Block a user