pinchflat/lib/pinchflat/downloading/media_downloader.ex
Kieran fd20ac5d84
[Enhancement] Use cookies on a per-source basis (#354)
* Added 'use_cookies' column to source

* Added cookies to the source form

* Updated every command to optionally include cookies

* LOTS of tests

* Made YT cookies off by default
2024-08-14 12:00:18 -07:00

119 lines
4.4 KiB
Elixir

defmodule Pinchflat.Downloading.MediaDownloader do
@moduledoc """
This is the integration layer for actually downloading media.
It takes into account the media profile's settings in order
to download the media with the desired options.
"""
require Logger
alias Pinchflat.Repo
alias Pinchflat.Media
alias Pinchflat.Media.MediaItem
alias Pinchflat.Metadata.NfoBuilder
alias Pinchflat.Metadata.MetadataParser
alias Pinchflat.Metadata.MetadataFileHelpers
alias Pinchflat.Utils.FilesystemUtils
alias Pinchflat.Downloading.DownloadOptionBuilder
alias Pinchflat.YtDlp.Media, as: YtDlpMedia
@doc """
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
media_metadata record.
NOTE: related methods (like the download worker) won't download if the media item's source
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.
Returns {:ok, %MediaItem{}} | {:error, any, ...any}
"""
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, override_opts) do
{:ok, parsed_json} ->
update_media_item_from_parsed_json(media_with_preloads, parsed_json)
{:error, message, _exit_code} ->
Logger.error("yt-dlp download error for media item ##{media_with_preloads.id}: #{inspect(message)}")
if String.contains?(to_string(message), recoverable_errors()) do
attempt_update_media_item(media_with_preloads, output_filepath)
{:recovered, message}
else
{:error, message}
end
err ->
Logger.error("Unknown error downloading media item ##{media_with_preloads.id}: #{inspect(err)}")
{:error, "Unknown error: #{inspect(err)}"}
end
end
defp attempt_update_media_item(media_with_preloads, output_filepath) do
with {:ok, contents} <- File.read(output_filepath),
{:ok, parsed_json} <- Phoenix.json_library().decode(contents) do
Logger.info("""
Recovery from yt-dlp error seems possible. Updating media item ##{media_with_preloads.id}
with parsed JSON from partial download attempt. Full download will be re-attemted in future
anyway
""")
update_media_item_from_parsed_json(media_with_preloads, parsed_json)
else
err ->
Logger.error("Unable to recover error for media item ##{media_with_preloads.id}: #{inspect(err)}")
{:error, :retry_failed}
end
end
defp update_media_item_from_parsed_json(media_with_preloads, parsed_json) do
parsed_attrs =
parsed_json
|> MetadataParser.parse_for_media_item()
|> Map.merge(%{
media_downloaded_at: DateTime.utc_now(),
culled_at: nil,
nfo_filepath: determine_nfo_filepath(media_with_preloads, parsed_json),
metadata: %{
# IDEA: might be worth kicking off a job for this since thumbnail fetching
# could fail and I want to handle that in isolation
metadata_filepath: MetadataFileHelpers.compress_and_store_metadata_for(media_with_preloads, parsed_json),
thumbnail_filepath: MetadataFileHelpers.download_and_store_thumbnail_for(media_with_preloads)
}
})
# Don't forgor to use preloaded associations or updates to
# associations won't work!
Media.update_media_item(media_with_preloads, parsed_attrs)
end
defp determine_nfo_filepath(media_item, parsed_json) do
if media_item.source.media_profile.download_nfo do
filepath = Path.rootname(parsed_json["filepath"]) <> ".nfo"
NfoBuilder.build_and_store_for_media_item(filepath, parsed_json)
else
nil
end
end
defp download_with_options(url, item_with_preloads, output_filepath, override_opts) do
{:ok, options} = DownloadOptionBuilder.build(item_with_preloads, override_opts)
runner_opts = [output_filepath: output_filepath, use_cookies: item_with_preloads.source.use_cookies]
YtDlpMedia.download(url, options, runner_opts)
end
defp recoverable_errors do
[
"Unable to communicate with SponsorBlock"
]
end
end