Compare commits
22 Commits
v2024.11.27
...
v2025.1.3
| Author | SHA1 | Date | |
|---|---|---|---|
| d8fdba8f6d | |||
| e4c186eacb | |||
| e150355874 | |||
| 967e21a8a1 | |||
| 9185f075ca | |||
| 09d1653f4b | |||
| 115f675ae3 | |||
| f51b219860 | |||
| 246ca3b299 | |||
| c9bd1ea7bd | |||
| a2a70fcce2 | |||
| 0be469dcb0 | |||
| 023f449dbe | |||
| e9d365ee9e | |||
| 53e106dac2 | |||
| e56f39a158 | |||
| 01c9afa1c3 | |||
| 5371b5d236 | |||
| 83b1fa23d0 | |||
| f8e7b2e42d | |||
| a5b65061f0 | |||
| 0fb971dd0a |
@@ -40,5 +40,9 @@ window.dispatchFor = (elementOrId, eventName, detail = {}) => {
|
|||||||
const element =
|
const element =
|
||||||
typeof elementOrId === 'string' ? document.getElementById(elementOrId) : elementOrId
|
typeof elementOrId === 'string' ? document.getElementById(elementOrId) : elementOrId
|
||||||
|
|
||||||
element.dispatchEvent(new CustomEvent(eventName, { detail }))
|
// This is needed to ensure the DOM has updated before dispatching the event.
|
||||||
|
// Doing so ensures that the latest DOM state is what's sent to the server
|
||||||
|
setTimeout(() => {
|
||||||
|
element.dispatchEvent(new Event(eventName, { bubbles: true, detail }))
|
||||||
|
}, 0)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -47,29 +47,6 @@ 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)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
+9
-4
@@ -55,11 +55,16 @@ config :pinchflat, Oban,
|
|||||||
]
|
]
|
||||||
|
|
||||||
if config_env() == :prod do
|
if config_env() == :prod do
|
||||||
config_path = "/config"
|
# Various paths. These ones shouldn't be tweaked if running in Docker
|
||||||
|
media_path = System.get_env("MEDIA_PATH", "/downloads")
|
||||||
|
config_path = System.get_env("CONFIG_PATH", "/config")
|
||||||
db_path = System.get_env("DATABASE_PATH", Path.join([config_path, "db", "pinchflat.db"]))
|
db_path = System.get_env("DATABASE_PATH", Path.join([config_path, "db", "pinchflat.db"]))
|
||||||
log_path = System.get_env("LOG_PATH", Path.join([config_path, "logs", "pinchflat.log"]))
|
log_path = System.get_env("LOG_PATH", Path.join([config_path, "logs", "pinchflat.log"]))
|
||||||
metadata_path = System.get_env("METADATA_PATH", Path.join([config_path, "metadata"]))
|
metadata_path = System.get_env("METADATA_PATH", Path.join([config_path, "metadata"]))
|
||||||
extras_path = System.get_env("EXTRAS_PATH", Path.join([config_path, "extras"]))
|
extras_path = System.get_env("EXTRAS_PATH", Path.join([config_path, "extras"]))
|
||||||
|
tmpfile_path = System.get_env("TMPFILE_PATH", Path.join([System.tmp_dir!(), "pinchflat", "data"]))
|
||||||
|
# This one can be changed if you want
|
||||||
|
tz_data_path = System.get_env("TZ_DATA_PATH", Path.join([extras_path, "elixir_tz_data"]))
|
||||||
# For running PF as a podcast host on self-hosted environments
|
# For running PF as a podcast host on self-hosted environments
|
||||||
expose_feed_endpoints = String.length(System.get_env("EXPOSE_FEED_ENDPOINTS", "")) > 0
|
expose_feed_endpoints = String.length(System.get_env("EXPOSE_FEED_ENDPOINTS", "")) > 0
|
||||||
# For testing alternate journal modes (see issue #137)
|
# For testing alternate journal modes (see issue #137)
|
||||||
@@ -73,10 +78,10 @@ if config_env() == :prod do
|
|||||||
config :pinchflat,
|
config :pinchflat,
|
||||||
yt_dlp_executable: System.find_executable("yt-dlp"),
|
yt_dlp_executable: System.find_executable("yt-dlp"),
|
||||||
apprise_executable: System.find_executable("apprise"),
|
apprise_executable: System.find_executable("apprise"),
|
||||||
media_directory: "/downloads",
|
media_directory: media_path,
|
||||||
metadata_directory: metadata_path,
|
metadata_directory: metadata_path,
|
||||||
extras_directory: extras_path,
|
extras_directory: extras_path,
|
||||||
tmpfile_directory: Path.join([System.tmp_dir!(), "pinchflat", "data"]),
|
tmpfile_directory: tmpfile_path,
|
||||||
dns_cluster_query: System.get_env("DNS_CLUSTER_QUERY"),
|
dns_cluster_query: System.get_env("DNS_CLUSTER_QUERY"),
|
||||||
expose_feed_endpoints: expose_feed_endpoints,
|
expose_feed_endpoints: expose_feed_endpoints,
|
||||||
# This is configured in application.ex
|
# This is configured in application.ex
|
||||||
@@ -84,7 +89,7 @@ if config_env() == :prod do
|
|||||||
log_path: log_path,
|
log_path: log_path,
|
||||||
base_route_path: base_route_path
|
base_route_path: base_route_path
|
||||||
|
|
||||||
config :tzdata, :data_dir, System.get_env("TZ_DATA_DIR", "/config/extras/elixir_tz_data")
|
config :tzdata, :data_dir, tz_data_path
|
||||||
|
|
||||||
config :pinchflat, Pinchflat.Repo,
|
config :pinchflat, Pinchflat.Repo,
|
||||||
database: db_path,
|
database: db_path,
|
||||||
|
|||||||
@@ -152,6 +152,7 @@ defmodule Pinchflat.Downloading.DownloadOptionBuilder do
|
|||||||
case {behaviour, categories} do
|
case {behaviour, categories} do
|
||||||
{_, []} -> []
|
{_, []} -> []
|
||||||
{:remove, _} -> [sponsorblock_remove: Enum.join(categories, ",")]
|
{:remove, _} -> [sponsorblock_remove: Enum.join(categories, ",")]
|
||||||
|
{:mark, _} -> [sponsorblock_mark: Enum.join(categories, ",")]
|
||||||
{:disabled, _} -> []
|
{:disabled, _} -> []
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -200,6 +201,9 @@ defmodule Pinchflat.Downloading.DownloadOptionBuilder do
|
|||||||
source = media_item_with_preloads.source
|
source = media_item_with_preloads.source
|
||||||
|
|
||||||
%{
|
%{
|
||||||
|
"media_item_id" => to_string(media_item_with_preloads.id),
|
||||||
|
"source_id" => to_string(source.id),
|
||||||
|
"media_profile_id" => to_string(source.media_profile_id),
|
||||||
"source_custom_name" => source.custom_name,
|
"source_custom_name" => source.custom_name,
|
||||||
"source_collection_id" => source.collection_id,
|
"source_collection_id" => source.collection_id,
|
||||||
"source_collection_name" => source.collection_name,
|
"source_collection_name" => source.collection_name,
|
||||||
|
|||||||
@@ -113,9 +113,10 @@ defmodule Pinchflat.Downloading.MediaDownloader do
|
|||||||
|
|
||||||
defp download_with_options(url, item_with_preloads, output_filepath, override_opts) do
|
defp download_with_options(url, item_with_preloads, output_filepath, override_opts) do
|
||||||
{:ok, options} = DownloadOptionBuilder.build(item_with_preloads, override_opts)
|
{:ok, options} = DownloadOptionBuilder.build(item_with_preloads, override_opts)
|
||||||
runner_opts = [output_filepath: output_filepath, use_cookies: item_with_preloads.source.use_cookies]
|
use_cookies = item_with_preloads.source.use_cookies
|
||||||
|
runner_opts = [output_filepath: output_filepath, use_cookies: use_cookies]
|
||||||
|
|
||||||
case YtDlpMedia.get_downloadable_status(url) do
|
case YtDlpMedia.get_downloadable_status(url, use_cookies: use_cookies) do
|
||||||
{:ok, :downloadable} -> YtDlpMedia.download(url, options, runner_opts)
|
{:ok, :downloadable} -> YtDlpMedia.download(url, options, runner_opts)
|
||||||
{:ok, :ignorable} -> {:error, :unsuitable_for_download}
|
{:ok, :ignorable} -> {:error, :unsuitable_for_download}
|
||||||
err -> err
|
err -> err
|
||||||
|
|||||||
@@ -0,0 +1,40 @@
|
|||||||
|
defmodule Pinchflat.Podcasts.OpmlFeedBuilder do
|
||||||
|
@moduledoc """
|
||||||
|
Methods for building an OPML feed for a list of sources.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import Pinchflat.Utils.XmlUtils, only: [safe: 1]
|
||||||
|
|
||||||
|
alias PinchflatWeb.Router.Helpers, as: Routes
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Builds an OPML feed for a given list of sources.
|
||||||
|
|
||||||
|
Returns an XML document as a string.
|
||||||
|
"""
|
||||||
|
def build(url_base, sources) do
|
||||||
|
sources_xml =
|
||||||
|
Enum.map(
|
||||||
|
sources,
|
||||||
|
&"""
|
||||||
|
<outline type="rss" text="#{safe(&1.custom_name)}" xmlUrl="#{safe(source_route(url_base, &1))}" />
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
|
||||||
|
"""
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<opml version="2.0">
|
||||||
|
<head>
|
||||||
|
<title>All Sources</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
#{Enum.join(sources_xml, "\n")}
|
||||||
|
</body>
|
||||||
|
</opml>
|
||||||
|
"""
|
||||||
|
end
|
||||||
|
|
||||||
|
defp source_route(url_base, source) do
|
||||||
|
Path.join(url_base, "#{Routes.podcast_path(PinchflatWeb.Endpoint, :rss_feed, source.uuid)}.xml")
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -5,11 +5,25 @@ defmodule Pinchflat.Podcasts.PodcastHelpers do
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
use Pinchflat.Media.MediaQuery
|
use Pinchflat.Media.MediaQuery
|
||||||
|
use Pinchflat.Sources.SourcesQuery
|
||||||
|
|
||||||
alias Pinchflat.Repo
|
alias Pinchflat.Repo
|
||||||
alias Pinchflat.Metadata.MediaMetadata
|
alias Pinchflat.Metadata.MediaMetadata
|
||||||
alias Pinchflat.Metadata.SourceMetadata
|
alias Pinchflat.Metadata.SourceMetadata
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Returns a list of sources that are not marked for deletion.
|
||||||
|
|
||||||
|
Returns: [%Source{}]
|
||||||
|
"""
|
||||||
|
def opml_sources() do
|
||||||
|
SourcesQuery.new()
|
||||||
|
|> select([s], %{custom_name: s.custom_name, uuid: s.uuid})
|
||||||
|
|> where([s], is_nil(s.marked_for_deletion_at))
|
||||||
|
|> order_by(asc: :custom_name)
|
||||||
|
|> Repo.all()
|
||||||
|
end
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
Returns a list of media items that have been downloaded to disk
|
Returns a list of media items that have been downloaded to disk
|
||||||
and have been proven to still exist there.
|
and have been proven to still exist there.
|
||||||
|
|||||||
@@ -55,7 +55,7 @@ defmodule Pinchflat.Profiles.MediaProfile do
|
|||||||
field :embed_metadata, :boolean, default: false
|
field :embed_metadata, :boolean, default: false
|
||||||
|
|
||||||
field :download_nfo, :boolean, default: false
|
field :download_nfo, :boolean, default: false
|
||||||
field :sponsorblock_behaviour, Ecto.Enum, values: [:disabled, :remove], default: :disabled
|
field :sponsorblock_behaviour, Ecto.Enum, values: [:disabled, :mark, :remove], default: :disabled
|
||||||
field :sponsorblock_categories, {:array, :string}, default: []
|
field :sponsorblock_categories, {:array, :string}, default: []
|
||||||
# NOTE: these do NOT speed up indexing - the indexer still has to go
|
# NOTE: these do NOT speed up indexing - the indexer still has to go
|
||||||
# through the entire collection to determine if a media is a short or
|
# through the entire collection to determine if a media is a short or
|
||||||
@@ -67,7 +67,7 @@ defmodule Pinchflat.Profiles.MediaProfile do
|
|||||||
field :shorts_behaviour, Ecto.Enum, values: ~w(include exclude only)a, default: :include
|
field :shorts_behaviour, Ecto.Enum, values: ~w(include exclude only)a, default: :include
|
||||||
field :livestream_behaviour, Ecto.Enum, values: ~w(include exclude only)a, default: :include
|
field :livestream_behaviour, Ecto.Enum, values: ~w(include exclude only)a, default: :include
|
||||||
field :audio_track, :string
|
field :audio_track, :string
|
||||||
field :preferred_resolution, Ecto.Enum, values: ~w(4320p 2160p 1080p 720p 480p 360p audio)a, default: :"1080p"
|
field :preferred_resolution, Ecto.Enum, values: ~w(4320p 2160p 1440p 1080p 720p 480p 360p audio)a, default: :"1080p"
|
||||||
field :media_container, :string, default: nil
|
field :media_container, :string, default: nil
|
||||||
|
|
||||||
field :marked_for_deletion_at, :utc_datetime
|
field :marked_for_deletion_at, :utc_datetime
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ defmodule Pinchflat.Settings.Setting do
|
|||||||
field :apprise_version, :string
|
field :apprise_version, :string
|
||||||
field :apprise_server, :string
|
field :apprise_server, :string
|
||||||
field :youtube_api_key, :string
|
field :youtube_api_key, :string
|
||||||
|
field :route_token, :string
|
||||||
|
|
||||||
field :video_codec_preference, :string
|
field :video_codec_preference, :string
|
||||||
field :audio_codec_preference, :string
|
field :audio_codec_preference, :string
|
||||||
|
|||||||
@@ -79,21 +79,21 @@ defmodule Pinchflat.SlowIndexing.MediaCollectionIndexingWorker do
|
|||||||
case {source.index_frequency_minutes, source.last_indexed_at} do
|
case {source.index_frequency_minutes, source.last_indexed_at} do
|
||||||
{index_freq, _} when index_freq > 0 ->
|
{index_freq, _} when index_freq > 0 ->
|
||||||
# If the indexing is on a schedule simply run indexing and reschedule
|
# If the indexing is on a schedule simply run indexing and reschedule
|
||||||
perform_indexing_and_notification(source)
|
perform_indexing_and_notification(source, was_forced: args["force"])
|
||||||
maybe_enqueue_fast_indexing_task(source)
|
maybe_enqueue_fast_indexing_task(source)
|
||||||
reschedule_indexing(source)
|
reschedule_indexing(source)
|
||||||
|
|
||||||
{_, nil} ->
|
{_, nil} ->
|
||||||
# If the source has never been indexed, index it once
|
# If the source has never been indexed, index it once
|
||||||
# even if it's not meant to reschedule
|
# even if it's not meant to reschedule
|
||||||
perform_indexing_and_notification(source)
|
perform_indexing_and_notification(source, was_forced: args["force"])
|
||||||
:ok
|
:ok
|
||||||
|
|
||||||
_ ->
|
_ ->
|
||||||
# If the source HAS been indexed and is not meant to reschedule,
|
# If the source HAS been indexed and is not meant to reschedule,
|
||||||
# perform a no-op (unless forced)
|
# perform a no-op (unless forced)
|
||||||
if args["force"] do
|
if args["force"] do
|
||||||
perform_indexing_and_notification(source)
|
perform_indexing_and_notification(source, was_forced: true)
|
||||||
end
|
end
|
||||||
|
|
||||||
:ok
|
:ok
|
||||||
@@ -103,11 +103,11 @@ defmodule Pinchflat.SlowIndexing.MediaCollectionIndexingWorker do
|
|||||||
Ecto.StaleEntryError -> Logger.info("#{__MODULE__} discarded: source #{source_id} stale")
|
Ecto.StaleEntryError -> Logger.info("#{__MODULE__} discarded: source #{source_id} stale")
|
||||||
end
|
end
|
||||||
|
|
||||||
defp perform_indexing_and_notification(source) do
|
defp perform_indexing_and_notification(source, indexing_opts) do
|
||||||
apprise_server = Settings.get!(:apprise_server)
|
apprise_server = Settings.get!(:apprise_server)
|
||||||
|
|
||||||
SourceNotifications.wrap_new_media_notification(apprise_server, source, fn ->
|
SourceNotifications.wrap_new_media_notification(apprise_server, source, fn ->
|
||||||
SlowIndexingHelpers.index_and_enqueue_download_for_media_items(source)
|
SlowIndexingHelpers.index_and_enqueue_download_for_media_items(source, indexing_opts)
|
||||||
end)
|
end)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,8 @@ defmodule Pinchflat.SlowIndexing.SlowIndexingHelpers do
|
|||||||
Many of these methods are made to be kickoff or be consumed by workers.
|
Many of these methods are made to be kickoff or be consumed by workers.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
use Pinchflat.Media.MediaQuery
|
||||||
|
|
||||||
require Logger
|
require Logger
|
||||||
|
|
||||||
alias Pinchflat.Repo
|
alias Pinchflat.Repo
|
||||||
@@ -14,6 +16,7 @@ defmodule Pinchflat.SlowIndexing.SlowIndexingHelpers do
|
|||||||
alias Pinchflat.Sources.Source
|
alias Pinchflat.Sources.Source
|
||||||
alias Pinchflat.Media.MediaItem
|
alias Pinchflat.Media.MediaItem
|
||||||
alias Pinchflat.YtDlp.MediaCollection
|
alias Pinchflat.YtDlp.MediaCollection
|
||||||
|
alias Pinchflat.Utils.FilesystemUtils
|
||||||
alias Pinchflat.Downloading.DownloadingHelpers
|
alias Pinchflat.Downloading.DownloadingHelpers
|
||||||
alias Pinchflat.SlowIndexing.FileFollowerServer
|
alias Pinchflat.SlowIndexing.FileFollowerServer
|
||||||
alias Pinchflat.Downloading.DownloadOptionBuilder
|
alias Pinchflat.Downloading.DownloadOptionBuilder
|
||||||
@@ -22,13 +25,19 @@ defmodule Pinchflat.SlowIndexing.SlowIndexingHelpers do
|
|||||||
alias Pinchflat.YtDlp.Media, as: YtDlpMedia
|
alias Pinchflat.YtDlp.Media, as: YtDlpMedia
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
Starts tasks for indexing a source's media regardless of the source's indexing
|
Kills old indexing tasks and starts a new task to index the media collection.
|
||||||
frequency. It's assumed the caller will check for indexing frequency.
|
|
||||||
|
The job is delayed based on the source's `index_frequency_minutes` setting unless
|
||||||
|
one of the following is true:
|
||||||
|
- The `force` option is set to true
|
||||||
|
- The source has never been indexed before
|
||||||
|
- The source has been indexed before, but the last indexing job was more than
|
||||||
|
`index_frequency_minutes` ago
|
||||||
|
|
||||||
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)
|
job_offset_seconds = if job_args[:force], do: 0, else: 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)
|
||||||
@@ -52,8 +61,8 @@ defmodule Pinchflat.SlowIndexing.SlowIndexingHelpers do
|
|||||||
@doc """
|
@doc """
|
||||||
Given a media source, creates (indexes) the media by creating media_items for each
|
Given a media source, creates (indexes) the media by creating media_items for each
|
||||||
media ID in the source. Afterward, kicks off a download task for each pending media
|
media ID in the source. Afterward, kicks off a download task for each pending media
|
||||||
item belonging to the source. You can't tell me the method name isn't descriptive!
|
item belonging to the source. Returns a list of media items or changesets
|
||||||
Returns a list of media items or changesets (if the media item couldn't be created).
|
(if the media item couldn't be created).
|
||||||
|
|
||||||
Indexing is slow and usually returns a list of all media data at once for record creation.
|
Indexing is slow and usually returns a list of all media data at once for record creation.
|
||||||
To help with this, we use a file follower to watch the file that yt-dlp writes to
|
To help with this, we use a file follower to watch the file that yt-dlp writes to
|
||||||
@@ -61,23 +70,33 @@ defmodule Pinchflat.SlowIndexing.SlowIndexingHelpers do
|
|||||||
clarity to the user experience. This has a few things to be aware of which are documented
|
clarity to the user experience. This has a few things to be aware of which are documented
|
||||||
below in the file watcher setup method.
|
below in the file watcher setup method.
|
||||||
|
|
||||||
|
Additionally, in the case of a repeat index we create a download archive file that
|
||||||
|
contains some media IDs that we've indexed in the past. Note that this archive doesn't
|
||||||
|
contain the most recent IDs but rather a subset of IDs that are offset by some amount.
|
||||||
|
Practically, this means that we'll re-index a small handful of media that we've recently
|
||||||
|
indexed, but this is a good thing since it'll let us pick up on any recent changes to the
|
||||||
|
most recent media items.
|
||||||
|
|
||||||
|
We don't create a download archive for playlists (only channels), nor do we create one if
|
||||||
|
the indexing was forced by the user.
|
||||||
|
|
||||||
NOTE: downloads are only enqueued if the source is set to download media. Downloads are
|
NOTE: downloads are only enqueued if the source is set to download media. Downloads are
|
||||||
also enqueued for ALL pending media items, not just the ones that were indexed in this
|
also enqueued for ALL pending media items, not just the ones that were indexed in this
|
||||||
job run. This should ensure that any stragglers are caught if, for some reason, they
|
job run. This should ensure that any stragglers are caught if, for some reason, they
|
||||||
weren't enqueued or somehow got de-queued.
|
weren't enqueued or somehow got de-queued.
|
||||||
|
|
||||||
Since indexing returns all media data EVERY TIME, we that that opportunity to update
|
Available options:
|
||||||
indexing metadata for media items that have already been created.
|
- `was_forced`: Whether the indexing was forced by the user
|
||||||
|
|
||||||
Returns [%MediaItem{} | %Ecto.Changeset{}]
|
Returns [%MediaItem{} | %Ecto.Changeset{}]
|
||||||
"""
|
"""
|
||||||
def index_and_enqueue_download_for_media_items(%Source{} = source) do
|
def index_and_enqueue_download_for_media_items(%Source{} = source, opts \\ []) do
|
||||||
# The media_profile is needed to determine the quality options to _then_ determine a more
|
# The media_profile is needed to determine the quality options to _then_ determine a more
|
||||||
# accurate predicted filepath
|
# accurate predicted filepath
|
||||||
source = Repo.preload(source, [:media_profile])
|
source = Repo.preload(source, [:media_profile])
|
||||||
# See the method definition below for more info on how file watchers work
|
# See the method definition below for more info on how file watchers work
|
||||||
# (important reading if you're not familiar with it)
|
# (important reading if you're not familiar with it)
|
||||||
{:ok, media_attributes} = setup_file_watcher_and_kickoff_indexing(source)
|
{:ok, media_attributes} = setup_file_watcher_and_kickoff_indexing(source, opts)
|
||||||
# Reload because the source may have been updated during the (long-running) indexing process
|
# Reload because the source may have been updated during the (long-running) indexing process
|
||||||
# and important settings like `download_media` may have changed.
|
# and important settings like `download_media` may have changed.
|
||||||
source = Repo.reload!(source)
|
source = Repo.reload!(source)
|
||||||
@@ -109,14 +128,16 @@ defmodule Pinchflat.SlowIndexing.SlowIndexingHelpers do
|
|||||||
# It attempts a graceful shutdown of the file follower after the indexing is done,
|
# It attempts a graceful shutdown of the file follower after the indexing is done,
|
||||||
# but the FileFollowerServer will also stop itself if it doesn't see any activity
|
# but the FileFollowerServer will also stop itself if it doesn't see any activity
|
||||||
# for a sufficiently long time.
|
# for a sufficiently long time.
|
||||||
defp setup_file_watcher_and_kickoff_indexing(source) do
|
defp setup_file_watcher_and_kickoff_indexing(source, opts) do
|
||||||
|
was_forced = Keyword.get(opts, :was_forced, false)
|
||||||
{:ok, pid} = FileFollowerServer.start_link()
|
{:ok, pid} = FileFollowerServer.start_link()
|
||||||
|
|
||||||
handler = fn filepath -> setup_file_follower_watcher(pid, filepath, source) end
|
handler = fn filepath -> setup_file_follower_watcher(pid, filepath, source) end
|
||||||
|
|
||||||
command_opts =
|
command_opts =
|
||||||
[output: DownloadOptionBuilder.build_output_path_for(source)] ++
|
[output: DownloadOptionBuilder.build_output_path_for(source)] ++
|
||||||
DownloadOptionBuilder.build_quality_options_for(source)
|
DownloadOptionBuilder.build_quality_options_for(source) ++
|
||||||
|
build_download_archive_options(source, was_forced)
|
||||||
|
|
||||||
runner_opts = [file_listener_handler: handler, use_cookies: source.use_cookies]
|
runner_opts = [file_listener_handler: handler, use_cookies: source.use_cookies]
|
||||||
result = MediaCollection.get_media_attributes_for_collection(source.original_url, command_opts, runner_opts)
|
result = MediaCollection.get_media_attributes_for_collection(source.original_url, command_opts, runner_opts)
|
||||||
@@ -166,4 +187,57 @@ defmodule Pinchflat.SlowIndexing.SlowIndexingHelpers do
|
|||||||
|
|
||||||
max(0, index_frequency_seconds - offset_seconds)
|
max(0, index_frequency_seconds - offset_seconds)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# The download archive file works in tandem with --break-on-existing to stop
|
||||||
|
# yt-dlp once we've hit media items we've already indexed. But we generate
|
||||||
|
# this list with a bit of an offset so we do intentionally re-scan some media
|
||||||
|
# items to pick up any recent changes (see `get_media_items_for_download_archive`).
|
||||||
|
#
|
||||||
|
# From there, we format the media IDs in the way that yt-dlp expects (ie: "<extractor> <media_id>")
|
||||||
|
# and return the filepath to the caller.
|
||||||
|
defp create_download_archive_file(source) do
|
||||||
|
tmpfile = FilesystemUtils.generate_metadata_tmpfile(:txt)
|
||||||
|
|
||||||
|
archive_contents =
|
||||||
|
source
|
||||||
|
|> get_media_items_for_download_archive()
|
||||||
|
|> Enum.map_join("\n", fn media_item -> "youtube #{media_item.media_id}" end)
|
||||||
|
|
||||||
|
case File.write(tmpfile, archive_contents) do
|
||||||
|
:ok -> tmpfile
|
||||||
|
err -> err
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# Sorting by `uploaded_at` is important because we want to re-index the most recent
|
||||||
|
# media items first but there is no guarantee of any correlation between ID and uploaded_at.
|
||||||
|
#
|
||||||
|
# The offset is important because we want to re-index some media items that we've
|
||||||
|
# recently indexed to pick up on any changes. The limit is because we want this mechanism
|
||||||
|
# to work even if, for example, the video we were using as a stopping point was deleted.
|
||||||
|
# It's not a perfect system, but it should do well enough.
|
||||||
|
#
|
||||||
|
# The chosen limit and offset are arbitary, independent, and vibes-based. Feel free to
|
||||||
|
# tweak as-needed
|
||||||
|
defp get_media_items_for_download_archive(source) do
|
||||||
|
MediaQuery.new()
|
||||||
|
|> where(^MediaQuery.for_source(source))
|
||||||
|
|> order_by(desc: :uploaded_at)
|
||||||
|
|> limit(50)
|
||||||
|
|> offset(20)
|
||||||
|
|> Repo.all()
|
||||||
|
end
|
||||||
|
|
||||||
|
# The download archive isn't useful for playlists (since those are ordered arbitrarily)
|
||||||
|
# and we don't want to use it if the indexing was forced by the user. In other words,
|
||||||
|
# only create an archive for channels that are being indexed as part of their regular
|
||||||
|
# indexing schedule
|
||||||
|
defp build_download_archive_options(%Source{collection_type: :playlist}, _was_forced), do: []
|
||||||
|
defp build_download_archive_options(_source, true), do: []
|
||||||
|
|
||||||
|
defp build_download_archive_options(source, _was_forced) do
|
||||||
|
archive_file = create_download_archive_file(source)
|
||||||
|
|
||||||
|
[:break_on_existing, download_archive: archive_file]
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -45,8 +45,20 @@ defmodule Pinchflat.Utils.FilesystemUtils do
|
|||||||
Returns binary()
|
Returns binary()
|
||||||
"""
|
"""
|
||||||
def generate_metadata_tmpfile(type) do
|
def generate_metadata_tmpfile(type) do
|
||||||
|
filename = StringUtils.random_string(64)
|
||||||
|
# This "namespacing" is more to help with development since things get
|
||||||
|
# weird in my editor when there are thousands of files in a single directory
|
||||||
|
first_two = String.slice(filename, 0..1)
|
||||||
|
second_two = String.slice(filename, 2..3)
|
||||||
tmpfile_directory = Application.get_env(:pinchflat, :tmpfile_directory)
|
tmpfile_directory = Application.get_env(:pinchflat, :tmpfile_directory)
|
||||||
filepath = Path.join([tmpfile_directory, "#{StringUtils.random_string(64)}.#{type}"])
|
|
||||||
|
filepath =
|
||||||
|
Path.join([
|
||||||
|
tmpfile_directory,
|
||||||
|
first_two,
|
||||||
|
second_two,
|
||||||
|
"#{filename}.#{type}"
|
||||||
|
])
|
||||||
|
|
||||||
:ok = write_p!(filepath, "")
|
:ok = write_p!(filepath, "")
|
||||||
|
|
||||||
|
|||||||
@@ -26,19 +26,13 @@ defmodule Pinchflat.Utils.StringUtils do
|
|||||||
end
|
end
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
Truncates a string to the given length and adds `...` if the string is longer than the given length.
|
Wraps a string in double braces. Useful as a UI helper now that
|
||||||
Will break on a word boundary. Nothing happens if the string is shorter than the given length.
|
LiveView 1.0.0 allows `{}` for interpolation so now we can't use braces
|
||||||
|
directly in the view.
|
||||||
|
|
||||||
Returns binary()
|
Returns binary()
|
||||||
"""
|
"""
|
||||||
def truncate(string, length) do
|
def double_brace(string) do
|
||||||
if String.length(string) > length do
|
"{{ #{string} }}"
|
||||||
string
|
|
||||||
|> String.slice(0..(length - 1))
|
|
||||||
|> String.replace(~r/\s+\S*$/, "")
|
|
||||||
|> Kernel.<>("...")
|
|
||||||
else
|
|
||||||
string
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ defmodule Pinchflat.YtDlp.CommandRunner do
|
|||||||
Runs yt-dlp commands using the `System.cmd/3` function
|
Runs yt-dlp commands using the `System.cmd/3` function
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
require Logger
|
||||||
|
|
||||||
alias Pinchflat.Utils.CliUtils
|
alias Pinchflat.Utils.CliUtils
|
||||||
alias Pinchflat.YtDlp.YtDlpCommandRunner
|
alias Pinchflat.YtDlp.YtDlpCommandRunner
|
||||||
alias Pinchflat.Utils.FilesystemUtils, as: FSUtils
|
alias Pinchflat.Utils.FilesystemUtils, as: FSUtils
|
||||||
@@ -24,7 +26,8 @@ defmodule Pinchflat.YtDlp.CommandRunner do
|
|||||||
Returns {:ok, binary()} | {:error, output, status}.
|
Returns {:ok, binary()} | {:error, output, status}.
|
||||||
"""
|
"""
|
||||||
@impl YtDlpCommandRunner
|
@impl YtDlpCommandRunner
|
||||||
def run(url, command_opts, output_template, addl_opts \\ []) do
|
def run(url, action_name, command_opts, output_template, addl_opts \\ []) do
|
||||||
|
Logger.debug("Running yt-dlp command for action: #{action_name}")
|
||||||
# This approach lets us mock the command for testing
|
# This approach lets us mock the command for testing
|
||||||
command = backend_executable()
|
command = backend_executable()
|
||||||
|
|
||||||
@@ -36,7 +39,13 @@ defmodule Pinchflat.YtDlp.CommandRunner do
|
|||||||
formatted_command_opts = [url] ++ CliUtils.parse_options(all_opts)
|
formatted_command_opts = [url] ++ CliUtils.parse_options(all_opts)
|
||||||
|
|
||||||
case CliUtils.wrap_cmd(command, formatted_command_opts, stderr_to_stdout: true) do
|
case CliUtils.wrap_cmd(command, formatted_command_opts, stderr_to_stdout: true) do
|
||||||
{_, 0} ->
|
# yt-dlp exit codes:
|
||||||
|
# 0 = Everything is successful
|
||||||
|
# 100 = yt-dlp must restart for update to complete
|
||||||
|
# 101 = Download cancelled by --max-downloads etc
|
||||||
|
# 2 = Error in user-provided options
|
||||||
|
# 1 = Any other error
|
||||||
|
{_, status} when status in [0, 101] ->
|
||||||
# IDEA: consider deleting the file after reading it. It's in the tmp dir, so it's not
|
# IDEA: consider deleting the file after reading it. It's in the tmp dir, so it's not
|
||||||
# a huge deal, but it's still a good idea to clean up after ourselves.
|
# a huge deal, but it's still a good idea to clean up after ourselves.
|
||||||
# (even on error? especially on error?)
|
# (even on error? especially on error?)
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ defmodule Pinchflat.YtDlp.Media do
|
|||||||
def download(url, command_opts \\ [], addl_opts \\ []) do
|
def download(url, command_opts \\ [], addl_opts \\ []) do
|
||||||
all_command_opts = [:no_simulate] ++ command_opts
|
all_command_opts = [:no_simulate] ++ command_opts
|
||||||
|
|
||||||
with {:ok, output} <- backend_runner().run(url, all_command_opts, "after_move:%()j", addl_opts),
|
with {:ok, output} <- backend_runner().run(url, :download, all_command_opts, "after_move:%()j", addl_opts),
|
||||||
{:ok, parsed_json} <- Phoenix.json_library().decode(output) do
|
{:ok, parsed_json} <- Phoenix.json_library().decode(output) do
|
||||||
{:ok, parsed_json}
|
{:ok, parsed_json}
|
||||||
else
|
else
|
||||||
@@ -55,8 +55,11 @@ defmodule Pinchflat.YtDlp.Media do
|
|||||||
|
|
||||||
Returns {:ok, :downloadable | :ignorable} | {:error, any}
|
Returns {:ok, :downloadable | :ignorable} | {:error, any}
|
||||||
"""
|
"""
|
||||||
def get_downloadable_status(url) do
|
def get_downloadable_status(url, addl_opts \\ []) do
|
||||||
case backend_runner().run(url, [:simulate, :skip_download], "%(.{live_status})j") do
|
action = :get_downloadable_status
|
||||||
|
command_opts = [:simulate, :skip_download]
|
||||||
|
|
||||||
|
case backend_runner().run(url, action, command_opts, "%(.{live_status})j", addl_opts) do
|
||||||
{:ok, output} ->
|
{:ok, output} ->
|
||||||
output
|
output
|
||||||
|> Phoenix.json_library().decode!()
|
|> Phoenix.json_library().decode!()
|
||||||
@@ -78,7 +81,7 @@ defmodule Pinchflat.YtDlp.Media do
|
|||||||
|
|
||||||
# NOTE: it doesn't seem like this command actually returns anything in `after_move` since
|
# NOTE: it doesn't seem like this command actually returns anything in `after_move` since
|
||||||
# we aren't downloading the main media file
|
# we aren't downloading the main media file
|
||||||
backend_runner().run(url, all_command_opts, "after_move:%()j", addl_opts)
|
backend_runner().run(url, :download_thumbnail, all_command_opts, "after_move:%()j", addl_opts)
|
||||||
end
|
end
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
@@ -92,7 +95,7 @@ defmodule Pinchflat.YtDlp.Media do
|
|||||||
all_command_opts = [:simulate, :skip_download] ++ command_opts
|
all_command_opts = [:simulate, :skip_download] ++ command_opts
|
||||||
output_template = indexing_output_template()
|
output_template = indexing_output_template()
|
||||||
|
|
||||||
case backend_runner().run(url, all_command_opts, output_template, addl_opts) do
|
case backend_runner().run(url, :get_media_attributes, all_command_opts, output_template, addl_opts) do
|
||||||
{:ok, output} ->
|
{:ok, output} ->
|
||||||
output
|
output
|
||||||
|> Phoenix.json_library().decode!()
|
|> Phoenix.json_library().decode!()
|
||||||
|
|||||||
@@ -23,7 +23,6 @@ defmodule Pinchflat.YtDlp.MediaCollection do
|
|||||||
Returns {:ok, [map()]} | {:error, any, ...}.
|
Returns {:ok, [map()]} | {:error, any, ...}.
|
||||||
"""
|
"""
|
||||||
def get_media_attributes_for_collection(url, command_opts \\ [], addl_opts \\ []) do
|
def get_media_attributes_for_collection(url, command_opts \\ [], addl_opts \\ []) do
|
||||||
runner = Application.get_env(:pinchflat, :yt_dlp_runner)
|
|
||||||
# `ignore_no_formats_error` is necessary because yt-dlp will error out if
|
# `ignore_no_formats_error` is necessary because yt-dlp will error out if
|
||||||
# the first video has not released yet (ie: is a premier). We don't care about
|
# the first video has not released yet (ie: is a premier). We don't care about
|
||||||
# available formats since we're just getting the media details
|
# available formats since we're just getting the media details
|
||||||
@@ -33,12 +32,13 @@ defmodule Pinchflat.YtDlp.MediaCollection do
|
|||||||
output_filepath = FilesystemUtils.generate_metadata_tmpfile(:json)
|
output_filepath = FilesystemUtils.generate_metadata_tmpfile(:json)
|
||||||
file_listener_handler = Keyword.get(addl_opts, :file_listener_handler, false)
|
file_listener_handler = Keyword.get(addl_opts, :file_listener_handler, false)
|
||||||
runner_opts = [output_filepath: output_filepath, use_cookies: use_cookies]
|
runner_opts = [output_filepath: output_filepath, use_cookies: use_cookies]
|
||||||
|
action = :get_media_attributes_for_collection
|
||||||
|
|
||||||
if file_listener_handler do
|
if file_listener_handler do
|
||||||
file_listener_handler.(output_filepath)
|
file_listener_handler.(output_filepath)
|
||||||
end
|
end
|
||||||
|
|
||||||
case runner.run(url, all_command_opts, output_template, runner_opts) do
|
case backend_runner().run(url, action, all_command_opts, output_template, runner_opts) do
|
||||||
{:ok, output} ->
|
{:ok, output} ->
|
||||||
parsed_lines =
|
parsed_lines =
|
||||||
output
|
output
|
||||||
@@ -82,8 +82,9 @@ defmodule Pinchflat.YtDlp.MediaCollection do
|
|||||||
|
|
||||||
all_command_opts = default_opts ++ command_opts
|
all_command_opts = default_opts ++ command_opts
|
||||||
output_template = "%(.{channel,channel_id,playlist_id,playlist_title,filename})j"
|
output_template = "%(.{channel,channel_id,playlist_id,playlist_title,filename})j"
|
||||||
|
action = :get_source_details
|
||||||
|
|
||||||
with {:ok, output} <- backend_runner().run(source_url, all_command_opts, output_template, addl_opts),
|
with {:ok, output} <- backend_runner().run(source_url, action, all_command_opts, output_template, addl_opts),
|
||||||
{:ok, parsed_json} <- Phoenix.json_library().decode(output) do
|
{:ok, parsed_json} <- Phoenix.json_library().decode(output) do
|
||||||
{:ok, format_source_details(parsed_json)}
|
{:ok, format_source_details(parsed_json)}
|
||||||
else
|
else
|
||||||
@@ -121,8 +122,9 @@ defmodule Pinchflat.YtDlp.MediaCollection do
|
|||||||
|
|
||||||
all_command_opts = [:skip_download] ++ command_opts
|
all_command_opts = [:skip_download] ++ command_opts
|
||||||
output_template = "playlist:%()j"
|
output_template = "playlist:%()j"
|
||||||
|
action = :get_source_metadata
|
||||||
|
|
||||||
with {:ok, output} <- backend_runner().run(source_url, all_command_opts, output_template, addl_opts),
|
with {:ok, output} <- backend_runner().run(source_url, action, all_command_opts, output_template, addl_opts),
|
||||||
{:ok, parsed_json} <- Phoenix.json_library().decode(output) do
|
{:ok, parsed_json} <- Phoenix.json_library().decode(output) do
|
||||||
{:ok, parsed_json}
|
{:ok, parsed_json}
|
||||||
else
|
else
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ defmodule Pinchflat.YtDlp.YtDlpCommandRunner do
|
|||||||
yt-dlp command.
|
yt-dlp command.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@callback run(binary(), keyword(), binary()) :: {:ok, binary()} | {:error, binary(), integer()}
|
@callback run(binary(), atom(), keyword(), binary()) :: {:ok, binary()} | {:error, binary(), integer()}
|
||||||
@callback run(binary(), keyword(), binary(), keyword()) :: {:ok, binary()} | {:error, binary(), integer()}
|
@callback run(binary(), atom(), keyword(), binary(), keyword()) :: {:ok, binary()} | {:error, binary(), integer()}
|
||||||
@callback version() :: {:ok, binary()} | {:error, binary()}
|
@callback version() :: {:ok, binary()} | {:error, binary()}
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -100,6 +100,7 @@ defmodule PinchflatWeb do
|
|||||||
import PinchflatWeb.CustomComponents.TextComponents
|
import PinchflatWeb.CustomComponents.TextComponents
|
||||||
import PinchflatWeb.CustomComponents.TableComponents
|
import PinchflatWeb.CustomComponents.TableComponents
|
||||||
import PinchflatWeb.CustomComponents.ButtonComponents
|
import PinchflatWeb.CustomComponents.ButtonComponents
|
||||||
|
import Pinchflat.Utils.StringUtils, only: [double_brace: 1]
|
||||||
|
|
||||||
alias Pinchflat.Settings
|
alias Pinchflat.Settings
|
||||||
alias Pinchflat.Utils.StringUtils
|
alias Pinchflat.Utils.StringUtils
|
||||||
|
|||||||
@@ -82,7 +82,7 @@ defmodule PinchflatWeb.CoreComponents do
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div id={"#{@id}-content"}>
|
<div id={"#{@id}-content"}>
|
||||||
<%= render_slot(@inner_block) %>
|
{render_slot(@inner_block)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -126,9 +126,9 @@ defmodule PinchflatWeb.CoreComponents do
|
|||||||
]}>
|
]}>
|
||||||
<main>
|
<main>
|
||||||
<h5 :if={@title} class="mb-2 text-lg font-bold">
|
<h5 :if={@title} class="mb-2 text-lg font-bold">
|
||||||
<%= @title %>
|
{@title}
|
||||||
</h5>
|
</h5>
|
||||||
<p class="mt-2 text-md leading-5 opacity-80"><%= msg %></p>
|
<p class="mt-2 text-md leading-5 opacity-80">{msg}</p>
|
||||||
</main>
|
</main>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
@@ -208,9 +208,9 @@ defmodule PinchflatWeb.CoreComponents do
|
|||||||
def simple_form(assigns) do
|
def simple_form(assigns) do
|
||||||
~H"""
|
~H"""
|
||||||
<.form :let={f} for={@for} as={@as} {@rest}>
|
<.form :let={f} for={@for} as={@as} {@rest}>
|
||||||
<%= render_slot(@inner_block, f) %>
|
{render_slot(@inner_block, f)}
|
||||||
<div :for={action <- @actions} class="mt-2 flex items-center justify-between gap-6">
|
<div :for={action <- @actions} class="mt-2 flex items-center justify-between gap-6">
|
||||||
<%= render_slot(action, f) %>
|
{render_slot(action, f)}
|
||||||
</div>
|
</div>
|
||||||
</.form>
|
</.form>
|
||||||
"""
|
"""
|
||||||
@@ -297,11 +297,11 @@ defmodule PinchflatWeb.CoreComponents do
|
|||||||
class={["rounded focus:ring-0", @inputclass]}
|
class={["rounded focus:ring-0", @inputclass]}
|
||||||
{@rest}
|
{@rest}
|
||||||
/>
|
/>
|
||||||
<%= @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>
|
||||||
<.help :if={@help}><%= if @html_help, do: Phoenix.HTML.raw(@help), else: @help %></.help>
|
<.help :if={@help}>{if @html_help, do: Phoenix.HTML.raw(@help), else: @help}</.help>
|
||||||
<.error :for={msg <- @errors}><%= msg %></.error>
|
<.error :for={msg <- @errors}>{msg}</.error>
|
||||||
</div>
|
</div>
|
||||||
"""
|
"""
|
||||||
end
|
end
|
||||||
@@ -310,7 +310,7 @@ defmodule PinchflatWeb.CoreComponents do
|
|||||||
~H"""
|
~H"""
|
||||||
<div phx-feedback-for={@name}>
|
<div phx-feedback-for={@name}>
|
||||||
<.label for={@id}>
|
<.label for={@id}>
|
||||||
<%= @label %><span :if={@label_suffix} class="text-xs text-bodydark"><%= @label_suffix %></span>
|
{@label}<span :if={@label_suffix} class="text-xs text-bodydark">{@label_suffix}</span>
|
||||||
</.label>
|
</.label>
|
||||||
<section class="grid grid-cols-1 gap-2 md:grid-cols-2 max-w-prose mb-4 ml-1">
|
<section class="grid grid-cols-1 gap-2 md:grid-cols-2 max-w-prose mb-4 ml-1">
|
||||||
<div :for={{option_name, option_value} <- @options} class="flex items-center">
|
<div :for={{option_name, option_value} <- @options} class="flex items-center">
|
||||||
@@ -323,12 +323,12 @@ defmodule PinchflatWeb.CoreComponents do
|
|||||||
class={["rounded focus:ring-offset-0 ring-offset-0 focus:ring-0 h-5 w-5 ", @inputclass]}
|
class={["rounded focus:ring-offset-0 ring-offset-0 focus:ring-0 h-5 w-5 ", @inputclass]}
|
||||||
/>
|
/>
|
||||||
<label for={"#{@id}-#{option_value}"} class="ml-2 cursor-pointer select-none">
|
<label for={"#{@id}-#{option_value}"} class="ml-2 cursor-pointer select-none">
|
||||||
<%= option_name %>
|
{option_name}
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
<.help :if={@help}><%= if @html_help, do: Phoenix.HTML.raw(@help), else: @help %></.help>
|
<.help :if={@help}>{if @html_help, do: Phoenix.HTML.raw(@help), else: @help}</.help>
|
||||||
<.error :for={msg <- @errors}><%= msg %></.error>
|
<.error :for={msg <- @errors}>{msg}</.error>
|
||||||
</div>
|
</div>
|
||||||
"""
|
"""
|
||||||
end
|
end
|
||||||
@@ -342,8 +342,8 @@ defmodule PinchflatWeb.CoreComponents do
|
|||||||
~H"""
|
~H"""
|
||||||
<div x-data={"{ enabled: #{@checked} }"} class="" phx-update="ignore" id={"#{@id}-wrapper"}>
|
<div x-data={"{ enabled: #{@checked} }"} class="" phx-update="ignore" id={"#{@id}-wrapper"}>
|
||||||
<.label :if={@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 flex flex-col">
|
<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} />
|
||||||
@@ -359,8 +359,8 @@ defmodule PinchflatWeb.CoreComponents do
|
|||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<.help :if={@help}><%= if @html_help, do: Phoenix.HTML.raw(@help), else: @help %></.help>
|
<.help :if={@help}>{if @html_help, do: Phoenix.HTML.raw(@help), else: @help}</.help>
|
||||||
<.error :for={msg <- @errors}><%= msg %></.error>
|
<.error :for={msg <- @errors}>{msg}</.error>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
"""
|
"""
|
||||||
@@ -370,7 +370,7 @@ defmodule PinchflatWeb.CoreComponents do
|
|||||||
~H"""
|
~H"""
|
||||||
<div phx-feedback-for={@name}>
|
<div phx-feedback-for={@name}>
|
||||||
<.label :if={@label} for={@id}>
|
<.label :if={@label} for={@id}>
|
||||||
<%= @label %><span :if={@label_suffix} class="text-xs text-bodydark"><%= @label_suffix %></span>
|
{@label}<span :if={@label_suffix} class="text-xs text-bodydark">{@label_suffix}</span>
|
||||||
</.label>
|
</.label>
|
||||||
<div class="flex">
|
<div class="flex">
|
||||||
<select
|
<select
|
||||||
@@ -385,13 +385,13 @@ defmodule PinchflatWeb.CoreComponents do
|
|||||||
multiple={@multiple}
|
multiple={@multiple}
|
||||||
{@rest}
|
{@rest}
|
||||||
>
|
>
|
||||||
<option :if={@prompt} value=""><%= @prompt %></option>
|
<option :if={@prompt} value="">{@prompt}</option>
|
||||||
<%= Phoenix.HTML.Form.options_for_select(@options, @value) %>
|
{Phoenix.HTML.Form.options_for_select(@options, @value)}
|
||||||
</select>
|
</select>
|
||||||
<%= render_slot(@inner_block) %>
|
{render_slot(@inner_block)}
|
||||||
</div>
|
</div>
|
||||||
<.help :if={@help}><%= if @html_help, do: Phoenix.HTML.raw(@help), else: @help %></.help>
|
<.help :if={@help}>{if @html_help, do: Phoenix.HTML.raw(@help), else: @help}</.help>
|
||||||
<.error :for={msg <- @errors}><%= msg %></.error>
|
<.error :for={msg <- @errors}>{msg}</.error>
|
||||||
</div>
|
</div>
|
||||||
"""
|
"""
|
||||||
end
|
end
|
||||||
@@ -400,7 +400,7 @@ defmodule PinchflatWeb.CoreComponents do
|
|||||||
~H"""
|
~H"""
|
||||||
<div phx-feedback-for={@name}>
|
<div phx-feedback-for={@name}>
|
||||||
<.label for={@id}>
|
<.label for={@id}>
|
||||||
<%= @label %><span :if={@label_suffix} class="text-xs text-bodydark"><%= @label_suffix %></span>
|
{@label}<span :if={@label_suffix} class="text-xs text-bodydark">{@label_suffix}</span>
|
||||||
</.label>
|
</.label>
|
||||||
<textarea
|
<textarea
|
||||||
id={@id}
|
id={@id}
|
||||||
@@ -414,8 +414,8 @@ defmodule PinchflatWeb.CoreComponents do
|
|||||||
]}
|
]}
|
||||||
{@rest}
|
{@rest}
|
||||||
><%= Phoenix.HTML.Form.normalize_value("textarea", @value) %></textarea>
|
><%= Phoenix.HTML.Form.normalize_value("textarea", @value) %></textarea>
|
||||||
<.help :if={@help}><%= if @html_help, do: Phoenix.HTML.raw(@help), else: @help %></.help>
|
<.help :if={@help}>{if @html_help, do: Phoenix.HTML.raw(@help), else: @help}</.help>
|
||||||
<.error :for={msg <- @errors}><%= msg %></.error>
|
<.error :for={msg <- @errors}>{msg}</.error>
|
||||||
</div>
|
</div>
|
||||||
"""
|
"""
|
||||||
end
|
end
|
||||||
@@ -425,7 +425,7 @@ defmodule PinchflatWeb.CoreComponents do
|
|||||||
~H"""
|
~H"""
|
||||||
<div phx-feedback-for={@name}>
|
<div phx-feedback-for={@name}>
|
||||||
<.label for={@id}>
|
<.label for={@id}>
|
||||||
<%= @label %><span :if={@label_suffix} class="text-xs text-bodydark"><%= @label_suffix %></span>
|
{@label}<span :if={@label_suffix} class="text-xs text-bodydark">{@label_suffix}</span>
|
||||||
</.label>
|
</.label>
|
||||||
<div class="flex items-center">
|
<div class="flex items-center">
|
||||||
<input
|
<input
|
||||||
@@ -442,10 +442,10 @@ defmodule PinchflatWeb.CoreComponents do
|
|||||||
]}
|
]}
|
||||||
{@rest}
|
{@rest}
|
||||||
/>
|
/>
|
||||||
<%= render_slot(@input_append) %>
|
{render_slot(@input_append)}
|
||||||
</div>
|
</div>
|
||||||
<.help :if={@help}><%= if @html_help, do: Phoenix.HTML.raw(@help), else: @help %></.help>
|
<.help :if={@help}>{if @html_help, do: Phoenix.HTML.raw(@help), else: @help}</.help>
|
||||||
<.error :for={msg <- @errors}><%= msg %></.error>
|
<.error :for={msg <- @errors}>{msg}</.error>
|
||||||
</div>
|
</div>
|
||||||
"""
|
"""
|
||||||
end
|
end
|
||||||
@@ -458,7 +458,7 @@ defmodule PinchflatWeb.CoreComponents do
|
|||||||
def help(assigns) do
|
def help(assigns) do
|
||||||
~H"""
|
~H"""
|
||||||
<p class="mt-1 text-sm leading-5">
|
<p class="mt-1 text-sm leading-5">
|
||||||
<%= render_slot(@inner_block) %>
|
{render_slot(@inner_block)}
|
||||||
</p>
|
</p>
|
||||||
"""
|
"""
|
||||||
end
|
end
|
||||||
@@ -472,7 +472,7 @@ defmodule PinchflatWeb.CoreComponents do
|
|||||||
def label(assigns) do
|
def label(assigns) do
|
||||||
~H"""
|
~H"""
|
||||||
<label for={@for} class="mt-5 mb-2 inline-block text-md font-medium text-black dark:text-white">
|
<label for={@for} class="mt-5 mb-2 inline-block text-md font-medium text-black dark:text-white">
|
||||||
<%= render_slot(@inner_block) %>
|
{render_slot(@inner_block)}
|
||||||
</label>
|
</label>
|
||||||
"""
|
"""
|
||||||
end
|
end
|
||||||
@@ -486,7 +486,7 @@ defmodule PinchflatWeb.CoreComponents do
|
|||||||
~H"""
|
~H"""
|
||||||
<p class="mt-1 mb-5 flex gap-3 text-md leading-6 text-rose-600 phx-no-feedback:hidden">
|
<p class="mt-1 mb-5 flex gap-3 text-md leading-6 text-rose-600 phx-no-feedback:hidden">
|
||||||
<.icon name="hero-exclamation-circle-mini" class="mt-0.5 h-5 w-5 flex-none" />
|
<.icon name="hero-exclamation-circle-mini" class="mt-0.5 h-5 w-5 flex-none" />
|
||||||
<%= render_slot(@inner_block) %>
|
{render_slot(@inner_block)}
|
||||||
</p>
|
</p>
|
||||||
"""
|
"""
|
||||||
end
|
end
|
||||||
@@ -505,13 +505,13 @@ defmodule PinchflatWeb.CoreComponents do
|
|||||||
<header class={[@actions != [] && "flex items-center justify-between gap-6", @class]}>
|
<header class={[@actions != [] && "flex items-center justify-between gap-6", @class]}>
|
||||||
<div>
|
<div>
|
||||||
<h1 class="text-lg font-semibold leading-8 text-zinc-800">
|
<h1 class="text-lg font-semibold leading-8 text-zinc-800">
|
||||||
<%= render_slot(@inner_block) %>
|
{render_slot(@inner_block)}
|
||||||
</h1>
|
</h1>
|
||||||
<p :if={@subtitle != []} class="mt-2 text-sm leading-6 text-zinc-600">
|
<p :if={@subtitle != []} class="mt-2 text-sm leading-6 text-zinc-600">
|
||||||
<%= render_slot(@subtitle) %>
|
{render_slot(@subtitle)}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex-none"><%= render_slot(@actions) %></div>
|
<div class="flex-none">{render_slot(@actions)}</div>
|
||||||
</header>
|
</header>
|
||||||
"""
|
"""
|
||||||
end
|
end
|
||||||
@@ -551,9 +551,9 @@ defmodule PinchflatWeb.CoreComponents do
|
|||||||
<table class="w-[40rem] mt-11 sm:w-full">
|
<table class="w-[40rem] mt-11 sm:w-full">
|
||||||
<thead class="text-sm text-left leading-6 text-zinc-500">
|
<thead class="text-sm text-left leading-6 text-zinc-500">
|
||||||
<tr>
|
<tr>
|
||||||
<th :for={col <- @col} class="p-0 pb-4 pr-6 font-normal"><%= col[:label] %></th>
|
<th :for={col <- @col} class="p-0 pb-4 pr-6 font-normal">{col[:label]}</th>
|
||||||
<th :if={@action != []} class="relative p-0 pb-4">
|
<th :if={@action != []} class="relative p-0 pb-4">
|
||||||
<span class="sr-only"><%= gettext("Actions") %></span>
|
<span class="sr-only">{gettext("Actions")}</span>
|
||||||
</th>
|
</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
@@ -571,7 +571,7 @@ defmodule PinchflatWeb.CoreComponents do
|
|||||||
<div class="block py-4 pr-6">
|
<div class="block py-4 pr-6">
|
||||||
<span class="absolute -inset-y-px right-0 -left-4 group-hover:bg-zinc-50 sm:rounded-l-xl" />
|
<span class="absolute -inset-y-px right-0 -left-4 group-hover:bg-zinc-50 sm:rounded-l-xl" />
|
||||||
<span class={["relative", i == 0 && "font-semibold text-zinc-900"]}>
|
<span class={["relative", i == 0 && "font-semibold text-zinc-900"]}>
|
||||||
<%= render_slot(col, @row_item.(row)) %>
|
{render_slot(col, @row_item.(row))}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
@@ -579,7 +579,7 @@ defmodule PinchflatWeb.CoreComponents do
|
|||||||
<div class="relative whitespace-nowrap py-4 text-right text-sm font-medium">
|
<div class="relative whitespace-nowrap py-4 text-right text-sm font-medium">
|
||||||
<span class="absolute -inset-y-px -right-4 left-0 group-hover:bg-zinc-50 sm:rounded-r-xl" />
|
<span class="absolute -inset-y-px -right-4 left-0 group-hover:bg-zinc-50 sm:rounded-r-xl" />
|
||||||
<span :for={action <- @action} class="relative ml-4 font-semibold leading-6 text-zinc-900 hover:text-zinc-700">
|
<span :for={action <- @action} class="relative ml-4 font-semibold leading-6 text-zinc-900 hover:text-zinc-700">
|
||||||
<%= render_slot(action, @row_item.(row)) %>
|
{render_slot(action, @row_item.(row))}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
@@ -608,8 +608,8 @@ defmodule PinchflatWeb.CoreComponents do
|
|||||||
<div class="mt-2 mb-14">
|
<div class="mt-2 mb-14">
|
||||||
<dl class="-my-4 divide-y dark:divide-strokedark">
|
<dl class="-my-4 divide-y dark:divide-strokedark">
|
||||||
<div :for={item <- @item} class="flex gap-4 py-4 text-sm leading-6 sm:gap-8">
|
<div :for={item <- @item} class="flex gap-4 py-4 text-sm leading-6 sm:gap-8">
|
||||||
<dt class="w-1/4 flex-none dark:text-white"><%= item.title %></dt>
|
<dt class="w-1/4 flex-none dark:text-white">{item.title}</dt>
|
||||||
<dd class="dark:text-white"><%= render_slot(item) %></dd>
|
<dd class="dark:text-white">{render_slot(item)}</dd>
|
||||||
</div>
|
</div>
|
||||||
</dl>
|
</dl>
|
||||||
</div>
|
</div>
|
||||||
@@ -644,12 +644,12 @@ defmodule PinchflatWeb.CoreComponents do
|
|||||||
~H"""
|
~H"""
|
||||||
<ul>
|
<ul>
|
||||||
<li :for={{k, v} <- @iterable_attributes} class="mb-2 w-2/3">
|
<li :for={{k, v} <- @iterable_attributes} class="mb-2 w-2/3">
|
||||||
<strong><%= k %>:</strong>
|
<strong>{k}:</strong>
|
||||||
<code class="inline-block text-sm font-mono text-gray p-0.5 mx-0.5">
|
<code class="inline-block text-sm font-mono text-gray p-0.5 mx-0.5">
|
||||||
<%= if is_binary(v) && URI.parse(v).scheme && URI.parse(v).scheme =~ "http" do %>
|
<%= if is_binary(v) && URI.parse(v).scheme && URI.parse(v).scheme =~ "http" do %>
|
||||||
<TextComponents.inline_link href={v}><%= v %></TextComponents.inline_link>
|
<TextComponents.inline_link href={v}>{v}</TextComponents.inline_link>
|
||||||
<% else %>
|
<% else %>
|
||||||
<%= v %>
|
{v}
|
||||||
<% end %>
|
<% end %>
|
||||||
</code>
|
</code>
|
||||||
</li>
|
</li>
|
||||||
@@ -672,7 +672,7 @@ defmodule PinchflatWeb.CoreComponents do
|
|||||||
<div class="mt-16">
|
<div class="mt-16">
|
||||||
<.link href={@href} class="text-sm font-semibold leading-6 text-zinc-900 hover:text-zinc-700">
|
<.link href={@href} class="text-sm font-semibold leading-6 text-zinc-900 hover:text-zinc-700">
|
||||||
<.icon name="hero-arrow-left-solid" class="h-3 w-3" />
|
<.icon name="hero-arrow-left-solid" class="h-3 w-3" />
|
||||||
<%= render_slot(@inner_block) %>
|
{render_slot(@inner_block)}
|
||||||
</.link>
|
</.link>
|
||||||
</div>
|
</div>
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ defmodule PinchflatWeb.CustomComponents.ButtonComponents do
|
|||||||
~H"""
|
~H"""
|
||||||
<button
|
<button
|
||||||
class={[
|
class={[
|
||||||
"text-center font-medium text-white",
|
"text-center font-medium text-white whitespace-nowrap",
|
||||||
"#{@rounding} inline-flex items-center justify-center px-8 py-4",
|
"#{@rounding} inline-flex items-center justify-center px-8 py-4",
|
||||||
"#{@color}",
|
"#{@color}",
|
||||||
"hover:bg-opacity-90 lg:px-8 xl:px-10",
|
"hover:bg-opacity-90 lg:px-8 xl:px-10",
|
||||||
@@ -37,7 +37,7 @@ defmodule PinchflatWeb.CustomComponents.ButtonComponents do
|
|||||||
disabled={@disabled}
|
disabled={@disabled}
|
||||||
{@rest}
|
{@rest}
|
||||||
>
|
>
|
||||||
<%= render_slot(@inner_block) %>
|
{render_slot(@inner_block)}
|
||||||
</button>
|
</button>
|
||||||
"""
|
"""
|
||||||
end
|
end
|
||||||
@@ -66,7 +66,7 @@ defmodule PinchflatWeb.CustomComponents.ButtonComponents do
|
|||||||
"font-medium text-white hover:bg-opacity-95"
|
"font-medium text-white hover:bg-opacity-95"
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
<%= @text %>
|
{@text}
|
||||||
<CoreComponents.icon
|
<CoreComponents.icon
|
||||||
name="hero-chevron-down"
|
name="hero-chevron-down"
|
||||||
class="fill-current duration-200 ease-linear mt-1"
|
class="fill-current duration-200 ease-linear mt-1"
|
||||||
@@ -81,7 +81,7 @@ defmodule PinchflatWeb.CustomComponents.ButtonComponents do
|
|||||||
<ul class="flex flex-col">
|
<ul class="flex flex-col">
|
||||||
<li :for={option <- @option}>
|
<li :for={option <- @option}>
|
||||||
<span class="flex px-5 py-2 font-medium text-bodydark2 hover:text-white cursor-pointer">
|
<span class="flex px-5 py-2 font-medium text-bodydark2 hover:text-white cursor-pointer">
|
||||||
<%= render_slot(option) %>
|
{render_slot(option)}
|
||||||
</span>
|
</span>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
@@ -126,7 +126,7 @@ defmodule PinchflatWeb.CustomComponents.ButtonComponents do
|
|||||||
>
|
>
|
||||||
<span class="border-light absolute -top-1 left-1/2 -z-10 h-2 w-2 -translate-x-1/2 rotate-45 rounded-sm bg-meta-4">
|
<span class="border-light absolute -top-1 left-1/2 -z-10 h-2 w-2 -translate-x-1/2 rotate-45 rounded-sm bg-meta-4">
|
||||||
</span>
|
</span>
|
||||||
<span><%= @tooltip %></span>
|
<span>{@tooltip}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -34,16 +34,16 @@ defmodule PinchflatWeb.CustomComponents.TabComponents do
|
|||||||
x-bind:class={"openTab === '#{tab.id}' ? activeClasses : inactiveClasses"}
|
x-bind:class={"openTab === '#{tab.id}' ? activeClasses : inactiveClasses"}
|
||||||
class="border-b-2 py-4 w-full sm:w-fit text-sm font-medium hover:text-meta-5 md:text-base"
|
class="border-b-2 py-4 w-full sm:w-fit text-sm font-medium hover:text-meta-5 md:text-base"
|
||||||
>
|
>
|
||||||
<span class="text-xl"><%= tab.title %></span>
|
<span class="text-xl">{tab.title}</span>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<div class="mx-4 my-4 lg:my-0 flex gap-5 sm:gap-10 items-center">
|
<div class="mx-4 my-4 lg:my-0 flex gap-5 sm:gap-10 items-center">
|
||||||
<%= render_slot(@tab_append) %>
|
{render_slot(@tab_append)}
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
<div class="mt-4 min-h-60">
|
<div class="mt-4 min-h-60">
|
||||||
<div :for={tab <- @tab} x-show={"openTab === '#{tab.id}'"} class="font-medium leading-relaxed">
|
<div :for={tab <- @tab} x-show={"openTab === '#{tab.id}'"} class="font-medium leading-relaxed">
|
||||||
<%= render_slot(tab) %>
|
{render_slot(tab)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -16,6 +16,8 @@ defmodule PinchflatWeb.CustomComponents.TableComponents do
|
|||||||
"""
|
"""
|
||||||
attr :rows, :list, required: true
|
attr :rows, :list, required: true
|
||||||
attr :table_class, :string, default: ""
|
attr :table_class, :string, default: ""
|
||||||
|
attr :sort_key, :string, default: nil
|
||||||
|
attr :sort_direction, :string, default: nil
|
||||||
|
|
||||||
attr :row_item, :any,
|
attr :row_item, :any,
|
||||||
default: &Function.identity/1,
|
default: &Function.identity/1,
|
||||||
@@ -24,6 +26,7 @@ defmodule PinchflatWeb.CustomComponents.TableComponents do
|
|||||||
slot :col, required: true do
|
slot :col, required: true do
|
||||||
attr :label, :string
|
attr :label, :string
|
||||||
attr :class, :string
|
attr :class, :string
|
||||||
|
attr :sort_key, :string
|
||||||
end
|
end
|
||||||
|
|
||||||
def table(assigns) do
|
def table(assigns) do
|
||||||
@@ -31,8 +34,20 @@ defmodule PinchflatWeb.CustomComponents.TableComponents do
|
|||||||
<table class={["w-full table-auto bg-boxdark", @table_class]}>
|
<table class={["w-full table-auto bg-boxdark", @table_class]}>
|
||||||
<thead>
|
<thead>
|
||||||
<tr class="text-left bg-meta-4">
|
<tr class="text-left bg-meta-4">
|
||||||
<th :for={col <- @col} class="px-4 py-4 font-medium text-white xl:pl-11">
|
<th
|
||||||
<%= col[:label] %>
|
:for={col <- @col}
|
||||||
|
class={["px-4 py-4 font-medium text-white", col[:sort_key] && "cursor-pointer"]}
|
||||||
|
phx-click={col[:sort_key] && "sort_update"}
|
||||||
|
phx-value-sort_key={col[:sort_key]}
|
||||||
|
>
|
||||||
|
<div class="relative">
|
||||||
|
{col[:label]}
|
||||||
|
<.icon
|
||||||
|
:if={to_string(@sort_key) == col[:sort_key]}
|
||||||
|
name={if @sort_direction == :asc, do: "hero-chevron-up", else: "hero-chevron-down"}
|
||||||
|
class="w-3 h-3 mt-2 ml-1 absolute"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</th>
|
</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
@@ -41,11 +56,11 @@ defmodule PinchflatWeb.CustomComponents.TableComponents do
|
|||||||
<td
|
<td
|
||||||
:for={col <- @col}
|
:for={col <- @col}
|
||||||
class={[
|
class={[
|
||||||
"px-4 py-5 pl-9 xl:pl-11",
|
"px-4 py-5",
|
||||||
col[:class]
|
col[:class]
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
<%= render_slot(col, @row_item.(row)) %>
|
{render_slot(col, @row_item.(row))}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
@@ -70,9 +85,9 @@ defmodule PinchflatWeb.CustomComponents.TableComponents do
|
|||||||
<li>
|
<li>
|
||||||
<span
|
<span
|
||||||
class={[
|
class={[
|
||||||
"flex h-8 w-8 items-center justify-center rounded",
|
"pagination-prev h-8 w-8 items-center justify-center rounded",
|
||||||
@page_number != 1 && "cursor-pointer hover:bg-primary hover:text-white",
|
@page_number != 1 && "cursor-pointer hover:bg-primary hover:text-white",
|
||||||
@page_number == 1 && "cursor-not-allowed"
|
@page_number <= 1 && "cursor-not-allowed"
|
||||||
]}
|
]}
|
||||||
phx-click={@page_number != 1 && "page_change"}
|
phx-click={@page_number != 1 && "page_change"}
|
||||||
phx-value-direction="dec"
|
phx-value-direction="dec"
|
||||||
@@ -88,9 +103,9 @@ defmodule PinchflatWeb.CustomComponents.TableComponents do
|
|||||||
<li>
|
<li>
|
||||||
<span
|
<span
|
||||||
class={[
|
class={[
|
||||||
"flex h-8 w-8 items-center justify-center rounded",
|
"pagination-next flex h-8 w-8 items-center justify-center rounded",
|
||||||
@page_number != @total_pages && "cursor-pointer hover:bg-primary hover:text-white",
|
@page_number != @total_pages && "cursor-pointer hover:bg-primary hover:text-white",
|
||||||
@page_number == @total_pages && "cursor-not-allowed"
|
@page_number >= @total_pages && "cursor-not-allowed"
|
||||||
]}
|
]}
|
||||||
phx-click={@page_number != @total_pages && "page_change"}
|
phx-click={@page_number != @total_pages && "page_change"}
|
||||||
phx-value-direction="inc"
|
phx-value-direction="inc"
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ defmodule PinchflatWeb.CustomComponents.TextComponents do
|
|||||||
@moduledoc false
|
@moduledoc false
|
||||||
use Phoenix.Component
|
use Phoenix.Component
|
||||||
|
|
||||||
|
alias Pinchflat.Utils.NumberUtils
|
||||||
alias PinchflatWeb.CoreComponents
|
alias PinchflatWeb.CoreComponents
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
@@ -12,7 +13,7 @@ defmodule PinchflatWeb.CustomComponents.TextComponents do
|
|||||||
def inline_code(assigns) do
|
def inline_code(assigns) do
|
||||||
~H"""
|
~H"""
|
||||||
<code class="inline-block text-sm font-mono text-gray bg-boxdark rounded-md p-0.5 mx-0.5 text-nowrap">
|
<code class="inline-block text-sm font-mono text-gray bg-boxdark rounded-md p-0.5 mx-0.5 text-nowrap">
|
||||||
<%= render_slot(@inner_block) %>
|
{render_slot(@inner_block)}
|
||||||
</code>
|
</code>
|
||||||
"""
|
"""
|
||||||
end
|
end
|
||||||
@@ -26,7 +27,7 @@ defmodule PinchflatWeb.CustomComponents.TextComponents do
|
|||||||
def inline_link(assigns) do
|
def inline_link(assigns) do
|
||||||
~H"""
|
~H"""
|
||||||
<.link href={@href} target="_blank" class="text-blue-500 hover:text-blue-300">
|
<.link href={@href} target="_blank" class="text-blue-500 hover:text-blue-300">
|
||||||
<%= render_slot(@inner_block) %>
|
{render_slot(@inner_block)}
|
||||||
</.link>
|
</.link>
|
||||||
"""
|
"""
|
||||||
end
|
end
|
||||||
@@ -41,7 +42,7 @@ defmodule PinchflatWeb.CustomComponents.TextComponents do
|
|||||||
def subtle_link(assigns) do
|
def subtle_link(assigns) do
|
||||||
~H"""
|
~H"""
|
||||||
<.link href={@href} target={@target} class="underline decoration-bodydark decoration-1 hover:decoration-white">
|
<.link href={@href} target={@target} class="underline decoration-bodydark decoration-1 hover:decoration-white">
|
||||||
<%= render_slot(@inner_block) %>
|
{render_slot(@inner_block)}
|
||||||
</.link>
|
</.link>
|
||||||
"""
|
"""
|
||||||
end
|
end
|
||||||
@@ -82,7 +83,7 @@ defmodule PinchflatWeb.CustomComponents.TextComponents do
|
|||||||
assigns = Map.put(assigns, :text, formatted_text)
|
assigns = Map.put(assigns, :text, formatted_text)
|
||||||
|
|
||||||
~H"""
|
~H"""
|
||||||
<span><%= @text %></span>
|
<span>{@text}</span>
|
||||||
"""
|
"""
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -98,7 +99,7 @@ defmodule PinchflatWeb.CustomComponents.TextComponents do
|
|||||||
assigns = Map.put(assigns, :timezone, timezone)
|
assigns = Map.put(assigns, :timezone, timezone)
|
||||||
|
|
||||||
~H"""
|
~H"""
|
||||||
<time><%= Calendar.strftime(Timex.Timezone.convert(@datetime, @timezone), @format) %></time>
|
<time>{Calendar.strftime(Timex.Timezone.convert(@datetime, @timezone), @format)}</time>
|
||||||
"""
|
"""
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -109,7 +110,7 @@ defmodule PinchflatWeb.CustomComponents.TextComponents do
|
|||||||
|
|
||||||
def localized_number(assigns) do
|
def localized_number(assigns) do
|
||||||
~H"""
|
~H"""
|
||||||
<span x-data x-text={"Intl.NumberFormat().format(#{@number})"}><%= @number %></span>
|
<span x-data x-text={"Intl.NumberFormat().format(#{@number})"}>{@number}</span>
|
||||||
"""
|
"""
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -122,7 +123,27 @@ defmodule PinchflatWeb.CustomComponents.TextComponents do
|
|||||||
|
|
||||||
def pluralize(assigns) do
|
def pluralize(assigns) do
|
||||||
~H"""
|
~H"""
|
||||||
<%= @word %><%= if @count == 1, do: "", else: @suffix %>
|
{@word}{if @count == 1, do: "", else: @suffix}
|
||||||
|
"""
|
||||||
|
end
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Renders a human-readable byte size
|
||||||
|
"""
|
||||||
|
|
||||||
|
attr :byte_size, :integer, required: true
|
||||||
|
|
||||||
|
def readable_filesize(assigns) do
|
||||||
|
{num, suffix} = NumberUtils.human_byte_size(assigns.byte_size, precision: 2)
|
||||||
|
|
||||||
|
assigns =
|
||||||
|
Map.merge(assigns, %{
|
||||||
|
num: num,
|
||||||
|
suffix: suffix
|
||||||
|
})
|
||||||
|
|
||||||
|
~H"""
|
||||||
|
<.localized_number number={@num} /> {@suffix}
|
||||||
"""
|
"""
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -61,7 +61,7 @@ defmodule PinchflatWeb.Layouts do
|
|||||||
x-on:click="selected = !selected"
|
x-on:click="selected = !selected"
|
||||||
>
|
>
|
||||||
<span class="flex items-center gap-2.5">
|
<span class="flex items-center gap-2.5">
|
||||||
<.icon name={@icon} /> <%= @text %>
|
<.icon name={@icon} /> {@text}
|
||||||
</span>
|
</span>
|
||||||
<span class="text-bodydark2">
|
<span class="text-bodydark2">
|
||||||
<.icon name="hero-chevron-down" x-bind:class="{ 'rotate-180': selected }" />
|
<.icon name="hero-chevron-down" x-bind:class="{ 'rotate-180': selected }" />
|
||||||
@@ -103,7 +103,7 @@ defmodule PinchflatWeb.Layouts do
|
|||||||
@class
|
@class
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
<.icon :if={@icon} name={@icon} /> <%= @text %>
|
<.icon :if={@icon} name={@icon} /> {@text}
|
||||||
</.link>
|
</.link>
|
||||||
"""
|
"""
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
<main>
|
<main>
|
||||||
<div class="mx-auto max-w-screen-2xl p-4 md:p-6 2xl:p-10">
|
<div class="mx-auto max-w-screen-2xl p-4 md:p-6 2xl:p-10">
|
||||||
<.flash_group flash={@flash} />
|
<.flash_group flash={@flash} />
|
||||||
<%= @inner_content %>
|
{@inner_content}
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -11,7 +11,7 @@
|
|||||||
<main>
|
<main>
|
||||||
<div class="mx-auto max-w-screen-2xl p-4 md:p-6 2xl:p-10">
|
<div class="mx-auto max-w-screen-2xl p-4 md:p-6 2xl:p-10">
|
||||||
<.flash_group flash={@flash} />
|
<.flash_group flash={@flash} />
|
||||||
<%= @inner_content %>
|
{@inner_content}
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -67,7 +67,7 @@
|
|||||||
class="group relative flex items-center gap-2.5 px-4 pt-2 text-sm"
|
class="group relative flex items-center gap-2.5 px-4 pt-2 text-sm"
|
||||||
x-on:click={"markVersionAsSeen('#{Application.spec(:pinchflat)[:vsn]}')"}
|
x-on:click={"markVersionAsSeen('#{Application.spec(:pinchflat)[:vsn]}')"}
|
||||||
>
|
>
|
||||||
<span>Pinchflat <%= Application.spec(:pinchflat)[:vsn] %></span>
|
<span>Pinchflat {Application.spec(:pinchflat)[:vsn]}</span>
|
||||||
<a
|
<a
|
||||||
href="https://github.com/kieraneglin/pinchflat/releases"
|
href="https://github.com/kieraneglin/pinchflat/releases"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
@@ -79,7 +79,7 @@
|
|||||||
</a>
|
</a>
|
||||||
</span>
|
</span>
|
||||||
<span class="group relative flex items-center gap-2.5 px-4 pt-2 text-sm">
|
<span class="group relative flex items-center gap-2.5 px-4 pt-2 text-sm">
|
||||||
yt-dlp <%= Settings.get!(:yt_dlp_version) %>
|
yt-dlp {Settings.get!(:yt_dlp_version)}
|
||||||
</span>
|
</span>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|||||||
@@ -25,6 +25,6 @@
|
|||||||
into the text box and press the button.
|
into the text box and press the button.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<%= live_render(@conn, Pinchflat.UpgradeButtonLive) %>
|
{live_render(@conn, Pinchflat.UpgradeButtonLive)}
|
||||||
</section>
|
</section>
|
||||||
</.modal>
|
</.modal>
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
<meta name="csrf-token" content={get_csrf_token()} />
|
<meta name="csrf-token" content={get_csrf_token()} />
|
||||||
<.live_title>
|
<.live_title>
|
||||||
<%= assigns[:page_title] || "Pinchflat" %>
|
{assigns[:page_title] || "Pinchflat"}
|
||||||
</.live_title>
|
</.live_title>
|
||||||
<link phx-track-static rel="stylesheet" href={~p"/assets/app.css"} />
|
<link phx-track-static rel="stylesheet" href={~p"/assets/app.css"} />
|
||||||
<link rel="icon" type="image/x-icon" href={~p"/favicon.ico?cachebust=2024-03-20"} />
|
<link rel="icon" type="image/x-icon" href={~p"/favicon.ico?cachebust=2024-03-20"} />
|
||||||
@@ -21,7 +21,7 @@
|
|||||||
class="dark text-bodydark bg-boxdark-2"
|
class="dark text-bodydark bg-boxdark-2"
|
||||||
data-socket-path={Path.join(Application.get_env(:pinchflat, :base_route_path), "/live")}
|
data-socket-path={Path.join(Application.get_env(:pinchflat, :base_route_path), "/live")}
|
||||||
>
|
>
|
||||||
<%= @inner_content %>
|
{@inner_content}
|
||||||
|
|
||||||
<.donate_modal conn={@conn} />
|
<.donate_modal conn={@conn} />
|
||||||
<template x-if="!proEnabled && !onboarding">
|
<template x-if="!proEnabled && !onboarding">
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<div class="mb-6 flex gap-3 flex-row items-center">
|
<div class="mb-6 flex gap-3 flex-row items-center">
|
||||||
<h2 class="text-title-md2 font-bold text-black dark:text-white ml-4">
|
<h2 class="text-title-md2 font-bold text-black dark:text-white ml-4 truncate">
|
||||||
Editing "<%= StringUtils.truncate(@media_item.title, 35) %>"
|
Editing "{@media_item.title}"
|
||||||
</h2>
|
</h2>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
<div class="mb-6 flex gap-3 flex-row items-center justify-between">
|
<div class="mb-6 flex gap-3 flex-row items-center justify-between">
|
||||||
<div class="flex gap-3 items-center">
|
<div class="flex gap-3 items-center min-w-0">
|
||||||
<.link href={~p"/sources/#{@media_item.source_id}"}>
|
<.link href={~p"/sources/#{@media_item.source_id}"}>
|
||||||
<.icon name="hero-arrow-left" class="w-10 h-10 hover:dark:text-white" />
|
<.icon name="hero-arrow-left" class="w-10 h-10 hover:dark:text-white" />
|
||||||
</.link>
|
</.link>
|
||||||
<h2 class="text-title-md2 font-bold text-black dark:text-white ml-4">
|
<h2 class="text-title-md2 font-bold text-black dark:text-white ml-4 truncate">
|
||||||
<%= StringUtils.truncate(@media_item.title, 35) %>
|
{@media_item.title}
|
||||||
</h2>
|
</h2>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -31,8 +31,8 @@
|
|||||||
<.media_preview media_item={@media_item} />
|
<.media_preview media_item={@media_item} />
|
||||||
</div>
|
</div>
|
||||||
<aside class="mt-4">
|
<aside class="mt-4">
|
||||||
<h2 class="text-xl mb-2"><%= @media_item.title %></h2>
|
<h2 class="text-xl mb-2">{@media_item.title}</h2>
|
||||||
<div>Uploaded: <%= DateTime.to_date(@media_item.uploaded_at) %></div>
|
<div>Uploaded: {DateTime.to_date(@media_item.uploaded_at)}</div>
|
||||||
<div>
|
<div>
|
||||||
<span :if={URI.parse(@media_item.original_url).scheme =~ "http"}>
|
<span :if={URI.parse(@media_item.original_url).scheme =~ "http"}>
|
||||||
<.subtle_link href={@media_item.original_url} target="_blank">Open Original</.subtle_link>
|
<.subtle_link href={@media_item.original_url} target="_blank">Open Original</.subtle_link>
|
||||||
@@ -58,7 +58,7 @@
|
|||||||
<section>
|
<section>
|
||||||
<strong>Source:</strong>
|
<strong>Source:</strong>
|
||||||
<.subtle_link href={~p"/sources/#{@media_item.source_id}"}>
|
<.subtle_link href={~p"/sources/#{@media_item.source_id}"}>
|
||||||
<%= @media_item.source.custom_name %>
|
{@media_item.source.custom_name}
|
||||||
</.subtle_link>
|
</.subtle_link>
|
||||||
<.list_items_from_map map={Map.from_struct(@media_item)} />
|
<.list_items_from_map map={Map.from_struct(@media_item)} />
|
||||||
</section>
|
</section>
|
||||||
@@ -68,10 +68,10 @@
|
|||||||
<%= if match?([_|_], @media_item.tasks) do %>
|
<%= if match?([_|_], @media_item.tasks) do %>
|
||||||
<.table rows={@media_item.tasks} table_class="text-black dark:text-white">
|
<.table rows={@media_item.tasks} table_class="text-black dark:text-white">
|
||||||
<:col :let={task} label="Worker">
|
<:col :let={task} label="Worker">
|
||||||
<%= task.job.worker %>
|
{task.job.worker}
|
||||||
</:col>
|
</:col>
|
||||||
<:col :let={task} label="State">
|
<:col :let={task} label="State">
|
||||||
<%= task.job.state %>
|
{task.job.state}
|
||||||
</:col>
|
</:col>
|
||||||
<:col :let={task} label="Scheduled At">
|
<:col :let={task} label="Scheduled At">
|
||||||
<.datetime_in_zone datetime={task.job.scheduled_at} />
|
<.datetime_in_zone datetime={task.job.scheduled_at} />
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ defmodule PinchflatWeb.MediaProfiles.MediaProfileHTML do
|
|||||||
[
|
[
|
||||||
{"8k", "4320p"},
|
{"8k", "4320p"},
|
||||||
{"4k", "2160p"},
|
{"4k", "2160p"},
|
||||||
|
{"1440p", "1440p"},
|
||||||
{"1080p", "1080p"},
|
{"1080p", "1080p"},
|
||||||
{"720p", "720p"},
|
{"720p", "720p"},
|
||||||
{"480p", "480p"},
|
{"480p", "480p"},
|
||||||
@@ -37,6 +38,7 @@ defmodule PinchflatWeb.MediaProfiles.MediaProfileHTML do
|
|||||||
def friendly_sponsorblock_options do
|
def friendly_sponsorblock_options do
|
||||||
[
|
[
|
||||||
{"Disabled (default)", "disabled"},
|
{"Disabled (default)", "disabled"},
|
||||||
|
{"Mark Segments as Chapters", "mark"},
|
||||||
{"Remove Segments", "remove"}
|
{"Remove Segments", "remove"}
|
||||||
]
|
]
|
||||||
end
|
end
|
||||||
@@ -55,7 +57,7 @@ defmodule PinchflatWeb.MediaProfiles.MediaProfileHTML do
|
|||||||
end
|
end
|
||||||
|
|
||||||
def media_center_custom_output_template_options do
|
def media_center_custom_output_template_options do
|
||||||
%{
|
[
|
||||||
season_by_year__episode_by_date: "<code>Season YYYY/sYYYYeMMDD</code>",
|
season_by_year__episode_by_date: "<code>Season YYYY/sYYYYeMMDD</code>",
|
||||||
season_by_year__episode_by_date_and_index:
|
season_by_year__episode_by_date_and_index:
|
||||||
"same as the above but it handles dates better. <strong>This is the recommended option</strong>",
|
"same as the above but it handles dates better. <strong>This is the recommended option</strong>",
|
||||||
@@ -63,11 +65,11 @@ defmodule PinchflatWeb.MediaProfiles.MediaProfileHTML do
|
|||||||
"<code>Season 1/s01eXX</code> where <code>XX</code> is the video's position in the playlist. Only recommended for playlists (not channels) that don't change",
|
"<code>Season 1/s01eXX</code> where <code>XX</code> is the video's position in the playlist. Only recommended for playlists (not channels) that don't change",
|
||||||
static_season__episode_by_date:
|
static_season__episode_by_date:
|
||||||
"<code>Season 1/s01eYYMMDD</code>. Recommended for playlists that might change or where order isn't important"
|
"<code>Season 1/s01eYYMMDD</code>. Recommended for playlists that might change or where order isn't important"
|
||||||
}
|
]
|
||||||
end
|
end
|
||||||
|
|
||||||
def other_custom_output_template_options do
|
def other_custom_output_template_options do
|
||||||
%{
|
[
|
||||||
upload_day: nil,
|
upload_day: nil,
|
||||||
upload_month: nil,
|
upload_month: nil,
|
||||||
upload_year: nil,
|
upload_year: nil,
|
||||||
@@ -83,8 +85,11 @@ defmodule PinchflatWeb.MediaProfiles.MediaProfileHTML do
|
|||||||
season_episode_index_from_date:
|
season_episode_index_from_date:
|
||||||
"the upload date formatted as <code>sYYYYeMMDDII</code> where <code>II</code> is an index to prevent date collisions",
|
"the upload date formatted as <code>sYYYYeMMDDII</code> where <code>II</code> is an index to prevent date collisions",
|
||||||
media_playlist_index:
|
media_playlist_index:
|
||||||
"the place of the media item in the playlist. Do not use with channels. May not work if the playlist is updated"
|
"the place of the media item in the playlist. Do not use with channels. May not work if the playlist is updated",
|
||||||
}
|
media_item_id: "the ID of the media item in Pinchflat's database",
|
||||||
|
source_id: "the ID of the source in Pinchflat's database",
|
||||||
|
media_profile_id: "the ID of the media profile in Pinchflat's database"
|
||||||
|
]
|
||||||
end
|
end
|
||||||
|
|
||||||
def common_output_template_options do
|
def common_output_template_options do
|
||||||
|
|||||||
@@ -2,8 +2,8 @@
|
|||||||
<.link href={~p"/media_profiles"}>
|
<.link href={~p"/media_profiles"}>
|
||||||
<.icon name="hero-arrow-left" class="w-10 h-10 hover:dark:text-white" />
|
<.icon name="hero-arrow-left" class="w-10 h-10 hover:dark:text-white" />
|
||||||
</.link>
|
</.link>
|
||||||
<h2 class="text-title-md2 font-bold text-black dark:text-white ml-4">
|
<h2 class="text-title-md2 font-bold text-black dark:text-white ml-4 truncate">
|
||||||
Editing "<%= @media_profile.name %>"
|
Editing "{@media_profile.name}"
|
||||||
</h2>
|
</h2>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -14,13 +14,13 @@
|
|||||||
<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={@media_profiles} table_class="text-black dark:text-white">
|
<.table rows={@media_profiles} table_class="text-black dark:text-white">
|
||||||
<:col :let={media_profile} label="Name">
|
<:col :let={media_profile} label="Name" class="truncate max-w-xs">
|
||||||
<.subtle_link href={~p"/media_profiles/#{media_profile.id}"}>
|
<.subtle_link href={~p"/media_profiles/#{media_profile.id}"}>
|
||||||
<%= media_profile.name %>
|
{media_profile.name}
|
||||||
</.subtle_link>
|
</.subtle_link>
|
||||||
</:col>
|
</:col>
|
||||||
<: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">
|
<:col :let={media_profile} label="Sources">
|
||||||
<.subtle_link href={~p"/media_profiles/#{media_profile.id}/#tab-sources"}>
|
<.subtle_link href={~p"/media_profiles/#{media_profile.id}/#tab-sources"}>
|
||||||
|
|||||||
+14
-10
@@ -6,7 +6,9 @@
|
|||||||
<ul class="list-disc list-inside ml-2 md:ml-5">
|
<ul class="list-disc list-inside ml-2 md:ml-5">
|
||||||
<li>
|
<li>
|
||||||
Liquid-style:
|
Liquid-style:
|
||||||
<.inline_code>/{{ channel }}/{{ title }} - {{ id }}.{{ ext }}</.inline_code>
|
<.inline_code>
|
||||||
|
{"/#{double_brace("channel")}/#{double_brace("title")} - #{double_brace("id")}.#{double_brace("ext")}"}
|
||||||
|
</.inline_code>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<code class="text-sm">yt-dlp</code>-style
|
<code class="text-sm">yt-dlp</code>-style
|
||||||
@@ -17,7 +19,7 @@
|
|||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
Any bare words:
|
Any bare words:
|
||||||
<.inline_code>/videos/1080p/{{ id }}.{{ ext }}</.inline_code>
|
<.inline_code>{"/videos/1080p/#{double_brace("id")}.#{double_brace("ext")}"}</.inline_code>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
<p class="my-2">
|
<p class="my-2">
|
||||||
@@ -25,7 +27,7 @@
|
|||||||
<em>any</em>
|
<em>any</em>
|
||||||
single-word <code class="text-sm">yt-dlp</code>
|
single-word <code class="text-sm">yt-dlp</code>
|
||||||
option can be used as liquid-style and it's automatically made filepath-safe. For example, the
|
option can be used as liquid-style and it's automatically made filepath-safe. For example, the
|
||||||
<.inline_code>{{ duration }}</.inline_code>
|
<.inline_code>{double_brace("duration")}</.inline_code>
|
||||||
option is translated to
|
option is translated to
|
||||||
<.inline_code>%(duration)S</.inline_code>
|
<.inline_code>%(duration)S</.inline_code>
|
||||||
</p>
|
</p>
|
||||||
@@ -33,12 +35,14 @@
|
|||||||
<strong>Major 🔑:</strong>
|
<strong>Major 🔑:</strong>
|
||||||
these syntaxes can be mixed and matched freely! I prefer to use liquid-style and bare words
|
these syntaxes can be mixed and matched freely! I prefer to use liquid-style and bare words
|
||||||
but I'll include <code class="text-sm">yt-dlp</code>-style when I need more control. For example:
|
but I'll include <code class="text-sm">yt-dlp</code>-style when I need more control. For example:
|
||||||
<.inline_code>/1080p/{{ channel }}/{{ title }}-(%(subtitles.en.-1.ext)s).{{ ext }}</.inline_code>
|
<.inline_code>
|
||||||
|
{"/1080p/#{double_brace("channel")}/#{double_brace("title")}-(%(subtitles.en.-1.ext)s).#{double_brace("ext")}"}
|
||||||
|
</.inline_code>
|
||||||
</p>
|
</p>
|
||||||
<p class="my-2">
|
<p class="my-2">
|
||||||
<strong>NOTE:</strong>
|
<strong>NOTE:</strong>
|
||||||
Your template <em>must</em>
|
Your template <em>must</em>
|
||||||
end with an extension option (<.inline_code>.{{ ext }}</.inline_code>
|
end with an extension option (<.inline_code>.{double_brace("ext")}</.inline_code>
|
||||||
or
|
or
|
||||||
<.inline_code>.%(ext)S</.inline_code>).
|
<.inline_code>.%(ext)S</.inline_code>).
|
||||||
Downloading won't work as expected without it.
|
Downloading won't work as expected without it.
|
||||||
@@ -59,21 +63,21 @@
|
|||||||
<h3 class="text-lg font-bold my-2">Media Center Custom Aliases</h3>
|
<h3 class="text-lg font-bold my-2">Media Center Custom Aliases</h3>
|
||||||
<ul class="list-disc list-inside mb-4 ml-2 md:ml-5 max-w-prose">
|
<ul class="list-disc list-inside mb-4 ml-2 md:ml-5 max-w-prose">
|
||||||
<li :for={{k, v} <- media_center_custom_output_template_options()} class="mt-1">
|
<li :for={{k, v} <- media_center_custom_output_template_options()} class="mt-1">
|
||||||
<.inline_code>{{ <%= k %> }}</.inline_code>
|
<.inline_code>{double_brace(k)}</.inline_code>
|
||||||
<span :if={v}>- <%= html_escape({:safe, v}) %></span>
|
<span :if={v}>- {html_escape({:safe, v})}</span>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
<h3 class="text-lg font-bold mb-2">Other Custom Aliases</h3>
|
<h3 class="text-lg font-bold mb-2">Other Custom Aliases</h3>
|
||||||
<ul class="list-disc list-inside mb-4 ml-2 md:ml-5 max-w-prose">
|
<ul class="list-disc list-inside mb-4 ml-2 md:ml-5 max-w-prose">
|
||||||
<li :for={{k, v} <- other_custom_output_template_options()} class="mt-1">
|
<li :for={{k, v} <- other_custom_output_template_options()} class="mt-1">
|
||||||
<.inline_code>{{ <%= k %> }}</.inline_code>
|
<.inline_code>{double_brace(k)}</.inline_code>
|
||||||
<span :if={v}>- <%= html_escape({:safe, v}) %></span>
|
<span :if={v}>- {html_escape({:safe, v})}</span>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
<h3 class="text-lg font-bold mb-2">Common Options</h3>
|
<h3 class="text-lg font-bold mb-2">Common Options</h3>
|
||||||
<ul class="list-disc list-inside ml-2 md:ml-5">
|
<ul class="list-disc list-inside ml-2 md:ml-5">
|
||||||
<li :for={opt <- common_output_template_options()} class="mt-1">
|
<li :for={opt <- common_output_template_options()} class="mt-1">
|
||||||
<.inline_code>{{ <%= opt %> }}</.inline_code>
|
<.inline_code>{double_brace(opt)}</.inline_code>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
<div class="mb-6 flex gap-3 flex-row items-center justify-between">
|
<div class="mb-6 flex gap-3 flex-row items-center justify-between">
|
||||||
<div class="flex items-center">
|
<div class="flex items-center min-w-0">
|
||||||
<.link href={~p"/media_profiles"}>
|
<.link href={~p"/media_profiles"}>
|
||||||
<.icon name="hero-arrow-left" class="w-10 h-10 hover:dark:text-white" />
|
<.icon name="hero-arrow-left" class="w-10 h-10 hover:dark:text-white" />
|
||||||
</.link>
|
</.link>
|
||||||
<h2 class="text-title-md2 font-bold text-black dark:text-white ml-2">
|
<h2 class="text-title-md2 font-bold text-black dark:text-white ml-2 truncate">
|
||||||
<%= @media_profile.name %>
|
{@media_profile.name}
|
||||||
</h2>
|
</h2>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -33,10 +33,10 @@
|
|||||||
<.table rows={@sources} table_class="text-black dark:text-white">
|
<.table rows={@sources} table_class="text-black dark:text-white">
|
||||||
<:col :let={source} label="Name">
|
<:col :let={source} label="Name">
|
||||||
<.subtle_link href={~p"/sources/#{source.id}"}>
|
<.subtle_link href={~p"/sources/#{source.id}"}>
|
||||||
<%= source.custom_name || source.collection_name %>
|
{source.custom_name || source.collection_name}
|
||||||
</.subtle_link>
|
</.subtle_link>
|
||||||
</:col>
|
</:col>
|
||||||
<:col :let={source} label="Type"><%= source.collection_type %></:col>
|
<:col :let={source} label="Type">{source.collection_type}</:col>
|
||||||
<:col :let={source} label="Should Download?">
|
<:col :let={source} label="Should Download?">
|
||||||
<.icon name={if source.download_media, do: "hero-check", else: "hero-x-mark"} />
|
<.icon name={if source.download_media, do: "hero-check", else: "hero-x-mark"} />
|
||||||
</:col>
|
</:col>
|
||||||
|
|||||||
@@ -1,23 +1,5 @@
|
|||||||
defmodule PinchflatWeb.Pages.PageHTML do
|
defmodule PinchflatWeb.Pages.PageHTML do
|
||||||
use PinchflatWeb, :html
|
use PinchflatWeb, :html
|
||||||
|
|
||||||
alias Pinchflat.Utils.NumberUtils
|
|
||||||
|
|
||||||
embed_templates "page_html/*"
|
embed_templates "page_html/*"
|
||||||
|
|
||||||
attr :media_filesize, :integer, required: true
|
|
||||||
|
|
||||||
def readable_media_filesize(assigns) do
|
|
||||||
{num, suffix} = NumberUtils.human_byte_size(assigns.media_filesize, precision: 2)
|
|
||||||
|
|
||||||
assigns =
|
|
||||||
Map.merge(assigns, %{
|
|
||||||
num: num,
|
|
||||||
suffix: suffix
|
|
||||||
})
|
|
||||||
|
|
||||||
~H"""
|
|
||||||
<.localized_number number={@num} /> <%= @suffix %>
|
|
||||||
"""
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -28,23 +28,23 @@ defmodule Pinchflat.Pages.HistoryTableLive do
|
|||||||
</span>
|
</span>
|
||||||
<div class="max-w-full overflow-x-auto">
|
<div class="max-w-full overflow-x-auto">
|
||||||
<.table rows={@records} table_class="text-white">
|
<.table rows={@records} table_class="text-white">
|
||||||
<:col :let={media_item} label="Title">
|
<:col :let={media_item} label="Title" class="truncate max-w-xs">
|
||||||
<.subtle_link href={~p"/sources/#{media_item.source_id}/media/#{media_item}"}>
|
<.subtle_link href={~p"/sources/#{media_item.source_id}/media/#{media_item}"}>
|
||||||
<%= StringUtils.truncate(media_item.title, 35) %>
|
{media_item.title}
|
||||||
</.subtle_link>
|
</.subtle_link>
|
||||||
</:col>
|
</:col>
|
||||||
<:col :let={media_item} label="Upload Date">
|
<:col :let={media_item} label="Upload Date">
|
||||||
<%= DateTime.to_date(media_item.uploaded_at) %>
|
{DateTime.to_date(media_item.uploaded_at)}
|
||||||
</:col>
|
</:col>
|
||||||
<:col :let={media_item} label="Indexed At">
|
<:col :let={media_item} label="Indexed At">
|
||||||
<%= format_datetime(media_item.inserted_at) %>
|
{format_datetime(media_item.inserted_at)}
|
||||||
</:col>
|
</:col>
|
||||||
<:col :let={media_item} label="Downloaded At">
|
<:col :let={media_item} label="Downloaded At">
|
||||||
<%= format_datetime(media_item.media_downloaded_at) %>
|
{format_datetime(media_item.media_downloaded_at)}
|
||||||
</:col>
|
</:col>
|
||||||
<:col :let={media_item} label="Source">
|
<:col :let={media_item} label="Source" class="truncate max-w-xs">
|
||||||
<.subtle_link href={~p"/sources/#{media_item.source_id}"}>
|
<.subtle_link href={~p"/sources/#{media_item.source_id}"}>
|
||||||
<%= StringUtils.truncate(media_item.source.custom_name, 35) %>
|
{media_item.source.custom_name}
|
||||||
</.subtle_link>
|
</.subtle_link>
|
||||||
</:col>
|
</:col>
|
||||||
</.table>
|
</.table>
|
||||||
@@ -56,9 +56,9 @@ defmodule Pinchflat.Pages.HistoryTableLive do
|
|||||||
"""
|
"""
|
||||||
end
|
end
|
||||||
|
|
||||||
def mount(_params, _session, socket) do
|
def mount(_params, session, socket) do
|
||||||
page = 1
|
page = 1
|
||||||
base_query = generate_base_query()
|
base_query = generate_base_query(session["media_state"])
|
||||||
pagination_attrs = fetch_pagination_attributes(base_query, page)
|
pagination_attrs = fetch_pagination_attributes(base_query, page)
|
||||||
|
|
||||||
{:ok, assign(socket, Map.merge(pagination_attrs, %{base_query: base_query}))}
|
{:ok, assign(socket, Map.merge(pagination_attrs, %{base_query: base_query}))}
|
||||||
@@ -97,10 +97,17 @@ defmodule Pinchflat.Pages.HistoryTableLive do
|
|||||||
|> Repo.preload(:source)
|
|> Repo.preload(:source)
|
||||||
end
|
end
|
||||||
|
|
||||||
defp generate_base_query do
|
defp generate_base_query("pending") do
|
||||||
MediaQuery.new()
|
MediaQuery.new()
|
||||||
|> MediaQuery.require_assoc(:media_profile)
|
|> MediaQuery.require_assoc(:media_profile)
|
||||||
|> where(^dynamic(^MediaQuery.downloaded() or ^MediaQuery.pending()))
|
|> where(^dynamic(^MediaQuery.pending()))
|
||||||
|
|> order_by(desc: :id)
|
||||||
|
end
|
||||||
|
|
||||||
|
defp generate_base_query("downloaded") do
|
||||||
|
MediaQuery.new()
|
||||||
|
|> MediaQuery.require_assoc(:media_profile)
|
||||||
|
|> where(^dynamic(^MediaQuery.downloaded()))
|
||||||
|> order_by(desc: :id)
|
|> order_by(desc: :id)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -33,7 +33,7 @@
|
|||||||
<span class="flex flex-col items-center py-2">
|
<span class="flex flex-col items-center py-2">
|
||||||
<span class="text-md font-medium">Library Size</span>
|
<span class="text-md font-medium">Library Size</span>
|
||||||
<h4 class="text-title-md font-bold text-white">
|
<h4 class="text-title-md font-bold text-white">
|
||||||
<.readable_media_filesize media_filesize={@media_item_size} />
|
<.readable_filesize byte_size={@media_item_size} />
|
||||||
</h4>
|
</h4>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -41,14 +41,27 @@
|
|||||||
|
|
||||||
<div class="rounded-sm border shadow-default border-strokedark bg-boxdark mt-4 p-5">
|
<div class="rounded-sm border shadow-default border-strokedark bg-boxdark mt-4 p-5">
|
||||||
<span class="text-2xl font-medium mb-4">Media History</span>
|
<span class="text-2xl font-medium mb-4">Media History</span>
|
||||||
<section class="mt-6">
|
<.tabbed_layout>
|
||||||
<%= live_render(@conn, Pinchflat.Pages.HistoryTableLive) %>
|
<:tab title="Downloaded" id="downloaded">
|
||||||
</section>
|
{live_render(
|
||||||
|
@conn,
|
||||||
|
Pinchflat.Pages.HistoryTableLive,
|
||||||
|
session: %{"media_state" => "downloaded"}
|
||||||
|
)}
|
||||||
|
</:tab>
|
||||||
|
<:tab title="Pending" id="pending">
|
||||||
|
{live_render(
|
||||||
|
@conn,
|
||||||
|
Pinchflat.Pages.HistoryTableLive,
|
||||||
|
session: %{"media_state" => "pending"}
|
||||||
|
)}
|
||||||
|
</:tab>
|
||||||
|
</.tabbed_layout>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="rounded-sm border shadow-default border-strokedark bg-boxdark mt-4 p-5">
|
<div class="rounded-sm border shadow-default border-strokedark bg-boxdark mt-4 p-5">
|
||||||
<span class="text-2xl font-medium mb-4">Active Tasks</span>
|
<span class="text-2xl font-medium mb-4">Active Tasks</span>
|
||||||
<section class="mt-6 min-h-80">
|
<section class="mt-6 min-h-80">
|
||||||
<%= live_render(@conn, Pinchflat.Pages.JobTableLive) %>
|
{live_render(@conn, Pinchflat.Pages.JobTableLive)}
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -19,18 +19,18 @@ defmodule Pinchflat.Pages.JobTableLive do
|
|||||||
<div class="max-w-full overflow-x-auto">
|
<div class="max-w-full overflow-x-auto">
|
||||||
<.table rows={@tasks} table_class="text-white">
|
<.table rows={@tasks} table_class="text-white">
|
||||||
<:col :let={task} label="Task">
|
<:col :let={task} label="Task">
|
||||||
<%= worker_to_task_name(task.job.worker) %>
|
{worker_to_task_name(task.job.worker)}
|
||||||
</:col>
|
</:col>
|
||||||
<:col :let={task} label="Subject">
|
<:col :let={task} label="Subject" class="truncate max-w-xs">
|
||||||
<.subtle_link href={task_to_link(task)}>
|
<.subtle_link href={task_to_link(task)}>
|
||||||
<%= StringUtils.truncate(task_to_record_name(task), 35) %>
|
{task_to_record_name(task)}
|
||||||
</.subtle_link>
|
</.subtle_link>
|
||||||
</:col>
|
</:col>
|
||||||
<:col :let={task} label="Attempt No.">
|
<:col :let={task} label="Attempt No.">
|
||||||
<%= task.job.attempt %>
|
{task.job.attempt}
|
||||||
</:col>
|
</:col>
|
||||||
<:col :let={task} label="Started At">
|
<:col :let={task} label="Started At">
|
||||||
<%= format_datetime(task.job.attempted_at) %>
|
{format_datetime(task.job.attempted_at)}
|
||||||
</:col>
|
</:col>
|
||||||
</.table>
|
</.table>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -6,8 +6,19 @@ defmodule PinchflatWeb.Podcasts.PodcastController do
|
|||||||
alias Pinchflat.Sources.Source
|
alias Pinchflat.Sources.Source
|
||||||
alias Pinchflat.Media.MediaItem
|
alias Pinchflat.Media.MediaItem
|
||||||
alias Pinchflat.Podcasts.RssFeedBuilder
|
alias Pinchflat.Podcasts.RssFeedBuilder
|
||||||
|
alias Pinchflat.Podcasts.OpmlFeedBuilder
|
||||||
alias Pinchflat.Podcasts.PodcastHelpers
|
alias Pinchflat.Podcasts.PodcastHelpers
|
||||||
|
|
||||||
|
def opml_feed(conn, _params) do
|
||||||
|
url_base = url(conn, ~p"/")
|
||||||
|
xml = OpmlFeedBuilder.build(url_base, PodcastHelpers.opml_sources())
|
||||||
|
|
||||||
|
conn
|
||||||
|
|> put_resp_content_type("application/opml+xml")
|
||||||
|
|> put_resp_header("content-disposition", "inline")
|
||||||
|
|> send_resp(200, xml)
|
||||||
|
end
|
||||||
|
|
||||||
def rss_feed(conn, %{"uuid" => uuid}) do
|
def rss_feed(conn, %{"uuid" => uuid}) do
|
||||||
source = Repo.get_by!(Source, uuid: uuid)
|
source = Repo.get_by!(Source, uuid: uuid)
|
||||||
url_base = url(conn, ~p"/")
|
url_base = url(conn, ~p"/")
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ defmodule PinchflatWeb.Searches.SearchHTML do
|
|||||||
|
|
||||||
~H"""
|
~H"""
|
||||||
<%= for fragment <- @split_string do %>
|
<%= for fragment <- @split_string do %>
|
||||||
<%= render_fragment(fragment) %>
|
{render_fragment(fragment)}
|
||||||
<% end %>
|
<% end %>
|
||||||
"""
|
"""
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<div class="mb-6 flex gap-3 flex-row items-center justify-between">
|
<div class="mb-6 flex gap-3 flex-row items-center justify-between">
|
||||||
<h2 class="text-title-md2 font-bold text-black dark:text-white">
|
<h2 class="text-title-md2 font-bold text-black dark:text-white truncate">
|
||||||
Results for "<%= StringUtils.truncate(@search_term, 50) %>"
|
Results for "{@search_term}"
|
||||||
</h2>
|
</h2>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -9,9 +9,9 @@
|
|||||||
<div class="flex flex-col gap-10 dark:text-white">
|
<div class="flex flex-col gap-10 dark:text-white">
|
||||||
<%= if match?([_|_], @search_results) do %>
|
<%= if match?([_|_], @search_results) do %>
|
||||||
<.table rows={@search_results} table_class="text-black dark:text-white">
|
<.table rows={@search_results} table_class="text-black dark:text-white">
|
||||||
<:col :let={result} label="Title">
|
<:col :let={result} label="Title" class="truncate max-w-xs">
|
||||||
<.subtle_link href={~p"/sources/#{result.source_id}/media/#{result.id}"}>
|
<.subtle_link href={~p"/sources/#{result.source_id}/media/#{result.id}"}>
|
||||||
<%= StringUtils.truncate(result.title, 35) %>
|
{result.title}
|
||||||
</.subtle_link>
|
</.subtle_link>
|
||||||
</:col>
|
</:col>
|
||||||
<:col :let={result} label="Excerpt">
|
<:col :let={result} label="Excerpt">
|
||||||
|
|||||||
@@ -19,11 +19,11 @@
|
|||||||
</span>
|
</span>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<%= live_render(
|
{live_render(
|
||||||
@conn,
|
@conn,
|
||||||
Pinchflat.Settings.AppriseServerLive,
|
Pinchflat.Settings.AppriseServerLive,
|
||||||
session: %{"value" => f[:apprise_server].value}
|
session: %{"value" => f[:apprise_server].value}
|
||||||
) %>
|
)}
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="mt-8">
|
<section class="mt-8">
|
||||||
|
|||||||
@@ -27,10 +27,28 @@ defmodule PinchflatWeb.Sources.SourceHTML do
|
|||||||
]
|
]
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def cutoff_date_presets do
|
||||||
|
[
|
||||||
|
{"7 days", compute_date_offset(7)},
|
||||||
|
{"14 days", compute_date_offset(14)},
|
||||||
|
{"30 days", compute_date_offset(30)},
|
||||||
|
{"60 days", compute_date_offset(60)},
|
||||||
|
{"90 days", compute_date_offset(90)},
|
||||||
|
{"180 days", compute_date_offset(180)},
|
||||||
|
{"365 days", compute_date_offset(365)}
|
||||||
|
]
|
||||||
|
end
|
||||||
|
|
||||||
def rss_feed_url(conn, source) do
|
def rss_feed_url(conn, source) do
|
||||||
|
# NOTE: The reason for this concatenation is to avoid what appears to be a bug in Phoenix
|
||||||
|
# See: https://github.com/phoenixframework/phoenix/issues/6033
|
||||||
url(conn, ~p"/sources/#{source.uuid}/feed") <> ".xml"
|
url(conn, ~p"/sources/#{source.uuid}/feed") <> ".xml"
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def opml_feed_url(conn) do
|
||||||
|
url(conn, ~p"/sources/opml.xml?#{[route_token: Settings.get!(:route_token)]}")
|
||||||
|
end
|
||||||
|
|
||||||
def output_path_template_override_placeholders(media_profiles) do
|
def output_path_template_override_placeholders(media_profiles) do
|
||||||
media_profiles
|
media_profiles
|
||||||
|> Enum.map(&{&1.id, &1.output_path_template})
|
|> Enum.map(&{&1.id, &1.output_path_template})
|
||||||
@@ -55,4 +73,13 @@ defmodule PinchflatWeb.Sources.SourceHTML do
|
|||||||
Must end with .{{ ext }}. Same rules as Media Profile output path templates. #{help_button} to load your media profile's output template
|
Must end with .{{ ext }}. Same rules as Media Profile output path templates. #{help_button} to load your media profile's output template
|
||||||
"""
|
"""
|
||||||
end
|
end
|
||||||
|
|
||||||
|
defp compute_date_offset(days) do
|
||||||
|
timezone = Application.get_env(:pinchflat, :timezone)
|
||||||
|
|
||||||
|
timezone
|
||||||
|
|> Timex.now()
|
||||||
|
|> Timex.shift(days: -days)
|
||||||
|
|> Timex.format!("{YYYY}-{0M}-{0D}")
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -1,18 +1,16 @@
|
|||||||
<.button_dropdown text="Actions" class="justify-center w-full sm:w-50">
|
<.button_dropdown text="Actions" class="justify-center w-full sm:w-50">
|
||||||
<:option>
|
<:option>
|
||||||
<span
|
<.link href={rss_feed_url(@conn, @source)} x-data="{ copied: false }" x-on:click={~s"
|
||||||
x-data="{ copied: false }"
|
$event.preventDefault();
|
||||||
x-on:click={"
|
copyWithCallbacks(
|
||||||
copyWithCallbacks(
|
'#{rss_feed_url(@conn, @source)}',
|
||||||
'#{rss_feed_url(@conn, @source)}',
|
() => copied = true,
|
||||||
() => copied = true,
|
() => copied = false
|
||||||
() => copied = false
|
)
|
||||||
)
|
"}>
|
||||||
"}
|
|
||||||
>
|
|
||||||
Copy RSS Feed
|
Copy RSS Feed
|
||||||
<span x-show="copied" x-transition.duration.150ms><.icon name="hero-check" class="ml-2 h-4 w-4" /></span>
|
<span x-show="copied" x-transition.duration.150ms><.icon name="hero-check" class="ml-2 h-4 w-4" /></span>
|
||||||
</span>
|
</.link>
|
||||||
</:option>
|
</:option>
|
||||||
<:option>
|
<:option>
|
||||||
<span x-data="{ copied: false }" x-on:click={~s"
|
<span x-data="{ copied: false }" x-on:click={~s"
|
||||||
@@ -56,7 +54,7 @@
|
|||||||
<.link
|
<.link
|
||||||
href={~p"/sources/#{@source}/force_index"}
|
href={~p"/sources/#{@source}/force_index"}
|
||||||
method="post"
|
method="post"
|
||||||
data-confirm="Are you sure you want to force an index of this source? This isn't normally needed."
|
data-confirm="Are you sure you want index all content from this source? This isn't normally needed."
|
||||||
>
|
>
|
||||||
Force Index
|
Force Index
|
||||||
</.link>
|
</.link>
|
||||||
|
|||||||
@@ -2,8 +2,8 @@
|
|||||||
<.link href={~p"/sources"}>
|
<.link href={~p"/sources"}>
|
||||||
<.icon name="hero-arrow-left" class="w-10 h-10 hover:dark:text-white" />
|
<.icon name="hero-arrow-left" class="w-10 h-10 hover:dark:text-white" />
|
||||||
</.link>
|
</.link>
|
||||||
<h2 class="text-title-md2 font-bold text-black dark:text-white ml-4">
|
<h2 class="text-title-md2 font-bold text-black dark:text-white ml-4 truncate">
|
||||||
Editing "<%= @source.custom_name %>"
|
Editing "{@source.custom_name}"
|
||||||
</h2>
|
</h2>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,17 @@
|
|||||||
<div class="mb-6 flex gap-3 flex-row items-center justify-between">
|
<div class="mb-6 flex gap-3 flex-row items-center justify-between">
|
||||||
<h2 class="text-title-md2 font-bold text-black dark:text-white">Sources</h2>
|
<h2 class="text-title-md2 font-bold text-black dark:text-white">Sources</h2>
|
||||||
<nav>
|
<nav class="flex items-center justify-between gap-6">
|
||||||
|
<.link href={opml_feed_url(@conn)} x-data="{ copied: false }" x-on:click={~s"
|
||||||
|
$event.preventDefault();
|
||||||
|
copyWithCallbacks(
|
||||||
|
'#{opml_feed_url(@conn)}',
|
||||||
|
() => copied = true,
|
||||||
|
() => copied = false
|
||||||
|
)
|
||||||
|
"}>
|
||||||
|
Copy OPML <span class="hidden sm:inline">Feed</span>
|
||||||
|
<span x-show="copied" x-transition.duration.150ms><.icon name="hero-check" class="ml-2 h-4 w-4" /></span>
|
||||||
|
</.link>
|
||||||
<.link href={~p"/sources/new"}>
|
<.link href={~p"/sources/new"}>
|
||||||
<.button color="bg-primary" rounding="rounded-lg">
|
<.button color="bg-primary" rounding="rounded-lg">
|
||||||
<span class="font-bold mx-2">+</span> New <span class="hidden sm:inline pl-1">Source</span>
|
<span class="font-bold mx-2">+</span> New <span class="hidden sm:inline pl-1">Source</span>
|
||||||
@@ -11,8 +22,12 @@
|
|||||||
|
|
||||||
<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">
|
{live_render(@conn, PinchflatWeb.Sources.SourceLive.IndexTableLive,
|
||||||
<%= live_render(@conn, PinchflatWeb.Sources.IndexTableLive) %>
|
session: %{
|
||||||
</div>
|
"initial_sort_key" => :custom_name,
|
||||||
|
"initial_sort_direction" => :asc,
|
||||||
|
"results_per_page" => 10
|
||||||
|
}
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -11,9 +11,9 @@ defmodule PinchflatWeb.Sources.IndexTableLive do
|
|||||||
def render(assigns) do
|
def render(assigns) do
|
||||||
~H"""
|
~H"""
|
||||||
<.table rows={@sources} table_class="text-white">
|
<.table rows={@sources} table_class="text-white">
|
||||||
<:col :let={source} label="Name">
|
<:col :let={source} label="Name" class="truncate max-w-xs">
|
||||||
<.subtle_link href={~p"/sources/#{source.id}"}>
|
<.subtle_link href={~p"/sources/#{source.id}"}>
|
||||||
<%= StringUtils.truncate(source.custom_name || source.collection_name, 35) %>
|
{source.custom_name || source.collection_name}
|
||||||
</.subtle_link>
|
</.subtle_link>
|
||||||
</:col>
|
</:col>
|
||||||
<:col :let={source} label="Pending">
|
<:col :let={source} label="Pending">
|
||||||
@@ -36,7 +36,7 @@ defmodule PinchflatWeb.Sources.IndexTableLive do
|
|||||||
</:col>
|
</:col>
|
||||||
<:col :let={source} label="Media Profile">
|
<:col :let={source} label="Media Profile">
|
||||||
<.subtle_link href={~p"/media_profiles/#{source.media_profile_id}"}>
|
<.subtle_link href={~p"/media_profiles/#{source.media_profile_id}"}>
|
||||||
<%= source.media_profile.name %>
|
{source.media_profile.name}
|
||||||
</.subtle_link>
|
</.subtle_link>
|
||||||
</:col>
|
</:col>
|
||||||
<:col :let={source} label="Enabled?">
|
<:col :let={source} label="Enabled?">
|
||||||
|
|||||||
@@ -46,16 +46,16 @@ defmodule PinchflatWeb.Sources.MediaItemTableLive do
|
|||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
<.table rows={@records} table_class="text-white">
|
<.table rows={@records} table_class="text-white">
|
||||||
<:col :let={media_item} label="Title">
|
<:col :let={media_item} label="Title" class="truncate max-w-xs">
|
||||||
<.subtle_link href={~p"/sources/#{@source.id}/media/#{media_item.id}"}>
|
<.subtle_link href={~p"/sources/#{@source.id}/media/#{media_item.id}"}>
|
||||||
<%= StringUtils.truncate(media_item.title, 50) %>
|
{media_item.title}
|
||||||
</.subtle_link>
|
</.subtle_link>
|
||||||
</:col>
|
</:col>
|
||||||
<:col :let={media_item} :if={@media_state == "other"} label="Manually Ignored?">
|
<:col :let={media_item} :if={@media_state == "other"} label="Manually Ignored?">
|
||||||
<.icon name={if media_item.prevent_download, do: "hero-check", else: "hero-x-mark"} />
|
<.icon name={if media_item.prevent_download, do: "hero-check", else: "hero-x-mark"} />
|
||||||
</:col>
|
</:col>
|
||||||
<:col :let={media_item} label="Upload Date">
|
<:col :let={media_item} label="Upload Date">
|
||||||
<%= DateTime.to_date(media_item.uploaded_at) %>
|
{DateTime.to_date(media_item.uploaded_at)}
|
||||||
</:col>
|
</:col>
|
||||||
<:col :let={media_item} label="" class="flex justify-end">
|
<:col :let={media_item} label="" class="flex justify-end">
|
||||||
<.icon_link href={~p"/sources/#{@source.id}/media/#{media_item.id}/edit"} icon="hero-pencil-square" class="mr-4" />
|
<.icon_link href={~p"/sources/#{@source.id}/media/#{media_item.id}/edit"} icon="hero-pencil-square" class="mr-4" />
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
<div class="mb-6 flex gap-3 flex-row items-center justify-between">
|
<div class="mb-6 flex gap-3 flex-row items-center justify-between">
|
||||||
<div class="flex gap-3 items-center">
|
<div class="flex gap-3 items-center min-w-0">
|
||||||
<.link href={~p"/sources"}>
|
<.link href={~p"/sources"}>
|
||||||
<.icon name="hero-arrow-left" class="w-10 h-10 hover:dark:text-white" />
|
<.icon name="hero-arrow-left" class="w-10 h-10 hover:dark:text-white" />
|
||||||
</.link>
|
</.link>
|
||||||
<h2 class="text-title-md2 font-bold text-black dark:text-white ml-4">
|
<h2 class="text-title-md2 font-bold text-black dark:text-white ml-4 truncate">
|
||||||
<%= @source.custom_name %>
|
{@source.custom_name}
|
||||||
</h2>
|
</h2>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -29,7 +29,7 @@
|
|||||||
<section>
|
<section>
|
||||||
<strong>Media Profile:</strong>
|
<strong>Media Profile:</strong>
|
||||||
<.subtle_link href={~p"/media_profiles/#{@source.media_profile_id}"}>
|
<.subtle_link href={~p"/media_profiles/#{@source.media_profile_id}"}>
|
||||||
<%= @source.media_profile.name %>
|
{@source.media_profile.name}
|
||||||
</.subtle_link>
|
</.subtle_link>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
@@ -37,34 +37,34 @@
|
|||||||
</div>
|
</div>
|
||||||
</:tab>
|
</:tab>
|
||||||
<:tab title="Pending" id="pending">
|
<:tab title="Pending" id="pending">
|
||||||
<%= live_render(
|
{live_render(
|
||||||
@conn,
|
@conn,
|
||||||
PinchflatWeb.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,
|
||||||
PinchflatWeb.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,
|
||||||
PinchflatWeb.Sources.MediaItemTableLive,
|
PinchflatWeb.Sources.MediaItemTableLive,
|
||||||
session: %{"source_id" => @source.id, "media_state" => "other"}
|
session: %{"source_id" => @source.id, "media_state" => "other"}
|
||||||
) %>
|
)}
|
||||||
</:tab>
|
</:tab>
|
||||||
<:tab title="Tasks" id="tasks">
|
<:tab title="Tasks" id="tasks">
|
||||||
<%= if match?([_|_], @pending_tasks) do %>
|
<%= if match?([_|_], @pending_tasks) do %>
|
||||||
<.table rows={@pending_tasks} table_class="text-black dark:text-white">
|
<.table rows={@pending_tasks} table_class="text-black dark:text-white">
|
||||||
<:col :let={task} label="Worker">
|
<:col :let={task} label="Worker">
|
||||||
<%= task.job.worker %>
|
{task.job.worker}
|
||||||
</:col>
|
</:col>
|
||||||
<:col :let={task} label="State">
|
<:col :let={task} label="State">
|
||||||
<%= task.job.state %>
|
{task.job.state}
|
||||||
</:col>
|
</:col>
|
||||||
<:col :let={task} label="Scheduled At">
|
<:col :let={task} label="Scheduled At">
|
||||||
<.datetime_in_zone datetime={task.job.scheduled_at} />
|
<.datetime_in_zone datetime={task.job.scheduled_at} />
|
||||||
|
|||||||
@@ -111,16 +111,31 @@
|
|||||||
/>
|
/>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<.input
|
<section x-data={"{ cutoffDate: '#{f[:download_cutoff_date].value}' }"}>
|
||||||
field={f[:download_cutoff_date]}
|
<.input
|
||||||
type="text"
|
field={f[:download_cutoff_date]}
|
||||||
label="Download Cutoff Date"
|
type="text"
|
||||||
placeholder="YYYY-MM-DD"
|
label="Download Cutoff Date"
|
||||||
maxlength="10"
|
placeholder="YYYY-MM-DD"
|
||||||
pattern="((?:19|20)[0-9][0-9])-(0[1-9]|1[012])-(0[1-9]|[12][0-9]|3[01])"
|
maxlength="10"
|
||||||
title="YYYY-MM-DD"
|
pattern="((?:19|20)[0-9][0-9])-(0[1-9]|1[012])-(0[1-9]|[12][0-9]|3[01])"
|
||||||
help="Only download media uploaded after this date. Leave blank to download all media. Must be in YYYY-MM-DD format. Old media may be deleted or downloaded if you change this date"
|
title="YYYY-MM-DD"
|
||||||
/>
|
help="Only download media uploaded after this date. Leave blank to download all media. Must be in YYYY-MM-DD format. Old media may be deleted or downloaded if you change this date"
|
||||||
|
x-model="cutoffDate"
|
||||||
|
>
|
||||||
|
<:input_append>
|
||||||
|
<.input
|
||||||
|
prompt="Select preset"
|
||||||
|
name="download_cutoff_date_preset"
|
||||||
|
value=""
|
||||||
|
options={cutoff_date_presets()}
|
||||||
|
type="select"
|
||||||
|
inputclass="w-30 lg:w-60 ml-2 md:ml-4"
|
||||||
|
x-on:change={"cutoffDate = $event.target.value || '#{f[:download_cutoff_date].value}'"}
|
||||||
|
/>
|
||||||
|
</:input_append>
|
||||||
|
</.input>
|
||||||
|
</section>
|
||||||
|
|
||||||
<.input
|
<.input
|
||||||
field={f[:retention_period_days]}
|
field={f[:retention_period_days]}
|
||||||
|
|||||||
@@ -0,0 +1,108 @@
|
|||||||
|
defmodule PinchflatWeb.Sources.SourceLive.IndexTableLive do
|
||||||
|
use PinchflatWeb, :live_view
|
||||||
|
use Pinchflat.Media.MediaQuery
|
||||||
|
use Pinchflat.Sources.SourcesQuery
|
||||||
|
|
||||||
|
import PinchflatWeb.Helpers.SortingHelpers
|
||||||
|
import PinchflatWeb.Helpers.PaginationHelpers
|
||||||
|
|
||||||
|
alias Pinchflat.Repo
|
||||||
|
alias Pinchflat.Sources.Source
|
||||||
|
alias Pinchflat.Media.MediaItem
|
||||||
|
|
||||||
|
def mount(_params, session, socket) do
|
||||||
|
limit = session["results_per_page"]
|
||||||
|
|
||||||
|
initial_params =
|
||||||
|
Map.merge(
|
||||||
|
%{
|
||||||
|
sort_key: session["initial_sort_key"],
|
||||||
|
sort_direction: session["initial_sort_direction"]
|
||||||
|
},
|
||||||
|
get_pagination_attributes(sources_query(), 1, limit)
|
||||||
|
)
|
||||||
|
|
||||||
|
socket
|
||||||
|
|> assign(initial_params)
|
||||||
|
|> set_sources()
|
||||||
|
|> then(&{:ok, &1})
|
||||||
|
end
|
||||||
|
|
||||||
|
def handle_event("page_change", %{"direction" => direction}, %{assigns: assigns} = socket) do
|
||||||
|
new_page = update_page_number(assigns.page, direction, assigns.total_pages)
|
||||||
|
|
||||||
|
socket
|
||||||
|
|> assign(get_pagination_attributes(sources_query(), new_page, assigns.limit))
|
||||||
|
|> set_sources()
|
||||||
|
|> then(&{:noreply, &1})
|
||||||
|
end
|
||||||
|
|
||||||
|
def handle_event("sort_update", %{"sort_key" => sort_key}, %{assigns: assigns} = socket) do
|
||||||
|
new_sort_key = String.to_existing_atom(sort_key)
|
||||||
|
|
||||||
|
new_params = %{
|
||||||
|
sort_key: new_sort_key,
|
||||||
|
sort_direction: get_sort_direction(assigns.sort_key, new_sort_key, assigns.sort_direction)
|
||||||
|
}
|
||||||
|
|
||||||
|
socket
|
||||||
|
|> assign(new_params)
|
||||||
|
|> set_sources()
|
||||||
|
|> then(&{:noreply, &1})
|
||||||
|
end
|
||||||
|
|
||||||
|
defp sort_attr(:pending_count), do: dynamic([s, mp, dl, pe], pe.pending_count)
|
||||||
|
defp sort_attr(:downloaded_count), do: dynamic([s, mp, dl], dl.downloaded_count)
|
||||||
|
defp sort_attr(:media_size_bytes), do: dynamic([s, mp, dl], dl.media_size_bytes)
|
||||||
|
defp sort_attr(:media_profile_name), do: dynamic([s, mp], mp.name)
|
||||||
|
defp sort_attr(:custom_name), do: dynamic([s], s.custom_name)
|
||||||
|
defp sort_attr(:enabled), do: dynamic([s], s.enabled)
|
||||||
|
|
||||||
|
defp set_sources(%{assigns: assigns} = socket) do
|
||||||
|
sources =
|
||||||
|
sources_query()
|
||||||
|
|> order_by(^[{assigns.sort_direction, sort_attr(assigns.sort_key)}, asc: :id])
|
||||||
|
|> limit(^assigns.limit)
|
||||||
|
|> offset(^assigns.offset)
|
||||||
|
|> Repo.all()
|
||||||
|
|
||||||
|
assign(socket, %{sources: sources})
|
||||||
|
end
|
||||||
|
|
||||||
|
defp sources_query do
|
||||||
|
downloaded_subquery =
|
||||||
|
from(
|
||||||
|
m in MediaItem,
|
||||||
|
select: %{downloaded_count: count(m.id), source_id: m.source_id, media_size_bytes: sum(m.media_size_bytes)},
|
||||||
|
where: ^MediaQuery.downloaded(),
|
||||||
|
group_by: m.source_id
|
||||||
|
)
|
||||||
|
|
||||||
|
pending_subquery =
|
||||||
|
from(
|
||||||
|
m in MediaItem,
|
||||||
|
inner_join: s in assoc(m, :source),
|
||||||
|
inner_join: mp in assoc(s, :media_profile),
|
||||||
|
select: %{pending_count: count(m.id), source_id: m.source_id},
|
||||||
|
where: ^MediaQuery.pending(),
|
||||||
|
group_by: m.source_id
|
||||||
|
)
|
||||||
|
|
||||||
|
from s in Source,
|
||||||
|
as: :source,
|
||||||
|
inner_join: mp in assoc(s, :media_profile),
|
||||||
|
left_join: d in subquery(downloaded_subquery),
|
||||||
|
on: d.source_id == s.id,
|
||||||
|
left_join: p in subquery(pending_subquery),
|
||||||
|
on: p.source_id == s.id,
|
||||||
|
on: d.source_id == s.id,
|
||||||
|
where: is_nil(s.marked_for_deletion_at) and is_nil(mp.marked_for_deletion_at),
|
||||||
|
preload: [media_profile: mp],
|
||||||
|
select: map(s, ^Source.__schema__(:fields)),
|
||||||
|
select_merge: %{
|
||||||
|
downloaded_count: coalesce(d.downloaded_count, 0),
|
||||||
|
pending_count: coalesce(p.pending_count, 0),
|
||||||
|
media_size_bytes: coalesce(d.media_size_bytes, 0)
|
||||||
|
}
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
<div class="flex flex-col min-w-max">
|
||||||
|
<.table rows={@sources} table_class="text-white" sort_key={@sort_key} sort_direction={@sort_direction}>
|
||||||
|
<:col :let={source} label="Name" sort_key="custom_name" class="truncate max-w-xs">
|
||||||
|
<.subtle_link href={~p"/sources/#{source.id}"}>
|
||||||
|
{source.custom_name}
|
||||||
|
</.subtle_link>
|
||||||
|
</:col>
|
||||||
|
<:col :let={source} label="Pending" sort_key="pending_count">
|
||||||
|
<.subtle_link href={~p"/sources/#{source.id}/#tab-pending"}>
|
||||||
|
<.localized_number number={source.pending_count} />
|
||||||
|
</.subtle_link>
|
||||||
|
</:col>
|
||||||
|
<:col :let={source} label="Downloaded" sort_key="downloaded_count">
|
||||||
|
<.subtle_link href={~p"/sources/#{source.id}/#tab-downloaded"}>
|
||||||
|
<.localized_number number={source.downloaded_count} />
|
||||||
|
</.subtle_link>
|
||||||
|
</:col>
|
||||||
|
<:col :let={source} label="Size" sort_key="media_size_bytes">
|
||||||
|
<.readable_filesize byte_size={source.media_size_bytes} />
|
||||||
|
</:col>
|
||||||
|
<:col :let={source} label="Media Profile" sort_key="media_profile_name" class="truncate max-w-xs">
|
||||||
|
<.subtle_link href={~p"/media_profiles/#{source.media_profile_id}"}>
|
||||||
|
{source.media_profile.name}
|
||||||
|
</.subtle_link>
|
||||||
|
</:col>
|
||||||
|
<:col :let={source} label="Enabled?" sort_key="enabled">
|
||||||
|
<.live_component
|
||||||
|
module={PinchflatWeb.Sources.SourceLive.SourceEnableToggle}
|
||||||
|
source={source}
|
||||||
|
id={"source_#{source.id}_enabled"}
|
||||||
|
/>
|
||||||
|
</: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>
|
||||||
|
|
||||||
|
<section class="flex justify-center my-5">
|
||||||
|
<.live_pagination_controls page_number={@page} total_pages={@total_pages} />
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
defmodule PinchflatWeb.Sources.SourceLive.SourceEnableToggle do
|
||||||
|
use PinchflatWeb, :live_component
|
||||||
|
|
||||||
|
alias Pinchflat.Sources
|
||||||
|
alias Pinchflat.Sources.Source
|
||||||
|
|
||||||
|
def render(assigns) do
|
||||||
|
~H"""
|
||||||
|
<div>
|
||||||
|
<.form :let={f} for={@form} phx-change="update" phx-target={@myself} class="enabled_toggle_form">
|
||||||
|
<.input id={"source_#{@source_id}_enabled_input"} field={f[:enabled]} type="toggle" />
|
||||||
|
</.form>
|
||||||
|
</div>
|
||||||
|
"""
|
||||||
|
end
|
||||||
|
|
||||||
|
def update(assigns, socket) do
|
||||||
|
initial_data = %{
|
||||||
|
source_id: assigns.source.id,
|
||||||
|
form: Sources.change_source(%Source{}, assigns.source)
|
||||||
|
}
|
||||||
|
|
||||||
|
socket
|
||||||
|
|> assign(initial_data)
|
||||||
|
|> then(&{:ok, &1})
|
||||||
|
end
|
||||||
|
|
||||||
|
def handle_event("update", %{"source" => source_params}, %{assigns: assigns} = socket) do
|
||||||
|
assigns.source_id
|
||||||
|
|> Sources.get_source!()
|
||||||
|
|> Sources.update_source(source_params)
|
||||||
|
|
||||||
|
{:noreply, socket}
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -73,6 +73,13 @@ defmodule PinchflatWeb.Endpoint do
|
|||||||
Phoenix.Controller.put_router_url(conn, new_base_url)
|
Phoenix.Controller.put_router_url(conn, new_base_url)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Some podcast clients require file extensions, and others still will _add_
|
||||||
|
# file extensions to XML files if they don't have them. This plug removes
|
||||||
|
# the extension from the path so that the correct route is matched, regardless
|
||||||
|
# of the provided extension.
|
||||||
|
#
|
||||||
|
# This has the downside of in-app generated verified routes not working with
|
||||||
|
# extensions so this behaviour may change in the future.
|
||||||
defp strip_trailing_extension(%{path_info: []} = conn, _opts), do: conn
|
defp strip_trailing_extension(%{path_info: []} = conn, _opts), do: conn
|
||||||
|
|
||||||
defp strip_trailing_extension(conn, _opts) do
|
defp strip_trailing_extension(conn, _opts) do
|
||||||
|
|||||||
@@ -0,0 +1,45 @@
|
|||||||
|
defmodule PinchflatWeb.Helpers.PaginationHelpers do
|
||||||
|
@moduledoc """
|
||||||
|
Methods for working with pagination, usually in the context of LiveViews or LiveComponents.
|
||||||
|
|
||||||
|
These methods are fairly simple, but they're commonly repeated across different Live entities
|
||||||
|
"""
|
||||||
|
|
||||||
|
alias Pinchflat.Repo
|
||||||
|
alias Pinchflat.Utils.NumberUtils
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Given a query, a page number, and a number of records per page, returns a map of pagination attributes.
|
||||||
|
|
||||||
|
Returns map()
|
||||||
|
"""
|
||||||
|
def get_pagination_attributes(query, page, records_per_page) do
|
||||||
|
total_record_count = Repo.aggregate(query, :count, :id)
|
||||||
|
total_pages = max(ceil(total_record_count / records_per_page), 1)
|
||||||
|
clamped_page = NumberUtils.clamp(page, 1, total_pages)
|
||||||
|
|
||||||
|
%{
|
||||||
|
page: clamped_page,
|
||||||
|
total_pages: total_pages,
|
||||||
|
total_record_count: total_record_count,
|
||||||
|
limit: records_per_page,
|
||||||
|
offset: (clamped_page - 1) * records_per_page
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Given a current page number, a direction to move in, and the total number of pages, returns the updated page number.
|
||||||
|
The updated page number is clamped to the range [1, total_pages].
|
||||||
|
|
||||||
|
Returns integer()
|
||||||
|
"""
|
||||||
|
def update_page_number(current_page, direction, total_pages) do
|
||||||
|
updated_page =
|
||||||
|
case to_string(direction) do
|
||||||
|
"inc" -> current_page + 1
|
||||||
|
"dec" -> current_page - 1
|
||||||
|
end
|
||||||
|
|
||||||
|
NumberUtils.clamp(updated_page, 1, total_pages)
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
defmodule PinchflatWeb.Helpers.SortingHelpers do
|
||||||
|
@moduledoc """
|
||||||
|
Methods for working with sorting, usually in the context of LiveViews or LiveComponents.
|
||||||
|
|
||||||
|
These methods are fairly simple, but they're commonly repeated across different Live entities
|
||||||
|
"""
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Given the old sort attribute, the new sort attribute, and the old sort direction, returns the new sort direction.
|
||||||
|
|
||||||
|
Returns :asc | :desc
|
||||||
|
"""
|
||||||
|
def get_sort_direction(old_sort_attr, new_sort_attr, old_sort_direction) do
|
||||||
|
case {new_sort_attr, old_sort_direction} do
|
||||||
|
{^old_sort_attr, :desc} -> :asc
|
||||||
|
{^old_sort_attr, _} -> :desc
|
||||||
|
_ -> :asc
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
defmodule PinchflatWeb.Plugs do
|
||||||
|
@moduledoc """
|
||||||
|
Custom plugs for PinchflatWeb.
|
||||||
|
"""
|
||||||
|
|
||||||
|
use PinchflatWeb, :router
|
||||||
|
alias Pinchflat.Settings
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
If the `expose_feed_endpoints` setting is true, this plug does nothing. Otherwise, it calls `basic_auth/2`.
|
||||||
|
"""
|
||||||
|
def maybe_basic_auth(conn, opts) do
|
||||||
|
if Application.get_env(:pinchflat, :expose_feed_endpoints) do
|
||||||
|
conn
|
||||||
|
else
|
||||||
|
basic_auth(conn, opts)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
If the `basic_auth_username` and `basic_auth_password` settings are set, this plug calls `Plug.BasicAuth.basic_auth/3`.
|
||||||
|
"""
|
||||||
|
def basic_auth(conn, _opts) do
|
||||||
|
username = Application.get_env(:pinchflat, :basic_auth_username)
|
||||||
|
password = Application.get_env(:pinchflat, :basic_auth_password)
|
||||||
|
|
||||||
|
if credential_set?(username) && credential_set?(password) do
|
||||||
|
Plug.BasicAuth.basic_auth(conn, username: username, password: password, realm: "Pinchflat")
|
||||||
|
else
|
||||||
|
conn
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Removes the `x-frame-options` header from the response to allow the page to be embedded in an iframe.
|
||||||
|
"""
|
||||||
|
def allow_iframe_embed(conn, _opts) do
|
||||||
|
delete_resp_header(conn, "x-frame-options")
|
||||||
|
end
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
If the `route_token` query parameter matches the `route_token` setting, this plug does nothing.
|
||||||
|
Otherwise, it sends a 401 response.
|
||||||
|
"""
|
||||||
|
def token_protected_route(%{query_params: %{"route_token" => route_token}} = conn, _opts) do
|
||||||
|
if Settings.get!(:route_token) == route_token do
|
||||||
|
conn
|
||||||
|
else
|
||||||
|
send_unauthorized(conn)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def token_protected_route(conn, _opts) do
|
||||||
|
send_unauthorized(conn)
|
||||||
|
end
|
||||||
|
|
||||||
|
defp credential_set?(credential) do
|
||||||
|
credential && credential != ""
|
||||||
|
end
|
||||||
|
|
||||||
|
defp send_unauthorized(conn) do
|
||||||
|
conn
|
||||||
|
|> send_resp(:unauthorized, "Unauthorized")
|
||||||
|
|> halt()
|
||||||
|
end
|
||||||
|
end
|
||||||
+18
-41
@@ -1,5 +1,6 @@
|
|||||||
defmodule PinchflatWeb.Router do
|
defmodule PinchflatWeb.Router do
|
||||||
use PinchflatWeb, :router
|
use PinchflatWeb, :router
|
||||||
|
import PinchflatWeb.Plugs
|
||||||
import Phoenix.LiveDashboard.Router
|
import Phoenix.LiveDashboard.Router
|
||||||
|
|
||||||
# IMPORTANT: `strip_trailing_extension` in endpoint.ex removes
|
# IMPORTANT: `strip_trailing_extension` in endpoint.ex removes
|
||||||
@@ -19,8 +20,23 @@ defmodule PinchflatWeb.Router do
|
|||||||
plug :accepts, ["json"]
|
plug :accepts, ["json"]
|
||||||
end
|
end
|
||||||
|
|
||||||
pipeline :feeds do
|
scope "/", PinchflatWeb do
|
||||||
plug :maybe_basic_auth
|
pipe_through [:maybe_basic_auth, :token_protected_route]
|
||||||
|
|
||||||
|
# has to match before /sources/:id
|
||||||
|
get "/sources/opml", Podcasts.PodcastController, :opml_feed
|
||||||
|
end
|
||||||
|
|
||||||
|
# Routes in here _may not be_ protected by basic auth. This is necessary for
|
||||||
|
# media streaming to work for RSS podcast feeds.
|
||||||
|
scope "/", PinchflatWeb do
|
||||||
|
pipe_through :maybe_basic_auth
|
||||||
|
|
||||||
|
get "/sources/:uuid/feed", Podcasts.PodcastController, :rss_feed
|
||||||
|
get "/sources/:uuid/feed_image", Podcasts.PodcastController, :feed_image
|
||||||
|
get "/media/:uuid/episode_image", Podcasts.PodcastController, :episode_image
|
||||||
|
|
||||||
|
get "/media/:uuid/stream", MediaItems.MediaItemController, :stream
|
||||||
end
|
end
|
||||||
|
|
||||||
scope "/", PinchflatWeb do
|
scope "/", PinchflatWeb do
|
||||||
@@ -48,18 +64,6 @@ defmodule PinchflatWeb.Router do
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
# Routes in here _may not be_ protected by basic auth. This is necessary for
|
|
||||||
# media streaming to work for RSS podcast feeds.
|
|
||||||
scope "/", PinchflatWeb do
|
|
||||||
pipe_through :feeds
|
|
||||||
|
|
||||||
get "/sources/:uuid/feed", Podcasts.PodcastController, :rss_feed
|
|
||||||
get "/sources/:uuid/feed_image", Podcasts.PodcastController, :feed_image
|
|
||||||
get "/media/:uuid/episode_image", Podcasts.PodcastController, :episode_image
|
|
||||||
|
|
||||||
get "/media/:uuid/stream", MediaItems.MediaItemController, :stream
|
|
||||||
end
|
|
||||||
|
|
||||||
# No auth or CSRF protection for the health check endpoint
|
# No auth or CSRF protection for the health check endpoint
|
||||||
scope "/", PinchflatWeb do
|
scope "/", PinchflatWeb do
|
||||||
pipe_through :api
|
pipe_through :api
|
||||||
@@ -74,31 +78,4 @@ defmodule PinchflatWeb.Router do
|
|||||||
metrics: PinchflatWeb.Telemetry,
|
metrics: PinchflatWeb.Telemetry,
|
||||||
ecto_repos: [Pinchflat.Repo]
|
ecto_repos: [Pinchflat.Repo]
|
||||||
end
|
end
|
||||||
|
|
||||||
defp maybe_basic_auth(conn, opts) do
|
|
||||||
if Application.get_env(:pinchflat, :expose_feed_endpoints) do
|
|
||||||
conn
|
|
||||||
else
|
|
||||||
basic_auth(conn, opts)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
defp basic_auth(conn, _opts) do
|
|
||||||
username = Application.get_env(:pinchflat, :basic_auth_username)
|
|
||||||
password = Application.get_env(:pinchflat, :basic_auth_password)
|
|
||||||
|
|
||||||
if credential_set?(username) && credential_set?(password) do
|
|
||||||
Plug.BasicAuth.basic_auth(conn, username: username, password: password, realm: "Pinchflat")
|
|
||||||
else
|
|
||||||
conn
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
defp credential_set?(credential) do
|
|
||||||
credential && credential != ""
|
|
||||||
end
|
|
||||||
|
|
||||||
defp allow_iframe_embed(conn, _opts) do
|
|
||||||
delete_resp_header(conn, "x-frame-options")
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ defmodule Pinchflat.MixProject do
|
|||||||
def project do
|
def project do
|
||||||
[
|
[
|
||||||
app: :pinchflat,
|
app: :pinchflat,
|
||||||
version: "2024.11.27",
|
version: "2025.1.3",
|
||||||
elixir: "~> 1.17",
|
elixir: "~> 1.17",
|
||||||
elixirc_paths: elixirc_paths(Mix.env()),
|
elixirc_paths: elixirc_paths(Mix.env()),
|
||||||
start_permanent: Mix.env() == :prod,
|
start_permanent: Mix.env() == :prod,
|
||||||
@@ -54,8 +54,8 @@ defmodule Pinchflat.MixProject do
|
|||||||
{:ecto_sqlite3_extras, "~> 1.2.0"},
|
{:ecto_sqlite3_extras, "~> 1.2.0"},
|
||||||
{:phoenix_html, "~> 3.3"},
|
{:phoenix_html, "~> 3.3"},
|
||||||
{:phoenix_live_reload, "~> 1.2", only: :dev},
|
{:phoenix_live_reload, "~> 1.2", only: :dev},
|
||||||
{:phoenix_live_view, "~> 0.20.1"},
|
{:phoenix_live_view, "~> 1.0.0"},
|
||||||
{:floki, ">= 0.30.0", only: :test},
|
{:floki, ">= 0.36.0", only: :test},
|
||||||
{:phoenix_live_dashboard, "~> 0.8.2"},
|
{:phoenix_live_dashboard, "~> 0.8.2"},
|
||||||
{:esbuild, "~> 0.8", runtime: Mix.env() == :dev},
|
{:esbuild, "~> 0.8", runtime: Mix.env() == :dev},
|
||||||
{:tailwind, "~> 0.2.0", runtime: Mix.env() == :dev},
|
{:tailwind, "~> 0.2.0", runtime: Mix.env() == :dev},
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
%{
|
%{
|
||||||
"bunt": {:hex, :bunt, "1.0.0", "081c2c665f086849e6d57900292b3a161727ab40431219529f13c4ddcf3e7a44", [:mix], [], "hexpm", "dc5f86aa08a5f6fa6b8096f0735c4e76d54ae5c9fa2c143e5a1fc7c1cd9bb6b5"},
|
"bunt": {:hex, :bunt, "1.0.0", "081c2c665f086849e6d57900292b3a161727ab40431219529f13c4ddcf3e7a44", [:mix], [], "hexpm", "dc5f86aa08a5f6fa6b8096f0735c4e76d54ae5c9fa2c143e5a1fc7c1cd9bb6b5"},
|
||||||
"castore": {:hex, :castore, "1.0.8", "dedcf20ea746694647f883590b82d9e96014057aff1d44d03ec90f36a5c0dc6e", [:mix], [], "hexpm", "0b2b66d2ee742cb1d9cb8c8be3b43c3a70ee8651f37b75a8b982e036752983f1"},
|
"castore": {:hex, :castore, "1.0.10", "43bbeeac820f16c89f79721af1b3e092399b3a1ecc8df1a472738fd853574911", [:mix], [], "hexpm", "1b0b7ea14d889d9ea21202c43a4fa015eb913021cb535e8ed91946f4b77a8848"},
|
||||||
"cc_precompiler": {:hex, :cc_precompiler, "0.1.10", "47c9c08d8869cf09b41da36538f62bc1abd3e19e41701c2cea2675b53c704258", [:mix], [{:elixir_make, "~> 0.7", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "f6e046254e53cd6b41c6bacd70ae728011aa82b2742a80d6e2214855c6e06b22"},
|
"cc_precompiler": {:hex, :cc_precompiler, "0.1.10", "47c9c08d8869cf09b41da36538f62bc1abd3e19e41701c2cea2675b53c704258", [:mix], [{:elixir_make, "~> 0.7", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "f6e046254e53cd6b41c6bacd70ae728011aa82b2742a80d6e2214855c6e06b22"},
|
||||||
"certifi": {:hex, :certifi, "2.12.0", "2d1cca2ec95f59643862af91f001478c9863c2ac9cb6e2f89780bfd8de987329", [:rebar3], [], "hexpm", "ee68d85df22e554040cdb4be100f33873ac6051387baf6a8f6ce82272340ff1c"},
|
"certifi": {:hex, :certifi, "2.12.0", "2d1cca2ec95f59643862af91f001478c9863c2ac9cb6e2f89780bfd8de987329", [:rebar3], [], "hexpm", "ee68d85df22e554040cdb4be100f33873ac6051387baf6a8f6ce82272340ff1c"},
|
||||||
"combine": {:hex, :combine, "0.10.0", "eff8224eeb56498a2af13011d142c5e7997a80c8f5b97c499f84c841032e429f", [:mix], [], "hexpm", "1b1dbc1790073076580d0d1d64e42eae2366583e7aecd455d1215b0d16f2451b"},
|
"combine": {:hex, :combine, "0.10.0", "eff8224eeb56498a2af13011d142c5e7997a80c8f5b97c499f84c841032e429f", [:mix], [], "hexpm", "1b1dbc1790073076580d0d1d64e42eae2366583e7aecd455d1215b0d16f2451b"},
|
||||||
@@ -10,7 +10,7 @@
|
|||||||
"credo": {:hex, :credo, "1.7.7", "771445037228f763f9b2afd612b6aa2fd8e28432a95dbbc60d8e03ce71ba4446", [:mix], [{:bunt, "~> 0.2.1 or ~> 1.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:file_system, "~> 0.2 or ~> 1.0", [hex: :file_system, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "8bc87496c9aaacdc3f90f01b7b0582467b69b4bd2441fe8aae3109d843cc2f2e"},
|
"credo": {:hex, :credo, "1.7.7", "771445037228f763f9b2afd612b6aa2fd8e28432a95dbbc60d8e03ce71ba4446", [:mix], [{:bunt, "~> 0.2.1 or ~> 1.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:file_system, "~> 0.2 or ~> 1.0", [hex: :file_system, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "8bc87496c9aaacdc3f90f01b7b0582467b69b4bd2441fe8aae3109d843cc2f2e"},
|
||||||
"credo_naming": {:hex, :credo_naming, "2.1.0", "d44ad58890d4db552e141ce64756a74ac1573665af766d1ac64931aa90d47744", [:make, :mix], [{:credo, "~> 1.6", [hex: :credo, repo: "hexpm", optional: false]}], "hexpm", "830e23b3fba972e2fccec49c0c089fe78c1e64bc16782a2682d78082351a2909"},
|
"credo_naming": {:hex, :credo_naming, "2.1.0", "d44ad58890d4db552e141ce64756a74ac1573665af766d1ac64931aa90d47744", [:make, :mix], [{:credo, "~> 1.6", [hex: :credo, repo: "hexpm", optional: false]}], "hexpm", "830e23b3fba972e2fccec49c0c089fe78c1e64bc16782a2682d78082351a2909"},
|
||||||
"db_connection": {:hex, :db_connection, "2.7.0", "b99faa9291bb09892c7da373bb82cba59aefa9b36300f6145c5f201c7adf48ec", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "dcf08f31b2701f857dfc787fbad78223d61a32204f217f15e881dd93e4bdd3ff"},
|
"db_connection": {:hex, :db_connection, "2.7.0", "b99faa9291bb09892c7da373bb82cba59aefa9b36300f6145c5f201c7adf48ec", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "dcf08f31b2701f857dfc787fbad78223d61a32204f217f15e881dd93e4bdd3ff"},
|
||||||
"decimal": {:hex, :decimal, "2.1.1", "5611dca5d4b2c3dd497dec8f68751f1f1a54755e8ed2a966c2633cf885973ad6", [:mix], [], "hexpm", "53cfe5f497ed0e7771ae1a475575603d77425099ba5faef9394932b35020ffcc"},
|
"decimal": {:hex, :decimal, "2.2.0", "df3d06bb9517e302b1bd265c1e7f16cda51547ad9d99892049340841f3e15836", [:mix], [], "hexpm", "af8daf87384b51b7e611fb1a1f2c4d4876b65ef968fa8bd3adf44cff401c7f21"},
|
||||||
"dns_cluster": {:hex, :dns_cluster, "0.1.2", "3eb5be824c7888dadf9781018e1a5f1d3d1113b333c50bce90fb1b83df1015f2", [:mix], [], "hexpm", "7494272040f847637bbdb01bcdf4b871e82daf09b813e7d3cb3b84f112c6f2f8"},
|
"dns_cluster": {:hex, :dns_cluster, "0.1.2", "3eb5be824c7888dadf9781018e1a5f1d3d1113b333c50bce90fb1b83df1015f2", [:mix], [], "hexpm", "7494272040f847637bbdb01bcdf4b871e82daf09b813e7d3cb3b84f112c6f2f8"},
|
||||||
"ecto": {:hex, :ecto, "3.12.3", "1a9111560731f6c3606924c81c870a68a34c819f6d4f03822f370ea31a582208", [:mix], [{:decimal, "~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "9efd91506ae722f95e48dc49e70d0cb632ede3b7a23896252a60a14ac6d59165"},
|
"ecto": {:hex, :ecto, "3.12.3", "1a9111560731f6c3606924c81c870a68a34c819f6d4f03822f370ea31a582208", [:mix], [{:decimal, "~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "9efd91506ae722f95e48dc49e70d0cb632ede3b7a23896252a60a14ac6d59165"},
|
||||||
"ecto_sql": {:hex, :ecto_sql, "3.12.0", "73cea17edfa54bde76ee8561b30d29ea08f630959685006d9c6e7d1e59113b7d", [:mix], [{:db_connection, "~> 2.4.1 or ~> 2.5", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.12", [hex: :ecto, repo: "hexpm", optional: false]}, {:myxql, "~> 0.7", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.19 or ~> 1.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:tds, "~> 2.1.1 or ~> 2.2", [hex: :tds, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.0 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "dc9e4d206f274f3947e96142a8fdc5f69a2a6a9abb4649ef5c882323b6d512f0"},
|
"ecto_sql": {:hex, :ecto_sql, "3.12.0", "73cea17edfa54bde76ee8561b30d29ea08f630959685006d9c6e7d1e59113b7d", [:mix], [{:db_connection, "~> 2.4.1 or ~> 2.5", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.12", [hex: :ecto, repo: "hexpm", optional: false]}, {:myxql, "~> 0.7", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.19 or ~> 1.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:tds, "~> 2.1.1 or ~> 2.2", [hex: :tds, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.0 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "dc9e4d206f274f3947e96142a8fdc5f69a2a6a9abb4649ef5c882323b6d512f0"},
|
||||||
@@ -24,7 +24,7 @@
|
|||||||
"faker": {:hex, :faker, "0.17.0", "671019d0652f63aefd8723b72167ecdb284baf7d47ad3a82a15e9b8a6df5d1fa", [:mix], [], "hexpm", "a7d4ad84a93fd25c5f5303510753789fc2433ff241bf3b4144d3f6f291658a6a"},
|
"faker": {:hex, :faker, "0.17.0", "671019d0652f63aefd8723b72167ecdb284baf7d47ad3a82a15e9b8a6df5d1fa", [:mix], [], "hexpm", "a7d4ad84a93fd25c5f5303510753789fc2433ff241bf3b4144d3f6f291658a6a"},
|
||||||
"file_system": {:hex, :file_system, "0.2.10", "fb082005a9cd1711c05b5248710f8826b02d7d1784e7c3451f9c1231d4fc162d", [:mix], [], "hexpm", "41195edbfb562a593726eda3b3e8b103a309b733ad25f3d642ba49696bf715dc"},
|
"file_system": {:hex, :file_system, "0.2.10", "fb082005a9cd1711c05b5248710f8826b02d7d1784e7c3451f9c1231d4fc162d", [:mix], [], "hexpm", "41195edbfb562a593726eda3b3e8b103a309b733ad25f3d642ba49696bf715dc"},
|
||||||
"finch": {:hex, :finch, "0.17.0", "17d06e1d44d891d20dbd437335eebe844e2426a0cd7e3a3e220b461127c73f70", [:mix], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: false]}, {:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mint, "~> 1.3", [hex: :mint, repo: "hexpm", optional: false]}, {:nimble_options, "~> 0.4 or ~> 1.0", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:nimble_pool, "~> 0.2.6 or ~> 1.0", [hex: :nimble_pool, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "8d014a661bb6a437263d4b5abf0bcbd3cf0deb26b1e8596f2a271d22e48934c7"},
|
"finch": {:hex, :finch, "0.17.0", "17d06e1d44d891d20dbd437335eebe844e2426a0cd7e3a3e220b461127c73f70", [:mix], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: false]}, {:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mint, "~> 1.3", [hex: :mint, repo: "hexpm", optional: false]}, {:nimble_options, "~> 0.4 or ~> 1.0", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:nimble_pool, "~> 0.2.6 or ~> 1.0", [hex: :nimble_pool, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "8d014a661bb6a437263d4b5abf0bcbd3cf0deb26b1e8596f2a271d22e48934c7"},
|
||||||
"floki": {:hex, :floki, "0.35.2", "87f8c75ed8654b9635b311774308b2760b47e9a579dabf2e4d5f1e1d42c39e0b", [:mix], [], "hexpm", "6b05289a8e9eac475f644f09c2e4ba7e19201fd002b89c28c1293e7bd16773d9"},
|
"floki": {:hex, :floki, "0.36.3", "1102f93b16a55bc5383b85ae3ec470f82dee056eaeff9195e8afdf0ef2a43c30", [:mix], [], "hexpm", "fe0158bff509e407735f6d40b3ee0d7deb47f3f3ee7c6c182ad28599f9f6b27a"},
|
||||||
"gettext": {:hex, :gettext, "0.24.0", "6f4d90ac5f3111673cbefc4ebee96fe5f37a114861ab8c7b7d5b30a1108ce6d8", [:mix], [{:expo, "~> 0.5.1", [hex: :expo, repo: "hexpm", optional: false]}], "hexpm", "bdf75cdfcbe9e4622dd18e034b227d77dd17f0f133853a1c73b97b3d6c770e8b"},
|
"gettext": {:hex, :gettext, "0.24.0", "6f4d90ac5f3111673cbefc4ebee96fe5f37a114861ab8c7b7d5b30a1108ce6d8", [:mix], [{:expo, "~> 0.5.1", [hex: :expo, repo: "hexpm", optional: false]}], "hexpm", "bdf75cdfcbe9e4622dd18e034b227d77dd17f0f133853a1c73b97b3d6c770e8b"},
|
||||||
"hackney": {:hex, :hackney, "1.20.1", "8d97aec62ddddd757d128bfd1df6c5861093419f8f7a4223823537bad5d064e2", [:rebar3], [{:certifi, "~> 2.12.0", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "~> 6.1.0", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "~> 1.0.0", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "~> 1.1", [hex: :mimerl, repo: "hexpm", optional: false]}, {:parse_trans, "3.4.1", [hex: :parse_trans, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "~> 1.1.0", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}, {:unicode_util_compat, "~> 0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "fe9094e5f1a2a2c0a7d10918fee36bfec0ec2a979994cff8cfe8058cd9af38e3"},
|
"hackney": {:hex, :hackney, "1.20.1", "8d97aec62ddddd757d128bfd1df6c5861093419f8f7a4223823537bad5d064e2", [:rebar3], [{:certifi, "~> 2.12.0", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "~> 6.1.0", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "~> 1.0.0", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "~> 1.1", [hex: :mimerl, repo: "hexpm", optional: false]}, {:parse_trans, "3.4.1", [hex: :parse_trans, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "~> 1.1.0", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}, {:unicode_util_compat, "~> 0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "fe9094e5f1a2a2c0a7d10918fee36bfec0ec2a979994cff8cfe8058cd9af38e3"},
|
||||||
"hpax": {:hex, :hpax, "0.1.2", "09a75600d9d8bbd064cdd741f21fc06fc1f4cf3d0fcc335e5aa19be1a7235c84", [:mix], [], "hexpm", "2c87843d5a23f5f16748ebe77969880e29809580efdaccd615cd3bed628a8c13"},
|
"hpax": {:hex, :hpax, "0.1.2", "09a75600d9d8bbd064cdd741f21fc06fc1f4cf3d0fcc335e5aa19be1a7235c84", [:mix], [], "hexpm", "2c87843d5a23f5f16748ebe77969880e29809580efdaccd615cd3bed628a8c13"},
|
||||||
@@ -40,12 +40,12 @@
|
|||||||
"nimble_pool": {:hex, :nimble_pool, "1.0.0", "5eb82705d138f4dd4423f69ceb19ac667b3b492ae570c9f5c900bb3d2f50a847", [:mix], [], "hexpm", "80be3b882d2d351882256087078e1b1952a28bf98d0a287be87e4a24a710b67a"},
|
"nimble_pool": {:hex, :nimble_pool, "1.0.0", "5eb82705d138f4dd4423f69ceb19ac667b3b492ae570c9f5c900bb3d2f50a847", [:mix], [], "hexpm", "80be3b882d2d351882256087078e1b1952a28bf98d0a287be87e4a24a710b67a"},
|
||||||
"oban": {:hex, :oban, "2.18.3", "1608c04f8856c108555c379f2f56bc0759149d35fa9d3b825cb8a6769f8ae926", [:mix], [{:ecto_sql, "~> 3.10", [hex: :ecto_sql, repo: "hexpm", optional: false]}, {:ecto_sqlite3, "~> 0.9", [hex: :ecto_sqlite3, repo: "hexpm", optional: true]}, {:jason, "~> 1.1", [hex: :jason, repo: "hexpm", optional: false]}, {:postgrex, "~> 0.16", [hex: :postgrex, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "36ca6ca84ef6518f9c2c759ea88efd438a3c81d667ba23b02b062a0aa785475e"},
|
"oban": {:hex, :oban, "2.18.3", "1608c04f8856c108555c379f2f56bc0759149d35fa9d3b825cb8a6769f8ae926", [:mix], [{:ecto_sql, "~> 3.10", [hex: :ecto_sql, repo: "hexpm", optional: false]}, {:ecto_sqlite3, "~> 0.9", [hex: :ecto_sqlite3, repo: "hexpm", optional: true]}, {:jason, "~> 1.1", [hex: :jason, repo: "hexpm", optional: false]}, {:postgrex, "~> 0.16", [hex: :postgrex, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "36ca6ca84ef6518f9c2c759ea88efd438a3c81d667ba23b02b062a0aa785475e"},
|
||||||
"parse_trans": {:hex, :parse_trans, "3.4.1", "6e6aa8167cb44cc8f39441d05193be6e6f4e7c2946cb2759f015f8c56b76e5ff", [:rebar3], [], "hexpm", "620a406ce75dada827b82e453c19cf06776be266f5a67cff34e1ef2cbb60e49a"},
|
"parse_trans": {:hex, :parse_trans, "3.4.1", "6e6aa8167cb44cc8f39441d05193be6e6f4e7c2946cb2759f015f8c56b76e5ff", [:rebar3], [], "hexpm", "620a406ce75dada827b82e453c19cf06776be266f5a67cff34e1ef2cbb60e49a"},
|
||||||
"phoenix": {:hex, :phoenix, "1.7.14", "a7d0b3f1bc95987044ddada111e77bd7f75646a08518942c72a8440278ae7825", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 2.1", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.7", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:plug_crypto, "~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:websock_adapter, "~> 0.5.3", [hex: :websock_adapter, repo: "hexpm", optional: false]}], "hexpm", "c7859bc56cc5dfef19ecfc240775dae358cbaa530231118a9e014df392ace61a"},
|
"phoenix": {:hex, :phoenix, "1.7.17", "2fcdceecc6fb90bec26fab008f96abbd0fd93bc9956ec7985e5892cf545152ca", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 2.1", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.7", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:plug_crypto, "~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:websock_adapter, "~> 0.5.3", [hex: :websock_adapter, repo: "hexpm", optional: false]}], "hexpm", "50e8ad537f3f7b0efb1509b2f75b5c918f697be6a45d48e49a30d3b7c0e464c9"},
|
||||||
"phoenix_ecto": {:hex, :phoenix_ecto, "4.4.3", "86e9878f833829c3f66da03d75254c155d91d72a201eb56ae83482328dc7ca93", [:mix], [{:ecto, "~> 3.5", [hex: :ecto, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 2.14.2 or ~> 3.0 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:plug, "~> 1.9", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "d36c401206f3011fefd63d04e8ef626ec8791975d9d107f9a0817d426f61ac07"},
|
"phoenix_ecto": {:hex, :phoenix_ecto, "4.4.3", "86e9878f833829c3f66da03d75254c155d91d72a201eb56ae83482328dc7ca93", [:mix], [{:ecto, "~> 3.5", [hex: :ecto, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 2.14.2 or ~> 3.0 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:plug, "~> 1.9", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "d36c401206f3011fefd63d04e8ef626ec8791975d9d107f9a0817d426f61ac07"},
|
||||||
"phoenix_html": {:hex, :phoenix_html, "3.3.4", "42a09fc443bbc1da37e372a5c8e6755d046f22b9b11343bf885067357da21cb3", [:mix], [{:plug, "~> 1.5", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "0249d3abec3714aff3415e7ee3d9786cb325be3151e6c4b3021502c585bf53fb"},
|
"phoenix_html": {:hex, :phoenix_html, "3.3.4", "42a09fc443bbc1da37e372a5c8e6755d046f22b9b11343bf885067357da21cb3", [:mix], [{:plug, "~> 1.5", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "0249d3abec3714aff3415e7ee3d9786cb325be3151e6c4b3021502c585bf53fb"},
|
||||||
"phoenix_live_dashboard": {:hex, :phoenix_live_dashboard, "0.8.3", "7ff51c9b6609470f681fbea20578dede0e548302b0c8bdf338b5a753a4f045bf", [:mix], [{:ecto, "~> 3.6.2 or ~> 3.7", [hex: :ecto, repo: "hexpm", optional: true]}, {:ecto_mysql_extras, "~> 0.5", [hex: :ecto_mysql_extras, repo: "hexpm", optional: true]}, {:ecto_psql_extras, "~> 0.7", [hex: :ecto_psql_extras, repo: "hexpm", optional: true]}, {:ecto_sqlite3_extras, "~> 1.1.7 or ~> 1.2.0", [hex: :ecto_sqlite3_extras, repo: "hexpm", optional: true]}, {:mime, "~> 1.6 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:phoenix_live_view, "~> 0.19 or ~> 1.0", [hex: :phoenix_live_view, repo: "hexpm", optional: false]}, {:telemetry_metrics, "~> 0.6 or ~> 1.0", [hex: :telemetry_metrics, repo: "hexpm", optional: false]}], "hexpm", "f9470a0a8bae4f56430a23d42f977b5a6205fdba6559d76f932b876bfaec652d"},
|
"phoenix_live_dashboard": {:hex, :phoenix_live_dashboard, "0.8.3", "7ff51c9b6609470f681fbea20578dede0e548302b0c8bdf338b5a753a4f045bf", [:mix], [{:ecto, "~> 3.6.2 or ~> 3.7", [hex: :ecto, repo: "hexpm", optional: true]}, {:ecto_mysql_extras, "~> 0.5", [hex: :ecto_mysql_extras, repo: "hexpm", optional: true]}, {:ecto_psql_extras, "~> 0.7", [hex: :ecto_psql_extras, repo: "hexpm", optional: true]}, {:ecto_sqlite3_extras, "~> 1.1.7 or ~> 1.2.0", [hex: :ecto_sqlite3_extras, repo: "hexpm", optional: true]}, {:mime, "~> 1.6 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:phoenix_live_view, "~> 0.19 or ~> 1.0", [hex: :phoenix_live_view, repo: "hexpm", optional: false]}, {:telemetry_metrics, "~> 0.6 or ~> 1.0", [hex: :telemetry_metrics, repo: "hexpm", optional: false]}], "hexpm", "f9470a0a8bae4f56430a23d42f977b5a6205fdba6559d76f932b876bfaec652d"},
|
||||||
"phoenix_live_reload": {:hex, :phoenix_live_reload, "1.4.1", "2aff698f5e47369decde4357ba91fc9c37c6487a512b41732818f2204a8ef1d3", [:mix], [{:file_system, "~> 0.2.1 or ~> 0.3", [hex: :file_system, repo: "hexpm", optional: false]}, {:phoenix, "~> 1.4", [hex: :phoenix, repo: "hexpm", optional: false]}], "hexpm", "9bffb834e7ddf08467fe54ae58b5785507aaba6255568ae22b4d46e2bb3615ab"},
|
"phoenix_live_reload": {:hex, :phoenix_live_reload, "1.4.1", "2aff698f5e47369decde4357ba91fc9c37c6487a512b41732818f2204a8ef1d3", [:mix], [{:file_system, "~> 0.2.1 or ~> 0.3", [hex: :file_system, repo: "hexpm", optional: false]}, {:phoenix, "~> 1.4", [hex: :phoenix, repo: "hexpm", optional: false]}], "hexpm", "9bffb834e7ddf08467fe54ae58b5785507aaba6255568ae22b4d46e2bb3615ab"},
|
||||||
"phoenix_live_view": {:hex, :phoenix_live_view, "0.20.3", "8b6406bc0a451f295407d7acff7f234a6314be5bbe0b3f90ed82b07f50049878", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix, "~> 1.6.15 or ~> 1.7.0", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 3.3 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: true]}, {:plug, "~> 1.15", [hex: :plug, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.2 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "a8e4385e05618b424779f894ed2df97d3c7518b7285fcd11979077ae6226466b"},
|
"phoenix_live_view": {:hex, :phoenix_live_view, "1.0.0", "3a10dfce8f87b2ad4dc65de0732fc2a11e670b2779a19e8d3281f4619a85bce4", [:mix], [{:floki, "~> 0.36", [hex: :floki, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix, "~> 1.6.15 or ~> 1.7.0", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 3.3 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: true]}, {:plug, "~> 1.15", [hex: :plug, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.2 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "254caef0028765965ca6bd104cc7d68dcc7d57cc42912bef92f6b03047251d99"},
|
||||||
"phoenix_pubsub": {:hex, :phoenix_pubsub, "2.1.3", "3168d78ba41835aecad272d5e8cd51aa87a7ac9eb836eabc42f6e57538e3731d", [:mix], [], "hexpm", "bba06bc1dcfd8cb086759f0edc94a8ba2bc8896d5331a1e2c2902bf8e36ee502"},
|
"phoenix_pubsub": {:hex, :phoenix_pubsub, "2.1.3", "3168d78ba41835aecad272d5e8cd51aa87a7ac9eb836eabc42f6e57538e3731d", [:mix], [], "hexpm", "bba06bc1dcfd8cb086759f0edc94a8ba2bc8896d5331a1e2c2902bf8e36ee502"},
|
||||||
"phoenix_template": {:hex, :phoenix_template, "1.0.4", "e2092c132f3b5e5b2d49c96695342eb36d0ed514c5b252a77048d5969330d639", [:mix], [{:phoenix_html, "~> 2.14.2 or ~> 3.0 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}], "hexpm", "2c0c81f0e5c6753faf5cca2f229c9709919aba34fab866d3bc05060c9c444206"},
|
"phoenix_template": {:hex, :phoenix_template, "1.0.4", "e2092c132f3b5e5b2d49c96695342eb36d0ed514c5b252a77048d5969330d639", [:mix], [{:phoenix_html, "~> 2.14.2 or ~> 3.0 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}], "hexpm", "2c0c81f0e5c6753faf5cca2f229c9709919aba34fab866d3bc05060c9c444206"},
|
||||||
"plug": {:hex, :plug, "1.16.1", "40c74619c12f82736d2214557dedec2e9762029b2438d6d175c5074c933edc9d", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "a13ff6b9006b03d7e33874945b2755253841b238c34071ed85b0e86057f8cddc"},
|
"plug": {:hex, :plug, "1.16.1", "40c74619c12f82736d2214557dedec2e9762029b2438d6d175c5074c933edc9d", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "a13ff6b9006b03d7e33874945b2755253841b238c34071ed85b0e86057f8cddc"},
|
||||||
@@ -64,5 +64,5 @@
|
|||||||
"tzdata": {:hex, :tzdata, "1.1.2", "45e5f1fcf8729525ec27c65e163be5b3d247ab1702581a94674e008413eef50b", [:mix], [{:hackney, "~> 1.17", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm", "cec7b286e608371602318c414f344941d5eb0375e14cfdab605cca2fe66cba8b"},
|
"tzdata": {:hex, :tzdata, "1.1.2", "45e5f1fcf8729525ec27c65e163be5b3d247ab1702581a94674e008413eef50b", [:mix], [{:hackney, "~> 1.17", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm", "cec7b286e608371602318c414f344941d5eb0375e14cfdab605cca2fe66cba8b"},
|
||||||
"unicode_util_compat": {:hex, :unicode_util_compat, "0.7.0", "bc84380c9ab48177092f43ac89e4dfa2c6d62b40b8bd132b1059ecc7232f9a78", [:rebar3], [], "hexpm", "25eee6d67df61960cf6a794239566599b09e17e668d3700247bc498638152521"},
|
"unicode_util_compat": {:hex, :unicode_util_compat, "0.7.0", "bc84380c9ab48177092f43ac89e4dfa2c6d62b40b8bd132b1059ecc7232f9a78", [:rebar3], [], "hexpm", "25eee6d67df61960cf6a794239566599b09e17e668d3700247bc498638152521"},
|
||||||
"websock": {:hex, :websock, "0.5.3", "2f69a6ebe810328555b6fe5c831a851f485e303a7c8ce6c5f675abeb20ebdadc", [:mix], [], "hexpm", "6105453d7fac22c712ad66fab1d45abdf049868f253cf719b625151460b8b453"},
|
"websock": {:hex, :websock, "0.5.3", "2f69a6ebe810328555b6fe5c831a851f485e303a7c8ce6c5f675abeb20ebdadc", [:mix], [], "hexpm", "6105453d7fac22c712ad66fab1d45abdf049868f253cf719b625151460b8b453"},
|
||||||
"websock_adapter": {:hex, :websock_adapter, "0.5.7", "65fa74042530064ef0570b75b43f5c49bb8b235d6515671b3d250022cb8a1f9e", [:mix], [{:bandit, ">= 0.6.0", [hex: :bandit, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.6", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:websock, "~> 0.5", [hex: :websock, repo: "hexpm", optional: false]}], "hexpm", "d0f478ee64deddfec64b800673fd6e0c8888b079d9f3444dd96d2a98383bdbd1"},
|
"websock_adapter": {:hex, :websock_adapter, "0.5.8", "3b97dc94e407e2d1fc666b2fb9acf6be81a1798a2602294aac000260a7c4a47d", [:mix], [{:bandit, ">= 0.6.0", [hex: :bandit, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.6", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:websock, "~> 0.5", [hex: :websock, repo: "hexpm", optional: false]}], "hexpm", "315b9a1865552212b5f35140ad194e67ce31af45bcee443d4ecb96b5fd3f3782"},
|
||||||
}
|
}
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 438 KiB After Width: | Height: | Size: 442 KiB |
@@ -0,0 +1,11 @@
|
|||||||
|
defmodule Pinchflat.Repo.Migrations.AddRouteTokenToSettings do
|
||||||
|
use Ecto.Migration
|
||||||
|
|
||||||
|
def change do
|
||||||
|
alter table(:settings) do
|
||||||
|
add :route_token, :string, null: false, default: "tmp-token"
|
||||||
|
end
|
||||||
|
|
||||||
|
execute "UPDATE settings SET route_token = gen_random_uuid();", "SELECT 1;"
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -287,7 +287,19 @@ defmodule Pinchflat.Downloading.DownloadOptionBuilderTest do
|
|||||||
assert {:sponsorblock_remove, "sponsor,intro"} in res
|
assert {:sponsorblock_remove, "sponsor,intro"} in res
|
||||||
end
|
end
|
||||||
|
|
||||||
test "does not include :sponsorblock_remove option without categories", %{media_item: media_item} do
|
test "includes :sponsorblock_mark option when specified", %{media_item: media_item} do
|
||||||
|
media_item =
|
||||||
|
update_media_profile_attribute(media_item, %{
|
||||||
|
sponsorblock_behaviour: :mark,
|
||||||
|
sponsorblock_categories: ["sponsor", "intro"]
|
||||||
|
})
|
||||||
|
|
||||||
|
assert {:ok, res} = DownloadOptionBuilder.build(media_item)
|
||||||
|
|
||||||
|
assert {:sponsorblock_mark, "sponsor,intro"} in res
|
||||||
|
end
|
||||||
|
|
||||||
|
test "does not include any sponsorblock option without categories", %{media_item: media_item} do
|
||||||
media_item =
|
media_item =
|
||||||
update_media_profile_attribute(media_item, %{
|
update_media_profile_attribute(media_item, %{
|
||||||
sponsorblock_behaviour: :remove,
|
sponsorblock_behaviour: :remove,
|
||||||
@@ -296,9 +308,10 @@ defmodule Pinchflat.Downloading.DownloadOptionBuilderTest do
|
|||||||
|
|
||||||
assert {:ok, res} = DownloadOptionBuilder.build(media_item)
|
assert {:ok, res} = DownloadOptionBuilder.build(media_item)
|
||||||
|
|
||||||
refute {:sponsorblock_remove, ""} in res
|
refute Keyword.has_key?(res, :sponsorblock_remove)
|
||||||
refute {:sponsorblock_remove, []} in res
|
refute Keyword.has_key?(res, :sponsorblock_mark)
|
||||||
refute :sponsorblock_remove in res
|
refute :sponsorblock_remove in res
|
||||||
|
refute :sponsorblock_mark in res
|
||||||
end
|
end
|
||||||
|
|
||||||
test "does not include any sponsorblock options when disabled", %{media_item: media_item} do
|
test "does not include any sponsorblock options when disabled", %{media_item: media_item} do
|
||||||
@@ -307,9 +320,10 @@ defmodule Pinchflat.Downloading.DownloadOptionBuilderTest do
|
|||||||
|
|
||||||
assert {:ok, res} = DownloadOptionBuilder.build(media_item)
|
assert {:ok, res} = DownloadOptionBuilder.build(media_item)
|
||||||
|
|
||||||
refute {:sponsorblock_remove, ""} in res
|
refute Keyword.has_key?(res, :sponsorblock_remove)
|
||||||
refute {:sponsorblock_remove, []} in res
|
refute Keyword.has_key?(res, :sponsorblock_mark)
|
||||||
refute :sponsorblock_remove in res
|
refute :sponsorblock_remove in res
|
||||||
|
refute :sponsorblock_mark in res
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -9,8 +9,12 @@ defmodule Pinchflat.Downloading.MediaDownloadWorkerTest do
|
|||||||
alias Pinchflat.Downloading.MediaDownloadWorker
|
alias Pinchflat.Downloading.MediaDownloadWorker
|
||||||
|
|
||||||
setup do
|
setup do
|
||||||
stub(YtDlpRunnerMock, :run, fn _url, _opts, _ot -> {:ok, "{}"} end)
|
stub(YtDlpRunnerMock, :run, fn
|
||||||
stub(YtDlpRunnerMock, :run, fn _url, _opts, _ot, _addl -> {:ok, ""} end)
|
_url, :get_downloadable_status, _opts, _ot, _addl -> {:ok, "{}"}
|
||||||
|
_url, :download_thumbnail, _opts, _ot, _addl -> {:ok, ""}
|
||||||
|
_url, :download, _opts, _ot, _addl -> {:ok, render_metadata(:media_metadata)}
|
||||||
|
end)
|
||||||
|
|
||||||
stub(UserScriptRunnerMock, :run, fn _event_type, _data -> {:ok, "", 0} end)
|
stub(UserScriptRunnerMock, :run, fn _event_type, _data -> {:ok, "", 0} end)
|
||||||
stub(HTTPClientMock, :get, fn _url, _headers, _opts -> {:ok, ""} end)
|
stub(HTTPClientMock, :get, fn _url, _headers, _opts -> {:ok, ""} end)
|
||||||
|
|
||||||
@@ -53,13 +57,17 @@ defmodule Pinchflat.Downloading.MediaDownloadWorkerTest do
|
|||||||
end
|
end
|
||||||
|
|
||||||
describe "perform/1" do
|
describe "perform/1" do
|
||||||
test "it saves attributes to the media_item", %{media_item: media_item} do
|
setup do
|
||||||
expect(YtDlpRunnerMock, :run, 1, fn _url, _opts, _ot, _addl ->
|
stub(YtDlpRunnerMock, :run, fn
|
||||||
{:ok, render_metadata(:media_metadata)}
|
_url, :get_downloadable_status, _opts, _ot, _addl -> {:ok, "{}"}
|
||||||
|
_url, :download, _opts, _ot, _addl -> {:ok, render_metadata(:media_metadata)}
|
||||||
|
_url, :download_thumbnail, _opts, _ot, _addl -> {:ok, ""}
|
||||||
end)
|
end)
|
||||||
|
|
||||||
expect(YtDlpRunnerMock, :run, 1, fn _url, _opts, _ot, _addl -> {:ok, ""} end)
|
:ok
|
||||||
|
end
|
||||||
|
|
||||||
|
test "it saves attributes to the media_item", %{media_item: media_item} do
|
||||||
assert media_item.media_filepath == nil
|
assert media_item.media_filepath == nil
|
||||||
perform_job(MediaDownloadWorker, %{id: media_item.id})
|
perform_job(MediaDownloadWorker, %{id: media_item.id})
|
||||||
media_item = Repo.reload(media_item)
|
media_item = Repo.reload(media_item)
|
||||||
@@ -68,12 +76,6 @@ defmodule Pinchflat.Downloading.MediaDownloadWorkerTest do
|
|||||||
end
|
end
|
||||||
|
|
||||||
test "it saves the metadata to the media_item", %{media_item: media_item} do
|
test "it saves the metadata to the media_item", %{media_item: media_item} do
|
||||||
expect(YtDlpRunnerMock, :run, 1, fn _url, _opts, _ot, _addl ->
|
|
||||||
{:ok, render_metadata(:media_metadata)}
|
|
||||||
end)
|
|
||||||
|
|
||||||
expect(YtDlpRunnerMock, :run, 1, fn _url, _opts, _ot, _addl -> {:ok, ""} end)
|
|
||||||
|
|
||||||
assert media_item.metadata == nil
|
assert media_item.metadata == nil
|
||||||
perform_job(MediaDownloadWorker, %{id: media_item.id})
|
perform_job(MediaDownloadWorker, %{id: media_item.id})
|
||||||
assert Repo.reload(media_item).metadata != nil
|
assert Repo.reload(media_item).metadata != nil
|
||||||
@@ -87,7 +89,10 @@ defmodule Pinchflat.Downloading.MediaDownloadWorkerTest do
|
|||||||
end
|
end
|
||||||
|
|
||||||
test "it sets the job to retryable if the download fails", %{media_item: media_item} do
|
test "it sets the job to retryable if the download fails", %{media_item: media_item} do
|
||||||
expect(YtDlpRunnerMock, :run, fn _url, _opts, _ot, _addl -> {:error, "error"} end)
|
expect(YtDlpRunnerMock, :run, 2, fn
|
||||||
|
_url, :get_downloadable_status, _opts, _ot, _addl -> {:ok, "{}"}
|
||||||
|
_url, :download, _opts, _ot, _addl -> {:error, "error"}
|
||||||
|
end)
|
||||||
|
|
||||||
Oban.Testing.with_testing_mode(:inline, fn ->
|
Oban.Testing.with_testing_mode(:inline, fn ->
|
||||||
{:ok, job} = Oban.insert(MediaDownloadWorker.new(%{id: media_item.id}))
|
{:ok, job} = Oban.insert(MediaDownloadWorker.new(%{id: media_item.id}))
|
||||||
@@ -97,8 +102,9 @@ defmodule Pinchflat.Downloading.MediaDownloadWorkerTest do
|
|||||||
end
|
end
|
||||||
|
|
||||||
test "sets the job to retryable if the download failed and was retried", %{media_item: media_item} do
|
test "sets the job to retryable if the download failed and was retried", %{media_item: media_item} do
|
||||||
expect(YtDlpRunnerMock, :run, fn _url, _opts, _ot, _addl ->
|
expect(YtDlpRunnerMock, :run, 2, fn
|
||||||
{:error, "Unable to communicate with SponsorBlock", 1}
|
_url, :get_downloadable_status, _opts, _ot, _addl -> {:ok, "{}"}
|
||||||
|
_url, :download, _opts, _ot, _addl -> {:error, "Unable to communicate with SponsorBlock", 1}
|
||||||
end)
|
end)
|
||||||
|
|
||||||
Oban.Testing.with_testing_mode(:inline, fn ->
|
Oban.Testing.with_testing_mode(:inline, fn ->
|
||||||
@@ -109,8 +115,9 @@ defmodule Pinchflat.Downloading.MediaDownloadWorkerTest do
|
|||||||
end
|
end
|
||||||
|
|
||||||
test "does not set the job to retryable if retrying wouldn't fix the issue", %{media_item: media_item} do
|
test "does not set the job to retryable if retrying wouldn't fix the issue", %{media_item: media_item} do
|
||||||
expect(YtDlpRunnerMock, :run, fn _url, _opts, _ot, _addl ->
|
expect(YtDlpRunnerMock, :run, 2, fn
|
||||||
{:error, "Something something Video unavailable something something", 1}
|
_url, :get_downloadable_status, _opts, _ot, _addl -> {:ok, "{}"}
|
||||||
|
_url, :download, _opts, _ot, _addl -> {:error, "Something something Video unavailable something something", 1}
|
||||||
end)
|
end)
|
||||||
|
|
||||||
Oban.Testing.with_testing_mode(:inline, fn ->
|
Oban.Testing.with_testing_mode(:inline, fn ->
|
||||||
@@ -121,13 +128,16 @@ defmodule Pinchflat.Downloading.MediaDownloadWorkerTest do
|
|||||||
end
|
end
|
||||||
|
|
||||||
test "it ensures error are returned in a 2-item tuple", %{media_item: media_item} do
|
test "it ensures error are returned in a 2-item tuple", %{media_item: media_item} do
|
||||||
expect(YtDlpRunnerMock, :run, fn _url, _opts, _ot, _addl -> {:error, "error", 1} end)
|
expect(YtDlpRunnerMock, :run, 2, fn
|
||||||
|
_url, :get_downloadable_status, _opts, _ot, _addl -> {:ok, "{}"}
|
||||||
|
_url, :download, _opts, _ot, _addl -> {:error, "error", 1}
|
||||||
|
end)
|
||||||
|
|
||||||
assert {:error, :download_failed} = perform_job(MediaDownloadWorker, %{id: media_item.id})
|
assert {:error, :download_failed} = perform_job(MediaDownloadWorker, %{id: media_item.id})
|
||||||
end
|
end
|
||||||
|
|
||||||
test "it does not download if the source is set to not download", %{media_item: media_item} do
|
test "it does not download if the source is set to not download", %{media_item: media_item} do
|
||||||
expect(YtDlpRunnerMock, :run, 0, fn _url, _opts, _ot, _addl -> :ok end)
|
expect(YtDlpRunnerMock, :run, 0, fn _url, :download, _opts, _ot, _addl -> :ok end)
|
||||||
|
|
||||||
Sources.update_source(media_item.source, %{download_media: false})
|
Sources.update_source(media_item.source, %{download_media: false})
|
||||||
|
|
||||||
@@ -135,7 +145,7 @@ defmodule Pinchflat.Downloading.MediaDownloadWorkerTest do
|
|||||||
end
|
end
|
||||||
|
|
||||||
test "does not download if the media item is set to not download", %{media_item: media_item} do
|
test "does not download if the media item is set to not download", %{media_item: media_item} do
|
||||||
expect(YtDlpRunnerMock, :run, 0, fn _url, _opts, _ot, _addl -> :ok end)
|
expect(YtDlpRunnerMock, :run, 0, fn _url, :download, _opts, _ot, _addl -> :ok end)
|
||||||
|
|
||||||
Media.update_media_item(media_item, %{prevent_download: true})
|
Media.update_media_item(media_item, %{prevent_download: true})
|
||||||
|
|
||||||
@@ -143,15 +153,20 @@ defmodule Pinchflat.Downloading.MediaDownloadWorkerTest do
|
|||||||
end
|
end
|
||||||
|
|
||||||
test "it saves the file's size to the database", %{media_item: media_item} do
|
test "it saves the file's size to the database", %{media_item: media_item} do
|
||||||
expect(YtDlpRunnerMock, :run, 1, fn _url, _opts, _ot, _addl ->
|
expect(YtDlpRunnerMock, :run, 3, fn
|
||||||
metadata = render_parsed_metadata(:media_metadata)
|
_url, :get_downloadable_status, _opts, _ot, _addl ->
|
||||||
FilesystemUtils.write_p!(metadata["filepath"], "test")
|
{:ok, "{}"}
|
||||||
|
|
||||||
{:ok, Phoenix.json_library().encode!(metadata)}
|
_url, :download, _opts, _ot, _addl ->
|
||||||
|
metadata = render_parsed_metadata(:media_metadata)
|
||||||
|
FilesystemUtils.write_p!(metadata["filepath"], "test")
|
||||||
|
|
||||||
|
{:ok, Phoenix.json_library().encode!(metadata)}
|
||||||
|
|
||||||
|
_url, :download_thumbnail, _opts, _ot, _addl ->
|
||||||
|
{:ok, ""}
|
||||||
end)
|
end)
|
||||||
|
|
||||||
expect(YtDlpRunnerMock, :run, 1, fn _url, _opts, _ot, _addl -> {:ok, ""} end)
|
|
||||||
|
|
||||||
perform_job(MediaDownloadWorker, %{id: media_item.id})
|
perform_job(MediaDownloadWorker, %{id: media_item.id})
|
||||||
media_item = Repo.reload(media_item)
|
media_item = Repo.reload(media_item)
|
||||||
|
|
||||||
@@ -159,10 +174,6 @@ defmodule Pinchflat.Downloading.MediaDownloadWorkerTest do
|
|||||||
end
|
end
|
||||||
|
|
||||||
test "does not set redownloaded_at by default", %{media_item: media_item} do
|
test "does not set redownloaded_at by default", %{media_item: media_item} do
|
||||||
expect(YtDlpRunnerMock, :run, 2, fn _url, _opts, _ot, _addl ->
|
|
||||||
{:ok, render_metadata(:media_metadata)}
|
|
||||||
end)
|
|
||||||
|
|
||||||
perform_job(MediaDownloadWorker, %{id: media_item.id})
|
perform_job(MediaDownloadWorker, %{id: media_item.id})
|
||||||
media_item = Repo.reload(media_item)
|
media_item = Repo.reload(media_item)
|
||||||
|
|
||||||
@@ -174,22 +185,27 @@ defmodule Pinchflat.Downloading.MediaDownloadWorkerTest do
|
|||||||
end
|
end
|
||||||
|
|
||||||
test "sets the no_force_overwrites runner option", %{media_item: media_item} do
|
test "sets the no_force_overwrites runner option", %{media_item: media_item} do
|
||||||
expect(YtDlpRunnerMock, :run, 1, fn _url, opts, _ot, _addl ->
|
expect(YtDlpRunnerMock, :run, 3, fn
|
||||||
assert :no_force_overwrites in opts
|
_url, :get_downloadable_status, _opts, _ot, _addl ->
|
||||||
refute :force_overwrites in opts
|
{:ok, "{}"}
|
||||||
|
|
||||||
{:ok, render_metadata(:media_metadata)}
|
_url, :download, opts, _ot, _addl ->
|
||||||
|
assert :no_force_overwrites in opts
|
||||||
|
refute :force_overwrites in opts
|
||||||
|
|
||||||
|
{:ok, render_metadata(:media_metadata)}
|
||||||
|
|
||||||
|
_url, :download_thumbnail, _opts, _ot, _addl ->
|
||||||
|
{:ok, ""}
|
||||||
end)
|
end)
|
||||||
|
|
||||||
expect(YtDlpRunnerMock, :run, 1, fn _url, _opts, _ot, _addl -> {:ok, ""} end)
|
|
||||||
|
|
||||||
perform_job(MediaDownloadWorker, %{id: media_item.id})
|
perform_job(MediaDownloadWorker, %{id: media_item.id})
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
describe "perform/1 when testing non-downloadable media" do
|
describe "perform/1 when testing non-downloadable media" do
|
||||||
test "does not retry the job if the media is currently not downloadable", %{media_item: media_item} do
|
test "does not retry the job if the media is currently not downloadable", %{media_item: media_item} do
|
||||||
expect(YtDlpRunnerMock, :run, fn _url, _opts, _ot ->
|
stub(YtDlpRunnerMock, :run, fn _url, :get_downloadable_status, _opts, _ot, _addl ->
|
||||||
{:ok, Phoenix.json_library().encode!(%{"live_status" => "is_live"})}
|
{:ok, Phoenix.json_library().encode!(%{"live_status" => "is_live"})}
|
||||||
end)
|
end)
|
||||||
|
|
||||||
@@ -203,8 +219,6 @@ defmodule Pinchflat.Downloading.MediaDownloadWorkerTest do
|
|||||||
|
|
||||||
describe "perform/1 when testing forced downloads" do
|
describe "perform/1 when testing forced downloads" do
|
||||||
test "ignores 'prevent_download' if forced", %{media_item: media_item} do
|
test "ignores 'prevent_download' if forced", %{media_item: media_item} do
|
||||||
expect(YtDlpRunnerMock, :run, fn _url, _opts, _ot, _addl -> :ok end)
|
|
||||||
|
|
||||||
Sources.update_source(media_item.source, %{download_media: false})
|
Sources.update_source(media_item.source, %{download_media: false})
|
||||||
Media.update_media_item(media_item, %{prevent_download: true})
|
Media.update_media_item(media_item, %{prevent_download: true})
|
||||||
|
|
||||||
@@ -212,27 +226,26 @@ defmodule Pinchflat.Downloading.MediaDownloadWorkerTest do
|
|||||||
end
|
end
|
||||||
|
|
||||||
test "sets force_overwrites runner option", %{media_item: media_item} do
|
test "sets force_overwrites runner option", %{media_item: media_item} do
|
||||||
expect(YtDlpRunnerMock, :run, 1, fn _url, opts, _ot, _addl ->
|
expect(YtDlpRunnerMock, :run, 3, fn
|
||||||
assert :force_overwrites in opts
|
_url, :get_downloadable_status, _opts, _ot, _addl ->
|
||||||
refute :no_force_overwrites in opts
|
{:ok, "{}"}
|
||||||
|
|
||||||
{:ok, render_metadata(:media_metadata)}
|
_url, :download, opts, _ot, _addl ->
|
||||||
|
assert :force_overwrites in opts
|
||||||
|
refute :no_force_overwrites in opts
|
||||||
|
|
||||||
|
{:ok, render_metadata(:media_metadata)}
|
||||||
|
|
||||||
|
_url, :download_thumbnail, _opts, _ot, _addl ->
|
||||||
|
{:ok, ""}
|
||||||
end)
|
end)
|
||||||
|
|
||||||
expect(YtDlpRunnerMock, :run, 1, fn _url, _opts, _ot, _addl -> {:ok, ""} end)
|
|
||||||
|
|
||||||
perform_job(MediaDownloadWorker, %{id: media_item.id, force: true})
|
perform_job(MediaDownloadWorker, %{id: media_item.id, force: true})
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
describe "perform/1 when testing re-downloads" do
|
describe "perform/1 when testing re-downloads" do
|
||||||
test "sets redownloaded_at on the media_item", %{media_item: media_item} do
|
test "sets redownloaded_at on the media_item", %{media_item: media_item} do
|
||||||
expect(YtDlpRunnerMock, :run, 1, fn _url, _opts, _ot, _addl ->
|
|
||||||
{:ok, render_metadata(:media_metadata)}
|
|
||||||
end)
|
|
||||||
|
|
||||||
expect(YtDlpRunnerMock, :run, 1, fn _url, _opts, _ot, _addl -> {:ok, ""} end)
|
|
||||||
|
|
||||||
perform_job(MediaDownloadWorker, %{id: media_item.id, quality_upgrade?: true})
|
perform_job(MediaDownloadWorker, %{id: media_item.id, quality_upgrade?: true})
|
||||||
media_item = Repo.reload(media_item)
|
media_item = Repo.reload(media_item)
|
||||||
|
|
||||||
@@ -240,29 +253,39 @@ defmodule Pinchflat.Downloading.MediaDownloadWorkerTest do
|
|||||||
end
|
end
|
||||||
|
|
||||||
test "sets force_overwrites runner option", %{media_item: media_item} do
|
test "sets force_overwrites runner option", %{media_item: media_item} do
|
||||||
expect(YtDlpRunnerMock, :run, 1, fn _url, opts, _ot, _addl ->
|
expect(YtDlpRunnerMock, :run, 3, fn
|
||||||
assert :force_overwrites in opts
|
_url, :get_downloadable_status, _opts, _ot, _addl ->
|
||||||
refute :no_force_overwrites in opts
|
{:ok, "{}"}
|
||||||
|
|
||||||
{:ok, render_metadata(:media_metadata)}
|
_url, :download, opts, _ot, _addl ->
|
||||||
|
assert :force_overwrites in opts
|
||||||
|
refute :no_force_overwrites in opts
|
||||||
|
|
||||||
|
{:ok, render_metadata(:media_metadata)}
|
||||||
|
|
||||||
|
_url, :download_thumbnail, _opts, _ot, _addl ->
|
||||||
|
{:ok, ""}
|
||||||
end)
|
end)
|
||||||
|
|
||||||
expect(YtDlpRunnerMock, :run, 1, fn _url, _opts, _ot, _addl -> {:ok, ""} end)
|
|
||||||
|
|
||||||
perform_job(MediaDownloadWorker, %{id: media_item.id, force: true})
|
perform_job(MediaDownloadWorker, %{id: media_item.id, force: true})
|
||||||
end
|
end
|
||||||
|
|
||||||
test "deletes old files if the media item has been updated" do
|
test "deletes old files if the media item has been updated" do
|
||||||
expect(YtDlpRunnerMock, :run, 1, fn _url, _opts, _ot, _addl ->
|
expect(YtDlpRunnerMock, :run, 3, fn
|
||||||
tmp_media_item = media_item_with_attachments()
|
_url, :get_downloadable_status, _opts, _ot, _addl ->
|
||||||
metadata = render_parsed_metadata(:media_metadata)
|
{:ok, "{}"}
|
||||||
metadata = Map.put(metadata, "filepath", tmp_media_item.media_filepath)
|
|
||||||
|
|
||||||
{:ok, Phoenix.json_library().encode!(metadata)}
|
_url, :download, _opts, _ot, _addl ->
|
||||||
|
tmp_media_item = media_item_with_attachments()
|
||||||
|
metadata = render_parsed_metadata(:media_metadata)
|
||||||
|
metadata = Map.put(metadata, "filepath", tmp_media_item.media_filepath)
|
||||||
|
|
||||||
|
{:ok, Phoenix.json_library().encode!(metadata)}
|
||||||
|
|
||||||
|
_url, :download_thumbnail, _opts, _ot, _addl ->
|
||||||
|
{:ok, ""}
|
||||||
end)
|
end)
|
||||||
|
|
||||||
expect(YtDlpRunnerMock, :run, 1, fn _url, _opts, _ot, _addl -> {:ok, ""} end)
|
|
||||||
|
|
||||||
old_media_item = media_item_with_attachments()
|
old_media_item = media_item_with_attachments()
|
||||||
perform_job(MediaDownloadWorker, %{id: old_media_item.id, force: true})
|
perform_job(MediaDownloadWorker, %{id: old_media_item.id, force: true})
|
||||||
updated_media_item = Repo.reload(old_media_item)
|
updated_media_item = Repo.reload(old_media_item)
|
||||||
@@ -274,14 +297,6 @@ defmodule Pinchflat.Downloading.MediaDownloadWorkerTest do
|
|||||||
end
|
end
|
||||||
|
|
||||||
describe "perform/1 when testing user script callbacks" do
|
describe "perform/1 when testing user script callbacks" do
|
||||||
setup do
|
|
||||||
stub(YtDlpRunnerMock, :run, fn _url, _opts, _ot, _addl ->
|
|
||||||
{:ok, render_metadata(:media_metadata)}
|
|
||||||
end)
|
|
||||||
|
|
||||||
:ok
|
|
||||||
end
|
|
||||||
|
|
||||||
test "calls the media_pre_download user script runner", %{media_item: media_item} do
|
test "calls the media_pre_download user script runner", %{media_item: media_item} do
|
||||||
expect(UserScriptRunnerMock, :run, fn :media_pre_download, data ->
|
expect(UserScriptRunnerMock, :run, fn :media_pre_download, data ->
|
||||||
assert data.id == media_item.id
|
assert data.id == media_item.id
|
||||||
|
|||||||
@@ -16,31 +16,36 @@ defmodule Pinchflat.Downloading.MediaDownloaderTest do
|
|||||||
)
|
)
|
||||||
|
|
||||||
stub(HTTPClientMock, :get, fn _url, _headers, _opts -> {:ok, ""} end)
|
stub(HTTPClientMock, :get, fn _url, _headers, _opts -> {:ok, ""} end)
|
||||||
stub(YtDlpRunnerMock, :run, fn _url, _opts, _ot -> {:ok, "{}"} end)
|
|
||||||
stub(YtDlpRunnerMock, :run, fn _url, _opts, _ot, _addl_args -> {:ok, ""} end)
|
|
||||||
|
|
||||||
{:ok, %{media_item: media_item}}
|
{:ok, %{media_item: media_item}}
|
||||||
end
|
end
|
||||||
|
|
||||||
describe "download_for_media_item/3" do
|
describe "download_for_media_item/3" do
|
||||||
test "calls the backend runner", %{media_item: media_item} do
|
test "calls the backend runner", %{media_item: media_item} do
|
||||||
expect(YtDlpRunnerMock, :run, 1, fn url, _opts, ot, addl ->
|
expect(YtDlpRunnerMock, :run, 3, fn
|
||||||
assert url == media_item.original_url
|
_url, :get_downloadable_status, _opts, _ot, _addl ->
|
||||||
assert ot == "after_move:%()j"
|
{:ok, "{}"}
|
||||||
assert [{:output_filepath, filepath} | _] = addl
|
|
||||||
assert is_binary(filepath)
|
|
||||||
|
|
||||||
{:ok, render_metadata(:media_metadata)}
|
_url, :download_thumbnail, _opts, _ot, _addl ->
|
||||||
|
{:ok, ""}
|
||||||
|
|
||||||
|
url, :download, _opts, ot, addl ->
|
||||||
|
assert url == media_item.original_url
|
||||||
|
assert ot == "after_move:%()j"
|
||||||
|
assert [{:output_filepath, filepath} | _] = addl
|
||||||
|
assert is_binary(filepath)
|
||||||
|
|
||||||
|
{:ok, render_metadata(:media_metadata)}
|
||||||
end)
|
end)
|
||||||
|
|
||||||
expect(YtDlpRunnerMock, :run, 1, fn _url, _opts, _ot, _addl_args -> {:ok, ""} end)
|
|
||||||
|
|
||||||
assert {:ok, _} = MediaDownloader.download_for_media_item(media_item)
|
assert {:ok, _} = MediaDownloader.download_for_media_item(media_item)
|
||||||
end
|
end
|
||||||
|
|
||||||
test "saves the metadata filepath to the database", %{media_item: media_item} do
|
test "saves the metadata filepath to the database", %{media_item: media_item} do
|
||||||
expect(YtDlpRunnerMock, :run, 2, fn _url, _opts, _ot, _addl ->
|
expect(YtDlpRunnerMock, :run, 3, fn
|
||||||
{:ok, render_metadata(:media_metadata)}
|
_url, :get_downloadable_status, _opts, _ot, _addl -> {:ok, "{}"}
|
||||||
|
_url, :download_thumbnail, _opts, _ot, _addl -> {:ok, ""}
|
||||||
|
_url, :download, _opts, _ot, _addl -> {:ok, render_metadata(:media_metadata)}
|
||||||
end)
|
end)
|
||||||
|
|
||||||
assert is_nil(media_item.metadata)
|
assert is_nil(media_item.metadata)
|
||||||
@@ -51,7 +56,7 @@ defmodule Pinchflat.Downloading.MediaDownloaderTest do
|
|||||||
end
|
end
|
||||||
|
|
||||||
test "errors for non-downloadable media are passed through", %{media_item: media_item} do
|
test "errors for non-downloadable media are passed through", %{media_item: media_item} do
|
||||||
expect(YtDlpRunnerMock, :run, fn _url, _opts, _ot ->
|
expect(YtDlpRunnerMock, :run, fn _url, :get_downloadable_status, _opts, _ot, _addl ->
|
||||||
{:ok, Phoenix.json_library().encode!(%{"live_status" => "is_live"})}
|
{:ok, Phoenix.json_library().encode!(%{"live_status" => "is_live"})}
|
||||||
end)
|
end)
|
||||||
|
|
||||||
@@ -59,16 +64,18 @@ defmodule Pinchflat.Downloading.MediaDownloaderTest do
|
|||||||
end
|
end
|
||||||
|
|
||||||
test "non-recoverable errors are passed through", %{media_item: media_item} do
|
test "non-recoverable errors are passed through", %{media_item: media_item} do
|
||||||
expect(YtDlpRunnerMock, :run, fn _url, _opts, _ot, _addl ->
|
expect(YtDlpRunnerMock, :run, 2, fn
|
||||||
{:error, :some_error, 1}
|
_url, :get_downloadable_status, _opts, _ot, _addl -> {:ok, "{}"}
|
||||||
|
_url, :download, _opts, _ot, _addl -> {:error, :some_error, 1}
|
||||||
end)
|
end)
|
||||||
|
|
||||||
assert {:error, :some_error} = MediaDownloader.download_for_media_item(media_item)
|
assert {:error, :some_error} = MediaDownloader.download_for_media_item(media_item)
|
||||||
end
|
end
|
||||||
|
|
||||||
test "unknown errors are passed through", %{media_item: media_item} do
|
test "unknown errors are passed through", %{media_item: media_item} do
|
||||||
expect(YtDlpRunnerMock, :run, fn _url, _opts, _ot, _addl ->
|
expect(YtDlpRunnerMock, :run, 2, fn
|
||||||
{:error, :some_error}
|
_url, :get_downloadable_status, _opts, _ot, _addl -> {:ok, "{}"}
|
||||||
|
_url, :download, _opts, _ot, _addl -> {:error, :some_error}
|
||||||
end)
|
end)
|
||||||
|
|
||||||
assert {:error, message} = MediaDownloader.download_for_media_item(media_item)
|
assert {:error, message} = MediaDownloader.download_for_media_item(media_item)
|
||||||
@@ -78,29 +85,33 @@ defmodule Pinchflat.Downloading.MediaDownloaderTest do
|
|||||||
|
|
||||||
describe "download_for_media_item/3 when testing non-downloadable media" do
|
describe "download_for_media_item/3 when testing non-downloadable media" do
|
||||||
test "calls the download runner if the media is currently downloadable", %{media_item: media_item} do
|
test "calls the download runner if the media is currently downloadable", %{media_item: media_item} do
|
||||||
expect(YtDlpRunnerMock, :run, fn _url, _opts, _ot ->
|
expect(YtDlpRunnerMock, :run, 3, fn
|
||||||
{:ok, Phoenix.json_library().encode!(%{"live_status" => "was_live"})}
|
_url, :get_downloadable_status, _opts, _ot, _addl ->
|
||||||
end)
|
{:ok, Phoenix.json_library().encode!(%{"live_status" => "was_live"})}
|
||||||
|
|
||||||
expect(YtDlpRunnerMock, :run, 2, fn _url, _opts, _ot, _addl ->
|
_url, :download, _opts, _ot, _addl ->
|
||||||
{:ok, render_metadata(:media_metadata)}
|
{:ok, render_metadata(:media_metadata)}
|
||||||
|
|
||||||
|
_url, :download_thumbnail, _opts, _ot, _addl ->
|
||||||
|
{:ok, ""}
|
||||||
end)
|
end)
|
||||||
|
|
||||||
assert {:ok, _} = MediaDownloader.download_for_media_item(media_item)
|
assert {:ok, _} = MediaDownloader.download_for_media_item(media_item)
|
||||||
end
|
end
|
||||||
|
|
||||||
test "does not call the download runner if the media is not downloadable", %{media_item: media_item} do
|
test "does not call the download runner if the media is not downloadable", %{media_item: media_item} do
|
||||||
expect(YtDlpRunnerMock, :run, fn _url, _opts, _ot ->
|
expect(YtDlpRunnerMock, :run, fn
|
||||||
{:ok, Phoenix.json_library().encode!(%{"live_status" => "is_live"})}
|
_url, :get_downloadable_status, _opts, _ot, _addl ->
|
||||||
|
{:ok, Phoenix.json_library().encode!(%{"live_status" => "is_live"})}
|
||||||
end)
|
end)
|
||||||
|
|
||||||
expect(YtDlpRunnerMock, :run, 0, fn _url, _opts, _ot, _addl -> {:ok, ""} end)
|
expect(YtDlpRunnerMock, :run, 0, fn _url, :download, _opts, _ot, _addl -> {:ok, ""} end)
|
||||||
|
|
||||||
assert {:error, :unsuitable_for_download} = MediaDownloader.download_for_media_item(media_item)
|
assert {:error, :unsuitable_for_download} = MediaDownloader.download_for_media_item(media_item)
|
||||||
end
|
end
|
||||||
|
|
||||||
test "returns unexpected errors from the download status determination method", %{media_item: media_item} do
|
test "returns unexpected errors from the download status determination method", %{media_item: media_item} do
|
||||||
expect(YtDlpRunnerMock, :run, fn _url, _opts, _ot -> {:error, :what_tha} end)
|
expect(YtDlpRunnerMock, :run, fn _url, :get_downloadable_status, _opts, _ot, _addl -> {:error, :what_tha} end)
|
||||||
|
|
||||||
assert {:error, "Unknown error: {:error, :what_tha}"} = MediaDownloader.download_for_media_item(media_item)
|
assert {:error, "Unknown error: {:error, :what_tha}"} = MediaDownloader.download_for_media_item(media_item)
|
||||||
end
|
end
|
||||||
@@ -108,15 +119,20 @@ defmodule Pinchflat.Downloading.MediaDownloaderTest do
|
|||||||
|
|
||||||
describe "download_for_media_item/3 when testing override options" do
|
describe "download_for_media_item/3 when testing override options" do
|
||||||
test "includes override opts if specified", %{media_item: media_item} do
|
test "includes override opts if specified", %{media_item: media_item} do
|
||||||
expect(YtDlpRunnerMock, :run, 1, fn _url, opts, _ot, _addl ->
|
expect(YtDlpRunnerMock, :run, 3, fn
|
||||||
refute :force_overwrites in opts
|
_url, :get_downloadable_status, _opts, _ot, _addl ->
|
||||||
assert :no_force_overwrites in opts
|
{:ok, "{}"}
|
||||||
|
|
||||||
{:ok, render_metadata(:media_metadata)}
|
_url, :download, opts, _ot, _addl ->
|
||||||
|
refute :force_overwrites in opts
|
||||||
|
assert :no_force_overwrites in opts
|
||||||
|
|
||||||
|
{:ok, render_metadata(:media_metadata)}
|
||||||
|
|
||||||
|
_url, :download_thumbnail, _opts, _ot, _addl ->
|
||||||
|
{:ok, ""}
|
||||||
end)
|
end)
|
||||||
|
|
||||||
expect(YtDlpRunnerMock, :run, 1, fn _url, _opts, _ot, _addl -> {:ok, ""} end)
|
|
||||||
|
|
||||||
override_opts = [overwrite_behaviour: :no_force_overwrites]
|
override_opts = [overwrite_behaviour: :no_force_overwrites]
|
||||||
|
|
||||||
assert {:ok, _} = MediaDownloader.download_for_media_item(media_item, override_opts)
|
assert {:ok, _} = MediaDownloader.download_for_media_item(media_item, override_opts)
|
||||||
@@ -125,12 +141,18 @@ defmodule Pinchflat.Downloading.MediaDownloaderTest do
|
|||||||
|
|
||||||
describe "download_for_media_item/3 when testing cookie usage" do
|
describe "download_for_media_item/3 when testing cookie usage" do
|
||||||
test "sets use_cookies if the source uses cookies" do
|
test "sets use_cookies if the source uses cookies" do
|
||||||
expect(YtDlpRunnerMock, :run, 1, fn _url, _opts, _ot, addl ->
|
expect(YtDlpRunnerMock, :run, 3, fn
|
||||||
assert {:use_cookies, true} in addl
|
_url, :get_downloadable_status, _opts, _ot, addl ->
|
||||||
{:ok, render_metadata(:media_metadata)}
|
assert {:use_cookies, true} in addl
|
||||||
end)
|
{:ok, "{}"}
|
||||||
|
|
||||||
expect(YtDlpRunnerMock, :run, 1, fn _url, _opts, _ot, _addl_args -> {:ok, ""} end)
|
_url, :download, _opts, _ot, addl ->
|
||||||
|
assert {:use_cookies, true} in addl
|
||||||
|
{:ok, render_metadata(:media_metadata)}
|
||||||
|
|
||||||
|
_url, :download_thumbnail, _opts, _ot, _addl ->
|
||||||
|
{:ok, ""}
|
||||||
|
end)
|
||||||
|
|
||||||
source = source_fixture(%{use_cookies: true})
|
source = source_fixture(%{use_cookies: true})
|
||||||
media_item = media_item_fixture(%{source_id: source.id})
|
media_item = media_item_fixture(%{source_id: source.id})
|
||||||
@@ -139,12 +161,17 @@ defmodule Pinchflat.Downloading.MediaDownloaderTest do
|
|||||||
end
|
end
|
||||||
|
|
||||||
test "does not set use_cookies if the source does not use cookies" do
|
test "does not set use_cookies if the source does not use cookies" do
|
||||||
expect(YtDlpRunnerMock, :run, 1, fn _url, _opts, _ot, addl ->
|
expect(YtDlpRunnerMock, :run, 3, fn
|
||||||
assert {:use_cookies, false} in addl
|
_url, :get_downloadable_status, _opts, _ot, _addl ->
|
||||||
{:ok, render_metadata(:media_metadata)}
|
{:ok, "{}"}
|
||||||
end)
|
|
||||||
|
|
||||||
expect(YtDlpRunnerMock, :run, 1, fn _url, _opts, _ot, _addl_args -> {:ok, ""} end)
|
_url, :download, _opts, _ot, addl ->
|
||||||
|
assert {:use_cookies, false} in addl
|
||||||
|
{:ok, render_metadata(:media_metadata)}
|
||||||
|
|
||||||
|
_url, :download_thumbnail, _opts, _ot, _addl ->
|
||||||
|
{:ok, ""}
|
||||||
|
end)
|
||||||
|
|
||||||
source = source_fixture(%{use_cookies: false})
|
source = source_fixture(%{use_cookies: false})
|
||||||
media_item = media_item_fixture(%{source_id: source.id})
|
media_item = media_item_fixture(%{source_id: source.id})
|
||||||
@@ -157,8 +184,12 @@ defmodule Pinchflat.Downloading.MediaDownloaderTest do
|
|||||||
test "returns a recovered tuple on recoverable errors", %{media_item: media_item} do
|
test "returns a recovered tuple on recoverable errors", %{media_item: media_item} do
|
||||||
message = "Unable to communicate with SponsorBlock"
|
message = "Unable to communicate with SponsorBlock"
|
||||||
|
|
||||||
expect(YtDlpRunnerMock, :run, fn _url, _opts, _ot, _addl ->
|
expect(YtDlpRunnerMock, :run, 2, fn
|
||||||
{:error, message, 1}
|
_url, :get_downloadable_status, _opts, _ot, _addl ->
|
||||||
|
{:ok, "{}"}
|
||||||
|
|
||||||
|
_url, :download, _opts, _ot, _addl ->
|
||||||
|
{:error, message, 1}
|
||||||
end)
|
end)
|
||||||
|
|
||||||
assert {:recovered, ^message} = MediaDownloader.download_for_media_item(media_item)
|
assert {:recovered, ^message} = MediaDownloader.download_for_media_item(media_item)
|
||||||
@@ -167,15 +198,20 @@ defmodule Pinchflat.Downloading.MediaDownloaderTest do
|
|||||||
test "attempts to update the media item on recoverable errors", %{media_item: media_item} do
|
test "attempts to update the media item on recoverable errors", %{media_item: media_item} do
|
||||||
message = "Unable to communicate with SponsorBlock"
|
message = "Unable to communicate with SponsorBlock"
|
||||||
|
|
||||||
expect(YtDlpRunnerMock, :run, 1, fn _url, _opts, _ot, addl ->
|
expect(YtDlpRunnerMock, :run, 3, fn
|
||||||
[{:output_filepath, filepath} | _] = addl
|
_url, :download, _opts, _ot, addl ->
|
||||||
File.write(filepath, render_metadata(:media_metadata))
|
[{:output_filepath, filepath} | _] = addl
|
||||||
|
File.write(filepath, render_metadata(:media_metadata))
|
||||||
|
|
||||||
{:error, message, 1}
|
{:error, message, 1}
|
||||||
|
|
||||||
|
_url, :get_downloadable_status, _opts, _ot, _addl ->
|
||||||
|
{:ok, "{}"}
|
||||||
|
|
||||||
|
_url, :download_thumbnail, _opts, _ot, _addl ->
|
||||||
|
{:ok, ""}
|
||||||
end)
|
end)
|
||||||
|
|
||||||
expect(YtDlpRunnerMock, :run, 1, fn _url, _opts, _ot, _addl_args -> {:ok, ""} end)
|
|
||||||
|
|
||||||
assert {:recovered, ^message} = MediaDownloader.download_for_media_item(media_item)
|
assert {:recovered, ^message} = MediaDownloader.download_for_media_item(media_item)
|
||||||
media_item = Repo.reload(media_item)
|
media_item = Repo.reload(media_item)
|
||||||
|
|
||||||
@@ -186,8 +222,10 @@ defmodule Pinchflat.Downloading.MediaDownloaderTest do
|
|||||||
|
|
||||||
describe "download_for_media_item/3 when testing media_item attributes" do
|
describe "download_for_media_item/3 when testing media_item attributes" do
|
||||||
setup do
|
setup do
|
||||||
stub(YtDlpRunnerMock, :run, fn _url, _opts, _ot, _addl ->
|
stub(YtDlpRunnerMock, :run, fn
|
||||||
{:ok, render_metadata(:media_metadata)}
|
_url, :download, _opts, _ot, _addl -> {:ok, render_metadata(:media_metadata)}
|
||||||
|
_url, :get_downloadable_status, _opts, _ot, _addl -> {:ok, "{}"}
|
||||||
|
_url, :download_thumbnail, _opts, _ot, _addl -> {:ok, ""}
|
||||||
end)
|
end)
|
||||||
|
|
||||||
:ok
|
:ok
|
||||||
@@ -234,20 +272,27 @@ defmodule Pinchflat.Downloading.MediaDownloaderTest do
|
|||||||
end
|
end
|
||||||
|
|
||||||
test "extracts the thumbnail_filepath", %{media_item: media_item} do
|
test "extracts the thumbnail_filepath", %{media_item: media_item} do
|
||||||
expect(YtDlpRunnerMock, :run, 2, fn _url, _opts, _ot, _addl ->
|
expect(YtDlpRunnerMock, :run, 3, fn
|
||||||
metadata = render_parsed_metadata(:media_metadata)
|
_url, :get_downloadable_status, _opts, _ot, _addl ->
|
||||||
|
{:ok, "{}"}
|
||||||
|
|
||||||
thumbnail_filepath =
|
_url, :download, _opts, _ot, _addl ->
|
||||||
metadata["thumbnails"]
|
metadata = render_parsed_metadata(:media_metadata)
|
||||||
|> Enum.reverse()
|
|
||||||
|> Enum.find_value(fn attrs -> attrs["filepath"] end)
|
|
||||||
|> String.split(~r{\.}, include_captures: true)
|
|
||||||
|> List.insert_at(-3, "-thumb")
|
|
||||||
|> Enum.join()
|
|
||||||
|
|
||||||
:ok = File.cp(thumbnail_filepath_fixture(), thumbnail_filepath)
|
thumbnail_filepath =
|
||||||
|
metadata["thumbnails"]
|
||||||
|
|> Enum.reverse()
|
||||||
|
|> Enum.find_value(fn attrs -> attrs["filepath"] end)
|
||||||
|
|> String.split(~r{\.}, include_captures: true)
|
||||||
|
|> List.insert_at(-3, "-thumb")
|
||||||
|
|> Enum.join()
|
||||||
|
|
||||||
{:ok, Phoenix.json_library().encode!(metadata)}
|
:ok = File.cp(thumbnail_filepath_fixture(), thumbnail_filepath)
|
||||||
|
|
||||||
|
{:ok, Phoenix.json_library().encode!(metadata)}
|
||||||
|
|
||||||
|
_url, :download_thumbnail, _opts, _ot, _addl ->
|
||||||
|
{:ok, ""}
|
||||||
end)
|
end)
|
||||||
|
|
||||||
assert media_item.thumbnail_filepath == nil
|
assert media_item.thumbnail_filepath == nil
|
||||||
@@ -258,13 +303,19 @@ defmodule Pinchflat.Downloading.MediaDownloaderTest do
|
|||||||
end
|
end
|
||||||
|
|
||||||
test "extracts the metadata_filepath", %{media_item: media_item} do
|
test "extracts the metadata_filepath", %{media_item: media_item} do
|
||||||
expect(YtDlpRunnerMock, :run, 2, fn _url, _opts, _ot, _addl ->
|
expect(YtDlpRunnerMock, :run, 3, fn
|
||||||
metadata = render_parsed_metadata(:media_metadata)
|
_url, :get_downloadable_status, _opts, _ot, _addl ->
|
||||||
|
{:ok, "{}"}
|
||||||
|
|
||||||
infojson_filepath = metadata["infojson_filename"]
|
_url, :download, _opts, _ot, _addl ->
|
||||||
:ok = File.cp(infojson_filepath_fixture(), infojson_filepath)
|
metadata = render_parsed_metadata(:media_metadata)
|
||||||
|
infojson_filepath = metadata["infojson_filename"]
|
||||||
|
:ok = File.cp(infojson_filepath_fixture(), infojson_filepath)
|
||||||
|
|
||||||
{:ok, Phoenix.json_library().encode!(metadata)}
|
{:ok, Phoenix.json_library().encode!(metadata)}
|
||||||
|
|
||||||
|
_url, :download_thumbnail, _opts, _ot, _addl ->
|
||||||
|
{:ok, ""}
|
||||||
end)
|
end)
|
||||||
|
|
||||||
assert media_item.metadata_filepath == nil
|
assert media_item.metadata_filepath == nil
|
||||||
@@ -277,8 +328,10 @@ defmodule Pinchflat.Downloading.MediaDownloaderTest do
|
|||||||
|
|
||||||
describe "download_for_media_item/3 when testing NFO generation" do
|
describe "download_for_media_item/3 when testing NFO generation" do
|
||||||
setup do
|
setup do
|
||||||
stub(YtDlpRunnerMock, :run, fn _url, _opts, _ot, _addl ->
|
stub(YtDlpRunnerMock, :run, fn
|
||||||
{:ok, render_metadata(:media_metadata)}
|
_url, :download, _opts, _ot, _addl -> {:ok, render_metadata(:media_metadata)}
|
||||||
|
_url, :get_downloadable_status, _opts, _ot, _addl -> {:ok, "{}"}
|
||||||
|
_url, :download_thumbnail, _opts, _ot, _addl -> {:ok, ""}
|
||||||
end)
|
end)
|
||||||
|
|
||||||
:ok
|
:ok
|
||||||
|
|||||||
@@ -68,7 +68,7 @@ defmodule Pinchflat.Downloading.QualityOptionBuilderTest do
|
|||||||
end
|
end
|
||||||
|
|
||||||
test "includes quality options" do
|
test "includes quality options" do
|
||||||
resolutions = ["360", "480", "720", "1080", "2160", "4320"]
|
resolutions = ["360", "480", "720", "1080", "1440", "2160", "4320"]
|
||||||
|
|
||||||
Enum.each(resolutions, fn resolution ->
|
Enum.each(resolutions, fn resolution ->
|
||||||
resolution_atom = String.to_existing_atom(resolution <> "p")
|
resolution_atom = String.to_existing_atom(resolution <> "p")
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ defmodule Pinchflat.FastIndexing.FastIndexingHelpersTest do
|
|||||||
alias Pinchflat.FastIndexing.FastIndexingHelpers
|
alias Pinchflat.FastIndexing.FastIndexingHelpers
|
||||||
|
|
||||||
setup do
|
setup do
|
||||||
stub(YtDlpRunnerMock, :run, fn _url, _opts, _ot, _addl ->
|
stub(YtDlpRunnerMock, :run, fn _url, :get_media_attributes, _opts, _ot, _addl ->
|
||||||
{:ok, media_attributes_return_fixture()}
|
{:ok, media_attributes_return_fixture()}
|
||||||
end)
|
end)
|
||||||
|
|
||||||
@@ -83,7 +83,7 @@ defmodule Pinchflat.FastIndexing.FastIndexingHelpersTest do
|
|||||||
test "passes the source's download options to the yt-dlp runner", %{source: source} do
|
test "passes the source's download options to the yt-dlp runner", %{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)
|
||||||
|
|
||||||
expect(YtDlpRunnerMock, :run, fn _url, opts, _ot, _addl_opts ->
|
expect(YtDlpRunnerMock, :run, fn _url, :get_media_attributes, opts, _ot, _addl_opts ->
|
||||||
assert {:output, "/tmp/test/media/%(title)S.%(ext)S"} in opts
|
assert {:output, "/tmp/test/media/%(title)S.%(ext)S"} in opts
|
||||||
assert {:remux_video, "mp4"} in opts
|
assert {:remux_video, "mp4"} in opts
|
||||||
{:ok, media_attributes_return_fixture()}
|
{:ok, media_attributes_return_fixture()}
|
||||||
@@ -95,7 +95,7 @@ defmodule Pinchflat.FastIndexing.FastIndexingHelpersTest do
|
|||||||
test "sets use_cookies if the source uses cookies" do
|
test "sets use_cookies if the source uses cookies" 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)
|
||||||
|
|
||||||
stub(YtDlpRunnerMock, :run, fn _url, _opts, _ot, addl ->
|
stub(YtDlpRunnerMock, :run, fn _url, :get_media_attributes, _opts, _ot, addl ->
|
||||||
assert {:use_cookies, true} in addl
|
assert {:use_cookies, true} in addl
|
||||||
|
|
||||||
{:ok, media_attributes_return_fixture()}
|
{:ok, media_attributes_return_fixture()}
|
||||||
@@ -109,7 +109,7 @@ defmodule Pinchflat.FastIndexing.FastIndexingHelpersTest do
|
|||||||
test "does not set use_cookies if the source does not use cookies" do
|
test "does not set use_cookies if the source does not use cookies" 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)
|
||||||
|
|
||||||
stub(YtDlpRunnerMock, :run, fn _url, _opts, _ot, addl ->
|
stub(YtDlpRunnerMock, :run, fn _url, :get_media_attributes, _opts, _ot, addl ->
|
||||||
assert {:use_cookies, false} in addl
|
assert {:use_cookies, false} in addl
|
||||||
|
|
||||||
{:ok, media_attributes_return_fixture()}
|
{:ok, media_attributes_return_fixture()}
|
||||||
@@ -126,7 +126,7 @@ defmodule Pinchflat.FastIndexing.FastIndexingHelpersTest do
|
|||||||
profile = media_profile_fixture(%{shorts_behaviour: :exclude})
|
profile = media_profile_fixture(%{shorts_behaviour: :exclude})
|
||||||
source = source_fixture(%{media_profile_id: profile.id})
|
source = source_fixture(%{media_profile_id: profile.id})
|
||||||
|
|
||||||
stub(YtDlpRunnerMock, :run, fn _url, _opts, _ot, _addl ->
|
stub(YtDlpRunnerMock, :run, fn _url, :get_media_attributes, _opts, _ot, _addl ->
|
||||||
output =
|
output =
|
||||||
Phoenix.json_library().encode!(%{
|
Phoenix.json_library().encode!(%{
|
||||||
id: "video2",
|
id: "video2",
|
||||||
@@ -150,7 +150,7 @@ defmodule Pinchflat.FastIndexing.FastIndexingHelpersTest do
|
|||||||
test "does not blow up if a media item cannot be created", %{source: source} do
|
test "does not blow up if a media item cannot be created", %{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)
|
||||||
|
|
||||||
stub(YtDlpRunnerMock, :run, fn _url, _opts, _ot, _addl ->
|
stub(YtDlpRunnerMock, :run, fn _url, :get_media_attributes, _opts, _ot, _addl ->
|
||||||
{:ok, "{}"}
|
{:ok, "{}"}
|
||||||
end)
|
end)
|
||||||
|
|
||||||
@@ -160,7 +160,7 @@ defmodule Pinchflat.FastIndexing.FastIndexingHelpersTest do
|
|||||||
test "does not blow up if a media item causes a yt-dlp error", %{source: source} do
|
test "does not blow up if a media item causes a yt-dlp error", %{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)
|
||||||
|
|
||||||
stub(YtDlpRunnerMock, :run, fn _url, _opts, _ot, _addl ->
|
stub(YtDlpRunnerMock, :run, fn _url, :get_media_attributes, _opts, _ot, _addl ->
|
||||||
{:error, "message", 1}
|
{:error, "message", 1}
|
||||||
end)
|
end)
|
||||||
|
|
||||||
|
|||||||
@@ -84,7 +84,10 @@ defmodule Pinchflat.FastIndexing.FastIndexingWorkerTest do
|
|||||||
source = source_fixture(fast_index: true)
|
source = source_fixture(fast_index: true)
|
||||||
|
|
||||||
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)
|
||||||
expect(YtDlpRunnerMock, :run, fn _url, _opts, _ot, _addl -> {:ok, render_metadata(:media_metadata)} end)
|
|
||||||
|
expect(YtDlpRunnerMock, :run, fn _url, :get_media_attributes, _opts, _ot, _addl ->
|
||||||
|
{:ok, render_metadata(:media_metadata)}
|
||||||
|
end)
|
||||||
|
|
||||||
expect(AppriseRunnerMock, :run, fn servers, opts ->
|
expect(AppriseRunnerMock, :run, fn servers, opts ->
|
||||||
assert "server_1" = servers
|
assert "server_1" = servers
|
||||||
@@ -110,7 +113,11 @@ defmodule Pinchflat.FastIndexing.FastIndexingWorkerTest do
|
|||||||
source = source_fixture(fast_index: true, download_media: false)
|
source = source_fixture(fast_index: true, download_media: false)
|
||||||
|
|
||||||
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)
|
||||||
expect(YtDlpRunnerMock, :run, fn _url, _opts, _ot, _addl -> {:ok, render_metadata(:media_metadata)} end)
|
|
||||||
|
expect(YtDlpRunnerMock, :run, fn _url, :get_media_attributes, _opts, _ot, _addl ->
|
||||||
|
{:ok, render_metadata(:media_metadata)}
|
||||||
|
end)
|
||||||
|
|
||||||
expect(AppriseRunnerMock, :run, 0, fn _servers, _opts -> {:ok, ""} end)
|
expect(AppriseRunnerMock, :run, 0, fn _servers, _opts -> {:ok, ""} end)
|
||||||
|
|
||||||
perform_job(FastIndexingWorker, %{id: source.id})
|
perform_job(FastIndexingWorker, %{id: source.id})
|
||||||
@@ -120,7 +127,11 @@ defmodule Pinchflat.FastIndexing.FastIndexingWorkerTest do
|
|||||||
source = source_fixture(fast_index: true, title_filter_regex: "foobar")
|
source = source_fixture(fast_index: true, title_filter_regex: "foobar")
|
||||||
|
|
||||||
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)
|
||||||
expect(YtDlpRunnerMock, :run, fn _url, _opts, _ot, _addl -> {:ok, render_metadata(:media_metadata)} end)
|
|
||||||
|
expect(YtDlpRunnerMock, :run, fn _url, :get_media_attributes, _opts, _ot, _addl ->
|
||||||
|
{:ok, render_metadata(:media_metadata)}
|
||||||
|
end)
|
||||||
|
|
||||||
expect(AppriseRunnerMock, :run, 0, fn _servers, _opts -> {:ok, ""} end)
|
expect(AppriseRunnerMock, :run, 0, fn _servers, _opts -> {:ok, ""} end)
|
||||||
|
|
||||||
perform_job(FastIndexingWorker, %{id: source.id})
|
perform_job(FastIndexingWorker, %{id: source.id})
|
||||||
|
|||||||
@@ -757,7 +757,7 @@ defmodule Pinchflat.MediaTest do
|
|||||||
end
|
end
|
||||||
|
|
||||||
test "does delete the media item's metadata files" do
|
test "does delete the media item's metadata files" do
|
||||||
stub(YtDlpRunnerMock, :run, fn _url, _opts, _ot, _addl -> {:ok, ""} end)
|
stub(YtDlpRunnerMock, :run, fn _url, :download_thumbnail, _opts, _ot, _addl -> {:ok, ""} end)
|
||||||
media_item = Repo.preload(media_item_with_attachments(), [:metadata, :source])
|
media_item = Repo.preload(media_item_with_attachments(), [:metadata, :source])
|
||||||
|
|
||||||
update_attrs = %{
|
update_attrs = %{
|
||||||
@@ -789,7 +789,7 @@ defmodule Pinchflat.MediaTest do
|
|||||||
end
|
end
|
||||||
|
|
||||||
test "deletes the media item's metadata files" do
|
test "deletes the media item's metadata files" do
|
||||||
stub(YtDlpRunnerMock, :run, fn _url, _opts, _ot, _addl -> {:ok, ""} end)
|
stub(YtDlpRunnerMock, :run, fn _url, :download_thumbnail, _opts, _ot, _addl -> {:ok, ""} end)
|
||||||
media_item = Repo.preload(media_item_with_attachments(), [:metadata, :source])
|
media_item = Repo.preload(media_item_with_attachments(), [:metadata, :source])
|
||||||
|
|
||||||
update_attrs = %{
|
update_attrs = %{
|
||||||
@@ -875,7 +875,7 @@ defmodule Pinchflat.MediaTest do
|
|||||||
end
|
end
|
||||||
|
|
||||||
test "does not delete the media item's metadata files" do
|
test "does not delete the media item's metadata files" do
|
||||||
stub(YtDlpRunnerMock, :run, fn _url, _opts, _ot, _addl -> {:ok, ""} end)
|
stub(YtDlpRunnerMock, :run, fn _url, :download_thumbnail, _opts, _ot, _addl -> {:ok, ""} end)
|
||||||
media_item = Repo.preload(media_item_with_attachments(), [:metadata, :source])
|
media_item = Repo.preload(media_item_with_attachments(), [:metadata, :source])
|
||||||
|
|
||||||
update_attrs = %{
|
update_attrs = %{
|
||||||
|
|||||||
@@ -62,7 +62,7 @@ defmodule Pinchflat.Metadata.MetadataFileHelpersTest do
|
|||||||
|
|
||||||
describe "download_and_store_thumbnail_for/2" do
|
describe "download_and_store_thumbnail_for/2" do
|
||||||
test "returns the filepath", %{media_item: media_item} do
|
test "returns the filepath", %{media_item: media_item} do
|
||||||
stub(YtDlpRunnerMock, :run, fn _url, _opts, _ot, _addl -> {:ok, ""} end)
|
stub(YtDlpRunnerMock, :run, fn _url, :download_thumbnail, _opts, _ot, _addl -> {:ok, ""} end)
|
||||||
|
|
||||||
filepath = Helpers.download_and_store_thumbnail_for(media_item)
|
filepath = Helpers.download_and_store_thumbnail_for(media_item)
|
||||||
|
|
||||||
@@ -70,7 +70,7 @@ defmodule Pinchflat.Metadata.MetadataFileHelpersTest do
|
|||||||
end
|
end
|
||||||
|
|
||||||
test "calls yt-dlp with the expected options", %{media_item: media_item} do
|
test "calls yt-dlp with the expected options", %{media_item: media_item} do
|
||||||
expect(YtDlpRunnerMock, :run, fn url, opts, ot, _addl ->
|
expect(YtDlpRunnerMock, :run, fn url, :download_thumbnail, opts, ot, _addl ->
|
||||||
assert url == media_item.original_url
|
assert url == media_item.original_url
|
||||||
assert ot == "after_move:%()j"
|
assert ot == "after_move:%()j"
|
||||||
|
|
||||||
@@ -89,7 +89,7 @@ defmodule Pinchflat.Metadata.MetadataFileHelpersTest do
|
|||||||
end
|
end
|
||||||
|
|
||||||
test "sets use_cookies if the source uses cookies" do
|
test "sets use_cookies if the source uses cookies" do
|
||||||
expect(YtDlpRunnerMock, :run, fn _url, _opts, _ot, addl ->
|
expect(YtDlpRunnerMock, :run, fn _url, :download_thumbnail, _opts, _ot, addl ->
|
||||||
assert {:use_cookies, true} in addl
|
assert {:use_cookies, true} in addl
|
||||||
{:ok, ""}
|
{:ok, ""}
|
||||||
end)
|
end)
|
||||||
@@ -101,7 +101,7 @@ defmodule Pinchflat.Metadata.MetadataFileHelpersTest do
|
|||||||
end
|
end
|
||||||
|
|
||||||
test "does not set use_cookies if the source does not use cookies" do
|
test "does not set use_cookies if the source does not use cookies" do
|
||||||
expect(YtDlpRunnerMock, :run, fn _url, _opts, _ot, addl ->
|
expect(YtDlpRunnerMock, :run, fn _url, :download_thumbnail, _opts, _ot, addl ->
|
||||||
assert {:use_cookies, false} in addl
|
assert {:use_cookies, false} in addl
|
||||||
{:ok, ""}
|
{:ok, ""}
|
||||||
end)
|
end)
|
||||||
@@ -113,7 +113,7 @@ defmodule Pinchflat.Metadata.MetadataFileHelpersTest do
|
|||||||
end
|
end
|
||||||
|
|
||||||
test "returns nil if yt-dlp fails", %{media_item: media_item} do
|
test "returns nil if yt-dlp fails", %{media_item: media_item} do
|
||||||
stub(YtDlpRunnerMock, :run, fn _url, _opts, _ot, _addl -> {:error, "error"} end)
|
stub(YtDlpRunnerMock, :run, fn _url, :download_thumbnail, _opts, _ot, _addl -> {:error, "error"} end)
|
||||||
|
|
||||||
filepath = Helpers.download_and_store_thumbnail_for(media_item)
|
filepath = Helpers.download_and_store_thumbnail_for(media_item)
|
||||||
|
|
||||||
|
|||||||
@@ -8,9 +8,6 @@ defmodule Pinchflat.Metadata.SourceMetadataStorageWorkerTest do
|
|||||||
alias Pinchflat.Metadata.MetadataFileHelpers
|
alias Pinchflat.Metadata.MetadataFileHelpers
|
||||||
alias Pinchflat.Metadata.SourceMetadataStorageWorker
|
alias Pinchflat.Metadata.SourceMetadataStorageWorker
|
||||||
|
|
||||||
@source_details_ot "%(.{channel,channel_id,playlist_id,playlist_title,filename})j"
|
|
||||||
@metadata_ot "playlist:%()j"
|
|
||||||
|
|
||||||
describe "kickoff_with_task/1" do
|
describe "kickoff_with_task/1" do
|
||||||
test "enqueues a new worker for the source" do
|
test "enqueues a new worker for the source" do
|
||||||
source = source_fixture()
|
source = source_fixture()
|
||||||
@@ -32,8 +29,8 @@ defmodule Pinchflat.Metadata.SourceMetadataStorageWorkerTest do
|
|||||||
describe "perform/1" do
|
describe "perform/1" do
|
||||||
test "won't call itself in an infinite loop" do
|
test "won't call itself in an infinite loop" do
|
||||||
stub(YtDlpRunnerMock, :run, fn
|
stub(YtDlpRunnerMock, :run, fn
|
||||||
_url, _opts, ot, _addl when ot == @source_details_ot -> {:ok, source_details_return_fixture()}
|
_url, :get_source_details, _opts, _ot, _addl -> {:ok, source_details_return_fixture()}
|
||||||
_url, _opts, ot, _addl when ot == @metadata_ot -> {:ok, "{}"}
|
_url, :get_source_metadata, _opts, _ot, _addl -> {:ok, "{}"}
|
||||||
end)
|
end)
|
||||||
|
|
||||||
source = source_fixture()
|
source = source_fixture()
|
||||||
@@ -51,8 +48,8 @@ defmodule Pinchflat.Metadata.SourceMetadataStorageWorkerTest do
|
|||||||
describe "perform/1 when testing attribute updates" do
|
describe "perform/1 when testing attribute updates" do
|
||||||
test "the source description is saved" do
|
test "the source description is saved" do
|
||||||
stub(YtDlpRunnerMock, :run, fn
|
stub(YtDlpRunnerMock, :run, fn
|
||||||
_url, _opts, ot, _addl when ot == @source_details_ot -> {:ok, source_details_return_fixture()}
|
_url, :get_source_details, _opts, _ot, _addl -> {:ok, source_details_return_fixture()}
|
||||||
_url, _opts, ot, _addl when ot == @metadata_ot -> {:ok, render_metadata(:channel_source_metadata)}
|
_url, :get_source_metadata, _opts, _ot, _addl -> {:ok, render_metadata(:channel_source_metadata)}
|
||||||
end)
|
end)
|
||||||
|
|
||||||
source = source_fixture(%{description: nil})
|
source = source_fixture(%{description: nil})
|
||||||
@@ -68,8 +65,8 @@ defmodule Pinchflat.Metadata.SourceMetadataStorageWorkerTest do
|
|||||||
describe "perform/1 when testing metadata storage" do
|
describe "perform/1 when testing metadata storage" do
|
||||||
test "sets metadata location for source" do
|
test "sets metadata location for source" do
|
||||||
stub(YtDlpRunnerMock, :run, fn
|
stub(YtDlpRunnerMock, :run, fn
|
||||||
_url, _opts, ot, _addl when ot == @source_details_ot -> {:ok, source_details_return_fixture()}
|
_url, :get_source_details, _opts, _ot, _addl -> {:ok, source_details_return_fixture()}
|
||||||
_url, _opts, ot, _addl when ot == @metadata_ot -> {:ok, "{}"}
|
_url, :get_source_metadata, _opts, _ot, _addl -> {:ok, "{}"}
|
||||||
end)
|
end)
|
||||||
|
|
||||||
source = Repo.preload(source_fixture(), :metadata)
|
source = Repo.preload(source_fixture(), :metadata)
|
||||||
@@ -87,8 +84,8 @@ defmodule Pinchflat.Metadata.SourceMetadataStorageWorkerTest do
|
|||||||
file_contents = Phoenix.json_library().encode!(%{"title" => "test"})
|
file_contents = Phoenix.json_library().encode!(%{"title" => "test"})
|
||||||
|
|
||||||
stub(YtDlpRunnerMock, :run, fn
|
stub(YtDlpRunnerMock, :run, fn
|
||||||
_url, _opts, ot, _addl when ot == @source_details_ot -> {:ok, source_details_return_fixture()}
|
_url, :get_source_details, _opts, _ot, _addl -> {:ok, source_details_return_fixture()}
|
||||||
_url, _opts, ot, _addl when ot == @metadata_ot -> {:ok, file_contents}
|
_url, :get_source_metadata, _opts, _ot, _addl -> {:ok, file_contents}
|
||||||
end)
|
end)
|
||||||
|
|
||||||
perform_job(SourceMetadataStorageWorker, %{id: source.id})
|
perform_job(SourceMetadataStorageWorker, %{id: source.id})
|
||||||
@@ -100,8 +97,8 @@ defmodule Pinchflat.Metadata.SourceMetadataStorageWorkerTest do
|
|||||||
|
|
||||||
test "sets metadata image location for source" do
|
test "sets metadata image location for source" do
|
||||||
stub(YtDlpRunnerMock, :run, fn
|
stub(YtDlpRunnerMock, :run, fn
|
||||||
_url, _opts, ot, _addl when ot == @source_details_ot -> {:ok, source_details_return_fixture()}
|
_url, :get_source_details, _opts, _ot, _addl -> {:ok, source_details_return_fixture()}
|
||||||
_url, _opts, ot, _addl when ot == @metadata_ot -> {:ok, render_metadata(:channel_source_metadata)}
|
_url, :get_source_metadata, _opts, _ot, _addl -> {:ok, render_metadata(:channel_source_metadata)}
|
||||||
end)
|
end)
|
||||||
|
|
||||||
source = source_fixture()
|
source = source_fixture()
|
||||||
@@ -118,8 +115,8 @@ defmodule Pinchflat.Metadata.SourceMetadataStorageWorkerTest do
|
|||||||
|
|
||||||
test "stores metadata images for source" do
|
test "stores metadata images for source" do
|
||||||
stub(YtDlpRunnerMock, :run, fn
|
stub(YtDlpRunnerMock, :run, fn
|
||||||
_url, _opts, ot, _addl when ot == @source_details_ot -> {:ok, source_details_return_fixture()}
|
_url, :get_source_details, _opts, _ot, _addl -> {:ok, source_details_return_fixture()}
|
||||||
_url, _opts, ot, _addl when ot == @metadata_ot -> {:ok, render_metadata(:channel_source_metadata)}
|
_url, :get_source_metadata, _opts, _ot, _addl -> {:ok, render_metadata(:channel_source_metadata)}
|
||||||
end)
|
end)
|
||||||
|
|
||||||
source = source_fixture()
|
source = source_fixture()
|
||||||
@@ -138,12 +135,12 @@ defmodule Pinchflat.Metadata.SourceMetadataStorageWorkerTest do
|
|||||||
describe "perform/1 when testing source image downloading" do
|
describe "perform/1 when testing source image downloading" do
|
||||||
test "downloads and stores source images" do
|
test "downloads and stores source images" do
|
||||||
stub(YtDlpRunnerMock, :run, fn
|
stub(YtDlpRunnerMock, :run, fn
|
||||||
_url, _opts, ot, _addl when ot == @source_details_ot ->
|
_url, :get_source_details, _opts, _ot, _addl ->
|
||||||
filename = Path.join([Application.get_env(:pinchflat, :media_directory), "Season 1", "bar.mp4"])
|
filename = Path.join([Application.get_env(:pinchflat, :media_directory), "Season 1", "bar.mp4"])
|
||||||
|
|
||||||
{:ok, source_details_return_fixture(%{filename: filename})}
|
{:ok, source_details_return_fixture(%{filename: filename})}
|
||||||
|
|
||||||
_url, opts, ot, _addl when ot == @metadata_ot ->
|
_url, :get_source_metadata, opts, _ot, _addl ->
|
||||||
assert {:convert_thumbnails, "jpg"} in opts
|
assert {:convert_thumbnails, "jpg"} in opts
|
||||||
|
|
||||||
{:ok, render_metadata(:channel_source_metadata)}
|
{:ok, render_metadata(:channel_source_metadata)}
|
||||||
@@ -168,10 +165,10 @@ defmodule Pinchflat.Metadata.SourceMetadataStorageWorkerTest do
|
|||||||
|
|
||||||
test "calls one set of yt-dlp metadata opts for channels" do
|
test "calls one set of yt-dlp metadata opts for channels" do
|
||||||
stub(YtDlpRunnerMock, :run, fn
|
stub(YtDlpRunnerMock, :run, fn
|
||||||
_url, _opts, ot, _addl when ot == @source_details_ot ->
|
_url, :get_source_details, _opts, _ot, _addl ->
|
||||||
{:ok, source_details_return_fixture()}
|
{:ok, source_details_return_fixture()}
|
||||||
|
|
||||||
_url, opts, ot, _addl when ot == @metadata_ot ->
|
_url, :get_source_metadata, opts, _ot, _addl ->
|
||||||
assert {:playlist_items, 0} in opts
|
assert {:playlist_items, 0} in opts
|
||||||
assert :write_all_thumbnails in opts
|
assert :write_all_thumbnails in opts
|
||||||
|
|
||||||
@@ -186,10 +183,10 @@ defmodule Pinchflat.Metadata.SourceMetadataStorageWorkerTest do
|
|||||||
|
|
||||||
test "calls another set of yt-dlp metadata opts for playlists" do
|
test "calls another set of yt-dlp metadata opts for playlists" do
|
||||||
stub(YtDlpRunnerMock, :run, fn
|
stub(YtDlpRunnerMock, :run, fn
|
||||||
_url, _opts, ot, _addl when ot == @source_details_ot ->
|
_url, :get_source_details, _opts, _ot, _addl ->
|
||||||
{:ok, source_details_return_fixture()}
|
{:ok, source_details_return_fixture()}
|
||||||
|
|
||||||
_url, opts, ot, _addl when ot == @metadata_ot ->
|
_url, :get_source_metadata, opts, _ot, _addl ->
|
||||||
assert {:playlist_items, 1} in opts
|
assert {:playlist_items, 1} in opts
|
||||||
assert :write_thumbnail in opts
|
assert :write_thumbnail in opts
|
||||||
|
|
||||||
@@ -204,12 +201,12 @@ defmodule Pinchflat.Metadata.SourceMetadataStorageWorkerTest do
|
|||||||
|
|
||||||
test "does not store source images if the profile is not set to" do
|
test "does not store source images if the profile is not set to" do
|
||||||
stub(YtDlpRunnerMock, :run, fn
|
stub(YtDlpRunnerMock, :run, fn
|
||||||
_url, _opts, ot, _addl when ot == @source_details_ot ->
|
_url, :get_source_details, _opts, _ot, _addl ->
|
||||||
filename = Path.join([Application.get_env(:pinchflat, :media_directory), "Season 1", "bar.mp4"])
|
filename = Path.join([Application.get_env(:pinchflat, :media_directory), "Season 1", "bar.mp4"])
|
||||||
|
|
||||||
{:ok, source_details_return_fixture(%{filename: filename})}
|
{:ok, source_details_return_fixture(%{filename: filename})}
|
||||||
|
|
||||||
_url, _opts, ot, _addl when ot == @metadata_ot ->
|
_url, :get_source_metadata, _opts, _ot, _addl ->
|
||||||
{:ok, render_metadata(:channel_source_metadata)}
|
{:ok, render_metadata(:channel_source_metadata)}
|
||||||
end)
|
end)
|
||||||
|
|
||||||
@@ -226,12 +223,12 @@ defmodule Pinchflat.Metadata.SourceMetadataStorageWorkerTest do
|
|||||||
|
|
||||||
test "does not store source images if the series directory cannot be determined" do
|
test "does not store source images if the series directory cannot be determined" do
|
||||||
stub(YtDlpRunnerMock, :run, fn
|
stub(YtDlpRunnerMock, :run, fn
|
||||||
_url, _opts, ot, _addl when ot == @source_details_ot ->
|
_url, :get_source_details, _opts, _ot, _addl ->
|
||||||
filename = Path.join([Application.get_env(:pinchflat, :media_directory), "foo", "bar.mp4"])
|
filename = Path.join([Application.get_env(:pinchflat, :media_directory), "foo", "bar.mp4"])
|
||||||
|
|
||||||
{:ok, source_details_return_fixture(%{filename: filename})}
|
{:ok, source_details_return_fixture(%{filename: filename})}
|
||||||
|
|
||||||
_url, _opts, ot, _addl when ot == @metadata_ot ->
|
_url, :get_source_metadata, _opts, _ot, _addl ->
|
||||||
{:ok, render_metadata(:channel_source_metadata)}
|
{:ok, render_metadata(:channel_source_metadata)}
|
||||||
end)
|
end)
|
||||||
|
|
||||||
@@ -248,10 +245,10 @@ defmodule Pinchflat.Metadata.SourceMetadataStorageWorkerTest do
|
|||||||
|
|
||||||
test "sets use_cookies if the source uses cookies" do
|
test "sets use_cookies if the source uses cookies" do
|
||||||
expect(YtDlpRunnerMock, :run, 2, fn
|
expect(YtDlpRunnerMock, :run, 2, fn
|
||||||
_url, _opts, ot, _addl when ot == @source_details_ot ->
|
_url, :get_source_details, _opts, _ot, _addl ->
|
||||||
{:ok, source_details_return_fixture()}
|
{:ok, source_details_return_fixture()}
|
||||||
|
|
||||||
_url, _opts, ot, addl when ot == @metadata_ot ->
|
_url, :get_source_metadata, _opts, _ot, addl ->
|
||||||
assert {:use_cookies, true} in addl
|
assert {:use_cookies, true} in addl
|
||||||
{:ok, render_metadata(:channel_source_metadata)}
|
{:ok, render_metadata(:channel_source_metadata)}
|
||||||
end)
|
end)
|
||||||
@@ -264,10 +261,10 @@ defmodule Pinchflat.Metadata.SourceMetadataStorageWorkerTest do
|
|||||||
|
|
||||||
test "does not set use_cookies if the source does not use cookies" do
|
test "does not set use_cookies if the source does not use cookies" do
|
||||||
expect(YtDlpRunnerMock, :run, 2, fn
|
expect(YtDlpRunnerMock, :run, 2, fn
|
||||||
_url, _opts, ot, _addl when ot == @source_details_ot ->
|
_url, :get_source_details, _opts, _ot, _addl ->
|
||||||
{:ok, source_details_return_fixture()}
|
{:ok, source_details_return_fixture()}
|
||||||
|
|
||||||
_url, _opts, ot, addl when ot == @metadata_ot ->
|
_url, :get_source_metadata, _opts, _ot, addl ->
|
||||||
assert {:use_cookies, false} in addl
|
assert {:use_cookies, false} in addl
|
||||||
{:ok, render_metadata(:channel_source_metadata)}
|
{:ok, render_metadata(:channel_source_metadata)}
|
||||||
end)
|
end)
|
||||||
@@ -282,12 +279,12 @@ defmodule Pinchflat.Metadata.SourceMetadataStorageWorkerTest do
|
|||||||
describe "perform/1 when determining the series_directory" do
|
describe "perform/1 when determining the series_directory" do
|
||||||
test "sets the series directory based on the returned media filepath" do
|
test "sets the series directory based on the returned media filepath" do
|
||||||
stub(YtDlpRunnerMock, :run, fn
|
stub(YtDlpRunnerMock, :run, fn
|
||||||
_url, _opts, ot, _addl when ot == @source_details_ot ->
|
_url, :get_source_details, _opts, _ot, _addl ->
|
||||||
filename = Path.join([Application.get_env(:pinchflat, :media_directory), "Season 1", "bar.mp4"])
|
filename = Path.join([Application.get_env(:pinchflat, :media_directory), "Season 1", "bar.mp4"])
|
||||||
|
|
||||||
{:ok, source_details_return_fixture(%{filename: filename})}
|
{:ok, source_details_return_fixture(%{filename: filename})}
|
||||||
|
|
||||||
_url, _opts, ot, _addl when ot == @metadata_ot ->
|
_url, :get_source_metadata, _opts, _ot, _addl ->
|
||||||
{:ok, "{}"}
|
{:ok, "{}"}
|
||||||
end)
|
end)
|
||||||
|
|
||||||
@@ -300,12 +297,12 @@ defmodule Pinchflat.Metadata.SourceMetadataStorageWorkerTest do
|
|||||||
|
|
||||||
test "does not set the series directory if it cannot be determined" do
|
test "does not set the series directory if it cannot be determined" do
|
||||||
stub(YtDlpRunnerMock, :run, fn
|
stub(YtDlpRunnerMock, :run, fn
|
||||||
_url, _opts, ot, _addl when ot == @source_details_ot ->
|
_url, :get_source_details, _opts, _ot, _addl ->
|
||||||
filename = Path.join([Application.get_env(:pinchflat, :media_directory), "foo", "bar.mp4"])
|
filename = Path.join([Application.get_env(:pinchflat, :media_directory), "foo", "bar.mp4"])
|
||||||
|
|
||||||
{:ok, source_details_return_fixture(%{filename: filename})}
|
{:ok, source_details_return_fixture(%{filename: filename})}
|
||||||
|
|
||||||
_url, _opts, ot, _addl when ot == @metadata_ot ->
|
_url, :get_source_metadata, _opts, _ot, _addl ->
|
||||||
{:ok, "{}"}
|
{:ok, "{}"}
|
||||||
end)
|
end)
|
||||||
|
|
||||||
@@ -318,11 +315,11 @@ defmodule Pinchflat.Metadata.SourceMetadataStorageWorkerTest do
|
|||||||
|
|
||||||
test "sets use_cookies if the source is set to use cookies" do
|
test "sets use_cookies if the source is set to use cookies" do
|
||||||
expect(YtDlpRunnerMock, :run, 2, fn
|
expect(YtDlpRunnerMock, :run, 2, fn
|
||||||
_url, _opts, ot, addl when ot == @source_details_ot ->
|
_url, :get_source_details, _opts, _ot, addl ->
|
||||||
assert {:use_cookies, true} in addl
|
assert {:use_cookies, true} in addl
|
||||||
{:ok, source_details_return_fixture()}
|
{:ok, source_details_return_fixture()}
|
||||||
|
|
||||||
_url, _opts, ot, _addl when ot == @metadata_ot ->
|
_url, :get_source_metadata, _opts, _ot, _addl ->
|
||||||
{:ok, "{}"}
|
{:ok, "{}"}
|
||||||
end)
|
end)
|
||||||
|
|
||||||
@@ -332,11 +329,11 @@ defmodule Pinchflat.Metadata.SourceMetadataStorageWorkerTest do
|
|||||||
|
|
||||||
test "does not set use_cookies if the source is not set to use cookies" do
|
test "does not set use_cookies if the source is not set to use cookies" do
|
||||||
expect(YtDlpRunnerMock, :run, 2, fn
|
expect(YtDlpRunnerMock, :run, 2, fn
|
||||||
_url, _opts, ot, addl when ot == @source_details_ot ->
|
_url, :get_source_details, _opts, _ot, addl ->
|
||||||
assert {:use_cookies, false} in addl
|
assert {:use_cookies, false} in addl
|
||||||
{:ok, source_details_return_fixture()}
|
{:ok, source_details_return_fixture()}
|
||||||
|
|
||||||
_url, _opts, ot, _addl when ot == @metadata_ot ->
|
_url, :get_source_metadata, _opts, _ot, _addl ->
|
||||||
{:ok, "{}"}
|
{:ok, "{}"}
|
||||||
end)
|
end)
|
||||||
|
|
||||||
@@ -348,12 +345,12 @@ defmodule Pinchflat.Metadata.SourceMetadataStorageWorkerTest do
|
|||||||
describe "perform/1 when storing the series NFO" do
|
describe "perform/1 when storing the series NFO" do
|
||||||
test "stores the NFO if specified" do
|
test "stores the NFO if specified" do
|
||||||
stub(YtDlpRunnerMock, :run, fn
|
stub(YtDlpRunnerMock, :run, fn
|
||||||
_url, _opts, ot, _addl when ot == @source_details_ot ->
|
_url, :get_source_details, _opts, _ot, _addl ->
|
||||||
filename = Path.join([Application.get_env(:pinchflat, :media_directory), "Season 1", "bar.mp4"])
|
filename = Path.join([Application.get_env(:pinchflat, :media_directory), "Season 1", "bar.mp4"])
|
||||||
|
|
||||||
{:ok, source_details_return_fixture(%{filename: filename})}
|
{:ok, source_details_return_fixture(%{filename: filename})}
|
||||||
|
|
||||||
_url, _opts, ot, _addl when ot == @metadata_ot ->
|
_url, :get_source_metadata, _opts, _ot, _addl ->
|
||||||
{:ok, "{}"}
|
{:ok, "{}"}
|
||||||
end)
|
end)
|
||||||
|
|
||||||
@@ -371,12 +368,12 @@ defmodule Pinchflat.Metadata.SourceMetadataStorageWorkerTest do
|
|||||||
|
|
||||||
test "does not store the NFO if not specified" do
|
test "does not store the NFO if not specified" do
|
||||||
stub(YtDlpRunnerMock, :run, fn
|
stub(YtDlpRunnerMock, :run, fn
|
||||||
_url, _opts, ot, _addl when ot == @source_details_ot ->
|
_url, :get_source_details, _opts, _ot, _addl ->
|
||||||
filename = Path.join([Application.get_env(:pinchflat, :media_directory), "Season 1", "bar.mp4"])
|
filename = Path.join([Application.get_env(:pinchflat, :media_directory), "Season 1", "bar.mp4"])
|
||||||
|
|
||||||
{:ok, source_details_return_fixture(%{filename: filename})}
|
{:ok, source_details_return_fixture(%{filename: filename})}
|
||||||
|
|
||||||
_url, _opts, ot, _addl when ot == @metadata_ot ->
|
_url, :get_source_metadata, _opts, _ot, _addl ->
|
||||||
{:ok, "{}"}
|
{:ok, "{}"}
|
||||||
end)
|
end)
|
||||||
|
|
||||||
@@ -390,12 +387,12 @@ defmodule Pinchflat.Metadata.SourceMetadataStorageWorkerTest do
|
|||||||
|
|
||||||
test "does not store the NFO if the series directory cannot be determined" do
|
test "does not store the NFO if the series directory cannot be determined" do
|
||||||
stub(YtDlpRunnerMock, :run, fn
|
stub(YtDlpRunnerMock, :run, fn
|
||||||
_url, _opts, ot, _addl when ot == @source_details_ot ->
|
_url, :get_source_details, _opts, _ot, _addl ->
|
||||||
filename = Path.join([Application.get_env(:pinchflat, :media_directory), "foo", "bar.mp4"])
|
filename = Path.join([Application.get_env(:pinchflat, :media_directory), "foo", "bar.mp4"])
|
||||||
|
|
||||||
{:ok, source_details_return_fixture(%{filename: filename})}
|
{:ok, source_details_return_fixture(%{filename: filename})}
|
||||||
|
|
||||||
_url, _opts, ot, _addl when ot == @metadata_ot ->
|
_url, :get_source_metadata, _opts, _ot, _addl ->
|
||||||
{:ok, "{}"}
|
{:ok, "{}"}
|
||||||
end)
|
end)
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,34 @@
|
|||||||
|
defmodule Pinchflat.Podcasts.OpmlFeedBuilderTest do
|
||||||
|
use Pinchflat.DataCase
|
||||||
|
|
||||||
|
import Pinchflat.SourcesFixtures
|
||||||
|
|
||||||
|
alias Pinchflat.Podcasts.OpmlFeedBuilder
|
||||||
|
|
||||||
|
setup do
|
||||||
|
source = source_fixture()
|
||||||
|
|
||||||
|
{:ok, source: source}
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "build/2" do
|
||||||
|
test "returns an XML document", %{source: source} do
|
||||||
|
res = OpmlFeedBuilder.build("http://example.com", [source])
|
||||||
|
|
||||||
|
assert String.contains?(res, ~s(<?xml version="1.0" encoding="UTF-8"?>))
|
||||||
|
end
|
||||||
|
|
||||||
|
test "escapes illegal characters" do
|
||||||
|
source = source_fixture(%{custom_name: "A & B"})
|
||||||
|
res = OpmlFeedBuilder.build("http://example.com", [source])
|
||||||
|
|
||||||
|
assert String.contains?(res, ~s(A & B))
|
||||||
|
end
|
||||||
|
|
||||||
|
test "build podcast link with URL base", %{source: source} do
|
||||||
|
res = OpmlFeedBuilder.build("http://example.com", [source])
|
||||||
|
|
||||||
|
assert String.contains?(res, ~s(http://example.com/sources/#{source.uuid}/feed.xml))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -6,6 +6,16 @@ defmodule Pinchflat.Podcasts.PodcastHelpersTest do
|
|||||||
|
|
||||||
alias Pinchflat.Podcasts.PodcastHelpers
|
alias Pinchflat.Podcasts.PodcastHelpers
|
||||||
|
|
||||||
|
describe "opml_sources" do
|
||||||
|
test "returns sources not marked for deletion" do
|
||||||
|
source = source_fixture()
|
||||||
|
source_fixture(%{marked_for_deletion_at: DateTime.utc_now()})
|
||||||
|
assert [found_source] = PodcastHelpers.opml_sources()
|
||||||
|
assert found_source.custom_name == source.custom_name
|
||||||
|
assert found_source.uuid == source.uuid
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
describe "persisted_media_items_for/2" do
|
describe "persisted_media_items_for/2" do
|
||||||
test "returns media items with files that exist on-disk" do
|
test "returns media items with files that exist on-disk" do
|
||||||
source = source_fixture()
|
source = source_fixture()
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
defmodule Pinchflat.SlowIndexing.FileFollowerServerTest do
|
defmodule Pinchflat.SlowIndexing.FileFollowerServerTest do
|
||||||
use Pinchflat.DataCase
|
use Pinchflat.DataCase
|
||||||
|
|
||||||
alias alias Pinchflat.Utils.FilesystemUtils
|
alias Pinchflat.Utils.FilesystemUtils
|
||||||
alias Pinchflat.SlowIndexing.FileFollowerServer
|
alias Pinchflat.SlowIndexing.FileFollowerServer
|
||||||
|
|
||||||
setup do
|
setup do
|
||||||
|
|||||||
@@ -50,45 +50,69 @@ defmodule Pinchflat.SlowIndexing.MediaCollectionIndexingWorkerTest do
|
|||||||
|
|
||||||
describe "perform/1" do
|
describe "perform/1" do
|
||||||
setup do
|
setup do
|
||||||
|
stub(YtDlpRunnerMock, :run, fn _url, :get_media_attributes_for_collection, _opts, _ot, _addl_opts -> {:ok, ""} end)
|
||||||
|
|
||||||
stub(AppriseRunnerMock, :run, fn _, _ -> {:ok, ""} end)
|
stub(AppriseRunnerMock, :run, fn _, _ -> {:ok, ""} end)
|
||||||
|
|
||||||
:ok
|
:ok
|
||||||
end
|
end
|
||||||
|
|
||||||
test "it indexes the source if it should be indexed" do
|
test "indexes the source if it should be indexed" do
|
||||||
expect(YtDlpRunnerMock, :run, fn _url, _opts, _ot, _addl_opts -> {:ok, ""} end)
|
expect(YtDlpRunnerMock, :run, fn _url, :get_media_attributes_for_collection, _opts, _ot, _addl_opts ->
|
||||||
|
{:ok, ""}
|
||||||
|
end)
|
||||||
|
|
||||||
source = source_fixture(index_frequency_minutes: 10)
|
source = source_fixture(index_frequency_minutes: 10)
|
||||||
|
|
||||||
perform_job(MediaCollectionIndexingWorker, %{id: source.id})
|
perform_job(MediaCollectionIndexingWorker, %{id: source.id})
|
||||||
end
|
end
|
||||||
|
|
||||||
test "it indexes the source no matter what if the source has never been indexed before" do
|
test "indexes the source no matter what if the source has never been indexed before" do
|
||||||
expect(YtDlpRunnerMock, :run, fn _url, _opts, _ot, _addl_opts -> {:ok, ""} end)
|
expect(YtDlpRunnerMock, :run, fn _url, :get_media_attributes_for_collection, _opts, _ot, _addl_opts ->
|
||||||
|
{:ok, ""}
|
||||||
|
end)
|
||||||
|
|
||||||
source = source_fixture(index_frequency_minutes: 0, last_indexed_at: nil)
|
source = source_fixture(index_frequency_minutes: 0, last_indexed_at: nil)
|
||||||
|
|
||||||
perform_job(MediaCollectionIndexingWorker, %{id: source.id})
|
perform_job(MediaCollectionIndexingWorker, %{id: source.id})
|
||||||
end
|
end
|
||||||
|
|
||||||
test "it indexes the source no matter what if the 'force' arg is passed" do
|
test "indexes the source no matter what if the 'force' arg is passed" do
|
||||||
expect(YtDlpRunnerMock, :run, fn _url, _opts, _ot, _addl_opts -> {:ok, ""} end)
|
expect(YtDlpRunnerMock, :run, fn _url, :get_media_attributes_for_collection, _opts, _ot, _addl_opts ->
|
||||||
|
{:ok, ""}
|
||||||
|
end)
|
||||||
|
|
||||||
source = source_fixture(index_frequency_minutes: 0, last_indexed_at: DateTime.utc_now())
|
source = source_fixture(index_frequency_minutes: 0, last_indexed_at: DateTime.utc_now())
|
||||||
|
|
||||||
perform_job(MediaCollectionIndexingWorker, %{id: source.id, force: true})
|
perform_job(MediaCollectionIndexingWorker, %{id: source.id, force: true})
|
||||||
end
|
end
|
||||||
|
|
||||||
test "it does not do any indexing if the source has been indexed and shouldn't be rescheduled" do
|
test "doesn't use a download archive if the index has been forced" do
|
||||||
expect(YtDlpRunnerMock, :run, 0, fn _url, _opts, _ot, _addl_opts -> {:ok, ""} end)
|
expect(YtDlpRunnerMock, :run, fn _url, :get_media_attributes_for_collection, opts, _ot, _addl_opts ->
|
||||||
|
refute :break_on_existing in opts
|
||||||
|
refute Keyword.has_key?(opts, :download_archive)
|
||||||
|
|
||||||
|
{:ok, ""}
|
||||||
|
end)
|
||||||
|
|
||||||
|
source =
|
||||||
|
source_fixture(collection_type: :channel, index_frequency_minutes: 0, last_indexed_at: DateTime.utc_now())
|
||||||
|
|
||||||
|
perform_job(MediaCollectionIndexingWorker, %{id: source.id, force: true})
|
||||||
|
end
|
||||||
|
|
||||||
|
test "does not do any indexing if the source has been indexed and shouldn't be rescheduled" do
|
||||||
|
expect(YtDlpRunnerMock, :run, 0, fn _url, :get_media_attributes_for_collection, _opts, _ot, _addl_opts ->
|
||||||
|
{:ok, ""}
|
||||||
|
end)
|
||||||
|
|
||||||
source = source_fixture(index_frequency_minutes: -1, last_indexed_at: DateTime.utc_now())
|
source = source_fixture(index_frequency_minutes: -1, last_indexed_at: DateTime.utc_now())
|
||||||
|
|
||||||
perform_job(MediaCollectionIndexingWorker, %{id: source.id})
|
perform_job(MediaCollectionIndexingWorker, %{id: source.id})
|
||||||
end
|
end
|
||||||
|
|
||||||
test "it does not reschedule if the source shouldn't be indexed" do
|
test "does not reschedule if the source shouldn't be indexed" do
|
||||||
stub(YtDlpRunnerMock, :run, fn _url, _opts, _ot, _addl_opts -> {:ok, ""} end)
|
stub(YtDlpRunnerMock, :run, fn _url, :get_media_attributes_for_collection, _opts, _ot, _addl_opts -> {:ok, ""} end)
|
||||||
|
|
||||||
source = source_fixture(index_frequency_minutes: -1)
|
source = source_fixture(index_frequency_minutes: -1)
|
||||||
perform_job(MediaCollectionIndexingWorker, %{id: source.id})
|
perform_job(MediaCollectionIndexingWorker, %{id: source.id})
|
||||||
@@ -96,8 +120,8 @@ defmodule Pinchflat.SlowIndexing.MediaCollectionIndexingWorkerTest do
|
|||||||
refute_enqueued(worker: MediaCollectionIndexingWorker, args: %{"id" => source.id})
|
refute_enqueued(worker: MediaCollectionIndexingWorker, args: %{"id" => source.id})
|
||||||
end
|
end
|
||||||
|
|
||||||
test "it kicks off a download job for each pending media item" do
|
test "kicks off a download job for each pending media item" do
|
||||||
expect(YtDlpRunnerMock, :run, fn _url, _opts, _ot, _addl_opts ->
|
expect(YtDlpRunnerMock, :run, fn _url, :get_media_attributes_for_collection, _opts, _ot, _addl_opts ->
|
||||||
{:ok, source_attributes_return_fixture()}
|
{:ok, source_attributes_return_fixture()}
|
||||||
end)
|
end)
|
||||||
|
|
||||||
@@ -107,8 +131,8 @@ defmodule Pinchflat.SlowIndexing.MediaCollectionIndexingWorkerTest do
|
|||||||
assert length(all_enqueued(worker: MediaDownloadWorker)) == 3
|
assert length(all_enqueued(worker: MediaDownloadWorker)) == 3
|
||||||
end
|
end
|
||||||
|
|
||||||
test "it starts a job for any pending media item even if it's from another run" do
|
test "starts a job for any pending media item even if it's from another run" do
|
||||||
expect(YtDlpRunnerMock, :run, fn _url, _opts, _ot, _addl_opts ->
|
expect(YtDlpRunnerMock, :run, fn _url, :get_media_attributes_for_collection, _opts, _ot, _addl_opts ->
|
||||||
{:ok, source_attributes_return_fixture()}
|
{:ok, source_attributes_return_fixture()}
|
||||||
end)
|
end)
|
||||||
|
|
||||||
@@ -119,8 +143,8 @@ defmodule Pinchflat.SlowIndexing.MediaCollectionIndexingWorkerTest do
|
|||||||
assert length(all_enqueued(worker: MediaDownloadWorker)) == 4
|
assert length(all_enqueued(worker: MediaDownloadWorker)) == 4
|
||||||
end
|
end
|
||||||
|
|
||||||
test "it does not kick off a job for media items that could not be saved" do
|
test "does not kick off a job for media items that could not be saved" do
|
||||||
expect(YtDlpRunnerMock, :run, fn _url, _opts, _ot, _addl_opts ->
|
expect(YtDlpRunnerMock, :run, fn _url, :get_media_attributes_for_collection, _opts, _ot, _addl_opts ->
|
||||||
{:ok, source_attributes_return_fixture()}
|
{:ok, source_attributes_return_fixture()}
|
||||||
end)
|
end)
|
||||||
|
|
||||||
@@ -132,9 +156,7 @@ defmodule Pinchflat.SlowIndexing.MediaCollectionIndexingWorkerTest do
|
|||||||
assert length(all_enqueued(worker: MediaDownloadWorker))
|
assert length(all_enqueued(worker: MediaDownloadWorker))
|
||||||
end
|
end
|
||||||
|
|
||||||
test "it reschedules the job based on the index frequency" do
|
test "reschedules the job based on the index frequency" do
|
||||||
expect(YtDlpRunnerMock, :run, fn _url, _opts, _ot, _addl_opts -> {:ok, ""} end)
|
|
||||||
|
|
||||||
source = source_fixture(index_frequency_minutes: 10)
|
source = source_fixture(index_frequency_minutes: 10)
|
||||||
perform_job(MediaCollectionIndexingWorker, %{id: source.id})
|
perform_job(MediaCollectionIndexingWorker, %{id: source.id})
|
||||||
|
|
||||||
@@ -145,9 +167,7 @@ defmodule Pinchflat.SlowIndexing.MediaCollectionIndexingWorkerTest do
|
|||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
test "it creates a task for the rescheduled job" do
|
test "creates a task for the rescheduled job" do
|
||||||
expect(YtDlpRunnerMock, :run, fn _url, _opts, _ot, _addl_opts -> {:ok, ""} end)
|
|
||||||
|
|
||||||
source = source_fixture(index_frequency_minutes: 10)
|
source = source_fixture(index_frequency_minutes: 10)
|
||||||
|
|
||||||
task_count_fetcher = fn ->
|
task_count_fetcher = fn ->
|
||||||
@@ -159,9 +179,7 @@ defmodule Pinchflat.SlowIndexing.MediaCollectionIndexingWorkerTest do
|
|||||||
end)
|
end)
|
||||||
end
|
end
|
||||||
|
|
||||||
test "it creates a future task for fast indexing if appropriate" do
|
test "creates a future task for fast indexing if appropriate" do
|
||||||
expect(YtDlpRunnerMock, :run, fn _url, _opts, _ot, _addl_opts -> {:ok, ""} end)
|
|
||||||
|
|
||||||
source = source_fixture(index_frequency_minutes: 10, fast_index: true)
|
source = source_fixture(index_frequency_minutes: 10, fast_index: true)
|
||||||
perform_job(MediaCollectionIndexingWorker, %{id: source.id})
|
perform_job(MediaCollectionIndexingWorker, %{id: source.id})
|
||||||
|
|
||||||
@@ -172,9 +190,7 @@ defmodule Pinchflat.SlowIndexing.MediaCollectionIndexingWorkerTest do
|
|||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
test "it deletes existing fast indexing tasks if a new one is created" do
|
test "deletes existing fast indexing tasks if a new one is created" do
|
||||||
expect(YtDlpRunnerMock, :run, fn _url, _opts, _ot, _addl_opts -> {:ok, ""} end)
|
|
||||||
|
|
||||||
source = source_fixture(index_frequency_minutes: 10, fast_index: true)
|
source = source_fixture(index_frequency_minutes: 10, fast_index: true)
|
||||||
{:ok, job} = Oban.insert(FastIndexingWorker.new(%{"id" => source.id}))
|
{:ok, job} = Oban.insert(FastIndexingWorker.new(%{"id" => source.id}))
|
||||||
task = task_fixture(source_id: source.id, job_id: job.id)
|
task = task_fixture(source_id: source.id, job_id: job.id)
|
||||||
@@ -184,17 +200,17 @@ defmodule Pinchflat.SlowIndexing.MediaCollectionIndexingWorkerTest do
|
|||||||
assert_raise Ecto.NoResultsError, fn -> Repo.reload!(task) end
|
assert_raise Ecto.NoResultsError, fn -> Repo.reload!(task) end
|
||||||
end
|
end
|
||||||
|
|
||||||
test "it does not create a task for fast indexing otherwise" do
|
test "does not create a task for fast indexing otherwise" do
|
||||||
expect(YtDlpRunnerMock, :run, fn _url, _opts, _ot, _addl_opts -> {:ok, ""} end)
|
|
||||||
|
|
||||||
source = source_fixture(index_frequency_minutes: 10, fast_index: false)
|
source = source_fixture(index_frequency_minutes: 10, fast_index: false)
|
||||||
perform_job(MediaCollectionIndexingWorker, %{id: source.id})
|
perform_job(MediaCollectionIndexingWorker, %{id: source.id})
|
||||||
|
|
||||||
refute_enqueued(worker: FastIndexingWorker)
|
refute_enqueued(worker: FastIndexingWorker)
|
||||||
end
|
end
|
||||||
|
|
||||||
test "it creates the basic media_item records" do
|
test "creates the basic media_item records" do
|
||||||
expect(YtDlpRunnerMock, :run, fn _url, _opts, _ot, _addl_opts -> {:ok, source_attributes_return_fixture()} end)
|
expect(YtDlpRunnerMock, :run, fn _url, :get_media_attributes_for_collection, _opts, _ot, _addl_opts ->
|
||||||
|
{:ok, source_attributes_return_fixture()}
|
||||||
|
end)
|
||||||
|
|
||||||
source = source_fixture(index_frequency_minutes: 10)
|
source = source_fixture(index_frequency_minutes: 10)
|
||||||
|
|
||||||
@@ -225,7 +241,7 @@ defmodule Pinchflat.SlowIndexing.MediaCollectionIndexingWorkerTest do
|
|||||||
test "sends a notification if new media was found" do
|
test "sends a notification if new media was found" do
|
||||||
source = source_fixture()
|
source = source_fixture()
|
||||||
|
|
||||||
expect(YtDlpRunnerMock, :run, fn _url, _opts, _ot, _addl_opts ->
|
expect(YtDlpRunnerMock, :run, fn _url, :get_media_attributes_for_collection, _opts, _ot, _addl_opts ->
|
||||||
{:ok, source_attributes_return_fixture()}
|
{:ok, source_attributes_return_fixture()}
|
||||||
end)
|
end)
|
||||||
|
|
||||||
|
|||||||
@@ -14,6 +14,10 @@ defmodule Pinchflat.SlowIndexing.SlowIndexingHelpersTest do
|
|||||||
alias Pinchflat.SlowIndexing.SlowIndexingHelpers
|
alias Pinchflat.SlowIndexing.SlowIndexingHelpers
|
||||||
alias Pinchflat.SlowIndexing.MediaCollectionIndexingWorker
|
alias Pinchflat.SlowIndexing.MediaCollectionIndexingWorker
|
||||||
|
|
||||||
|
setup do
|
||||||
|
{:ok, %{source: source_fixture()}}
|
||||||
|
end
|
||||||
|
|
||||||
describe "kickoff_indexing_task/3" do
|
describe "kickoff_indexing_task/3" do
|
||||||
test "schedules a job" do
|
test "schedules a job" do
|
||||||
source = source_fixture(index_frequency_minutes: 1)
|
source = source_fixture(index_frequency_minutes: 1)
|
||||||
@@ -53,6 +57,16 @@ defmodule Pinchflat.SlowIndexing.SlowIndexingHelpersTest do
|
|||||||
assert_in_delta DateTime.diff(job.scheduled_at, DateTime.utc_now(), :second), 0, 1
|
assert_in_delta DateTime.diff(job.scheduled_at, DateTime.utc_now(), :second), 0, 1
|
||||||
end
|
end
|
||||||
|
|
||||||
|
test "schedules a job immediately if the user is forcing an index" do
|
||||||
|
source = source_fixture(index_frequency_minutes: 30, last_indexed_at: now_minus(5, :minutes))
|
||||||
|
|
||||||
|
assert {:ok, _} = SlowIndexingHelpers.kickoff_indexing_task(source, %{force: true})
|
||||||
|
|
||||||
|
[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)
|
||||||
|
|
||||||
@@ -123,12 +137,6 @@ defmodule Pinchflat.SlowIndexing.SlowIndexingHelpersTest do
|
|||||||
end
|
end
|
||||||
|
|
||||||
describe "delete_indexing_tasks/2" do
|
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
|
test "deletes slow indexing tasks for the source", %{source: source} do
|
||||||
{:ok, job} = Oban.insert(MediaCollectionIndexingWorker.new(%{"id" => source.id}))
|
{:ok, job} = Oban.insert(MediaCollectionIndexingWorker.new(%{"id" => source.id}))
|
||||||
_task = task_fixture(source_id: source.id, job_id: job.id)
|
_task = task_fixture(source_id: source.id, job_id: job.id)
|
||||||
@@ -172,13 +180,13 @@ defmodule Pinchflat.SlowIndexing.SlowIndexingHelpersTest do
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
describe "index_and_enqueue_download_for_media_items/1" do
|
describe "index_and_enqueue_download_for_media_items/2" do
|
||||||
setup do
|
setup do
|
||||||
stub(YtDlpRunnerMock, :run, fn _url, _opts, _ot, _addl_opts ->
|
stub(YtDlpRunnerMock, :run, fn _url, :get_media_attributes_for_collection, _opts, _ot, _addl_opts ->
|
||||||
{:ok, source_attributes_return_fixture()}
|
{:ok, source_attributes_return_fixture()}
|
||||||
end)
|
end)
|
||||||
|
|
||||||
{:ok, [source: source_fixture()]}
|
:ok
|
||||||
end
|
end
|
||||||
|
|
||||||
test "creates a media_item record for each media ID returned", %{source: source} do
|
test "creates a media_item record for each media ID returned", %{source: source} do
|
||||||
@@ -259,7 +267,7 @@ defmodule Pinchflat.SlowIndexing.SlowIndexingHelpersTest do
|
|||||||
end
|
end
|
||||||
|
|
||||||
test "doesn't blow up if a media item cannot be coerced into a struct", %{source: source} do
|
test "doesn't blow up if a media item cannot be coerced into a struct", %{source: source} do
|
||||||
stub(YtDlpRunnerMock, :run, fn _url, _opts, _ot, _addl_opts ->
|
stub(YtDlpRunnerMock, :run, fn _url, :get_media_attributes_for_collection, _opts, _ot, _addl_opts ->
|
||||||
response =
|
response =
|
||||||
Phoenix.json_library().encode!(%{
|
Phoenix.json_library().encode!(%{
|
||||||
id: "video3",
|
id: "video3",
|
||||||
@@ -283,7 +291,7 @@ defmodule Pinchflat.SlowIndexing.SlowIndexingHelpersTest do
|
|||||||
end
|
end
|
||||||
|
|
||||||
test "passes the source's download options to the yt-dlp runner", %{source: source} do
|
test "passes the source's download options to the yt-dlp runner", %{source: source} do
|
||||||
expect(YtDlpRunnerMock, :run, fn _url, opts, _ot, _addl_opts ->
|
expect(YtDlpRunnerMock, :run, fn _url, :get_media_attributes_for_collection, opts, _ot, _addl_opts ->
|
||||||
assert {:output, "/tmp/test/media/%(title)S.%(ext)S"} in opts
|
assert {:output, "/tmp/test/media/%(title)S.%(ext)S"} in opts
|
||||||
assert {:remux_video, "mp4"} in opts
|
assert {:remux_video, "mp4"} in opts
|
||||||
{:ok, source_attributes_return_fixture()}
|
{:ok, source_attributes_return_fixture()}
|
||||||
@@ -293,7 +301,7 @@ defmodule Pinchflat.SlowIndexing.SlowIndexingHelpersTest do
|
|||||||
end
|
end
|
||||||
|
|
||||||
test "sets use_cookies if the source uses cookies" do
|
test "sets use_cookies if the source uses cookies" do
|
||||||
expect(YtDlpRunnerMock, :run, fn _url, _opts, _ot, addl_opts ->
|
expect(YtDlpRunnerMock, :run, fn _url, :get_media_attributes_for_collection, _opts, _ot, addl_opts ->
|
||||||
assert {:use_cookies, true} in addl_opts
|
assert {:use_cookies, true} in addl_opts
|
||||||
{:ok, source_attributes_return_fixture()}
|
{:ok, source_attributes_return_fixture()}
|
||||||
end)
|
end)
|
||||||
@@ -304,7 +312,7 @@ defmodule Pinchflat.SlowIndexing.SlowIndexingHelpersTest do
|
|||||||
end
|
end
|
||||||
|
|
||||||
test "doesn't set use_cookies if the source doesn't use cookies" do
|
test "doesn't set use_cookies if the source doesn't use cookies" do
|
||||||
expect(YtDlpRunnerMock, :run, fn _url, _opts, _ot, addl_opts ->
|
expect(YtDlpRunnerMock, :run, fn _url, :get_media_attributes_for_collection, _opts, _ot, addl_opts ->
|
||||||
assert {:use_cookies, false} in addl_opts
|
assert {:use_cookies, false} in addl_opts
|
||||||
{:ok, source_attributes_return_fixture()}
|
{:ok, source_attributes_return_fixture()}
|
||||||
end)
|
end)
|
||||||
@@ -315,15 +323,11 @@ defmodule Pinchflat.SlowIndexing.SlowIndexingHelpersTest do
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
describe "index_and_enqueue_download_for_media_items/1 when testing file watcher" do
|
describe "index_and_enqueue_download_for_media_items/2 when testing file watcher" do
|
||||||
setup do
|
|
||||||
{:ok, [source: source_fixture()]}
|
|
||||||
end
|
|
||||||
|
|
||||||
test "creates a new media item for everything already in the file", %{source: source} do
|
test "creates a new media item for everything already in the file", %{source: source} do
|
||||||
watcher_poll_interval = Application.get_env(:pinchflat, :file_watcher_poll_interval)
|
watcher_poll_interval = Application.get_env(:pinchflat, :file_watcher_poll_interval)
|
||||||
|
|
||||||
stub(YtDlpRunnerMock, :run, fn _url, _opts, _ot, addl_opts ->
|
stub(YtDlpRunnerMock, :run, fn _url, :get_media_attributes_for_collection, _opts, _ot, addl_opts ->
|
||||||
filepath = Keyword.get(addl_opts, :output_filepath)
|
filepath = Keyword.get(addl_opts, :output_filepath)
|
||||||
File.write(filepath, source_attributes_return_fixture())
|
File.write(filepath, source_attributes_return_fixture())
|
||||||
|
|
||||||
@@ -342,7 +346,7 @@ defmodule Pinchflat.SlowIndexing.SlowIndexingHelpersTest do
|
|||||||
test "enqueues a download for everything already in the file", %{source: source} do
|
test "enqueues a download for everything already in the file", %{source: source} do
|
||||||
watcher_poll_interval = Application.get_env(:pinchflat, :file_watcher_poll_interval)
|
watcher_poll_interval = Application.get_env(:pinchflat, :file_watcher_poll_interval)
|
||||||
|
|
||||||
stub(YtDlpRunnerMock, :run, fn _url, _opts, _ot, addl_opts ->
|
stub(YtDlpRunnerMock, :run, fn _url, :get_media_attributes_for_collection, _opts, _ot, addl_opts ->
|
||||||
filepath = Keyword.get(addl_opts, :output_filepath)
|
filepath = Keyword.get(addl_opts, :output_filepath)
|
||||||
File.write(filepath, source_attributes_return_fixture())
|
File.write(filepath, source_attributes_return_fixture())
|
||||||
|
|
||||||
@@ -362,7 +366,7 @@ defmodule Pinchflat.SlowIndexing.SlowIndexingHelpersTest do
|
|||||||
watcher_poll_interval = Application.get_env(:pinchflat, :file_watcher_poll_interval)
|
watcher_poll_interval = Application.get_env(:pinchflat, :file_watcher_poll_interval)
|
||||||
source = source_fixture(download_media: false)
|
source = source_fixture(download_media: false)
|
||||||
|
|
||||||
stub(YtDlpRunnerMock, :run, fn _url, _opts, _ot, addl_opts ->
|
stub(YtDlpRunnerMock, :run, fn _url, :get_media_attributes_for_collection, _opts, _ot, addl_opts ->
|
||||||
filepath = Keyword.get(addl_opts, :output_filepath)
|
filepath = Keyword.get(addl_opts, :output_filepath)
|
||||||
File.write(filepath, source_attributes_return_fixture())
|
File.write(filepath, source_attributes_return_fixture())
|
||||||
|
|
||||||
@@ -382,7 +386,7 @@ defmodule Pinchflat.SlowIndexing.SlowIndexingHelpersTest do
|
|||||||
profile = media_profile_fixture(%{shorts_behaviour: :exclude})
|
profile = media_profile_fixture(%{shorts_behaviour: :exclude})
|
||||||
source = source_fixture(%{media_profile_id: profile.id})
|
source = source_fixture(%{media_profile_id: profile.id})
|
||||||
|
|
||||||
stub(YtDlpRunnerMock, :run, fn _url, _opts, _ot, addl_opts ->
|
stub(YtDlpRunnerMock, :run, fn _url, :get_media_attributes_for_collection, _opts, _ot, addl_opts ->
|
||||||
filepath = Keyword.get(addl_opts, :output_filepath)
|
filepath = Keyword.get(addl_opts, :output_filepath)
|
||||||
|
|
||||||
contents =
|
contents =
|
||||||
@@ -413,7 +417,7 @@ defmodule Pinchflat.SlowIndexing.SlowIndexingHelpersTest do
|
|||||||
test "does not enqueue multiple download jobs for the same media items", %{source: source} do
|
test "does not enqueue multiple download jobs for the same media items", %{source: source} do
|
||||||
watcher_poll_interval = Application.get_env(:pinchflat, :file_watcher_poll_interval)
|
watcher_poll_interval = Application.get_env(:pinchflat, :file_watcher_poll_interval)
|
||||||
|
|
||||||
stub(YtDlpRunnerMock, :run, fn _url, _opts, _ot, addl_opts ->
|
stub(YtDlpRunnerMock, :run, fn _url, :get_media_attributes_for_collection, _opts, _ot, addl_opts ->
|
||||||
filepath = Keyword.get(addl_opts, :output_filepath)
|
filepath = Keyword.get(addl_opts, :output_filepath)
|
||||||
File.write(filepath, source_attributes_return_fixture())
|
File.write(filepath, source_attributes_return_fixture())
|
||||||
|
|
||||||
@@ -432,7 +436,7 @@ defmodule Pinchflat.SlowIndexing.SlowIndexingHelpersTest do
|
|||||||
test "does not blow up if the file returns invalid json", %{source: source} do
|
test "does not blow up if the file returns invalid json", %{source: source} do
|
||||||
watcher_poll_interval = Application.get_env(:pinchflat, :file_watcher_poll_interval)
|
watcher_poll_interval = Application.get_env(:pinchflat, :file_watcher_poll_interval)
|
||||||
|
|
||||||
stub(YtDlpRunnerMock, :run, fn _url, _opts, _ot, addl_opts ->
|
stub(YtDlpRunnerMock, :run, fn _url, :get_media_attributes_for_collection, _opts, _ot, addl_opts ->
|
||||||
filepath = Keyword.get(addl_opts, :output_filepath)
|
filepath = Keyword.get(addl_opts, :output_filepath)
|
||||||
File.write(filepath, "INVALID")
|
File.write(filepath, "INVALID")
|
||||||
|
|
||||||
@@ -446,4 +450,62 @@ defmodule Pinchflat.SlowIndexing.SlowIndexingHelpersTest do
|
|||||||
assert [] = SlowIndexingHelpers.index_and_enqueue_download_for_media_items(source)
|
assert [] = SlowIndexingHelpers.index_and_enqueue_download_for_media_items(source)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
describe "index_and_enqueue_download_for_media_items when testing the download archive" do
|
||||||
|
test "a download archive is used if the source is a channel", %{source: source} do
|
||||||
|
expect(YtDlpRunnerMock, :run, fn _url, :get_media_attributes_for_collection, opts, _ot, _addl_opts ->
|
||||||
|
assert :break_on_existing in opts
|
||||||
|
assert Keyword.has_key?(opts, :download_archive)
|
||||||
|
|
||||||
|
{:ok, source_attributes_return_fixture()}
|
||||||
|
end)
|
||||||
|
|
||||||
|
SlowIndexingHelpers.index_and_enqueue_download_for_media_items(source)
|
||||||
|
end
|
||||||
|
|
||||||
|
test "a download archive is not used if the source is not a channel" do
|
||||||
|
source = source_fixture(%{collection_type: :playlist})
|
||||||
|
|
||||||
|
expect(YtDlpRunnerMock, :run, fn _url, :get_media_attributes_for_collection, opts, _ot, _addl_opts ->
|
||||||
|
refute :break_on_existing in opts
|
||||||
|
refute Keyword.has_key?(opts, :download_archive)
|
||||||
|
|
||||||
|
{:ok, source_attributes_return_fixture()}
|
||||||
|
end)
|
||||||
|
|
||||||
|
SlowIndexingHelpers.index_and_enqueue_download_for_media_items(source)
|
||||||
|
end
|
||||||
|
|
||||||
|
test "a download archive is not used if the index has been forced to run" do
|
||||||
|
source = source_fixture(%{collection_type: :channel})
|
||||||
|
|
||||||
|
expect(YtDlpRunnerMock, :run, fn _url, :get_media_attributes_for_collection, opts, _ot, _addl_opts ->
|
||||||
|
refute :break_on_existing in opts
|
||||||
|
refute Keyword.has_key?(opts, :download_archive)
|
||||||
|
|
||||||
|
{:ok, source_attributes_return_fixture()}
|
||||||
|
end)
|
||||||
|
|
||||||
|
SlowIndexingHelpers.index_and_enqueue_download_for_media_items(source, was_forced: true)
|
||||||
|
end
|
||||||
|
|
||||||
|
test "the download archive is formatted correctly and contains the right video", %{source: source} do
|
||||||
|
media_items =
|
||||||
|
1..21
|
||||||
|
|> Enum.map(fn n ->
|
||||||
|
media_item_fixture(%{source_id: source.id, uploaded_at: now_minus(n, :days)})
|
||||||
|
end)
|
||||||
|
|
||||||
|
expect(YtDlpRunnerMock, :run, fn _url, :get_media_attributes_for_collection, opts, _ot, _addl_opts ->
|
||||||
|
archive_file = Keyword.get(opts, :download_archive)
|
||||||
|
last_media_item = List.last(media_items)
|
||||||
|
|
||||||
|
assert File.read!(archive_file) == "youtube #{last_media_item.media_id}"
|
||||||
|
|
||||||
|
{:ok, source_attributes_return_fixture()}
|
||||||
|
end)
|
||||||
|
|
||||||
|
SlowIndexingHelpers.index_and_enqueue_download_for_media_items(source)
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -85,7 +85,7 @@ defmodule Pinchflat.SourcesTest do
|
|||||||
|
|
||||||
describe "create_source/2" do
|
describe "create_source/2" do
|
||||||
test "automatically sets the UUID" do
|
test "automatically sets the UUID" do
|
||||||
expect(YtDlpRunnerMock, :run, &channel_mock/4)
|
expect(YtDlpRunnerMock, :run, &channel_mock/5)
|
||||||
|
|
||||||
valid_attrs = %{
|
valid_attrs = %{
|
||||||
media_profile_id: media_profile_fixture().id,
|
media_profile_id: media_profile_fixture().id,
|
||||||
@@ -97,7 +97,7 @@ defmodule Pinchflat.SourcesTest do
|
|||||||
end
|
end
|
||||||
|
|
||||||
test "UUID is not writable by the user" do
|
test "UUID is not writable by the user" do
|
||||||
expect(YtDlpRunnerMock, :run, &channel_mock/4)
|
expect(YtDlpRunnerMock, :run, &channel_mock/5)
|
||||||
|
|
||||||
valid_attrs = %{
|
valid_attrs = %{
|
||||||
media_profile_id: media_profile_fixture().id,
|
media_profile_id: media_profile_fixture().id,
|
||||||
@@ -110,7 +110,7 @@ defmodule Pinchflat.SourcesTest do
|
|||||||
end
|
end
|
||||||
|
|
||||||
test "creates a source and adds name + ID from runner response for channels" do
|
test "creates a source and adds name + ID from runner response for channels" do
|
||||||
expect(YtDlpRunnerMock, :run, &channel_mock/4)
|
expect(YtDlpRunnerMock, :run, &channel_mock/5)
|
||||||
|
|
||||||
valid_attrs = %{
|
valid_attrs = %{
|
||||||
media_profile_id: media_profile_fixture().id,
|
media_profile_id: media_profile_fixture().id,
|
||||||
@@ -123,7 +123,7 @@ defmodule Pinchflat.SourcesTest do
|
|||||||
end
|
end
|
||||||
|
|
||||||
test "creates a source and adds name + ID for playlists" do
|
test "creates a source and adds name + ID for playlists" do
|
||||||
expect(YtDlpRunnerMock, :run, &playlist_mock/4)
|
expect(YtDlpRunnerMock, :run, &playlist_mock/5)
|
||||||
|
|
||||||
valid_attrs = %{
|
valid_attrs = %{
|
||||||
media_profile_id: media_profile_fixture().id,
|
media_profile_id: media_profile_fixture().id,
|
||||||
@@ -136,7 +136,7 @@ defmodule Pinchflat.SourcesTest do
|
|||||||
end
|
end
|
||||||
|
|
||||||
test "adds an error if the runner fails" do
|
test "adds an error if the runner fails" do
|
||||||
expect(YtDlpRunnerMock, :run, fn _url, _opts, _ot, _addl -> {:error, "some error", 1} end)
|
expect(YtDlpRunnerMock, :run, fn _url, :get_source_details, _opts, _ot, _addl -> {:error, "some error", 1} end)
|
||||||
|
|
||||||
valid_attrs = %{
|
valid_attrs = %{
|
||||||
media_profile_id: media_profile_fixture().id,
|
media_profile_id: media_profile_fixture().id,
|
||||||
@@ -148,7 +148,7 @@ defmodule Pinchflat.SourcesTest do
|
|||||||
end
|
end
|
||||||
|
|
||||||
test "adds an error if the runner succeeds but the result was invalid JSON" do
|
test "adds an error if the runner succeeds but the result was invalid JSON" do
|
||||||
expect(YtDlpRunnerMock, :run, fn _url, _opts, _ot, _addl -> {:ok, "Not JSON"} end)
|
expect(YtDlpRunnerMock, :run, fn _url, :get_source_details, _opts, _ot, _addl -> {:ok, "Not JSON"} end)
|
||||||
|
|
||||||
valid_attrs = %{
|
valid_attrs = %{
|
||||||
media_profile_id: media_profile_fixture().id,
|
media_profile_id: media_profile_fixture().id,
|
||||||
@@ -160,7 +160,7 @@ defmodule Pinchflat.SourcesTest do
|
|||||||
end
|
end
|
||||||
|
|
||||||
test "you can specify a custom custom_name" do
|
test "you can specify a custom custom_name" do
|
||||||
expect(YtDlpRunnerMock, :run, &channel_mock/4)
|
expect(YtDlpRunnerMock, :run, &channel_mock/5)
|
||||||
|
|
||||||
valid_attrs = %{
|
valid_attrs = %{
|
||||||
media_profile_id: media_profile_fixture().id,
|
media_profile_id: media_profile_fixture().id,
|
||||||
@@ -174,7 +174,7 @@ defmodule Pinchflat.SourcesTest do
|
|||||||
end
|
end
|
||||||
|
|
||||||
test "friendly name is pulled from collection_name if not specified" do
|
test "friendly name is pulled from collection_name if not specified" do
|
||||||
expect(YtDlpRunnerMock, :run, &channel_mock/4)
|
expect(YtDlpRunnerMock, :run, &channel_mock/5)
|
||||||
|
|
||||||
valid_attrs = %{
|
valid_attrs = %{
|
||||||
media_profile_id: media_profile_fixture().id,
|
media_profile_id: media_profile_fixture().id,
|
||||||
@@ -187,7 +187,7 @@ defmodule Pinchflat.SourcesTest do
|
|||||||
end
|
end
|
||||||
|
|
||||||
test "creation enforces uniqueness of collection_id scoped to the media_profile and title regex" do
|
test "creation enforces uniqueness of collection_id scoped to the media_profile and title regex" do
|
||||||
expect(YtDlpRunnerMock, :run, 2, fn _url, _opts, _ot, _addl ->
|
expect(YtDlpRunnerMock, :run, 2, fn _url, :get_source_details, _opts, _ot, _addl ->
|
||||||
{:ok,
|
{:ok,
|
||||||
Phoenix.json_library().encode!(%{
|
Phoenix.json_library().encode!(%{
|
||||||
channel: "some channel name",
|
channel: "some channel name",
|
||||||
@@ -208,7 +208,7 @@ defmodule Pinchflat.SourcesTest do
|
|||||||
end
|
end
|
||||||
|
|
||||||
test "creation lets you duplicate collection_ids and profiles as long as the regex is different" do
|
test "creation lets you duplicate collection_ids and profiles as long as the regex is different" do
|
||||||
expect(YtDlpRunnerMock, :run, 2, fn _url, _opts, _ot, _addl ->
|
expect(YtDlpRunnerMock, :run, 2, fn _url, :get_source_details, _opts, _ot, _addl ->
|
||||||
{:ok,
|
{:ok,
|
||||||
Phoenix.json_library().encode!(%{
|
Phoenix.json_library().encode!(%{
|
||||||
channel: "some channel name",
|
channel: "some channel name",
|
||||||
@@ -232,7 +232,7 @@ defmodule Pinchflat.SourcesTest do
|
|||||||
end
|
end
|
||||||
|
|
||||||
test "creation lets you duplicate collection_ids as long as the media profile is different" do
|
test "creation lets you duplicate collection_ids as long as the media profile is different" do
|
||||||
expect(YtDlpRunnerMock, :run, 2, fn _url, _opts, _ot, _addl ->
|
expect(YtDlpRunnerMock, :run, 2, fn _url, :get_source_details, _opts, _ot, _addl ->
|
||||||
{:ok,
|
{:ok,
|
||||||
Phoenix.json_library().encode!(%{
|
Phoenix.json_library().encode!(%{
|
||||||
channel: "some channel name",
|
channel: "some channel name",
|
||||||
@@ -256,8 +256,8 @@ defmodule Pinchflat.SourcesTest do
|
|||||||
end
|
end
|
||||||
|
|
||||||
test "collection_type is inferred from source details" do
|
test "collection_type is inferred from source details" do
|
||||||
expect(YtDlpRunnerMock, :run, &channel_mock/4)
|
expect(YtDlpRunnerMock, :run, &channel_mock/5)
|
||||||
expect(YtDlpRunnerMock, :run, &playlist_mock/4)
|
expect(YtDlpRunnerMock, :run, &playlist_mock/5)
|
||||||
|
|
||||||
valid_attrs = %{
|
valid_attrs = %{
|
||||||
media_profile_id: media_profile_fixture().id,
|
media_profile_id: media_profile_fixture().id,
|
||||||
@@ -276,13 +276,13 @@ defmodule Pinchflat.SourcesTest do
|
|||||||
end
|
end
|
||||||
|
|
||||||
test "creation with invalid data fails fast and does not call the runner" do
|
test "creation with invalid data fails fast and does not call the runner" do
|
||||||
expect(YtDlpRunnerMock, :run, 0, &channel_mock/4)
|
expect(YtDlpRunnerMock, :run, 0, &channel_mock/5)
|
||||||
|
|
||||||
assert {:error, %Ecto.Changeset{}} = Sources.create_source(@invalid_source_attrs)
|
assert {:error, %Ecto.Changeset{}} = Sources.create_source(@invalid_source_attrs)
|
||||||
end
|
end
|
||||||
|
|
||||||
test "creation will schedule the indexing task" do
|
test "creation will schedule the indexing task" do
|
||||||
expect(YtDlpRunnerMock, :run, &channel_mock/4)
|
expect(YtDlpRunnerMock, :run, &channel_mock/5)
|
||||||
|
|
||||||
valid_attrs = %{
|
valid_attrs = %{
|
||||||
media_profile_id: media_profile_fixture().id,
|
media_profile_id: media_profile_fixture().id,
|
||||||
@@ -295,7 +295,7 @@ defmodule Pinchflat.SourcesTest do
|
|||||||
end
|
end
|
||||||
|
|
||||||
test "creation schedules an index test even if the index frequency is 0" do
|
test "creation schedules an index test even if the index frequency is 0" do
|
||||||
expect(YtDlpRunnerMock, :run, &channel_mock/4)
|
expect(YtDlpRunnerMock, :run, &channel_mock/5)
|
||||||
|
|
||||||
valid_attrs = %{
|
valid_attrs = %{
|
||||||
media_profile_id: media_profile_fixture().id,
|
media_profile_id: media_profile_fixture().id,
|
||||||
@@ -309,7 +309,7 @@ defmodule Pinchflat.SourcesTest do
|
|||||||
end
|
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
|
||||||
expect(YtDlpRunnerMock, :run, &channel_mock/4)
|
expect(YtDlpRunnerMock, :run, &channel_mock/5)
|
||||||
|
|
||||||
valid_attrs = %{
|
valid_attrs = %{
|
||||||
media_profile_id: media_profile_fixture().id,
|
media_profile_id: media_profile_fixture().id,
|
||||||
@@ -324,7 +324,7 @@ defmodule Pinchflat.SourcesTest do
|
|||||||
end
|
end
|
||||||
|
|
||||||
test "disabling fast index will not change the index frequency" do
|
test "disabling fast index will not change the index frequency" do
|
||||||
expect(YtDlpRunnerMock, :run, &channel_mock/4)
|
expect(YtDlpRunnerMock, :run, &channel_mock/5)
|
||||||
|
|
||||||
valid_attrs = %{
|
valid_attrs = %{
|
||||||
media_profile_id: media_profile_fixture().id,
|
media_profile_id: media_profile_fixture().id,
|
||||||
@@ -339,7 +339,7 @@ defmodule Pinchflat.SourcesTest do
|
|||||||
end
|
end
|
||||||
|
|
||||||
test "creating will kickoff a metadata storage worker" do
|
test "creating will kickoff a metadata storage worker" do
|
||||||
expect(YtDlpRunnerMock, :run, &channel_mock/4)
|
expect(YtDlpRunnerMock, :run, &channel_mock/5)
|
||||||
|
|
||||||
valid_attrs = %{
|
valid_attrs = %{
|
||||||
media_profile_id: media_profile_fixture().id,
|
media_profile_id: media_profile_fixture().id,
|
||||||
@@ -356,7 +356,7 @@ defmodule Pinchflat.SourcesTest do
|
|||||||
|
|
||||||
describe "create_source/2 when testing options" do
|
describe "create_source/2 when testing options" do
|
||||||
test "run_post_commit_tasks: false won't enqueue post-commit tasks" do
|
test "run_post_commit_tasks: false won't enqueue post-commit tasks" do
|
||||||
expect(YtDlpRunnerMock, :run, &channel_mock/4)
|
expect(YtDlpRunnerMock, :run, &channel_mock/5)
|
||||||
|
|
||||||
valid_attrs = %{
|
valid_attrs = %{
|
||||||
media_profile_id: media_profile_fixture().id,
|
media_profile_id: media_profile_fixture().id,
|
||||||
@@ -380,7 +380,7 @@ defmodule Pinchflat.SourcesTest do
|
|||||||
end
|
end
|
||||||
|
|
||||||
test "updates with invalid data fails fast and does not call the runner" do
|
test "updates with invalid data fails fast and does not call the runner" do
|
||||||
expect(YtDlpRunnerMock, :run, 0, &channel_mock/4)
|
expect(YtDlpRunnerMock, :run, 0, &channel_mock/5)
|
||||||
|
|
||||||
source = source_fixture()
|
source = source_fixture()
|
||||||
|
|
||||||
@@ -388,7 +388,7 @@ defmodule Pinchflat.SourcesTest do
|
|||||||
end
|
end
|
||||||
|
|
||||||
test "updating the original_url will re-fetch the source details for channels" do
|
test "updating the original_url will re-fetch the source details for channels" do
|
||||||
expect(YtDlpRunnerMock, :run, &channel_mock/4)
|
expect(YtDlpRunnerMock, :run, &channel_mock/5)
|
||||||
|
|
||||||
source = source_fixture()
|
source = source_fixture()
|
||||||
update_attrs = %{original_url: "https://www.youtube.com/channel/abc123"}
|
update_attrs = %{original_url: "https://www.youtube.com/channel/abc123"}
|
||||||
@@ -399,7 +399,7 @@ defmodule Pinchflat.SourcesTest do
|
|||||||
end
|
end
|
||||||
|
|
||||||
test "updating the original_url will re-fetch the source details for playlists" do
|
test "updating the original_url will re-fetch the source details for playlists" do
|
||||||
expect(YtDlpRunnerMock, :run, &playlist_mock/4)
|
expect(YtDlpRunnerMock, :run, &playlist_mock/5)
|
||||||
|
|
||||||
source = source_fixture()
|
source = source_fixture()
|
||||||
update_attrs = %{original_url: "https://www.youtube.com/playlist?list=abc123"}
|
update_attrs = %{original_url: "https://www.youtube.com/playlist?list=abc123"}
|
||||||
@@ -410,7 +410,7 @@ defmodule Pinchflat.SourcesTest do
|
|||||||
end
|
end
|
||||||
|
|
||||||
test "not updating the original_url will not re-fetch the source details" do
|
test "not updating the original_url will not re-fetch the source details" do
|
||||||
expect(YtDlpRunnerMock, :run, 0, &channel_mock/4)
|
expect(YtDlpRunnerMock, :run, 0, &channel_mock/5)
|
||||||
|
|
||||||
source = source_fixture()
|
source = source_fixture()
|
||||||
update_attrs = %{name: "some updated name"}
|
update_attrs = %{name: "some updated name"}
|
||||||
@@ -428,7 +428,7 @@ defmodule Pinchflat.SourcesTest do
|
|||||||
end
|
end
|
||||||
|
|
||||||
test "updating will kickoff a metadata storage worker if the original_url changes" do
|
test "updating will kickoff a metadata storage worker if the original_url changes" do
|
||||||
expect(YtDlpRunnerMock, :run, &playlist_mock/4)
|
expect(YtDlpRunnerMock, :run, &playlist_mock/5)
|
||||||
source = source_fixture()
|
source = source_fixture()
|
||||||
update_attrs = %{original_url: "https://www.youtube.com/channel/cba321"}
|
update_attrs = %{original_url: "https://www.youtube.com/channel/cba321"}
|
||||||
|
|
||||||
@@ -901,7 +901,7 @@ defmodule Pinchflat.SourcesTest do
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
defp playlist_mock(_url, _opts, _ot, _addl) do
|
defp playlist_mock(_url, :get_source_details, _opts, _ot, _addl) do
|
||||||
{
|
{
|
||||||
:ok,
|
:ok,
|
||||||
Phoenix.json_library().encode!(%{
|
Phoenix.json_library().encode!(%{
|
||||||
@@ -913,7 +913,7 @@ defmodule Pinchflat.SourcesTest do
|
|||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
defp channel_mock(_url, _opts, _ot, _addl) do
|
defp channel_mock(_url, :get_source_details, _opts, _ot, _addl) do
|
||||||
channel_id = "some_channel_id_#{:rand.uniform(1_000_000)}"
|
channel_id = "some_channel_id_#{:rand.uniform(1_000_000)}"
|
||||||
|
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -28,17 +28,9 @@ defmodule Pinchflat.Utils.StringUtilsTest do
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
describe "truncate/2" do
|
describe "double_brace/1" do
|
||||||
test "truncates a string to the given length and adds ..." do
|
test "wraps a string in double braces" do
|
||||||
assert StringUtils.truncate("hello world", 5) == "hello..."
|
assert StringUtils.double_brace("hello") == "{{ hello }}"
|
||||||
end
|
|
||||||
|
|
||||||
test "breaks on a word boundary" do
|
|
||||||
assert StringUtils.truncate("hello world", 7) == "hello..."
|
|
||||||
end
|
|
||||||
|
|
||||||
test "does not truncate a string shorter than the given length" do
|
|
||||||
assert StringUtils.truncate("hello", 10) == "hello"
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -13,30 +13,36 @@ defmodule Pinchflat.YtDlp.CommandRunnerTest do
|
|||||||
end
|
end
|
||||||
|
|
||||||
describe "run/4" do
|
describe "run/4" do
|
||||||
test "it returns the output and status when the command succeeds" do
|
test "returns the output and status when the command succeeds" do
|
||||||
assert {:ok, _output} = Runner.run(@media_url, [], "")
|
assert {:ok, _output} = Runner.run(@media_url, :foo, [], "")
|
||||||
end
|
end
|
||||||
|
|
||||||
test "it includes the media url as the first argument" do
|
test "considers a 101 exit code as being successful" do
|
||||||
assert {:ok, output} = Runner.run(@media_url, [:ignore_errors], "")
|
wrap_executable("/app/test/support/scripts/yt-dlp-mocks/101_exit_code.sh", fn ->
|
||||||
|
assert {:ok, _output} = Runner.run(@media_url, :foo, [], "")
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
|
test "includes the media url as the first argument" do
|
||||||
|
assert {:ok, output} = Runner.run(@media_url, :foo, [:ignore_errors], "")
|
||||||
|
|
||||||
assert String.contains?(output, "#{@media_url} --ignore-errors")
|
assert String.contains?(output, "#{@media_url} --ignore-errors")
|
||||||
end
|
end
|
||||||
|
|
||||||
test "it automatically includes the --print-to-file flag" do
|
test "automatically includes the --print-to-file flag" do
|
||||||
assert {:ok, output} = Runner.run(@media_url, [], "%(id)s")
|
assert {:ok, output} = Runner.run(@media_url, :foo, [], "%(id)s")
|
||||||
|
|
||||||
assert String.contains?(output, "--print-to-file %(id)s /tmp/")
|
assert String.contains?(output, "--print-to-file %(id)s /tmp/")
|
||||||
end
|
end
|
||||||
|
|
||||||
test "it returns the output and status when the command fails" do
|
test "returns the output and status when the command fails" do
|
||||||
wrap_executable("/bin/false", fn ->
|
wrap_executable("/bin/false", fn ->
|
||||||
assert {:error, "", 1} = Runner.run(@media_url, [], "")
|
assert {:error, "", 1} = Runner.run(@media_url, :foo, [], "")
|
||||||
end)
|
end)
|
||||||
end
|
end
|
||||||
|
|
||||||
test "optionally lets you specify an output_filepath" do
|
test "optionally lets you specify an output_filepath" do
|
||||||
assert {:ok, output} = Runner.run(@media_url, [], "%(id)s", output_filepath: "/tmp/yt-dlp-output.json")
|
assert {:ok, output} = Runner.run(@media_url, :foo, [], "%(id)s", output_filepath: "/tmp/yt-dlp-output.json")
|
||||||
|
|
||||||
assert String.contains?(output, "--print-to-file %(id)s /tmp/yt-dlp-output.json")
|
assert String.contains?(output, "--print-to-file %(id)s /tmp/yt-dlp-output.json")
|
||||||
end
|
end
|
||||||
@@ -54,7 +60,7 @@ defmodule Pinchflat.YtDlp.CommandRunnerTest do
|
|||||||
test "includes cookie options when cookies.txt exists and enabled", %{cookie_file: cookie_file} do
|
test "includes cookie options when cookies.txt exists and enabled", %{cookie_file: cookie_file} do
|
||||||
FilesystemUtils.write_p!(cookie_file, "cookie data")
|
FilesystemUtils.write_p!(cookie_file, "cookie data")
|
||||||
|
|
||||||
assert {:ok, output} = Runner.run(@media_url, [], "", use_cookies: true)
|
assert {:ok, output} = Runner.run(@media_url, :foo, [], "", use_cookies: true)
|
||||||
|
|
||||||
assert String.contains?(output, "--cookies #{cookie_file}")
|
assert String.contains?(output, "--cookies #{cookie_file}")
|
||||||
end
|
end
|
||||||
@@ -62,7 +68,7 @@ defmodule Pinchflat.YtDlp.CommandRunnerTest do
|
|||||||
test "doesn't include cookie options when cookies.txt exists but disabled", %{cookie_file: cookie_file} do
|
test "doesn't include cookie options when cookies.txt exists but disabled", %{cookie_file: cookie_file} do
|
||||||
FilesystemUtils.write_p!(cookie_file, "cookie data")
|
FilesystemUtils.write_p!(cookie_file, "cookie data")
|
||||||
|
|
||||||
assert {:ok, output} = Runner.run(@media_url, [], "", use_cookies: false)
|
assert {:ok, output} = Runner.run(@media_url, :foo, [], "", use_cookies: false)
|
||||||
|
|
||||||
refute String.contains?(output, "--cookies #{cookie_file}")
|
refute String.contains?(output, "--cookies #{cookie_file}")
|
||||||
end
|
end
|
||||||
@@ -70,7 +76,7 @@ defmodule Pinchflat.YtDlp.CommandRunnerTest do
|
|||||||
test "doesn't include cookie options when cookies.txt blank", %{cookie_file: cookie_file} do
|
test "doesn't include cookie options when cookies.txt blank", %{cookie_file: cookie_file} do
|
||||||
FilesystemUtils.write_p!(cookie_file, " \n \n ")
|
FilesystemUtils.write_p!(cookie_file, " \n \n ")
|
||||||
|
|
||||||
assert {:ok, output} = Runner.run(@media_url, [], "", use_cookies: true)
|
assert {:ok, output} = Runner.run(@media_url, :foo, [], "", use_cookies: true)
|
||||||
|
|
||||||
refute String.contains?(output, "--cookies")
|
refute String.contains?(output, "--cookies")
|
||||||
refute String.contains?(output, cookie_file)
|
refute String.contains?(output, cookie_file)
|
||||||
@@ -79,7 +85,7 @@ defmodule Pinchflat.YtDlp.CommandRunnerTest do
|
|||||||
test "doesn't include cookie options when cookies.txt doesn't exist", %{cookie_file: cookie_file} do
|
test "doesn't include cookie options when cookies.txt doesn't exist", %{cookie_file: cookie_file} do
|
||||||
File.rm(cookie_file)
|
File.rm(cookie_file)
|
||||||
|
|
||||||
assert {:ok, output} = Runner.run(@media_url, [], "")
|
assert {:ok, output} = Runner.run(@media_url, :foo, [], "")
|
||||||
|
|
||||||
refute String.contains?(output, "--cookies")
|
refute String.contains?(output, "--cookies")
|
||||||
refute String.contains?(output, cookie_file)
|
refute String.contains?(output, cookie_file)
|
||||||
@@ -91,19 +97,19 @@ defmodule Pinchflat.YtDlp.CommandRunnerTest do
|
|||||||
|
|
||||||
describe "run/4 when testing global options" do
|
describe "run/4 when testing global options" do
|
||||||
test "creates windows-safe filenames" do
|
test "creates windows-safe filenames" do
|
||||||
assert {:ok, output} = Runner.run(@media_url, [], "")
|
assert {:ok, output} = Runner.run(@media_url, :foo, [], "")
|
||||||
|
|
||||||
assert String.contains?(output, "--windows-filenames")
|
assert String.contains?(output, "--windows-filenames")
|
||||||
end
|
end
|
||||||
|
|
||||||
test "runs quietly" do
|
test "runs quietly" do
|
||||||
assert {:ok, output} = Runner.run(@media_url, [], "")
|
assert {:ok, output} = Runner.run(@media_url, :foo, [], "")
|
||||||
|
|
||||||
assert String.contains?(output, "--quiet")
|
assert String.contains?(output, "--quiet")
|
||||||
end
|
end
|
||||||
|
|
||||||
test "sets the cache directory" do
|
test "sets the cache directory" do
|
||||||
assert {:ok, output} = Runner.run(@media_url, [], "")
|
assert {:ok, output} = Runner.run(@media_url, :foo, [], "")
|
||||||
|
|
||||||
assert String.contains?(output, "--cache-dir /tmp/test/tmpfiles/yt-dlp-cache")
|
assert String.contains?(output, "--cache-dir /tmp/test/tmpfiles/yt-dlp-cache")
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ defmodule Pinchflat.YtDlp.MediaCollectionTest do
|
|||||||
|
|
||||||
describe "get_media_attributes_for_collection/2" do
|
describe "get_media_attributes_for_collection/2" do
|
||||||
test "returns a list of video attributes with no blank elements" do
|
test "returns a list of video attributes with no blank elements" do
|
||||||
expect(YtDlpRunnerMock, :run, fn _url, _opts, _ot, _addl_opts ->
|
expect(YtDlpRunnerMock, :run, fn _url, :get_media_attributes_for_collection, _opts, _ot, _addl_opts ->
|
||||||
{:ok, source_attributes_return_fixture() <> "\n\n"}
|
{:ok, source_attributes_return_fixture() <> "\n\n"}
|
||||||
end)
|
end)
|
||||||
|
|
||||||
@@ -19,7 +19,7 @@ defmodule Pinchflat.YtDlp.MediaCollectionTest do
|
|||||||
end
|
end
|
||||||
|
|
||||||
test "passes the expected default args" do
|
test "passes the expected default args" do
|
||||||
expect(YtDlpRunnerMock, :run, fn _url, opts, ot, _addl_opts ->
|
expect(YtDlpRunnerMock, :run, fn _url, :get_media_attributes_for_collection, opts, ot, _addl_opts ->
|
||||||
assert opts == [:simulate, :skip_download, :ignore_no_formats_error, :no_warnings]
|
assert opts == [:simulate, :skip_download, :ignore_no_formats_error, :no_warnings]
|
||||||
assert ot == Media.indexing_output_template()
|
assert ot == Media.indexing_output_template()
|
||||||
|
|
||||||
@@ -30,13 +30,15 @@ defmodule Pinchflat.YtDlp.MediaCollectionTest do
|
|||||||
end
|
end
|
||||||
|
|
||||||
test "returns the error straight through when the command fails" do
|
test "returns the error straight through when the command fails" do
|
||||||
expect(YtDlpRunnerMock, :run, fn _url, _opts, _ot, _addl_opts -> {:error, "Big issue", 1} end)
|
expect(YtDlpRunnerMock, :run, fn _url, :get_media_attributes_for_collection, _opts, _ot, _addl_opts ->
|
||||||
|
{:error, "Big issue", 1}
|
||||||
|
end)
|
||||||
|
|
||||||
assert {:error, "Big issue", 1} = MediaCollection.get_media_attributes_for_collection(@channel_url)
|
assert {:error, "Big issue", 1} = MediaCollection.get_media_attributes_for_collection(@channel_url)
|
||||||
end
|
end
|
||||||
|
|
||||||
test "passes long additional command options" do
|
test "passes long additional command options" do
|
||||||
expect(YtDlpRunnerMock, :run, fn _url, opts, _ot, _addl_opts ->
|
expect(YtDlpRunnerMock, :run, fn _url, :get_media_attributes_for_collection, opts, _ot, _addl_opts ->
|
||||||
assert :foo in opts
|
assert :foo in opts
|
||||||
|
|
||||||
{:ok, ""}
|
{:ok, ""}
|
||||||
@@ -46,7 +48,7 @@ defmodule Pinchflat.YtDlp.MediaCollectionTest do
|
|||||||
end
|
end
|
||||||
|
|
||||||
test "passes additional args to runner" do
|
test "passes additional args to runner" do
|
||||||
expect(YtDlpRunnerMock, :run, fn _url, _opts, _ot, addl_opts ->
|
expect(YtDlpRunnerMock, :run, fn _url, :get_media_attributes_for_collection, _opts, _ot, addl_opts ->
|
||||||
assert [{:output_filepath, filepath} | _] = addl_opts
|
assert [{:output_filepath, filepath} | _] = addl_opts
|
||||||
assert {:use_cookies, false} in addl_opts
|
assert {:use_cookies, false} in addl_opts
|
||||||
assert String.ends_with?(filepath, ".json")
|
assert String.ends_with?(filepath, ".json")
|
||||||
@@ -58,7 +60,10 @@ defmodule Pinchflat.YtDlp.MediaCollectionTest do
|
|||||||
end
|
end
|
||||||
|
|
||||||
test "supports an optional file_listener_handler that gets passed a filename" do
|
test "supports an optional file_listener_handler that gets passed a filename" do
|
||||||
expect(YtDlpRunnerMock, :run, fn _url, _opts, _ot, _addl_opts -> {:ok, ""} end)
|
expect(YtDlpRunnerMock, :run, fn _url, :get_media_attributes_for_collection, _opts, _ot, _addl_opts ->
|
||||||
|
{:ok, ""}
|
||||||
|
end)
|
||||||
|
|
||||||
current_self = self()
|
current_self = self()
|
||||||
|
|
||||||
handler = fn filename ->
|
handler = fn filename ->
|
||||||
@@ -73,7 +78,7 @@ defmodule Pinchflat.YtDlp.MediaCollectionTest do
|
|||||||
end
|
end
|
||||||
|
|
||||||
test "gracefully handles partially failed responses" do
|
test "gracefully handles partially failed responses" do
|
||||||
expect(YtDlpRunnerMock, :run, fn _url, _opts, _ot, _addl_opts ->
|
expect(YtDlpRunnerMock, :run, fn _url, :get_media_attributes_for_collection, _opts, _ot, _addl_opts ->
|
||||||
{:ok, "INVALID\n\n" <> source_attributes_return_fixture() <> "\nINVALID\n"}
|
{:ok, "INVALID\n\n" <> source_attributes_return_fixture() <> "\nINVALID\n"}
|
||||||
end)
|
end)
|
||||||
|
|
||||||
@@ -84,7 +89,7 @@ defmodule Pinchflat.YtDlp.MediaCollectionTest do
|
|||||||
|
|
||||||
describe "get_source_details/1" do
|
describe "get_source_details/1" do
|
||||||
test "returns a map with data on success" do
|
test "returns a map with data on success" do
|
||||||
expect(YtDlpRunnerMock, :run, fn _url, _opts, _ot, _addl_opts ->
|
expect(YtDlpRunnerMock, :run, fn _url, :get_source_details, _opts, _ot, _addl_opts ->
|
||||||
Phoenix.json_library().encode(%{
|
Phoenix.json_library().encode(%{
|
||||||
channel: "PinchflatTestChannel",
|
channel: "PinchflatTestChannel",
|
||||||
channel_id: "UCQH2",
|
channel_id: "UCQH2",
|
||||||
@@ -104,7 +109,7 @@ defmodule Pinchflat.YtDlp.MediaCollectionTest do
|
|||||||
end
|
end
|
||||||
|
|
||||||
test "passes the expected args to the runner" do
|
test "passes the expected args to the runner" do
|
||||||
expect(YtDlpRunnerMock, :run, fn @channel_url, opts, ot, _addl_opts ->
|
expect(YtDlpRunnerMock, :run, fn @channel_url, :get_source_details, opts, ot, _addl_opts ->
|
||||||
assert opts == [:simulate, :skip_download, :ignore_no_formats_error, playlist_end: 1]
|
assert opts == [:simulate, :skip_download, :ignore_no_formats_error, playlist_end: 1]
|
||||||
assert ot == "%(.{channel,channel_id,playlist_id,playlist_title,filename})j"
|
assert ot == "%(.{channel,channel_id,playlist_id,playlist_title,filename})j"
|
||||||
|
|
||||||
@@ -115,7 +120,7 @@ defmodule Pinchflat.YtDlp.MediaCollectionTest do
|
|||||||
end
|
end
|
||||||
|
|
||||||
test "passes custom args to the runner" do
|
test "passes custom args to the runner" do
|
||||||
expect(YtDlpRunnerMock, :run, fn @channel_url, opts, _ot, _addl_opts ->
|
expect(YtDlpRunnerMock, :run, fn @channel_url, :get_source_details, opts, _ot, _addl_opts ->
|
||||||
assert {:foo, :bar} in opts
|
assert {:foo, :bar} in opts
|
||||||
|
|
||||||
{:ok, "{}"}
|
{:ok, "{}"}
|
||||||
@@ -125,7 +130,7 @@ defmodule Pinchflat.YtDlp.MediaCollectionTest do
|
|||||||
end
|
end
|
||||||
|
|
||||||
test "passes additional args to the runner" do
|
test "passes additional args to the runner" do
|
||||||
expect(YtDlpRunnerMock, :run, fn @channel_url, _opts, _ot, addl_opts ->
|
expect(YtDlpRunnerMock, :run, fn @channel_url, :get_source_details, _opts, _ot, addl_opts ->
|
||||||
assert {:use_cookies, true} in addl_opts
|
assert {:use_cookies, true} in addl_opts
|
||||||
|
|
||||||
{:ok, "{}"}
|
{:ok, "{}"}
|
||||||
@@ -135,13 +140,13 @@ defmodule Pinchflat.YtDlp.MediaCollectionTest do
|
|||||||
end
|
end
|
||||||
|
|
||||||
test "returns an error if the runner returns an error" do
|
test "returns an error if the runner returns an error" do
|
||||||
expect(YtDlpRunnerMock, :run, fn _url, _opts, _ot, _addl_opts -> {:error, "Big issue", 1} end)
|
expect(YtDlpRunnerMock, :run, fn _url, :get_source_details, _opts, _ot, _addl_opts -> {:error, "Big issue", 1} end)
|
||||||
|
|
||||||
assert {:error, "Big issue", 1} = MediaCollection.get_source_details(@channel_url)
|
assert {:error, "Big issue", 1} = MediaCollection.get_source_details(@channel_url)
|
||||||
end
|
end
|
||||||
|
|
||||||
test "returns an error if the output is not JSON" do
|
test "returns an error if the output is not JSON" do
|
||||||
expect(YtDlpRunnerMock, :run, fn _url, _opts, _ot, _addl_opts -> {:ok, "Not JSON"} end)
|
expect(YtDlpRunnerMock, :run, fn _url, :get_source_details, _opts, _ot, _addl_opts -> {:ok, "Not JSON"} end)
|
||||||
|
|
||||||
assert {:error, "Error decoding JSON response"} = MediaCollection.get_source_details(@channel_url)
|
assert {:error, "Error decoding JSON response"} = MediaCollection.get_source_details(@channel_url)
|
||||||
end
|
end
|
||||||
@@ -149,7 +154,7 @@ defmodule Pinchflat.YtDlp.MediaCollectionTest do
|
|||||||
|
|
||||||
describe "get_source_metadata/1" do
|
describe "get_source_metadata/1" do
|
||||||
test "returns a map with data on success" do
|
test "returns a map with data on success" do
|
||||||
expect(YtDlpRunnerMock, :run, fn _url, _opts, _ot, _addl_opts ->
|
expect(YtDlpRunnerMock, :run, fn _url, :get_source_metadata, _opts, _ot, _addl_opts ->
|
||||||
Phoenix.json_library().encode(%{channel: "PinchflatTestChannel"})
|
Phoenix.json_library().encode(%{channel: "PinchflatTestChannel"})
|
||||||
end)
|
end)
|
||||||
|
|
||||||
@@ -159,7 +164,7 @@ defmodule Pinchflat.YtDlp.MediaCollectionTest do
|
|||||||
end
|
end
|
||||||
|
|
||||||
test "passes the expected args to the backend runner" do
|
test "passes the expected args to the backend runner" do
|
||||||
expect(YtDlpRunnerMock, :run, fn @channel_url, opts, ot, _addl_opts ->
|
expect(YtDlpRunnerMock, :run, fn @channel_url, :get_source_metadata, opts, ot, _addl_opts ->
|
||||||
assert opts == [:skip_download, playlist_items: 0]
|
assert opts == [:skip_download, playlist_items: 0]
|
||||||
assert ot == "playlist:%()j"
|
assert ot == "playlist:%()j"
|
||||||
|
|
||||||
@@ -170,7 +175,7 @@ defmodule Pinchflat.YtDlp.MediaCollectionTest do
|
|||||||
end
|
end
|
||||||
|
|
||||||
test "passes additional args to the runner" do
|
test "passes additional args to the runner" do
|
||||||
expect(YtDlpRunnerMock, :run, fn @channel_url, _opts, _ot, addl_opts ->
|
expect(YtDlpRunnerMock, :run, fn @channel_url, :get_source_metadata, _opts, _ot, addl_opts ->
|
||||||
assert {:use_cookies, true} in addl_opts
|
assert {:use_cookies, true} in addl_opts
|
||||||
|
|
||||||
{:ok, "{}"}
|
{:ok, "{}"}
|
||||||
@@ -180,7 +185,7 @@ defmodule Pinchflat.YtDlp.MediaCollectionTest do
|
|||||||
end
|
end
|
||||||
|
|
||||||
test "passes custom args to the runner" do
|
test "passes custom args to the runner" do
|
||||||
expect(YtDlpRunnerMock, :run, fn _url, opts, _ot, _addl_opts ->
|
expect(YtDlpRunnerMock, :run, fn _url, :get_source_metadata, opts, _ot, _addl_opts ->
|
||||||
assert opts == [:skip_download, playlist_items: 1, real_opt: :yup]
|
assert opts == [:skip_download, playlist_items: 1, real_opt: :yup]
|
||||||
|
|
||||||
{:ok, "{}"}
|
{:ok, "{}"}
|
||||||
@@ -196,13 +201,15 @@ defmodule Pinchflat.YtDlp.MediaCollectionTest do
|
|||||||
end
|
end
|
||||||
|
|
||||||
test "returns an error if the runner returns an error" do
|
test "returns an error if the runner returns an error" do
|
||||||
expect(YtDlpRunnerMock, :run, fn _url, _opts, _ot, _addl_opts -> {:error, "Big issue", 1} end)
|
expect(YtDlpRunnerMock, :run, fn _url, :get_source_metadata, _opts, _ot, _addl_opts ->
|
||||||
|
{:error, "Big issue", 1}
|
||||||
|
end)
|
||||||
|
|
||||||
assert {:error, "Big issue", 1} = MediaCollection.get_source_metadata(@channel_url, playlist_items: 0)
|
assert {:error, "Big issue", 1} = MediaCollection.get_source_metadata(@channel_url, playlist_items: 0)
|
||||||
end
|
end
|
||||||
|
|
||||||
test "returns an error if the output is not JSON" do
|
test "returns an error if the output is not JSON" do
|
||||||
expect(YtDlpRunnerMock, :run, fn _url, _opts, _ot, _addl_opts -> {:ok, "Not JSON"} end)
|
expect(YtDlpRunnerMock, :run, fn _url, :get_source_metadata, _opts, _ot, _addl_opts -> {:ok, "Not JSON"} end)
|
||||||
|
|
||||||
assert {:error, %Jason.DecodeError{}} = MediaCollection.get_source_metadata(@channel_url, playlist_items: 0)
|
assert {:error, %Jason.DecodeError{}} = MediaCollection.get_source_metadata(@channel_url, playlist_items: 0)
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ defmodule Pinchflat.YtDlp.MediaTest do
|
|||||||
|
|
||||||
describe "download/3" do
|
describe "download/3" do
|
||||||
test "calls the backend runner with the expected arguments" do
|
test "calls the backend runner with the expected arguments" do
|
||||||
expect(YtDlpRunnerMock, :run, fn @media_url, opts, ot, addl ->
|
expect(YtDlpRunnerMock, :run, fn @media_url, :download, opts, ot, addl ->
|
||||||
assert [:no_simulate] = opts
|
assert [:no_simulate] = opts
|
||||||
assert "after_move:%()j" = ot
|
assert "after_move:%()j" = ot
|
||||||
assert addl == []
|
assert addl == []
|
||||||
@@ -21,7 +21,7 @@ defmodule Pinchflat.YtDlp.MediaTest do
|
|||||||
end
|
end
|
||||||
|
|
||||||
test "passes along custom command args" do
|
test "passes along custom command args" do
|
||||||
expect(YtDlpRunnerMock, :run, fn _url, opts, _ot, _addl ->
|
expect(YtDlpRunnerMock, :run, fn _url, :download, opts, _ot, _addl ->
|
||||||
assert [:no_simulate, :custom_arg] = opts
|
assert [:no_simulate, :custom_arg] = opts
|
||||||
|
|
||||||
{:ok, "{}"}
|
{:ok, "{}"}
|
||||||
@@ -31,7 +31,7 @@ defmodule Pinchflat.YtDlp.MediaTest do
|
|||||||
end
|
end
|
||||||
|
|
||||||
test "passes along additional options" do
|
test "passes along additional options" do
|
||||||
expect(YtDlpRunnerMock, :run, fn _url, _opts, _ot, addl ->
|
expect(YtDlpRunnerMock, :run, fn _url, :download, _opts, _ot, addl ->
|
||||||
assert [addl_arg: true] = addl
|
assert [addl_arg: true] = addl
|
||||||
|
|
||||||
{:ok, "{}"}
|
{:ok, "{}"}
|
||||||
@@ -41,7 +41,7 @@ defmodule Pinchflat.YtDlp.MediaTest do
|
|||||||
end
|
end
|
||||||
|
|
||||||
test "parses and returns the generated file as JSON" do
|
test "parses and returns the generated file as JSON" do
|
||||||
expect(YtDlpRunnerMock, :run, fn _url, _opts, _ot, _addl ->
|
expect(YtDlpRunnerMock, :run, fn _url, :download, _opts, _ot, _addl ->
|
||||||
{:ok, render_metadata(:media_metadata)}
|
{:ok, render_metadata(:media_metadata)}
|
||||||
end)
|
end)
|
||||||
|
|
||||||
@@ -50,7 +50,7 @@ defmodule Pinchflat.YtDlp.MediaTest do
|
|||||||
end
|
end
|
||||||
|
|
||||||
test "returns errors" do
|
test "returns errors" do
|
||||||
expect(YtDlpRunnerMock, :run, fn _url, _opt, _ot, _addl ->
|
expect(YtDlpRunnerMock, :run, fn _url, :download, _opt, _ot, _addl ->
|
||||||
{:error, "something"}
|
{:error, "something"}
|
||||||
end)
|
end)
|
||||||
|
|
||||||
@@ -60,7 +60,7 @@ defmodule Pinchflat.YtDlp.MediaTest do
|
|||||||
|
|
||||||
describe "get_downloadable_status/1" do
|
describe "get_downloadable_status/1" do
|
||||||
test "returns :downloadable if the media was never live" do
|
test "returns :downloadable if the media was never live" do
|
||||||
expect(YtDlpRunnerMock, :run, fn _url, _opts, _ot ->
|
expect(YtDlpRunnerMock, :run, fn _url, :get_downloadable_status, _opts, _ot, _addl ->
|
||||||
{:ok, Phoenix.json_library().encode!(%{"live_status" => "not_live"})}
|
{:ok, Phoenix.json_library().encode!(%{"live_status" => "not_live"})}
|
||||||
end)
|
end)
|
||||||
|
|
||||||
@@ -68,7 +68,7 @@ defmodule Pinchflat.YtDlp.MediaTest do
|
|||||||
end
|
end
|
||||||
|
|
||||||
test "returns :downloadable if the media was live and has been processed" do
|
test "returns :downloadable if the media was live and has been processed" do
|
||||||
expect(YtDlpRunnerMock, :run, fn _url, _opts, _ot ->
|
expect(YtDlpRunnerMock, :run, fn _url, :get_downloadable_status, _opts, _ot, _addl ->
|
||||||
{:ok, Phoenix.json_library().encode!(%{"live_status" => "was_live"})}
|
{:ok, Phoenix.json_library().encode!(%{"live_status" => "was_live"})}
|
||||||
end)
|
end)
|
||||||
|
|
||||||
@@ -76,7 +76,7 @@ defmodule Pinchflat.YtDlp.MediaTest do
|
|||||||
end
|
end
|
||||||
|
|
||||||
test "returns :downloadable if the media's live_status is nil" do
|
test "returns :downloadable if the media's live_status is nil" do
|
||||||
expect(YtDlpRunnerMock, :run, fn _url, _opts, _ot ->
|
expect(YtDlpRunnerMock, :run, fn _url, :get_downloadable_status, _opts, _ot, _addl ->
|
||||||
{:ok, Phoenix.json_library().encode!(%{"live_status" => nil})}
|
{:ok, Phoenix.json_library().encode!(%{"live_status" => nil})}
|
||||||
end)
|
end)
|
||||||
|
|
||||||
@@ -84,7 +84,7 @@ defmodule Pinchflat.YtDlp.MediaTest do
|
|||||||
end
|
end
|
||||||
|
|
||||||
test "returns :ignorable if the media is currently live" do
|
test "returns :ignorable if the media is currently live" do
|
||||||
expect(YtDlpRunnerMock, :run, fn _url, _opts, _ot ->
|
expect(YtDlpRunnerMock, :run, fn _url, :get_downloadable_status, _opts, _ot, _addl ->
|
||||||
{:ok, Phoenix.json_library().encode!(%{"live_status" => "is_live"})}
|
{:ok, Phoenix.json_library().encode!(%{"live_status" => "is_live"})}
|
||||||
end)
|
end)
|
||||||
|
|
||||||
@@ -92,7 +92,7 @@ defmodule Pinchflat.YtDlp.MediaTest do
|
|||||||
end
|
end
|
||||||
|
|
||||||
test "returns :ignorable if the media is scheduled to be live" do
|
test "returns :ignorable if the media is scheduled to be live" do
|
||||||
expect(YtDlpRunnerMock, :run, fn _url, _opts, _ot ->
|
expect(YtDlpRunnerMock, :run, fn _url, :get_downloadable_status, _opts, _ot, _addl ->
|
||||||
{:ok, Phoenix.json_library().encode!(%{"live_status" => "is_upcoming"})}
|
{:ok, Phoenix.json_library().encode!(%{"live_status" => "is_upcoming"})}
|
||||||
end)
|
end)
|
||||||
|
|
||||||
@@ -100,7 +100,7 @@ defmodule Pinchflat.YtDlp.MediaTest do
|
|||||||
end
|
end
|
||||||
|
|
||||||
test "returns :ignorable if the media was live but hasn't been processed" do
|
test "returns :ignorable if the media was live but hasn't been processed" do
|
||||||
expect(YtDlpRunnerMock, :run, fn _url, _opts, _ot ->
|
expect(YtDlpRunnerMock, :run, fn _url, :get_downloadable_status, _opts, _ot, _addl ->
|
||||||
{:ok, Phoenix.json_library().encode!(%{"live_status" => "post_live"})}
|
{:ok, Phoenix.json_library().encode!(%{"live_status" => "post_live"})}
|
||||||
end)
|
end)
|
||||||
|
|
||||||
@@ -108,17 +108,27 @@ defmodule Pinchflat.YtDlp.MediaTest do
|
|||||||
end
|
end
|
||||||
|
|
||||||
test "returns an error if the downloadable status can't be determined" do
|
test "returns an error if the downloadable status can't be determined" do
|
||||||
expect(YtDlpRunnerMock, :run, fn _url, _opts, _ot ->
|
expect(YtDlpRunnerMock, :run, fn _url, :get_downloadable_status, _opts, _ot, _addl ->
|
||||||
{:ok, Phoenix.json_library().encode!(%{"live_status" => "what_tha"})}
|
{:ok, Phoenix.json_library().encode!(%{"live_status" => "what_tha"})}
|
||||||
end)
|
end)
|
||||||
|
|
||||||
assert {:error, "Unknown live status: what_tha"} = Media.get_downloadable_status(@media_url)
|
assert {:error, "Unknown live status: what_tha"} = Media.get_downloadable_status(@media_url)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
test "optionally accepts additional args" do
|
||||||
|
expect(YtDlpRunnerMock, :run, fn _url, :get_downloadable_status, _opts, _ot, addl ->
|
||||||
|
assert [addl_arg: true] = addl
|
||||||
|
|
||||||
|
{:ok, Phoenix.json_library().encode!(%{"live_status" => "not_live"})}
|
||||||
|
end)
|
||||||
|
|
||||||
|
assert {:ok, :downloadable} = Media.get_downloadable_status(@media_url, addl_arg: true)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
describe "download_thumbnail/2" do
|
describe "download_thumbnail/2" do
|
||||||
test "calls the backend runner with the expected arguments" do
|
test "calls the backend runner with the expected arguments" do
|
||||||
expect(YtDlpRunnerMock, :run, fn @media_url, opts, ot, _addl ->
|
expect(YtDlpRunnerMock, :run, fn @media_url, :download_thumbnail, opts, ot, _addl ->
|
||||||
assert opts == [:no_simulate, :skip_download, :write_thumbnail, {:convert_thumbnail, "jpg"}]
|
assert opts == [:no_simulate, :skip_download, :write_thumbnail, {:convert_thumbnail, "jpg"}]
|
||||||
assert ot == "after_move:%()j"
|
assert ot == "after_move:%()j"
|
||||||
|
|
||||||
@@ -129,7 +139,7 @@ defmodule Pinchflat.YtDlp.MediaTest do
|
|||||||
end
|
end
|
||||||
|
|
||||||
test "passes along custom command args" do
|
test "passes along custom command args" do
|
||||||
expect(YtDlpRunnerMock, :run, fn _url, opts, _ot, _addl ->
|
expect(YtDlpRunnerMock, :run, fn _url, :download_thumbnail, opts, _ot, _addl ->
|
||||||
assert :custom_arg in opts
|
assert :custom_arg in opts
|
||||||
|
|
||||||
{:ok, "{}"}
|
{:ok, "{}"}
|
||||||
@@ -139,7 +149,7 @@ defmodule Pinchflat.YtDlp.MediaTest do
|
|||||||
end
|
end
|
||||||
|
|
||||||
test "passes along additional options" do
|
test "passes along additional options" do
|
||||||
expect(YtDlpRunnerMock, :run, fn _url, _opts, _ot, addl ->
|
expect(YtDlpRunnerMock, :run, fn _url, :download_thumbnail, _opts, _ot, addl ->
|
||||||
assert [addl_arg: true] = addl
|
assert [addl_arg: true] = addl
|
||||||
|
|
||||||
{:ok, "{}"}
|
{:ok, "{}"}
|
||||||
@@ -149,7 +159,7 @@ defmodule Pinchflat.YtDlp.MediaTest do
|
|||||||
end
|
end
|
||||||
|
|
||||||
test "returns errors" do
|
test "returns errors" do
|
||||||
expect(YtDlpRunnerMock, :run, fn _url, _opt, _ot, _addl ->
|
expect(YtDlpRunnerMock, :run, fn _url, :download_thumbnail, _opt, _ot, _addl ->
|
||||||
{:error, "something"}
|
{:error, "something"}
|
||||||
end)
|
end)
|
||||||
|
|
||||||
@@ -159,7 +169,7 @@ defmodule Pinchflat.YtDlp.MediaTest do
|
|||||||
|
|
||||||
describe "get_media_attributes/1" do
|
describe "get_media_attributes/1" do
|
||||||
test "returns a list of video attributes" do
|
test "returns a list of video attributes" do
|
||||||
expect(YtDlpRunnerMock, :run, fn _url, _opts, _ot, _addl ->
|
expect(YtDlpRunnerMock, :run, fn _url, :get_media_attributes, _opts, _ot, _addl ->
|
||||||
{:ok, media_attributes_return_fixture()}
|
{:ok, media_attributes_return_fixture()}
|
||||||
end)
|
end)
|
||||||
|
|
||||||
@@ -168,7 +178,7 @@ defmodule Pinchflat.YtDlp.MediaTest do
|
|||||||
end
|
end
|
||||||
|
|
||||||
test "it passes the expected default args" do
|
test "it passes the expected default args" do
|
||||||
expect(YtDlpRunnerMock, :run, fn _url, opts, ot, _addl ->
|
expect(YtDlpRunnerMock, :run, fn _url, :get_media_attributes, opts, ot, _addl ->
|
||||||
assert opts == [:simulate, :skip_download]
|
assert opts == [:simulate, :skip_download]
|
||||||
assert ot == Media.indexing_output_template()
|
assert ot == Media.indexing_output_template()
|
||||||
|
|
||||||
@@ -179,7 +189,7 @@ defmodule Pinchflat.YtDlp.MediaTest do
|
|||||||
end
|
end
|
||||||
|
|
||||||
test "passes along additional command options" do
|
test "passes along additional command options" do
|
||||||
expect(YtDlpRunnerMock, :run, fn _url, opts, _ot, _addl ->
|
expect(YtDlpRunnerMock, :run, fn _url, :get_media_attributes, opts, _ot, _addl ->
|
||||||
assert [:simulate, :skip_download, :custom_arg] = opts
|
assert [:simulate, :skip_download, :custom_arg] = opts
|
||||||
{:ok, media_attributes_return_fixture()}
|
{:ok, media_attributes_return_fixture()}
|
||||||
end)
|
end)
|
||||||
@@ -188,7 +198,7 @@ defmodule Pinchflat.YtDlp.MediaTest do
|
|||||||
end
|
end
|
||||||
|
|
||||||
test "passes along additional options" do
|
test "passes along additional options" do
|
||||||
expect(YtDlpRunnerMock, :run, fn _url, _opts, _ot, addl ->
|
expect(YtDlpRunnerMock, :run, fn _url, :get_media_attributes, _opts, _ot, addl ->
|
||||||
assert [addl_arg: true] = addl
|
assert [addl_arg: true] = addl
|
||||||
{:ok, media_attributes_return_fixture()}
|
{:ok, media_attributes_return_fixture()}
|
||||||
end)
|
end)
|
||||||
@@ -197,7 +207,7 @@ defmodule Pinchflat.YtDlp.MediaTest do
|
|||||||
end
|
end
|
||||||
|
|
||||||
test "returns the error straight through when the command fails" do
|
test "returns the error straight through when the command fails" do
|
||||||
expect(YtDlpRunnerMock, :run, fn _url, _opts, _ot, _addl -> {:error, "Big issue", 1} end)
|
expect(YtDlpRunnerMock, :run, fn _url, :get_media_attributes, _opts, _ot, _addl -> {:error, "Big issue", 1} end)
|
||||||
|
|
||||||
assert {:error, "Big issue", 1} = Media.get_media_attributes(@media_url)
|
assert {:error, "Big issue", 1} = Media.get_media_attributes(@media_url)
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ defmodule PinchflatWeb.Pages.JobTableLiveTest do
|
|||||||
import Pinchflat.MediaFixtures
|
import Pinchflat.MediaFixtures
|
||||||
import Pinchflat.SourcesFixtures
|
import Pinchflat.SourcesFixtures
|
||||||
|
|
||||||
alias Pinchflat.Utils.StringUtils
|
|
||||||
alias Pinchflat.Pages.JobTableLive
|
alias Pinchflat.Pages.JobTableLive
|
||||||
alias Pinchflat.Downloading.MediaDownloadWorker
|
alias Pinchflat.Downloading.MediaDownloadWorker
|
||||||
alias Pinchflat.FastIndexing.FastIndexingWorker
|
alias Pinchflat.FastIndexing.FastIndexingWorker
|
||||||
@@ -48,7 +47,7 @@ defmodule PinchflatWeb.Pages.JobTableLiveTest do
|
|||||||
{_source, media_item, _task, _job} = create_media_item_job()
|
{_source, media_item, _task, _job} = create_media_item_job()
|
||||||
{:ok, _view, html} = live_isolated(conn, JobTableLive, session: %{})
|
{:ok, _view, html} = live_isolated(conn, JobTableLive, session: %{})
|
||||||
|
|
||||||
assert html =~ StringUtils.truncate(media_item.title, 35)
|
assert html =~ media_item.title
|
||||||
end
|
end
|
||||||
|
|
||||||
test "shows a media item link", %{conn: conn} do
|
test "shows a media item link", %{conn: conn} do
|
||||||
@@ -62,7 +61,7 @@ defmodule PinchflatWeb.Pages.JobTableLiveTest do
|
|||||||
{source, _task, _job} = create_source_job()
|
{source, _task, _job} = create_source_job()
|
||||||
{:ok, _view, html} = live_isolated(conn, JobTableLive, session: %{})
|
{:ok, _view, html} = live_isolated(conn, JobTableLive, session: %{})
|
||||||
|
|
||||||
assert html =~ StringUtils.truncate(source.custom_name, 35)
|
assert html =~ source.custom_name
|
||||||
end
|
end
|
||||||
|
|
||||||
test "shows a source link", %{conn: conn} do
|
test "shows a source link", %{conn: conn} do
|
||||||
|
|||||||
@@ -4,6 +4,37 @@ defmodule PinchflatWeb.PodcastControllerTest do
|
|||||||
import Pinchflat.MediaFixtures
|
import Pinchflat.MediaFixtures
|
||||||
import Pinchflat.SourcesFixtures
|
import Pinchflat.SourcesFixtures
|
||||||
|
|
||||||
|
alias Pinchflat.Settings
|
||||||
|
|
||||||
|
describe "opml_feed" do
|
||||||
|
test "renders the XML document", %{conn: conn} do
|
||||||
|
source = source_fixture()
|
||||||
|
route_token = Settings.get!(:route_token)
|
||||||
|
|
||||||
|
conn = get(conn, ~p"/sources/opml.xml?#{[route_token: route_token]}")
|
||||||
|
|
||||||
|
assert conn.status == 200
|
||||||
|
assert {"content-type", "application/opml+xml; charset=utf-8"} in conn.resp_headers
|
||||||
|
assert {"content-disposition", "inline"} in conn.resp_headers
|
||||||
|
assert conn.resp_body =~ ~s"http://www.example.com/sources/#{source.uuid}/feed.xml"
|
||||||
|
assert conn.resp_body =~ "text=\"#{source.custom_name}\""
|
||||||
|
end
|
||||||
|
|
||||||
|
test "returns 401 if the route token is incorrect", %{conn: conn} do
|
||||||
|
conn = get(conn, ~p"/sources/opml.xml?route_token=incorrect")
|
||||||
|
|
||||||
|
assert conn.status == 401
|
||||||
|
assert conn.resp_body == "Unauthorized"
|
||||||
|
end
|
||||||
|
|
||||||
|
test "returns 401 if the route token is missing", %{conn: conn} do
|
||||||
|
conn = get(conn, ~p"/sources/opml.xml")
|
||||||
|
|
||||||
|
assert conn.status == 401
|
||||||
|
assert conn.resp_body == "Unauthorized"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
describe "rss_feed" do
|
describe "rss_feed" do
|
||||||
test "renders the XML document", %{conn: conn} do
|
test "renders the XML document", %{conn: conn} do
|
||||||
source = source_fixture()
|
source = source_fixture()
|
||||||
|
|||||||
@@ -66,7 +66,7 @@ defmodule PinchflatWeb.SourceControllerTest do
|
|||||||
|
|
||||||
describe "create source" do
|
describe "create source" do
|
||||||
test "redirects to show when data is valid", %{conn: conn, create_attrs: create_attrs} do
|
test "redirects to show when data is valid", %{conn: conn, create_attrs: create_attrs} do
|
||||||
expect(YtDlpRunnerMock, :run, 1, &runner_function_mock/4)
|
expect(YtDlpRunnerMock, :run, 1, &runner_function_mock/5)
|
||||||
conn = post(conn, ~p"/sources", source: create_attrs)
|
conn = post(conn, ~p"/sources", source: create_attrs)
|
||||||
|
|
||||||
assert %{id: id} = redirected_params(conn)
|
assert %{id: id} = redirected_params(conn)
|
||||||
@@ -82,7 +82,7 @@ defmodule PinchflatWeb.SourceControllerTest do
|
|||||||
end
|
end
|
||||||
|
|
||||||
test "redirects to onboarding when onboarding", %{conn: conn, create_attrs: create_attrs} do
|
test "redirects to onboarding when onboarding", %{conn: conn, create_attrs: create_attrs} do
|
||||||
expect(YtDlpRunnerMock, :run, 1, &runner_function_mock/4)
|
expect(YtDlpRunnerMock, :run, 1, &runner_function_mock/5)
|
||||||
|
|
||||||
Settings.set(onboarding: true)
|
Settings.set(onboarding: true)
|
||||||
conn = post(conn, ~p"/sources", source: create_attrs)
|
conn = post(conn, ~p"/sources", source: create_attrs)
|
||||||
@@ -111,7 +111,7 @@ defmodule PinchflatWeb.SourceControllerTest do
|
|||||||
setup [:create_source]
|
setup [:create_source]
|
||||||
|
|
||||||
test "redirects when data is valid", %{conn: conn, source: source, update_attrs: update_attrs} do
|
test "redirects when data is valid", %{conn: conn, source: source, update_attrs: update_attrs} do
|
||||||
expect(YtDlpRunnerMock, :run, 1, &runner_function_mock/4)
|
expect(YtDlpRunnerMock, :run, 1, &runner_function_mock/5)
|
||||||
|
|
||||||
conn = put(conn, ~p"/sources/#{source}", source: update_attrs)
|
conn = put(conn, ~p"/sources/#{source}", source: update_attrs)
|
||||||
assert redirected_to(conn) == ~p"/sources/#{source}"
|
assert redirected_to(conn) == ~p"/sources/#{source}"
|
||||||
@@ -276,7 +276,7 @@ defmodule PinchflatWeb.SourceControllerTest do
|
|||||||
%{source: source, media_item: media_item}
|
%{source: source, media_item: media_item}
|
||||||
end
|
end
|
||||||
|
|
||||||
defp runner_function_mock(_url, _opts, _ot, _addl) do
|
defp runner_function_mock(_url, :get_source_details, _opts, _ot, _addl) do
|
||||||
{
|
{
|
||||||
:ok,
|
:ok,
|
||||||
Phoenix.json_library().encode!(%{
|
Phoenix.json_library().encode!(%{
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
defmodule PinchflatWeb.Sources.IndexTableLiveTest do
|
defmodule PinchflatWeb.Sources.SourceLive.IndexTableLiveTest do
|
||||||
use PinchflatWeb.ConnCase
|
use PinchflatWeb.ConnCase
|
||||||
|
|
||||||
import Phoenix.LiveViewTest
|
import Phoenix.LiveViewTest
|
||||||
@@ -6,13 +6,13 @@ defmodule PinchflatWeb.Sources.IndexTableLiveTest do
|
|||||||
import Pinchflat.ProfilesFixtures
|
import Pinchflat.ProfilesFixtures
|
||||||
|
|
||||||
alias Pinchflat.Sources.Source
|
alias Pinchflat.Sources.Source
|
||||||
alias PinchflatWeb.Sources.IndexTableLive
|
alias PinchflatWeb.Sources.SourceLive.IndexTableLive
|
||||||
|
|
||||||
describe "initial rendering" do
|
describe "initial rendering" do
|
||||||
test "lists all sources", %{conn: conn} do
|
test "lists all sources", %{conn: conn} do
|
||||||
source = source_fixture()
|
source = source_fixture()
|
||||||
|
|
||||||
{:ok, _view, html} = live_isolated(conn, IndexTableLive)
|
{:ok, _view, html} = live_isolated(conn, IndexTableLive, session: create_session())
|
||||||
|
|
||||||
assert html =~ source.custom_name
|
assert html =~ source.custom_name
|
||||||
end
|
end
|
||||||
@@ -20,7 +20,7 @@ defmodule PinchflatWeb.Sources.IndexTableLiveTest do
|
|||||||
test "omits sources that have marked_for_deletion_at set", %{conn: conn} do
|
test "omits sources that have marked_for_deletion_at set", %{conn: conn} do
|
||||||
source = source_fixture(marked_for_deletion_at: DateTime.utc_now())
|
source = source_fixture(marked_for_deletion_at: DateTime.utc_now())
|
||||||
|
|
||||||
{:ok, _view, html} = live_isolated(conn, IndexTableLive)
|
{:ok, _view, html} = live_isolated(conn, IndexTableLive, session: create_session())
|
||||||
|
|
||||||
refute html =~ source.custom_name
|
refute html =~ source.custom_name
|
||||||
end
|
end
|
||||||
@@ -29,27 +29,102 @@ defmodule PinchflatWeb.Sources.IndexTableLiveTest do
|
|||||||
media_profile = media_profile_fixture(marked_for_deletion_at: DateTime.utc_now())
|
media_profile = media_profile_fixture(marked_for_deletion_at: DateTime.utc_now())
|
||||||
source = source_fixture(media_profile_id: media_profile.id)
|
source = source_fixture(media_profile_id: media_profile.id)
|
||||||
|
|
||||||
{:ok, _view, html} = live_isolated(conn, IndexTableLive)
|
{:ok, _view, html} = live_isolated(conn, IndexTableLive, session: create_session())
|
||||||
|
|
||||||
refute html =~ source.custom_name
|
refute html =~ source.custom_name
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
describe "when a source is enabled or disabled" do
|
describe "when testing sorting" do
|
||||||
|
test "sorts by the custom_name by default", %{conn: conn} do
|
||||||
|
source1 = source_fixture(custom_name: "Source_B")
|
||||||
|
source2 = source_fixture(custom_name: "Source_A")
|
||||||
|
|
||||||
|
{:ok, view, _html} = live_isolated(conn, IndexTableLive, session: create_session())
|
||||||
|
assert render_element(view, "tbody tr:first-child") =~ source2.custom_name
|
||||||
|
assert render_element(view, "tbody tr:last-child") =~ source1.custom_name
|
||||||
|
end
|
||||||
|
|
||||||
|
test "clicking the row will change the sort direction", %{conn: conn} do
|
||||||
|
source1 = source_fixture(custom_name: "Source_B")
|
||||||
|
source2 = source_fixture(custom_name: "Source_A")
|
||||||
|
|
||||||
|
{:ok, view, _html} = live_isolated(conn, IndexTableLive, session: create_session())
|
||||||
|
|
||||||
|
# Click the row to change the sort direction
|
||||||
|
click_element(view, "th", "Name")
|
||||||
|
|
||||||
|
assert render_element(view, "tbody tr:first-child") =~ source1.custom_name
|
||||||
|
assert render_element(view, "tbody tr:last-child") =~ source2.custom_name
|
||||||
|
end
|
||||||
|
|
||||||
|
test "clicking a different row will sort by that attribute", %{conn: conn} do
|
||||||
|
source1 = source_fixture(custom_name: "Source_A", enabled: true)
|
||||||
|
source2 = source_fixture(custom_name: "Source_A", enabled: false)
|
||||||
|
|
||||||
|
{:ok, view, _html} = live_isolated(conn, IndexTableLive, session: create_session())
|
||||||
|
|
||||||
|
# Click the row to change the sort field
|
||||||
|
click_element(view, "th", "Enabled?")
|
||||||
|
|
||||||
|
assert render_element(view, "tbody tr:first-child") =~ source2.custom_name
|
||||||
|
assert render_element(view, "tbody tr:last-child") =~ source1.custom_name
|
||||||
|
|
||||||
|
# Click the row to again change the sort direcation
|
||||||
|
click_element(view, "th", "Enabled?")
|
||||||
|
assert render_element(view, "tbody tr:first-child") =~ source1.custom_name
|
||||||
|
assert render_element(view, "tbody tr:last-child") =~ source2.custom_name
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "when testing pagination" do
|
||||||
|
test "moving to the next page loads new records", %{conn: conn} do
|
||||||
|
source1 = source_fixture(custom_name: "Source_A")
|
||||||
|
source2 = source_fixture(custom_name: "Source_B")
|
||||||
|
|
||||||
|
session = Map.merge(create_session(), %{"results_per_page" => 1})
|
||||||
|
{:ok, view, _html} = live_isolated(conn, IndexTableLive, session: session)
|
||||||
|
|
||||||
|
assert render_element(view, "tbody") =~ source1.custom_name
|
||||||
|
refute render_element(view, "tbody") =~ source2.custom_name
|
||||||
|
|
||||||
|
click_element(view, "span.pagination-next")
|
||||||
|
|
||||||
|
refute render_element(view, "tbody") =~ source1.custom_name
|
||||||
|
assert render_element(view, "tbody") =~ source2.custom_name
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "when testing the enable toggle" do
|
||||||
test "updates the source's enabled status", %{conn: conn} do
|
test "updates the source's enabled status", %{conn: conn} do
|
||||||
source = source_fixture(enabled: true)
|
source = source_fixture(enabled: true)
|
||||||
{:ok, view, _html} = live_isolated(conn, IndexTableLive)
|
{:ok, view, _html} = live_isolated(conn, IndexTableLive, session: create_session())
|
||||||
|
|
||||||
params = %{
|
view
|
||||||
"event" => "toggle_enabled",
|
|> element(".enabled_toggle_form")
|
||||||
"id" => source.id,
|
|> render_change(%{source: %{"enabled" => false}})
|
||||||
"value" => "false"
|
|
||||||
}
|
|
||||||
|
|
||||||
# Send an event to the server directly
|
|
||||||
render_change(view, "formless-input", params)
|
|
||||||
|
|
||||||
assert %{enabled: false} = Repo.get!(Source, source.id)
|
assert %{enabled: false} = Repo.get!(Source, source.id)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
defp click_element(view, selector, text_filter \\ nil) do
|
||||||
|
view
|
||||||
|
|> element(selector, text_filter)
|
||||||
|
|> render_click()
|
||||||
|
end
|
||||||
|
|
||||||
|
defp render_element(view, selector) do
|
||||||
|
view
|
||||||
|
|> element(selector)
|
||||||
|
|> render()
|
||||||
|
end
|
||||||
|
|
||||||
|
defp create_session do
|
||||||
|
%{
|
||||||
|
"initial_sort_key" => :custom_name,
|
||||||
|
"initial_sort_direction" => :asc,
|
||||||
|
"results_per_page" => 10
|
||||||
|
}
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -0,0 +1,26 @@
|
|||||||
|
defmodule PinchflatWeb.Sources.SourceLive.SourceEnableToggleTest do
|
||||||
|
use PinchflatWeb.ConnCase
|
||||||
|
|
||||||
|
import Phoenix.LiveViewTest
|
||||||
|
|
||||||
|
alias PinchflatWeb.Sources.SourceLive.SourceEnableToggle
|
||||||
|
|
||||||
|
describe "initial rendering" do
|
||||||
|
test "renders a toggle in the on position if the source is enabled" do
|
||||||
|
source = %{id: 1, enabled: true}
|
||||||
|
|
||||||
|
html = render_component(SourceEnableToggle, %{id: :foo, source: source})
|
||||||
|
|
||||||
|
# This is checking the Alpine attrs which is a good-enough proxy for the toggle position
|
||||||
|
assert html =~ "{ enabled: true }"
|
||||||
|
end
|
||||||
|
|
||||||
|
test "renders a toggle in the off position if the source is disabled" do
|
||||||
|
source = %{id: 1, enabled: false}
|
||||||
|
|
||||||
|
html = render_component(SourceEnableToggle, %{id: :foo, source: source})
|
||||||
|
|
||||||
|
assert html =~ "{ enabled: false }"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -0,0 +1,96 @@
|
|||||||
|
defmodule PinchflatWeb.Helpers.PaginationHelpersTest do
|
||||||
|
use Pinchflat.DataCase
|
||||||
|
import Pinchflat.SourcesFixtures
|
||||||
|
|
||||||
|
alias Pinchflat.Sources.Source
|
||||||
|
alias PinchflatWeb.Helpers.PaginationHelpers
|
||||||
|
|
||||||
|
describe "get_pagination_attributes/3" do
|
||||||
|
test "returns the correct pagination attributes" do
|
||||||
|
source_fixture()
|
||||||
|
query = from(s in Source, select: s.id)
|
||||||
|
page = 1
|
||||||
|
records_per_page = 10
|
||||||
|
|
||||||
|
pagination_attributes = PaginationHelpers.get_pagination_attributes(query, page, records_per_page)
|
||||||
|
|
||||||
|
assert pagination_attributes.page == 1
|
||||||
|
assert pagination_attributes.total_pages == 1
|
||||||
|
assert pagination_attributes.total_record_count == 1
|
||||||
|
assert pagination_attributes.limit == 10
|
||||||
|
assert pagination_attributes.offset == 0
|
||||||
|
end
|
||||||
|
|
||||||
|
test "returns the correct pagination attributes when there are multiple pages" do
|
||||||
|
source_fixture()
|
||||||
|
source_fixture()
|
||||||
|
|
||||||
|
query = from(s in Source, select: s.id)
|
||||||
|
page = 1
|
||||||
|
records_per_page = 1
|
||||||
|
|
||||||
|
pagination_attributes = PaginationHelpers.get_pagination_attributes(query, page, records_per_page)
|
||||||
|
|
||||||
|
assert pagination_attributes.page == 1
|
||||||
|
assert pagination_attributes.total_pages == 2
|
||||||
|
assert pagination_attributes.total_record_count == 2
|
||||||
|
assert pagination_attributes.limit == 1
|
||||||
|
assert pagination_attributes.offset == 0
|
||||||
|
end
|
||||||
|
|
||||||
|
test "returns the correct attributes when on a page other than the first" do
|
||||||
|
source_fixture()
|
||||||
|
source_fixture()
|
||||||
|
|
||||||
|
query = from(s in Source, select: s.id)
|
||||||
|
page = 2
|
||||||
|
records_per_page = 1
|
||||||
|
|
||||||
|
pagination_attributes = PaginationHelpers.get_pagination_attributes(query, page, records_per_page)
|
||||||
|
|
||||||
|
assert pagination_attributes.page == 2
|
||||||
|
assert pagination_attributes.total_pages == 2
|
||||||
|
assert pagination_attributes.total_record_count == 2
|
||||||
|
assert pagination_attributes.limit == 1
|
||||||
|
assert pagination_attributes.offset == 1
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "update_page_number/3" do
|
||||||
|
test "increments the page number" do
|
||||||
|
current_page = 1
|
||||||
|
total_pages = 2
|
||||||
|
|
||||||
|
updated_page = PaginationHelpers.update_page_number(current_page, :inc, total_pages)
|
||||||
|
|
||||||
|
assert updated_page == 2
|
||||||
|
end
|
||||||
|
|
||||||
|
test "decrements the page number" do
|
||||||
|
current_page = 2
|
||||||
|
total_pages = 2
|
||||||
|
|
||||||
|
updated_page = PaginationHelpers.update_page_number(current_page, :dec, total_pages)
|
||||||
|
|
||||||
|
assert updated_page == 1
|
||||||
|
end
|
||||||
|
|
||||||
|
test "doesn't overflow the page number" do
|
||||||
|
current_page = 2
|
||||||
|
total_pages = 2
|
||||||
|
|
||||||
|
updated_page = PaginationHelpers.update_page_number(current_page, :inc, total_pages)
|
||||||
|
|
||||||
|
assert updated_page == 2
|
||||||
|
end
|
||||||
|
|
||||||
|
test "doesn't underflow the page number" do
|
||||||
|
current_page = 1
|
||||||
|
total_pages = 2
|
||||||
|
|
||||||
|
updated_page = PaginationHelpers.update_page_number(current_page, :dec, total_pages)
|
||||||
|
|
||||||
|
assert updated_page == 1
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
defmodule PinchflatWeb.Helpers.SortingHelpersTest do
|
||||||
|
use Pinchflat.DataCase
|
||||||
|
|
||||||
|
alias PinchflatWeb.Helpers.SortingHelpers
|
||||||
|
|
||||||
|
describe "get_sort_direction/3" do
|
||||||
|
test "returns the correct sort direction when the new sort attribute is the same as the old sort attribute" do
|
||||||
|
old_sort_attr = "name"
|
||||||
|
new_sort_attr = "name"
|
||||||
|
old_sort_direction = :desc
|
||||||
|
|
||||||
|
assert SortingHelpers.get_sort_direction(old_sort_attr, new_sort_attr, old_sort_direction) == :asc
|
||||||
|
end
|
||||||
|
|
||||||
|
test "returns the correct sort direction when the new sort attribute is the same as the old sort attribute in the other direction" do
|
||||||
|
old_sort_attr = "name"
|
||||||
|
new_sort_attr = "name"
|
||||||
|
old_sort_direction = :asc
|
||||||
|
|
||||||
|
assert SortingHelpers.get_sort_direction(old_sort_attr, new_sort_attr, old_sort_direction) == :desc
|
||||||
|
end
|
||||||
|
|
||||||
|
test "returns the correct sort direction when the new sort attribute is different from the old sort attribute" do
|
||||||
|
old_sort_attr = "name"
|
||||||
|
new_sort_attr = "date"
|
||||||
|
old_sort_direction = :asc
|
||||||
|
|
||||||
|
assert SortingHelpers.get_sort_direction(old_sort_attr, new_sort_attr, old_sort_direction) == :asc
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -0,0 +1,166 @@
|
|||||||
|
defmodule PinchflatWeb.PlugsTest do
|
||||||
|
use PinchflatWeb.ConnCase
|
||||||
|
|
||||||
|
alias PinchflatWeb.Plugs
|
||||||
|
alias Pinchflat.Settings
|
||||||
|
|
||||||
|
describe "maybe_basic_auth/2" do
|
||||||
|
setup do
|
||||||
|
old_username = Application.get_env(:pinchflat, :basic_auth_username)
|
||||||
|
old_password = Application.get_env(:pinchflat, :basic_auth_password)
|
||||||
|
old_expose_feed_endpoints = Application.get_env(:pinchflat, :expose_feed_endpoints)
|
||||||
|
|
||||||
|
on_exit(fn ->
|
||||||
|
Application.put_env(:pinchflat, :basic_auth_username, old_username)
|
||||||
|
Application.put_env(:pinchflat, :basic_auth_password, old_password)
|
||||||
|
Application.put_env(:pinchflat, :expose_feed_endpoints, old_expose_feed_endpoints)
|
||||||
|
end)
|
||||||
|
|
||||||
|
:ok
|
||||||
|
end
|
||||||
|
|
||||||
|
test "uses basic auth when expose_feed_endpoints is false" do
|
||||||
|
Application.put_env(:pinchflat, :basic_auth_username, "user")
|
||||||
|
Application.put_env(:pinchflat, :basic_auth_password, "pass")
|
||||||
|
Application.put_env(:pinchflat, :expose_feed_endpoints, false)
|
||||||
|
|
||||||
|
conn = Plugs.maybe_basic_auth(build_conn(), [])
|
||||||
|
|
||||||
|
assert conn.status == 401
|
||||||
|
assert {"www-authenticate", "Basic realm=\"Pinchflat\""} in conn.resp_headers
|
||||||
|
end
|
||||||
|
|
||||||
|
test "supplying the correct username and password allows access" do
|
||||||
|
Application.put_env(:pinchflat, :basic_auth_username, "user")
|
||||||
|
Application.put_env(:pinchflat, :basic_auth_password, "pass")
|
||||||
|
Application.put_env(:pinchflat, :expose_feed_endpoints, false)
|
||||||
|
|
||||||
|
encoded_auth = Plug.BasicAuth.encode_basic_auth("user", "pass")
|
||||||
|
|
||||||
|
conn =
|
||||||
|
build_conn()
|
||||||
|
|> put_req_header("authorization", encoded_auth)
|
||||||
|
|> Plugs.maybe_basic_auth([])
|
||||||
|
|
||||||
|
# nil here means the response is unset, but that's good. It just means we're moving to the next stage
|
||||||
|
assert conn.status == nil
|
||||||
|
end
|
||||||
|
|
||||||
|
test "does not use basic auth when expose_feed_endpoints is true" do
|
||||||
|
Application.put_env(:pinchflat, :basic_auth_username, "user")
|
||||||
|
Application.put_env(:pinchflat, :basic_auth_password, "pass")
|
||||||
|
Application.put_env(:pinchflat, :expose_feed_endpoints, true)
|
||||||
|
|
||||||
|
conn = Plugs.maybe_basic_auth(build_conn(), [])
|
||||||
|
|
||||||
|
assert conn.status == nil
|
||||||
|
end
|
||||||
|
|
||||||
|
test "does not use basic auth when username/password aren't set" do
|
||||||
|
Application.put_env(:pinchflat, :basic_auth_username, nil)
|
||||||
|
Application.put_env(:pinchflat, :basic_auth_password, nil)
|
||||||
|
Application.put_env(:pinchflat, :expose_feed_endpoints, false)
|
||||||
|
|
||||||
|
conn = Plugs.maybe_basic_auth(build_conn(), [])
|
||||||
|
|
||||||
|
# nil here means the response is unset, but that's good. It just means we're moving to the next stage
|
||||||
|
assert conn.status == nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "basic_auth/2" do
|
||||||
|
setup do
|
||||||
|
old_username = Application.get_env(:pinchflat, :basic_auth_username)
|
||||||
|
old_password = Application.get_env(:pinchflat, :basic_auth_password)
|
||||||
|
|
||||||
|
on_exit(fn ->
|
||||||
|
Application.put_env(:pinchflat, :basic_auth_username, old_username)
|
||||||
|
Application.put_env(:pinchflat, :basic_auth_password, old_password)
|
||||||
|
end)
|
||||||
|
|
||||||
|
:ok
|
||||||
|
end
|
||||||
|
|
||||||
|
test "uses basic auth when both username and password are set", %{conn: conn} do
|
||||||
|
Application.put_env(:pinchflat, :basic_auth_username, "user")
|
||||||
|
Application.put_env(:pinchflat, :basic_auth_password, "pass")
|
||||||
|
|
||||||
|
conn = Plugs.basic_auth(conn, [])
|
||||||
|
|
||||||
|
assert conn.status == 401
|
||||||
|
assert {"www-authenticate", "Basic realm=\"Pinchflat\""} in conn.resp_headers
|
||||||
|
end
|
||||||
|
|
||||||
|
test "providing the username and password allows access", %{conn: conn} do
|
||||||
|
Application.put_env(:pinchflat, :basic_auth_username, "user")
|
||||||
|
Application.put_env(:pinchflat, :basic_auth_password, "pass")
|
||||||
|
|
||||||
|
conn =
|
||||||
|
conn
|
||||||
|
|> put_req_header("authorization", Plug.BasicAuth.encode_basic_auth("user", "pass"))
|
||||||
|
|> Plugs.basic_auth([])
|
||||||
|
|
||||||
|
# nil here means the response is unset, but that's good. It just means we're moving to the next stage
|
||||||
|
assert conn.status == nil
|
||||||
|
end
|
||||||
|
|
||||||
|
test "does not use basic auth when either username or password is not set", %{conn: conn} do
|
||||||
|
Application.put_env(:pinchflat, :basic_auth_username, nil)
|
||||||
|
Application.put_env(:pinchflat, :basic_auth_password, "pass")
|
||||||
|
|
||||||
|
conn = Plugs.basic_auth(conn, [])
|
||||||
|
|
||||||
|
assert conn.status == nil
|
||||||
|
end
|
||||||
|
|
||||||
|
test "treats empty strings as not being set when using basic auth", %{conn: conn} do
|
||||||
|
Application.put_env(:pinchflat, :basic_auth_username, "")
|
||||||
|
Application.put_env(:pinchflat, :basic_auth_password, "pass")
|
||||||
|
|
||||||
|
conn = Plugs.basic_auth(conn, [])
|
||||||
|
|
||||||
|
assert conn.status == nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "allow_iframe_embed/2" do
|
||||||
|
test "deletes the x-frame-options header", %{conn: conn} do
|
||||||
|
conn = put_resp_header(conn, "x-frame-options", "DENY")
|
||||||
|
assert ["DENY"] = get_resp_header(conn, "x-frame-options")
|
||||||
|
|
||||||
|
conn = Plugs.allow_iframe_embed(conn, [])
|
||||||
|
|
||||||
|
assert [] = get_resp_header(conn, "x-frame-options")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "token_protected_route/2" do
|
||||||
|
test "allows access when the route token is correct", %{conn: conn} do
|
||||||
|
route_token = Settings.get!(:route_token)
|
||||||
|
conn = %{conn | query_params: %{"route_token" => route_token}}
|
||||||
|
|
||||||
|
conn = Plugs.token_protected_route(conn, [])
|
||||||
|
|
||||||
|
# nil here means the response is unset, but that's good. It just means we're moving to the next stage
|
||||||
|
assert conn.status == nil
|
||||||
|
end
|
||||||
|
|
||||||
|
test "does not allow access when the route token is incorrect", %{conn: conn} do
|
||||||
|
conn = %{conn | query_params: %{"route_token" => "incorrect"}}
|
||||||
|
|
||||||
|
conn = Plugs.token_protected_route(conn, [])
|
||||||
|
|
||||||
|
assert conn.status == 401
|
||||||
|
assert conn.resp_body == "Unauthorized"
|
||||||
|
end
|
||||||
|
|
||||||
|
test "does not allow access when the route token is missing", %{conn: conn} do
|
||||||
|
conn = %{conn | query_params: %{}}
|
||||||
|
|
||||||
|
conn = Plugs.token_protected_route(conn, [])
|
||||||
|
|
||||||
|
assert conn.status == 401
|
||||||
|
assert conn.resp_body == "Unauthorized"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -1,108 +0,0 @@
|
|||||||
defmodule PinchflatWeb.RoutingTest do
|
|
||||||
use PinchflatWeb.ConnCase
|
|
||||||
|
|
||||||
import Pinchflat.SourcesFixtures
|
|
||||||
|
|
||||||
describe "basic_auth plug" do
|
|
||||||
setup do
|
|
||||||
old_username = Application.get_env(:pinchflat, :basic_auth_username)
|
|
||||||
old_password = Application.get_env(:pinchflat, :basic_auth_password)
|
|
||||||
|
|
||||||
on_exit(fn ->
|
|
||||||
Application.put_env(:pinchflat, :basic_auth_username, old_username)
|
|
||||||
Application.put_env(:pinchflat, :basic_auth_password, old_password)
|
|
||||||
end)
|
|
||||||
|
|
||||||
:ok
|
|
||||||
end
|
|
||||||
|
|
||||||
test "it uses basic auth when both username and password are set", %{conn: conn} do
|
|
||||||
Application.put_env(:pinchflat, :basic_auth_username, "user")
|
|
||||||
Application.put_env(:pinchflat, :basic_auth_password, "pass")
|
|
||||||
|
|
||||||
conn = get(conn, "/")
|
|
||||||
|
|
||||||
assert conn.status == 401
|
|
||||||
assert {"www-authenticate", "Basic realm=\"Pinchflat\""} in conn.resp_headers
|
|
||||||
end
|
|
||||||
|
|
||||||
test "providing the username and password allows access", %{conn: conn} do
|
|
||||||
Application.put_env(:pinchflat, :basic_auth_username, "user")
|
|
||||||
Application.put_env(:pinchflat, :basic_auth_password, "pass")
|
|
||||||
|
|
||||||
conn =
|
|
||||||
conn
|
|
||||||
|> put_req_header("authorization", Plug.BasicAuth.encode_basic_auth("user", "pass"))
|
|
||||||
|> get("/")
|
|
||||||
|
|
||||||
assert conn.status == 200
|
|
||||||
end
|
|
||||||
|
|
||||||
test "it does not use basic auth when either username or password is not set", %{conn: conn} do
|
|
||||||
Application.put_env(:pinchflat, :basic_auth_username, nil)
|
|
||||||
Application.put_env(:pinchflat, :basic_auth_password, "pass")
|
|
||||||
|
|
||||||
conn = get(conn, "/")
|
|
||||||
|
|
||||||
assert conn.status == 200
|
|
||||||
end
|
|
||||||
|
|
||||||
test "it treats empty strings as not being set when using basic auth", %{conn: conn} do
|
|
||||||
Application.put_env(:pinchflat, :basic_auth_username, "")
|
|
||||||
Application.put_env(:pinchflat, :basic_auth_password, "pass")
|
|
||||||
|
|
||||||
conn = get(conn, "/")
|
|
||||||
|
|
||||||
assert conn.status == 200
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
describe "maybe_basic_auth plug" do
|
|
||||||
setup do
|
|
||||||
old_username = Application.get_env(:pinchflat, :basic_auth_username)
|
|
||||||
old_password = Application.get_env(:pinchflat, :basic_auth_password)
|
|
||||||
old_expose_feed_endpoints = Application.get_env(:pinchflat, :expose_feed_endpoints)
|
|
||||||
|
|
||||||
source = source_fixture()
|
|
||||||
|
|
||||||
on_exit(fn ->
|
|
||||||
Application.put_env(:pinchflat, :basic_auth_username, old_username)
|
|
||||||
Application.put_env(:pinchflat, :basic_auth_password, old_password)
|
|
||||||
Application.put_env(:pinchflat, :expose_feed_endpoints, old_expose_feed_endpoints)
|
|
||||||
end)
|
|
||||||
|
|
||||||
{:ok, source: source}
|
|
||||||
end
|
|
||||||
|
|
||||||
test "uses basic auth when expose_feed_endpoints is false", %{source: source} do
|
|
||||||
Application.put_env(:pinchflat, :basic_auth_username, "user")
|
|
||||||
Application.put_env(:pinchflat, :basic_auth_password, "pass")
|
|
||||||
Application.put_env(:pinchflat, :expose_feed_endpoints, false)
|
|
||||||
|
|
||||||
conn = get(build_conn(), "/sources/#{source.uuid}/feed")
|
|
||||||
|
|
||||||
assert conn.status == 401
|
|
||||||
assert {"www-authenticate", "Basic realm=\"Pinchflat\""} in conn.resp_headers
|
|
||||||
end
|
|
||||||
|
|
||||||
test "does not use basic auth when expose_feed_endpoints is true", %{source: source} do
|
|
||||||
Application.put_env(:pinchflat, :basic_auth_username, "user")
|
|
||||||
Application.put_env(:pinchflat, :basic_auth_password, "pass")
|
|
||||||
Application.put_env(:pinchflat, :expose_feed_endpoints, true)
|
|
||||||
|
|
||||||
conn = get(build_conn(), "/sources/#{source.uuid}/feed")
|
|
||||||
|
|
||||||
assert conn.status == 200
|
|
||||||
end
|
|
||||||
|
|
||||||
test "does not use basic auth when username/password aren't set", %{source: source} do
|
|
||||||
Application.put_env(:pinchflat, :basic_auth_username, nil)
|
|
||||||
Application.put_env(:pinchflat, :basic_auth_password, nil)
|
|
||||||
Application.put_env(:pinchflat, :expose_feed_endpoints, false)
|
|
||||||
|
|
||||||
conn = get(build_conn(), "/sources/#{source.uuid}/feed")
|
|
||||||
|
|
||||||
assert conn.status == 200
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
+3
@@ -0,0 +1,3 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
exit 101
|
||||||
@@ -10,6 +10,8 @@ Application.put_env(:pinchflat, :http_client, HTTPClientMock)
|
|||||||
Mox.defmock(UserScriptRunnerMock, for: Pinchflat.Lifecycle.UserScripts.UserScriptCommandRunner)
|
Mox.defmock(UserScriptRunnerMock, for: Pinchflat.Lifecycle.UserScripts.UserScriptCommandRunner)
|
||||||
Application.put_env(:pinchflat, :user_script_runner, UserScriptRunnerMock)
|
Application.put_env(:pinchflat, :user_script_runner, UserScriptRunnerMock)
|
||||||
|
|
||||||
|
if System.get_env("EX_CHECK"), do: Code.put_compiler_option(:warnings_as_errors, true)
|
||||||
|
|
||||||
ExUnit.start()
|
ExUnit.start()
|
||||||
Ecto.Adapters.SQL.Sandbox.mode(Pinchflat.Repo, :manual)
|
Ecto.Adapters.SQL.Sandbox.mode(Pinchflat.Repo, :manual)
|
||||||
Faker.start()
|
Faker.start()
|
||||||
|
|||||||
+2
-1
@@ -17,7 +17,8 @@
|
|||||||
{:formatter, env: %{"MIX_ENV" => "test"}},
|
{:formatter, env: %{"MIX_ENV" => "test"}},
|
||||||
{:sobelow, "mix sobelow --config"},
|
{:sobelow, "mix sobelow --config"},
|
||||||
{:prettier_formatting, "yarn run lint:check", fix: "yarn run lint:fix"},
|
{:prettier_formatting, "yarn run lint:check", fix: "yarn run lint:fix"},
|
||||||
{:npm_test, false}
|
{:npm_test, false},
|
||||||
|
{:ex_unit, env: %{"MIX_ENV" => "test", "EX_CHECK" => "1"}}
|
||||||
|
|
||||||
## curated tools may be disabled (e.g. the check for compilation warnings)
|
## curated tools may be disabled (e.g. the check for compilation warnings)
|
||||||
# {:compiler, false},
|
# {:compiler, false},
|
||||||
|
|||||||
@@ -12,3 +12,5 @@ sed -i "s/version: \"$VERSION\"/version: \"$DATE\"/g" mix.exs
|
|||||||
|
|
||||||
# Run checks to ensure it's a valid mix.exs file
|
# Run checks to ensure it's a valid mix.exs file
|
||||||
mix check
|
mix check
|
||||||
|
|
||||||
|
echo "Version bumped successfully to $DATE"
|
||||||
|
|||||||
Reference in New Issue
Block a user