Compare commits
19 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 20547e60e4 | |||
| 0fd3106cb8 | |||
| 108a141c65 | |||
| 6a1b7b0160 | |||
| 3c8d99196a | |||
| cae86953a0 | |||
| f661747a0c | |||
| 85feb67299 | |||
| 4843d7d6c2 | |||
| a0b9e49486 | |||
| 0163e85e76 | |||
| 7fb6fa3af4 | |||
| bb8788f03d | |||
| ed4434512b | |||
| f6344d43d2 | |||
| e0745bdfbe | |||
| a0a02cb4be | |||
| 7866f7258a | |||
| 5cd82f9dd5 |
@@ -72,7 +72,7 @@ jobs:
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Build and Push
|
||||
uses: docker/build-push-action@v5
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: .
|
||||
file: ./docker/selfhosted.Dockerfile
|
||||
|
||||
@@ -29,7 +29,7 @@ jobs:
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Build docker image
|
||||
uses: docker/build-push-action@v5
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: .
|
||||
file: ./docker/dev.Dockerfile
|
||||
|
||||
@@ -131,9 +131,6 @@ You _must_ ensure the host directories you've mounted are writable by the user r
|
||||
> [!IMPORTANT]
|
||||
> It's not recommended to run the container as root. Doing so can create permission issues if other apps need to work with the downloaded media.
|
||||
|
||||
> [!TIP]
|
||||
> If you need to run any command as root, you can run `su` from the container's shell as there is no password set for the root user.
|
||||
|
||||
### ADVANCED: Storing Pinchflat config directory on a network share
|
||||
|
||||
As pointed out in [#137](https://github.com/kieraneglin/pinchflat/issues/137), SQLite doesn't like being run in WAL mode on network shares. If you're running Pinchflat on a network share, you can disable WAL mode by setting the `JOURNAL_MODE` environment variable to `delete`. This will make Pinchflat run in rollback journal mode which is less performant but should work on network shares.
|
||||
@@ -155,7 +152,7 @@ If you change this setting and it works well for you, please leave a comment on
|
||||
| `ENABLE_IPV6` | No | `false` | Setting to _any_ non-blank value will enable IPv6 |
|
||||
| `JOURNAL_MODE` | No | `wal` | Set to `delete` if your config directory is stored on a network share (not recommended) |
|
||||
| `TZ_DATA_DIR` | No | `/etc/elixir_tzdata_data` | The container path where the timezone database is stored |
|
||||
| `BASE_ROUTE_PATH` | No | `/` | The base path for route generation. Useful when running behind certain reverse proxies |
|
||||
| `BASE_ROUTE_PATH` | No | `/` | The base path for route generation. Useful when running behind certain reverse proxies, but prefix must be stripped. |
|
||||
| `YT_DLP_WORKER_CONCURRENCY` | No | `2` | The number of concurrent workers that use `yt-dlp` _per queue_. Set to 1 if you're getting IP limited, otherwise don't touch it |
|
||||
|
||||
## EFF donations
|
||||
|
||||
@@ -49,14 +49,16 @@ ENV LANG=en_US.UTF-8
|
||||
ENV LANGUAGE=en_US:en
|
||||
ENV LC_ALL=en_US.UTF-8
|
||||
|
||||
# Create app directory and copy the Elixir projects into it.
|
||||
WORKDIR /app
|
||||
COPY . ./
|
||||
|
||||
COPY mix.exs mix.lock ./
|
||||
# Install Elixir deps
|
||||
# RUN mix archive.install github hexpm/hex branch latest
|
||||
# NOTE: this has to be before the bulk copy to ensure that deps are cached
|
||||
RUN MIX_ENV=dev mix deps.get && MIX_ENV=dev mix deps.compile
|
||||
RUN MIX_ENV=test mix deps.get && MIX_ENV=test mix deps.compile
|
||||
|
||||
COPY . ./
|
||||
|
||||
# Gives us iex shell history
|
||||
ENV ERL_AFLAGS="-kernel shell_history enabled"
|
||||
|
||||
|
||||
@@ -127,17 +127,6 @@ EXPOSE ${PORT}
|
||||
# Only copy the final release from the build stage
|
||||
COPY --from=builder /app/_build/${MIX_ENV}/rel/pinchflat ./
|
||||
|
||||
# NEVER do this if you're running in an environment where you don't trust the user
|
||||
# (ie: most environments). This is only acceptable in a self-hosted environment.
|
||||
# The user could just run the whole container as root and bypass this anyway so
|
||||
# it's not a huge deal.
|
||||
# This removes the root password to allow users to assume root if needed. This is
|
||||
# preferrable to running the whole container as root so that the files/directories
|
||||
# created by the app aren't owned by root and are therefore easier for other users
|
||||
# and processes to interact with. If you want to just run the whole container as
|
||||
# root, use --user 0:0 or something.
|
||||
RUN passwd -d root
|
||||
|
||||
HEALTHCHECK --interval=120s --start-period=10s \
|
||||
CMD curl --fail http://localhost:${PORT}/healthcheck || exit 1
|
||||
|
||||
|
||||
@@ -127,11 +127,12 @@ defmodule Pinchflat.Downloading.DownloadOptionBuilder do
|
||||
defp quality_options(media_profile) do
|
||||
vcodec = Settings.get!(:video_codec_preference)
|
||||
acodec = Settings.get!(:audio_codec_preference)
|
||||
container = media_profile.media_container
|
||||
|
||||
case media_profile.preferred_resolution do
|
||||
# Also be aware that :audio disabled all embedding options for subtitles
|
||||
:audio ->
|
||||
[:extract_audio, format_sort: "+acodec:#{acodec}"]
|
||||
[:extract_audio, format_sort: "+acodec:#{acodec}", audio_format: container || "best"]
|
||||
|
||||
resolution_atom ->
|
||||
{resolution_string, _} =
|
||||
@@ -141,7 +142,7 @@ defmodule Pinchflat.Downloading.DownloadOptionBuilder do
|
||||
|
||||
[
|
||||
# Since Plex doesn't support reading metadata from MKV
|
||||
remux_video: "mp4",
|
||||
remux_video: container || "mp4",
|
||||
format_sort: "res:#{resolution_string},+codec:#{vcodec}:#{acodec}"
|
||||
]
|
||||
end
|
||||
|
||||
@@ -12,6 +12,7 @@ defmodule Pinchflat.Downloading.MediaDownloadWorker do
|
||||
alias Pinchflat.Tasks
|
||||
alias Pinchflat.Repo
|
||||
alias Pinchflat.Media
|
||||
alias Pinchflat.Media.FileSyncing
|
||||
alias Pinchflat.Downloading.MediaDownloader
|
||||
|
||||
alias Pinchflat.Lifecycle.UserScripts.CommandRunner, as: UserScriptRunner
|
||||
@@ -85,6 +86,7 @@ defmodule Pinchflat.Downloading.MediaDownloadWorker do
|
||||
media_redownloaded_at: get_redownloaded_at(is_quality_upgrade)
|
||||
})
|
||||
|
||||
:ok = FileSyncing.delete_outdated_files(media_item, updated_media_item)
|
||||
run_user_script(:media_downloaded, updated_media_item)
|
||||
|
||||
:ok
|
||||
|
||||
@@ -49,6 +49,8 @@ defmodule Pinchflat.Downloading.MediaRetentionWorker do
|
||||
end)
|
||||
end
|
||||
|
||||
# NOTE: Since this is a date and not a datetime, we can't add logic to have to-the-minute
|
||||
# comparison like we can with retention periods. We can only compare to the day.
|
||||
defp delete_media_items_from_before_cutoff do
|
||||
deletable_media =
|
||||
MediaQuery.new()
|
||||
|
||||
@@ -0,0 +1,93 @@
|
||||
defmodule Pinchflat.Media.FileSyncing do
|
||||
@moduledoc """
|
||||
Functions for ensuring file state is accurately reflected in the database.
|
||||
"""
|
||||
|
||||
alias Pinchflat.Media
|
||||
alias Pinchflat.Utils.MapUtils
|
||||
alias Pinchflat.Media.MediaItem
|
||||
alias Pinchflat.Utils.FilesystemUtils, as: FSUtils
|
||||
|
||||
@doc """
|
||||
Deletes files that are no longer needed by a media item.
|
||||
|
||||
This means that if a media item has been updated, the old and new versions
|
||||
can be passed and any files that are no longer needed will be deleted.
|
||||
|
||||
An example is a video that gets its quality upgraded and its name changes
|
||||
between original download and re-download. The old file will exist on-disk
|
||||
with the old name but the database entry will point to the new file. This
|
||||
function can be used to delete the old file in this case.
|
||||
|
||||
Returns :ok
|
||||
"""
|
||||
def delete_outdated_files(old_media_item, new_media_item) do
|
||||
non_subtitle_keys = MediaItem.filepath_attributes() -- [:subtitle_filepaths]
|
||||
|
||||
old_non_subtitles = Map.take(old_media_item, non_subtitle_keys)
|
||||
old_subtitles = MapUtils.from_nested_list(old_media_item.subtitle_filepaths)
|
||||
new_non_subtitles = Map.take(new_media_item, non_subtitle_keys)
|
||||
new_subtitles = MapUtils.from_nested_list(new_media_item.subtitle_filepaths)
|
||||
|
||||
handle_file_deletion(old_non_subtitles, new_non_subtitles)
|
||||
handle_file_deletion(old_subtitles, new_subtitles)
|
||||
|
||||
:ok
|
||||
end
|
||||
|
||||
@doc """
|
||||
Nillifies any media item filepaths that don't exist on disk for a list of media items
|
||||
|
||||
returns [%MediaItem{}]
|
||||
"""
|
||||
def sync_file_presence_on_disk(media_items) do
|
||||
Enum.map(media_items, fn media_item ->
|
||||
new_attributes = sync_media_item_files(media_item)
|
||||
# Doing this one-by-one instead of batching since this process
|
||||
# can take time and a batch could let MediaItem state get out of sync
|
||||
{:ok, updated_media_item} = Media.update_media_item(media_item, new_attributes)
|
||||
|
||||
updated_media_item
|
||||
end)
|
||||
end
|
||||
|
||||
defp handle_file_deletion(old_attributes, new_attributes) do
|
||||
# The logic:
|
||||
# - A file should only be deleted if it exists and the new file is different
|
||||
# - The new attributes are the ones we're interested in keeping
|
||||
# - If the old attributes have a key that doesn't exist in the new attributes, don't touch it.
|
||||
# This is good for archiving but may be unpopular for other users so this may change.
|
||||
|
||||
Enum.each(new_attributes, fn {key, new_filepath} ->
|
||||
old_filepath = Map.get(old_attributes, key)
|
||||
files_have_changed = old_filepath && new_filepath && old_filepath != new_filepath
|
||||
files_exist_on_disk = files_have_changed && File.exists?(old_filepath) && File.exists?(new_filepath)
|
||||
|
||||
if files_exist_on_disk && !FSUtils.filepaths_reference_same_file?(old_filepath, new_filepath) do
|
||||
FSUtils.delete_file_and_remove_empty_directories(old_filepath)
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
defp sync_media_item_files(media_item) do
|
||||
non_subtitle_keys = MediaItem.filepath_attributes() -- [:subtitle_filepaths]
|
||||
subtitle_keys = MapUtils.from_nested_list(media_item.subtitle_filepaths)
|
||||
non_subtitles = Map.take(media_item, non_subtitle_keys)
|
||||
|
||||
# This one is checking for the negative (ie: only update if the file doesn't exist)
|
||||
new_non_subtitle_attrs =
|
||||
Enum.reduce(non_subtitles, %{}, fn {key, filepath}, acc ->
|
||||
if filepath && File.exists?(filepath), do: acc, else: Map.put(acc, key, nil)
|
||||
end)
|
||||
|
||||
# This one is checking for the positive (ie: only update if the file exists)
|
||||
# This is because subtitles, being an array type in the DB, are most easily updated
|
||||
# by a full replacement rather than finding the actual diff
|
||||
new_subtitle_attrs =
|
||||
Enum.reduce(subtitle_keys, [], fn {key, filepath}, acc ->
|
||||
if filepath && File.exists?(filepath), do: acc ++ [[key, filepath]], else: acc
|
||||
end)
|
||||
|
||||
Map.put(new_non_subtitle_attrs, :subtitle_filepaths, new_subtitle_attrs)
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,38 @@
|
||||
defmodule Pinchflat.Media.FileSyncingWorker do
|
||||
@moduledoc false
|
||||
|
||||
use Oban.Worker,
|
||||
queue: :local_data,
|
||||
tags: ["sources", "local_data"]
|
||||
|
||||
alias __MODULE__
|
||||
alias Pinchflat.Repo
|
||||
alias Pinchflat.Tasks
|
||||
alias Pinchflat.Sources
|
||||
alias Pinchflat.Media.FileSyncing
|
||||
|
||||
@doc """
|
||||
Starts the source file syncing worker.
|
||||
|
||||
Returns {:ok, %Task{}} | {:error, %Ecto.Changeset{}}
|
||||
"""
|
||||
def kickoff_with_task(source, opts \\ []) do
|
||||
%{id: source.id}
|
||||
|> FileSyncingWorker.new(opts)
|
||||
|> Tasks.create_job_with_task(source)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Deletes a profile and optionally deletes its files
|
||||
|
||||
Returns :ok
|
||||
"""
|
||||
@impl Oban.Worker
|
||||
def perform(%Oban.Job{args: %{"id" => source_id}}) do
|
||||
source = Repo.preload(Sources.get_source!(source_id), :media_items)
|
||||
|
||||
FileSyncing.sync_file_presence_on_disk(source.media_items)
|
||||
|
||||
:ok
|
||||
end
|
||||
end
|
||||
@@ -88,7 +88,7 @@ defmodule Pinchflat.Media.MediaQuery do
|
||||
[mi, source],
|
||||
fragment("""
|
||||
IFNULL(retention_period_days, 0) > 0 AND
|
||||
DATETIME('now', '-' || retention_period_days || ' day') > media_downloaded_at
|
||||
DATETIME(media_downloaded_at, '+' || retention_period_days || ' day') < DATETIME('now')
|
||||
""")
|
||||
)
|
||||
end
|
||||
@@ -100,8 +100,8 @@ defmodule Pinchflat.Media.MediaQuery do
|
||||
# downloaded_at minus the redownload_delay_days is before the upload date
|
||||
fragment("""
|
||||
IFNULL(redownload_delay_days, 0) > 0 AND
|
||||
DATETIME('now', '-' || redownload_delay_days || ' day') > uploaded_at AND
|
||||
DATETIME(media_downloaded_at, '-' || redownload_delay_days || ' day') < uploaded_at
|
||||
DATE('now', '-' || redownload_delay_days || ' day') > DATE(uploaded_at) AND
|
||||
DATE(media_downloaded_at, '-' || redownload_delay_days || ' day') < DATE(uploaded_at)
|
||||
""")
|
||||
)
|
||||
end
|
||||
|
||||
@@ -27,6 +27,7 @@ defmodule Pinchflat.Profiles.MediaProfile do
|
||||
shorts_behaviour
|
||||
livestream_behaviour
|
||||
preferred_resolution
|
||||
media_container
|
||||
redownload_delay_days
|
||||
marked_for_deletion_at
|
||||
)a
|
||||
@@ -65,6 +66,7 @@ defmodule Pinchflat.Profiles.MediaProfile do
|
||||
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 :preferred_resolution, Ecto.Enum, values: ~w(4320p 2160p 1080p 720p 480p 360p audio)a, default: :"1080p"
|
||||
field :media_container, :string, default: nil
|
||||
|
||||
field :marked_for_deletion_at, :utc_datetime
|
||||
|
||||
|
||||
@@ -28,7 +28,7 @@ defmodule Pinchflat.SlowIndexing.SlowIndexingHelpers do
|
||||
"""
|
||||
def kickoff_indexing_task(%Source{} = source, job_args \\ %{}, job_opts \\ []) do
|
||||
Tasks.delete_pending_tasks_for(source, "FastIndexingWorker")
|
||||
Tasks.delete_pending_tasks_for(source, "MediaCollectionIndexingWorker")
|
||||
Tasks.delete_pending_tasks_for(source, "MediaCollectionIndexingWorker", include_executing: true)
|
||||
|
||||
MediaCollectionIndexingWorker.kickoff_with_task(source, job_args, job_opts)
|
||||
end
|
||||
|
||||
@@ -186,7 +186,13 @@ defmodule Pinchflat.Sources do
|
||||
{:ok, source_details} ->
|
||||
add_source_details_by_collection_type(source, changeset, source_details)
|
||||
|
||||
{:error, runner_error, _status_code} ->
|
||||
err ->
|
||||
runner_error =
|
||||
case err do
|
||||
{:error, error_msg, _status_code} -> error_msg
|
||||
{:error, error_msg} -> error_msg
|
||||
end
|
||||
|
||||
Ecto.Changeset.add_error(
|
||||
changeset,
|
||||
:original_url,
|
||||
|
||||
@@ -53,20 +53,6 @@ defmodule Pinchflat.Tasks do
|
||||
)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns the list of pending tasks for a given record type and ID. Optionally allows you to specify
|
||||
which worker to include.
|
||||
|
||||
Returns [%Task{}, ...]
|
||||
"""
|
||||
def list_pending_tasks_for(record, worker_name \\ nil) do
|
||||
list_tasks_for(
|
||||
record,
|
||||
worker_name,
|
||||
[:available, :scheduled, :retryable]
|
||||
)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Gets a single task.
|
||||
|
||||
@@ -127,13 +113,13 @@ defmodule Pinchflat.Tasks do
|
||||
|
||||
@doc """
|
||||
Deletes all tasks attached to a given record, cancelling any attached jobs.
|
||||
Optionally allows you to specify which worker to include.
|
||||
Optionally allows you to specify which worker and job states to include.
|
||||
|
||||
Returns :ok
|
||||
"""
|
||||
def delete_tasks_for(record, worker_name \\ nil) do
|
||||
def delete_tasks_for(record, worker_name \\ nil, job_states \\ Oban.Job.states()) do
|
||||
record
|
||||
|> list_tasks_for(worker_name)
|
||||
|> list_tasks_for(worker_name, job_states)
|
||||
|> Enum.each(&delete_task/1)
|
||||
end
|
||||
|
||||
@@ -143,10 +129,12 @@ defmodule Pinchflat.Tasks do
|
||||
|
||||
Returns :ok
|
||||
"""
|
||||
def delete_pending_tasks_for(record, worker_name \\ nil) do
|
||||
record
|
||||
|> list_pending_tasks_for(worker_name)
|
||||
|> Enum.each(&delete_task/1)
|
||||
def delete_pending_tasks_for(record, worker_name \\ nil, opts \\ []) do
|
||||
include_executing = Keyword.get(opts, :include_executing, false)
|
||||
base_job_states = [:available, :scheduled, :retryable]
|
||||
job_states = if include_executing, do: base_job_states ++ [:executing], else: base_job_states
|
||||
|
||||
delete_tasks_for(record, worker_name, job_states)
|
||||
end
|
||||
|
||||
@doc """
|
||||
|
||||
@@ -20,6 +20,24 @@ defmodule Pinchflat.Utils.FilesystemUtils do
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Checks if two filepaths reference the same file.
|
||||
|
||||
Useful if you have a relative and absolute filepath and want to be sure they're the same file.
|
||||
Also works with symlinks.
|
||||
|
||||
Returns boolean()
|
||||
"""
|
||||
def filepaths_reference_same_file?(filepath_1, filepath_2) do
|
||||
{:ok, stat_1} = File.stat(filepath_1)
|
||||
{:ok, stat_2} = File.stat(filepath_2)
|
||||
|
||||
identifier_1 = "#{stat_1.major_device}:#{stat_1.minor_device}:#{stat_1.inode}"
|
||||
identifier_2 = "#{stat_2.major_device}:#{stat_2.minor_device}:#{stat_2.inode}"
|
||||
|
||||
identifier_1 == identifier_2
|
||||
end
|
||||
|
||||
@doc """
|
||||
Generates a temporary file and returns its path. The file is empty and has the given type.
|
||||
Generates all the directories in the path if they don't exist.
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
defmodule Pinchflat.Utils.MapUtils do
|
||||
@moduledoc """
|
||||
Utility methods for working with maps
|
||||
"""
|
||||
|
||||
@doc """
|
||||
Converts a nested list of 2-element tuples or lists into a map.
|
||||
|
||||
Returns map()
|
||||
"""
|
||||
def from_nested_list(list) do
|
||||
Enum.reduce(list, %{}, fn
|
||||
[key, value], acc -> Map.put(acc, key, value)
|
||||
{key, value}, acc -> Map.put(acc, key, value)
|
||||
end)
|
||||
end
|
||||
end
|
||||
@@ -124,7 +124,7 @@ defmodule Pinchflat.YtDlp.Media do
|
||||
#
|
||||
# These don't fail if duration or aspect_ratio are missing
|
||||
# due to Elixir's comparison semantics
|
||||
response["duration"] <= 60 && response["aspect_ratio"] < 0.8
|
||||
response["duration"] <= 60 && response["aspect_ratio"] <= 0.85
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -84,6 +84,7 @@ defmodule Pinchflat.YtDlp.MediaCollection do
|
||||
{:ok, parsed_json} <- Phoenix.json_library().decode(output) do
|
||||
{:ok, format_source_details(parsed_json)}
|
||||
else
|
||||
{:error, %Jason.DecodeError{}} -> {:error, "Error decoding JSON response"}
|
||||
err -> err
|
||||
end
|
||||
end
|
||||
|
||||
@@ -62,17 +62,24 @@ defmodule PinchflatWeb.CustomComponents.TextComponents do
|
||||
end
|
||||
|
||||
@doc """
|
||||
Renders a block of text with each line broken into a separate span.
|
||||
Renders a block of text with each line broken into a separate span and links highlighted.
|
||||
"""
|
||||
attr :text, :string, required: true
|
||||
|
||||
def break_on_newline(assigns) do
|
||||
broken_text =
|
||||
assigns.text
|
||||
|> String.split("\n", trim: false)
|
||||
|> Enum.intersperse(Phoenix.HTML.Tag.tag(:span, class: "inline-block mt-2"))
|
||||
def render_description(assigns) do
|
||||
formatted_text =
|
||||
Regex.split(~r{https?://\S+}, assigns.text, include_captures: true)
|
||||
|> Enum.map(fn
|
||||
"http" <> _ = url ->
|
||||
Phoenix.HTML.Tag.content_tag(:a, url, class: "text-blue-500 hover:text-blue-300", href: url, target: "_blank")
|
||||
|
||||
assigns = Map.put(assigns, :text, broken_text)
|
||||
text ->
|
||||
text
|
||||
|> String.split("\n", trim: false)
|
||||
|> Enum.intersperse(Phoenix.HTML.Tag.tag(:span, class: "inline-block mt-2"))
|
||||
end)
|
||||
|
||||
assigns = Map.put(assigns, :text, formatted_text)
|
||||
|
||||
~H"""
|
||||
<span><%= @text %></span>
|
||||
|
||||
@@ -64,7 +64,7 @@ defmodule PinchflatWeb.Layouts do
|
||||
<.icon name={@icon} /> <%= @text %>
|
||||
</span>
|
||||
<span class="text-bodydark2">
|
||||
<.icon name="hero-chevron-up" x-bind:class="{ 'rotate-180': selected }" />
|
||||
<.icon name="hero-chevron-down" x-bind:class="{ 'rotate-180': selected }" />
|
||||
</span>
|
||||
</span>
|
||||
|
||||
|
||||
+1
-1
@@ -2,7 +2,7 @@
|
||||
<:option>
|
||||
<span x-data="{ copied: false }" x-on:click={~s"
|
||||
copyWithCallbacks(
|
||||
`#{Jason.Formatter.pretty_print(Phoenix.json_library().encode!(@media_item))}`,
|
||||
String.raw`#{Jason.Formatter.pretty_print(Phoenix.json_library().encode!(@media_item))}`,
|
||||
() => copied = true,
|
||||
() => copied = false
|
||||
)
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<%= if media_type(@media_item) == :video do %>
|
||||
<video controls class="max-h-96 w-full">
|
||||
<video controls class="max-h-128 w-full">
|
||||
<source src={~p"/media/#{@media_item.uuid}/stream"} type="video/mp4" />
|
||||
Your browser does not support the video element.
|
||||
</video>
|
||||
|
||||
@@ -26,11 +26,12 @@
|
||||
<:tab title="Media" id="media">
|
||||
<div class="flex flex-col gap-10 dark:text-white">
|
||||
<%= if media_file_exists?(@media_item) do %>
|
||||
<section class="grid grid-cols-1 xl:grid-cols-2 xl:gap-6 mt-6">
|
||||
<section class="grid grid-cols-1 xl:gap-6 mt-6">
|
||||
<div>
|
||||
<.media_preview media_item={@media_item} />
|
||||
</div>
|
||||
<aside class="mt-4 xl:mt-0">
|
||||
<aside class="mt-4">
|
||||
<h2 class="text-xl mb-2"><%= @media_item.title %></h2>
|
||||
<div>Uploaded: <%= DateTime.to_date(@media_item.uploaded_at) %></div>
|
||||
<div>
|
||||
<span :if={URI.parse(@media_item.original_url).scheme =~ "http"}>
|
||||
@@ -44,7 +45,7 @@
|
||||
</span>
|
||||
</div>
|
||||
<div :if={@media_item.description} class="mt-4 text-bodydark">
|
||||
<.break_on_newline text={@media_item.description} />
|
||||
<.render_description text={@media_item.description} />
|
||||
</div>
|
||||
</aside>
|
||||
</section>
|
||||
@@ -70,7 +71,7 @@
|
||||
<%= task.job.state %>
|
||||
</:col>
|
||||
<:col :let={task} label="Scheduled At">
|
||||
<%= Calendar.strftime(task.job.scheduled_at, "%y-%m-%d %I:%M:%S %p %Z") %>
|
||||
<.datetime_in_zone datetime={task.job.scheduled_at} />
|
||||
</:col>
|
||||
</.table>
|
||||
<% else %>
|
||||
|
||||
+1
-1
@@ -2,7 +2,7 @@
|
||||
<:option>
|
||||
<span x-data="{ copied: false }" x-on:click={~s"
|
||||
copyWithCallbacks(
|
||||
`#{Jason.Formatter.pretty_print(Phoenix.json_library().encode!(@media_profile))}`,
|
||||
String.raw`#{Jason.Formatter.pretty_print(Phoenix.json_library().encode!(@media_profile))}`,
|
||||
() => copied = true,
|
||||
() => copied = false
|
||||
)
|
||||
|
||||
+25
-5
@@ -1,4 +1,10 @@
|
||||
<.simple_form :let={f} for={@changeset} action={@action}>
|
||||
<.simple_form
|
||||
:let={f}
|
||||
for={@changeset}
|
||||
action={@action}
|
||||
x-data="{ advancedMode: !!JSON.parse(localStorage.getItem('advancedMode')) }"
|
||||
x-init="$watch('advancedMode', value => localStorage.setItem('advancedMode', JSON.stringify(value)))"
|
||||
>
|
||||
<.error :if={@changeset.action}>
|
||||
Oops, something went wrong! Please check the errors below.
|
||||
</.error>
|
||||
@@ -30,10 +36,14 @@
|
||||
</.input>
|
||||
</section>
|
||||
|
||||
<h3 class="mt-8 text-2xl text-black dark:text-white">
|
||||
General Options
|
||||
</h3>
|
||||
|
||||
<section class="flex justify-between items-center mt-8">
|
||||
<h3 class="text-2xl text-white">
|
||||
General Options
|
||||
</h3>
|
||||
<span class="cursor-pointer hover:underline" x-on:click="advancedMode = !advancedMode">
|
||||
Editing Mode: <span x-text="advancedMode ? 'Advanced' : 'Standard'"></span>
|
||||
</span>
|
||||
</section>
|
||||
<section x-data="{
|
||||
presets: {
|
||||
default: 'Default',
|
||||
@@ -203,6 +213,16 @@
|
||||
/>
|
||||
</section>
|
||||
|
||||
<section x-show="advancedMode">
|
||||
<.input
|
||||
field={f[:media_container]}
|
||||
type="text"
|
||||
label="Media Container"
|
||||
placeholder="mp4"
|
||||
help="Don't change this if you're going to consume media via Plex. Leave blank for default"
|
||||
/>
|
||||
</section>
|
||||
|
||||
<section x-data="{ presets: { default: null, media_center: 1, audio: null, archiving: 1 } }">
|
||||
<.input
|
||||
field={f[:redownload_delay_days]}
|
||||
|
||||
@@ -8,6 +8,7 @@ defmodule PinchflatWeb.Sources.SourceController do
|
||||
alias Pinchflat.Sources.Source
|
||||
alias Pinchflat.Media.MediaItem
|
||||
alias Pinchflat.Profiles.MediaProfile
|
||||
alias Pinchflat.Media.FileSyncingWorker
|
||||
alias Pinchflat.Sources.SourceDeletionWorker
|
||||
alias Pinchflat.Downloading.DownloadingHelpers
|
||||
alias Pinchflat.SlowIndexing.SlowIndexingHelpers
|
||||
@@ -175,6 +176,15 @@ defmodule PinchflatWeb.Sources.SourceController do
|
||||
)
|
||||
end
|
||||
|
||||
def sync_files_on_disk(conn, %{"source_id" => id}) do
|
||||
wrap_forced_action(
|
||||
conn,
|
||||
id,
|
||||
"File sync enqueued.",
|
||||
&FileSyncingWorker.kickoff_with_task/1
|
||||
)
|
||||
end
|
||||
|
||||
defp wrap_forced_action(conn, source_id, message, fun) do
|
||||
source = Sources.get_source!(source_id)
|
||||
fun.(source)
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
<:option>
|
||||
<span x-data="{ copied: false }" x-on:click={~s"
|
||||
copyWithCallbacks(
|
||||
`#{Jason.Formatter.pretty_print(Phoenix.json_library().encode!(@source))}`,
|
||||
String.raw`#{Jason.Formatter.pretty_print(Phoenix.json_library().encode!(@source))}`,
|
||||
() => copied = true,
|
||||
() => copied = false
|
||||
)
|
||||
@@ -38,7 +38,7 @@
|
||||
<.link
|
||||
href={~p"/sources/#{@source}/force_download_pending"}
|
||||
method="post"
|
||||
data-confirm="Are you sure you want to force a download of all *pending* media items? This isn't normally needed."
|
||||
data-confirm="Are you sure you want to force a download of all pending media items? This isn't normally needed."
|
||||
>
|
||||
Download Pending
|
||||
</.link>
|
||||
@@ -47,7 +47,7 @@
|
||||
<.link
|
||||
href={~p"/sources/#{@source}/force_redownload"}
|
||||
method="post"
|
||||
data-confirm="Are you sure you want to re-download all currently downloaded media items? This isn't normally needed and won't change anything if the files already exist."
|
||||
data-confirm="Are you sure you want to re-download all currently downloaded media items? This doesn't upgrade your media, but will download any missing files if your settings have changed. This isn't normally needed."
|
||||
>
|
||||
Redownload Existing
|
||||
</.link>
|
||||
@@ -70,6 +70,15 @@
|
||||
Refresh Metadata
|
||||
</.link>
|
||||
</:option>
|
||||
<:option>
|
||||
<.link
|
||||
href={~p"/sources/#{@source}/sync_files_on_disk"}
|
||||
method="post"
|
||||
data-confirm="Are you sure you want to sync files? This will update media items if their files have been deleted. File addition or moves are not detected. This isn't normally needed."
|
||||
>
|
||||
Sync Files on Disk
|
||||
</.link>
|
||||
</:option>
|
||||
<:option>
|
||||
<div class="h-px w-full bg-bodydark2"></div>
|
||||
</:option>
|
||||
|
||||
@@ -67,7 +67,7 @@
|
||||
<%= task.job.state %>
|
||||
</:col>
|
||||
<:col :let={task} label="Scheduled At">
|
||||
<%= Calendar.strftime(task.job.scheduled_at, "%y-%m-%d %I:%M:%S %p %Z") %>
|
||||
<.datetime_in_zone datetime={task.job.scheduled_at} />
|
||||
</:col>
|
||||
</.table>
|
||||
<% else %>
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
|
||||
<section x-data="{ mediaProfileId: null }">
|
||||
<section class="flex justify-between items-center mt-4">
|
||||
<h3 class=" text-2xl text-black dark:text-white">
|
||||
<h3 class="text-2xl text-white">
|
||||
General Options
|
||||
</h3>
|
||||
<span class="cursor-pointer hover:underline" x-on:click="advancedMode = !advancedMode">
|
||||
|
||||
@@ -40,6 +40,7 @@ defmodule PinchflatWeb.Router do
|
||||
post "/force_redownload", Sources.SourceController, :force_redownload
|
||||
post "/force_index", Sources.SourceController, :force_index
|
||||
post "/force_metadata_refresh", Sources.SourceController, :force_metadata_refresh
|
||||
post "/sync_files_on_disk", Sources.SourceController, :sync_files_on_disk
|
||||
|
||||
resources "/media", MediaItems.MediaItemController, only: [:show, :edit, :update, :delete] do
|
||||
post "/force_download", MediaItems.MediaItemController, :force_download
|
||||
|
||||
@@ -4,7 +4,7 @@ defmodule Pinchflat.MixProject do
|
||||
def project do
|
||||
[
|
||||
app: :pinchflat,
|
||||
version: "2024.8.28",
|
||||
version: "2024.10.25",
|
||||
elixir: "~> 1.17",
|
||||
elixirc_paths: elixirc_paths(Mix.env()),
|
||||
start_permanent: Mix.env() == :prod,
|
||||
@@ -46,10 +46,10 @@ defmodule Pinchflat.MixProject do
|
||||
# Type `mix help deps` for examples and options.
|
||||
defp deps do
|
||||
[
|
||||
{:phoenix, "~> 1.7.10"},
|
||||
{:phoenix, "~> 1.7.14"},
|
||||
{:phoenix_ecto, "~> 4.4"},
|
||||
{:ecto, "~> 3.11.2"},
|
||||
{:ecto_sql, "~> 3.10"},
|
||||
{:ecto, "~> 3.12.3"},
|
||||
{:ecto_sql, "~> 3.12"},
|
||||
{:ecto_sqlite3, ">= 0.0.0"},
|
||||
{:ecto_sqlite3_extras, "~> 1.2.0"},
|
||||
{:phoenix_html, "~> 3.3"},
|
||||
@@ -67,7 +67,7 @@ defmodule Pinchflat.MixProject do
|
||||
{:jason, "~> 1.2"},
|
||||
{:dns_cluster, "~> 0.1.1"},
|
||||
{:plug_cowboy, "~> 2.5"},
|
||||
{:oban, "~> 2.16"},
|
||||
{:oban, "~> 2.17"},
|
||||
{:nimble_parsec, "~> 1.4"},
|
||||
{:timex, "~> 3.0"},
|
||||
{:mox, "~> 1.0", only: :test},
|
||||
|
||||
@@ -1,26 +1,26 @@
|
||||
%{
|
||||
"bunt": {:hex, :bunt, "1.0.0", "081c2c665f086849e6d57900292b3a161727ab40431219529f13c4ddcf3e7a44", [:mix], [], "hexpm", "dc5f86aa08a5f6fa6b8096f0735c4e76d54ae5c9fa2c143e5a1fc7c1cd9bb6b5"},
|
||||
"castore": {:hex, :castore, "1.0.5", "9eeebb394cc9a0f3ae56b813459f990abb0a3dedee1be6b27fdb50301930502f", [:mix], [], "hexpm", "8d7c597c3e4a64c395980882d4bca3cebb8d74197c590dc272cfd3b6a6310578"},
|
||||
"cc_precompiler": {:hex, :cc_precompiler, "0.1.9", "e8d3364f310da6ce6463c3dd20cf90ae7bbecbf6c5203b98bf9b48035592649b", [:mix], [{:elixir_make, "~> 0.7", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "9dcab3d0f3038621f1601f13539e7a9ee99843862e66ad62827b0c42b2f58a54"},
|
||||
"castore": {:hex, :castore, "1.0.8", "dedcf20ea746694647f883590b82d9e96014057aff1d44d03ec90f36a5c0dc6e", [:mix], [], "hexpm", "0b2b66d2ee742cb1d9cb8c8be3b43c3a70ee8651f37b75a8b982e036752983f1"},
|
||||
"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"},
|
||||
"combine": {:hex, :combine, "0.10.0", "eff8224eeb56498a2af13011d142c5e7997a80c8f5b97c499f84c841032e429f", [:mix], [], "hexpm", "1b1dbc1790073076580d0d1d64e42eae2366583e7aecd455d1215b0d16f2451b"},
|
||||
"cowboy": {:hex, :cowboy, "2.10.0", "ff9ffeff91dae4ae270dd975642997afe2a1179d94b1887863e43f681a203e26", [:make, :rebar3], [{:cowlib, "2.12.1", [hex: :cowlib, repo: "hexpm", optional: false]}, {:ranch, "1.8.0", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm", "3afdccb7183cc6f143cb14d3cf51fa00e53db9ec80cdcd525482f5e99bc41d6b"},
|
||||
"cowboy": {:hex, :cowboy, "2.12.0", "f276d521a1ff88b2b9b4c54d0e753da6c66dd7be6c9fca3d9418b561828a3731", [:make, :rebar3], [{:cowlib, "2.13.0", [hex: :cowlib, repo: "hexpm", optional: false]}, {:ranch, "1.8.0", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm", "8a7abe6d183372ceb21caa2709bec928ab2b72e18a3911aa1771639bef82651e"},
|
||||
"cowboy_telemetry": {:hex, :cowboy_telemetry, "0.4.0", "f239f68b588efa7707abce16a84d0d2acf3a0f50571f8bb7f56a15865aae820c", [:rebar3], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "7d98bac1ee4565d31b62d59f8823dfd8356a169e7fcbb83831b8a5397404c9de"},
|
||||
"cowlib": {:hex, :cowlib, "2.12.1", "a9fa9a625f1d2025fe6b462cb865881329b5caff8f1854d1cbc9f9533f00e1e1", [:make, :rebar3], [], "hexpm", "163b73f6367a7341b33c794c4e88e7dbfe6498ac42dcd69ef44c5bc5507c8db0"},
|
||||
"cowlib": {:hex, :cowlib, "2.13.0", "db8f7505d8332d98ef50a3ef34b34c1afddec7506e4ee4dd4a3a266285d282ca", [:make, :rebar3], [], "hexpm", "e1e1284dc3fc030a64b1ad0d8382ae7e99da46c3246b815318a4b848873800a4"},
|
||||
"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"},
|
||||
"db_connection": {:hex, :db_connection, "2.6.0", "77d835c472b5b67fc4f29556dee74bf511bbafecdcaf98c27d27fa5918152086", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "c2f992d15725e721ec7fbc1189d4ecdb8afef76648c746a8e1cad35e3b8a35f3"},
|
||||
"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"},
|
||||
"dns_cluster": {:hex, :dns_cluster, "0.1.2", "3eb5be824c7888dadf9781018e1a5f1d3d1113b333c50bce90fb1b83df1015f2", [:mix], [], "hexpm", "7494272040f847637bbdb01bcdf4b871e82daf09b813e7d3cb3b84f112c6f2f8"},
|
||||
"ecto": {:hex, :ecto, "3.11.2", "e1d26be989db350a633667c5cda9c3d115ae779b66da567c68c80cfb26a8c9ee", [: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", "3c38bca2c6f8d8023f2145326cc8a80100c3ffe4dcbd9842ff867f7fc6156c65"},
|
||||
"ecto_sql": {:hex, :ecto_sql, "3.11.1", "e9abf28ae27ef3916b43545f9578b4750956ccea444853606472089e7d169470", [:mix], [{:db_connection, "~> 2.4.1 or ~> 2.5", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.11.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:myxql, "~> 0.6.0", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.16.0 or ~> 0.17.0 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", "ce14063ab3514424276e7e360108ad6c2308f6d88164a076aac8a387e1fea634"},
|
||||
"ecto_sqlite3": {:hex, :ecto_sqlite3, "0.15.1", "40f2fbd9e246455f8c42e7e0a77009ef806caa1b3ce6f717b2a0a80e8432fcfd", [:mix], [{:decimal, "~> 1.6 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:ecto, "~> 3.11", [hex: :ecto, repo: "hexpm", optional: false]}, {:ecto_sql, "~> 3.11", [hex: :ecto_sql, repo: "hexpm", optional: false]}, {:exqlite, "~> 0.19", [hex: :exqlite, repo: "hexpm", optional: false]}], "hexpm", "28b16e177123c688948357176662bf9ff9084daddf950ef5b6baf3ee93707064"},
|
||||
"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_sqlite3": {:hex, :ecto_sqlite3, "0.17.2", "200226e057f76c40be55fbac77771eb1a233260ab8ec7283f5da6d9402bde8de", [:mix], [{:decimal, "~> 1.6 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:ecto, "~> 3.12", [hex: :ecto, repo: "hexpm", optional: false]}, {:ecto_sql, "~> 3.12", [hex: :ecto_sql, repo: "hexpm", optional: false]}, {:exqlite, "~> 0.22", [hex: :exqlite, repo: "hexpm", optional: false]}], "hexpm", "a3838919c5a34c268c28cafab87b910bcda354a9a4e778658da46c149bb2c1da"},
|
||||
"ecto_sqlite3_extras": {:hex, :ecto_sqlite3_extras, "1.2.2", "36e60b561a11441d15f26c791817999269fb578b985162207ebb08b04ca71e40", [:mix], [{:exqlite, ">= 0.13.2", [hex: :exqlite, repo: "hexpm", optional: false]}, {:table_rex, "~> 4.0", [hex: :table_rex, repo: "hexpm", optional: false]}], "hexpm", "2b66ba7246bb4f7e39e2578acd4a0e4e4be54f60ff52d450a01be95eeb78ff1e"},
|
||||
"elixir_make": {:hex, :elixir_make, "0.7.8", "505026f266552ee5aabca0b9f9c229cbb496c689537c9f922f3eb5431157efc7", [:mix], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:certifi, "~> 2.0", [hex: :certifi, repo: "hexpm", optional: true]}], "hexpm", "7a71945b913d37ea89b06966e1342c85cfe549b15e6d6d081e8081c493062c07"},
|
||||
"elixir_make": {:hex, :elixir_make, "0.8.4", "4960a03ce79081dee8fe119d80ad372c4e7badb84c493cc75983f9d3bc8bde0f", [:mix], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:certifi, "~> 2.0", [hex: :certifi, repo: "hexpm", optional: true]}], "hexpm", "6e7f1d619b5f61dfabd0a20aa268e575572b542ac31723293a4c1a567d5ef040"},
|
||||
"esbuild": {:hex, :esbuild, "0.8.1", "0cbf919f0eccb136d2eeef0df49c4acf55336de864e63594adcea3814f3edf41", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}, {:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "25fc876a67c13cb0a776e7b5d7974851556baeda2085296c14ab48555ea7560f"},
|
||||
"ex_check": {:hex, :ex_check, "0.14.0", "d6fbe0bcc51cf38fea276f5bc2af0c9ae0a2bb059f602f8de88709421dae4f0e", [:mix], [], "hexpm", "8a602e98c66e6a4be3a639321f1f545292042f290f91fa942a285888c6868af0"},
|
||||
"expo": {:hex, :expo, "0.5.1", "249e826a897cac48f591deba863b26c16682b43711dd15ee86b92f25eafd96d9", [:mix], [], "hexpm", "68a4233b0658a3d12ee00d27d37d856b1ba48607e7ce20fd376958d0ba6ce92b"},
|
||||
"exqlite": {:hex, :exqlite, "0.19.0", "0f3ee29e35bed38552dd0ed59600aa81c78f867f5b5ff0e17d330148e0465483", [:make, :mix], [{:cc_precompiler, "~> 0.1", [hex: :cc_precompiler, repo: "hexpm", optional: false]}, {:db_connection, "~> 2.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:elixir_make, "~> 0.7", [hex: :elixir_make, repo: "hexpm", optional: false]}, {:table, "~> 0.1.0", [hex: :table, repo: "hexpm", optional: true]}], "hexpm", "55a8fbb0443f03d4a256e3458bd1203eff5037a6624b76460eaaa9080f462b06"},
|
||||
"exqlite": {:hex, :exqlite, "0.23.0", "6e851c937a033299d0784994c66da24845415072adbc455a337e20087bce9033", [:make, :mix], [{:cc_precompiler, "~> 0.1", [hex: :cc_precompiler, repo: "hexpm", optional: false]}, {:db_connection, "~> 2.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:elixir_make, "~> 0.8", [hex: :elixir_make, repo: "hexpm", optional: false]}, {:table, "~> 0.1.0", [hex: :table, repo: "hexpm", optional: true]}], "hexpm", "404341cceec5e6466aaed160cf0b58be2019b60af82588c215e1224ebd3ec831"},
|
||||
"faker": {:hex, :faker, "0.17.0", "671019d0652f63aefd8723b72167ecdb284baf7d47ad3a82a15e9b8a6df5d1fa", [:mix], [], "hexpm", "a7d4ad84a93fd25c5f5303510753789fc2433ff241bf3b4144d3f6f291658a6a"},
|
||||
"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"},
|
||||
@@ -29,40 +29,40 @@
|
||||
"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"},
|
||||
"idna": {:hex, :idna, "6.1.1", "8a63070e9f7d0c62eb9d9fcb360a7de382448200fbbd1b106cc96d3d8099df8d", [:rebar3], [{:unicode_util_compat, "~> 0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "92376eb7894412ed19ac475e4a86f7b413c1b9fbb5bd16dccd57934157944cea"},
|
||||
"jason": {:hex, :jason, "1.4.1", "af1504e35f629ddcdd6addb3513c3853991f694921b1b9368b0bd32beb9f1b63", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "fbb01ecdfd565b56261302f7e1fcc27c4fb8f32d56eab74db621fc154604a7a1"},
|
||||
"jason": {:hex, :jason, "1.4.4", "b9226785a9aa77b6857ca22832cffa5d5011a667207eb2a0ad56adb5db443b8a", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "c5eb0cab91f094599f94d55bc63409236a8ec69a21a67814529e8d5f6cc90b3b"},
|
||||
"metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], [], "hexpm", "69b09adddc4f74a40716ae54d140f93beb0fb8978d8636eaded0c31b6f099f16"},
|
||||
"mime": {:hex, :mime, "2.0.5", "dc34c8efd439abe6ae0343edbb8556f4d63f178594894720607772a041b04b02", [:mix], [], "hexpm", "da0d64a365c45bc9935cc5c8a7fc5e49a0e0f9932a761c55d6c52b142780a05c"},
|
||||
"mime": {:hex, :mime, "2.0.6", "8f18486773d9b15f95f4f4f1e39b710045fa1de891fada4516559967276e4dc2", [:mix], [], "hexpm", "c9945363a6b26d747389aac3643f8e0e09d30499a138ad64fe8fd1d13d9b153e"},
|
||||
"mimerl": {:hex, :mimerl, "1.3.0", "d0cd9fc04b9061f82490f6581e0128379830e78535e017f7780f37fea7545726", [:rebar3], [], "hexpm", "a1e15a50d1887217de95f0b9b0793e32853f7c258a5cd227650889b38839fe9d"},
|
||||
"mint": {:hex, :mint, "1.5.2", "4805e059f96028948870d23d7783613b7e6b0e2fb4e98d720383852a760067fd", [:mix], [{:castore, "~> 0.1.0 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:hpax, "~> 0.1.1", [hex: :hpax, repo: "hexpm", optional: false]}], "hexpm", "d77d9e9ce4eb35941907f1d3df38d8f750c357865353e21d335bdcdf6d892a02"},
|
||||
"mox": {:hex, :mox, "1.1.0", "0f5e399649ce9ab7602f72e718305c0f9cdc351190f72844599545e4996af73c", [:mix], [], "hexpm", "d44474c50be02d5b72131070281a5d3895c0e7a95c780e90bc0cfe712f633a13"},
|
||||
"nimble_options": {:hex, :nimble_options, "1.1.0", "3b31a57ede9cb1502071fade751ab0c7b8dbe75a9a4c2b5bbb0943a690b63172", [:mix], [], "hexpm", "8bbbb3941af3ca9acc7835f5655ea062111c9c27bcac53e004460dfd19008a99"},
|
||||
"nimble_parsec": {:hex, :nimble_parsec, "1.4.0", "51f9b613ea62cfa97b25ccc2c1b4216e81df970acd8e16e8d1bdc58fef21370d", [:mix], [], "hexpm", "9c565862810fb383e9838c1dd2d7d2c437b3d13b267414ba6af33e50d2d1cf28"},
|
||||
"nimble_pool": {:hex, :nimble_pool, "1.0.0", "5eb82705d138f4dd4423f69ceb19ac667b3b492ae570c9f5c900bb3d2f50a847", [:mix], [], "hexpm", "80be3b882d2d351882256087078e1b1952a28bf98d0a287be87e4a24a710b67a"},
|
||||
"oban": {:hex, :oban, "2.17.3", "ddfd5710aadcd550d2e174c8d73ce5f1865601418cf54a91775f20443fb832b7", [:mix], [{:ecto_sql, "~> 3.6", [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", "452eada8bfe0d0fefd0740ab5fa8cf3ef6c375df0b4a3c3805d179022a04738a"},
|
||||
"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"},
|
||||
"phoenix": {:hex, :phoenix, "1.7.10", "02189140a61b2ce85bb633a9b6fd02dff705a5f1596869547aeb2b2b95edd729", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 2.1", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.6", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:plug_crypto, "~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:websock_adapter, "~> 0.5.3", [hex: :websock_adapter, repo: "hexpm", optional: false]}], "hexpm", "cf784932e010fd736d656d7fead6a584a4498efefe5b8227e9f383bf15bb79d0"},
|
||||
"phoenix": {: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_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.3", "380b8fb45912b5638d2f1d925a3771b4516b9a78587249cabe394e0a5d579dc9", [:mix], [{:plug, "~> 1.5", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "923ebe6fec6e2e3b3e569dfbdc6560de932cd54b000ada0208b5f45024bdd76c"},
|
||||
"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_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_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"},
|
||||
"plug": {:hex, :plug, "1.15.3", "712976f504418f6dff0a3e554c40d705a9bcf89a7ccef92fc6a5ef8f16a30a97", [: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", "cc4365a3c010a56af402e0809208873d113e9c38c401cabd88027ef4f5c01fd2"},
|
||||
"plug_cowboy": {:hex, :plug_cowboy, "2.6.1", "9a3bbfceeb65eff5f39dab529e5cd79137ac36e913c02067dba3963a26efe9b2", [:mix], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:cowboy_telemetry, "~> 0.3", [hex: :cowboy_telemetry, repo: "hexpm", optional: false]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "de36e1a21f451a18b790f37765db198075c25875c64834bcc82d90b309eb6613"},
|
||||
"plug_crypto": {:hex, :plug_crypto, "2.0.0", "77515cc10af06645abbfb5e6ad7a3e9714f805ae118fa1a70205f80d2d70fe73", [:mix], [], "hexpm", "53695bae57cc4e54566d993eb01074e4d894b65a3766f1c43e2c61a1b0f45ea9"},
|
||||
"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_cowboy": {:hex, :plug_cowboy, "2.7.2", "fdadb973799ae691bf9ecad99125b16625b1c6039999da5fe544d99218e662e4", [:mix], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:cowboy_telemetry, "~> 0.3", [hex: :cowboy_telemetry, repo: "hexpm", optional: false]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "245d8a11ee2306094840c000e8816f0cbed69a23fc0ac2bcf8d7835ae019bb2f"},
|
||||
"plug_crypto": {:hex, :plug_crypto, "2.1.0", "f44309c2b06d249c27c8d3f65cfe08158ade08418cf540fd4f72d4d6863abb7b", [:mix], [], "hexpm", "131216a4b030b8f8ce0f26038bc4421ae60e4bb95c5cf5395e1421437824c4fa"},
|
||||
"ranch": {:hex, :ranch, "1.8.0", "8c7a100a139fd57f17327b6413e4167ac559fbc04ca7448e9be9057311597a1d", [:make, :rebar3], [], "hexpm", "49fbcfd3682fab1f5d109351b61257676da1a2fdbe295904176d5e521a2ddfe5"},
|
||||
"sobelow": {:hex, :sobelow, "0.13.0", "218afe9075904793f5c64b8837cc356e493d88fddde126a463839351870b8d1e", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "cd6e9026b85fc35d7529da14f95e85a078d9dd1907a9097b3ba6ac7ebbe34a0d"},
|
||||
"ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.7", "354c321cf377240c7b8716899e182ce4890c5938111a1296add3ec74cf1715df", [:make, :mix, :rebar3], [], "hexpm", "fe4c190e8f37401d30167c8c405eda19469f34577987c76dde613e838bbc67f8"},
|
||||
"swoosh": {:hex, :swoosh, "1.14.4", "94e9dba91f7695a10f49b0172c4a4cb658ef24abef7e8140394521b7f3bbb2d4", [:mix], [{:cowboy, "~> 1.1 or ~> 2.4", [hex: :cowboy, repo: "hexpm", optional: true]}, {:ex_aws, "~> 2.1", [hex: :ex_aws, repo: "hexpm", optional: true]}, {:finch, "~> 0.6", [hex: :finch, repo: "hexpm", optional: true]}, {:gen_smtp, "~> 0.13 or ~> 1.0", [hex: :gen_smtp, repo: "hexpm", optional: true]}, {:hackney, "~> 1.9", [hex: :hackney, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mail, "~> 0.2", [hex: :mail, repo: "hexpm", optional: true]}, {:mime, "~> 1.1 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug, "~> 1.9", [hex: :plug, repo: "hexpm", optional: true]}, {:plug_cowboy, ">= 1.0.0", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:req, "~> 0.4 or ~> 1.0", [hex: :req, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.2 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "081c5a590e4ba85cc89baddf7b2beecf6c13f7f84a958f1cd969290815f0f026"},
|
||||
"table_rex": {:hex, :table_rex, "4.0.0", "3c613a68ebdc6d4d1e731bc973c233500974ec3993c99fcdabb210407b90959b", [:mix], [], "hexpm", "c35c4d5612ca49ebb0344ea10387da4d2afe278387d4019e4d8111e815df8f55"},
|
||||
"tailwind": {:hex, :tailwind, "0.2.2", "9e27288b568ede1d88517e8c61259bc214a12d7eed271e102db4c93fcca9b2cd", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}], "hexpm", "ccfb5025179ea307f7f899d1bb3905cd0ac9f687ed77feebc8f67bdca78565c4"},
|
||||
"telemetry": {:hex, :telemetry, "1.2.1", "68fdfe8d8f05a8428483a97d7aab2f268aaff24b49e0f599faa091f1d4e7f61c", [:rebar3], [], "hexpm", "dad9ce9d8effc621708f99eac538ef1cbe05d6a874dd741de2e689c47feafed5"},
|
||||
"telemetry": {:hex, :telemetry, "1.3.0", "fedebbae410d715cf8e7062c96a1ef32ec22e764197f70cda73d82778d61e7a2", [:rebar3], [], "hexpm", "7015fc8919dbe63764f4b4b87a95b7c0996bd539e0d499be6ec9d7f3875b79e6"},
|
||||
"telemetry_metrics": {:hex, :telemetry_metrics, "0.6.2", "2caabe9344ec17eafe5403304771c3539f3b6e2f7fb6a6f602558c825d0d0bfb", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "9b43db0dc33863930b9ef9d27137e78974756f5f198cae18409970ed6fa5b561"},
|
||||
"telemetry_poller": {:hex, :telemetry_poller, "1.0.0", "db91bb424e07f2bb6e73926fcafbfcbcb295f0193e0a00e825e589a0a47e8453", [:rebar3], [{:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "b3a24eafd66c3f42da30fc3ca7dda1e9d546c12250a2d60d7b81d264fbec4f6e"},
|
||||
"timex": {:hex, :timex, "3.7.11", "bb95cb4eb1d06e27346325de506bcc6c30f9c6dea40d1ebe390b262fad1862d1", [:mix], [{:combine, "~> 0.10", [hex: :combine, repo: "hexpm", optional: false]}, {:gettext, "~> 0.20", [hex: :gettext, repo: "hexpm", optional: false]}, {:tzdata, "~> 1.1", [hex: :tzdata, repo: "hexpm", optional: false]}], "hexpm", "8b9024f7efbabaf9bd7aa04f65cf8dcd7c9818ca5737677c7b76acbc6a94d1aa"},
|
||||
"tzdata": {:hex, :tzdata, "1.1.1", "20c8043476dfda8504952d00adac41c6eda23912278add38edc140ae0c5bcc46", [:mix], [{:hackney, "~> 1.17", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm", "a69cec8352eafcd2e198dea28a34113b60fdc6cb57eb5ad65c10292a6ba89787"},
|
||||
"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"},
|
||||
"websock": {:hex, :websock, "0.5.3", "2f69a6ebe810328555b6fe5c831a851f485e303a7c8ce6c5f675abeb20ebdadc", [:mix], [], "hexpm", "6105453d7fac22c712ad66fab1d45abdf049868f253cf719b625151460b8b453"},
|
||||
"websock_adapter": {:hex, :websock_adapter, "0.5.5", "9dfeee8269b27e958a65b3e235b7e447769f66b5b5925385f5a569269164a210", [: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", "4b977ba4a01918acbf77045ff88de7f6972c2a009213c515a445c48f224ffce9"},
|
||||
"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"},
|
||||
}
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 436 KiB After Width: | Height: | Size: 433 KiB |
@@ -0,0 +1,9 @@
|
||||
defmodule Pinchflat.Repo.Migrations.AddMediaContainerToMediaProfiles do
|
||||
use Ecto.Migration
|
||||
|
||||
def change do
|
||||
alter table(:media_profiles) do
|
||||
add :media_container, :string
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -252,7 +252,7 @@ defmodule Pinchflat.Downloading.DownloadOptionBuilderTest do
|
||||
end
|
||||
end
|
||||
|
||||
describe "build/1 when testing quality options" do
|
||||
describe "build/1 when testing media quality and format options" do
|
||||
test "includes quality options" do
|
||||
resolutions = ["360", "480", "720", "1080", "2160", "4320"]
|
||||
|
||||
@@ -291,6 +291,22 @@ defmodule Pinchflat.Downloading.DownloadOptionBuilderTest do
|
||||
|
||||
assert {:format_sort, "res:1080,+codec:av01:aac"} in res
|
||||
end
|
||||
|
||||
test "includes custom remux target for videos if specified", %{media_item: media_item} do
|
||||
media_item = update_media_profile_attribute(media_item, %{media_container: "mkv"})
|
||||
|
||||
assert {:ok, res} = DownloadOptionBuilder.build(media_item)
|
||||
|
||||
assert {:remux_video, "mkv"} in res
|
||||
end
|
||||
|
||||
test "includes custom format target for audio if specified", %{media_item: media_item} do
|
||||
media_item = update_media_profile_attribute(media_item, %{media_container: "flac", preferred_resolution: :audio})
|
||||
|
||||
assert {:ok, res} = DownloadOptionBuilder.build(media_item)
|
||||
|
||||
assert {:audio_format, "flac"} in res
|
||||
end
|
||||
end
|
||||
|
||||
describe "build/1 when testing sponsorblock options" do
|
||||
|
||||
@@ -236,6 +236,26 @@ defmodule Pinchflat.Downloading.MediaDownloadWorkerTest do
|
||||
|
||||
perform_job(MediaDownloadWorker, %{id: media_item.id, force: true})
|
||||
end
|
||||
|
||||
test "deletes old files if the media item has been updated" do
|
||||
expect(YtDlpRunnerMock, :run, 1, fn _url, _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)}
|
||||
end)
|
||||
|
||||
expect(YtDlpRunnerMock, :run, 1, fn _url, _opts, _ot, _addl -> {:ok, ""} end)
|
||||
|
||||
old_media_item = media_item_with_attachments()
|
||||
perform_job(MediaDownloadWorker, %{id: old_media_item.id, force: true})
|
||||
updated_media_item = Repo.reload(old_media_item)
|
||||
|
||||
assert updated_media_item.media_filepath != old_media_item.media_filepath
|
||||
refute File.exists?(old_media_item.media_filepath)
|
||||
assert File.exists?(updated_media_item.media_filepath)
|
||||
end
|
||||
end
|
||||
|
||||
describe "perform/1 when testing user script callbacks" do
|
||||
|
||||
@@ -46,6 +46,23 @@ defmodule Pinchflat.Downloading.MediaRetentionWorkerTest do
|
||||
refute Repo.reload!(old_media_item).media_filepath
|
||||
end
|
||||
|
||||
test "deletes media files that are on their retention date per the 24-h clock" do
|
||||
{_source, old_media_item, new_media_item} = prepare_records_for_retention_date(2)
|
||||
|
||||
just_over_two_days_ago = now_minus(2, :days) |> DateTime.add(-1, :minute)
|
||||
just_under_two_days_ago = now_minus(2, :days) |> DateTime.add(1, :minute)
|
||||
|
||||
Media.update_media_item(old_media_item, %{media_downloaded_at: just_over_two_days_ago})
|
||||
Media.update_media_item(new_media_item, %{media_downloaded_at: just_under_two_days_ago})
|
||||
|
||||
perform_job(MediaRetentionWorker, %{})
|
||||
|
||||
assert File.exists?(new_media_item.media_filepath)
|
||||
refute File.exists?(old_media_item.media_filepath)
|
||||
assert Repo.reload!(new_media_item).media_filepath
|
||||
refute Repo.reload!(old_media_item).media_filepath
|
||||
end
|
||||
|
||||
test "sets culled_at and prevent_download" do
|
||||
{_source, old_media_item, new_media_item} = prepare_records_for_retention_date()
|
||||
|
||||
@@ -106,6 +123,25 @@ defmodule Pinchflat.Downloading.MediaRetentionWorkerTest do
|
||||
refute Repo.reload!(old_media_item).media_filepath
|
||||
end
|
||||
|
||||
# NOTE: Since this is a date and not a datetime, we can't add logic to have to-the-minute
|
||||
# comparison like we can with retention periods. We can only compare to the day.
|
||||
test "doesn't cull media from on or after the cutoff date" do
|
||||
{_source, old_media_item, new_media_item} = prepare_records_for_source_cutoff_date(2)
|
||||
|
||||
Media.update_media_item(old_media_item, %{uploaded_at: now_minus(2, :days)})
|
||||
Media.update_media_item(new_media_item, %{uploaded_at: now_minus(1, :day)})
|
||||
|
||||
perform_job(MediaRetentionWorker, %{})
|
||||
|
||||
assert File.exists?(new_media_item.media_filepath)
|
||||
assert File.exists?(old_media_item.media_filepath)
|
||||
assert Repo.reload!(new_media_item).media_filepath
|
||||
assert Repo.reload!(old_media_item).media_filepath
|
||||
|
||||
refute Repo.reload!(new_media_item).culled_at
|
||||
refute Repo.reload!(old_media_item).culled_at
|
||||
end
|
||||
|
||||
test "sets culled_at but not prevent_download" do
|
||||
{_source, old_media_item, new_media_item} = prepare_records_for_source_cutoff_date()
|
||||
|
||||
@@ -131,23 +167,6 @@ defmodule Pinchflat.Downloading.MediaRetentionWorkerTest do
|
||||
refute Repo.reload!(old_media_item).culled_at
|
||||
end
|
||||
|
||||
test "doesn't cull media from on or after the cutoff date" do
|
||||
{_source, old_media_item, new_media_item} = prepare_records_for_source_cutoff_date(2)
|
||||
|
||||
Media.update_media_item(old_media_item, %{uploaded_at: now_minus(2, :days)})
|
||||
Media.update_media_item(new_media_item, %{uploaded_at: now_minus(1, :day)})
|
||||
|
||||
perform_job(MediaRetentionWorker, %{})
|
||||
|
||||
assert File.exists?(new_media_item.media_filepath)
|
||||
assert File.exists?(old_media_item.media_filepath)
|
||||
assert Repo.reload!(new_media_item).media_filepath
|
||||
assert Repo.reload!(old_media_item).media_filepath
|
||||
|
||||
refute Repo.reload!(new_media_item).culled_at
|
||||
refute Repo.reload!(old_media_item).culled_at
|
||||
end
|
||||
|
||||
test "doesn't cull media items that have prevent_culling set" do
|
||||
{_source, old_media_item, _new_media_item} = prepare_records_for_source_cutoff_date()
|
||||
|
||||
|
||||
@@ -0,0 +1,122 @@
|
||||
defmodule Pinchflat.Media.FileSyncingTest do
|
||||
use Pinchflat.DataCase
|
||||
|
||||
import Pinchflat.MediaFixtures
|
||||
|
||||
alias Pinchflat.Media.FileSyncing
|
||||
|
||||
describe "delete_outdated_files/2" do
|
||||
test "deletes outdated non-subtitle files" do
|
||||
new_media_item = media_item_with_attachments()
|
||||
old_media_item = media_item_with_attachments()
|
||||
|
||||
assert :ok = FileSyncing.delete_outdated_files(old_media_item, new_media_item)
|
||||
|
||||
assert File.exists?(new_media_item.media_filepath)
|
||||
refute File.exists?(old_media_item.media_filepath)
|
||||
end
|
||||
|
||||
test "doesn't delete non-subtitle files if the new file is the same" do
|
||||
new_media_item = media_item_with_attachments()
|
||||
old_media_item = media_item_fixture(%{media_filepath: new_media_item.media_filepath})
|
||||
|
||||
assert :ok = FileSyncing.delete_outdated_files(old_media_item, new_media_item)
|
||||
|
||||
assert File.exists?(new_media_item.media_filepath)
|
||||
assert File.exists?(old_media_item.media_filepath)
|
||||
end
|
||||
|
||||
test "doesn't delete the old file if the new file is missing that key" do
|
||||
new_media_item = media_item_fixture(%{media_filepath: nil})
|
||||
old_media_item = media_item_with_attachments()
|
||||
|
||||
assert :ok = FileSyncing.delete_outdated_files(old_media_item, new_media_item)
|
||||
|
||||
assert File.exists?(old_media_item.media_filepath)
|
||||
end
|
||||
|
||||
test "deletes outdated subtitle files" do
|
||||
new_media_item = media_item_with_attachments()
|
||||
old_media_item = media_item_with_attachments()
|
||||
|
||||
assert :ok = FileSyncing.delete_outdated_files(old_media_item, new_media_item)
|
||||
|
||||
assert File.exists?(get_subtitle_filepath(new_media_item, "en"))
|
||||
refute File.exists?(get_subtitle_filepath(old_media_item, "en"))
|
||||
end
|
||||
|
||||
test "keeps old subtitle files if the new file is the same" do
|
||||
new_media_item = media_item_with_attachments()
|
||||
old_media_item = media_item_fixture(%{subtitle_filepaths: new_media_item.subtitle_filepaths})
|
||||
|
||||
assert :ok = FileSyncing.delete_outdated_files(old_media_item, new_media_item)
|
||||
|
||||
assert File.exists?(get_subtitle_filepath(new_media_item, "en"))
|
||||
assert File.exists?(get_subtitle_filepath(old_media_item, "en"))
|
||||
end
|
||||
|
||||
test "doesn't delete old subtitle files if the new file is missing that key" do
|
||||
new_media_item = media_item_fixture(%{subtitle_filepaths: []})
|
||||
old_media_item = media_item_with_attachments()
|
||||
|
||||
assert :ok = FileSyncing.delete_outdated_files(old_media_item, new_media_item)
|
||||
|
||||
assert File.exists?(get_subtitle_filepath(old_media_item, "en"))
|
||||
end
|
||||
end
|
||||
|
||||
describe "sync_file_presence_on_disk/1" do
|
||||
test "removes attributes whose files are missing" do
|
||||
media_item = media_item_fixture(%{media_filepath: "/tmp/missing_file.mp4"})
|
||||
|
||||
assert media_item.media_filepath
|
||||
assert [updated_media_item] = FileSyncing.sync_file_presence_on_disk([media_item])
|
||||
refute updated_media_item.media_filepath
|
||||
end
|
||||
|
||||
test "doesn't remove attributes where the files still exist" do
|
||||
media_item = media_item_with_attachments()
|
||||
|
||||
assert media_item.media_filepath
|
||||
assert [updated_media_item] = FileSyncing.sync_file_presence_on_disk([media_item])
|
||||
assert updated_media_item.media_filepath
|
||||
end
|
||||
|
||||
test "doesn't touch other attributes if some are missing and some aren't" do
|
||||
media_item = media_item_with_attachments()
|
||||
File.rm(media_item.media_filepath)
|
||||
|
||||
assert media_item.thumbnail_filepath
|
||||
assert media_item.media_filepath
|
||||
assert [updated_media_item] = FileSyncing.sync_file_presence_on_disk([media_item])
|
||||
assert updated_media_item.thumbnail_filepath
|
||||
refute updated_media_item.media_filepath
|
||||
end
|
||||
|
||||
test "removes subtitle files that are missing" do
|
||||
media_item = media_item_fixture(%{subtitle_filepaths: [["en", "/tmp/missing_file.srt"]]})
|
||||
|
||||
assert get_subtitle_filepath(media_item, "en")
|
||||
assert [updated_media_item] = FileSyncing.sync_file_presence_on_disk([media_item])
|
||||
refute get_subtitle_filepath(updated_media_item, "en")
|
||||
end
|
||||
|
||||
test "doesn't remove subtitle files that still exist" do
|
||||
media_item = media_item_with_attachments()
|
||||
|
||||
assert get_subtitle_filepath(media_item, "en")
|
||||
assert [updated_media_item] = FileSyncing.sync_file_presence_on_disk([media_item])
|
||||
assert get_subtitle_filepath(updated_media_item, "en")
|
||||
end
|
||||
end
|
||||
|
||||
defp get_subtitle_filepath(media_item, language) do
|
||||
Enum.reduce_while(media_item.subtitle_filepaths, nil, fn [lang, filepath], acc ->
|
||||
if lang == language do
|
||||
{:halt, filepath}
|
||||
else
|
||||
{:cont, acc}
|
||||
end
|
||||
end)
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,37 @@
|
||||
defmodule Pinchflat.Media.FileSyncingWorkerTest do
|
||||
use Pinchflat.DataCase
|
||||
|
||||
import Pinchflat.MediaFixtures
|
||||
import Pinchflat.SourcesFixtures
|
||||
|
||||
alias Pinchflat.Media.FileSyncingWorker
|
||||
|
||||
describe "kickoff_with_task/3" do
|
||||
test "starts the worker" do
|
||||
source = source_fixture()
|
||||
|
||||
assert [] = all_enqueued(worker: FileSyncingWorker)
|
||||
assert {:ok, _} = FileSyncingWorker.kickoff_with_task(source)
|
||||
assert [_] = all_enqueued(worker: FileSyncingWorker)
|
||||
end
|
||||
|
||||
test "attaches a task" do
|
||||
source = source_fixture()
|
||||
|
||||
assert {:ok, task} = FileSyncingWorker.kickoff_with_task(source)
|
||||
assert task.source_id == source.id
|
||||
end
|
||||
end
|
||||
|
||||
describe "perform/1" do
|
||||
test "syncs file presence on disk" do
|
||||
source = source_fixture()
|
||||
media_item = media_item_fixture(%{media_filepath: "/tmp/missing.mp4", source_id: source.id})
|
||||
|
||||
perform_job(FileSyncingWorker, %{"id" => source.id})
|
||||
updated_media_item = Repo.reload!(media_item)
|
||||
|
||||
refute updated_media_item.media_filepath
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -441,6 +441,13 @@ defmodule Pinchflat.MediaTest do
|
||||
assert Media.pending_download?(media_item)
|
||||
end
|
||||
|
||||
test "returns true if the cutoff date is equal to the upload date" do
|
||||
source = source_fixture(%{download_cutoff_date: now_minus(2, :days)})
|
||||
media_item = media_item_fixture(%{source_id: source.id, media_filepath: nil, uploaded_at: now_minus(2, :days)})
|
||||
|
||||
assert Media.pending_download?(media_item)
|
||||
end
|
||||
|
||||
test "returns false if there is a cutoff date after the media's upload date" do
|
||||
source = source_fixture(%{download_cutoff_date: now_minus(1, :day)})
|
||||
media_item = media_item_fixture(%{source_id: source.id, media_filepath: nil, uploaded_at: now_minus(2, :days)})
|
||||
|
||||
@@ -41,6 +41,17 @@ defmodule Pinchflat.SlowIndexing.SlowIndexingHelpersTest do
|
||||
assert_raise Ecto.NoResultsError, fn -> Repo.reload!(task) end
|
||||
end
|
||||
|
||||
test "deletes any executing media collection tasks for the source" do
|
||||
source = source_fixture()
|
||||
{:ok, job} = Oban.insert(MediaCollectionIndexingWorker.new(%{"id" => source.id}))
|
||||
task = task_fixture(source_id: source.id, job_id: job.id)
|
||||
Repo.update_all(from(Oban.Job, where: [id: ^task.job_id], update: [set: [state: "executing"]]), [])
|
||||
|
||||
assert {:ok, _} = SlowIndexingHelpers.kickoff_indexing_task(source)
|
||||
|
||||
assert_raise Ecto.NoResultsError, fn -> Repo.reload!(task) end
|
||||
end
|
||||
|
||||
test "deletes any pending media tasks for the source" do
|
||||
source = source_fixture()
|
||||
{:ok, job} = Oban.insert(FastIndexingWorker.new(%{"id" => source.id}))
|
||||
|
||||
@@ -147,6 +147,18 @@ defmodule Pinchflat.SourcesTest do
|
||||
assert "could not fetch source details from URL" in errors_on(changeset).original_url
|
||||
end
|
||||
|
||||
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)
|
||||
|
||||
valid_attrs = %{
|
||||
media_profile_id: media_profile_fixture().id,
|
||||
original_url: "https://www.youtube.com/channel/abc123"
|
||||
}
|
||||
|
||||
assert {:error, %Ecto.Changeset{} = changeset} = Sources.create_source(valid_attrs)
|
||||
assert "could not fetch source details from URL" in errors_on(changeset).original_url
|
||||
end
|
||||
|
||||
test "you can specify a custom custom_name" do
|
||||
expect(YtDlpRunnerMock, :run, &channel_mock/4)
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ defmodule Pinchflat.TasksTest do
|
||||
@invalid_attrs %{job_id: nil}
|
||||
|
||||
describe "schema" do
|
||||
test "it deletes a task when the job gets deleted" do
|
||||
test "deletes a task when the job gets deleted" do
|
||||
task = Repo.preload(task_fixture(), [:job])
|
||||
|
||||
{:ok, _} = Repo.delete(task.job)
|
||||
@@ -20,7 +20,7 @@ defmodule Pinchflat.TasksTest do
|
||||
assert_raise Ecto.NoResultsError, fn -> Repo.reload!(task) end
|
||||
end
|
||||
|
||||
test "it does not delete the other record when a job gets deleted" do
|
||||
test "does not delete the other record when a job gets deleted" do
|
||||
task = Repo.preload(task_fixture(), [:source, :job])
|
||||
|
||||
{:ok, _} = Repo.delete(task.job)
|
||||
@@ -30,21 +30,21 @@ defmodule Pinchflat.TasksTest do
|
||||
end
|
||||
|
||||
describe "list_tasks/0" do
|
||||
test "it returns all tasks" do
|
||||
test "returns all tasks" do
|
||||
task = task_fixture()
|
||||
assert Tasks.list_tasks() == [task]
|
||||
end
|
||||
end
|
||||
|
||||
describe "list_tasks_for/3" do
|
||||
test "it lets you specify which record type/ID to join on" do
|
||||
test "lets you specify which record type/ID to join on" do
|
||||
source = source_fixture()
|
||||
task = task_fixture(source_id: source.id)
|
||||
|
||||
assert Tasks.list_tasks_for(source, nil, [:available]) == [task]
|
||||
end
|
||||
|
||||
test "it lets you specify which job states to include" do
|
||||
test "lets you specify which job states to include" do
|
||||
source = source_fixture()
|
||||
task = task_fixture(source_id: source.id)
|
||||
|
||||
@@ -52,7 +52,7 @@ defmodule Pinchflat.TasksTest do
|
||||
assert Tasks.list_tasks_for(source, nil, [:cancelled]) == []
|
||||
end
|
||||
|
||||
test "it lets you specify which worker to include" do
|
||||
test "lets you specify which worker to include" do
|
||||
source = source_fixture()
|
||||
task = task_fixture(source_id: source.id)
|
||||
|
||||
@@ -60,7 +60,7 @@ defmodule Pinchflat.TasksTest do
|
||||
assert Tasks.list_tasks_for(source, "FooBarWorker") == []
|
||||
end
|
||||
|
||||
test "it includes all workers if no worker is specified" do
|
||||
test "includes all workers if no worker is specified" do
|
||||
source = source_fixture()
|
||||
task = task_fixture(source_id: source.id)
|
||||
|
||||
@@ -68,32 +68,8 @@ defmodule Pinchflat.TasksTest do
|
||||
end
|
||||
end
|
||||
|
||||
describe "list_pending_tasks_for/3" do
|
||||
test "it lists pending tasks" do
|
||||
source = source_fixture()
|
||||
task = task_fixture(source_id: source.id)
|
||||
|
||||
assert Tasks.list_pending_tasks_for(source) == [task]
|
||||
end
|
||||
|
||||
test "it does not list non-pending tasks" do
|
||||
task = Repo.preload(task_fixture(), [:job, :source])
|
||||
:ok = Oban.cancel_job(task.job)
|
||||
|
||||
assert Tasks.list_pending_tasks_for(task.source) == []
|
||||
end
|
||||
|
||||
test "it lets you specify which worker to include" do
|
||||
source = source_fixture()
|
||||
task = task_fixture(source_id: source.id)
|
||||
|
||||
assert Tasks.list_pending_tasks_for(source, "TestJobWorker") == [task]
|
||||
assert Tasks.list_pending_tasks_for(source, "FooBarWorker") == []
|
||||
end
|
||||
end
|
||||
|
||||
describe "get_task!/1" do
|
||||
test "it returns the task with given id" do
|
||||
test "returns the task with given id" do
|
||||
task = task_fixture()
|
||||
assert Tasks.get_task!(task.id) == task
|
||||
end
|
||||
@@ -132,7 +108,7 @@ defmodule Pinchflat.TasksTest do
|
||||
end
|
||||
|
||||
describe "create_job_with_task/2" do
|
||||
test "it enqueues the given job" do
|
||||
test "enqueues the given job" do
|
||||
media_item = media_item_fixture()
|
||||
|
||||
refute_enqueued(worker: TestJobWorker)
|
||||
@@ -140,7 +116,7 @@ defmodule Pinchflat.TasksTest do
|
||||
assert_enqueued(worker: TestJobWorker)
|
||||
end
|
||||
|
||||
test "it creates a task record if successful" do
|
||||
test "creates a task record if successful" do
|
||||
source = source_fixture()
|
||||
|
||||
assert {:ok, %Task{} = task} = Tasks.create_job_with_task(TestJobWorker.new(%{}), source)
|
||||
@@ -148,7 +124,7 @@ defmodule Pinchflat.TasksTest do
|
||||
assert task.source_id == source.id
|
||||
end
|
||||
|
||||
test "it returns an error if the job already exists" do
|
||||
test "returns an error if the job already exists" do
|
||||
source = source_fixture()
|
||||
job = TestJobWorker.new(%{foo: "bar"}, unique: [period: :infinity])
|
||||
|
||||
@@ -156,7 +132,7 @@ defmodule Pinchflat.TasksTest do
|
||||
assert {:error, :duplicate_job} = Tasks.create_job_with_task(job, source)
|
||||
end
|
||||
|
||||
test "it returns an error if the job fails to enqueue" do
|
||||
test "returns an error if the job fails to enqueue" do
|
||||
source = source_fixture()
|
||||
|
||||
assert {:error, %Ecto.Changeset{}} = Tasks.create_job_with_task(%Ecto.Changeset{}, source)
|
||||
@@ -181,7 +157,7 @@ defmodule Pinchflat.TasksTest do
|
||||
end
|
||||
|
||||
describe "delete_tasks_for/2" do
|
||||
test "it deletes tasks attached to a source" do
|
||||
test "deletes tasks attached to a source" do
|
||||
source = source_fixture()
|
||||
task = task_fixture(source_id: source.id)
|
||||
|
||||
@@ -189,7 +165,7 @@ defmodule Pinchflat.TasksTest do
|
||||
assert_raise Ecto.NoResultsError, fn -> Tasks.get_task!(task.id) end
|
||||
end
|
||||
|
||||
test "it deletes the tasks attached to a media_item" do
|
||||
test "deletes the tasks attached to a media_item" do
|
||||
media_item = media_item_fixture()
|
||||
task = task_fixture(media_item_id: media_item.id)
|
||||
|
||||
@@ -208,6 +184,17 @@ defmodule Pinchflat.TasksTest do
|
||||
assert_raise Ecto.NoResultsError, fn -> Repo.reload!(task) end
|
||||
end
|
||||
|
||||
test "deletion can specify which states to include" do
|
||||
source = source_fixture()
|
||||
task = task_fixture(source_id: source.id)
|
||||
|
||||
assert :ok = Tasks.delete_tasks_for(source, nil, [:executing])
|
||||
assert Repo.reload!(task)
|
||||
|
||||
assert :ok = Tasks.delete_tasks_for(source, nil, [:available])
|
||||
assert_raise Ecto.NoResultsError, fn -> Repo.reload!(task) end
|
||||
end
|
||||
|
||||
test "deletion does not impact unintended records" do
|
||||
source = source_fixture()
|
||||
task = task_fixture(source_id: source.id)
|
||||
@@ -221,7 +208,7 @@ defmodule Pinchflat.TasksTest do
|
||||
end
|
||||
|
||||
describe "delete_pending_tasks_for/1" do
|
||||
test "it deletes pending tasks attached to a source" do
|
||||
test "deletes pending tasks attached to a source" do
|
||||
source = source_fixture()
|
||||
task = task_fixture(source_id: source.id)
|
||||
|
||||
@@ -229,7 +216,7 @@ defmodule Pinchflat.TasksTest do
|
||||
assert_raise Ecto.NoResultsError, fn -> Tasks.get_task!(task.id) end
|
||||
end
|
||||
|
||||
test "it does not delete non-pending tasks" do
|
||||
test "does not delete non-pending tasks" do
|
||||
source = source_fixture()
|
||||
task = Repo.preload(task_fixture(source_id: source.id), :job)
|
||||
:ok = Oban.cancel_job(task.job)
|
||||
@@ -238,7 +225,7 @@ defmodule Pinchflat.TasksTest do
|
||||
assert Tasks.get_task!(task.id)
|
||||
end
|
||||
|
||||
test "it works on media_items" do
|
||||
test "works on media_items" do
|
||||
media_item = media_item_fixture()
|
||||
pending_task = task_fixture(media_item_id: media_item.id)
|
||||
cancelled_task = Repo.preload(task_fixture(media_item_id: media_item.id), :job)
|
||||
@@ -259,10 +246,23 @@ defmodule Pinchflat.TasksTest do
|
||||
assert :ok = Tasks.delete_pending_tasks_for(media_item, "TestJobWorker")
|
||||
assert_raise Ecto.NoResultsError, fn -> Repo.reload!(task) end
|
||||
end
|
||||
|
||||
test "deletion can optionall include executing tasks" do
|
||||
source = source_fixture()
|
||||
task = task_fixture(source_id: source.id)
|
||||
|
||||
from(Oban.Job, where: [id: ^task.job_id], update: [set: [state: "executing"]])
|
||||
|> Repo.update_all([])
|
||||
|
||||
assert :ok = Tasks.delete_pending_tasks_for(source, nil, include_executing: false)
|
||||
assert Repo.reload!(task)
|
||||
assert :ok = Tasks.delete_pending_tasks_for(source, nil, include_executing: true)
|
||||
assert_raise Ecto.NoResultsError, fn -> Repo.reload!(task) end
|
||||
end
|
||||
end
|
||||
|
||||
describe "change_task/1" do
|
||||
test "it returns a task changeset" do
|
||||
test "returns a task changeset" do
|
||||
task = task_fixture()
|
||||
assert %Ecto.Changeset{} = Tasks.change_task(task)
|
||||
end
|
||||
|
||||
@@ -37,6 +37,46 @@ defmodule Pinchflat.Utils.FilesystemUtilsTest do
|
||||
end
|
||||
end
|
||||
|
||||
describe "filepaths_reference_same_file?/2" do
|
||||
setup do
|
||||
filepath = FilesystemUtils.generate_metadata_tmpfile(:json)
|
||||
|
||||
on_exit(fn -> File.rm!(filepath) end)
|
||||
|
||||
{:ok, %{filepath: filepath}}
|
||||
end
|
||||
|
||||
test "returns true if the files are the same", %{filepath: filepath} do
|
||||
assert FilesystemUtils.filepaths_reference_same_file?(filepath, filepath)
|
||||
end
|
||||
|
||||
test "returns true if different filepaths point to the same file", %{filepath: filepath} do
|
||||
short_path = Path.expand(filepath)
|
||||
long_path = Path.join(["/tmp", "..", filepath])
|
||||
|
||||
assert short_path != long_path
|
||||
assert FilesystemUtils.filepaths_reference_same_file?(short_path, long_path)
|
||||
end
|
||||
|
||||
test "returns true if the files are symlinked", %{filepath: filepath} do
|
||||
tmpfile_directory = Application.get_env(:pinchflat, :tmpfile_directory)
|
||||
other_filepath = Path.join([tmpfile_directory, "symlink.json"])
|
||||
:ok = File.ln_s!(filepath, other_filepath)
|
||||
|
||||
assert FilesystemUtils.filepaths_reference_same_file?(filepath, other_filepath)
|
||||
|
||||
File.rm!(other_filepath)
|
||||
end
|
||||
|
||||
test "returns false if the files are different", %{filepath: filepath} do
|
||||
other_filepath = FilesystemUtils.generate_metadata_tmpfile(:json)
|
||||
|
||||
refute FilesystemUtils.filepaths_reference_same_file?(filepath, other_filepath)
|
||||
|
||||
File.rm!(other_filepath)
|
||||
end
|
||||
end
|
||||
|
||||
describe "generate_metadata_tmpfile/1" do
|
||||
test "creates a tmpfile and returns its path" do
|
||||
res = FilesystemUtils.generate_metadata_tmpfile(:json)
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
defmodule Pinchflat.Utils.MapUtilsTest do
|
||||
use Pinchflat.DataCase
|
||||
|
||||
alias Pinchflat.Utils.MapUtils
|
||||
|
||||
describe "from_nested_list/1" do
|
||||
test "creates a map from a nested 2-element tuple list" do
|
||||
list = [
|
||||
{"key1", "value1"},
|
||||
{"key2", "value2"}
|
||||
]
|
||||
|
||||
assert MapUtils.from_nested_list(list) == %{
|
||||
"key1" => "value1",
|
||||
"key2" => "value2"
|
||||
}
|
||||
end
|
||||
|
||||
test "creates a map from a nested 2-element list of lists" do
|
||||
list = [
|
||||
["key1", "value1"],
|
||||
["key2", "value2"]
|
||||
]
|
||||
|
||||
assert MapUtils.from_nested_list(list) == %{
|
||||
"key1" => "value1",
|
||||
"key2" => "value2"
|
||||
}
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -133,7 +133,7 @@ defmodule Pinchflat.YtDlp.MediaCollectionTest 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)
|
||||
|
||||
assert {:error, %Jason.DecodeError{}} = MediaCollection.get_source_details(@channel_url)
|
||||
assert {:error, "Error decoding JSON response"} = MediaCollection.get_source_details(@channel_url)
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ defmodule PinchflatWeb.SourceControllerTest do
|
||||
|
||||
alias Pinchflat.Repo
|
||||
alias Pinchflat.Settings
|
||||
alias Pinchflat.Media.FileSyncingWorker
|
||||
alias Pinchflat.Sources.SourceDeletionWorker
|
||||
alias Pinchflat.Downloading.MediaDownloadWorker
|
||||
alias Pinchflat.Metadata.SourceMetadataStorageWorker
|
||||
@@ -268,6 +269,23 @@ defmodule PinchflatWeb.SourceControllerTest do
|
||||
end
|
||||
end
|
||||
|
||||
describe "sync_files_on_disk" do
|
||||
test "forces a file sync", %{conn: conn} do
|
||||
source = source_fixture()
|
||||
|
||||
assert [] = all_enqueued(worker: FileSyncingWorker)
|
||||
post(conn, ~p"/sources/#{source.id}/sync_files_on_disk")
|
||||
assert [_] = all_enqueued(worker: FileSyncingWorker)
|
||||
end
|
||||
|
||||
test "redirects to the source page", %{conn: conn} do
|
||||
source = source_fixture()
|
||||
|
||||
conn = post(conn, ~p"/sources/#{source.id}/sync_files_on_disk")
|
||||
assert redirected_to(conn) == ~p"/sources/#{source.id}"
|
||||
end
|
||||
end
|
||||
|
||||
defp create_source(_) do
|
||||
source = source_fixture()
|
||||
media_item = media_item_with_attachments(%{source_id: source.id})
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
1
|
||||
00:00:00,000 --> 00:00:02,500
|
||||
Welcome to the Example Subtitle File!
|
||||
|
||||
2
|
||||
00:00:03,000 --> 00:00:06,000
|
||||
This is a demonstration of SRT subtitles.
|
||||
|
||||
3
|
||||
00:00:07,000 --> 00:00:10,500
|
||||
You can use SRT files to add subtitles to your videos.
|
||||
|
||||
4
|
||||
00:00:12,000 --> 00:00:15,000
|
||||
Each subtitle entry consists of a number, a timecode,
|
||||
and the subtitle text.
|
||||
|
||||
5
|
||||
00:00:16,000 --> 00:00:20,000
|
||||
The timecode format is hours:minutes:seconds,milliseconds.
|
||||
|
||||
6
|
||||
00:00:21,000 --> 00:00:25,000
|
||||
You can adjust the timing to match your video.
|
||||
|
||||
7
|
||||
00:00:26,000 --> 00:00:30,000
|
||||
Make sure the subtitle text is clear and readable.
|
||||
|
||||
8
|
||||
00:00:31,000 --> 00:00:35,000
|
||||
And that's how you create an SRT subtitle file!
|
||||
|
||||
9
|
||||
00:00:36,000 --> 00:00:40,000
|
||||
Enjoy adding subtitles to your videos!
|
||||
@@ -73,16 +73,19 @@ defmodule Pinchflat.MediaFixtures do
|
||||
"#{:rand.uniform(1_000_000)}"
|
||||
])
|
||||
|
||||
stored_media_filepath = Path.join(base_dir, "#media.mp4")
|
||||
stored_media_filepath = Path.join(base_dir, "media.mp4")
|
||||
thumbnail_filepath = Path.join(base_dir, "thumbnail.jpg")
|
||||
subtitle_filepath = Path.join(base_dir, "subtitle.en.srt")
|
||||
|
||||
FilesystemUtils.cp_p!(media_filepath_fixture(), stored_media_filepath)
|
||||
FilesystemUtils.cp_p!(thumbnail_filepath_fixture(), thumbnail_filepath)
|
||||
FilesystemUtils.cp_p!(subtitle_filepath_fixture(), subtitle_filepath)
|
||||
|
||||
merged_attrs =
|
||||
Map.merge(attrs, %{
|
||||
media_filepath: stored_media_filepath,
|
||||
thumbnail_filepath: thumbnail_filepath
|
||||
thumbnail_filepath: thumbnail_filepath,
|
||||
subtitle_filepaths: [["en", subtitle_filepath]]
|
||||
})
|
||||
|
||||
media_item_fixture(merged_attrs)
|
||||
@@ -124,6 +127,16 @@ defmodule Pinchflat.MediaFixtures do
|
||||
])
|
||||
end
|
||||
|
||||
def subtitle_filepath_fixture do
|
||||
Path.join([
|
||||
File.cwd!(),
|
||||
"test",
|
||||
"support",
|
||||
"files",
|
||||
"subtitle.srt"
|
||||
])
|
||||
end
|
||||
|
||||
def infojson_filepath_fixture do
|
||||
Path.join([
|
||||
File.cwd!(),
|
||||
|
||||
Reference in New Issue
Block a user