Compare commits

..

4 Commits

Author SHA1 Message Date
Kieran Eglin 3018a9c46b Bumped version 2024-04-10 14:35:45 -07:00
Kieran 314118341e [Enhancement] Allow custom yt-dlp options (#176)
* Added option for yt-dlp config file usage

* renamed yt-dlp config file

* refactored to use a precedence-based approach

* Updated README
2024-04-10 13:17:22 -07:00
Kieran Eglin b4438c1ba5 Renamed FS Helpers module to FS Utils 2024-04-10 09:02:26 -07:00
Kieran 725edc3fd8 [Enhancement] Redownload new media after a delay (#173)
* Added redownload-related columns

* Added methods for fetching re-downloadable media items

* Filled out redownload worker + tests

* Added redownload worker to config.exs cron

* Added to UI and README
2024-04-10 08:54:45 -07:00
36 changed files with 589 additions and 123 deletions
+3
View File
@@ -49,11 +49,14 @@ If it doesn't work for your use case, please make a feature request! You can als
- Supports downloading audio content
- Custom rules for handling YouTube Shorts and livestreams
- Apprise support for notifications
- Allows automatically redownloading new media after a set period
- This can help improve the download quality of new content or improve SponsorBlock tags
- Optionally automatically delete old content ([docs](https://github.com/kieraneglin/pinchflat/wiki/Automatically-Delete-Media))
- Advanced options like setting cutoff dates and filtering by title
- Reliable hands-off operation
- Can pass cookies to YouTube to download your private playlists ([docs](https://github.com/kieraneglin/pinchflat/wiki/YouTube-Cookies))
- Sponsorblock integration
- \[Advanced\] allows custom `yt-dlp` options ([docs](https://github.com/kieraneglin/pinchflat/wiki/%5BAdvanced%5D-Custom-yt%E2%80%90dlp-options))
## Screenshots
+2 -1
View File
@@ -52,7 +52,8 @@ config :pinchflat, Oban,
{Oban.Plugins.Pruner, max_age: 30 * 24 * 60 * 60},
{Oban.Plugins.Cron,
crontab: [
{"@daily", Pinchflat.Downloading.MediaRetentionWorker}
{"0 1 * * *", Pinchflat.Downloading.MediaRetentionWorker},
{"0 2 * * *", Pinchflat.Downloading.MediaRedownloadWorker}
]}
],
# TODO: consider making this an env var or something?
+12 -8
View File
@@ -14,7 +14,7 @@ defmodule Pinchflat.Boot.PreJobStartupTasks do
alias Pinchflat.Repo
alias Pinchflat.Settings
alias Pinchflat.Filesystem.FilesystemHelpers
alias Pinchflat.Utils.FilesystemUtils
def start_link(opts \\ []) do
GenServer.start_link(__MODULE__, %{}, opts)
@@ -32,7 +32,7 @@ defmodule Pinchflat.Boot.PreJobStartupTasks do
@impl true
def init(state) do
reset_executing_jobs()
create_blank_cookie_file()
create_blank_yt_dlp_files()
apply_default_settings()
{:ok, state}
@@ -50,15 +50,19 @@ defmodule Pinchflat.Boot.PreJobStartupTasks do
Logger.info("Reset #{count} executing jobs")
end
defp create_blank_cookie_file do
defp create_blank_yt_dlp_files do
files = ["cookies.txt", "yt-dlp-configs/base-config.txt"]
base_dir = Application.get_env(:pinchflat, :extras_directory)
filepath = Path.join(base_dir, "cookies.txt")
if !File.exists?(filepath) do
Logger.info("yt-dlp cookie file does not exist - creating it")
Enum.each(files, fn file ->
filepath = Path.join(base_dir, file)
FilesystemHelpers.write_p!(filepath, "")
end
if !File.exists?(filepath) do
Logger.info("Creating blank file: #{filepath}")
FilesystemUtils.write_p!(filepath, "")
end
end)
end
defp apply_default_settings do
@@ -10,8 +10,7 @@ defmodule Pinchflat.Downloading.DownloadOptionBuilder do
@doc """
Builds the options for yt-dlp to download media based on the given media's profile.
IDEA: consider adding the ability to pass in a second argument to override
these options
Returns {:ok, [Keyword.t()]}
"""
def build(%MediaItem{} = media_item_with_preloads) do
media_profile = media_item_with_preloads.source.media_profile
@@ -23,7 +22,8 @@ defmodule Pinchflat.Downloading.DownloadOptionBuilder do
metadata_options(media_profile) ++
quality_options(media_profile) ++
sponsorblock_options(media_profile) ++
output_options(media_item_with_preloads)
output_options(media_item_with_preloads) ++
config_file_options(media_item_with_preloads)
{:ok, built_options}
end
@@ -128,6 +128,35 @@ defmodule Pinchflat.Downloading.DownloadOptionBuilder do
end
end
# This is put here instead of the CommandRunner module because it should only
# be applied to downloading - if it were in CommandRunner it would apply to
# all yt-dlp commands (like indexing)
defp config_file_options(media_item) do
base_dir = Path.join(Application.get_env(:pinchflat, :extras_directory), "yt-dlp-configs")
# Ordered by priority - the first file has the highest priority
filenames = [
"media-item-#{media_item.id}-config.txt",
"source-#{media_item.source_id}-config.txt",
"media-profile-#{media_item.source.media_profile_id}-config.txt",
"base-config.txt"
]
config_filepaths =
Enum.reduce(filenames, [], fn filename, acc ->
filepath = Path.join(base_dir, filename)
case File.read(filepath) do
{:ok, file_data} ->
if String.trim(file_data) != "", do: [filepath | acc], else: acc
{:error, _} ->
acc
end
end)
Enum.map(config_filepaths, fn filepath -> {:config_locations, filepath} end)
end
defp output_options(media_item_with_preloads) do
[
output: build_output_path_for(media_item_with_preloads.source)
@@ -35,14 +35,17 @@ defmodule Pinchflat.Downloading.MediaDownloadWorker do
"""
@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)
media_item =
media_item_id
|> Media.get_media_item!()
|> Repo.preload(:source)
# 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) || args["force"] do
download_media_and_schedule_jobs(media_item)
if (media_item.source.download_media && !media_item.prevent_download) || should_force do
download_media_and_schedule_jobs(media_item, is_redownload)
else
:ok
end
@@ -51,10 +54,13 @@ 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) do
defp download_media_and_schedule_jobs(media_item, is_redownload) do
case MediaDownloader.download_for_media_item(media_item) do
{:ok, updated_media_item} ->
compute_and_save_media_filesize(updated_media_item)
Media.update_media_item(updated_media_item, %{
media_size_bytes: compute_media_filesize(updated_media_item),
media_redownloaded_at: get_redownloaded_at(is_redownload)
})
{:ok, updated_media_item}
@@ -66,13 +72,21 @@ defmodule Pinchflat.Downloading.MediaDownloadWorker do
end
end
defp compute_and_save_media_filesize(media_item) do
defp compute_media_filesize(media_item) do
case File.stat(media_item.media_filepath) do
{:ok, %{size: size}} ->
Media.update_media_item(media_item, %{media_size_bytes: size})
size
_ ->
:ok
nil
end
end
defp get_redownloaded_at(is_redownload) do
if is_redownload do
DateTime.utc_now()
else
nil
end
end
end
@@ -13,7 +13,7 @@ defmodule Pinchflat.Downloading.MediaDownloader do
alias Pinchflat.Metadata.NfoBuilder
alias Pinchflat.Metadata.MetadataParser
alias Pinchflat.Metadata.MetadataFileHelpers
alias Pinchflat.Filesystem.FilesystemHelpers
alias Pinchflat.Utils.FilesystemUtils
alias Pinchflat.Downloading.DownloadOptionBuilder
alias Pinchflat.YtDlp.Media, as: YtDlpMedia
@@ -30,7 +30,7 @@ defmodule Pinchflat.Downloading.MediaDownloader do
Returns {:ok, %MediaItem{}} | {:error, any, ...any}
"""
def download_for_media_item(%MediaItem{} = media_item) do
output_filepath = FilesystemHelpers.generate_metadata_tmpfile(:json)
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
@@ -0,0 +1,31 @@
defmodule Pinchflat.Downloading.MediaRedownloadWorker do
@moduledoc false
use Oban.Worker,
queue: :media_fetching,
unique: [period: :infinity, states: [:available, :scheduled, :retryable, :executing]],
tags: ["media_item", "media_fetching"]
require Logger
alias Pinchflat.Media
alias Pinchflat.Downloading.MediaDownloadWorker
@doc """
Redownloads media items that are eligible for redownload.
This worker is scheduled to run daily via the Oban Cron plugin
and it should run _after_ the retention worker.
Returns :ok
"""
@impl Oban.Worker
def perform(%Oban.Job{}) do
redownloadable_media = Media.list_redownloadable_media_items()
Logger.info("Redownloading #{length(redownloadable_media)} media items")
Enum.each(redownloadable_media, fn media_item ->
MediaDownloadWorker.kickoff_with_task(media_item, %{redownload?: true})
end)
end
end
+32 -8
View File
@@ -11,7 +11,7 @@ defmodule Pinchflat.Media do
alias Pinchflat.Media.MediaItem
alias Pinchflat.Media.MediaQuery
alias Pinchflat.Metadata.MediaMetadata
alias Pinchflat.Filesystem.FilesystemHelpers
alias Pinchflat.Utils.FilesystemUtils
@doc """
Returns the list of media_items.
@@ -31,15 +31,39 @@ defmodule Pinchflat.Media do
def list_cullable_media_items do
MediaQuery.new()
|> MediaQuery.with_media_filepath()
|> MediaQuery.with_passed_retention_period()
|> MediaQuery.with_no_culling_prevention()
|> MediaQuery.where_past_retention_period()
|> MediaQuery.where_culling_not_prevented()
|> Repo.all()
end
@doc """
Returns a list of media_items that are redownloadable based on the redownload delay
of the media_profile their source belongs to.
The logic is that a media_item is past_redownload_delay if the media_item's
upload_date is at least redownload_delay_days ago AND
`media_downloaded_at` - `redownload_delay_days` is before the media_item's `upload_date`.
This logic grabs media that we've recently downloaded AND is recently uploaded, but
doesn't grab media that we've recently downloaded and was uploaded a long time ago.
This also makes things work as expected when downloading media from a source for the
first time.
Returns [%MediaItem{}, ...]
"""
def list_redownloadable_media_items do
MediaQuery.new()
|> MediaQuery.with_media_downloaded_at()
|> MediaQuery.where_download_not_prevented()
|> MediaQuery.where_not_culled()
|> MediaQuery.where_media_not_redownloaded()
|> MediaQuery.where_past_redownload_delay()
|> Repo.all()
end
@doc """
Returns a list of pending media_items for a given source, where
pending means the `media_filepath` is `nil` AND the media_item
matches satisfies `MediaQuery.with_media_pending_download`. You
matches satisfies `MediaQuery.where_pending_download`. You
should really check out that function if you need to know more
because it has a lot going on.
@@ -48,7 +72,7 @@ defmodule Pinchflat.Media do
def list_pending_media_items_for(%Source{} = source) do
MediaQuery.new()
|> MediaQuery.for_source(source)
|> MediaQuery.with_media_pending_download()
|> MediaQuery.where_pending_download()
|> Repo.all()
end
@@ -66,7 +90,7 @@ defmodule Pinchflat.Media do
MediaQuery.new()
|> MediaQuery.with_id(media_item.id)
|> MediaQuery.with_media_pending_download()
|> MediaQuery.where_pending_download()
|> Repo.exists?()
end
@@ -199,7 +223,7 @@ defmodule Pinchflat.Media do
end)
|> List.flatten()
|> Enum.filter(&is_binary/1)
|> Enum.each(&FilesystemHelpers.delete_file_and_remove_empty_directories/1)
|> Enum.each(&FilesystemUtils.delete_file_and_remove_empty_directories/1)
{:ok, media_item}
end
@@ -211,6 +235,6 @@ defmodule Pinchflat.Media do
MediaMetadata.filepath_attributes()
|> Enum.map(fn field -> mapped_struct[field] end)
|> Enum.filter(&is_binary/1)
|> Enum.each(&FilesystemHelpers.delete_file_and_remove_empty_directories/1)
|> Enum.each(&FilesystemUtils.delete_file_and_remove_empty_directories/1)
end
end
+3 -1
View File
@@ -34,7 +34,8 @@ defmodule Pinchflat.Media.MediaItem do
# These are user or system controlled fields
:prevent_download,
:prevent_culling,
:culled_at
:culled_at,
:media_redownloaded_at
]
# Pretty much all the fields captured at index are required.
@required_fields ~w(
@@ -61,6 +62,7 @@ defmodule Pinchflat.Media.MediaItem do
field :livestream, :boolean, default: false
field :short_form_content, :boolean, default: false
field :media_downloaded_at, :utc_datetime
field :media_redownloaded_at, :utc_datetime
field :upload_date, :date
field :duration_seconds, :integer
+38 -12
View File
@@ -15,7 +15,7 @@ defmodule Pinchflat.Media.MediaQuery do
# Prefixes:
# - for_* - belonging to a certain record
# - join_* - for joining on a certain record
# - with_* - for filtering based on full, concrete attributes
# - with_*, where_* - for filtering based on full, concrete attributes
# - matching_* - for filtering based on partial attributes (e.g. LIKE, regex, full-text search)
#
# Suffixes:
@@ -33,24 +33,46 @@ defmodule Pinchflat.Media.MediaQuery do
from(mi in query, join: s in assoc(mi, :source), as: :sources)
end
def with_passed_retention_period(query) do
def where_past_retention_period(query) do
query
|> require_assoc(:source)
|> where(
[mi, source],
fragment(
"IFNULL(?, 0) > 0 AND DATETIME('now', '-' || ? || ' day') > ?",
source.retention_period_days,
source.retention_period_days,
mi.media_downloaded_at
)
fragment("""
IFNULL(retention_period_days, 0) > 0 AND
DATETIME('now', '-' || retention_period_days || ' day') > media_downloaded_at
""")
)
end
def with_no_culling_prevention(query) do
def where_past_redownload_delay(query) do
query
|> require_assoc(:source)
|> require_assoc(:media_profile)
|> where(
[_mi, _source, _media_profile],
# Returns media items where the upload_date is at least redownload_delay_days ago AND
# downloaded_at minus the redownload_delay_days is before the upload date
fragment("""
IFNULL(redownload_delay_days, 0) > 0 AND
DATETIME('now', '-' || redownload_delay_days || ' day') > upload_date AND
DATETIME(media_downloaded_at, '-' || redownload_delay_days || ' day') < upload_date
""")
)
end
def where_culling_not_prevented(query) do
where(query, [mi], mi.prevent_culling == false)
end
def where_not_culled(query) do
where(query, [mi], is_nil(mi.culled_at))
end
def where_media_not_redownloaded(query) do
where(query, [mi], is_nil(mi.media_redownloaded_at))
end
def with_id(query, id) do
where(query, [mi], mi.id == ^id)
end
@@ -59,6 +81,10 @@ defmodule Pinchflat.Media.MediaQuery do
where(query, [mi], mi.media_id in ^media_ids)
end
def with_media_downloaded_at(query) do
where(query, [mi], not is_nil(mi.media_downloaded_at))
end
def with_media_filepath(query) do
where(query, [mi], not is_nil(mi.media_filepath))
end
@@ -73,7 +99,7 @@ defmodule Pinchflat.Media.MediaQuery do
|> where([mi, source], is_nil(source.download_cutoff_date) or mi.upload_date >= source.download_cutoff_date)
end
def with_no_prevented_download(query) do
def where_download_not_prevented(query) do
where(query, [mi], mi.prevent_download == false)
end
@@ -129,9 +155,9 @@ defmodule Pinchflat.Media.MediaQuery do
)
end
def with_media_pending_download(query) do
def where_pending_download(query) do
query
|> with_no_prevented_download()
|> where_download_not_prevented()
|> with_no_media_filepath()
|> with_upload_date_after_source_cutoff()
|> with_format_matching_profile_preference()
@@ -9,7 +9,7 @@ defmodule Pinchflat.Metadata.MetadataFileHelpers do
needed
"""
alias Pinchflat.Filesystem.FilesystemHelpers
alias Pinchflat.Utils.FilesystemUtils
@doc """
Returns the directory where metadata for a database record should be stored.
@@ -36,7 +36,7 @@ defmodule Pinchflat.Metadata.MetadataFileHelpers do
filepath = generate_filepath_for(database_record, "metadata.json.gz")
{:ok, json} = Phoenix.json_library().encode(metadata_map)
:ok = FilesystemHelpers.write_p!(filepath, json, [:compressed])
:ok = FilesystemUtils.write_p!(filepath, json, [:compressed])
filepath
end
@@ -62,7 +62,7 @@ defmodule Pinchflat.Metadata.MetadataFileHelpers do
filepath = generate_filepath_for(database_record, Path.basename(thumbnail_url))
thumbnail_blob = fetch_thumbnail_from_url(thumbnail_url)
:ok = FilesystemHelpers.write_p!(filepath, thumbnail_blob)
:ok = FilesystemUtils.write_p!(filepath, thumbnail_blob)
filepath
end
+3 -3
View File
@@ -7,7 +7,7 @@ defmodule Pinchflat.Metadata.NfoBuilder do
import Pinchflat.Utils.XmlUtils, only: [safe: 1]
alias Pinchflat.Metadata.MetadataFileHelpers
alias Pinchflat.Filesystem.FilesystemHelpers
alias Pinchflat.Utils.FilesystemUtils
@doc """
Builds an NFO file for a media item (read: single "episode") and
@@ -18,7 +18,7 @@ defmodule Pinchflat.Metadata.NfoBuilder do
def build_and_store_for_media_item(filepath, metadata) do
nfo = build_for_media_item(metadata)
FilesystemHelpers.write_p!(filepath, nfo)
FilesystemUtils.write_p!(filepath, nfo)
filepath
end
@@ -32,7 +32,7 @@ defmodule Pinchflat.Metadata.NfoBuilder do
def build_and_store_for_source(filepath, metadata) do
nfo = build_for_source(metadata)
FilesystemHelpers.write_p!(filepath, nfo)
FilesystemUtils.write_p!(filepath, nfo)
filepath
end
@@ -2,7 +2,7 @@ defmodule Pinchflat.Metadata.SourceImageParser do
@moduledoc """
Functions for parsing and storing source images.
"""
alias Pinchflat.Filesystem.FilesystemHelpers
alias Pinchflat.Utils.FilesystemUtils
@doc """
Given a base directory and source metadata, look for the appropriate images
@@ -62,7 +62,7 @@ defmodule Pinchflat.Metadata.SourceImageParser do
extension = Path.extname(tmp_filepath)
final_filepath = Path.join([base_directory, "#{filename}#{extension}"])
FilesystemHelpers.cp_p!(tmp_filepath, final_filepath)
FilesystemUtils.cp_p!(tmp_filepath, final_filepath)
{source_attr_name, final_filepath}
end
@@ -59,7 +59,7 @@ defmodule Pinchflat.Notifications.SourceNotifications do
defp pending_media_item_count(source) do
MediaQuery.new()
|> MediaQuery.for_source(source)
|> MediaQuery.with_media_pending_download()
|> MediaQuery.where_pending_download()
|> Repo.aggregate(:count)
end
+3 -1
View File
@@ -26,12 +26,14 @@ defmodule Pinchflat.Profiles.MediaProfile do
shorts_behaviour
livestream_behaviour
preferred_resolution
redownload_delay_days
)a
@required_fields ~w(name output_path_template)a
schema "media_profiles" do
field :name, :string
field :redownload_delay_days, :integer
field :output_path_template, :string,
default: "/{{ source_custom_name }}/{{ upload_yyyy_mm_dd }} {{ title }}/{{ title }} [{{ id }}].{{ ext }}"
@@ -60,7 +62,6 @@ defmodule Pinchflat.Profiles.MediaProfile do
# See `build_format_clauses` in the Media context for more.
field :shorts_behaviour, Ecto.Enum, values: ~w(include exclude only)a, default: :include
field :livestream_behaviour, Ecto.Enum, values: ~w(include exclude only)a, default: :include
field :preferred_resolution, Ecto.Enum, values: ~w(2160p 1080p 720p 480p 360p audio)a, default: :"1080p"
has_many :sources, Source
@@ -75,6 +76,7 @@ defmodule Pinchflat.Profiles.MediaProfile do
|> validate_required(@required_fields)
# Ensures it ends with `.{{ ext }}` or `.%(ext)s` or similar (with a little wiggle room)
|> validate_format(:output_path_template, ext_regex(), message: "must end with .{{ ext }}")
|> validate_number(:redownload_delay_days, greater_than_or_equal_to: 0)
|> unique_constraint(:name)
end
+2 -2
View File
@@ -7,7 +7,7 @@ defmodule Pinchflat.Release do
require Logger
alias Pinchflat.Filesystem.FilesystemHelpers
alias Pinchflat.Utils.FilesystemUtils
def migrate do
load_app()
@@ -39,7 +39,7 @@ defmodule Pinchflat.Release do
Logger.info("Checking permissions for #{dir}")
filepath = Path.join([dir, ".keep"])
case FilesystemHelpers.write_p(filepath, "") do
case FilesystemUtils.write_p(filepath, "") do
:ok ->
Logger.info("Permissions OK")
+3 -3
View File
@@ -13,7 +13,7 @@ defmodule Pinchflat.Sources do
alias Pinchflat.Profiles.MediaProfile
alias Pinchflat.YtDlp.MediaCollection
alias Pinchflat.Metadata.SourceMetadata
alias Pinchflat.Filesystem.FilesystemHelpers
alias Pinchflat.Utils.FilesystemUtils
alias Pinchflat.Downloading.DownloadingHelpers
alias Pinchflat.FastIndexing.FastIndexingWorker
alias Pinchflat.SlowIndexing.SlowIndexingHelpers
@@ -153,7 +153,7 @@ defmodule Pinchflat.Sources do
Source.filepath_attributes()
|> Enum.map(fn field -> mapped_struct[field] end)
|> Enum.filter(&is_binary/1)
|> Enum.each(&FilesystemHelpers.delete_file_and_remove_empty_directories/1)
|> Enum.each(&FilesystemUtils.delete_file_and_remove_empty_directories/1)
end
defp delete_internal_metadata_files(source) do
@@ -163,7 +163,7 @@ defmodule Pinchflat.Sources do
SourceMetadata.filepath_attributes()
|> Enum.map(fn field -> mapped_struct[field] end)
|> Enum.filter(&is_binary/1)
|> Enum.each(&FilesystemHelpers.delete_file_and_remove_empty_directories/1)
|> Enum.each(&FilesystemUtils.delete_file_and_remove_empty_directories/1)
end
defp add_source_details_to_changeset(source, changeset) do
@@ -1,4 +1,4 @@
defmodule Pinchflat.Filesystem.FilesystemHelpers do
defmodule Pinchflat.Utils.FilesystemUtils do
@moduledoc """
Utility methods for working with the filesystem
"""
+22 -13
View File
@@ -7,7 +7,7 @@ defmodule Pinchflat.YtDlp.CommandRunner do
alias Pinchflat.Utils.CliUtils
alias Pinchflat.YtDlp.YtDlpCommandRunner
alias Pinchflat.Filesystem.FilesystemHelpers, as: FSUtils
alias Pinchflat.Utils.FilesystemUtils, as: FSUtils
@behaviour YtDlpCommandRunner
@@ -27,13 +27,14 @@ defmodule Pinchflat.YtDlp.CommandRunner do
def run(url, command_opts, output_template, addl_opts \\ []) do
# This approach lets us mock the command for testing
command = backend_executable()
# These must stay in exactly this order, hence why I'm giving it its own variable.
# Also, can't use RAM file since yt-dlp needs a concrete filepath.
output_filepath = generate_output_filepath(addl_opts)
print_to_file_opts = [{:print_to_file, output_template}, output_filepath]
cookie_opts = build_cookie_options()
formatted_command_opts = [url] ++ CliUtils.parse_options(command_opts ++ print_to_file_opts ++ cookie_opts)
external_file_opts = build_external_file_options()
# These must stay in exactly this order, hence why I'm giving it its own variable.
all_opts = command_opts ++ print_to_file_opts ++ external_file_opts
formatted_command_opts = [url] ++ CliUtils.parse_options(all_opts)
Logger.info("[yt-dlp] called with: #{Enum.join(formatted_command_opts, " ")}")
case System.cmd(command, formatted_command_opts, stderr_to_stdout: true) do
@@ -73,17 +74,25 @@ defmodule Pinchflat.YtDlp.CommandRunner do
end
end
defp build_cookie_options do
defp build_external_file_options do
base_dir = Application.get_env(:pinchflat, :extras_directory)
cookie_file = Path.join(base_dir, "cookies.txt")
filename_options_map = %{cookies: "cookies.txt"}
case File.read(cookie_file) do
{:ok, cookie_data} ->
if String.trim(cookie_data) != "", do: [cookies: cookie_file], else: []
Enum.reduce(filename_options_map, [], fn {opt_name, filename}, acc ->
filepath = Path.join(base_dir, filename)
{:error, _} ->
[]
end
case File.read(filepath) do
{:ok, file_data} ->
if String.trim(file_data) != "" do
[{opt_name, filepath} | acc]
else
acc
end
{:error, _} ->
acc
end
end)
end
defp backend_executable do
+2 -2
View File
@@ -6,7 +6,7 @@ defmodule Pinchflat.YtDlp.MediaCollection do
require Logger
alias Pinchflat.Filesystem.FilesystemHelpers
alias Pinchflat.Utils.FilesystemUtils
alias Pinchflat.YtDlp.Media, as: YtDlpMedia
@doc """
@@ -26,7 +26,7 @@ defmodule Pinchflat.YtDlp.MediaCollection do
# available formats since we're just getting the media details
command_opts = [:simulate, :skip_download, :ignore_no_formats_error]
output_template = YtDlpMedia.indexing_output_template()
output_filepath = FilesystemHelpers.generate_metadata_tmpfile(:json)
output_filepath = FilesystemUtils.generate_metadata_tmpfile(:json)
file_listener_handler = Keyword.get(addl_opts, :file_listener_handler, false)
if file_listener_handler do
@@ -203,6 +203,17 @@
/>
</section>
<section x-data="{ presets: { default: null, media_center: 1, audio: null, archiving: 1 } }">
<.input
field={f[:redownload_delay_days]}
type="number"
label="Redownload Delay (days)"
min="0"
help="Delay in days until new media is redownloaded. Redownloading new media can improve its quality or SponsorBlock tags. Leave blank to not redownload"
x-init="$watch('selectedPreset', p => p && ($el.value = presets[p]))"
/>
</section>
<h3 class="mt-8 text-2xl text-black dark:text-white">
Media Center Options
</h3>
@@ -63,7 +63,7 @@ defmodule PinchflatWeb.Sources.SourceController do
pending_media =
MediaQuery.new()
|> MediaQuery.for_source(source)
|> MediaQuery.with_media_pending_download()
|> MediaQuery.where_pending_download()
|> order_by(desc: :id)
|> limit(100)
|> Repo.all()
+1 -1
View File
@@ -4,7 +4,7 @@ defmodule Pinchflat.MixProject do
def project do
[
app: :pinchflat,
version: "0.1.10",
version: "0.1.11",
elixir: "~> 1.16",
elixirc_paths: elixirc_paths(Mix.env()),
start_permanent: Mix.env() == :prod,
@@ -0,0 +1,13 @@
defmodule Pinchflat.Repo.Migrations.AddRedownloadedFields do
use Ecto.Migration
def change do
alter table(:media_profiles) do
add :redownload_delay_days, :integer
end
alter table(:media_items) do
add :media_redownloaded_at, :utc_datetime
end
end
end
@@ -27,7 +27,7 @@ defmodule Pinchflat.Boot.PreJobStartupTasksTest do
end
end
describe "create_blank_cookie_file" do
describe "create_blank_yt_dlp_files" do
test "creates a blank cookie file" do
base_dir = Application.get_env(:pinchflat, :extras_directory)
filepath = Path.join(base_dir, "cookies.txt")
@@ -39,6 +39,18 @@ defmodule Pinchflat.Boot.PreJobStartupTasksTest do
assert File.exists?(filepath)
end
test "creates a blank yt-dlp config file" do
base_dir = Application.get_env(:pinchflat, :extras_directory)
filepath = Path.join([base_dir, "yt-dlp-configs", "base-config.txt"])
File.rm(filepath)
refute File.exists?(filepath)
PreJobStartupTasks.init(%{})
assert File.exists?(filepath)
end
end
describe "apply_default_settings" do
@@ -5,6 +5,7 @@ defmodule Pinchflat.Downloading.DownloadOptionBuilderTest do
import Pinchflat.ProfilesFixtures
alias Pinchflat.Profiles
alias Pinchflat.Utils.FilesystemUtils
alias Pinchflat.Downloading.DownloadOptionBuilder
setup do
@@ -261,6 +262,96 @@ defmodule Pinchflat.Downloading.DownloadOptionBuilderTest do
end
end
describe "build/1 when testing config file options" do
setup do
base_dir = Path.join(Application.get_env(:pinchflat, :extras_directory), "yt-dlp-configs")
{:ok, %{base_dir: base_dir}}
end
test "includes base config file if it's present", %{media_item: media_item, base_dir: base_dir} do
filepath = Path.join(base_dir, "base-config.txt")
FilesystemUtils.write_p!(filepath, "base config")
assert {:ok, res} = DownloadOptionBuilder.build(media_item)
assert {:config_locations, filepath} in res
end
test "includes media profile config file if it's present", %{media_item: media_item, base_dir: base_dir} do
media_profile = media_item.source.media_profile
filepath = Path.join(base_dir, "media-profile-#{media_profile.id}-config.txt")
FilesystemUtils.write_p!(filepath, "profile config")
assert {:ok, res} = DownloadOptionBuilder.build(media_item)
assert {:config_locations, filepath} in res
end
test "includes source config file if it's present", %{media_item: media_item, base_dir: base_dir} do
source = media_item.source
filepath = Path.join(base_dir, "source-#{source.id}-config.txt")
FilesystemUtils.write_p!(filepath, "profile config")
assert {:ok, res} = DownloadOptionBuilder.build(media_item)
assert {:config_locations, filepath} in res
end
test "includes media item config file if it's present", %{media_item: media_item, base_dir: base_dir} do
filepath = Path.join(base_dir, "media-item-#{media_item.id}-config.txt")
FilesystemUtils.write_p!(filepath, "media item config")
assert {:ok, res} = DownloadOptionBuilder.build(media_item)
assert {:config_locations, filepath} in res
end
test "does not include config file options if they are not present", %{media_item: media_item} do
assert {:ok, res} = DownloadOptionBuilder.build(media_item)
refute :config_locations in res
end
test "does not return a config file if it's blank", %{media_item: media_item, base_dir: base_dir} do
filepath = Path.join(base_dir, "base-config.txt")
FilesystemUtils.write_p!(filepath, " \n \n ")
assert {:ok, res} = DownloadOptionBuilder.build(media_item)
refute :config_locations in res
end
test "returns config files in order of precedence", %{media_item: media_item, base_dir: base_dir} do
source = media_item.source
media_profile = source.media_profile
base_filepath = Path.join(base_dir, "base-config.txt")
source_filepath = Path.join(base_dir, "source-#{source.id}-config.txt")
media_item_filepath = Path.join(base_dir, "media-item-#{media_item.id}-config.txt")
media_profile_filepath = Path.join(base_dir, "media-profile-#{media_profile.id}-config.txt")
FilesystemUtils.write_p!(base_filepath, "config")
FilesystemUtils.write_p!(source_filepath, "config")
FilesystemUtils.write_p!(media_item_filepath, "config")
FilesystemUtils.write_p!(media_profile_filepath, "config")
assert {:ok, res} = DownloadOptionBuilder.build(media_item)
expected_order = [
{:config_locations, base_filepath},
{:config_locations, media_profile_filepath},
{:config_locations, source_filepath},
{:config_locations, media_item_filepath}
]
assert Enum.filter(res, fn
{:config_locations, _} -> true
_ -> false
end) == expected_order
end
end
describe "build_output_path_for/1" do
test "builds an output path for a source", %{media_item: media_item} do
path = DownloadOptionBuilder.build_output_path_for(media_item.source)
@@ -6,7 +6,7 @@ defmodule Pinchflat.Downloading.MediaDownloadWorkerTest do
alias Pinchflat.Media
alias Pinchflat.Sources
alias Pinchflat.Filesystem.FilesystemHelpers
alias Pinchflat.Utils.FilesystemUtils
alias Pinchflat.Downloading.MediaDownloadWorker
setup :verify_on_exit!
@@ -62,7 +62,9 @@ defmodule Pinchflat.Downloading.MediaDownloadWorkerTest do
assert media_item.media_filepath == nil
perform_job(MediaDownloadWorker, %{id: media_item.id})
assert Repo.reload(media_item).media_filepath != nil
media_item = Repo.reload(media_item)
assert media_item.media_filepath != nil
end
test "it saves the metadata to the media_item", %{media_item: media_item} do
@@ -138,7 +140,7 @@ defmodule Pinchflat.Downloading.MediaDownloadWorkerTest do
test "it saves the file's size to the database", %{media_item: media_item} do
expect(YtDlpRunnerMock, :run, fn _url, _opts, _ot, _addl ->
metadata = render_parsed_metadata(:media_metadata)
FilesystemHelpers.write_p!(metadata["filepath"], "test")
FilesystemUtils.write_p!(metadata["filepath"], "test")
{:ok, Phoenix.json_library().encode!(metadata)}
end)
@@ -149,6 +151,28 @@ defmodule Pinchflat.Downloading.MediaDownloadWorkerTest do
assert media_item.media_size_bytes > 0
end
test "saves redownloaded_at if this is for a redownload", %{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 ->
{:ok, render_metadata(:media_metadata)}
end)
perform_job(MediaDownloadWorker, %{id: media_item.id})
media_item = Repo.reload(media_item)
assert media_item.media_redownloaded_at == nil
end
test "does not blow up if the record doesn't exist" do
assert :ok = perform_job(MediaDownloadWorker, %{id: 0})
end
@@ -0,0 +1,44 @@
defmodule Pinchflat.Downloading.MediaRedownloadWorkerTest do
use Pinchflat.DataCase
import Pinchflat.MediaFixtures
import Pinchflat.SourcesFixtures
import Pinchflat.ProfilesFixtures
alias Pinchflat.Downloading.MediaDownloadWorker
alias Pinchflat.Downloading.MediaRedownloadWorker
describe "perform/1" do
test "kicks off a task for redownloadable media items" do
media_profile = media_profile_fixture(%{redownload_delay_days: 4})
source = source_fixture(%{media_profile_id: media_profile.id, inserted_at: now_minus(10, :days)})
media_item =
media_item_fixture(%{
source_id: source.id,
upload_date: now_minus(6, :days),
media_downloaded_at: now_minus(5, :days)
})
perform_job(MediaRedownloadWorker, %{})
assert [_] = all_enqueued(worker: MediaDownloadWorker, args: %{id: media_item.id, redownload?: true})
end
test "does not kickoff a task for non-redownloadable media items" do
media_profile = media_profile_fixture(%{redownload_delay_days: 4})
source = source_fixture(%{media_profile_id: media_profile.id, inserted_at: now_minus(10, :days)})
_media_item =
media_item_fixture(%{
source_id: source.id,
upload_date: now_minus(6, :days),
media_downloaded_at: now_minus(1, :day)
})
perform_job(MediaRedownloadWorker, %{})
assert [] = all_enqueued(worker: MediaDownloadWorker)
end
end
end
+125
View File
@@ -130,6 +130,131 @@ defmodule Pinchflat.MediaTest do
end
end
describe "list_redownloadable_media_items/0" do
setup do
media_profile = media_profile_fixture(%{redownload_delay_days: 4})
source = source_fixture(%{media_profile_id: media_profile.id, inserted_at: now_minus(10, :days)})
{:ok, %{media_profile: media_profile, source: source}}
end
test "returns media eligible for redownload", %{source: source} do
media_item =
media_item_fixture(%{
source_id: source.id,
upload_date: now_minus(6, :days),
media_downloaded_at: now_minus(5, :days)
})
assert Media.list_redownloadable_media_items() == [media_item]
end
test "returns media items that were downloaded in past but still meet redownload delay", %{source: source} do
media_item =
media_item_fixture(%{
source_id: source.id,
upload_date: now_minus(20, :days),
media_downloaded_at: now_minus(19, :days)
})
assert Media.list_redownloadable_media_items() == [media_item]
end
test "does not return media items without a media_downloaded_at", %{source: source} do
_media_item =
media_item_fixture(%{
source_id: source.id,
upload_date: now_minus(5, :days),
media_downloaded_at: nil
})
assert Media.list_redownloadable_media_items() == []
end
test "does not return media items that are set to prevent download", %{source: source} do
_media_item =
media_item_fixture(%{
source_id: source.id,
upload_date: now_minus(5, :days),
media_downloaded_at: now(),
prevent_download: true
})
assert Media.list_redownloadable_media_items() == []
end
test "does not return media items that have been culled", %{source: source} do
_media_item =
media_item_fixture(%{
source_id: source.id,
upload_date: now_minus(5, :days),
media_downloaded_at: now(),
culled_at: now()
})
assert Media.list_redownloadable_media_items() == []
end
test "does not return media items before the download delay", %{source: source} do
_media_item =
media_item_fixture(%{
source_id: source.id,
upload_date: now_minus(3, :days),
media_downloaded_at: now_minus(3, :days)
})
assert Media.list_redownloadable_media_items() == []
end
test "does not return media items that have already been redownloaded", %{source: source} do
_media_item =
media_item_fixture(%{
source_id: source.id,
upload_date: now_minus(5, :days),
media_downloaded_at: now(),
media_redownloaded_at: now()
})
assert Media.list_redownloadable_media_items() == []
end
test "does not return media items that were first downloaded well after the upload_date", %{source: source} do
_media_item =
media_item_fixture(%{
source_id: source.id,
media_downloaded_at: now(),
upload_date: now_minus(20, :days)
})
assert Media.list_redownloadable_media_items() == []
end
test "does not return media items that were recently uploaded", %{source: source} do
_media_item =
media_item_fixture(%{
source_id: source.id,
media_downloaded_at: now(),
upload_date: now_minus(2, :days)
})
assert Media.list_redownloadable_media_items() == []
end
test "does not return media items without a redownload delay" do
media_profile = media_profile_fixture(%{redownload_delay_days: nil})
source = source_fixture(%{media_profile_id: media_profile.id})
_media_item =
media_item_fixture(%{
source_id: source.id,
upload_date: now_minus(6, :days),
media_downloaded_at: now_minus(5, :days)
})
assert Media.list_redownloadable_media_items() == []
end
end
describe "list_pending_media_items_for/1" do
test "it returns pending without a filepath for a given source" do
source = source_fixture()
+2 -2
View File
@@ -2,10 +2,10 @@ defmodule Pinchflat.Metadata.NfoBuilderTest do
use Pinchflat.DataCase
alias Pinchflat.Metadata.NfoBuilder
alias Pinchflat.Filesystem.FilesystemHelpers
alias Pinchflat.Utils.FilesystemUtils
setup do
filepath = FilesystemHelpers.generate_metadata_tmpfile(:json)
filepath = FilesystemUtils.generate_metadata_tmpfile(:json)
on_exit(fn -> File.rm!(filepath) end)
@@ -1,12 +1,12 @@
defmodule Pinchflat.SlowIndexing.FileFollowerServerTest do
use ExUnit.Case, async: true
alias alias Pinchflat.Filesystem.FilesystemHelpers
alias alias Pinchflat.Utils.FilesystemUtils
alias Pinchflat.SlowIndexing.FileFollowerServer
setup do
{:ok, pid} = FileFollowerServer.start_link()
tmpfile = FilesystemHelpers.generate_metadata_tmpfile(:txt)
tmpfile = FilesystemUtils.generate_metadata_tmpfile(:txt)
{:ok, %{pid: pid, tmpfile: tmpfile}}
end
+3 -3
View File
@@ -8,7 +8,7 @@ defmodule Pinchflat.SourcesTest do
alias Pinchflat.Sources
alias Pinchflat.Sources.Source
alias Pinchflat.Filesystem.FilesystemHelpers
alias Pinchflat.Utils.FilesystemUtils
alias Pinchflat.Metadata.MetadataFileHelpers
alias Pinchflat.Downloading.DownloadingHelpers
alias Pinchflat.FastIndexing.FastIndexingWorker
@@ -561,7 +561,7 @@ defmodule Pinchflat.SourcesTest do
end
test "does not delete the source's non-metadata files" do
filepath = FilesystemHelpers.generate_metadata_tmpfile(:nfo)
filepath = FilesystemUtils.generate_metadata_tmpfile(:nfo)
source = source_fixture(%{nfo_filepath: filepath})
assert {:ok, _} = Sources.delete_source(source)
@@ -592,7 +592,7 @@ defmodule Pinchflat.SourcesTest do
end
test "deletes the source's non-metadata files" do
filepath = FilesystemHelpers.generate_metadata_tmpfile(:nfo)
filepath = FilesystemUtils.generate_metadata_tmpfile(:nfo)
source = source_fixture(%{nfo_filepath: filepath})
assert {:ok, _} = Sources.delete_source(source, delete_files: true)
@@ -1,13 +1,13 @@
defmodule Pinchflat.Filesystem.FilesystemHelpersTest do
defmodule Pinchflat.Utils.FilesystemUtilsTest do
use Pinchflat.DataCase
import Pinchflat.MediaFixtures
alias Pinchflat.Filesystem.FilesystemHelpers
alias Pinchflat.Utils.FilesystemUtils
describe "generate_metadata_tmpfile/1" do
test "creates a tmpfile and returns its path" do
res = FilesystemHelpers.generate_metadata_tmpfile(:json)
res = FilesystemUtils.generate_metadata_tmpfile(:json)
assert String.ends_with?(res, ".json")
assert File.exists?(res)
@@ -22,7 +22,7 @@ defmodule Pinchflat.Filesystem.FilesystemHelpersTest do
refute media_item.media_size_bytes
assert {:ok, media_item} = FilesystemHelpers.compute_and_save_media_filesize(media_item)
assert {:ok, media_item} = FilesystemUtils.compute_and_save_media_filesize(media_item)
assert Repo.reload!(media_item).media_size_bytes
end
@@ -30,16 +30,16 @@ defmodule Pinchflat.Filesystem.FilesystemHelpersTest do
test "returns the error if operation fails" do
media_item = media_item_fixture(%{media_filepath: "/nonexistent/file.mkv"})
assert {:error, _} = FilesystemHelpers.compute_and_save_media_filesize(media_item)
assert {:error, _} = FilesystemUtils.compute_and_save_media_filesize(media_item)
end
end
describe "write_p/3" do
test "writes content to a file" do
filepath = FilesystemHelpers.generate_metadata_tmpfile(:json)
filepath = FilesystemUtils.generate_metadata_tmpfile(:json)
content = "{}"
assert :ok = FilesystemHelpers.write_p(filepath, content)
assert :ok = FilesystemUtils.write_p(filepath, content)
assert File.read!(filepath) == content
File.rm!(filepath)
@@ -50,7 +50,7 @@ defmodule Pinchflat.Filesystem.FilesystemHelpersTest do
filepath = Path.join([tmpfile_directory, "foo", "bar", "file.json"])
content = "{}"
assert :ok = FilesystemHelpers.write_p(filepath, content)
assert :ok = FilesystemUtils.write_p(filepath, content)
assert File.read!(filepath) == content
File.rm!(filepath)
@@ -59,10 +59,10 @@ defmodule Pinchflat.Filesystem.FilesystemHelpersTest do
describe "write_p!/3" do
test "writes content to a file" do
filepath = FilesystemHelpers.generate_metadata_tmpfile(:json)
filepath = FilesystemUtils.generate_metadata_tmpfile(:json)
content = "{}"
assert :ok = FilesystemHelpers.write_p!(filepath, content)
assert :ok = FilesystemUtils.write_p!(filepath, content)
assert File.read!(filepath) == content
File.rm!(filepath)
@@ -73,7 +73,7 @@ defmodule Pinchflat.Filesystem.FilesystemHelpersTest do
filepath = Path.join([tmpfile_directory, "foo", "bar", "file.json"])
content = "{}"
assert :ok = FilesystemHelpers.write_p!(filepath, content)
assert :ok = FilesystemUtils.write_p!(filepath, content)
assert File.read!(filepath) == content
File.rm!(filepath)
@@ -82,11 +82,11 @@ defmodule Pinchflat.Filesystem.FilesystemHelpersTest do
describe "delete_file_and_remove_empty_directories/1" do
test "deletes file at the provided filepath" do
filepath = FilesystemHelpers.generate_metadata_tmpfile(:json)
filepath = FilesystemUtils.generate_metadata_tmpfile(:json)
assert File.exists?(filepath)
assert :ok = FilesystemHelpers.delete_file_and_remove_empty_directories(filepath)
assert :ok = FilesystemUtils.delete_file_and_remove_empty_directories(filepath)
refute File.exists?(filepath)
end
@@ -94,9 +94,9 @@ defmodule Pinchflat.Filesystem.FilesystemHelpersTest do
test "deletes empty directories" do
tmpfile_directory = Application.get_env(:pinchflat, :tmpfile_directory)
filepath = Path.join([tmpfile_directory, "foo", "bar", "baz", "qux.json"])
FilesystemHelpers.write_p!(filepath, "")
FilesystemUtils.write_p!(filepath, "")
assert :ok = FilesystemHelpers.delete_file_and_remove_empty_directories(filepath)
assert :ok = FilesystemUtils.delete_file_and_remove_empty_directories(filepath)
refute File.exists?(filepath)
refute File.exists?(Path.join([tmpfile_directory, "foo", "bar", "baz"]))
@@ -108,10 +108,10 @@ defmodule Pinchflat.Filesystem.FilesystemHelpersTest do
tmpfile_directory = Application.get_env(:pinchflat, :tmpfile_directory)
filepath_1 = Path.join([tmpfile_directory, "foo", "bar", "baz", "qux.json"])
filepath_2 = Path.join([tmpfile_directory, "foo", "baz.json"])
FilesystemHelpers.write_p!(filepath_1, "")
FilesystemHelpers.write_p!(filepath_2, "")
FilesystemUtils.write_p!(filepath_1, "")
FilesystemUtils.write_p!(filepath_2, "")
assert :ok = FilesystemHelpers.delete_file_and_remove_empty_directories(filepath_1)
assert :ok = FilesystemUtils.delete_file_and_remove_empty_directories(filepath_1)
refute File.exists?(filepath_1)
refute File.exists?(Path.join([tmpfile_directory, "foo", "bar", "baz"]))
@@ -121,24 +121,24 @@ defmodule Pinchflat.Filesystem.FilesystemHelpersTest do
assert File.exists?(Path.join([tmpfile_directory, "foo"]))
# cleanup
FilesystemHelpers.delete_file_and_remove_empty_directories(filepath_2)
FilesystemUtils.delete_file_and_remove_empty_directories(filepath_2)
end
test "returns an error if file could not be deleted" do
filepath = "/nonexistent/file.json"
assert {:error, _} = FilesystemHelpers.delete_file_and_remove_empty_directories(filepath)
assert {:error, _} = FilesystemUtils.delete_file_and_remove_empty_directories(filepath)
end
end
describe "cp_p!/2" do
test "copies a file from source to destination" do
source = "#{tmpfile_directory()}/source.json"
FilesystemHelpers.write_p!(source, "TEST")
FilesystemUtils.write_p!(source, "TEST")
destination = "#{tmpfile_directory()}/destination.json"
refute File.exists?(destination)
FilesystemHelpers.cp_p!(source, destination)
FilesystemUtils.cp_p!(source, destination)
assert File.exists?(destination)
assert File.read!(destination) == "TEST"
@@ -148,11 +148,11 @@ defmodule Pinchflat.Filesystem.FilesystemHelpersTest do
test "creates directories as needed" do
source = "#{tmpfile_directory()}/source.json"
FilesystemHelpers.write_p!(source, "TEST")
FilesystemUtils.write_p!(source, "TEST")
destination = "#{tmpfile_directory()}/foo/bar/destination.json"
refute File.exists?(destination)
FilesystemHelpers.cp_p!(source, destination)
FilesystemUtils.cp_p!(source, destination)
assert File.exists?(destination)
File.rm!(source)
@@ -1,7 +1,7 @@
defmodule Pinchflat.YtDlp.CommandRunnerTest do
use ExUnit.Case, async: true
alias Pinchflat.Filesystem.FilesystemHelpers
alias Pinchflat.Utils.FilesystemUtils
alias Pinchflat.YtDlp.CommandRunner, as: Runner
@@ -42,16 +42,17 @@ defmodule Pinchflat.YtDlp.CommandRunnerTest do
end
end
describe "run/4 when testing cookie options" do
describe "run/4 when testing external file options" do
setup do
base_dir = Application.get_env(:pinchflat, :extras_directory)
cookie_file = Path.join(base_dir, "cookies.txt")
yt_dlp_file = Path.join([base_dir, "yt-dlp-configs", "main.txt"])
{:ok, cookie_file: cookie_file}
{:ok, cookie_file: cookie_file, yt_dlp_file: yt_dlp_file}
end
test "includes cookie options when cookies.txt exists", %{cookie_file: cookie_file} do
FilesystemHelpers.write_p!(cookie_file, "cookie data")
FilesystemUtils.write_p!(cookie_file, "cookie data")
assert {:ok, output} = Runner.run(@media_url, [], "")
@@ -59,7 +60,7 @@ defmodule Pinchflat.YtDlp.CommandRunnerTest do
end
test "doesn't include cookie options when cookies.txt blank", %{cookie_file: cookie_file} do
FilesystemHelpers.write_p!(cookie_file, " \n \n ")
FilesystemUtils.write_p!(cookie_file, " \n \n ")
assert {:ok, output} = Runner.run(@media_url, [], "")
+4 -4
View File
@@ -5,7 +5,7 @@ defmodule Pinchflat.MediaFixtures do
"""
alias Pinchflat.SourcesFixtures
alias Pinchflat.Filesystem.FilesystemHelpers
alias Pinchflat.Utils.FilesystemUtils
@doc """
Generate a media_item.
@@ -52,8 +52,8 @@ defmodule Pinchflat.MediaFixtures do
json_gz_filepath = Path.join(metadata_dir, "metadata.json.gz")
thumbnail_filepath = Path.join(metadata_dir, "thumbnail.jpg")
FilesystemHelpers.cp_p!(media_metadata_filepath_fixture(), json_gz_filepath)
FilesystemHelpers.cp_p!(thumbnail_filepath_fixture(), thumbnail_filepath)
FilesystemUtils.cp_p!(media_metadata_filepath_fixture(), json_gz_filepath)
FilesystemUtils.cp_p!(thumbnail_filepath_fixture(), thumbnail_filepath)
merged_attrs =
Map.merge(attrs, %{
@@ -74,7 +74,7 @@ defmodule Pinchflat.MediaFixtures do
"#{:rand.uniform(1_000_000)}_media.mp4"
])
FilesystemHelpers.cp_p!(media_filepath_fixture(), stored_media_filepath)
FilesystemUtils.cp_p!(media_filepath_fixture(), stored_media_filepath)
merged_attrs = Map.merge(attrs, %{media_filepath: stored_media_filepath})
media_item_fixture(merged_attrs)
+4 -4
View File
@@ -8,7 +8,7 @@ defmodule Pinchflat.SourcesFixtures do
alias Pinchflat.MediaFixtures
alias Pinchflat.Sources.Source
alias Pinchflat.ProfilesFixtures
alias Pinchflat.Filesystem.FilesystemHelpers
alias Pinchflat.Utils.FilesystemUtils
@doc """
Generate a source.
@@ -59,9 +59,9 @@ defmodule Pinchflat.SourcesFixtures do
poster_filepath = Path.join(metadata_dir, "poster.jpg")
fanart_filepath = Path.join(metadata_dir, "fanart.jpg")
FilesystemHelpers.cp_p!(MediaFixtures.media_metadata_filepath_fixture(), json_gz_filepath)
FilesystemHelpers.cp_p!(MediaFixtures.thumbnail_filepath_fixture(), poster_filepath)
FilesystemHelpers.cp_p!(MediaFixtures.thumbnail_filepath_fixture(), fanart_filepath)
FilesystemUtils.cp_p!(MediaFixtures.media_metadata_filepath_fixture(), json_gz_filepath)
FilesystemUtils.cp_p!(MediaFixtures.thumbnail_filepath_fixture(), poster_filepath)
FilesystemUtils.cp_p!(MediaFixtures.thumbnail_filepath_fixture(), fanart_filepath)
merged_attrs =
Map.merge(attrs, %{