diff --git a/src/clients/deluge.py b/src/clients/deluge.py index 2263b8d..7e031b5 100644 --- a/src/clients/deluge.py +++ b/src/clients/deluge.py @@ -3,13 +3,17 @@ import requests from pathlib import Path -from ..errors import TorrentClientError +from ..errors import TorrentClientError, TorrentClientAuthenticationError from .torrent_client import TorrentClient from requests.exceptions import RequestException from requests.structures import CaseInsensitiveDict class Deluge(TorrentClient): + ERROR_CODES = { + "NO_AUTH": 1, + } + def __init__(self, rpc_url): super().__init__() self._rpc_url = rpc_url @@ -37,7 +41,7 @@ def get_torrent_info(self, infohash): {"hash": infohash}, ] - response = self.__request("web.update_ui", params) + response = self.__wrap_request("web.update_ui", params) if "torrents" in response: torrent = response["torrents"].get(infohash) @@ -75,7 +79,7 @@ def inject_torrent(self, source_torrent_infohash, new_torrent_filepath, save_pat }, ] - new_torrent_infohash = self.__request("core.add_torrent_file", params) + new_torrent_infohash = self.__wrap_request("core.add_torrent_file", params) newtorrent_label = self.__determine_label(source_torrent_info) self.__set_label(new_torrent_infohash, newtorrent_label) @@ -86,6 +90,7 @@ def __authenticate(self): if not password: raise Exception("You need to define a password in the Deluge RPC URL. (e.g. http://:@localhost:8112)") + # This method specifically cannot use __wrap_request because an auth error would create an infinite loop auth_response = self.__request("auth.login", [password]) if not auth_response: raise TorrentClientError("Reached Deluge RPC endpoint but failed to authenticate") @@ -93,7 +98,7 @@ def __authenticate(self): return self.__request("web.connected") def __is_label_plugin_enabled(self): - response = self.__request("core.get_enabled_plugins") + response = self.__wrap_request("core.get_enabled_plugins") return "Label" in response @@ -109,11 +114,18 @@ def __set_label(self, infohash, label): if not self._label_plugin_enabled: return - current_labels = self.__request("label.get_labels") + current_labels = self.__wrap_request("label.get_labels") if label not in current_labels: - self.__request("label.add", [label]) + self.__wrap_request("label.add", [label]) + + return self.__wrap_request("label.set_torrent", [infohash, label]) - return self.__request("label.set_torrent", [infohash, label]) + def __wrap_request(self, method, params=[]): + try: + return self.__request(method, params) + except TorrentClientAuthenticationError: + self.__authenticate() + return self.__request(method, params) def __request(self, method, params=[]): href, _, _ = self._extract_credentials_from_url(self._rpc_url) @@ -148,6 +160,8 @@ def __request(self, method, params=[]): self.__handle_response_headers(response.headers) if "error" in json_response and json_response["error"]: + if json_response["error"]["code"] == self.ERROR_CODES["NO_AUTH"]: + raise TorrentClientAuthenticationError("Failed to authenticate with Deluge") raise TorrentClientError(f"Deluge method {method} returned an error: {json_response['error']}") return json_response["result"] diff --git a/src/errors.py b/src/errors.py index 65836eb..ab8c975 100644 --- a/src/errors.py +++ b/src/errors.py @@ -50,5 +50,9 @@ class TorrentClientError(Exception): pass +class TorrentClientAuthenticationError(Exception): + pass + + class TorrentInjectionError(Exception): pass diff --git a/tests/clients/test_deluge.py b/tests/clients/test_deluge.py index b108e71..da7a6a0 100644 --- a/tests/clients/test_deluge.py +++ b/tests/clients/test_deluge.py @@ -14,7 +14,7 @@ torrent_info_matcher, ) -from src.errors import TorrentClientError +from src.errors import TorrentClientError, TorrentClientAuthenticationError from src.clients.deluge import Deluge @@ -68,6 +68,15 @@ def test_raises_exception_on_failed_auth(self, api_url, deluge_client): assert "Reached Deluge RPC endpoint but failed to authenticate" in str(excinfo.value) + def test_raises_exception_on_errored_auth(self, api_url, deluge_client): + with requests_mock.Mocker() as m: + m.post(api_url, additional_matcher=auth_matcher, json={"error": {"code": 1}}) + + with pytest.raises(TorrentClientAuthenticationError) as excinfo: + deluge_client.setup() + + assert "Failed to authenticate with Deluge" in str(excinfo.value) + def test_sets_label_plugin_enabled_when_true(self, api_url, deluge_client): assert not deluge_client._label_plugin_enabled @@ -188,6 +197,21 @@ def test_returns_completed_if_seeding(self, api_url, deluge_client, torrent_info assert response["complete"] + def test_attempts_reauth_if_deluge_cookie_expired(self, api_url, deluge_client, torrent_info_response): + with requests_mock.Mocker() as m: + m.post(api_url, additional_matcher=torrent_info_matcher, json={"error": {"code": 1}}) + m.post(api_url, additional_matcher=auth_matcher, json={"result": True}, headers={"Set-Cookie": "supersecret"}) + m.post(api_url, additional_matcher=connected_matcher, json={"result": True}) + + deluge_client._deluge_cookie = None + with pytest.raises(TorrentClientAuthenticationError): + deluge_client.get_torrent_info("foo") + + assert deluge_client._deluge_cookie is not None + assert m.request_history[-3].json()["method"] == "auth.login" + assert m.request_history[-2].json()["method"] == "web.connected" + assert m.request_history[-1].json()["method"] == "web.update_ui" + class TestInjectTorrent(SetupTeardown): def test_injects_torrent(self, api_url, deluge_client, torrent_info_response):