From 74c803c6d4521335b58c8c528dfebe2493c64e40 Mon Sep 17 00:00:00 2001 From: IgorKha <2268209+IgorKha@users.noreply.github.com> Date: Tue, 28 Oct 2025 17:26:49 +0500 Subject: [PATCH 1/2] Fix issue #402: Update qBittorrent plugin and lib for compatibility --- Dockerfile | 4 +- monitorrent/plugins/clients/qbittorrent.py | 156 +++++++++------------ requirements.txt | 2 +- 3 files changed, 68 insertions(+), 94 deletions(-) diff --git a/Dockerfile b/Dockerfile index 9d8dbb0d..5498066d 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM debian:buster-slim AS download +FROM debian:bookworm-slim AS download RUN apt update && apt install -y wget WORKDIR /deb @@ -20,7 +20,7 @@ FROM scratch AS mount COPY . /app FROM python:3.9.11-slim-bullseye -MAINTAINER Alexander Puzynia +LABEL maintainer="Alexander Puzynia " # For docker layers caching it is better to install Playwight first with all dependencies COPY --from=download /deb /deb diff --git a/monitorrent/plugins/clients/qbittorrent.py b/monitorrent/plugins/clients/qbittorrent.py index 592091cf..4d53884a 100644 --- a/monitorrent/plugins/clients/qbittorrent.py +++ b/monitorrent/plugins/clients/qbittorrent.py @@ -1,20 +1,16 @@ -from __future__ import absolute_import -from __future__ import division -from __future__ import print_function -from __future__ import unicode_literals +from __future__ import absolute_import, division, print_function, unicode_literals -import six import time +from datetime import datetime +import six from pytz import utc -from sqlalchemy import Column, Integer, String - from qbittorrentapi import Client +from sqlalchemy import Column, Integer, String from monitorrent.db import Base, DBSession from monitorrent.plugin_managers import register_plugin from monitorrent.utils.bittorrent_ex import Torrent -from datetime import datetime class QBittorrentCredentials(Base): @@ -29,65 +25,56 @@ class QBittorrentCredentials(Base): class QBittorrentClientPlugin(object): name = "qbittorrent" - form = [{ - 'type': 'row', - 'content': [{ - 'type': 'text', - 'label': 'Host', - 'model': 'host', - 'flex': 80 - }, { - 'type': 'text', - 'label': 'Port', - 'model': 'port', - 'flex': 20 - }] - }, { - 'type': 'row', - 'content': [{ - 'type': 'text', - 'label': 'Username', - 'model': 'username', - 'flex': 50 - }, { - 'type': 'password', - 'label': 'Password', - 'model': 'password', - 'flex': 50 - }] - }] + form = [ + { + "type": "row", + "content": [ + {"type": "text", "label": "Host", "model": "host", "flex": 80}, + {"type": "text", "label": "Port", "model": "port", "flex": 20}, + ], + }, + { + "type": "row", + "content": [ + {"type": "text", "label": "Username", "model": "username", "flex": 50}, + { + "type": "password", + "label": "Password", + "model": "password", + "flex": 50, + }, + ], + }, + ] DEFAULT_PORT = 8080 - SUPPORTED_FIELDS = ['download_dir'] + SUPPORTED_FIELDS = ["download_dir"] ADDRESS_FORMAT = "{0}:{1}" - _client = None + + def __init__(self): + self._client = None def get_client(self): if not self._client: - self._client = self._get_client() + self._client = self._create_client() return self._client - def _get_client(self): + def _create_client(self): with DBSession() as db: cred = db.query(QBittorrentCredentials).first() - if not cred: - return False - - if not cred.port: - cred.port = self.DEFAULT_PORT + return None - address = self.ADDRESS_FORMAT.format(cred.host, cred.port) + port = cred.port or self.DEFAULT_PORT + address = self.ADDRESS_FORMAT.format(cred.host, port) - client = Client(host=address, username=cred.username, password=cred.password) - client.app_version() - return QBittorrentClientPlugin._decorate_post(client) + return Client(host=address, username=cred.username, password=cred.password) def get_settings(self): with DBSession() as db: cred = db.query(QBittorrentCredentials).first() if not cred: return None - return {'host': cred.host, 'port': cred.port, 'username': cred.username} + return {"host": cred.host, "port": cred.port, "username": cred.username} def set_settings(self, settings): with DBSession() as db: @@ -95,29 +82,31 @@ def set_settings(self, settings): if not cred: cred = QBittorrentCredentials() db.add(cred) - cred.host = settings['host'] - cred.port = settings.get('port', None) - cred.username = settings.get('username', None) - cred.password = settings.get('password', None) + cred.host = settings["host"] + cred.port = settings.get("port", None) + cred.username = settings.get("username", None) + cred.password = settings.get("password", None) def check_connection(self): client = self.get_client() - client.app_version() - return True + if not client: + return False + try: + client.app_version() + return True + except Exception: + return False def find_torrent(self, torrent_hash): client = self.get_client() if not client: return False - torrents = client.torrents_info(hashes=[torrent_hash.lower()]) + torrents = client.torrents_info(torrent_hashes=torrent_hash.lower()) if torrents: - time = torrents[0].info.added_on - result_date = datetime.fromtimestamp(time, utc) - return { - "name": torrents[0].name, - "date_added": result_date - } + torrent = torrents[0] + result_date = datetime.fromtimestamp(torrent.added_on, utc) + return {"name": torrent.name, "date_added": result_date} return False def get_download_dir(self): @@ -129,51 +118,36 @@ def get_download_dir(self): return six.text_type(result) def add_torrent(self, torrent_content, torrent_settings): - """ - :type torrent_settings: clients.TopicSettings | None - """ client = self.get_client() if not client: return False - savepath = None - auto_tmm = None - if torrent_settings is not None and torrent_settings.download_dir is not None: - savepath = torrent_settings.download_dir - auto_tmm = False + kwargs = {} + if torrent_settings and torrent_settings.download_dir: + kwargs["save_path"] = torrent_settings.download_dir + kwargs["use_auto_torrent_management"] = False - res = client.torrents_add(save_path=savepath, use_auto_torrent_management=auto_tmm, torrent_contents=[('file.torrent', torrent_content)]) - if 'Ok' in res: + result = client.torrents_add(torrent_files=torrent_content, **kwargs) + + if result == "Ok.": torrent = Torrent(torrent_content) torrent_hash = torrent.info_hash - for i in range(0, 10): - found = self.find_torrent(torrent_hash) - if found: + + for _ in range(10): + if self.find_torrent(torrent_hash): return True time.sleep(1) + return True - return res + return False def remove_torrent(self, torrent_hash): client = self.get_client() if not client: return False - client.torrents_delete(hashes=[torrent_hash.lower()]) + client.torrents_delete(delete_files=False, torrent_hashes=torrent_hash.lower()) return True - @staticmethod - def _decorate_post(client): - def _post_decorator(func): - def _post_wrapper(*args, **kwargs): - if 'torrent_contents' in kwargs: - kwargs['files'] = kwargs['torrent_contents'] - del kwargs['torrent_contents'] - return func(*args, **kwargs) - return _post_wrapper - - client._post = _post_decorator(client._post) - return client - -register_plugin('client', 'qbittorrent', QBittorrentClientPlugin()) +register_plugin("client", "qbittorrent", QBittorrentClientPlugin()) diff --git a/requirements.txt b/requirements.txt index 8181c5b6..9bb6a69e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -7,7 +7,7 @@ SQLAlchemy-Enum34==1.0.1 transmissionrpc==0.11 beautifulsoup4==4.7.1 deluge-client==1.7.0 -qbittorrent-api==2023.3.44 +qbittorrent-api==2025.7.0 feedparser==6.0.8 alembic==1.0.8 falcon==1.4.1 From 46bfd0a26105b6b060fd519814bc401c9755878c Mon Sep 17 00:00:00 2001 From: IgorKha <2268209+IgorKha@users.noreply.github.com> Date: Wed, 29 Oct 2025 14:21:19 +0500 Subject: [PATCH 2/2] tests(qbittorrent): Refactor torrent info handling in qbittorrent plugin tests for consistency --- tests/plugins/clients/test_qbittorrent.py | 31 +++++------------------ 1 file changed, 6 insertions(+), 25 deletions(-) diff --git a/tests/plugins/clients/test_qbittorrent.py b/tests/plugins/clients/test_qbittorrent.py index d560c806..d26e3f84 100644 --- a/tests/plugins/clients/test_qbittorrent.py +++ b/tests/plugins/clients/test_qbittorrent.py @@ -68,14 +68,14 @@ def test_find_torrent(self, qbittorrent_client): settings = self.DEFAULT_SETTINGS plugin.set_settings(settings) - torrent_info = new('torrent', {'name': 'Torrent 1', 'info': new('info', {'added_on': date_added.astimezone(pytz.utc).timestamp()})}) + torrent_info = new('torrent', {'name': 'Torrent 1', 'added_on': date_added.astimezone(pytz.utc).timestamp()}) client.torrents_info.return_value = [torrent_info] torrent = plugin.find_torrent(torrent_hash) self.assertEqual({'name': 'Torrent 1', 'date_added': date_added.astimezone(pytz.utc)}, torrent) - client.torrents_info.assert_called_once_with(hashes=[torrent_hash.lower()]) + client.torrents_info.assert_called_once_with(torrent_hashes=torrent_hash.lower()) @patch('monitorrent.plugins.clients.qbittorrent.Client') def test_find_torrent_failed(self, qbittorrent_client): @@ -91,7 +91,7 @@ def test_find_torrent_failed(self, qbittorrent_client): @patch('monitorrent.plugins.clients.qbittorrent.Client') def test_find_torrent_no_settings(self, qbittorrent_client): client = qbittorrent_client.return_value - + torrent_hash = "8347DD6415598A7409DFC3D1AB95078F959BFB93" plugin = QBittorrentClientPlugin() @@ -112,9 +112,7 @@ def test_add_torrent_success(self, qbittorrent_client): torrent_info = [ new('torrent', { 'name': 'Hell.On.Wheels.S05E02.720p.WEB.rus.LostFilm.TV.mp4', - 'info': new('info', { - "added_on": 1616424630 - }) + "added_on": 1616424630 }) ] client.torrents_info.return_value = torrent_info @@ -133,9 +131,7 @@ def test_add_torrent_with_settings_success(self, qbittorrent_client): torrent_info = [ new('torrent', { 'name': 'Hell.On.Wheels.S05E02.720p.WEB.rus.LostFilm.TV.mp4', - 'info': new('info', { - "added_on": 1616424630 - }) + "added_on": 1616424630 }) ] client.torrents_info.return_value = torrent_info @@ -164,7 +160,7 @@ def test_remove_torrent_success(self, qbittorrent_client): torrent = b'torrent' self.assertTrue(plugin.remove_torrent(torrent)) - client.torrents_delete.assert_called_once_with(hashes=[torrent.lower()]) + client.torrents_delete.assert_called_once_with(delete_files=False, torrent_hashes=torrent.lower()) @patch('monitorrent.plugins.clients.qbittorrent.Client') def test_get_download_dir_success(self, qbittorrent_client): @@ -181,18 +177,3 @@ def test_get_download_dir_success(self, qbittorrent_client): assert plugin.get_download_dir() == u'/mnt/media/downloads' client.app_default_save_path.assert_called_once() - - def test_decorate_post_method(self): - client = QBittorrentPluginTest.ClassWithPostMethod() - client = QBittorrentClientPlugin._decorate_post(client) - - (args, kwargs) = client._post(torrent_contents=[('file.torrent', b'torrent')]) - - assert len(args) == 0 - assert len(kwargs) == 1 - assert 'files' in kwargs - assert kwargs['files'] == [('file.torrent', b'torrent')] - - class ClassWithPostMethod(object): - def _post(self, *args, **kwargs): - return (args, kwargs)