Compare commits
20 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 0fbf810cb6 | |||
| a97bb248e2 | |||
| ac895944a8 | |||
| 59f8aa69cd | |||
| b790e05133 | |||
| 9953e4d316 | |||
| b62eb2bc6b | |||
| 464a595045 | |||
| 05f33acd78 | |||
| e7adc9d68f | |||
| fe5c00dbef | |||
| 28f0d8ca6e | |||
| b62d5c201b | |||
| 6ead29182d | |||
| 62214b80a6 | |||
| 704d29dc7e | |||
| 3dd20141e0 | |||
| 993c57f853 | |||
| 63bb4d2327 | |||
| 80406c9e0e |
@@ -1,3 +1,6 @@
|
|||||||
|
> [!IMPORTANT]
|
||||||
|
> (2025-02-14) [zakkarry](https://github.com/sponsors/zakkarry), who is a collaborator on [cross-seed](https://github.com/cross-seed/cross-seed) and an extremely helpful community member in general, is facing hard times due to medical debt and family illness. If you're able, please consider [sponsoring him on GitHub](https://github.com/sponsors/zakkarry) or donating via [buymeacoffee](https://tip.ary.dev). Tell him I sent you!
|
||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
<img
|
<img
|
||||||
src="priv/static/images/originals/logo-white-wordmark-with-background.png"
|
src="priv/static/images/originals/logo-white-wordmark-with-background.png"
|
||||||
@@ -148,6 +151,7 @@ If you change this setting and it works well for you, please leave a comment on
|
|||||||
| --------------------------- | --------- | ------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------- |
|
| --------------------------- | --------- | ------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------- |
|
||||||
| `TZ` | No | `UTC` | Must follow IANA TZ format |
|
| `TZ` | No | `UTC` | Must follow IANA TZ format |
|
||||||
| `LOG_LEVEL` | No | `debug` | Can be set to `info` but `debug` is strongly recommended |
|
| `LOG_LEVEL` | No | `debug` | Can be set to `info` but `debug` is strongly recommended |
|
||||||
|
| `UMASK` | No | `022` | Unraid users may want to set this to `000` |
|
||||||
| `BASIC_AUTH_USERNAME` | No | | See [authentication docs](https://github.com/kieraneglin/pinchflat/wiki/Username-and-Password) |
|
| `BASIC_AUTH_USERNAME` | No | | See [authentication docs](https://github.com/kieraneglin/pinchflat/wiki/Username-and-Password) |
|
||||||
| `BASIC_AUTH_PASSWORD` | No | | See [authentication docs](https://github.com/kieraneglin/pinchflat/wiki/Username-and-Password) |
|
| `BASIC_AUTH_PASSWORD` | No | | See [authentication docs](https://github.com/kieraneglin/pinchflat/wiki/Username-and-Password) |
|
||||||
| `EXPOSE_FEED_ENDPOINTS` | No | `false` | See [RSS feed docs](https://github.com/kieraneglin/pinchflat/wiki/Podcast-RSS-Feeds) |
|
| `EXPOSE_FEED_ENDPOINTS` | No | `false` | See [RSS feed docs](https://github.com/kieraneglin/pinchflat/wiki/Podcast-RSS-Feeds) |
|
||||||
|
|||||||
+2
-10
@@ -10,6 +10,7 @@ import Config
|
|||||||
config :pinchflat,
|
config :pinchflat,
|
||||||
ecto_repos: [Pinchflat.Repo],
|
ecto_repos: [Pinchflat.Repo],
|
||||||
generators: [timestamp_type: :utc_datetime],
|
generators: [timestamp_type: :utc_datetime],
|
||||||
|
env: config_env(),
|
||||||
# Specifying backend data here makes mocking and local testing SUPER easy
|
# Specifying backend data here makes mocking and local testing SUPER easy
|
||||||
yt_dlp_executable: System.find_executable("yt-dlp"),
|
yt_dlp_executable: System.find_executable("yt-dlp"),
|
||||||
apprise_executable: System.find_executable("apprise"),
|
apprise_executable: System.find_executable("apprise"),
|
||||||
@@ -49,16 +50,7 @@ config :pinchflat, PinchflatWeb.Endpoint,
|
|||||||
|
|
||||||
config :pinchflat, Oban,
|
config :pinchflat, Oban,
|
||||||
engine: Oban.Engines.Lite,
|
engine: Oban.Engines.Lite,
|
||||||
repo: Pinchflat.Repo,
|
repo: Pinchflat.Repo
|
||||||
# Keep old jobs for 30 days for display in the UI
|
|
||||||
plugins: [
|
|
||||||
{Oban.Plugins.Pruner, max_age: 30 * 24 * 60 * 60},
|
|
||||||
{Oban.Plugins.Cron,
|
|
||||||
crontab: [
|
|
||||||
{"0 1 * * *", Pinchflat.Downloading.MediaRetentionWorker},
|
|
||||||
{"0 2 * * *", Pinchflat.Downloading.MediaQualityUpgradeWorker}
|
|
||||||
]}
|
|
||||||
]
|
|
||||||
|
|
||||||
# Configures the mailer
|
# Configures the mailer
|
||||||
#
|
#
|
||||||
|
|||||||
+16
-1
@@ -43,15 +43,30 @@ config :pinchflat, Pinchflat.Repo,
|
|||||||
# Some users may want to increase the number of workers that use yt-dlp to improve speeds
|
# Some users may want to increase the number of workers that use yt-dlp to improve speeds
|
||||||
# Others may want to decrease the number of these workers to lessen the chance of an IP ban
|
# Others may want to decrease the number of these workers to lessen the chance of an IP ban
|
||||||
{yt_dlp_worker_count, _} = Integer.parse(System.get_env("YT_DLP_WORKER_CONCURRENCY", "2"))
|
{yt_dlp_worker_count, _} = Integer.parse(System.get_env("YT_DLP_WORKER_CONCURRENCY", "2"))
|
||||||
|
# Used to set the cron for the yt-dlp update worker. The reason for this is
|
||||||
|
# to avoid all instances of PF updating yt-dlp at the same time, which 1)
|
||||||
|
# could result in rate limiting and 2) gives me time to react if an update
|
||||||
|
# breaks something
|
||||||
|
%{hour: current_hour, minute: current_minute} = DateTime.utc_now()
|
||||||
|
|
||||||
config :pinchflat, Oban,
|
config :pinchflat, Oban,
|
||||||
queues: [
|
queues: [
|
||||||
default: 10,
|
default: 10,
|
||||||
fast_indexing: 6,
|
fast_indexing: yt_dlp_worker_count,
|
||||||
media_collection_indexing: yt_dlp_worker_count,
|
media_collection_indexing: yt_dlp_worker_count,
|
||||||
media_fetching: yt_dlp_worker_count,
|
media_fetching: yt_dlp_worker_count,
|
||||||
remote_metadata: yt_dlp_worker_count,
|
remote_metadata: yt_dlp_worker_count,
|
||||||
local_data: 8
|
local_data: 8
|
||||||
|
],
|
||||||
|
plugins: [
|
||||||
|
# Keep old jobs for 30 days for display in the UI
|
||||||
|
{Oban.Plugins.Pruner, max_age: 30 * 24 * 60 * 60},
|
||||||
|
{Oban.Plugins.Cron,
|
||||||
|
crontab: [
|
||||||
|
{"#{current_minute} #{current_hour} * * *", Pinchflat.YtDlp.UpdateWorker},
|
||||||
|
{"0 1 * * *", Pinchflat.Downloading.MediaRetentionWorker},
|
||||||
|
{"0 2 * * *", Pinchflat.Downloading.MediaQualityUpgradeWorker}
|
||||||
|
]}
|
||||||
]
|
]
|
||||||
|
|
||||||
if config_env() == :prod do
|
if config_env() == :prod do
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ ARG DEBIAN_VERSION=bookworm-20240612-slim
|
|||||||
ARG BUILDER_IMAGE="hexpm/elixir:${ELIXIR_VERSION}-erlang-${OTP_VERSION}-debian-${DEBIAN_VERSION}"
|
ARG BUILDER_IMAGE="hexpm/elixir:${ELIXIR_VERSION}-erlang-${OTP_VERSION}-debian-${DEBIAN_VERSION}"
|
||||||
ARG RUNNER_IMAGE="debian:${DEBIAN_VERSION}"
|
ARG RUNNER_IMAGE="debian:${DEBIAN_VERSION}"
|
||||||
|
|
||||||
FROM ${BUILDER_IMAGE} as builder
|
FROM ${BUILDER_IMAGE} AS builder
|
||||||
|
|
||||||
ARG TARGETPLATFORM
|
ARG TARGETPLATFORM
|
||||||
RUN echo "Building for ${TARGETPLATFORM:?}"
|
RUN echo "Building for ${TARGETPLATFORM:?}"
|
||||||
@@ -110,20 +110,21 @@ RUN apt-get update -y && \
|
|||||||
rm -rf /var/lib/apt/lists/*
|
rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
# More locale setup
|
# More locale setup
|
||||||
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
|
||||||
|
|
||||||
WORKDIR "/app"
|
WORKDIR "/app"
|
||||||
|
|
||||||
# Set up data volumes
|
# Set up data volumes
|
||||||
RUN mkdir -p /config /downloads /etc/elixir_tzdata_data /etc/yt-dlp/plugins && \
|
RUN mkdir -p /config /downloads /etc/elixir_tzdata_data /etc/yt-dlp/plugins && \
|
||||||
chmod ugo+rw /etc/elixir_tzdata_data /etc/yt-dlp /etc/yt-dlp/plugins
|
chmod ugo+rw /etc/elixir_tzdata_data /etc/yt-dlp /etc/yt-dlp/plugins /usr/local/bin /usr/local/bin/yt-dlp
|
||||||
|
|
||||||
# set runner ENV
|
# set runner ENV
|
||||||
ENV MIX_ENV="prod"
|
ENV MIX_ENV="prod"
|
||||||
ENV PORT=${PORT}
|
ENV PORT=${PORT}
|
||||||
ENV RUN_CONTEXT="selfhosted"
|
ENV RUN_CONTEXT="selfhosted"
|
||||||
|
ENV UMASK=022
|
||||||
EXPOSE ${PORT}
|
EXPOSE ${PORT}
|
||||||
|
|
||||||
# Only copy the final release from the build stage
|
# Only copy the final release from the build stage
|
||||||
|
|||||||
@@ -9,8 +9,12 @@ defmodule Pinchflat.Application do
|
|||||||
@impl true
|
@impl true
|
||||||
def start(_type, _args) do
|
def start(_type, _args) do
|
||||||
check_and_update_timezone()
|
check_and_update_timezone()
|
||||||
|
attach_oban_telemetry()
|
||||||
|
Logger.add_handlers(:pinchflat)
|
||||||
|
|
||||||
children = [
|
# See https://hexdocs.pm/elixir/Supervisor.html
|
||||||
|
# for other strategies and supported options
|
||||||
|
[
|
||||||
Pinchflat.PromEx,
|
Pinchflat.PromEx,
|
||||||
PinchflatWeb.Telemetry,
|
PinchflatWeb.Telemetry,
|
||||||
Pinchflat.Repo,
|
Pinchflat.Repo,
|
||||||
@@ -24,17 +28,11 @@ defmodule Pinchflat.Application do
|
|||||||
{Finch, name: Pinchflat.Finch},
|
{Finch, name: Pinchflat.Finch},
|
||||||
# Start a worker by calling: Pinchflat.Worker.start_link(arg)
|
# Start a worker by calling: Pinchflat.Worker.start_link(arg)
|
||||||
# {Pinchflat.Worker, arg},
|
# {Pinchflat.Worker, arg},
|
||||||
# Start to serve requests, typically the last entry
|
# Start to serve requests, typically the last entry (except for the post-boot tasks)
|
||||||
PinchflatWeb.Endpoint
|
PinchflatWeb.Endpoint,
|
||||||
|
Pinchflat.Boot.PostBootStartupTasks
|
||||||
]
|
]
|
||||||
|
|> Supervisor.start_link(strategy: :one_for_one, name: Pinchflat.Supervisor)
|
||||||
attach_oban_telemetry()
|
|
||||||
Logger.add_handlers(:pinchflat)
|
|
||||||
|
|
||||||
# See https://hexdocs.pm/elixir/Supervisor.html
|
|
||||||
# for other strategies and supported options
|
|
||||||
opts = [strategy: :one_for_one, name: Pinchflat.Supervisor]
|
|
||||||
Supervisor.start_link(children, opts)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
# Tell Phoenix to update the endpoint configuration
|
# Tell Phoenix to update the endpoint configuration
|
||||||
|
|||||||
@@ -0,0 +1,46 @@
|
|||||||
|
defmodule Pinchflat.Boot.PostBootStartupTasks do
|
||||||
|
@moduledoc """
|
||||||
|
This module is responsible for running startup tasks on app boot
|
||||||
|
AFTER all other boot steps have taken place and the app is ready to serve requests.
|
||||||
|
|
||||||
|
It's a GenServer because that plays REALLY nicely with the existing
|
||||||
|
Phoenix supervision tree.
|
||||||
|
"""
|
||||||
|
|
||||||
|
alias Pinchflat.YtDlp.UpdateWorker, as: YtDlpUpdateWorker
|
||||||
|
|
||||||
|
# restart: :temporary means that this process will never be restarted (ie: will run once and then die)
|
||||||
|
use GenServer, restart: :temporary
|
||||||
|
import Ecto.Query, warn: false
|
||||||
|
|
||||||
|
def start_link(opts \\ []) do
|
||||||
|
GenServer.start_link(__MODULE__, %{env: Application.get_env(:pinchflat, :env)}, opts)
|
||||||
|
end
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Runs post-boot application startup tasks.
|
||||||
|
|
||||||
|
Any code defined here will run every time the application starts. You must
|
||||||
|
make sure that the code is idempotent and safe to run multiple times.
|
||||||
|
|
||||||
|
This is a good place to set up default settings, create initial records, stuff like that.
|
||||||
|
Should be fast - anything with the potential to be slow should be kicked off as a job instead.
|
||||||
|
"""
|
||||||
|
@impl true
|
||||||
|
def init(%{env: :test} = state) do
|
||||||
|
# Do nothing _as part of the app bootup process_.
|
||||||
|
# Since bootup calls `start_link` and that's where the `env` state is injected,
|
||||||
|
# you can still call `.init()` manually to run these tasks for testing purposes
|
||||||
|
{:ok, state}
|
||||||
|
end
|
||||||
|
|
||||||
|
def init(state) do
|
||||||
|
update_yt_dlp()
|
||||||
|
|
||||||
|
{:ok, state}
|
||||||
|
end
|
||||||
|
|
||||||
|
defp update_yt_dlp do
|
||||||
|
YtDlpUpdateWorker.kickoff()
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
defmodule Pinchflat.Boot.PostJobStartupTasks do
|
defmodule Pinchflat.Boot.PostJobStartupTasks do
|
||||||
@moduledoc """
|
@moduledoc """
|
||||||
This module is responsible for running startup tasks on app boot
|
This module is responsible for running startup tasks on app boot
|
||||||
AFTER the job runner has initiallized.
|
AFTER the job runner has initialized.
|
||||||
|
|
||||||
It's a GenServer because that plays REALLY nicely with the existing
|
It's a GenServer because that plays REALLY nicely with the existing
|
||||||
Phoenix supervision tree.
|
Phoenix supervision tree.
|
||||||
@@ -12,7 +12,7 @@ defmodule Pinchflat.Boot.PostJobStartupTasks do
|
|||||||
import Ecto.Query, warn: false
|
import Ecto.Query, warn: false
|
||||||
|
|
||||||
def start_link(opts \\ []) do
|
def start_link(opts \\ []) do
|
||||||
GenServer.start_link(__MODULE__, %{}, opts)
|
GenServer.start_link(__MODULE__, %{env: Application.get_env(:pinchflat, :env)}, opts)
|
||||||
end
|
end
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
@@ -25,6 +25,13 @@ defmodule Pinchflat.Boot.PostJobStartupTasks do
|
|||||||
Should be fast - anything with the potential to be slow should be kicked off as a job instead.
|
Should be fast - anything with the potential to be slow should be kicked off as a job instead.
|
||||||
"""
|
"""
|
||||||
@impl true
|
@impl true
|
||||||
|
def init(%{env: :test} = state) do
|
||||||
|
# Do nothing _as part of the app bootup process_.
|
||||||
|
# Since bootup calls `start_link` and that's where the `env` state is injected,
|
||||||
|
# you can still call `.init()` manually to run these tasks for testing purposes
|
||||||
|
{:ok, state}
|
||||||
|
end
|
||||||
|
|
||||||
def init(state) do
|
def init(state) do
|
||||||
# Nothing at the moment!
|
# Nothing at the moment!
|
||||||
|
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ defmodule Pinchflat.Boot.PreJobStartupTasks do
|
|||||||
alias Pinchflat.Lifecycle.UserScripts.CommandRunner, as: UserScriptRunner
|
alias Pinchflat.Lifecycle.UserScripts.CommandRunner, as: UserScriptRunner
|
||||||
|
|
||||||
def start_link(opts \\ []) do
|
def start_link(opts \\ []) do
|
||||||
GenServer.start_link(__MODULE__, %{}, opts)
|
GenServer.start_link(__MODULE__, %{env: Application.get_env(:pinchflat, :env)}, opts)
|
||||||
end
|
end
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
@@ -32,6 +32,13 @@ defmodule Pinchflat.Boot.PreJobStartupTasks do
|
|||||||
Should be fast - anything with the potential to be slow should be kicked off as a job instead.
|
Should be fast - anything with the potential to be slow should be kicked off as a job instead.
|
||||||
"""
|
"""
|
||||||
@impl true
|
@impl true
|
||||||
|
def init(%{env: :test} = state) do
|
||||||
|
# Do nothing _as part of the app bootup process_.
|
||||||
|
# Since bootup calls `start_link` and that's where the `env` state is injected,
|
||||||
|
# you can still call `.init()` manually to run these tasks for testing purposes
|
||||||
|
{:ok, state}
|
||||||
|
end
|
||||||
|
|
||||||
def init(state) do
|
def init(state) do
|
||||||
ensure_tmpfile_directory()
|
ensure_tmpfile_directory()
|
||||||
reset_executing_jobs()
|
reset_executing_jobs()
|
||||||
|
|||||||
@@ -27,13 +27,15 @@ defmodule Pinchflat.Downloading.DownloadingHelpers do
|
|||||||
|
|
||||||
Returns :ok
|
Returns :ok
|
||||||
"""
|
"""
|
||||||
def enqueue_pending_download_tasks(%Source{download_media: true} = source) do
|
def enqueue_pending_download_tasks(source, job_opts \\ [])
|
||||||
|
|
||||||
|
def enqueue_pending_download_tasks(%Source{download_media: true} = source, job_opts) do
|
||||||
source
|
source
|
||||||
|> Media.list_pending_media_items_for()
|
|> Media.list_pending_media_items_for()
|
||||||
|> Enum.each(&MediaDownloadWorker.kickoff_with_task/1)
|
|> Enum.each(&MediaDownloadWorker.kickoff_with_task(&1, %{}, job_opts))
|
||||||
end
|
end
|
||||||
|
|
||||||
def enqueue_pending_download_tasks(%Source{download_media: false}) do
|
def enqueue_pending_download_tasks(%Source{download_media: false}, _job_opts) do
|
||||||
:ok
|
:ok
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -55,13 +57,13 @@ defmodule Pinchflat.Downloading.DownloadingHelpers do
|
|||||||
|
|
||||||
Returns {:ok, %Task{}} | {:error, :should_not_download} | {:error, any()}
|
Returns {:ok, %Task{}} | {:error, :should_not_download} | {:error, any()}
|
||||||
"""
|
"""
|
||||||
def kickoff_download_if_pending(%MediaItem{} = media_item) do
|
def kickoff_download_if_pending(%MediaItem{} = media_item, job_opts \\ []) do
|
||||||
media_item = Repo.preload(media_item, :source)
|
media_item = Repo.preload(media_item, :source)
|
||||||
|
|
||||||
if media_item.source.download_media && Media.pending_download?(media_item) do
|
if media_item.source.download_media && Media.pending_download?(media_item) do
|
||||||
Logger.info("Kicking off download for media item ##{media_item.id} (#{media_item.media_id})")
|
Logger.info("Kicking off download for media item ##{media_item.id} (#{media_item.media_id})")
|
||||||
|
|
||||||
MediaDownloadWorker.kickoff_with_task(media_item)
|
MediaDownloadWorker.kickoff_with_task(media_item, %{}, job_opts)
|
||||||
else
|
else
|
||||||
{:error, :should_not_download}
|
{:error, :should_not_download}
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ defmodule Pinchflat.Downloading.MediaDownloadWorker do
|
|||||||
|
|
||||||
use Oban.Worker,
|
use Oban.Worker,
|
||||||
queue: :media_fetching,
|
queue: :media_fetching,
|
||||||
|
priority: 5,
|
||||||
unique: [period: :infinity, states: [:available, :scheduled, :retryable, :executing]],
|
unique: [period: :infinity, states: [:available, :scheduled, :retryable, :executing]],
|
||||||
tags: ["media_item", "media_fetching", "show_in_dashboard"]
|
tags: ["media_item", "media_fetching", "show_in_dashboard"]
|
||||||
|
|
||||||
@@ -49,8 +50,7 @@ defmodule Pinchflat.Downloading.MediaDownloadWorker do
|
|||||||
|
|
||||||
media_item = fetch_and_run_prevent_download_user_script(media_item_id)
|
media_item = fetch_and_run_prevent_download_user_script(media_item_id)
|
||||||
|
|
||||||
# If the source or media item is set to not download media, perform a no-op unless forced
|
if should_download_media?(media_item, should_force, is_quality_upgrade) do
|
||||||
if (media_item.source.download_media && !media_item.prevent_download) || should_force do
|
|
||||||
download_media_and_schedule_jobs(media_item, is_quality_upgrade, should_force)
|
download_media_and_schedule_jobs(media_item, is_quality_upgrade, should_force)
|
||||||
else
|
else
|
||||||
:ok
|
:ok
|
||||||
@@ -60,6 +60,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 this is a quality upgrade, only check if the source is set to download media
|
||||||
|
# or that the media item's download hasn't been prevented
|
||||||
|
defp should_download_media?(media_item, should_force, true = _is_quality_upgrade) do
|
||||||
|
(media_item.source.download_media && !media_item.prevent_download) || should_force
|
||||||
|
end
|
||||||
|
|
||||||
|
# If it's not a quality upgrade, additionally check if the media item is pending download
|
||||||
|
defp should_download_media?(media_item, should_force, _is_quality_upgrade) do
|
||||||
|
source = media_item.source
|
||||||
|
is_pending = Media.pending_download?(media_item)
|
||||||
|
|
||||||
|
(is_pending && source.download_media && !media_item.prevent_download) || should_force
|
||||||
|
end
|
||||||
|
|
||||||
# If a user script exists and, when run, returns a non-zero exit code, prevent this and all future downloads
|
# If a user script exists and, when run, returns a non-zero exit code, prevent this and all future downloads
|
||||||
# of the media item.
|
# of the media item.
|
||||||
defp fetch_and_run_prevent_download_user_script(media_item_id) do
|
defp fetch_and_run_prevent_download_user_script(media_item_id) do
|
||||||
@@ -91,13 +105,13 @@ defmodule Pinchflat.Downloading.MediaDownloadWorker do
|
|||||||
|
|
||||||
:ok
|
:ok
|
||||||
|
|
||||||
{:recovered, _} ->
|
{:recovered, _media_item, _message} ->
|
||||||
{:error, :retry}
|
{:error, :retry}
|
||||||
|
|
||||||
{:error, :unsuitable_for_download} ->
|
{:error, :unsuitable_for_download, _message} ->
|
||||||
{:ok, :non_retry}
|
{:ok, :non_retry}
|
||||||
|
|
||||||
{:error, message} ->
|
{:error, _error_atom, message} ->
|
||||||
action_on_error(message)
|
action_on_error(message)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -115,7 +129,11 @@ defmodule Pinchflat.Downloading.MediaDownloadWorker do
|
|||||||
defp action_on_error(message) do
|
defp action_on_error(message) do
|
||||||
# This will attempt re-download at the next indexing, but it won't be retried
|
# This will attempt re-download at the next indexing, but it won't be retried
|
||||||
# immediately as part of job failure logic
|
# immediately as part of job failure logic
|
||||||
non_retryable_errors = ["Video unavailable", "Sign in to confirm"]
|
non_retryable_errors = [
|
||||||
|
"Video unavailable",
|
||||||
|
"Sign in to confirm",
|
||||||
|
"This video is available to this channel's members"
|
||||||
|
]
|
||||||
|
|
||||||
if String.contains?(to_string(message), non_retryable_errors) do
|
if String.contains?(to_string(message), non_retryable_errors) do
|
||||||
Logger.error("yt-dlp download will not be retried: #{inspect(message)}")
|
Logger.error("yt-dlp download will not be retried: #{inspect(message)}")
|
||||||
|
|||||||
@@ -9,7 +9,9 @@ defmodule Pinchflat.Downloading.MediaDownloader do
|
|||||||
|
|
||||||
alias Pinchflat.Repo
|
alias Pinchflat.Repo
|
||||||
alias Pinchflat.Media
|
alias Pinchflat.Media
|
||||||
|
alias Pinchflat.Sources
|
||||||
alias Pinchflat.Media.MediaItem
|
alias Pinchflat.Media.MediaItem
|
||||||
|
alias Pinchflat.Utils.StringUtils
|
||||||
alias Pinchflat.Metadata.NfoBuilder
|
alias Pinchflat.Metadata.NfoBuilder
|
||||||
alias Pinchflat.Metadata.MetadataParser
|
alias Pinchflat.Metadata.MetadataParser
|
||||||
alias Pinchflat.Metadata.MetadataFileHelpers
|
alias Pinchflat.Metadata.MetadataFileHelpers
|
||||||
@@ -20,16 +22,57 @@ defmodule Pinchflat.Downloading.MediaDownloader do
|
|||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
Downloads media for a media item, updating the media item based on the metadata
|
Downloads media for a media item, updating the media item based on the metadata
|
||||||
returned by yt-dlp. Also saves the entire metadata response to the associated
|
returned by yt-dlp. Encountered errors are saved to the Media Item record. Saves
|
||||||
media_metadata record.
|
the entire metadata response to the associated media_metadata record.
|
||||||
|
|
||||||
NOTE: related methods (like the download worker) won't download if the media item's source
|
NOTE: related methods (like the download worker) won't download if Pthe media item's source
|
||||||
is set to not download media. However, I'm not enforcing that here since I need this for testing.
|
is set to not download media. However, I'm not enforcing that here since I need this for testing.
|
||||||
This may change in the future but I'm not stressed.
|
This may change in the future but I'm not stressed.
|
||||||
|
|
||||||
Returns {:ok, %MediaItem{}} | {:error, any, ...any}
|
Returns {:ok, %MediaItem{}} | {:error, atom(), String.t()} | {:recovered, %MediaItem{}, String.t()}
|
||||||
"""
|
"""
|
||||||
def download_for_media_item(%MediaItem{} = media_item, override_opts \\ []) do
|
def download_for_media_item(%MediaItem{} = media_item, override_opts \\ []) do
|
||||||
|
case attempt_download_and_update_for_media_item(media_item, override_opts) do
|
||||||
|
{:ok, media_item} ->
|
||||||
|
# Returns {:ok, %MediaItem{}}
|
||||||
|
Media.update_media_item(media_item, %{last_error: nil})
|
||||||
|
|
||||||
|
{:error, error_atom, message} ->
|
||||||
|
Media.update_media_item(media_item, %{last_error: StringUtils.wrap_string(message)})
|
||||||
|
|
||||||
|
{:error, error_atom, message}
|
||||||
|
|
||||||
|
{:recovered, media_item, message} ->
|
||||||
|
{:ok, updated_media_item} = Media.update_media_item(media_item, %{last_error: StringUtils.wrap_string(message)})
|
||||||
|
|
||||||
|
{:recovered, updated_media_item, message}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# Looks complicated, but here's the key points:
|
||||||
|
# - download_with_options runs a pre-check to see if the media item is suitable for download.
|
||||||
|
# - If the media item fails the precheck, it returns {:error, :unsuitable_for_download, message}
|
||||||
|
# - However, if the precheck fails in a way that we think can be fixed by using cookies, we retry with cookies
|
||||||
|
# and return the result of that
|
||||||
|
# - If the precheck passes but the download fails, it normally returns {:error, :download_failed, message}
|
||||||
|
# - However, there are some errors we can recover from (eg: failure to communicate with SponsorBlock).
|
||||||
|
# In this case, we attempt the download anyway and update the media item with what details we do have.
|
||||||
|
# This case returns {:recovered, updated_media_item, message}
|
||||||
|
# - If we attempt a retry but it fails, we return {:error, :unrecoverable, message}
|
||||||
|
# - If there is an unknown error unrelated to the above, we return {:error, :unknown, message}
|
||||||
|
# - Finally, if there is no error, we update the media item with the parsed JSON and return {:ok, updated_media_item}
|
||||||
|
#
|
||||||
|
# Restated, here are the return values for each case:
|
||||||
|
# - On success: {:ok, updated_media_item}
|
||||||
|
# - On initial failure but successfully recovered: {:recovered, updated_media_item, message}
|
||||||
|
# - On error: {:error, error_atom, message} where error_atom is one of:
|
||||||
|
# - `:unsuitable_for_download` if the media item fails the precheck
|
||||||
|
# - `:unrecoverable` if there was an initial failure and the recovery attempt failed
|
||||||
|
# - `:download_failed` for all other yt-dlp-related downloading errors
|
||||||
|
# - `:unknown` for any other errors, including those not related to yt-dlp
|
||||||
|
# - If we retry using cookies, all of the above return values apply. The cookie retry
|
||||||
|
# logic is handled transparently as far as the caller is concerned
|
||||||
|
defp attempt_download_and_update_for_media_item(media_item, override_opts) do
|
||||||
output_filepath = FilesystemUtils.generate_metadata_tmpfile(:json)
|
output_filepath = FilesystemUtils.generate_metadata_tmpfile(:json)
|
||||||
media_with_preloads = Repo.preload(media_item, [:metadata, source: :media_profile])
|
media_with_preloads = Repo.preload(media_item, [:metadata, source: :media_profile])
|
||||||
|
|
||||||
@@ -38,31 +81,30 @@ defmodule Pinchflat.Downloading.MediaDownloader do
|
|||||||
update_media_item_from_parsed_json(media_with_preloads, parsed_json)
|
update_media_item_from_parsed_json(media_with_preloads, parsed_json)
|
||||||
|
|
||||||
{:error, :unsuitable_for_download} ->
|
{:error, :unsuitable_for_download} ->
|
||||||
Logger.warning(
|
message =
|
||||||
"Media item ##{media_with_preloads.id} isn't suitable for download yet. May be an active or processing live stream"
|
"Media item ##{media_with_preloads.id} isn't suitable for download yet. May be an active or processing live stream"
|
||||||
)
|
|
||||||
|
|
||||||
{:error, :unsuitable_for_download}
|
Logger.warning(message)
|
||||||
|
|
||||||
|
{:error, :unsuitable_for_download, message}
|
||||||
|
|
||||||
{:error, message, _exit_code} ->
|
{:error, message, _exit_code} ->
|
||||||
Logger.error("yt-dlp download error for media item ##{media_with_preloads.id}: #{inspect(message)}")
|
Logger.error("yt-dlp download error for media item ##{media_with_preloads.id}: #{inspect(message)}")
|
||||||
|
|
||||||
if String.contains?(to_string(message), recoverable_errors()) do
|
if String.contains?(to_string(message), recoverable_errors()) do
|
||||||
attempt_update_media_item(media_with_preloads, output_filepath)
|
attempt_recovery_from_error(media_with_preloads, output_filepath, message)
|
||||||
|
|
||||||
{:recovered, message}
|
|
||||||
else
|
else
|
||||||
{:error, message}
|
{:error, :download_failed, message}
|
||||||
end
|
end
|
||||||
|
|
||||||
err ->
|
err ->
|
||||||
Logger.error("Unknown error downloading media item ##{media_with_preloads.id}: #{inspect(err)}")
|
Logger.error("Unknown error downloading media item ##{media_with_preloads.id}: #{inspect(err)}")
|
||||||
|
|
||||||
{:error, "Unknown error: #{inspect(err)}"}
|
{:error, :unknown, "Unknown error: #{inspect(err)}"}
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
defp attempt_update_media_item(media_with_preloads, output_filepath) do
|
defp attempt_recovery_from_error(media_with_preloads, output_filepath, error_message) do
|
||||||
with {:ok, contents} <- File.read(output_filepath),
|
with {:ok, contents} <- File.read(output_filepath),
|
||||||
{:ok, parsed_json} <- Phoenix.json_library().decode(contents) do
|
{:ok, parsed_json} <- Phoenix.json_library().decode(contents) do
|
||||||
Logger.info("""
|
Logger.info("""
|
||||||
@@ -71,12 +113,13 @@ defmodule Pinchflat.Downloading.MediaDownloader do
|
|||||||
anyway
|
anyway
|
||||||
""")
|
""")
|
||||||
|
|
||||||
update_media_item_from_parsed_json(media_with_preloads, parsed_json)
|
{:ok, updated_media_item} = update_media_item_from_parsed_json(media_with_preloads, parsed_json)
|
||||||
|
{:recovered, updated_media_item, error_message}
|
||||||
else
|
else
|
||||||
err ->
|
err ->
|
||||||
Logger.error("Unable to recover error for media item ##{media_with_preloads.id}: #{inspect(err)}")
|
Logger.error("Unable to recover error for media item ##{media_with_preloads.id}: #{inspect(err)}")
|
||||||
|
|
||||||
{:error, :retry_failed}
|
{:error, :unrecoverable, error_message}
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -113,13 +156,48 @@ defmodule Pinchflat.Downloading.MediaDownloader do
|
|||||||
|
|
||||||
defp download_with_options(url, item_with_preloads, output_filepath, override_opts) do
|
defp download_with_options(url, item_with_preloads, output_filepath, override_opts) do
|
||||||
{:ok, options} = DownloadOptionBuilder.build(item_with_preloads, override_opts)
|
{:ok, options} = DownloadOptionBuilder.build(item_with_preloads, override_opts)
|
||||||
use_cookies = item_with_preloads.source.use_cookies
|
force_use_cookies = Keyword.get(override_opts, :force_use_cookies, false)
|
||||||
runner_opts = [output_filepath: output_filepath, use_cookies: use_cookies]
|
source_uses_cookies = Sources.use_cookies?(item_with_preloads.source, :downloading)
|
||||||
|
should_use_cookies = force_use_cookies || source_uses_cookies
|
||||||
|
|
||||||
case YtDlpMedia.get_downloadable_status(url, use_cookies: use_cookies) do
|
runner_opts = [output_filepath: output_filepath, use_cookies: should_use_cookies]
|
||||||
{:ok, :downloadable} -> YtDlpMedia.download(url, options, runner_opts)
|
|
||||||
{:ok, :ignorable} -> {:error, :unsuitable_for_download}
|
case {YtDlpMedia.get_downloadable_status(url, use_cookies: should_use_cookies), should_use_cookies} do
|
||||||
err -> err
|
{{:ok, :downloadable}, _} ->
|
||||||
|
YtDlpMedia.download(url, options, runner_opts)
|
||||||
|
|
||||||
|
{{:ok, :ignorable}, _} ->
|
||||||
|
{:error, :unsuitable_for_download}
|
||||||
|
|
||||||
|
{{:error, _message, _exit_code} = err, false} ->
|
||||||
|
# If there was an error and we don't have cookies, this method will retry with cookies
|
||||||
|
# if doing so would help AND the source allows. Otherwise, it will return the error as-is
|
||||||
|
maybe_retry_with_cookies(url, item_with_preloads, output_filepath, override_opts, err)
|
||||||
|
|
||||||
|
# This gets hit if cookies are enabled which, importantly, also covers the case where we
|
||||||
|
# retry a download with cookies and it fails again
|
||||||
|
{{:error, message, exit_code}, true} ->
|
||||||
|
{:error, message, exit_code}
|
||||||
|
|
||||||
|
{err, _} ->
|
||||||
|
err
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp maybe_retry_with_cookies(url, item_with_preloads, output_filepath, override_opts, err) do
|
||||||
|
{:error, message, _} = err
|
||||||
|
source = item_with_preloads.source
|
||||||
|
message_contains_cookie_error = String.contains?(to_string(message), recoverable_cookie_errors())
|
||||||
|
|
||||||
|
if Sources.use_cookies?(source, :error_recovery) && message_contains_cookie_error do
|
||||||
|
download_with_options(
|
||||||
|
url,
|
||||||
|
item_with_preloads,
|
||||||
|
output_filepath,
|
||||||
|
Keyword.put(override_opts, :force_use_cookies, true)
|
||||||
|
)
|
||||||
|
else
|
||||||
|
err
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -128,4 +206,11 @@ defmodule Pinchflat.Downloading.MediaDownloader do
|
|||||||
"Unable to communicate with SponsorBlock"
|
"Unable to communicate with SponsorBlock"
|
||||||
]
|
]
|
||||||
end
|
end
|
||||||
|
|
||||||
|
defp recoverable_cookie_errors do
|
||||||
|
[
|
||||||
|
"Sign in to confirm",
|
||||||
|
"This video is available to this channel's members"
|
||||||
|
]
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ defmodule Pinchflat.FastIndexing.FastIndexingHelpers do
|
|||||||
alias Pinchflat.Repo
|
alias Pinchflat.Repo
|
||||||
alias Pinchflat.Media
|
alias Pinchflat.Media
|
||||||
alias Pinchflat.Tasks
|
alias Pinchflat.Tasks
|
||||||
|
alias Pinchflat.Sources
|
||||||
alias Pinchflat.Sources.Source
|
alias Pinchflat.Sources.Source
|
||||||
alias Pinchflat.FastIndexing.YoutubeRss
|
alias Pinchflat.FastIndexing.YoutubeRss
|
||||||
alias Pinchflat.FastIndexing.YoutubeApi
|
alias Pinchflat.FastIndexing.YoutubeApi
|
||||||
@@ -40,7 +41,7 @@ defmodule Pinchflat.FastIndexing.FastIndexingHelpers do
|
|||||||
Returns [%MediaItem{}] where each item is a new media item that was created _but not necessarily
|
Returns [%MediaItem{}] where each item is a new media item that was created _but not necessarily
|
||||||
downloaded_.
|
downloaded_.
|
||||||
"""
|
"""
|
||||||
def kickoff_download_tasks_from_youtube_rss_feed(%Source{} = source) do
|
def index_and_kickoff_downloads(%Source{} = source) do
|
||||||
# The media_profile is needed to determine the quality options to _then_ determine a more
|
# The media_profile is needed to determine the quality options to _then_ determine a more
|
||||||
# accurate predicted filepath
|
# accurate predicted filepath
|
||||||
source = Repo.preload(source, [:media_profile])
|
source = Repo.preload(source, [:media_profile])
|
||||||
@@ -53,6 +54,7 @@ defmodule Pinchflat.FastIndexing.FastIndexingHelpers do
|
|||||||
Enum.map(new_media_ids, fn media_id ->
|
Enum.map(new_media_ids, fn media_id ->
|
||||||
case create_media_item_from_media_id(source, media_id) do
|
case create_media_item_from_media_id(source, media_id) do
|
||||||
{:ok, media_item} ->
|
{:ok, media_item} ->
|
||||||
|
DownloadingHelpers.kickoff_download_if_pending(media_item, priority: 0)
|
||||||
media_item
|
media_item
|
||||||
|
|
||||||
err ->
|
err ->
|
||||||
@@ -61,7 +63,9 @@ defmodule Pinchflat.FastIndexing.FastIndexingHelpers do
|
|||||||
end
|
end
|
||||||
end)
|
end)
|
||||||
|
|
||||||
DownloadingHelpers.enqueue_pending_download_tasks(source)
|
# Pick up any stragglers. Intentionally has a lower priority than the per-media item
|
||||||
|
# kickoff above
|
||||||
|
DownloadingHelpers.enqueue_pending_download_tasks(source, priority: 1)
|
||||||
|
|
||||||
Enum.filter(maybe_new_media_items, & &1)
|
Enum.filter(maybe_new_media_items, & &1)
|
||||||
end
|
end
|
||||||
@@ -85,12 +89,16 @@ defmodule Pinchflat.FastIndexing.FastIndexingHelpers do
|
|||||||
|
|
||||||
defp create_media_item_from_media_id(source, media_id) do
|
defp create_media_item_from_media_id(source, media_id) do
|
||||||
url = "https://www.youtube.com/watch?v=#{media_id}"
|
url = "https://www.youtube.com/watch?v=#{media_id}"
|
||||||
|
# This is set to :metadata instead of :indexing since this happens _after_ the
|
||||||
|
# actual indexing process. In reality, slow indexing is the only thing that
|
||||||
|
# should be using :indexing.
|
||||||
|
should_use_cookies = Sources.use_cookies?(source, :metadata)
|
||||||
|
|
||||||
command_opts =
|
command_opts =
|
||||||
[output: DownloadOptionBuilder.build_output_path_for(source)] ++
|
[output: DownloadOptionBuilder.build_output_path_for(source)] ++
|
||||||
DownloadOptionBuilder.build_quality_options_for(source)
|
DownloadOptionBuilder.build_quality_options_for(source)
|
||||||
|
|
||||||
case YtDlpMedia.get_media_attributes(url, command_opts, use_cookies: source.use_cookies) do
|
case YtDlpMedia.get_media_attributes(url, command_opts, use_cookies: should_use_cookies) do
|
||||||
{:ok, media_attrs} ->
|
{:ok, media_attrs} ->
|
||||||
Media.create_media_item_from_backend_attrs(source, media_attrs)
|
Media.create_media_item_from_backend_attrs(source, media_attrs)
|
||||||
|
|
||||||
|
|||||||
@@ -38,8 +38,8 @@ defmodule Pinchflat.FastIndexing.FastIndexingWorker do
|
|||||||
|
|
||||||
Order of operations:
|
Order of operations:
|
||||||
1. FastIndexingWorker (this module) periodically checks the YouTube RSS feed for new media.
|
1. FastIndexingWorker (this module) periodically checks the YouTube RSS feed for new media.
|
||||||
with `FastIndexingHelpers.kickoff_download_tasks_from_youtube_rss_feed`
|
with `FastIndexingHelpers.index_and_kickoff_downloads`
|
||||||
2. If the above `kickoff_download_tasks_from_youtube_rss_feed` finds new media items in the RSS feed,
|
2. If the above `index_and_kickoff_downloads` finds new media items in the RSS feed,
|
||||||
it indexes them with a yt-dlp call to create the media item records then kicks off downloading
|
it indexes them with a yt-dlp call to create the media item records then kicks off downloading
|
||||||
tasks (MediaDownloadWorker) for any new media items _that should be downloaded_.
|
tasks (MediaDownloadWorker) for any new media items _that should be downloaded_.
|
||||||
3. Once downloads are kicked off, this worker sends a notification to the apprise server if applicable
|
3. Once downloads are kicked off, this worker sends a notification to the apprise server if applicable
|
||||||
@@ -67,7 +67,7 @@ defmodule Pinchflat.FastIndexing.FastIndexingWorker do
|
|||||||
|
|
||||||
new_media_items =
|
new_media_items =
|
||||||
source
|
source
|
||||||
|> FastIndexingHelpers.kickoff_download_tasks_from_youtube_rss_feed()
|
|> FastIndexingHelpers.index_and_kickoff_downloads()
|
||||||
|> Enum.filter(&Media.pending_download?(&1))
|
|> Enum.filter(&Media.pending_download?(&1))
|
||||||
|
|
||||||
if source.download_media do
|
if source.download_media do
|
||||||
|
|||||||
@@ -12,6 +12,8 @@ defmodule Pinchflat.FastIndexing.YoutubeApi do
|
|||||||
|
|
||||||
@behaviour YoutubeBehaviour
|
@behaviour YoutubeBehaviour
|
||||||
|
|
||||||
|
@agent_name {:global, __MODULE__.KeyIndex}
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
Determines if the YouTube API is enabled for fast indexing by checking
|
Determines if the YouTube API is enabled for fast indexing by checking
|
||||||
if the user has an API key set
|
if the user has an API key set
|
||||||
@@ -19,7 +21,7 @@ defmodule Pinchflat.FastIndexing.YoutubeApi do
|
|||||||
Returns boolean()
|
Returns boolean()
|
||||||
"""
|
"""
|
||||||
@impl YoutubeBehaviour
|
@impl YoutubeBehaviour
|
||||||
def enabled?(), do: is_binary(api_key())
|
def enabled?, do: Enum.any?(api_keys())
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
Fetches the recent media IDs from the YouTube API for a given source.
|
Fetches the recent media IDs from the YouTube API for a given source.
|
||||||
@@ -74,8 +76,45 @@ defmodule Pinchflat.FastIndexing.YoutubeApi do
|
|||||||
|> FunctionUtils.wrap_ok()
|
|> FunctionUtils.wrap_ok()
|
||||||
end
|
end
|
||||||
|
|
||||||
defp api_key do
|
defp api_keys do
|
||||||
Settings.get!(:youtube_api_key)
|
case Settings.get!(:youtube_api_key) do
|
||||||
|
nil ->
|
||||||
|
[]
|
||||||
|
|
||||||
|
keys ->
|
||||||
|
keys
|
||||||
|
|> String.split(",")
|
||||||
|
|> Enum.map(&String.trim/1)
|
||||||
|
|> Enum.reject(&(&1 == ""))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp get_or_start_api_key_agent do
|
||||||
|
case Agent.start(fn -> 0 end, name: @agent_name) do
|
||||||
|
{:ok, pid} -> pid
|
||||||
|
{:error, {:already_started, pid}} -> pid
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# Gets the next API key in round-robin fashion
|
||||||
|
defp next_api_key do
|
||||||
|
keys = api_keys()
|
||||||
|
|
||||||
|
case keys do
|
||||||
|
[] ->
|
||||||
|
nil
|
||||||
|
|
||||||
|
keys ->
|
||||||
|
pid = get_or_start_api_key_agent()
|
||||||
|
|
||||||
|
current_index =
|
||||||
|
Agent.get_and_update(pid, fn current ->
|
||||||
|
{current, rem(current + 1, length(keys))}
|
||||||
|
end)
|
||||||
|
|
||||||
|
Logger.debug("Using YouTube API key: #{Enum.at(keys, current_index)}")
|
||||||
|
Enum.at(keys, current_index)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
defp construct_api_endpoint(playlist_id) do
|
defp construct_api_endpoint(playlist_id) do
|
||||||
@@ -83,7 +122,7 @@ defmodule Pinchflat.FastIndexing.YoutubeApi do
|
|||||||
property_type = "contentDetails"
|
property_type = "contentDetails"
|
||||||
max_results = 50
|
max_results = 50
|
||||||
|
|
||||||
"#{api_base}?part=#{property_type}&maxResults=#{max_results}&playlistId=#{playlist_id}&key=#{api_key()}"
|
"#{api_base}?part=#{property_type}&maxResults=#{max_results}&playlistId=#{playlist_id}&key=#{next_api_key()}"
|
||||||
end
|
end
|
||||||
|
|
||||||
defp http_client do
|
defp http_client do
|
||||||
|
|||||||
@@ -40,6 +40,7 @@ defmodule Pinchflat.Media.MediaItem do
|
|||||||
:thumbnail_filepath,
|
:thumbnail_filepath,
|
||||||
:metadata_filepath,
|
:metadata_filepath,
|
||||||
:nfo_filepath,
|
:nfo_filepath,
|
||||||
|
:last_error,
|
||||||
# These are user or system controlled fields
|
# These are user or system controlled fields
|
||||||
:prevent_download,
|
:prevent_download,
|
||||||
:prevent_culling,
|
:prevent_culling,
|
||||||
@@ -88,6 +89,7 @@ defmodule Pinchflat.Media.MediaItem do
|
|||||||
# Will very likely revisit because I can't leave well-enough alone.
|
# Will very likely revisit because I can't leave well-enough alone.
|
||||||
field :subtitle_filepaths, {:array, {:array, :string}}, default: []
|
field :subtitle_filepaths, {:array, {:array, :string}}, default: []
|
||||||
|
|
||||||
|
field :last_error, :string
|
||||||
field :prevent_download, :boolean, default: false
|
field :prevent_download, :boolean, default: false
|
||||||
field :prevent_culling, :boolean, default: false
|
field :prevent_culling, :boolean, default: false
|
||||||
field :culled_at, :utc_datetime
|
field :culled_at, :utc_datetime
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ defmodule Pinchflat.Metadata.MetadataFileHelpers do
|
|||||||
needed
|
needed
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
alias Pinchflat.Sources
|
||||||
alias Pinchflat.Utils.FilesystemUtils
|
alias Pinchflat.Utils.FilesystemUtils
|
||||||
|
|
||||||
alias Pinchflat.YtDlp.Media, as: YtDlpMedia
|
alias Pinchflat.YtDlp.Media, as: YtDlpMedia
|
||||||
@@ -66,7 +67,7 @@ defmodule Pinchflat.Metadata.MetadataFileHelpers do
|
|||||||
yt_dlp_filepath = generate_filepath_for(media_item_with_preloads, "thumbnail.%(ext)s")
|
yt_dlp_filepath = generate_filepath_for(media_item_with_preloads, "thumbnail.%(ext)s")
|
||||||
real_filepath = generate_filepath_for(media_item_with_preloads, "thumbnail.jpg")
|
real_filepath = generate_filepath_for(media_item_with_preloads, "thumbnail.jpg")
|
||||||
command_opts = [output: yt_dlp_filepath]
|
command_opts = [output: yt_dlp_filepath]
|
||||||
addl_opts = [use_cookies: media_item_with_preloads.source.use_cookies]
|
addl_opts = [use_cookies: Sources.use_cookies?(media_item_with_preloads.source, :metadata)]
|
||||||
|
|
||||||
case YtDlpMedia.download_thumbnail(media_item_with_preloads.original_url, command_opts, addl_opts) do
|
case YtDlpMedia.download_thumbnail(media_item_with_preloads.original_url, command_opts, addl_opts) do
|
||||||
{:ok, _} -> real_filepath
|
{:ok, _} -> real_filepath
|
||||||
|
|||||||
@@ -93,7 +93,7 @@ defmodule Pinchflat.Metadata.SourceMetadataStorageWorker do
|
|||||||
defp determine_series_directory(source) do
|
defp determine_series_directory(source) do
|
||||||
output_path = DownloadOptionBuilder.build_output_path_for(source)
|
output_path = DownloadOptionBuilder.build_output_path_for(source)
|
||||||
runner_opts = [output: output_path]
|
runner_opts = [output: output_path]
|
||||||
addl_opts = [use_cookies: source.use_cookies]
|
addl_opts = [use_cookies: Sources.use_cookies?(source, :metadata)]
|
||||||
{:ok, %{filepath: filepath}} = MediaCollection.get_source_details(source.original_url, runner_opts, addl_opts)
|
{:ok, %{filepath: filepath}} = MediaCollection.get_source_details(source.original_url, runner_opts, addl_opts)
|
||||||
|
|
||||||
case MetadataFileHelpers.series_directory_from_media_filepath(filepath) do
|
case MetadataFileHelpers.series_directory_from_media_filepath(filepath) do
|
||||||
@@ -113,6 +113,7 @@ defmodule Pinchflat.Metadata.SourceMetadataStorageWorker do
|
|||||||
defp fetch_metadata_for_source(source) do
|
defp fetch_metadata_for_source(source) do
|
||||||
tmp_output_path = "#{tmp_directory()}/#{StringUtils.random_string(16)}/source_image.%(ext)S"
|
tmp_output_path = "#{tmp_directory()}/#{StringUtils.random_string(16)}/source_image.%(ext)S"
|
||||||
base_opts = [convert_thumbnails: "jpg", output: tmp_output_path]
|
base_opts = [convert_thumbnails: "jpg", output: tmp_output_path]
|
||||||
|
should_use_cookies = Sources.use_cookies?(source, :metadata)
|
||||||
|
|
||||||
opts =
|
opts =
|
||||||
if source.collection_type == :channel do
|
if source.collection_type == :channel do
|
||||||
@@ -121,7 +122,7 @@ defmodule Pinchflat.Metadata.SourceMetadataStorageWorker do
|
|||||||
base_opts ++ [:write_thumbnail, playlist_items: 1]
|
base_opts ++ [:write_thumbnail, playlist_items: 1]
|
||||||
end
|
end
|
||||||
|
|
||||||
MediaCollection.get_source_metadata(source.original_url, opts, use_cookies: source.use_cookies)
|
MediaCollection.get_source_metadata(source.original_url, opts, use_cookies: should_use_cookies)
|
||||||
end
|
end
|
||||||
|
|
||||||
defp tmp_directory do
|
defp tmp_directory do
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ defmodule Pinchflat.Profiles.MediaProfileDeletionWorker do
|
|||||||
Starts the profile deletion worker. Does not attach it to a task like `kickoff_with_task/2`
|
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
|
since deletion also cancels all tasks for the profile
|
||||||
|
|
||||||
Returns {:ok, %Task{}} | {:error, %Ecto.Changeset{}}
|
Returns {:ok, %Oban.Job{}} | {:error, %Ecto.Changeset{}}
|
||||||
"""
|
"""
|
||||||
def kickoff(profile, job_args \\ %{}, job_opts \\ []) do
|
def kickoff(profile, job_args \\ %{}, job_opts \\ []) do
|
||||||
%{id: profile.id}
|
%{id: profile.id}
|
||||||
|
|||||||
@@ -45,6 +45,6 @@ defmodule Pinchflat.Settings.Setting do
|
|||||||
setting
|
setting
|
||||||
|> cast(attrs, @allowed_fields)
|
|> cast(attrs, @allowed_fields)
|
||||||
|> validate_required(@required_fields)
|
|> validate_required(@required_fields)
|
||||||
|> validate_number(:extractor_sleep_interval_seconds, greater_than: 0)
|
|> validate_number(:extractor_sleep_interval_seconds, greater_than_or_equal_to: 0)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -39,7 +39,6 @@ defmodule Pinchflat.SlowIndexing.SlowIndexingHelpers do
|
|||||||
def kickoff_indexing_task(%Source{} = source, job_args \\ %{}, job_opts \\ []) do
|
def kickoff_indexing_task(%Source{} = source, job_args \\ %{}, job_opts \\ []) do
|
||||||
job_offset_seconds = if job_args[:force], do: 0, else: calculate_job_offset_seconds(source)
|
job_offset_seconds = if job_args[:force], do: 0, else: calculate_job_offset_seconds(source)
|
||||||
|
|
||||||
Tasks.delete_pending_tasks_for(source, "FastIndexingWorker")
|
|
||||||
Tasks.delete_pending_tasks_for(source, "MediaCollectionIndexingWorker", include_executing: true)
|
Tasks.delete_pending_tasks_for(source, "MediaCollectionIndexingWorker", include_executing: true)
|
||||||
|
|
||||||
MediaCollectionIndexingWorker.kickoff_with_task(source, job_args, job_opts ++ [schedule_in: job_offset_seconds])
|
MediaCollectionIndexingWorker.kickoff_with_task(source, job_args, job_opts ++ [schedule_in: job_offset_seconds])
|
||||||
@@ -133,13 +132,14 @@ defmodule Pinchflat.SlowIndexing.SlowIndexingHelpers do
|
|||||||
{:ok, pid} = FileFollowerServer.start_link()
|
{:ok, pid} = FileFollowerServer.start_link()
|
||||||
|
|
||||||
handler = fn filepath -> setup_file_follower_watcher(pid, filepath, source) end
|
handler = fn filepath -> setup_file_follower_watcher(pid, filepath, source) end
|
||||||
|
should_use_cookies = Sources.use_cookies?(source, :indexing)
|
||||||
|
|
||||||
command_opts =
|
command_opts =
|
||||||
[output: DownloadOptionBuilder.build_output_path_for(source)] ++
|
[output: DownloadOptionBuilder.build_output_path_for(source)] ++
|
||||||
DownloadOptionBuilder.build_quality_options_for(source) ++
|
DownloadOptionBuilder.build_quality_options_for(source) ++
|
||||||
build_download_archive_options(source, was_forced)
|
build_download_archive_options(source, was_forced)
|
||||||
|
|
||||||
runner_opts = [file_listener_handler: handler, use_cookies: source.use_cookies]
|
runner_opts = [file_listener_handler: handler, use_cookies: should_use_cookies]
|
||||||
result = MediaCollection.get_media_attributes_for_collection(source.original_url, command_opts, runner_opts)
|
result = MediaCollection.get_media_attributes_for_collection(source.original_url, command_opts, runner_opts)
|
||||||
|
|
||||||
FileFollowerServer.stop(pid)
|
FileFollowerServer.stop(pid)
|
||||||
@@ -231,8 +231,9 @@ defmodule Pinchflat.SlowIndexing.SlowIndexingHelpers do
|
|||||||
# The download archive isn't useful for playlists (since those are ordered arbitrarily)
|
# The download archive isn't useful for playlists (since those are ordered arbitrarily)
|
||||||
# and we don't want to use it if the indexing was forced by the user. In other words,
|
# and we don't want to use it if the indexing was forced by the user. In other words,
|
||||||
# only create an archive for channels that are being indexed as part of their regular
|
# only create an archive for channels that are being indexed as part of their regular
|
||||||
# indexing schedule
|
# indexing schedule. The first indexing pass should also not create an archive.
|
||||||
defp build_download_archive_options(%Source{collection_type: :playlist}, _was_forced), do: []
|
defp build_download_archive_options(%Source{collection_type: :playlist}, _was_forced), do: []
|
||||||
|
defp build_download_archive_options(%Source{last_indexed_at: nil}, _was_forced), do: []
|
||||||
defp build_download_archive_options(_source, true), do: []
|
defp build_download_archive_options(_source, true), do: []
|
||||||
|
|
||||||
defp build_download_archive_options(source, _was_forced) do
|
defp build_download_archive_options(source, _was_forced) do
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ defmodule Pinchflat.Sources.Source do
|
|||||||
series_directory
|
series_directory
|
||||||
index_frequency_minutes
|
index_frequency_minutes
|
||||||
fast_index
|
fast_index
|
||||||
use_cookies
|
cookie_behaviour
|
||||||
download_media
|
download_media
|
||||||
last_indexed_at
|
last_indexed_at
|
||||||
original_url
|
original_url
|
||||||
@@ -78,7 +78,7 @@ defmodule Pinchflat.Sources.Source do
|
|||||||
field :collection_type, Ecto.Enum, values: [:channel, :playlist]
|
field :collection_type, Ecto.Enum, values: [:channel, :playlist]
|
||||||
field :index_frequency_minutes, :integer, default: 60 * 24
|
field :index_frequency_minutes, :integer, default: 60 * 24
|
||||||
field :fast_index, :boolean, default: false
|
field :fast_index, :boolean, default: false
|
||||||
field :use_cookies, :boolean, default: false
|
field :cookie_behaviour, Ecto.Enum, values: [:disabled, :when_needed, :all_operations], default: :disabled
|
||||||
field :download_media, :boolean, default: true
|
field :download_media, :boolean, default: true
|
||||||
field :last_indexed_at, :utc_datetime
|
field :last_indexed_at, :utc_datetime
|
||||||
# Only download media items that were published after this date
|
# Only download media items that were published after this date
|
||||||
|
|||||||
@@ -32,6 +32,19 @@ defmodule Pinchflat.Sources do
|
|||||||
source.output_path_template_override || media_profile.output_path_template
|
source.output_path_template_override || media_profile.output_path_template
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Returns a boolean indicating whether or not cookies should be used for a given operation.
|
||||||
|
|
||||||
|
Returns boolean()
|
||||||
|
"""
|
||||||
|
def use_cookies?(source, operation) when operation in [:indexing, :downloading, :metadata, :error_recovery] do
|
||||||
|
case source.cookie_behaviour do
|
||||||
|
:disabled -> false
|
||||||
|
:all_operations -> true
|
||||||
|
:when_needed -> operation in [:indexing, :error_recovery]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
Returns the list of sources. Returns [%Source{}, ...]
|
Returns the list of sources. Returns [%Source{}, ...]
|
||||||
"""
|
"""
|
||||||
@@ -181,9 +194,9 @@ defmodule Pinchflat.Sources do
|
|||||||
|
|
||||||
defp add_source_details_to_changeset(source, changeset) do
|
defp add_source_details_to_changeset(source, changeset) do
|
||||||
original_url = changeset.changes.original_url
|
original_url = changeset.changes.original_url
|
||||||
use_cookies = Ecto.Changeset.get_field(changeset, :use_cookies)
|
should_use_cookies = Ecto.Changeset.get_field(changeset, :cookie_behaviour) == :all_operations
|
||||||
# Skipping sleep interval since this is UI blocking and we want to keep this as fast as possible
|
# Skipping sleep interval since this is UI blocking and we want to keep this as fast as possible
|
||||||
addl_opts = [use_cookies: use_cookies, skip_sleep_interval: true]
|
addl_opts = [use_cookies: should_use_cookies, skip_sleep_interval: true]
|
||||||
|
|
||||||
case MediaCollection.get_source_details(original_url, [], addl_opts) do
|
case MediaCollection.get_source_details(original_url, [], addl_opts) do
|
||||||
{:ok, source_details} ->
|
{:ok, source_details} ->
|
||||||
@@ -300,6 +313,10 @@ defmodule Pinchflat.Sources do
|
|||||||
%{__meta__: %{state: :built}} ->
|
%{__meta__: %{state: :built}} ->
|
||||||
SlowIndexingHelpers.kickoff_indexing_task(source)
|
SlowIndexingHelpers.kickoff_indexing_task(source)
|
||||||
|
|
||||||
|
if Ecto.Changeset.get_field(changeset, :fast_index) do
|
||||||
|
FastIndexingHelpers.kickoff_indexing_task(source)
|
||||||
|
end
|
||||||
|
|
||||||
# If the record has been persisted, only run indexing if the
|
# If the record has been persisted, only run indexing if the
|
||||||
# indexing frequency has been changed and is now greater than 0
|
# indexing frequency has been changed and is now greater than 0
|
||||||
%{__meta__: %{state: :loaded}} ->
|
%{__meta__: %{state: :loaded}} ->
|
||||||
|
|||||||
@@ -35,4 +35,13 @@ defmodule Pinchflat.Utils.StringUtils do
|
|||||||
def double_brace(string) do
|
def double_brace(string) do
|
||||||
"{{ #{string} }}"
|
"{{ #{string} }}"
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Wraps a string in quotes if it's not already a string. Useful for working with
|
||||||
|
error messages whose types can vary.
|
||||||
|
|
||||||
|
Returns binary()
|
||||||
|
"""
|
||||||
|
def wrap_string(message) when is_binary(message), do: message
|
||||||
|
def wrap_string(message), do: "#{inspect(message)}"
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -76,6 +76,24 @@ defmodule Pinchflat.YtDlp.CommandRunner do
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Updates yt-dlp to the latest version
|
||||||
|
|
||||||
|
Returns {:ok, binary()} | {:error, binary()}
|
||||||
|
"""
|
||||||
|
@impl YtDlpCommandRunner
|
||||||
|
def update do
|
||||||
|
command = backend_executable()
|
||||||
|
|
||||||
|
case CliUtils.wrap_cmd(command, ["--update"]) do
|
||||||
|
{output, 0} ->
|
||||||
|
{:ok, String.trim(output)}
|
||||||
|
|
||||||
|
{output, _} ->
|
||||||
|
{:error, output}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
defp generate_output_filepath(addl_opts) do
|
defp generate_output_filepath(addl_opts) do
|
||||||
case Keyword.get(addl_opts, :output_filepath) do
|
case Keyword.get(addl_opts, :output_filepath) do
|
||||||
nil -> FSUtils.generate_metadata_tmpfile(:json)
|
nil -> FSUtils.generate_metadata_tmpfile(:json)
|
||||||
|
|||||||
@@ -151,7 +151,7 @@ defmodule Pinchflat.YtDlp.Media do
|
|||||||
#
|
#
|
||||||
# These don't fail if duration or aspect_ratio are missing
|
# These don't fail if duration or aspect_ratio are missing
|
||||||
# due to Elixir's comparison semantics
|
# due to Elixir's comparison semantics
|
||||||
response["duration"] <= 60 && response["aspect_ratio"] <= 0.85
|
response["duration"] <= 180 && response["aspect_ratio"] <= 0.85
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,44 @@
|
|||||||
|
defmodule Pinchflat.YtDlp.UpdateWorker do
|
||||||
|
@moduledoc false
|
||||||
|
|
||||||
|
use Oban.Worker,
|
||||||
|
queue: :local_data,
|
||||||
|
tags: ["local_data"]
|
||||||
|
|
||||||
|
require Logger
|
||||||
|
|
||||||
|
alias __MODULE__
|
||||||
|
alias Pinchflat.Settings
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Starts the yt-dlp update worker. Does not attach it to a task like `kickoff_with_task/2`
|
||||||
|
|
||||||
|
Returns {:ok, %Oban.Job{}} | {:error, %Ecto.Changeset{}}
|
||||||
|
"""
|
||||||
|
def kickoff do
|
||||||
|
Oban.insert(UpdateWorker.new(%{}))
|
||||||
|
end
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Updates yt-dlp and saves the version to the settings.
|
||||||
|
|
||||||
|
This worker is scheduled to run via the Oban Cron plugin as well as on app boot.
|
||||||
|
|
||||||
|
Returns :ok
|
||||||
|
"""
|
||||||
|
@impl Oban.Worker
|
||||||
|
def perform(%Oban.Job{}) do
|
||||||
|
Logger.info("Updating yt-dlp")
|
||||||
|
|
||||||
|
yt_dlp_runner().update()
|
||||||
|
|
||||||
|
{:ok, yt_dlp_version} = yt_dlp_runner().version()
|
||||||
|
Settings.set(yt_dlp_version: yt_dlp_version)
|
||||||
|
|
||||||
|
:ok
|
||||||
|
end
|
||||||
|
|
||||||
|
defp yt_dlp_runner do
|
||||||
|
Application.get_env(:pinchflat, :yt_dlp_runner)
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -9,4 +9,5 @@ defmodule Pinchflat.YtDlp.YtDlpCommandRunner do
|
|||||||
@callback run(binary(), atom(), keyword(), binary()) :: {:ok, binary()} | {:error, binary(), integer()}
|
@callback run(binary(), atom(), keyword(), binary()) :: {:ok, binary()} | {:error, binary(), integer()}
|
||||||
@callback run(binary(), atom(), keyword(), binary(), keyword()) :: {:ok, binary()} | {:error, binary(), integer()}
|
@callback run(binary(), atom(), keyword(), binary(), keyword()) :: {:ok, binary()} | {:error, binary(), integer()}
|
||||||
@callback version() :: {:ok, binary()} | {:error, binary()}
|
@callback version() :: {:ok, binary()} | {:error, binary()}
|
||||||
|
@callback update() :: {:ok, binary()} | {:error, binary()}
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ defmodule PinchflatWeb.CustomComponents.ButtonComponents do
|
|||||||
use Phoenix.Component, global_prefixes: ~w(x-)
|
use Phoenix.Component, global_prefixes: ~w(x-)
|
||||||
|
|
||||||
alias PinchflatWeb.CoreComponents
|
alias PinchflatWeb.CoreComponents
|
||||||
|
alias PinchflatWeb.CustomComponents.TextComponents
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
Render a button
|
Render a button
|
||||||
@@ -104,7 +105,7 @@ defmodule PinchflatWeb.CustomComponents.ButtonComponents do
|
|||||||
|
|
||||||
def icon_button(assigns) do
|
def icon_button(assigns) do
|
||||||
~H"""
|
~H"""
|
||||||
<div class="group relative inline-block">
|
<TextComponents.tooltip position="bottom" tooltip={@tooltip} tooltip_class="text-nowrap">
|
||||||
<button
|
<button
|
||||||
class={[
|
class={[
|
||||||
"flex justify-center items-center rounded-lg ",
|
"flex justify-center items-center rounded-lg ",
|
||||||
@@ -117,18 +118,7 @@ defmodule PinchflatWeb.CustomComponents.ButtonComponents do
|
|||||||
>
|
>
|
||||||
<CoreComponents.icon name={@icon_name} class="text-stroke" />
|
<CoreComponents.icon name={@icon_name} class="text-stroke" />
|
||||||
</button>
|
</button>
|
||||||
<div
|
</TextComponents.tooltip>
|
||||||
:if={@tooltip}
|
|
||||||
class={[
|
|
||||||
"hidden absolute left-1/2 top-full z-20 mt-3 -translate-x-1/2 whitespace-nowrap rounded-md",
|
|
||||||
"px-4.5 py-1.5 text-sm font-medium opacity-0 drop-shadow-4 group-hover:opacity-100 group-hover:block bg-meta-4"
|
|
||||||
]}
|
|
||||||
>
|
|
||||||
<span class="border-light absolute -top-1 left-1/2 -z-10 h-2 w-2 -translate-x-1/2 rotate-45 rounded-sm bg-meta-4">
|
|
||||||
</span>
|
|
||||||
<span>{@tooltip}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
"""
|
"""
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -146,4 +146,60 @@ defmodule PinchflatWeb.CustomComponents.TextComponents do
|
|||||||
<.localized_number number={@num} /> {@suffix}
|
<.localized_number number={@num} /> {@suffix}
|
||||||
"""
|
"""
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Renders a tooltip with the given content
|
||||||
|
"""
|
||||||
|
|
||||||
|
attr :tooltip, :string, required: true
|
||||||
|
attr :position, :string, default: ""
|
||||||
|
attr :tooltip_class, :any, default: ""
|
||||||
|
attr :tooltip_arrow_class, :any, default: ""
|
||||||
|
slot :inner_block
|
||||||
|
|
||||||
|
def tooltip(%{position: "bottom-right"} = assigns) do
|
||||||
|
~H"""
|
||||||
|
<.tooltip tooltip={@tooltip} tooltip_class={@tooltip_class} tooltip_arrow_class={["-top-1", @tooltip_arrow_class]}>
|
||||||
|
{render_slot(@inner_block)}
|
||||||
|
</.tooltip>
|
||||||
|
"""
|
||||||
|
end
|
||||||
|
|
||||||
|
def tooltip(%{position: "bottom"} = assigns) do
|
||||||
|
~H"""
|
||||||
|
<.tooltip
|
||||||
|
tooltip={@tooltip}
|
||||||
|
tooltip_class={["left-1/2 -translate-x-1/2", @tooltip_class]}
|
||||||
|
tooltip_arrow_class={["-top-1 left-1/2 -translate-x-1/2", @tooltip_arrow_class]}
|
||||||
|
>
|
||||||
|
{render_slot(@inner_block)}
|
||||||
|
</.tooltip>
|
||||||
|
"""
|
||||||
|
end
|
||||||
|
|
||||||
|
def tooltip(assigns) do
|
||||||
|
~H"""
|
||||||
|
<div class="group relative inline-block cursor-pointer">
|
||||||
|
<div>
|
||||||
|
{render_slot(@inner_block)}
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
:if={@tooltip}
|
||||||
|
class={[
|
||||||
|
"hidden absolute top-full z-20 mt-3 whitespace-nowrap rounded-md",
|
||||||
|
"p-1.5 text-sm font-medium opacity-0 drop-shadow-4 group-hover:opacity-100 group-hover:block bg-meta-4",
|
||||||
|
"border border-form-strokedark text-wrap",
|
||||||
|
@tooltip_class
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<span class={[
|
||||||
|
"border-t border-l border-form-strokedark absolute -z-10 h-2 w-2 rotate-45 rounded-sm bg-meta-4",
|
||||||
|
@tooltip_arrow_class
|
||||||
|
]}>
|
||||||
|
</span>
|
||||||
|
<div class="px-3">{@tooltip}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
"""
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -16,7 +16,7 @@
|
|||||||
</.link>
|
</.link>
|
||||||
</nav>
|
</nav>
|
||||||
</div>
|
</div>
|
||||||
<div class="rounded-sm border border-stroke bg-white py-5 pt-6 shadow-default dark:border-strokedark dark:bg-boxdark px-7.5">
|
<div class="rounded-sm border py-5 pt-6 shadow-default border-strokedark bg-boxdark px-7.5">
|
||||||
<div class="max-w-full">
|
<div class="max-w-full">
|
||||||
<.tabbed_layout>
|
<.tabbed_layout>
|
||||||
<:tab_append>
|
<:tab_append>
|
||||||
@@ -24,7 +24,15 @@
|
|||||||
</:tab_append>
|
</:tab_append>
|
||||||
|
|
||||||
<:tab title="Media" id="media">
|
<:tab title="Media" id="media">
|
||||||
<div class="flex flex-col gap-10 dark:text-white">
|
<div class="flex flex-col gap-10 text-white">
|
||||||
|
<section :if={@media_item.last_error} class="mt-6">
|
||||||
|
<div class="flex items-center gap-1 mb-2">
|
||||||
|
<.icon name="hero-exclamation-circle-solid" class="text-red-500" />
|
||||||
|
<h3 class="font-bold text-xl">Last Error</h3>
|
||||||
|
</div>
|
||||||
|
<span>{@media_item.last_error}</span>
|
||||||
|
</section>
|
||||||
|
|
||||||
<%= if media_file_exists?(@media_item) do %>
|
<%= if media_file_exists?(@media_item) do %>
|
||||||
<section class="grid grid-cols-1 xl:gap-6 mt-6">
|
<section class="grid grid-cols-1 xl:gap-6 mt-6">
|
||||||
<div>
|
<div>
|
||||||
@@ -54,19 +62,21 @@
|
|||||||
</section>
|
</section>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|
||||||
<h3 class="font-bold text-xl mt-6">Raw Attributes</h3>
|
|
||||||
<section>
|
<section>
|
||||||
<strong>Source:</strong>
|
<h3 class="font-bold text-xl mb-2">Raw Attributes</h3>
|
||||||
<.subtle_link href={~p"/sources/#{@media_item.source_id}"}>
|
<section>
|
||||||
{@media_item.source.custom_name}
|
<strong>Source:</strong>
|
||||||
</.subtle_link>
|
<.subtle_link href={~p"/sources/#{@media_item.source_id}"}>
|
||||||
<.list_items_from_map map={Map.from_struct(@media_item)} />
|
{@media_item.source.custom_name}
|
||||||
|
</.subtle_link>
|
||||||
|
<.list_items_from_map map={Map.from_struct(@media_item)} />
|
||||||
|
</section>
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
</:tab>
|
</:tab>
|
||||||
<:tab title="Tasks" id="tasks">
|
<:tab title="Tasks" id="tasks">
|
||||||
<%= if match?([_|_], @media_item.tasks) do %>
|
<%= if match?([_|_], @media_item.tasks) do %>
|
||||||
<.table rows={@media_item.tasks} table_class="text-black dark:text-white">
|
<.table rows={@media_item.tasks} table_class="text-white">
|
||||||
<:col :let={task} label="Worker">
|
<:col :let={task} label="Worker">
|
||||||
{task.job.worker}
|
{task.job.worker}
|
||||||
</:col>
|
</:col>
|
||||||
|
|||||||
@@ -25,8 +25,10 @@
|
|||||||
|
|
||||||
<:tab title="Media Profile" id="media-profile">
|
<:tab title="Media Profile" id="media-profile">
|
||||||
<div class="flex flex-col gap-10 text-white">
|
<div class="flex flex-col gap-10 text-white">
|
||||||
<h3 class="font-bold text-xl mt-6">Raw Attributes</h3>
|
<section>
|
||||||
<.list_items_from_map map={Map.from_struct(@media_profile)} />
|
<h3 class="font-bold text-xl mt-6 mb-2">Raw Attributes</h3>
|
||||||
|
<.list_items_from_map map={Map.from_struct(@media_profile)} />
|
||||||
|
</section>
|
||||||
</div>
|
</div>
|
||||||
</:tab>
|
</:tab>
|
||||||
<:tab title="Sources" id="sources">
|
<:tab title="Sources" id="sources">
|
||||||
|
|||||||
@@ -28,10 +28,22 @@ defmodule Pinchflat.Pages.HistoryTableLive do
|
|||||||
</span>
|
</span>
|
||||||
<div class="max-w-full overflow-x-auto">
|
<div class="max-w-full overflow-x-auto">
|
||||||
<.table rows={@records} table_class="text-white">
|
<.table rows={@records} table_class="text-white">
|
||||||
<:col :let={media_item} label="Title" class="truncate max-w-xs">
|
<:col :let={media_item} label="Title" class="max-w-xs">
|
||||||
<.subtle_link href={~p"/sources/#{media_item.source_id}/media/#{media_item}"}>
|
<section class="flex items-center space-x-1">
|
||||||
{media_item.title}
|
<.tooltip
|
||||||
</.subtle_link>
|
:if={media_item.last_error}
|
||||||
|
tooltip={media_item.last_error}
|
||||||
|
position="bottom-right"
|
||||||
|
tooltip_class="w-64"
|
||||||
|
>
|
||||||
|
<.icon name="hero-exclamation-circle-solid" class="text-red-500" />
|
||||||
|
</.tooltip>
|
||||||
|
<span class="truncate">
|
||||||
|
<.subtle_link href={~p"/sources/#{media_item.source_id}/media/#{media_item.id}"}>
|
||||||
|
{media_item.title}
|
||||||
|
</.subtle_link>
|
||||||
|
</span>
|
||||||
|
</section>
|
||||||
</:col>
|
</:col>
|
||||||
<:col :let={media_item} label="Upload Date">
|
<:col :let={media_item} label="Upload Date">
|
||||||
{DateTime.to_date(media_item.uploaded_at)}
|
{DateTime.to_date(media_item.uploaded_at)}
|
||||||
|
|||||||
@@ -34,9 +34,9 @@
|
|||||||
|
|
||||||
<.input
|
<.input
|
||||||
field={f[:youtube_api_key]}
|
field={f[:youtube_api_key]}
|
||||||
placeholder="ABC123"
|
placeholder="ABC123,DEF456"
|
||||||
type="text"
|
type="text"
|
||||||
label="YouTube API Key"
|
label="YouTube API Key(s)"
|
||||||
help={youtube_api_help()}
|
help={youtube_api_help()}
|
||||||
html_help={true}
|
html_help={true}
|
||||||
inputclass="font-mono text-sm mr-4"
|
inputclass="font-mono text-sm mr-4"
|
||||||
|
|||||||
@@ -27,6 +27,14 @@ defmodule PinchflatWeb.Sources.SourceHTML do
|
|||||||
]
|
]
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def friendly_cookie_behaviours do
|
||||||
|
[
|
||||||
|
{"Disabled", :disabled},
|
||||||
|
{"When Needed", :when_needed},
|
||||||
|
{"All Operations", :all_operations}
|
||||||
|
]
|
||||||
|
end
|
||||||
|
|
||||||
def cutoff_date_presets do
|
def cutoff_date_presets do
|
||||||
[
|
[
|
||||||
{"7 days", compute_date_offset(7)},
|
{"7 days", compute_date_offset(7)},
|
||||||
|
|||||||
@@ -46,10 +46,22 @@ defmodule PinchflatWeb.Sources.MediaItemTableLive do
|
|||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
<.table rows={@records} table_class="text-white">
|
<.table rows={@records} table_class="text-white">
|
||||||
<:col :let={media_item} label="Title" class="truncate max-w-xs">
|
<:col :let={media_item} label="Title" class="max-w-xs">
|
||||||
<.subtle_link href={~p"/sources/#{@source.id}/media/#{media_item.id}"}>
|
<section class="flex items-center space-x-1">
|
||||||
{media_item.title}
|
<.tooltip
|
||||||
</.subtle_link>
|
:if={media_item.last_error}
|
||||||
|
tooltip={media_item.last_error}
|
||||||
|
position="bottom-right"
|
||||||
|
tooltip_class="w-64"
|
||||||
|
>
|
||||||
|
<.icon name="hero-exclamation-circle-solid" class="text-red-500" />
|
||||||
|
</.tooltip>
|
||||||
|
<span class="truncate">
|
||||||
|
<.subtle_link href={~p"/sources/#{@source.id}/media/#{media_item.id}"}>
|
||||||
|
{media_item.title}
|
||||||
|
</.subtle_link>
|
||||||
|
</span>
|
||||||
|
</section>
|
||||||
</:col>
|
</:col>
|
||||||
<:col :let={media_item} :if={@media_state == "other"} label="Manually Ignored?">
|
<:col :let={media_item} :if={@media_state == "other"} label="Manually Ignored?">
|
||||||
<.icon name={if media_item.prevent_download, do: "hero-check", else: "hero-x-mark"} />
|
<.icon name={if media_item.prevent_download, do: "hero-check", else: "hero-x-mark"} />
|
||||||
@@ -205,6 +217,6 @@ defmodule PinchflatWeb.Sources.MediaItemTableLive do
|
|||||||
|
|
||||||
# Selecting only what we need GREATLY speeds up queries on large tables
|
# Selecting only what we need GREATLY speeds up queries on large tables
|
||||||
defp select_fields do
|
defp select_fields do
|
||||||
[:id, :title, :uploaded_at, :prevent_download]
|
[:id, :title, :uploaded_at, :prevent_download, :last_error]
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -24,16 +24,18 @@
|
|||||||
</:tab_append>
|
</:tab_append>
|
||||||
|
|
||||||
<:tab title="Source" id="source">
|
<:tab title="Source" id="source">
|
||||||
<div class="flex flex-col gap-10 text-white">
|
<div class="flex flex-col text-white gap-10">
|
||||||
<h3 class="font-bold text-xl mt-6">Raw Attributes</h3>
|
|
||||||
<section>
|
<section>
|
||||||
<strong>Media Profile:</strong>
|
<h3 class="font-bold text-xl mb-2 mt-6">Raw Attributes</h3>
|
||||||
<.subtle_link href={~p"/media_profiles/#{@source.media_profile_id}"}>
|
<section>
|
||||||
{@source.media_profile.name}
|
<strong>Media Profile:</strong>
|
||||||
</.subtle_link>
|
<.subtle_link href={~p"/media_profiles/#{@source.media_profile_id}"}>
|
||||||
</section>
|
{@source.media_profile.name}
|
||||||
|
</.subtle_link>
|
||||||
|
</section>
|
||||||
|
|
||||||
<.list_items_from_map map={Map.from_struct(@source)} />
|
<.list_items_from_map map={Map.from_struct(@source)} />
|
||||||
|
</section>
|
||||||
</div>
|
</div>
|
||||||
</:tab>
|
</:tab>
|
||||||
<:tab title="Pending" id="pending">
|
<:tab title="Pending" id="pending">
|
||||||
|
|||||||
@@ -87,10 +87,11 @@
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<.input
|
<.input
|
||||||
field={f[:use_cookies]}
|
field={f[:cookie_behaviour]}
|
||||||
type="toggle"
|
options={friendly_cookie_behaviours()}
|
||||||
label="Use Cookies for Downloading"
|
type="select"
|
||||||
help="Uses your YouTube cookies for this source (if configured). Used for downloading private playlists and videos. See docs for important details"
|
label="Cookie Behaviour"
|
||||||
|
help="Uses your YouTube cookies for this source (if configured). 'When Needed' tries to minimize cookie usage except for certain indexing and downloading tasks. See docs"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<section x-show="advancedMode">
|
<section x-show="advancedMode">
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ defmodule PinchflatWeb.Endpoint do
|
|||||||
plug Plug.Static,
|
plug Plug.Static,
|
||||||
at: "/",
|
at: "/",
|
||||||
from: :pinchflat,
|
from: :pinchflat,
|
||||||
gzip: Mix.env() == :prod,
|
gzip: Application.compile_env(:pinchflat, :env) == :prod,
|
||||||
only: PinchflatWeb.static_paths()
|
only: PinchflatWeb.static_paths()
|
||||||
|
|
||||||
# Code reloading can be explicitly enabled under the
|
# Code reloading can be explicitly enabled under the
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ defmodule Pinchflat.MixProject do
|
|||||||
def project do
|
def project do
|
||||||
[
|
[
|
||||||
app: :pinchflat,
|
app: :pinchflat,
|
||||||
version: "2025.1.14",
|
version: "2025.3.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,
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 443 KiB After Width: | Height: | Size: 497 KiB |
@@ -0,0 +1,9 @@
|
|||||||
|
defmodule Pinchflat.Repo.Migrations.AddLastErrorToMediaItem do
|
||||||
|
use Ecto.Migration
|
||||||
|
|
||||||
|
def change do
|
||||||
|
alter table(:media_items) do
|
||||||
|
add :last_error, :string
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
defmodule Pinchflat.Repo.Migrations.AddCookieBehaviourToSources do
|
||||||
|
use Ecto.Migration
|
||||||
|
|
||||||
|
def change do
|
||||||
|
alter table(:sources) do
|
||||||
|
add :cookie_behaviour, :string, null: false, default: "disabled"
|
||||||
|
end
|
||||||
|
|
||||||
|
execute(
|
||||||
|
"UPDATE sources SET cookie_behaviour = 'all_operations' WHERE use_cookies = TRUE",
|
||||||
|
"UPDATE sources SET use_cookies = TRUE WHERE cookie_behaviour = 'all_operations'"
|
||||||
|
)
|
||||||
|
|
||||||
|
alter table(:sources) do
|
||||||
|
remove :use_cookies, :boolean, null: false, default: false
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -6,6 +6,9 @@ if [ $? -ne 0 ]; then
|
|||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
echo "Setting umask to ${UMASK}"
|
||||||
|
umask ${UMASK}
|
||||||
|
|
||||||
/app/bin/migrate
|
/app/bin/migrate
|
||||||
|
|
||||||
cd -P -- "$(dirname -- "$0")"
|
cd -P -- "$(dirname -- "$0")"
|
||||||
|
|||||||
@@ -0,0 +1,16 @@
|
|||||||
|
defmodule Pinchflat.Boot.PostBootStartupTasksTest do
|
||||||
|
use Pinchflat.DataCase
|
||||||
|
|
||||||
|
alias Pinchflat.YtDlp.UpdateWorker
|
||||||
|
alias Pinchflat.Boot.PostBootStartupTasks
|
||||||
|
|
||||||
|
describe "update_yt_dlp" do
|
||||||
|
test "enqueues an update job" do
|
||||||
|
assert [] = all_enqueued(worker: UpdateWorker)
|
||||||
|
|
||||||
|
PostBootStartupTasks.init(%{})
|
||||||
|
|
||||||
|
assert [%Oban.Job{}] = all_enqueued(worker: UpdateWorker)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -10,7 +10,7 @@ defmodule Pinchflat.Downloading.DownloadingHelpersTest do
|
|||||||
alias Pinchflat.Downloading.MediaDownloadWorker
|
alias Pinchflat.Downloading.MediaDownloadWorker
|
||||||
|
|
||||||
describe "enqueue_pending_download_tasks/1" do
|
describe "enqueue_pending_download_tasks/1" do
|
||||||
test "it enqueues a job for each pending media item" do
|
test "enqueues a job for each pending media item" do
|
||||||
source = source_fixture()
|
source = source_fixture()
|
||||||
media_item = media_item_fixture(source_id: source.id, media_filepath: nil)
|
media_item = media_item_fixture(source_id: source.id, media_filepath: nil)
|
||||||
|
|
||||||
@@ -19,7 +19,7 @@ defmodule Pinchflat.Downloading.DownloadingHelpersTest do
|
|||||||
assert_enqueued(worker: MediaDownloadWorker, args: %{"id" => media_item.id})
|
assert_enqueued(worker: MediaDownloadWorker, args: %{"id" => media_item.id})
|
||||||
end
|
end
|
||||||
|
|
||||||
test "it does not enqueue a job for media items with a filepath" do
|
test "does not enqueue a job for media items with a filepath" do
|
||||||
source = source_fixture()
|
source = source_fixture()
|
||||||
_media_item = media_item_fixture(source_id: source.id, media_filepath: "some/filepath.mp4")
|
_media_item = media_item_fixture(source_id: source.id, media_filepath: "some/filepath.mp4")
|
||||||
|
|
||||||
@@ -28,7 +28,7 @@ defmodule Pinchflat.Downloading.DownloadingHelpersTest do
|
|||||||
refute_enqueued(worker: MediaDownloadWorker)
|
refute_enqueued(worker: MediaDownloadWorker)
|
||||||
end
|
end
|
||||||
|
|
||||||
test "it attaches a task to each enqueued job" do
|
test "attaches a task to each enqueued job" do
|
||||||
source = source_fixture()
|
source = source_fixture()
|
||||||
media_item = media_item_fixture(source_id: source.id, media_filepath: nil)
|
media_item = media_item_fixture(source_id: source.id, media_filepath: nil)
|
||||||
|
|
||||||
@@ -39,7 +39,7 @@ defmodule Pinchflat.Downloading.DownloadingHelpersTest do
|
|||||||
assert [_] = Tasks.list_tasks_for(media_item)
|
assert [_] = Tasks.list_tasks_for(media_item)
|
||||||
end
|
end
|
||||||
|
|
||||||
test "it does not create a job if the source is set to not download" do
|
test "does not create a job if the source is set to not download" do
|
||||||
source = source_fixture(download_media: false)
|
source = source_fixture(download_media: false)
|
||||||
|
|
||||||
assert :ok = DownloadingHelpers.enqueue_pending_download_tasks(source)
|
assert :ok = DownloadingHelpers.enqueue_pending_download_tasks(source)
|
||||||
@@ -47,17 +47,26 @@ defmodule Pinchflat.Downloading.DownloadingHelpersTest do
|
|||||||
refute_enqueued(worker: MediaDownloadWorker)
|
refute_enqueued(worker: MediaDownloadWorker)
|
||||||
end
|
end
|
||||||
|
|
||||||
test "it does not attach tasks if the source is set to not download" do
|
test "does not attach tasks if the source is set to not download" do
|
||||||
source = source_fixture(download_media: false)
|
source = source_fixture(download_media: false)
|
||||||
media_item = media_item_fixture(source_id: source.id, media_filepath: nil)
|
media_item = media_item_fixture(source_id: source.id, media_filepath: nil)
|
||||||
|
|
||||||
assert :ok = DownloadingHelpers.enqueue_pending_download_tasks(source)
|
assert :ok = DownloadingHelpers.enqueue_pending_download_tasks(source)
|
||||||
assert [] = Tasks.list_tasks_for(media_item)
|
assert [] = Tasks.list_tasks_for(media_item)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
test "can pass job options" do
|
||||||
|
source = source_fixture()
|
||||||
|
media_item = media_item_fixture(source_id: source.id, media_filepath: nil)
|
||||||
|
|
||||||
|
assert :ok = DownloadingHelpers.enqueue_pending_download_tasks(source, priority: 1)
|
||||||
|
|
||||||
|
assert_enqueued(worker: MediaDownloadWorker, args: %{"id" => media_item.id}, priority: 1)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
describe "dequeue_pending_download_tasks/1" do
|
describe "dequeue_pending_download_tasks/1" do
|
||||||
test "it deletes all pending tasks for a source's media items" do
|
test "deletes all pending tasks for a source's media items" do
|
||||||
source = source_fixture()
|
source = source_fixture()
|
||||||
media_item = media_item_fixture(source_id: source.id, media_filepath: nil)
|
media_item = media_item_fixture(source_id: source.id, media_filepath: nil)
|
||||||
|
|
||||||
@@ -109,6 +118,14 @@ defmodule Pinchflat.Downloading.DownloadingHelpersTest do
|
|||||||
|
|
||||||
refute_enqueued(worker: MediaDownloadWorker)
|
refute_enqueued(worker: MediaDownloadWorker)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
test "can pass job options" do
|
||||||
|
media_item = media_item_fixture(media_filepath: nil)
|
||||||
|
|
||||||
|
assert {:ok, _} = DownloadingHelpers.kickoff_download_if_pending(media_item, priority: 1)
|
||||||
|
|
||||||
|
assert_enqueued(worker: MediaDownloadWorker, args: %{"id" => media_item.id}, priority: 1)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
describe "kickoff_redownload_for_existing_media/1" do
|
describe "kickoff_redownload_for_existing_media/1" do
|
||||||
|
|||||||
@@ -46,13 +46,20 @@ defmodule Pinchflat.Downloading.MediaDownloadWorkerTest do
|
|||||||
assert_enqueued(worker: MediaDownloadWorker, args: %{"id" => media_item.id, "force" => true})
|
assert_enqueued(worker: MediaDownloadWorker, args: %{"id" => media_item.id, "force" => true})
|
||||||
end
|
end
|
||||||
|
|
||||||
test "can be called with additional job options", %{media_item: media_item} do
|
test "has a priority of 5 by default", %{media_item: media_item} do
|
||||||
job_opts = [max_attempts: 5]
|
assert {:ok, _} = MediaDownloadWorker.kickoff_with_task(media_item)
|
||||||
|
|
||||||
assert {:ok, _} = MediaDownloadWorker.kickoff_with_task(media_item, %{}, job_opts)
|
|
||||||
|
|
||||||
[job] = all_enqueued(worker: MediaDownloadWorker, args: %{"id" => media_item.id})
|
[job] = all_enqueued(worker: MediaDownloadWorker, args: %{"id" => media_item.id})
|
||||||
assert job.max_attempts == 5
|
|
||||||
|
assert job.priority == 5
|
||||||
|
end
|
||||||
|
|
||||||
|
test "priority can be set", %{media_item: media_item} do
|
||||||
|
assert {:ok, _} = MediaDownloadWorker.kickoff_with_task(media_item, %{}, priority: 0)
|
||||||
|
|
||||||
|
[job] = all_enqueued(worker: MediaDownloadWorker, args: %{"id" => media_item.id})
|
||||||
|
|
||||||
|
assert job.priority == 0
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -67,7 +74,7 @@ defmodule Pinchflat.Downloading.MediaDownloadWorkerTest do
|
|||||||
:ok
|
:ok
|
||||||
end
|
end
|
||||||
|
|
||||||
test "it saves attributes to the media_item", %{media_item: media_item} do
|
test "saves attributes to the media_item", %{media_item: media_item} do
|
||||||
assert media_item.media_filepath == nil
|
assert media_item.media_filepath == nil
|
||||||
perform_job(MediaDownloadWorker, %{id: media_item.id})
|
perform_job(MediaDownloadWorker, %{id: media_item.id})
|
||||||
media_item = Repo.reload(media_item)
|
media_item = Repo.reload(media_item)
|
||||||
@@ -75,20 +82,20 @@ defmodule Pinchflat.Downloading.MediaDownloadWorkerTest do
|
|||||||
assert media_item.media_filepath != nil
|
assert media_item.media_filepath != nil
|
||||||
end
|
end
|
||||||
|
|
||||||
test "it saves the metadata to the media_item", %{media_item: media_item} do
|
test "saves the metadata to the media_item", %{media_item: media_item} do
|
||||||
assert media_item.metadata == nil
|
assert media_item.metadata == nil
|
||||||
perform_job(MediaDownloadWorker, %{id: media_item.id})
|
perform_job(MediaDownloadWorker, %{id: media_item.id})
|
||||||
assert Repo.reload(media_item).metadata != nil
|
assert Repo.reload(media_item).metadata != nil
|
||||||
end
|
end
|
||||||
|
|
||||||
test "it won't double-schedule downloading jobs", %{media_item: media_item} do
|
test "won't double-schedule downloading jobs", %{media_item: media_item} do
|
||||||
Oban.insert(MediaDownloadWorker.new(%{id: media_item.id}))
|
Oban.insert(MediaDownloadWorker.new(%{id: media_item.id}))
|
||||||
Oban.insert(MediaDownloadWorker.new(%{id: media_item.id}))
|
Oban.insert(MediaDownloadWorker.new(%{id: media_item.id}))
|
||||||
|
|
||||||
assert [_] = all_enqueued(worker: MediaDownloadWorker)
|
assert [_] = all_enqueued(worker: MediaDownloadWorker)
|
||||||
end
|
end
|
||||||
|
|
||||||
test "it sets the job to retryable if the download fails", %{media_item: media_item} do
|
test "sets the job to retryable if the download fails", %{media_item: media_item} do
|
||||||
expect(YtDlpRunnerMock, :run, 2, fn
|
expect(YtDlpRunnerMock, :run, 2, fn
|
||||||
_url, :get_downloadable_status, _opts, _ot, _addl -> {:ok, "{}"}
|
_url, :get_downloadable_status, _opts, _ot, _addl -> {:ok, "{}"}
|
||||||
_url, :download, _opts, _ot, _addl -> {:error, "error"}
|
_url, :download, _opts, _ot, _addl -> {:error, "error"}
|
||||||
@@ -140,7 +147,23 @@ defmodule Pinchflat.Downloading.MediaDownloadWorkerTest do
|
|||||||
end)
|
end)
|
||||||
end
|
end
|
||||||
|
|
||||||
test "it ensures error are returned in a 2-item tuple", %{media_item: media_item} do
|
test "does not set the job to retryable you aren't a member", %{media_item: media_item} do
|
||||||
|
expect(YtDlpRunnerMock, :run, 2, fn
|
||||||
|
_url, :get_downloadable_status, _opts, _ot, _addl ->
|
||||||
|
{:ok, "{}"}
|
||||||
|
|
||||||
|
_url, :download, _opts, _ot, _addl ->
|
||||||
|
{:error, "This video is available to this channel's members on level: foo", 1}
|
||||||
|
end)
|
||||||
|
|
||||||
|
Oban.Testing.with_testing_mode(:inline, fn ->
|
||||||
|
{:ok, job} = Oban.insert(MediaDownloadWorker.new(%{id: media_item.id, quality_upgrade?: true}))
|
||||||
|
|
||||||
|
assert job.state == "completed"
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
|
test "ensures error are returned in a 2-item tuple", %{media_item: media_item} do
|
||||||
expect(YtDlpRunnerMock, :run, 2, fn
|
expect(YtDlpRunnerMock, :run, 2, fn
|
||||||
_url, :get_downloadable_status, _opts, _ot, _addl -> {:ok, "{}"}
|
_url, :get_downloadable_status, _opts, _ot, _addl -> {:ok, "{}"}
|
||||||
_url, :download, _opts, _ot, _addl -> {:error, "error", 1}
|
_url, :download, _opts, _ot, _addl -> {:error, "error", 1}
|
||||||
@@ -149,7 +172,7 @@ defmodule Pinchflat.Downloading.MediaDownloadWorkerTest do
|
|||||||
assert {:error, :download_failed} = perform_job(MediaDownloadWorker, %{id: media_item.id})
|
assert {:error, :download_failed} = perform_job(MediaDownloadWorker, %{id: media_item.id})
|
||||||
end
|
end
|
||||||
|
|
||||||
test "it does not download if the source is set to not download", %{media_item: media_item} do
|
test "does not download if the source is set to not download", %{media_item: media_item} do
|
||||||
expect(YtDlpRunnerMock, :run, 0, fn _url, :download, _opts, _ot, _addl -> :ok end)
|
expect(YtDlpRunnerMock, :run, 0, fn _url, :download, _opts, _ot, _addl -> :ok end)
|
||||||
|
|
||||||
Sources.update_source(media_item.source, %{download_media: false})
|
Sources.update_source(media_item.source, %{download_media: false})
|
||||||
@@ -165,7 +188,7 @@ defmodule Pinchflat.Downloading.MediaDownloadWorkerTest do
|
|||||||
perform_job(MediaDownloadWorker, %{id: media_item.id})
|
perform_job(MediaDownloadWorker, %{id: media_item.id})
|
||||||
end
|
end
|
||||||
|
|
||||||
test "it saves the file's size to the database", %{media_item: media_item} do
|
test "saves the file's size to the database", %{media_item: media_item} do
|
||||||
expect(YtDlpRunnerMock, :run, 3, fn
|
expect(YtDlpRunnerMock, :run, 3, fn
|
||||||
_url, :get_downloadable_status, _opts, _ot, _addl ->
|
_url, :get_downloadable_status, _opts, _ot, _addl ->
|
||||||
{:ok, "{}"}
|
{:ok, "{}"}
|
||||||
@@ -214,6 +237,14 @@ defmodule Pinchflat.Downloading.MediaDownloadWorkerTest do
|
|||||||
|
|
||||||
perform_job(MediaDownloadWorker, %{id: media_item.id})
|
perform_job(MediaDownloadWorker, %{id: media_item.id})
|
||||||
end
|
end
|
||||||
|
|
||||||
|
test "does not download if the media item isn't pending download", %{media_item: media_item} do
|
||||||
|
expect(YtDlpRunnerMock, :run, 0, fn _url, :download, _opts, _ot, _addl -> :ok end)
|
||||||
|
|
||||||
|
Media.update_media_item(media_item, %{media_filepath: "foo.mp4"})
|
||||||
|
|
||||||
|
perform_job(MediaDownloadWorker, %{id: media_item.id})
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
describe "perform/1 when testing non-downloadable media" do
|
describe "perform/1 when testing non-downloadable media" do
|
||||||
@@ -232,12 +263,30 @@ defmodule Pinchflat.Downloading.MediaDownloadWorkerTest do
|
|||||||
|
|
||||||
describe "perform/1 when testing forced downloads" do
|
describe "perform/1 when testing forced downloads" do
|
||||||
test "ignores 'prevent_download' if forced", %{media_item: media_item} do
|
test "ignores 'prevent_download' if forced", %{media_item: media_item} do
|
||||||
|
expect(YtDlpRunnerMock, :run, 3, fn
|
||||||
|
_url, :get_downloadable_status, _opts, _ot, _addl -> {:ok, "{}"}
|
||||||
|
_url, :download, _opts, _ot, _addl -> {:ok, render_metadata(:media_metadata)}
|
||||||
|
_url, :download_thumbnail, _opts, _ot, _addl -> {:ok, ""}
|
||||||
|
end)
|
||||||
|
|
||||||
Sources.update_source(media_item.source, %{download_media: false})
|
Sources.update_source(media_item.source, %{download_media: false})
|
||||||
Media.update_media_item(media_item, %{prevent_download: true})
|
Media.update_media_item(media_item, %{prevent_download: true})
|
||||||
|
|
||||||
perform_job(MediaDownloadWorker, %{id: media_item.id, force: true})
|
perform_job(MediaDownloadWorker, %{id: media_item.id, force: true})
|
||||||
end
|
end
|
||||||
|
|
||||||
|
test "ignores whether the media item is pending when forced", %{media_item: media_item} do
|
||||||
|
expect(YtDlpRunnerMock, :run, 3, fn
|
||||||
|
_url, :get_downloadable_status, _opts, _ot, _addl -> {:ok, "{}"}
|
||||||
|
_url, :download, _opts, _ot, _addl -> {:ok, render_metadata(:media_metadata)}
|
||||||
|
_url, :download_thumbnail, _opts, _ot, _addl -> {:ok, ""}
|
||||||
|
end)
|
||||||
|
|
||||||
|
Media.update_media_item(media_item, %{media_filepath: "foo.mp4"})
|
||||||
|
|
||||||
|
perform_job(MediaDownloadWorker, %{id: media_item.id, force: true})
|
||||||
|
end
|
||||||
|
|
||||||
test "sets force_overwrites runner option", %{media_item: media_item} do
|
test "sets force_overwrites runner option", %{media_item: media_item} do
|
||||||
expect(YtDlpRunnerMock, :run, 3, fn
|
expect(YtDlpRunnerMock, :run, 3, fn
|
||||||
_url, :get_downloadable_status, _opts, _ot, _addl ->
|
_url, :get_downloadable_status, _opts, _ot, _addl ->
|
||||||
@@ -265,6 +314,34 @@ defmodule Pinchflat.Downloading.MediaDownloadWorkerTest do
|
|||||||
assert media_item.media_redownloaded_at != nil
|
assert media_item.media_redownloaded_at != nil
|
||||||
end
|
end
|
||||||
|
|
||||||
|
test "ignores whether the media item is pending when re-downloaded", %{media_item: media_item} do
|
||||||
|
expect(YtDlpRunnerMock, :run, 3, fn
|
||||||
|
_url, :get_downloadable_status, _opts, _ot, _addl -> {:ok, "{}"}
|
||||||
|
_url, :download, _opts, _ot, _addl -> {:ok, render_metadata(:media_metadata)}
|
||||||
|
_url, :download_thumbnail, _opts, _ot, _addl -> {:ok, ""}
|
||||||
|
end)
|
||||||
|
|
||||||
|
Media.update_media_item(media_item, %{media_filepath: "foo.mp4"})
|
||||||
|
|
||||||
|
perform_job(MediaDownloadWorker, %{id: media_item.id, quality_upgrade?: true})
|
||||||
|
end
|
||||||
|
|
||||||
|
test "doesn't redownload if the source is set to not download", %{media_item: media_item} do
|
||||||
|
expect(YtDlpRunnerMock, :run, 0, fn _url, :download, _opts, _ot, _addl -> :ok end)
|
||||||
|
|
||||||
|
Sources.update_source(media_item.source, %{download_media: false})
|
||||||
|
|
||||||
|
perform_job(MediaDownloadWorker, %{id: media_item.id, quality_upgrade?: true})
|
||||||
|
end
|
||||||
|
|
||||||
|
test "doesn't redownload if the media item is set to not download", %{media_item: media_item} do
|
||||||
|
expect(YtDlpRunnerMock, :run, 0, fn _url, :download, _opts, _ot, _addl -> :ok end)
|
||||||
|
|
||||||
|
Media.update_media_item(media_item, %{prevent_download: true})
|
||||||
|
|
||||||
|
perform_job(MediaDownloadWorker, %{id: media_item.id, quality_upgrade?: true})
|
||||||
|
end
|
||||||
|
|
||||||
test "sets force_overwrites runner option", %{media_item: media_item} do
|
test "sets force_overwrites runner option", %{media_item: media_item} do
|
||||||
expect(YtDlpRunnerMock, :run, 3, fn
|
expect(YtDlpRunnerMock, :run, 3, fn
|
||||||
_url, :get_downloadable_status, _opts, _ot, _addl ->
|
_url, :get_downloadable_status, _opts, _ot, _addl ->
|
||||||
|
|||||||
@@ -60,7 +60,8 @@ defmodule Pinchflat.Downloading.MediaDownloaderTest do
|
|||||||
{:ok, Phoenix.json_library().encode!(%{"live_status" => "is_live"})}
|
{:ok, Phoenix.json_library().encode!(%{"live_status" => "is_live"})}
|
||||||
end)
|
end)
|
||||||
|
|
||||||
assert {:error, :unsuitable_for_download} = MediaDownloader.download_for_media_item(media_item)
|
assert {:error, :unsuitable_for_download, message} = MediaDownloader.download_for_media_item(media_item)
|
||||||
|
assert message =~ "Media item ##{media_item.id} isn't suitable for download yet."
|
||||||
end
|
end
|
||||||
|
|
||||||
test "non-recoverable errors are passed through", %{media_item: media_item} do
|
test "non-recoverable errors are passed through", %{media_item: media_item} do
|
||||||
@@ -69,7 +70,7 @@ defmodule Pinchflat.Downloading.MediaDownloaderTest do
|
|||||||
_url, :download, _opts, _ot, _addl -> {:error, :some_error, 1}
|
_url, :download, _opts, _ot, _addl -> {:error, :some_error, 1}
|
||||||
end)
|
end)
|
||||||
|
|
||||||
assert {:error, :some_error} = MediaDownloader.download_for_media_item(media_item)
|
assert {:error, :download_failed, :some_error} = MediaDownloader.download_for_media_item(media_item)
|
||||||
end
|
end
|
||||||
|
|
||||||
test "unknown errors are passed through", %{media_item: media_item} do
|
test "unknown errors are passed through", %{media_item: media_item} do
|
||||||
@@ -78,7 +79,7 @@ defmodule Pinchflat.Downloading.MediaDownloaderTest do
|
|||||||
_url, :download, _opts, _ot, _addl -> {:error, :some_error}
|
_url, :download, _opts, _ot, _addl -> {:error, :some_error}
|
||||||
end)
|
end)
|
||||||
|
|
||||||
assert {:error, message} = MediaDownloader.download_for_media_item(media_item)
|
assert {:error, :unknown, message} = MediaDownloader.download_for_media_item(media_item)
|
||||||
assert message == "Unknown error: {:error, :some_error}"
|
assert message == "Unknown error: {:error, :some_error}"
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -107,13 +108,15 @@ defmodule Pinchflat.Downloading.MediaDownloaderTest do
|
|||||||
|
|
||||||
expect(YtDlpRunnerMock, :run, 0, fn _url, :download, _opts, _ot, _addl -> {:ok, ""} end)
|
expect(YtDlpRunnerMock, :run, 0, fn _url, :download, _opts, _ot, _addl -> {:ok, ""} end)
|
||||||
|
|
||||||
assert {:error, :unsuitable_for_download} = MediaDownloader.download_for_media_item(media_item)
|
assert {:error, :unsuitable_for_download, message} = MediaDownloader.download_for_media_item(media_item)
|
||||||
|
assert message =~ "Media item ##{media_item.id} isn't suitable for download yet."
|
||||||
end
|
end
|
||||||
|
|
||||||
test "returns unexpected errors from the download status determination method", %{media_item: media_item} do
|
test "returns unexpected errors from the download status determination method", %{media_item: media_item} do
|
||||||
expect(YtDlpRunnerMock, :run, fn _url, :get_downloadable_status, _opts, _ot, _addl -> {:error, :what_tha} end)
|
expect(YtDlpRunnerMock, :run, fn _url, :get_downloadable_status, _opts, _ot, _addl -> {:error, :what_tha} end)
|
||||||
|
|
||||||
assert {:error, "Unknown error: {:error, :what_tha}"} = MediaDownloader.download_for_media_item(media_item)
|
assert {:error, :unknown, "Unknown error: {:error, :what_tha}"} =
|
||||||
|
MediaDownloader.download_for_media_item(media_item)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -154,15 +157,16 @@ defmodule Pinchflat.Downloading.MediaDownloaderTest do
|
|||||||
{:ok, ""}
|
{:ok, ""}
|
||||||
end)
|
end)
|
||||||
|
|
||||||
source = source_fixture(%{use_cookies: true})
|
source = source_fixture(%{cookie_behaviour: :all_operations})
|
||||||
media_item = media_item_fixture(%{source_id: source.id})
|
media_item = media_item_fixture(%{source_id: source.id})
|
||||||
|
|
||||||
assert {:ok, _} = MediaDownloader.download_for_media_item(media_item)
|
assert {:ok, _} = MediaDownloader.download_for_media_item(media_item)
|
||||||
end
|
end
|
||||||
|
|
||||||
test "does not set use_cookies if the source does not use cookies" do
|
test "does not set use_cookies if the source uses cookies when needed" do
|
||||||
expect(YtDlpRunnerMock, :run, 3, fn
|
expect(YtDlpRunnerMock, :run, 3, fn
|
||||||
_url, :get_downloadable_status, _opts, _ot, _addl ->
|
_url, :get_downloadable_status, _opts, _ot, addl ->
|
||||||
|
assert {:use_cookies, false} in addl
|
||||||
{:ok, "{}"}
|
{:ok, "{}"}
|
||||||
|
|
||||||
_url, :download, _opts, _ot, addl ->
|
_url, :download, _opts, _ot, addl ->
|
||||||
@@ -173,26 +177,52 @@ defmodule Pinchflat.Downloading.MediaDownloaderTest do
|
|||||||
{:ok, ""}
|
{:ok, ""}
|
||||||
end)
|
end)
|
||||||
|
|
||||||
source = source_fixture(%{use_cookies: false})
|
source = source_fixture(%{cookie_behaviour: :when_needed})
|
||||||
|
media_item = media_item_fixture(%{source_id: source.id})
|
||||||
|
|
||||||
|
assert {:ok, _} = MediaDownloader.download_for_media_item(media_item)
|
||||||
|
end
|
||||||
|
|
||||||
|
test "does not set use_cookies if the source does not use cookies" do
|
||||||
|
expect(YtDlpRunnerMock, :run, 3, fn
|
||||||
|
_url, :get_downloadable_status, _opts, _ot, addl ->
|
||||||
|
assert {:use_cookies, false} in addl
|
||||||
|
{:ok, "{}"}
|
||||||
|
|
||||||
|
_url, :download, _opts, _ot, addl ->
|
||||||
|
assert {:use_cookies, false} in addl
|
||||||
|
{:ok, render_metadata(:media_metadata)}
|
||||||
|
|
||||||
|
_url, :download_thumbnail, _opts, _ot, _addl ->
|
||||||
|
{:ok, ""}
|
||||||
|
end)
|
||||||
|
|
||||||
|
source = source_fixture(%{cookie_behaviour: :disabled})
|
||||||
media_item = media_item_fixture(%{source_id: source.id})
|
media_item = media_item_fixture(%{source_id: source.id})
|
||||||
|
|
||||||
assert {:ok, _} = MediaDownloader.download_for_media_item(media_item)
|
assert {:ok, _} = MediaDownloader.download_for_media_item(media_item)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
describe "download_for_media_item/3 when testing retries" do
|
describe "download_for_media_item/3 when testing non-cookie retries" do
|
||||||
test "returns a recovered tuple on recoverable errors", %{media_item: media_item} do
|
test "returns a recovered tuple on recoverable errors", %{media_item: media_item} do
|
||||||
message = "Unable to communicate with SponsorBlock"
|
message = "Unable to communicate with SponsorBlock"
|
||||||
|
|
||||||
expect(YtDlpRunnerMock, :run, 2, fn
|
expect(YtDlpRunnerMock, :run, 3, fn
|
||||||
_url, :get_downloadable_status, _opts, _ot, _addl ->
|
_url, :get_downloadable_status, _opts, _ot, _addl ->
|
||||||
{:ok, "{}"}
|
{:ok, "{}"}
|
||||||
|
|
||||||
_url, :download, _opts, _ot, _addl ->
|
_url, :download, _opts, _ot, addl ->
|
||||||
|
[{:output_filepath, filepath} | _] = addl
|
||||||
|
File.write(filepath, render_metadata(:media_metadata))
|
||||||
|
|
||||||
{:error, message, 1}
|
{:error, message, 1}
|
||||||
|
|
||||||
|
_url, :download_thumbnail, _opts, _ot, _addl ->
|
||||||
|
{:ok, ""}
|
||||||
end)
|
end)
|
||||||
|
|
||||||
assert {:recovered, ^message} = MediaDownloader.download_for_media_item(media_item)
|
assert {:recovered, _media_item, ^message} = MediaDownloader.download_for_media_item(media_item)
|
||||||
end
|
end
|
||||||
|
|
||||||
test "attempts to update the media item on recoverable errors", %{media_item: media_item} do
|
test "attempts to update the media item on recoverable errors", %{media_item: media_item} do
|
||||||
@@ -212,11 +242,121 @@ defmodule Pinchflat.Downloading.MediaDownloaderTest do
|
|||||||
{:ok, ""}
|
{:ok, ""}
|
||||||
end)
|
end)
|
||||||
|
|
||||||
assert {:recovered, ^message} = MediaDownloader.download_for_media_item(media_item)
|
assert {:recovered, updated_media_item, ^message} = MediaDownloader.download_for_media_item(media_item)
|
||||||
|
|
||||||
|
assert DateTime.diff(DateTime.utc_now(), updated_media_item.media_downloaded_at) < 2
|
||||||
|
assert String.ends_with?(updated_media_item.media_filepath, ".mkv")
|
||||||
|
end
|
||||||
|
|
||||||
|
test "returns an unrecoverable tuple if recovery fails", %{media_item: media_item} do
|
||||||
|
message = "Unable to communicate with SponsorBlock"
|
||||||
|
|
||||||
|
expect(YtDlpRunnerMock, :run, 2, fn
|
||||||
|
_url, :get_downloadable_status, _opts, _ot, _addl ->
|
||||||
|
{:ok, "{}"}
|
||||||
|
|
||||||
|
_url, :download, _opts, _ot, _addl ->
|
||||||
|
# This errors because the metadata is not written to the file so JSON parsing fails
|
||||||
|
{:error, message, 1}
|
||||||
|
end)
|
||||||
|
|
||||||
|
assert {:error, :unrecoverable, ^message} = MediaDownloader.download_for_media_item(media_item)
|
||||||
|
end
|
||||||
|
|
||||||
|
test "sets the last_error appropriately when recovered", %{media_item: media_item} do
|
||||||
|
expect(YtDlpRunnerMock, :run, 3, fn
|
||||||
|
_url, :download, _opts, _ot, addl ->
|
||||||
|
[{:output_filepath, filepath} | _] = addl
|
||||||
|
File.write(filepath, render_metadata(:media_metadata))
|
||||||
|
|
||||||
|
{:error, "Unable to communicate with SponsorBlock", 1}
|
||||||
|
|
||||||
|
_url, :get_downloadable_status, _opts, _ot, _addl ->
|
||||||
|
{:ok, "{}"}
|
||||||
|
|
||||||
|
_url, :download_thumbnail, _opts, _ot, _addl ->
|
||||||
|
{:ok, ""}
|
||||||
|
end)
|
||||||
|
|
||||||
|
assert {:recovered, updated_media_item, _message} = MediaDownloader.download_for_media_item(media_item)
|
||||||
|
assert updated_media_item.last_error == "Unable to communicate with SponsorBlock"
|
||||||
|
end
|
||||||
|
|
||||||
|
test "sets the last_error appropriately when unrecoverable", %{media_item: media_item} do
|
||||||
|
expect(YtDlpRunnerMock, :run, 2, fn
|
||||||
|
_url, :get_downloadable_status, _opts, _ot, _addl ->
|
||||||
|
{:ok, "{}"}
|
||||||
|
|
||||||
|
_url, :download, _opts, _ot, _addl ->
|
||||||
|
{:error, "Unable to communicate with SponsorBlock", 1}
|
||||||
|
end)
|
||||||
|
|
||||||
|
assert {:error, :unrecoverable, _message} = MediaDownloader.download_for_media_item(media_item)
|
||||||
media_item = Repo.reload(media_item)
|
media_item = Repo.reload(media_item)
|
||||||
|
|
||||||
assert DateTime.diff(DateTime.utc_now(), media_item.media_downloaded_at) < 2
|
assert media_item.last_error == "Unable to communicate with SponsorBlock"
|
||||||
assert String.ends_with?(media_item.media_filepath, ".mkv")
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "download_for_media_item/3 when testing cookie retries" do
|
||||||
|
test "retries with cookies if we think it would help and the source allows" do
|
||||||
|
expect(YtDlpRunnerMock, :run, 4, fn
|
||||||
|
_url, :get_downloadable_status, _opts, _ot, [use_cookies: false] ->
|
||||||
|
{:error, "Sign in to confirm your age", 1}
|
||||||
|
|
||||||
|
_url, :get_downloadable_status, _opts, _ot, [use_cookies: true] ->
|
||||||
|
{:ok, "{}"}
|
||||||
|
|
||||||
|
_url, :download, _opts, _ot, addl ->
|
||||||
|
assert {:use_cookies, true} in addl
|
||||||
|
{:ok, render_metadata(:media_metadata)}
|
||||||
|
|
||||||
|
_url, :download_thumbnail, _opts, _ot, _addl ->
|
||||||
|
{:ok, ""}
|
||||||
|
end)
|
||||||
|
|
||||||
|
source = source_fixture(%{cookie_behaviour: :when_needed})
|
||||||
|
media_item = media_item_fixture(%{source_id: source.id})
|
||||||
|
|
||||||
|
assert {:ok, _} = MediaDownloader.download_for_media_item(media_item)
|
||||||
|
end
|
||||||
|
|
||||||
|
test "does not retry with cookies if we don't think it would help even the source allows" do
|
||||||
|
expect(YtDlpRunnerMock, :run, 1, fn
|
||||||
|
_url, :get_downloadable_status, _opts, _ot, [use_cookies: false] ->
|
||||||
|
{:error, "Some other error", 1}
|
||||||
|
end)
|
||||||
|
|
||||||
|
source = source_fixture(%{cookie_behaviour: :when_needed})
|
||||||
|
media_item = media_item_fixture(%{source_id: source.id})
|
||||||
|
|
||||||
|
assert {:error, :download_failed, "Some other error"} = MediaDownloader.download_for_media_item(media_item)
|
||||||
|
end
|
||||||
|
|
||||||
|
test "does not retry with cookies even if we think it would help but source doesn't allow" do
|
||||||
|
expect(YtDlpRunnerMock, :run, 1, fn
|
||||||
|
_url, :get_downloadable_status, _opts, _ot, [use_cookies: false] ->
|
||||||
|
{:error, "Sign in to confirm your age", 1}
|
||||||
|
end)
|
||||||
|
|
||||||
|
source = source_fixture(%{cookie_behaviour: :disabled})
|
||||||
|
media_item = media_item_fixture(%{source_id: source.id})
|
||||||
|
|
||||||
|
assert {:error, :download_failed, "Sign in to confirm your age"} =
|
||||||
|
MediaDownloader.download_for_media_item(media_item)
|
||||||
|
end
|
||||||
|
|
||||||
|
test "does not retry with cookies if cookies were already used" do
|
||||||
|
expect(YtDlpRunnerMock, :run, 1, fn
|
||||||
|
_url, :get_downloadable_status, _opts, _ot, [use_cookies: true] ->
|
||||||
|
{:error, "This video is available to this channel's members", 1}
|
||||||
|
end)
|
||||||
|
|
||||||
|
source = source_fixture(%{cookie_behaviour: :all_operations})
|
||||||
|
media_item = media_item_fixture(%{source_id: source.id})
|
||||||
|
|
||||||
|
assert {:error, :download_failed, "This video is available to this channel's members"} =
|
||||||
|
MediaDownloader.download_for_media_item(media_item)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -324,6 +464,25 @@ defmodule Pinchflat.Downloading.MediaDownloaderTest do
|
|||||||
|
|
||||||
File.rm(updated_media_item.metadata_filepath)
|
File.rm(updated_media_item.metadata_filepath)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
test "sets the last_error to nil on success" do
|
||||||
|
media_item = media_item_fixture(%{last_error: "Some error"})
|
||||||
|
|
||||||
|
assert {:ok, updated_media_item} = MediaDownloader.download_for_media_item(media_item)
|
||||||
|
assert updated_media_item.last_error == nil
|
||||||
|
end
|
||||||
|
|
||||||
|
test "sets the last_error to the error message on failure", %{media_item: media_item} do
|
||||||
|
expect(YtDlpRunnerMock, :run, 2, fn
|
||||||
|
_url, :get_downloadable_status, _opts, _ot, _addl -> {:ok, "{}"}
|
||||||
|
_url, :download, _opts, _ot, _addl -> {:error, :some_error}
|
||||||
|
end)
|
||||||
|
|
||||||
|
assert {:error, :unknown, _message} = MediaDownloader.download_for_media_item(media_item)
|
||||||
|
media_item = Repo.reload(media_item)
|
||||||
|
|
||||||
|
assert media_item.last_error == "Unknown error: {:error, :some_error}"
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
describe "download_for_media_item/3 when testing NFO generation" do
|
describe "download_for_media_item/3 when testing NFO generation" do
|
||||||
|
|||||||
@@ -38,36 +38,48 @@ defmodule Pinchflat.FastIndexing.FastIndexingHelpersTest do
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
describe "kickoff_download_tasks_from_youtube_rss_feed/1" do
|
describe "index_and_kickoff_downloads/1" do
|
||||||
test "enqueues a new worker for each new media_id in the source's RSS feed", %{source: source} do
|
test "enqueues a worker for each new media_id in the source's RSS feed", %{source: source} do
|
||||||
expect(HTTPClientMock, :get, fn _url -> {:ok, "<yt:videoId>test_1</yt:videoId>"} end)
|
expect(HTTPClientMock, :get, fn _url -> {:ok, "<yt:videoId>test_1</yt:videoId>"} end)
|
||||||
|
|
||||||
assert [media_item] = FastIndexingHelpers.kickoff_download_tasks_from_youtube_rss_feed(source)
|
assert [media_item] = FastIndexingHelpers.index_and_kickoff_downloads(source)
|
||||||
|
|
||||||
assert [worker] = all_enqueued(worker: MediaDownloadWorker)
|
assert [worker] = all_enqueued(worker: MediaDownloadWorker)
|
||||||
assert worker.args["id"] == media_item.id
|
assert worker.args["id"] == media_item.id
|
||||||
|
assert worker.priority == 0
|
||||||
end
|
end
|
||||||
|
|
||||||
test "does not enqueue a new worker for the source's media IDs we already know about", %{source: source} do
|
test "does not enqueue a new worker for the source's media IDs we already know about", %{source: source} do
|
||||||
expect(HTTPClientMock, :get, fn _url -> {:ok, "<yt:videoId>test_1</yt:videoId>"} end)
|
expect(HTTPClientMock, :get, fn _url -> {:ok, "<yt:videoId>test_1</yt:videoId>"} end)
|
||||||
media_item_fixture(source_id: source.id, media_id: "test_1")
|
media_item_fixture(source_id: source.id, media_id: "test_1")
|
||||||
|
|
||||||
assert [] = FastIndexingHelpers.kickoff_download_tasks_from_youtube_rss_feed(source)
|
assert [] = FastIndexingHelpers.index_and_kickoff_downloads(source)
|
||||||
|
|
||||||
refute_enqueued(worker: MediaDownloadWorker)
|
refute_enqueued(worker: MediaDownloadWorker)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
test "kicks off a download task for all pending media but at a lower priority", %{source: source} do
|
||||||
|
pending_item = media_item_fixture(source_id: source.id, media_filepath: nil)
|
||||||
|
expect(HTTPClientMock, :get, fn _url -> {:ok, "<yt:videoId>test_1</yt:videoId>"} end)
|
||||||
|
|
||||||
|
assert [%MediaItem{}] = FastIndexingHelpers.index_and_kickoff_downloads(source)
|
||||||
|
|
||||||
|
assert [worker_1, _worker_2] = all_enqueued(worker: MediaDownloadWorker)
|
||||||
|
assert worker_1.args["id"] == pending_item.id
|
||||||
|
assert worker_1.priority == 1
|
||||||
|
end
|
||||||
|
|
||||||
test "returns the found media items", %{source: source} do
|
test "returns the found media items", %{source: source} do
|
||||||
expect(HTTPClientMock, :get, fn _url -> {:ok, "<yt:videoId>test_1</yt:videoId>"} end)
|
expect(HTTPClientMock, :get, fn _url -> {:ok, "<yt:videoId>test_1</yt:videoId>"} end)
|
||||||
|
|
||||||
assert [%MediaItem{}] = FastIndexingHelpers.kickoff_download_tasks_from_youtube_rss_feed(source)
|
assert [%MediaItem{}] = FastIndexingHelpers.index_and_kickoff_downloads(source)
|
||||||
end
|
end
|
||||||
|
|
||||||
test "does not enqueue a download job if the source does not allow it" do
|
test "does not enqueue a download job if the source does not allow it" do
|
||||||
expect(HTTPClientMock, :get, fn _url -> {:ok, "<yt:videoId>test_1</yt:videoId>"} end)
|
expect(HTTPClientMock, :get, fn _url -> {:ok, "<yt:videoId>test_1</yt:videoId>"} end)
|
||||||
source = source_fixture(%{download_media: false})
|
source = source_fixture(%{download_media: false})
|
||||||
|
|
||||||
assert [%MediaItem{}] = FastIndexingHelpers.kickoff_download_tasks_from_youtube_rss_feed(source)
|
assert [%MediaItem{}] = FastIndexingHelpers.index_and_kickoff_downloads(source)
|
||||||
|
|
||||||
refute_enqueued(worker: MediaDownloadWorker)
|
refute_enqueued(worker: MediaDownloadWorker)
|
||||||
end
|
end
|
||||||
@@ -75,7 +87,7 @@ defmodule Pinchflat.FastIndexing.FastIndexingHelpersTest do
|
|||||||
test "creates a download task record", %{source: source} do
|
test "creates a download task record", %{source: source} do
|
||||||
expect(HTTPClientMock, :get, fn _url -> {:ok, "<yt:videoId>test_1</yt:videoId>"} end)
|
expect(HTTPClientMock, :get, fn _url -> {:ok, "<yt:videoId>test_1</yt:videoId>"} end)
|
||||||
|
|
||||||
assert [media_item] = FastIndexingHelpers.kickoff_download_tasks_from_youtube_rss_feed(source)
|
assert [media_item] = FastIndexingHelpers.index_and_kickoff_downloads(source)
|
||||||
|
|
||||||
assert [_] = Tasks.list_tasks_for(media_item, "MediaDownloadWorker")
|
assert [_] = Tasks.list_tasks_for(media_item, "MediaDownloadWorker")
|
||||||
end
|
end
|
||||||
@@ -89,35 +101,7 @@ defmodule Pinchflat.FastIndexing.FastIndexingHelpersTest do
|
|||||||
{:ok, media_attributes_return_fixture()}
|
{:ok, media_attributes_return_fixture()}
|
||||||
end)
|
end)
|
||||||
|
|
||||||
FastIndexingHelpers.kickoff_download_tasks_from_youtube_rss_feed(source)
|
FastIndexingHelpers.index_and_kickoff_downloads(source)
|
||||||
end
|
|
||||||
|
|
||||||
test "sets use_cookies if the source uses cookies" do
|
|
||||||
expect(HTTPClientMock, :get, fn _url -> {:ok, "<yt:videoId>test_1</yt:videoId>"} end)
|
|
||||||
|
|
||||||
stub(YtDlpRunnerMock, :run, fn _url, :get_media_attributes, _opts, _ot, addl ->
|
|
||||||
assert {:use_cookies, true} in addl
|
|
||||||
|
|
||||||
{:ok, media_attributes_return_fixture()}
|
|
||||||
end)
|
|
||||||
|
|
||||||
source = source_fixture(%{use_cookies: true})
|
|
||||||
|
|
||||||
assert [%MediaItem{}] = FastIndexingHelpers.kickoff_download_tasks_from_youtube_rss_feed(source)
|
|
||||||
end
|
|
||||||
|
|
||||||
test "does not set use_cookies if the source does not use cookies" do
|
|
||||||
expect(HTTPClientMock, :get, fn _url -> {:ok, "<yt:videoId>test_1</yt:videoId>"} end)
|
|
||||||
|
|
||||||
stub(YtDlpRunnerMock, :run, fn _url, :get_media_attributes, _opts, _ot, addl ->
|
|
||||||
assert {:use_cookies, false} in addl
|
|
||||||
|
|
||||||
{:ok, media_attributes_return_fixture()}
|
|
||||||
end)
|
|
||||||
|
|
||||||
source = source_fixture(%{use_cookies: false})
|
|
||||||
|
|
||||||
assert [%MediaItem{}] = FastIndexingHelpers.kickoff_download_tasks_from_youtube_rss_feed(source)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
test "does not enqueue a download job if the media item does not match the format rules" do
|
test "does not enqueue a download job if the media item does not match the format rules" do
|
||||||
@@ -142,7 +126,7 @@ defmodule Pinchflat.FastIndexing.FastIndexingHelpersTest do
|
|||||||
{:ok, output}
|
{:ok, output}
|
||||||
end)
|
end)
|
||||||
|
|
||||||
assert [%MediaItem{}] = FastIndexingHelpers.kickoff_download_tasks_from_youtube_rss_feed(source)
|
assert [%MediaItem{}] = FastIndexingHelpers.index_and_kickoff_downloads(source)
|
||||||
|
|
||||||
refute_enqueued(worker: MediaDownloadWorker)
|
refute_enqueued(worker: MediaDownloadWorker)
|
||||||
end
|
end
|
||||||
@@ -154,7 +138,7 @@ defmodule Pinchflat.FastIndexing.FastIndexingHelpersTest do
|
|||||||
{:ok, "{}"}
|
{:ok, "{}"}
|
||||||
end)
|
end)
|
||||||
|
|
||||||
assert [] = FastIndexingHelpers.kickoff_download_tasks_from_youtube_rss_feed(source)
|
assert [] = FastIndexingHelpers.index_and_kickoff_downloads(source)
|
||||||
end
|
end
|
||||||
|
|
||||||
test "does not blow up if a media item causes a yt-dlp error", %{source: source} do
|
test "does not blow up if a media item causes a yt-dlp error", %{source: source} do
|
||||||
@@ -164,11 +148,55 @@ defmodule Pinchflat.FastIndexing.FastIndexingHelpersTest do
|
|||||||
{:error, "message", 1}
|
{:error, "message", 1}
|
||||||
end)
|
end)
|
||||||
|
|
||||||
assert [] = FastIndexingHelpers.kickoff_download_tasks_from_youtube_rss_feed(source)
|
assert [] = FastIndexingHelpers.index_and_kickoff_downloads(source)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
describe "kickoff_download_tasks_from_youtube_rss_feed/1 when testing backends" do
|
describe "index_and_kickoff_downloads/1 when testing cookies" do
|
||||||
|
test "sets use_cookies if the source uses cookies" do
|
||||||
|
expect(HTTPClientMock, :get, fn _url -> {:ok, "<yt:videoId>test_1</yt:videoId>"} end)
|
||||||
|
|
||||||
|
stub(YtDlpRunnerMock, :run, fn _url, :get_media_attributes, _opts, _ot, addl ->
|
||||||
|
assert {:use_cookies, true} in addl
|
||||||
|
|
||||||
|
{:ok, media_attributes_return_fixture()}
|
||||||
|
end)
|
||||||
|
|
||||||
|
source = source_fixture(%{cookie_behaviour: :all_operations})
|
||||||
|
|
||||||
|
assert [%MediaItem{}] = FastIndexingHelpers.index_and_kickoff_downloads(source)
|
||||||
|
end
|
||||||
|
|
||||||
|
test "does not set use_cookies if the source uses cookies when needed" do
|
||||||
|
expect(HTTPClientMock, :get, fn _url -> {:ok, "<yt:videoId>test_1</yt:videoId>"} end)
|
||||||
|
|
||||||
|
stub(YtDlpRunnerMock, :run, fn _url, :get_media_attributes, _opts, _ot, addl ->
|
||||||
|
assert {:use_cookies, false} in addl
|
||||||
|
|
||||||
|
{:ok, media_attributes_return_fixture()}
|
||||||
|
end)
|
||||||
|
|
||||||
|
source = source_fixture(%{cookie_behaviour: :when_needed})
|
||||||
|
|
||||||
|
assert [%MediaItem{}] = FastIndexingHelpers.index_and_kickoff_downloads(source)
|
||||||
|
end
|
||||||
|
|
||||||
|
test "does not set use_cookies if the source does not use cookies" do
|
||||||
|
expect(HTTPClientMock, :get, fn _url -> {:ok, "<yt:videoId>test_1</yt:videoId>"} end)
|
||||||
|
|
||||||
|
stub(YtDlpRunnerMock, :run, fn _url, :get_media_attributes, _opts, _ot, addl ->
|
||||||
|
assert {:use_cookies, false} in addl
|
||||||
|
|
||||||
|
{:ok, media_attributes_return_fixture()}
|
||||||
|
end)
|
||||||
|
|
||||||
|
source = source_fixture(%{cookie_behaviour: :disabled})
|
||||||
|
|
||||||
|
assert [%MediaItem{}] = FastIndexingHelpers.index_and_kickoff_downloads(source)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "index_and_kickoff_downloads/1 when testing backends" do
|
||||||
test "uses the YouTube API if it is enabled", %{source: source} do
|
test "uses the YouTube API if it is enabled", %{source: source} do
|
||||||
expect(HTTPClientMock, :get, fn url, _headers ->
|
expect(HTTPClientMock, :get, fn url, _headers ->
|
||||||
assert url =~ "https://youtube.googleapis.com/youtube/v3/playlistItems"
|
assert url =~ "https://youtube.googleapis.com/youtube/v3/playlistItems"
|
||||||
@@ -178,7 +206,7 @@ defmodule Pinchflat.FastIndexing.FastIndexingHelpersTest do
|
|||||||
|
|
||||||
Settings.set(youtube_api_key: "test_key")
|
Settings.set(youtube_api_key: "test_key")
|
||||||
|
|
||||||
assert [] = FastIndexingHelpers.kickoff_download_tasks_from_youtube_rss_feed(source)
|
assert [] = FastIndexingHelpers.index_and_kickoff_downloads(source)
|
||||||
end
|
end
|
||||||
|
|
||||||
test "the YouTube API creates records as expected", %{source: source} do
|
test "the YouTube API creates records as expected", %{source: source} do
|
||||||
@@ -188,7 +216,7 @@ defmodule Pinchflat.FastIndexing.FastIndexingHelpersTest do
|
|||||||
|
|
||||||
Settings.set(youtube_api_key: "test_key")
|
Settings.set(youtube_api_key: "test_key")
|
||||||
|
|
||||||
assert [%MediaItem{}] = FastIndexingHelpers.kickoff_download_tasks_from_youtube_rss_feed(source)
|
assert [%MediaItem{}] = FastIndexingHelpers.index_and_kickoff_downloads(source)
|
||||||
end
|
end
|
||||||
|
|
||||||
test "RSS is used as a backup if the API fails", %{source: source} do
|
test "RSS is used as a backup if the API fails", %{source: source} do
|
||||||
@@ -197,7 +225,7 @@ defmodule Pinchflat.FastIndexing.FastIndexingHelpersTest do
|
|||||||
|
|
||||||
Settings.set(youtube_api_key: "test_key")
|
Settings.set(youtube_api_key: "test_key")
|
||||||
|
|
||||||
assert [%MediaItem{}] = FastIndexingHelpers.kickoff_download_tasks_from_youtube_rss_feed(source)
|
assert [%MediaItem{}] = FastIndexingHelpers.index_and_kickoff_downloads(source)
|
||||||
end
|
end
|
||||||
|
|
||||||
test "RSS is used if the API is not enabled", %{source: source} do
|
test "RSS is used if the API is not enabled", %{source: source} do
|
||||||
@@ -209,7 +237,7 @@ defmodule Pinchflat.FastIndexing.FastIndexingHelpersTest do
|
|||||||
|
|
||||||
Settings.set(youtube_api_key: nil)
|
Settings.set(youtube_api_key: nil)
|
||||||
|
|
||||||
assert [%MediaItem{}] = FastIndexingHelpers.kickoff_download_tasks_from_youtube_rss_feed(source)
|
assert [%MediaItem{}] = FastIndexingHelpers.index_and_kickoff_downloads(source)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -7,31 +7,67 @@ defmodule Pinchflat.FastIndexing.YoutubeApiTest do
|
|||||||
alias Pinchflat.FastIndexing.YoutubeApi
|
alias Pinchflat.FastIndexing.YoutubeApi
|
||||||
|
|
||||||
describe "enabled?/0" do
|
describe "enabled?/0" do
|
||||||
test "returns true if the user has set a YouTube API key" do
|
test "returns true if the user has set YouTube API keys" do
|
||||||
|
Settings.set(youtube_api_key: "key1, key2")
|
||||||
|
assert YoutubeApi.enabled?()
|
||||||
|
end
|
||||||
|
|
||||||
|
test "returns true with a single API key" do
|
||||||
Settings.set(youtube_api_key: "test_key")
|
Settings.set(youtube_api_key: "test_key")
|
||||||
|
|
||||||
assert YoutubeApi.enabled?()
|
assert YoutubeApi.enabled?()
|
||||||
end
|
end
|
||||||
|
|
||||||
test "returns false if the user has not set an API key" do
|
test "returns false if the user has not set any API keys" do
|
||||||
Settings.set(youtube_api_key: nil)
|
Settings.set(youtube_api_key: nil)
|
||||||
|
refute YoutubeApi.enabled?()
|
||||||
|
end
|
||||||
|
|
||||||
|
test "returns false if only empty or whitespace keys are provided" do
|
||||||
|
Settings.set(youtube_api_key: " , ,")
|
||||||
refute YoutubeApi.enabled?()
|
refute YoutubeApi.enabled?()
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
describe "get_recent_media_ids/1" do
|
describe "get_recent_media_ids/1" do
|
||||||
setup do
|
setup do
|
||||||
|
case :global.whereis_name(YoutubeApi.KeyIndex) do
|
||||||
|
:undefined -> :ok
|
||||||
|
pid -> Agent.stop(pid)
|
||||||
|
end
|
||||||
|
|
||||||
source = source_fixture()
|
source = source_fixture()
|
||||||
Settings.set(youtube_api_key: "test_key")
|
Settings.set(youtube_api_key: "key1, key2")
|
||||||
|
|
||||||
{:ok, source: source}
|
{:ok, source: source}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
test "rotates through API keys", %{source: source} do
|
||||||
|
expect(HTTPClientMock, :get, fn url, _headers ->
|
||||||
|
assert url =~ "key=key1"
|
||||||
|
{:ok, "{}"}
|
||||||
|
end)
|
||||||
|
|
||||||
|
expect(HTTPClientMock, :get, fn url, _headers ->
|
||||||
|
assert url =~ "key=key2"
|
||||||
|
{:ok, "{}"}
|
||||||
|
end)
|
||||||
|
|
||||||
|
expect(HTTPClientMock, :get, fn url, _headers ->
|
||||||
|
assert url =~ "key=key1"
|
||||||
|
{:ok, "{}"}
|
||||||
|
end)
|
||||||
|
|
||||||
|
# three calls to verify rotation
|
||||||
|
YoutubeApi.get_recent_media_ids(source)
|
||||||
|
YoutubeApi.get_recent_media_ids(source)
|
||||||
|
YoutubeApi.get_recent_media_ids(source)
|
||||||
|
end
|
||||||
|
|
||||||
test "calls the expected URL", %{source: source} do
|
test "calls the expected URL", %{source: source} do
|
||||||
expect(HTTPClientMock, :get, fn url, headers ->
|
expect(HTTPClientMock, :get, fn url, headers ->
|
||||||
api_base = "https://youtube.googleapis.com/youtube/v3/playlistItems"
|
api_base = "https://youtube.googleapis.com/youtube/v3/playlistItems"
|
||||||
request_url = "#{api_base}?part=contentDetails&maxResults=50&playlistId=#{source.collection_id}&key=test_key"
|
request_url = "#{api_base}?part=contentDetails&maxResults=50&playlistId=#{source.collection_id}&key=key1"
|
||||||
|
|
||||||
assert url == request_url
|
assert url == request_url
|
||||||
assert headers == [accept: "application/json"]
|
assert headers == [accept: "application/json"]
|
||||||
|
|||||||
@@ -88,13 +88,35 @@ defmodule Pinchflat.Metadata.MetadataFileHelpersTest do
|
|||||||
Helpers.download_and_store_thumbnail_for(media_item)
|
Helpers.download_and_store_thumbnail_for(media_item)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
test "returns nil if yt-dlp fails", %{media_item: media_item} do
|
||||||
|
stub(YtDlpRunnerMock, :run, fn _url, :download_thumbnail, _opts, _ot, _addl -> {:error, "error"} end)
|
||||||
|
|
||||||
|
filepath = Helpers.download_and_store_thumbnail_for(media_item)
|
||||||
|
|
||||||
|
assert filepath == nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "download_and_store_thumbnail_for/2 when testing cookie usage" do
|
||||||
test "sets use_cookies if the source uses cookies" do
|
test "sets use_cookies if the source uses cookies" do
|
||||||
expect(YtDlpRunnerMock, :run, fn _url, :download_thumbnail, _opts, _ot, addl ->
|
expect(YtDlpRunnerMock, :run, fn _url, :download_thumbnail, _opts, _ot, addl ->
|
||||||
assert {:use_cookies, true} in addl
|
assert {:use_cookies, true} in addl
|
||||||
{:ok, ""}
|
{:ok, ""}
|
||||||
end)
|
end)
|
||||||
|
|
||||||
source = source_fixture(%{use_cookies: true})
|
source = source_fixture(%{cookie_behaviour: :all_operations})
|
||||||
|
media_item = Repo.preload(media_item_fixture(%{source_id: source.id}), :source)
|
||||||
|
|
||||||
|
Helpers.download_and_store_thumbnail_for(media_item)
|
||||||
|
end
|
||||||
|
|
||||||
|
test "does not set use_cookies if the source uses cookies when needed" do
|
||||||
|
expect(YtDlpRunnerMock, :run, fn _url, :download_thumbnail, _opts, _ot, addl ->
|
||||||
|
assert {:use_cookies, false} in addl
|
||||||
|
{:ok, ""}
|
||||||
|
end)
|
||||||
|
|
||||||
|
source = source_fixture(%{cookie_behaviour: :when_needed})
|
||||||
media_item = Repo.preload(media_item_fixture(%{source_id: source.id}), :source)
|
media_item = Repo.preload(media_item_fixture(%{source_id: source.id}), :source)
|
||||||
|
|
||||||
Helpers.download_and_store_thumbnail_for(media_item)
|
Helpers.download_and_store_thumbnail_for(media_item)
|
||||||
@@ -106,19 +128,11 @@ defmodule Pinchflat.Metadata.MetadataFileHelpersTest do
|
|||||||
{:ok, ""}
|
{:ok, ""}
|
||||||
end)
|
end)
|
||||||
|
|
||||||
source = source_fixture(%{use_cookies: false})
|
source = source_fixture(%{cookie_behaviour: :disabled})
|
||||||
media_item = Repo.preload(media_item_fixture(%{source_id: source.id}), :source)
|
media_item = Repo.preload(media_item_fixture(%{source_id: source.id}), :source)
|
||||||
|
|
||||||
Helpers.download_and_store_thumbnail_for(media_item)
|
Helpers.download_and_store_thumbnail_for(media_item)
|
||||||
end
|
end
|
||||||
|
|
||||||
test "returns nil if yt-dlp fails", %{media_item: media_item} do
|
|
||||||
stub(YtDlpRunnerMock, :run, fn _url, :download_thumbnail, _opts, _ot, _addl -> {:error, "error"} end)
|
|
||||||
|
|
||||||
filepath = Helpers.download_and_store_thumbnail_for(media_item)
|
|
||||||
|
|
||||||
assert filepath == nil
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
describe "parse_upload_date/1" do
|
describe "parse_upload_date/1" do
|
||||||
|
|||||||
@@ -254,7 +254,23 @@ defmodule Pinchflat.Metadata.SourceMetadataStorageWorkerTest do
|
|||||||
end)
|
end)
|
||||||
|
|
||||||
profile = media_profile_fixture(%{download_source_images: true})
|
profile = media_profile_fixture(%{download_source_images: true})
|
||||||
source = source_fixture(media_profile_id: profile.id, use_cookies: true)
|
source = source_fixture(media_profile_id: profile.id, cookie_behaviour: :all_operations)
|
||||||
|
|
||||||
|
perform_job(SourceMetadataStorageWorker, %{id: source.id})
|
||||||
|
end
|
||||||
|
|
||||||
|
test "does not set use_cookies if the source uses cookies when needed" do
|
||||||
|
expect(YtDlpRunnerMock, :run, 2, fn
|
||||||
|
_url, :get_source_details, _opts, _ot, _addl ->
|
||||||
|
{:ok, source_details_return_fixture()}
|
||||||
|
|
||||||
|
_url, :get_source_metadata, _opts, _ot, addl ->
|
||||||
|
assert {:use_cookies, false} in addl
|
||||||
|
{:ok, render_metadata(:channel_source_metadata)}
|
||||||
|
end)
|
||||||
|
|
||||||
|
profile = media_profile_fixture(%{download_source_images: true})
|
||||||
|
source = source_fixture(media_profile_id: profile.id, cookie_behaviour: :when_needed)
|
||||||
|
|
||||||
perform_job(SourceMetadataStorageWorker, %{id: source.id})
|
perform_job(SourceMetadataStorageWorker, %{id: source.id})
|
||||||
end
|
end
|
||||||
@@ -270,7 +286,7 @@ defmodule Pinchflat.Metadata.SourceMetadataStorageWorkerTest do
|
|||||||
end)
|
end)
|
||||||
|
|
||||||
profile = media_profile_fixture(%{download_source_images: true})
|
profile = media_profile_fixture(%{download_source_images: true})
|
||||||
source = source_fixture(media_profile_id: profile.id, use_cookies: false)
|
source = source_fixture(media_profile_id: profile.id, cookie_behaviour: :disabled)
|
||||||
|
|
||||||
perform_job(SourceMetadataStorageWorker, %{id: source.id})
|
perform_job(SourceMetadataStorageWorker, %{id: source.id})
|
||||||
end
|
end
|
||||||
@@ -323,7 +339,21 @@ defmodule Pinchflat.Metadata.SourceMetadataStorageWorkerTest do
|
|||||||
{:ok, "{}"}
|
{:ok, "{}"}
|
||||||
end)
|
end)
|
||||||
|
|
||||||
source = source_fixture(%{series_directory: nil, use_cookies: true})
|
source = source_fixture(%{series_directory: nil, cookie_behaviour: :all_operations})
|
||||||
|
perform_job(SourceMetadataStorageWorker, %{id: source.id})
|
||||||
|
end
|
||||||
|
|
||||||
|
test "does not set use_cookies if the source uses cookies when needed" do
|
||||||
|
expect(YtDlpRunnerMock, :run, 2, fn
|
||||||
|
_url, :get_source_details, _opts, _ot, addl ->
|
||||||
|
assert {:use_cookies, false} in addl
|
||||||
|
{:ok, source_details_return_fixture()}
|
||||||
|
|
||||||
|
_url, :get_source_metadata, _opts, _ot, _addl ->
|
||||||
|
{:ok, "{}"}
|
||||||
|
end)
|
||||||
|
|
||||||
|
source = source_fixture(%{series_directory: nil, cookie_behaviour: :when_needed})
|
||||||
perform_job(SourceMetadataStorageWorker, %{id: source.id})
|
perform_job(SourceMetadataStorageWorker, %{id: source.id})
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -337,7 +367,7 @@ defmodule Pinchflat.Metadata.SourceMetadataStorageWorkerTest do
|
|||||||
{:ok, "{}"}
|
{:ok, "{}"}
|
||||||
end)
|
end)
|
||||||
|
|
||||||
source = source_fixture(%{series_directory: nil, use_cookies: false})
|
source = source_fixture(%{series_directory: nil, cookie_behaviour: :disabled})
|
||||||
perform_job(SourceMetadataStorageWorker, %{id: source.id})
|
perform_job(SourceMetadataStorageWorker, %{id: source.id})
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -85,5 +85,12 @@ defmodule Pinchflat.SettingsTest do
|
|||||||
assert %Ecto.Changeset{valid?: true} = Settings.change_setting(setting, %{extractor_sleep_interval_seconds: 0})
|
assert %Ecto.Changeset{valid?: true} = Settings.change_setting(setting, %{extractor_sleep_interval_seconds: 0})
|
||||||
assert %Ecto.Changeset{valid?: false} = Settings.change_setting(setting, %{extractor_sleep_interval_seconds: -1})
|
assert %Ecto.Changeset{valid?: false} = Settings.change_setting(setting, %{extractor_sleep_interval_seconds: -1})
|
||||||
end
|
end
|
||||||
|
|
||||||
|
test "allows you to reset the extractor sleep interval" do
|
||||||
|
setting = Settings.record()
|
||||||
|
assert {:ok, setting} = Settings.update_setting(setting, %{extractor_sleep_interval_seconds: 1})
|
||||||
|
|
||||||
|
assert %Ecto.Changeset{valid?: true} = Settings.change_setting(setting, %{extractor_sleep_interval_seconds: 0})
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -96,26 +96,6 @@ defmodule Pinchflat.SlowIndexing.SlowIndexingHelpersTest do
|
|||||||
assert_raise Ecto.NoResultsError, fn -> Repo.reload!(task) end
|
assert_raise Ecto.NoResultsError, fn -> Repo.reload!(task) end
|
||||||
end
|
end
|
||||||
|
|
||||||
test "deletes any pending media tasks for the source" do
|
|
||||||
source = source_fixture()
|
|
||||||
{:ok, job} = Oban.insert(FastIndexingWorker.new(%{"id" => source.id}))
|
|
||||||
task = task_fixture(source_id: source.id, job_id: job.id)
|
|
||||||
|
|
||||||
assert {:ok, _} = SlowIndexingHelpers.kickoff_indexing_task(source)
|
|
||||||
|
|
||||||
assert_raise Ecto.NoResultsError, fn -> Repo.reload!(task) end
|
|
||||||
end
|
|
||||||
|
|
||||||
test "deletes any fast indexing tasks for the source" do
|
|
||||||
source = source_fixture()
|
|
||||||
{:ok, job} = Oban.insert(FastIndexingWorker.new(%{"id" => source.id}))
|
|
||||||
task = task_fixture(source_id: source.id, job_id: job.id)
|
|
||||||
|
|
||||||
assert {:ok, _} = SlowIndexingHelpers.kickoff_indexing_task(source)
|
|
||||||
|
|
||||||
assert_raise Ecto.NoResultsError, fn -> Repo.reload!(task) end
|
|
||||||
end
|
|
||||||
|
|
||||||
test "can be called with additional job arguments" do
|
test "can be called with additional job arguments" do
|
||||||
source = source_fixture(index_frequency_minutes: 1)
|
source = source_fixture(index_frequency_minutes: 1)
|
||||||
job_args = %{"force" => true}
|
job_args = %{"force" => true}
|
||||||
@@ -322,14 +302,27 @@ defmodule Pinchflat.SlowIndexing.SlowIndexingHelpersTest do
|
|||||||
|
|
||||||
SlowIndexingHelpers.index_and_enqueue_download_for_media_items(source)
|
SlowIndexingHelpers.index_and_enqueue_download_for_media_items(source)
|
||||||
end
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "index_and_enqueue_download_for_media_items/2 when testing cookies" do
|
||||||
test "sets use_cookies if the source uses cookies" do
|
test "sets use_cookies if the source uses cookies" do
|
||||||
expect(YtDlpRunnerMock, :run, fn _url, :get_media_attributes_for_collection, _opts, _ot, addl_opts ->
|
expect(YtDlpRunnerMock, :run, fn _url, :get_media_attributes_for_collection, _opts, _ot, addl_opts ->
|
||||||
assert {:use_cookies, true} in addl_opts
|
assert {:use_cookies, true} in addl_opts
|
||||||
{:ok, source_attributes_return_fixture()}
|
{:ok, source_attributes_return_fixture()}
|
||||||
end)
|
end)
|
||||||
|
|
||||||
source = source_fixture(%{use_cookies: true})
|
source = source_fixture(%{cookie_behaviour: :all_operations})
|
||||||
|
|
||||||
|
SlowIndexingHelpers.index_and_enqueue_download_for_media_items(source)
|
||||||
|
end
|
||||||
|
|
||||||
|
test "sets use_cookies if the source uses cookies when needed" do
|
||||||
|
expect(YtDlpRunnerMock, :run, fn _url, :get_media_attributes_for_collection, _opts, _ot, addl_opts ->
|
||||||
|
assert {:use_cookies, true} in addl_opts
|
||||||
|
{:ok, source_attributes_return_fixture()}
|
||||||
|
end)
|
||||||
|
|
||||||
|
source = source_fixture(%{cookie_behaviour: :when_needed})
|
||||||
|
|
||||||
SlowIndexingHelpers.index_and_enqueue_download_for_media_items(source)
|
SlowIndexingHelpers.index_and_enqueue_download_for_media_items(source)
|
||||||
end
|
end
|
||||||
@@ -340,7 +333,7 @@ defmodule Pinchflat.SlowIndexing.SlowIndexingHelpersTest do
|
|||||||
{:ok, source_attributes_return_fixture()}
|
{:ok, source_attributes_return_fixture()}
|
||||||
end)
|
end)
|
||||||
|
|
||||||
source = source_fixture(%{use_cookies: false})
|
source = source_fixture(%{cookie_behaviour: :disabled})
|
||||||
|
|
||||||
SlowIndexingHelpers.index_and_enqueue_download_for_media_items(source)
|
SlowIndexingHelpers.index_and_enqueue_download_for_media_items(source)
|
||||||
end
|
end
|
||||||
@@ -475,7 +468,9 @@ defmodule Pinchflat.SlowIndexing.SlowIndexingHelpersTest do
|
|||||||
end
|
end
|
||||||
|
|
||||||
describe "index_and_enqueue_download_for_media_items when testing the download archive" do
|
describe "index_and_enqueue_download_for_media_items when testing the download archive" do
|
||||||
test "a download archive is used if the source is a channel", %{source: source} do
|
test "a download archive is used if the source is a channel that has been indexed before" do
|
||||||
|
source = source_fixture(%{collection_type: :channel, last_indexed_at: now()})
|
||||||
|
|
||||||
expect(YtDlpRunnerMock, :run, fn _url, :get_media_attributes_for_collection, opts, _ot, _addl_opts ->
|
expect(YtDlpRunnerMock, :run, fn _url, :get_media_attributes_for_collection, opts, _ot, _addl_opts ->
|
||||||
assert :break_on_existing in opts
|
assert :break_on_existing in opts
|
||||||
assert Keyword.has_key?(opts, :download_archive)
|
assert Keyword.has_key?(opts, :download_archive)
|
||||||
@@ -499,6 +494,19 @@ defmodule Pinchflat.SlowIndexing.SlowIndexingHelpersTest do
|
|||||||
SlowIndexingHelpers.index_and_enqueue_download_for_media_items(source)
|
SlowIndexingHelpers.index_and_enqueue_download_for_media_items(source)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
test "a download archive is not used if the source has never been indexed before" do
|
||||||
|
source = source_fixture(%{collection_type: :channel, last_indexed_at: nil})
|
||||||
|
|
||||||
|
expect(YtDlpRunnerMock, :run, fn _url, :get_media_attributes_for_collection, opts, _ot, _addl_opts ->
|
||||||
|
refute :break_on_existing in opts
|
||||||
|
refute Keyword.has_key?(opts, :download_archive)
|
||||||
|
|
||||||
|
{:ok, source_attributes_return_fixture()}
|
||||||
|
end)
|
||||||
|
|
||||||
|
SlowIndexingHelpers.index_and_enqueue_download_for_media_items(source)
|
||||||
|
end
|
||||||
|
|
||||||
test "a download archive is not used if the index has been forced to run" do
|
test "a download archive is not used if the index has been forced to run" do
|
||||||
source = source_fixture(%{collection_type: :channel})
|
source = source_fixture(%{collection_type: :channel})
|
||||||
|
|
||||||
@@ -512,7 +520,9 @@ defmodule Pinchflat.SlowIndexing.SlowIndexingHelpersTest do
|
|||||||
SlowIndexingHelpers.index_and_enqueue_download_for_media_items(source, was_forced: true)
|
SlowIndexingHelpers.index_and_enqueue_download_for_media_items(source, was_forced: true)
|
||||||
end
|
end
|
||||||
|
|
||||||
test "the download archive is formatted correctly and contains the right video", %{source: source} do
|
test "the download archive is formatted correctly and contains the right video" do
|
||||||
|
source = source_fixture(%{collection_type: :channel, last_indexed_at: now()})
|
||||||
|
|
||||||
media_items =
|
media_items =
|
||||||
1..21
|
1..21
|
||||||
|> Enum.map(fn n ->
|
|> Enum.map(fn n ->
|
||||||
|
|||||||
@@ -60,6 +60,33 @@ defmodule Pinchflat.SourcesTest do
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
describe "use_cookies?/2" do
|
||||||
|
test "returns true if the source has been set to use cookies" do
|
||||||
|
source = source_fixture(%{cookie_behaviour: :all_operations})
|
||||||
|
assert Sources.use_cookies?(source, :downloading)
|
||||||
|
end
|
||||||
|
|
||||||
|
test "returns false if the source has not been set to use cookies" do
|
||||||
|
source = source_fixture(%{cookie_behaviour: :disabled})
|
||||||
|
refute Sources.use_cookies?(source, :downloading)
|
||||||
|
end
|
||||||
|
|
||||||
|
test "returns true if the action is indexing and the source is set to :when_needed" do
|
||||||
|
source = source_fixture(%{cookie_behaviour: :when_needed})
|
||||||
|
assert Sources.use_cookies?(source, :indexing)
|
||||||
|
end
|
||||||
|
|
||||||
|
test "returns false if the action is downloading and the source is set to :when_needed" do
|
||||||
|
source = source_fixture(%{cookie_behaviour: :when_needed})
|
||||||
|
refute Sources.use_cookies?(source, :downloading)
|
||||||
|
end
|
||||||
|
|
||||||
|
test "returns true if the action is error_recovery and the source is set to :when_needed" do
|
||||||
|
source = source_fixture(%{cookie_behaviour: :when_needed})
|
||||||
|
assert Sources.use_cookies?(source, :error_recovery)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
describe "list_sources/0" do
|
describe "list_sources/0" do
|
||||||
test "it returns all sources" do
|
test "it returns all sources" do
|
||||||
source = source_fixture()
|
source = source_fixture()
|
||||||
@@ -294,6 +321,34 @@ defmodule Pinchflat.SourcesTest do
|
|||||||
assert_enqueued(worker: MediaCollectionIndexingWorker, args: %{"id" => source.id})
|
assert_enqueued(worker: MediaCollectionIndexingWorker, args: %{"id" => source.id})
|
||||||
end
|
end
|
||||||
|
|
||||||
|
test "creation will schedule a fast indexing job if the fast_index option is set" do
|
||||||
|
expect(YtDlpRunnerMock, :run, &channel_mock/5)
|
||||||
|
|
||||||
|
valid_attrs = %{
|
||||||
|
media_profile_id: media_profile_fixture().id,
|
||||||
|
original_url: "https://www.youtube.com/channel/abc123",
|
||||||
|
fast_index: true
|
||||||
|
}
|
||||||
|
|
||||||
|
assert {:ok, %Source{} = source} = Sources.create_source(valid_attrs)
|
||||||
|
|
||||||
|
assert_enqueued(worker: FastIndexingWorker, args: %{"id" => source.id})
|
||||||
|
end
|
||||||
|
|
||||||
|
test "creation will not schedule a fast indexing job if the fast_index option is not set" do
|
||||||
|
expect(YtDlpRunnerMock, :run, &channel_mock/5)
|
||||||
|
|
||||||
|
valid_attrs = %{
|
||||||
|
media_profile_id: media_profile_fixture().id,
|
||||||
|
original_url: "https://www.youtube.com/channel/abc123",
|
||||||
|
fast_index: false
|
||||||
|
}
|
||||||
|
|
||||||
|
assert {:ok, %Source{}} = Sources.create_source(valid_attrs)
|
||||||
|
|
||||||
|
refute_enqueued(worker: FastIndexingWorker)
|
||||||
|
end
|
||||||
|
|
||||||
test "creation schedules an index test even if the index frequency is 0" do
|
test "creation schedules an index test even if the index frequency is 0" do
|
||||||
expect(YtDlpRunnerMock, :run, &channel_mock/5)
|
expect(YtDlpRunnerMock, :run, &channel_mock/5)
|
||||||
|
|
||||||
@@ -365,13 +420,13 @@ defmodule Pinchflat.SourcesTest do
|
|||||||
valid_attrs = %{
|
valid_attrs = %{
|
||||||
media_profile_id: media_profile_fixture().id,
|
media_profile_id: media_profile_fixture().id,
|
||||||
original_url: "https://www.youtube.com/channel/abc123",
|
original_url: "https://www.youtube.com/channel/abc123",
|
||||||
use_cookies: true
|
cookie_behaviour: :all_operations
|
||||||
}
|
}
|
||||||
|
|
||||||
assert {:ok, %Source{}} = Sources.create_source(valid_attrs)
|
assert {:ok, %Source{}} = Sources.create_source(valid_attrs)
|
||||||
end
|
end
|
||||||
|
|
||||||
test "does not set use_cookies to false if the source has not been set to use cookies" do
|
test "does not set use_cookies if the source uses cookies when needed" do
|
||||||
expect(YtDlpRunnerMock, :run, fn _url, :get_source_details, _opts, _ot, addl ->
|
expect(YtDlpRunnerMock, :run, fn _url, :get_source_details, _opts, _ot, addl ->
|
||||||
refute Keyword.get(addl, :use_cookies)
|
refute Keyword.get(addl, :use_cookies)
|
||||||
|
|
||||||
@@ -381,7 +436,23 @@ defmodule Pinchflat.SourcesTest do
|
|||||||
valid_attrs = %{
|
valid_attrs = %{
|
||||||
media_profile_id: media_profile_fixture().id,
|
media_profile_id: media_profile_fixture().id,
|
||||||
original_url: "https://www.youtube.com/channel/abc123",
|
original_url: "https://www.youtube.com/channel/abc123",
|
||||||
use_cookies: false
|
cookie_behaviour: :when_needed
|
||||||
|
}
|
||||||
|
|
||||||
|
assert {:ok, %Source{}} = Sources.create_source(valid_attrs)
|
||||||
|
end
|
||||||
|
|
||||||
|
test "does not set use_cookies if the source has not been set to use cookies" do
|
||||||
|
expect(YtDlpRunnerMock, :run, fn _url, :get_source_details, _opts, _ot, addl ->
|
||||||
|
refute Keyword.get(addl, :use_cookies)
|
||||||
|
|
||||||
|
{:ok, playlist_return()}
|
||||||
|
end)
|
||||||
|
|
||||||
|
valid_attrs = %{
|
||||||
|
media_profile_id: media_profile_fixture().id,
|
||||||
|
original_url: "https://www.youtube.com/channel/abc123",
|
||||||
|
cookie_behaviour: :disabled
|
||||||
}
|
}
|
||||||
|
|
||||||
assert {:ok, %Source{}} = Sources.create_source(valid_attrs)
|
assert {:ok, %Source{}} = Sources.create_source(valid_attrs)
|
||||||
|
|||||||
@@ -33,4 +33,14 @@ defmodule Pinchflat.Utils.StringUtilsTest do
|
|||||||
assert StringUtils.double_brace("hello") == "{{ hello }}"
|
assert StringUtils.double_brace("hello") == "{{ hello }}"
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
describe "wrap_string/1" do
|
||||||
|
test "returns strings as-is" do
|
||||||
|
assert StringUtils.wrap_string("hello") == "hello"
|
||||||
|
end
|
||||||
|
|
||||||
|
test "returns other values as inspected strings" do
|
||||||
|
assert StringUtils.wrap_string(1) == "1"
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -154,6 +154,14 @@ defmodule Pinchflat.YtDlp.CommandRunnerTest do
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
describe "update/0" do
|
||||||
|
test "adds the update arg" do
|
||||||
|
assert {:ok, output} = Runner.update()
|
||||||
|
|
||||||
|
assert String.contains?(output, "--update")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
defp wrap_executable(new_executable, fun) do
|
defp wrap_executable(new_executable, fun) do
|
||||||
Application.put_env(:pinchflat, :yt_dlp_executable, new_executable)
|
Application.put_env(:pinchflat, :yt_dlp_executable, new_executable)
|
||||||
fun.()
|
fun.()
|
||||||
|
|||||||
@@ -269,7 +269,7 @@ defmodule Pinchflat.YtDlp.MediaTest do
|
|||||||
response = %{
|
response = %{
|
||||||
"original_url" => "https://www.youtube.com/watch?v=TiZPUDkDYbk",
|
"original_url" => "https://www.youtube.com/watch?v=TiZPUDkDYbk",
|
||||||
"aspect_ratio" => 0.5,
|
"aspect_ratio" => 0.5,
|
||||||
"duration" => 59,
|
"duration" => 150,
|
||||||
"upload_date" => "20210101"
|
"upload_date" => "20210101"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,24 @@
|
|||||||
|
defmodule Pinchflat.YtDlp.UpdateWorkerTest do
|
||||||
|
use Pinchflat.DataCase
|
||||||
|
|
||||||
|
alias Pinchflat.Settings
|
||||||
|
alias Pinchflat.YtDlp.UpdateWorker
|
||||||
|
|
||||||
|
describe "perform/1" do
|
||||||
|
test "calls the yt-dlp runner to update yt-dlp" do
|
||||||
|
expect(YtDlpRunnerMock, :update, fn -> {:ok, ""} end)
|
||||||
|
expect(YtDlpRunnerMock, :version, fn -> {:ok, ""} end)
|
||||||
|
|
||||||
|
perform_job(UpdateWorker, %{})
|
||||||
|
end
|
||||||
|
|
||||||
|
test "saves the new version to the database" do
|
||||||
|
expect(YtDlpRunnerMock, :update, fn -> {:ok, ""} end)
|
||||||
|
expect(YtDlpRunnerMock, :version, fn -> {:ok, "1.2.3"} end)
|
||||||
|
|
||||||
|
perform_job(UpdateWorker, %{})
|
||||||
|
|
||||||
|
assert {:ok, "1.2.3"} = Settings.get(:yt_dlp_version)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
Reference in New Issue
Block a user