Compare commits
44 Commits
v2024.6.25
...
v2024.9.12
| Author | SHA1 | Date | |
|---|---|---|---|
| 7fb6fa3af4 | |||
| bb8788f03d | |||
| ed4434512b | |||
| f6344d43d2 | |||
| e0745bdfbe | |||
| a0a02cb4be | |||
| 7866f7258a | |||
| 5cd82f9dd5 | |||
| c85e9cb5f1 | |||
| 4837dd44c9 | |||
| e3f42c28d0 | |||
| 4a11d47692 | |||
| 612c760755 | |||
| 14b8ecbe44 | |||
| a6c61ccd0d | |||
| 80209240d2 | |||
| 1d5b62ccf9 | |||
| af8235c6b3 | |||
| 8e9f02c807 | |||
| fd20ac5d84 | |||
| 23492961d5 | |||
| 2326775d78 | |||
| e0a84a521b | |||
| d217ad5744 | |||
| d5ae41cdab | |||
| 7a01db05dd | |||
| d392fc3818 | |||
| 47596d5f72 | |||
| dbc25e5730 | |||
| a3ebf59b29 | |||
| 8dd8e49051 | |||
| 029981b7ee | |||
| d423e4486f | |||
| 4c7a63c970 | |||
| 4822127e3f | |||
| 4f25878f2f | |||
| aec56e3a03 | |||
| e06e050f95 | |||
| 5a10015634 | |||
| 0d5a41fd35 | |||
| 8f91c4e6a2 | |||
| 7f1daf90ca | |||
| bb0e104b7e | |||
| c0b8dec520 |
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"name": "Pinchflat Dev",
|
||||
"dockerComposeFile": "../docker-compose.yml",
|
||||
"service": "phx",
|
||||
"workspaceFolder": "/app",
|
||||
"shutdownAction": "stopCompose",
|
||||
"customizations": {
|
||||
"vscode": {
|
||||
"extensions": ["phoenixframework.phoenix", "JakeBecker.elixir-ls", "esbenp.prettier-vscode"]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -72,7 +72,7 @@ jobs:
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Build and Push
|
||||
uses: docker/build-push-action@v5
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: .
|
||||
file: ./docker/selfhosted.Dockerfile
|
||||
|
||||
@@ -16,24 +16,28 @@ jobs:
|
||||
if: "! contains(toJSON(github.event.commits.*.message), '[skip ci]')"
|
||||
env:
|
||||
COMPOSE_FILE: ./docker-compose.ci.yml
|
||||
MIX_ENV: test
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Pull prebuilt images
|
||||
run: docker compose pull
|
||||
|
||||
- name: Setup Docker layer caching
|
||||
uses: jpribyl/action-docker-layer-caching@v0.1.1
|
||||
continue-on-error: true
|
||||
with:
|
||||
key: ci-docker-cache-{hash}
|
||||
restore-keys: |
|
||||
ci-docker-cache-
|
||||
layer-ci-docker-cache-
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Build and Run Docker image
|
||||
- name: Build docker image
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: .
|
||||
file: ./docker/dev.Dockerfile
|
||||
load: true
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
|
||||
- name: Run Docker image
|
||||
run: docker compose up --detach
|
||||
|
||||
# NOTE: All exec commands use the -T flag to compensate for
|
||||
@@ -43,8 +47,7 @@ jobs:
|
||||
# See https://github.com/actions/runner/issues/241 and https://github.com/docker/compose/issues/8537
|
||||
- name: Install Elixir and JS deps
|
||||
run: |
|
||||
docker compose exec -T phx yarn install && cd assets && yarn install && cd ..
|
||||
docker compose exec -T phx mix deps.get
|
||||
docker compose exec -T phx mix deps.get && yarn install && cd assets && yarn install && cd ..
|
||||
|
||||
- name: Create and Migrate database
|
||||
run: |
|
||||
|
||||
@@ -23,3 +23,11 @@ alias Pinchflat.Metadata.MetadataFileHelpers
|
||||
alias Pinchflat.SlowIndexing.FileFollowerServer
|
||||
|
||||
Pinchflat.Release.check_file_permissions()
|
||||
|
||||
defmodule IexHelpers do
|
||||
def restart do
|
||||
:init.restart()
|
||||
end
|
||||
end
|
||||
|
||||
import IexHelpers
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
assets/vendor/
|
||||
@@ -17,6 +17,7 @@
|
||||
[](LICENSE)
|
||||
[](https://github.com/kieraneglin/pinchflat/releases)
|
||||
[](#)
|
||||
[](https://vscode.dev/redirect?url=vscode://ms-vscode-remote.remote-containers/cloneInVolume?url=https://github.com/kieraneglin/pinchflat)
|
||||
|
||||
</div>
|
||||
|
||||
@@ -145,15 +146,17 @@ If you change this setting and it works well for you, please leave a comment on
|
||||
### 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 |
|
||||
| BASE_ROUTE_PATH | No | `/` | The base path for route generation. Useful when running behind certain reverse proxies |
|
||||
| --------------------------- | --------- | ------------------------- | ------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `TZ` | No | `UTC` | Must follow IANA TZ format |
|
||||
| `LOG_LEVEL` | No | `debug` | Can be set to `info` but `debug` is strongly recommended |
|
||||
| `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 | `false` | See [RSS feed docs](https://github.com/kieraneglin/pinchflat/wiki/Podcast-RSS-Feeds) |
|
||||
| `ENABLE_IPV6` | No | `false` | Setting to _any_ non-blank value will enable IPv6 |
|
||||
| `JOURNAL_MODE` | No | `wal` | Set to `delete` if your config directory is stored on a network share (not recommended) |
|
||||
| `TZ_DATA_DIR` | No | `/etc/elixir_tzdata_data` | The container path where the timezone database is stored |
|
||||
| `BASE_ROUTE_PATH` | No | `/` | The base path for route generation. Useful when running behind certain reverse proxies |
|
||||
| `YT_DLP_WORKER_CONCURRENCY` | No | `2` | The number of concurrent workers that use `yt-dlp` _per queue_. Set to 1 if you're getting IP limited, otherwise don't touch it |
|
||||
|
||||
## EFF donations
|
||||
|
||||
|
||||
+4
-13
@@ -41,7 +41,8 @@ config :pinchflat, PinchflatWeb.Endpoint,
|
||||
adapter: Phoenix.Endpoint.Cowboy2Adapter,
|
||||
render_errors: [
|
||||
formats: [html: PinchflatWeb.ErrorHTML, json: PinchflatWeb.ErrorJSON],
|
||||
layout: false
|
||||
root_layout: {PinchflatWeb.Layouts, :root},
|
||||
layout: {PinchflatWeb.Layouts, :app}
|
||||
],
|
||||
pubsub_server: Pinchflat.PubSub,
|
||||
live_view: [signing_salt: "/t5878kO"]
|
||||
@@ -57,16 +58,6 @@ config :pinchflat, Oban,
|
||||
{"0 1 * * *", Pinchflat.Downloading.MediaRetentionWorker},
|
||||
{"0 2 * * *", Pinchflat.Downloading.MediaQualityUpgradeWorker}
|
||||
]}
|
||||
],
|
||||
# TODO: consider making this an env var or something?
|
||||
queues: [
|
||||
default: 10,
|
||||
fast_indexing: 6,
|
||||
media_indexing: 2,
|
||||
media_collection_indexing: 2,
|
||||
media_fetching: 2,
|
||||
local_metadata: 8,
|
||||
remote_metadata: 4
|
||||
]
|
||||
|
||||
# Configures the mailer
|
||||
@@ -101,8 +92,8 @@ config :tailwind,
|
||||
]
|
||||
|
||||
# Configures Elixir's Logger
|
||||
config :logger, :console,
|
||||
format: "$time $metadata[$level] $message\n",
|
||||
config :logger, :default_formatter,
|
||||
format: "$date $time $metadata[$level] | $message\n",
|
||||
metadata: [:request_id]
|
||||
|
||||
# Use Jason for JSON parsing in Phoenix
|
||||
|
||||
+1
-1
@@ -67,7 +67,7 @@ config :pinchflat, PinchflatWeb.Endpoint,
|
||||
config :pinchflat, dev_routes: true
|
||||
|
||||
# Do not include metadata nor timestamps in development logs
|
||||
config :logger, :console, format: "[$level] $message\n"
|
||||
config :logger, :default_formatter, format: "[$level] $message\n"
|
||||
|
||||
# Set a higher stacktrace during development. Avoid configuring such
|
||||
# in production as building large stacktraces may be expensive.
|
||||
|
||||
+16
-1
@@ -40,6 +40,20 @@ config :pinchflat, Pinchflat.Repo,
|
||||
Path.join([:code.priv_dir(:pinchflat), "repo", "extensions", "sqlean-linux-#{system_arch}", "sqlean"])
|
||||
]
|
||||
|
||||
# Some users may want to increase the number of workers that use yt-dlp to improve speeds
|
||||
# Others may want to decrease the number of these workers to lessen the chance of an IP ban
|
||||
{yt_dlp_worker_count, _} = Integer.parse(System.get_env("YT_DLP_WORKER_CONCURRENCY", "2"))
|
||||
|
||||
config :pinchflat, Oban,
|
||||
queues: [
|
||||
default: 10,
|
||||
fast_indexing: 6,
|
||||
media_collection_indexing: yt_dlp_worker_count,
|
||||
media_fetching: yt_dlp_worker_count,
|
||||
remote_metadata: yt_dlp_worker_count,
|
||||
local_data: 8
|
||||
]
|
||||
|
||||
if config_env() == :prod do
|
||||
config_path = "/config"
|
||||
db_path = System.get_env("DATABASE_PATH", Path.join([config_path, "db", "pinchflat.db"]))
|
||||
@@ -52,6 +66,7 @@ if config_env() == :prod do
|
||||
journal_mode = String.to_existing_atom(System.get_env("JOURNAL_MODE", "wal"))
|
||||
# For running PF in a subdirectory via a reverse proxy
|
||||
base_route_path = System.get_env("BASE_ROUTE_PATH", "/")
|
||||
enable_ipv6 = String.length(System.get_env("ENABLE_IPV6", "")) > 0
|
||||
|
||||
config :logger, level: String.to_existing_atom(System.get_env("LOG_LEVEL", "debug"))
|
||||
|
||||
@@ -106,7 +121,7 @@ if config_env() == :prod do
|
||||
# Set it to {0, 0, 0, 0, 0, 0, 0, 1} for local network only access.
|
||||
# See the documentation on https://hexdocs.pm/plug_cowboy/Plug.Cowboy.html
|
||||
# for details about using IPv6 vs IPv4 and loopback vs public addresses.
|
||||
ip: {0, 0, 0, 0},
|
||||
ip: if(enable_ipv6, do: {0, 0, 0, 0, 0, 0, 0, 0}, else: {0, 0, 0, 0}),
|
||||
port: String.to_integer(System.get_env("PORT") || "4000")
|
||||
],
|
||||
url: [path: base_route_path],
|
||||
|
||||
@@ -7,6 +7,9 @@ services:
|
||||
- MIX_ENV=test
|
||||
volumes:
|
||||
- '.:/app'
|
||||
# These lines ensure the deps can be saved as build artifacts for caching
|
||||
- '/app/deps'
|
||||
- '/app/_build'
|
||||
ports:
|
||||
- '4008:4008'
|
||||
command: tail -F /dev/null
|
||||
|
||||
@@ -10,5 +10,3 @@ services:
|
||||
command: bash -c "chmod +x docker/docker-run.dev.sh && docker/docker-run.dev.sh"
|
||||
stdin_open: true
|
||||
tty: true
|
||||
env_file:
|
||||
- .env
|
||||
|
||||
+31
-28
@@ -9,10 +9,10 @@ ARG TARGETPLATFORM
|
||||
RUN echo "Building for ${TARGETPLATFORM:?}"
|
||||
|
||||
# Install debian packages
|
||||
RUN apt-get update -qq
|
||||
RUN apt-get install -y inotify-tools curl git openssh-client jq \
|
||||
RUN apt-get update -qq && \
|
||||
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 graphviz
|
||||
python3-mutagen locales procps build-essential graphviz zsh
|
||||
|
||||
# Install ffmpeg
|
||||
RUN export FFMPEG_DOWNLOAD=$(case ${TARGETPLATFORM:-linux/amd64} in \
|
||||
@@ -23,39 +23,42 @@ RUN export FFMPEG_DOWNLOAD=$(case ${TARGETPLATFORM:-linux/amd64} in \
|
||||
tar -xf /tmp/ffmpeg.tar.xz --strip-components=2 --no-anchored -C /usr/bin/ "ffmpeg" && \
|
||||
tar -xf /tmp/ffmpeg.tar.xz --strip-components=2 --no-anchored -C /usr/bin/ "ffprobe"
|
||||
|
||||
# Install nodejs
|
||||
RUN curl -sL https://deb.nodesource.com/setup_20.x -o nodesource_setup.sh
|
||||
RUN bash nodesource_setup.sh
|
||||
RUN apt-get install nodejs
|
||||
RUN npm install -g yarn
|
||||
|
||||
# Install baseline Elixir packages
|
||||
RUN mix local.hex --force
|
||||
RUN mix local.rebar --force
|
||||
|
||||
# Download and update YT-DLP
|
||||
RUN curl -L https://github.com/yt-dlp/yt-dlp/releases/latest/download/yt-dlp -o /usr/local/bin/yt-dlp
|
||||
RUN chmod a+rx /usr/local/bin/yt-dlp
|
||||
RUN yt-dlp -U
|
||||
|
||||
# Install Apprise
|
||||
RUN export PIPX_HOME=/opt/pipx && \
|
||||
# Install nodejs and Yarn
|
||||
RUN curl -sL https://deb.nodesource.com/setup_20.x -o nodesource_setup.sh && \
|
||||
bash nodesource_setup.sh && \
|
||||
apt-get install -y nodejs && \
|
||||
npm install -g yarn && \
|
||||
# Install baseline Elixir packages
|
||||
mix local.hex --force && \
|
||||
mix local.rebar --force && \
|
||||
# Download and update YT-DLP
|
||||
curl -L https://github.com/yt-dlp/yt-dlp/releases/latest/download/yt-dlp -o /usr/local/bin/yt-dlp && \
|
||||
chmod a+rx /usr/local/bin/yt-dlp && \
|
||||
yt-dlp -U && \
|
||||
# Install Apprise
|
||||
export PIPX_HOME=/opt/pipx && \
|
||||
export PIPX_BIN_DIR=/usr/local/bin && \
|
||||
pipx install apprise
|
||||
pipx install apprise && \
|
||||
# Set up ZSH tools
|
||||
chsh -s $(which zsh) && \
|
||||
sh -c "$(curl -fsSL https://raw.githubusercontent.com/ohmyzsh/ohmyzsh/master/tools/install.sh)"
|
||||
|
||||
# Set the locale
|
||||
RUN sed -i '/en_US.UTF-8/s/^# //g' /etc/locale.gen && locale-gen
|
||||
ENV LANG en_US.UTF-8
|
||||
ENV LANGUAGE en_US:en
|
||||
ENV LC_ALL en_US.UTF-8
|
||||
ENV LANG=en_US.UTF-8
|
||||
ENV LANGUAGE=en_US:en
|
||||
ENV LC_ALL=en_US.UTF-8
|
||||
|
||||
# Create app directory and copy the Elixir projects into it.
|
||||
WORKDIR /app
|
||||
|
||||
COPY mix.exs mix.lock ./
|
||||
# Install Elixir deps
|
||||
# NOTE: this has to be before the bulk copy to ensure that deps are cached
|
||||
RUN MIX_ENV=dev mix deps.get && MIX_ENV=dev mix deps.compile
|
||||
RUN MIX_ENV=test mix deps.get && MIX_ENV=test mix deps.compile
|
||||
|
||||
COPY . ./
|
||||
|
||||
# Install Elixir deps
|
||||
# RUN mix archive.install github hexpm/hex branch latest
|
||||
RUN mix deps.get
|
||||
# Gives us iex shell history
|
||||
ENV ERL_AFLAGS="-kernel shell_history enabled"
|
||||
|
||||
|
||||
@@ -2,12 +2,12 @@
|
||||
|
||||
set -e
|
||||
|
||||
# Ensure the app's deps are installed
|
||||
echo "\nInstalling Elixir deps..."
|
||||
mix deps.get
|
||||
|
||||
# Install JS deps
|
||||
echo "\nInstalling JS..."
|
||||
cd assets && yarn install
|
||||
# Install both project-level and assets-level JS dependencies
|
||||
echo "\nInstalling JS deps..."
|
||||
yarn install && cd assets && yarn install
|
||||
cd ..
|
||||
|
||||
# Potentially Set up the database
|
||||
|
||||
@@ -27,10 +27,10 @@ RUN apt-get update -y && \
|
||||
# Hex and Rebar
|
||||
mix local.hex --force && \
|
||||
mix local.rebar --force && \
|
||||
# FFmpeg
|
||||
# FFmpeg (latest build that doesn't cause an illegal instruction error for some users - see #347)
|
||||
export FFMPEG_DOWNLOAD=$(case ${TARGETPLATFORM:-linux/amd64} in \
|
||||
"linux/amd64") echo "https://github.com/yt-dlp/FFmpeg-Builds/releases/download/latest/ffmpeg-master-latest-linux64-gpl.tar.xz" ;; \
|
||||
"linux/arm64") echo "https://github.com/yt-dlp/FFmpeg-Builds/releases/download/latest/ffmpeg-master-latest-linuxarm64-gpl.tar.xz" ;; \
|
||||
"linux/amd64") echo "https://github.com/yt-dlp/FFmpeg-Builds/releases/download/autobuild-2024-07-30-14-10/ffmpeg-N-116468-g0e09f6d690-linux64-gpl.tar.xz" ;; \
|
||||
"linux/arm64") echo "https://github.com/yt-dlp/FFmpeg-Builds/releases/download/autobuild-2024-07-30-14-10/ffmpeg-N-116468-g0e09f6d690-linuxarm64-gpl.tar.xz" ;; \
|
||||
*) echo "" ;; esac) && \
|
||||
curl -L ${FFMPEG_DOWNLOAD} --output /tmp/ffmpeg.tar.xz && \
|
||||
tar -xf /tmp/ffmpeg.tar.xz --strip-components=2 --no-anchored -C /usr/local/bin/ "ffmpeg" && \
|
||||
|
||||
@@ -74,6 +74,9 @@ defmodule Pinchflat.Downloading.DownloadOptionBuilder do
|
||||
{{:download_auto_subs, true}, %{download_subs: true}} ->
|
||||
acc ++ [:write_auto_subs]
|
||||
|
||||
{{:download_auto_subs, true}, %{embed_subs: true}} ->
|
||||
acc ++ [:write_auto_subs]
|
||||
|
||||
{{:embed_subs, true}, %{preferred_resolution: pr}} when pr != :audio ->
|
||||
acc ++ [:embed_subs]
|
||||
|
||||
@@ -124,11 +127,12 @@ defmodule Pinchflat.Downloading.DownloadOptionBuilder do
|
||||
defp quality_options(media_profile) do
|
||||
vcodec = Settings.get!(:video_codec_preference)
|
||||
acodec = Settings.get!(:audio_codec_preference)
|
||||
container = media_profile.media_container
|
||||
|
||||
case media_profile.preferred_resolution do
|
||||
# Also be aware that :audio disabled all embedding options for subtitles
|
||||
:audio ->
|
||||
[:extract_audio, format_sort: "+acodec:#{acodec}"]
|
||||
[:extract_audio, format_sort: "+acodec:#{acodec}", audio_format: container || "best"]
|
||||
|
||||
resolution_atom ->
|
||||
{resolution_string, _} =
|
||||
@@ -138,7 +142,7 @@ defmodule Pinchflat.Downloading.DownloadOptionBuilder do
|
||||
|
||||
[
|
||||
# Since Plex doesn't support reading metadata from MKV
|
||||
remux_video: "mp4",
|
||||
remux_video: container || "mp4",
|
||||
format_sort: "res:#{resolution_string},+codec:#{vcodec}:#{acodec}"
|
||||
]
|
||||
end
|
||||
@@ -203,10 +207,8 @@ defmodule Pinchflat.Downloading.DownloadOptionBuilder do
|
||||
"source_collection_id" => source.collection_id,
|
||||
"source_collection_name" => source.collection_name,
|
||||
"source_collection_type" => to_string(source.collection_type),
|
||||
"media_upload_date_index" =>
|
||||
media_item_with_preloads.upload_date_index
|
||||
|> to_string()
|
||||
|> String.pad_leading(2, "0")
|
||||
"media_playlist_index" => pad_int(media_item_with_preloads.playlist_index),
|
||||
"media_upload_date_index" => pad_int(media_item_with_preloads.upload_date_index)
|
||||
}
|
||||
end
|
||||
|
||||
@@ -223,6 +225,12 @@ defmodule Pinchflat.Downloading.DownloadOptionBuilder do
|
||||
|> build_output_path(media_item_with_preloads)
|
||||
end
|
||||
|
||||
defp pad_int(integer, count \\ 2, padding \\ "0") do
|
||||
integer
|
||||
|> to_string()
|
||||
|> String.pad_leading(count, padding)
|
||||
end
|
||||
|
||||
defp base_directory do
|
||||
Application.get_env(:pinchflat, :media_directory)
|
||||
end
|
||||
|
||||
@@ -91,8 +91,7 @@ defmodule Pinchflat.Downloading.DownloadingHelpers do
|
||||
[m, s, mp],
|
||||
^MediaQuery.for_source(source) and
|
||||
^MediaQuery.downloaded() and
|
||||
not (^MediaQuery.download_prevented()) and
|
||||
not (^MediaQuery.culled())
|
||||
not (^MediaQuery.download_prevented())
|
||||
)
|
||||
)
|
||||
|> Repo.all()
|
||||
|
||||
@@ -39,17 +39,14 @@ defmodule Pinchflat.Downloading.MediaDownloadWorker do
|
||||
- `quality_upgrade?`: re-downloads media, including the video. Does not force download
|
||||
if the source is set to not download media
|
||||
|
||||
Returns :ok | {:ok, %MediaItem{}} | {:error, any, ...any}
|
||||
Returns :ok | {:error, any, ...any}
|
||||
"""
|
||||
@impl Oban.Worker
|
||||
def perform(%Oban.Job{args: %{"id" => media_item_id} = args}) do
|
||||
should_force = Map.get(args, "force", false)
|
||||
is_quality_upgrade = Map.get(args, "quality_upgrade?", false)
|
||||
|
||||
media_item =
|
||||
media_item_id
|
||||
|> Media.get_media_item!()
|
||||
|> Repo.preload(:source)
|
||||
media_item = fetch_and_run_prevent_download_user_script(media_item_id)
|
||||
|
||||
# If the source or media item is set to not download media, perform a no-op unless forced
|
||||
if (media_item.source.download_media && !media_item.prevent_download) || should_force do
|
||||
@@ -62,6 +59,20 @@ defmodule Pinchflat.Downloading.MediaDownloadWorker do
|
||||
Ecto.StaleEntryError -> Logger.info("#{__MODULE__} discarded: media item #{media_item_id} stale")
|
||||
end
|
||||
|
||||
# If a user script exists and, when run, returns a non-zero exit code, prevent this and all future downloads
|
||||
# of the media item.
|
||||
defp fetch_and_run_prevent_download_user_script(media_item_id) do
|
||||
media_item = Media.get_media_item!(media_item_id)
|
||||
|
||||
{:ok, media_item} =
|
||||
case run_user_script(:media_pre_download, media_item) do
|
||||
{:ok, _, exit_code} when exit_code != 0 -> Media.update_media_item(media_item, %{prevent_download: true})
|
||||
_ -> {:ok, media_item}
|
||||
end
|
||||
|
||||
Repo.preload(media_item, :source)
|
||||
end
|
||||
|
||||
defp download_media_and_schedule_jobs(media_item, is_quality_upgrade, should_force) do
|
||||
overwrite_behaviour = if should_force || is_quality_upgrade, do: :force_overwrites, else: :no_force_overwrites
|
||||
override_opts = [overwrite_behaviour: overwrite_behaviour]
|
||||
@@ -74,9 +85,9 @@ defmodule Pinchflat.Downloading.MediaDownloadWorker do
|
||||
media_redownloaded_at: get_redownloaded_at(is_quality_upgrade)
|
||||
})
|
||||
|
||||
:ok = run_user_script(updated_media_item)
|
||||
run_user_script(:media_downloaded, updated_media_item)
|
||||
|
||||
{:ok, updated_media_item}
|
||||
:ok
|
||||
|
||||
{:recovered, _} ->
|
||||
{:error, :retry}
|
||||
@@ -112,9 +123,9 @@ defmodule Pinchflat.Downloading.MediaDownloadWorker do
|
||||
|
||||
# NOTE: I like this pattern of using the default value so that I don't have to
|
||||
# define it in config.exs (and friends). Consider using this elsewhere.
|
||||
defp run_user_script(media_item) do
|
||||
defp run_user_script(event, media_item) do
|
||||
runner = Application.get_env(:pinchflat, :user_script_runner, UserScriptRunner)
|
||||
|
||||
runner.run(:media_downloaded, media_item)
|
||||
runner.run(event, media_item)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -79,6 +79,7 @@ defmodule Pinchflat.Downloading.MediaDownloader do
|
||||
|> MetadataParser.parse_for_media_item()
|
||||
|> Map.merge(%{
|
||||
media_downloaded_at: DateTime.utc_now(),
|
||||
culled_at: nil,
|
||||
nfo_filepath: determine_nfo_filepath(media_with_preloads, parsed_json),
|
||||
metadata: %{
|
||||
# IDEA: might be worth kicking off a job for this since thumbnail fetching
|
||||
@@ -105,8 +106,9 @@ defmodule Pinchflat.Downloading.MediaDownloader do
|
||||
|
||||
defp download_with_options(url, item_with_preloads, output_filepath, override_opts) do
|
||||
{:ok, options} = DownloadOptionBuilder.build(item_with_preloads, override_opts)
|
||||
runner_opts = [output_filepath: output_filepath, use_cookies: item_with_preloads.source.use_cookies]
|
||||
|
||||
YtDlpMedia.download(url, options, output_filepath: output_filepath)
|
||||
YtDlpMedia.download(url, options, runner_opts)
|
||||
end
|
||||
|
||||
defp recoverable_errors do
|
||||
|
||||
@@ -23,10 +23,10 @@ defmodule Pinchflat.Downloading.MediaQualityUpgradeWorker do
|
||||
"""
|
||||
@impl Oban.Worker
|
||||
def perform(%Oban.Job{}) do
|
||||
redownloadable_media = Media.list_redownloadable_media_items()
|
||||
Logger.info("Redownloading #{length(redownloadable_media)} media items")
|
||||
upgradable_media = Media.list_upgradeable_media_items()
|
||||
Logger.info("Redownloading #{length(upgradable_media)} media items")
|
||||
|
||||
Enum.each(redownloadable_media, fn media_item ->
|
||||
Enum.each(upgradable_media, fn media_item ->
|
||||
MediaDownloadWorker.kickoff_with_task(media_item, %{quality_upgrade?: true})
|
||||
end)
|
||||
end
|
||||
|
||||
@@ -2,12 +2,15 @@ defmodule Pinchflat.Downloading.MediaRetentionWorker do
|
||||
@moduledoc false
|
||||
|
||||
use Oban.Worker,
|
||||
queue: :local_metadata,
|
||||
queue: :local_data,
|
||||
unique: [period: :infinity, states: [:available, :scheduled, :retryable, :executing]],
|
||||
tags: ["media_item", "local_metadata"]
|
||||
tags: ["media_item", "local_data"]
|
||||
|
||||
use Pinchflat.Media.MediaQuery
|
||||
|
||||
require Logger
|
||||
|
||||
alias Pinchflat.Repo
|
||||
alias Pinchflat.Media
|
||||
|
||||
@doc """
|
||||
@@ -20,14 +23,51 @@ defmodule Pinchflat.Downloading.MediaRetentionWorker do
|
||||
"""
|
||||
@impl Oban.Worker
|
||||
def perform(%Oban.Job{}) do
|
||||
cullable_media = Media.list_cullable_media_items()
|
||||
cull_cullable_media_items()
|
||||
delete_media_items_from_before_cutoff()
|
||||
|
||||
:ok
|
||||
end
|
||||
|
||||
defp cull_cullable_media_items do
|
||||
cullable_media =
|
||||
MediaQuery.new()
|
||||
|> MediaQuery.require_assoc(:source)
|
||||
|> where(^MediaQuery.cullable())
|
||||
|> Repo.all()
|
||||
|
||||
Logger.info("Culling #{length(cullable_media)} media items past their retention date")
|
||||
|
||||
Enum.each(cullable_media, fn media_item ->
|
||||
# Setting `prevent_download` does what it says on the tin, but `culled_at` is purely informational.
|
||||
# We don't actually do anything with that in terms of queries and it gets set to nil if the media item
|
||||
# gets re-downloaded.
|
||||
Media.delete_media_files(media_item, %{
|
||||
prevent_download: true,
|
||||
culled_at: DateTime.utc_now()
|
||||
})
|
||||
end)
|
||||
end
|
||||
|
||||
defp delete_media_items_from_before_cutoff do
|
||||
deletable_media =
|
||||
MediaQuery.new()
|
||||
|> MediaQuery.require_assoc(:source)
|
||||
|> where(^MediaQuery.deletable_based_on_source_cutoff())
|
||||
|> Repo.all()
|
||||
|
||||
Logger.info("Deleting #{length(deletable_media)} media items that are from before the source cutoff")
|
||||
|
||||
Enum.each(deletable_media, fn media_item ->
|
||||
# Note that I'm not setting `prevent_download` on the media_item here.
|
||||
# That's because cutoff_date can easily change and it's a valid behavior to re-download older
|
||||
# media items if the cutoff_date changes.
|
||||
# Download is ultimately prevented because `MediaQuery.pending()` only returns media items
|
||||
# from after the cutoff date (among other things), so it's not like the media will just immediately
|
||||
# be re-downloaded.
|
||||
Media.delete_media_files(media_item, %{
|
||||
culled_at: DateTime.utc_now()
|
||||
})
|
||||
end)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -54,7 +54,12 @@ defmodule Pinchflat.Downloading.OutputPathBuilder do
|
||||
"season_from_date" => "%(upload_date>%Y)S",
|
||||
"season_episode_from_date" => "s%(upload_date>%Y)Se%(upload_date>%m%d)S",
|
||||
"season_episode_index_from_date" => "s%(upload_date>%Y)Se%(upload_date>%m%d)S{{ media_upload_date_index }}",
|
||||
"artist_name" => "%(artist,creator,uploader,uploader_id)S"
|
||||
"artist_name" => "%(artist,creator,uploader,uploader_id)S",
|
||||
"static_season__episode_by_index" => "Season 1/s01e{{ media_playlist_index }}",
|
||||
"static_season__episode_by_date" => "Season 1/s01e%(upload_date>%y%m%d)S",
|
||||
"season_by_year__episode_by_date" => "Season %(upload_date>%Y)S/s%(upload_date>%Y)Se%(upload_date>%m%d)S",
|
||||
"season_by_year__episode_by_date_and_index" =>
|
||||
"Season %(upload_date>%Y)S/s%(upload_date>%Y)Se%(upload_date>%m%d)S{{ media_upload_date_index }}"
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
@@ -68,7 +68,7 @@ defmodule Pinchflat.FastIndexing.FastIndexingHelpers do
|
||||
defp create_media_item_from_media_id(source, media_id) do
|
||||
url = "https://www.youtube.com/watch?v=#{media_id}"
|
||||
|
||||
case YtDlpMedia.get_media_attributes(url) do
|
||||
case YtDlpMedia.get_media_attributes(url, use_cookies: source.use_cookies) do
|
||||
{:ok, media_attrs} ->
|
||||
Media.create_media_item_from_backend_attrs(source, media_attrs)
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@ defmodule Pinchflat.Lifecycle.UserScripts.CommandRunner do
|
||||
@behaviour UserScriptCommandRunner
|
||||
|
||||
@event_types [
|
||||
:media_pre_download,
|
||||
:media_downloaded,
|
||||
:media_deleted
|
||||
]
|
||||
@@ -22,24 +23,25 @@ defmodule Pinchflat.Lifecycle.UserScripts.CommandRunner do
|
||||
|
||||
This function will succeed in almost all cases, even if the user script command
|
||||
failed - this is because I don't want bad scripts to stop the whole process.
|
||||
If something fails, it'll be logged.
|
||||
If something fails, it'll be logged and returned BUT the tuple will always
|
||||
start with {:ok, ...}.
|
||||
|
||||
The only things that can cause a true failure are passing in an invalid event
|
||||
type or if the passed data cannot be encoded into JSON - both indicative of
|
||||
failures in the development process.
|
||||
|
||||
Returns :ok
|
||||
Returns {:ok, :no_executable} | {:ok, output, exit_code}
|
||||
"""
|
||||
@impl UserScriptCommandRunner
|
||||
def run(event_type, encodable_data) when event_type in @event_types do
|
||||
case backend_executable() do
|
||||
{:ok, :no_executable} ->
|
||||
:ok
|
||||
{:ok, :no_executable}
|
||||
|
||||
{:ok, executable_path} ->
|
||||
{:ok, encoded_data} = Phoenix.json_library().encode(encodable_data)
|
||||
|
||||
{_output, _exit_code} =
|
||||
{output, exit_code} =
|
||||
CliUtils.wrap_cmd(
|
||||
executable_path,
|
||||
[to_string(event_type), encoded_data],
|
||||
@@ -47,7 +49,7 @@ defmodule Pinchflat.Lifecycle.UserScripts.CommandRunner do
|
||||
logging_arg_override: "[suppressed]"
|
||||
)
|
||||
|
||||
:ok
|
||||
{:ok, output, exit_code}
|
||||
end
|
||||
end
|
||||
|
||||
@@ -62,7 +64,7 @@ defmodule Pinchflat.Lifecycle.UserScripts.CommandRunner do
|
||||
if FilesystemUtils.exists_and_nonempty?(filepath) do
|
||||
{:ok, filepath}
|
||||
else
|
||||
Logger.warning("User scripts lifecyle file either not present or is empty. Skipping.")
|
||||
Logger.info("User scripts lifecyle file either not present or is empty. Skipping.")
|
||||
|
||||
{:ok, :no_executable}
|
||||
end
|
||||
|
||||
@@ -25,21 +25,10 @@ defmodule Pinchflat.Media do
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns a list of media_items that are cullable based on the retention period
|
||||
of the source they belong to.
|
||||
|
||||
Returns [%MediaItem{}, ...]
|
||||
"""
|
||||
def list_cullable_media_items do
|
||||
MediaQuery.new()
|
||||
|> MediaQuery.require_assoc(:source)
|
||||
|> where(^MediaQuery.cullable())
|
||||
|> Repo.all()
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns a list of media_items that are redownloadable based on the redownload delay
|
||||
of the media_profile their source belongs to.
|
||||
Returns a list of media_items that are upgradeable based on the redownload delay
|
||||
of the media_profile their source belongs to. In this context, upgradeable means
|
||||
that it's been long enough since upload that the video may be in a higher quality
|
||||
or have better sponsorblock segments (or similar).
|
||||
|
||||
The logic is that a media_item is past_redownload_delay if the media_item's uploaded_at is
|
||||
at least redownload_delay_days ago AND `media_downloaded_at` - `redownload_delay_days`
|
||||
@@ -52,10 +41,10 @@ defmodule Pinchflat.Media do
|
||||
|
||||
Returns [%MediaItem{}, ...]
|
||||
"""
|
||||
def list_redownloadable_media_items do
|
||||
def list_upgradeable_media_items do
|
||||
MediaQuery.new()
|
||||
|> MediaQuery.require_assoc(:media_profile)
|
||||
|> where(^MediaQuery.redownloadable())
|
||||
|> where(^MediaQuery.upgradeable())
|
||||
|> Repo.all()
|
||||
end
|
||||
|
||||
@@ -142,12 +131,17 @@ defmodule Pinchflat.Media do
|
||||
"""
|
||||
def create_media_item_from_backend_attrs(source, media_attrs_struct) do
|
||||
attrs = Map.merge(%{source_id: source.id}, Map.from_struct(media_attrs_struct))
|
||||
# Some fields should only be set on insert and not on update.
|
||||
fields_to_drop_on_update = [:playlist_index]
|
||||
|
||||
%MediaItem{}
|
||||
|> MediaItem.changeset(attrs)
|
||||
|> Repo.insert(
|
||||
on_conflict: [
|
||||
set: Map.to_list(attrs)
|
||||
set:
|
||||
attrs
|
||||
|> Map.drop(fields_to_drop_on_update)
|
||||
|> Map.to_list()
|
||||
],
|
||||
conflict_target: [:source_id, :media_id]
|
||||
)
|
||||
@@ -177,7 +171,7 @@ defmodule Pinchflat.Media do
|
||||
|
||||
if delete_files do
|
||||
{:ok, _} = do_delete_media_files(media_item)
|
||||
:ok = run_user_script(:media_deleted, media_item)
|
||||
run_user_script(:media_deleted, media_item)
|
||||
end
|
||||
|
||||
# Should delete these no matter what
|
||||
@@ -200,7 +194,7 @@ defmodule Pinchflat.Media do
|
||||
|
||||
Tasks.delete_tasks_for(media_item)
|
||||
{:ok, _} = do_delete_media_files(media_item)
|
||||
:ok = run_user_script(:media_deleted, media_item)
|
||||
run_user_script(:media_deleted, media_item)
|
||||
|
||||
update_media_item(media_item, Map.merge(filepath_attrs, addl_attrs))
|
||||
end
|
||||
|
||||
@@ -18,6 +18,8 @@ defmodule Pinchflat.Media.MediaItem do
|
||||
alias Pinchflat.Media.MediaItemsSearchIndex
|
||||
|
||||
@allowed_fields [
|
||||
# these fields are only captured on index
|
||||
:playlist_index,
|
||||
# these fields are captured on indexing (and again on download)
|
||||
:title,
|
||||
:media_id,
|
||||
@@ -72,6 +74,7 @@ defmodule Pinchflat.Media.MediaItem do
|
||||
field :uploaded_at, :utc_datetime
|
||||
field :upload_date_index, :integer, default: 0
|
||||
field :duration_seconds, :integer
|
||||
field :playlist_index, :integer, default: 0
|
||||
|
||||
field :media_filepath, :string
|
||||
field :media_size_bytes, :integer
|
||||
|
||||
@@ -33,7 +33,6 @@ defmodule Pinchflat.Media.MediaQuery do
|
||||
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_matches(other_date), do: dynamic([mi], fragment("date(?) = date(?)", mi.uploaded_at, ^other_date))
|
||||
|
||||
@@ -76,6 +75,14 @@ defmodule Pinchflat.Media.MediaQuery do
|
||||
)
|
||||
end
|
||||
|
||||
def meets_min_and_max_duration do
|
||||
dynamic(
|
||||
[mi, source],
|
||||
(is_nil(source.min_duration_seconds) or fragment("duration_seconds >= ?", source.min_duration_seconds)) and
|
||||
(is_nil(source.max_duration_seconds) or fragment("duration_seconds <= ?", source.max_duration_seconds))
|
||||
)
|
||||
end
|
||||
|
||||
def past_retention_period do
|
||||
dynamic(
|
||||
[mi, source],
|
||||
@@ -108,6 +115,15 @@ defmodule Pinchflat.Media.MediaQuery do
|
||||
)
|
||||
end
|
||||
|
||||
def deletable_based_on_source_cutoff do
|
||||
dynamic(
|
||||
[mi, source],
|
||||
^downloaded() and
|
||||
not (^upload_date_after_source_cutoff()) and
|
||||
not (^culling_prevented())
|
||||
)
|
||||
end
|
||||
|
||||
def pending do
|
||||
dynamic(
|
||||
[mi],
|
||||
@@ -115,16 +131,16 @@ defmodule Pinchflat.Media.MediaQuery do
|
||||
not (^download_prevented()) and
|
||||
^upload_date_after_source_cutoff() and
|
||||
^format_matching_profile_preference() and
|
||||
^matches_source_title_regex()
|
||||
^matches_source_title_regex() and
|
||||
^meets_min_and_max_duration()
|
||||
)
|
||||
end
|
||||
|
||||
def redownloadable do
|
||||
def upgradeable do
|
||||
dynamic(
|
||||
[mi, source],
|
||||
^downloaded() and
|
||||
not (^download_prevented()) and
|
||||
not (^culled()) and
|
||||
not (^redownloaded()) and
|
||||
^past_redownload_delay()
|
||||
)
|
||||
|
||||
@@ -62,11 +62,13 @@ defmodule Pinchflat.Metadata.MetadataFileHelpers do
|
||||
|
||||
Returns binary() | nil
|
||||
"""
|
||||
def download_and_store_thumbnail_for(database_record) do
|
||||
yt_dlp_filepath = generate_filepath_for(database_record, "thumbnail.%(ext)s")
|
||||
real_filepath = generate_filepath_for(database_record, "thumbnail.jpg")
|
||||
def download_and_store_thumbnail_for(media_item_with_preloads) do
|
||||
yt_dlp_filepath = generate_filepath_for(media_item_with_preloads, "thumbnail.%(ext)s")
|
||||
real_filepath = generate_filepath_for(media_item_with_preloads, "thumbnail.jpg")
|
||||
command_opts = [output: yt_dlp_filepath]
|
||||
addl_opts = [use_cookies: media_item_with_preloads.source.use_cookies]
|
||||
|
||||
case YtDlpMedia.download_thumbnail(database_record.original_url, output: yt_dlp_filepath) do
|
||||
case YtDlpMedia.download_thumbnail(media_item_with_preloads.original_url, command_opts, addl_opts) do
|
||||
{:ok, _} -> real_filepath
|
||||
_ -> nil
|
||||
end
|
||||
@@ -124,6 +126,21 @@ defmodule Pinchflat.Metadata.MetadataFileHelpers do
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Attempts to determine the season and episode number from a media filepath.
|
||||
|
||||
Returns {:ok, {binary(), binary()}} | {:error, :indeterminable}
|
||||
"""
|
||||
def season_and_episode_from_media_filepath(media_filepath) do
|
||||
# matches s + 1 or more digits + e + 1 or more digits (case-insensitive)
|
||||
season_episode_regex = ~r/s(\d+)e(\d+)/i
|
||||
|
||||
case Regex.scan(season_episode_regex, media_filepath) do
|
||||
[[_, season, episode] | _] -> {:ok, {season, episode}}
|
||||
_ -> {:error, :indeterminable}
|
||||
end
|
||||
end
|
||||
|
||||
defp generate_filepath_for(database_record, filename) do
|
||||
Path.join([
|
||||
metadata_directory_for(database_record),
|
||||
|
||||
@@ -6,8 +6,8 @@ defmodule Pinchflat.Metadata.NfoBuilder do
|
||||
|
||||
import Pinchflat.Utils.XmlUtils, only: [safe: 1]
|
||||
|
||||
alias Pinchflat.Metadata.MetadataFileHelpers
|
||||
alias Pinchflat.Utils.FilesystemUtils
|
||||
alias Pinchflat.Metadata.MetadataFileHelpers
|
||||
|
||||
@doc """
|
||||
Builds an NFO file for a media item (read: single "episode") and
|
||||
@@ -15,12 +15,12 @@ defmodule Pinchflat.Metadata.NfoBuilder do
|
||||
|
||||
Returns the filepath of the NFO file.
|
||||
"""
|
||||
def build_and_store_for_media_item(filepath, metadata) do
|
||||
nfo = build_for_media_item(metadata)
|
||||
def build_and_store_for_media_item(nfo_filepath, metadata) do
|
||||
nfo = build_for_media_item(nfo_filepath, metadata)
|
||||
|
||||
FilesystemUtils.write_p!(filepath, nfo)
|
||||
FilesystemUtils.write_p!(nfo_filepath, nfo)
|
||||
|
||||
filepath
|
||||
nfo_filepath
|
||||
end
|
||||
|
||||
@doc """
|
||||
@@ -37,10 +37,15 @@ defmodule Pinchflat.Metadata.NfoBuilder do
|
||||
filepath
|
||||
end
|
||||
|
||||
defp build_for_media_item(metadata) do
|
||||
defp build_for_media_item(nfo_filepath, metadata) do
|
||||
upload_date = MetadataFileHelpers.parse_upload_date(metadata["upload_date"])
|
||||
# NOTE: the filepath here isn't the path of the media item, it's the path that
|
||||
# the NFO should be saved to. This works because the NFO's path is the same as
|
||||
# the media's path, just with a different extension. If this ever changes I'll
|
||||
# need to pass in the media item's path as well.
|
||||
{season, episode} = determine_season_and_episode_number(nfo_filepath, upload_date)
|
||||
|
||||
# Cribbed from a combination of the Kodi wiki, ytdl-nfo, and ytdl-sub.
|
||||
# WHO NEEDS A FANCY XML PARSER ANYWAY?!
|
||||
"""
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="yes" ?>
|
||||
<episodedetails>
|
||||
@@ -49,8 +54,8 @@ defmodule Pinchflat.Metadata.NfoBuilder do
|
||||
<uniqueid type="youtube" default="true">#{safe(metadata["id"])}</uniqueid>
|
||||
<plot>#{safe(metadata["description"])}</plot>
|
||||
<aired>#{safe(upload_date)}</aired>
|
||||
<season>#{safe(upload_date.year)}</season>
|
||||
<episode>#{Calendar.strftime(upload_date, "%m%d")}</episode>
|
||||
<season>#{safe(season)}</season>
|
||||
<episode>#{episode}</episode>
|
||||
<genre>YouTube</genre>
|
||||
</episodedetails>
|
||||
"""
|
||||
@@ -67,4 +72,11 @@ defmodule Pinchflat.Metadata.NfoBuilder do
|
||||
</tvshow>
|
||||
"""
|
||||
end
|
||||
|
||||
defp determine_season_and_episode_number(filepath, upload_date) do
|
||||
case MetadataFileHelpers.season_and_episode_from_media_filepath(filepath) do
|
||||
{:ok, {season, episode}} -> {season, episode}
|
||||
{:error, _} -> {upload_date.year, Calendar.strftime(upload_date, "%m%d")}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -20,29 +20,49 @@ defmodule Pinchflat.Metadata.SourceImageParser do
|
||||
def store_source_images(base_directory, source_metadata) do
|
||||
(source_metadata["thumbnails"] || [])
|
||||
|> Enum.filter(&(&1["filepath"] != nil))
|
||||
|> select_useful_images()
|
||||
|> select_useful_images(source_metadata)
|
||||
|> Enum.map(&move_image(&1, base_directory))
|
||||
|> Enum.into(%{})
|
||||
end
|
||||
|
||||
defp select_useful_images(images) do
|
||||
defp select_useful_images(images, source_metadata) do
|
||||
labelled_images =
|
||||
Enum.reduce(images, [], fn image_map, acc ->
|
||||
Enum.reduce(images, %{}, fn image_map, acc ->
|
||||
case image_map do
|
||||
%{"id" => "avatar_uncropped"} ->
|
||||
acc ++ [{:poster, :poster_filepath, image_map["filepath"]}]
|
||||
|
||||
%{"id" => "banner_uncropped"} ->
|
||||
acc ++ [{:fanart, :fanart_filepath, image_map["filepath"]}]
|
||||
|
||||
_ ->
|
||||
acc
|
||||
%{"id" => "avatar_uncropped"} -> put_image_key(acc, :poster, image_map["filepath"])
|
||||
%{"id" => "banner_uncropped"} -> put_image_key(acc, :fanart, image_map["filepath"])
|
||||
_ -> acc
|
||||
end
|
||||
end)
|
||||
|
||||
labelled_images
|
||||
|> Enum.concat([{:banner, :banner_filepath, determine_best_banner(images)}])
|
||||
|> Enum.filter(fn {_, _, tmp_filepath} -> tmp_filepath end)
|
||||
|> add_fallback_poster(source_metadata)
|
||||
|> put_image_key(:banner, determine_best_banner(images))
|
||||
|> Enum.filter(fn {_key, attrs} -> attrs.current_filepath end)
|
||||
end
|
||||
|
||||
# If a poster is set, short-circuit and return the images as-is
|
||||
defp add_fallback_poster(%{poster: _} = images, _), do: images
|
||||
|
||||
# If a poster is NOT set, see if we can find a suitable image to use as a fallback
|
||||
defp add_fallback_poster(images, source_metadata) do
|
||||
case source_metadata["entries"] do
|
||||
nil -> images
|
||||
[] -> images
|
||||
[first_entry | _] -> add_poster_from_entry_thumbnail(images, first_entry)
|
||||
end
|
||||
end
|
||||
|
||||
defp add_poster_from_entry_thumbnail(images, entry) do
|
||||
thumbnail =
|
||||
(entry["thumbnails"] || [])
|
||||
|> Enum.reverse()
|
||||
|> Enum.find(& &1["filepath"])
|
||||
|
||||
case thumbnail do
|
||||
nil -> images
|
||||
_ -> put_image_key(images, :poster, thumbnail["filepath"])
|
||||
end
|
||||
end
|
||||
|
||||
defp determine_best_banner(images) do
|
||||
@@ -58,12 +78,22 @@ defmodule Pinchflat.Metadata.SourceImageParser do
|
||||
Map.get(best_candidate || %{}, "filepath")
|
||||
end
|
||||
|
||||
defp move_image({filename, source_attr_name, tmp_filepath}, base_directory) do
|
||||
extension = Path.extname(tmp_filepath)
|
||||
final_filepath = Path.join([base_directory, "#{filename}#{extension}"])
|
||||
defp move_image({_key, attrs}, base_directory) do
|
||||
extension = Path.extname(attrs.current_filepath)
|
||||
final_filepath = Path.join([base_directory, "#{attrs.final_filename}#{extension}"])
|
||||
|
||||
FilesystemUtils.cp_p!(tmp_filepath, final_filepath)
|
||||
FilesystemUtils.cp_p!(attrs.current_filepath, final_filepath)
|
||||
|
||||
{source_attr_name, final_filepath}
|
||||
{attrs.attribute_name, final_filepath}
|
||||
end
|
||||
|
||||
defp put_image_key(map, key, image) do
|
||||
attribute_atom = String.to_existing_atom("#{key}_filepath")
|
||||
|
||||
Map.put(map, key, %{
|
||||
attribute_name: attribute_atom,
|
||||
final_filename: to_string(key),
|
||||
current_filepath: image
|
||||
})
|
||||
end
|
||||
end
|
||||
|
||||
@@ -77,10 +77,8 @@ defmodule Pinchflat.Metadata.SourceMetadataStorageWorker do
|
||||
|
||||
defp fetch_source_metadata_and_images(series_directory, source) do
|
||||
metadata_directory = MetadataFileHelpers.metadata_directory_for(source)
|
||||
tmp_output_path = "#{tmp_directory()}/#{StringUtils.random_string(16)}/source_image.%(ext)S"
|
||||
opts = [:write_all_thumbnails, convert_thumbnails: "jpg", output: tmp_output_path]
|
||||
|
||||
{:ok, metadata} = MediaCollection.get_source_metadata(source.original_url, opts)
|
||||
{:ok, metadata} = fetch_metadata_for_source(source)
|
||||
metadata_image_attrs = SourceImageParser.store_source_images(metadata_directory, metadata)
|
||||
|
||||
if source.media_profile.download_source_images && series_directory do
|
||||
@@ -94,7 +92,9 @@ defmodule Pinchflat.Metadata.SourceMetadataStorageWorker do
|
||||
|
||||
defp determine_series_directory(source) do
|
||||
output_path = DownloadOptionBuilder.build_output_path_for(source)
|
||||
{:ok, %{filepath: filepath}} = MediaCollection.get_source_details(source.original_url, output: output_path)
|
||||
runner_opts = [output: output_path]
|
||||
addl_opts = [use_cookies: source.use_cookies]
|
||||
{:ok, %{filepath: filepath}} = MediaCollection.get_source_details(source.original_url, runner_opts, addl_opts)
|
||||
|
||||
case MetadataFileHelpers.series_directory_from_media_filepath(filepath) do
|
||||
{:ok, series_directory} -> series_directory
|
||||
@@ -110,6 +110,20 @@ defmodule Pinchflat.Metadata.SourceMetadataStorageWorker do
|
||||
end
|
||||
end
|
||||
|
||||
defp fetch_metadata_for_source(source) do
|
||||
tmp_output_path = "#{tmp_directory()}/#{StringUtils.random_string(16)}/source_image.%(ext)S"
|
||||
base_opts = [convert_thumbnails: "jpg", output: tmp_output_path]
|
||||
|
||||
opts =
|
||||
if source.collection_type == :channel do
|
||||
base_opts ++ [:write_all_thumbnails, playlist_items: 0]
|
||||
else
|
||||
base_opts ++ [:write_thumbnail, playlist_items: 1]
|
||||
end
|
||||
|
||||
MediaCollection.get_source_metadata(source.original_url, opts, use_cookies: source.use_cookies)
|
||||
end
|
||||
|
||||
defp tmp_directory do
|
||||
Application.get_env(:pinchflat, :tmpfile_directory)
|
||||
end
|
||||
|
||||
@@ -27,7 +27,9 @@ defmodule Pinchflat.Profiles.MediaProfile do
|
||||
shorts_behaviour
|
||||
livestream_behaviour
|
||||
preferred_resolution
|
||||
media_container
|
||||
redownload_delay_days
|
||||
marked_for_deletion_at
|
||||
)a
|
||||
|
||||
@required_fields ~w(name output_path_template)a
|
||||
@@ -64,6 +66,9 @@ defmodule Pinchflat.Profiles.MediaProfile do
|
||||
field :shorts_behaviour, Ecto.Enum, values: ~w(include exclude only)a, default: :include
|
||||
field :livestream_behaviour, Ecto.Enum, values: ~w(include exclude only)a, default: :include
|
||||
field :preferred_resolution, Ecto.Enum, values: ~w(4320p 2160p 1080p 720p 480p 360p audio)a, default: :"1080p"
|
||||
field :media_container, :string, default: nil
|
||||
|
||||
field :marked_for_deletion_at, :utc_datetime
|
||||
|
||||
has_many :sources, Source
|
||||
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
defmodule Pinchflat.Profiles.MediaProfileDeletionWorker do
|
||||
@moduledoc false
|
||||
|
||||
use Oban.Worker,
|
||||
queue: :local_data,
|
||||
tags: ["media_profiles", "local_data"]
|
||||
|
||||
require Logger
|
||||
|
||||
alias __MODULE__
|
||||
alias Pinchflat.Profiles
|
||||
|
||||
@doc """
|
||||
Starts the profile deletion worker. Does not attach it to a task like `kickoff_with_task/2`
|
||||
since deletion also cancels all tasks for the profile
|
||||
|
||||
Returns {:ok, %Task{}} | {:error, %Ecto.Changeset{}}
|
||||
"""
|
||||
def kickoff(profile, job_args \\ %{}, job_opts \\ []) do
|
||||
%{id: profile.id}
|
||||
|> Map.merge(job_args)
|
||||
|> MediaProfileDeletionWorker.new(job_opts)
|
||||
|> Oban.insert()
|
||||
end
|
||||
|
||||
@doc """
|
||||
Deletes a profile and optionally deletes its files
|
||||
|
||||
Returns :ok
|
||||
"""
|
||||
@impl Oban.Worker
|
||||
def perform(%Oban.Job{args: %{"id" => profile_id} = args}) do
|
||||
delete_files = Map.get(args, "delete_files", false)
|
||||
profile = Profiles.get_media_profile!(profile_id)
|
||||
|
||||
Profiles.delete_media_profile(profile, delete_files: delete_files)
|
||||
end
|
||||
end
|
||||
@@ -28,7 +28,7 @@ defmodule Pinchflat.SlowIndexing.SlowIndexingHelpers do
|
||||
"""
|
||||
def kickoff_indexing_task(%Source{} = source, job_args \\ %{}, job_opts \\ []) do
|
||||
Tasks.delete_pending_tasks_for(source, "FastIndexingWorker")
|
||||
Tasks.delete_pending_tasks_for(source, "MediaCollectionIndexingWorker")
|
||||
Tasks.delete_pending_tasks_for(source, "MediaCollectionIndexingWorker", include_executing: true)
|
||||
|
||||
MediaCollectionIndexingWorker.kickoff_with_task(source, job_args, job_opts)
|
||||
end
|
||||
@@ -94,7 +94,8 @@ defmodule Pinchflat.SlowIndexing.SlowIndexingHelpers do
|
||||
{:ok, pid} = FileFollowerServer.start_link()
|
||||
|
||||
handler = fn filepath -> setup_file_follower_watcher(pid, filepath, source) end
|
||||
result = MediaCollection.get_media_attributes_for_collection(source.original_url, file_listener_handler: handler)
|
||||
runner_opts = [file_listener_handler: handler, use_cookies: source.use_cookies]
|
||||
result = MediaCollection.get_media_attributes_for_collection(source.original_url, runner_opts)
|
||||
|
||||
FileFollowerServer.stop(pid)
|
||||
|
||||
|
||||
@@ -27,6 +27,7 @@ defmodule Pinchflat.Sources.Source do
|
||||
series_directory
|
||||
index_frequency_minutes
|
||||
fast_index
|
||||
use_cookies
|
||||
download_media
|
||||
last_indexed_at
|
||||
original_url
|
||||
@@ -35,6 +36,9 @@ defmodule Pinchflat.Sources.Source do
|
||||
title_filter_regex
|
||||
media_profile_id
|
||||
output_path_template_override
|
||||
marked_for_deletion_at
|
||||
min_duration_seconds
|
||||
max_duration_seconds
|
||||
)a
|
||||
|
||||
# Expensive API calls are made when a source is inserted/updated so
|
||||
@@ -72,6 +76,7 @@ defmodule Pinchflat.Sources.Source do
|
||||
field :collection_type, Ecto.Enum, values: [:channel, :playlist]
|
||||
field :index_frequency_minutes, :integer, default: 60 * 24
|
||||
field :fast_index, :boolean, default: false
|
||||
field :use_cookies, :boolean, default: false
|
||||
field :download_media, :boolean, default: true
|
||||
field :last_indexed_at, :utc_datetime
|
||||
# Only download media items that were published after this date
|
||||
@@ -81,12 +86,17 @@ defmodule Pinchflat.Sources.Source do
|
||||
field :title_filter_regex, :string
|
||||
field :output_path_template_override, :string
|
||||
|
||||
field :min_duration_seconds, :integer
|
||||
field :max_duration_seconds, :integer
|
||||
|
||||
field :series_directory, :string
|
||||
field :nfo_filepath, :string
|
||||
field :poster_filepath, :string
|
||||
field :fanart_filepath, :string
|
||||
field :banner_filepath, :string
|
||||
|
||||
field :marked_for_deletion_at, :utc_datetime
|
||||
|
||||
belongs_to :media_profile, MediaProfile
|
||||
|
||||
has_one :metadata, SourceMetadata, on_replace: :update
|
||||
@@ -113,6 +123,7 @@ defmodule Pinchflat.Sources.Source do
|
||||
|> dynamic_default(:uuid, fn _ -> Ecto.UUID.generate() end)
|
||||
|> validate_required(required_fields)
|
||||
|> validate_title_regex()
|
||||
|> validate_min_and_max_durations()
|
||||
|> validate_number(:retention_period_days, greater_than_or_equal_to: 0)
|
||||
# Ensures it ends with `.{{ ext }}` or `.%(ext)s` or similar (with a little wiggle room)
|
||||
|> validate_format(:output_path_template_override, MediaProfile.ext_regex(), message: "must end with .{{ ext }}")
|
||||
@@ -159,6 +170,17 @@ defmodule Pinchflat.Sources.Source do
|
||||
|
||||
defp validate_title_regex(changeset), do: changeset
|
||||
|
||||
defp validate_min_and_max_durations(changeset) do
|
||||
min_duration = get_change(changeset, :min_duration_seconds)
|
||||
max_duration = get_change(changeset, :max_duration_seconds)
|
||||
|
||||
case {min_duration, max_duration} do
|
||||
{min, max} when is_nil(min) or is_nil(max) -> changeset
|
||||
{min, max} when min >= max -> add_error(changeset, :max_duration_seconds, "must be greater than minumum duration")
|
||||
_ -> changeset
|
||||
end
|
||||
end
|
||||
|
||||
defimpl Jason.Encoder, for: Source do
|
||||
def encode(value, opts) do
|
||||
value
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
defmodule Pinchflat.Sources.SourceDeletionWorker do
|
||||
@moduledoc false
|
||||
|
||||
use Oban.Worker,
|
||||
queue: :local_data,
|
||||
tags: ["sources", "local_data"]
|
||||
|
||||
require Logger
|
||||
|
||||
alias __MODULE__
|
||||
alias Pinchflat.Sources
|
||||
|
||||
@doc """
|
||||
Starts the source deletion worker. Does not attach it to a task like `kickoff_with_task/2`
|
||||
since deletion also cancels all tasks for the source
|
||||
|
||||
Returns {:ok, %Task{}} | {:error, %Ecto.Changeset{}}
|
||||
"""
|
||||
def kickoff(source, job_args \\ %{}, job_opts \\ []) do
|
||||
%{id: source.id}
|
||||
|> Map.merge(job_args)
|
||||
|> SourceDeletionWorker.new(job_opts)
|
||||
|> Oban.insert()
|
||||
end
|
||||
|
||||
@doc """
|
||||
Deletes a source and optionally deletes its files
|
||||
|
||||
Returns :ok
|
||||
"""
|
||||
@impl Oban.Worker
|
||||
def perform(%Oban.Job{args: %{"id" => source_id} = args}) do
|
||||
delete_files = Map.get(args, "delete_files", false)
|
||||
source = Sources.get_source!(source_id)
|
||||
|
||||
Sources.delete_source(source, delete_files: delete_files)
|
||||
end
|
||||
end
|
||||
@@ -180,7 +180,9 @@ defmodule Pinchflat.Sources do
|
||||
end
|
||||
|
||||
defp add_source_details_to_changeset(source, changeset) do
|
||||
case MediaCollection.get_source_details(changeset.changes.original_url) do
|
||||
use_cookies = Ecto.Changeset.get_field(changeset, :use_cookies)
|
||||
|
||||
case MediaCollection.get_source_details(changeset.changes.original_url, [], use_cookies: use_cookies) do
|
||||
{:ok, source_details} ->
|
||||
add_source_details_by_collection_type(source, changeset, source_details)
|
||||
|
||||
|
||||
@@ -53,20 +53,6 @@ defmodule Pinchflat.Tasks do
|
||||
)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns the list of pending tasks for a given record type and ID. Optionally allows you to specify
|
||||
which worker to include.
|
||||
|
||||
Returns [%Task{}, ...]
|
||||
"""
|
||||
def list_pending_tasks_for(record, worker_name \\ nil) do
|
||||
list_tasks_for(
|
||||
record,
|
||||
worker_name,
|
||||
[:available, :scheduled, :retryable]
|
||||
)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Gets a single task.
|
||||
|
||||
@@ -127,13 +113,13 @@ defmodule Pinchflat.Tasks do
|
||||
|
||||
@doc """
|
||||
Deletes all tasks attached to a given record, cancelling any attached jobs.
|
||||
Optionally allows you to specify which worker to include.
|
||||
Optionally allows you to specify which worker and job states to include.
|
||||
|
||||
Returns :ok
|
||||
"""
|
||||
def delete_tasks_for(record, worker_name \\ nil) do
|
||||
def delete_tasks_for(record, worker_name \\ nil, job_states \\ Oban.Job.states()) do
|
||||
record
|
||||
|> list_tasks_for(worker_name)
|
||||
|> list_tasks_for(worker_name, job_states)
|
||||
|> Enum.each(&delete_task/1)
|
||||
end
|
||||
|
||||
@@ -143,10 +129,12 @@ defmodule Pinchflat.Tasks do
|
||||
|
||||
Returns :ok
|
||||
"""
|
||||
def delete_pending_tasks_for(record, worker_name \\ nil) do
|
||||
record
|
||||
|> list_pending_tasks_for(worker_name)
|
||||
|> Enum.each(&delete_task/1)
|
||||
def delete_pending_tasks_for(record, worker_name \\ nil, opts \\ []) do
|
||||
include_executing = Keyword.get(opts, :include_executing, false)
|
||||
base_job_states = [:available, :scheduled, :retryable]
|
||||
job_states = if include_executing, do: base_job_states ++ [:executing], else: base_job_states
|
||||
|
||||
delete_tasks_for(record, worker_name, job_states)
|
||||
end
|
||||
|
||||
@doc """
|
||||
|
||||
@@ -18,6 +18,8 @@ defmodule Pinchflat.YtDlp.CommandRunner do
|
||||
- :output_filepath - the path to save the output to. If not provided, a temporary
|
||||
file will be created and used. Useful for if you need a reference to the file
|
||||
for a file watcher.
|
||||
- :use_cookies - if true, will add a cookie file to the command options. Will not
|
||||
attach a cookie file if the user hasn't set one up.
|
||||
|
||||
Returns {:ok, binary()} | {:error, output, status}.
|
||||
"""
|
||||
@@ -28,7 +30,7 @@ defmodule Pinchflat.YtDlp.CommandRunner do
|
||||
|
||||
output_filepath = generate_output_filepath(addl_opts)
|
||||
print_to_file_opts = [{:print_to_file, output_template}, output_filepath]
|
||||
user_configured_opts = cookie_file_options()
|
||||
user_configured_opts = cookie_file_options(addl_opts)
|
||||
# These must stay in exactly this order, hence why I'm giving it its own variable.
|
||||
all_opts = command_opts ++ print_to_file_opts ++ user_configured_opts ++ global_options()
|
||||
formatted_command_opts = [url] ++ CliUtils.parse_options(all_opts)
|
||||
@@ -78,7 +80,14 @@ defmodule Pinchflat.YtDlp.CommandRunner do
|
||||
]
|
||||
end
|
||||
|
||||
defp cookie_file_options do
|
||||
defp cookie_file_options(addl_opts) do
|
||||
case Keyword.get(addl_opts, :use_cookies) do
|
||||
true -> add_cookie_file()
|
||||
_ -> []
|
||||
end
|
||||
end
|
||||
|
||||
defp add_cookie_file do
|
||||
base_dir = Application.get_env(:pinchflat, :extras_directory)
|
||||
filename_options_map = %{cookies: "cookies.txt"}
|
||||
|
||||
|
||||
@@ -22,7 +22,8 @@ defmodule Pinchflat.YtDlp.Media do
|
||||
:livestream,
|
||||
:short_form_content,
|
||||
:uploaded_at,
|
||||
:duration_seconds
|
||||
:duration_seconds,
|
||||
:playlist_index
|
||||
]
|
||||
|
||||
alias __MODULE__
|
||||
@@ -36,9 +37,9 @@ defmodule Pinchflat.YtDlp.Media do
|
||||
Returns {:ok, map()} | {:error, any, ...}.
|
||||
"""
|
||||
def download(url, command_opts \\ [], addl_opts \\ []) do
|
||||
opts = [:no_simulate] ++ command_opts
|
||||
all_command_opts = [:no_simulate] ++ command_opts
|
||||
|
||||
with {:ok, output} <- backend_runner().run(url, opts, "after_move:%()j", addl_opts),
|
||||
with {:ok, output} <- backend_runner().run(url, all_command_opts, "after_move:%()j", addl_opts),
|
||||
{:ok, parsed_json} <- Phoenix.json_library().decode(output) do
|
||||
{:ok, parsed_json}
|
||||
else
|
||||
@@ -52,25 +53,25 @@ defmodule Pinchflat.YtDlp.Media do
|
||||
|
||||
Returns {:ok, ""} | {:error, any, ...}.
|
||||
"""
|
||||
def download_thumbnail(url, command_opts \\ []) do
|
||||
opts = [:no_simulate, :skip_download, :write_thumbnail, convert_thumbnail: "jpg"] ++ command_opts
|
||||
def download_thumbnail(url, command_opts \\ [], addl_opts \\ []) do
|
||||
all_command_opts = [:no_simulate, :skip_download, :write_thumbnail, convert_thumbnail: "jpg"] ++ command_opts
|
||||
|
||||
# NOTE: it doesn't seem like this command actually returns anything in `after_move` since
|
||||
# we aren't downloading the main media file
|
||||
backend_runner().run(url, opts, "after_move:%()j")
|
||||
backend_runner().run(url, all_command_opts, "after_move:%()j", addl_opts)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns a map representing the media at the given URL.
|
||||
|
||||
Returns {:ok, [map()]} | {:error, any, ...}.
|
||||
Returns {:ok, %Media{}} | {:error, any, ...}.
|
||||
"""
|
||||
def get_media_attributes(url) do
|
||||
def get_media_attributes(url, addl_opts \\ []) do
|
||||
runner = Application.get_env(:pinchflat, :yt_dlp_runner)
|
||||
command_opts = [:simulate, :skip_download]
|
||||
output_template = indexing_output_template()
|
||||
|
||||
case runner.run(url, command_opts, output_template) do
|
||||
case runner.run(url, command_opts, output_template, addl_opts) do
|
||||
{:ok, output} ->
|
||||
output
|
||||
|> Phoenix.json_library().decode!()
|
||||
@@ -84,9 +85,11 @@ defmodule Pinchflat.YtDlp.Media do
|
||||
|
||||
@doc """
|
||||
Returns the output template for yt-dlp's indexing command.
|
||||
|
||||
NOTE: playlist_index is really only useful for playlists that will never change their order.
|
||||
"""
|
||||
def indexing_output_template do
|
||||
"%(.{id,title,was_live,webpage_url,description,aspect_ratio,duration,upload_date,timestamp})j"
|
||||
"%(.{id,title,live_status,webpage_url,description,aspect_ratio,duration,upload_date,timestamp,playlist_index})j"
|
||||
end
|
||||
|
||||
@doc """
|
||||
@@ -101,10 +104,11 @@ defmodule Pinchflat.YtDlp.Media do
|
||||
title: response["title"],
|
||||
description: response["description"],
|
||||
original_url: response["webpage_url"],
|
||||
livestream: !!response["was_live"],
|
||||
livestream: !!response["live_status"] && response["live_status"] != "not_live",
|
||||
duration_seconds: response["duration"] && round(response["duration"]),
|
||||
short_form_content: response["webpage_url"] && short_form_content?(response),
|
||||
uploaded_at: response["upload_date"] && parse_uploaded_at(response)
|
||||
uploaded_at: response["upload_date"] && parse_uploaded_at(response),
|
||||
playlist_index: response["playlist_index"] || 0
|
||||
}
|
||||
end
|
||||
|
||||
@@ -120,7 +124,7 @@ defmodule Pinchflat.YtDlp.Media do
|
||||
#
|
||||
# These don't fail if duration or aspect_ratio are missing
|
||||
# due to Elixir's comparison semantics
|
||||
response["duration"] <= 60 && response["aspect_ratio"] < 0.8
|
||||
response["duration"] <= 60 && response["aspect_ratio"] <= 0.85
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -25,15 +25,17 @@ defmodule Pinchflat.YtDlp.MediaCollection do
|
||||
# the first video has not released yet (ie: is a premier). We don't care about
|
||||
# available formats since we're just getting the media details
|
||||
command_opts = [:simulate, :skip_download, :ignore_no_formats_error, :no_warnings]
|
||||
use_cookies = Keyword.get(addl_opts, :use_cookies, false)
|
||||
output_template = YtDlpMedia.indexing_output_template()
|
||||
output_filepath = FilesystemUtils.generate_metadata_tmpfile(:json)
|
||||
file_listener_handler = Keyword.get(addl_opts, :file_listener_handler, false)
|
||||
runner_opts = [output_filepath: output_filepath, use_cookies: use_cookies]
|
||||
|
||||
if file_listener_handler do
|
||||
file_listener_handler.(output_filepath)
|
||||
end
|
||||
|
||||
case runner.run(url, command_opts, output_template, output_filepath: output_filepath) do
|
||||
case runner.run(url, command_opts, output_template, runner_opts) do
|
||||
{:ok, output} ->
|
||||
parsed_lines =
|
||||
output
|
||||
@@ -64,7 +66,7 @@ defmodule Pinchflat.YtDlp.MediaCollection do
|
||||
|
||||
Returns {:ok, map()} | {:error, any, ...}.
|
||||
"""
|
||||
def get_source_details(source_url, addl_opts \\ []) do
|
||||
def get_source_details(source_url, command_opts \\ [], addl_opts \\ []) do
|
||||
# `ignore_no_formats_error` is necessary because yt-dlp will error out if
|
||||
# the first video has not released yet (ie: is a premier). We don't care about
|
||||
# available formats since we're just getting the source details
|
||||
@@ -75,10 +77,10 @@ defmodule Pinchflat.YtDlp.MediaCollection do
|
||||
playlist_end: 1
|
||||
]
|
||||
|
||||
command_opts = default_opts ++ addl_opts
|
||||
all_command_opts = default_opts ++ command_opts
|
||||
output_template = "%(.{channel,channel_id,playlist_id,playlist_title,filename})j"
|
||||
|
||||
with {:ok, output} <- backend_runner().run(source_url, command_opts, output_template),
|
||||
with {:ok, output} <- backend_runner().run(source_url, all_command_opts, output_template, addl_opts),
|
||||
{:ok, parsed_json} <- Phoenix.json_library().decode(output) do
|
||||
{:ok, format_source_details(parsed_json)}
|
||||
else
|
||||
@@ -99,13 +101,24 @@ defmodule Pinchflat.YtDlp.MediaCollection do
|
||||
as a compressed blob for possible future use. That's why it's not getting formatted like
|
||||
`get_source_details/1`
|
||||
|
||||
! IMPORTANT ! - you'll always want to set `playlist_items: int` in `addl_opts.
|
||||
This is great if you want to also return details about the videos in the playlists,
|
||||
but it should be set in all cases to not over-fetch data.
|
||||
For channels you should usually set this to 0 since channels return all the
|
||||
metadata we need without needing to fetch the videos. On the other hand, playlists
|
||||
don't return very useful images so you can set this to 1 to get the first video's
|
||||
images, for instance.
|
||||
|
||||
Returns {:ok, map()} | {:error, any, ...}.
|
||||
"""
|
||||
def get_source_metadata(source_url, addl_opts \\ []) do
|
||||
opts = [playlist_items: 0] ++ addl_opts
|
||||
def get_source_metadata(source_url, command_opts, addl_opts \\ []) do
|
||||
# This only validates that the `playlist_items` key is present. It's otherwise unused
|
||||
_playlist_items = Keyword.fetch!(command_opts, :playlist_items)
|
||||
|
||||
all_command_opts = [:skip_download] ++ command_opts
|
||||
output_template = "playlist:%()j"
|
||||
|
||||
with {:ok, output} <- backend_runner().run(source_url, opts, output_template),
|
||||
with {:ok, output} <- backend_runner().run(source_url, all_command_opts, output_template, addl_opts),
|
||||
{:ok, parsed_json} <- Phoenix.json_library().decode(output) do
|
||||
{:ok, parsed_json}
|
||||
else
|
||||
|
||||
@@ -19,6 +19,7 @@ defmodule PinchflatWeb.CoreComponents do
|
||||
import PinchflatWeb.Gettext
|
||||
|
||||
alias Phoenix.LiveView.JS
|
||||
alias PinchflatWeb.CustomComponents.TextComponents
|
||||
|
||||
@doc """
|
||||
Renders a modal.
|
||||
@@ -644,7 +645,11 @@ defmodule PinchflatWeb.CoreComponents do
|
||||
<li :for={{k, v} <- @iterable_attributes} class="mb-2 w-2/3">
|
||||
<strong><%= k %>:</strong>
|
||||
<code class="inline-block text-sm font-mono text-gray p-0.5 mx-0.5">
|
||||
<%= if is_binary(v) && URI.parse(v).scheme && URI.parse(v).scheme =~ "http" do %>
|
||||
<TextComponents.inline_link href={v}><%= v %></TextComponents.inline_link>
|
||||
<% else %>
|
||||
<%= v %>
|
||||
<% end %>
|
||||
</code>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
@@ -1,14 +1,7 @@
|
||||
defmodule PinchflatWeb.ErrorHTML do
|
||||
use PinchflatWeb, :html
|
||||
|
||||
# If you want to customize your error pages,
|
||||
# uncomment the embed_templates/1 call below
|
||||
# and add pages to the error directory:
|
||||
#
|
||||
# * lib/pinchflat_web/controllers/error_html/404.html.heex
|
||||
# * lib/pinchflat_web/controllers/error_html/500.html.heex
|
||||
#
|
||||
# embed_templates "error_html/*"
|
||||
embed_templates "error_html/*"
|
||||
|
||||
# The default is to render a plain text page based on
|
||||
# the template name. For example, "404.html" becomes
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
<section>
|
||||
<h2 class="text-title-md2 font-bold text-white">404 (not found)</h2>
|
||||
</section>
|
||||
@@ -0,0 +1,29 @@
|
||||
<section>
|
||||
<h2 class="text-title-md2 font-bold text-white">Internal Server Error</h2>
|
||||
<p class="text-body-md text-white mt-2">
|
||||
This shouldn't happen! Please make a
|
||||
<.inline_link href="https://github.com/kieraneglin/pinchflat/issues/new/choose">GitHub issue</.inline_link>
|
||||
with the following information:
|
||||
</p>
|
||||
|
||||
<ul class="list-disc ml-8 mb-8">
|
||||
<li>What you were doing when you saw this page</li>
|
||||
<li>
|
||||
Your system details and logs from
|
||||
<.inline_link href={~p"/app_info"}>app info</.inline_link>
|
||||
</li>
|
||||
<li>All the information in the textarea below (use select all + copy)</li>
|
||||
</ul>
|
||||
<textarea class="w-full min-h-96 font-mono inline-block rounded-lg" readonly>
|
||||
**Status**:
|
||||
`<%= if Map.has_key?(assigns, :status), do: @status, else: "" %>`
|
||||
|
||||
**Reason**:
|
||||
`<%= if Map.has_key?(assigns, :reason), do: inspect(@reason), else: "" %>`
|
||||
|
||||
**Stacktrace**:
|
||||
```
|
||||
<%= if Map.has_key?(assigns, :stack), do: Exception.format_stacktrace(@stack), else: "" %>
|
||||
```
|
||||
</textarea>
|
||||
</section>
|
||||
@@ -5,10 +5,12 @@ defmodule PinchflatWeb.MediaProfiles.MediaProfileController do
|
||||
alias Pinchflat.Repo
|
||||
alias Pinchflat.Profiles
|
||||
alias Pinchflat.Profiles.MediaProfile
|
||||
alias Pinchflat.Profiles.MediaProfileDeletionWorker
|
||||
|
||||
def index(conn, _params) do
|
||||
media_profiles =
|
||||
MediaProfile
|
||||
|> where([mp], is_nil(mp.marked_for_deletion_at))
|
||||
|> order_by(asc: :name)
|
||||
|> Repo.all()
|
||||
|
||||
@@ -70,19 +72,15 @@ defmodule PinchflatWeb.MediaProfiles.MediaProfileController do
|
||||
end
|
||||
|
||||
def delete(conn, %{"id" => id} = params) do
|
||||
delete_files = Map.get(params, "delete_files", false)
|
||||
# This awkward comparison converts the string to a boolean
|
||||
delete_files = Map.get(params, "delete_files", "") == "true"
|
||||
media_profile = Profiles.get_media_profile!(id)
|
||||
{:ok, _media_profile} = Profiles.delete_media_profile(media_profile, delete_files: delete_files)
|
||||
|
||||
flash_message =
|
||||
if delete_files do
|
||||
"Media profile, its sources, and its files deleted successfully."
|
||||
else
|
||||
"Media profile and its sources deleted successfully. Files were not deleted."
|
||||
end
|
||||
{:ok, _} = Profiles.update_media_profile(media_profile, %{marked_for_deletion_at: DateTime.utc_now()})
|
||||
MediaProfileDeletionWorker.kickoff(media_profile, %{delete_files: delete_files})
|
||||
|
||||
conn
|
||||
|> put_flash(:info, flash_message)
|
||||
|> put_flash(:info, "Media Profile deletion started. This may take a while to complete.")
|
||||
|> redirect(to: ~p"/media_profiles")
|
||||
end
|
||||
|
||||
|
||||
@@ -53,12 +53,24 @@ defmodule PinchflatWeb.MediaProfiles.MediaProfileHTML do
|
||||
]
|
||||
end
|
||||
|
||||
def custom_output_template_options do
|
||||
def media_center_custom_output_template_options do
|
||||
%{
|
||||
season_by_year__episode_by_date: "<code>Season YYYY/sYYYYeMMDD</code>",
|
||||
season_by_year__episode_by_date_and_index:
|
||||
"same as the above but it handles dates better. <strong>This is the recommended option</strong>",
|
||||
static_season__episode_by_index:
|
||||
"<code>Season 1/s01eXX</code> where <code>XX</code> is the video's position in the playlist. Only recommended for playlists (not channels) that don't change",
|
||||
static_season__episode_by_date:
|
||||
"<code>Season 1/s01eYYMMDD</code>. Recommended for playlists that might change or where order isn't important"
|
||||
}
|
||||
end
|
||||
|
||||
def other_custom_output_template_options do
|
||||
%{
|
||||
upload_day: nil,
|
||||
upload_month: nil,
|
||||
upload_year: nil,
|
||||
upload_yyyy_mm_dd: "the upload date in the format YYYY-MM-DD",
|
||||
upload_yyyy_mm_dd: "the upload date in the format <code>YYYY-MM-DD</code>",
|
||||
source_custom_name: "the name of the sources that use this profile",
|
||||
source_collection_id: "the YouTube ID of the sources that use this profile",
|
||||
source_collection_name:
|
||||
@@ -66,9 +78,11 @@ defmodule PinchflatWeb.MediaProfiles.MediaProfileHTML do
|
||||
source_collection_type: "the collection type of the sources using this profile. Either 'channel' or 'playlist'",
|
||||
artist_name: "the name of the artist with fallbacks to other uploader fields",
|
||||
season_from_date: "alias for upload_year",
|
||||
season_episode_from_date: "the upload date formatted as sYYYYeMMDD",
|
||||
season_episode_from_date: "the upload date formatted as <code>sYYYYeMMDD</code>",
|
||||
season_episode_index_from_date:
|
||||
"the upload date formatted as sYYYYeMMDDII where II is an index to prevent date collisions"
|
||||
"the upload date formatted as <code>sYYYYeMMDDII</code> where <code>II</code> is an index to prevent date collisions",
|
||||
media_playlist_index:
|
||||
"the place of the media item in the playlist. Do not use with channels. May not work if the playlist is updated"
|
||||
}
|
||||
end
|
||||
|
||||
@@ -77,7 +91,6 @@ defmodule PinchflatWeb.MediaProfiles.MediaProfileHTML do
|
||||
id
|
||||
ext
|
||||
title
|
||||
fulltitle
|
||||
uploader
|
||||
channel
|
||||
upload_date
|
||||
@@ -99,7 +112,7 @@ defmodule PinchflatWeb.MediaProfiles.MediaProfileHTML do
|
||||
end
|
||||
|
||||
defp media_center_output_template do
|
||||
"/shows/{{ source_custom_name }}/Season {{ season_from_date }}/{{ season_episode_index_from_date }} - {{ title }}.{{ ext }}"
|
||||
"/shows/{{ source_custom_name }}/{{ season_by_year__episode_by_date_and_index }} - {{ title }}.{{ ext }}"
|
||||
end
|
||||
|
||||
defp audio_output_template do
|
||||
|
||||
+34
-14
@@ -1,4 +1,10 @@
|
||||
<.simple_form :let={f} for={@changeset} action={@action}>
|
||||
<.simple_form
|
||||
:let={f}
|
||||
for={@changeset}
|
||||
action={@action}
|
||||
x-data="{ advancedMode: !!JSON.parse(localStorage.getItem('advancedMode')) }"
|
||||
x-init="$watch('advancedMode', value => localStorage.setItem('advancedMode', JSON.stringify(value)))"
|
||||
>
|
||||
<.error :if={@changeset.action}>
|
||||
Oops, something went wrong! Please check the errors below.
|
||||
</.error>
|
||||
@@ -30,10 +36,14 @@
|
||||
</.input>
|
||||
</section>
|
||||
|
||||
<h3 class="mt-8 text-2xl text-black dark:text-white">
|
||||
<section class="flex justify-between items-center mt-8">
|
||||
<h3 class="text-2xl text-white">
|
||||
General Options
|
||||
</h3>
|
||||
|
||||
<span class="cursor-pointer hover:underline" x-on:click="advancedMode = !advancedMode">
|
||||
Editing Mode: <span x-text="advancedMode ? 'Advanced' : 'Standard'"></span>
|
||||
</span>
|
||||
</section>
|
||||
<section x-data="{
|
||||
presets: {
|
||||
default: 'Default',
|
||||
@@ -84,22 +94,22 @@
|
||||
/>
|
||||
</section>
|
||||
|
||||
<section x-data="{ presets: { default: false, media_center: false, audio: false, archiving: false } }">
|
||||
<.input
|
||||
field={f[:download_auto_subs]}
|
||||
type="toggle"
|
||||
label="Download Autogenerated Subtitles"
|
||||
help="Prefers normal subs but will download autogenerated if needed. Requires 'Download Subtitles' to be enabled"
|
||||
x-init="$watch('selectedPreset', p => p && (enabled = presets[p]))"
|
||||
/>
|
||||
</section>
|
||||
|
||||
<section x-data="{ presets: { default: true, media_center: true, audio: false, archiving: true } }">
|
||||
<.input
|
||||
field={f[:embed_subs]}
|
||||
type="toggle"
|
||||
label="Embed Subtitles"
|
||||
help="Downloads and embeds subtitles in the media file itself, if supported. Uneffected by 'Download Subtitles' (recommended)"
|
||||
help="Downloads and embeds subtitles in the media file itself, if supported. Uneffected by 'Download Subtitles'"
|
||||
x-init="$watch('selectedPreset', p => p && (enabled = presets[p]))"
|
||||
/>
|
||||
</section>
|
||||
|
||||
<section x-data="{ presets: { default: false, media_center: false, audio: false, archiving: false } }">
|
||||
<.input
|
||||
field={f[:download_auto_subs]}
|
||||
type="toggle"
|
||||
label="Use Autogenerated Subtitles"
|
||||
help="Prefers normal subs with 'Download Subtitles' or 'Embed Subtitles' but will use autogenerated subs if needed."
|
||||
x-init="$watch('selectedPreset', p => p && (enabled = presets[p]))"
|
||||
/>
|
||||
</section>
|
||||
@@ -203,6 +213,16 @@
|
||||
/>
|
||||
</section>
|
||||
|
||||
<section x-show="advancedMode">
|
||||
<.input
|
||||
field={f[:media_container]}
|
||||
type="text"
|
||||
label="Media Container"
|
||||
placeholder="mp4"
|
||||
help="Don't change this if you're going to consume media via Plex. Leave blank for default"
|
||||
/>
|
||||
</section>
|
||||
|
||||
<section x-data="{ presets: { default: null, media_center: 1, audio: null, archiving: 1 } }">
|
||||
<.input
|
||||
field={f[:redownload_delay_days]}
|
||||
|
||||
+14
-7
@@ -1,7 +1,7 @@
|
||||
<%!-- The heex HTML formatter is really struggling with this file - I apologize in advance --%>
|
||||
<aside>
|
||||
<h2 class="text-xl font-bold mb-2">Output Template Syntax</h2>
|
||||
<section class="ml-2 md:ml-4 mb-4 max-w-prose">
|
||||
<section class="ml-2 md:ml-4 mb-4 max-w-prose overflow-auto">
|
||||
<p>When generating an output template, you have 3 options for syntax:</p>
|
||||
<ul class="list-disc list-inside ml-2 md:ml-5">
|
||||
<li>
|
||||
@@ -46,7 +46,7 @@
|
||||
</section>
|
||||
|
||||
<h2 class="text-xl font-bold mb-2">Template Options</h2>
|
||||
<section class="ml-2 md:ml-4 mb-4">
|
||||
<section class="ml-2 md:ml-4 mb-4 overflow-auto">
|
||||
<p class="max-w-prose">
|
||||
Any single-word <code class="text-sm">yt-dlp</code>
|
||||
option
|
||||
@@ -56,16 +56,23 @@
|
||||
can be used with the curly braced liquid-style syntax.
|
||||
This is just a list of the most common options as well as some custom aliases
|
||||
</p>
|
||||
<h3 class="text-lg font-bold mb-2">Custom Aliases</h3>
|
||||
<ul class="list-disc list-inside ml-2 md:ml-5">
|
||||
<li :for={{k, v} <- custom_output_template_options()}>
|
||||
<h3 class="text-lg font-bold my-2">Media Center Custom Aliases</h3>
|
||||
<ul class="list-disc list-inside mb-4 ml-2 md:ml-5 max-w-prose">
|
||||
<li :for={{k, v} <- media_center_custom_output_template_options()} class="mt-1">
|
||||
<.inline_code>{{ <%= k %> }}</.inline_code>
|
||||
<span :if={v}>- <%= v %></span>
|
||||
<span :if={v}>- <%= html_escape({:safe, v}) %></span>
|
||||
</li>
|
||||
</ul>
|
||||
<h3 class="text-lg font-bold mb-2">Other Custom Aliases</h3>
|
||||
<ul class="list-disc list-inside mb-4 ml-2 md:ml-5 max-w-prose">
|
||||
<li :for={{k, v} <- other_custom_output_template_options()} class="mt-1">
|
||||
<.inline_code>{{ <%= k %> }}</.inline_code>
|
||||
<span :if={v}>- <%= html_escape({:safe, v}) %></span>
|
||||
</li>
|
||||
</ul>
|
||||
<h3 class="text-lg font-bold mb-2">Common Options</h3>
|
||||
<ul class="list-disc list-inside ml-2 md:ml-5">
|
||||
<li :for={opt <- common_output_template_options()}>
|
||||
<li :for={opt <- common_output_template_options()} class="mt-1">
|
||||
<.inline_code>{{ <%= opt %> }}</.inline_code>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
@@ -8,6 +8,7 @@ defmodule PinchflatWeb.Sources.SourceController do
|
||||
alias Pinchflat.Sources.Source
|
||||
alias Pinchflat.Media.MediaItem
|
||||
alias Pinchflat.Profiles.MediaProfile
|
||||
alias Pinchflat.Sources.SourceDeletionWorker
|
||||
alias Pinchflat.Downloading.DownloadingHelpers
|
||||
alias Pinchflat.SlowIndexing.SlowIndexingHelpers
|
||||
alias Pinchflat.Metadata.SourceMetadataStorageWorker
|
||||
@@ -17,6 +18,7 @@ defmodule PinchflatWeb.Sources.SourceController do
|
||||
from s in Source,
|
||||
as: :source,
|
||||
inner_join: mp in assoc(s, :media_profile),
|
||||
where: is_nil(s.marked_for_deletion_at) and is_nil(mp.marked_for_deletion_at),
|
||||
preload: [media_profile: mp],
|
||||
order_by: [asc: s.custom_name],
|
||||
select: map(s, ^Source.__schema__(:fields)),
|
||||
@@ -41,13 +43,31 @@ defmodule PinchflatWeb.Sources.SourceController do
|
||||
render(conn, :index, sources: Repo.all(source_query))
|
||||
end
|
||||
|
||||
def new(conn, _params) do
|
||||
changeset = Sources.change_source(%Source{})
|
||||
def new(conn, params) do
|
||||
# This lets me preload the settings from another source for more efficient creation
|
||||
cs_struct =
|
||||
case to_string(params["template_id"]) do
|
||||
"" -> %Source{}
|
||||
template_id -> Repo.get(Source, template_id) || %Source{}
|
||||
end
|
||||
|
||||
render(conn, :new,
|
||||
changeset: changeset,
|
||||
media_profiles: media_profiles(),
|
||||
layout: get_onboarding_layout()
|
||||
layout: get_onboarding_layout(),
|
||||
# Most of these don't actually _need_ to be nullified at this point,
|
||||
# but if I don't do it now I know it'll bite me
|
||||
changeset:
|
||||
Sources.change_source(%Source{
|
||||
cs_struct
|
||||
| id: nil,
|
||||
uuid: nil,
|
||||
custom_name: nil,
|
||||
description: nil,
|
||||
collection_name: nil,
|
||||
collection_id: nil,
|
||||
collection_type: nil,
|
||||
original_url: nil
|
||||
})
|
||||
)
|
||||
end
|
||||
|
||||
@@ -107,19 +127,15 @@ defmodule PinchflatWeb.Sources.SourceController do
|
||||
end
|
||||
|
||||
def delete(conn, %{"id" => id} = params) do
|
||||
delete_files = Map.get(params, "delete_files", false)
|
||||
# This awkward comparison converts the string to a boolean
|
||||
delete_files = Map.get(params, "delete_files", "") == "true"
|
||||
source = Sources.get_source!(id)
|
||||
{:ok, _source} = Sources.delete_source(source, delete_files: delete_files)
|
||||
|
||||
flash_message =
|
||||
if delete_files do
|
||||
"Source and files deleted successfully."
|
||||
else
|
||||
"Source deleted successfully. Files were not deleted."
|
||||
end
|
||||
{:ok, _} = Sources.update_source(source, %{marked_for_deletion_at: DateTime.utc_now()})
|
||||
SourceDeletionWorker.kickoff(source, %{delete_files: delete_files})
|
||||
|
||||
conn
|
||||
|> put_flash(:info, flash_message)
|
||||
|> put_flash(:info, "Source deletion started. This may take a while to complete.")
|
||||
|> redirect(to: ~p"/sources")
|
||||
end
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ defmodule PinchflatWeb.Sources.SourceHTML do
|
||||
attr :changeset, Ecto.Changeset, required: true
|
||||
attr :action, :string, required: true
|
||||
attr :media_profiles, :list, required: true
|
||||
attr :method, :string, required: true
|
||||
|
||||
def source_form(assigns)
|
||||
|
||||
|
||||
@@ -26,6 +26,11 @@
|
||||
<span x-show="copied" x-transition.duration.150ms><.icon name="hero-check" class="ml-2 h-4 w-4" /></span>
|
||||
</span>
|
||||
</:option>
|
||||
<:option>
|
||||
<.link href={~p"/sources/new?template_id=#{@source}"} method="get">
|
||||
Use as Template
|
||||
</.link>
|
||||
</:option>
|
||||
<:option>
|
||||
<div class="h-px w-full bg-bodydark2"></div>
|
||||
</:option>
|
||||
|
||||
@@ -10,7 +10,12 @@
|
||||
<div class="rounded-sm border border-stroke bg-white px-5 pb-2.5 pt-6 shadow-default dark:border-strokedark dark:bg-boxdark sm:px-7.5 xl:pb-1">
|
||||
<div class="max-w-full">
|
||||
<div class="flex flex-col gap-10">
|
||||
<.source_form changeset={@changeset} media_profiles={@media_profiles} action={~p"/sources/#{@source}"} />
|
||||
<.source_form
|
||||
changeset={@changeset}
|
||||
media_profiles={@media_profiles}
|
||||
action={~p"/sources/#{@source}"}
|
||||
method="patch"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
<div class="rounded-sm border border-stroke bg-white px-5 pb-2.5 pt-6 shadow-default dark:border-strokedark dark:bg-boxdark sm:px-7.5 xl:pb-1">
|
||||
<div class="max-w-full">
|
||||
<div class="flex flex-col gap-10">
|
||||
<.source_form changeset={@changeset} media_profiles={@media_profiles} action={~p"/sources"} />
|
||||
<.source_form changeset={@changeset} media_profiles={@media_profiles} action={~p"/sources"} method="post" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
:let={f}
|
||||
for={@changeset}
|
||||
action={@action}
|
||||
method={@method}
|
||||
x-data="{ advancedMode: !!JSON.parse(localStorage.getItem('advancedMode')) }"
|
||||
x-init="$watch('advancedMode', value => localStorage.setItem('advancedMode', JSON.stringify(value)))"
|
||||
>
|
||||
@@ -10,8 +11,8 @@
|
||||
</.error>
|
||||
|
||||
<section x-data="{ mediaProfileId: null }">
|
||||
<section class="flex justify-between items-center mt-8">
|
||||
<h3 class=" text-2xl text-black dark:text-white">
|
||||
<section class="flex justify-between items-center mt-4">
|
||||
<h3 class="text-2xl text-white">
|
||||
General Options
|
||||
</h3>
|
||||
<span class="cursor-pointer hover:underline" x-on:click="advancedMode = !advancedMode">
|
||||
@@ -19,6 +20,14 @@
|
||||
</span>
|
||||
</section>
|
||||
|
||||
<.input
|
||||
field={f[:original_url]}
|
||||
type="text"
|
||||
label="Source URL"
|
||||
help="URL of a channel or playlist (required)"
|
||||
x-init="$el.focus()"
|
||||
/>
|
||||
|
||||
<.input
|
||||
field={f[:custom_name]}
|
||||
type="text"
|
||||
@@ -26,8 +35,6 @@
|
||||
help="Does not impact indexing or downloading. Will be inferred from the source if left blank"
|
||||
/>
|
||||
|
||||
<.input field={f[:original_url]} type="text" label="Source URL" help="URL of a channel or playlist (required)" />
|
||||
|
||||
<.input
|
||||
field={f[:media_profile_id]}
|
||||
options={Enum.map(@media_profiles, &{&1.name, &1.id})}
|
||||
@@ -79,6 +86,31 @@
|
||||
help="Unchecking still indexes media but it won't be downloaded until you enable this option"
|
||||
/>
|
||||
|
||||
<.input
|
||||
field={f[:use_cookies]}
|
||||
type="toggle"
|
||||
label="Use Cookies for Downloading"
|
||||
help="Uses your YouTube cookies for this source (if configured). Used for downloading private playlists and videos. See docs for important details"
|
||||
/>
|
||||
|
||||
<section x-show="advancedMode">
|
||||
<.input
|
||||
field={f[:min_duration_seconds]}
|
||||
type="number"
|
||||
label="Minimum Duration (seconds)"
|
||||
min="0"
|
||||
help="Minimum duration of the media to be downloaded. Can be blank"
|
||||
/>
|
||||
|
||||
<.input
|
||||
field={f[:max_duration_seconds]}
|
||||
type="number"
|
||||
label="Maximum Duration (seconds)"
|
||||
min="0"
|
||||
help="Maximum duration of the media to be downloaded. Can be blank"
|
||||
/>
|
||||
</section>
|
||||
|
||||
<.input
|
||||
field={f[:download_cutoff_date]}
|
||||
type="text"
|
||||
@@ -87,7 +119,7 @@
|
||||
maxlength="10"
|
||||
pattern="((?:19|20)[0-9][0-9])-(0[1-9]|1[012])-(0[1-9]|[12][0-9]|3[01])"
|
||||
title="YYYY-MM-DD"
|
||||
help="Only download media uploaded after this date. Leave blank to download all media. Must be in YYYY-MM-DD format"
|
||||
help="Only download media uploaded after this date. Leave blank to download all media. Must be in YYYY-MM-DD format. Old media may be deleted or downloaded if you change this date"
|
||||
/>
|
||||
|
||||
<.input
|
||||
|
||||
@@ -4,7 +4,7 @@ defmodule Pinchflat.MixProject do
|
||||
def project do
|
||||
[
|
||||
app: :pinchflat,
|
||||
version: "2024.6.25",
|
||||
version: "2024.9.12",
|
||||
elixir: "~> 1.17",
|
||||
elixirc_paths: elixirc_paths(Mix.env()),
|
||||
start_permanent: Mix.env() == :prod,
|
||||
@@ -46,10 +46,10 @@ defmodule Pinchflat.MixProject do
|
||||
# Type `mix help deps` for examples and options.
|
||||
defp deps do
|
||||
[
|
||||
{:phoenix, "~> 1.7.10"},
|
||||
{:phoenix, "~> 1.7.14"},
|
||||
{:phoenix_ecto, "~> 4.4"},
|
||||
{:ecto, "~> 3.11.2"},
|
||||
{:ecto_sql, "~> 3.10"},
|
||||
{:ecto, "~> 3.12.3"},
|
||||
{:ecto_sql, "~> 3.12"},
|
||||
{:ecto_sqlite3, ">= 0.0.0"},
|
||||
{:ecto_sqlite3_extras, "~> 1.2.0"},
|
||||
{:phoenix_html, "~> 3.3"},
|
||||
@@ -67,7 +67,7 @@ defmodule Pinchflat.MixProject do
|
||||
{:jason, "~> 1.2"},
|
||||
{:dns_cluster, "~> 0.1.1"},
|
||||
{:plug_cowboy, "~> 2.5"},
|
||||
{:oban, "~> 2.16"},
|
||||
{:oban, "~> 2.17"},
|
||||
{:nimble_parsec, "~> 1.4"},
|
||||
{:timex, "~> 3.0"},
|
||||
{:mox, "~> 1.0", only: :test},
|
||||
@@ -103,7 +103,8 @@ defmodule Pinchflat.MixProject do
|
||||
"ecto.rollback": [
|
||||
"ecto.rollback",
|
||||
~s(cmd [ -z "$MIX_ENV" ] && yarn run create-erd || echo "No ERD generated")
|
||||
]
|
||||
],
|
||||
"version.bump": "cmd ./tooling/version_bump.sh"
|
||||
]
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,26 +1,26 @@
|
||||
%{
|
||||
"bunt": {:hex, :bunt, "1.0.0", "081c2c665f086849e6d57900292b3a161727ab40431219529f13c4ddcf3e7a44", [:mix], [], "hexpm", "dc5f86aa08a5f6fa6b8096f0735c4e76d54ae5c9fa2c143e5a1fc7c1cd9bb6b5"},
|
||||
"castore": {:hex, :castore, "1.0.5", "9eeebb394cc9a0f3ae56b813459f990abb0a3dedee1be6b27fdb50301930502f", [:mix], [], "hexpm", "8d7c597c3e4a64c395980882d4bca3cebb8d74197c590dc272cfd3b6a6310578"},
|
||||
"cc_precompiler": {:hex, :cc_precompiler, "0.1.9", "e8d3364f310da6ce6463c3dd20cf90ae7bbecbf6c5203b98bf9b48035592649b", [:mix], [{:elixir_make, "~> 0.7", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "9dcab3d0f3038621f1601f13539e7a9ee99843862e66ad62827b0c42b2f58a54"},
|
||||
"castore": {:hex, :castore, "1.0.8", "dedcf20ea746694647f883590b82d9e96014057aff1d44d03ec90f36a5c0dc6e", [:mix], [], "hexpm", "0b2b66d2ee742cb1d9cb8c8be3b43c3a70ee8651f37b75a8b982e036752983f1"},
|
||||
"cc_precompiler": {:hex, :cc_precompiler, "0.1.10", "47c9c08d8869cf09b41da36538f62bc1abd3e19e41701c2cea2675b53c704258", [:mix], [{:elixir_make, "~> 0.7", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "f6e046254e53cd6b41c6bacd70ae728011aa82b2742a80d6e2214855c6e06b22"},
|
||||
"certifi": {:hex, :certifi, "2.12.0", "2d1cca2ec95f59643862af91f001478c9863c2ac9cb6e2f89780bfd8de987329", [:rebar3], [], "hexpm", "ee68d85df22e554040cdb4be100f33873ac6051387baf6a8f6ce82272340ff1c"},
|
||||
"combine": {:hex, :combine, "0.10.0", "eff8224eeb56498a2af13011d142c5e7997a80c8f5b97c499f84c841032e429f", [:mix], [], "hexpm", "1b1dbc1790073076580d0d1d64e42eae2366583e7aecd455d1215b0d16f2451b"},
|
||||
"cowboy": {:hex, :cowboy, "2.10.0", "ff9ffeff91dae4ae270dd975642997afe2a1179d94b1887863e43f681a203e26", [:make, :rebar3], [{:cowlib, "2.12.1", [hex: :cowlib, repo: "hexpm", optional: false]}, {:ranch, "1.8.0", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm", "3afdccb7183cc6f143cb14d3cf51fa00e53db9ec80cdcd525482f5e99bc41d6b"},
|
||||
"cowboy": {:hex, :cowboy, "2.12.0", "f276d521a1ff88b2b9b4c54d0e753da6c66dd7be6c9fca3d9418b561828a3731", [:make, :rebar3], [{:cowlib, "2.13.0", [hex: :cowlib, repo: "hexpm", optional: false]}, {:ranch, "1.8.0", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm", "8a7abe6d183372ceb21caa2709bec928ab2b72e18a3911aa1771639bef82651e"},
|
||||
"cowboy_telemetry": {:hex, :cowboy_telemetry, "0.4.0", "f239f68b588efa7707abce16a84d0d2acf3a0f50571f8bb7f56a15865aae820c", [:rebar3], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "7d98bac1ee4565d31b62d59f8823dfd8356a169e7fcbb83831b8a5397404c9de"},
|
||||
"cowlib": {:hex, :cowlib, "2.12.1", "a9fa9a625f1d2025fe6b462cb865881329b5caff8f1854d1cbc9f9533f00e1e1", [:make, :rebar3], [], "hexpm", "163b73f6367a7341b33c794c4e88e7dbfe6498ac42dcd69ef44c5bc5507c8db0"},
|
||||
"cowlib": {:hex, :cowlib, "2.13.0", "db8f7505d8332d98ef50a3ef34b34c1afddec7506e4ee4dd4a3a266285d282ca", [:make, :rebar3], [], "hexpm", "e1e1284dc3fc030a64b1ad0d8382ae7e99da46c3246b815318a4b848873800a4"},
|
||||
"credo": {:hex, :credo, "1.7.7", "771445037228f763f9b2afd612b6aa2fd8e28432a95dbbc60d8e03ce71ba4446", [:mix], [{:bunt, "~> 0.2.1 or ~> 1.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:file_system, "~> 0.2 or ~> 1.0", [hex: :file_system, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "8bc87496c9aaacdc3f90f01b7b0582467b69b4bd2441fe8aae3109d843cc2f2e"},
|
||||
"credo_naming": {:hex, :credo_naming, "2.1.0", "d44ad58890d4db552e141ce64756a74ac1573665af766d1ac64931aa90d47744", [:make, :mix], [{:credo, "~> 1.6", [hex: :credo, repo: "hexpm", optional: false]}], "hexpm", "830e23b3fba972e2fccec49c0c089fe78c1e64bc16782a2682d78082351a2909"},
|
||||
"db_connection": {:hex, :db_connection, "2.6.0", "77d835c472b5b67fc4f29556dee74bf511bbafecdcaf98c27d27fa5918152086", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "c2f992d15725e721ec7fbc1189d4ecdb8afef76648c746a8e1cad35e3b8a35f3"},
|
||||
"db_connection": {:hex, :db_connection, "2.7.0", "b99faa9291bb09892c7da373bb82cba59aefa9b36300f6145c5f201c7adf48ec", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "dcf08f31b2701f857dfc787fbad78223d61a32204f217f15e881dd93e4bdd3ff"},
|
||||
"decimal": {:hex, :decimal, "2.1.1", "5611dca5d4b2c3dd497dec8f68751f1f1a54755e8ed2a966c2633cf885973ad6", [:mix], [], "hexpm", "53cfe5f497ed0e7771ae1a475575603d77425099ba5faef9394932b35020ffcc"},
|
||||
"dns_cluster": {:hex, :dns_cluster, "0.1.2", "3eb5be824c7888dadf9781018e1a5f1d3d1113b333c50bce90fb1b83df1015f2", [:mix], [], "hexpm", "7494272040f847637bbdb01bcdf4b871e82daf09b813e7d3cb3b84f112c6f2f8"},
|
||||
"ecto": {:hex, :ecto, "3.11.2", "e1d26be989db350a633667c5cda9c3d115ae779b66da567c68c80cfb26a8c9ee", [:mix], [{:decimal, "~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "3c38bca2c6f8d8023f2145326cc8a80100c3ffe4dcbd9842ff867f7fc6156c65"},
|
||||
"ecto_sql": {:hex, :ecto_sql, "3.11.1", "e9abf28ae27ef3916b43545f9578b4750956ccea444853606472089e7d169470", [:mix], [{:db_connection, "~> 2.4.1 or ~> 2.5", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.11.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:myxql, "~> 0.6.0", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.16.0 or ~> 0.17.0 or ~> 1.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:tds, "~> 2.1.1 or ~> 2.2", [hex: :tds, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.0 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "ce14063ab3514424276e7e360108ad6c2308f6d88164a076aac8a387e1fea634"},
|
||||
"ecto_sqlite3": {:hex, :ecto_sqlite3, "0.15.1", "40f2fbd9e246455f8c42e7e0a77009ef806caa1b3ce6f717b2a0a80e8432fcfd", [:mix], [{:decimal, "~> 1.6 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:ecto, "~> 3.11", [hex: :ecto, repo: "hexpm", optional: false]}, {:ecto_sql, "~> 3.11", [hex: :ecto_sql, repo: "hexpm", optional: false]}, {:exqlite, "~> 0.19", [hex: :exqlite, repo: "hexpm", optional: false]}], "hexpm", "28b16e177123c688948357176662bf9ff9084daddf950ef5b6baf3ee93707064"},
|
||||
"ecto": {:hex, :ecto, "3.12.3", "1a9111560731f6c3606924c81c870a68a34c819f6d4f03822f370ea31a582208", [:mix], [{:decimal, "~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "9efd91506ae722f95e48dc49e70d0cb632ede3b7a23896252a60a14ac6d59165"},
|
||||
"ecto_sql": {:hex, :ecto_sql, "3.12.0", "73cea17edfa54bde76ee8561b30d29ea08f630959685006d9c6e7d1e59113b7d", [:mix], [{:db_connection, "~> 2.4.1 or ~> 2.5", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.12", [hex: :ecto, repo: "hexpm", optional: false]}, {:myxql, "~> 0.7", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.19 or ~> 1.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:tds, "~> 2.1.1 or ~> 2.2", [hex: :tds, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.0 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "dc9e4d206f274f3947e96142a8fdc5f69a2a6a9abb4649ef5c882323b6d512f0"},
|
||||
"ecto_sqlite3": {:hex, :ecto_sqlite3, "0.17.2", "200226e057f76c40be55fbac77771eb1a233260ab8ec7283f5da6d9402bde8de", [:mix], [{:decimal, "~> 1.6 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:ecto, "~> 3.12", [hex: :ecto, repo: "hexpm", optional: false]}, {:ecto_sql, "~> 3.12", [hex: :ecto_sql, repo: "hexpm", optional: false]}, {:exqlite, "~> 0.22", [hex: :exqlite, repo: "hexpm", optional: false]}], "hexpm", "a3838919c5a34c268c28cafab87b910bcda354a9a4e778658da46c149bb2c1da"},
|
||||
"ecto_sqlite3_extras": {:hex, :ecto_sqlite3_extras, "1.2.2", "36e60b561a11441d15f26c791817999269fb578b985162207ebb08b04ca71e40", [:mix], [{:exqlite, ">= 0.13.2", [hex: :exqlite, repo: "hexpm", optional: false]}, {:table_rex, "~> 4.0", [hex: :table_rex, repo: "hexpm", optional: false]}], "hexpm", "2b66ba7246bb4f7e39e2578acd4a0e4e4be54f60ff52d450a01be95eeb78ff1e"},
|
||||
"elixir_make": {:hex, :elixir_make, "0.7.8", "505026f266552ee5aabca0b9f9c229cbb496c689537c9f922f3eb5431157efc7", [:mix], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:certifi, "~> 2.0", [hex: :certifi, repo: "hexpm", optional: true]}], "hexpm", "7a71945b913d37ea89b06966e1342c85cfe549b15e6d6d081e8081c493062c07"},
|
||||
"elixir_make": {:hex, :elixir_make, "0.8.4", "4960a03ce79081dee8fe119d80ad372c4e7badb84c493cc75983f9d3bc8bde0f", [:mix], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:certifi, "~> 2.0", [hex: :certifi, repo: "hexpm", optional: true]}], "hexpm", "6e7f1d619b5f61dfabd0a20aa268e575572b542ac31723293a4c1a567d5ef040"},
|
||||
"esbuild": {:hex, :esbuild, "0.8.1", "0cbf919f0eccb136d2eeef0df49c4acf55336de864e63594adcea3814f3edf41", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}, {:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "25fc876a67c13cb0a776e7b5d7974851556baeda2085296c14ab48555ea7560f"},
|
||||
"ex_check": {:hex, :ex_check, "0.14.0", "d6fbe0bcc51cf38fea276f5bc2af0c9ae0a2bb059f602f8de88709421dae4f0e", [:mix], [], "hexpm", "8a602e98c66e6a4be3a639321f1f545292042f290f91fa942a285888c6868af0"},
|
||||
"expo": {:hex, :expo, "0.5.1", "249e826a897cac48f591deba863b26c16682b43711dd15ee86b92f25eafd96d9", [:mix], [], "hexpm", "68a4233b0658a3d12ee00d27d37d856b1ba48607e7ce20fd376958d0ba6ce92b"},
|
||||
"exqlite": {:hex, :exqlite, "0.19.0", "0f3ee29e35bed38552dd0ed59600aa81c78f867f5b5ff0e17d330148e0465483", [:make, :mix], [{:cc_precompiler, "~> 0.1", [hex: :cc_precompiler, repo: "hexpm", optional: false]}, {:db_connection, "~> 2.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:elixir_make, "~> 0.7", [hex: :elixir_make, repo: "hexpm", optional: false]}, {:table, "~> 0.1.0", [hex: :table, repo: "hexpm", optional: true]}], "hexpm", "55a8fbb0443f03d4a256e3458bd1203eff5037a6624b76460eaaa9080f462b06"},
|
||||
"exqlite": {:hex, :exqlite, "0.23.0", "6e851c937a033299d0784994c66da24845415072adbc455a337e20087bce9033", [:make, :mix], [{:cc_precompiler, "~> 0.1", [hex: :cc_precompiler, repo: "hexpm", optional: false]}, {:db_connection, "~> 2.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:elixir_make, "~> 0.8", [hex: :elixir_make, repo: "hexpm", optional: false]}, {:table, "~> 0.1.0", [hex: :table, repo: "hexpm", optional: true]}], "hexpm", "404341cceec5e6466aaed160cf0b58be2019b60af82588c215e1224ebd3ec831"},
|
||||
"faker": {:hex, :faker, "0.17.0", "671019d0652f63aefd8723b72167ecdb284baf7d47ad3a82a15e9b8a6df5d1fa", [:mix], [], "hexpm", "a7d4ad84a93fd25c5f5303510753789fc2433ff241bf3b4144d3f6f291658a6a"},
|
||||
"file_system": {:hex, :file_system, "0.2.10", "fb082005a9cd1711c05b5248710f8826b02d7d1784e7c3451f9c1231d4fc162d", [:mix], [], "hexpm", "41195edbfb562a593726eda3b3e8b103a309b733ad25f3d642ba49696bf715dc"},
|
||||
"finch": {:hex, :finch, "0.17.0", "17d06e1d44d891d20dbd437335eebe844e2426a0cd7e3a3e220b461127c73f70", [:mix], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: false]}, {:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mint, "~> 1.3", [hex: :mint, repo: "hexpm", optional: false]}, {:nimble_options, "~> 0.4 or ~> 1.0", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:nimble_pool, "~> 0.2.6 or ~> 1.0", [hex: :nimble_pool, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "8d014a661bb6a437263d4b5abf0bcbd3cf0deb26b1e8596f2a271d22e48934c7"},
|
||||
@@ -29,40 +29,40 @@
|
||||
"hackney": {:hex, :hackney, "1.20.1", "8d97aec62ddddd757d128bfd1df6c5861093419f8f7a4223823537bad5d064e2", [:rebar3], [{:certifi, "~> 2.12.0", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "~> 6.1.0", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "~> 1.0.0", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "~> 1.1", [hex: :mimerl, repo: "hexpm", optional: false]}, {:parse_trans, "3.4.1", [hex: :parse_trans, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "~> 1.1.0", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}, {:unicode_util_compat, "~> 0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "fe9094e5f1a2a2c0a7d10918fee36bfec0ec2a979994cff8cfe8058cd9af38e3"},
|
||||
"hpax": {:hex, :hpax, "0.1.2", "09a75600d9d8bbd064cdd741f21fc06fc1f4cf3d0fcc335e5aa19be1a7235c84", [:mix], [], "hexpm", "2c87843d5a23f5f16748ebe77969880e29809580efdaccd615cd3bed628a8c13"},
|
||||
"idna": {:hex, :idna, "6.1.1", "8a63070e9f7d0c62eb9d9fcb360a7de382448200fbbd1b106cc96d3d8099df8d", [:rebar3], [{:unicode_util_compat, "~> 0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "92376eb7894412ed19ac475e4a86f7b413c1b9fbb5bd16dccd57934157944cea"},
|
||||
"jason": {:hex, :jason, "1.4.1", "af1504e35f629ddcdd6addb3513c3853991f694921b1b9368b0bd32beb9f1b63", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "fbb01ecdfd565b56261302f7e1fcc27c4fb8f32d56eab74db621fc154604a7a1"},
|
||||
"jason": {:hex, :jason, "1.4.4", "b9226785a9aa77b6857ca22832cffa5d5011a667207eb2a0ad56adb5db443b8a", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "c5eb0cab91f094599f94d55bc63409236a8ec69a21a67814529e8d5f6cc90b3b"},
|
||||
"metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], [], "hexpm", "69b09adddc4f74a40716ae54d140f93beb0fb8978d8636eaded0c31b6f099f16"},
|
||||
"mime": {:hex, :mime, "2.0.5", "dc34c8efd439abe6ae0343edbb8556f4d63f178594894720607772a041b04b02", [:mix], [], "hexpm", "da0d64a365c45bc9935cc5c8a7fc5e49a0e0f9932a761c55d6c52b142780a05c"},
|
||||
"mime": {:hex, :mime, "2.0.6", "8f18486773d9b15f95f4f4f1e39b710045fa1de891fada4516559967276e4dc2", [:mix], [], "hexpm", "c9945363a6b26d747389aac3643f8e0e09d30499a138ad64fe8fd1d13d9b153e"},
|
||||
"mimerl": {:hex, :mimerl, "1.3.0", "d0cd9fc04b9061f82490f6581e0128379830e78535e017f7780f37fea7545726", [:rebar3], [], "hexpm", "a1e15a50d1887217de95f0b9b0793e32853f7c258a5cd227650889b38839fe9d"},
|
||||
"mint": {:hex, :mint, "1.5.2", "4805e059f96028948870d23d7783613b7e6b0e2fb4e98d720383852a760067fd", [:mix], [{:castore, "~> 0.1.0 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:hpax, "~> 0.1.1", [hex: :hpax, repo: "hexpm", optional: false]}], "hexpm", "d77d9e9ce4eb35941907f1d3df38d8f750c357865353e21d335bdcdf6d892a02"},
|
||||
"mox": {:hex, :mox, "1.1.0", "0f5e399649ce9ab7602f72e718305c0f9cdc351190f72844599545e4996af73c", [:mix], [], "hexpm", "d44474c50be02d5b72131070281a5d3895c0e7a95c780e90bc0cfe712f633a13"},
|
||||
"nimble_options": {:hex, :nimble_options, "1.1.0", "3b31a57ede9cb1502071fade751ab0c7b8dbe75a9a4c2b5bbb0943a690b63172", [:mix], [], "hexpm", "8bbbb3941af3ca9acc7835f5655ea062111c9c27bcac53e004460dfd19008a99"},
|
||||
"nimble_parsec": {:hex, :nimble_parsec, "1.4.0", "51f9b613ea62cfa97b25ccc2c1b4216e81df970acd8e16e8d1bdc58fef21370d", [:mix], [], "hexpm", "9c565862810fb383e9838c1dd2d7d2c437b3d13b267414ba6af33e50d2d1cf28"},
|
||||
"nimble_pool": {:hex, :nimble_pool, "1.0.0", "5eb82705d138f4dd4423f69ceb19ac667b3b492ae570c9f5c900bb3d2f50a847", [:mix], [], "hexpm", "80be3b882d2d351882256087078e1b1952a28bf98d0a287be87e4a24a710b67a"},
|
||||
"oban": {:hex, :oban, "2.17.3", "ddfd5710aadcd550d2e174c8d73ce5f1865601418cf54a91775f20443fb832b7", [:mix], [{:ecto_sql, "~> 3.6", [hex: :ecto_sql, repo: "hexpm", optional: false]}, {:ecto_sqlite3, "~> 0.9", [hex: :ecto_sqlite3, repo: "hexpm", optional: true]}, {:jason, "~> 1.1", [hex: :jason, repo: "hexpm", optional: false]}, {:postgrex, "~> 0.16", [hex: :postgrex, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "452eada8bfe0d0fefd0740ab5fa8cf3ef6c375df0b4a3c3805d179022a04738a"},
|
||||
"oban": {:hex, :oban, "2.18.3", "1608c04f8856c108555c379f2f56bc0759149d35fa9d3b825cb8a6769f8ae926", [:mix], [{:ecto_sql, "~> 3.10", [hex: :ecto_sql, repo: "hexpm", optional: false]}, {:ecto_sqlite3, "~> 0.9", [hex: :ecto_sqlite3, repo: "hexpm", optional: true]}, {:jason, "~> 1.1", [hex: :jason, repo: "hexpm", optional: false]}, {:postgrex, "~> 0.16", [hex: :postgrex, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "36ca6ca84ef6518f9c2c759ea88efd438a3c81d667ba23b02b062a0aa785475e"},
|
||||
"parse_trans": {:hex, :parse_trans, "3.4.1", "6e6aa8167cb44cc8f39441d05193be6e6f4e7c2946cb2759f015f8c56b76e5ff", [:rebar3], [], "hexpm", "620a406ce75dada827b82e453c19cf06776be266f5a67cff34e1ef2cbb60e49a"},
|
||||
"phoenix": {:hex, :phoenix, "1.7.10", "02189140a61b2ce85bb633a9b6fd02dff705a5f1596869547aeb2b2b95edd729", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 2.1", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.6", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:plug_crypto, "~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:websock_adapter, "~> 0.5.3", [hex: :websock_adapter, repo: "hexpm", optional: false]}], "hexpm", "cf784932e010fd736d656d7fead6a584a4498efefe5b8227e9f383bf15bb79d0"},
|
||||
"phoenix": {:hex, :phoenix, "1.7.14", "a7d0b3f1bc95987044ddada111e77bd7f75646a08518942c72a8440278ae7825", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 2.1", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.7", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:plug_crypto, "~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:websock_adapter, "~> 0.5.3", [hex: :websock_adapter, repo: "hexpm", optional: false]}], "hexpm", "c7859bc56cc5dfef19ecfc240775dae358cbaa530231118a9e014df392ace61a"},
|
||||
"phoenix_ecto": {:hex, :phoenix_ecto, "4.4.3", "86e9878f833829c3f66da03d75254c155d91d72a201eb56ae83482328dc7ca93", [:mix], [{:ecto, "~> 3.5", [hex: :ecto, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 2.14.2 or ~> 3.0 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:plug, "~> 1.9", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "d36c401206f3011fefd63d04e8ef626ec8791975d9d107f9a0817d426f61ac07"},
|
||||
"phoenix_html": {:hex, :phoenix_html, "3.3.3", "380b8fb45912b5638d2f1d925a3771b4516b9a78587249cabe394e0a5d579dc9", [:mix], [{:plug, "~> 1.5", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "923ebe6fec6e2e3b3e569dfbdc6560de932cd54b000ada0208b5f45024bdd76c"},
|
||||
"phoenix_html": {:hex, :phoenix_html, "3.3.4", "42a09fc443bbc1da37e372a5c8e6755d046f22b9b11343bf885067357da21cb3", [:mix], [{:plug, "~> 1.5", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "0249d3abec3714aff3415e7ee3d9786cb325be3151e6c4b3021502c585bf53fb"},
|
||||
"phoenix_live_dashboard": {:hex, :phoenix_live_dashboard, "0.8.3", "7ff51c9b6609470f681fbea20578dede0e548302b0c8bdf338b5a753a4f045bf", [:mix], [{:ecto, "~> 3.6.2 or ~> 3.7", [hex: :ecto, repo: "hexpm", optional: true]}, {:ecto_mysql_extras, "~> 0.5", [hex: :ecto_mysql_extras, repo: "hexpm", optional: true]}, {:ecto_psql_extras, "~> 0.7", [hex: :ecto_psql_extras, repo: "hexpm", optional: true]}, {:ecto_sqlite3_extras, "~> 1.1.7 or ~> 1.2.0", [hex: :ecto_sqlite3_extras, repo: "hexpm", optional: true]}, {:mime, "~> 1.6 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:phoenix_live_view, "~> 0.19 or ~> 1.0", [hex: :phoenix_live_view, repo: "hexpm", optional: false]}, {:telemetry_metrics, "~> 0.6 or ~> 1.0", [hex: :telemetry_metrics, repo: "hexpm", optional: false]}], "hexpm", "f9470a0a8bae4f56430a23d42f977b5a6205fdba6559d76f932b876bfaec652d"},
|
||||
"phoenix_live_reload": {:hex, :phoenix_live_reload, "1.4.1", "2aff698f5e47369decde4357ba91fc9c37c6487a512b41732818f2204a8ef1d3", [:mix], [{:file_system, "~> 0.2.1 or ~> 0.3", [hex: :file_system, repo: "hexpm", optional: false]}, {:phoenix, "~> 1.4", [hex: :phoenix, repo: "hexpm", optional: false]}], "hexpm", "9bffb834e7ddf08467fe54ae58b5785507aaba6255568ae22b4d46e2bb3615ab"},
|
||||
"phoenix_live_view": {:hex, :phoenix_live_view, "0.20.3", "8b6406bc0a451f295407d7acff7f234a6314be5bbe0b3f90ed82b07f50049878", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix, "~> 1.6.15 or ~> 1.7.0", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 3.3 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: true]}, {:plug, "~> 1.15", [hex: :plug, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.2 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "a8e4385e05618b424779f894ed2df97d3c7518b7285fcd11979077ae6226466b"},
|
||||
"phoenix_pubsub": {:hex, :phoenix_pubsub, "2.1.3", "3168d78ba41835aecad272d5e8cd51aa87a7ac9eb836eabc42f6e57538e3731d", [:mix], [], "hexpm", "bba06bc1dcfd8cb086759f0edc94a8ba2bc8896d5331a1e2c2902bf8e36ee502"},
|
||||
"phoenix_template": {:hex, :phoenix_template, "1.0.4", "e2092c132f3b5e5b2d49c96695342eb36d0ed514c5b252a77048d5969330d639", [:mix], [{:phoenix_html, "~> 2.14.2 or ~> 3.0 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}], "hexpm", "2c0c81f0e5c6753faf5cca2f229c9709919aba34fab866d3bc05060c9c444206"},
|
||||
"plug": {:hex, :plug, "1.15.3", "712976f504418f6dff0a3e554c40d705a9bcf89a7ccef92fc6a5ef8f16a30a97", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "cc4365a3c010a56af402e0809208873d113e9c38c401cabd88027ef4f5c01fd2"},
|
||||
"plug_cowboy": {:hex, :plug_cowboy, "2.6.1", "9a3bbfceeb65eff5f39dab529e5cd79137ac36e913c02067dba3963a26efe9b2", [:mix], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:cowboy_telemetry, "~> 0.3", [hex: :cowboy_telemetry, repo: "hexpm", optional: false]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "de36e1a21f451a18b790f37765db198075c25875c64834bcc82d90b309eb6613"},
|
||||
"plug_crypto": {:hex, :plug_crypto, "2.0.0", "77515cc10af06645abbfb5e6ad7a3e9714f805ae118fa1a70205f80d2d70fe73", [:mix], [], "hexpm", "53695bae57cc4e54566d993eb01074e4d894b65a3766f1c43e2c61a1b0f45ea9"},
|
||||
"plug": {:hex, :plug, "1.16.1", "40c74619c12f82736d2214557dedec2e9762029b2438d6d175c5074c933edc9d", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "a13ff6b9006b03d7e33874945b2755253841b238c34071ed85b0e86057f8cddc"},
|
||||
"plug_cowboy": {:hex, :plug_cowboy, "2.7.2", "fdadb973799ae691bf9ecad99125b16625b1c6039999da5fe544d99218e662e4", [:mix], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:cowboy_telemetry, "~> 0.3", [hex: :cowboy_telemetry, repo: "hexpm", optional: false]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "245d8a11ee2306094840c000e8816f0cbed69a23fc0ac2bcf8d7835ae019bb2f"},
|
||||
"plug_crypto": {:hex, :plug_crypto, "2.1.0", "f44309c2b06d249c27c8d3f65cfe08158ade08418cf540fd4f72d4d6863abb7b", [:mix], [], "hexpm", "131216a4b030b8f8ce0f26038bc4421ae60e4bb95c5cf5395e1421437824c4fa"},
|
||||
"ranch": {:hex, :ranch, "1.8.0", "8c7a100a139fd57f17327b6413e4167ac559fbc04ca7448e9be9057311597a1d", [:make, :rebar3], [], "hexpm", "49fbcfd3682fab1f5d109351b61257676da1a2fdbe295904176d5e521a2ddfe5"},
|
||||
"sobelow": {:hex, :sobelow, "0.13.0", "218afe9075904793f5c64b8837cc356e493d88fddde126a463839351870b8d1e", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "cd6e9026b85fc35d7529da14f95e85a078d9dd1907a9097b3ba6ac7ebbe34a0d"},
|
||||
"ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.7", "354c321cf377240c7b8716899e182ce4890c5938111a1296add3ec74cf1715df", [:make, :mix, :rebar3], [], "hexpm", "fe4c190e8f37401d30167c8c405eda19469f34577987c76dde613e838bbc67f8"},
|
||||
"swoosh": {:hex, :swoosh, "1.14.4", "94e9dba91f7695a10f49b0172c4a4cb658ef24abef7e8140394521b7f3bbb2d4", [:mix], [{:cowboy, "~> 1.1 or ~> 2.4", [hex: :cowboy, repo: "hexpm", optional: true]}, {:ex_aws, "~> 2.1", [hex: :ex_aws, repo: "hexpm", optional: true]}, {:finch, "~> 0.6", [hex: :finch, repo: "hexpm", optional: true]}, {:gen_smtp, "~> 0.13 or ~> 1.0", [hex: :gen_smtp, repo: "hexpm", optional: true]}, {:hackney, "~> 1.9", [hex: :hackney, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mail, "~> 0.2", [hex: :mail, repo: "hexpm", optional: true]}, {:mime, "~> 1.1 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug, "~> 1.9", [hex: :plug, repo: "hexpm", optional: true]}, {:plug_cowboy, ">= 1.0.0", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:req, "~> 0.4 or ~> 1.0", [hex: :req, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.2 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "081c5a590e4ba85cc89baddf7b2beecf6c13f7f84a958f1cd969290815f0f026"},
|
||||
"table_rex": {:hex, :table_rex, "4.0.0", "3c613a68ebdc6d4d1e731bc973c233500974ec3993c99fcdabb210407b90959b", [:mix], [], "hexpm", "c35c4d5612ca49ebb0344ea10387da4d2afe278387d4019e4d8111e815df8f55"},
|
||||
"tailwind": {:hex, :tailwind, "0.2.2", "9e27288b568ede1d88517e8c61259bc214a12d7eed271e102db4c93fcca9b2cd", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}], "hexpm", "ccfb5025179ea307f7f899d1bb3905cd0ac9f687ed77feebc8f67bdca78565c4"},
|
||||
"telemetry": {:hex, :telemetry, "1.2.1", "68fdfe8d8f05a8428483a97d7aab2f268aaff24b49e0f599faa091f1d4e7f61c", [:rebar3], [], "hexpm", "dad9ce9d8effc621708f99eac538ef1cbe05d6a874dd741de2e689c47feafed5"},
|
||||
"telemetry": {:hex, :telemetry, "1.3.0", "fedebbae410d715cf8e7062c96a1ef32ec22e764197f70cda73d82778d61e7a2", [:rebar3], [], "hexpm", "7015fc8919dbe63764f4b4b87a95b7c0996bd539e0d499be6ec9d7f3875b79e6"},
|
||||
"telemetry_metrics": {:hex, :telemetry_metrics, "0.6.2", "2caabe9344ec17eafe5403304771c3539f3b6e2f7fb6a6f602558c825d0d0bfb", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "9b43db0dc33863930b9ef9d27137e78974756f5f198cae18409970ed6fa5b561"},
|
||||
"telemetry_poller": {:hex, :telemetry_poller, "1.0.0", "db91bb424e07f2bb6e73926fcafbfcbcb295f0193e0a00e825e589a0a47e8453", [:rebar3], [{:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "b3a24eafd66c3f42da30fc3ca7dda1e9d546c12250a2d60d7b81d264fbec4f6e"},
|
||||
"timex": {:hex, :timex, "3.7.11", "bb95cb4eb1d06e27346325de506bcc6c30f9c6dea40d1ebe390b262fad1862d1", [:mix], [{:combine, "~> 0.10", [hex: :combine, repo: "hexpm", optional: false]}, {:gettext, "~> 0.20", [hex: :gettext, repo: "hexpm", optional: false]}, {:tzdata, "~> 1.1", [hex: :tzdata, repo: "hexpm", optional: false]}], "hexpm", "8b9024f7efbabaf9bd7aa04f65cf8dcd7c9818ca5737677c7b76acbc6a94d1aa"},
|
||||
"tzdata": {:hex, :tzdata, "1.1.1", "20c8043476dfda8504952d00adac41c6eda23912278add38edc140ae0c5bcc46", [:mix], [{:hackney, "~> 1.17", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm", "a69cec8352eafcd2e198dea28a34113b60fdc6cb57eb5ad65c10292a6ba89787"},
|
||||
"unicode_util_compat": {:hex, :unicode_util_compat, "0.7.0", "bc84380c9ab48177092f43ac89e4dfa2c6d62b40b8bd132b1059ecc7232f9a78", [:rebar3], [], "hexpm", "25eee6d67df61960cf6a794239566599b09e17e668d3700247bc498638152521"},
|
||||
"websock": {:hex, :websock, "0.5.3", "2f69a6ebe810328555b6fe5c831a851f485e303a7c8ce6c5f675abeb20ebdadc", [:mix], [], "hexpm", "6105453d7fac22c712ad66fab1d45abdf049868f253cf719b625151460b8b453"},
|
||||
"websock_adapter": {:hex, :websock_adapter, "0.5.5", "9dfeee8269b27e958a65b3e235b7e447769f66b5b5925385f5a569269164a210", [:mix], [{:bandit, ">= 0.6.0", [hex: :bandit, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.6", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:websock, "~> 0.5", [hex: :websock, repo: "hexpm", optional: false]}], "hexpm", "4b977ba4a01918acbf77045ff88de7f6972c2a009213c515a445c48f224ffce9"},
|
||||
"websock_adapter": {:hex, :websock_adapter, "0.5.7", "65fa74042530064ef0570b75b43f5c49bb8b235d6515671b3d250022cb8a1f9e", [:mix], [{:bandit, ">= 0.6.0", [hex: :bandit, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.6", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:websock, "~> 0.5", [hex: :websock, repo: "hexpm", optional: false]}], "hexpm", "d0f478ee64deddfec64b800673fd6e0c8888b079d9f3444dd96d2a98383bdbd1"},
|
||||
}
|
||||
|
||||
+2
-2
@@ -6,8 +6,8 @@
|
||||
},
|
||||
"scripts": {
|
||||
"create-erd": "sqleton -o priv/repo/erd.png priv/repo/pinchflat_dev.db",
|
||||
"lint:check": "prettier . --check --config=tooling/.prettierrc.js --ignore-path=tooling/.prettierignore --ignore-path=.gitignore",
|
||||
"lint:fix": "prettier . --write --config=tooling/.prettierrc.js --ignore-path=tooling/.prettierignore --ignore-path=.gitignore"
|
||||
"lint:check": "prettier . --check --config=.prettierrc.js --ignore-path=.prettierignore --ignore-path=.gitignore",
|
||||
"lint:fix": "prettier . --write --config=.prettierrc.js --ignore-path=.prettierignore --ignore-path=.gitignore"
|
||||
},
|
||||
"private": true
|
||||
}
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 468 KiB After Width: | Height: | Size: 433 KiB |
@@ -0,0 +1,9 @@
|
||||
defmodule Pinchflat.Repo.Migrations.AddPlaylistIndexToMediaItems do
|
||||
use Ecto.Migration
|
||||
|
||||
def change do
|
||||
alter table(:media_items) do
|
||||
add :playlist_index, :integer, null: false, default: 0
|
||||
end
|
||||
end
|
||||
end
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
defmodule Pinchflat.Repo.Migrations.AddMarkedForDeletionAtToSources do
|
||||
use Ecto.Migration
|
||||
|
||||
def change do
|
||||
alter table(:sources) do
|
||||
add :marked_for_deletion_at, :utc_datetime
|
||||
end
|
||||
|
||||
alter table(:media_profiles) do
|
||||
add :marked_for_deletion_at, :utc_datetime
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,9 @@
|
||||
defmodule Pinchflat.Repo.Migrations.AddUseCookiesToSources do
|
||||
use Ecto.Migration
|
||||
|
||||
def change do
|
||||
alter table(:sources) do
|
||||
add :use_cookies, :boolean, default: true, null: false
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,10 @@
|
||||
defmodule Pinchflat.Repo.Migrations.AddDurationLimitsToSources do
|
||||
use Ecto.Migration
|
||||
|
||||
def change do
|
||||
alter table(:sources) do
|
||||
add :min_duration_seconds, :integer
|
||||
add :max_duration_seconds, :integer
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,9 @@
|
||||
defmodule Pinchflat.Repo.Migrations.AddMediaContainerToMediaProfiles do
|
||||
use Ecto.Migration
|
||||
|
||||
def change do
|
||||
alter table(:media_profiles) do
|
||||
add :media_container, :string
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -93,15 +93,23 @@ defmodule Pinchflat.Downloading.DownloadOptionBuilderTest do
|
||||
end
|
||||
|
||||
test "includes :write_auto_subs option when specified", %{media_item: media_item} do
|
||||
media_item = update_media_profile_attribute(media_item, %{download_subs: true, download_auto_subs: true})
|
||||
media_item_1 = update_media_profile_attribute(media_item, %{download_subs: true, download_auto_subs: true})
|
||||
media_item_2 = update_media_profile_attribute(media_item, %{embed_subs: true, download_auto_subs: true})
|
||||
|
||||
assert {:ok, res} = DownloadOptionBuilder.build(media_item)
|
||||
assert {:ok, res_1} = DownloadOptionBuilder.build(media_item_1)
|
||||
assert {:ok, res_2} = DownloadOptionBuilder.build(media_item_2)
|
||||
|
||||
assert :write_auto_subs in res
|
||||
assert :write_auto_subs in res_1
|
||||
assert :write_auto_subs in res_2
|
||||
end
|
||||
|
||||
test "doesn't include :write_auto_subs option when download_subs is false", %{media_item: media_item} do
|
||||
media_item = update_media_profile_attribute(media_item, %{download_subs: false, download_auto_subs: true})
|
||||
test "doesn't include :write_auto_subs option when download_subs and embed_subs is false", %{media_item: media_item} do
|
||||
media_item =
|
||||
update_media_profile_attribute(media_item, %{
|
||||
download_subs: false,
|
||||
embed_subs: false,
|
||||
download_auto_subs: true
|
||||
})
|
||||
|
||||
assert {:ok, res} = DownloadOptionBuilder.build(media_item)
|
||||
|
||||
@@ -244,7 +252,7 @@ defmodule Pinchflat.Downloading.DownloadOptionBuilderTest do
|
||||
end
|
||||
end
|
||||
|
||||
describe "build/1 when testing quality options" do
|
||||
describe "build/1 when testing media quality and format options" do
|
||||
test "includes quality options" do
|
||||
resolutions = ["360", "480", "720", "1080", "2160", "4320"]
|
||||
|
||||
@@ -283,6 +291,22 @@ defmodule Pinchflat.Downloading.DownloadOptionBuilderTest do
|
||||
|
||||
assert {:format_sort, "res:1080,+codec:av01:aac"} in res
|
||||
end
|
||||
|
||||
test "includes custom remux target for videos if specified", %{media_item: media_item} do
|
||||
media_item = update_media_profile_attribute(media_item, %{media_container: "mkv"})
|
||||
|
||||
assert {:ok, res} = DownloadOptionBuilder.build(media_item)
|
||||
|
||||
assert {:remux_video, "mkv"} in res
|
||||
end
|
||||
|
||||
test "includes custom format target for audio if specified", %{media_item: media_item} do
|
||||
media_item = update_media_profile_attribute(media_item, %{media_container: "flac", preferred_resolution: :audio})
|
||||
|
||||
assert {:ok, res} = DownloadOptionBuilder.build(media_item)
|
||||
|
||||
assert {:audio_format, "flac"} in res
|
||||
end
|
||||
end
|
||||
|
||||
describe "build/1 when testing sponsorblock options" do
|
||||
|
||||
@@ -130,9 +130,6 @@ defmodule Pinchflat.Downloading.DownloadingHelpersTest do
|
||||
_download_prevented =
|
||||
media_item_fixture(source_id: source.id, media_filepath: "some/filepath.mp4", prevent_download: true)
|
||||
|
||||
_culled =
|
||||
media_item_fixture(source_id: source.id, media_filepath: "some/filepath.mp4", culled_at: now())
|
||||
|
||||
assert [] = DownloadingHelpers.kickoff_redownload_for_existing_media(source)
|
||||
|
||||
refute_enqueued(worker: MediaDownloadWorker)
|
||||
|
||||
@@ -9,8 +9,8 @@ defmodule Pinchflat.Downloading.MediaDownloadWorkerTest do
|
||||
alias Pinchflat.Downloading.MediaDownloadWorker
|
||||
|
||||
setup do
|
||||
stub(YtDlpRunnerMock, :run, fn _url, _opts, _ot -> {:ok, ""} end)
|
||||
stub(UserScriptRunnerMock, :run, fn _event_type, _data -> :ok end)
|
||||
stub(YtDlpRunnerMock, :run, fn _url, _opts, _ot, _addl -> {:ok, ""} end)
|
||||
stub(UserScriptRunnerMock, :run, fn _event_type, _data -> {:ok, "", 0} end)
|
||||
stub(HTTPClientMock, :get, fn _url, _headers, _opts -> {:ok, ""} end)
|
||||
|
||||
media_item =
|
||||
@@ -53,10 +53,12 @@ defmodule Pinchflat.Downloading.MediaDownloadWorkerTest do
|
||||
|
||||
describe "perform/1" do
|
||||
test "it saves attributes to the media_item", %{media_item: media_item} do
|
||||
expect(YtDlpRunnerMock, :run, fn _url, _opts, _ot, _addl ->
|
||||
expect(YtDlpRunnerMock, :run, 1, fn _url, _opts, _ot, _addl ->
|
||||
{:ok, render_metadata(:media_metadata)}
|
||||
end)
|
||||
|
||||
expect(YtDlpRunnerMock, :run, 1, fn _url, _opts, _ot, _addl -> {:ok, ""} end)
|
||||
|
||||
assert media_item.media_filepath == nil
|
||||
perform_job(MediaDownloadWorker, %{id: media_item.id})
|
||||
media_item = Repo.reload(media_item)
|
||||
@@ -65,10 +67,12 @@ defmodule Pinchflat.Downloading.MediaDownloadWorkerTest do
|
||||
end
|
||||
|
||||
test "it saves the metadata to the media_item", %{media_item: media_item} do
|
||||
expect(YtDlpRunnerMock, :run, fn _url, _opts, _ot, _addl ->
|
||||
expect(YtDlpRunnerMock, :run, 1, fn _url, _opts, _ot, _addl ->
|
||||
{:ok, render_metadata(:media_metadata)}
|
||||
end)
|
||||
|
||||
expect(YtDlpRunnerMock, :run, 1, fn _url, _opts, _ot, _addl -> {:ok, ""} end)
|
||||
|
||||
assert media_item.metadata == nil
|
||||
perform_job(MediaDownloadWorker, %{id: media_item.id})
|
||||
assert Repo.reload(media_item).metadata != nil
|
||||
@@ -138,13 +142,15 @@ defmodule Pinchflat.Downloading.MediaDownloadWorkerTest do
|
||||
end
|
||||
|
||||
test "it saves the file's size to the database", %{media_item: media_item} do
|
||||
expect(YtDlpRunnerMock, :run, fn _url, _opts, _ot, _addl ->
|
||||
expect(YtDlpRunnerMock, :run, 1, fn _url, _opts, _ot, _addl ->
|
||||
metadata = render_parsed_metadata(:media_metadata)
|
||||
FilesystemUtils.write_p!(metadata["filepath"], "test")
|
||||
|
||||
{:ok, Phoenix.json_library().encode!(metadata)}
|
||||
end)
|
||||
|
||||
expect(YtDlpRunnerMock, :run, 1, fn _url, _opts, _ot, _addl -> {:ok, ""} end)
|
||||
|
||||
perform_job(MediaDownloadWorker, %{id: media_item.id})
|
||||
media_item = Repo.reload(media_item)
|
||||
|
||||
@@ -152,7 +158,7 @@ defmodule Pinchflat.Downloading.MediaDownloadWorkerTest do
|
||||
end
|
||||
|
||||
test "does not set redownloaded_at by default", %{media_item: media_item} do
|
||||
expect(YtDlpRunnerMock, :run, fn _url, _opts, _ot, _addl ->
|
||||
expect(YtDlpRunnerMock, :run, 2, fn _url, _opts, _ot, _addl ->
|
||||
{:ok, render_metadata(:media_metadata)}
|
||||
end)
|
||||
|
||||
@@ -162,32 +168,20 @@ defmodule Pinchflat.Downloading.MediaDownloadWorkerTest do
|
||||
assert media_item.media_redownloaded_at == nil
|
||||
end
|
||||
|
||||
test "calls the user script runner", %{media_item: media_item} do
|
||||
expect(YtDlpRunnerMock, :run, fn _url, _opts, _ot, _addl ->
|
||||
{:ok, render_metadata(:media_metadata)}
|
||||
end)
|
||||
|
||||
expect(UserScriptRunnerMock, :run, fn :media_downloaded, data ->
|
||||
assert data.id == media_item.id
|
||||
|
||||
:ok
|
||||
end)
|
||||
|
||||
perform_job(MediaDownloadWorker, %{id: media_item.id})
|
||||
end
|
||||
|
||||
test "does not blow up if the record doesn't exist" do
|
||||
assert :ok = perform_job(MediaDownloadWorker, %{id: 0})
|
||||
end
|
||||
|
||||
test "sets the no_force_overwrites runner option", %{media_item: media_item} do
|
||||
expect(YtDlpRunnerMock, :run, fn _url, opts, _ot, _addl ->
|
||||
expect(YtDlpRunnerMock, :run, 1, fn _url, opts, _ot, _addl ->
|
||||
assert :no_force_overwrites in opts
|
||||
refute :force_overwrites in opts
|
||||
|
||||
{:ok, render_metadata(:media_metadata)}
|
||||
end)
|
||||
|
||||
expect(YtDlpRunnerMock, :run, 1, fn _url, _opts, _ot, _addl -> {:ok, ""} end)
|
||||
|
||||
perform_job(MediaDownloadWorker, %{id: media_item.id})
|
||||
end
|
||||
end
|
||||
@@ -203,23 +197,27 @@ defmodule Pinchflat.Downloading.MediaDownloadWorkerTest do
|
||||
end
|
||||
|
||||
test "sets force_overwrites runner option", %{media_item: media_item} do
|
||||
expect(YtDlpRunnerMock, :run, fn _url, opts, _ot, _addl ->
|
||||
expect(YtDlpRunnerMock, :run, 1, fn _url, opts, _ot, _addl ->
|
||||
assert :force_overwrites in opts
|
||||
refute :no_force_overwrites in opts
|
||||
|
||||
{:ok, render_metadata(:media_metadata)}
|
||||
end)
|
||||
|
||||
expect(YtDlpRunnerMock, :run, 1, fn _url, _opts, _ot, _addl -> {:ok, ""} end)
|
||||
|
||||
perform_job(MediaDownloadWorker, %{id: media_item.id, force: true})
|
||||
end
|
||||
end
|
||||
|
||||
describe "perform/1 when testing re-downloads" do
|
||||
test "sets redownloaded_at on the media_item", %{media_item: media_item} do
|
||||
expect(YtDlpRunnerMock, :run, fn _url, _opts, _ot, _addl ->
|
||||
expect(YtDlpRunnerMock, :run, 1, fn _url, _opts, _ot, _addl ->
|
||||
{:ok, render_metadata(:media_metadata)}
|
||||
end)
|
||||
|
||||
expect(YtDlpRunnerMock, :run, 1, fn _url, _opts, _ot, _addl -> {:ok, ""} end)
|
||||
|
||||
perform_job(MediaDownloadWorker, %{id: media_item.id, quality_upgrade?: true})
|
||||
media_item = Repo.reload(media_item)
|
||||
|
||||
@@ -227,14 +225,71 @@ defmodule Pinchflat.Downloading.MediaDownloadWorkerTest do
|
||||
end
|
||||
|
||||
test "sets force_overwrites runner option", %{media_item: media_item} do
|
||||
expect(YtDlpRunnerMock, :run, fn _url, opts, _ot, _addl ->
|
||||
expect(YtDlpRunnerMock, :run, 1, fn _url, opts, _ot, _addl ->
|
||||
assert :force_overwrites in opts
|
||||
refute :no_force_overwrites in opts
|
||||
|
||||
{:ok, render_metadata(:media_metadata)}
|
||||
end)
|
||||
|
||||
expect(YtDlpRunnerMock, :run, 1, fn _url, _opts, _ot, _addl -> {:ok, ""} end)
|
||||
|
||||
perform_job(MediaDownloadWorker, %{id: media_item.id, force: true})
|
||||
end
|
||||
end
|
||||
|
||||
describe "perform/1 when testing user script callbacks" do
|
||||
setup do
|
||||
stub(YtDlpRunnerMock, :run, fn _url, _opts, _ot, _addl ->
|
||||
{:ok, render_metadata(:media_metadata)}
|
||||
end)
|
||||
|
||||
:ok
|
||||
end
|
||||
|
||||
test "calls the media_pre_download user script runner", %{media_item: media_item} do
|
||||
expect(UserScriptRunnerMock, :run, fn :media_pre_download, data ->
|
||||
assert data.id == media_item.id
|
||||
|
||||
{:ok, "", 0}
|
||||
end)
|
||||
|
||||
expect(UserScriptRunnerMock, :run, fn :media_downloaded, _ -> {:ok, "", 0} end)
|
||||
|
||||
perform_job(MediaDownloadWorker, %{id: media_item.id})
|
||||
end
|
||||
|
||||
test "does not download the media if the pre-download script returns an error", %{media_item: media_item} do
|
||||
expect(UserScriptRunnerMock, :run, fn :media_pre_download, _ -> {:ok, "", 1} end)
|
||||
|
||||
assert :ok = perform_job(MediaDownloadWorker, %{id: media_item.id})
|
||||
media_item = Repo.reload!(media_item)
|
||||
|
||||
refute media_item.media_filepath
|
||||
assert media_item.prevent_download
|
||||
end
|
||||
|
||||
test "downloads media if the pre-download script is not present", %{media_item: media_item} do
|
||||
expect(UserScriptRunnerMock, :run, fn :media_pre_download, _ -> {:ok, :no_executable} end)
|
||||
expect(UserScriptRunnerMock, :run, fn :media_downloaded, _ -> {:ok, :no_executable} end)
|
||||
|
||||
assert :ok = perform_job(MediaDownloadWorker, %{id: media_item.id})
|
||||
media_item = Repo.reload!(media_item)
|
||||
|
||||
assert media_item.media_filepath
|
||||
refute media_item.prevent_download
|
||||
end
|
||||
|
||||
test "calls the media_downloaded user script runner", %{media_item: media_item} do
|
||||
expect(UserScriptRunnerMock, :run, fn :media_pre_download, _ -> {:ok, "", 0} end)
|
||||
|
||||
expect(UserScriptRunnerMock, :run, fn :media_downloaded, data ->
|
||||
assert data.id == media_item.id
|
||||
|
||||
{:ok, "", 0}
|
||||
end)
|
||||
|
||||
perform_job(MediaDownloadWorker, %{id: media_item.id})
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -5,6 +5,7 @@ defmodule Pinchflat.Downloading.MediaDownloaderTest do
|
||||
import Pinchflat.SourcesFixtures
|
||||
import Pinchflat.ProfilesFixtures
|
||||
|
||||
alias Pinchflat.Media
|
||||
alias Pinchflat.Downloading.MediaDownloader
|
||||
|
||||
setup do
|
||||
@@ -15,27 +16,29 @@ defmodule Pinchflat.Downloading.MediaDownloaderTest do
|
||||
)
|
||||
|
||||
stub(HTTPClientMock, :get, fn _url, _headers, _opts -> {:ok, ""} end)
|
||||
stub(YtDlpRunnerMock, :run, fn _url, _opts, _ot -> {:ok, ""} end)
|
||||
stub(YtDlpRunnerMock, :run, fn _url, _opts, _ot, _addl_args -> {:ok, ""} end)
|
||||
|
||||
{:ok, %{media_item: media_item}}
|
||||
end
|
||||
|
||||
describe "download_for_media_item/3" do
|
||||
test "it calls the backend runner", %{media_item: media_item} do
|
||||
expect(YtDlpRunnerMock, :run, fn url, _opts, ot, addl ->
|
||||
test "calls the backend runner", %{media_item: media_item} do
|
||||
expect(YtDlpRunnerMock, :run, 1, fn url, _opts, ot, addl ->
|
||||
assert url == media_item.original_url
|
||||
assert ot == "after_move:%()j"
|
||||
assert [{:output_filepath, filepath}] = addl
|
||||
assert [{:output_filepath, filepath} | _] = addl
|
||||
assert is_binary(filepath)
|
||||
|
||||
{:ok, render_metadata(:media_metadata)}
|
||||
end)
|
||||
|
||||
expect(YtDlpRunnerMock, :run, 1, fn _url, _opts, _ot, _addl_args -> {:ok, ""} end)
|
||||
|
||||
assert {:ok, _} = MediaDownloader.download_for_media_item(media_item)
|
||||
end
|
||||
|
||||
test "it saves the metadata filepath to the database", %{media_item: media_item} do
|
||||
expect(YtDlpRunnerMock, :run, fn _url, _opts, _ot, _addl ->
|
||||
test "saves the metadata filepath to the database", %{media_item: media_item} do
|
||||
expect(YtDlpRunnerMock, :run, 2, fn _url, _opts, _ot, _addl ->
|
||||
{:ok, render_metadata(:media_metadata)}
|
||||
end)
|
||||
|
||||
@@ -66,19 +69,51 @@ defmodule Pinchflat.Downloading.MediaDownloaderTest do
|
||||
|
||||
describe "download_for_media_item/3 when testing override options" do
|
||||
test "includes override opts if specified", %{media_item: media_item} do
|
||||
expect(YtDlpRunnerMock, :run, fn _url, opts, _ot, _addl ->
|
||||
expect(YtDlpRunnerMock, :run, 1, fn _url, opts, _ot, _addl ->
|
||||
refute :force_overwrites in opts
|
||||
assert :no_force_overwrites in opts
|
||||
|
||||
{:ok, render_metadata(:media_metadata)}
|
||||
end)
|
||||
|
||||
expect(YtDlpRunnerMock, :run, 1, fn _url, _opts, _ot, _addl -> {:ok, ""} end)
|
||||
|
||||
override_opts = [overwrite_behaviour: :no_force_overwrites]
|
||||
|
||||
assert {:ok, _} = MediaDownloader.download_for_media_item(media_item, override_opts)
|
||||
end
|
||||
end
|
||||
|
||||
describe "download_for_media_item/3 when testing cookie usage" do
|
||||
test "sets use_cookies if the source uses cookies" do
|
||||
expect(YtDlpRunnerMock, :run, 1, fn _url, _opts, _ot, addl ->
|
||||
assert {:use_cookies, true} in addl
|
||||
{:ok, render_metadata(:media_metadata)}
|
||||
end)
|
||||
|
||||
expect(YtDlpRunnerMock, :run, 1, fn _url, _opts, _ot, _addl_args -> {:ok, ""} end)
|
||||
|
||||
source = source_fixture(%{use_cookies: true})
|
||||
media_item = media_item_fixture(%{source_id: source.id})
|
||||
|
||||
assert {:ok, _} = MediaDownloader.download_for_media_item(media_item)
|
||||
end
|
||||
|
||||
test "does not set use_cookies if the source does not use cookies" do
|
||||
expect(YtDlpRunnerMock, :run, 1, fn _url, _opts, _ot, addl ->
|
||||
assert {:use_cookies, false} in addl
|
||||
{:ok, render_metadata(:media_metadata)}
|
||||
end)
|
||||
|
||||
expect(YtDlpRunnerMock, :run, 1, fn _url, _opts, _ot, _addl_args -> {:ok, ""} end)
|
||||
|
||||
source = source_fixture(%{use_cookies: false})
|
||||
media_item = media_item_fixture(%{source_id: source.id})
|
||||
|
||||
assert {:ok, _} = MediaDownloader.download_for_media_item(media_item)
|
||||
end
|
||||
end
|
||||
|
||||
describe "download_for_media_item/3 when testing retries" do
|
||||
test "returns a recovered tuple on recoverable errors", %{media_item: media_item} do
|
||||
message = "Unable to communicate with SponsorBlock"
|
||||
@@ -93,13 +128,15 @@ defmodule Pinchflat.Downloading.MediaDownloaderTest do
|
||||
test "attempts to update the media item on recoverable errors", %{media_item: media_item} do
|
||||
message = "Unable to communicate with SponsorBlock"
|
||||
|
||||
expect(YtDlpRunnerMock, :run, fn _url, _opts, _ot, addl ->
|
||||
[{:output_filepath, filepath}] = addl
|
||||
expect(YtDlpRunnerMock, :run, 1, fn _url, _opts, _ot, addl ->
|
||||
[{:output_filepath, filepath} | _] = addl
|
||||
File.write(filepath, render_metadata(:media_metadata))
|
||||
|
||||
{:error, message, 1}
|
||||
end)
|
||||
|
||||
expect(YtDlpRunnerMock, :run, 1, fn _url, _opts, _ot, _addl_args -> {:ok, ""} end)
|
||||
|
||||
assert {:recovered, ^message} = MediaDownloader.download_for_media_item(media_item)
|
||||
media_item = Repo.reload(media_item)
|
||||
|
||||
@@ -117,42 +154,48 @@ defmodule Pinchflat.Downloading.MediaDownloaderTest do
|
||||
:ok
|
||||
end
|
||||
|
||||
test "it sets the media_downloaded_at", %{media_item: media_item} do
|
||||
test "sets the media_downloaded_at", %{media_item: media_item} do
|
||||
assert media_item.media_downloaded_at == nil
|
||||
assert {:ok, updated_media_item} = MediaDownloader.download_for_media_item(media_item)
|
||||
assert DateTime.diff(DateTime.utc_now(), updated_media_item.media_downloaded_at) < 2
|
||||
end
|
||||
|
||||
test "it extracts the title", %{media_item: media_item} do
|
||||
test "sets the culled_at to nil", %{media_item: media_item} do
|
||||
Media.update_media_item(media_item, %{culled_at: DateTime.utc_now()})
|
||||
assert {:ok, updated_media_item} = MediaDownloader.download_for_media_item(media_item)
|
||||
assert updated_media_item.culled_at == nil
|
||||
end
|
||||
|
||||
test "extracts the title", %{media_item: media_item} do
|
||||
assert {:ok, updated_media_item} = MediaDownloader.download_for_media_item(media_item)
|
||||
assert updated_media_item.title == "Pinchflat Example Video"
|
||||
end
|
||||
|
||||
test "it extracts the description", %{media_item: media_item} do
|
||||
test "extracts the description", %{media_item: media_item} do
|
||||
assert {:ok, updated_media_item} = MediaDownloader.download_for_media_item(media_item)
|
||||
assert is_binary(updated_media_item.description)
|
||||
end
|
||||
|
||||
test "it extracts the media_filepath", %{media_item: media_item} do
|
||||
test "extracts the media_filepath", %{media_item: media_item} do
|
||||
assert media_item.media_filepath == nil
|
||||
assert {:ok, updated_media_item} = MediaDownloader.download_for_media_item(media_item)
|
||||
assert String.ends_with?(updated_media_item.media_filepath, ".mkv")
|
||||
end
|
||||
|
||||
test "it extracts the subtitle_filepaths", %{media_item: media_item} do
|
||||
test "extracts the subtitle_filepaths", %{media_item: media_item} do
|
||||
assert media_item.subtitle_filepaths == []
|
||||
assert {:ok, updated_media_item} = MediaDownloader.download_for_media_item(media_item)
|
||||
assert [["de", _], ["en", _] | _rest] = updated_media_item.subtitle_filepaths
|
||||
end
|
||||
|
||||
test "it extracts the duration_seconds", %{media_item: media_item} do
|
||||
test "extracts the duration_seconds", %{media_item: media_item} do
|
||||
assert media_item.duration_seconds == nil
|
||||
assert {:ok, updated_media_item} = MediaDownloader.download_for_media_item(media_item)
|
||||
assert is_integer(updated_media_item.duration_seconds)
|
||||
end
|
||||
|
||||
test "it extracts the thumbnail_filepath", %{media_item: media_item} do
|
||||
expect(YtDlpRunnerMock, :run, fn _url, _opts, _ot, _addl ->
|
||||
test "extracts the thumbnail_filepath", %{media_item: media_item} do
|
||||
expect(YtDlpRunnerMock, :run, 2, fn _url, _opts, _ot, _addl ->
|
||||
metadata = render_parsed_metadata(:media_metadata)
|
||||
|
||||
thumbnail_filepath =
|
||||
@@ -175,8 +218,8 @@ defmodule Pinchflat.Downloading.MediaDownloaderTest do
|
||||
File.rm(updated_media_item.thumbnail_filepath)
|
||||
end
|
||||
|
||||
test "it extracts the metadata_filepath", %{media_item: media_item} do
|
||||
expect(YtDlpRunnerMock, :run, fn _url, _opts, _ot, _addl ->
|
||||
test "extracts the metadata_filepath", %{media_item: media_item} do
|
||||
expect(YtDlpRunnerMock, :run, 2, fn _url, _opts, _ot, _addl ->
|
||||
metadata = render_parsed_metadata(:media_metadata)
|
||||
|
||||
infojson_filepath = metadata["infojson_filename"]
|
||||
@@ -195,14 +238,14 @@ defmodule Pinchflat.Downloading.MediaDownloaderTest do
|
||||
|
||||
describe "download_for_media_item/3 when testing NFO generation" do
|
||||
setup do
|
||||
expect(YtDlpRunnerMock, :run, fn _url, _opts, _ot, _addl ->
|
||||
stub(YtDlpRunnerMock, :run, fn _url, _opts, _ot, _addl ->
|
||||
{:ok, render_metadata(:media_metadata)}
|
||||
end)
|
||||
|
||||
:ok
|
||||
end
|
||||
|
||||
test "it generates an NFO file if the source is set to download NFOs" do
|
||||
test "generates an NFO file if the source is set to download NFOs" do
|
||||
profile = media_profile_fixture(%{download_nfo: true})
|
||||
source = source_fixture(%{media_profile_id: profile.id})
|
||||
media_item = media_item_fixture(%{source_id: source.id})
|
||||
@@ -215,7 +258,7 @@ defmodule Pinchflat.Downloading.MediaDownloaderTest do
|
||||
File.rm!(updated_media_item.nfo_filepath)
|
||||
end
|
||||
|
||||
test "it does not generate an NFO file if the source is set to not download NFOs" do
|
||||
test "does not generate an NFO file if the source is set to not download NFOs" do
|
||||
profile = media_profile_fixture(%{download_nfo: false})
|
||||
source = source_fixture(%{media_profile_id: profile.id})
|
||||
media_item = media_item_fixture(%{source_id: source.id})
|
||||
|
||||
@@ -7,15 +7,36 @@ defmodule Pinchflat.Downloading.MediaRetentionWorkerTest do
|
||||
alias Pinchflat.Media
|
||||
alias Pinchflat.Downloading.MediaRetentionWorker
|
||||
|
||||
describe "perform/1" do
|
||||
setup do
|
||||
stub(UserScriptRunnerMock, :run, fn _event_type, _data -> :ok end)
|
||||
stub(UserScriptRunnerMock, :run, fn _event_type, _data -> {:ok, "", 0} end)
|
||||
|
||||
:ok
|
||||
end
|
||||
|
||||
describe "perform/1" do
|
||||
test "sets deleted media to not re-download" do
|
||||
{_source, old_media_item, new_media_item} = prepare_records_for_retention_date()
|
||||
|
||||
perform_job(MediaRetentionWorker, %{})
|
||||
|
||||
refute Repo.reload!(new_media_item).prevent_download
|
||||
assert Repo.reload!(old_media_item).prevent_download
|
||||
end
|
||||
|
||||
test "sets culled_at timestamp on deleted media" do
|
||||
{_source, old_media_item, new_media_item} = prepare_records_for_retention_date()
|
||||
|
||||
perform_job(MediaRetentionWorker, %{})
|
||||
|
||||
refute Repo.reload!(new_media_item).culled_at
|
||||
assert Repo.reload!(old_media_item).culled_at
|
||||
assert DateTime.diff(now(), Repo.reload!(old_media_item).culled_at) < 1
|
||||
end
|
||||
end
|
||||
|
||||
describe "perform/1 when testing retention_period-based culling" do
|
||||
test "deletes media files that are past their retention date" do
|
||||
{_source, old_media_item, new_media_item} = prepare_records()
|
||||
{_source, old_media_item, new_media_item} = prepare_records_for_retention_date()
|
||||
|
||||
perform_job(MediaRetentionWorker, %{})
|
||||
|
||||
@@ -25,27 +46,33 @@ defmodule Pinchflat.Downloading.MediaRetentionWorkerTest do
|
||||
refute Repo.reload!(old_media_item).media_filepath
|
||||
end
|
||||
|
||||
test "sets deleted media to not re-download" do
|
||||
{_source, old_media_item, new_media_item} = prepare_records()
|
||||
|
||||
perform_job(MediaRetentionWorker, %{})
|
||||
|
||||
refute Repo.reload!(new_media_item).prevent_download
|
||||
assert Repo.reload!(old_media_item).prevent_download
|
||||
end
|
||||
|
||||
test "sets culled_at timestamp on deleted media" do
|
||||
{_source, old_media_item, new_media_item} = prepare_records()
|
||||
test "sets culled_at and prevent_download" do
|
||||
{_source, old_media_item, new_media_item} = prepare_records_for_retention_date()
|
||||
|
||||
perform_job(MediaRetentionWorker, %{})
|
||||
|
||||
refute Repo.reload!(new_media_item).culled_at
|
||||
assert Repo.reload!(old_media_item).culled_at
|
||||
assert DateTime.diff(now(), Repo.reload!(old_media_item).culled_at) < 1
|
||||
refute Repo.reload!(new_media_item).prevent_download
|
||||
assert Repo.reload!(old_media_item).prevent_download
|
||||
end
|
||||
|
||||
test "doesn't cull if the source doesn't have a retention period" do
|
||||
{_source, old_media_item, new_media_item} = prepare_records_for_retention_date(nil)
|
||||
|
||||
perform_job(MediaRetentionWorker, %{})
|
||||
|
||||
assert File.exists?(new_media_item.media_filepath)
|
||||
assert File.exists?(old_media_item.media_filepath)
|
||||
assert Repo.reload!(new_media_item).media_filepath
|
||||
assert Repo.reload!(old_media_item).media_filepath
|
||||
|
||||
refute Repo.reload!(new_media_item).culled_at
|
||||
refute Repo.reload!(old_media_item).culled_at
|
||||
end
|
||||
|
||||
test "doesn't cull media items that have prevent_culling set" do
|
||||
{_source, old_media_item, _new_media_item} = prepare_records()
|
||||
{_source, old_media_item, _new_media_item} = prepare_records_for_retention_date()
|
||||
|
||||
Media.update_media_item(old_media_item, %{prevent_culling: true})
|
||||
|
||||
@@ -53,11 +80,99 @@ defmodule Pinchflat.Downloading.MediaRetentionWorkerTest do
|
||||
|
||||
assert File.exists?(old_media_item.media_filepath)
|
||||
assert Repo.reload!(old_media_item).media_filepath
|
||||
refute Repo.reload!(old_media_item).culled_at
|
||||
end
|
||||
|
||||
test "doesn't cull if the media item has no media_filepath" do
|
||||
{_source, old_media_item, _new_media_item} = prepare_records_for_retention_date()
|
||||
|
||||
Media.update_media_item(old_media_item, %{media_filepath: nil})
|
||||
|
||||
perform_job(MediaRetentionWorker, %{})
|
||||
|
||||
refute Repo.reload!(old_media_item).culled_at
|
||||
end
|
||||
end
|
||||
|
||||
defp prepare_records do
|
||||
source = source_fixture(%{retention_period_days: 2})
|
||||
describe "perform/1 when testing source cutoff-based culling" do
|
||||
test "culls media from before the cutoff date" do
|
||||
{_source, old_media_item, new_media_item} = prepare_records_for_source_cutoff_date()
|
||||
|
||||
perform_job(MediaRetentionWorker, %{})
|
||||
|
||||
assert File.exists?(new_media_item.media_filepath)
|
||||
refute File.exists?(old_media_item.media_filepath)
|
||||
assert Repo.reload!(new_media_item).media_filepath
|
||||
refute Repo.reload!(old_media_item).media_filepath
|
||||
end
|
||||
|
||||
test "sets culled_at but not prevent_download" do
|
||||
{_source, old_media_item, new_media_item} = prepare_records_for_source_cutoff_date()
|
||||
|
||||
perform_job(MediaRetentionWorker, %{})
|
||||
|
||||
refute Repo.reload!(new_media_item).culled_at
|
||||
assert Repo.reload!(old_media_item).culled_at
|
||||
refute Repo.reload!(new_media_item).prevent_download
|
||||
refute Repo.reload!(old_media_item).prevent_download
|
||||
end
|
||||
|
||||
test "doesn't cull media if the source doesn't have a cutoff date" do
|
||||
{_source, old_media_item, new_media_item} = prepare_records_for_source_cutoff_date(nil)
|
||||
|
||||
perform_job(MediaRetentionWorker, %{})
|
||||
|
||||
assert File.exists?(new_media_item.media_filepath)
|
||||
assert File.exists?(old_media_item.media_filepath)
|
||||
assert Repo.reload!(new_media_item).media_filepath
|
||||
assert Repo.reload!(old_media_item).media_filepath
|
||||
|
||||
refute Repo.reload!(new_media_item).culled_at
|
||||
refute Repo.reload!(old_media_item).culled_at
|
||||
end
|
||||
|
||||
test "doesn't cull media from on or after the cutoff date" do
|
||||
{_source, old_media_item, new_media_item} = prepare_records_for_source_cutoff_date(2)
|
||||
|
||||
Media.update_media_item(old_media_item, %{uploaded_at: now_minus(2, :days)})
|
||||
Media.update_media_item(new_media_item, %{uploaded_at: now_minus(1, :day)})
|
||||
|
||||
perform_job(MediaRetentionWorker, %{})
|
||||
|
||||
assert File.exists?(new_media_item.media_filepath)
|
||||
assert File.exists?(old_media_item.media_filepath)
|
||||
assert Repo.reload!(new_media_item).media_filepath
|
||||
assert Repo.reload!(old_media_item).media_filepath
|
||||
|
||||
refute Repo.reload!(new_media_item).culled_at
|
||||
refute Repo.reload!(old_media_item).culled_at
|
||||
end
|
||||
|
||||
test "doesn't cull media items that have prevent_culling set" do
|
||||
{_source, old_media_item, _new_media_item} = prepare_records_for_source_cutoff_date()
|
||||
|
||||
Media.update_media_item(old_media_item, %{prevent_culling: true})
|
||||
|
||||
perform_job(MediaRetentionWorker, %{})
|
||||
|
||||
assert File.exists?(old_media_item.media_filepath)
|
||||
assert Repo.reload!(old_media_item).media_filepath
|
||||
refute Repo.reload!(old_media_item).culled_at
|
||||
end
|
||||
|
||||
test "doesn't cull if the media item has no media_filepath" do
|
||||
{_source, old_media_item, _new_media_item} = prepare_records_for_source_cutoff_date()
|
||||
|
||||
Media.update_media_item(old_media_item, %{media_filepath: nil})
|
||||
|
||||
perform_job(MediaRetentionWorker, %{})
|
||||
|
||||
refute Repo.reload!(old_media_item).culled_at
|
||||
end
|
||||
end
|
||||
|
||||
defp prepare_records_for_retention_date(retention_period_days \\ 2) do
|
||||
source = source_fixture(%{retention_period_days: retention_period_days})
|
||||
|
||||
old_media_item =
|
||||
media_item_with_attachments(%{
|
||||
@@ -73,4 +188,23 @@ defmodule Pinchflat.Downloading.MediaRetentionWorkerTest do
|
||||
|
||||
{source, old_media_item, new_media_item}
|
||||
end
|
||||
|
||||
defp prepare_records_for_source_cutoff_date(download_cutoff_date_days_ago \\ 2) do
|
||||
cutoff_date = if download_cutoff_date_days_ago, do: now_minus(download_cutoff_date_days_ago, :days), else: nil
|
||||
source = source_fixture(%{download_cutoff_date: cutoff_date})
|
||||
|
||||
old_media_item =
|
||||
media_item_with_attachments(%{
|
||||
source_id: source.id,
|
||||
uploaded_at: now_minus(3, :days)
|
||||
})
|
||||
|
||||
new_media_item =
|
||||
media_item_with_attachments(%{
|
||||
source_id: source.id,
|
||||
uploaded_at: now_minus(1, :day)
|
||||
})
|
||||
|
||||
{source, old_media_item, new_media_item}
|
||||
end
|
||||
end
|
||||
|
||||
@@ -12,7 +12,7 @@ defmodule Pinchflat.FastIndexing.FastIndexingHelpersTest do
|
||||
alias Pinchflat.FastIndexing.FastIndexingHelpers
|
||||
|
||||
setup do
|
||||
stub(YtDlpRunnerMock, :run, fn _url, _opts, _ot ->
|
||||
stub(YtDlpRunnerMock, :run, fn _url, _opts, _ot, _addl ->
|
||||
{:ok, media_attributes_return_fixture()}
|
||||
end)
|
||||
|
||||
@@ -61,19 +61,47 @@ defmodule Pinchflat.FastIndexing.FastIndexingHelpersTest do
|
||||
assert [_] = Tasks.list_tasks_for(media_item, "MediaDownloadWorker")
|
||||
end
|
||||
|
||||
test "sets use_cookies if the source uses cookies" do
|
||||
expect(HTTPClientMock, :get, fn _url -> {:ok, "<yt:videoId>test_1</yt:videoId>"} end)
|
||||
|
||||
stub(YtDlpRunnerMock, :run, fn _url, _opts, _ot, addl ->
|
||||
assert {:use_cookies, true} in addl
|
||||
|
||||
{:ok, media_attributes_return_fixture()}
|
||||
end)
|
||||
|
||||
source = source_fixture(%{use_cookies: true})
|
||||
|
||||
assert [%MediaItem{}] = FastIndexingHelpers.kickoff_download_tasks_from_youtube_rss_feed(source)
|
||||
end
|
||||
|
||||
test "does not set use_cookies if the source does not use cookies" do
|
||||
expect(HTTPClientMock, :get, fn _url -> {:ok, "<yt:videoId>test_1</yt:videoId>"} end)
|
||||
|
||||
stub(YtDlpRunnerMock, :run, fn _url, _opts, _ot, addl ->
|
||||
assert {:use_cookies, false} in addl
|
||||
|
||||
{:ok, media_attributes_return_fixture()}
|
||||
end)
|
||||
|
||||
source = source_fixture(%{use_cookies: false})
|
||||
|
||||
assert [%MediaItem{}] = FastIndexingHelpers.kickoff_download_tasks_from_youtube_rss_feed(source)
|
||||
end
|
||||
|
||||
test "does not enqueue a download job if the media item does not match the format rules" do
|
||||
expect(HTTPClientMock, :get, fn _url -> {:ok, "<yt:videoId>test_1</yt:videoId>"} end)
|
||||
|
||||
profile = media_profile_fixture(%{shorts_behaviour: :exclude})
|
||||
source = source_fixture(%{media_profile_id: profile.id})
|
||||
|
||||
stub(YtDlpRunnerMock, :run, fn _url, _opts, _ot ->
|
||||
stub(YtDlpRunnerMock, :run, fn _url, _opts, _ot, _addl ->
|
||||
output =
|
||||
Phoenix.json_library().encode!(%{
|
||||
id: "video2",
|
||||
title: "Video 2",
|
||||
webpage_url: "https://example.com/shorts/video2",
|
||||
was_live: true,
|
||||
live_status: "is_live",
|
||||
description: "desc2",
|
||||
aspect_ratio: 1.67,
|
||||
duration: 345.67,
|
||||
@@ -91,7 +119,7 @@ defmodule Pinchflat.FastIndexing.FastIndexingHelpersTest do
|
||||
test "does not blow up if a media item cannot be created", %{source: source} do
|
||||
expect(HTTPClientMock, :get, fn _url -> {:ok, "<yt:videoId>test_1</yt:videoId>"} end)
|
||||
|
||||
stub(YtDlpRunnerMock, :run, fn _url, _opts, _ot ->
|
||||
stub(YtDlpRunnerMock, :run, fn _url, _opts, _ot, _addl ->
|
||||
{:ok, "{}"}
|
||||
end)
|
||||
|
||||
@@ -101,7 +129,7 @@ defmodule Pinchflat.FastIndexing.FastIndexingHelpersTest do
|
||||
test "does not blow up if a media item causes a yt-dlp error", %{source: source} do
|
||||
expect(HTTPClientMock, :get, fn _url -> {:ok, "<yt:videoId>test_1</yt:videoId>"} end)
|
||||
|
||||
stub(YtDlpRunnerMock, :run, fn _url, _opts, _ot ->
|
||||
stub(YtDlpRunnerMock, :run, fn _url, _opts, _ot, _addl ->
|
||||
{:error, "message", 1}
|
||||
end)
|
||||
|
||||
|
||||
@@ -84,7 +84,7 @@ defmodule Pinchflat.FastIndexing.FastIndexingWorkerTest do
|
||||
source = source_fixture(fast_index: true)
|
||||
|
||||
expect(HTTPClientMock, :get, fn _url -> {:ok, "<yt:videoId>test_1</yt:videoId>"} end)
|
||||
expect(YtDlpRunnerMock, :run, fn _url, _opts, _ot -> {:ok, render_metadata(:media_metadata)} end)
|
||||
expect(YtDlpRunnerMock, :run, fn _url, _opts, _ot, _addl -> {:ok, render_metadata(:media_metadata)} end)
|
||||
|
||||
expect(AppriseRunnerMock, :run, fn servers, opts ->
|
||||
assert "server_1" = servers
|
||||
@@ -110,7 +110,7 @@ defmodule Pinchflat.FastIndexing.FastIndexingWorkerTest do
|
||||
source = source_fixture(fast_index: true, download_media: false)
|
||||
|
||||
expect(HTTPClientMock, :get, fn _url -> {:ok, "<yt:videoId>test_1</yt:videoId>"} end)
|
||||
expect(YtDlpRunnerMock, :run, fn _url, _opts, _ot -> {:ok, render_metadata(:media_metadata)} end)
|
||||
expect(YtDlpRunnerMock, :run, fn _url, _opts, _ot, _addl -> {:ok, render_metadata(:media_metadata)} end)
|
||||
expect(AppriseRunnerMock, :run, 0, fn _servers, _opts -> {:ok, ""} end)
|
||||
|
||||
perform_job(FastIndexingWorker, %{id: source.id})
|
||||
@@ -120,7 +120,7 @@ defmodule Pinchflat.FastIndexing.FastIndexingWorkerTest do
|
||||
source = source_fixture(fast_index: true, title_filter_regex: "foobar")
|
||||
|
||||
expect(HTTPClientMock, :get, fn _url -> {:ok, "<yt:videoId>test_1</yt:videoId>"} end)
|
||||
expect(YtDlpRunnerMock, :run, fn _url, _opts, _ot -> {:ok, render_metadata(:media_metadata)} end)
|
||||
expect(YtDlpRunnerMock, :run, fn _url, _opts, _ot, _addl -> {:ok, render_metadata(:media_metadata)} end)
|
||||
expect(AppriseRunnerMock, :run, 0, fn _servers, _opts -> {:ok, ""} end)
|
||||
|
||||
perform_job(FastIndexingWorker, %{id: source.id})
|
||||
|
||||
@@ -19,7 +19,7 @@ defmodule Pinchflat.Lifecycle.UserScripts.CommandRunnerTest do
|
||||
File.write(filepath(), "#!/bin/bash\ntouch #{filename}\n")
|
||||
|
||||
refute File.exists?(filename)
|
||||
assert :ok = Runner.run(:media_downloaded, %{})
|
||||
assert {:ok, _, _} = Runner.run(:media_downloaded, %{})
|
||||
assert File.exists?(filename)
|
||||
end
|
||||
|
||||
@@ -27,7 +27,7 @@ defmodule Pinchflat.Lifecycle.UserScripts.CommandRunnerTest do
|
||||
tmp_dir = Application.get_env(:pinchflat, :tmpfile_directory)
|
||||
File.write(filepath(), "#!/bin/bash\necho $1 > #{tmp_dir}/event_name\n")
|
||||
|
||||
assert :ok = Runner.run(:media_downloaded, %{})
|
||||
assert {:ok, _, _} = Runner.run(:media_downloaded, %{})
|
||||
assert File.read!("#{tmp_dir}/event_name") == "media_downloaded\n"
|
||||
end
|
||||
|
||||
@@ -35,26 +35,32 @@ defmodule Pinchflat.Lifecycle.UserScripts.CommandRunnerTest do
|
||||
tmp_dir = Application.get_env(:pinchflat, :tmpfile_directory)
|
||||
File.write(filepath(), "#!/bin/bash\necho $2 > #{tmp_dir}/encoded_data\n")
|
||||
|
||||
assert :ok = Runner.run(:media_downloaded, %{foo: "bar"})
|
||||
assert {:ok, _, _} = Runner.run(:media_downloaded, %{foo: "bar"})
|
||||
assert File.read!("#{tmp_dir}/encoded_data") == "{\"foo\":\"bar\"}\n"
|
||||
end
|
||||
|
||||
test "does nothing if the lifecycle file is not present" do
|
||||
:ok = File.rm(filepath())
|
||||
|
||||
assert :ok = Runner.run(:media_downloaded, %{})
|
||||
assert {:ok, :no_executable} = Runner.run(:media_downloaded, %{})
|
||||
end
|
||||
|
||||
test "does nothing if the lifecycle file is empty" do
|
||||
File.write(filepath(), "")
|
||||
|
||||
assert :ok = Runner.run(:media_downloaded, %{})
|
||||
assert {:ok, :no_executable} = Runner.run(:media_downloaded, %{})
|
||||
end
|
||||
|
||||
test "returns :ok if the command exits with a non-zero status" do
|
||||
File.write(filepath(), "#!/bin/bash\nexit 1\n")
|
||||
|
||||
assert :ok = Runner.run(:media_downloaded, %{})
|
||||
assert {:ok, _, 1} = Runner.run(:media_downloaded, %{})
|
||||
end
|
||||
|
||||
test "returns the output of the command" do
|
||||
File.write(filepath(), "#!/bin/bash\necho 'hello'\n")
|
||||
|
||||
assert {:ok, "hello\n", 0} = Runner.run(:media_downloaded, %{})
|
||||
end
|
||||
|
||||
test "gets upset if you pass an invalid event type" do
|
||||
|
||||
+109
-114
@@ -41,99 +41,7 @@ defmodule Pinchflat.MediaTest do
|
||||
end
|
||||
end
|
||||
|
||||
describe "list_cullable_media_items/0" do
|
||||
test "returns media items where the source has a retention period" do
|
||||
source_one = source_fixture(%{retention_period_days: 2})
|
||||
source_two = source_fixture(%{retention_period_days: 0})
|
||||
source_three = source_fixture(%{retention_period_days: nil})
|
||||
|
||||
_media_item =
|
||||
media_item_fixture(%{
|
||||
source_id: source_two.id,
|
||||
media_filepath: "/video/#{Faker.File.file_name(:video)}",
|
||||
media_downloaded_at: now_minus(3, :days)
|
||||
})
|
||||
|
||||
_media_item =
|
||||
media_item_fixture(%{
|
||||
source_id: source_three.id,
|
||||
media_filepath: "/video/#{Faker.File.file_name(:video)}",
|
||||
media_downloaded_at: now_minus(3, :days)
|
||||
})
|
||||
|
||||
expected_media_item =
|
||||
media_item_fixture(%{
|
||||
source_id: source_one.id,
|
||||
media_filepath: "/video/#{Faker.File.file_name(:video)}",
|
||||
media_downloaded_at: now_minus(3, :days)
|
||||
})
|
||||
|
||||
assert Media.list_cullable_media_items() == [expected_media_item]
|
||||
end
|
||||
|
||||
test "returns media_items with a media_filepath" do
|
||||
source = source_fixture(%{retention_period_days: 2})
|
||||
|
||||
_media_item =
|
||||
media_item_fixture(%{
|
||||
source_id: source.id,
|
||||
media_filepath: nil,
|
||||
media_downloaded_at: now_minus(3, :days)
|
||||
})
|
||||
|
||||
expected_media_item =
|
||||
media_item_fixture(%{
|
||||
source_id: source.id,
|
||||
media_filepath: "/video/#{Faker.File.file_name(:video)}",
|
||||
media_downloaded_at: now_minus(3, :days)
|
||||
})
|
||||
|
||||
assert Media.list_cullable_media_items() == [expected_media_item]
|
||||
end
|
||||
|
||||
test "returns items that have passed their retention period" do
|
||||
source = source_fixture(%{retention_period_days: 2})
|
||||
|
||||
_media_item =
|
||||
media_item_fixture(%{
|
||||
source_id: source.id,
|
||||
media_filepath: "/video/#{Faker.File.file_name(:video)}",
|
||||
media_downloaded_at: now_minus(2, :days)
|
||||
})
|
||||
|
||||
expected_media_item =
|
||||
media_item_fixture(%{
|
||||
source_id: source.id,
|
||||
media_filepath: "/video/#{Faker.File.file_name(:video)}",
|
||||
media_downloaded_at: now_minus(3, :days)
|
||||
})
|
||||
|
||||
assert Media.list_cullable_media_items() == [expected_media_item]
|
||||
end
|
||||
|
||||
test "doesn't return items that are set to prevent culling" do
|
||||
source = source_fixture(%{retention_period_days: 2})
|
||||
|
||||
_media_item =
|
||||
media_item_fixture(%{
|
||||
source_id: source.id,
|
||||
media_filepath: "/video/#{Faker.File.file_name(:video)}",
|
||||
media_downloaded_at: now_minus(3, :days),
|
||||
prevent_culling: true
|
||||
})
|
||||
|
||||
expected_media_item =
|
||||
media_item_fixture(%{
|
||||
source_id: source.id,
|
||||
media_filepath: "/video/#{Faker.File.file_name(:video)}",
|
||||
media_downloaded_at: now_minus(3, :days)
|
||||
})
|
||||
|
||||
assert Media.list_cullable_media_items() == [expected_media_item]
|
||||
end
|
||||
end
|
||||
|
||||
describe "list_redownloadable_media_items/0" do
|
||||
describe "list_upgradeable_media_items/0" do
|
||||
setup do
|
||||
media_profile = media_profile_fixture(%{redownload_delay_days: 4})
|
||||
source = source_fixture(%{media_profile_id: media_profile.id, inserted_at: now_minus(10, :days)})
|
||||
@@ -149,7 +57,7 @@ defmodule Pinchflat.MediaTest do
|
||||
media_downloaded_at: now_minus(5, :days)
|
||||
})
|
||||
|
||||
assert Media.list_redownloadable_media_items() == [media_item]
|
||||
assert Media.list_upgradeable_media_items() == [media_item]
|
||||
end
|
||||
|
||||
test "returns media items that were downloaded in past but still meet redownload delay", %{source: source} do
|
||||
@@ -160,7 +68,7 @@ defmodule Pinchflat.MediaTest do
|
||||
media_downloaded_at: now_minus(19, :days)
|
||||
})
|
||||
|
||||
assert Media.list_redownloadable_media_items() == [media_item]
|
||||
assert Media.list_upgradeable_media_items() == [media_item]
|
||||
end
|
||||
|
||||
test "does not return media items without a media_downloaded_at", %{source: source} do
|
||||
@@ -171,7 +79,7 @@ defmodule Pinchflat.MediaTest do
|
||||
media_downloaded_at: nil
|
||||
})
|
||||
|
||||
assert Media.list_redownloadable_media_items() == []
|
||||
assert Media.list_upgradeable_media_items() == []
|
||||
end
|
||||
|
||||
test "does not return media items that are set to prevent download", %{source: source} do
|
||||
@@ -183,7 +91,7 @@ defmodule Pinchflat.MediaTest do
|
||||
prevent_download: true
|
||||
})
|
||||
|
||||
assert Media.list_redownloadable_media_items() == []
|
||||
assert Media.list_upgradeable_media_items() == []
|
||||
end
|
||||
|
||||
test "does not return media items that have been culled", %{source: source} do
|
||||
@@ -195,7 +103,7 @@ defmodule Pinchflat.MediaTest do
|
||||
culled_at: now()
|
||||
})
|
||||
|
||||
assert Media.list_redownloadable_media_items() == []
|
||||
assert Media.list_upgradeable_media_items() == []
|
||||
end
|
||||
|
||||
test "does not return media items before the download delay", %{source: source} do
|
||||
@@ -206,7 +114,7 @@ defmodule Pinchflat.MediaTest do
|
||||
media_downloaded_at: now_minus(3, :days)
|
||||
})
|
||||
|
||||
assert Media.list_redownloadable_media_items() == []
|
||||
assert Media.list_upgradeable_media_items() == []
|
||||
end
|
||||
|
||||
test "does not return media items that have already been redownloaded", %{source: source} do
|
||||
@@ -218,7 +126,7 @@ defmodule Pinchflat.MediaTest do
|
||||
media_redownloaded_at: now()
|
||||
})
|
||||
|
||||
assert Media.list_redownloadable_media_items() == []
|
||||
assert Media.list_upgradeable_media_items() == []
|
||||
end
|
||||
|
||||
test "does not return media items that were first downloaded well after the uploaded_at", %{source: source} do
|
||||
@@ -229,7 +137,7 @@ defmodule Pinchflat.MediaTest do
|
||||
uploaded_at: now_minus(20, :days)
|
||||
})
|
||||
|
||||
assert Media.list_redownloadable_media_items() == []
|
||||
assert Media.list_upgradeable_media_items() == []
|
||||
end
|
||||
|
||||
test "does not return media items that were recently uploaded", %{source: source} do
|
||||
@@ -240,7 +148,7 @@ defmodule Pinchflat.MediaTest do
|
||||
uploaded_at: now_minus(2, :days)
|
||||
})
|
||||
|
||||
assert Media.list_redownloadable_media_items() == []
|
||||
assert Media.list_upgradeable_media_items() == []
|
||||
end
|
||||
|
||||
test "does not return media items without a redownload delay" do
|
||||
@@ -254,7 +162,7 @@ defmodule Pinchflat.MediaTest do
|
||||
media_downloaded_at: now_minus(5, :days)
|
||||
})
|
||||
|
||||
assert Media.list_redownloadable_media_items() == []
|
||||
assert Media.list_upgradeable_media_items() == []
|
||||
end
|
||||
end
|
||||
|
||||
@@ -447,6 +355,48 @@ defmodule Pinchflat.MediaTest do
|
||||
end
|
||||
end
|
||||
|
||||
describe "list_pending_media_items_for/1 when min and max durations" do
|
||||
test "returns media items that meet the min and max duration" do
|
||||
source = source_fixture(%{min_duration_seconds: 10, max_duration_seconds: 20})
|
||||
|
||||
_short_media_item = media_item_fixture(%{source_id: source.id, media_filepath: nil, duration_seconds: 5})
|
||||
normal_media_item = media_item_fixture(%{source_id: source.id, media_filepath: nil, duration_seconds: 15})
|
||||
_long_media_item = media_item_fixture(%{source_id: source.id, media_filepath: nil, duration_seconds: 25})
|
||||
|
||||
assert Media.list_pending_media_items_for(source) == [normal_media_item]
|
||||
end
|
||||
|
||||
test "does not apply a min duration if none is specified" do
|
||||
source = source_fixture(%{min_duration_seconds: nil, max_duration_seconds: 20})
|
||||
|
||||
short_media_item = media_item_fixture(%{source_id: source.id, media_filepath: nil, duration_seconds: 5})
|
||||
normal_media_item = media_item_fixture(%{source_id: source.id, media_filepath: nil, duration_seconds: 15})
|
||||
_long_media_item = media_item_fixture(%{source_id: source.id, media_filepath: nil, duration_seconds: 25})
|
||||
|
||||
assert Media.list_pending_media_items_for(source) == [short_media_item, normal_media_item]
|
||||
end
|
||||
|
||||
test "does not apply a max duration if none is specified" do
|
||||
source = source_fixture(%{min_duration_seconds: 10, max_duration_seconds: nil})
|
||||
|
||||
_short_media_item = media_item_fixture(%{source_id: source.id, media_filepath: nil, duration_seconds: 5})
|
||||
normal_media_item = media_item_fixture(%{source_id: source.id, media_filepath: nil, duration_seconds: 15})
|
||||
long_media_item = media_item_fixture(%{source_id: source.id, media_filepath: nil, duration_seconds: 25})
|
||||
|
||||
assert Media.list_pending_media_items_for(source) == [normal_media_item, long_media_item]
|
||||
end
|
||||
|
||||
test "does not apply a min or max duration if none are specified" do
|
||||
source = source_fixture(%{min_duration_seconds: nil, max_duration_seconds: nil})
|
||||
|
||||
short_media_item = media_item_fixture(%{source_id: source.id, media_filepath: nil, duration_seconds: 5})
|
||||
normal_media_item = media_item_fixture(%{source_id: source.id, media_filepath: nil, duration_seconds: 15})
|
||||
long_media_item = media_item_fixture(%{source_id: source.id, media_filepath: nil, duration_seconds: 25})
|
||||
|
||||
assert Media.list_pending_media_items_for(source) == [short_media_item, normal_media_item, long_media_item]
|
||||
end
|
||||
end
|
||||
|
||||
describe "list_pending_media_items_for/1 when testing download prevention" do
|
||||
test "returns only media items that are not prevented from downloading" do
|
||||
source = source_fixture()
|
||||
@@ -526,6 +476,34 @@ defmodule Pinchflat.MediaTest do
|
||||
assert Media.pending_download?(media_item)
|
||||
end
|
||||
|
||||
test "returns true if the duration is between the min and max" do
|
||||
source = source_fixture(%{min_duration_seconds: 10, max_duration_seconds: 20})
|
||||
media_item = media_item_fixture(%{source_id: source.id, media_filepath: nil, duration_seconds: 15})
|
||||
|
||||
assert Media.pending_download?(media_item)
|
||||
end
|
||||
|
||||
test "returns false if the duration is below the min" do
|
||||
source = source_fixture(%{min_duration_seconds: 10, max_duration_seconds: 20})
|
||||
media_item = media_item_fixture(%{source_id: source.id, media_filepath: nil, duration_seconds: 5})
|
||||
|
||||
refute Media.pending_download?(media_item)
|
||||
end
|
||||
|
||||
test "returns false if the duration is above the max" do
|
||||
source = source_fixture(%{min_duration_seconds: 10, max_duration_seconds: 20})
|
||||
media_item = media_item_fixture(%{source_id: source.id, media_filepath: nil, duration_seconds: 25})
|
||||
|
||||
refute Media.pending_download?(media_item)
|
||||
end
|
||||
|
||||
test "returns true if there is no min or max duration" do
|
||||
source = source_fixture(%{min_duration_seconds: nil, max_duration_seconds: nil})
|
||||
media_item = media_item_fixture(%{source_id: source.id, media_filepath: nil, duration_seconds: 15})
|
||||
|
||||
assert Media.pending_download?(media_item)
|
||||
end
|
||||
|
||||
test "returns true if the media item is not prevented from downloading" do
|
||||
media_item = media_item_fixture(%{media_filepath: nil, prevent_download: false})
|
||||
|
||||
@@ -689,7 +667,24 @@ defmodule Pinchflat.MediaTest do
|
||||
assert {:ok, %MediaItem{} = media_item_2} = Media.create_media_item_from_backend_attrs(source, different_attrs)
|
||||
|
||||
assert media_item_1.id == media_item_2.id
|
||||
assert media_item_2.title == different_attrs.title
|
||||
assert Repo.reload(media_item_2).title == different_attrs.title
|
||||
end
|
||||
|
||||
test "doesn't update fields like playlist_index" do
|
||||
source = source_fixture()
|
||||
|
||||
media_attrs =
|
||||
media_attributes_return_fixture()
|
||||
|> Phoenix.json_library().decode!()
|
||||
|> Map.put("playlist_index", 1)
|
||||
|> YtDlpMedia.response_to_struct()
|
||||
|
||||
different_attrs = %YtDlpMedia{media_attrs | playlist_index: 9999}
|
||||
|
||||
assert {:ok, %MediaItem{} = _media_item_1} = Media.create_media_item_from_backend_attrs(source, media_attrs)
|
||||
assert {:ok, %MediaItem{} = media_item_2} = Media.create_media_item_from_backend_attrs(source, different_attrs)
|
||||
|
||||
assert Repo.reload(media_item_2).playlist_index == media_attrs.playlist_index
|
||||
end
|
||||
end
|
||||
|
||||
@@ -740,8 +735,8 @@ defmodule Pinchflat.MediaTest do
|
||||
end
|
||||
|
||||
test "does delete the media item's metadata files" do
|
||||
stub(YtDlpRunnerMock, :run, fn _url, _opts, _ot -> {:ok, ""} end)
|
||||
media_item = Repo.preload(media_item_with_attachments(), :metadata)
|
||||
stub(YtDlpRunnerMock, :run, fn _url, _opts, _ot, _addl -> {:ok, ""} end)
|
||||
media_item = Repo.preload(media_item_with_attachments(), [:metadata, :source])
|
||||
|
||||
update_attrs = %{
|
||||
metadata: %{
|
||||
@@ -759,7 +754,7 @@ defmodule Pinchflat.MediaTest do
|
||||
|
||||
describe "delete_media_item/2 when testing file deletion" do
|
||||
setup do
|
||||
stub(UserScriptRunnerMock, :run, fn _event_type, _data -> :ok end)
|
||||
stub(UserScriptRunnerMock, :run, fn _event_type, _data -> {:ok, "", 0} end)
|
||||
|
||||
:ok
|
||||
end
|
||||
@@ -772,8 +767,8 @@ defmodule Pinchflat.MediaTest do
|
||||
end
|
||||
|
||||
test "deletes the media item's metadata files" do
|
||||
stub(YtDlpRunnerMock, :run, fn _url, _opts, _ot -> {:ok, ""} end)
|
||||
media_item = Repo.preload(media_item_with_attachments(), :metadata)
|
||||
stub(YtDlpRunnerMock, :run, fn _url, _opts, _ot, _addl -> {:ok, ""} end)
|
||||
media_item = Repo.preload(media_item_with_attachments(), [:metadata, :source])
|
||||
|
||||
update_attrs = %{
|
||||
metadata: %{
|
||||
@@ -820,7 +815,7 @@ defmodule Pinchflat.MediaTest do
|
||||
expect(UserScriptRunnerMock, :run, fn :media_deleted, data ->
|
||||
assert data.id == media_item.id
|
||||
|
||||
:ok
|
||||
{:ok, "", 0}
|
||||
end)
|
||||
|
||||
assert {:ok, _} = Media.delete_media_item(media_item, delete_files: true)
|
||||
@@ -829,7 +824,7 @@ defmodule Pinchflat.MediaTest do
|
||||
|
||||
describe "delete_media_files/2" do
|
||||
setup do
|
||||
stub(UserScriptRunnerMock, :run, fn _event_type, _data -> :ok end)
|
||||
stub(UserScriptRunnerMock, :run, fn _event_type, _data -> {:ok, "", 0} end)
|
||||
|
||||
:ok
|
||||
end
|
||||
@@ -858,8 +853,8 @@ defmodule Pinchflat.MediaTest do
|
||||
end
|
||||
|
||||
test "does not delete the media item's metadata files" do
|
||||
stub(YtDlpRunnerMock, :run, fn _url, _opts, _ot -> {:ok, ""} end)
|
||||
media_item = Repo.preload(media_item_with_attachments(), :metadata)
|
||||
stub(YtDlpRunnerMock, :run, fn _url, _opts, _ot, _addl -> {:ok, ""} end)
|
||||
media_item = Repo.preload(media_item_with_attachments(), [:metadata, :source])
|
||||
|
||||
update_attrs = %{
|
||||
metadata: %{
|
||||
@@ -892,7 +887,7 @@ defmodule Pinchflat.MediaTest do
|
||||
expect(UserScriptRunnerMock, :run, fn :media_deleted, data ->
|
||||
assert data.id == media_item.id
|
||||
|
||||
:ok
|
||||
{:ok, "", 0}
|
||||
end)
|
||||
|
||||
assert {:ok, _} = Media.delete_media_files(media_item)
|
||||
|
||||
@@ -2,15 +2,26 @@ defmodule Pinchflat.Metadata.MetadataFileHelpersTest do
|
||||
use Pinchflat.DataCase
|
||||
|
||||
import Pinchflat.MediaFixtures
|
||||
import Pinchflat.SourcesFixtures
|
||||
|
||||
alias Pinchflat.Metadata.MetadataFileHelpers, as: Helpers
|
||||
|
||||
setup do
|
||||
media_item = media_item_fixture()
|
||||
media_item = Repo.preload(media_item_fixture(), :source)
|
||||
|
||||
{:ok, %{media_item: media_item}}
|
||||
end
|
||||
|
||||
describe "metadata_directory_for/1" do
|
||||
test "returns the metadata directory for the given record", %{media_item: media_item} do
|
||||
base_metadata_directory = Application.get_env(:pinchflat, :metadata_directory)
|
||||
|
||||
metadata_directory = Helpers.metadata_directory_for(media_item)
|
||||
|
||||
assert metadata_directory == Path.join([base_metadata_directory, "media_items", "#{media_item.id}"])
|
||||
end
|
||||
end
|
||||
|
||||
describe "compress_and_store_metadata_for/2" do
|
||||
test "returns the filepath", %{media_item: media_item} do
|
||||
metadata_map = %{"foo" => "bar"}
|
||||
@@ -51,7 +62,7 @@ defmodule Pinchflat.Metadata.MetadataFileHelpersTest do
|
||||
|
||||
describe "download_and_store_thumbnail_for/2" do
|
||||
test "returns the filepath", %{media_item: media_item} do
|
||||
stub(YtDlpRunnerMock, :run, fn _url, _opts, _ot -> {:ok, ""} end)
|
||||
stub(YtDlpRunnerMock, :run, fn _url, _opts, _ot, _addl -> {:ok, ""} end)
|
||||
|
||||
filepath = Helpers.download_and_store_thumbnail_for(media_item)
|
||||
|
||||
@@ -59,7 +70,7 @@ defmodule Pinchflat.Metadata.MetadataFileHelpersTest do
|
||||
end
|
||||
|
||||
test "calls yt-dlp with the expected options", %{media_item: media_item} do
|
||||
expect(YtDlpRunnerMock, :run, fn url, opts, ot ->
|
||||
expect(YtDlpRunnerMock, :run, fn url, opts, ot, _addl ->
|
||||
assert url == media_item.original_url
|
||||
assert ot == "after_move:%()j"
|
||||
|
||||
@@ -77,8 +88,32 @@ defmodule Pinchflat.Metadata.MetadataFileHelpersTest do
|
||||
Helpers.download_and_store_thumbnail_for(media_item)
|
||||
end
|
||||
|
||||
test "sets use_cookies if the source uses cookies" do
|
||||
expect(YtDlpRunnerMock, :run, fn _url, _opts, _ot, addl ->
|
||||
assert {:use_cookies, true} in addl
|
||||
{:ok, ""}
|
||||
end)
|
||||
|
||||
source = source_fixture(%{use_cookies: true})
|
||||
media_item = Repo.preload(media_item_fixture(%{source_id: source.id}), :source)
|
||||
|
||||
Helpers.download_and_store_thumbnail_for(media_item)
|
||||
end
|
||||
|
||||
test "does not set use_cookies if the source does not use cookies" do
|
||||
expect(YtDlpRunnerMock, :run, fn _url, _opts, _ot, addl ->
|
||||
assert {:use_cookies, false} in addl
|
||||
{:ok, ""}
|
||||
end)
|
||||
|
||||
source = source_fixture(%{use_cookies: false})
|
||||
media_item = Repo.preload(media_item_fixture(%{source_id: source.id}), :source)
|
||||
|
||||
Helpers.download_and_store_thumbnail_for(media_item)
|
||||
end
|
||||
|
||||
test "returns nil if yt-dlp fails", %{media_item: media_item} do
|
||||
stub(YtDlpRunnerMock, :run, fn _url, _opts, _ot -> {:error, "error"} end)
|
||||
stub(YtDlpRunnerMock, :run, fn _url, _opts, _ot, _addl -> {:error, "error"} end)
|
||||
|
||||
filepath = Helpers.download_and_store_thumbnail_for(media_item)
|
||||
|
||||
@@ -142,13 +177,20 @@ defmodule Pinchflat.Metadata.MetadataFileHelpersTest do
|
||||
end
|
||||
end
|
||||
|
||||
describe "metadata_directory_for/1" do
|
||||
test "returns the metadata directory for the given record", %{media_item: media_item} do
|
||||
base_metadata_directory = Application.get_env(:pinchflat, :metadata_directory)
|
||||
describe "season_and_episode_from_media_filepath/1" do
|
||||
test "returns a season and episode if one can be determined" do
|
||||
assert {:ok, {"1", "2"}} = Helpers.season_and_episode_from_media_filepath("/foo/s1e2 - test.mp4")
|
||||
assert {:ok, {"1", "2"}} = Helpers.season_and_episode_from_media_filepath("/foo/S1E2 - test.mp4")
|
||||
assert {:ok, {"001", "002"}} = Helpers.season_and_episode_from_media_filepath("/foo/s001e002 - test.mp4")
|
||||
assert {:ok, {"1", "2"}} = Helpers.season_and_episode_from_media_filepath("/foo/s1e2bar - test.mp4")
|
||||
assert {:ok, {"1", "2"}} = Helpers.season_and_episode_from_media_filepath("/foo/bar s1e2 - test.mp4")
|
||||
end
|
||||
|
||||
metadata_directory = Helpers.metadata_directory_for(media_item)
|
||||
|
||||
assert metadata_directory == Path.join([base_metadata_directory, "media_items", "#{media_item.id}"])
|
||||
test "returns an error if a season and episode can't be determined" do
|
||||
assert {:error, :indeterminable} = Helpers.season_and_episode_from_media_filepath("/foo/test.mp4")
|
||||
assert {:error, :indeterminable} = Helpers.season_and_episode_from_media_filepath("/foo/s1 - test.mp4")
|
||||
assert {:error, :indeterminable} = Helpers.season_and_episode_from_media_filepath("/foo/s1e - test.mp4")
|
||||
assert {:error, :indeterminable} = Helpers.season_and_episode_from_media_filepath("/foo/s1etest.mp4")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -43,11 +43,12 @@ defmodule Pinchflat.Metadata.MetadataParserTest do
|
||||
test "it extracts the livestream flag", %{metadata: metadata} do
|
||||
result = Parser.parse_for_media_item(metadata)
|
||||
|
||||
assert result.livestream == metadata["was_live"]
|
||||
assert metadata["live_status"] == "not_live"
|
||||
refute result.livestream
|
||||
end
|
||||
|
||||
test "the livestream flag defaults to false", %{metadata: metadata} do
|
||||
metadata = Map.put(metadata, "was_live", nil)
|
||||
metadata = Map.put(metadata, "live_status", nil)
|
||||
|
||||
result = Parser.parse_for_media_item(metadata)
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ defmodule Pinchflat.Metadata.NfoBuilderTest do
|
||||
alias Pinchflat.Utils.FilesystemUtils
|
||||
|
||||
setup do
|
||||
filepath = FilesystemUtils.generate_metadata_tmpfile(:json)
|
||||
filepath = FilesystemUtils.generate_metadata_tmpfile(:nfo)
|
||||
|
||||
on_exit(fn -> File.rm!(filepath) end)
|
||||
|
||||
@@ -45,6 +45,43 @@ defmodule Pinchflat.Metadata.NfoBuilderTest do
|
||||
|
||||
assert String.contains?(nfo, "hello' & <world>")
|
||||
end
|
||||
|
||||
test "uses the season and episode number from the filepath if it can be determined" do
|
||||
metadata = %{
|
||||
"title" => "title",
|
||||
"uploader" => "uploader",
|
||||
"id" => "id",
|
||||
"description" => "description",
|
||||
"upload_date" => "20210101"
|
||||
}
|
||||
|
||||
tmpfile_directory = Application.get_env(:pinchflat, :tmpfile_directory)
|
||||
filepath = Path.join([tmpfile_directory, "foo/s0123e456.nfo"])
|
||||
|
||||
result = NfoBuilder.build_and_store_for_media_item(filepath, metadata)
|
||||
nfo = File.read!(result)
|
||||
|
||||
assert String.contains?(nfo, "<season>0123</season>")
|
||||
assert String.contains?(nfo, "<episode>456</episode>")
|
||||
|
||||
File.rm!(filepath)
|
||||
end
|
||||
|
||||
test "uses the upload date if the season and episode number can't be determined", %{filepath: filepath} do
|
||||
metadata = %{
|
||||
"title" => "title",
|
||||
"uploader" => "uploader",
|
||||
"id" => "id",
|
||||
"description" => "description",
|
||||
"upload_date" => "20210101"
|
||||
}
|
||||
|
||||
result = NfoBuilder.build_and_store_for_media_item(filepath, metadata)
|
||||
nfo = File.read!(result)
|
||||
|
||||
assert String.contains?(nfo, "<season>2021</season>")
|
||||
assert String.contains?(nfo, "<episode>0101</episode>")
|
||||
end
|
||||
end
|
||||
|
||||
describe "build_and_store_for_source/2" do
|
||||
|
||||
@@ -75,4 +75,51 @@ defmodule Pinchflat.Metadata.SourceImageParserTest do
|
||||
assert SourceImageParser.store_source_images(@base_dir, metadata) == %{}
|
||||
end
|
||||
end
|
||||
|
||||
describe "store_source_images/2 when testing fallbacks" do
|
||||
test "uses the entries list for a fallback poster if needed" do
|
||||
metadata = %{
|
||||
"thumbnails" => [],
|
||||
"entries" => [
|
||||
%{
|
||||
"thumbnails" => [%{"filepath" => "/app/test/support/files/channel_photos/a.0.jpg"}]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
expected = %{
|
||||
poster_filepath: "#{@base_dir}/poster.jpg"
|
||||
}
|
||||
|
||||
assert SourceImageParser.store_source_images(@base_dir, metadata) == expected
|
||||
end
|
||||
|
||||
test "doesn't blow up if the entries list doesn't have any suitable thumbnails" do
|
||||
metadata = %{
|
||||
"thumbnails" => [],
|
||||
"entries" => [
|
||||
%{"thumbnails" => [%{"id" => "1"}]}
|
||||
]
|
||||
}
|
||||
|
||||
assert SourceImageParser.store_source_images(@base_dir, metadata) == %{}
|
||||
end
|
||||
|
||||
test "doesn't use the entries list if it's empty" do
|
||||
metadata = %{
|
||||
"thumbnails" => [],
|
||||
"entries" => []
|
||||
}
|
||||
|
||||
assert SourceImageParser.store_source_images(@base_dir, metadata) == %{}
|
||||
end
|
||||
|
||||
test "doesn't use the entries list if it's not present" do
|
||||
metadata = %{
|
||||
"thumbnails" => []
|
||||
}
|
||||
|
||||
assert SourceImageParser.store_source_images(@base_dir, metadata) == %{}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -32,8 +32,8 @@ defmodule Pinchflat.Metadata.SourceMetadataStorageWorkerTest do
|
||||
describe "perform/1" do
|
||||
test "won't call itself in an infinite loop" do
|
||||
stub(YtDlpRunnerMock, :run, fn
|
||||
_url, _opts, ot when ot == @source_details_ot -> {:ok, source_details_return_fixture()}
|
||||
_url, _opts, ot when ot == @metadata_ot -> {:ok, "{}"}
|
||||
_url, _opts, ot, _addl when ot == @source_details_ot -> {:ok, source_details_return_fixture()}
|
||||
_url, _opts, ot, _addl when ot == @metadata_ot -> {:ok, "{}"}
|
||||
end)
|
||||
|
||||
source = source_fixture()
|
||||
@@ -51,8 +51,8 @@ defmodule Pinchflat.Metadata.SourceMetadataStorageWorkerTest do
|
||||
describe "perform/1 when testing attribute updates" do
|
||||
test "the source description is saved" do
|
||||
stub(YtDlpRunnerMock, :run, fn
|
||||
_url, _opts, ot when ot == @source_details_ot -> {:ok, source_details_return_fixture()}
|
||||
_url, _opts, ot when ot == @metadata_ot -> {:ok, render_metadata(:channel_source_metadata)}
|
||||
_url, _opts, ot, _addl when ot == @source_details_ot -> {:ok, source_details_return_fixture()}
|
||||
_url, _opts, ot, _addl when ot == @metadata_ot -> {:ok, render_metadata(:channel_source_metadata)}
|
||||
end)
|
||||
|
||||
source = source_fixture(%{description: nil})
|
||||
@@ -68,8 +68,8 @@ defmodule Pinchflat.Metadata.SourceMetadataStorageWorkerTest do
|
||||
describe "perform/1 when testing metadata storage" do
|
||||
test "sets metadata location for source" do
|
||||
stub(YtDlpRunnerMock, :run, fn
|
||||
_url, _opts, ot when ot == @source_details_ot -> {:ok, source_details_return_fixture()}
|
||||
_url, _opts, ot when ot == @metadata_ot -> {:ok, "{}"}
|
||||
_url, _opts, ot, _addl when ot == @source_details_ot -> {:ok, source_details_return_fixture()}
|
||||
_url, _opts, ot, _addl when ot == @metadata_ot -> {:ok, "{}"}
|
||||
end)
|
||||
|
||||
source = Repo.preload(source_fixture(), :metadata)
|
||||
@@ -87,8 +87,8 @@ defmodule Pinchflat.Metadata.SourceMetadataStorageWorkerTest do
|
||||
file_contents = Phoenix.json_library().encode!(%{"title" => "test"})
|
||||
|
||||
stub(YtDlpRunnerMock, :run, fn
|
||||
_url, _opts, ot when ot == @source_details_ot -> {:ok, source_details_return_fixture()}
|
||||
_url, _opts, ot when ot == @metadata_ot -> {:ok, file_contents}
|
||||
_url, _opts, ot, _addl when ot == @source_details_ot -> {:ok, source_details_return_fixture()}
|
||||
_url, _opts, ot, _addl when ot == @metadata_ot -> {:ok, file_contents}
|
||||
end)
|
||||
|
||||
perform_job(SourceMetadataStorageWorker, %{id: source.id})
|
||||
@@ -100,8 +100,8 @@ defmodule Pinchflat.Metadata.SourceMetadataStorageWorkerTest do
|
||||
|
||||
test "sets metadata image location for source" do
|
||||
stub(YtDlpRunnerMock, :run, fn
|
||||
_url, _opts, ot when ot == @source_details_ot -> {:ok, source_details_return_fixture()}
|
||||
_url, _opts, ot when ot == @metadata_ot -> {:ok, render_metadata(:channel_source_metadata)}
|
||||
_url, _opts, ot, _addl when ot == @source_details_ot -> {:ok, source_details_return_fixture()}
|
||||
_url, _opts, ot, _addl when ot == @metadata_ot -> {:ok, render_metadata(:channel_source_metadata)}
|
||||
end)
|
||||
|
||||
source = source_fixture()
|
||||
@@ -118,8 +118,8 @@ defmodule Pinchflat.Metadata.SourceMetadataStorageWorkerTest do
|
||||
|
||||
test "stores metadata images for source" do
|
||||
stub(YtDlpRunnerMock, :run, fn
|
||||
_url, _opts, ot when ot == @source_details_ot -> {:ok, source_details_return_fixture()}
|
||||
_url, _opts, ot when ot == @metadata_ot -> {:ok, render_metadata(:channel_source_metadata)}
|
||||
_url, _opts, ot, _addl when ot == @source_details_ot -> {:ok, source_details_return_fixture()}
|
||||
_url, _opts, ot, _addl when ot == @metadata_ot -> {:ok, render_metadata(:channel_source_metadata)}
|
||||
end)
|
||||
|
||||
source = source_fixture()
|
||||
@@ -138,12 +138,14 @@ defmodule Pinchflat.Metadata.SourceMetadataStorageWorkerTest do
|
||||
describe "perform/1 when testing source image downloading" do
|
||||
test "downloads and stores source images" do
|
||||
stub(YtDlpRunnerMock, :run, fn
|
||||
_url, _opts, ot when ot == @source_details_ot ->
|
||||
_url, _opts, ot, _addl when ot == @source_details_ot ->
|
||||
filename = Path.join([Application.get_env(:pinchflat, :media_directory), "Season 1", "bar.mp4"])
|
||||
|
||||
{:ok, source_details_return_fixture(%{filename: filename})}
|
||||
|
||||
_url, _opts, ot when ot == @metadata_ot ->
|
||||
_url, opts, ot, _addl when ot == @metadata_ot ->
|
||||
assert {:convert_thumbnails, "jpg"} in opts
|
||||
|
||||
{:ok, render_metadata(:channel_source_metadata)}
|
||||
end)
|
||||
|
||||
@@ -164,14 +166,50 @@ defmodule Pinchflat.Metadata.SourceMetadataStorageWorkerTest do
|
||||
Sources.delete_source(source, delete_files: true)
|
||||
end
|
||||
|
||||
test "calls one set of yt-dlp metadata opts for channels" do
|
||||
stub(YtDlpRunnerMock, :run, fn
|
||||
_url, _opts, ot, _addl when ot == @source_details_ot ->
|
||||
{:ok, source_details_return_fixture()}
|
||||
|
||||
_url, opts, ot, _addl when ot == @metadata_ot ->
|
||||
assert {:playlist_items, 0} in opts
|
||||
assert :write_all_thumbnails in opts
|
||||
|
||||
{:ok, render_metadata(:channel_source_metadata)}
|
||||
end)
|
||||
|
||||
profile = media_profile_fixture(%{download_source_images: true})
|
||||
source = source_fixture(media_profile_id: profile.id, collection_type: :channel)
|
||||
|
||||
perform_job(SourceMetadataStorageWorker, %{id: source.id})
|
||||
end
|
||||
|
||||
test "calls another set of yt-dlp metadata opts for playlists" do
|
||||
stub(YtDlpRunnerMock, :run, fn
|
||||
_url, _opts, ot, _addl when ot == @source_details_ot ->
|
||||
{:ok, source_details_return_fixture()}
|
||||
|
||||
_url, opts, ot, _addl when ot == @metadata_ot ->
|
||||
assert {:playlist_items, 1} in opts
|
||||
assert :write_thumbnail in opts
|
||||
|
||||
{:ok, render_metadata(:channel_source_metadata)}
|
||||
end)
|
||||
|
||||
profile = media_profile_fixture(%{download_source_images: true})
|
||||
source = source_fixture(media_profile_id: profile.id, collection_type: :playlist)
|
||||
|
||||
perform_job(SourceMetadataStorageWorker, %{id: source.id})
|
||||
end
|
||||
|
||||
test "does not store source images if the profile is not set to" do
|
||||
stub(YtDlpRunnerMock, :run, fn
|
||||
_url, _opts, ot when ot == @source_details_ot ->
|
||||
_url, _opts, ot, _addl when ot == @source_details_ot ->
|
||||
filename = Path.join([Application.get_env(:pinchflat, :media_directory), "Season 1", "bar.mp4"])
|
||||
|
||||
{:ok, source_details_return_fixture(%{filename: filename})}
|
||||
|
||||
_url, _opts, ot when ot == @metadata_ot ->
|
||||
_url, _opts, ot, _addl when ot == @metadata_ot ->
|
||||
{:ok, render_metadata(:channel_source_metadata)}
|
||||
end)
|
||||
|
||||
@@ -188,12 +226,12 @@ defmodule Pinchflat.Metadata.SourceMetadataStorageWorkerTest do
|
||||
|
||||
test "does not store source images if the series directory cannot be determined" do
|
||||
stub(YtDlpRunnerMock, :run, fn
|
||||
_url, _opts, ot when ot == @source_details_ot ->
|
||||
_url, _opts, ot, _addl when ot == @source_details_ot ->
|
||||
filename = Path.join([Application.get_env(:pinchflat, :media_directory), "foo", "bar.mp4"])
|
||||
|
||||
{:ok, source_details_return_fixture(%{filename: filename})}
|
||||
|
||||
_url, _opts, ot when ot == @metadata_ot ->
|
||||
_url, _opts, ot, _addl when ot == @metadata_ot ->
|
||||
{:ok, render_metadata(:channel_source_metadata)}
|
||||
end)
|
||||
|
||||
@@ -207,17 +245,49 @@ defmodule Pinchflat.Metadata.SourceMetadataStorageWorkerTest do
|
||||
refute source.poster_filepath
|
||||
refute source.banner_filepath
|
||||
end
|
||||
|
||||
test "sets use_cookies if the source uses cookies" do
|
||||
expect(YtDlpRunnerMock, :run, 2, fn
|
||||
_url, _opts, ot, _addl when ot == @source_details_ot ->
|
||||
{:ok, source_details_return_fixture()}
|
||||
|
||||
_url, _opts, ot, addl when ot == @metadata_ot ->
|
||||
assert {:use_cookies, true} in addl
|
||||
{:ok, render_metadata(:channel_source_metadata)}
|
||||
end)
|
||||
|
||||
profile = media_profile_fixture(%{download_source_images: true})
|
||||
source = source_fixture(media_profile_id: profile.id, use_cookies: true)
|
||||
|
||||
perform_job(SourceMetadataStorageWorker, %{id: source.id})
|
||||
end
|
||||
|
||||
test "does not set use_cookies if the source does not use cookies" do
|
||||
expect(YtDlpRunnerMock, :run, 2, fn
|
||||
_url, _opts, ot, _addl when ot == @source_details_ot ->
|
||||
{:ok, source_details_return_fixture()}
|
||||
|
||||
_url, _opts, ot, addl when ot == @metadata_ot ->
|
||||
assert {:use_cookies, false} in addl
|
||||
{:ok, render_metadata(:channel_source_metadata)}
|
||||
end)
|
||||
|
||||
profile = media_profile_fixture(%{download_source_images: true})
|
||||
source = source_fixture(media_profile_id: profile.id, use_cookies: false)
|
||||
|
||||
perform_job(SourceMetadataStorageWorker, %{id: source.id})
|
||||
end
|
||||
end
|
||||
|
||||
describe "perform/1 when determining the series_directory" do
|
||||
test "sets the series directory based on the returned media filepath" do
|
||||
stub(YtDlpRunnerMock, :run, fn
|
||||
_url, _opts, ot when ot == @source_details_ot ->
|
||||
_url, _opts, ot, _addl when ot == @source_details_ot ->
|
||||
filename = Path.join([Application.get_env(:pinchflat, :media_directory), "Season 1", "bar.mp4"])
|
||||
|
||||
{:ok, source_details_return_fixture(%{filename: filename})}
|
||||
|
||||
_url, _opts, ot when ot == @metadata_ot ->
|
||||
_url, _opts, ot, _addl when ot == @metadata_ot ->
|
||||
{:ok, "{}"}
|
||||
end)
|
||||
|
||||
@@ -230,12 +300,12 @@ defmodule Pinchflat.Metadata.SourceMetadataStorageWorkerTest do
|
||||
|
||||
test "does not set the series directory if it cannot be determined" do
|
||||
stub(YtDlpRunnerMock, :run, fn
|
||||
_url, _opts, ot when ot == @source_details_ot ->
|
||||
_url, _opts, ot, _addl when ot == @source_details_ot ->
|
||||
filename = Path.join([Application.get_env(:pinchflat, :media_directory), "foo", "bar.mp4"])
|
||||
|
||||
{:ok, source_details_return_fixture(%{filename: filename})}
|
||||
|
||||
_url, _opts, ot when ot == @metadata_ot ->
|
||||
_url, _opts, ot, _addl when ot == @metadata_ot ->
|
||||
{:ok, "{}"}
|
||||
end)
|
||||
|
||||
@@ -245,17 +315,45 @@ defmodule Pinchflat.Metadata.SourceMetadataStorageWorkerTest do
|
||||
|
||||
refute source.series_directory
|
||||
end
|
||||
|
||||
test "sets use_cookies if the source is set to use cookies" do
|
||||
expect(YtDlpRunnerMock, :run, 2, fn
|
||||
_url, _opts, ot, addl when ot == @source_details_ot ->
|
||||
assert {:use_cookies, true} in addl
|
||||
{:ok, source_details_return_fixture()}
|
||||
|
||||
_url, _opts, ot, _addl when ot == @metadata_ot ->
|
||||
{:ok, "{}"}
|
||||
end)
|
||||
|
||||
source = source_fixture(%{series_directory: nil, use_cookies: true})
|
||||
perform_job(SourceMetadataStorageWorker, %{id: source.id})
|
||||
end
|
||||
|
||||
test "does not set use_cookies if the source is not set to use cookies" do
|
||||
expect(YtDlpRunnerMock, :run, 2, fn
|
||||
_url, _opts, ot, addl when ot == @source_details_ot ->
|
||||
assert {:use_cookies, false} in addl
|
||||
{:ok, source_details_return_fixture()}
|
||||
|
||||
_url, _opts, ot, _addl when ot == @metadata_ot ->
|
||||
{:ok, "{}"}
|
||||
end)
|
||||
|
||||
source = source_fixture(%{series_directory: nil, use_cookies: false})
|
||||
perform_job(SourceMetadataStorageWorker, %{id: source.id})
|
||||
end
|
||||
end
|
||||
|
||||
describe "perform/1 when storing the series NFO" do
|
||||
test "stores the NFO if specified" do
|
||||
stub(YtDlpRunnerMock, :run, fn
|
||||
_url, _opts, ot when ot == @source_details_ot ->
|
||||
_url, _opts, ot, _addl when ot == @source_details_ot ->
|
||||
filename = Path.join([Application.get_env(:pinchflat, :media_directory), "Season 1", "bar.mp4"])
|
||||
|
||||
{:ok, source_details_return_fixture(%{filename: filename})}
|
||||
|
||||
_url, _opts, ot when ot == @metadata_ot ->
|
||||
_url, _opts, ot, _addl when ot == @metadata_ot ->
|
||||
{:ok, "{}"}
|
||||
end)
|
||||
|
||||
@@ -273,12 +371,12 @@ defmodule Pinchflat.Metadata.SourceMetadataStorageWorkerTest do
|
||||
|
||||
test "does not store the NFO if not specified" do
|
||||
stub(YtDlpRunnerMock, :run, fn
|
||||
_url, _opts, ot when ot == @source_details_ot ->
|
||||
_url, _opts, ot, _addl when ot == @source_details_ot ->
|
||||
filename = Path.join([Application.get_env(:pinchflat, :media_directory), "Season 1", "bar.mp4"])
|
||||
|
||||
{:ok, source_details_return_fixture(%{filename: filename})}
|
||||
|
||||
_url, _opts, ot when ot == @metadata_ot ->
|
||||
_url, _opts, ot, _addl when ot == @metadata_ot ->
|
||||
{:ok, "{}"}
|
||||
end)
|
||||
|
||||
@@ -292,12 +390,12 @@ defmodule Pinchflat.Metadata.SourceMetadataStorageWorkerTest do
|
||||
|
||||
test "does not store the NFO if the series directory cannot be determined" do
|
||||
stub(YtDlpRunnerMock, :run, fn
|
||||
_url, _opts, ot when ot == @source_details_ot ->
|
||||
_url, _opts, ot, _addl when ot == @source_details_ot ->
|
||||
filename = Path.join([Application.get_env(:pinchflat, :media_directory), "foo", "bar.mp4"])
|
||||
|
||||
{:ok, source_details_return_fixture(%{filename: filename})}
|
||||
|
||||
_url, _opts, ot when ot == @metadata_ot ->
|
||||
_url, _opts, ot, _addl when ot == @metadata_ot ->
|
||||
{:ok, "{}"}
|
||||
end)
|
||||
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
defmodule Pinchflat.Profiles.MediaProfileDeletionWorkerTest do
|
||||
use Pinchflat.DataCase
|
||||
|
||||
import Pinchflat.MediaFixtures
|
||||
import Pinchflat.SourcesFixtures
|
||||
import Pinchflat.ProfilesFixtures
|
||||
|
||||
alias Pinchflat.Profiles.MediaProfileDeletionWorker
|
||||
|
||||
setup do
|
||||
stub(UserScriptRunnerMock, :run, fn _event_type, _data -> :ok end)
|
||||
|
||||
{:ok, %{profile: media_profile_fixture()}}
|
||||
end
|
||||
|
||||
describe "kickoff/3" do
|
||||
test "starts the worker", %{profile: profile} do
|
||||
assert [] = all_enqueued(worker: MediaProfileDeletionWorker)
|
||||
assert {:ok, _} = MediaProfileDeletionWorker.kickoff(profile)
|
||||
assert [_] = all_enqueued(worker: MediaProfileDeletionWorker)
|
||||
end
|
||||
|
||||
test "can be called with additional job arguments", %{profile: profile} do
|
||||
job_args = %{"delete_files" => true}
|
||||
|
||||
assert {:ok, _} = MediaProfileDeletionWorker.kickoff(profile, job_args)
|
||||
|
||||
assert_enqueued(worker: MediaProfileDeletionWorker, args: %{"id" => profile.id, "delete_files" => true})
|
||||
end
|
||||
end
|
||||
|
||||
describe "perform/1" do
|
||||
test "deletes the profile, sources, and media but leaves the files", %{profile: profile} do
|
||||
source = source_fixture(%{media_profile_id: profile.id})
|
||||
media_item = media_item_with_attachments(%{source_id: source.id})
|
||||
|
||||
perform_job(MediaProfileDeletionWorker, %{"id" => profile.id})
|
||||
|
||||
assert_raise Ecto.NoResultsError, fn -> Repo.reload!(profile) end
|
||||
assert_raise Ecto.NoResultsError, fn -> Repo.reload!(source) end
|
||||
assert_raise Ecto.NoResultsError, fn -> Repo.reload!(media_item) end
|
||||
assert File.exists?(media_item.media_filepath)
|
||||
end
|
||||
|
||||
test "deletes the profile, sources, and media and files if specified", %{profile: profile} do
|
||||
source = source_fixture(%{media_profile_id: profile.id})
|
||||
media_item = media_item_with_attachments(%{source_id: source.id})
|
||||
|
||||
perform_job(MediaProfileDeletionWorker, %{"id" => profile.id, "delete_files" => true})
|
||||
|
||||
assert_raise Ecto.NoResultsError, fn -> Repo.reload!(profile) end
|
||||
assert_raise Ecto.NoResultsError, fn -> Repo.reload!(source) end
|
||||
assert_raise Ecto.NoResultsError, fn -> Repo.reload!(media_item) end
|
||||
refute File.exists?(media_item.media_filepath)
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -113,7 +113,7 @@ defmodule Pinchflat.ProfilesTest do
|
||||
|
||||
describe "delete_media_profile/2 when deleting files" do
|
||||
setup do
|
||||
stub(UserScriptRunnerMock, :run, fn _event_type, _data -> :ok end)
|
||||
stub(UserScriptRunnerMock, :run, fn _event_type, _data -> {:ok, "", 0} end)
|
||||
|
||||
:ok
|
||||
end
|
||||
|
||||
@@ -15,7 +15,7 @@ defmodule Pinchflat.SlowIndexing.SlowIndexingHelpersTest do
|
||||
alias Pinchflat.SlowIndexing.MediaCollectionIndexingWorker
|
||||
|
||||
describe "kickoff_indexing_task/3" do
|
||||
test "it schedules a job" do
|
||||
test "schedules a job" do
|
||||
source = source_fixture(index_frequency_minutes: 1)
|
||||
|
||||
assert {:ok, _} = SlowIndexingHelpers.kickoff_indexing_task(source)
|
||||
@@ -23,7 +23,7 @@ defmodule Pinchflat.SlowIndexing.SlowIndexingHelpersTest do
|
||||
assert_enqueued(worker: MediaCollectionIndexingWorker, args: %{"id" => source.id})
|
||||
end
|
||||
|
||||
test "it creates and attaches a task" do
|
||||
test "creates and attaches a task" do
|
||||
source = source_fixture(index_frequency_minutes: 1)
|
||||
|
||||
assert {:ok, %Task{} = task} = SlowIndexingHelpers.kickoff_indexing_task(source)
|
||||
@@ -31,7 +31,7 @@ defmodule Pinchflat.SlowIndexing.SlowIndexingHelpersTest do
|
||||
assert task.source_id == source.id
|
||||
end
|
||||
|
||||
test "it deletes any pending media collection tasks for the source" do
|
||||
test "deletes any pending media collection tasks for the source" do
|
||||
source = source_fixture()
|
||||
{:ok, job} = Oban.insert(MediaCollectionIndexingWorker.new(%{"id" => source.id}))
|
||||
task = task_fixture(source_id: source.id, job_id: job.id)
|
||||
@@ -41,7 +41,18 @@ defmodule Pinchflat.SlowIndexing.SlowIndexingHelpersTest do
|
||||
assert_raise Ecto.NoResultsError, fn -> Repo.reload!(task) end
|
||||
end
|
||||
|
||||
test "it deletes any pending media tasks for the source" do
|
||||
test "deletes any executing media collection tasks for the source" do
|
||||
source = source_fixture()
|
||||
{:ok, job} = Oban.insert(MediaCollectionIndexingWorker.new(%{"id" => source.id}))
|
||||
task = task_fixture(source_id: source.id, job_id: job.id)
|
||||
Repo.update_all(from(Oban.Job, where: [id: ^task.job_id], update: [set: [state: "executing"]]), [])
|
||||
|
||||
assert {:ok, _} = SlowIndexingHelpers.kickoff_indexing_task(source)
|
||||
|
||||
assert_raise Ecto.NoResultsError, fn -> Repo.reload!(task) end
|
||||
end
|
||||
|
||||
test "deletes any pending media tasks for the source" do
|
||||
source = source_fixture()
|
||||
{:ok, job} = Oban.insert(FastIndexingWorker.new(%{"id" => source.id}))
|
||||
task = task_fixture(source_id: source.id, job_id: job.id)
|
||||
@@ -51,7 +62,7 @@ defmodule Pinchflat.SlowIndexing.SlowIndexingHelpersTest do
|
||||
assert_raise Ecto.NoResultsError, fn -> Repo.reload!(task) end
|
||||
end
|
||||
|
||||
test "it deletes any fast indexing tasks for the source" do
|
||||
test "deletes any fast indexing tasks for the source" do
|
||||
source = source_fixture()
|
||||
{:ok, job} = Oban.insert(FastIndexingWorker.new(%{"id" => source.id}))
|
||||
task = task_fixture(source_id: source.id, job_id: job.id)
|
||||
@@ -90,7 +101,7 @@ defmodule Pinchflat.SlowIndexing.SlowIndexingHelpersTest do
|
||||
{:ok, [source: source_fixture()]}
|
||||
end
|
||||
|
||||
test "it creates a media_item record for each media ID returned", %{source: source} do
|
||||
test "creates a media_item record for each media ID returned", %{source: source} do
|
||||
assert media_items = SlowIndexingHelpers.index_and_enqueue_download_for_media_items(source)
|
||||
|
||||
assert Enum.count(media_items) == 3
|
||||
@@ -101,7 +112,7 @@ defmodule Pinchflat.SlowIndexing.SlowIndexingHelpersTest do
|
||||
assert Enum.all?(media_items, fn %MediaItem{} -> true end)
|
||||
end
|
||||
|
||||
test "it attaches all media_items to the given source", %{source: source} do
|
||||
test "attaches all media_items to the given source", %{source: source} do
|
||||
source_id = source.id
|
||||
assert media_items = SlowIndexingHelpers.index_and_enqueue_download_for_media_items(source)
|
||||
|
||||
@@ -109,7 +120,7 @@ defmodule Pinchflat.SlowIndexing.SlowIndexingHelpersTest do
|
||||
assert Enum.all?(media_items, fn %MediaItem{source_id: ^source_id} -> true end)
|
||||
end
|
||||
|
||||
test "it won't duplicate media_items based on media_id and source", %{source: source} do
|
||||
test "won't duplicate media_items based on media_id and source", %{source: source} do
|
||||
_first_run = SlowIndexingHelpers.index_and_enqueue_download_for_media_items(source)
|
||||
_duplicate_run = SlowIndexingHelpers.index_and_enqueue_download_for_media_items(source)
|
||||
|
||||
@@ -117,7 +128,7 @@ defmodule Pinchflat.SlowIndexing.SlowIndexingHelpersTest do
|
||||
assert Enum.count(media_items) == 3
|
||||
end
|
||||
|
||||
test "it can duplicate media_ids for different sources", %{source: source} do
|
||||
test "can duplicate media_ids for different sources", %{source: source} do
|
||||
other_source = source_fixture()
|
||||
|
||||
media_items = SlowIndexingHelpers.index_and_enqueue_download_for_media_items(source)
|
||||
@@ -130,7 +141,7 @@ defmodule Pinchflat.SlowIndexing.SlowIndexingHelpersTest do
|
||||
Enum.map(media_items_other_source, & &1.media_id)
|
||||
end
|
||||
|
||||
test "it returns a list of media_items", %{source: source} do
|
||||
test "returns a list of media_items", %{source: source} do
|
||||
first_run = SlowIndexingHelpers.index_and_enqueue_download_for_media_items(source)
|
||||
duplicate_run = SlowIndexingHelpers.index_and_enqueue_download_for_media_items(source)
|
||||
|
||||
@@ -140,7 +151,7 @@ defmodule Pinchflat.SlowIndexing.SlowIndexingHelpersTest do
|
||||
assert first_ids == duplicate_ids
|
||||
end
|
||||
|
||||
test "it updates the source's last_indexed_at field", %{source: source} do
|
||||
test "updates the source's last_indexed_at field", %{source: source} do
|
||||
assert source.last_indexed_at == nil
|
||||
|
||||
SlowIndexingHelpers.index_and_enqueue_download_for_media_items(source)
|
||||
@@ -149,7 +160,7 @@ defmodule Pinchflat.SlowIndexing.SlowIndexingHelpersTest do
|
||||
assert DateTime.diff(DateTime.utc_now(), source.last_indexed_at) < 2
|
||||
end
|
||||
|
||||
test "it enqueues a job for each pending media item" do
|
||||
test "enqueues a job for each pending media item" do
|
||||
source = source_fixture()
|
||||
media_item = media_item_fixture(source_id: source.id, media_filepath: nil)
|
||||
|
||||
@@ -158,7 +169,7 @@ defmodule Pinchflat.SlowIndexing.SlowIndexingHelpersTest do
|
||||
assert_enqueued(worker: MediaDownloadWorker, args: %{"id" => media_item.id})
|
||||
end
|
||||
|
||||
test "it does not attach tasks if the source is set to not download" do
|
||||
test "does not attach tasks if the source is set to not download" do
|
||||
source = source_fixture(download_media: false)
|
||||
media_item = media_item_fixture(source_id: source.id, media_filepath: nil)
|
||||
|
||||
@@ -167,13 +178,13 @@ defmodule Pinchflat.SlowIndexing.SlowIndexingHelpersTest do
|
||||
assert [] = Tasks.list_tasks_for(media_item)
|
||||
end
|
||||
|
||||
test "it doesn't blow up if a media item cannot be coerced into a struct", %{source: source} do
|
||||
test "doesn't blow up if a media item cannot be coerced into a struct", %{source: source} do
|
||||
stub(YtDlpRunnerMock, :run, fn _url, _opts, _ot, _addl_opts ->
|
||||
response =
|
||||
Phoenix.json_library().encode!(%{
|
||||
id: "video3",
|
||||
title: "Video 3",
|
||||
was_live: false,
|
||||
live_status: "not_live",
|
||||
description: "desc3",
|
||||
# Only focusing on these because these are passed to functions that
|
||||
# could fail if they're not present
|
||||
@@ -190,6 +201,28 @@ defmodule Pinchflat.SlowIndexing.SlowIndexingHelpersTest do
|
||||
|
||||
assert %Ecto.Changeset{} = changeset
|
||||
end
|
||||
|
||||
test "sets use_cookies if the source uses cookies" do
|
||||
expect(YtDlpRunnerMock, :run, fn _url, _opts, _ot, addl_opts ->
|
||||
assert {:use_cookies, true} in addl_opts
|
||||
{:ok, source_attributes_return_fixture()}
|
||||
end)
|
||||
|
||||
source = source_fixture(%{use_cookies: true})
|
||||
|
||||
SlowIndexingHelpers.index_and_enqueue_download_for_media_items(source)
|
||||
end
|
||||
|
||||
test "doesn't set use_cookies if the source doesn't use cookies" do
|
||||
expect(YtDlpRunnerMock, :run, fn _url, _opts, _ot, addl_opts ->
|
||||
assert {:use_cookies, false} in addl_opts
|
||||
{:ok, source_attributes_return_fixture()}
|
||||
end)
|
||||
|
||||
source = source_fixture(%{use_cookies: false})
|
||||
|
||||
SlowIndexingHelpers.index_and_enqueue_download_for_media_items(source)
|
||||
end
|
||||
end
|
||||
|
||||
describe "index_and_enqueue_download_for_media_items/1 when testing file watcher" do
|
||||
@@ -267,7 +300,7 @@ defmodule Pinchflat.SlowIndexing.SlowIndexingHelpersTest do
|
||||
id: "video2",
|
||||
title: "Video 2",
|
||||
webpage_url: "https://example.com/shorts/video2",
|
||||
was_live: true,
|
||||
live_status: "is_live",
|
||||
description: "desc2",
|
||||
aspect_ratio: 1.67,
|
||||
duration: 345.67,
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
defmodule Pinchflat.Sources.SourceDeletionWorkerTest do
|
||||
use Pinchflat.DataCase
|
||||
|
||||
import Pinchflat.MediaFixtures
|
||||
import Pinchflat.SourcesFixtures
|
||||
|
||||
alias Pinchflat.Sources.SourceDeletionWorker
|
||||
|
||||
setup do
|
||||
stub(UserScriptRunnerMock, :run, fn _event_type, _data -> :ok end)
|
||||
|
||||
{:ok, %{source: source_fixture()}}
|
||||
end
|
||||
|
||||
describe "kickoff/3" do
|
||||
test "starts the worker", %{source: source} do
|
||||
assert [] = all_enqueued(worker: SourceDeletionWorker)
|
||||
assert {:ok, _} = SourceDeletionWorker.kickoff(source)
|
||||
assert [_] = all_enqueued(worker: SourceDeletionWorker)
|
||||
end
|
||||
|
||||
test "can be called with additional job arguments", %{source: source} do
|
||||
job_args = %{"delete_files" => true}
|
||||
|
||||
assert {:ok, _} = SourceDeletionWorker.kickoff(source, job_args)
|
||||
|
||||
assert_enqueued(worker: SourceDeletionWorker, args: %{"id" => source.id, "delete_files" => true})
|
||||
end
|
||||
end
|
||||
|
||||
describe "perform/1" do
|
||||
test "deletes the source but leaves the files", %{source: source} do
|
||||
media_item = media_item_with_attachments(%{source_id: source.id})
|
||||
|
||||
perform_job(SourceDeletionWorker, %{"id" => source.id})
|
||||
|
||||
assert_raise Ecto.NoResultsError, fn -> Repo.reload!(source) end
|
||||
assert_raise Ecto.NoResultsError, fn -> Repo.reload!(media_item) end
|
||||
assert File.exists?(media_item.media_filepath)
|
||||
end
|
||||
|
||||
test "deletes the source and files if specified", %{source: source} do
|
||||
media_item = media_item_with_attachments(%{source_id: source.id})
|
||||
|
||||
perform_job(SourceDeletionWorker, %{"id" => source.id, "delete_files" => true})
|
||||
|
||||
assert_raise Ecto.NoResultsError, fn -> Repo.reload!(source) end
|
||||
assert_raise Ecto.NoResultsError, fn -> Repo.reload!(media_item) end
|
||||
refute File.exists?(media_item.media_filepath)
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -85,7 +85,7 @@ defmodule Pinchflat.SourcesTest do
|
||||
|
||||
describe "create_source/2" do
|
||||
test "automatically sets the UUID" do
|
||||
expect(YtDlpRunnerMock, :run, &channel_mock/3)
|
||||
expect(YtDlpRunnerMock, :run, &channel_mock/4)
|
||||
|
||||
valid_attrs = %{
|
||||
media_profile_id: media_profile_fixture().id,
|
||||
@@ -97,7 +97,7 @@ defmodule Pinchflat.SourcesTest do
|
||||
end
|
||||
|
||||
test "UUID is not writable by the user" do
|
||||
expect(YtDlpRunnerMock, :run, &channel_mock/3)
|
||||
expect(YtDlpRunnerMock, :run, &channel_mock/4)
|
||||
|
||||
valid_attrs = %{
|
||||
media_profile_id: media_profile_fixture().id,
|
||||
@@ -110,7 +110,7 @@ defmodule Pinchflat.SourcesTest do
|
||||
end
|
||||
|
||||
test "creates a source and adds name + ID from runner response for channels" do
|
||||
expect(YtDlpRunnerMock, :run, &channel_mock/3)
|
||||
expect(YtDlpRunnerMock, :run, &channel_mock/4)
|
||||
|
||||
valid_attrs = %{
|
||||
media_profile_id: media_profile_fixture().id,
|
||||
@@ -123,7 +123,7 @@ defmodule Pinchflat.SourcesTest do
|
||||
end
|
||||
|
||||
test "creates a source and adds name + ID for playlists" do
|
||||
expect(YtDlpRunnerMock, :run, &playlist_mock/3)
|
||||
expect(YtDlpRunnerMock, :run, &playlist_mock/4)
|
||||
|
||||
valid_attrs = %{
|
||||
media_profile_id: media_profile_fixture().id,
|
||||
@@ -136,7 +136,7 @@ defmodule Pinchflat.SourcesTest do
|
||||
end
|
||||
|
||||
test "adds an error if the runner fails" do
|
||||
expect(YtDlpRunnerMock, :run, fn _url, _opts, _ot -> {:error, "some error", 1} end)
|
||||
expect(YtDlpRunnerMock, :run, fn _url, _opts, _ot, _addl -> {:error, "some error", 1} end)
|
||||
|
||||
valid_attrs = %{
|
||||
media_profile_id: media_profile_fixture().id,
|
||||
@@ -148,7 +148,7 @@ defmodule Pinchflat.SourcesTest do
|
||||
end
|
||||
|
||||
test "you can specify a custom custom_name" do
|
||||
expect(YtDlpRunnerMock, :run, &channel_mock/3)
|
||||
expect(YtDlpRunnerMock, :run, &channel_mock/4)
|
||||
|
||||
valid_attrs = %{
|
||||
media_profile_id: media_profile_fixture().id,
|
||||
@@ -162,7 +162,7 @@ defmodule Pinchflat.SourcesTest do
|
||||
end
|
||||
|
||||
test "friendly name is pulled from collection_name if not specified" do
|
||||
expect(YtDlpRunnerMock, :run, &channel_mock/3)
|
||||
expect(YtDlpRunnerMock, :run, &channel_mock/4)
|
||||
|
||||
valid_attrs = %{
|
||||
media_profile_id: media_profile_fixture().id,
|
||||
@@ -175,7 +175,7 @@ defmodule Pinchflat.SourcesTest do
|
||||
end
|
||||
|
||||
test "creation enforces uniqueness of collection_id scoped to the media_profile and title regex" do
|
||||
expect(YtDlpRunnerMock, :run, 2, fn _url, _opts, _ot ->
|
||||
expect(YtDlpRunnerMock, :run, 2, fn _url, _opts, _ot, _addl ->
|
||||
{:ok,
|
||||
Phoenix.json_library().encode!(%{
|
||||
channel: "some channel name",
|
||||
@@ -196,7 +196,7 @@ defmodule Pinchflat.SourcesTest do
|
||||
end
|
||||
|
||||
test "creation lets you duplicate collection_ids and profiles as long as the regex is different" do
|
||||
expect(YtDlpRunnerMock, :run, 2, fn _url, _opts, _ot ->
|
||||
expect(YtDlpRunnerMock, :run, 2, fn _url, _opts, _ot, _addl ->
|
||||
{:ok,
|
||||
Phoenix.json_library().encode!(%{
|
||||
channel: "some channel name",
|
||||
@@ -220,7 +220,7 @@ defmodule Pinchflat.SourcesTest do
|
||||
end
|
||||
|
||||
test "creation lets you duplicate collection_ids as long as the media profile is different" do
|
||||
expect(YtDlpRunnerMock, :run, 2, fn _url, _opts, _ot ->
|
||||
expect(YtDlpRunnerMock, :run, 2, fn _url, _opts, _ot, _addl ->
|
||||
{:ok,
|
||||
Phoenix.json_library().encode!(%{
|
||||
channel: "some channel name",
|
||||
@@ -244,8 +244,8 @@ defmodule Pinchflat.SourcesTest do
|
||||
end
|
||||
|
||||
test "collection_type is inferred from source details" do
|
||||
expect(YtDlpRunnerMock, :run, &channel_mock/3)
|
||||
expect(YtDlpRunnerMock, :run, &playlist_mock/3)
|
||||
expect(YtDlpRunnerMock, :run, &channel_mock/4)
|
||||
expect(YtDlpRunnerMock, :run, &playlist_mock/4)
|
||||
|
||||
valid_attrs = %{
|
||||
media_profile_id: media_profile_fixture().id,
|
||||
@@ -264,13 +264,13 @@ defmodule Pinchflat.SourcesTest do
|
||||
end
|
||||
|
||||
test "creation with invalid data fails fast and does not call the runner" do
|
||||
expect(YtDlpRunnerMock, :run, 0, &channel_mock/3)
|
||||
expect(YtDlpRunnerMock, :run, 0, &channel_mock/4)
|
||||
|
||||
assert {:error, %Ecto.Changeset{}} = Sources.create_source(@invalid_source_attrs)
|
||||
end
|
||||
|
||||
test "creation will schedule the indexing task" do
|
||||
expect(YtDlpRunnerMock, :run, &channel_mock/3)
|
||||
expect(YtDlpRunnerMock, :run, &channel_mock/4)
|
||||
|
||||
valid_attrs = %{
|
||||
media_profile_id: media_profile_fixture().id,
|
||||
@@ -283,7 +283,7 @@ defmodule Pinchflat.SourcesTest do
|
||||
end
|
||||
|
||||
test "creation schedules an index test even if the index frequency is 0" do
|
||||
expect(YtDlpRunnerMock, :run, &channel_mock/3)
|
||||
expect(YtDlpRunnerMock, :run, &channel_mock/4)
|
||||
|
||||
valid_attrs = %{
|
||||
media_profile_id: media_profile_fixture().id,
|
||||
@@ -297,7 +297,7 @@ defmodule Pinchflat.SourcesTest do
|
||||
end
|
||||
|
||||
test "fast_index forces the index frequency to be a default value" do
|
||||
expect(YtDlpRunnerMock, :run, &channel_mock/3)
|
||||
expect(YtDlpRunnerMock, :run, &channel_mock/4)
|
||||
|
||||
valid_attrs = %{
|
||||
media_profile_id: media_profile_fixture().id,
|
||||
@@ -312,7 +312,7 @@ defmodule Pinchflat.SourcesTest do
|
||||
end
|
||||
|
||||
test "disabling fast index will not change the index frequency" do
|
||||
expect(YtDlpRunnerMock, :run, &channel_mock/3)
|
||||
expect(YtDlpRunnerMock, :run, &channel_mock/4)
|
||||
|
||||
valid_attrs = %{
|
||||
media_profile_id: media_profile_fixture().id,
|
||||
@@ -327,7 +327,7 @@ defmodule Pinchflat.SourcesTest do
|
||||
end
|
||||
|
||||
test "creating will kickoff a metadata storage worker" do
|
||||
expect(YtDlpRunnerMock, :run, &channel_mock/3)
|
||||
expect(YtDlpRunnerMock, :run, &channel_mock/4)
|
||||
|
||||
valid_attrs = %{
|
||||
media_profile_id: media_profile_fixture().id,
|
||||
@@ -344,7 +344,7 @@ defmodule Pinchflat.SourcesTest do
|
||||
|
||||
describe "create_source/2 when testing options" do
|
||||
test "run_post_commit_tasks: false won't enqueue post-commit tasks" do
|
||||
expect(YtDlpRunnerMock, :run, &channel_mock/3)
|
||||
expect(YtDlpRunnerMock, :run, &channel_mock/4)
|
||||
|
||||
valid_attrs = %{
|
||||
media_profile_id: media_profile_fixture().id,
|
||||
@@ -368,7 +368,7 @@ defmodule Pinchflat.SourcesTest do
|
||||
end
|
||||
|
||||
test "updates with invalid data fails fast and does not call the runner" do
|
||||
expect(YtDlpRunnerMock, :run, 0, &channel_mock/3)
|
||||
expect(YtDlpRunnerMock, :run, 0, &channel_mock/4)
|
||||
|
||||
source = source_fixture()
|
||||
|
||||
@@ -376,7 +376,7 @@ defmodule Pinchflat.SourcesTest do
|
||||
end
|
||||
|
||||
test "updating the original_url will re-fetch the source details for channels" do
|
||||
expect(YtDlpRunnerMock, :run, &channel_mock/3)
|
||||
expect(YtDlpRunnerMock, :run, &channel_mock/4)
|
||||
|
||||
source = source_fixture()
|
||||
update_attrs = %{original_url: "https://www.youtube.com/channel/abc123"}
|
||||
@@ -387,7 +387,7 @@ defmodule Pinchflat.SourcesTest do
|
||||
end
|
||||
|
||||
test "updating the original_url will re-fetch the source details for playlists" do
|
||||
expect(YtDlpRunnerMock, :run, &playlist_mock/3)
|
||||
expect(YtDlpRunnerMock, :run, &playlist_mock/4)
|
||||
|
||||
source = source_fixture()
|
||||
update_attrs = %{original_url: "https://www.youtube.com/playlist?list=abc123"}
|
||||
@@ -398,7 +398,7 @@ defmodule Pinchflat.SourcesTest do
|
||||
end
|
||||
|
||||
test "not updating the original_url will not re-fetch the source details" do
|
||||
expect(YtDlpRunnerMock, :run, 0, &channel_mock/3)
|
||||
expect(YtDlpRunnerMock, :run, 0, &channel_mock/4)
|
||||
|
||||
source = source_fixture()
|
||||
update_attrs = %{name: "some updated name"}
|
||||
@@ -519,7 +519,7 @@ defmodule Pinchflat.SourcesTest do
|
||||
end
|
||||
|
||||
test "updating will kickoff a metadata storage worker if the original_url changes" do
|
||||
expect(YtDlpRunnerMock, :run, &playlist_mock/3)
|
||||
expect(YtDlpRunnerMock, :run, &playlist_mock/4)
|
||||
source = source_fixture()
|
||||
update_attrs = %{original_url: "https://www.youtube.com/channel/cba321"}
|
||||
|
||||
@@ -617,7 +617,7 @@ defmodule Pinchflat.SourcesTest do
|
||||
|
||||
describe "delete_source/2 when deleting files" do
|
||||
setup do
|
||||
stub(UserScriptRunnerMock, :run, fn _event_type, _data -> :ok end)
|
||||
stub(UserScriptRunnerMock, :run, fn _event_type, _data -> {:ok, "", 0} end)
|
||||
|
||||
:ok
|
||||
end
|
||||
@@ -681,6 +681,34 @@ defmodule Pinchflat.SourcesTest do
|
||||
end
|
||||
end
|
||||
|
||||
describe "change_source/3 when testing min/max duration validations" do
|
||||
test "succeeds if min and max are nil" do
|
||||
source = source_fixture()
|
||||
|
||||
assert %{errors: []} = Sources.change_source(source, %{min_duration_seconds: nil, max_duration_seconds: nil})
|
||||
end
|
||||
|
||||
test "succeeds if either min or max is nil" do
|
||||
source = source_fixture()
|
||||
|
||||
assert %{errors: []} = Sources.change_source(source, %{min_duration_seconds: nil, max_duration_seconds: 100})
|
||||
assert %{errors: []} = Sources.change_source(source, %{min_duration_seconds: 100, max_duration_seconds: nil})
|
||||
end
|
||||
|
||||
test "succeeds if min is less than max" do
|
||||
source = source_fixture()
|
||||
|
||||
assert %{errors: []} = Sources.change_source(source, %{min_duration_seconds: 100, max_duration_seconds: 200})
|
||||
end
|
||||
|
||||
test "fails if min is greater than or equal to max" do
|
||||
source = source_fixture()
|
||||
|
||||
assert %{errors: [_]} = Sources.change_source(source, %{min_duration_seconds: 200, max_duration_seconds: 100})
|
||||
assert %{errors: [_]} = Sources.change_source(source, %{min_duration_seconds: 100, max_duration_seconds: 100})
|
||||
end
|
||||
end
|
||||
|
||||
describe "change_source/3 when testing original_url validation" do
|
||||
test "succeeds when an original URL is valid" do
|
||||
source = source_fixture()
|
||||
@@ -736,7 +764,7 @@ defmodule Pinchflat.SourcesTest do
|
||||
end
|
||||
end
|
||||
|
||||
defp playlist_mock(_url, _opts, _ot) do
|
||||
defp playlist_mock(_url, _opts, _ot, _addl) do
|
||||
{
|
||||
:ok,
|
||||
Phoenix.json_library().encode!(%{
|
||||
@@ -748,7 +776,7 @@ defmodule Pinchflat.SourcesTest do
|
||||
}
|
||||
end
|
||||
|
||||
defp channel_mock(_url, _opts, _ot) do
|
||||
defp channel_mock(_url, _opts, _ot, _addl) do
|
||||
channel_id = "some_channel_id_#{:rand.uniform(1_000_000)}"
|
||||
|
||||
{
|
||||
|
||||
@@ -12,7 +12,7 @@ defmodule Pinchflat.TasksTest do
|
||||
@invalid_attrs %{job_id: nil}
|
||||
|
||||
describe "schema" do
|
||||
test "it deletes a task when the job gets deleted" do
|
||||
test "deletes a task when the job gets deleted" do
|
||||
task = Repo.preload(task_fixture(), [:job])
|
||||
|
||||
{:ok, _} = Repo.delete(task.job)
|
||||
@@ -20,7 +20,7 @@ defmodule Pinchflat.TasksTest do
|
||||
assert_raise Ecto.NoResultsError, fn -> Repo.reload!(task) end
|
||||
end
|
||||
|
||||
test "it does not delete the other record when a job gets deleted" do
|
||||
test "does not delete the other record when a job gets deleted" do
|
||||
task = Repo.preload(task_fixture(), [:source, :job])
|
||||
|
||||
{:ok, _} = Repo.delete(task.job)
|
||||
@@ -30,21 +30,21 @@ defmodule Pinchflat.TasksTest do
|
||||
end
|
||||
|
||||
describe "list_tasks/0" do
|
||||
test "it returns all tasks" do
|
||||
test "returns all tasks" do
|
||||
task = task_fixture()
|
||||
assert Tasks.list_tasks() == [task]
|
||||
end
|
||||
end
|
||||
|
||||
describe "list_tasks_for/3" do
|
||||
test "it lets you specify which record type/ID to join on" do
|
||||
test "lets you specify which record type/ID to join on" do
|
||||
source = source_fixture()
|
||||
task = task_fixture(source_id: source.id)
|
||||
|
||||
assert Tasks.list_tasks_for(source, nil, [:available]) == [task]
|
||||
end
|
||||
|
||||
test "it lets you specify which job states to include" do
|
||||
test "lets you specify which job states to include" do
|
||||
source = source_fixture()
|
||||
task = task_fixture(source_id: source.id)
|
||||
|
||||
@@ -52,7 +52,7 @@ defmodule Pinchflat.TasksTest do
|
||||
assert Tasks.list_tasks_for(source, nil, [:cancelled]) == []
|
||||
end
|
||||
|
||||
test "it lets you specify which worker to include" do
|
||||
test "lets you specify which worker to include" do
|
||||
source = source_fixture()
|
||||
task = task_fixture(source_id: source.id)
|
||||
|
||||
@@ -60,7 +60,7 @@ defmodule Pinchflat.TasksTest do
|
||||
assert Tasks.list_tasks_for(source, "FooBarWorker") == []
|
||||
end
|
||||
|
||||
test "it includes all workers if no worker is specified" do
|
||||
test "includes all workers if no worker is specified" do
|
||||
source = source_fixture()
|
||||
task = task_fixture(source_id: source.id)
|
||||
|
||||
@@ -68,32 +68,8 @@ defmodule Pinchflat.TasksTest do
|
||||
end
|
||||
end
|
||||
|
||||
describe "list_pending_tasks_for/3" do
|
||||
test "it lists pending tasks" do
|
||||
source = source_fixture()
|
||||
task = task_fixture(source_id: source.id)
|
||||
|
||||
assert Tasks.list_pending_tasks_for(source) == [task]
|
||||
end
|
||||
|
||||
test "it does not list non-pending tasks" do
|
||||
task = Repo.preload(task_fixture(), [:job, :source])
|
||||
:ok = Oban.cancel_job(task.job)
|
||||
|
||||
assert Tasks.list_pending_tasks_for(task.source) == []
|
||||
end
|
||||
|
||||
test "it lets you specify which worker to include" do
|
||||
source = source_fixture()
|
||||
task = task_fixture(source_id: source.id)
|
||||
|
||||
assert Tasks.list_pending_tasks_for(source, "TestJobWorker") == [task]
|
||||
assert Tasks.list_pending_tasks_for(source, "FooBarWorker") == []
|
||||
end
|
||||
end
|
||||
|
||||
describe "get_task!/1" do
|
||||
test "it returns the task with given id" do
|
||||
test "returns the task with given id" do
|
||||
task = task_fixture()
|
||||
assert Tasks.get_task!(task.id) == task
|
||||
end
|
||||
@@ -132,7 +108,7 @@ defmodule Pinchflat.TasksTest do
|
||||
end
|
||||
|
||||
describe "create_job_with_task/2" do
|
||||
test "it enqueues the given job" do
|
||||
test "enqueues the given job" do
|
||||
media_item = media_item_fixture()
|
||||
|
||||
refute_enqueued(worker: TestJobWorker)
|
||||
@@ -140,7 +116,7 @@ defmodule Pinchflat.TasksTest do
|
||||
assert_enqueued(worker: TestJobWorker)
|
||||
end
|
||||
|
||||
test "it creates a task record if successful" do
|
||||
test "creates a task record if successful" do
|
||||
source = source_fixture()
|
||||
|
||||
assert {:ok, %Task{} = task} = Tasks.create_job_with_task(TestJobWorker.new(%{}), source)
|
||||
@@ -148,7 +124,7 @@ defmodule Pinchflat.TasksTest do
|
||||
assert task.source_id == source.id
|
||||
end
|
||||
|
||||
test "it returns an error if the job already exists" do
|
||||
test "returns an error if the job already exists" do
|
||||
source = source_fixture()
|
||||
job = TestJobWorker.new(%{foo: "bar"}, unique: [period: :infinity])
|
||||
|
||||
@@ -156,7 +132,7 @@ defmodule Pinchflat.TasksTest do
|
||||
assert {:error, :duplicate_job} = Tasks.create_job_with_task(job, source)
|
||||
end
|
||||
|
||||
test "it returns an error if the job fails to enqueue" do
|
||||
test "returns an error if the job fails to enqueue" do
|
||||
source = source_fixture()
|
||||
|
||||
assert {:error, %Ecto.Changeset{}} = Tasks.create_job_with_task(%Ecto.Changeset{}, source)
|
||||
@@ -181,7 +157,7 @@ defmodule Pinchflat.TasksTest do
|
||||
end
|
||||
|
||||
describe "delete_tasks_for/2" do
|
||||
test "it deletes tasks attached to a source" do
|
||||
test "deletes tasks attached to a source" do
|
||||
source = source_fixture()
|
||||
task = task_fixture(source_id: source.id)
|
||||
|
||||
@@ -189,7 +165,7 @@ defmodule Pinchflat.TasksTest do
|
||||
assert_raise Ecto.NoResultsError, fn -> Tasks.get_task!(task.id) end
|
||||
end
|
||||
|
||||
test "it deletes the tasks attached to a media_item" do
|
||||
test "deletes the tasks attached to a media_item" do
|
||||
media_item = media_item_fixture()
|
||||
task = task_fixture(media_item_id: media_item.id)
|
||||
|
||||
@@ -208,6 +184,17 @@ defmodule Pinchflat.TasksTest do
|
||||
assert_raise Ecto.NoResultsError, fn -> Repo.reload!(task) end
|
||||
end
|
||||
|
||||
test "deletion can specify which states to include" do
|
||||
source = source_fixture()
|
||||
task = task_fixture(source_id: source.id)
|
||||
|
||||
assert :ok = Tasks.delete_tasks_for(source, nil, [:executing])
|
||||
assert Repo.reload!(task)
|
||||
|
||||
assert :ok = Tasks.delete_tasks_for(source, nil, [:available])
|
||||
assert_raise Ecto.NoResultsError, fn -> Repo.reload!(task) end
|
||||
end
|
||||
|
||||
test "deletion does not impact unintended records" do
|
||||
source = source_fixture()
|
||||
task = task_fixture(source_id: source.id)
|
||||
@@ -221,7 +208,7 @@ defmodule Pinchflat.TasksTest do
|
||||
end
|
||||
|
||||
describe "delete_pending_tasks_for/1" do
|
||||
test "it deletes pending tasks attached to a source" do
|
||||
test "deletes pending tasks attached to a source" do
|
||||
source = source_fixture()
|
||||
task = task_fixture(source_id: source.id)
|
||||
|
||||
@@ -229,7 +216,7 @@ defmodule Pinchflat.TasksTest do
|
||||
assert_raise Ecto.NoResultsError, fn -> Tasks.get_task!(task.id) end
|
||||
end
|
||||
|
||||
test "it does not delete non-pending tasks" do
|
||||
test "does not delete non-pending tasks" do
|
||||
source = source_fixture()
|
||||
task = Repo.preload(task_fixture(source_id: source.id), :job)
|
||||
:ok = Oban.cancel_job(task.job)
|
||||
@@ -238,7 +225,7 @@ defmodule Pinchflat.TasksTest do
|
||||
assert Tasks.get_task!(task.id)
|
||||
end
|
||||
|
||||
test "it works on media_items" do
|
||||
test "works on media_items" do
|
||||
media_item = media_item_fixture()
|
||||
pending_task = task_fixture(media_item_id: media_item.id)
|
||||
cancelled_task = Repo.preload(task_fixture(media_item_id: media_item.id), :job)
|
||||
@@ -259,10 +246,23 @@ defmodule Pinchflat.TasksTest do
|
||||
assert :ok = Tasks.delete_pending_tasks_for(media_item, "TestJobWorker")
|
||||
assert_raise Ecto.NoResultsError, fn -> Repo.reload!(task) end
|
||||
end
|
||||
|
||||
test "deletion can optionall include executing tasks" do
|
||||
source = source_fixture()
|
||||
task = task_fixture(source_id: source.id)
|
||||
|
||||
from(Oban.Job, where: [id: ^task.job_id], update: [set: [state: "executing"]])
|
||||
|> Repo.update_all([])
|
||||
|
||||
assert :ok = Tasks.delete_pending_tasks_for(source, nil, include_executing: false)
|
||||
assert Repo.reload!(task)
|
||||
assert :ok = Tasks.delete_pending_tasks_for(source, nil, include_executing: true)
|
||||
assert_raise Ecto.NoResultsError, fn -> Repo.reload!(task) end
|
||||
end
|
||||
end
|
||||
|
||||
describe "change_task/1" do
|
||||
test "it returns a task changeset" do
|
||||
test "returns a task changeset" do
|
||||
task = task_fixture()
|
||||
assert %Ecto.Changeset{} = Tasks.change_task(task)
|
||||
end
|
||||
|
||||
@@ -51,18 +51,26 @@ defmodule Pinchflat.YtDlp.CommandRunnerTest do
|
||||
{:ok, cookie_file: cookie_file, yt_dlp_file: yt_dlp_file}
|
||||
end
|
||||
|
||||
test "includes cookie options when cookies.txt exists", %{cookie_file: cookie_file} do
|
||||
test "includes cookie options when cookies.txt exists and enabled", %{cookie_file: cookie_file} do
|
||||
FilesystemUtils.write_p!(cookie_file, "cookie data")
|
||||
|
||||
assert {:ok, output} = Runner.run(@media_url, [], "")
|
||||
assert {:ok, output} = Runner.run(@media_url, [], "", use_cookies: true)
|
||||
|
||||
assert String.contains?(output, "--cookies #{cookie_file}")
|
||||
end
|
||||
|
||||
test "doesn't include cookie options when cookies.txt exists but disabled", %{cookie_file: cookie_file} do
|
||||
FilesystemUtils.write_p!(cookie_file, "cookie data")
|
||||
|
||||
assert {:ok, output} = Runner.run(@media_url, [], "", use_cookies: false)
|
||||
|
||||
refute String.contains?(output, "--cookies #{cookie_file}")
|
||||
end
|
||||
|
||||
test "doesn't include cookie options when cookies.txt blank", %{cookie_file: cookie_file} do
|
||||
FilesystemUtils.write_p!(cookie_file, " \n \n ")
|
||||
|
||||
assert {:ok, output} = Runner.run(@media_url, [], "")
|
||||
assert {:ok, output} = Runner.run(@media_url, [], "", use_cookies: true)
|
||||
|
||||
refute String.contains?(output, "--cookies")
|
||||
refute String.contains?(output, cookie_file)
|
||||
|
||||
@@ -18,7 +18,7 @@ defmodule Pinchflat.YtDlp.MediaCollectionTest do
|
||||
MediaCollection.get_media_attributes_for_collection(@channel_url)
|
||||
end
|
||||
|
||||
test "it passes the expected default args" do
|
||||
test "passes the expected default args" do
|
||||
expect(YtDlpRunnerMock, :run, fn _url, opts, ot, _addl_opts ->
|
||||
assert opts == [:simulate, :skip_download, :ignore_no_formats_error, :no_warnings]
|
||||
assert ot == Media.indexing_output_template()
|
||||
@@ -35,9 +35,10 @@ defmodule Pinchflat.YtDlp.MediaCollectionTest do
|
||||
assert {:error, "Big issue", 1} = MediaCollection.get_media_attributes_for_collection(@channel_url)
|
||||
end
|
||||
|
||||
test "passes the explict tmpfile path to runner" do
|
||||
test "passes additional args to runner" do
|
||||
expect(YtDlpRunnerMock, :run, fn _url, _opts, _ot, addl_opts ->
|
||||
assert [{:output_filepath, filepath}] = addl_opts
|
||||
assert [{:output_filepath, filepath} | _] = addl_opts
|
||||
assert {:use_cookies, false} in addl_opts
|
||||
assert String.ends_with?(filepath, ".json")
|
||||
|
||||
{:ok, ""}
|
||||
@@ -72,8 +73,8 @@ defmodule Pinchflat.YtDlp.MediaCollectionTest do
|
||||
end
|
||||
|
||||
describe "get_source_details/1" do
|
||||
test "it returns a map with data on success" do
|
||||
expect(YtDlpRunnerMock, :run, fn _url, _opts, _ot ->
|
||||
test "returns a map with data on success" do
|
||||
expect(YtDlpRunnerMock, :run, fn _url, _opts, _ot, _addl_opts ->
|
||||
Phoenix.json_library().encode(%{
|
||||
channel: "PinchflatTestChannel",
|
||||
channel_id: "UCQH2",
|
||||
@@ -92,8 +93,8 @@ defmodule Pinchflat.YtDlp.MediaCollectionTest do
|
||||
} = res
|
||||
end
|
||||
|
||||
test "it passes the expected args to the backend runner" do
|
||||
expect(YtDlpRunnerMock, :run, fn @channel_url, opts, ot ->
|
||||
test "passes the expected args to the runner" do
|
||||
expect(YtDlpRunnerMock, :run, fn @channel_url, opts, ot, _addl_opts ->
|
||||
assert opts == [:simulate, :skip_download, :ignore_no_formats_error, playlist_end: 1]
|
||||
assert ot == "%(.{channel,channel_id,playlist_id,playlist_title,filename})j"
|
||||
|
||||
@@ -103,61 +104,97 @@ defmodule Pinchflat.YtDlp.MediaCollectionTest do
|
||||
assert {:ok, _} = MediaCollection.get_source_details(@channel_url)
|
||||
end
|
||||
|
||||
test "it returns an error if the runner returns an error" do
|
||||
expect(YtDlpRunnerMock, :run, fn _url, _opts, _ot -> {:error, "Big issue", 1} end)
|
||||
test "passes custom args to the runner" do
|
||||
expect(YtDlpRunnerMock, :run, fn @channel_url, opts, _ot, _addl_opts ->
|
||||
assert {:foo, :bar} in opts
|
||||
|
||||
{:ok, "{}"}
|
||||
end)
|
||||
|
||||
assert {:ok, _} = MediaCollection.get_source_details(@channel_url, foo: :bar)
|
||||
end
|
||||
|
||||
test "passes additional args to the runner" do
|
||||
expect(YtDlpRunnerMock, :run, fn @channel_url, _opts, _ot, addl_opts ->
|
||||
assert {:use_cookies, true} in addl_opts
|
||||
|
||||
{:ok, "{}"}
|
||||
end)
|
||||
|
||||
assert {:ok, _} = MediaCollection.get_source_details(@channel_url, [], use_cookies: true)
|
||||
end
|
||||
|
||||
test "returns an error if the runner returns an error" do
|
||||
expect(YtDlpRunnerMock, :run, fn _url, _opts, _ot, _addl_opts -> {:error, "Big issue", 1} end)
|
||||
|
||||
assert {:error, "Big issue", 1} = MediaCollection.get_source_details(@channel_url)
|
||||
end
|
||||
|
||||
test "it returns an error if the output is not JSON" do
|
||||
expect(YtDlpRunnerMock, :run, fn _url, _opts, _ot -> {:ok, "Not JSON"} end)
|
||||
test "returns an error if the output is not JSON" do
|
||||
expect(YtDlpRunnerMock, :run, fn _url, _opts, _ot, _addl_opts -> {:ok, "Not JSON"} end)
|
||||
|
||||
assert {:error, %Jason.DecodeError{}} = MediaCollection.get_source_details(@channel_url)
|
||||
end
|
||||
end
|
||||
|
||||
describe "get_source_metadata/1" do
|
||||
test "it returns a map with data on success" do
|
||||
expect(YtDlpRunnerMock, :run, fn _url, _opts, _ot ->
|
||||
test "returns a map with data on success" do
|
||||
expect(YtDlpRunnerMock, :run, fn _url, _opts, _ot, _addl_opts ->
|
||||
Phoenix.json_library().encode(%{channel: "PinchflatTestChannel"})
|
||||
end)
|
||||
|
||||
assert {:ok, res} = MediaCollection.get_source_metadata(@channel_url)
|
||||
assert {:ok, res} = MediaCollection.get_source_metadata(@channel_url, playlist_items: 0)
|
||||
|
||||
assert %{"channel" => "PinchflatTestChannel"} = res
|
||||
end
|
||||
|
||||
test "it passes the expected args to the backend runner" do
|
||||
expect(YtDlpRunnerMock, :run, fn @channel_url, opts, ot ->
|
||||
assert opts == [playlist_items: 0]
|
||||
test "passes the expected args to the backend runner" do
|
||||
expect(YtDlpRunnerMock, :run, fn @channel_url, opts, ot, _addl_opts ->
|
||||
assert opts == [:skip_download, playlist_items: 0]
|
||||
assert ot == "playlist:%()j"
|
||||
|
||||
{:ok, "{}"}
|
||||
end)
|
||||
|
||||
assert {:ok, _} = MediaCollection.get_source_metadata(@channel_url)
|
||||
assert {:ok, _} = MediaCollection.get_source_metadata(@channel_url, playlist_items: 0)
|
||||
end
|
||||
|
||||
test "it returns an error if the runner returns an error" do
|
||||
expect(YtDlpRunnerMock, :run, fn _url, _opts, _ot -> {:error, "Big issue", 1} end)
|
||||
|
||||
assert {:error, "Big issue", 1} = MediaCollection.get_source_metadata(@channel_url)
|
||||
end
|
||||
|
||||
test "it returns an error if the output is not JSON" do
|
||||
expect(YtDlpRunnerMock, :run, fn _url, _opts, _ot -> {:ok, "Not JSON"} end)
|
||||
|
||||
assert {:error, %Jason.DecodeError{}} = MediaCollection.get_source_metadata(@channel_url)
|
||||
end
|
||||
|
||||
test "allows you to pass additional opts" do
|
||||
expect(YtDlpRunnerMock, :run, fn _url, opts, _ot ->
|
||||
assert opts == [playlist_items: 0, real_opt: :yup]
|
||||
test "passes additional args to the runner" do
|
||||
expect(YtDlpRunnerMock, :run, fn @channel_url, _opts, _ot, addl_opts ->
|
||||
assert {:use_cookies, true} in addl_opts
|
||||
|
||||
{:ok, "{}"}
|
||||
end)
|
||||
|
||||
assert {:ok, _} = MediaCollection.get_source_metadata(@channel_url, real_opt: :yup)
|
||||
assert {:ok, _} = MediaCollection.get_source_metadata(@channel_url, [playlist_items: 0], use_cookies: true)
|
||||
end
|
||||
|
||||
test "passes custom args to the runner" do
|
||||
expect(YtDlpRunnerMock, :run, fn _url, opts, _ot, _addl_opts ->
|
||||
assert opts == [:skip_download, playlist_items: 1, real_opt: :yup]
|
||||
|
||||
{:ok, "{}"}
|
||||
end)
|
||||
|
||||
assert {:ok, _} = MediaCollection.get_source_metadata(@channel_url, playlist_items: 1, real_opt: :yup)
|
||||
end
|
||||
|
||||
test "blows up if you pass addl opts but don't pass playlist items" do
|
||||
assert_raise KeyError, fn ->
|
||||
MediaCollection.get_source_metadata(@channel_url, real_opt: :yup)
|
||||
end
|
||||
end
|
||||
|
||||
test "returns an error if the runner returns an error" do
|
||||
expect(YtDlpRunnerMock, :run, fn _url, _opts, _ot, _addl_opts -> {:error, "Big issue", 1} end)
|
||||
|
||||
assert {:error, "Big issue", 1} = MediaCollection.get_source_metadata(@channel_url, playlist_items: 0)
|
||||
end
|
||||
|
||||
test "returns an error if the output is not JSON" do
|
||||
expect(YtDlpRunnerMock, :run, fn _url, _opts, _ot, _addl_opts -> {:ok, "Not JSON"} end)
|
||||
|
||||
assert {:error, %Jason.DecodeError{}} = MediaCollection.get_source_metadata(@channel_url, playlist_items: 0)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -20,15 +20,24 @@ defmodule Pinchflat.YtDlp.MediaTest do
|
||||
assert {:ok, _} = Media.download(@media_url)
|
||||
end
|
||||
|
||||
test "passes along additional options" do
|
||||
expect(YtDlpRunnerMock, :run, fn _url, opts, _ot, addl ->
|
||||
test "passes along custom command args" do
|
||||
expect(YtDlpRunnerMock, :run, fn _url, opts, _ot, _addl ->
|
||||
assert [:no_simulate, :custom_arg] = opts
|
||||
|
||||
{:ok, "{}"}
|
||||
end)
|
||||
|
||||
assert {:ok, _} = Media.download(@media_url, [:custom_arg])
|
||||
end
|
||||
|
||||
test "passes along additional options" do
|
||||
expect(YtDlpRunnerMock, :run, fn _url, _opts, _ot, addl ->
|
||||
assert [addl_arg: true] = addl
|
||||
|
||||
{:ok, "{}"}
|
||||
end)
|
||||
|
||||
assert {:ok, _} = Media.download(@media_url, [:custom_arg], addl_arg: true)
|
||||
assert {:ok, _} = Media.download(@media_url, [], addl_arg: true)
|
||||
end
|
||||
|
||||
test "parses and returns the generated file as JSON" do
|
||||
@@ -51,7 +60,7 @@ defmodule Pinchflat.YtDlp.MediaTest do
|
||||
|
||||
describe "download_thumbnail/2" do
|
||||
test "calls the backend runner with the expected arguments" do
|
||||
expect(YtDlpRunnerMock, :run, fn @media_url, opts, ot ->
|
||||
expect(YtDlpRunnerMock, :run, fn @media_url, opts, ot, _addl ->
|
||||
assert opts == [:no_simulate, :skip_download, :write_thumbnail, {:convert_thumbnail, "jpg"}]
|
||||
assert ot == "after_move:%()j"
|
||||
|
||||
@@ -61,8 +70,8 @@ defmodule Pinchflat.YtDlp.MediaTest do
|
||||
assert {:ok, _} = Media.download_thumbnail(@media_url)
|
||||
end
|
||||
|
||||
test "passes along additional options" do
|
||||
expect(YtDlpRunnerMock, :run, fn _url, opts, _ot ->
|
||||
test "passes along custom command args" do
|
||||
expect(YtDlpRunnerMock, :run, fn _url, opts, _ot, _addl ->
|
||||
assert :custom_arg in opts
|
||||
|
||||
{:ok, "{}"}
|
||||
@@ -71,8 +80,18 @@ defmodule Pinchflat.YtDlp.MediaTest do
|
||||
assert {:ok, _} = Media.download_thumbnail(@media_url, [:custom_arg])
|
||||
end
|
||||
|
||||
test "passes along additional options" do
|
||||
expect(YtDlpRunnerMock, :run, fn _url, _opts, _ot, addl ->
|
||||
assert [addl_arg: true] = addl
|
||||
|
||||
{:ok, "{}"}
|
||||
end)
|
||||
|
||||
assert {:ok, _} = Media.download_thumbnail(@media_url, [], addl_arg: true)
|
||||
end
|
||||
|
||||
test "returns errors" do
|
||||
expect(YtDlpRunnerMock, :run, fn _url, _opt, _ot ->
|
||||
expect(YtDlpRunnerMock, :run, fn _url, _opt, _ot, _addl ->
|
||||
{:error, "something"}
|
||||
end)
|
||||
|
||||
@@ -82,7 +101,7 @@ defmodule Pinchflat.YtDlp.MediaTest do
|
||||
|
||||
describe "get_media_attributes/1" do
|
||||
test "returns a list of video attributes" do
|
||||
expect(YtDlpRunnerMock, :run, fn _url, _opts, _ot ->
|
||||
expect(YtDlpRunnerMock, :run, fn _url, _opts, _ot, _addl ->
|
||||
{:ok, media_attributes_return_fixture()}
|
||||
end)
|
||||
|
||||
@@ -91,7 +110,7 @@ defmodule Pinchflat.YtDlp.MediaTest do
|
||||
end
|
||||
|
||||
test "it passes the expected default args" do
|
||||
expect(YtDlpRunnerMock, :run, fn _url, opts, ot ->
|
||||
expect(YtDlpRunnerMock, :run, fn _url, opts, ot, _addl ->
|
||||
assert opts == [:simulate, :skip_download]
|
||||
assert ot == Media.indexing_output_template()
|
||||
|
||||
@@ -101,8 +120,17 @@ defmodule Pinchflat.YtDlp.MediaTest do
|
||||
assert {:ok, _} = Media.get_media_attributes(@media_url)
|
||||
end
|
||||
|
||||
test "passes along additional options" do
|
||||
expect(YtDlpRunnerMock, :run, fn _url, _opts, _ot, addl ->
|
||||
assert [addl_arg: true] = addl
|
||||
{:ok, media_attributes_return_fixture()}
|
||||
end)
|
||||
|
||||
assert {:ok, _} = Media.get_media_attributes(@media_url, addl_arg: true)
|
||||
end
|
||||
|
||||
test "returns the error straight through when the command fails" do
|
||||
expect(YtDlpRunnerMock, :run, fn _url, _opts, _ot -> {:error, "Big issue", 1} end)
|
||||
expect(YtDlpRunnerMock, :run, fn _url, _opts, _ot, _addl -> {:error, "Big issue", 1} end)
|
||||
|
||||
assert {:error, "Big issue", 1} = Media.get_media_attributes(@media_url)
|
||||
end
|
||||
@@ -110,8 +138,12 @@ defmodule Pinchflat.YtDlp.MediaTest do
|
||||
|
||||
describe "indexing_output_template/0" do
|
||||
test "contains all the greatest hits" do
|
||||
assert "%(.{id,title,was_live,webpage_url,description,aspect_ratio,duration,upload_date,timestamp})j" ==
|
||||
Media.indexing_output_template()
|
||||
attrs =
|
||||
~w(id title live_status webpage_url description aspect_ratio duration upload_date timestamp playlist_index)a
|
||||
|
||||
formatted_attrs = "%(.{#{Enum.join(attrs, ",")}})j"
|
||||
|
||||
assert formatted_attrs == Media.indexing_output_template()
|
||||
end
|
||||
end
|
||||
|
||||
@@ -122,11 +154,12 @@ defmodule Pinchflat.YtDlp.MediaTest do
|
||||
"title" => "Trying to Wheelie Without the Rear Brake",
|
||||
"description" => "I'm not sure what I expected.",
|
||||
"webpage_url" => "https://www.youtube.com/watch?v=TiZPUDkDYbk",
|
||||
"was_live" => false,
|
||||
"live_status" => "not_live",
|
||||
"aspect_ratio" => 1.0,
|
||||
"duration" => 60,
|
||||
"upload_date" => "20210101",
|
||||
"timestamp" => 1_600_000_000
|
||||
"timestamp" => 1_600_000_000,
|
||||
"playlist_index" => 1
|
||||
}
|
||||
|
||||
assert %Media{
|
||||
@@ -137,7 +170,8 @@ defmodule Pinchflat.YtDlp.MediaTest do
|
||||
livestream: false,
|
||||
short_form_content: false,
|
||||
uploaded_at: ~U[2020-09-13 12:26:40Z],
|
||||
duration_seconds: 60
|
||||
duration_seconds: 60,
|
||||
playlist_index: 1
|
||||
} == Media.response_to_struct(response)
|
||||
end
|
||||
|
||||
@@ -207,7 +241,7 @@ defmodule Pinchflat.YtDlp.MediaTest do
|
||||
assert %Media{duration_seconds: nil} = Media.response_to_struct(response)
|
||||
end
|
||||
|
||||
test "sets livestream to false if the was_live field isn't present" do
|
||||
test "sets livestream to false if the live_status field isn't present" do
|
||||
response = %{
|
||||
"webpage_url" => "https://www.youtube.com/watch?v=TiZPUDkDYbk",
|
||||
"aspect_ratio" => 1.0,
|
||||
@@ -217,6 +251,17 @@ defmodule Pinchflat.YtDlp.MediaTest do
|
||||
|
||||
assert %Media{livestream: false} = Media.response_to_struct(response)
|
||||
end
|
||||
|
||||
test "doesn't blow up if playlist_index is missing" do
|
||||
response = %{
|
||||
"webpage_url" => "https://www.youtube.com/watch?v=TiZPUDkDYbk",
|
||||
"aspect_ratio" => 1.0,
|
||||
"duration" => nil,
|
||||
"upload_date" => "20210101"
|
||||
}
|
||||
|
||||
assert %Media{playlist_index: 0} = Media.response_to_struct(response)
|
||||
end
|
||||
end
|
||||
|
||||
describe "response_to_struct/1 when testing uploaded_at" do
|
||||
|
||||
@@ -5,10 +5,10 @@ defmodule PinchflatWeb.ErrorHTMLTest do
|
||||
import Phoenix.Template
|
||||
|
||||
test "renders 404.html" do
|
||||
assert render_to_string(PinchflatWeb.ErrorHTML, "404", "html", []) == "Not Found"
|
||||
assert render_to_string(PinchflatWeb.ErrorHTML, "404", "html", []) =~ "404 (not found)"
|
||||
end
|
||||
|
||||
test "renders 500.html" do
|
||||
assert render_to_string(PinchflatWeb.ErrorHTML, "500", "html", []) == "Internal Server Error"
|
||||
assert render_to_string(PinchflatWeb.ErrorHTML, "500", "html", []) =~ "Internal Server Error"
|
||||
end
|
||||
end
|
||||
|
||||
@@ -56,7 +56,7 @@ defmodule PinchflatWeb.MediaItemControllerTest do
|
||||
describe "delete media" do
|
||||
setup do
|
||||
media_item = media_item_with_attachments()
|
||||
stub(UserScriptRunnerMock, :run, fn _event_type, _data -> :ok end)
|
||||
stub(UserScriptRunnerMock, :run, fn _event_type, _data -> {:ok, "", 0} end)
|
||||
|
||||
%{media_item: media_item}
|
||||
end
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
defmodule PinchflatWeb.MediaProfileControllerTest do
|
||||
use PinchflatWeb.ConnCase
|
||||
|
||||
import Pinchflat.MediaFixtures
|
||||
import Pinchflat.SourcesFixtures
|
||||
import Pinchflat.ProfilesFixtures
|
||||
|
||||
alias Pinchflat.Repo
|
||||
alias Pinchflat.Settings
|
||||
alias Pinchflat.Profiles.MediaProfileDeletionWorker
|
||||
|
||||
@create_attrs %{name: "some name", output_path_template: "output_template.{{ ext }}"}
|
||||
@update_attrs %{
|
||||
@@ -23,8 +22,17 @@ defmodule PinchflatWeb.MediaProfileControllerTest do
|
||||
|
||||
describe "index" do
|
||||
test "lists all media_profiles", %{conn: conn} do
|
||||
profile = media_profile_fixture()
|
||||
conn = get(conn, ~p"/media_profiles")
|
||||
|
||||
assert html_response(conn, 200) =~ "Media Profiles"
|
||||
assert html_response(conn, 200) =~ profile.name
|
||||
end
|
||||
|
||||
test "omits profiles that have marked_for_deletion_at set", %{conn: conn} do
|
||||
profile = media_profile_fixture(marked_for_deletion_at: DateTime.utc_now())
|
||||
conn = get(conn, ~p"/media_profiles")
|
||||
refute html_response(conn, 200) =~ profile.name
|
||||
end
|
||||
end
|
||||
|
||||
@@ -102,34 +110,28 @@ defmodule PinchflatWeb.MediaProfileControllerTest do
|
||||
end
|
||||
end
|
||||
|
||||
describe "delete media_profile when just deleting the records" do
|
||||
describe "delete media_profile in all cases" do
|
||||
setup [:create_media_profile]
|
||||
|
||||
test "deletes chosen media_profile and its associations", %{conn: conn, media_profile: media_profile} do
|
||||
source = source_fixture(media_profile_id: media_profile.id)
|
||||
media_item = media_item_with_attachments(%{source_id: source.id})
|
||||
|
||||
conn = delete(conn, ~p"/media_profiles/#{media_profile}")
|
||||
assert redirected_to(conn) == ~p"/media_profiles"
|
||||
|
||||
assert_raise Ecto.NoResultsError, fn -> Repo.reload!(media_profile) end
|
||||
assert_raise Ecto.NoResultsError, fn -> Repo.reload!(source) end
|
||||
assert_raise Ecto.NoResultsError, fn -> Repo.reload!(media_item) end
|
||||
end
|
||||
|
||||
test "redirects to the media_profiles page", %{conn: conn, media_profile: media_profile} do
|
||||
conn = delete(conn, ~p"/media_profiles/#{media_profile}")
|
||||
|
||||
assert redirected_to(conn) == ~p"/media_profiles"
|
||||
end
|
||||
|
||||
test "doesn't delete any files", %{conn: conn, media_profile: media_profile} do
|
||||
source = source_fixture(media_profile_id: media_profile.id)
|
||||
media_item = media_item_with_attachments(%{source_id: source.id})
|
||||
test "sets marked_for_deletion_at", %{conn: conn, media_profile: media_profile} do
|
||||
delete(conn, ~p"/media_profiles/#{media_profile}")
|
||||
assert Repo.reload!(media_profile).marked_for_deletion_at
|
||||
end
|
||||
end
|
||||
|
||||
describe "delete media_profile when just deleting the records" do
|
||||
setup [:create_media_profile]
|
||||
|
||||
test "enqueues a job without the delete_files arg", %{conn: conn, media_profile: media_profile} do
|
||||
delete(conn, ~p"/media_profiles/#{media_profile}")
|
||||
|
||||
assert File.exists?(media_item.media_filepath)
|
||||
assert [%{args: %{"delete_files" => false}}] = all_enqueued(worker: MediaProfileDeletionWorker)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -137,36 +139,15 @@ defmodule PinchflatWeb.MediaProfileControllerTest do
|
||||
setup [:create_media_profile]
|
||||
|
||||
setup do
|
||||
stub(UserScriptRunnerMock, :run, fn _event_type, _data -> :ok end)
|
||||
stub(UserScriptRunnerMock, :run, fn _event_type, _data -> {:ok, "", 0} end)
|
||||
|
||||
:ok
|
||||
end
|
||||
|
||||
test "deletes chosen media_profile and its associations", %{conn: conn, media_profile: media_profile} do
|
||||
source = source_fixture(media_profile_id: media_profile.id)
|
||||
media_item = media_item_with_attachments(%{source_id: source.id})
|
||||
|
||||
conn = delete(conn, ~p"/media_profiles/#{media_profile}?delete_files=true")
|
||||
assert redirected_to(conn) == ~p"/media_profiles"
|
||||
|
||||
assert_raise Ecto.NoResultsError, fn -> Repo.reload!(media_profile) end
|
||||
assert_raise Ecto.NoResultsError, fn -> Repo.reload!(source) end
|
||||
assert_raise Ecto.NoResultsError, fn -> Repo.reload!(media_item) end
|
||||
end
|
||||
|
||||
test "redirects to the media_profiles page", %{conn: conn, media_profile: media_profile} do
|
||||
conn = delete(conn, ~p"/media_profiles/#{media_profile}?delete_files=true")
|
||||
|
||||
assert redirected_to(conn) == ~p"/media_profiles"
|
||||
end
|
||||
|
||||
test "deletes the files", %{conn: conn, media_profile: media_profile} do
|
||||
source = source_fixture(media_profile_id: media_profile.id)
|
||||
media_item = media_item_with_attachments(%{source_id: source.id})
|
||||
|
||||
test "enqueues a job with the delete_files arg", %{conn: conn, media_profile: media_profile} do
|
||||
delete(conn, ~p"/media_profiles/#{media_profile}?delete_files=true")
|
||||
|
||||
refute File.exists?(media_item.media_filepath)
|
||||
assert [%{args: %{"delete_files" => true}}] = all_enqueued(worker: MediaProfileDeletionWorker)
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ defmodule PinchflatWeb.SourceControllerTest do
|
||||
|
||||
alias Pinchflat.Repo
|
||||
alias Pinchflat.Settings
|
||||
alias Pinchflat.Sources.SourceDeletionWorker
|
||||
alias Pinchflat.Downloading.MediaDownloadWorker
|
||||
alias Pinchflat.Metadata.SourceMetadataStorageWorker
|
||||
alias Pinchflat.SlowIndexing.MediaCollectionIndexingWorker
|
||||
@@ -33,8 +34,26 @@ defmodule PinchflatWeb.SourceControllerTest do
|
||||
|
||||
describe "index" do
|
||||
test "lists all sources", %{conn: conn} do
|
||||
source = source_fixture()
|
||||
conn = get(conn, ~p"/sources")
|
||||
|
||||
assert html_response(conn, 200) =~ "Sources"
|
||||
assert html_response(conn, 200) =~ source.custom_name
|
||||
end
|
||||
|
||||
test "omits sources that have marked_for_deletion_at set", %{conn: conn} do
|
||||
source = source_fixture(marked_for_deletion_at: DateTime.utc_now())
|
||||
conn = get(conn, ~p"/sources")
|
||||
|
||||
refute html_response(conn, 200) =~ source.custom_name
|
||||
end
|
||||
|
||||
test "omits sources who's media profile has marked_for_deletion_at set", %{conn: conn} do
|
||||
media_profile = media_profile_fixture(marked_for_deletion_at: DateTime.utc_now())
|
||||
source = source_fixture(media_profile_id: media_profile.id)
|
||||
conn = get(conn, ~p"/sources")
|
||||
|
||||
refute html_response(conn, 200) =~ source.custom_name
|
||||
end
|
||||
end
|
||||
|
||||
@@ -50,11 +69,20 @@ defmodule PinchflatWeb.SourceControllerTest do
|
||||
|
||||
refute html_response(conn, 200) =~ "MENU"
|
||||
end
|
||||
|
||||
test "preloads some attributes when using a template", %{conn: conn} do
|
||||
source = source_fixture(custom_name: "My first source", download_cutoff_date: "2021-01-01")
|
||||
|
||||
conn = get(conn, ~p"/sources/new", %{"template_id" => source.id})
|
||||
assert html_response(conn, 200) =~ "New Source"
|
||||
assert html_response(conn, 200) =~ "2021-01-01"
|
||||
refute html_response(conn, 200) =~ source.custom_name
|
||||
end
|
||||
end
|
||||
|
||||
describe "create source" do
|
||||
test "redirects to show when data is valid", %{conn: conn, create_attrs: create_attrs} do
|
||||
expect(YtDlpRunnerMock, :run, 1, &runner_function_mock/3)
|
||||
expect(YtDlpRunnerMock, :run, 1, &runner_function_mock/4)
|
||||
conn = post(conn, ~p"/sources", source: create_attrs)
|
||||
|
||||
assert %{id: id} = redirected_params(conn)
|
||||
@@ -70,7 +98,7 @@ defmodule PinchflatWeb.SourceControllerTest do
|
||||
end
|
||||
|
||||
test "redirects to onboarding when onboarding", %{conn: conn, create_attrs: create_attrs} do
|
||||
expect(YtDlpRunnerMock, :run, 1, &runner_function_mock/3)
|
||||
expect(YtDlpRunnerMock, :run, 1, &runner_function_mock/4)
|
||||
|
||||
Settings.set(onboarding: true)
|
||||
conn = post(conn, ~p"/sources", source: create_attrs)
|
||||
@@ -99,7 +127,7 @@ defmodule PinchflatWeb.SourceControllerTest do
|
||||
setup [:create_source]
|
||||
|
||||
test "redirects when data is valid", %{conn: conn, source: source, update_attrs: update_attrs} do
|
||||
expect(YtDlpRunnerMock, :run, 1, &runner_function_mock/3)
|
||||
expect(YtDlpRunnerMock, :run, 1, &runner_function_mock/4)
|
||||
|
||||
conn = put(conn, ~p"/sources/#{source}", source: update_attrs)
|
||||
assert redirected_to(conn) == ~p"/sources/#{source}"
|
||||
@@ -118,51 +146,37 @@ defmodule PinchflatWeb.SourceControllerTest do
|
||||
end
|
||||
end
|
||||
|
||||
describe "delete source when just deleting the records" do
|
||||
describe "delete source in all cases" do
|
||||
setup [:create_source]
|
||||
|
||||
test "deletes chosen source and media_items", %{conn: conn, source: source, media_item: media_item} do
|
||||
delete(conn, ~p"/sources/#{source}")
|
||||
|
||||
assert_raise Ecto.NoResultsError, fn -> Repo.reload!(source) end
|
||||
assert_raise Ecto.NoResultsError, fn -> Repo.reload!(media_item) end
|
||||
end
|
||||
|
||||
test "redirects to the sources page", %{conn: conn, source: source} do
|
||||
conn = delete(conn, ~p"/sources/#{source}")
|
||||
assert redirected_to(conn) == ~p"/sources"
|
||||
end
|
||||
|
||||
test "does not delete the files", %{conn: conn, source: source, media_item: media_item} do
|
||||
test "sets marked_for_deletion_at", %{conn: conn, source: source} do
|
||||
delete(conn, ~p"/sources/#{source}")
|
||||
assert File.exists?(media_item.media_filepath)
|
||||
assert Repo.reload!(source).marked_for_deletion_at
|
||||
end
|
||||
end
|
||||
|
||||
describe "delete source when just deleting the records" do
|
||||
setup [:create_source]
|
||||
|
||||
test "enqueues a job without the delete_files arg", %{conn: conn, source: source} do
|
||||
delete(conn, ~p"/sources/#{source}")
|
||||
|
||||
assert [%{args: %{"delete_files" => false}}] = all_enqueued(worker: SourceDeletionWorker)
|
||||
end
|
||||
end
|
||||
|
||||
describe "delete source when deleting the records and files" do
|
||||
setup [:create_source]
|
||||
|
||||
setup do
|
||||
stub(UserScriptRunnerMock, :run, fn _event_type, _data -> :ok end)
|
||||
|
||||
:ok
|
||||
end
|
||||
|
||||
test "deletes chosen source and media_items", %{conn: conn, source: source, media_item: media_item} do
|
||||
test "enqueues a job without the delete_files arg", %{conn: conn, source: source} do
|
||||
delete(conn, ~p"/sources/#{source}?delete_files=true")
|
||||
|
||||
assert_raise Ecto.NoResultsError, fn -> Repo.reload!(source) end
|
||||
assert_raise Ecto.NoResultsError, fn -> Repo.reload!(media_item) end
|
||||
end
|
||||
|
||||
test "redirects to the sources page", %{conn: conn, source: source} do
|
||||
conn = delete(conn, ~p"/sources/#{source}?delete_files=true")
|
||||
assert redirected_to(conn) == ~p"/sources"
|
||||
end
|
||||
|
||||
test "deletes the files", %{conn: conn, source: source, media_item: media_item} do
|
||||
delete(conn, ~p"/sources/#{source}?delete_files=true")
|
||||
refute File.exists?(media_item.media_filepath)
|
||||
assert [%{args: %{"delete_files" => true}}] = all_enqueued(worker: SourceDeletionWorker)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -261,7 +275,7 @@ defmodule PinchflatWeb.SourceControllerTest do
|
||||
%{source: source, media_item: media_item}
|
||||
end
|
||||
|
||||
defp runner_function_mock(_url, _opts, _ot) do
|
||||
defp runner_function_mock(_url, _opts, _ot, _addl) do
|
||||
{
|
||||
:ok,
|
||||
Phoenix.json_library().encode!(%{
|
||||
|
||||
@@ -93,7 +93,7 @@ defmodule Pinchflat.MediaFixtures do
|
||||
id: "video1",
|
||||
title: "Video 1",
|
||||
webpage_url: "https://example.com/video1",
|
||||
was_live: false,
|
||||
live_status: "not_live",
|
||||
description: "desc1",
|
||||
aspect_ratio: 1.67,
|
||||
duration: 123.45,
|
||||
|
||||
@@ -81,7 +81,7 @@ defmodule Pinchflat.SourcesFixtures do
|
||||
id: "video1",
|
||||
title: "Video 1",
|
||||
webpage_url: "https://example.com/video1",
|
||||
was_live: false,
|
||||
live_status: "not_live",
|
||||
description: "desc1",
|
||||
aspect_ratio: 1.67,
|
||||
duration: 12.34,
|
||||
@@ -91,7 +91,7 @@ defmodule Pinchflat.SourcesFixtures do
|
||||
id: "video2",
|
||||
title: "Video 2",
|
||||
webpage_url: "https://example.com/video2",
|
||||
was_live: true,
|
||||
live_status: "is_live",
|
||||
description: "desc2",
|
||||
aspect_ratio: 1.67,
|
||||
duration: 345.67,
|
||||
@@ -101,7 +101,7 @@ defmodule Pinchflat.SourcesFixtures do
|
||||
id: "video3",
|
||||
title: "Video 3",
|
||||
webpage_url: "https://example.com/video3",
|
||||
was_live: false,
|
||||
live_status: "not_live",
|
||||
description: "desc3",
|
||||
aspect_ratio: 1.0,
|
||||
duration: 678.90,
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
../assets/vendor/
|
||||
Executable
+14
@@ -0,0 +1,14 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Get the current date in the format YYYY.MM.DD (stripping leading zeros)
|
||||
DATE=$(date +"%Y.%-m.%-d")
|
||||
|
||||
# Get the current version from mix.exs
|
||||
VERSION=$(grep "version: " mix.exs | cut -d '"' -f2)
|
||||
|
||||
echo "Bumping version from $VERSION to $DATE"
|
||||
# Replace the version in mix.exs with the new version
|
||||
sed -i "s/version: \"$VERSION\"/version: \"$DATE\"/g" mix.exs
|
||||
|
||||
# Run checks to ensure it's a valid mix.exs file
|
||||
mix check
|
||||
Reference in New Issue
Block a user