From 1a5b37d9d360f52df4978f78e5944558592a39c2 Mon Sep 17 00:00:00 2001 From: splitsec2 <35583321+splitsec2@users.noreply.github.com> Date: Sun, 20 Sep 2026 21:06:14 -0600 Subject: [PATCH] fix(deluge): send seeding ratio limit under Deluge's own keys (#1367) Deluge's per-torrent options are `stop_at_ratio` (bool) and `stop_ratio` (float), and `torrentmanager` checks `options['stop_at_ratio'] and get_ratio() >= options['stop_ratio']`. We were putting the indexer's float into `stop_at_ratio`, which only switched stopping on and left the daemon's global ratio (default 2.0) as the one actually enforced. A `ratio_limit` of 0 turned stopping off entirely. `stop_at_ratio_enabled` is not a Deluge option at all. Deluge has no per-torrent seeding time limit, `seed_time_limit` is a global core preference, so the value is logged as unapplied instead of sent as a key the daemon drops. qBittorrent and Transmission already honour both indexer limits, so this removes a silent difference between clients. ## Verification - `tests/prowlarr/test_deluge_client.py`: the ratio arrives as `stop_ratio` with `stop_at_ratio` set, and no key Deluge does not define is sent. Both fail on current main and pass here. - Full suite (3165), ruff, ruff format, basedpyright, vulture green. --- shelfmark/download/clients/deluge.py | 9 ++-- tests/prowlarr/test_deluge_client.py | 76 ++++++++++++++++++++++++++++ 2 files changed, 82 insertions(+), 3 deletions(-) diff --git a/shelfmark/download/clients/deluge.py b/shelfmark/download/clients/deluge.py index e55104af..bfbae6f6 100644 --- a/shelfmark/download/clients/deluge.py +++ b/shelfmark/download/clients/deluge.py @@ -292,11 +292,14 @@ class DelugeClient(DownloadClient): # Per-torrent seeding limits from indexer seeding_time_limit = coerce_optional_int(kwargs.get("seeding_time_limit")) if seeding_time_limit is not None: - options["seed_time_limit"] = seeding_time_limit + logger.debug( + "Deluge has no per-torrent seeding time limit, ignoring %s minutes", + seeding_time_limit, + ) ratio_limit = coerce_optional_float(kwargs.get("ratio_limit")) if ratio_limit is not None: - options["stop_at_ratio"] = ratio_limit - options["stop_at_ratio_enabled"] = True + options["stop_ratio"] = ratio_limit + options["stop_at_ratio"] = True if torrent_info.is_magnet: magnet_url = torrent_info.magnet_url or url diff --git a/tests/prowlarr/test_deluge_client.py b/tests/prowlarr/test_deluge_client.py index 4002e9fb..b43e2c99 100644 --- a/tests/prowlarr/test_deluge_client.py +++ b/tests/prowlarr/test_deluge_client.py @@ -114,6 +114,82 @@ class TestDelugeClientAddDownload: {}, ) + def test_add_download_sends_ratio_limit_as_stop_ratio(self, monkeypatch): + """Ratio limits should use Deluge's stop_ratio float plus the stop_at_ratio switch.""" + config_values = { + "DELUGE_HOST": "http://localhost", + "DELUGE_PORT": "8112", + "DELUGE_PASSWORD": "password", + "DELUGE_CATEGORY": "books", + } + monkeypatch.setattr( + "shelfmark.download.clients.deluge.config.get", + make_config_getter(config_values), + ) + + from shelfmark.download.clients.deluge import DelugeClient + + client = DelugeClient() + monkeypatch.setattr(client, "_ensure_connected", lambda: None) + mock_rpc_call = MagicMock(return_value="abcdef1234567890abcdef1234567890abcdef12") + monkeypatch.setattr(client, "_rpc_call", mock_rpc_call) + monkeypatch.setattr(client, "_try_set_label", MagicMock()) + + magnet = "magnet:?xt=urn:btih:ABCDEF1234567890ABCDEF1234567890ABCDEF12&dn=test" + with patch( + "shelfmark.download.clients.deluge.extract_torrent_info", autospec=True + ) as mock_extract: + mock_extract.return_value = TorrentInfo( + info_hash="abcdef1234567890abcdef1234567890abcdef12", + torrent_data=None, + is_magnet=True, + magnet_url=magnet, + ) + client.add_download(magnet, "Test", ratio_limit=1.5) + + mock_rpc_call.assert_called_once_with( + "core.add_torrent_magnet", + magnet, + {"stop_ratio": 1.5, "stop_at_ratio": True}, + ) + + def test_add_download_skips_options_deluge_does_not_define(self, monkeypatch): + """Deluge has no per-torrent seeding time limit, so no unknown keys are sent.""" + config_values = { + "DELUGE_HOST": "http://localhost", + "DELUGE_PORT": "8112", + "DELUGE_PASSWORD": "password", + "DELUGE_CATEGORY": "books", + } + monkeypatch.setattr( + "shelfmark.download.clients.deluge.config.get", + make_config_getter(config_values), + ) + + from shelfmark.download.clients.deluge import DelugeClient + + client = DelugeClient() + monkeypatch.setattr(client, "_ensure_connected", lambda: None) + mock_rpc_call = MagicMock(return_value="abcdef1234567890abcdef1234567890abcdef12") + monkeypatch.setattr(client, "_rpc_call", mock_rpc_call) + monkeypatch.setattr(client, "_try_set_label", MagicMock()) + + magnet = "magnet:?xt=urn:btih:ABCDEF1234567890ABCDEF1234567890ABCDEF12&dn=test" + with patch( + "shelfmark.download.clients.deluge.extract_torrent_info", autospec=True + ) as mock_extract: + mock_extract.return_value = TorrentInfo( + info_hash="abcdef1234567890abcdef1234567890abcdef12", + torrent_data=None, + is_magnet=True, + magnet_url=magnet, + ) + client.add_download(magnet, "Test", ratio_limit=1.5, seeding_time_limit=120) + + options = mock_rpc_call.call_args.args[2] + assert "stop_at_ratio_enabled" not in options + assert "seed_time_limit" not in options + class TestDelugeClientErrors: """Tests for Deluge error handling fallbacks."""