Compare commits

..

19 Commits

Author SHA1 Message Date
Kieran Eglin 99fc2eb8f8 Bumped version 2024-05-24 09:38:06 -07:00
Kieran Eglin e824b1a9f5 Fixed header height bug 2024-05-23 15:25:57 -07:00
Kieran 95a0c29358 [Enhancement] Added search to source forms (#259)
* Changed sqlite FTS to use a trigram tokenizer

* Added search UI to source tables

* Fix bug with special chars in search form

* improved main search results form

* Improved centering for media table header elements
2024-05-23 15:09:49 -07:00
Kieran Eglin 8b0b41186a Added code comments 2024-05-23 10:28:08 -07:00
Kieran d2f91a8253 [Enhancement] Added "other" tab to see media that's not set for download (#258)
* Added tab for other (non-downloaded and non-pending) media

* Added column for tracking if media was manually ignored
2024-05-23 10:24:52 -07:00
Kieran Eglin 776b84c585 Switched back to mainline sqleton [skip-ci] 2024-05-23 10:15:35 -07:00
Kieran Eglin 29803c9b26 added bullet points to diag info string 2024-05-22 16:52:32 -07:00
Kieran Eglin 85be8744d4 Linting 2024-05-22 16:51:08 -07:00
Kieran Eglin 1cbc62dc27 [Bugfix] removed layout flash for config dropdown 2024-05-22 14:45:44 -07:00
Kieran 5af22d3a2f Update issue templates 2024-05-22 14:27:04 -07:00
Kieran Eglin bf917febe8 bumped version 2024-05-22 12:33:26 -07:00
Kieran Eglin 8189dcce71 Added link to env vars section 2024-05-22 12:18:20 -07:00
Kieran Eglin 4e4092c792 Add list of env vars to README 2024-05-22 12:17:09 -07:00
Kieran 0cd6ac797b [Dev] Add ERD for tracking database state (#256)
* Added ERD triggered on migration

* Added alias
2024-05-22 10:40:30 -07:00
Kieran 81d5efd4c1 [Enhancement] Allow setting preference for video/audio codec (#255)
* [WIP] started on codec parser

* Added codec preferences to settings model

* [WIP] added basic codec preference fields to settings form

* Hooked up the backend portion of the codec preference work

* Added codec settings to frontend

* Ensured you can remove codec data
2024-05-21 16:28:19 -07:00
Kieran dec3938780 Added 8k support (#254) 2024-05-21 09:59:05 -07:00
Kieran a2a0f48065 [Enhancement] Add media count to sources page (#251)
* [WIP] Got query kinda working, now to refactor other queries

* Refactored all query methods to use dynamic snippets

* Refactored tab layout to grab tabs by name

* Removed standalone show buttons from in-app tables

* Removed unneeded comment
2024-05-21 08:53:22 -07:00
Kieran 94c0cf9970 [Enhancement] Show currently executing jobs on homepage (#250)
* [WIP] Added PoC job table

* [WIP] hooked up tasks table with real data

* Added reload on websocket command

* Hooked it all up to a websocket
2024-05-17 10:55:27 -07:00
Kieran Eglin bbdc56c656 Removed old file 2024-05-16 10:15:58 -07:00
67 changed files with 2158 additions and 328 deletions
+34
View File
@@ -0,0 +1,34 @@
---
name: Bug report
about: Create a report to help us improve
title: '[Triage] <your title here> '
labels: triage
assignees: kieraneglin
---
**Describe the bug**
A clear and concise description of what the bug is.
**To Reproduce**
Steps to reproduce the behavior:
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error
**Expected behavior**
A clear and concise description of what you expected to happen.
**Screenshots**
If applicable, add screenshots to help explain your problem.
**Diagnostic info**
<!-- Go to Config > App Info > Copy Diagnostic Info and paste that here -->
**Additional context**
<!-- Go to Config > App Info > Download Logs and attach them, if applicable -->
Add any other context about the problem here.
+19
View File
@@ -0,0 +1,19 @@
---
name: Feature request
about: Suggest an idea for this project
title: '[FR] <your title here>'
labels: feature request
assignees: kieraneglin
---
**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. It's too complicated to [...]
**Describe the solution you'd like**
A clear and concise description of what you want to happen.
**Describe alternatives you've considered**
A clear and concise description of any alternative solutions or features you've considered.
**Additional context**
Add any other context or screenshots about the feature request here.
+7
View File
@@ -0,0 +1,7 @@
---
name: Other
about: For everything else
title: ''
labels: ''
assignees: kieraneglin
---
+13
View File
@@ -31,6 +31,7 @@
- [Unraid](#unraid)
- [Portainer](#portainer)
- [Docker](#docker)
- [Environment Variables](#environment-variables)
- [Username and Password (authentication)](https://github.com/kieraneglin/pinchflat/wiki/Username-and-Password)
- [Frequently asked questions](https://github.com/kieraneglin/pinchflat/wiki/Frequently-Asked-Questions)
- [Documentation](https://github.com/kieraneglin/pinchflat/wiki)
@@ -141,6 +142,18 @@ As pointed out in [#137](https://github.com/kieraneglin/pinchflat/issues/137), S
If you change this setting and it works well for you, please leave a comment on [#137](https://github.com/kieraneglin/pinchflat/issues/137)! Doubly so if it does _not_ work well.
### Environment variables
| Name | Required? | Default | Notes |
| --------------------- | --------- | ------------------------- | ---------------------------------------------------------------------------------------------- |
| TZ | No | `UTC` | Must follow IANA TZ format |
| LOG_LEVEL | No | `debug` | Can be set to `info` |
| BASIC_AUTH_USERNAME | No | | See [authentication docs](https://github.com/kieraneglin/pinchflat/wiki/Username-and-Password) |
| BASIC_AUTH_PASSWORD | No | | See [authentication docs](https://github.com/kieraneglin/pinchflat/wiki/Username-and-Password) |
| EXPOSE_FEED_ENDPOINTS | No | | See [RSS feed docs](https://github.com/kieraneglin/pinchflat/wiki/Podcast-RSS-Feeds) |
| 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 |
## EFF donations
Prior to 2024-05-10, a portion of all donations were given to the [Electronic Frontier Foundation](https://www.eff.org/). Now, the app doesn't accept donations that go to me personally and instead directs you straight to the EFF. [Here](https://github.com/kieraneglin/pinchflat/issues/234) are some people that have generously donated.
+7 -7
View File
@@ -1,20 +1,20 @@
window.setTabIndex = (index) => {
window.location.hash = `tab-${index}`
window.setTabByName = (tabName) => {
window.location.hash = `tab-${tabName}`
return index
return tabName
}
// The conditionals and currIndex stuff ensures that
// the tab index is always set to 0 if the hash is empty
// AND other hash values are ignored
window.getTabIndex = (currIndex) => {
window.getTabFromHash = (currentTabName, defaultTabName) => {
if (window.location.hash === '' || window.location.hash === '#') {
return 0
return defaultTabName
}
if (window.location.hash.startsWith('#tab-')) {
return parseInt(window.location.hash.replace('#tab-', ''))
return window.location.hash.replace('#tab-', '')
}
return currIndex
return currentTabName
}
+1 -1
View File
@@ -12,7 +12,7 @@ RUN echo "Building for ${TARGETPLATFORM:?}"
RUN apt-get update -qq
RUN apt-get install -y inotify-tools curl git openssh-client jq \
python3 python3-setuptools python3-wheel python3-dev pipx \
python3-mutagen locales procps build-essential
python3-mutagen locales procps build-essential graphviz
# Install ffmpeg
RUN export FFMPEG_DOWNLOAD=$(case ${TARGETPLATFORM:-linux/amd64} in \
-4
View File
@@ -1,4 +0,0 @@
- Write media datbase ID as metadata/to file/whatever so it gives us an option to retroactively match media to the DB down the line. Useful if someone moves the media without informing the UI
- Use a UUID for the media database ID (or at least alongside it)
- Look into this and its recommended plugins https://hexdocs.pm/ex_check/readme.html
- Add output template option for the source's friendly name
+8 -1
View File
@@ -24,7 +24,7 @@ defmodule Pinchflat.Application do
PinchflatWeb.Endpoint
]
:ok = Oban.Telemetry.attach_default_logger()
attach_oban_telemetry()
Logger.add_handlers(:pinchflat)
# See https://hexdocs.pm/elixir/Supervisor.html
@@ -40,4 +40,11 @@ defmodule Pinchflat.Application do
PinchflatWeb.Endpoint.config_change(changed, removed)
:ok
end
defp attach_oban_telemetry do
events = [[:oban, :job, :start], [:oban, :job, :stop], [:oban, :job, :exception]]
:ok = Oban.Telemetry.attach_default_logger()
:telemetry.attach_many("job-telemetry-broadcast", events, &PinchflatWeb.Telemetry.job_state_change_broadcast/4, [])
end
end
+82
View File
@@ -0,0 +1,82 @@
defmodule Pinchflat.Downloading.CodecParser do
@moduledoc """
Functions for generating yt-dlp codec strings
"""
alias Pinchflat.Settings
@doc """
Generate a video codec string based on the value of the video_codec_preference setting.
Returns binary()
"""
def generate_vcodec_string_from_settings do
generate_vcodec_string(Settings.get!(:video_codec_preference))
end
@doc """
Generate an audio codec string based on the value of the audio_codec_preference setting.
Returns binary()
"""
def generate_acodec_string_from_settings do
generate_acodec_string(Settings.get!(:audio_codec_preference))
end
@doc """
Generate a video codec string from a list of video codecs.
If the list is nil or empty, the default video codec is AVC.
Returns binary()
"""
def generate_vcodec_string(nil), do: "bestvideo[vcodec~='^avc']/bestvideo"
def generate_vcodec_string([]), do: generate_vcodec_string(nil)
def generate_vcodec_string(video_codecs) do
video_codecs
|> Enum.map(&video_codec_map()[&1])
|> Enum.reject(&is_nil/1)
|> Enum.map(&"bestvideo[vcodec~='^#{&1}']")
|> Enum.concat(["bestvideo"])
|> Enum.join("/")
end
@doc """
Generate an audio codec string from a list of audio codecs.
If the list is nil or empty, the default audio codec is MP4A.
Returns binary()
"""
def generate_acodec_string(nil), do: "bestaudio[acodec~='^mp4a']/bestaudio"
def generate_acodec_string([]), do: generate_acodec_string(nil)
def generate_acodec_string(audio_codecs) do
audio_codecs
|> Enum.map(&audio_codec_map()[&1])
|> Enum.reject(&is_nil/1)
|> Enum.map(&"bestaudio[acodec~='^#{&1}']")
|> Enum.concat(["bestaudio"])
|> Enum.join("/")
end
@doc false
def video_codec_map do
%{
"av01" => "av01",
"avc" => "avc",
"vp9" => "vp0?9"
}
end
@doc false
def audio_codec_map do
%{
"aac" => "aac",
"mp4a" => "mp4a",
"mp3" => "mp3",
"opus" => "opus"
}
end
end
@@ -6,6 +6,7 @@ defmodule Pinchflat.Downloading.DownloadOptionBuilder do
alias Pinchflat.Sources
alias Pinchflat.Sources.Source
alias Pinchflat.Media.MediaItem
alias Pinchflat.Downloading.CodecParser
alias Pinchflat.Downloading.OutputPathBuilder
alias Pinchflat.Utils.FilesystemUtils, as: FSUtils
@@ -121,27 +122,26 @@ defmodule Pinchflat.Downloading.DownloadOptionBuilder do
end
defp quality_options(media_profile) do
video_codec_option = fn res ->
[format_sort: "res:#{res},+codec:avc:m4a", remux_video: "mp4"]
end
audio_format_precedence = [
"bestaudio[ext=m4a]",
"bestaudio[ext=mp3]",
"bestaudio",
"best[ext=m4a]",
"best[ext=mp3]",
"best"
]
vcodec_string = CodecParser.generate_vcodec_string_from_settings()
acodec_string = CodecParser.generate_acodec_string_from_settings()
case media_profile.preferred_resolution do
# Also be aware that :audio disabled all embedding options for subtitles
:audio -> [:extract_audio, format: Enum.join(audio_format_precedence, "/")]
:"360p" -> video_codec_option.("360")
:"480p" -> video_codec_option.("480")
:"720p" -> video_codec_option.("720")
:"1080p" -> video_codec_option.("1080")
:"2160p" -> video_codec_option.("2160")
:audio ->
[:extract_audio, format: "#{acodec_string}/best"]
resolution_atom ->
{resolution_string, _} =
resolution_atom
|> Atom.to_string()
|> Integer.parse()
[
format_sort: "res:#{resolution_string}",
# Since Plex doesn't support reading metadata from MKV
remux_video: "mp4",
format: "((#{vcodec_string})+(#{acodec_string}))/best"
]
end
end
@@ -85,10 +85,16 @@ defmodule Pinchflat.Downloading.DownloadingHelpers do
"""
def kickoff_redownload_for_existing_media(%Source{} = source) do
MediaQuery.new()
|> MediaQuery.for_source(source)
|> MediaQuery.with_media_downloaded_at()
|> MediaQuery.where_download_not_prevented()
|> MediaQuery.where_not_culled()
|> MediaQuery.require_assoc(:media_profile)
|> where(
^dynamic(
[m, s, mp],
^MediaQuery.for_source(source) and
^MediaQuery.downloaded() and
not (^MediaQuery.download_prevented()) and
not (^MediaQuery.culled())
)
)
|> Repo.all()
|> Enum.map(&MediaDownloadWorker.kickoff_with_task/1)
end
@@ -4,7 +4,7 @@ defmodule Pinchflat.Downloading.MediaDownloadWorker do
use Oban.Worker,
queue: :media_fetching,
unique: [period: :infinity, states: [:available, :scheduled, :retryable, :executing]],
tags: ["media_item", "media_fetching"]
tags: ["media_item", "media_fetching", "show_in_dashboard"]
require Logger
@@ -4,7 +4,7 @@ defmodule Pinchflat.Downloading.MediaQualityUpgradeWorker do
use Oban.Worker,
queue: :media_fetching,
unique: [period: :infinity, states: [:available, :scheduled, :retryable, :executing]],
tags: ["media_item", "media_fetching"]
tags: ["media_item", "media_fetching", "show_in_dashboard"]
require Logger
@@ -49,8 +49,7 @@ defmodule Pinchflat.FastIndexing.FastIndexingHelpers do
defp list_media_items_by_media_id_for(source, media_ids) do
MediaQuery.new()
|> MediaQuery.for_source(source)
|> MediaQuery.with_media_ids(media_ids)
|> where(^dynamic([mi], ^MediaQuery.for_source(source) and mi.media_id in ^media_ids))
|> Repo.all()
end
@@ -4,7 +4,7 @@ defmodule Pinchflat.FastIndexing.FastIndexingWorker do
use Oban.Worker,
queue: :fast_indexing,
unique: [period: :infinity, states: [:available, :scheduled, :retryable]],
tags: ["media_source", "fast_indexing"]
tags: ["media_source", "fast_indexing", "show_in_dashboard"]
require Logger
@@ -63,15 +63,14 @@ defmodule Pinchflat.Lifecycle.Notifications.SourceNotifications do
defp pending_media_item_count(source) do
MediaQuery.new()
|> MediaQuery.for_source(source)
|> MediaQuery.where_pending_download()
|> MediaQuery.require_assoc(:media_profile)
|> where(^dynamic(^MediaQuery.for_source(source) and ^MediaQuery.pending()))
|> Repo.aggregate(:count)
end
defp downloaded_media_item_count(source) do
MediaQuery.new()
|> MediaQuery.for_source(source)
|> MediaQuery.with_media_filepath()
|> where(^dynamic(^MediaQuery.for_source(source) and ^MediaQuery.downloaded()))
|> Repo.aggregate(:count)
end
+10 -16
View File
@@ -32,9 +32,8 @@ defmodule Pinchflat.Media do
"""
def list_cullable_media_items do
MediaQuery.new()
|> MediaQuery.with_media_filepath()
|> MediaQuery.where_past_retention_period()
|> MediaQuery.where_culling_not_prevented()
|> MediaQuery.require_assoc(:source)
|> where(^MediaQuery.cullable())
|> Repo.all()
end
@@ -54,18 +53,14 @@ defmodule Pinchflat.Media do
"""
def list_redownloadable_media_items do
MediaQuery.new()
|> MediaQuery.with_media_downloaded_at()
|> MediaQuery.where_download_not_prevented()
|> MediaQuery.where_not_culled()
|> MediaQuery.where_media_not_redownloaded()
|> MediaQuery.where_past_redownload_delay()
|> MediaQuery.require_assoc(:media_profile)
|> where(^MediaQuery.redownloadable())
|> Repo.all()
end
@doc """
Returns a list of pending media_items for a given source, where
pending means the `media_filepath` is `nil` AND the media_item
matches satisfies `MediaQuery.where_pending_download`. You
pending means the media_item satisfies `MediaQuery.pending`. You
should really check out that function if you need to know more
because it has a lot going on.
@@ -73,15 +68,14 @@ defmodule Pinchflat.Media do
"""
def list_pending_media_items_for(%Source{} = source) do
MediaQuery.new()
|> MediaQuery.for_source(source)
|> MediaQuery.where_pending_download()
|> MediaQuery.require_assoc(:media_profile)
|> where(^dynamic(^MediaQuery.for_source(source) and ^MediaQuery.pending()))
|> Repo.all()
end
@doc """
For a given media_item, tells you if it is pending download. This is defined as
the media_item having a `media_filepath` of `nil` and matching the format selection
rules of the parent media_profile.
the media_item satisfying `MediaQuery.pending` which you should really check out.
Intentionally does not take the `download_media` setting of the source into account.
@@ -91,8 +85,8 @@ defmodule Pinchflat.Media do
media_item = Repo.preload(media_item, source: :media_profile)
MediaQuery.new()
|> MediaQuery.with_id(media_item.id)
|> MediaQuery.where_pending_download()
|> MediaQuery.require_assoc(:media_profile)
|> where(^dynamic([m, s, mp], m.id == ^media_item.id and ^MediaQuery.pending()))
|> Repo.exists?()
end
+1 -2
View File
@@ -142,8 +142,7 @@ defmodule Pinchflat.Media.MediaItem do
current_max =
MediaQuery.new()
|> MediaQuery.for_source(source_id)
|> MediaQuery.where_uploaded_on_date(changes.upload_date)
|> where(^dynamic([mi], mi.upload_date == ^changes.upload_date and ^MediaQuery.for_source(source)))
|> Repo.aggregate(aggregator, :upload_date_index)
case current_max do
+132 -138
View File
@@ -23,138 +23,26 @@ defmodule Pinchflat.Media.MediaQuery do
end
end
# Prefixes:
# - for_* - belonging to a certain record
# - join_* - for joining on a certain record
# - with_*, where_* - for filtering based on full, concrete attributes
# - matching_* - for filtering based on partial attributes (e.g. LIKE, regex, full-text search)
#
# Suffixes:
# - _for - the arg passed is an association record
# NOTE: that dyanmic query approach kinda rocked - should refactor in future
def new do
MediaItem
end
def for_source(query, source_id) when is_integer(source_id) do
where(query, [mi], mi.source_id == ^source_id)
def for_source(source_id) when is_integer(source_id), do: dynamic([mi], mi.source_id == ^source_id)
def for_source(source), do: dynamic([mi], mi.source_id == ^source.id)
def downloaded, do: dynamic([mi], not is_nil(mi.media_filepath))
def download_prevented, do: dynamic([mi], mi.prevent_download == true)
def culling_prevented, do: dynamic([mi], mi.prevent_culling == true)
def culled, do: dynamic([mi], not is_nil(mi.culled_at))
def redownloaded, do: dynamic([mi], not is_nil(mi.media_redownloaded_at))
def upload_date_after_source_cutoff do
dynamic([mi, source], is_nil(source.download_cutoff_date) or mi.upload_date >= source.download_cutoff_date)
end
def for_source(query, source) do
where(query, [mi], mi.source_id == ^source.id)
end
def join_sources(query) do
from(mi in query, join: s in assoc(mi, :source), as: :sources)
end
def where_past_retention_period(query) do
query
|> require_assoc(:source)
|> where(
[mi, source],
fragment("""
IFNULL(retention_period_days, 0) > 0 AND
DATETIME('now', '-' || retention_period_days || ' day') > media_downloaded_at
""")
)
end
def where_past_redownload_delay(query) do
query
|> require_assoc(:source)
|> require_assoc(:media_profile)
|> where(
[_mi, _source, _media_profile],
# Returns media items where the upload_date is at least redownload_delay_days ago AND
# 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') > upload_date AND
DATETIME(media_downloaded_at, '-' || redownload_delay_days || ' day') < upload_date
""")
)
end
def where_culling_not_prevented(query) do
where(query, [mi], mi.prevent_culling == false)
end
def where_not_culled(query) do
where(query, [mi], is_nil(mi.culled_at))
end
def where_media_not_redownloaded(query) do
where(query, [mi], is_nil(mi.media_redownloaded_at))
end
def with_id(query, id) do
where(query, [mi], mi.id == ^id)
end
def with_media_ids(query, media_ids) do
where(query, [mi], mi.media_id in ^media_ids)
end
def with_media_downloaded_at(query) do
where(query, [mi], not is_nil(mi.media_downloaded_at))
end
def with_media_filepath(query) do
where(query, [mi], not is_nil(mi.media_filepath))
end
def with_no_media_filepath(query) do
where(query, [mi], is_nil(mi.media_filepath))
end
def with_upload_date_after_source_cutoff(query) do
query
|> require_assoc(:source)
|> where([mi, source], is_nil(source.download_cutoff_date) or mi.upload_date >= source.download_cutoff_date)
end
def where_uploaded_on_date(query, date) do
where(query, [mi], mi.upload_date == ^date)
end
def where_download_not_prevented(query) do
where(query, [mi], mi.prevent_download == false)
end
def matching_source_title_regex(query) do
query
|> require_assoc(:source)
|> where(
[mi, source],
is_nil(source.title_filter_regex) or fragment("regexp_like(?, ?)", mi.title, source.title_filter_regex)
)
end
def matching_search_term(query, nil), do: query
def matching_search_term(query, term) do
from(mi in query,
join: mi_search_index in assoc(mi, :media_items_search_index),
where: fragment("media_items_search_index MATCH ?", ^term),
select_merge: %{
matching_search_term:
fragment("""
coalesce(snippet(media_items_search_index, 0, '[PF_HIGHLIGHT]', '[/PF_HIGHLIGHT]', '...', 20), '') ||
' ' ||
coalesce(snippet(media_items_search_index, 1, '[PF_HIGHLIGHT]', '[/PF_HIGHLIGHT]', '...', 20), '')
""")
},
order_by: [desc: fragment("rank")]
)
end
def with_format_matching_profile_preference(query) do
query
|> require_assoc(:media_profile)
|> where(
def format_matching_profile_preference do
dynamic(
[mi, source, media_profile],
fragment("""
CASE
WHEN shorts_behaviour = 'only' AND livestream_behaviour = 'only' THEN
@@ -176,22 +64,80 @@ defmodule Pinchflat.Media.MediaQuery do
)
end
def where_pending_download(query) do
query
|> where_download_not_prevented()
|> with_no_media_filepath()
|> with_upload_date_after_source_cutoff()
|> with_format_matching_profile_preference()
|> matching_source_title_regex()
def matches_source_title_regex do
dynamic(
[mi, source],
is_nil(source.title_filter_regex) or fragment("regexp_like(?, ?)", mi.title, source.title_filter_regex)
)
end
def where_pending_or_downloaded(query) do
query
|> where_pending_download()
|> or_where([mi], not is_nil(mi.media_downloaded_at))
def past_retention_period do
dynamic(
[mi, source],
fragment("""
IFNULL(retention_period_days, 0) > 0 AND
DATETIME('now', '-' || retention_period_days || ' day') > media_downloaded_at
""")
)
end
defp require_assoc(query, identifier) do
def past_redownload_delay do
dynamic(
[mi, source, media_profile],
# Returns media items where the upload_date is at least redownload_delay_days ago AND
# 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') > upload_date AND
DATETIME(media_downloaded_at, '-' || redownload_delay_days || ' day') < upload_date
""")
)
end
def cullable do
dynamic(
[mi, source],
^downloaded() and
^past_retention_period() and
not (^culling_prevented())
)
end
def pending do
dynamic(
[mi],
not (^downloaded()) and
not (^download_prevented()) and
^upload_date_after_source_cutoff() and
^format_matching_profile_preference() and
^matches_source_title_regex()
)
end
def redownloadable do
dynamic(
[mi, source],
^downloaded() and
not (^download_prevented()) and
not (^culled()) and
not (^redownloaded()) and
^past_redownload_delay()
)
end
def matches_search_term(nil), do: dynamic([mi], true)
def matches_search_term(term) do
escaped_term = clean_search_term(term)
# Matching on `term` instead of `escaped_term` because the latter can mangle empty strings
case String.trim(term) do
"" -> dynamic([mi], true)
_ -> dynamic([mi], fragment("media_items_search_index MATCH ?", ^escaped_term))
end
end
def require_assoc(query, identifier) do
if has_named_binding?(query, identifier) do
query
else
@@ -199,6 +145,10 @@ defmodule Pinchflat.Media.MediaQuery do
end
end
defp do_require_assoc(query, :media_items_search_index) do
from(mi in query, join: s in assoc(mi, :media_items_search_index), as: :media_items_search_index)
end
defp do_require_assoc(query, :source) do
from(mi in query, join: s in assoc(mi, :source), as: :source)
end
@@ -208,4 +158,48 @@ defmodule Pinchflat.Media.MediaQuery do
|> require_assoc(:source)
|> join(:inner, [mi, source], mp in assoc(source, :media_profile), as: :media_profile)
end
# This needs to be a non-dynamic query because it alone should control things like
# ordering and `snippets` for full-text search
def matching_search_term(query, nil), do: query
def matching_search_term(query, term) do
escaped_term = clean_search_term(term)
from(mi in query,
join: mi_search_index in assoc(mi, :media_items_search_index),
where: fragment("media_items_search_index MATCH ?", ^escaped_term),
select_merge: %{
matching_search_term:
fragment("""
coalesce(snippet(media_items_search_index, 0, '[PF_HIGHLIGHT]', '[/PF_HIGHLIGHT]', '...', 20), '') ||
' ' ||
coalesce(snippet(media_items_search_index, 1, '[PF_HIGHLIGHT]', '[/PF_HIGHLIGHT]', '...', 20), '')
""")
},
order_by: [desc: fragment("rank")]
)
end
# SQLite's FTS5 is very picky about what it will accept as a search term.
# To that end, we need to clean up the search term before passing it to the
# MATCH clause.
# This method:
# - Trims leading and trailing whitespace
# - Collapses multiple spaces into a single space
# - Removes quote characters
# - Wraps any word in quotes (must happen after the double quote replacement)
#
# This allows for works with apostrophes and quotes to be searched for correctly
defp clean_search_term(nil), do: ""
defp clean_search_term(""), do: ""
defp clean_search_term(term) do
term
|> String.trim()
|> String.replace(~r/\s+/, " ")
|> String.split(~r/\s+/)
|> Enum.map(fn str -> String.replace(str, ~s("), "") end)
|> Enum.map_join(" ", fn str -> ~s("#{str}") end)
end
end
@@ -3,7 +3,7 @@ defmodule Pinchflat.Metadata.SourceMetadataStorageWorker do
use Oban.Worker,
queue: :remote_metadata,
tags: ["media_source", "source_metadata", "remote_metadata"],
tags: ["media_source", "source_metadata", "remote_metadata", "show_in_dashboard"],
max_attempts: 3
require Logger
+1 -2
View File
@@ -27,8 +27,7 @@ defmodule Pinchflat.Podcasts.PodcastHelpers do
limit = Keyword.get(opts, :limit, 1_000)
MediaQuery.new()
|> MediaQuery.for_source(source)
|> MediaQuery.with_media_filepath()
|> where(^dynamic(^MediaQuery.for_source(source) and ^MediaQuery.downloaded()))
|> order_by(desc: :upload_date)
|> Repo.maybe_limit(limit)
|> Repo.all()
+1 -1
View File
@@ -63,7 +63,7 @@ defmodule Pinchflat.Profiles.MediaProfile do
# See `build_format_clauses` in the Media context for more.
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(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"
has_many :sources, Source
+39 -1
View File
@@ -11,7 +11,14 @@ defmodule Pinchflat.Settings.Setting do
:pro_enabled,
:yt_dlp_version,
:apprise_version,
:apprise_server
:apprise_server,
:video_codec_preference,
:audio_codec_preference
]
@virtual_fields [
:video_codec_preference_string,
:audio_codec_preference_string
]
@required_fields ~w(
@@ -25,12 +32,43 @@ defmodule Pinchflat.Settings.Setting do
field :yt_dlp_version, :string
field :apprise_version, :string
field :apprise_server, :string
field :video_codec_preference, {:array, :string}, default: []
field :audio_codec_preference, {:array, :string}, default: []
field :video_codec_preference_string, :string, default: nil, virtual: true
field :audio_codec_preference_string, :string, default: nil, virtual: true
end
@doc false
def changeset(setting, attrs) do
setting
|> cast(attrs, @allowed_fields)
|> cast(attrs, @virtual_fields, empty_values: [])
|> convert_codec_preference_strings()
|> validate_required(@required_fields)
end
defp convert_codec_preference_strings(changeset) do
fields = [
video_codec_preference_string: :video_codec_preference,
audio_codec_preference_string: :audio_codec_preference
]
Enum.reduce(fields, changeset, fn {virtual_field, actual_field}, changeset ->
case get_change(changeset, virtual_field) do
nil ->
changeset
value ->
new_value =
value
|> String.split(">")
|> Enum.map(&String.trim/1)
|> Enum.reject(&(String.trim(&1) == ""))
|> Enum.map(&String.downcase/1)
put_change(changeset, actual_field, new_value)
end
end)
end
end
@@ -4,7 +4,7 @@ defmodule Pinchflat.SlowIndexing.MediaCollectionIndexingWorker do
use Oban.Worker,
queue: :media_collection_indexing,
unique: [period: :infinity, states: [:available, :scheduled, :retryable]],
tags: ["media_source", "media_collection_indexing"]
tags: ["media_source", "media_collection_indexing", "show_in_dashboard"]
require Logger
+1 -1
View File
@@ -127,7 +127,7 @@ defmodule Pinchflat.Sources do
Tasks.delete_tasks_for(source)
MediaQuery.new()
|> MediaQuery.for_source(source)
|> where(^MediaQuery.for_source(source))
|> Repo.all()
|> Enum.each(fn media_item ->
Media.delete_media_item(media_item, delete_files: delete_files)
+16 -10
View File
@@ -12,20 +12,26 @@ defmodule Pinchflat.Sources.SourcesQuery do
alias Pinchflat.Sources.Source
# Prefixes:
# - for_* - belonging to a certain record
# - join_* - for joining on a certain record
# - with_* - for filtering based on full, concrete attributes
# - matching_* - for filtering based on partial attributes (e.g. LIKE, regex, full-text search)
#
# Suffixes:
# - _for - the arg passed is an association record
# This allows the module to be aliased and query methods to be used
# all in one go
# usage: use Pinchflat.Sources.SourcesQuery
defmacro __using__(_opts) do
quote do
import Ecto.Query, warn: false
alias unquote(__MODULE__)
end
end
def new do
Source
end
def for_media_profile(query, media_profile) do
where(query, [s], s.media_profile_id == ^media_profile.id)
def for_media_profile(media_profile_id) when is_integer(media_profile_id) do
dynamic([s], s.media_profile_id == ^media_profile_id)
end
def for_media_profile(media_profile) do
dynamic([s], s.media_profile_id == ^media_profile.id)
end
end
+45
View File
@@ -0,0 +1,45 @@
defmodule Pinchflat.Tasks.TasksQuery do
@moduledoc """
Query helpers for the Tasks context.
These methods are made to be one-ish liners used
to compose queries. Each method should strive to do
_one_ thing. These don't need to be tested as
they are just building blocks for other functionality
which, itself, will be tested.
"""
import Ecto.Query, warn: false
alias Pinchflat.Tasks.Task
# This allows the module to be aliased and query methods to be used
# all in one go
# usage: use Pinchflat.Tasks.TasksQuery
defmacro __using__(_opts) do
quote do
import Ecto.Query, warn: false
alias unquote(__MODULE__)
end
end
def new do
Task
end
def join_job(query) do
query
|> join(:left, [t], j in assoc(t, :job))
|> preload([t, j], job: j)
end
def in_state(states) when is_list(states) do
dynamic([t, j], j.state in ^states)
end
def in_state(state), do: in_state([state])
def has_tag(tag) do
dynamic([t, j], ^tag in j.tags)
end
end
@@ -6,25 +6,32 @@ defmodule PinchflatWeb.CustomComponents.TabComponents do
Takes a list of tabs and renders them in a tabbed layout.
"""
slot :tab, required: true do
attr :id, :string, required: true
attr :title, :string, required: true
end
slot :tab_append, required: false
def tabbed_layout(assigns) do
assigns = Map.put(assigns, :first_tab_id, hd(assigns.tab).id)
~H"""
<div
x-data="{ openTab: getTabIndex(0), activeClasses: 'text-meta-5 border-meta-5', inactiveClasses: 'border-transparent' }"
@hashchange.window="openTab = getTabIndex(openTab)"
x-data={"{
openTab: getTabFromHash('#{@first_tab_id}', '#{@first_tab_id}'),
activeClasses: 'text-meta-5 border-meta-5',
inactiveClasses: 'border-transparent'
}"}
@hashchange.window={"openTab = getTabFromHash(openTab, '#{@first_tab_id}')"}
class="w-full"
>
<header class="flex flex-col md:flex-row md:justify-between border-b border-strokedark">
<div class="flex flex-wrap gap-5 sm:gap-10">
<a
:for={{tab, idx} <- Enum.with_index(@tab)}
:for={tab <- @tab}
href="#"
@click.prevent={"openTab = setTabIndex(#{idx})"}
x-bind:class={"openTab === #{idx} ? activeClasses : inactiveClasses"}
@click.prevent={"openTab = setTabByName('#{tab.id}')"}
x-bind:class={"openTab === '#{tab.id}' ? activeClasses : inactiveClasses"}
class="border-b-2 py-4 w-full sm:w-fit text-sm font-medium hover:text-meta-5 md:text-base"
>
<span class="text-xl"><%= tab.title %></span>
@@ -35,7 +42,7 @@ defmodule PinchflatWeb.CustomComponents.TabComponents do
</div>
</header>
<div class="mt-4 min-h-60">
<div :for={{tab, idx} <- Enum.with_index(@tab)} x-show={"openTab === #{idx}"} class="font-medium leading-relaxed">
<div :for={tab <- @tab} x-show={"openTab === '#{tab.id}'"} class="font-medium leading-relaxed">
<%= render_slot(tab) %>
</div>
</div>
@@ -36,7 +36,7 @@ defmodule PinchflatWeb.CustomComponents.TableComponents do
</tr>
</thead>
<tbody>
<tr :for={row <- @rows} class="border-b border-boxdark hover:border-strokedark">
<tr :for={row <- @rows} class="hover:bg-strokedark">
<td
:for={col <- @col}
class={[
+1 -1
View File
@@ -68,7 +68,7 @@ defmodule PinchflatWeb.Layouts do
</span>
</span>
<ul x-bind:class="selected ? 'block' :'hidden'">
<ul x-cloak x-show="selected">
<li :for={menu <- @submenu} class="text-bodydark2">
<.sidebar_link icon={menu[:icon]} text={menu[:text]} href={menu[:href]} target={menu[:target]} class="pl-10" />
</li>
@@ -1,4 +1,4 @@
<header class="sticky top-0 z-999 flex h-20 w-full bg-white drop-shadow-1 dark:bg-boxdark dark:drop-shadow-none">
<header class="sticky top-0 z-999 flex min-h-20 w-full bg-white drop-shadow-1 dark:bg-boxdark dark:drop-shadow-none">
<div class="flex flex-grow items-center justify-between lg:justify-end px-4 py-4 shadow-2 md:px-6 2xl:px-11">
<div class="flex items-center gap-2 sm:gap-4 lg:hidden w-2/6">
<section class="pr-1">
@@ -22,7 +22,7 @@
type="text"
name="q"
value={@params["q"]}
placeholder="Type to search..."
placeholder="Search all media..."
class="w-full bg-transparent pl-9 pr-4 border-0 focus:ring-0 focus:outline-none"
/>
</form>
@@ -18,7 +18,7 @@ defmodule PinchflatWeb.MediaItems.MediaItemHTML do
def media_type(media_item) do
case Path.extname(media_item.media_filepath) do
ext when ext in [".mp4", ".webm", ".mkv"] -> :video
ext when ext in [".mp3", ".m4a"] -> :audio
ext when ext in [".mp3", ".m4a", ".opus"] -> :audio
_ -> :unknown
end
end
@@ -9,7 +9,7 @@
Oops, something went wrong! Please check the errors below.
</.error>
<h3 class=" text-2xl text-black dark:text-white">
<h3 class="text-2xl text-black dark:text-white">
General Options
</h3>
@@ -23,7 +23,7 @@
<.actions_dropdown media_item={@media_item} />
</:tab_append>
<:tab title="Media">
<: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">
@@ -52,7 +52,7 @@
</section>
</div>
</:tab>
<:tab title="Tasks">
<:tab title="Tasks" id="tasks">
<%= if match?([_|_], @media_item.tasks) do %>
<.table rows={@media_item.tasks} table_class="text-black dark:text-white">
<:col :let={task} label="Worker">
@@ -1,11 +1,9 @@
defmodule PinchflatWeb.MediaProfiles.MediaProfileController do
use PinchflatWeb, :controller
import Ecto.Query, warn: false
use Pinchflat.Sources.SourcesQuery
alias Pinchflat.Repo
alias Pinchflat.Profiles
alias Pinchflat.Sources.SourcesQuery
alias Pinchflat.Profiles.MediaProfile
def index(conn, _params) do
@@ -43,7 +41,7 @@ defmodule PinchflatWeb.MediaProfiles.MediaProfileController do
sources =
SourcesQuery.new()
|> SourcesQuery.for_media_profile(media_profile)
|> where(^SourcesQuery.for_media_profile(media_profile))
|> order_by(asc: :custom_name)
|> Repo.all()
@@ -23,6 +23,7 @@ defmodule PinchflatWeb.MediaProfiles.MediaProfileHTML do
def friendly_resolution_options do
[
{"8k", "4320p"},
{"4k", "2160p"},
{"1080p", "1080p"},
{"720p", "720p"},
@@ -23,9 +23,8 @@
<:col :let={media_profile} label="Preferred Resolution">
<%= media_profile.preferred_resolution %>
</:col>
<:col :let={media_profile} label="" class="flex place-content-evenly">
<.icon_link href={~p"/media_profiles/#{media_profile.id}"} icon="hero-eye" class="mx-1" />
<.icon_link href={~p"/media_profiles/#{media_profile.id}/edit"} icon="hero-pencil-square" class="mx-1" />
<:col :let={media_profile} label="" class="flex justify-end">
<.icon_link href={~p"/media_profiles/#{media_profile.id}/edit"} icon="hero-pencil-square" class="mr-4" />
</:col>
</.table>
</div>
@@ -23,13 +23,13 @@
<.actions_dropdown media_profile={@media_profile} />
</:tab_append>
<:tab title="Media Profile">
<:tab title="Media Profile" id="media-profile">
<div class="flex flex-col gap-10 text-white">
<h3 class="font-bold text-xl mt-6">Raw Attributes</h3>
<.list_items_from_map map={Map.from_struct(@media_profile)} />
</div>
</:tab>
<:tab title="Sources">
<:tab title="Sources" id="sources">
<.table rows={@sources} table_class="text-black dark:text-white">
<:col :let={source} label="Name">
<.subtle_link href={~p"/sources/#{source.id}"}>
@@ -40,9 +40,8 @@
<:col :let={source} label="Should Download?">
<.icon name={if source.download_media, do: "hero-check", else: "hero-x-mark"} />
</:col>
<:col :let={source} label="" class="flex place-content-evenly">
<.icon_link href={~p"/sources/#{source.id}"} icon="hero-eye" class="mx-1" />
<.icon_link href={~p"/sources/#{source.id}/edit"} icon="hero-pencil-square" class="mx-1" />
<:col :let={source} label="" class="flex justify-end">
<.icon_link href={~p"/sources/#{source.id}/edit"} icon="hero-pencil-square" class="mr-4" />
</:col>
</.table>
</:tab>
@@ -26,7 +26,7 @@ defmodule PinchflatWeb.Pages.PageController do
source_count: Repo.aggregate(Source, :count, :id),
media_item_count:
MediaQuery.new()
|> MediaQuery.with_media_downloaded_at()
|> where(^MediaQuery.downloaded())
|> Repo.aggregate(:count, :id)
)
end
@@ -6,7 +6,7 @@ defmodule Pinchflat.Pages.HistoryTableLive do
alias Pinchflat.Utils.NumberUtils
alias PinchflatWeb.CustomComponents.TextComponents
@limit 10
@limit 5
def render(%{records: []} = assigns) do
~H"""
@@ -97,7 +97,8 @@ defmodule Pinchflat.Pages.HistoryTableLive do
defp generate_base_query do
MediaQuery.new()
|> MediaQuery.where_pending_or_downloaded()
|> MediaQuery.require_assoc(:media_profile)
|> where(^dynamic(^MediaQuery.downloaded() or ^MediaQuery.pending()))
|> order_by(desc: :id)
end
@@ -26,8 +26,15 @@
</div>
<div class="rounded-sm border shadow-default border-strokedark bg-boxdark mt-4 p-5">
<span class="text-2xl font-medium mb-4">History</span>
<span class="text-2xl font-medium mb-4">Media History</span>
<section class="mt-6">
<%= live_render(@conn, Pinchflat.Pages.HistoryTableLive) %>
</section>
</div>
<div class="rounded-sm border shadow-default border-strokedark bg-boxdark mt-4 p-5">
<span class="text-2xl font-medium mb-4">Active Tasks</span>
<section class="mt-6 min-h-80">
<%= live_render(@conn, Pinchflat.Pages.JobTableLive) %>
</section>
</div>
@@ -0,0 +1,97 @@
defmodule Pinchflat.Pages.JobTableLive do
use PinchflatWeb, :live_view
use Pinchflat.Tasks.TasksQuery
alias Pinchflat.Repo
alias Pinchflat.Tasks.Task
alias PinchflatWeb.CustomComponents.TextComponents
def render(%{tasks: []} = assigns) do
~H"""
<div class="mb-4 flex items-center">
<p>Nothing Here!</p>
</div>
"""
end
def render(assigns) do
~H"""
<div class="max-w-full overflow-x-auto">
<.table rows={@tasks} table_class="text-white">
<:col :let={task} label="Task">
<%= worker_to_task_name(task.job.worker) %>
</:col>
<:col :let={task} label="Subject">
<.subtle_link href={task_to_link(task)}>
<%= StringUtils.truncate(task_to_record_name(task), 35) %>
</.subtle_link>
</:col>
<:col :let={task} label="Attempt No.">
<%= task.job.attempt %>
</:col>
<:col :let={task} label="Started At">
<%= format_datetime(task.job.attempted_at) %>
</:col>
</.table>
</div>
"""
end
def mount(_params, _session, socket) do
PinchflatWeb.Endpoint.subscribe("job:state")
{:ok, assign(socket, tasks: get_tasks())}
end
def handle_info(%{topic: "job:state", event: "change"}, socket) do
{:noreply, assign(socket, tasks: get_tasks())}
end
defp get_tasks do
TasksQuery.new()
|> TasksQuery.join_job()
|> where(^TasksQuery.in_state("executing"))
|> where(^TasksQuery.has_tag("show_in_dashboard"))
|> order_by([t, j], desc: j.attempted_at)
|> Repo.all()
|> Repo.preload([:media_item, :source])
end
defp worker_to_task_name(worker) do
final_module_part =
worker
|> String.split(".")
|> Enum.at(-1)
map_worker_to_task_name(final_module_part)
end
defp map_worker_to_task_name("FastIndexingWorker"), do: "Fast Indexing Source"
defp map_worker_to_task_name("MediaDownloadWorker"), do: "Downloading Media"
defp map_worker_to_task_name("MediaCollectionIndexingWorker"), do: "Indexing Source"
defp map_worker_to_task_name("MediaQualityUpgradeWorker"), do: "Upgrading Media Quality"
defp map_worker_to_task_name("SourceMetadataStorageWorker"), do: "Fetching Source Metadata"
defp map_worker_to_task_name(other), do: other <> " (Report to Devs)"
defp task_to_record_name(%Task{} = task) do
case task do
%Task{source: source} when source != nil -> source.custom_name
%Task{media_item: mi} when mi != nil -> mi.title
_ -> "Unknown Record"
end
end
defp task_to_link(%Task{} = task) do
case task do
%Task{source: source} when source != nil -> ~p"/sources/#{source.id}"
%Task{media_item: mi} when mi != nil -> ~p"/sources/#{mi.source_id}/media/#{mi}"
_ -> "#"
end
end
defp format_datetime(nil), do: ""
defp format_datetime(datetime) do
TextComponents.datetime_in_zone(%{datetime: datetime, format: "%Y-%m-%d %H:%M"})
end
end
@@ -26,8 +26,7 @@ defmodule PinchflatWeb.Podcasts.PodcastController do
# if the source doesn't have any usable images
media_items =
MediaQuery.new()
|> MediaQuery.for_source(source)
|> MediaQuery.with_media_filepath()
|> where(^dynamic(^MediaQuery.for_source(source) and ^MediaQuery.downloaded()))
|> Repo.maybe_limit(1)
|> Repo.all()
@@ -10,19 +10,13 @@
<%= if match?([_|_], @search_results) do %>
<.table rows={@search_results} table_class="text-black dark:text-white">
<:col :let={result} label="Title">
<%= result.title %>
<.subtle_link href={~p"/sources/#{result.source_id}/media/#{result.id}"}>
<%= StringUtils.truncate(result.title, 35) %>
</.subtle_link>
</:col>
<:col :let={result} label="Excerpt">
<.highlight_search_terms text={result.matching_search_term} />
</:col>
<:col :let={result} label="" class="flex place-content-evenly">
<.link
href={~p"/sources/#{result.source_id}/media/#{result.id}"}
class="hover:text-secondary duration-200 ease-in-out mx-0.5"
>
<.icon name="hero-eye" />
</.link>
</:col>
</.table>
<% else %>
<p class="font-bold text-lg text-center text-black dark:text-white">No results found</p>
@@ -1,6 +1,8 @@
defmodule PinchflatWeb.Settings.SettingHTML do
use PinchflatWeb, :html
alias Pinchflat.Downloading.CodecParser
embed_templates "setting_html/*"
@doc """
@@ -21,11 +23,11 @@ defmodule PinchflatWeb.Settings.SettingHTML do
def diagnostic_info_string do
"""
App Version: #{Application.spec(:pinchflat)[:vsn]}
yt-dlp Version: #{Settings.get!(:yt_dlp_version)}
Apprise Version: #{Settings.get!(:apprise_version)}
System Architecture: #{to_string(:erlang.system_info(:system_architecture))}
Timezone: #{Application.get_env(:pinchflat, :timezone)}
- App Version: #{Application.spec(:pinchflat)[:vsn]}
- yt-dlp Version: #{Settings.get!(:yt_dlp_version)}
- Apprise Version: #{Settings.get!(:apprise_version)}
- System Architecture: #{to_string(:erlang.system_info(:system_architecture))}
- Timezone: #{Application.get_env(:pinchflat, :timezone)}
"""
end
end
@@ -0,0 +1,17 @@
<aside>
<h2 class="text-xl font-bold mb-2">Codec Preferences</h2>
<section class="ml-2 md:ml-4 mb-2 max-w-prose">
<section>
Available video codecs:
<ul class="list-disc ml-8">
<li :for={{codec, _} <- CodecParser.video_codec_map()}><%= codec %></li>
</ul>
</section>
<section class="mt-4">
Available audio codecs:
<ul class="list-disc ml-8">
<li :for={{codec, _} <- CodecParser.audio_codec_map()}><%= codec %></li>
</ul>
</section>
</section>
</aside>
@@ -1,13 +1,24 @@
<.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>
<h3 class="mt-2 md:mt-8 text-2xl text-black dark:text-white">
Notification Settings
</h3>
<section>
<section class="flex justify-between items-center mt-4">
<h3 class="text-2xl text-black dark:text-white">
Notification Settings
</h3>
<span class="cursor-pointer hover:underline" x-on:click="advancedMode = !advancedMode">
Editing Mode: <span x-text="advancedMode ? 'Advanced' : 'Basic'"></span>
</span>
</section>
<%= live_render(
@conn,
Pinchflat.Settings.AppriseServerLive,
@@ -15,5 +26,43 @@
) %>
</section>
<section class="mt-10" x-show="advancedMode">
<section>
<h3 class="text-2xl text-black dark:text-white">
Codec Options
</h3>
<p class="text-sm mt-2 max-w-prose">
The best available codec will be used if your preferred codecs are not found
</p>
<.input
id="video_codec_preference_string"
name="setting[video_codec_preference_string]"
value={Enum.join(f[:video_codec_preference].value, ">")}
placeholder="avc>vp9>av01"
type="text"
label="Video Codec Preference"
help="Order of preference for video codecs. Separate with >. Will be remuxed into an MP4 container. See below for available codecs"
inputclass="font-mono text-sm mr-4"
/>
<.input
id="audio_codec_preference_string"
name="setting[audio_codec_preference_string]"
value={Enum.join(f[:audio_codec_preference].value, ">")}
placeholder="mp4a>opus>aac"
type="text"
label="Audio Codec Preference"
help="Order of preference for audio codecs. Separate with >. See below for available codecs"
inputclass="font-mono text-sm mr-4"
/>
</section>
<div class="rounded-sm dark:bg-meta-4 p-4 md:p-6 mt-5">
<.codec_settings_help />
</div>
</section>
<.button class="mt-10 mb-4 sm:mb-8 w-full sm:w-auto" rounding="rounded-lg">Save Settings</.button>
</.simple_form>
@@ -1,25 +1,44 @@
defmodule PinchflatWeb.Sources.SourceController do
use PinchflatWeb, :controller
import Ecto.Query, warn: false
use Pinchflat.Media.MediaQuery
alias Pinchflat.Repo
alias Pinchflat.Tasks
alias Pinchflat.Sources
alias Pinchflat.Sources.Source
alias Pinchflat.Media.MediaItem
alias Pinchflat.Profiles.MediaProfile
alias Pinchflat.Downloading.DownloadingHelpers
alias Pinchflat.SlowIndexing.SlowIndexingHelpers
alias Pinchflat.Metadata.SourceMetadataStorageWorker
def index(conn, _params) do
sources =
Source
|> order_by(asc: :custom_name)
|> Repo.all()
|> Repo.preload(:media_profile)
source_query =
from s in Source,
as: :source,
inner_join: mp in assoc(s, :media_profile),
preload: [media_profile: mp],
order_by: [asc: s.custom_name],
select: map(s, ^Source.__schema__(:fields)),
select_merge: %{
downloaded_count:
subquery(
from m in MediaItem,
where: m.source_id == parent_as(:source).id,
where: ^MediaQuery.downloaded(),
select: count(m.id)
),
pending_count:
subquery(
from m in MediaItem,
join: s in assoc(m, :source),
where: m.source_id == parent_as(:source).id,
where: ^MediaQuery.pending(),
select: count(m.id)
)
}
render(conn, :index, sources: sources)
render(conn, :index, sources: Repo.all(source_query))
end
def new(conn, _params) do
@@ -15,13 +15,12 @@
<.table rows={@sources} table_class="text-black dark:text-white">
<:col :let={source} label="Name">
<.subtle_link href={~p"/sources/#{source.id}"}>
<%= source.custom_name || source.collection_name %>
<%= StringUtils.truncate(source.custom_name || source.collection_name, 35) %>
</.subtle_link>
</:col>
<:col :let={source} label="Type"><%= source.collection_type %></:col>
<:col :let={source} label="Should Download?">
<.icon name={if source.download_media, do: "hero-check", else: "hero-x-mark"} />
</:col>
<:col :let={source} label="Pending"><%= source.pending_count %></:col>
<:col :let={source} label="Downloaded"><%= source.downloaded_count %></:col>
<:col :let={source} label="Retention">
<%= if source.retention_period_days && source.retention_period_days > 0 do %>
<%= source.retention_period_days %> day(s)
@@ -35,7 +34,6 @@
</.subtle_link>
</:col>
<:col :let={source} label="" class="flex place-content-evenly">
<.icon_link href={~p"/sources/#{source.id}"} icon="hero-eye" class="mx-1" />
<.icon_link href={~p"/sources/#{source.id}/edit"} icon="hero-pencil-square" class="mx-1" />
</:col>
</.table>
@@ -8,7 +8,7 @@ defmodule Pinchflat.Sources.MediaItemTableLive do
@limit 10
def render(%{records: []} = assigns) do
def render(%{total_record_count: 0} = assigns) do
~H"""
<div class="mb-4 flex items-center">
<.icon_button icon_name="hero-arrow-path" class="h-10 w-10" phx-click="reload_page" />
@@ -20,19 +20,40 @@ defmodule Pinchflat.Sources.MediaItemTableLive do
def render(assigns) do
~H"""
<div>
<span class="mb-4 flex items-center">
<.icon_button icon_name="hero-arrow-path" class="h-10 w-10" phx-click="reload_page" tooltip="Refresh" />
<span class="ml-2">Showing <%= length(@records) %> of <%= @total_record_count %></span>
</span>
<header class="flex justify-between items-center mb-4">
<span class="flex items-center">
<.icon_button icon_name="hero-arrow-path" class="h-10 w-10" phx-click="reload_page" tooltip="Refresh" />
<span class="ml-2">Showing <%= length(@records) %> of <%= @filtered_record_count %></span>
</span>
<div class="bg-meta-4 rounded-md">
<div class="relative">
<span class="absolute left-2 top-1/2 -translate-y-1/2 flex">
<.icon name="hero-magnifying-glass" />
</span>
<form phx-change="search_term" phx-submit="search_term">
<input
type="text"
name="q"
value={@search_term}
placeholder="Search in table..."
class="w-full bg-transparent pl-9 pr-4 border-0 focus:ring-0 focus:outline-none"
phx-debounce="200"
/>
</form>
</div>
</div>
</header>
<.table rows={@records} table_class="text-white">
<:col :let={media_item} label="Title">
<.subtle_link href={~p"/sources/#{@source.id}/media/#{media_item.id}"}>
<%= StringUtils.truncate(media_item.title, 50) %>
</.subtle_link>
</:col>
<:col :let={media_item} label="" class="flex place-content-evenly">
<.icon_link href={~p"/sources/#{@source.id}/media/#{media_item.id}"} icon="hero-eye" class="mx-1" />
<.icon_link href={~p"/sources/#{@source.id}/media/#{media_item.id}/edit"} icon="hero-pencil-square" class="mx-1" />
<:col :let={media_item} :if={@media_state == "other"} label="Manually Ignored?">
<.icon name={if media_item.prevent_download, do: "hero-check", else: "hero-x-mark"} />
</:col>
<:col :let={media_item} label="" class="flex justify-end">
<.icon_link href={~p"/sources/#{@source.id}/media/#{media_item.id}/edit"} icon="hero-pencil-square" class="mr-4" />
</:col>
</.table>
<section class="flex justify-center mt-5">
@@ -43,36 +64,73 @@ defmodule Pinchflat.Sources.MediaItemTableLive do
end
def mount(_params, session, socket) do
PinchflatWeb.Endpoint.subscribe("media_table")
page = 1
media_state = session["media_state"]
source = Sources.get_source!(session["source_id"])
base_query = generate_base_query(source, media_state)
pagination_attrs = fetch_pagination_attributes(base_query, page)
pagination_attrs = fetch_pagination_attributes(base_query, page, nil)
{:ok, assign(socket, Map.merge(pagination_attrs, %{base_query: base_query, source: source}))}
new_assigns =
Map.merge(
pagination_attrs,
%{
base_query: base_query,
source: source,
media_state: media_state
}
)
{:ok, assign(socket, new_assigns)}
end
def handle_event("page_change", %{"direction" => direction}, %{assigns: assigns} = socket) do
direction = if direction == "inc", do: 1, else: -1
new_page = assigns.page + direction
new_assigns = fetch_pagination_attributes(assigns.base_query, new_page)
new_assigns = fetch_pagination_attributes(assigns.base_query, new_page, assigns.search_term)
{:noreply, assign(socket, new_assigns)}
end
def handle_event("reload_page", _params, %{assigns: assigns} = socket) do
new_assigns = fetch_pagination_attributes(assigns.base_query, assigns.page)
def handle_event("search_term", params, socket) do
search_term = Map.get(params, "q", nil)
new_assigns = fetch_pagination_attributes(socket.assigns.base_query, 1, search_term)
{:noreply, assign(socket, new_assigns)}
end
defp fetch_pagination_attributes(base_query, page) do
# This, along with the handle_info below, is a pattern to reload _all_
# tables on page rather than just the one that triggered the reload.
def handle_event("reload_page", _params, socket) do
PinchflatWeb.Endpoint.broadcast("media_table", "reload", nil)
{:noreply, socket}
end
def handle_info(%{topic: "media_table", event: "reload"}, %{assigns: assigns} = socket) do
new_assigns = fetch_pagination_attributes(assigns.base_query, assigns.page, assigns.search_term)
{:noreply, assign(socket, new_assigns)}
end
defp fetch_pagination_attributes(base_query, page, search_term) do
filtered_base_query = filter_base_query(base_query, search_term)
total_record_count = Repo.aggregate(base_query, :count, :id)
total_pages = max(ceil(total_record_count / @limit), 1)
filtered_record_count = Repo.aggregate(filtered_base_query, :count, :id)
total_pages = max(ceil(filtered_record_count / @limit), 1)
page = NumberUtils.clamp(page, 1, total_pages)
records = fetch_records(base_query, page)
records = fetch_records(filtered_base_query, page)
%{page: page, total_pages: total_pages, records: records, total_record_count: total_record_count}
%{
page: page,
total_pages: total_pages,
records: records,
search_term: search_term,
total_record_count: total_record_count,
filtered_record_count: filtered_record_count
}
end
defp fetch_records(base_query, page) do
@@ -86,15 +144,34 @@ defmodule Pinchflat.Sources.MediaItemTableLive do
defp generate_base_query(source, "pending") do
MediaQuery.new()
|> MediaQuery.for_source(source)
|> MediaQuery.where_pending_download()
|> order_by(desc: :id)
|> MediaQuery.require_assoc(:media_profile)
|> MediaQuery.require_assoc(:media_items_search_index)
|> where(^dynamic(^MediaQuery.for_source(source) and ^MediaQuery.pending()))
|> order_by(desc: fragment("rank"), desc: :id)
end
defp generate_base_query(source, "downloaded") do
MediaQuery.new()
|> MediaQuery.for_source(source)
|> MediaQuery.with_media_filepath()
|> order_by(desc: :id)
|> MediaQuery.require_assoc(:media_items_search_index)
|> where(^dynamic(^MediaQuery.for_source(source) and ^MediaQuery.downloaded()))
|> order_by(desc: fragment("rank"), desc: :id)
end
defp generate_base_query(source, "other") do
MediaQuery.new()
|> MediaQuery.require_assoc(:media_profile)
|> MediaQuery.require_assoc(:media_items_search_index)
|> where(
^dynamic(
^MediaQuery.for_source(source) and
(not (^MediaQuery.downloaded()) and not (^MediaQuery.pending()))
)
)
|> order_by(desc: fragment("rank"), desc: :id)
end
defp filter_base_query(base_query, search_term) do
base_query
|> where(^MediaQuery.matches_search_term(search_term))
end
end
@@ -23,7 +23,7 @@
<.actions_dropdown source={@source} conn={@conn} />
</:tab_append>
<:tab title="Source">
<:tab title="Source" id="source">
<div class="flex flex-col gap-10 text-white">
<h3 class="font-bold text-xl mt-6">Raw Attributes</h3>
<section>
@@ -36,21 +36,28 @@
<.list_items_from_map map={Map.from_struct(@source)} />
</div>
</:tab>
<:tab title="Pending Media">
<:tab title="Pending" id="pending">
<%= live_render(
@conn,
Pinchflat.Sources.MediaItemTableLive,
session: %{"source_id" => @source.id, "media_state" => "pending"}
) %>
</:tab>
<:tab title="Downloaded Media">
<:tab title="Downloaded" id="downloaded">
<%= live_render(
@conn,
Pinchflat.Sources.MediaItemTableLive,
session: %{"source_id" => @source.id, "media_state" => "downloaded"}
) %>
</:tab>
<:tab title="Pending Tasks">
<:tab title="Other" id="other">
<%= live_render(
@conn,
Pinchflat.Sources.MediaItemTableLive,
session: %{"source_id" => @source.id, "media_state" => "other"}
) %>
</:tab>
<:tab title="Tasks" id="tasks">
<%= if match?([_|_], @pending_tasks) do %>
<.table rows={@pending_tasks} table_class="text-black dark:text-white">
<:col :let={task} label="Worker">
+5
View File
@@ -19,6 +19,11 @@ defmodule PinchflatWeb.Telemetry do
Supervisor.init(children, strategy: :one_for_one)
end
@doc false
def job_state_change_broadcast(_event, _measure, _meta, _config) do
PinchflatWeb.Endpoint.broadcast("job:state", "change", nil)
end
def metrics do
[
# Phoenix Metrics
+11 -2
View File
@@ -4,7 +4,7 @@ defmodule Pinchflat.MixProject do
def project do
[
app: :pinchflat,
version: "2024.5.16",
version: "2024.5.24",
elixir: "~> 1.16",
elixirc_paths: elixirc_paths(Mix.env()),
start_permanent: Mix.env() == :prod,
@@ -48,6 +48,7 @@ defmodule Pinchflat.MixProject do
[
{:phoenix, "~> 1.7.10"},
{:phoenix_ecto, "~> 4.4"},
{:ecto, "~> 3.11.2"},
{:ecto_sql, "~> 3.10"},
{:ecto_sqlite3, ">= 0.0.0"},
{:ecto_sqlite3_extras, "~> 1.2.0"},
@@ -92,7 +93,15 @@ defmodule Pinchflat.MixProject do
test: ["ecto.create --quiet", "ecto.migrate --quiet", "test"],
"assets.setup": ["tailwind.install --if-missing", "esbuild.install --if-missing"],
"assets.build": ["tailwind default", "esbuild default"],
"assets.deploy": ["tailwind default --minify", "esbuild default --minify", "phx.digest"]
"assets.deploy": ["tailwind default --minify", "esbuild default --minify", "phx.digest"],
"ecto.migrate": [
"ecto.migrate",
~s(cmd [ -z "$MIX_ENV" ] && yarn run create-erd || echo "No ERD generated")
],
"ecto.rollback": [
"ecto.rollback",
~s(cmd [ -z "$MIX_ENV" ] && yarn run create-erd || echo "No ERD generated")
]
]
end
end
+1 -1
View File
@@ -12,7 +12,7 @@
"db_connection": {:hex, :db_connection, "2.6.0", "77d835c472b5b67fc4f29556dee74bf511bbafecdcaf98c27d27fa5918152086", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "c2f992d15725e721ec7fbc1189d4ecdb8afef76648c746a8e1cad35e3b8a35f3"},
"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.1", "4b4972b717e7ca83d30121b12998f5fcdc62ba0ed4f20fd390f16f3270d85c3e", [: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", "ebd3d3772cd0dfcd8d772659e41ed527c28b2a8bde4b00fe03e0463da0f1983b"},
"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_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"},
+6 -2
View File
@@ -1,7 +1,11 @@
{
"description": "Prettier is used for linting of all files so this package has to live in the root of the project. Use the other package.json files for dependencies. Also, look into making this global or something to remove the need for this file.",
"devDependencies": {
"prettier": "3.2.4"
"prettier": "3.2.4",
"sqleton": "^2.2.0"
},
"license": "<See the LICENSE file>"
"scripts": {
"create-erd": "sqleton -o priv/repo/erd.png priv/repo/pinchflat_dev.db"
},
"private": true
}
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 449 KiB

@@ -0,0 +1,10 @@
defmodule Pinchflat.Repo.Migrations.AddCodecPreferencesToSettings do
use Ecto.Migration
def change do
alter table(:settings) do
add :video_codec_preference, {:array, :string}, default: []
add :audio_codec_preference, {:array, :string}, default: []
end
end
end
@@ -0,0 +1,60 @@
defmodule Pinchflat.Repo.Migrations.ChangeMediaItemsSearchIndexTokenizer do
use Ecto.Migration
def up do
# These all need to run as part of separate `execute` blocks. Do NOT ask me why.
execute "DROP TRIGGER IF EXISTS media_items_search_index_insert;"
execute "DROP TRIGGER IF EXISTS media_items_search_index_update;"
execute "DROP TRIGGER IF EXISTS media_items_search_index_delete;"
execute "DROP TABLE IF EXISTS media_items_search_index;"
execute """
CREATE VIRTUAL TABLE media_items_search_index USING fts5(
title,
description,
tokenize=trigram
);
"""
execute """
CREATE TRIGGER media_items_search_index_insert AFTER INSERT ON media_items BEGIN
INSERT INTO media_items_search_index(
rowid,
title,
description
)
VALUES(
new.id,
new.title,
new.description
);
END;
"""
execute """
CREATE TRIGGER media_items_search_index_update AFTER UPDATE ON media_items BEGIN
UPDATE media_items_search_index SET
title = new.title,
description = new.description
WHERE
rowid = old.id;
END;
"""
execute """
CREATE TRIGGER media_items_search_index_delete AFTER DELETE ON media_items BEGIN
DELETE FROM media_items_search_index WHERE rowid = old.id;
END;
"""
# Fully re-index the media_items table
execute """
INSERT INTO media_items_search_index(rowid, title, description)
SELECT id, title, description FROM media_items;
"""
end
def down do
execute "DROP TABLE media_items_search_index;"
end
end
@@ -0,0 +1,70 @@
defmodule Pinchflat.Downloading.CodecParserTest do
use Pinchflat.DataCase
alias Pinchflat.Settings
alias Pinchflat.Downloading.CodecParser
describe "generate_vcodec_string_from_settings/1" do
test "returns a default vcodec string when setting isn't set" do
Settings.set(video_codec_preference: [])
assert "bestvideo[vcodec~='^avc']/bestvideo" == CodecParser.generate_vcodec_string_from_settings()
end
test "generates a vcodec string" do
Settings.set(video_codec_preference: ["av01"])
assert "bestvideo[vcodec~='^av01']/bestvideo" == CodecParser.generate_vcodec_string_from_settings()
end
end
describe "generate_acodec_string_from_settings/1" do
test "returns a default acodec string when setting isn't set" do
Settings.set(audio_codec_preference: [])
assert "bestaudio[acodec~='^mp4a']/bestaudio" == CodecParser.generate_acodec_string_from_settings()
end
test "generates an acodec string" do
Settings.set(audio_codec_preference: ["mp3"])
assert "bestaudio[acodec~='^mp3']/bestaudio" == CodecParser.generate_acodec_string_from_settings()
end
end
describe "generate_vcodec_string/1" do
test "returns a default vcodec string when nil" do
assert "bestvideo[vcodec~='^avc']/bestvideo" == CodecParser.generate_vcodec_string(nil)
end
test "returns a default vcodec string when empty" do
assert "bestvideo[vcodec~='^avc']/bestvideo" == CodecParser.generate_vcodec_string([])
end
test "generates a vcodec string" do
assert "bestvideo[vcodec~='^av01']/bestvideo" == CodecParser.generate_vcodec_string(["av01"])
end
test "ignores options that don't exist" do
assert "bestvideo[vcodec~='^av01']/bestvideo" == CodecParser.generate_vcodec_string(["av01", "foo"])
end
end
describe "generate_acodec_string/1" do
test "returns a default acodec string when nil" do
assert "bestaudio[acodec~='^mp4a']/bestaudio" == CodecParser.generate_acodec_string(nil)
end
test "returns a default acodec string when empty" do
assert "bestaudio[acodec~='^mp4a']/bestaudio" == CodecParser.generate_acodec_string([])
end
test "generates an acodec string" do
assert "bestaudio[acodec~='^mp3']/bestaudio" == CodecParser.generate_acodec_string(["mp3"])
end
test "ignores options that don't exist" do
assert "bestaudio[acodec~='^mp3']/bestaudio" == CodecParser.generate_acodec_string(["mp3", "foo"])
end
end
end
@@ -6,6 +6,7 @@ defmodule Pinchflat.Downloading.DownloadOptionBuilderTest do
alias Pinchflat.Sources
alias Pinchflat.Profiles
alias Pinchflat.Settings
alias Pinchflat.Utils.FilesystemUtils
alias Pinchflat.Downloading.DownloadOptionBuilder
@@ -244,8 +245,8 @@ defmodule Pinchflat.Downloading.DownloadOptionBuilderTest do
end
describe "build/1 when testing quality options" do
test "it includes quality options" do
resolutions = ["360", "480", "720", "1080", "2160"]
test "includes quality options" do
resolutions = ["360", "480", "720", "1080", "2160", "4320"]
Enum.each(resolutions, fn resolution ->
resolution_atom = String.to_existing_atom(resolution <> "p")
@@ -255,21 +256,35 @@ defmodule Pinchflat.Downloading.DownloadOptionBuilderTest do
media_item = Repo.preload(media_item_fixture(source_id: source.id), source: :media_profile)
assert {:ok, res} = DownloadOptionBuilder.build(media_item)
assert {:format_sort, "res:#{resolution},+codec:avc:m4a"} in res
assert {:format_sort, "res:#{resolution}"} in res
assert {:format, "((bestvideo[vcodec~='^avc']/bestvideo)+(bestaudio[acodec~='^mp4a']/bestaudio))/best"} in res
assert {:remux_video, "mp4"} in res
end)
end
test "it includes quality options for audio only", %{media_item: media_item} do
test "includes quality options for audio only", %{media_item: media_item} do
media_item = update_media_profile_attribute(media_item, %{preferred_resolution: :audio})
assert {:ok, res} = DownloadOptionBuilder.build(media_item)
assert :extract_audio in res
assert {:format, "bestaudio[ext=m4a]/bestaudio[ext=mp3]/bestaudio/best[ext=m4a]/best[ext=mp3]/best"} in res
assert {:format, "bestaudio[acodec~='^mp4a']/bestaudio/best"} in res
refute {:remux_video, "mp4"} in res
end
test "includes custom quality options if specified", %{media_item: media_item} do
Settings.set(video_codec_preference: ["av01"])
Settings.set(audio_codec_preference: ["aac"])
media_item = update_media_profile_attribute(media_item, %{preferred_resolution: :"1080p"})
assert {:ok, res} = DownloadOptionBuilder.build(media_item)
assert {:format, "((bestvideo[vcodec~='^av01']/bestvideo)+(bestaudio[acodec~='^aac']/bestaudio))/best"} in res
end
end
describe "build/1 when testing sponsorblock options" do
@@ -114,7 +114,7 @@ defmodule Pinchflat.Downloading.DownloadingHelpersTest do
describe "kickoff_redownload_for_existing_media/1" do
test "enqueues a download job for each downloaded media item" do
source = source_fixture()
media_item = media_item_fixture(source_id: source.id, media_downloaded_at: now())
media_item = media_item_fixture(source_id: source.id, media_filepath: "some/filepath.mp4")
assert [{:ok, _}] = DownloadingHelpers.kickoff_redownload_for_existing_media(source)
@@ -124,14 +124,14 @@ defmodule Pinchflat.Downloading.DownloadingHelpersTest do
test "doesn't enqueue jobs for media that should be ignored" do
source = source_fixture()
other_source = source_fixture()
_not_downloaded = media_item_fixture(source_id: source.id, media_downloaded_at: nil)
_other_source = media_item_fixture(source_id: other_source.id, media_downloaded_at: now())
_not_downloaded = media_item_fixture(source_id: source.id, media_filepath: nil)
_other_source = media_item_fixture(source_id: other_source.id, media_filepath: "some/filepath.mp4")
_download_prevented =
media_item_fixture(source_id: source.id, media_downloaded_at: now(), prevent_download: true)
media_item_fixture(source_id: source.id, media_filepath: "some/filepath.mp4", prevent_download: true)
_culled =
media_item_fixture(source_id: source.id, media_downloaded_at: now(), culled_at: now())
media_item_fixture(source_id: source.id, media_filepath: "some/filepath.mp4", culled_at: now())
assert [] = DownloadingHelpers.kickoff_redownload_for_existing_media(source)
+13
View File
@@ -405,7 +405,9 @@ defmodule Pinchflat.MediaTest do
describe "list_pending_media_items_for/1" do
test "it returns pending without a filepath for a given source" do
source = source_fixture()
other_source = source_fixture()
media_item = media_item_fixture(%{source_id: source.id, media_filepath: nil})
_other_media_item = media_item_fixture(%{source_id: other_source.id, media_filepath: nil})
assert Media.list_pending_media_items_for(source) == [media_item]
end
@@ -725,6 +727,17 @@ defmodule Pinchflat.MediaTest do
test "returns an empty list when the search term is nil" do
assert [] = Media.search(nil)
end
test "doesn't blow up if there's an apostrophe or quotes in the search term" do
assert [] = Media.search("don't expl'ode")
assert [] = Media.search(~s(dont expl"o"de))
assert [] = Media.search(~s(dont explo"de))
end
test "doesn't blow up if there is a trailing operand" do
assert [] = Media.search("foo OR")
assert [] = Media.search("foo AND")
end
end
describe "get_media_item!/1" do
+60
View File
@@ -78,4 +78,64 @@ defmodule Pinchflat.SettingsTest do
assert %Ecto.Changeset{} = Settings.change_setting(setting, %{onboarding: true})
end
end
describe "change_setting/2 when testing codec preferences" do
test "converts (video|audio)_codec_preference_string to an array" do
setting = Settings.record()
new_setting = %{
video_codec_preference_string: "avc>vp9",
audio_codec_preference_string: "aac>opus"
}
changeset = Settings.change_setting(setting, new_setting)
assert ["avc", "vp9"] = changeset.changes.video_codec_preference
assert ["aac", "opus"] = changeset.changes.audio_codec_preference
end
test "removes whitespace from (video|audio)_codec_preference" do
setting = Settings.record()
new_setting = %{
video_codec_preference_string: " avc > > vp9 ",
audio_codec_preference_string: "aac> opus "
}
changeset = Settings.change_setting(setting, new_setting)
assert ["avc", "vp9"] = changeset.changes.video_codec_preference
assert ["aac", "opus"] = changeset.changes.audio_codec_preference
end
test "downcases (video|audio)_codec_preference" do
setting = Settings.record()
new_setting = %{
video_codec_preference_string: "AVC>VP9",
audio_codec_preference_string: "AAC>OPUS"
}
changeset = Settings.change_setting(setting, new_setting)
assert ["avc", "vp9"] = changeset.changes.video_codec_preference
assert ["aac", "opus"] = changeset.changes.audio_codec_preference
end
test "an empty value will remove the codec settings" do
Settings.set(video_codec_preference: ["avc", "vp9"])
Settings.set(audio_codec_preference: ["aac", "opus"])
setting = Settings.record()
new_setting = %{
video_codec_preference_string: "",
audio_codec_preference_string: ""
}
changeset = Settings.change_setting(setting, new_setting)
assert [] = changeset.changes.video_codec_preference
assert [] = changeset.changes.audio_codec_preference
end
end
end
@@ -0,0 +1,111 @@
defmodule PinchflatWeb.Pages.JobTableLiveTest do
use PinchflatWeb.ConnCase
import Ecto.Query, warn: false
import Phoenix.LiveViewTest
import Pinchflat.MediaFixtures
import Pinchflat.SourcesFixtures
alias Pinchflat.Utils.StringUtils
alias Pinchflat.Pages.JobTableLive
alias Pinchflat.Downloading.MediaDownloadWorker
alias Pinchflat.FastIndexing.FastIndexingWorker
describe "initial rendering" do
test "shows message when no records", %{conn: conn} do
{:ok, _view, html} = live_isolated(conn, JobTableLive, session: %{})
assert html =~ "Nothing Here!"
refute html =~ "Subject"
end
test "shows records when present", %{conn: conn} do
{_source, _media_item, _task, _job} = create_media_item_job()
{:ok, _view, html} = live_isolated(conn, JobTableLive, session: %{})
assert html =~ "Subject"
end
test "doesn't show records when not in executing state", %{conn: conn} do
{_source, _media_item, _task, _job} = create_media_item_job(:scheduled)
{_source, _media_item, _task, _job} = create_media_item_job(:completed)
{:ok, _view, html} = live_isolated(conn, JobTableLive, session: %{})
assert html =~ "Nothing Here!"
refute html =~ "Subject"
end
end
describe "job rendering" do
test "shows worker name", %{conn: conn} do
{_source, _media_item, _task, _job} = create_media_item_job()
{:ok, _view, html} = live_isolated(conn, JobTableLive, session: %{})
assert html =~ "Downloading Media"
end
test "shows the media item title", %{conn: conn} do
{_source, media_item, _task, _job} = create_media_item_job()
{:ok, _view, html} = live_isolated(conn, JobTableLive, session: %{})
assert html =~ StringUtils.truncate(media_item.title, 35)
end
test "shows a media item link", %{conn: conn} do
{_source, media_item, _task, _job} = create_media_item_job()
{:ok, _view, html} = live_isolated(conn, JobTableLive, session: %{})
assert html =~ ~p"/sources/#{media_item.source_id}/media/#{media_item}"
end
test "shows the source custom name", %{conn: conn} do
{source, _task, _job} = create_source_job()
{:ok, _view, html} = live_isolated(conn, JobTableLive, session: %{})
assert html =~ StringUtils.truncate(source.custom_name, 35)
end
test "shows a source link", %{conn: conn} do
{source, _task, _job} = create_source_job()
{:ok, _view, html} = live_isolated(conn, JobTableLive, session: %{})
assert html =~ ~p"/sources/#{source.id}"
end
test "listens for job:state change events", %{conn: conn} do
{_source, _media_item, _task, _job} = create_media_item_job()
{:ok, _view, _html} = live_isolated(conn, JobTableLive, session: %{})
PinchflatWeb.Endpoint.broadcast("job:state", "change", nil)
assert_receive %Phoenix.Socket.Broadcast{topic: "job:state", event: "change", payload: nil}
end
end
defp create_media_item_job(job_state \\ :executing) do
source = source_fixture()
media_item = media_item_fixture(source_id: source.id)
{:ok, task} = MediaDownloadWorker.kickoff_with_task(media_item)
Oban.Job
|> where([j], j.id == ^task.job_id)
|> Repo.update_all(set: [state: to_string(job_state)])
job = Repo.get!(Oban.Job, task.job_id)
{source, media_item, task, job}
end
defp create_source_job(job_state \\ :executing) do
source = source_fixture()
{:ok, task} = FastIndexingWorker.kickoff_with_task(source)
Oban.Job
|> where([j], j.id == ^task.job_id)
|> Repo.update_all(set: [state: to_string(job_state)])
job = Repo.get!(Oban.Job, task.job_id)
{source, task, job}
end
end
@@ -4,6 +4,7 @@ defmodule PinchflatWeb.Sources.MediaItemTableLiveTest do
import Phoenix.LiveViewTest
import Pinchflat.MediaFixtures
import Pinchflat.SourcesFixtures
import Pinchflat.ProfilesFixtures
alias Pinchflat.Sources.MediaItemTableLive
@@ -34,10 +35,8 @@ defmodule PinchflatWeb.Sources.MediaItemTableLiveTest do
describe "media_state" do
test "shows pending media when pending", %{conn: conn, source: source} do
downloaded_media_item = media_item_fixture(source_id: source.id, title: "DL-#{Enum.random(0..9999)}")
pending_media_item =
media_item_fixture(source_id: source.id, media_filepath: nil, title: "P-#{Enum.random(0..9999)}")
downloaded_media_item = media_item_fixture(source_id: source.id)
pending_media_item = media_item_fixture(source_id: source.id, media_filepath: nil)
{:ok, _view, html} = live_isolated(conn, MediaItemTableLive, session: create_session(source, "pending"))
@@ -54,6 +53,29 @@ defmodule PinchflatWeb.Sources.MediaItemTableLiveTest do
assert html =~ downloaded_media_item.title
refute html =~ pending_media_item.title
end
test "shows records that aren't pending or downloaded when other", %{conn: conn} do
media_profile = media_profile_fixture(shorts_behaviour: :exclude)
source = source_fixture(media_profile_id: media_profile.id)
downloaded_media_item = media_item_fixture(source_id: source.id)
pending_media_item = media_item_fixture(source_id: source.id, media_filepath: nil)
other_media_item = media_item_fixture(source_id: source.id, media_filepath: nil, short_form_content: true)
{:ok, _view, html} = live_isolated(conn, MediaItemTableLive, session: create_session(source, "other"))
assert html =~ other_media_item.title
refute html =~ downloaded_media_item.title
refute html =~ pending_media_item.title
end
test "shows 'Manually Ignored' column when other", %{conn: conn, source: source} do
_media_item = media_item_fixture(source_id: source.id, prevent_download: true, media_filepath: nil)
{:ok, _view, html} = live_isolated(conn, MediaItemTableLive, session: create_session(source, "other"))
assert html =~ "Manually Ignored?"
end
end
defp create_session(source, media_state \\ "pending") do
+1 -1
View File
@@ -17,7 +17,7 @@ defmodule Pinchflat.MediaFixtures do
attrs
|> Enum.into(%{
media_id: media_id,
title: Faker.Commerce.product_name(),
title: Faker.Commerce.product_name() <> " #{media_id}",
original_url: "https://www.youtube.com/watch?v=#{media_id}",
livestream: false,
short_form_content: false,
+943
View File
@@ -2,7 +2,950 @@
# yarn lockfile v1
"@gar/promisify@^1.0.1":
version "1.1.3"
resolved "https://registry.yarnpkg.com/@gar/promisify/-/promisify-1.1.3.tgz#555193ab2e3bb3b6adc3d551c9c030d9e860daf6"
integrity sha512-k2Ty1JcVojjJFwrg/ThKi2ujJ7XNLYaFGNB/bWT9wGR+oSMJHMa5w+CUq6p/pVrKeNNgA7pCqEcjSnHVoqJQFw==
"@npmcli/fs@^1.0.0":
version "1.1.1"
resolved "https://registry.yarnpkg.com/@npmcli/fs/-/fs-1.1.1.tgz#72f719fe935e687c56a4faecf3c03d06ba593257"
integrity sha512-8KG5RD0GVP4ydEzRn/I4BNDuxDtqVbOdm8675T49OIG/NGhaK0pjPX7ZcDlvKYbA+ulvVK3ztfcF4uBdOxuJbQ==
dependencies:
"@gar/promisify" "^1.0.1"
semver "^7.3.5"
"@npmcli/move-file@^1.0.1":
version "1.1.2"
resolved "https://registry.yarnpkg.com/@npmcli/move-file/-/move-file-1.1.2.tgz#1a82c3e372f7cae9253eb66d72543d6b8685c674"
integrity sha512-1SUf/Cg2GzGDyaf15aR9St9TWlb+XvbZXWpDx8YKs7MLzMH/BCeopv+y9vzrzgkfykCGuWOlSu3mZhj2+FQcrg==
dependencies:
mkdirp "^1.0.4"
rimraf "^3.0.2"
"@tootallnate/once@1":
version "1.1.2"
resolved "https://registry.yarnpkg.com/@tootallnate/once/-/once-1.1.2.tgz#ccb91445360179a04e7fe6aff78c00ffc1eeaf82"
integrity sha512-RbzJvlNzmRq5c3O09UipeuXno4tA1FE6ikOjxZK0tuxVv3412l64l5t1W5pj4+rJq9vpkm/kwiR07aZXnsKPxw==
abbrev@1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-1.1.1.tgz#f8f2c887ad10bf67f634f005b6987fed3179aac8"
integrity sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==
agent-base@6, agent-base@^6.0.2:
version "6.0.2"
resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-6.0.2.tgz#49fff58577cfee3f37176feab4c22e00f86d7f77"
integrity sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==
dependencies:
debug "4"
agentkeepalive@^4.1.3:
version "4.5.0"
resolved "https://registry.yarnpkg.com/agentkeepalive/-/agentkeepalive-4.5.0.tgz#2673ad1389b3c418c5a20c5d7364f93ca04be923"
integrity sha512-5GG/5IbQQpC9FpkRGsSvZI5QYeSCzlJHdpBQntCsuTOxhKD8lqKhrleg2Yi7yvMIf82Ycmmqln9U8V9qwEiJew==
dependencies:
humanize-ms "^1.2.1"
aggregate-error@^3.0.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/aggregate-error/-/aggregate-error-3.1.0.tgz#92670ff50f5359bdb7a3e0d40d0ec30c5737687a"
integrity sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==
dependencies:
clean-stack "^2.0.0"
indent-string "^4.0.0"
ansi-regex@^5.0.1:
version "5.0.1"
resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.1.tgz#082cb2c89c9fe8659a311a53bd6a4dc5301db304"
integrity sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==
ansi-styles@^4.0.0:
version "4.3.0"
resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-4.3.0.tgz#edd803628ae71c04c85ae7a0906edad34b648937"
integrity sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==
dependencies:
color-convert "^2.0.1"
"aproba@^1.0.3 || ^2.0.0":
version "2.0.0"
resolved "https://registry.yarnpkg.com/aproba/-/aproba-2.0.0.tgz#52520b8ae5b569215b354efc0caa3fe1e45a8adc"
integrity sha512-lYe4Gx7QT+MKGbDsA+Z+he/Wtef0BiwDOlK/XkBrdfsh9J/jPPXbX0tE9x9cl27Tmu5gg3QUbUrQYa/y+KOHPQ==
are-we-there-yet@^3.0.0:
version "3.0.1"
resolved "https://registry.yarnpkg.com/are-we-there-yet/-/are-we-there-yet-3.0.1.tgz#679df222b278c64f2cdba1175cdc00b0d96164bd"
integrity sha512-QZW4EDmGwlYur0Yyf/b2uGucHQMa8aFUP7eu9ddR73vvhFyt4V0Vl3QHPcTNJ8l6qYOBdxgXdnBXQrHilfRQBg==
dependencies:
delegates "^1.0.0"
readable-stream "^3.6.0"
balanced-match@^1.0.0:
version "1.0.2"
resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee"
integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==
base64-js@^1.3.1:
version "1.5.1"
resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a"
integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==
bindings@^1.5.0:
version "1.5.0"
resolved "https://registry.yarnpkg.com/bindings/-/bindings-1.5.0.tgz#10353c9e945334bc0511a6d90b38fbc7c9c504df"
integrity sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==
dependencies:
file-uri-to-path "1.0.0"
bl@^4.0.3:
version "4.1.0"
resolved "https://registry.yarnpkg.com/bl/-/bl-4.1.0.tgz#451535264182bec2fbbc83a62ab98cf11d9f7b3a"
integrity sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==
dependencies:
buffer "^5.5.0"
inherits "^2.0.4"
readable-stream "^3.4.0"
brace-expansion@^1.1.7:
version "1.1.11"
resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd"
integrity sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==
dependencies:
balanced-match "^1.0.0"
concat-map "0.0.1"
buffer@^5.5.0:
version "5.7.1"
resolved "https://registry.yarnpkg.com/buffer/-/buffer-5.7.1.tgz#ba62e7c13133053582197160851a8f648e99eed0"
integrity sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==
dependencies:
base64-js "^1.3.1"
ieee754 "^1.1.13"
cacache@^15.2.0:
version "15.3.0"
resolved "https://registry.yarnpkg.com/cacache/-/cacache-15.3.0.tgz#dc85380fb2f556fe3dda4c719bfa0ec875a7f1eb"
integrity sha512-VVdYzXEn+cnbXpFgWs5hTT7OScegHVmLhJIR8Ufqk3iFD6A6j5iSX1KuBTfNEv4tdJWE2PzA6IVFtcLC7fN9wQ==
dependencies:
"@npmcli/fs" "^1.0.0"
"@npmcli/move-file" "^1.0.1"
chownr "^2.0.0"
fs-minipass "^2.0.0"
glob "^7.1.4"
infer-owner "^1.0.4"
lru-cache "^6.0.0"
minipass "^3.1.1"
minipass-collect "^1.0.2"
minipass-flush "^1.0.5"
minipass-pipeline "^1.2.2"
mkdirp "^1.0.3"
p-map "^4.0.0"
promise-inflight "^1.0.1"
rimraf "^3.0.2"
ssri "^8.0.1"
tar "^6.0.2"
unique-filename "^1.1.1"
chownr@^1.1.1:
version "1.1.4"
resolved "https://registry.yarnpkg.com/chownr/-/chownr-1.1.4.tgz#6fc9d7b42d32a583596337666e7d08084da2cc6b"
integrity sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==
chownr@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/chownr/-/chownr-2.0.0.tgz#15bfbe53d2eab4cf70f18a8cd68ebe5b3cb1dece"
integrity sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==
clean-stack@^2.0.0:
version "2.2.0"
resolved "https://registry.yarnpkg.com/clean-stack/-/clean-stack-2.2.0.tgz#ee8472dbb129e727b31e8a10a427dee9dfe4008b"
integrity sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==
cliui@^8.0.1:
version "8.0.1"
resolved "https://registry.yarnpkg.com/cliui/-/cliui-8.0.1.tgz#0c04b075db02cbfe60dc8e6cf2f5486b1a3608aa"
integrity sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==
dependencies:
string-width "^4.2.0"
strip-ansi "^6.0.1"
wrap-ansi "^7.0.0"
color-convert@^2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-2.0.1.tgz#72d3a68d598c9bdb3af2ad1e84f21d896abd4de3"
integrity sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==
dependencies:
color-name "~1.1.4"
color-name@~1.1.4:
version "1.1.4"
resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2"
integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==
color-support@^1.1.3:
version "1.1.3"
resolved "https://registry.yarnpkg.com/color-support/-/color-support-1.1.3.tgz#93834379a1cc9a0c61f82f52f0d04322251bd5a2"
integrity sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==
concat-map@0.0.1:
version "0.0.1"
resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b"
integrity sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==
console-control-strings@^1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/console-control-strings/-/console-control-strings-1.1.0.tgz#3d7cf4464db6446ea644bf4b39507f9851008e8e"
integrity sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==
debug@4, debug@^4.3.3:
version "4.3.4"
resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.4.tgz#1319f6579357f2338d3337d2cdd4914bb5dcc865"
integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==
dependencies:
ms "2.1.2"
decompress-response@^6.0.0:
version "6.0.0"
resolved "https://registry.yarnpkg.com/decompress-response/-/decompress-response-6.0.0.tgz#ca387612ddb7e104bd16d85aab00d5ecf09c66fc"
integrity sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==
dependencies:
mimic-response "^3.1.0"
deep-extend@^0.6.0:
version "0.6.0"
resolved "https://registry.yarnpkg.com/deep-extend/-/deep-extend-0.6.0.tgz#c4fa7c95404a17a9c3e8ca7e1537312b736330ac"
integrity sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==
delegates@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/delegates/-/delegates-1.0.0.tgz#84c6e159b81904fdca59a0ef44cd870d31250f9a"
integrity sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==
detect-libc@^2.0.0:
version "2.0.3"
resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-2.0.3.tgz#f0cd503b40f9939b894697d19ad50895e30cf700"
integrity sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==
emoji-regex@^8.0.0:
version "8.0.0"
resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37"
integrity sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==
encoding@^0.1.12:
version "0.1.13"
resolved "https://registry.yarnpkg.com/encoding/-/encoding-0.1.13.tgz#56574afdd791f54a8e9b2785c0582a2d26210fa9"
integrity sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==
dependencies:
iconv-lite "^0.6.2"
end-of-stream@^1.1.0, end-of-stream@^1.4.1:
version "1.4.4"
resolved "https://registry.yarnpkg.com/end-of-stream/-/end-of-stream-1.4.4.tgz#5ae64a5f45057baf3626ec14da0ca5e4b2431eb0"
integrity sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==
dependencies:
once "^1.4.0"
env-paths@^2.2.0:
version "2.2.1"
resolved "https://registry.yarnpkg.com/env-paths/-/env-paths-2.2.1.tgz#420399d416ce1fbe9bc0a07c62fa68d67fd0f8f2"
integrity sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==
err-code@^2.0.2:
version "2.0.3"
resolved "https://registry.yarnpkg.com/err-code/-/err-code-2.0.3.tgz#23c2f3b756ffdfc608d30e27c9a941024807e7f9"
integrity sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA==
escalade@^3.1.1:
version "3.1.2"
resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.1.2.tgz#54076e9ab29ea5bf3d8f1ed62acffbb88272df27"
integrity sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA==
expand-template@^2.0.3:
version "2.0.3"
resolved "https://registry.yarnpkg.com/expand-template/-/expand-template-2.0.3.tgz#6e14b3fcee0f3a6340ecb57d2e8918692052a47c"
integrity sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==
file-uri-to-path@1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz#553a7b8446ff6f684359c445f1e37a05dacc33dd"
integrity sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==
fs-constants@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/fs-constants/-/fs-constants-1.0.0.tgz#6be0de9be998ce16af8afc24497b9ee9b7ccd9ad"
integrity sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==
fs-minipass@^2.0.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/fs-minipass/-/fs-minipass-2.1.0.tgz#7f5036fdbf12c63c169190cbe4199c852271f9fb"
integrity sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==
dependencies:
minipass "^3.0.0"
fs.realpath@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f"
integrity sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==
gauge@^4.0.3:
version "4.0.4"
resolved "https://registry.yarnpkg.com/gauge/-/gauge-4.0.4.tgz#52ff0652f2bbf607a989793d53b751bef2328dce"
integrity sha512-f9m+BEN5jkg6a0fZjleidjN51VE1X+mPFQ2DJ0uv1V39oCLCbsGe6yjbBnp7eK7z/+GAon99a3nHuqbuuthyPg==
dependencies:
aproba "^1.0.3 || ^2.0.0"
color-support "^1.1.3"
console-control-strings "^1.1.0"
has-unicode "^2.0.1"
signal-exit "^3.0.7"
string-width "^4.2.3"
strip-ansi "^6.0.1"
wide-align "^1.1.5"
get-caller-file@^2.0.5:
version "2.0.5"
resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e"
integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==
github-from-package@0.0.0:
version "0.0.0"
resolved "https://registry.yarnpkg.com/github-from-package/-/github-from-package-0.0.0.tgz#97fb5d96bfde8973313f20e8288ef9a167fa64ce"
integrity sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==
glob@^7.1.3, glob@^7.1.4:
version "7.2.3"
resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.3.tgz#b8df0fb802bbfa8e89bd1d938b4e16578ed44f2b"
integrity sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==
dependencies:
fs.realpath "^1.0.0"
inflight "^1.0.4"
inherits "2"
minimatch "^3.1.1"
once "^1.3.0"
path-is-absolute "^1.0.0"
graceful-fs@^4.2.6:
version "4.2.11"
resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.11.tgz#4183e4e8bf08bb6e05bbb2f7d2e0c8f712ca40e3"
integrity sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==
has-unicode@^2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/has-unicode/-/has-unicode-2.0.1.tgz#e0e6fe6a28cf51138855e086d1691e771de2a8b9"
integrity sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==
http-cache-semantics@^4.1.0:
version "4.1.1"
resolved "https://registry.yarnpkg.com/http-cache-semantics/-/http-cache-semantics-4.1.1.tgz#abe02fcb2985460bf0323be664436ec3476a6d5a"
integrity sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ==
http-proxy-agent@^4.0.1:
version "4.0.1"
resolved "https://registry.yarnpkg.com/http-proxy-agent/-/http-proxy-agent-4.0.1.tgz#8a8c8ef7f5932ccf953c296ca8291b95aa74aa3a"
integrity sha512-k0zdNgqWTGA6aeIRVpvfVob4fL52dTfaehylg0Y4UvSySvOq/Y+BOyPrgpUrA7HylqvU8vIZGsRuXmspskV0Tg==
dependencies:
"@tootallnate/once" "1"
agent-base "6"
debug "4"
https-proxy-agent@^5.0.0:
version "5.0.1"
resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz#c59ef224a04fe8b754f3db0063a25ea30d0005d6"
integrity sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==
dependencies:
agent-base "6"
debug "4"
humanize-ms@^1.2.1:
version "1.2.1"
resolved "https://registry.yarnpkg.com/humanize-ms/-/humanize-ms-1.2.1.tgz#c46e3159a293f6b896da29316d8b6fe8bb79bbed"
integrity sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==
dependencies:
ms "^2.0.0"
iconv-lite@^0.6.2:
version "0.6.3"
resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.6.3.tgz#a52f80bf38da1952eb5c681790719871a1a72501"
integrity sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==
dependencies:
safer-buffer ">= 2.1.2 < 3.0.0"
ieee754@^1.1.13:
version "1.2.1"
resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352"
integrity sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==
imurmurhash@^0.1.4:
version "0.1.4"
resolved "https://registry.yarnpkg.com/imurmurhash/-/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea"
integrity sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==
indent-string@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/indent-string/-/indent-string-4.0.0.tgz#624f8f4497d619b2d9768531d58f4122854d7251"
integrity sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==
infer-owner@^1.0.4:
version "1.0.4"
resolved "https://registry.yarnpkg.com/infer-owner/-/infer-owner-1.0.4.tgz#c4cefcaa8e51051c2a40ba2ce8a3d27295af9467"
integrity sha512-IClj+Xz94+d7irH5qRyfJonOdfTzuDaifE6ZPWfx0N0+/ATZCbuTPq2prFl526urkQd90WyUKIh1DfBQ2hMz9A==
inflight@^1.0.4:
version "1.0.6"
resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9"
integrity sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==
dependencies:
once "^1.3.0"
wrappy "1"
inherits@2, inherits@^2.0.3, inherits@^2.0.4:
version "2.0.4"
resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c"
integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==
ini@~1.3.0:
version "1.3.8"
resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.8.tgz#a29da425b48806f34767a4efce397269af28432c"
integrity sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==
ip-address@^9.0.5:
version "9.0.5"
resolved "https://registry.yarnpkg.com/ip-address/-/ip-address-9.0.5.tgz#117a960819b08780c3bd1f14ef3c1cc1d3f3ea5a"
integrity sha512-zHtQzGojZXTwZTHQqra+ETKd4Sn3vgi7uBmlPoXVWZqYvuKmtI0l/VZTjqGmJY9x88GGOaZ9+G9ES8hC4T4X8g==
dependencies:
jsbn "1.1.0"
sprintf-js "^1.1.3"
is-fullwidth-code-point@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz#f116f8064fe90b3f7844a38997c0b75051269f1d"
integrity sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==
is-lambda@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/is-lambda/-/is-lambda-1.0.1.tgz#3d9877899e6a53efc0160504cde15f82e6f061d5"
integrity sha512-z7CMFGNrENq5iFB9Bqo64Xk6Y9sg+epq1myIcdHaGnbMTYOxvzsEtdYqQUylB7LxfkvgrrjP32T6Ywciio9UIQ==
isexe@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10"
integrity sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==
jsbn@1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/jsbn/-/jsbn-1.1.0.tgz#b01307cb29b618a1ed26ec79e911f803c4da0040"
integrity sha512-4bYVV3aAMtDTTu4+xsDYa6sy9GyJ69/amsu9sYF2zqjiEoZA5xJi3BrfX3uY+/IekIu7MwdObdbDWpoZdBv3/A==
lru-cache@^6.0.0:
version "6.0.0"
resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-6.0.0.tgz#6d6fe6570ebd96aaf90fcad1dafa3b2566db3a94"
integrity sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==
dependencies:
yallist "^4.0.0"
make-fetch-happen@^9.1.0:
version "9.1.0"
resolved "https://registry.yarnpkg.com/make-fetch-happen/-/make-fetch-happen-9.1.0.tgz#53085a09e7971433e6765f7971bf63f4e05cb968"
integrity sha512-+zopwDy7DNknmwPQplem5lAZX/eCOzSvSNNcSKm5eVwTkOBzoktEfXsa9L23J/GIRhxRsaxzkPEhrJEpE2F4Gg==
dependencies:
agentkeepalive "^4.1.3"
cacache "^15.2.0"
http-cache-semantics "^4.1.0"
http-proxy-agent "^4.0.1"
https-proxy-agent "^5.0.0"
is-lambda "^1.0.1"
lru-cache "^6.0.0"
minipass "^3.1.3"
minipass-collect "^1.0.2"
minipass-fetch "^1.3.2"
minipass-flush "^1.0.5"
minipass-pipeline "^1.2.4"
negotiator "^0.6.2"
promise-retry "^2.0.1"
socks-proxy-agent "^6.0.0"
ssri "^8.0.0"
mimic-response@^3.1.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/mimic-response/-/mimic-response-3.1.0.tgz#2d1d59af9c1b129815accc2c46a022a5ce1fa3c9"
integrity sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==
minimatch@^3.1.1:
version "3.1.2"
resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.2.tgz#19cd194bfd3e428f049a70817c038d89ab4be35b"
integrity sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==
dependencies:
brace-expansion "^1.1.7"
minimist@^1.2.0, minimist@^1.2.3:
version "1.2.8"
resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.8.tgz#c1a464e7693302e082a075cee0c057741ac4772c"
integrity sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==
minipass-collect@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/minipass-collect/-/minipass-collect-1.0.2.tgz#22b813bf745dc6edba2576b940022ad6edc8c617"
integrity sha512-6T6lH0H8OG9kITm/Jm6tdooIbogG9e0tLgpY6mphXSm/A9u8Nq1ryBG+Qspiub9LjWlBPsPS3tWQ/Botq4FdxA==
dependencies:
minipass "^3.0.0"
minipass-fetch@^1.3.2:
version "1.4.1"
resolved "https://registry.yarnpkg.com/minipass-fetch/-/minipass-fetch-1.4.1.tgz#d75e0091daac1b0ffd7e9d41629faff7d0c1f1b6"
integrity sha512-CGH1eblLq26Y15+Azk7ey4xh0J/XfJfrCox5LDJiKqI2Q2iwOLOKrlmIaODiSQS8d18jalF6y2K2ePUm0CmShw==
dependencies:
minipass "^3.1.0"
minipass-sized "^1.0.3"
minizlib "^2.0.0"
optionalDependencies:
encoding "^0.1.12"
minipass-flush@^1.0.5:
version "1.0.5"
resolved "https://registry.yarnpkg.com/minipass-flush/-/minipass-flush-1.0.5.tgz#82e7135d7e89a50ffe64610a787953c4c4cbb373"
integrity sha512-JmQSYYpPUqX5Jyn1mXaRwOda1uQ8HP5KAT/oDSLCzt1BYRhQU0/hDtsB1ufZfEEzMZ9aAVmsBw8+FWsIXlClWw==
dependencies:
minipass "^3.0.0"
minipass-pipeline@^1.2.2, minipass-pipeline@^1.2.4:
version "1.2.4"
resolved "https://registry.yarnpkg.com/minipass-pipeline/-/minipass-pipeline-1.2.4.tgz#68472f79711c084657c067c5c6ad93cddea8214c"
integrity sha512-xuIq7cIOt09RPRJ19gdi4b+RiNvDFYe5JH+ggNvBqGqpQXcru3PcRmOZuHBKWK1Txf9+cQ+HMVN4d6z46LZP7A==
dependencies:
minipass "^3.0.0"
minipass-sized@^1.0.3:
version "1.0.3"
resolved "https://registry.yarnpkg.com/minipass-sized/-/minipass-sized-1.0.3.tgz#70ee5a7c5052070afacfbc22977ea79def353b70"
integrity sha512-MbkQQ2CTiBMlA2Dm/5cY+9SWFEN8pzzOXi6rlM5Xxq0Yqbda5ZQy9sU75a673FE9ZK0Zsbr6Y5iP6u9nktfg2g==
dependencies:
minipass "^3.0.0"
minipass@^3.0.0, minipass@^3.1.0, minipass@^3.1.1, minipass@^3.1.3:
version "3.3.6"
resolved "https://registry.yarnpkg.com/minipass/-/minipass-3.3.6.tgz#7bba384db3a1520d18c9c0e5251c3444e95dd94a"
integrity sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==
dependencies:
yallist "^4.0.0"
minipass@^5.0.0:
version "5.0.0"
resolved "https://registry.yarnpkg.com/minipass/-/minipass-5.0.0.tgz#3e9788ffb90b694a5d0ec94479a45b5d8738133d"
integrity sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==
minizlib@^2.0.0, minizlib@^2.1.1:
version "2.1.2"
resolved "https://registry.yarnpkg.com/minizlib/-/minizlib-2.1.2.tgz#e90d3466ba209b932451508a11ce3d3632145931"
integrity sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==
dependencies:
minipass "^3.0.0"
yallist "^4.0.0"
mkdirp-classic@^0.5.2, mkdirp-classic@^0.5.3:
version "0.5.3"
resolved "https://registry.yarnpkg.com/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz#fa10c9115cc6d8865be221ba47ee9bed78601113"
integrity sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==
mkdirp@^1.0.3, mkdirp@^1.0.4:
version "1.0.4"
resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-1.0.4.tgz#3eb5ed62622756d79a5f0e2a221dfebad75c2f7e"
integrity sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==
ms@2.1.2:
version "2.1.2"
resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009"
integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==
ms@^2.0.0:
version "2.1.3"
resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2"
integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==
napi-build-utils@^1.0.1:
version "1.0.2"
resolved "https://registry.yarnpkg.com/napi-build-utils/-/napi-build-utils-1.0.2.tgz#b1fddc0b2c46e380a0b7a76f984dd47c41a13806"
integrity sha512-ONmRUqK7zj7DWX0D9ADe03wbwOBZxNAfF20PlGfCWQcD3+/MakShIHrMqx9YwPTfxDdF1zLeL+RGZiR9kGMLdg==
negotiator@^0.6.2:
version "0.6.3"
resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.3.tgz#58e323a72fedc0d6f9cd4d31fe49f51479590ccd"
integrity sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==
node-abi@^3.3.0:
version "3.62.0"
resolved "https://registry.yarnpkg.com/node-abi/-/node-abi-3.62.0.tgz#017958ed120f89a3a14a7253da810f5d724e3f36"
integrity sha512-CPMcGa+y33xuL1E0TcNIu4YyaZCxnnvkVaEXrsosR3FxN+fV8xvb7Mzpb7IgKler10qeMkE6+Dp8qJhpzdq35g==
dependencies:
semver "^7.3.5"
node-addon-api@^7.0.0:
version "7.1.0"
resolved "https://registry.yarnpkg.com/node-addon-api/-/node-addon-api-7.1.0.tgz#71f609369379c08e251c558527a107107b5e0fdb"
integrity sha512-mNcltoe1R8o7STTegSOHdnJNN7s5EUvhoS7ShnTHDyOSd+8H+UdWODq6qSv67PjC8Zc5JRT8+oLAMCr0SIXw7g==
node-gyp@8.x:
version "8.4.1"
resolved "https://registry.yarnpkg.com/node-gyp/-/node-gyp-8.4.1.tgz#3d49308fc31f768180957d6b5746845fbd429937"
integrity sha512-olTJRgUtAb/hOXG0E93wZDs5YiJlgbXxTwQAFHyNlRsXQnYzUaF2aGgujZbw+hR8aF4ZG/rST57bWMWD16jr9w==
dependencies:
env-paths "^2.2.0"
glob "^7.1.4"
graceful-fs "^4.2.6"
make-fetch-happen "^9.1.0"
nopt "^5.0.0"
npmlog "^6.0.0"
rimraf "^3.0.2"
semver "^7.3.5"
tar "^6.1.2"
which "^2.0.2"
nopt@^5.0.0:
version "5.0.0"
resolved "https://registry.yarnpkg.com/nopt/-/nopt-5.0.0.tgz#530942bb58a512fccafe53fe210f13a25355dc88"
integrity sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ==
dependencies:
abbrev "1"
npmlog@^6.0.0:
version "6.0.2"
resolved "https://registry.yarnpkg.com/npmlog/-/npmlog-6.0.2.tgz#c8166017a42f2dea92d6453168dd865186a70830"
integrity sha512-/vBvz5Jfr9dT/aFWd0FIRf+T/Q2WBsLENygUaFUqstqsycmZAP/t5BvFJTK0viFmSUxiUKTUplWy5vt+rvKIxg==
dependencies:
are-we-there-yet "^3.0.0"
console-control-strings "^1.1.0"
gauge "^4.0.3"
set-blocking "^2.0.0"
once@^1.3.0, once@^1.3.1, once@^1.4.0:
version "1.4.0"
resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1"
integrity sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==
dependencies:
wrappy "1"
p-map@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/p-map/-/p-map-4.0.0.tgz#bb2f95a5eda2ec168ec9274e06a747c3e2904d2b"
integrity sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==
dependencies:
aggregate-error "^3.0.0"
path-is-absolute@^1.0.0:
version "1.0.1"
resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f"
integrity sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==
prebuild-install@^7.1.1:
version "7.1.2"
resolved "https://registry.yarnpkg.com/prebuild-install/-/prebuild-install-7.1.2.tgz#a5fd9986f5a6251fbc47e1e5c65de71e68c0a056"
integrity sha512-UnNke3IQb6sgarcZIDU3gbMeTp/9SSU1DAIkil7PrqG1vZlBtY5msYccSKSHDqa3hNg436IXK+SNImReuA1wEQ==
dependencies:
detect-libc "^2.0.0"
expand-template "^2.0.3"
github-from-package "0.0.0"
minimist "^1.2.3"
mkdirp-classic "^0.5.3"
napi-build-utils "^1.0.1"
node-abi "^3.3.0"
pump "^3.0.0"
rc "^1.2.7"
simple-get "^4.0.0"
tar-fs "^2.0.0"
tunnel-agent "^0.6.0"
prettier@3.2.4:
version "3.2.4"
resolved "https://registry.yarnpkg.com/prettier/-/prettier-3.2.4.tgz#4723cadeac2ce7c9227de758e5ff9b14e075f283"
integrity sha512-FWu1oLHKCrtpO1ypU6J0SbK2d9Ckwysq6bHj/uaCP26DxrPpppCLQRGVuqAxSTvhF00AcvDRyYrLNW7ocBhFFQ==
promise-inflight@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/promise-inflight/-/promise-inflight-1.0.1.tgz#98472870bf228132fcbdd868129bad12c3c029e3"
integrity sha512-6zWPyEOFaQBJYcGMHBKTKJ3u6TBsnMFOIZSa6ce1e/ZrrsOlnHRHbabMjLiBYKp+n44X9eUI6VUPaukCXHuG4g==
promise-retry@^2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/promise-retry/-/promise-retry-2.0.1.tgz#ff747a13620ab57ba688f5fc67855410c370da22"
integrity sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g==
dependencies:
err-code "^2.0.2"
retry "^0.12.0"
pump@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/pump/-/pump-3.0.0.tgz#b4a2116815bde2f4e1ea602354e8c75565107a64"
integrity sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==
dependencies:
end-of-stream "^1.1.0"
once "^1.3.1"
rc@^1.2.7:
version "1.2.8"
resolved "https://registry.yarnpkg.com/rc/-/rc-1.2.8.tgz#cd924bf5200a075b83c188cd6b9e211b7fc0d3ed"
integrity sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==
dependencies:
deep-extend "^0.6.0"
ini "~1.3.0"
minimist "^1.2.0"
strip-json-comments "~2.0.1"
readable-stream@^3.1.1, readable-stream@^3.4.0, readable-stream@^3.6.0:
version "3.6.2"
resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.2.tgz#56a9b36ea965c00c5a93ef31eb111a0f11056967"
integrity sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==
dependencies:
inherits "^2.0.3"
string_decoder "^1.1.1"
util-deprecate "^1.0.1"
require-directory@^2.1.1:
version "2.1.1"
resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42"
integrity sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==
retry@^0.12.0:
version "0.12.0"
resolved "https://registry.yarnpkg.com/retry/-/retry-0.12.0.tgz#1b42a6266a21f07421d1b0b54b7dc167b01c013b"
integrity sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==
rimraf@^3.0.2:
version "3.0.2"
resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-3.0.2.tgz#f1a5402ba6220ad52cc1282bac1ae3aa49fd061a"
integrity sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==
dependencies:
glob "^7.1.3"
safe-buffer@^5.0.1, safe-buffer@~5.2.0:
version "5.2.1"
resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6"
integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==
"safer-buffer@>= 2.1.2 < 3.0.0":
version "2.1.2"
resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a"
integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==
semver@^7.3.5:
version "7.6.2"
resolved "https://registry.yarnpkg.com/semver/-/semver-7.6.2.tgz#1e3b34759f896e8f14d6134732ce798aeb0c6e13"
integrity sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w==
set-blocking@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/set-blocking/-/set-blocking-2.0.0.tgz#045f9782d011ae9a6803ddd382b24392b3d890f7"
integrity sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==
signal-exit@^3.0.7:
version "3.0.7"
resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.7.tgz#a9a1767f8af84155114eaabd73f99273c8f59ad9"
integrity sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==
simple-concat@^1.0.0:
version "1.0.1"
resolved "https://registry.yarnpkg.com/simple-concat/-/simple-concat-1.0.1.tgz#f46976082ba35c2263f1c8ab5edfe26c41c9552f"
integrity sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==
simple-get@^4.0.0:
version "4.0.1"
resolved "https://registry.yarnpkg.com/simple-get/-/simple-get-4.0.1.tgz#4a39db549287c979d352112fa03fd99fd6bc3543"
integrity sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==
dependencies:
decompress-response "^6.0.0"
once "^1.3.1"
simple-concat "^1.0.0"
smart-buffer@^4.2.0:
version "4.2.0"
resolved "https://registry.yarnpkg.com/smart-buffer/-/smart-buffer-4.2.0.tgz#6e1d71fa4f18c05f7d0ff216dd16a481d0e8d9ae"
integrity sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==
socks-proxy-agent@^6.0.0:
version "6.2.1"
resolved "https://registry.yarnpkg.com/socks-proxy-agent/-/socks-proxy-agent-6.2.1.tgz#2687a31f9d7185e38d530bef1944fe1f1496d6ce"
integrity sha512-a6KW9G+6B3nWZ1yB8G7pJwL3ggLy1uTzKAgCb7ttblwqdz9fMGJUuTy3uFzEP48FAs9FLILlmzDlE2JJhVQaXQ==
dependencies:
agent-base "^6.0.2"
debug "^4.3.3"
socks "^2.6.2"
socks@^2.6.2:
version "2.8.3"
resolved "https://registry.yarnpkg.com/socks/-/socks-2.8.3.tgz#1ebd0f09c52ba95a09750afe3f3f9f724a800cb5"
integrity sha512-l5x7VUUWbjVFbafGLxPWkYsHIhEvmF85tbIeFZWc8ZPtoMyybuEhL7Jye/ooC4/d48FgOjSJXgsF/AJPYCW8Zw==
dependencies:
ip-address "^9.0.5"
smart-buffer "^4.2.0"
sprintf-js@^1.1.3:
version "1.1.3"
resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.1.3.tgz#4914b903a2f8b685d17fdf78a70e917e872e444a"
integrity sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==
sqleton@^2.2.0:
version "2.2.0"
resolved "https://registry.yarnpkg.com/sqleton/-/sqleton-2.2.0.tgz#d265b625c43ec552b5d3275c25b85dfec240d910"
integrity sha512-pfjBQRmrRNi4DEiX5X1akyLIG9z6UiG7Hi+5vwB/dG1ksgJ8yuL5na+3HI5nZqaWBJf91L340x55ZLdake9/yg==
dependencies:
sqlite3 "^5.1.4"
yargs "^17.2.1"
sqlite3@^5.1.4:
version "5.1.7"
resolved "https://registry.yarnpkg.com/sqlite3/-/sqlite3-5.1.7.tgz#59ca1053c1ab38647396586edad019b1551041b7"
integrity sha512-GGIyOiFaG+TUra3JIfkI/zGP8yZYLPQ0pl1bH+ODjiX57sPhrLU5sQJn1y9bDKZUFYkX1crlrPfSYt0BKKdkog==
dependencies:
bindings "^1.5.0"
node-addon-api "^7.0.0"
prebuild-install "^7.1.1"
tar "^6.1.11"
optionalDependencies:
node-gyp "8.x"
ssri@^8.0.0, ssri@^8.0.1:
version "8.0.1"
resolved "https://registry.yarnpkg.com/ssri/-/ssri-8.0.1.tgz#638e4e439e2ffbd2cd289776d5ca457c4f51a2af"
integrity sha512-97qShzy1AiyxvPNIkLWoGua7xoQzzPjQ0HAH4B0rWKo7SZ6USuPcrUiAFrws0UH8RrbWmgq3LMTObhPIHbbBeQ==
dependencies:
minipass "^3.1.1"
"string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3:
version "4.2.3"
resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
dependencies:
emoji-regex "^8.0.0"
is-fullwidth-code-point "^3.0.0"
strip-ansi "^6.0.1"
string_decoder@^1.1.1:
version "1.3.0"
resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.3.0.tgz#42f114594a46cf1a8e30b0a84f56c78c3edac21e"
integrity sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==
dependencies:
safe-buffer "~5.2.0"
strip-ansi@^6.0.0, strip-ansi@^6.0.1:
version "6.0.1"
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
dependencies:
ansi-regex "^5.0.1"
strip-json-comments@~2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-2.0.1.tgz#3c531942e908c2697c0ec344858c286c7ca0a60a"
integrity sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==
tar-fs@^2.0.0:
version "2.1.1"
resolved "https://registry.yarnpkg.com/tar-fs/-/tar-fs-2.1.1.tgz#489a15ab85f1f0befabb370b7de4f9eb5cbe8784"
integrity sha512-V0r2Y9scmbDRLCNex/+hYzvp/zyYjvFbHPNgVTKfQvVrb6guiE/fxP+XblDNR011utopbkex2nM4dHNV6GDsng==
dependencies:
chownr "^1.1.1"
mkdirp-classic "^0.5.2"
pump "^3.0.0"
tar-stream "^2.1.4"
tar-stream@^2.1.4:
version "2.2.0"
resolved "https://registry.yarnpkg.com/tar-stream/-/tar-stream-2.2.0.tgz#acad84c284136b060dc3faa64474aa9aebd77287"
integrity sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==
dependencies:
bl "^4.0.3"
end-of-stream "^1.4.1"
fs-constants "^1.0.0"
inherits "^2.0.3"
readable-stream "^3.1.1"
tar@^6.0.2, tar@^6.1.11, tar@^6.1.2:
version "6.2.1"
resolved "https://registry.yarnpkg.com/tar/-/tar-6.2.1.tgz#717549c541bc3c2af15751bea94b1dd068d4b03a"
integrity sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==
dependencies:
chownr "^2.0.0"
fs-minipass "^2.0.0"
minipass "^5.0.0"
minizlib "^2.1.1"
mkdirp "^1.0.3"
yallist "^4.0.0"
tunnel-agent@^0.6.0:
version "0.6.0"
resolved "https://registry.yarnpkg.com/tunnel-agent/-/tunnel-agent-0.6.0.tgz#27a5dea06b36b04a0a9966774b290868f0fc40fd"
integrity sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==
dependencies:
safe-buffer "^5.0.1"
unique-filename@^1.1.1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/unique-filename/-/unique-filename-1.1.1.tgz#1d69769369ada0583103a1e6ae87681b56573230"
integrity sha512-Vmp0jIp2ln35UTXuryvjzkjGdRyf9b2lTXuSYUiPmzRcl3FDtYqAwOnTJkAngD9SWhnoJzDbTKwaOrZ+STtxNQ==
dependencies:
unique-slug "^2.0.0"
unique-slug@^2.0.0:
version "2.0.2"
resolved "https://registry.yarnpkg.com/unique-slug/-/unique-slug-2.0.2.tgz#baabce91083fc64e945b0f3ad613e264f7cd4e6c"
integrity sha512-zoWr9ObaxALD3DOPfjPSqxt4fnZiWblxHIgeWqW8x7UqDzEtHEQLzji2cuJYQFCU6KmoJikOYAZlrTHHebjx2w==
dependencies:
imurmurhash "^0.1.4"
util-deprecate@^1.0.1:
version "1.0.2"
resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf"
integrity sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==
which@^2.0.2:
version "2.0.2"
resolved "https://registry.yarnpkg.com/which/-/which-2.0.2.tgz#7c6a8dd0a636a0327e10b59c9286eee93f3f51b1"
integrity sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==
dependencies:
isexe "^2.0.0"
wide-align@^1.1.5:
version "1.1.5"
resolved "https://registry.yarnpkg.com/wide-align/-/wide-align-1.1.5.tgz#df1d4c206854369ecf3c9a4898f1b23fbd9d15d3"
integrity sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==
dependencies:
string-width "^1.0.2 || 2 || 3 || 4"
wrap-ansi@^7.0.0:
version "7.0.0"
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43"
integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==
dependencies:
ansi-styles "^4.0.0"
string-width "^4.1.0"
strip-ansi "^6.0.0"
wrappy@1:
version "1.0.2"
resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f"
integrity sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==
y18n@^5.0.5:
version "5.0.8"
resolved "https://registry.yarnpkg.com/y18n/-/y18n-5.0.8.tgz#7f4934d0f7ca8c56f95314939ddcd2dd91ce1d55"
integrity sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==
yallist@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72"
integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==
yargs-parser@^21.1.1:
version "21.1.1"
resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-21.1.1.tgz#9096bceebf990d21bb31fa9516e0ede294a77d35"
integrity sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==
yargs@^17.2.1:
version "17.7.2"
resolved "https://registry.yarnpkg.com/yargs/-/yargs-17.7.2.tgz#991df39aca675a192b816e1e0363f9d75d2aa269"
integrity sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==
dependencies:
cliui "^8.0.1"
escalade "^3.1.1"
get-caller-file "^2.0.5"
require-directory "^2.1.1"
string-width "^4.2.3"
y18n "^5.0.5"
yargs-parser "^21.1.1"