Skip to content

Commit a97bb24

Browse files
authored
[Enhancement] Retry a download using cookies if doing so might help (#641)
* Sources that use cookies when_needed now retry downloads if we think it'd help * tweaked error message we're checking on to match media_download_worker
1 parent ac89594 commit a97bb24

File tree

4 files changed

+121
-9
lines changed

4 files changed

+121
-9
lines changed

lib/pinchflat/downloading/media_downloader.ex

+51-6
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,8 @@ defmodule Pinchflat.Downloading.MediaDownloader do
5252
# Looks complicated, but here's the key points:
5353
# - download_with_options runs a pre-check to see if the media item is suitable for download.
5454
# - If the media item fails the precheck, it returns {:error, :unsuitable_for_download, message}
55+
# - However, if the precheck fails in a way that we think can be fixed by using cookies, we retry with cookies
56+
# and return the result of that
5557
# - If the precheck passes but the download fails, it normally returns {:error, :download_failed, message}
5658
# - However, there are some errors we can recover from (eg: failure to communicate with SponsorBlock).
5759
# In this case, we attempt the download anyway and update the media item with what details we do have.
@@ -68,6 +70,8 @@ defmodule Pinchflat.Downloading.MediaDownloader do
6870
# - `:unrecoverable` if there was an initial failure and the recovery attempt failed
6971
# - `:download_failed` for all other yt-dlp-related downloading errors
7072
# - `:unknown` for any other errors, including those not related to yt-dlp
73+
# - If we retry using cookies, all of the above return values apply. The cookie retry
74+
# logic is handled transparently as far as the caller is concerned
7175
defp attempt_download_and_update_for_media_item(media_item, override_opts) do
7276
output_filepath = FilesystemUtils.generate_metadata_tmpfile(:json)
7377
media_with_preloads = Repo.preload(media_item, [:metadata, source: :media_profile])
@@ -152,14 +156,48 @@ defmodule Pinchflat.Downloading.MediaDownloader do
152156

153157
defp download_with_options(url, item_with_preloads, output_filepath, override_opts) do
154158
{:ok, options} = DownloadOptionBuilder.build(item_with_preloads, override_opts)
159+
force_use_cookies = Keyword.get(override_opts, :force_use_cookies, false)
160+
source_uses_cookies = Sources.use_cookies?(item_with_preloads.source, :downloading)
161+
should_use_cookies = force_use_cookies || source_uses_cookies
155162

156-
use_cookies = Sources.use_cookies?(item_with_preloads.source, :downloading)
157-
runner_opts = [output_filepath: output_filepath, use_cookies: use_cookies]
163+
runner_opts = [output_filepath: output_filepath, use_cookies: should_use_cookies]
158164

159-
case YtDlpMedia.get_downloadable_status(url, use_cookies: use_cookies) do
160-
{:ok, :downloadable} -> YtDlpMedia.download(url, options, runner_opts)
161-
{:ok, :ignorable} -> {:error, :unsuitable_for_download}
162-
err -> err
165+
case {YtDlpMedia.get_downloadable_status(url, use_cookies: should_use_cookies), should_use_cookies} do
166+
{{:ok, :downloadable}, _} ->
167+
YtDlpMedia.download(url, options, runner_opts)
168+
169+
{{:ok, :ignorable}, _} ->
170+
{:error, :unsuitable_for_download}
171+
172+
{{:error, _message, _exit_code} = err, false} ->
173+
# If there was an error and we don't have cookies, this method will retry with cookies
174+
# if doing so would help AND the source allows. Otherwise, it will return the error as-is
175+
maybe_retry_with_cookies(url, item_with_preloads, output_filepath, override_opts, err)
176+
177+
# This gets hit if cookies are enabled which, importantly, also covers the case where we
178+
# retry a download with cookies and it fails again
179+
{{:error, message, exit_code}, true} ->
180+
{:error, message, exit_code}
181+
182+
{err, _} ->
183+
err
184+
end
185+
end
186+
187+
defp maybe_retry_with_cookies(url, item_with_preloads, output_filepath, override_opts, err) do
188+
{:error, message, _} = err
189+
source = item_with_preloads.source
190+
message_contains_cookie_error = String.contains?(to_string(message), recoverable_cookie_errors())
191+
192+
if Sources.use_cookies?(source, :error_recovery) && message_contains_cookie_error do
193+
download_with_options(
194+
url,
195+
item_with_preloads,
196+
output_filepath,
197+
Keyword.put(override_opts, :force_use_cookies, true)
198+
)
199+
else
200+
err
163201
end
164202
end
165203

@@ -168,4 +206,11 @@ defmodule Pinchflat.Downloading.MediaDownloader do
168206
"Unable to communicate with SponsorBlock"
169207
]
170208
end
209+
210+
defp recoverable_cookie_errors do
211+
[
212+
"Sign in to confirm",
213+
"This video is available to this channel's members"
214+
]
215+
end
171216
end

lib/pinchflat/sources/sources.ex

+2-2
Original file line numberDiff line numberDiff line change
@@ -37,11 +37,11 @@ defmodule Pinchflat.Sources do
3737
3838
Returns boolean()
3939
"""
40-
def use_cookies?(source, operation) when operation in [:indexing, :downloading, :metadata] do
40+
def use_cookies?(source, operation) when operation in [:indexing, :downloading, :metadata, :error_recovery] do
4141
case source.cookie_behaviour do
4242
:disabled -> false
4343
:all_operations -> true
44-
:when_needed -> operation == :indexing
44+
:when_needed -> operation in [:indexing, :error_recovery]
4545
end
4646
end
4747

test/pinchflat/downloading/media_downloader_test.exs

+63-1
Original file line numberDiff line numberDiff line change
@@ -204,7 +204,7 @@ defmodule Pinchflat.Downloading.MediaDownloaderTest do
204204
end
205205
end
206206

207-
describe "download_for_media_item/3 when testing retries" do
207+
describe "download_for_media_item/3 when testing non-cookie retries" do
208208
test "returns a recovered tuple on recoverable errors", %{media_item: media_item} do
209209
message = "Unable to communicate with SponsorBlock"
210210

@@ -298,6 +298,68 @@ defmodule Pinchflat.Downloading.MediaDownloaderTest do
298298
end
299299
end
300300

301+
describe "download_for_media_item/3 when testing cookie retries" do
302+
test "retries with cookies if we think it would help and the source allows" do
303+
expect(YtDlpRunnerMock, :run, 4, fn
304+
_url, :get_downloadable_status, _opts, _ot, [use_cookies: false] ->
305+
{:error, "Sign in to confirm your age", 1}
306+
307+
_url, :get_downloadable_status, _opts, _ot, [use_cookies: true] ->
308+
{:ok, "{}"}
309+
310+
_url, :download, _opts, _ot, addl ->
311+
assert {:use_cookies, true} in addl
312+
{:ok, render_metadata(:media_metadata)}
313+
314+
_url, :download_thumbnail, _opts, _ot, _addl ->
315+
{:ok, ""}
316+
end)
317+
318+
source = source_fixture(%{cookie_behaviour: :when_needed})
319+
media_item = media_item_fixture(%{source_id: source.id})
320+
321+
assert {:ok, _} = MediaDownloader.download_for_media_item(media_item)
322+
end
323+
324+
test "does not retry with cookies if we don't think it would help even the source allows" do
325+
expect(YtDlpRunnerMock, :run, 1, fn
326+
_url, :get_downloadable_status, _opts, _ot, [use_cookies: false] ->
327+
{:error, "Some other error", 1}
328+
end)
329+
330+
source = source_fixture(%{cookie_behaviour: :when_needed})
331+
media_item = media_item_fixture(%{source_id: source.id})
332+
333+
assert {:error, :download_failed, "Some other error"} = MediaDownloader.download_for_media_item(media_item)
334+
end
335+
336+
test "does not retry with cookies even if we think it would help but source doesn't allow" do
337+
expect(YtDlpRunnerMock, :run, 1, fn
338+
_url, :get_downloadable_status, _opts, _ot, [use_cookies: false] ->
339+
{:error, "Sign in to confirm your age", 1}
340+
end)
341+
342+
source = source_fixture(%{cookie_behaviour: :disabled})
343+
media_item = media_item_fixture(%{source_id: source.id})
344+
345+
assert {:error, :download_failed, "Sign in to confirm your age"} =
346+
MediaDownloader.download_for_media_item(media_item)
347+
end
348+
349+
test "does not retry with cookies if cookies were already used" do
350+
expect(YtDlpRunnerMock, :run, 1, fn
351+
_url, :get_downloadable_status, _opts, _ot, [use_cookies: true] ->
352+
{:error, "This video is available to this channel's members", 1}
353+
end)
354+
355+
source = source_fixture(%{cookie_behaviour: :all_operations})
356+
media_item = media_item_fixture(%{source_id: source.id})
357+
358+
assert {:error, :download_failed, "This video is available to this channel's members"} =
359+
MediaDownloader.download_for_media_item(media_item)
360+
end
361+
end
362+
301363
describe "download_for_media_item/3 when testing media_item attributes" do
302364
setup do
303365
stub(YtDlpRunnerMock, :run, fn

test/pinchflat/sources_test.exs

+5
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,11 @@ defmodule Pinchflat.SourcesTest do
8080
source = source_fixture(%{cookie_behaviour: :when_needed})
8181
refute Sources.use_cookies?(source, :downloading)
8282
end
83+
84+
test "returns true if the action is error_recovery and the source is set to :when_needed" do
85+
source = source_fixture(%{cookie_behaviour: :when_needed})
86+
assert Sources.use_cookies?(source, :error_recovery)
87+
end
8388
end
8489

8590
describe "list_sources/0" do

0 commit comments

Comments
 (0)