[Enhancement] Allow redownloading of files for existing media items (#239)

* Added ability to specify overwrite behaviour when downloading media

* Added helper for redownloading media items

* renamed media redownload worker to disambiguate it from similarly named methods

* Added new redownload option to source actions dropdown

* Refactored MediaQuery to use a __using__ macro

* docs
This commit is contained in:
Kieran 2024-05-13 14:25:39 -07:00 committed by GitHub
parent 5c86e7192e
commit a38ffbc55b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
27 changed files with 247 additions and 71 deletions

View file

@ -54,7 +54,7 @@ config :pinchflat, Oban,
{Oban.Plugins.Cron, {Oban.Plugins.Cron,
crontab: [ crontab: [
{"0 1 * * *", Pinchflat.Downloading.MediaRetentionWorker}, {"0 1 * * *", Pinchflat.Downloading.MediaRetentionWorker},
{"0 2 * * *", Pinchflat.Downloading.MediaRedownloadWorker} {"0 2 * * *", Pinchflat.Downloading.MediaQualityUpgradeWorker}
]} ]}
], ],
# TODO: consider making this an env var or something? # TODO: consider making this an env var or something?

View file

@ -15,11 +15,11 @@ defmodule Pinchflat.Downloading.DownloadOptionBuilder do
Returns {:ok, [Keyword.t()]} Returns {:ok, [Keyword.t()]}
""" """
def build(%MediaItem{} = media_item_with_preloads) do def build(%MediaItem{} = media_item_with_preloads, override_opts \\ []) do
media_profile = media_item_with_preloads.source.media_profile media_profile = media_item_with_preloads.source.media_profile
built_options = built_options =
default_options() ++ default_options(override_opts) ++
subtitle_options(media_profile) ++ subtitle_options(media_profile) ++
thumbnail_options(media_item_with_preloads) ++ thumbnail_options(media_item_with_preloads) ++
metadata_options(media_profile) ++ metadata_options(media_profile) ++
@ -50,11 +50,12 @@ defmodule Pinchflat.Downloading.DownloadOptionBuilder do
build_output_path_for(%MediaItem{source: source_with_preloads}) build_output_path_for(%MediaItem{source: source_with_preloads})
end end
defp default_options do defp default_options(override_opts) do
overwrite_behaviour = Keyword.get(override_opts, :overwrite_behaviour, :force_overwrites)
[ [
:no_progress, :no_progress,
# Add force-overwrites to make sure redownloading works overwrite_behaviour,
:force_overwrites,
# This makes the date metadata conform to what jellyfin expects # This makes the date metadata conform to what jellyfin expects
parse_metadata: "%(upload_date>%Y-%m-%d)s:(?P<meta_date>.+)" parse_metadata: "%(upload_date>%Y-%m-%d)s:(?P<meta_date>.+)"
] ]

View file

@ -7,6 +7,8 @@ defmodule Pinchflat.Downloading.DownloadingHelpers do
require Logger require Logger
use Pinchflat.Media.MediaQuery
alias Pinchflat.Repo alias Pinchflat.Repo
alias Pinchflat.Media alias Pinchflat.Media
alias Pinchflat.Tasks alias Pinchflat.Tasks
@ -64,4 +66,30 @@ defmodule Pinchflat.Downloading.DownloadingHelpers do
{:error, :should_not_download} {:error, :should_not_download}
end end
end end
@doc """
For a given source, enqueues download jobs for all media items _that have already been downloaded_.
This is useful for when a source's download settings have changed and you want to run through all
existing media and retry the download. For instance, if the source didn't originally download thumbnails
and you've changed the source to download them, you can use this to download all the thumbnails for
existing media items.
NOTE: does not delete existing files whatsoever. Does not overwrite the existing media file if it exists
at the location it expects. Will cause a full redownload of everything if the output template has changed
NOTE: unrelated to the MediaQualityUpgradeWorker, which is for redownloading media items for quality upgrades
or improved sponsorblock segments
Returns [{:ok, %Task{}} | {:error, any()}]
"""
def kickoff_redownload_for_existing_media(%Source{} = source) do
MediaQuery.new()
|> MediaQuery.for_source(source)
|> MediaQuery.with_media_downloaded_at()
|> MediaQuery.where_download_not_prevented()
|> MediaQuery.where_not_culled()
|> Repo.all()
|> Enum.map(&MediaDownloadWorker.kickoff_with_task/1)
end
end end

View file

@ -33,12 +33,18 @@ defmodule Pinchflat.Downloading.MediaDownloadWorker do
Does not download media if its source is set to not download media Does not download media if its source is set to not download media
(unless forced). (unless forced).
Options:
- `force`: force download even if the source is set to not download media. Fully
re-downloads media, including the video
- `quality_upgrade?`: re-downloads media, including the video. Does not force download
if the source is set to not download media
Returns :ok | {:ok, %MediaItem{}} | {:error, any, ...any} Returns :ok | {:ok, %MediaItem{}} | {:error, any, ...any}
""" """
@impl Oban.Worker @impl Oban.Worker
def perform(%Oban.Job{args: %{"id" => media_item_id} = args}) do def perform(%Oban.Job{args: %{"id" => media_item_id} = args}) do
should_force = Map.get(args, "force", false) should_force = Map.get(args, "force", false)
is_redownload = Map.get(args, "redownload?", false) is_quality_upgrade = Map.get(args, "quality_upgrade?", false)
media_item = media_item =
media_item_id media_item_id
@ -47,7 +53,7 @@ defmodule Pinchflat.Downloading.MediaDownloadWorker do
# If the source or media item is set to not download media, perform a no-op unless forced # If the source or media item is set to not download media, perform a no-op unless forced
if (media_item.source.download_media && !media_item.prevent_download) || should_force do if (media_item.source.download_media && !media_item.prevent_download) || should_force do
download_media_and_schedule_jobs(media_item, is_redownload) download_media_and_schedule_jobs(media_item, is_quality_upgrade, should_force)
else else
:ok :ok
end end
@ -56,13 +62,16 @@ 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
defp download_media_and_schedule_jobs(media_item, is_redownload) do defp download_media_and_schedule_jobs(media_item, is_quality_upgrade, should_force) do
case MediaDownloader.download_for_media_item(media_item) do overwrite_behaviour = if should_force || is_quality_upgrade, do: :force_overwrites, else: :no_force_overwrites
override_opts = [overwrite_behaviour: overwrite_behaviour]
case MediaDownloader.download_for_media_item(media_item, override_opts) do
{:ok, downloaded_media_item} -> {:ok, downloaded_media_item} ->
{:ok, updated_media_item} = {:ok, updated_media_item} =
Media.update_media_item(downloaded_media_item, %{ Media.update_media_item(downloaded_media_item, %{
media_size_bytes: compute_media_filesize(downloaded_media_item), media_size_bytes: compute_media_filesize(downloaded_media_item),
media_redownloaded_at: get_redownloaded_at(is_redownload) media_redownloaded_at: get_redownloaded_at(is_quality_upgrade)
}) })
:ok = run_user_script(updated_media_item) :ok = run_user_script(updated_media_item)

View file

@ -29,11 +29,11 @@ defmodule Pinchflat.Downloading.MediaDownloader do
Returns {:ok, %MediaItem{}} | {:error, any, ...any} Returns {:ok, %MediaItem{}} | {:error, any, ...any}
""" """
def download_for_media_item(%MediaItem{} = media_item) do def download_for_media_item(%MediaItem{} = 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])
case download_with_options(media_item.original_url, media_with_preloads, output_filepath) do case download_with_options(media_item.original_url, media_with_preloads, output_filepath, override_opts) do
{:ok, parsed_json} -> {:ok, parsed_json} ->
update_media_item_from_parsed_json(media_with_preloads, parsed_json) update_media_item_from_parsed_json(media_with_preloads, parsed_json)
@ -103,8 +103,8 @@ defmodule Pinchflat.Downloading.MediaDownloader do
end end
end end
defp download_with_options(url, item_with_preloads, output_filepath) do defp download_with_options(url, item_with_preloads, output_filepath, override_opts) do
{:ok, options} = DownloadOptionBuilder.build(item_with_preloads) {:ok, options} = DownloadOptionBuilder.build(item_with_preloads, override_opts)
YtDlpMedia.download(url, options, output_filepath: output_filepath) YtDlpMedia.download(url, options, output_filepath: output_filepath)
end end

View file

@ -1,4 +1,4 @@
defmodule Pinchflat.Downloading.MediaRedownloadWorker do defmodule Pinchflat.Downloading.MediaQualityUpgradeWorker do
@moduledoc false @moduledoc false
use Oban.Worker, use Oban.Worker,
@ -12,7 +12,9 @@ defmodule Pinchflat.Downloading.MediaRedownloadWorker do
alias Pinchflat.Downloading.MediaDownloadWorker alias Pinchflat.Downloading.MediaDownloadWorker
@doc """ @doc """
Redownloads media items that are eligible for redownload. Redownloads media items that are eligible for redownload for the purpose
of upgrading the quality of the media or improving things like sponsorblock
segments.
This worker is scheduled to run daily via the Oban Cron plugin This worker is scheduled to run daily via the Oban Cron plugin
and it should run _after_ the retention worker. and it should run _after_ the retention worker.
@ -25,7 +27,7 @@ defmodule Pinchflat.Downloading.MediaRedownloadWorker do
Logger.info("Redownloading #{length(redownloadable_media)} media items") Logger.info("Redownloading #{length(redownloadable_media)} media items")
Enum.each(redownloadable_media, fn media_item -> Enum.each(redownloadable_media, fn media_item ->
MediaDownloadWorker.kickoff_with_task(media_item, %{redownload?: true}) MediaDownloadWorker.kickoff_with_task(media_item, %{quality_upgrade?: true})
end) end)
end end
end end

View file

@ -7,10 +7,11 @@ defmodule Pinchflat.FastIndexing.FastIndexingHelpers do
require Logger require Logger
use Pinchflat.Media.MediaQuery
alias Pinchflat.Repo alias Pinchflat.Repo
alias Pinchflat.Media alias Pinchflat.Media
alias Pinchflat.Sources.Source alias Pinchflat.Sources.Source
alias Pinchflat.Media.MediaQuery
alias Pinchflat.FastIndexing.YoutubeRss alias Pinchflat.FastIndexing.YoutubeRss
alias Pinchflat.Downloading.DownloadingHelpers alias Pinchflat.Downloading.DownloadingHelpers

View file

@ -5,8 +5,9 @@ defmodule Pinchflat.Lifecycle.Notifications.SourceNotifications do
require Logger require Logger
use Pinchflat.Media.MediaQuery
alias Pinchflat.Repo alias Pinchflat.Repo
alias Pinchflat.Media.MediaQuery
@doc """ @doc """
Wraps a function that may change the number of pending or downloaded Wraps a function that may change the number of pending or downloaded

View file

@ -4,12 +4,12 @@ defmodule Pinchflat.Media do
""" """
import Ecto.Query, warn: false import Ecto.Query, warn: false
use Pinchflat.Media.MediaQuery
alias Pinchflat.Repo alias Pinchflat.Repo
alias Pinchflat.Tasks alias Pinchflat.Tasks
alias Pinchflat.Sources.Source alias Pinchflat.Sources.Source
alias Pinchflat.Media.MediaItem alias Pinchflat.Media.MediaItem
alias Pinchflat.Media.MediaQuery
alias Pinchflat.Utils.FilesystemUtils alias Pinchflat.Utils.FilesystemUtils
alias Pinchflat.Metadata.MediaMetadata alias Pinchflat.Metadata.MediaMetadata

View file

@ -4,6 +4,8 @@ defmodule Pinchflat.Media.MediaItem do
""" """
use Ecto.Schema use Ecto.Schema
use Pinchflat.Media.MediaQuery
import Ecto.Changeset import Ecto.Changeset
import Pinchflat.Utils.ChangesetUtils import Pinchflat.Utils.ChangesetUtils
@ -12,7 +14,6 @@ defmodule Pinchflat.Media.MediaItem do
alias Pinchflat.Sources alias Pinchflat.Sources
alias Pinchflat.Tasks.Task alias Pinchflat.Tasks.Task
alias Pinchflat.Sources.Source alias Pinchflat.Sources.Source
alias Pinchflat.Media.MediaQuery
alias Pinchflat.Metadata.MediaMetadata alias Pinchflat.Metadata.MediaMetadata
alias Pinchflat.Media.MediaItemsSearchIndex alias Pinchflat.Media.MediaItemsSearchIndex

View file

@ -12,6 +12,17 @@ defmodule Pinchflat.Media.MediaQuery do
alias Pinchflat.Media.MediaItem alias Pinchflat.Media.MediaItem
# This allows the module to be aliased and query methods to be used
# all in one go
# usage: use Pinchflat.Media.MediaQuery
defmacro __using__(_opts) do
quote do
import Ecto.Query, warn: false
alias unquote(__MODULE__)
end
end
# Prefixes: # Prefixes:
# - for_* - belonging to a certain record # - for_* - belonging to a certain record
# - join_* - for joining on a certain record # - join_* - for joining on a certain record

View file

@ -4,8 +4,9 @@ defmodule Pinchflat.Podcasts.PodcastHelpers do
or its media items or its media items
""" """
use Pinchflat.Media.MediaQuery
alias Pinchflat.Repo alias Pinchflat.Repo
alias Pinchflat.Media.MediaQuery
alias Pinchflat.Metadata.MediaMetadata alias Pinchflat.Metadata.MediaMetadata
alias Pinchflat.Metadata.SourceMetadata alias Pinchflat.Metadata.SourceMetadata

View file

@ -4,12 +4,12 @@ defmodule Pinchflat.Sources do
""" """
import Ecto.Query, warn: false import Ecto.Query, warn: false
alias Pinchflat.Repo use Pinchflat.Media.MediaQuery
alias Pinchflat.Repo
alias Pinchflat.Media alias Pinchflat.Media
alias Pinchflat.Tasks alias Pinchflat.Tasks
alias Pinchflat.Sources.Source alias Pinchflat.Sources.Source
alias Pinchflat.Media.MediaQuery
alias Pinchflat.Profiles.MediaProfile alias Pinchflat.Profiles.MediaProfile
alias Pinchflat.YtDlp.MediaCollection alias Pinchflat.YtDlp.MediaCollection
alias Pinchflat.Metadata.SourceMetadata alias Pinchflat.Metadata.SourceMetadata

View file

@ -27,21 +27,20 @@ defmodule PinchflatWeb.CustomComponents.TableComponents do
def table(assigns) do def table(assigns) do
~H""" ~H"""
<table class={["w-full table-auto", @table_class]}> <table class={["w-full table-auto bg-boxdark", @table_class]}>
<thead> <thead>
<tr class="bg-gray-2 text-left dark:bg-meta-4"> <tr class="text-left bg-meta-4">
<th :for={col <- @col} class="px-4 py-4 font-medium text-black dark:text-white xl:pl-11"> <th :for={col <- @col} class="px-4 py-4 font-medium text-white xl:pl-11">
<%= col[:label] %> <%= col[:label] %>
</th> </th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
<tr :for={{row, i} <- Enum.with_index(@rows)}> <tr :for={row <- @rows} class="border-b border-boxdark hover:border-strokedark">
<td <td
:for={col <- @col} :for={col <- @col}
class={[ class={[
"px-4 py-5 pl-9 dark:border-strokedark xl:pl-11", "px-4 py-5 pl-9 xl:pl-11",
i + 1 > length(@rows) && "border-b border-[#eee] dark:border-π",
col[:class] col[:class]
]} ]}
> >

View file

@ -1,9 +1,9 @@
defmodule PinchflatWeb.Pages.PageController do defmodule PinchflatWeb.Pages.PageController do
use PinchflatWeb, :controller use PinchflatWeb, :controller
use Pinchflat.Media.MediaQuery
alias Pinchflat.Repo alias Pinchflat.Repo
alias Pinchflat.Sources.Source alias Pinchflat.Sources.Source
alias Pinchflat.Media.MediaQuery
alias Pinchflat.Profiles.MediaProfile alias Pinchflat.Profiles.MediaProfile
def home(conn, params) do def home(conn, params) do

View file

@ -1,9 +1,8 @@
defmodule Pinchflat.Pages.HistoryTableLive do defmodule Pinchflat.Pages.HistoryTableLive do
use PinchflatWeb, :live_view use PinchflatWeb, :live_view
import Ecto.Query, warn: false use Pinchflat.Media.MediaQuery
alias Pinchflat.Repo alias Pinchflat.Repo
alias Pinchflat.Media.MediaQuery
alias Pinchflat.Utils.NumberUtils alias Pinchflat.Utils.NumberUtils
alias PinchflatWeb.CustomComponents.TextComponents alias PinchflatWeb.CustomComponents.TextComponents

View file

@ -1,10 +1,10 @@
defmodule PinchflatWeb.Podcasts.PodcastController do defmodule PinchflatWeb.Podcasts.PodcastController do
use PinchflatWeb, :controller use PinchflatWeb, :controller
use Pinchflat.Media.MediaQuery
alias Pinchflat.Repo alias Pinchflat.Repo
alias Pinchflat.Sources.Source alias Pinchflat.Sources.Source
alias Pinchflat.Media.MediaItem alias Pinchflat.Media.MediaItem
alias Pinchflat.Media.MediaQuery
alias Pinchflat.Podcasts.RssFeedBuilder alias Pinchflat.Podcasts.RssFeedBuilder
alias Pinchflat.Podcasts.PodcastHelpers alias Pinchflat.Podcasts.PodcastHelpers

View file

@ -104,7 +104,7 @@ defmodule PinchflatWeb.Sources.SourceController do
|> redirect(to: ~p"/sources") |> redirect(to: ~p"/sources")
end end
def force_download(conn, %{"source_id" => id}) do def force_download_pending(conn, %{"source_id" => id}) do
wrap_forced_action( wrap_forced_action(
conn, conn,
id, id,
@ -113,6 +113,15 @@ defmodule PinchflatWeb.Sources.SourceController do
) )
end end
def force_redownload(conn, %{"source_id" => id}) do
wrap_forced_action(
conn,
id,
"Forcing re-download of downloaded media items.",
&DownloadingHelpers.kickoff_redownload_for_existing_media/1
)
end
def force_index(conn, %{"source_id" => id}) do def force_index(conn, %{"source_id" => id}) do
wrap_forced_action( wrap_forced_action(
conn, conn,

View file

@ -31,11 +31,20 @@
</:option> </:option>
<:option :if={@source.download_media}> <:option :if={@source.download_media}>
<.link <.link
href={~p"/sources/#{@source}/force_download"} href={~p"/sources/#{@source}/force_download_pending"}
method="post" method="post"
data-confirm="Are you sure you want to force a download of all *pending* media items? This isn't normally needed." data-confirm="Are you sure you want to force a download of all *pending* media items? This isn't normally needed."
> >
Force Download Download Pending
</.link>
</:option>
<:option :if={@source.download_media}>
<.link
href={~p"/sources/#{@source}/force_redownload"}
method="post"
data-confirm="Are you sure you want to re-download all currently downloaded media items? This isn't normally needed and won't change anything if the files already exist."
>
Redownload Existing
</.link> </.link>
</:option> </:option>
<:option> <:option>

View file

@ -1,10 +1,9 @@
defmodule Pinchflat.Sources.MediaItemTableLive do defmodule Pinchflat.Sources.MediaItemTableLive do
use PinchflatWeb, :live_view use PinchflatWeb, :live_view
import Ecto.Query, warn: false use Pinchflat.Media.MediaQuery
alias Pinchflat.Repo alias Pinchflat.Repo
alias Pinchflat.Sources alias Pinchflat.Sources
alias Pinchflat.Media.MediaQuery
alias Pinchflat.Utils.NumberUtils alias Pinchflat.Utils.NumberUtils
@limit 10 @limit 10

View file

@ -33,7 +33,8 @@ defmodule PinchflatWeb.Router do
resources "/settings", Settings.SettingController, only: [:show, :update], singleton: true resources "/settings", Settings.SettingController, only: [:show, :update], singleton: true
resources "/sources", Sources.SourceController do resources "/sources", Sources.SourceController do
post "/force_download", Sources.SourceController, :force_download post "/force_download_pending", Sources.SourceController, :force_download_pending
post "/force_redownload", Sources.SourceController, :force_redownload
post "/force_index", Sources.SourceController, :force_index post "/force_index", Sources.SourceController, :force_index
post "/force_metadata_refresh", Sources.SourceController, :force_metadata_refresh post "/force_metadata_refresh", Sources.SourceController, :force_metadata_refresh

View file

@ -65,6 +65,13 @@ defmodule Pinchflat.Downloading.DownloadOptionBuilderTest do
assert :force_overwrites in res assert :force_overwrites in res
assert {:parse_metadata, "%(upload_date>%Y-%m-%d)s:(?P<meta_date>.+)"} in res assert {:parse_metadata, "%(upload_date>%Y-%m-%d)s:(?P<meta_date>.+)"} in res
end end
test "includes override options if specified", %{media_item: media_item} do
assert {:ok, res} = DownloadOptionBuilder.build(media_item, overwrite_behaviour: :no_force_overwrites)
refute :force_overwrites in res
assert :no_force_overwrites in res
end
end end
describe "build/1 when testing subtitle options" do describe "build/1 when testing subtitle options" do

View file

@ -110,4 +110,32 @@ defmodule Pinchflat.Downloading.DownloadingHelpersTest do
refute_enqueued(worker: MediaDownloadWorker) refute_enqueued(worker: MediaDownloadWorker)
end end
end end
describe "kickoff_redownload_for_existing_media/1" do
test "enqueues a download job for each downloaded media item" do
source = source_fixture()
media_item = media_item_fixture(source_id: source.id, media_downloaded_at: now())
assert [{:ok, _}] = DownloadingHelpers.kickoff_redownload_for_existing_media(source)
assert_enqueued(worker: MediaDownloadWorker, args: %{"id" => media_item.id})
end
test "doesn't enqueue jobs for media that should be ignored" do
source = source_fixture()
other_source = source_fixture()
_not_downloaded = media_item_fixture(source_id: source.id, media_downloaded_at: nil)
_other_source = media_item_fixture(source_id: other_source.id, media_downloaded_at: now())
_download_prevented =
media_item_fixture(source_id: source.id, media_downloaded_at: now(), prevent_download: true)
_culled =
media_item_fixture(source_id: source.id, media_downloaded_at: now(), culled_at: now())
assert [] = DownloadingHelpers.kickoff_redownload_for_existing_media(source)
refute_enqueued(worker: MediaDownloadWorker)
end
end
end end

View file

@ -108,7 +108,7 @@ defmodule Pinchflat.Downloading.MediaDownloadWorkerTest do
end) end)
Oban.Testing.with_testing_mode(:inline, fn -> Oban.Testing.with_testing_mode(:inline, fn ->
{:ok, job} = Oban.insert(MediaDownloadWorker.new(%{id: media_item.id, redownload?: true})) {:ok, job} = Oban.insert(MediaDownloadWorker.new(%{id: media_item.id, quality_upgrade?: true}))
assert job.state == "completed" assert job.state == "completed"
end) end)
@ -136,15 +136,6 @@ defmodule Pinchflat.Downloading.MediaDownloadWorkerTest do
perform_job(MediaDownloadWorker, %{id: media_item.id}) perform_job(MediaDownloadWorker, %{id: media_item.id})
end end
test "downloads anyway if forced", %{media_item: media_item} do
expect(YtDlpRunnerMock, :run, fn _url, _opts, _ot, _addl -> :ok end)
Sources.update_source(media_item.source, %{download_media: false})
Media.update_media_item(media_item, %{prevent_download: true})
perform_job(MediaDownloadWorker, %{id: media_item.id, force: true})
end
test "it saves the file's size to the database", %{media_item: media_item} do test "it saves the file's size to the database", %{media_item: media_item} do
expect(YtDlpRunnerMock, :run, fn _url, _opts, _ot, _addl -> expect(YtDlpRunnerMock, :run, fn _url, _opts, _ot, _addl ->
metadata = render_parsed_metadata(:media_metadata) metadata = render_parsed_metadata(:media_metadata)
@ -159,18 +150,7 @@ defmodule Pinchflat.Downloading.MediaDownloadWorkerTest do
assert media_item.media_size_bytes > 0 assert media_item.media_size_bytes > 0
end end
test "saves redownloaded_at if this is for a redownload", %{media_item: media_item} do test "does not set redownloaded_at by default", %{media_item: media_item} do
expect(YtDlpRunnerMock, :run, fn _url, _opts, _ot, _addl ->
{:ok, render_metadata(:media_metadata)}
end)
perform_job(MediaDownloadWorker, %{id: media_item.id, redownload?: true})
media_item = Repo.reload(media_item)
assert media_item.media_redownloaded_at != nil
end
test "doesn't save redownloaded_at if this is not for a redownload", %{media_item: media_item} do
expect(YtDlpRunnerMock, :run, fn _url, _opts, _ot, _addl -> expect(YtDlpRunnerMock, :run, fn _url, _opts, _ot, _addl ->
{:ok, render_metadata(:media_metadata)} {:ok, render_metadata(:media_metadata)}
end) end)
@ -198,5 +178,62 @@ defmodule Pinchflat.Downloading.MediaDownloadWorkerTest do
test "does not blow up if the record doesn't exist" do test "does not blow up if the record doesn't exist" do
assert :ok = perform_job(MediaDownloadWorker, %{id: 0}) assert :ok = perform_job(MediaDownloadWorker, %{id: 0})
end end
test "sets the no_force_overwrites runner option", %{media_item: media_item} do
expect(YtDlpRunnerMock, :run, fn _url, opts, _ot, _addl ->
assert :no_force_overwrites in opts
refute :force_overwrites in opts
{:ok, render_metadata(:media_metadata)}
end)
perform_job(MediaDownloadWorker, %{id: media_item.id})
end
end
describe "perform/1 when testing forced downloads" do
test "ignores 'prevent_download' if forced", %{media_item: media_item} do
expect(YtDlpRunnerMock, :run, fn _url, _opts, _ot, _addl -> :ok end)
Sources.update_source(media_item.source, %{download_media: false})
Media.update_media_item(media_item, %{prevent_download: true})
perform_job(MediaDownloadWorker, %{id: media_item.id, force: true})
end
test "sets force_overwrites runner option", %{media_item: media_item} do
expect(YtDlpRunnerMock, :run, fn _url, opts, _ot, _addl ->
assert :force_overwrites in opts
refute :no_force_overwrites in opts
{:ok, render_metadata(:media_metadata)}
end)
perform_job(MediaDownloadWorker, %{id: media_item.id, force: true})
end
end
describe "perform/1 when testing re-downloads" do
test "sets redownloaded_at on the media_item", %{media_item: media_item} do
expect(YtDlpRunnerMock, :run, fn _url, _opts, _ot, _addl ->
{:ok, render_metadata(:media_metadata)}
end)
perform_job(MediaDownloadWorker, %{id: media_item.id, quality_upgrade?: true})
media_item = Repo.reload(media_item)
assert media_item.media_redownloaded_at != nil
end
test "sets force_overwrites runner option", %{media_item: media_item} do
expect(YtDlpRunnerMock, :run, fn _url, opts, _ot, _addl ->
assert :force_overwrites in opts
refute :no_force_overwrites in opts
{:ok, render_metadata(:media_metadata)}
end)
perform_job(MediaDownloadWorker, %{id: media_item.id, force: true})
end
end end
end end

View file

@ -65,6 +65,21 @@ defmodule Pinchflat.Downloading.MediaDownloaderTest do
end end
end end
describe "download_for_media_item/3 when testing override options" do
test "includes override opts if specified", %{media_item: media_item} do
expect(YtDlpRunnerMock, :run, fn _url, opts, _ot, _addl ->
refute :force_overwrites in opts
assert :no_force_overwrites in opts
{:ok, render_metadata(:media_metadata)}
end)
override_opts = [overwrite_behaviour: :no_force_overwrites]
assert {:ok, _} = MediaDownloader.download_for_media_item(media_item, override_opts)
end
end
describe "download_for_media_item/3 when testing retries" do describe "download_for_media_item/3 when testing 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"

View file

@ -1,4 +1,4 @@
defmodule Pinchflat.Downloading.MediaRedownloadWorkerTest do defmodule Pinchflat.Downloading.MediaQualityUpgradeWorkerTest do
use Pinchflat.DataCase use Pinchflat.DataCase
import Pinchflat.MediaFixtures import Pinchflat.MediaFixtures
@ -6,7 +6,7 @@ defmodule Pinchflat.Downloading.MediaRedownloadWorkerTest do
import Pinchflat.ProfilesFixtures import Pinchflat.ProfilesFixtures
alias Pinchflat.Downloading.MediaDownloadWorker alias Pinchflat.Downloading.MediaDownloadWorker
alias Pinchflat.Downloading.MediaRedownloadWorker alias Pinchflat.Downloading.MediaQualityUpgradeWorker
describe "perform/1" do describe "perform/1" do
test "kicks off a task for redownloadable media items" do test "kicks off a task for redownloadable media items" do
@ -20,9 +20,9 @@ defmodule Pinchflat.Downloading.MediaRedownloadWorkerTest do
media_downloaded_at: now_minus(5, :days) media_downloaded_at: now_minus(5, :days)
}) })
perform_job(MediaRedownloadWorker, %{}) perform_job(MediaQualityUpgradeWorker, %{})
assert [_] = all_enqueued(worker: MediaDownloadWorker, args: %{id: media_item.id, redownload?: true}) assert [_] = all_enqueued(worker: MediaDownloadWorker, args: %{id: media_item.id, quality_upgrade?: true})
end end
test "does not kickoff a task for non-redownloadable media items" do test "does not kickoff a task for non-redownloadable media items" do
@ -36,7 +36,7 @@ defmodule Pinchflat.Downloading.MediaRedownloadWorkerTest do
media_downloaded_at: now_minus(1, :day) media_downloaded_at: now_minus(1, :day)
}) })
perform_job(MediaRedownloadWorker, %{}) perform_job(MediaQualityUpgradeWorker, %{})
assert [] = all_enqueued(worker: MediaDownloadWorker) assert [] = all_enqueued(worker: MediaDownloadWorker)
end end

View file

@ -166,20 +166,38 @@ defmodule PinchflatWeb.SourceControllerTest do
end end
end end
describe "force_download" do describe "force_download_pending" do
test "enqueues pending download tasks", %{conn: conn} do test "enqueues pending download tasks", %{conn: conn} 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})
assert [] = all_enqueued(worker: MediaDownloadWorker) assert [] = all_enqueued(worker: MediaDownloadWorker)
post(conn, ~p"/sources/#{source.id}/force_download") post(conn, ~p"/sources/#{source.id}/force_download_pending")
assert [_] = all_enqueued(worker: MediaDownloadWorker) assert [_] = all_enqueued(worker: MediaDownloadWorker)
end end
test "redirects to the source page", %{conn: conn} do test "redirects to the source page", %{conn: conn} do
source = source_fixture() source = source_fixture()
conn = post(conn, ~p"/sources/#{source.id}/force_download") conn = post(conn, ~p"/sources/#{source.id}/force_download_pending")
assert redirected_to(conn) == ~p"/sources/#{source.id}"
end
end
describe "force_redownload" do
test "enqueues re-download tasks", %{conn: conn} do
source = source_fixture()
_media_item = media_item_fixture(source_id: source.id, media_downloaded_at: now())
assert [] = all_enqueued(worker: MediaDownloadWorker)
post(conn, ~p"/sources/#{source.id}/force_redownload")
assert [_] = all_enqueued(worker: MediaDownloadWorker)
end
test "redirects to the source page", %{conn: conn} do
source = source_fixture()
conn = post(conn, ~p"/sources/#{source.id}/force_redownload")
assert redirected_to(conn) == ~p"/sources/#{source.id}" assert redirected_to(conn) == ~p"/sources/#{source.id}"
end end
end end