Compare commits
23 Commits
v2024.6.25
...
v2024.8.6
| Author | SHA1 | Date | |
|---|---|---|---|
| 2326775d78 | |||
| e0a84a521b | |||
| d217ad5744 | |||
| d5ae41cdab | |||
| 7a01db05dd | |||
| d392fc3818 | |||
| 47596d5f72 | |||
| dbc25e5730 | |||
| a3ebf59b29 | |||
| 8dd8e49051 | |||
| 029981b7ee | |||
| d423e4486f | |||
| 4c7a63c970 | |||
| 4822127e3f | |||
| 4f25878f2f | |||
| aec56e3a03 | |||
| e06e050f95 | |||
| 5a10015634 | |||
| 0d5a41fd35 | |||
| 8f91c4e6a2 | |||
| 7f1daf90ca | |||
| bb0e104b7e | |||
| c0b8dec520 |
@@ -16,24 +16,28 @@ jobs:
|
|||||||
if: "! contains(toJSON(github.event.commits.*.message), '[skip ci]')"
|
if: "! contains(toJSON(github.event.commits.*.message), '[skip ci]')"
|
||||||
env:
|
env:
|
||||||
COMPOSE_FILE: ./docker-compose.ci.yml
|
COMPOSE_FILE: ./docker-compose.ci.yml
|
||||||
|
MIX_ENV: test
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Pull prebuilt images
|
- name: Pull prebuilt images
|
||||||
run: docker compose pull
|
run: docker compose pull
|
||||||
|
|
||||||
- name: Setup Docker layer caching
|
- name: Set up Docker Buildx
|
||||||
uses: jpribyl/action-docker-layer-caching@v0.1.1
|
uses: docker/setup-buildx-action@v3
|
||||||
continue-on-error: true
|
|
||||||
with:
|
|
||||||
key: ci-docker-cache-{hash}
|
|
||||||
restore-keys: |
|
|
||||||
ci-docker-cache-
|
|
||||||
layer-ci-docker-cache-
|
|
||||||
|
|
||||||
- name: Build and Run Docker image
|
- name: Build docker image
|
||||||
|
uses: docker/build-push-action@v5
|
||||||
|
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
|
run: docker compose up --detach
|
||||||
|
|
||||||
# NOTE: All exec commands use the -T flag to compensate for
|
# 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
|
# See https://github.com/actions/runner/issues/241 and https://github.com/docker/compose/issues/8537
|
||||||
- name: Install Elixir and JS deps
|
- name: Install Elixir and JS deps
|
||||||
run: |
|
run: |
|
||||||
docker compose exec -T phx yarn install && cd assets && yarn install && cd ..
|
docker compose exec -T phx mix deps.get && yarn install && cd assets && yarn install && cd ..
|
||||||
docker compose exec -T phx mix deps.get
|
|
||||||
|
|
||||||
- name: Create and Migrate database
|
- name: Create and Migrate database
|
||||||
run: |
|
run: |
|
||||||
|
|||||||
+2
-2
@@ -65,7 +65,7 @@ config :pinchflat, Oban,
|
|||||||
media_indexing: 2,
|
media_indexing: 2,
|
||||||
media_collection_indexing: 2,
|
media_collection_indexing: 2,
|
||||||
media_fetching: 2,
|
media_fetching: 2,
|
||||||
local_metadata: 8,
|
local_data: 8,
|
||||||
remote_metadata: 4
|
remote_metadata: 4
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -102,7 +102,7 @@ config :tailwind,
|
|||||||
|
|
||||||
# Configures Elixir's Logger
|
# Configures Elixir's Logger
|
||||||
config :logger, :console,
|
config :logger, :console,
|
||||||
format: "$time $metadata[$level] $message\n",
|
format: "$date $time $metadata[$level] | $message\n",
|
||||||
metadata: [:request_id]
|
metadata: [:request_id]
|
||||||
|
|
||||||
# Use Jason for JSON parsing in Phoenix
|
# Use Jason for JSON parsing in Phoenix
|
||||||
|
|||||||
@@ -7,6 +7,9 @@ services:
|
|||||||
- MIX_ENV=test
|
- MIX_ENV=test
|
||||||
volumes:
|
volumes:
|
||||||
- '.:/app'
|
- '.:/app'
|
||||||
|
# These lines ensure the deps can be saved as build artifacts for caching
|
||||||
|
- '/app/deps'
|
||||||
|
- '/app/_build'
|
||||||
ports:
|
ports:
|
||||||
- '4008:4008'
|
- '4008:4008'
|
||||||
command: tail -F /dev/null
|
command: tail -F /dev/null
|
||||||
|
|||||||
+23
-22
@@ -9,10 +9,10 @@ ARG TARGETPLATFORM
|
|||||||
RUN echo "Building for ${TARGETPLATFORM:?}"
|
RUN echo "Building for ${TARGETPLATFORM:?}"
|
||||||
|
|
||||||
# Install debian packages
|
# Install debian packages
|
||||||
RUN apt-get update -qq
|
RUN apt-get update -qq && \
|
||||||
RUN apt-get install -y inotify-tools curl git openssh-client jq \
|
apt-get install -y inotify-tools curl git openssh-client jq \
|
||||||
python3 python3-setuptools python3-wheel python3-dev pipx \
|
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
|
# Install ffmpeg
|
||||||
RUN export FFMPEG_DOWNLOAD=$(case ${TARGETPLATFORM:-linux/amd64} in \
|
RUN export FFMPEG_DOWNLOAD=$(case ${TARGETPLATFORM:-linux/amd64} in \
|
||||||
@@ -23,31 +23,31 @@ 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/ "ffmpeg" && \
|
||||||
tar -xf /tmp/ffmpeg.tar.xz --strip-components=2 --no-anchored -C /usr/bin/ "ffprobe"
|
tar -xf /tmp/ffmpeg.tar.xz --strip-components=2 --no-anchored -C /usr/bin/ "ffprobe"
|
||||||
|
|
||||||
# Install nodejs
|
# Install nodejs and Yarn
|
||||||
RUN curl -sL https://deb.nodesource.com/setup_20.x -o nodesource_setup.sh
|
RUN curl -sL https://deb.nodesource.com/setup_20.x -o nodesource_setup.sh && \
|
||||||
RUN bash nodesource_setup.sh
|
bash nodesource_setup.sh && \
|
||||||
RUN apt-get install nodejs
|
apt-get install -y nodejs && \
|
||||||
RUN npm install -g yarn
|
npm install -g yarn && \
|
||||||
|
|
||||||
# Install baseline Elixir packages
|
# Install baseline Elixir packages
|
||||||
RUN mix local.hex --force
|
mix local.hex --force && \
|
||||||
RUN mix local.rebar --force
|
mix local.rebar --force && \
|
||||||
|
|
||||||
# Download and update YT-DLP
|
# 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
|
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
|
chmod a+rx /usr/local/bin/yt-dlp && \
|
||||||
RUN yt-dlp -U
|
yt-dlp -U && \
|
||||||
|
|
||||||
# Install Apprise
|
# Install Apprise
|
||||||
RUN export PIPX_HOME=/opt/pipx && \
|
export PIPX_HOME=/opt/pipx && \
|
||||||
export PIPX_BIN_DIR=/usr/local/bin && \
|
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
|
# Set the locale
|
||||||
RUN sed -i '/en_US.UTF-8/s/^# //g' /etc/locale.gen && locale-gen
|
RUN sed -i '/en_US.UTF-8/s/^# //g' /etc/locale.gen && locale-gen
|
||||||
ENV LANG en_US.UTF-8
|
ENV LANG=en_US.UTF-8
|
||||||
ENV LANGUAGE en_US:en
|
ENV LANGUAGE=en_US:en
|
||||||
ENV LC_ALL en_US.UTF-8
|
ENV LC_ALL=en_US.UTF-8
|
||||||
|
|
||||||
# Create app directory and copy the Elixir projects into it.
|
# Create app directory and copy the Elixir projects into it.
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
@@ -55,7 +55,8 @@ COPY . ./
|
|||||||
|
|
||||||
# Install Elixir deps
|
# Install Elixir deps
|
||||||
# RUN mix archive.install github hexpm/hex branch latest
|
# RUN mix archive.install github hexpm/hex branch latest
|
||||||
RUN mix deps.get
|
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
|
||||||
# Gives us iex shell history
|
# Gives us iex shell history
|
||||||
ENV ERL_AFLAGS="-kernel shell_history enabled"
|
ENV ERL_AFLAGS="-kernel shell_history enabled"
|
||||||
|
|
||||||
|
|||||||
@@ -2,12 +2,12 @@
|
|||||||
|
|
||||||
set -e
|
set -e
|
||||||
|
|
||||||
# Ensure the app's deps are installed
|
echo "\nInstalling Elixir deps..."
|
||||||
mix deps.get
|
mix deps.get
|
||||||
|
|
||||||
# Install JS deps
|
# Install both project-level and assets-level JS dependencies
|
||||||
echo "\nInstalling JS..."
|
echo "\nInstalling JS deps..."
|
||||||
cd assets && yarn install
|
yarn install && cd assets && yarn install
|
||||||
cd ..
|
cd ..
|
||||||
|
|
||||||
# Potentially Set up the database
|
# Potentially Set up the database
|
||||||
|
|||||||
@@ -74,6 +74,9 @@ defmodule Pinchflat.Downloading.DownloadOptionBuilder do
|
|||||||
{{:download_auto_subs, true}, %{download_subs: true}} ->
|
{{:download_auto_subs, true}, %{download_subs: true}} ->
|
||||||
acc ++ [:write_auto_subs]
|
acc ++ [:write_auto_subs]
|
||||||
|
|
||||||
|
{{:download_auto_subs, true}, %{embed_subs: true}} ->
|
||||||
|
acc ++ [:write_auto_subs]
|
||||||
|
|
||||||
{{:embed_subs, true}, %{preferred_resolution: pr}} when pr != :audio ->
|
{{:embed_subs, true}, %{preferred_resolution: pr}} when pr != :audio ->
|
||||||
acc ++ [:embed_subs]
|
acc ++ [:embed_subs]
|
||||||
|
|
||||||
@@ -203,10 +206,8 @@ defmodule Pinchflat.Downloading.DownloadOptionBuilder do
|
|||||||
"source_collection_id" => source.collection_id,
|
"source_collection_id" => source.collection_id,
|
||||||
"source_collection_name" => source.collection_name,
|
"source_collection_name" => source.collection_name,
|
||||||
"source_collection_type" => to_string(source.collection_type),
|
"source_collection_type" => to_string(source.collection_type),
|
||||||
"media_upload_date_index" =>
|
"media_playlist_index" => pad_int(media_item_with_preloads.playlist_index),
|
||||||
media_item_with_preloads.upload_date_index
|
"media_upload_date_index" => pad_int(media_item_with_preloads.upload_date_index)
|
||||||
|> to_string()
|
|
||||||
|> String.pad_leading(2, "0")
|
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -223,6 +224,12 @@ defmodule Pinchflat.Downloading.DownloadOptionBuilder do
|
|||||||
|> build_output_path(media_item_with_preloads)
|
|> build_output_path(media_item_with_preloads)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
defp pad_int(integer, count \\ 2, padding \\ "0") do
|
||||||
|
integer
|
||||||
|
|> to_string()
|
||||||
|
|> String.pad_leading(count, padding)
|
||||||
|
end
|
||||||
|
|
||||||
defp base_directory do
|
defp base_directory do
|
||||||
Application.get_env(:pinchflat, :media_directory)
|
Application.get_env(:pinchflat, :media_directory)
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -91,8 +91,7 @@ defmodule Pinchflat.Downloading.DownloadingHelpers do
|
|||||||
[m, s, mp],
|
[m, s, mp],
|
||||||
^MediaQuery.for_source(source) and
|
^MediaQuery.for_source(source) and
|
||||||
^MediaQuery.downloaded() and
|
^MediaQuery.downloaded() and
|
||||||
not (^MediaQuery.download_prevented()) and
|
not (^MediaQuery.download_prevented())
|
||||||
not (^MediaQuery.culled())
|
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|> Repo.all()
|
|> Repo.all()
|
||||||
|
|||||||
@@ -39,17 +39,14 @@ defmodule Pinchflat.Downloading.MediaDownloadWorker do
|
|||||||
- `quality_upgrade?`: re-downloads media, including the video. Does not force download
|
- `quality_upgrade?`: re-downloads media, including the video. Does not force download
|
||||||
if the source is set to not download media
|
if the source is set to not download media
|
||||||
|
|
||||||
Returns :ok | {:ok, %MediaItem{}} | {:error, any, ...any}
|
Returns :ok | {:error, any, ...any}
|
||||||
"""
|
"""
|
||||||
@impl Oban.Worker
|
@impl Oban.Worker
|
||||||
def perform(%Oban.Job{args: %{"id" => media_item_id} = args}) do
|
def perform(%Oban.Job{args: %{"id" => media_item_id} = args}) do
|
||||||
should_force = Map.get(args, "force", false)
|
should_force = Map.get(args, "force", false)
|
||||||
is_quality_upgrade = Map.get(args, "quality_upgrade?", false)
|
is_quality_upgrade = Map.get(args, "quality_upgrade?", false)
|
||||||
|
|
||||||
media_item =
|
media_item = fetch_and_run_prevent_download_user_script(media_item_id)
|
||||||
media_item_id
|
|
||||||
|> Media.get_media_item!()
|
|
||||||
|> Repo.preload(:source)
|
|
||||||
|
|
||||||
# If the source or media item is set to not download media, perform a no-op unless forced
|
# 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
|
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")
|
Ecto.StaleEntryError -> Logger.info("#{__MODULE__} discarded: media item #{media_item_id} stale")
|
||||||
end
|
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
|
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
|
overwrite_behaviour = if should_force || is_quality_upgrade, do: :force_overwrites, else: :no_force_overwrites
|
||||||
override_opts = [overwrite_behaviour: overwrite_behaviour]
|
override_opts = [overwrite_behaviour: overwrite_behaviour]
|
||||||
@@ -74,9 +85,9 @@ defmodule Pinchflat.Downloading.MediaDownloadWorker do
|
|||||||
media_redownloaded_at: get_redownloaded_at(is_quality_upgrade)
|
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, _} ->
|
{:recovered, _} ->
|
||||||
{:error, :retry}
|
{: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
|
# 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.
|
# 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 = Application.get_env(:pinchflat, :user_script_runner, UserScriptRunner)
|
||||||
|
|
||||||
runner.run(:media_downloaded, media_item)
|
runner.run(event, media_item)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -79,6 +79,7 @@ defmodule Pinchflat.Downloading.MediaDownloader do
|
|||||||
|> MetadataParser.parse_for_media_item()
|
|> MetadataParser.parse_for_media_item()
|
||||||
|> Map.merge(%{
|
|> Map.merge(%{
|
||||||
media_downloaded_at: DateTime.utc_now(),
|
media_downloaded_at: DateTime.utc_now(),
|
||||||
|
culled_at: nil,
|
||||||
nfo_filepath: determine_nfo_filepath(media_with_preloads, parsed_json),
|
nfo_filepath: determine_nfo_filepath(media_with_preloads, parsed_json),
|
||||||
metadata: %{
|
metadata: %{
|
||||||
# IDEA: might be worth kicking off a job for this since thumbnail fetching
|
# IDEA: might be worth kicking off a job for this since thumbnail fetching
|
||||||
|
|||||||
@@ -23,10 +23,10 @@ defmodule Pinchflat.Downloading.MediaQualityUpgradeWorker do
|
|||||||
"""
|
"""
|
||||||
@impl Oban.Worker
|
@impl Oban.Worker
|
||||||
def perform(%Oban.Job{}) do
|
def perform(%Oban.Job{}) do
|
||||||
redownloadable_media = Media.list_redownloadable_media_items()
|
upgradable_media = Media.list_upgradeable_media_items()
|
||||||
Logger.info("Redownloading #{length(redownloadable_media)} 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})
|
MediaDownloadWorker.kickoff_with_task(media_item, %{quality_upgrade?: true})
|
||||||
end)
|
end)
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -2,12 +2,15 @@ defmodule Pinchflat.Downloading.MediaRetentionWorker do
|
|||||||
@moduledoc false
|
@moduledoc false
|
||||||
|
|
||||||
use Oban.Worker,
|
use Oban.Worker,
|
||||||
queue: :local_metadata,
|
queue: :local_data,
|
||||||
unique: [period: :infinity, states: [:available, :scheduled, :retryable, :executing]],
|
unique: [period: :infinity, states: [:available, :scheduled, :retryable, :executing]],
|
||||||
tags: ["media_item", "local_metadata"]
|
tags: ["media_item", "local_data"]
|
||||||
|
|
||||||
|
use Pinchflat.Media.MediaQuery
|
||||||
|
|
||||||
require Logger
|
require Logger
|
||||||
|
|
||||||
|
alias Pinchflat.Repo
|
||||||
alias Pinchflat.Media
|
alias Pinchflat.Media
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
@@ -20,14 +23,51 @@ defmodule Pinchflat.Downloading.MediaRetentionWorker do
|
|||||||
"""
|
"""
|
||||||
@impl Oban.Worker
|
@impl Oban.Worker
|
||||||
def perform(%Oban.Job{}) do
|
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")
|
Logger.info("Culling #{length(cullable_media)} media items past their retention date")
|
||||||
|
|
||||||
Enum.each(cullable_media, fn media_item ->
|
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, %{
|
Media.delete_media_files(media_item, %{
|
||||||
prevent_download: true,
|
prevent_download: true,
|
||||||
culled_at: DateTime.utc_now()
|
culled_at: DateTime.utc_now()
|
||||||
})
|
})
|
||||||
end)
|
end)
|
||||||
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
|
end
|
||||||
|
|||||||
@@ -54,7 +54,12 @@ defmodule Pinchflat.Downloading.OutputPathBuilder do
|
|||||||
"season_from_date" => "%(upload_date>%Y)S",
|
"season_from_date" => "%(upload_date>%Y)S",
|
||||||
"season_episode_from_date" => "s%(upload_date>%Y)Se%(upload_date>%m%d)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 }}",
|
"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
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ defmodule Pinchflat.Lifecycle.UserScripts.CommandRunner do
|
|||||||
@behaviour UserScriptCommandRunner
|
@behaviour UserScriptCommandRunner
|
||||||
|
|
||||||
@event_types [
|
@event_types [
|
||||||
|
:media_pre_download,
|
||||||
:media_downloaded,
|
:media_downloaded,
|
||||||
:media_deleted
|
: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
|
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.
|
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
|
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
|
type or if the passed data cannot be encoded into JSON - both indicative of
|
||||||
failures in the development process.
|
failures in the development process.
|
||||||
|
|
||||||
Returns :ok
|
Returns {:ok, :no_executable} | {:ok, output, exit_code}
|
||||||
"""
|
"""
|
||||||
@impl UserScriptCommandRunner
|
@impl UserScriptCommandRunner
|
||||||
def run(event_type, encodable_data) when event_type in @event_types do
|
def run(event_type, encodable_data) when event_type in @event_types do
|
||||||
case backend_executable() do
|
case backend_executable() do
|
||||||
{:ok, :no_executable} ->
|
{:ok, :no_executable} ->
|
||||||
:ok
|
{:ok, :no_executable}
|
||||||
|
|
||||||
{:ok, executable_path} ->
|
{:ok, executable_path} ->
|
||||||
{:ok, encoded_data} = Phoenix.json_library().encode(encodable_data)
|
{:ok, encoded_data} = Phoenix.json_library().encode(encodable_data)
|
||||||
|
|
||||||
{_output, _exit_code} =
|
{output, exit_code} =
|
||||||
CliUtils.wrap_cmd(
|
CliUtils.wrap_cmd(
|
||||||
executable_path,
|
executable_path,
|
||||||
[to_string(event_type), encoded_data],
|
[to_string(event_type), encoded_data],
|
||||||
@@ -47,7 +49,7 @@ defmodule Pinchflat.Lifecycle.UserScripts.CommandRunner do
|
|||||||
logging_arg_override: "[suppressed]"
|
logging_arg_override: "[suppressed]"
|
||||||
)
|
)
|
||||||
|
|
||||||
:ok
|
{:ok, output, exit_code}
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -62,7 +64,7 @@ defmodule Pinchflat.Lifecycle.UserScripts.CommandRunner do
|
|||||||
if FilesystemUtils.exists_and_nonempty?(filepath) do
|
if FilesystemUtils.exists_and_nonempty?(filepath) do
|
||||||
{:ok, filepath}
|
{:ok, filepath}
|
||||||
else
|
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}
|
{:ok, :no_executable}
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -25,21 +25,10 @@ defmodule Pinchflat.Media do
|
|||||||
end
|
end
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
Returns a list of media_items that are cullable based on the retention period
|
Returns a list of media_items that are upgradeable based on the redownload delay
|
||||||
of the source they belong to.
|
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
|
||||||
Returns [%MediaItem{}, ...]
|
or have better sponsorblock segments (or similar).
|
||||||
"""
|
|
||||||
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.
|
|
||||||
|
|
||||||
The logic is that a media_item is past_redownload_delay if the media_item's uploaded_at is
|
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`
|
at least redownload_delay_days ago AND `media_downloaded_at` - `redownload_delay_days`
|
||||||
@@ -52,10 +41,10 @@ defmodule Pinchflat.Media do
|
|||||||
|
|
||||||
Returns [%MediaItem{}, ...]
|
Returns [%MediaItem{}, ...]
|
||||||
"""
|
"""
|
||||||
def list_redownloadable_media_items do
|
def list_upgradeable_media_items do
|
||||||
MediaQuery.new()
|
MediaQuery.new()
|
||||||
|> MediaQuery.require_assoc(:media_profile)
|
|> MediaQuery.require_assoc(:media_profile)
|
||||||
|> where(^MediaQuery.redownloadable())
|
|> where(^MediaQuery.upgradeable())
|
||||||
|> Repo.all()
|
|> Repo.all()
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -142,12 +131,17 @@ defmodule Pinchflat.Media do
|
|||||||
"""
|
"""
|
||||||
def create_media_item_from_backend_attrs(source, media_attrs_struct) 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))
|
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{}
|
||||||
|> MediaItem.changeset(attrs)
|
|> MediaItem.changeset(attrs)
|
||||||
|> Repo.insert(
|
|> Repo.insert(
|
||||||
on_conflict: [
|
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]
|
conflict_target: [:source_id, :media_id]
|
||||||
)
|
)
|
||||||
@@ -177,7 +171,7 @@ defmodule Pinchflat.Media do
|
|||||||
|
|
||||||
if delete_files do
|
if delete_files do
|
||||||
{:ok, _} = do_delete_media_files(media_item)
|
{:ok, _} = do_delete_media_files(media_item)
|
||||||
:ok = run_user_script(:media_deleted, media_item)
|
run_user_script(:media_deleted, media_item)
|
||||||
end
|
end
|
||||||
|
|
||||||
# Should delete these no matter what
|
# Should delete these no matter what
|
||||||
@@ -200,7 +194,7 @@ defmodule Pinchflat.Media do
|
|||||||
|
|
||||||
Tasks.delete_tasks_for(media_item)
|
Tasks.delete_tasks_for(media_item)
|
||||||
{:ok, _} = do_delete_media_files(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))
|
update_media_item(media_item, Map.merge(filepath_attrs, addl_attrs))
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -18,6 +18,8 @@ defmodule Pinchflat.Media.MediaItem do
|
|||||||
alias Pinchflat.Media.MediaItemsSearchIndex
|
alias Pinchflat.Media.MediaItemsSearchIndex
|
||||||
|
|
||||||
@allowed_fields [
|
@allowed_fields [
|
||||||
|
# these fields are only captured on index
|
||||||
|
:playlist_index,
|
||||||
# these fields are captured on indexing (and again on download)
|
# these fields are captured on indexing (and again on download)
|
||||||
:title,
|
:title,
|
||||||
:media_id,
|
:media_id,
|
||||||
@@ -72,6 +74,7 @@ defmodule Pinchflat.Media.MediaItem do
|
|||||||
field :uploaded_at, :utc_datetime
|
field :uploaded_at, :utc_datetime
|
||||||
field :upload_date_index, :integer, default: 0
|
field :upload_date_index, :integer, default: 0
|
||||||
field :duration_seconds, :integer
|
field :duration_seconds, :integer
|
||||||
|
field :playlist_index, :integer, default: 0
|
||||||
|
|
||||||
field :media_filepath, :string
|
field :media_filepath, :string
|
||||||
field :media_size_bytes, :integer
|
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 downloaded, do: dynamic([mi], not is_nil(mi.media_filepath))
|
||||||
def download_prevented, do: dynamic([mi], mi.prevent_download == true)
|
def download_prevented, do: dynamic([mi], mi.prevent_download == true)
|
||||||
def culling_prevented, do: dynamic([mi], mi.prevent_culling == 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 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))
|
def upload_date_matches(other_date), do: dynamic([mi], fragment("date(?) = date(?)", mi.uploaded_at, ^other_date))
|
||||||
|
|
||||||
@@ -108,6 +107,15 @@ defmodule Pinchflat.Media.MediaQuery do
|
|||||||
)
|
)
|
||||||
end
|
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
|
def pending do
|
||||||
dynamic(
|
dynamic(
|
||||||
[mi],
|
[mi],
|
||||||
@@ -119,12 +127,11 @@ defmodule Pinchflat.Media.MediaQuery do
|
|||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
def redownloadable do
|
def upgradeable do
|
||||||
dynamic(
|
dynamic(
|
||||||
[mi, source],
|
[mi, source],
|
||||||
^downloaded() and
|
^downloaded() and
|
||||||
not (^download_prevented()) and
|
not (^download_prevented()) and
|
||||||
not (^culled()) and
|
|
||||||
not (^redownloaded()) and
|
not (^redownloaded()) and
|
||||||
^past_redownload_delay()
|
^past_redownload_delay()
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -124,6 +124,21 @@ defmodule Pinchflat.Metadata.MetadataFileHelpers do
|
|||||||
end
|
end
|
||||||
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
|
defp generate_filepath_for(database_record, filename) do
|
||||||
Path.join([
|
Path.join([
|
||||||
metadata_directory_for(database_record),
|
metadata_directory_for(database_record),
|
||||||
|
|||||||
@@ -6,8 +6,8 @@ defmodule Pinchflat.Metadata.NfoBuilder do
|
|||||||
|
|
||||||
import Pinchflat.Utils.XmlUtils, only: [safe: 1]
|
import Pinchflat.Utils.XmlUtils, only: [safe: 1]
|
||||||
|
|
||||||
alias Pinchflat.Metadata.MetadataFileHelpers
|
|
||||||
alias Pinchflat.Utils.FilesystemUtils
|
alias Pinchflat.Utils.FilesystemUtils
|
||||||
|
alias Pinchflat.Metadata.MetadataFileHelpers
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
Builds an NFO file for a media item (read: single "episode") and
|
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.
|
Returns the filepath of the NFO file.
|
||||||
"""
|
"""
|
||||||
def build_and_store_for_media_item(filepath, metadata) do
|
def build_and_store_for_media_item(nfo_filepath, metadata) do
|
||||||
nfo = build_for_media_item(metadata)
|
nfo = build_for_media_item(nfo_filepath, metadata)
|
||||||
|
|
||||||
FilesystemUtils.write_p!(filepath, nfo)
|
FilesystemUtils.write_p!(nfo_filepath, nfo)
|
||||||
|
|
||||||
filepath
|
nfo_filepath
|
||||||
end
|
end
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
@@ -37,10 +37,15 @@ defmodule Pinchflat.Metadata.NfoBuilder do
|
|||||||
filepath
|
filepath
|
||||||
end
|
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"])
|
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.
|
# 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" ?>
|
<?xml version="1.0" encoding="UTF-8" standalone="yes" ?>
|
||||||
<episodedetails>
|
<episodedetails>
|
||||||
@@ -49,8 +54,8 @@ defmodule Pinchflat.Metadata.NfoBuilder do
|
|||||||
<uniqueid type="youtube" default="true">#{safe(metadata["id"])}</uniqueid>
|
<uniqueid type="youtube" default="true">#{safe(metadata["id"])}</uniqueid>
|
||||||
<plot>#{safe(metadata["description"])}</plot>
|
<plot>#{safe(metadata["description"])}</plot>
|
||||||
<aired>#{safe(upload_date)}</aired>
|
<aired>#{safe(upload_date)}</aired>
|
||||||
<season>#{safe(upload_date.year)}</season>
|
<season>#{safe(season)}</season>
|
||||||
<episode>#{Calendar.strftime(upload_date, "%m%d")}</episode>
|
<episode>#{episode}</episode>
|
||||||
<genre>YouTube</genre>
|
<genre>YouTube</genre>
|
||||||
</episodedetails>
|
</episodedetails>
|
||||||
"""
|
"""
|
||||||
@@ -67,4 +72,11 @@ defmodule Pinchflat.Metadata.NfoBuilder do
|
|||||||
</tvshow>
|
</tvshow>
|
||||||
"""
|
"""
|
||||||
end
|
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
|
end
|
||||||
|
|||||||
@@ -20,29 +20,49 @@ defmodule Pinchflat.Metadata.SourceImageParser do
|
|||||||
def store_source_images(base_directory, source_metadata) do
|
def store_source_images(base_directory, source_metadata) do
|
||||||
(source_metadata["thumbnails"] || [])
|
(source_metadata["thumbnails"] || [])
|
||||||
|> Enum.filter(&(&1["filepath"] != nil))
|
|> Enum.filter(&(&1["filepath"] != nil))
|
||||||
|> select_useful_images()
|
|> select_useful_images(source_metadata)
|
||||||
|> Enum.map(&move_image(&1, base_directory))
|
|> Enum.map(&move_image(&1, base_directory))
|
||||||
|> Enum.into(%{})
|
|> Enum.into(%{})
|
||||||
end
|
end
|
||||||
|
|
||||||
defp select_useful_images(images) do
|
defp select_useful_images(images, source_metadata) do
|
||||||
labelled_images =
|
labelled_images =
|
||||||
Enum.reduce(images, [], fn image_map, acc ->
|
Enum.reduce(images, %{}, fn image_map, acc ->
|
||||||
case image_map do
|
case image_map do
|
||||||
%{"id" => "avatar_uncropped"} ->
|
%{"id" => "avatar_uncropped"} -> put_image_key(acc, :poster, image_map["filepath"])
|
||||||
acc ++ [{:poster, :poster_filepath, image_map["filepath"]}]
|
%{"id" => "banner_uncropped"} -> put_image_key(acc, :fanart, image_map["filepath"])
|
||||||
|
_ -> acc
|
||||||
%{"id" => "banner_uncropped"} ->
|
|
||||||
acc ++ [{:fanart, :fanart_filepath, image_map["filepath"]}]
|
|
||||||
|
|
||||||
_ ->
|
|
||||||
acc
|
|
||||||
end
|
end
|
||||||
end)
|
end)
|
||||||
|
|
||||||
labelled_images
|
labelled_images
|
||||||
|> Enum.concat([{:banner, :banner_filepath, determine_best_banner(images)}])
|
|> add_fallback_poster(source_metadata)
|
||||||
|> Enum.filter(fn {_, _, tmp_filepath} -> tmp_filepath end)
|
|> 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
|
end
|
||||||
|
|
||||||
defp determine_best_banner(images) do
|
defp determine_best_banner(images) do
|
||||||
@@ -58,12 +78,22 @@ defmodule Pinchflat.Metadata.SourceImageParser do
|
|||||||
Map.get(best_candidate || %{}, "filepath")
|
Map.get(best_candidate || %{}, "filepath")
|
||||||
end
|
end
|
||||||
|
|
||||||
defp move_image({filename, source_attr_name, tmp_filepath}, base_directory) do
|
defp move_image({_key, attrs}, base_directory) do
|
||||||
extension = Path.extname(tmp_filepath)
|
extension = Path.extname(attrs.current_filepath)
|
||||||
final_filepath = Path.join([base_directory, "#{filename}#{extension}"])
|
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
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -77,10 +77,8 @@ defmodule Pinchflat.Metadata.SourceMetadataStorageWorker do
|
|||||||
|
|
||||||
defp fetch_source_metadata_and_images(series_directory, source) do
|
defp fetch_source_metadata_and_images(series_directory, source) do
|
||||||
metadata_directory = MetadataFileHelpers.metadata_directory_for(source)
|
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)
|
metadata_image_attrs = SourceImageParser.store_source_images(metadata_directory, metadata)
|
||||||
|
|
||||||
if source.media_profile.download_source_images && series_directory do
|
if source.media_profile.download_source_images && series_directory do
|
||||||
@@ -110,6 +108,20 @@ defmodule Pinchflat.Metadata.SourceMetadataStorageWorker do
|
|||||||
end
|
end
|
||||||
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)
|
||||||
|
end
|
||||||
|
|
||||||
defp tmp_directory do
|
defp tmp_directory do
|
||||||
Application.get_env(:pinchflat, :tmpfile_directory)
|
Application.get_env(:pinchflat, :tmpfile_directory)
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ defmodule Pinchflat.Profiles.MediaProfile do
|
|||||||
livestream_behaviour
|
livestream_behaviour
|
||||||
preferred_resolution
|
preferred_resolution
|
||||||
redownload_delay_days
|
redownload_delay_days
|
||||||
|
marked_for_deletion_at
|
||||||
)a
|
)a
|
||||||
|
|
||||||
@required_fields ~w(name output_path_template)a
|
@required_fields ~w(name output_path_template)a
|
||||||
@@ -65,6 +66,8 @@ defmodule Pinchflat.Profiles.MediaProfile do
|
|||||||
field :livestream_behaviour, Ecto.Enum, values: ~w(include exclude only)a, default: :include
|
field :livestream_behaviour, Ecto.Enum, values: ~w(include exclude only)a, default: :include
|
||||||
field :preferred_resolution, Ecto.Enum, values: ~w(4320p 2160p 1080p 720p 480p 360p audio)a, default: :"1080p"
|
field :preferred_resolution, Ecto.Enum, values: ~w(4320p 2160p 1080p 720p 480p 360p audio)a, default: :"1080p"
|
||||||
|
|
||||||
|
field :marked_for_deletion_at, :utc_datetime
|
||||||
|
|
||||||
has_many :sources, Source
|
has_many :sources, Source
|
||||||
|
|
||||||
timestamps(type: :utc_datetime)
|
timestamps(type: :utc_datetime)
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -35,6 +35,7 @@ defmodule Pinchflat.Sources.Source do
|
|||||||
title_filter_regex
|
title_filter_regex
|
||||||
media_profile_id
|
media_profile_id
|
||||||
output_path_template_override
|
output_path_template_override
|
||||||
|
marked_for_deletion_at
|
||||||
)a
|
)a
|
||||||
|
|
||||||
# Expensive API calls are made when a source is inserted/updated so
|
# Expensive API calls are made when a source is inserted/updated so
|
||||||
@@ -87,6 +88,8 @@ defmodule Pinchflat.Sources.Source do
|
|||||||
field :fanart_filepath, :string
|
field :fanart_filepath, :string
|
||||||
field :banner_filepath, :string
|
field :banner_filepath, :string
|
||||||
|
|
||||||
|
field :marked_for_deletion_at, :utc_datetime
|
||||||
|
|
||||||
belongs_to :media_profile, MediaProfile
|
belongs_to :media_profile, MediaProfile
|
||||||
|
|
||||||
has_one :metadata, SourceMetadata, on_replace: :update
|
has_one :metadata, SourceMetadata, on_replace: :update
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -22,7 +22,8 @@ defmodule Pinchflat.YtDlp.Media do
|
|||||||
:livestream,
|
:livestream,
|
||||||
:short_form_content,
|
:short_form_content,
|
||||||
:uploaded_at,
|
:uploaded_at,
|
||||||
:duration_seconds
|
:duration_seconds,
|
||||||
|
:playlist_index
|
||||||
]
|
]
|
||||||
|
|
||||||
alias __MODULE__
|
alias __MODULE__
|
||||||
@@ -63,7 +64,7 @@ defmodule Pinchflat.YtDlp.Media do
|
|||||||
@doc """
|
@doc """
|
||||||
Returns a map representing the media at the given URL.
|
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) do
|
||||||
runner = Application.get_env(:pinchflat, :yt_dlp_runner)
|
runner = Application.get_env(:pinchflat, :yt_dlp_runner)
|
||||||
@@ -84,9 +85,11 @@ defmodule Pinchflat.YtDlp.Media do
|
|||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
Returns the output template for yt-dlp's indexing command.
|
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
|
def indexing_output_template do
|
||||||
"%(.{id,title,was_live,webpage_url,description,aspect_ratio,duration,upload_date,timestamp})j"
|
"%(.{id,title,was_live,webpage_url,description,aspect_ratio,duration,upload_date,timestamp,playlist_index})j"
|
||||||
end
|
end
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
@@ -104,7 +107,8 @@ defmodule Pinchflat.YtDlp.Media do
|
|||||||
livestream: !!response["was_live"],
|
livestream: !!response["was_live"],
|
||||||
duration_seconds: response["duration"] && round(response["duration"]),
|
duration_seconds: response["duration"] && round(response["duration"]),
|
||||||
short_form_content: response["webpage_url"] && short_form_content?(response),
|
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
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -99,10 +99,21 @@ defmodule Pinchflat.YtDlp.MediaCollection do
|
|||||||
as a compressed blob for possible future use. That's why it's not getting formatted like
|
as a compressed blob for possible future use. That's why it's not getting formatted like
|
||||||
`get_source_details/1`
|
`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, ...}.
|
Returns {:ok, map()} | {:error, any, ...}.
|
||||||
"""
|
"""
|
||||||
def get_source_metadata(source_url, addl_opts \\ []) do
|
def get_source_metadata(source_url, addl_opts \\ [playlist_items: 0]) do
|
||||||
opts = [playlist_items: 0] ++ addl_opts
|
# This only validates that the `playlist_items` key is present. It's otherwise unused
|
||||||
|
_playlist_items = Keyword.fetch!(addl_opts, :playlist_items)
|
||||||
|
|
||||||
|
opts = [:skip_download] ++ addl_opts
|
||||||
output_template = "playlist:%()j"
|
output_template = "playlist:%()j"
|
||||||
|
|
||||||
with {:ok, output} <- backend_runner().run(source_url, opts, output_template),
|
with {:ok, output} <- backend_runner().run(source_url, opts, output_template),
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ defmodule PinchflatWeb.CoreComponents do
|
|||||||
import PinchflatWeb.Gettext
|
import PinchflatWeb.Gettext
|
||||||
|
|
||||||
alias Phoenix.LiveView.JS
|
alias Phoenix.LiveView.JS
|
||||||
|
alias PinchflatWeb.CustomComponents.TextComponents
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
Renders a modal.
|
Renders a modal.
|
||||||
@@ -644,7 +645,11 @@ defmodule PinchflatWeb.CoreComponents do
|
|||||||
<li :for={{k, v} <- @iterable_attributes} class="mb-2 w-2/3">
|
<li :for={{k, v} <- @iterable_attributes} class="mb-2 w-2/3">
|
||||||
<strong><%= k %>:</strong>
|
<strong><%= k %>:</strong>
|
||||||
<code class="inline-block text-sm font-mono text-gray p-0.5 mx-0.5">
|
<code class="inline-block text-sm font-mono text-gray p-0.5 mx-0.5">
|
||||||
|
<%= if is_binary(v) && URI.parse(v).scheme && URI.parse(v).scheme =~ "http" do %>
|
||||||
|
<TextComponents.inline_link href={v}><%= v %></TextComponents.inline_link>
|
||||||
|
<% else %>
|
||||||
<%= v %>
|
<%= v %>
|
||||||
|
<% end %>
|
||||||
</code>
|
</code>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|||||||
@@ -5,10 +5,12 @@ defmodule PinchflatWeb.MediaProfiles.MediaProfileController do
|
|||||||
alias Pinchflat.Repo
|
alias Pinchflat.Repo
|
||||||
alias Pinchflat.Profiles
|
alias Pinchflat.Profiles
|
||||||
alias Pinchflat.Profiles.MediaProfile
|
alias Pinchflat.Profiles.MediaProfile
|
||||||
|
alias Pinchflat.Profiles.MediaProfileDeletionWorker
|
||||||
|
|
||||||
def index(conn, _params) do
|
def index(conn, _params) do
|
||||||
media_profiles =
|
media_profiles =
|
||||||
MediaProfile
|
MediaProfile
|
||||||
|
|> where([mp], is_nil(mp.marked_for_deletion_at))
|
||||||
|> order_by(asc: :name)
|
|> order_by(asc: :name)
|
||||||
|> Repo.all()
|
|> Repo.all()
|
||||||
|
|
||||||
@@ -70,19 +72,15 @@ defmodule PinchflatWeb.MediaProfiles.MediaProfileController do
|
|||||||
end
|
end
|
||||||
|
|
||||||
def delete(conn, %{"id" => id} = params) do
|
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)
|
media_profile = Profiles.get_media_profile!(id)
|
||||||
{:ok, _media_profile} = Profiles.delete_media_profile(media_profile, delete_files: delete_files)
|
|
||||||
|
|
||||||
flash_message =
|
{:ok, _} = Profiles.update_media_profile(media_profile, %{marked_for_deletion_at: DateTime.utc_now()})
|
||||||
if delete_files do
|
MediaProfileDeletionWorker.kickoff(media_profile, %{delete_files: delete_files})
|
||||||
"Media profile, its sources, and its files deleted successfully."
|
|
||||||
else
|
|
||||||
"Media profile and its sources deleted successfully. Files were not deleted."
|
|
||||||
end
|
|
||||||
|
|
||||||
conn
|
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")
|
|> redirect(to: ~p"/media_profiles")
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -53,12 +53,24 @@ defmodule PinchflatWeb.MediaProfiles.MediaProfileHTML do
|
|||||||
]
|
]
|
||||||
end
|
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_day: nil,
|
||||||
upload_month: nil,
|
upload_month: nil,
|
||||||
upload_year: 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_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_id: "the YouTube ID of the sources that use this profile",
|
||||||
source_collection_name:
|
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'",
|
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",
|
artist_name: "the name of the artist with fallbacks to other uploader fields",
|
||||||
season_from_date: "alias for upload_year",
|
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:
|
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
|
end
|
||||||
|
|
||||||
@@ -77,7 +91,6 @@ defmodule PinchflatWeb.MediaProfiles.MediaProfileHTML do
|
|||||||
id
|
id
|
||||||
ext
|
ext
|
||||||
title
|
title
|
||||||
fulltitle
|
|
||||||
uploader
|
uploader
|
||||||
channel
|
channel
|
||||||
upload_date
|
upload_date
|
||||||
@@ -99,7 +112,7 @@ defmodule PinchflatWeb.MediaProfiles.MediaProfileHTML do
|
|||||||
end
|
end
|
||||||
|
|
||||||
defp media_center_output_template do
|
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
|
end
|
||||||
|
|
||||||
defp audio_output_template do
|
defp audio_output_template do
|
||||||
|
|||||||
+11
-11
@@ -84,22 +84,22 @@
|
|||||||
/>
|
/>
|
||||||
</section>
|
</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 } }">
|
<section x-data="{ presets: { default: true, media_center: true, audio: false, archiving: true } }">
|
||||||
<.input
|
<.input
|
||||||
field={f[:embed_subs]}
|
field={f[:embed_subs]}
|
||||||
type="toggle"
|
type="toggle"
|
||||||
label="Embed Subtitles"
|
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]))"
|
x-init="$watch('selectedPreset', p => p && (enabled = presets[p]))"
|
||||||
/>
|
/>
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
+14
-7
@@ -1,7 +1,7 @@
|
|||||||
<%!-- The heex HTML formatter is really struggling with this file - I apologize in advance --%>
|
<%!-- The heex HTML formatter is really struggling with this file - I apologize in advance --%>
|
||||||
<aside>
|
<aside>
|
||||||
<h2 class="text-xl font-bold mb-2">Output Template Syntax</h2>
|
<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>
|
<p>When generating an output template, you have 3 options for syntax:</p>
|
||||||
<ul class="list-disc list-inside ml-2 md:ml-5">
|
<ul class="list-disc list-inside ml-2 md:ml-5">
|
||||||
<li>
|
<li>
|
||||||
@@ -46,7 +46,7 @@
|
|||||||
</section>
|
</section>
|
||||||
|
|
||||||
<h2 class="text-xl font-bold mb-2">Template Options</h2>
|
<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">
|
<p class="max-w-prose">
|
||||||
Any single-word <code class="text-sm">yt-dlp</code>
|
Any single-word <code class="text-sm">yt-dlp</code>
|
||||||
option
|
option
|
||||||
@@ -56,16 +56,23 @@
|
|||||||
can be used with the curly braced liquid-style syntax.
|
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
|
This is just a list of the most common options as well as some custom aliases
|
||||||
</p>
|
</p>
|
||||||
<h3 class="text-lg font-bold mb-2">Custom Aliases</h3>
|
<h3 class="text-lg font-bold my-2">Media Center Custom Aliases</h3>
|
||||||
<ul class="list-disc list-inside ml-2 md:ml-5">
|
<ul class="list-disc list-inside mb-4 ml-2 md:ml-5 max-w-prose">
|
||||||
<li :for={{k, v} <- custom_output_template_options()}>
|
<li :for={{k, v} <- media_center_custom_output_template_options()} class="mt-1">
|
||||||
<.inline_code>{{ <%= k %> }}</.inline_code>
|
<.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>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
<h3 class="text-lg font-bold mb-2">Common Options</h3>
|
<h3 class="text-lg font-bold mb-2">Common Options</h3>
|
||||||
<ul class="list-disc list-inside ml-2 md:ml-5">
|
<ul class="list-disc list-inside ml-2 md:ml-5">
|
||||||
<li :for={opt <- common_output_template_options()}>
|
<li :for={opt <- common_output_template_options()} class="mt-1">
|
||||||
<.inline_code>{{ <%= opt %> }}</.inline_code>
|
<.inline_code>{{ <%= opt %> }}</.inline_code>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ defmodule PinchflatWeb.Sources.SourceController do
|
|||||||
alias Pinchflat.Sources.Source
|
alias Pinchflat.Sources.Source
|
||||||
alias Pinchflat.Media.MediaItem
|
alias Pinchflat.Media.MediaItem
|
||||||
alias Pinchflat.Profiles.MediaProfile
|
alias Pinchflat.Profiles.MediaProfile
|
||||||
|
alias Pinchflat.Sources.SourceDeletionWorker
|
||||||
alias Pinchflat.Downloading.DownloadingHelpers
|
alias Pinchflat.Downloading.DownloadingHelpers
|
||||||
alias Pinchflat.SlowIndexing.SlowIndexingHelpers
|
alias Pinchflat.SlowIndexing.SlowIndexingHelpers
|
||||||
alias Pinchflat.Metadata.SourceMetadataStorageWorker
|
alias Pinchflat.Metadata.SourceMetadataStorageWorker
|
||||||
@@ -17,6 +18,7 @@ defmodule PinchflatWeb.Sources.SourceController do
|
|||||||
from s in Source,
|
from s in Source,
|
||||||
as: :source,
|
as: :source,
|
||||||
inner_join: mp in assoc(s, :media_profile),
|
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],
|
preload: [media_profile: mp],
|
||||||
order_by: [asc: s.custom_name],
|
order_by: [asc: s.custom_name],
|
||||||
select: map(s, ^Source.__schema__(:fields)),
|
select: map(s, ^Source.__schema__(:fields)),
|
||||||
@@ -41,13 +43,30 @@ defmodule PinchflatWeb.Sources.SourceController do
|
|||||||
render(conn, :index, sources: Repo.all(source_query))
|
render(conn, :index, sources: Repo.all(source_query))
|
||||||
end
|
end
|
||||||
|
|
||||||
def new(conn, _params) do
|
def new(conn, params) do
|
||||||
changeset = Sources.change_source(%Source{})
|
# 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,
|
render(conn, :new,
|
||||||
changeset: changeset,
|
|
||||||
media_profiles: media_profiles(),
|
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,
|
||||||
|
collection_name: nil,
|
||||||
|
collection_id: nil,
|
||||||
|
collection_type: nil,
|
||||||
|
original_url: nil
|
||||||
|
})
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -107,19 +126,15 @@ defmodule PinchflatWeb.Sources.SourceController do
|
|||||||
end
|
end
|
||||||
|
|
||||||
def delete(conn, %{"id" => id} = params) do
|
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)
|
source = Sources.get_source!(id)
|
||||||
{:ok, _source} = Sources.delete_source(source, delete_files: delete_files)
|
|
||||||
|
|
||||||
flash_message =
|
{:ok, _} = Sources.update_source(source, %{marked_for_deletion_at: DateTime.utc_now()})
|
||||||
if delete_files do
|
SourceDeletionWorker.kickoff(source, %{delete_files: delete_files})
|
||||||
"Source and files deleted successfully."
|
|
||||||
else
|
|
||||||
"Source deleted successfully. Files were not deleted."
|
|
||||||
end
|
|
||||||
|
|
||||||
conn
|
conn
|
||||||
|> put_flash(:info, flash_message)
|
|> put_flash(:info, "Source deletion started. This may take a while to complete.")
|
||||||
|> redirect(to: ~p"/sources")
|
|> redirect(to: ~p"/sources")
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ defmodule PinchflatWeb.Sources.SourceHTML do
|
|||||||
attr :changeset, Ecto.Changeset, required: true
|
attr :changeset, Ecto.Changeset, required: true
|
||||||
attr :action, :string, required: true
|
attr :action, :string, required: true
|
||||||
attr :media_profiles, :list, required: true
|
attr :media_profiles, :list, required: true
|
||||||
|
attr :method, :string, required: true
|
||||||
|
|
||||||
def source_form(assigns)
|
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 x-show="copied" x-transition.duration.150ms><.icon name="hero-check" class="ml-2 h-4 w-4" /></span>
|
||||||
</span>
|
</span>
|
||||||
</:option>
|
</:option>
|
||||||
|
<:option>
|
||||||
|
<.link href={~p"/sources/new?template_id=#{@source}"} method="get">
|
||||||
|
Use as Template
|
||||||
|
</.link>
|
||||||
|
</:option>
|
||||||
<:option>
|
<:option>
|
||||||
<div class="h-px w-full bg-bodydark2"></div>
|
<div class="h-px w-full bg-bodydark2"></div>
|
||||||
</:option>
|
</: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="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="max-w-full">
|
||||||
<div class="flex flex-col gap-10">
|
<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>
|
</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="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="max-w-full">
|
||||||
<div class="flex flex-col gap-10">
|
<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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
:let={f}
|
:let={f}
|
||||||
for={@changeset}
|
for={@changeset}
|
||||||
action={@action}
|
action={@action}
|
||||||
|
method={@method}
|
||||||
x-data="{ advancedMode: !!JSON.parse(localStorage.getItem('advancedMode')) }"
|
x-data="{ advancedMode: !!JSON.parse(localStorage.getItem('advancedMode')) }"
|
||||||
x-init="$watch('advancedMode', value => localStorage.setItem('advancedMode', JSON.stringify(value)))"
|
x-init="$watch('advancedMode', value => localStorage.setItem('advancedMode', JSON.stringify(value)))"
|
||||||
>
|
>
|
||||||
@@ -10,7 +11,7 @@
|
|||||||
</.error>
|
</.error>
|
||||||
|
|
||||||
<section x-data="{ mediaProfileId: null }">
|
<section x-data="{ mediaProfileId: null }">
|
||||||
<section class="flex justify-between items-center mt-8">
|
<section class="flex justify-between items-center mt-4">
|
||||||
<h3 class=" text-2xl text-black dark:text-white">
|
<h3 class=" text-2xl text-black dark:text-white">
|
||||||
General Options
|
General Options
|
||||||
</h3>
|
</h3>
|
||||||
@@ -19,6 +20,14 @@
|
|||||||
</span>
|
</span>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
<.input
|
||||||
|
field={f[:original_url]}
|
||||||
|
type="text"
|
||||||
|
label="Source URL"
|
||||||
|
help="URL of a channel or playlist (required)"
|
||||||
|
x-init="$el.focus()"
|
||||||
|
/>
|
||||||
|
|
||||||
<.input
|
<.input
|
||||||
field={f[:custom_name]}
|
field={f[:custom_name]}
|
||||||
type="text"
|
type="text"
|
||||||
@@ -26,8 +35,6 @@
|
|||||||
help="Does not impact indexing or downloading. Will be inferred from the source if left blank"
|
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
|
<.input
|
||||||
field={f[:media_profile_id]}
|
field={f[:media_profile_id]}
|
||||||
options={Enum.map(@media_profiles, &{&1.name, &1.id})}
|
options={Enum.map(@media_profiles, &{&1.name, &1.id})}
|
||||||
@@ -87,7 +94,7 @@
|
|||||||
maxlength="10"
|
maxlength="10"
|
||||||
pattern="((?:19|20)[0-9][0-9])-(0[1-9]|1[012])-(0[1-9]|[12][0-9]|3[01])"
|
pattern="((?:19|20)[0-9][0-9])-(0[1-9]|1[012])-(0[1-9]|[12][0-9]|3[01])"
|
||||||
title="YYYY-MM-DD"
|
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
|
<.input
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ defmodule Pinchflat.MixProject do
|
|||||||
def project do
|
def project do
|
||||||
[
|
[
|
||||||
app: :pinchflat,
|
app: :pinchflat,
|
||||||
version: "2024.6.25",
|
version: "2024.8.6",
|
||||||
elixir: "~> 1.17",
|
elixir: "~> 1.17",
|
||||||
elixirc_paths: elixirc_paths(Mix.env()),
|
elixirc_paths: elixirc_paths(Mix.env()),
|
||||||
start_permanent: Mix.env() == :prod,
|
start_permanent: Mix.env() == :prod,
|
||||||
@@ -103,7 +103,8 @@ defmodule Pinchflat.MixProject do
|
|||||||
"ecto.rollback": [
|
"ecto.rollback": [
|
||||||
"ecto.rollback",
|
"ecto.rollback",
|
||||||
~s(cmd [ -z "$MIX_ENV" ] && yarn run create-erd || echo "No ERD generated")
|
~s(cmd [ -z "$MIX_ENV" ] && yarn run create-erd || echo "No ERD generated")
|
||||||
]
|
],
|
||||||
|
"version.bump": "cmd ./tooling/version_bump.sh"
|
||||||
]
|
]
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 468 KiB After Width: | Height: | Size: 498 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
|
||||||
@@ -93,15 +93,23 @@ defmodule Pinchflat.Downloading.DownloadOptionBuilderTest do
|
|||||||
end
|
end
|
||||||
|
|
||||||
test "includes :write_auto_subs option when specified", %{media_item: media_item} do
|
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
|
end
|
||||||
|
|
||||||
test "doesn't include :write_auto_subs option when download_subs is false", %{media_item: media_item} do
|
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, download_auto_subs: true})
|
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)
|
assert {:ok, res} = DownloadOptionBuilder.build(media_item)
|
||||||
|
|
||||||
|
|||||||
@@ -130,9 +130,6 @@ defmodule Pinchflat.Downloading.DownloadingHelpersTest do
|
|||||||
_download_prevented =
|
_download_prevented =
|
||||||
media_item_fixture(source_id: source.id, media_filepath: "some/filepath.mp4", prevent_download: true)
|
media_item_fixture(source_id: source.id, media_filepath: "some/filepath.mp4", prevent_download: true)
|
||||||
|
|
||||||
_culled =
|
|
||||||
media_item_fixture(source_id: source.id, media_filepath: "some/filepath.mp4", culled_at: now())
|
|
||||||
|
|
||||||
assert [] = DownloadingHelpers.kickoff_redownload_for_existing_media(source)
|
assert [] = DownloadingHelpers.kickoff_redownload_for_existing_media(source)
|
||||||
|
|
||||||
refute_enqueued(worker: MediaDownloadWorker)
|
refute_enqueued(worker: MediaDownloadWorker)
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ defmodule Pinchflat.Downloading.MediaDownloadWorkerTest do
|
|||||||
|
|
||||||
setup do
|
setup do
|
||||||
stub(YtDlpRunnerMock, :run, fn _url, _opts, _ot -> {:ok, ""} end)
|
stub(YtDlpRunnerMock, :run, fn _url, _opts, _ot -> {:ok, ""} end)
|
||||||
stub(UserScriptRunnerMock, :run, fn _event_type, _data -> :ok end)
|
stub(UserScriptRunnerMock, :run, fn _event_type, _data -> {:ok, "", 0} end)
|
||||||
stub(HTTPClientMock, :get, fn _url, _headers, _opts -> {:ok, ""} end)
|
stub(HTTPClientMock, :get, fn _url, _headers, _opts -> {:ok, ""} end)
|
||||||
|
|
||||||
media_item =
|
media_item =
|
||||||
@@ -162,20 +162,6 @@ defmodule Pinchflat.Downloading.MediaDownloadWorkerTest do
|
|||||||
assert media_item.media_redownloaded_at == nil
|
assert media_item.media_redownloaded_at == nil
|
||||||
end
|
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
|
test "does not blow up if the record doesn't exist" do
|
||||||
assert :ok = perform_job(MediaDownloadWorker, %{id: 0})
|
assert :ok = perform_job(MediaDownloadWorker, %{id: 0})
|
||||||
end
|
end
|
||||||
@@ -237,4 +223,59 @@ defmodule Pinchflat.Downloading.MediaDownloadWorkerTest do
|
|||||||
perform_job(MediaDownloadWorker, %{id: media_item.id, force: true})
|
perform_job(MediaDownloadWorker, %{id: media_item.id, force: true})
|
||||||
end
|
end
|
||||||
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
|
end
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ defmodule Pinchflat.Downloading.MediaDownloaderTest do
|
|||||||
import Pinchflat.SourcesFixtures
|
import Pinchflat.SourcesFixtures
|
||||||
import Pinchflat.ProfilesFixtures
|
import Pinchflat.ProfilesFixtures
|
||||||
|
|
||||||
|
alias Pinchflat.Media
|
||||||
alias Pinchflat.Downloading.MediaDownloader
|
alias Pinchflat.Downloading.MediaDownloader
|
||||||
|
|
||||||
setup do
|
setup do
|
||||||
@@ -123,6 +124,12 @@ defmodule Pinchflat.Downloading.MediaDownloaderTest do
|
|||||||
assert DateTime.diff(DateTime.utc_now(), updated_media_item.media_downloaded_at) < 2
|
assert DateTime.diff(DateTime.utc_now(), updated_media_item.media_downloaded_at) < 2
|
||||||
end
|
end
|
||||||
|
|
||||||
|
test "it 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 "it extracts the title", %{media_item: media_item} do
|
test "it extracts the title", %{media_item: media_item} do
|
||||||
assert {:ok, updated_media_item} = MediaDownloader.download_for_media_item(media_item)
|
assert {:ok, updated_media_item} = MediaDownloader.download_for_media_item(media_item)
|
||||||
assert updated_media_item.title == "Pinchflat Example Video"
|
assert updated_media_item.title == "Pinchflat Example Video"
|
||||||
|
|||||||
@@ -7,15 +7,36 @@ defmodule Pinchflat.Downloading.MediaRetentionWorkerTest do
|
|||||||
alias Pinchflat.Media
|
alias Pinchflat.Media
|
||||||
alias Pinchflat.Downloading.MediaRetentionWorker
|
alias Pinchflat.Downloading.MediaRetentionWorker
|
||||||
|
|
||||||
describe "perform/1" do
|
|
||||||
setup do
|
setup do
|
||||||
stub(UserScriptRunnerMock, :run, fn _event_type, _data -> :ok end)
|
stub(UserScriptRunnerMock, :run, fn _event_type, _data -> {:ok, "", 0} end)
|
||||||
|
|
||||||
:ok
|
:ok
|
||||||
end
|
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
|
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, %{})
|
perform_job(MediaRetentionWorker, %{})
|
||||||
|
|
||||||
@@ -25,27 +46,33 @@ defmodule Pinchflat.Downloading.MediaRetentionWorkerTest do
|
|||||||
refute Repo.reload!(old_media_item).media_filepath
|
refute Repo.reload!(old_media_item).media_filepath
|
||||||
end
|
end
|
||||||
|
|
||||||
test "sets deleted media to not re-download" do
|
test "sets culled_at and prevent_download" 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, %{})
|
|
||||||
|
|
||||||
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()
|
|
||||||
|
|
||||||
perform_job(MediaRetentionWorker, %{})
|
perform_job(MediaRetentionWorker, %{})
|
||||||
|
|
||||||
refute Repo.reload!(new_media_item).culled_at
|
refute Repo.reload!(new_media_item).culled_at
|
||||||
assert Repo.reload!(old_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
|
end
|
||||||
|
|
||||||
test "doesn't cull media items that have prevent_culling set" do
|
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})
|
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 File.exists?(old_media_item.media_filepath)
|
||||||
assert Repo.reload!(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
|
||||||
end
|
end
|
||||||
|
|
||||||
defp prepare_records do
|
describe "perform/1 when testing source cutoff-based culling" do
|
||||||
source = source_fixture(%{retention_period_days: 2})
|
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 =
|
old_media_item =
|
||||||
media_item_with_attachments(%{
|
media_item_with_attachments(%{
|
||||||
@@ -73,4 +188,23 @@ defmodule Pinchflat.Downloading.MediaRetentionWorkerTest do
|
|||||||
|
|
||||||
{source, old_media_item, new_media_item}
|
{source, old_media_item, new_media_item}
|
||||||
end
|
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
|
end
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ defmodule Pinchflat.Lifecycle.UserScripts.CommandRunnerTest do
|
|||||||
File.write(filepath(), "#!/bin/bash\ntouch #{filename}\n")
|
File.write(filepath(), "#!/bin/bash\ntouch #{filename}\n")
|
||||||
|
|
||||||
refute File.exists?(filename)
|
refute File.exists?(filename)
|
||||||
assert :ok = Runner.run(:media_downloaded, %{})
|
assert {:ok, _, _} = Runner.run(:media_downloaded, %{})
|
||||||
assert File.exists?(filename)
|
assert File.exists?(filename)
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -27,7 +27,7 @@ defmodule Pinchflat.Lifecycle.UserScripts.CommandRunnerTest do
|
|||||||
tmp_dir = Application.get_env(:pinchflat, :tmpfile_directory)
|
tmp_dir = Application.get_env(:pinchflat, :tmpfile_directory)
|
||||||
File.write(filepath(), "#!/bin/bash\necho $1 > #{tmp_dir}/event_name\n")
|
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"
|
assert File.read!("#{tmp_dir}/event_name") == "media_downloaded\n"
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -35,26 +35,32 @@ defmodule Pinchflat.Lifecycle.UserScripts.CommandRunnerTest do
|
|||||||
tmp_dir = Application.get_env(:pinchflat, :tmpfile_directory)
|
tmp_dir = Application.get_env(:pinchflat, :tmpfile_directory)
|
||||||
File.write(filepath(), "#!/bin/bash\necho $2 > #{tmp_dir}/encoded_data\n")
|
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"
|
assert File.read!("#{tmp_dir}/encoded_data") == "{\"foo\":\"bar\"}\n"
|
||||||
end
|
end
|
||||||
|
|
||||||
test "does nothing if the lifecycle file is not present" do
|
test "does nothing if the lifecycle file is not present" do
|
||||||
:ok = File.rm(filepath())
|
:ok = File.rm(filepath())
|
||||||
|
|
||||||
assert :ok = Runner.run(:media_downloaded, %{})
|
assert {:ok, :no_executable} = Runner.run(:media_downloaded, %{})
|
||||||
end
|
end
|
||||||
|
|
||||||
test "does nothing if the lifecycle file is empty" do
|
test "does nothing if the lifecycle file is empty" do
|
||||||
File.write(filepath(), "")
|
File.write(filepath(), "")
|
||||||
|
|
||||||
assert :ok = Runner.run(:media_downloaded, %{})
|
assert {:ok, :no_executable} = Runner.run(:media_downloaded, %{})
|
||||||
end
|
end
|
||||||
|
|
||||||
test "returns :ok if the command exits with a non-zero status" do
|
test "returns :ok if the command exits with a non-zero status" do
|
||||||
File.write(filepath(), "#!/bin/bash\nexit 1\n")
|
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
|
end
|
||||||
|
|
||||||
test "gets upset if you pass an invalid event type" do
|
test "gets upset if you pass an invalid event type" do
|
||||||
|
|||||||
+33
-108
@@ -41,99 +41,7 @@ defmodule Pinchflat.MediaTest do
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
describe "list_cullable_media_items/0" do
|
describe "list_upgradeable_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
|
|
||||||
setup do
|
setup do
|
||||||
media_profile = media_profile_fixture(%{redownload_delay_days: 4})
|
media_profile = media_profile_fixture(%{redownload_delay_days: 4})
|
||||||
source = source_fixture(%{media_profile_id: media_profile.id, inserted_at: now_minus(10, :days)})
|
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)
|
media_downloaded_at: now_minus(5, :days)
|
||||||
})
|
})
|
||||||
|
|
||||||
assert Media.list_redownloadable_media_items() == [media_item]
|
assert Media.list_upgradeable_media_items() == [media_item]
|
||||||
end
|
end
|
||||||
|
|
||||||
test "returns media items that were downloaded in past but still meet redownload delay", %{source: source} do
|
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)
|
media_downloaded_at: now_minus(19, :days)
|
||||||
})
|
})
|
||||||
|
|
||||||
assert Media.list_redownloadable_media_items() == [media_item]
|
assert Media.list_upgradeable_media_items() == [media_item]
|
||||||
end
|
end
|
||||||
|
|
||||||
test "does not return media items without a media_downloaded_at", %{source: source} do
|
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
|
media_downloaded_at: nil
|
||||||
})
|
})
|
||||||
|
|
||||||
assert Media.list_redownloadable_media_items() == []
|
assert Media.list_upgradeable_media_items() == []
|
||||||
end
|
end
|
||||||
|
|
||||||
test "does not return media items that are set to prevent download", %{source: source} do
|
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
|
prevent_download: true
|
||||||
})
|
})
|
||||||
|
|
||||||
assert Media.list_redownloadable_media_items() == []
|
assert Media.list_upgradeable_media_items() == []
|
||||||
end
|
end
|
||||||
|
|
||||||
test "does not return media items that have been culled", %{source: source} do
|
test "does not return media items that have been culled", %{source: source} do
|
||||||
@@ -195,7 +103,7 @@ defmodule Pinchflat.MediaTest do
|
|||||||
culled_at: now()
|
culled_at: now()
|
||||||
})
|
})
|
||||||
|
|
||||||
assert Media.list_redownloadable_media_items() == []
|
assert Media.list_upgradeable_media_items() == []
|
||||||
end
|
end
|
||||||
|
|
||||||
test "does not return media items before the download delay", %{source: source} do
|
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)
|
media_downloaded_at: now_minus(3, :days)
|
||||||
})
|
})
|
||||||
|
|
||||||
assert Media.list_redownloadable_media_items() == []
|
assert Media.list_upgradeable_media_items() == []
|
||||||
end
|
end
|
||||||
|
|
||||||
test "does not return media items that have already been redownloaded", %{source: source} do
|
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()
|
media_redownloaded_at: now()
|
||||||
})
|
})
|
||||||
|
|
||||||
assert Media.list_redownloadable_media_items() == []
|
assert Media.list_upgradeable_media_items() == []
|
||||||
end
|
end
|
||||||
|
|
||||||
test "does not return media items that were first downloaded well after the uploaded_at", %{source: source} do
|
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)
|
uploaded_at: now_minus(20, :days)
|
||||||
})
|
})
|
||||||
|
|
||||||
assert Media.list_redownloadable_media_items() == []
|
assert Media.list_upgradeable_media_items() == []
|
||||||
end
|
end
|
||||||
|
|
||||||
test "does not return media items that were recently uploaded", %{source: source} do
|
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)
|
uploaded_at: now_minus(2, :days)
|
||||||
})
|
})
|
||||||
|
|
||||||
assert Media.list_redownloadable_media_items() == []
|
assert Media.list_upgradeable_media_items() == []
|
||||||
end
|
end
|
||||||
|
|
||||||
test "does not return media items without a redownload delay" do
|
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)
|
media_downloaded_at: now_minus(5, :days)
|
||||||
})
|
})
|
||||||
|
|
||||||
assert Media.list_redownloadable_media_items() == []
|
assert Media.list_upgradeable_media_items() == []
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -689,7 +597,24 @@ defmodule Pinchflat.MediaTest do
|
|||||||
assert {:ok, %MediaItem{} = media_item_2} = Media.create_media_item_from_backend_attrs(source, different_attrs)
|
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_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
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -759,7 +684,7 @@ defmodule Pinchflat.MediaTest do
|
|||||||
|
|
||||||
describe "delete_media_item/2 when testing file deletion" do
|
describe "delete_media_item/2 when testing file deletion" do
|
||||||
setup do
|
setup do
|
||||||
stub(UserScriptRunnerMock, :run, fn _event_type, _data -> :ok end)
|
stub(UserScriptRunnerMock, :run, fn _event_type, _data -> {:ok, "", 0} end)
|
||||||
|
|
||||||
:ok
|
:ok
|
||||||
end
|
end
|
||||||
@@ -820,7 +745,7 @@ defmodule Pinchflat.MediaTest do
|
|||||||
expect(UserScriptRunnerMock, :run, fn :media_deleted, data ->
|
expect(UserScriptRunnerMock, :run, fn :media_deleted, data ->
|
||||||
assert data.id == media_item.id
|
assert data.id == media_item.id
|
||||||
|
|
||||||
:ok
|
{:ok, "", 0}
|
||||||
end)
|
end)
|
||||||
|
|
||||||
assert {:ok, _} = Media.delete_media_item(media_item, delete_files: true)
|
assert {:ok, _} = Media.delete_media_item(media_item, delete_files: true)
|
||||||
@@ -829,7 +754,7 @@ defmodule Pinchflat.MediaTest do
|
|||||||
|
|
||||||
describe "delete_media_files/2" do
|
describe "delete_media_files/2" do
|
||||||
setup do
|
setup do
|
||||||
stub(UserScriptRunnerMock, :run, fn _event_type, _data -> :ok end)
|
stub(UserScriptRunnerMock, :run, fn _event_type, _data -> {:ok, "", 0} end)
|
||||||
|
|
||||||
:ok
|
:ok
|
||||||
end
|
end
|
||||||
@@ -892,7 +817,7 @@ defmodule Pinchflat.MediaTest do
|
|||||||
expect(UserScriptRunnerMock, :run, fn :media_deleted, data ->
|
expect(UserScriptRunnerMock, :run, fn :media_deleted, data ->
|
||||||
assert data.id == media_item.id
|
assert data.id == media_item.id
|
||||||
|
|
||||||
:ok
|
{:ok, "", 0}
|
||||||
end)
|
end)
|
||||||
|
|
||||||
assert {:ok, _} = Media.delete_media_files(media_item)
|
assert {:ok, _} = Media.delete_media_files(media_item)
|
||||||
|
|||||||
@@ -11,6 +11,16 @@ defmodule Pinchflat.Metadata.MetadataFileHelpersTest do
|
|||||||
{:ok, %{media_item: media_item}}
|
{:ok, %{media_item: media_item}}
|
||||||
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)
|
||||||
|
|
||||||
|
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
|
describe "compress_and_store_metadata_for/2" do
|
||||||
test "returns the filepath", %{media_item: media_item} do
|
test "returns the filepath", %{media_item: media_item} do
|
||||||
metadata_map = %{"foo" => "bar"}
|
metadata_map = %{"foo" => "bar"}
|
||||||
@@ -142,13 +152,20 @@ defmodule Pinchflat.Metadata.MetadataFileHelpersTest do
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
describe "metadata_directory_for/1" do
|
describe "season_and_episode_from_media_filepath/1" do
|
||||||
test "returns the metadata directory for the given record", %{media_item: media_item} do
|
test "returns a season and episode if one can be determined" do
|
||||||
base_metadata_directory = Application.get_env(:pinchflat, :metadata_directory)
|
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)
|
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 metadata_directory == Path.join([base_metadata_directory, "media_items", "#{media_item.id}"])
|
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
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ defmodule Pinchflat.Metadata.NfoBuilderTest do
|
|||||||
alias Pinchflat.Utils.FilesystemUtils
|
alias Pinchflat.Utils.FilesystemUtils
|
||||||
|
|
||||||
setup do
|
setup do
|
||||||
filepath = FilesystemUtils.generate_metadata_tmpfile(:json)
|
filepath = FilesystemUtils.generate_metadata_tmpfile(:nfo)
|
||||||
|
|
||||||
on_exit(fn -> File.rm!(filepath) end)
|
on_exit(fn -> File.rm!(filepath) end)
|
||||||
|
|
||||||
@@ -45,6 +45,43 @@ defmodule Pinchflat.Metadata.NfoBuilderTest do
|
|||||||
|
|
||||||
assert String.contains?(nfo, "hello' & <world>")
|
assert String.contains?(nfo, "hello' & <world>")
|
||||||
end
|
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
|
end
|
||||||
|
|
||||||
describe "build_and_store_for_source/2" do
|
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) == %{}
|
assert SourceImageParser.store_source_images(@base_dir, metadata) == %{}
|
||||||
end
|
end
|
||||||
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
|
end
|
||||||
|
|||||||
@@ -143,7 +143,9 @@ defmodule Pinchflat.Metadata.SourceMetadataStorageWorkerTest do
|
|||||||
|
|
||||||
{:ok, source_details_return_fixture(%{filename: filename})}
|
{:ok, source_details_return_fixture(%{filename: filename})}
|
||||||
|
|
||||||
_url, _opts, ot when ot == @metadata_ot ->
|
_url, opts, ot when ot == @metadata_ot ->
|
||||||
|
assert {:convert_thumbnails, "jpg"} in opts
|
||||||
|
|
||||||
{:ok, render_metadata(:channel_source_metadata)}
|
{:ok, render_metadata(:channel_source_metadata)}
|
||||||
end)
|
end)
|
||||||
|
|
||||||
@@ -164,6 +166,42 @@ defmodule Pinchflat.Metadata.SourceMetadataStorageWorkerTest do
|
|||||||
Sources.delete_source(source, delete_files: true)
|
Sources.delete_source(source, delete_files: true)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
test "calls one set of yt-dlp metadata opts for channels" 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 ->
|
||||||
|
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 when ot == @source_details_ot ->
|
||||||
|
{:ok, source_details_return_fixture()}
|
||||||
|
|
||||||
|
_url, opts, ot 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
|
test "does not store source images if the profile is not set to" do
|
||||||
stub(YtDlpRunnerMock, :run, fn
|
stub(YtDlpRunnerMock, :run, fn
|
||||||
_url, _opts, ot when ot == @source_details_ot ->
|
_url, _opts, ot when ot == @source_details_ot ->
|
||||||
|
|||||||
@@ -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
|
describe "delete_media_profile/2 when deleting files" do
|
||||||
setup do
|
setup do
|
||||||
stub(UserScriptRunnerMock, :run, fn _event_type, _data -> :ok end)
|
stub(UserScriptRunnerMock, :run, fn _event_type, _data -> {:ok, "", 0} end)
|
||||||
|
|
||||||
:ok
|
:ok
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -617,7 +617,7 @@ defmodule Pinchflat.SourcesTest do
|
|||||||
|
|
||||||
describe "delete_source/2 when deleting files" do
|
describe "delete_source/2 when deleting files" do
|
||||||
setup do
|
setup do
|
||||||
stub(UserScriptRunnerMock, :run, fn _event_type, _data -> :ok end)
|
stub(UserScriptRunnerMock, :run, fn _event_type, _data -> {:ok, "", 0} end)
|
||||||
|
|
||||||
:ok
|
:ok
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -129,7 +129,7 @@ defmodule Pinchflat.YtDlp.MediaCollectionTest do
|
|||||||
|
|
||||||
test "it passes the expected args to the backend runner" do
|
test "it passes the expected args to the backend runner" do
|
||||||
expect(YtDlpRunnerMock, :run, fn @channel_url, opts, ot ->
|
expect(YtDlpRunnerMock, :run, fn @channel_url, opts, ot ->
|
||||||
assert opts == [playlist_items: 0]
|
assert opts == [:skip_download, playlist_items: 0]
|
||||||
assert ot == "playlist:%()j"
|
assert ot == "playlist:%()j"
|
||||||
|
|
||||||
{:ok, "{}"}
|
{:ok, "{}"}
|
||||||
@@ -152,12 +152,18 @@ defmodule Pinchflat.YtDlp.MediaCollectionTest do
|
|||||||
|
|
||||||
test "allows you to pass additional opts" do
|
test "allows you to pass additional opts" do
|
||||||
expect(YtDlpRunnerMock, :run, fn _url, opts, _ot ->
|
expect(YtDlpRunnerMock, :run, fn _url, opts, _ot ->
|
||||||
assert opts == [playlist_items: 0, real_opt: :yup]
|
assert opts == [:skip_download, playlist_items: 1, real_opt: :yup]
|
||||||
|
|
||||||
{:ok, "{}"}
|
{:ok, "{}"}
|
||||||
end)
|
end)
|
||||||
|
|
||||||
assert {:ok, _} = MediaCollection.get_source_metadata(@channel_url, real_opt: :yup)
|
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
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -110,8 +110,10 @@ defmodule Pinchflat.YtDlp.MediaTest do
|
|||||||
|
|
||||||
describe "indexing_output_template/0" do
|
describe "indexing_output_template/0" do
|
||||||
test "contains all the greatest hits" do
|
test "contains all the greatest hits" do
|
||||||
assert "%(.{id,title,was_live,webpage_url,description,aspect_ratio,duration,upload_date,timestamp})j" ==
|
attrs = ~w(id title was_live webpage_url description aspect_ratio duration upload_date timestamp playlist_index)a
|
||||||
Media.indexing_output_template()
|
formatted_attrs = "%(.{#{Enum.join(attrs, ",")}})j"
|
||||||
|
|
||||||
|
assert formatted_attrs == Media.indexing_output_template()
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -126,7 +128,8 @@ defmodule Pinchflat.YtDlp.MediaTest do
|
|||||||
"aspect_ratio" => 1.0,
|
"aspect_ratio" => 1.0,
|
||||||
"duration" => 60,
|
"duration" => 60,
|
||||||
"upload_date" => "20210101",
|
"upload_date" => "20210101",
|
||||||
"timestamp" => 1_600_000_000
|
"timestamp" => 1_600_000_000,
|
||||||
|
"playlist_index" => 1
|
||||||
}
|
}
|
||||||
|
|
||||||
assert %Media{
|
assert %Media{
|
||||||
@@ -137,7 +140,8 @@ defmodule Pinchflat.YtDlp.MediaTest do
|
|||||||
livestream: false,
|
livestream: false,
|
||||||
short_form_content: false,
|
short_form_content: false,
|
||||||
uploaded_at: ~U[2020-09-13 12:26:40Z],
|
uploaded_at: ~U[2020-09-13 12:26:40Z],
|
||||||
duration_seconds: 60
|
duration_seconds: 60,
|
||||||
|
playlist_index: 1
|
||||||
} == Media.response_to_struct(response)
|
} == Media.response_to_struct(response)
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -217,6 +221,17 @@ defmodule Pinchflat.YtDlp.MediaTest do
|
|||||||
|
|
||||||
assert %Media{livestream: false} = Media.response_to_struct(response)
|
assert %Media{livestream: false} = Media.response_to_struct(response)
|
||||||
end
|
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
|
end
|
||||||
|
|
||||||
describe "response_to_struct/1 when testing uploaded_at" do
|
describe "response_to_struct/1 when testing uploaded_at" do
|
||||||
|
|||||||
@@ -56,7 +56,7 @@ defmodule PinchflatWeb.MediaItemControllerTest do
|
|||||||
describe "delete media" do
|
describe "delete media" do
|
||||||
setup do
|
setup do
|
||||||
media_item = media_item_with_attachments()
|
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}
|
%{media_item: media_item}
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -1,12 +1,11 @@
|
|||||||
defmodule PinchflatWeb.MediaProfileControllerTest do
|
defmodule PinchflatWeb.MediaProfileControllerTest do
|
||||||
use PinchflatWeb.ConnCase
|
use PinchflatWeb.ConnCase
|
||||||
|
|
||||||
import Pinchflat.MediaFixtures
|
|
||||||
import Pinchflat.SourcesFixtures
|
|
||||||
import Pinchflat.ProfilesFixtures
|
import Pinchflat.ProfilesFixtures
|
||||||
|
|
||||||
alias Pinchflat.Repo
|
alias Pinchflat.Repo
|
||||||
alias Pinchflat.Settings
|
alias Pinchflat.Settings
|
||||||
|
alias Pinchflat.Profiles.MediaProfileDeletionWorker
|
||||||
|
|
||||||
@create_attrs %{name: "some name", output_path_template: "output_template.{{ ext }}"}
|
@create_attrs %{name: "some name", output_path_template: "output_template.{{ ext }}"}
|
||||||
@update_attrs %{
|
@update_attrs %{
|
||||||
@@ -23,8 +22,17 @@ defmodule PinchflatWeb.MediaProfileControllerTest do
|
|||||||
|
|
||||||
describe "index" do
|
describe "index" do
|
||||||
test "lists all media_profiles", %{conn: conn} do
|
test "lists all media_profiles", %{conn: conn} do
|
||||||
|
profile = media_profile_fixture()
|
||||||
conn = get(conn, ~p"/media_profiles")
|
conn = get(conn, ~p"/media_profiles")
|
||||||
|
|
||||||
assert html_response(conn, 200) =~ "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
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -102,34 +110,28 @@ defmodule PinchflatWeb.MediaProfileControllerTest do
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
describe "delete media_profile when just deleting the records" do
|
describe "delete media_profile in all cases" do
|
||||||
setup [:create_media_profile]
|
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
|
test "redirects to the media_profiles page", %{conn: conn, media_profile: media_profile} do
|
||||||
conn = delete(conn, ~p"/media_profiles/#{media_profile}")
|
conn = delete(conn, ~p"/media_profiles/#{media_profile}")
|
||||||
|
|
||||||
assert redirected_to(conn) == ~p"/media_profiles"
|
assert redirected_to(conn) == ~p"/media_profiles"
|
||||||
end
|
end
|
||||||
|
|
||||||
test "doesn't delete any files", %{conn: conn, media_profile: media_profile} do
|
test "sets marked_for_deletion_at", %{conn: conn, media_profile: media_profile} do
|
||||||
source = source_fixture(media_profile_id: media_profile.id)
|
delete(conn, ~p"/media_profiles/#{media_profile}")
|
||||||
media_item = media_item_with_attachments(%{source_id: source.id})
|
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}")
|
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
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -137,36 +139,15 @@ defmodule PinchflatWeb.MediaProfileControllerTest do
|
|||||||
setup [:create_media_profile]
|
setup [:create_media_profile]
|
||||||
|
|
||||||
setup do
|
setup do
|
||||||
stub(UserScriptRunnerMock, :run, fn _event_type, _data -> :ok end)
|
stub(UserScriptRunnerMock, :run, fn _event_type, _data -> {:ok, "", 0} end)
|
||||||
|
|
||||||
:ok
|
:ok
|
||||||
end
|
end
|
||||||
|
|
||||||
test "deletes chosen media_profile and its associations", %{conn: conn, media_profile: media_profile} do
|
test "enqueues a job with the delete_files arg", %{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})
|
|
||||||
|
|
||||||
delete(conn, ~p"/media_profiles/#{media_profile}?delete_files=true")
|
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
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ defmodule PinchflatWeb.SourceControllerTest do
|
|||||||
|
|
||||||
alias Pinchflat.Repo
|
alias Pinchflat.Repo
|
||||||
alias Pinchflat.Settings
|
alias Pinchflat.Settings
|
||||||
|
alias Pinchflat.Sources.SourceDeletionWorker
|
||||||
alias Pinchflat.Downloading.MediaDownloadWorker
|
alias Pinchflat.Downloading.MediaDownloadWorker
|
||||||
alias Pinchflat.Metadata.SourceMetadataStorageWorker
|
alias Pinchflat.Metadata.SourceMetadataStorageWorker
|
||||||
alias Pinchflat.SlowIndexing.MediaCollectionIndexingWorker
|
alias Pinchflat.SlowIndexing.MediaCollectionIndexingWorker
|
||||||
@@ -33,8 +34,26 @@ defmodule PinchflatWeb.SourceControllerTest do
|
|||||||
|
|
||||||
describe "index" do
|
describe "index" do
|
||||||
test "lists all sources", %{conn: conn} do
|
test "lists all sources", %{conn: conn} do
|
||||||
|
source = source_fixture()
|
||||||
conn = get(conn, ~p"/sources")
|
conn = get(conn, ~p"/sources")
|
||||||
|
|
||||||
assert html_response(conn, 200) =~ "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
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -50,6 +69,15 @@ defmodule PinchflatWeb.SourceControllerTest do
|
|||||||
|
|
||||||
refute html_response(conn, 200) =~ "MENU"
|
refute html_response(conn, 200) =~ "MENU"
|
||||||
end
|
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
|
end
|
||||||
|
|
||||||
describe "create source" do
|
describe "create source" do
|
||||||
@@ -118,51 +146,37 @@ defmodule PinchflatWeb.SourceControllerTest do
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
describe "delete source when just deleting the records" do
|
describe "delete source in all cases" do
|
||||||
setup [:create_source]
|
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
|
test "redirects to the sources page", %{conn: conn, source: source} do
|
||||||
conn = delete(conn, ~p"/sources/#{source}")
|
conn = delete(conn, ~p"/sources/#{source}")
|
||||||
assert redirected_to(conn) == ~p"/sources"
|
assert redirected_to(conn) == ~p"/sources"
|
||||||
end
|
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}")
|
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
|
||||||
end
|
end
|
||||||
|
|
||||||
describe "delete source when deleting the records and files" do
|
describe "delete source when deleting the records and files" do
|
||||||
setup [:create_source]
|
setup [:create_source]
|
||||||
|
|
||||||
setup do
|
test "enqueues a job without the delete_files arg", %{conn: conn, source: source} 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
|
|
||||||
delete(conn, ~p"/sources/#{source}?delete_files=true")
|
delete(conn, ~p"/sources/#{source}?delete_files=true")
|
||||||
|
|
||||||
assert_raise Ecto.NoResultsError, fn -> Repo.reload!(source) end
|
assert [%{args: %{"delete_files" => true}}] = all_enqueued(worker: SourceDeletionWorker)
|
||||||
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)
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
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