Added forcing of downloads for media items

This commit is contained in:
Kieran Eglin 2024-04-03 13:12:13 -07:00
parent 1f2070aa52
commit 1a06405c29
No known key found for this signature in database
GPG key ID: 193984967FCF432D
10 changed files with 111 additions and 33 deletions

View file

@ -19,27 +19,29 @@ defmodule Pinchflat.Downloading.MediaDownloadWorker do
Returns {:ok, %Task{}} | {:error, :duplicate_job} | {:error, %Ecto.Changeset{}} Returns {:ok, %Task{}} | {:error, :duplicate_job} | {:error, %Ecto.Changeset{}}
""" """
def kickoff_with_task(media_item, opts \\ []) do def kickoff_with_task(media_item, job_args \\ %{}, job_opts \\ []) do
%{id: media_item.id} %{id: media_item.id}
|> MediaDownloadWorker.new(opts) |> Map.merge(job_args)
|> MediaDownloadWorker.new(job_opts)
|> Tasks.create_job_with_task(media_item) |> Tasks.create_job_with_task(media_item)
end end
@doc """ @doc """
For a given media item, download the media alongside any options. For a given media item, download the media alongside any options.
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).
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}}) do def perform(%Oban.Job{args: %{"id" => media_item_id} = args}) do
media_item = media_item =
media_item_id media_item_id
|> Media.get_media_item!() |> Media.get_media_item!()
|> Repo.preload(:source) |> Repo.preload(:source)
# If the source is set to not download media, perform a no-op # If the source is set to not download media, perform a no-op
if media_item.source.download_media do if media_item.source.download_media || args["force"] do
download_media_and_schedule_jobs(media_item) download_media_and_schedule_jobs(media_item)
else else
:ok :ok

View file

@ -6,6 +6,7 @@ defmodule PinchflatWeb.MediaItems.MediaItemController do
alias Pinchflat.Repo alias Pinchflat.Repo
alias Pinchflat.Media alias Pinchflat.Media
alias Pinchflat.Media.MediaItem alias Pinchflat.Media.MediaItem
alias Pinchflat.Downloading.MediaDownloadWorker
def show(conn, %{"id" => id}) do def show(conn, %{"id" => id}) do
media_item = media_item =
@ -47,6 +48,15 @@ defmodule PinchflatWeb.MediaItems.MediaItemController do
|> redirect(to: ~p"/sources/#{media_item.source_id}") |> redirect(to: ~p"/sources/#{media_item.source_id}")
end end
def force_download(conn, %{"media_item_id" => id}) do
media_item = Media.get_media_item!(id)
{:ok, _} = MediaDownloadWorker.kickoff_with_task(media_item, %{force: true})
conn
|> put_flash(:info, "Download task enqueued.")
|> redirect(to: ~p"/sources/#{media_item.source_id}/media/#{media_item}")
end
# See here for details on streaming files and range requests: # See here for details on streaming files and range requests:
# https://www.zeng.dev/post/2023-http-range-and-play-mp4-in-browser/ # https://www.zeng.dev/post/2023-http-range-and-play-mp4-in-browser/
# #

View file

@ -0,0 +1,32 @@
<.button_dropdown text="Actions" class="justify-center w-full sm:w-50">
<:option>
<.link
href={~p"/sources/#{@media_item.source_id}/media/#{@media_item}/force_download"}
method="post"
data-confirm="Are you sure you force a download of this media?"
>
Force Download
</.link>
</:option>
<:option>
<div class="h-px w-full bg-bodydark2"></div>
</:option>
<:option>
<.link
href={~p"/sources/#{@media_item.source_id}/media/#{@media_item}"}
method="delete"
data-confirm="Are you sure you want to delete all files for this media item? This cannot be undone."
>
Delete Files
</.link>
</:option>
<:option>
<.link
href={~p"/sources/#{@media_item.source_id}/media/#{@media_item}?prevent_download=true"}
method="delete"
data-confirm="Are you sure you want to delete all files for this media item and prevent it from re-downloading in the future? This cannot be undone."
>
Delete and Ignore
</.link>
</:option>
</.button_dropdown>

View file

@ -17,7 +17,7 @@
field={f[:prevent_download]} field={f[:prevent_download]}
type="toggle" type="toggle"
label="Prevent Download" label="Prevent Download"
help="Checking excludes this media item from being downloaded" help="Checking excludes this media item from automatic download. Download can still be manually forced"
/> />
<.input <.input

View file

@ -20,26 +20,7 @@
<div class="max-w-full overflow-x-auto"> <div class="max-w-full overflow-x-auto">
<.tabbed_layout> <.tabbed_layout>
<:tab_append> <:tab_append>
<.button_dropdown text="Actions" class="justify-center w-full sm:w-50"> <.actions_dropdown media_item={@media_item} />
<:option>
<.link
href={~p"/sources/#{@media_item.source_id}/media/#{@media_item}"}
method="delete"
data-confirm="Are you sure you want to delete all files for this media item? This cannot be undone."
>
Delete Files
</.link>
</:option>
<:option>
<.link
href={~p"/sources/#{@media_item.source_id}/media/#{@media_item}?prevent_download=true"}
method="delete"
data-confirm="Are you sure you want to delete all files for this media item and prevent it from re-downloading in the future? This cannot be undone."
>
Delete and Ignore
</.link>
</:option>
</.button_dropdown>
</:tab_append> </:tab_append>
<:tab title="Attributes"> <:tab title="Attributes">
@ -53,9 +34,9 @@
<h3 class="font-bold text-xl">Attributes</h3> <h3 class="font-bold text-xl">Attributes</h3>
<section> <section>
<strong>Source:</strong> <strong>Source:</strong>
<.inline_link href={~p"/sources/#{@media_item.source_id}"}> <.subtle_link href={~p"/sources/#{@media_item.source_id}"}>
<%= @media_item.source.custom_name %> <%= @media_item.source.custom_name %>
</.inline_link> </.subtle_link>
</section> </section>
<.list_items_from_map map={Map.from_struct(@media_item)} /> <.list_items_from_map map={Map.from_struct(@media_item)} />

View file

@ -16,7 +16,7 @@
<.link <.link
href={~p"/sources/#{@source}/force_download"} href={~p"/sources/#{@source}/force_download"}
method="post" method="post"
data-confirm="Are you sure you 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 Force Download
</.link> </.link>
@ -25,7 +25,7 @@
<.link <.link
href={~p"/sources/#{@source}/force_index"} href={~p"/sources/#{@source}/force_index"}
method="post" method="post"
data-confirm="Are you sure you force an index of this source? This isn't normally needed." data-confirm="Are you sure you want to force an index of this source? This isn't normally needed."
> >
Force Index Force Index
</.link> </.link>

View file

@ -28,9 +28,9 @@
<h3 class="font-bold text-lg">Attributes</h3> <h3 class="font-bold text-lg">Attributes</h3>
<section> <section>
<strong>Media Profile:</strong> <strong>Media Profile:</strong>
<.inline_link href={~p"/media_profiles/#{@source.media_profile_id}"}> <.subtle_link href={~p"/media_profiles/#{@source.media_profile_id}"}>
<%= @source.media_profile.name %> <%= @source.media_profile.name %>
</.inline_link> </.subtle_link>
</section> </section>
<.list_items_from_map map={Map.from_struct(@source)} /> <.list_items_from_map map={Map.from_struct(@source)} />

View file

@ -35,7 +35,9 @@ defmodule PinchflatWeb.Router do
post "/force_download", Sources.SourceController, :force_download post "/force_download", Sources.SourceController, :force_download
post "/force_index", Sources.SourceController, :force_index post "/force_index", Sources.SourceController, :force_index
resources "/media", MediaItems.MediaItemController, only: [:show, :edit, :update, :delete] resources "/media", MediaItems.MediaItemController, only: [:show, :edit, :update, :delete] do
post "/force_download", MediaItems.MediaItemController, :force_download
end
end end
end end

View file

@ -34,6 +34,23 @@ defmodule Pinchflat.Downloading.MediaDownloadWorkerTest do
assert {:ok, task} = MediaDownloadWorker.kickoff_with_task(media_item) assert {:ok, task} = MediaDownloadWorker.kickoff_with_task(media_item)
assert task.media_item_id == media_item.id assert task.media_item_id == media_item.id
end end
test "can be called with additional job arguments", %{media_item: media_item} do
job_args = %{"force" => true}
assert {:ok, _} = MediaDownloadWorker.kickoff_with_task(media_item, job_args)
assert_enqueued(worker: MediaDownloadWorker, args: %{"id" => media_item.id, "force" => true})
end
test "can be called with additional job options", %{media_item: media_item} do
job_opts = [max_attempts: 5]
assert {:ok, _} = MediaDownloadWorker.kickoff_with_task(media_item, %{}, job_opts)
[job] = all_enqueued(worker: MediaDownloadWorker, args: %{"id" => media_item.id})
assert job.max_attempts == 5
end
end end
describe "perform/1" do describe "perform/1" do
@ -88,6 +105,14 @@ 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 -> :ok end)
Sources.update_source(media_item.source, %{download_media: false})
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 -> expect(YtDlpRunnerMock, :run, fn _url, _opts, _ot ->
metadata = render_parsed_metadata(:media_metadata) metadata = render_parsed_metadata(:media_metadata)

View file

@ -4,6 +4,7 @@ defmodule PinchflatWeb.MediaItemControllerTest do
import Pinchflat.MediaFixtures import Pinchflat.MediaFixtures
alias Pinchflat.Repo alias Pinchflat.Repo
alias Pinchflat.Downloading.MediaDownloadWorker
describe "show media" do describe "show media" do
setup [:create_media_item] setup [:create_media_item]
@ -87,6 +88,31 @@ defmodule PinchflatWeb.MediaItemControllerTest do
end end
end end
describe "force_download" do
test "enqueues download task", %{conn: conn} do
media_item = media_item_fixture()
assert [] = all_enqueued(worker: MediaDownloadWorker)
post(conn, ~p"/sources/#{media_item.source_id}/media/#{media_item.id}/force_download")
assert [_] = all_enqueued(worker: MediaDownloadWorker)
end
test "forces a download even if one wouldn't normally run", %{conn: conn} do
media_item = media_item_fixture(%{media_filepath: nil})
post(conn, ~p"/sources/#{media_item.source_id}/media/#{media_item.id}/force_download")
assert [_] = all_enqueued(worker: MediaDownloadWorker, args: %{"id" => media_item.id, "force" => true})
end
test "redirects to the show page", %{conn: conn} do
media_item = media_item_fixture()
conn = post(conn, ~p"/sources/#{media_item.source_id}/media/#{media_item.id}/force_download")
assert redirected_to(conn) == ~p"/sources/#{media_item.source_id}/media/#{media_item.id}"
end
end
describe "streaming media" do describe "streaming media" do
test "returns 404 if the media isn't found", %{conn: conn} do test "returns 404 if the media isn't found", %{conn: conn} do
media_item = media_item_fixture() media_item = media_item_fixture()