Skip to content

Commit

Permalink
[Enhancement] Allow redownloading of files for existing media items (#…
Browse files Browse the repository at this point in the history
…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
  • Loading branch information
kieraneglin committed May 13, 2024
1 parent 5c86e71 commit a38ffbc
Show file tree
Hide file tree
Showing 27 changed files with 247 additions and 71 deletions.
2 changes: 1 addition & 1 deletion config/config.exs
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ config :pinchflat, Oban,
{Oban.Plugins.Cron,
crontab: [
{"0 1 * * *", Pinchflat.Downloading.MediaRetentionWorker},
{"0 2 * * *", Pinchflat.Downloading.MediaRedownloadWorker}
{"0 2 * * *", Pinchflat.Downloading.MediaQualityUpgradeWorker}
]}
],
# TODO: consider making this an env var or something?
Expand Down
11 changes: 6 additions & 5 deletions lib/pinchflat/downloading/download_option_builder.ex
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,11 @@ defmodule Pinchflat.Downloading.DownloadOptionBuilder do
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

built_options =
default_options() ++
default_options(override_opts) ++
subtitle_options(media_profile) ++
thumbnail_options(media_item_with_preloads) ++
metadata_options(media_profile) ++
Expand Down Expand Up @@ -50,11 +50,12 @@ defmodule Pinchflat.Downloading.DownloadOptionBuilder do
build_output_path_for(%MediaItem{source: source_with_preloads})
end

defp default_options do
defp default_options(override_opts) do
overwrite_behaviour = Keyword.get(override_opts, :overwrite_behaviour, :force_overwrites)

[
:no_progress,
# Add force-overwrites to make sure redownloading works
:force_overwrites,
overwrite_behaviour,
# This makes the date metadata conform to what jellyfin expects
parse_metadata: "%(upload_date>%Y-%m-%d)s:(?P<meta_date>.+)"
]
Expand Down
28 changes: 28 additions & 0 deletions lib/pinchflat/downloading/downloading_helpers.ex
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ defmodule Pinchflat.Downloading.DownloadingHelpers do

require Logger

use Pinchflat.Media.MediaQuery

alias Pinchflat.Repo
alias Pinchflat.Media
alias Pinchflat.Tasks
Expand Down Expand Up @@ -64,4 +66,30 @@ defmodule Pinchflat.Downloading.DownloadingHelpers do
{:error, :should_not_download}
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
19 changes: 14 additions & 5 deletions lib/pinchflat/downloading/media_download_worker.ex
Original file line number Diff line number Diff line change
Expand Up @@ -33,12 +33,18 @@ defmodule Pinchflat.Downloading.MediaDownloadWorker do
Does not download media if its source is set to not download media
(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}
"""
@impl Oban.Worker
def perform(%Oban.Job{args: %{"id" => media_item_id} = args}) do
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_id
Expand All @@ -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 (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
:ok
end
Expand All @@ -56,13 +62,16 @@ defmodule Pinchflat.Downloading.MediaDownloadWorker do
Ecto.StaleEntryError -> Logger.info("#{__MODULE__} discarded: media item #{media_item_id} stale")
end

defp download_media_and_schedule_jobs(media_item, is_redownload) do
case MediaDownloader.download_for_media_item(media_item) do
defp download_media_and_schedule_jobs(media_item, is_quality_upgrade, should_force) do
overwrite_behaviour = if should_force || is_quality_upgrade, do: :force_overwrites, else: :no_force_overwrites
override_opts = [overwrite_behaviour: overwrite_behaviour]

case MediaDownloader.download_for_media_item(media_item, override_opts) do
{:ok, downloaded_media_item} ->
{:ok, updated_media_item} =
Media.update_media_item(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)
Expand Down
8 changes: 4 additions & 4 deletions lib/pinchflat/downloading/media_downloader.ex
Original file line number Diff line number Diff line change
Expand Up @@ -29,11 +29,11 @@ defmodule Pinchflat.Downloading.MediaDownloader do
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)
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} ->
update_media_item_from_parsed_json(media_with_preloads, parsed_json)

Expand Down Expand Up @@ -103,8 +103,8 @@ defmodule Pinchflat.Downloading.MediaDownloader do
end
end

defp download_with_options(url, item_with_preloads, output_filepath) do
{:ok, options} = DownloadOptionBuilder.build(item_with_preloads)
defp download_with_options(url, item_with_preloads, output_filepath, override_opts) do
{:ok, options} = DownloadOptionBuilder.build(item_with_preloads, override_opts)

YtDlpMedia.download(url, options, output_filepath: output_filepath)
end
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
defmodule Pinchflat.Downloading.MediaRedownloadWorker do
defmodule Pinchflat.Downloading.MediaQualityUpgradeWorker do
@moduledoc false

use Oban.Worker,
Expand All @@ -12,7 +12,9 @@ defmodule Pinchflat.Downloading.MediaRedownloadWorker do
alias Pinchflat.Downloading.MediaDownloadWorker

@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
and it should run _after_ the retention worker.
Expand All @@ -25,7 +27,7 @@ defmodule Pinchflat.Downloading.MediaRedownloadWorker do
Logger.info("Redownloading #{length(redownloadable_media)} media items")

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
3 changes: 2 additions & 1 deletion lib/pinchflat/fast_indexing/fast_indexing_helpers.ex
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,11 @@ defmodule Pinchflat.FastIndexing.FastIndexingHelpers do

require Logger

use Pinchflat.Media.MediaQuery

alias Pinchflat.Repo
alias Pinchflat.Media
alias Pinchflat.Sources.Source
alias Pinchflat.Media.MediaQuery
alias Pinchflat.FastIndexing.YoutubeRss
alias Pinchflat.Downloading.DownloadingHelpers

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,9 @@ defmodule Pinchflat.Lifecycle.Notifications.SourceNotifications do

require Logger

use Pinchflat.Media.MediaQuery

alias Pinchflat.Repo
alias Pinchflat.Media.MediaQuery

@doc """
Wraps a function that may change the number of pending or downloaded
Expand Down
2 changes: 1 addition & 1 deletion lib/pinchflat/media/media.ex
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,12 @@ defmodule Pinchflat.Media do
"""

import Ecto.Query, warn: false
use Pinchflat.Media.MediaQuery

alias Pinchflat.Repo
alias Pinchflat.Tasks
alias Pinchflat.Sources.Source
alias Pinchflat.Media.MediaItem
alias Pinchflat.Media.MediaQuery
alias Pinchflat.Utils.FilesystemUtils
alias Pinchflat.Metadata.MediaMetadata

Expand Down
3 changes: 2 additions & 1 deletion lib/pinchflat/media/media_item.ex
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ defmodule Pinchflat.Media.MediaItem do
"""

use Ecto.Schema
use Pinchflat.Media.MediaQuery

import Ecto.Changeset
import Pinchflat.Utils.ChangesetUtils

Expand All @@ -12,7 +14,6 @@ defmodule Pinchflat.Media.MediaItem do
alias Pinchflat.Sources
alias Pinchflat.Tasks.Task
alias Pinchflat.Sources.Source
alias Pinchflat.Media.MediaQuery
alias Pinchflat.Metadata.MediaMetadata
alias Pinchflat.Media.MediaItemsSearchIndex

Expand Down
11 changes: 11 additions & 0 deletions lib/pinchflat/media/media_query.ex
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,17 @@ defmodule Pinchflat.Media.MediaQuery do

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:
# - for_* - belonging to a certain record
# - join_* - for joining on a certain record
Expand Down
3 changes: 2 additions & 1 deletion lib/pinchflat/podcasts/podcast_helpers.ex
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,9 @@ defmodule Pinchflat.Podcasts.PodcastHelpers do
or its media items
"""

use Pinchflat.Media.MediaQuery

alias Pinchflat.Repo
alias Pinchflat.Media.MediaQuery
alias Pinchflat.Metadata.MediaMetadata
alias Pinchflat.Metadata.SourceMetadata

Expand Down
4 changes: 2 additions & 2 deletions lib/pinchflat/sources/sources.ex
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,12 @@ defmodule Pinchflat.Sources do
"""

import Ecto.Query, warn: false
alias Pinchflat.Repo
use Pinchflat.Media.MediaQuery

alias Pinchflat.Repo
alias Pinchflat.Media
alias Pinchflat.Tasks
alias Pinchflat.Sources.Source
alias Pinchflat.Media.MediaQuery
alias Pinchflat.Profiles.MediaProfile
alias Pinchflat.YtDlp.MediaCollection
alias Pinchflat.Metadata.SourceMetadata
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,21 +27,20 @@ defmodule PinchflatWeb.CustomComponents.TableComponents do

def table(assigns) do
~H"""
<table class={["w-full table-auto", @table_class]}>
<table class={["w-full table-auto bg-boxdark", @table_class]}>
<thead>
<tr class="bg-gray-2 text-left dark:bg-meta-4">
<th :for={col <- @col} class="px-4 py-4 font-medium text-black dark:text-white xl:pl-11">
<tr class="text-left bg-meta-4">
<th :for={col <- @col} class="px-4 py-4 font-medium text-white xl:pl-11">
<%= col[:label] %>
</th>
</tr>
</thead>
<tbody>
<tr :for={{row, i} <- Enum.with_index(@rows)}>
<tr :for={row <- @rows} class="border-b border-boxdark hover:border-strokedark">
<td
:for={col <- @col}
class={[
"px-4 py-5 pl-9 dark:border-strokedark xl:pl-11",
i + 1 > length(@rows) && "border-b border-[#eee] dark:border-π",
"px-4 py-5 pl-9 xl:pl-11",
col[:class]
]}
>
Expand Down
2 changes: 1 addition & 1 deletion lib/pinchflat_web/controllers/pages/page_controller.ex
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
defmodule PinchflatWeb.Pages.PageController do
use PinchflatWeb, :controller
use Pinchflat.Media.MediaQuery

alias Pinchflat.Repo
alias Pinchflat.Sources.Source
alias Pinchflat.Media.MediaQuery
alias Pinchflat.Profiles.MediaProfile

def home(conn, params) do
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
defmodule Pinchflat.Pages.HistoryTableLive do
use PinchflatWeb, :live_view
import Ecto.Query, warn: false
use Pinchflat.Media.MediaQuery

alias Pinchflat.Repo
alias Pinchflat.Media.MediaQuery
alias Pinchflat.Utils.NumberUtils
alias PinchflatWeb.CustomComponents.TextComponents

Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
defmodule PinchflatWeb.Podcasts.PodcastController do
use PinchflatWeb, :controller
use Pinchflat.Media.MediaQuery

alias Pinchflat.Repo
alias Pinchflat.Sources.Source
alias Pinchflat.Media.MediaItem
alias Pinchflat.Media.MediaQuery
alias Pinchflat.Podcasts.RssFeedBuilder
alias Pinchflat.Podcasts.PodcastHelpers

Expand Down
11 changes: 10 additions & 1 deletion lib/pinchflat_web/controllers/sources/source_controller.ex
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ defmodule PinchflatWeb.Sources.SourceController do
|> redirect(to: ~p"/sources")
end

def force_download(conn, %{"source_id" => id}) do
def force_download_pending(conn, %{"source_id" => id}) do
wrap_forced_action(
conn,
id,
Expand All @@ -113,6 +113,15 @@ defmodule PinchflatWeb.Sources.SourceController do
)
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
wrap_forced_action(
conn,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,11 +31,20 @@
</:option>
<:option :if={@source.download_media}>
<.link
href={~p"/sources/#{@source}/force_download"}
href={~p"/sources/#{@source}/force_download_pending"}
method="post"
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>
</:option>
<:option>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
defmodule Pinchflat.Sources.MediaItemTableLive do
use PinchflatWeb, :live_view
import Ecto.Query, warn: false
use Pinchflat.Media.MediaQuery

alias Pinchflat.Repo
alias Pinchflat.Sources
alias Pinchflat.Media.MediaQuery
alias Pinchflat.Utils.NumberUtils

@limit 10
Expand Down

0 comments on commit a38ffbc

Please sign in to comment.