Made network requests safe, fixed connection reestablish mem leak

This commit is contained in:
j4nk 2025-09-03 22:00:19 -04:00
parent f7075b01e3
commit db315652d1
1 changed files with 121 additions and 33 deletions

142
bot.py
View File

@ -39,7 +39,8 @@ class QBittorrentAPICaller():
self.username = username self.username = username
self.password = password self.password = password
self.session = requests.Session() self.session = requests.Session()
self.reestablish_session() # self.reestablish_session()
self.run_session_maintainer()
def __del__(self): def __del__(self):
self.logout() self.logout()
@ -58,55 +59,122 @@ class QBittorrentAPICaller():
return to_ret return to_ret
# We need to reestablish a connection to the server periodically, this does that # We need to reestablish a connection to the server periodically, this does that
def reestablish_session(self): #def reestablish_session(self):
# Every 10 minutes, run this # Every 10 minutes, run this
threading.Timer(600, self.reestablish_session).start() #threading.Timer(600, self.reestablish_session).start()
# self.logout()
# self.login()
# logging.info("Reestablished connection to QBittorrent server")
def run_session_maintainer(self):
def loop():
while True:
try:
self.logout() self.logout()
self.login() self.login()
logging.info("Reestablished connection to QBittorrent server") logging.info("Reestablished connection to QBittorrent server")
except Exception as e:
logging.error(f"Session maintainer error: {e}", exc_info=True)
time.sleep(600) # 10 minutes
threading.Thread(target=loop, daemon=True).start()
def truncate_string(self, s, max_length=25): def truncate_string(self, s, max_length=25):
return s[:max_length] + '...' if len(s) > max_length else s return s[:max_length] + '...' if len(s) > max_length else s
def safe_get(self, path, params=None):
try:
# resp = self.session.get(self.url + path, params=params, timeout=5)
resp = self.session.get(path, params=params, timeout=5)
resp.raise_for_status()
return resp
except requests.exceptions.RequestException as e:
logging.warning(f"GET {path} failed: {e}")
return None
def safe_post(self, path, data=None):
try:
# resp = self.session.post(self.url + path, data=data, timeout=5)
resp = self.session.post(path, data=data, timeout=5)
resp.raise_for_status()
return resp
except requests.exceptions.RequestException as e:
logging.warning(f"POST {path} failed: {e}")
return None
def login(self): def login(self):
the_url = self.url + "/" + "api/v2/auth/login" the_url = self.url + "/" + "api/v2/auth/login"
data={"username":self.username, "password":self.password} data={"username":self.username, "password":self.password}
resp = self.session.post(the_url, data=data) try:
resp = self.session.post(the_url, data=data, timeout=5)
resp.raise_for_status()
except requests.exceptions.RequestException as e:
logging.warning(f"Login failed: {e}")
#resp = self.session.post(the_url, data=data)
def logout(self): def logout(self):
the_url = self.url + "/" + "api/v2/auth/logout" the_url = self.url + "/" + "api/v2/auth/logout"
resp = self.session.post(the_url) # resp = self.session.post(the_url)
try:
resp = self.session.post(the_url, timeout=5)
resp.raise_for_status()
except requests.exceptions.RequestException as e:
logging.warning(f"Logout failed: {e}")
couldnt_connect_text = "Could not connect to qbittorrent server"
def version(self): def version(self):
the_url = self.url + "/" + "api/v2/app/version" the_url = self.url + "/" + "api/v2/app/version"
resp = self.session.get(the_url) # resp = self.session.get(the_url)
if resp.status_code != 200: # if resp.status_code != 200:
return "Got status" + str(resp.status_code) # return "Got status" + str(resp.status_code)
# return resp.text
resp = self.safe_get(the_url)
if not resp:
return self.couldnt_connect_text
else:
return resp.text return resp.text
def webapiVersion(self): def webapiVersion(self):
the_url = self.url + "/" + "api/v2/app/webapiVersion" the_url = self.url + "/" + "api/v2/app/webapiVersion"
resp = self.session.get(the_url) resp = self.safe_get(the_url)
if resp.status_code != 200: if not resp:
return "Got status" + str(resp.status_code) return self.couldnt_connect_text
else:
return resp.text return resp.text
# try:
# resp = self.session.get(the_url)
# resp.raise_for_status()
# return resp.text
# except requests.exceptions.RequestException as e:
# logging.warning(f"Couldn't get version")
# if resp.status_code != 200:
# return "Got status" + str(resp.status_code)
def buildInfo(self): def buildInfo(self):
the_url = self.url + "/" + "api/v2/app/buildInfo" the_url = self.url + "/" + "api/v2/app/buildInfo"
resp = self.session.get(the_url) resp = self.safe_get(the_url)
if resp.status_code != 200: if not resp:
return "Got status" + str(resp.status_code) return self.couldnt_connect_text
else:
return json_to_key_value_string(resp.text) return json_to_key_value_string(resp.text)
# resp = self.session.get(the_url)
# if resp.status_code != 200:
# return "Got status" + str(resp.status_code)
# return json_to_key_value_string(resp.text)
def torrentList(self, modifier=""): def torrentList(self, modifier=""):
the_url = self.url + "/" + "api/v2/torrents/info" the_url = self.url + "/" + "api/v2/torrents/info"
to_ret = "```\n" to_ret = "```\n"
for f in ["downloading", "completed"]: for f in ["downloading", "completed"]:
resp = self.session.post(the_url, data={"filter":f}) # resp = self.session.post(the_url, data={"filter":f})
if resp.status_code != 200: # if resp.status_code != 200:
return "Got status" + str(resp.status_code) # return "Got status" + str(resp.status_code)
resp = self.safe_post(the_url, data={"filter":f})
if not resp:
return self.couldnt_connect_text
parsed = json.loads(resp.text) parsed = json.loads(resp.text)
to_ret += f.upper() + "\n" to_ret += f.upper() + "\n"
@ -122,13 +190,17 @@ class QBittorrentAPICaller():
to_ret += "```" to_ret += "```"
return to_ret return to_ret
# Too lazy to make this safe, hopefully will be fine idk
def add(self, url, username, category="unknown"): def add(self, url, username, category="unknown"):
if not url.startswith("magnet:"): if not url.startswith("magnet:"):
return "Please supply a magnet link (begins with \"magnet:\")" return "Please supply a magnet link (begins with \"magnet:\")"
the_url = self.url + "/" + "api/v2/torrents/add" the_url = self.url + "/" + "api/v2/torrents/add"
resp = self.session.post(the_url, data={"urls":url,"category":category,"tags":username}) # resp = self.session.post(the_url, data={"urls":url,"category":category,"tags":username})
resp = self.safe_post(the_url, data={"urls":url,"category":category,"tags":username})
if not resp:
return self.couldnt_connect_text
if (resp.status_code == 415): if (resp.status_code == 415):
return "Torrent file not valid" return "Torrent file not valid"
@ -138,7 +210,10 @@ class QBittorrentAPICaller():
# Make request to torrentlist with particular hash # Make request to torrentlist with particular hash
the_url2 = self.url + "/" + "api/v2/torrents/info" the_url2 = self.url + "/" + "api/v2/torrents/info"
time.sleep(5) time.sleep(5)
resp2 = self.session.post(the_url2) # resp2 = self.session.post(the_url2)
resp2 = self.safe_post(the_url2)
if not resp2:
return self.couldnt_connect_text
if (resp2.status_code != 200): if (resp2.status_code != 200):
return "Could not verify if torrent was added" return "Could not verify if torrent was added"
parsed = json.loads(resp2.text) parsed = json.loads(resp2.text)
@ -153,27 +228,39 @@ class QBittorrentAPICaller():
def get_search_plugins(self): def get_search_plugins(self):
the_url = self.url + "/" + "api/v2/search/plugins" the_url = self.url + "/" + "api/v2/search/plugins"
return self.session.post(the_url) # return self.session.post(the_url)
resp = self.safe_post(the_url)
return resp if resp else self.couldnt_connect_text
def search_start(self, searchstring, category="all"): def search_start(self, searchstring, category="all"):
the_url = self.url + "/" + "api/v2/search/start" the_url = self.url + "/" + "api/v2/search/start"
return self.session.post(the_url, data={"pattern":searchstring, "plugins":"enabled", "category":category}) resp = self.safe_post(the_url, data={"pattern":searchstring, "plugins":"enabled", "category":category})
return resp if resp else self.couldnt_connect_text
# return self.session.post(the_url, data={"pattern":searchstring, "plugins":"enabled", "category":category})
def search_status(self, search_id): def search_status(self, search_id):
the_url = self.url + "/" + "api/v2/search/status" the_url = self.url + "/" + "api/v2/search/status"
return self.session.post(the_url, data={"id":search_id}) resp = self.safe_post(the_url, data={"id":search_id})
return resp if resp else self.couldnt_connect_text
# return self.session.post(the_url, data={"id":search_id})
def search_stop(self, search_id): def search_stop(self, search_id):
the_url = self.url + "/" + "api/v2/search/stop" the_url = self.url + "/" + "api/v2/search/stop"
return self.session.post(the_url, data={"id":search_id}) resp = self.safe_post(the_url, data={"id":search_id})
return resp if resp else self.couldnt_connect_text
# return self.session.post(the_url, data={"id":search_id})
def search_results(self, search_id): def search_results(self, search_id):
the_url = self.url + "/" + "api/v2/search/results" the_url = self.url + "/" + "api/v2/search/results"
return self.session.post(the_url, data={"id":search_id}) resp = self.safe_post(the_url, data={"id":search_id})
return resp if resp else self.couldnt_connect_text
# return self.session.post(the_url, data={"id":search_id})
def search_delete(self, search_id): def search_delete(self, search_id):
the_url = self.url + "/" + "api/v2/search/delete" the_url = self.url + "/" + "api/v2/search/delete"
return self.session.post(the_url, data={"id":search_id}) resp = self.safe_post(the_url, data={"id":search_id})
return resp if resp else self.couldnt_connect_text
# return self.session.post(the_url, data={"id":search_id})
@ -198,7 +285,8 @@ class QBBot(slixmpp.ClientXMPP):
# If you wanted more functionality, here's how to register plugins: # If you wanted more functionality, here's how to register plugins:
# self.register_plugin('xep_0030') # Service Discovery # self.register_plugin('xep_0030') # Service Discovery
# self.register_plugin('xep_0199') # XMPP Ping self.register_plugin('xep_0199') # XMPP Ping
self['xep_0199'].enable_keepalive(interval=60, timeout=10)
# Here's how to access plugins once you've registered them: # Here's how to access plugins once you've registered them:
# self['xep_0030'].add_feature('echo_demo') # self['xep_0030'].add_feature('echo_demo')