Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 20547e60e4 | |||
| 0fd3106cb8 | |||
| 108a141c65 | |||
| 6a1b7b0160 | |||
| 3c8d99196a | |||
| cae86953a0 | |||
| f661747a0c |
@@ -131,9 +131,6 @@ You _must_ ensure the host directories you've mounted are writable by the user r
|
|||||||
> [!IMPORTANT]
|
> [!IMPORTANT]
|
||||||
> It's not recommended to run the container as root. Doing so can create permission issues if other apps need to work with the downloaded media.
|
> It's not recommended to run the container as root. Doing so can create permission issues if other apps need to work with the downloaded media.
|
||||||
|
|
||||||
> [!TIP]
|
|
||||||
> If you need to run any command as root, you can run `su` from the container's shell as there is no password set for the root user.
|
|
||||||
|
|
||||||
### ADVANCED: Storing Pinchflat config directory on a network share
|
### ADVANCED: Storing Pinchflat config directory on a network share
|
||||||
|
|
||||||
As pointed out in [#137](https://github.com/kieraneglin/pinchflat/issues/137), SQLite doesn't like being run in WAL mode on network shares. If you're running Pinchflat on a network share, you can disable WAL mode by setting the `JOURNAL_MODE` environment variable to `delete`. This will make Pinchflat run in rollback journal mode which is less performant but should work on network shares.
|
As pointed out in [#137](https://github.com/kieraneglin/pinchflat/issues/137), SQLite doesn't like being run in WAL mode on network shares. If you're running Pinchflat on a network share, you can disable WAL mode by setting the `JOURNAL_MODE` environment variable to `delete`. This will make Pinchflat run in rollback journal mode which is less performant but should work on network shares.
|
||||||
@@ -155,7 +152,7 @@ If you change this setting and it works well for you, please leave a comment on
|
|||||||
| `ENABLE_IPV6` | No | `false` | Setting to _any_ non-blank value will enable IPv6 |
|
| `ENABLE_IPV6` | No | `false` | Setting to _any_ non-blank value will enable IPv6 |
|
||||||
| `JOURNAL_MODE` | No | `wal` | Set to `delete` if your config directory is stored on a network share (not recommended) |
|
| `JOURNAL_MODE` | No | `wal` | Set to `delete` if your config directory is stored on a network share (not recommended) |
|
||||||
| `TZ_DATA_DIR` | No | `/etc/elixir_tzdata_data` | The container path where the timezone database is stored |
|
| `TZ_DATA_DIR` | No | `/etc/elixir_tzdata_data` | The container path where the timezone database is stored |
|
||||||
| `BASE_ROUTE_PATH` | No | `/` | The base path for route generation. Useful when running behind certain reverse proxies |
|
| `BASE_ROUTE_PATH` | No | `/` | The base path for route generation. Useful when running behind certain reverse proxies, but prefix must be stripped. |
|
||||||
| `YT_DLP_WORKER_CONCURRENCY` | No | `2` | The number of concurrent workers that use `yt-dlp` _per queue_. Set to 1 if you're getting IP limited, otherwise don't touch it |
|
| `YT_DLP_WORKER_CONCURRENCY` | No | `2` | The number of concurrent workers that use `yt-dlp` _per queue_. Set to 1 if you're getting IP limited, otherwise don't touch it |
|
||||||
|
|
||||||
## EFF donations
|
## EFF donations
|
||||||
|
|||||||
@@ -127,17 +127,6 @@ EXPOSE ${PORT}
|
|||||||
# Only copy the final release from the build stage
|
# Only copy the final release from the build stage
|
||||||
COPY --from=builder /app/_build/${MIX_ENV}/rel/pinchflat ./
|
COPY --from=builder /app/_build/${MIX_ENV}/rel/pinchflat ./
|
||||||
|
|
||||||
# NEVER do this if you're running in an environment where you don't trust the user
|
|
||||||
# (ie: most environments). This is only acceptable in a self-hosted environment.
|
|
||||||
# The user could just run the whole container as root and bypass this anyway so
|
|
||||||
# it's not a huge deal.
|
|
||||||
# This removes the root password to allow users to assume root if needed. This is
|
|
||||||
# preferrable to running the whole container as root so that the files/directories
|
|
||||||
# created by the app aren't owned by root and are therefore easier for other users
|
|
||||||
# and processes to interact with. If you want to just run the whole container as
|
|
||||||
# root, use --user 0:0 or something.
|
|
||||||
RUN passwd -d root
|
|
||||||
|
|
||||||
HEALTHCHECK --interval=120s --start-period=10s \
|
HEALTHCHECK --interval=120s --start-period=10s \
|
||||||
CMD curl --fail http://localhost:${PORT}/healthcheck || exit 1
|
CMD curl --fail http://localhost:${PORT}/healthcheck || exit 1
|
||||||
|
|
||||||
|
|||||||
@@ -49,6 +49,8 @@ defmodule Pinchflat.Downloading.MediaRetentionWorker do
|
|||||||
end)
|
end)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# NOTE: Since this is a date and not a datetime, we can't add logic to have to-the-minute
|
||||||
|
# comparison like we can with retention periods. We can only compare to the day.
|
||||||
defp delete_media_items_from_before_cutoff do
|
defp delete_media_items_from_before_cutoff do
|
||||||
deletable_media =
|
deletable_media =
|
||||||
MediaQuery.new()
|
MediaQuery.new()
|
||||||
|
|||||||
@@ -88,7 +88,7 @@ defmodule Pinchflat.Media.MediaQuery do
|
|||||||
[mi, source],
|
[mi, source],
|
||||||
fragment("""
|
fragment("""
|
||||||
IFNULL(retention_period_days, 0) > 0 AND
|
IFNULL(retention_period_days, 0) > 0 AND
|
||||||
DATETIME('now', '-' || retention_period_days || ' day') > media_downloaded_at
|
DATETIME(media_downloaded_at, '+' || retention_period_days || ' day') < DATETIME('now')
|
||||||
""")
|
""")
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
@@ -100,8 +100,8 @@ defmodule Pinchflat.Media.MediaQuery do
|
|||||||
# downloaded_at minus the redownload_delay_days is before the upload date
|
# downloaded_at minus the redownload_delay_days is before the upload date
|
||||||
fragment("""
|
fragment("""
|
||||||
IFNULL(redownload_delay_days, 0) > 0 AND
|
IFNULL(redownload_delay_days, 0) > 0 AND
|
||||||
DATETIME('now', '-' || redownload_delay_days || ' day') > uploaded_at AND
|
DATE('now', '-' || redownload_delay_days || ' day') > DATE(uploaded_at) AND
|
||||||
DATETIME(media_downloaded_at, '-' || redownload_delay_days || ' day') < uploaded_at
|
DATE(media_downloaded_at, '-' || redownload_delay_days || ' day') < DATE(uploaded_at)
|
||||||
""")
|
""")
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -186,7 +186,13 @@ defmodule Pinchflat.Sources do
|
|||||||
{:ok, source_details} ->
|
{:ok, source_details} ->
|
||||||
add_source_details_by_collection_type(source, changeset, source_details)
|
add_source_details_by_collection_type(source, changeset, source_details)
|
||||||
|
|
||||||
{:error, runner_error, _status_code} ->
|
err ->
|
||||||
|
runner_error =
|
||||||
|
case err do
|
||||||
|
{:error, error_msg, _status_code} -> error_msg
|
||||||
|
{:error, error_msg} -> error_msg
|
||||||
|
end
|
||||||
|
|
||||||
Ecto.Changeset.add_error(
|
Ecto.Changeset.add_error(
|
||||||
changeset,
|
changeset,
|
||||||
:original_url,
|
:original_url,
|
||||||
|
|||||||
@@ -84,6 +84,7 @@ defmodule Pinchflat.YtDlp.MediaCollection do
|
|||||||
{:ok, parsed_json} <- Phoenix.json_library().decode(output) do
|
{:ok, parsed_json} <- Phoenix.json_library().decode(output) do
|
||||||
{:ok, format_source_details(parsed_json)}
|
{:ok, format_source_details(parsed_json)}
|
||||||
else
|
else
|
||||||
|
{:error, %Jason.DecodeError{}} -> {:error, "Error decoding JSON response"}
|
||||||
err -> err
|
err -> err
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -62,17 +62,24 @@ defmodule PinchflatWeb.CustomComponents.TextComponents do
|
|||||||
end
|
end
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
Renders a block of text with each line broken into a separate span.
|
Renders a block of text with each line broken into a separate span and links highlighted.
|
||||||
"""
|
"""
|
||||||
attr :text, :string, required: true
|
attr :text, :string, required: true
|
||||||
|
|
||||||
def break_on_newline(assigns) do
|
def render_description(assigns) do
|
||||||
broken_text =
|
formatted_text =
|
||||||
assigns.text
|
Regex.split(~r{https?://\S+}, assigns.text, include_captures: true)
|
||||||
|
|> Enum.map(fn
|
||||||
|
"http" <> _ = url ->
|
||||||
|
Phoenix.HTML.Tag.content_tag(:a, url, class: "text-blue-500 hover:text-blue-300", href: url, target: "_blank")
|
||||||
|
|
||||||
|
text ->
|
||||||
|
text
|
||||||
|> String.split("\n", trim: false)
|
|> String.split("\n", trim: false)
|
||||||
|> Enum.intersperse(Phoenix.HTML.Tag.tag(:span, class: "inline-block mt-2"))
|
|> Enum.intersperse(Phoenix.HTML.Tag.tag(:span, class: "inline-block mt-2"))
|
||||||
|
end)
|
||||||
|
|
||||||
assigns = Map.put(assigns, :text, broken_text)
|
assigns = Map.put(assigns, :text, formatted_text)
|
||||||
|
|
||||||
~H"""
|
~H"""
|
||||||
<span><%= @text %></span>
|
<span><%= @text %></span>
|
||||||
|
|||||||
@@ -64,7 +64,7 @@ defmodule PinchflatWeb.Layouts do
|
|||||||
<.icon name={@icon} /> <%= @text %>
|
<.icon name={@icon} /> <%= @text %>
|
||||||
</span>
|
</span>
|
||||||
<span class="text-bodydark2">
|
<span class="text-bodydark2">
|
||||||
<.icon name="hero-chevron-up" x-bind:class="{ 'rotate-180': selected }" />
|
<.icon name="hero-chevron-down" x-bind:class="{ 'rotate-180': selected }" />
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -2,7 +2,7 @@
|
|||||||
<:option>
|
<:option>
|
||||||
<span x-data="{ copied: false }" x-on:click={~s"
|
<span x-data="{ copied: false }" x-on:click={~s"
|
||||||
copyWithCallbacks(
|
copyWithCallbacks(
|
||||||
`#{Jason.Formatter.pretty_print(Phoenix.json_library().encode!(@media_item))}`,
|
String.raw`#{Jason.Formatter.pretty_print(Phoenix.json_library().encode!(@media_item))}`,
|
||||||
() => copied = true,
|
() => copied = true,
|
||||||
() => copied = false
|
() => copied = false
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<%= if media_type(@media_item) == :video do %>
|
<%= if media_type(@media_item) == :video do %>
|
||||||
<video controls class="max-h-96 w-full">
|
<video controls class="max-h-128 w-full">
|
||||||
<source src={~p"/media/#{@media_item.uuid}/stream"} type="video/mp4" />
|
<source src={~p"/media/#{@media_item.uuid}/stream"} type="video/mp4" />
|
||||||
Your browser does not support the video element.
|
Your browser does not support the video element.
|
||||||
</video>
|
</video>
|
||||||
|
|||||||
@@ -26,11 +26,12 @@
|
|||||||
<:tab title="Media" id="media">
|
<:tab title="Media" id="media">
|
||||||
<div class="flex flex-col gap-10 dark:text-white">
|
<div class="flex flex-col gap-10 dark:text-white">
|
||||||
<%= if media_file_exists?(@media_item) do %>
|
<%= if media_file_exists?(@media_item) do %>
|
||||||
<section class="grid grid-cols-1 xl:grid-cols-2 xl:gap-6 mt-6">
|
<section class="grid grid-cols-1 xl:gap-6 mt-6">
|
||||||
<div>
|
<div>
|
||||||
<.media_preview media_item={@media_item} />
|
<.media_preview media_item={@media_item} />
|
||||||
</div>
|
</div>
|
||||||
<aside class="mt-4 xl:mt-0">
|
<aside class="mt-4">
|
||||||
|
<h2 class="text-xl mb-2"><%= @media_item.title %></h2>
|
||||||
<div>Uploaded: <%= DateTime.to_date(@media_item.uploaded_at) %></div>
|
<div>Uploaded: <%= DateTime.to_date(@media_item.uploaded_at) %></div>
|
||||||
<div>
|
<div>
|
||||||
<span :if={URI.parse(@media_item.original_url).scheme =~ "http"}>
|
<span :if={URI.parse(@media_item.original_url).scheme =~ "http"}>
|
||||||
@@ -44,7 +45,7 @@
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div :if={@media_item.description} class="mt-4 text-bodydark">
|
<div :if={@media_item.description} class="mt-4 text-bodydark">
|
||||||
<.break_on_newline text={@media_item.description} />
|
<.render_description text={@media_item.description} />
|
||||||
</div>
|
</div>
|
||||||
</aside>
|
</aside>
|
||||||
</section>
|
</section>
|
||||||
@@ -70,7 +71,7 @@
|
|||||||
<%= task.job.state %>
|
<%= task.job.state %>
|
||||||
</:col>
|
</:col>
|
||||||
<:col :let={task} label="Scheduled At">
|
<:col :let={task} label="Scheduled At">
|
||||||
<%= Calendar.strftime(task.job.scheduled_at, "%y-%m-%d %I:%M:%S %p %Z") %>
|
<.datetime_in_zone datetime={task.job.scheduled_at} />
|
||||||
</:col>
|
</:col>
|
||||||
</.table>
|
</.table>
|
||||||
<% else %>
|
<% else %>
|
||||||
|
|||||||
+1
-1
@@ -2,7 +2,7 @@
|
|||||||
<:option>
|
<:option>
|
||||||
<span x-data="{ copied: false }" x-on:click={~s"
|
<span x-data="{ copied: false }" x-on:click={~s"
|
||||||
copyWithCallbacks(
|
copyWithCallbacks(
|
||||||
`#{Jason.Formatter.pretty_print(Phoenix.json_library().encode!(@media_profile))}`,
|
String.raw`#{Jason.Formatter.pretty_print(Phoenix.json_library().encode!(@media_profile))}`,
|
||||||
() => copied = true,
|
() => copied = true,
|
||||||
() => copied = false
|
() => copied = false
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -17,7 +17,7 @@
|
|||||||
<:option>
|
<:option>
|
||||||
<span x-data="{ copied: false }" x-on:click={~s"
|
<span x-data="{ copied: false }" x-on:click={~s"
|
||||||
copyWithCallbacks(
|
copyWithCallbacks(
|
||||||
`#{Jason.Formatter.pretty_print(Phoenix.json_library().encode!(@source))}`,
|
String.raw`#{Jason.Formatter.pretty_print(Phoenix.json_library().encode!(@source))}`,
|
||||||
() => copied = true,
|
() => copied = true,
|
||||||
() => copied = false
|
() => copied = false
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -67,7 +67,7 @@
|
|||||||
<%= task.job.state %>
|
<%= task.job.state %>
|
||||||
</:col>
|
</:col>
|
||||||
<:col :let={task} label="Scheduled At">
|
<:col :let={task} label="Scheduled At">
|
||||||
<%= Calendar.strftime(task.job.scheduled_at, "%y-%m-%d %I:%M:%S %p %Z") %>
|
<.datetime_in_zone datetime={task.job.scheduled_at} />
|
||||||
</:col>
|
</:col>
|
||||||
</.table>
|
</.table>
|
||||||
<% else %>
|
<% else %>
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ defmodule Pinchflat.MixProject do
|
|||||||
def project do
|
def project do
|
||||||
[
|
[
|
||||||
app: :pinchflat,
|
app: :pinchflat,
|
||||||
version: "2024.10.2",
|
version: "2024.10.25",
|
||||||
elixir: "~> 1.17",
|
elixir: "~> 1.17",
|
||||||
elixirc_paths: elixirc_paths(Mix.env()),
|
elixirc_paths: elixirc_paths(Mix.env()),
|
||||||
start_permanent: Mix.env() == :prod,
|
start_permanent: Mix.env() == :prod,
|
||||||
|
|||||||
@@ -46,6 +46,23 @@ defmodule Pinchflat.Downloading.MediaRetentionWorkerTest do
|
|||||||
refute Repo.reload!(old_media_item).media_filepath
|
refute Repo.reload!(old_media_item).media_filepath
|
||||||
end
|
end
|
||||||
|
|
||||||
|
test "deletes media files that are on their retention date per the 24-h clock" do
|
||||||
|
{_source, old_media_item, new_media_item} = prepare_records_for_retention_date(2)
|
||||||
|
|
||||||
|
just_over_two_days_ago = now_minus(2, :days) |> DateTime.add(-1, :minute)
|
||||||
|
just_under_two_days_ago = now_minus(2, :days) |> DateTime.add(1, :minute)
|
||||||
|
|
||||||
|
Media.update_media_item(old_media_item, %{media_downloaded_at: just_over_two_days_ago})
|
||||||
|
Media.update_media_item(new_media_item, %{media_downloaded_at: just_under_two_days_ago})
|
||||||
|
|
||||||
|
perform_job(MediaRetentionWorker, %{})
|
||||||
|
|
||||||
|
assert File.exists?(new_media_item.media_filepath)
|
||||||
|
refute File.exists?(old_media_item.media_filepath)
|
||||||
|
assert Repo.reload!(new_media_item).media_filepath
|
||||||
|
refute Repo.reload!(old_media_item).media_filepath
|
||||||
|
end
|
||||||
|
|
||||||
test "sets culled_at and prevent_download" do
|
test "sets culled_at and prevent_download" do
|
||||||
{_source, old_media_item, new_media_item} = prepare_records_for_retention_date()
|
{_source, old_media_item, new_media_item} = prepare_records_for_retention_date()
|
||||||
|
|
||||||
@@ -106,6 +123,25 @@ defmodule Pinchflat.Downloading.MediaRetentionWorkerTest do
|
|||||||
refute Repo.reload!(old_media_item).media_filepath
|
refute Repo.reload!(old_media_item).media_filepath
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# NOTE: Since this is a date and not a datetime, we can't add logic to have to-the-minute
|
||||||
|
# comparison like we can with retention periods. We can only compare to the day.
|
||||||
|
test "doesn't cull media from on or after the cutoff date" do
|
||||||
|
{_source, old_media_item, new_media_item} = prepare_records_for_source_cutoff_date(2)
|
||||||
|
|
||||||
|
Media.update_media_item(old_media_item, %{uploaded_at: now_minus(2, :days)})
|
||||||
|
Media.update_media_item(new_media_item, %{uploaded_at: now_minus(1, :day)})
|
||||||
|
|
||||||
|
perform_job(MediaRetentionWorker, %{})
|
||||||
|
|
||||||
|
assert File.exists?(new_media_item.media_filepath)
|
||||||
|
assert File.exists?(old_media_item.media_filepath)
|
||||||
|
assert Repo.reload!(new_media_item).media_filepath
|
||||||
|
assert Repo.reload!(old_media_item).media_filepath
|
||||||
|
|
||||||
|
refute Repo.reload!(new_media_item).culled_at
|
||||||
|
refute Repo.reload!(old_media_item).culled_at
|
||||||
|
end
|
||||||
|
|
||||||
test "sets culled_at but not prevent_download" do
|
test "sets culled_at but not prevent_download" do
|
||||||
{_source, old_media_item, new_media_item} = prepare_records_for_source_cutoff_date()
|
{_source, old_media_item, new_media_item} = prepare_records_for_source_cutoff_date()
|
||||||
|
|
||||||
@@ -131,23 +167,6 @@ defmodule Pinchflat.Downloading.MediaRetentionWorkerTest do
|
|||||||
refute Repo.reload!(old_media_item).culled_at
|
refute Repo.reload!(old_media_item).culled_at
|
||||||
end
|
end
|
||||||
|
|
||||||
test "doesn't cull media from on or after the cutoff date" do
|
|
||||||
{_source, old_media_item, new_media_item} = prepare_records_for_source_cutoff_date(2)
|
|
||||||
|
|
||||||
Media.update_media_item(old_media_item, %{uploaded_at: now_minus(2, :days)})
|
|
||||||
Media.update_media_item(new_media_item, %{uploaded_at: now_minus(1, :day)})
|
|
||||||
|
|
||||||
perform_job(MediaRetentionWorker, %{})
|
|
||||||
|
|
||||||
assert File.exists?(new_media_item.media_filepath)
|
|
||||||
assert File.exists?(old_media_item.media_filepath)
|
|
||||||
assert Repo.reload!(new_media_item).media_filepath
|
|
||||||
assert Repo.reload!(old_media_item).media_filepath
|
|
||||||
|
|
||||||
refute Repo.reload!(new_media_item).culled_at
|
|
||||||
refute Repo.reload!(old_media_item).culled_at
|
|
||||||
end
|
|
||||||
|
|
||||||
test "doesn't cull media items that have prevent_culling set" do
|
test "doesn't cull media items that have prevent_culling set" do
|
||||||
{_source, old_media_item, _new_media_item} = prepare_records_for_source_cutoff_date()
|
{_source, old_media_item, _new_media_item} = prepare_records_for_source_cutoff_date()
|
||||||
|
|
||||||
|
|||||||
@@ -441,6 +441,13 @@ defmodule Pinchflat.MediaTest do
|
|||||||
assert Media.pending_download?(media_item)
|
assert Media.pending_download?(media_item)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
test "returns true if the cutoff date is equal to the upload date" do
|
||||||
|
source = source_fixture(%{download_cutoff_date: now_minus(2, :days)})
|
||||||
|
media_item = media_item_fixture(%{source_id: source.id, media_filepath: nil, uploaded_at: now_minus(2, :days)})
|
||||||
|
|
||||||
|
assert Media.pending_download?(media_item)
|
||||||
|
end
|
||||||
|
|
||||||
test "returns false if there is a cutoff date after the media's upload date" do
|
test "returns false if there is a cutoff date after the media's upload date" do
|
||||||
source = source_fixture(%{download_cutoff_date: now_minus(1, :day)})
|
source = source_fixture(%{download_cutoff_date: now_minus(1, :day)})
|
||||||
media_item = media_item_fixture(%{source_id: source.id, media_filepath: nil, uploaded_at: now_minus(2, :days)})
|
media_item = media_item_fixture(%{source_id: source.id, media_filepath: nil, uploaded_at: now_minus(2, :days)})
|
||||||
|
|||||||
@@ -147,6 +147,18 @@ defmodule Pinchflat.SourcesTest do
|
|||||||
assert "could not fetch source details from URL" in errors_on(changeset).original_url
|
assert "could not fetch source details from URL" in errors_on(changeset).original_url
|
||||||
end
|
end
|
||||||
|
|
||||||
|
test "adds an error if the runner succeeds but the result was invalid JSON" do
|
||||||
|
expect(YtDlpRunnerMock, :run, fn _url, _opts, _ot, _addl -> {:ok, "Not JSON"} end)
|
||||||
|
|
||||||
|
valid_attrs = %{
|
||||||
|
media_profile_id: media_profile_fixture().id,
|
||||||
|
original_url: "https://www.youtube.com/channel/abc123"
|
||||||
|
}
|
||||||
|
|
||||||
|
assert {:error, %Ecto.Changeset{} = changeset} = Sources.create_source(valid_attrs)
|
||||||
|
assert "could not fetch source details from URL" in errors_on(changeset).original_url
|
||||||
|
end
|
||||||
|
|
||||||
test "you can specify a custom custom_name" do
|
test "you can specify a custom custom_name" do
|
||||||
expect(YtDlpRunnerMock, :run, &channel_mock/4)
|
expect(YtDlpRunnerMock, :run, &channel_mock/4)
|
||||||
|
|
||||||
|
|||||||
@@ -133,7 +133,7 @@ defmodule Pinchflat.YtDlp.MediaCollectionTest do
|
|||||||
test "returns an error if the output is not JSON" do
|
test "returns an error if the output is not JSON" do
|
||||||
expect(YtDlpRunnerMock, :run, fn _url, _opts, _ot, _addl_opts -> {:ok, "Not JSON"} end)
|
expect(YtDlpRunnerMock, :run, fn _url, _opts, _ot, _addl_opts -> {:ok, "Not JSON"} end)
|
||||||
|
|
||||||
assert {:error, %Jason.DecodeError{}} = MediaCollection.get_source_details(@channel_url)
|
assert {:error, "Error decoding JSON response"} = MediaCollection.get_source_details(@channel_url)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user