22from requests .cookies import RequestsCookieJar
33
44from src .settings ._constants import ApiEndpoints , MinVersions
5- from src .utils .common import extract_json_from_response , make_request , wait_and_exit
5+ from src .utils .common import (
6+ extract_json_from_response ,
7+ is_definitive_setup_error ,
8+ make_request ,
9+ )
610from src .utils .log_setup import logger
711
812
913class QbitError (Exception ):
10- pass
14+ def __init__ (self , message , tip = "" , definitive = False ):
15+ super ().__init__ (message )
16+ self .tip = tip
17+ self .definitive = definitive
1118
1219
1320class QbitClients (list ):
@@ -39,6 +46,10 @@ class QbitClient:
3946 cookie : dict [str , str ] = None
4047 version : str = None
4148 bandwidth_usage : int = 0
49+ ready : bool = False
50+ failure_kind : str = None # None | "transient" | "definitive"
51+ last_error : str = None
52+ setup_tip : str = ""
4253
4354 def __init__ (
4455 self ,
@@ -160,7 +171,7 @@ async def validate_version(self):
160171 f"Please update qBittorrent to at least version { min_version } . Current version: { self .version } " ,
161172 )
162173 error = f"qBittorrent version { self .version } is too old. Please update."
163- raise QbitError (error )
174+ raise QbitError (error , definitive = True )
164175 if version .parse (self .version ) < version .parse ("5.0.0" ):
165176 logger .info (
166177 "[Tip!] Consider upgrading to qBittorrent v5.0.0 or newer to reduce network overhead." ,
@@ -228,7 +239,7 @@ async def set_unwanted_folder(self):
228239 )
229240
230241 async def check_qbit_reachability (self ):
231- """Check if the qBittorrent URL is reachable."""
242+ """Check if the qBittorrent URL is reachable (and the credentials work) ."""
232243 try :
233244 logger .debug (
234245 "_download_clients_qBit.py/check_qbit_reachability: Checking if qbit is reachable" ,
@@ -239,7 +250,7 @@ async def check_qbit_reachability(self):
239250 "password" : getattr (self , "password" , "" ),
240251 }
241252 headers = {"content-type" : "application/x-www-form-urlencoded" }
242- await make_request (
253+ response = await make_request (
243254 "post" ,
244255 endpoint ,
245256 self .settings ,
@@ -249,11 +260,21 @@ async def check_qbit_reachability(self):
249260 log_error = False ,
250261 ignore_test_run = True ,
251262 )
252-
253263 except Exception as e : # noqa: BLE001
254264 tip = "💡 Tip: Did you specify the URL (and username/password if required) correctly?"
255- logger .error (f"-- | qBittorrent\n ❗️ { e } \n { tip } \n " )
256- wait_and_exit ()
265+ if str (e ) != self .last_error : # Only report new failure modes in full
266+ logger .error (f"-- | qBittorrent\n ❗️ { e } \n { tip } \n " )
267+ raise QbitError (e , tip = tip ) from e
268+
269+ # Bad credentials: qBit answers HTTP 200 with the body "Fails." Treat this
270+ # as a definitive config error so we don't retry-loop every cycle and get
271+ # the source IP banned by qBittorrent's failed-login protection.
272+ if getattr (response , "text" , None ) == "Fails." :
273+ tip = "💡 Tip: Check the qBittorrent username/password."
274+ error = "qBittorrent login failed (incorrect username/password)."
275+ if error != self .last_error :
276+ logger .error (f"-- | qBittorrent\n ❗️ { error } \n { tip } \n " )
277+ raise QbitError (error , tip = tip , definitive = True )
257278
258279 async def check_connected (self ):
259280 """Check if the qBittorrent is connected to internet."""
@@ -283,26 +304,38 @@ async def check_connected(self):
283304 return True
284305
285306 async def setup (self ):
286- """Perform the qBittorrent setup by calling relevant managers ."""
287- # Check reachabilty
288- await self .check_qbit_reachability ()
307+ """Perform the qBittorrent setup; degrade instead of exiting on failure ."""
308+ try :
309+ await self .check_qbit_reachability ()
289310
290- # Refresh the qBittorrent cookie first
291- await self .refresh_cookie ()
311+ # Refresh the qBittorrent cookie first
312+ await self .refresh_cookie ()
292313
293- try :
294314 # Fetch version and validate it
295315 await self .fetch_version ()
296316 await self .validate_version ()
297- logger .info (f"OK | qBittorrent ({ self .base_url } )" )
298- except QbitError as e :
299- logger .error (f"qBittorrent version check failed: { e } " )
300- wait_and_exit () # Exit if version check fails
301317
302- # Continue with other setup tasks regardless of version check result
303- await self .create_required_tags ()
304- await self .set_unwanted_folder ()
305- await self .warn_no_bandwidth_limit_set ()
318+ await self .create_required_tags ()
319+ await self .set_unwanted_folder ()
320+ await self .warn_no_bandwidth_limit_set ()
321+
322+ logger .info (f"OK | qBittorrent ({ self .base_url } )" )
323+ self .ready = True
324+ self .failure_kind = None
325+ self .last_error = None
326+ self .setup_tip = ""
327+ except Exception as e : # noqa: BLE001
328+ if not isinstance (e , QbitError ) and str (e ) != self .last_error :
329+ logger .error (
330+ f"Unhandled error during qBittorrent setup: { e } " , exc_info = True
331+ )
332+ self .ready = False
333+ self .failure_kind = (
334+ "definitive" if is_definitive_setup_error (e ) else "transient"
335+ )
336+ self .last_error = str (e )
337+ self .setup_tip = getattr (e , "tip" , "" )
338+ return self .ready
306339
307340 async def get_protected_and_private (self ):
308341 """Fetch torrents from qBittorrent and checks for protected and private status."""
@@ -429,7 +462,6 @@ async def get_torrent_properties(self, qbit_hash):
429462 )
430463 return response .json ()
431464
432-
433465 async def get_torrent_files (self , download_id ):
434466 # this may not work if the wrong qbit
435467 logger .debug ("_download_clients_qBit/get_torrent_files: Getting torrent files" )
0 commit comments