From 51ecfab5e7769f058c6a3b0e1384c02105ebfe43 Mon Sep 17 00:00:00 2001 From: Arathen Date: Wed, 13 Feb 2019 00:46:12 +1100 Subject: [PATCH] IMP: Update all qBittorrent WebAPI paths for client v4.1.0+. IMP: New functions to add torrents to qBittorrent by URL and file. IMP: Remove variable to distinguish between old and new WebAPI in qBittorrent --- data/interfaces/default/config.html | 8 +- lib/qbittorrent/client.py | 178 +++++++++++++--------------- mylar/config.py | 1 - mylar/webserve.py | 3 +- 4 files changed, 87 insertions(+), 103 deletions(-) diff --git a/data/interfaces/default/config.html b/data/interfaces/default/config.html index 0d301f41..d80baab9 100755 --- a/data/interfaces/default/config.html +++ b/data/interfaces/default/config.html @@ -704,21 +704,17 @@ Folder path where torrents will be assigned to
- Add torrent using client default action + Add torrent only
Add torrent and force start
- Add torrent paused + Add torrent and pause
qBittorrent "Add torrent" options
-
- - Use the new qBittorrent client WebAPI available since v4.1.0 -
diff --git a/lib/qbittorrent/client.py b/lib/qbittorrent/client.py index 73d8d753..6809144d 100644 --- a/lib/qbittorrent/client.py +++ b/lib/qbittorrent/client.py @@ -1,7 +1,6 @@ import requests import json - class LoginRequired(Exception): def __str__(self): return 'Please login first.' @@ -15,7 +14,7 @@ class Client(object): self.url = url session = requests.Session() - check_prefs = session.get(url+'query/preferences') + check_prefs = session.get(url+'api/v2/app/preferences') if check_prefs.status_code == 200: self._is_authenticated = True @@ -24,9 +23,9 @@ class Client(object): elif check_prefs.status_code == 404: self._is_authenticated = False raise RuntimeError(""" - This wrapper only supports qBittorrent applications - with version higher than 3.1.x. - Please use the latest qBittorrent release. + This wrapper only supports qBittorrent applications with + version higher than 4.1.0 (which implemented Web API v2.0). + Please use the latest qBittorrent release. """) else: @@ -104,7 +103,7 @@ class Client(object): :return: Response to login request to the API. """ self.session = requests.Session() - login = self.session.post(self.url+'login', + login = self.session.post(self.url+'api/v2/auth/login', data={'username': username, 'password': password}) if login.text == 'Ok.': @@ -116,7 +115,7 @@ class Client(object): """ Logout the current session. """ - response = self._get('logout') + response = self._get('api/v2/auth/logout') self._is_authenticated = False return response @@ -125,27 +124,20 @@ class Client(object): """ Get qBittorrent version. """ - return self._get('version/qbittorrent') + return self._get('api/v2/app/version') @property def api_version(self): """ Get WEB API version. """ - return self._get('version/api') - - @property - def api_min_version(self): - """ - Get minimum WEB API version. - """ - return self._get('version/api_min') + return self._get('api/v2/app/webapiVersion') def shutdown(self): """ Shutdown qBittorrent. """ - return self._get('command/shutdown') + return self._get('api/v2/app/shutdown') def torrents(self, **filters): """ @@ -157,6 +149,7 @@ class Client(object): :param reverse: Enable reverse sorting. :param limit: Limit the number of torrents returned. :param offset: Set offset (if less than 0, offset from end). + :param hashes: Filter by hashes. Can contain multiple hashes separated by |. :return: list() of torrent with matching filter. """ @@ -166,7 +159,7 @@ class Client(object): name = 'filter' if name == 'status' else name params[name] = value - return self._get('query/torrents', params=params) + return self._get('api/v2/torrents/info', params=params) def get_torrent(self, infohash): """ @@ -174,7 +167,7 @@ class Client(object): :param infohash: INFO HASH of the torrent. """ - return self._get('query/propertiesGeneral/' + infohash.lower()) + return self._get('api/v2/torrents/properties', params={'hash': infohash.lower()}) def get_torrent_trackers(self, infohash): """ @@ -182,7 +175,7 @@ class Client(object): :param infohash: INFO HASH of the torrent. """ - return self._get('query/propertiesTrackers/' + infohash.lower()) + return self._get('api/v2/torrents/trackers', params={'hash': infohash.lower()}) def get_torrent_webseeds(self, infohash): """ @@ -190,7 +183,7 @@ class Client(object): :param infohash: INFO HASH of the torrent. """ - return self._get('query/propertiesWebSeeds/' + infohash.lower()) + return self._get('api/v2/torrents/webseeds', params={'hash': infohash.lower()}) def get_torrent_files(self, infohash): """ @@ -198,14 +191,14 @@ class Client(object): :param infohash: INFO HASH of the torrent. """ - return self._get('query/propertiesFiles/' + infohash.lower()) + return self._get('api/v2/torrents/files', params={'hash': infohash.lower()}) @property def global_transfer_info(self): """ Get JSON data of the global transfer info of qBittorrent. """ - return self._get('query/transferInfo') + return self._get('api/v2/transfer/info') @property def preferences(self): @@ -228,7 +221,7 @@ class Client(object): qb.preferences() """ - prefs = self._get('query/preferences') + prefs = self._get('api/v2/app/preferences') class Proxy(Client): """ @@ -270,11 +263,11 @@ class Client(object): def sync(self, rid=0): """ Sync the torrents by supplied LAST RESPONSE ID. - Read more @ http://git.io/vEgXr + Read more @ https://git.io/fxgB8 :param rid: Response ID of last request. """ - return self._get('sync/maindata', params={'rid': rid}) + return self._get('api/v2/sync/maindata', params={'rid': rid}) def download_from_link(self, link, **kwargs): """ @@ -286,22 +279,20 @@ class Client(object): :return: Empty JSON data. """ - # old:new format - old_arg_map = {'save_path': 'savepath'} # , 'label': 'category'} - - # convert old option names to new option names - options = kwargs.copy() - for old_arg, new_arg in old_arg_map.items(): - if options.get(old_arg) and not options.get(new_arg): - options[new_arg] = options[old_arg] - - options['urls'] = link - - # workaround to send multipart/formdata request - # http://stackoverflow.com/a/23131823/4726598 - dummy_file = {'_dummy': (None, '_dummy')} - - return self._post('command/download', data=options, files=dummy_file) + # qBittorrent requires adds to be done with multipath/form-data + # POST requests for both URLs and .torrent files. Info on this + # can be found here, and here: + # http://docs.python-requests.org/en/master/user/quickstart/#post-a-multipart-encoded-file + # http://docs.python-requests.org/en/master/user/advanced/#post-multiple-multipart-encoded-files + if isinstance(link, list): + links = '\n'.join(link) + else: + links = link + torrent_data = {} + torrent_data['urls'] = (None, links) + for k, v in kwargs.iteritems(): + torrent_data[k] = (None, v) + return self._post('api/v2/torrents/add', data=None, files=torrent_data) def download_from_file(self, file_buffer, **kwargs): """ @@ -313,18 +304,23 @@ class Client(object): :return: Empty JSON data. """ + # qBittorrent requires adds to be done with multipath/form-data + # POST requests for both URLs and .torrent files. Info on this + # can be found here, and here: + # http://docs.python-requests.org/en/master/user/quickstart/#post-a-multipart-encoded-file + # http://docs.python-requests.org/en/master/user/advanced/#post-multiple-multipart-encoded-files if isinstance(file_buffer, list): - torrent_files = {} - for i, f in enumerate(file_buffer): - torrent_files.update({'torrents%s' % i: f}) + torrent_data = [] + for f in file_buffer: + fname = f.name + torrent_data.append(('torrents', (fname, f))) else: - torrent_files = {'torrents': file_buffer} + fname = file_buffer.name + torrent_data = [('torrents', (fname, file_buffer))] + for k, v in kwargs.iteritems(): + torrent_data.append((k, (None, v))) - data = kwargs.copy() - - if data.get('save_path'): - data.update({'savepath': data['save_path']}) - return self._post('command/upload', data=data, files=torrent_files) + return self._post('api/v2/torrents/add', data=None, files=torrent_data) def add_trackers(self, infohash, trackers): """ @@ -335,7 +331,7 @@ class Client(object): """ data = {'hash': infohash.lower(), 'urls': trackers} - return self._post('command/addTrackers', data=data) + return self._post('api/v2/torrents/addTrackers', data=data) @staticmethod def _process_infohash_list(infohash_list): @@ -356,13 +352,13 @@ class Client(object): :param infohash: INFO HASH of torrent. """ - return self._post('command/pause', data={'hash': infohash.lower()}) + return self._post('api/v2/torrents/pause', data={'hashes': infohash.lower()}) def pause_all(self): """ Pause all torrents. """ - return self._get('command/pauseAll') + return self._post('api/v2/torrents/pause', data={'hashes': 'all'}) def pause_multiple(self, infohash_list): """ @@ -371,18 +367,7 @@ class Client(object): :param infohash_list: Single or list() of infohashes. """ data = self._process_infohash_list(infohash_list) - return self._post('command/pauseAll', data=data) - - def set_label(self, infohash_list, label): - """ - Set the label on multiple torrents. - IMPORTANT: OLD API method, kept as it is to avoid breaking stuffs. - - :param infohash_list: Single or list() of infohashes. - """ - data = self._process_infohash_list(infohash_list) - data['label'] = label - return self._post('command/setLabel', data=data) + return self._post('api/v2/torrents/pause', data=data) def set_category(self, infohash_list, category): """ @@ -392,7 +377,7 @@ class Client(object): """ data = self._process_infohash_list(infohash_list) data['category'] = category - return self._post('command/setCategory', data=data) + return self._post('api/v2/torrents/setCategory', data=data) def resume(self, infohash): """ @@ -400,13 +385,13 @@ class Client(object): :param infohash: INFO HASH of torrent. """ - return self._post('command/resume', data={'hash': infohash.lower()}) + return self._post('api/v2/torrents/resume', data={'hashes': infohash.lower()}) def resume_all(self): """ Resume all torrents. """ - return self._get('command/resumeAll') + return self._get('api/v2/torrents/resume', data={'hashes': 'all'}) def resume_multiple(self, infohash_list): """ @@ -415,7 +400,7 @@ class Client(object): :param infohash_list: Single or list() of infohashes. """ data = self._process_infohash_list(infohash_list) - return self._post('command/resumeAll', data=data) + return self._post('api/v2/torrents/resume', data=data) def delete(self, infohash_list): """ @@ -424,16 +409,21 @@ class Client(object): :param infohash_list: Single or list() of infohashes. """ data = self._process_infohash_list(infohash_list) - return self._post('command/delete', data=data) + data['deleteFiles'] = 'false' + return self._post('api/v2/torrents/delete', data=data) def delete_permanently(self, infohash_list): """ Permanently delete torrents. + *** WARNING : This will instruct qBittorrent to delete files + *** from your hard disk. Use with caution. + :param infohash_list: Single or list() of infohashes. """ data = self._process_infohash_list(infohash_list) - return self._post('command/deletePerm', data=data) + data['deleteFiles'] = 'true' + return self._post('api/v2/torrents/delete', data=data) def recheck(self, infohash_list): """ @@ -442,7 +432,7 @@ class Client(object): :param infohash_list: Single or list() of infohashes. """ data = self._process_infohash_list(infohash_list) - return self._post('command/recheck', data=data) + return self._post('api/v2/torrents/recheck', data=data) def increase_priority(self, infohash_list): """ @@ -451,7 +441,7 @@ class Client(object): :param infohash_list: Single or list() of infohashes. """ data = self._process_infohash_list(infohash_list) - return self._post('command/increasePrio', data=data) + return self._post('api/v2/torrents/increasePrio', data=data) def decrease_priority(self, infohash_list): """ @@ -460,7 +450,7 @@ class Client(object): :param infohash_list: Single or list() of infohashes. """ data = self._process_infohash_list(infohash_list) - return self._post('command/decreasePrio', data=data) + return self._post('api/v2/torrents/decreasePrio', data=data) def set_max_priority(self, infohash_list): """ @@ -469,7 +459,7 @@ class Client(object): :param infohash_list: Single or list() of infohashes. """ data = self._process_infohash_list(infohash_list) - return self._post('command/topPrio', data=data) + return self._post('api/v2/torrents/topPrio', data=data) def set_min_priority(self, infohash_list): """ @@ -478,7 +468,7 @@ class Client(object): :param infohash_list: Single or list() of infohashes. """ data = self._process_infohash_list(infohash_list) - return self._post('command/bottomPrio', data=data) + return self._post('api/v2/torrents/bottomPrio', data=data) def set_file_priority(self, infohash, file_id, priority): """ @@ -488,7 +478,7 @@ class Client(object): :param file_id: ID of the file to set priority. :param priority: Priority level of the file. """ - if priority not in [0, 1, 2, 7]: + if priority not in [0, 1, 6, 7]: raise ValueError("Invalid priority, refer WEB-UI docs for info.") elif not isinstance(file_id, int): raise TypeError("File ID must be an int") @@ -497,7 +487,7 @@ class Client(object): 'id': file_id, 'priority': priority} - return self._post('command/setFilePrio', data=data) + return self._post('api/v2/torrents/filePrio', data=data) # Get-set global download and upload speed limits. @@ -505,7 +495,7 @@ class Client(object): """ Get global download speed limit. """ - return self._get('command/getGlobalDlLimit') + return self._get('api/v2/transfer/downloadLimit') def set_global_download_limit(self, limit): """ @@ -513,7 +503,7 @@ class Client(object): :param limit: Speed limit in bytes. """ - return self._post('command/setGlobalDlLimit', data={'limit': limit}) + return self._post('api/v2/transfer/setDownloadLimit', data={'limit': limit}) global_download_limit = property(get_global_download_limit, set_global_download_limit) @@ -522,7 +512,7 @@ class Client(object): """ Get global upload speed limit. """ - return self._get('command/getGlobalUpLimit') + return self._get('api/v2/transfer/uploadLimit') def set_global_upload_limit(self, limit): """ @@ -530,7 +520,7 @@ class Client(object): :param limit: Speed limit in bytes. """ - return self._post('command/setGlobalUpLimit', data={'limit': limit}) + return self._post('api/v2/transfer/setUploadLimit', data={'limit': limit}) global_upload_limit = property(get_global_upload_limit, set_global_upload_limit) @@ -543,7 +533,7 @@ class Client(object): :param infohash_list: Single or list() of infohashes. """ data = self._process_infohash_list(infohash_list) - return self._post('command/getTorrentsDlLimit', data=data) + return self._post('api/v2/torrents/downloadLimit', data=data) def set_torrent_download_limit(self, infohash_list, limit): """ @@ -554,7 +544,7 @@ class Client(object): """ data = self._process_infohash_list(infohash_list) data.update({'limit': limit}) - return self._post('command/setTorrentsDlLimit', data=data) + return self._post('api/v2/torrents/setDownloadLimit', data=data) def get_torrent_upload_limit(self, infohash_list): """ @@ -563,7 +553,7 @@ class Client(object): :param infohash_list: Single or list() of infohashes. """ data = self._process_infohash_list(infohash_list) - return self._post('command/getTorrentsUpLimit', data=data) + return self._post('api/v2/torrents/uploadLimit', data=data) def set_torrent_upload_limit(self, infohash_list, limit): """ @@ -574,26 +564,26 @@ class Client(object): """ data = self._process_infohash_list(infohash_list) data.update({'limit': limit}) - return self._post('command/setTorrentsUpLimit', data=data) + return self._post('api/v2/torrents/setUploadLimit', data=data) # setting preferences def set_preferences(self, **kwargs): """ Set preferences of qBittorrent. - Read all possible preferences @ http://git.io/vEgDQ + Read all possible preferences @ https://git.io/fx2Y9 :param kwargs: set preferences in kwargs form. """ json_data = "json={}".format(json.dumps(kwargs)) headers = {'content-type': 'application/x-www-form-urlencoded'} - return self._post('command/setPreferences', data=json_data, + return self._post('api/v2/app/setPreferences', data=json_data, headers=headers) def get_alternative_speed_status(self): """ Get Alternative speed limits. (1/0) """ - return self._get('command/alternativeSpeedLimitsEnabled') + return self._get('api/v2/transfer/speedLimitsMode') alternative_speed_status = property(get_alternative_speed_status) @@ -601,7 +591,7 @@ class Client(object): """ Toggle alternative speed limits. """ - return self._get('command/toggleAlternativeSpeedLimits') + return self._get('api/v2/transfer/toggleSpeedLimitsMode') def toggle_sequential_download(self, infohash_list): """ @@ -610,7 +600,7 @@ class Client(object): :param infohash_list: Single or list() of infohashes. """ data = self._process_infohash_list(infohash_list) - return self._post('command/toggleSequentialDownload', data=data) + return self._post('api/v2/torrents/toggleSequentialDownload', data=data) def toggle_first_last_piece_priority(self, infohash_list): """ @@ -619,7 +609,7 @@ class Client(object): :param infohash_list: Single or list() of infohashes. """ data = self._process_infohash_list(infohash_list) - return self._post('command/toggleFirstLastPiecePrio', data=data) + return self._post('api/v2/torrents/toggleFirstLastPiecePrio', data=data) def force_start(self, infohash_list, value=True): """ @@ -630,4 +620,4 @@ class Client(object): """ data = self._process_infohash_list(infohash_list) data.update({'value': json.dumps(value)}) - return self._post('command/setForceStart', data=data) + return self._post('api/v2/torrents/setForceStart', data=data) diff --git a/mylar/config.py b/mylar/config.py index 203e93ef..8cb47788 100644 --- a/mylar/config.py +++ b/mylar/config.py @@ -349,7 +349,6 @@ _CONFIG_DEFINITIONS = OrderedDict({ 'QBITTORRENT_LABEL': (str, 'qBittorrent', None), 'QBITTORRENT_FOLDER': (str, 'qBittorrent', None), 'QBITTORRENT_LOADACTION': (int, 'qBittorrent', 0), #0': client default, #1': force start, #2': add paused - 'QBITTORRENT_NEWAPI': (bool, 'qBittorrent', True), 'OPDS_ENABLE': (bool, 'OPDS', False), 'OPDS_AUTHENTICATION': (bool, 'OPDS', False), diff --git a/mylar/webserve.py b/mylar/webserve.py index d3f36fbc..9abccf5e 100644 --- a/mylar/webserve.py +++ b/mylar/webserve.py @@ -4742,7 +4742,6 @@ class WebInterface(object): "qbittorrent_clientdefault": helpers.radio(mylar.CONFIG.QBITTORRENT_LOADACTION, 0), "qbittorrent_forcestart": helpers.radio(mylar.CONFIG.QBITTORRENT_LOADACTION, 1), "qbittorrent_addpaused": helpers.radio(mylar.CONFIG.QBITTORRENT_LOADACTION, 2), - "qbittorrent_newapi": helpers.checked(mylar.CONFIG.QBITTORRENT_NEWAPI), "blackhole_dir": mylar.CONFIG.BLACKHOLE_DIR, "usenet_retention": mylar.CONFIG.USENET_RETENTION, "nzbsu": helpers.checked(mylar.CONFIG.NZBSU), @@ -5107,7 +5106,7 @@ class WebInterface(object): def configUpdate(self, **kwargs): checked_configs = ['enable_https', 'launch_browser', 'syno_fix', 'auto_update', 'annuals_on', 'api_enabled', 'nzb_startup_search', 'enforce_perms', 'sab_to_mylar', 'torrent_local', 'torrent_seedbox', 'rtorrent_ssl', 'rtorrent_verify', 'rtorrent_startonload', - 'enable_torrents', 'qbittorrent_newapi', 'enable_rss', 'nzbsu', 'nzbsu_verify', + 'enable_torrents', 'enable_rss', 'nzbsu', 'nzbsu_verify', 'dognzb', 'dognzb_verify', 'experimental', 'enable_torrent_search', 'enable_public', 'enable_32p', 'enable_torznab', 'newznab', 'use_minsize', 'use_maxsize', 'ddump', 'failed_download_handling', 'sab_client_post_processing', 'nzbget_client_post_processing', 'failed_auto', 'post_processing', 'enable_check_folder', 'enable_pre_scripts', 'enable_snatch_script', 'enable_extra_scripts',