Compare commits

...

12 Commits

Author SHA1 Message Date
Kieran Eglin 85feb67299 Bumped version 2024-10-02 14:44:47 -07:00
Kieran 4843d7d6c2 Updated tzinfo package (#402) 2024-10-02 14:38:14 -07:00
Kieran a0b9e49486 [Enhancement] Added ability to detect when files have been deleted (#399)
* Added function for updating a media item's filepaths on-disk

* Added placeholder action to source page

* Turned the file sync into a job and properly hooked it up to the controller
2024-09-26 12:39:31 -07:00
Kieran 0163e85e76 [Bugfix] prevent duplicate videos from being downloaded if the video's name changes (#396)
* Added methods for deleting outdated files

* Hooked up outdated file deletion to media download worker
2024-09-23 15:59:48 -07:00
Kieran Eglin 7fb6fa3af4 Bumped version 2024-09-12 14:39:07 -07:00
Kieran bb8788f03d [Dev] Version updates 2024-09-12 (#388)
* Updated some deps

* Updated phoenix
2024-09-12 13:04:49 -07:00
Kieran ed4434512b [Dev] Speed up CI (#385)
* Moved around copy steps

* no-op change

* Reverted the no-op change

* Testing if docker-compose volume is needed

* Nope, it's needed

* Testing alternate driver

* Add containerd

* Bump version

* Revert
2024-09-12 12:38:12 -07:00
Kieran f6344d43d2 [Bugfix] Disallow concurrent slow-indexing runs for the same source (#384)
* Updated deps in case that does anything

* Kills running slow-indexes when a new slow-index is enqueued

* Revert deps upgrade (saving that for its own PR)
2024-09-10 14:16:10 -07:00
Kieran e0745bdfbe [Enhancement] Allow configuration of media container format (#383)
* Adds media container to media_profiles and updates option builder

* Added media container to media profile form
2024-09-10 11:44:57 -07:00
Kieran Eglin a0a02cb4be Updated docker-build-push version in docker release file as well 2024-09-10 11:42:10 -07:00
Kieran Eglin 7866f7258a Updated docker-build-push version 2024-09-10 11:36:54 -07:00
Kieran 5cd82f9dd5 Updated the aspect ratio threshold for short detection (#382) 2024-09-10 08:49:30 -07:00
33 changed files with 663 additions and 109 deletions
+1 -1
View File
@@ -72,7 +72,7 @@ jobs:
password: ${{ secrets.GITHUB_TOKEN }} password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and Push - name: Build and Push
uses: docker/build-push-action@v5 uses: docker/build-push-action@v6
with: with:
context: . context: .
file: ./docker/selfhosted.Dockerfile file: ./docker/selfhosted.Dockerfile
+1 -1
View File
@@ -29,7 +29,7 @@ jobs:
uses: docker/setup-buildx-action@v3 uses: docker/setup-buildx-action@v3
- name: Build docker image - name: Build docker image
uses: docker/build-push-action@v5 uses: docker/build-push-action@v6
with: with:
context: . context: .
file: ./docker/dev.Dockerfile file: ./docker/dev.Dockerfile
+5 -3
View File
@@ -49,14 +49,16 @@ ENV LANG=en_US.UTF-8
ENV LANGUAGE=en_US:en ENV LANGUAGE=en_US:en
ENV LC_ALL=en_US.UTF-8 ENV LC_ALL=en_US.UTF-8
# Create app directory and copy the Elixir projects into it.
WORKDIR /app WORKDIR /app
COPY . ./
COPY mix.exs mix.lock ./
# Install Elixir deps # 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=dev mix deps.get && MIX_ENV=dev mix deps.compile
RUN MIX_ENV=test mix deps.get && MIX_ENV=test mix deps.compile RUN MIX_ENV=test mix deps.get && MIX_ENV=test mix deps.compile
COPY . ./
# Gives us iex shell history # Gives us iex shell history
ENV ERL_AFLAGS="-kernel shell_history enabled" ENV ERL_AFLAGS="-kernel shell_history enabled"
@@ -127,11 +127,12 @@ defmodule Pinchflat.Downloading.DownloadOptionBuilder do
defp quality_options(media_profile) do defp quality_options(media_profile) do
vcodec = Settings.get!(:video_codec_preference) vcodec = Settings.get!(:video_codec_preference)
acodec = Settings.get!(:audio_codec_preference) acodec = Settings.get!(:audio_codec_preference)
container = media_profile.media_container
case media_profile.preferred_resolution do case media_profile.preferred_resolution do
# Also be aware that :audio disabled all embedding options for subtitles # Also be aware that :audio disabled all embedding options for subtitles
:audio -> :audio ->
[:extract_audio, format_sort: "+acodec:#{acodec}"] [:extract_audio, format_sort: "+acodec:#{acodec}", audio_format: container || "best"]
resolution_atom -> resolution_atom ->
{resolution_string, _} = {resolution_string, _} =
@@ -141,7 +142,7 @@ defmodule Pinchflat.Downloading.DownloadOptionBuilder do
[ [
# Since Plex doesn't support reading metadata from MKV # Since Plex doesn't support reading metadata from MKV
remux_video: "mp4", remux_video: container || "mp4",
format_sort: "res:#{resolution_string},+codec:#{vcodec}:#{acodec}" format_sort: "res:#{resolution_string},+codec:#{vcodec}:#{acodec}"
] ]
end end
@@ -12,6 +12,7 @@ defmodule Pinchflat.Downloading.MediaDownloadWorker do
alias Pinchflat.Tasks alias Pinchflat.Tasks
alias Pinchflat.Repo alias Pinchflat.Repo
alias Pinchflat.Media alias Pinchflat.Media
alias Pinchflat.Media.FileSyncing
alias Pinchflat.Downloading.MediaDownloader alias Pinchflat.Downloading.MediaDownloader
alias Pinchflat.Lifecycle.UserScripts.CommandRunner, as: UserScriptRunner 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) 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) run_user_script(:media_downloaded, updated_media_item)
:ok :ok
+93
View File
@@ -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
+2
View File
@@ -27,6 +27,7 @@ defmodule Pinchflat.Profiles.MediaProfile do
shorts_behaviour shorts_behaviour
livestream_behaviour livestream_behaviour
preferred_resolution preferred_resolution
media_container
redownload_delay_days redownload_delay_days
marked_for_deletion_at marked_for_deletion_at
)a )a
@@ -65,6 +66,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 :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 1080p 720p 480p 360p audio)a, default: :"1080p"
field :media_container, :string, default: nil
field :marked_for_deletion_at, :utc_datetime 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 def kickoff_indexing_task(%Source{} = source, job_args \\ %{}, job_opts \\ []) do
Tasks.delete_pending_tasks_for(source, "FastIndexingWorker") 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) MediaCollectionIndexingWorker.kickoff_with_task(source, job_args, job_opts)
end end
+9 -21
View File
@@ -53,20 +53,6 @@ defmodule Pinchflat.Tasks do
) )
end 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 """ @doc """
Gets a single task. Gets a single task.
@@ -127,13 +113,13 @@ defmodule Pinchflat.Tasks do
@doc """ @doc """
Deletes all tasks attached to a given record, cancelling any attached jobs. 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 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 record
|> list_tasks_for(worker_name) |> list_tasks_for(worker_name, job_states)
|> Enum.each(&delete_task/1) |> Enum.each(&delete_task/1)
end end
@@ -143,10 +129,12 @@ defmodule Pinchflat.Tasks do
Returns :ok Returns :ok
""" """
def delete_pending_tasks_for(record, worker_name \\ nil) do def delete_pending_tasks_for(record, worker_name \\ nil, opts \\ []) do
record include_executing = Keyword.get(opts, :include_executing, false)
|> list_pending_tasks_for(worker_name) base_job_states = [:available, :scheduled, :retryable]
|> Enum.each(&delete_task/1) job_states = if include_executing, do: base_job_states ++ [:executing], else: base_job_states
delete_tasks_for(record, worker_name, job_states)
end end
@doc """ @doc """
+18
View File
@@ -20,6 +20,24 @@ defmodule Pinchflat.Utils.FilesystemUtils do
end end
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 """ @doc """
Generates a temporary file and returns its path. The file is empty and has the given type. 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. Generates all the directories in the path if they don't exist.
+17
View File
@@ -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
+1 -1
View File
@@ -124,7 +124,7 @@ defmodule Pinchflat.YtDlp.Media do
# #
# These don't fail if duration or aspect_ratio are missing # These don't fail if duration or aspect_ratio are missing
# due to Elixir's comparison semantics # due to Elixir's comparison semantics
response["duration"] <= 60 && response["aspect_ratio"] < 0.8 response["duration"] <= 60 && response["aspect_ratio"] <= 0.85
end end
end end
@@ -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}> <.error :if={@changeset.action}>
Oops, something went wrong! Please check the errors below. Oops, something went wrong! Please check the errors below.
</.error> </.error>
@@ -30,10 +36,14 @@
</.input> </.input>
</section> </section>
<h3 class="mt-8 text-2xl text-black dark:text-white"> <section class="flex justify-between items-center mt-8">
General Options <h3 class="text-2xl text-white">
</h3> 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="{ <section x-data="{
presets: { presets: {
default: 'Default', default: 'Default',
@@ -203,6 +213,16 @@
/> />
</section> </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 } }"> <section x-data="{ presets: { default: null, media_center: 1, audio: null, archiving: 1 } }">
<.input <.input
field={f[:redownload_delay_days]} field={f[:redownload_delay_days]}
@@ -8,6 +8,7 @@ defmodule PinchflatWeb.Sources.SourceController do
alias Pinchflat.Sources.Source alias Pinchflat.Sources.Source
alias Pinchflat.Media.MediaItem alias Pinchflat.Media.MediaItem
alias Pinchflat.Profiles.MediaProfile alias Pinchflat.Profiles.MediaProfile
alias Pinchflat.Media.FileSyncingWorker
alias Pinchflat.Sources.SourceDeletionWorker alias Pinchflat.Sources.SourceDeletionWorker
alias Pinchflat.Downloading.DownloadingHelpers alias Pinchflat.Downloading.DownloadingHelpers
alias Pinchflat.SlowIndexing.SlowIndexingHelpers alias Pinchflat.SlowIndexing.SlowIndexingHelpers
@@ -175,6 +176,15 @@ defmodule PinchflatWeb.Sources.SourceController do
) )
end 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 defp wrap_forced_action(conn, source_id, message, fun) do
source = Sources.get_source!(source_id) source = Sources.get_source!(source_id)
fun.(source) fun.(source)
@@ -38,7 +38,7 @@
<.link <.link
href={~p"/sources/#{@source}/force_download_pending"} href={~p"/sources/#{@source}/force_download_pending"}
method="post" 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 Download Pending
</.link> </.link>
@@ -47,7 +47,7 @@
<.link <.link
href={~p"/sources/#{@source}/force_redownload"} href={~p"/sources/#{@source}/force_redownload"}
method="post" 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 Redownload Existing
</.link> </.link>
@@ -70,6 +70,15 @@
Refresh Metadata Refresh Metadata
</.link> </.link>
</:option> </: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> <:option>
<div class="h-px w-full bg-bodydark2"></div> <div class="h-px w-full bg-bodydark2"></div>
</:option> </:option>
@@ -12,7 +12,7 @@
<section x-data="{ mediaProfileId: null }"> <section x-data="{ mediaProfileId: null }">
<section class="flex justify-between items-center mt-4"> <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 General Options
</h3> </h3>
<span class="cursor-pointer hover:underline" x-on:click="advancedMode = !advancedMode"> <span class="cursor-pointer hover:underline" x-on:click="advancedMode = !advancedMode">
+1
View File
@@ -40,6 +40,7 @@ defmodule PinchflatWeb.Router do
post "/force_redownload", Sources.SourceController, :force_redownload post "/force_redownload", Sources.SourceController, :force_redownload
post "/force_index", Sources.SourceController, :force_index post "/force_index", Sources.SourceController, :force_index
post "/force_metadata_refresh", Sources.SourceController, :force_metadata_refresh 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 resources "/media", MediaItems.MediaItemController, only: [:show, :edit, :update, :delete] do
post "/force_download", MediaItems.MediaItemController, :force_download post "/force_download", MediaItems.MediaItemController, :force_download
+5 -5
View File
@@ -4,7 +4,7 @@ defmodule Pinchflat.MixProject do
def project do def project do
[ [
app: :pinchflat, app: :pinchflat,
version: "2024.8.28", version: "2024.10.2",
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,
@@ -46,10 +46,10 @@ defmodule Pinchflat.MixProject do
# Type `mix help deps` for examples and options. # Type `mix help deps` for examples and options.
defp deps do defp deps do
[ [
{:phoenix, "~> 1.7.10"}, {:phoenix, "~> 1.7.14"},
{:phoenix_ecto, "~> 4.4"}, {:phoenix_ecto, "~> 4.4"},
{:ecto, "~> 3.11.2"}, {:ecto, "~> 3.12.3"},
{:ecto_sql, "~> 3.10"}, {:ecto_sql, "~> 3.12"},
{:ecto_sqlite3, ">= 0.0.0"}, {:ecto_sqlite3, ">= 0.0.0"},
{:ecto_sqlite3_extras, "~> 1.2.0"}, {:ecto_sqlite3_extras, "~> 1.2.0"},
{:phoenix_html, "~> 3.3"}, {:phoenix_html, "~> 3.3"},
@@ -67,7 +67,7 @@ defmodule Pinchflat.MixProject do
{:jason, "~> 1.2"}, {:jason, "~> 1.2"},
{:dns_cluster, "~> 0.1.1"}, {:dns_cluster, "~> 0.1.1"},
{:plug_cowboy, "~> 2.5"}, {:plug_cowboy, "~> 2.5"},
{:oban, "~> 2.16"}, {:oban, "~> 2.17"},
{:nimble_parsec, "~> 1.4"}, {:nimble_parsec, "~> 1.4"},
{:timex, "~> 3.0"}, {:timex, "~> 3.0"},
{:mox, "~> 1.0", only: :test}, {:mox, "~> 1.0", only: :test},
+21 -21
View File
@@ -1,26 +1,26 @@
%{ %{
"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.5", "9eeebb394cc9a0f3ae56b813459f990abb0a3dedee1be6b27fdb50301930502f", [:mix], [], "hexpm", "8d7c597c3e4a64c395980882d4bca3cebb8d74197c590dc272cfd3b6a6310578"}, "castore": {:hex, :castore, "1.0.8", "dedcf20ea746694647f883590b82d9e96014057aff1d44d03ec90f36a5c0dc6e", [:mix], [], "hexpm", "0b2b66d2ee742cb1d9cb8c8be3b43c3a70ee8651f37b75a8b982e036752983f1"},
"cc_precompiler": {:hex, :cc_precompiler, "0.1.9", "e8d3364f310da6ce6463c3dd20cf90ae7bbecbf6c5203b98bf9b48035592649b", [:mix], [{:elixir_make, "~> 0.7", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "9dcab3d0f3038621f1601f13539e7a9ee99843862e66ad62827b0c42b2f58a54"}, "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"},
"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"}, "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": {: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.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"}, "decimal": {:hex, :decimal, "2.1.1", "5611dca5d4b2c3dd497dec8f68751f1f1a54755e8ed2a966c2633cf885973ad6", [:mix], [], "hexpm", "53cfe5f497ed0e7771ae1a475575603d77425099ba5faef9394932b35020ffcc"},
"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.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": {: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.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_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.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_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"}, "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"}, "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"}, "ex_check": {:hex, :ex_check, "0.14.0", "d6fbe0bcc51cf38fea276f5bc2af0c9ae0a2bb059f602f8de88709421dae4f0e", [:mix], [], "hexpm", "8a602e98c66e6a4be3a639321f1f545292042f290f91fa942a285888c6868af0"},
"expo": {:hex, :expo, "0.5.1", "249e826a897cac48f591deba863b26c16682b43711dd15ee86b92f25eafd96d9", [:mix], [], "hexpm", "68a4233b0658a3d12ee00d27d37d856b1ba48607e7ce20fd376958d0ba6ce92b"}, "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"}, "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"},
@@ -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"}, "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"},
"idna": {:hex, :idna, "6.1.1", "8a63070e9f7d0c62eb9d9fcb360a7de382448200fbbd1b106cc96d3d8099df8d", [:rebar3], [{:unicode_util_compat, "~> 0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "92376eb7894412ed19ac475e4a86f7b413c1b9fbb5bd16dccd57934157944cea"}, "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"}, "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"}, "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"}, "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"}, "mox": {:hex, :mox, "1.1.0", "0f5e399649ce9ab7602f72e718305c0f9cdc351190f72844599545e4996af73c", [:mix], [], "hexpm", "d44474c50be02d5b72131070281a5d3895c0e7a95c780e90bc0cfe712f633a13"},
"nimble_options": {:hex, :nimble_options, "1.1.0", "3b31a57ede9cb1502071fade751ab0c7b8dbe75a9a4c2b5bbb0943a690b63172", [:mix], [], "hexpm", "8bbbb3941af3ca9acc7835f5655ea062111c9c27bcac53e004460dfd19008a99"}, "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_parsec": {:hex, :nimble_parsec, "1.4.0", "51f9b613ea62cfa97b25ccc2c1b4216e81df970acd8e16e8d1bdc58fef21370d", [:mix], [], "hexpm", "9c565862810fb383e9838c1dd2d7d2c437b3d13b267414ba6af33e50d2d1cf28"},
"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.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"}, "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_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_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, "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_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.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": {: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.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_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.0.0", "77515cc10af06645abbfb5e6ad7a3e9714f805ae118fa1a70205f80d2d70fe73", [:mix], [], "hexpm", "53695bae57cc4e54566d993eb01074e4d894b65a3766f1c43e2c61a1b0f45ea9"}, "plug_crypto": {:hex, :plug_crypto, "2.1.0", "f44309c2b06d249c27c8d3f65cfe08158ade08418cf540fd4f72d4d6863abb7b", [:mix], [], "hexpm", "131216a4b030b8f8ce0f26038bc4421ae60e4bb95c5cf5395e1421437824c4fa"},
"ranch": {:hex, :ranch, "1.8.0", "8c7a100a139fd57f17327b6413e4167ac559fbc04ca7448e9be9057311597a1d", [:make, :rebar3], [], "hexpm", "49fbcfd3682fab1f5d109351b61257676da1a2fdbe295904176d5e521a2ddfe5"}, "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"}, "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"}, "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"}, "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"}, "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"}, "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_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"}, "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"}, "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"}, "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.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"},
} }
BIN
View File
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
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 test "includes quality options" do
resolutions = ["360", "480", "720", "1080", "2160", "4320"] 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 assert {:format_sort, "res:1080,+codec:av01:aac"} in res
end 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 end
describe "build/1 when testing sponsorblock options" do 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}) perform_job(MediaDownloadWorker, %{id: media_item.id, force: true})
end 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 end
describe "perform/1 when testing user script callbacks" do describe "perform/1 when testing user script callbacks" do
+122
View File
@@ -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
@@ -41,6 +41,17 @@ defmodule Pinchflat.SlowIndexing.SlowIndexingHelpersTest do
assert_raise Ecto.NoResultsError, fn -> Repo.reload!(task) end assert_raise Ecto.NoResultsError, fn -> Repo.reload!(task) end
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 test "deletes any pending media tasks for the source" do
source = source_fixture() source = source_fixture()
{:ok, job} = Oban.insert(FastIndexingWorker.new(%{"id" => source.id})) {:ok, job} = Oban.insert(FastIndexingWorker.new(%{"id" => source.id}))
+42 -42
View File
@@ -12,7 +12,7 @@ defmodule Pinchflat.TasksTest do
@invalid_attrs %{job_id: nil} @invalid_attrs %{job_id: nil}
describe "schema" do 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]) task = Repo.preload(task_fixture(), [:job])
{:ok, _} = Repo.delete(task.job) {:ok, _} = Repo.delete(task.job)
@@ -20,7 +20,7 @@ defmodule Pinchflat.TasksTest do
assert_raise Ecto.NoResultsError, fn -> Repo.reload!(task) end assert_raise Ecto.NoResultsError, fn -> Repo.reload!(task) end
end end
test "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]) task = Repo.preload(task_fixture(), [:source, :job])
{:ok, _} = Repo.delete(task.job) {:ok, _} = Repo.delete(task.job)
@@ -30,21 +30,21 @@ defmodule Pinchflat.TasksTest do
end end
describe "list_tasks/0" do describe "list_tasks/0" do
test "it returns all tasks" do test "returns all tasks" do
task = task_fixture() task = task_fixture()
assert Tasks.list_tasks() == [task] assert Tasks.list_tasks() == [task]
end end
end end
describe "list_tasks_for/3" do 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() source = source_fixture()
task = task_fixture(source_id: source.id) task = task_fixture(source_id: source.id)
assert Tasks.list_tasks_for(source, nil, [:available]) == [task] assert Tasks.list_tasks_for(source, nil, [:available]) == [task]
end 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() source = source_fixture()
task = task_fixture(source_id: source.id) task = task_fixture(source_id: source.id)
@@ -52,7 +52,7 @@ defmodule Pinchflat.TasksTest do
assert Tasks.list_tasks_for(source, nil, [:cancelled]) == [] assert Tasks.list_tasks_for(source, nil, [:cancelled]) == []
end end
test "it lets you specify which worker to include" do test "lets you specify which worker to include" do
source = source_fixture() source = source_fixture()
task = task_fixture(source_id: source.id) task = task_fixture(source_id: source.id)
@@ -60,7 +60,7 @@ defmodule Pinchflat.TasksTest do
assert Tasks.list_tasks_for(source, "FooBarWorker") == [] assert Tasks.list_tasks_for(source, "FooBarWorker") == []
end 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() source = source_fixture()
task = task_fixture(source_id: source.id) task = task_fixture(source_id: source.id)
@@ -68,32 +68,8 @@ defmodule Pinchflat.TasksTest do
end end
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 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() task = task_fixture()
assert Tasks.get_task!(task.id) == task assert Tasks.get_task!(task.id) == task
end end
@@ -132,7 +108,7 @@ defmodule Pinchflat.TasksTest do
end end
describe "create_job_with_task/2" do 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() media_item = media_item_fixture()
refute_enqueued(worker: TestJobWorker) refute_enqueued(worker: TestJobWorker)
@@ -140,7 +116,7 @@ defmodule Pinchflat.TasksTest do
assert_enqueued(worker: TestJobWorker) assert_enqueued(worker: TestJobWorker)
end end
test "it creates a task record if successful" do test "creates a task record if successful" do
source = source_fixture() source = source_fixture()
assert {:ok, %Task{} = task} = Tasks.create_job_with_task(TestJobWorker.new(%{}), source) 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 assert task.source_id == source.id
end 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() source = source_fixture()
job = TestJobWorker.new(%{foo: "bar"}, unique: [period: :infinity]) 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) assert {:error, :duplicate_job} = Tasks.create_job_with_task(job, source)
end 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() source = source_fixture()
assert {:error, %Ecto.Changeset{}} = Tasks.create_job_with_task(%Ecto.Changeset{}, source) assert {:error, %Ecto.Changeset{}} = Tasks.create_job_with_task(%Ecto.Changeset{}, source)
@@ -181,7 +157,7 @@ defmodule Pinchflat.TasksTest do
end end
describe "delete_tasks_for/2" do 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() source = source_fixture()
task = task_fixture(source_id: source.id) 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 assert_raise Ecto.NoResultsError, fn -> Tasks.get_task!(task.id) end
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() media_item = media_item_fixture()
task = task_fixture(media_item_id: media_item.id) 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 assert_raise Ecto.NoResultsError, fn -> Repo.reload!(task) end
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 test "deletion does not impact unintended records" do
source = source_fixture() source = source_fixture()
task = task_fixture(source_id: source.id) task = task_fixture(source_id: source.id)
@@ -221,7 +208,7 @@ defmodule Pinchflat.TasksTest do
end end
describe "delete_pending_tasks_for/1" do 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() source = source_fixture()
task = task_fixture(source_id: source.id) 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 assert_raise Ecto.NoResultsError, fn -> Tasks.get_task!(task.id) end
end end
test "it does not delete non-pending tasks" do test "does not delete non-pending tasks" do
source = source_fixture() source = source_fixture()
task = Repo.preload(task_fixture(source_id: source.id), :job) task = Repo.preload(task_fixture(source_id: source.id), :job)
:ok = Oban.cancel_job(task.job) :ok = Oban.cancel_job(task.job)
@@ -238,7 +225,7 @@ defmodule Pinchflat.TasksTest do
assert Tasks.get_task!(task.id) assert Tasks.get_task!(task.id)
end end
test "it works on media_items" do test "works on media_items" do
media_item = media_item_fixture() media_item = media_item_fixture()
pending_task = task_fixture(media_item_id: media_item.id) pending_task = task_fixture(media_item_id: media_item.id)
cancelled_task = Repo.preload(task_fixture(media_item_id: media_item.id), :job) 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 :ok = Tasks.delete_pending_tasks_for(media_item, "TestJobWorker")
assert_raise Ecto.NoResultsError, fn -> Repo.reload!(task) end assert_raise Ecto.NoResultsError, fn -> Repo.reload!(task) end
end end
test "deletion can optionall include executing tasks" do
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 end
describe "change_task/1" do describe "change_task/1" do
test "it returns a task changeset" do test "returns a task changeset" do
task = task_fixture() task = task_fixture()
assert %Ecto.Changeset{} = Tasks.change_task(task) assert %Ecto.Changeset{} = Tasks.change_task(task)
end end
@@ -37,6 +37,46 @@ defmodule Pinchflat.Utils.FilesystemUtilsTest do
end end
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 describe "generate_metadata_tmpfile/1" do
test "creates a tmpfile and returns its path" do test "creates a tmpfile and returns its path" do
res = FilesystemUtils.generate_metadata_tmpfile(:json) res = FilesystemUtils.generate_metadata_tmpfile(:json)
+31
View File
@@ -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
@@ -7,6 +7,7 @@ defmodule PinchflatWeb.SourceControllerTest do
alias Pinchflat.Repo alias Pinchflat.Repo
alias Pinchflat.Settings alias Pinchflat.Settings
alias Pinchflat.Media.FileSyncingWorker
alias Pinchflat.Sources.SourceDeletionWorker alias Pinchflat.Sources.SourceDeletionWorker
alias Pinchflat.Downloading.MediaDownloadWorker alias Pinchflat.Downloading.MediaDownloadWorker
alias Pinchflat.Metadata.SourceMetadataStorageWorker alias Pinchflat.Metadata.SourceMetadataStorageWorker
@@ -268,6 +269,23 @@ defmodule PinchflatWeb.SourceControllerTest do
end end
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 defp create_source(_) do
source = source_fixture() source = source_fixture()
media_item = media_item_with_attachments(%{source_id: source.id}) media_item = media_item_with_attachments(%{source_id: source.id})
+36
View File
@@ -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!
+15 -2
View File
@@ -73,16 +73,19 @@ defmodule Pinchflat.MediaFixtures do
"#{:rand.uniform(1_000_000)}" "#{: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") 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!(media_filepath_fixture(), stored_media_filepath)
FilesystemUtils.cp_p!(thumbnail_filepath_fixture(), thumbnail_filepath) FilesystemUtils.cp_p!(thumbnail_filepath_fixture(), thumbnail_filepath)
FilesystemUtils.cp_p!(subtitle_filepath_fixture(), subtitle_filepath)
merged_attrs = merged_attrs =
Map.merge(attrs, %{ Map.merge(attrs, %{
media_filepath: stored_media_filepath, media_filepath: stored_media_filepath,
thumbnail_filepath: thumbnail_filepath thumbnail_filepath: thumbnail_filepath,
subtitle_filepaths: [["en", subtitle_filepath]]
}) })
media_item_fixture(merged_attrs) media_item_fixture(merged_attrs)
@@ -124,6 +127,16 @@ defmodule Pinchflat.MediaFixtures do
]) ])
end end
def subtitle_filepath_fixture do
Path.join([
File.cwd!(),
"test",
"support",
"files",
"subtitle.srt"
])
end
def infojson_filepath_fixture do def infojson_filepath_fixture do
Path.join([ Path.join([
File.cwd!(), File.cwd!(),