Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 11 additions & 4 deletions shelfmark/download/clients/debridlink.py
Original file line number Diff line number Diff line change
Expand Up @@ -592,7 +592,7 @@ def _handle_torrent_info(self, info: dict[str, Any], state: _DownloadState) -> D
and all(_as_float(f.get("downloadPercent", 100)) >= _COMPLETE_PERCENT for f in files)
)
if ready:
self._maybe_start_download_thread(state)
self._maybe_start_download_thread(state, files)
return DownloadStatus(
progress=50.0,
state=DownloadState.DOWNLOADING,
Expand Down Expand Up @@ -657,8 +657,15 @@ def _start_waiting_torrent(self, state: _DownloadState) -> None:
with state.lock:
state.start_requested = False

def _maybe_start_download_thread(self, state: _DownloadState) -> None:
"""Spawn a background thread to download the seedbox's files."""
def _maybe_start_download_thread(
self, state: _DownloadState, files: list[dict[str, Any]] | None = None
) -> None:
"""Spawn a background thread to download the seedbox's files.

``files`` is the list the status check already fetched by ``ids``; passing it on
saves the thread a second request that has none of the status path's flood and
transient-error handling.
"""
with state.lock:
already_running = state.phase in ("downloading_http", "complete")
thread_alive = state.download_thread is not None and state.download_thread.is_alive()
Expand All @@ -670,7 +677,7 @@ def _maybe_start_download_thread(self, state: _DownloadState) -> None:
state.progress = 50.0
t = threading.Thread(
target=self._process_and_download,
args=(state,),
args=(state, files),
daemon=True,
)
state.download_thread = t
Expand Down
26 changes: 24 additions & 2 deletions tests/prowlarr/test_debridlink_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -256,7 +256,7 @@ def test_completion_hands_off_only_when_every_file_is_ready(self, tmp_path, monk
status = client._handle_torrent_info(partial, state)

assert status.progress == 50.0
started.assert_called_once_with(state)
started.assert_called_once_with(state, partial["files"])

def test_an_empty_file_list_is_not_treated_as_ready(self, tmp_path, monkeypatch):
client = self._bare_client()
Expand Down Expand Up @@ -326,7 +326,7 @@ def test_retrieval_starts_at_half_progress_rather_than_zero(self, tmp_path, monk
state = _state(tmp_path)
DebridLinkClient._downloads["DL1"] = state
release = threading.Event()
monkeypatch.setattr(client, "_process_and_download", lambda _state: release.wait(5))
monkeypatch.setattr(client, "_process_and_download", lambda _state, _files: release.wait(5))

client._maybe_start_download_thread(state)
try:
Expand All @@ -339,6 +339,28 @@ def test_retrieval_starts_at_half_progress_rather_than_zero(self, tmp_path, monk
assert status.message == "Downloading files via HTTP..."
assert status.progress == 50.0

def test_retrieval_reuses_the_files_the_status_check_already_has(self, tmp_path, monkeypatch):
client = self._bare_client()
state = _state(tmp_path)
DebridLinkClient._downloads["DL1"] = state
refetched = MagicMock(side_effect=AssertionError("file list fetched a second time"))
monkeypatch.setattr(client, "_fetch_file_list", refetched)
seen: list[list[dict]] = []
monkeypatch.setattr(
client, "_process_and_download", lambda _state, files=None: seen.append(files)
)

ready = {
"downloadPercent": 100,
"files": [{"name": "Dune.epub", "downloadPercent": 100, "downloadUrl": "https://dl/x"}],
}
client._handle_torrent_info(ready, state)
assert state.download_thread is not None
state.download_thread.join(5)

assert seen == [ready["files"]]
refetched.assert_not_called()

def test_a_vanished_torrent_becomes_an_error_rather_than_a_stall(self, monkeypatch, tmp_path):
monkeypatch.setattr("shelfmark.download.clients.debridlink.TMP_DIR", tmp_path)
_mock_request(monkeypatch, {"success": True, "value": []})
Expand Down
Loading