diff --git a/headphones/qbittorrent.py b/headphones/qbittorrent.py old mode 100644 new mode 100755 index ef499a21..00cccde1 --- a/headphones/qbittorrent.py +++ b/headphones/qbittorrent.py @@ -28,6 +28,8 @@ import headphones from headphones import logger from collections import namedtuple +from qbittorrentv2 import Client + class qbittorrentclient(object): @@ -49,9 +51,20 @@ class qbittorrentclient(object): self.base_url = host self.username = headphones.CONFIG.QBITTORRENT_USERNAME self.password = headphones.CONFIG.QBITTORRENT_PASSWORD - self.cookiejar = cookielib.CookieJar() - self.opener = self._make_opener() - self._get_sid(self.base_url, self.username, self.password) + + # Try new v2 api + try: + self.qb = Client(self.base_url) + login_text = self.qb.login(self.username, self.password) + if login_text: + logger.warning("Could not login to qBittorrent v2 api, check credentials: %s", login_text) + self.version = 2 + except Exception as e: + logger.warning("Error with qBittorrent v2 api, check settings or update, will try v1: %s" % e) + self.cookiejar = cookielib.CookieJar() + self.opener = self._make_opener() + self._get_sid(self.base_url, self.username, self.password) + self.version = 1 def _make_opener(self): # create opener with cookie handler to carry QBitTorrent SID cookie @@ -165,12 +178,22 @@ def removeTorrent(hash, remove_data=False): logger.debug('removeTorrent(%s,%s)' % (hash, remove_data)) qbclient = qbittorrentclient() - status, torrentList = qbclient._get_list() - for torrent in torrentList: - if torrent['hash'].upper() == hash.upper(): - if torrent['state'] == 'uploading' or torrent['state'] == 'stalledUP': - logger.info('%s has finished seeding, removing torrent and data' % torrent['name']) - qbclient.remove(hash, remove_data) + if qbclient.version == 2: + torrentlist = qbclient.qb.torrents(hashes=hash.lower()) + else: + status, torrentlist = qbclient._get_list() + for torrent in torrentlist: + if torrent['hash'].lower() == hash.lower(): + if torrent['ratio'] >= torrent['ratio_limit'] and torrent['ratio_limit'] >= 0: + if qbclient.version == 2: + if remove_data: + logger.info('%s has finished seeding, removing torrent and data' % torrent['name']) + qbclient.qb.delete_permanently(hash) + else: + logger.info('%s has finished seeding, removing torrent' % torrent['name']) + qbclient.qb.delete(hash) + else: + qbclient.remove(hash, remove_data) return True else: logger.info('%s has not finished seeding yet, torrent will not be removed, will try again on next run' % torrent['name']) @@ -182,20 +205,27 @@ def addTorrent(link): logger.debug('addTorrent(%s)' % link) qbclient = qbittorrentclient() - args = {'urls': link, 'savepath': headphones.CONFIG.DOWNLOAD_TORRENT_DIR} - if headphones.CONFIG.QBITTORRENT_LABEL: - args['category'] = headphones.CONFIG.QBITTORRENT_LABEL + if qbclient.version == 2: + return qbclient.qb.download_from_link(link, savepath=headphones.CONFIG.DOWNLOAD_TORRENT_DIR, + category=headphones.CONFIG.QBITTORRENT_LABEL) + else: + args = {'urls': link, 'savepath': headphones.CONFIG.DOWNLOAD_TORRENT_DIR} + if headphones.CONFIG.QBITTORRENT_LABEL: + args['category'] = headphones.CONFIG.QBITTORRENT_LABEL - return qbclient._command('command/download', args, 'multipart/form-data') + return qbclient._command('command/download', args, 'multipart/form-data') def addFile(data): logger.debug('addFile(data)') qbclient = qbittorrentclient() - files = {'torrents': {'filename': '', 'content': data}} - - return qbclient._command('command/upload', filelist=files) + if qbclient.version == 2: + return qbclient.qb.download_from_file(data, savepath=headphones.CONFIG.DOWNLOAD_TORRENT_DIR, + category=headphones.CONFIG.QBITTORRENT_LABEL) + else: + files = {'torrents': {'filename': '', 'content': data}} + return qbclient._command('command/upload', filelist=files) def getName(hash): @@ -206,7 +236,10 @@ def getName(hash): tries = 1 while tries <= 6: time.sleep(10) - status, torrentlist = qbclient._get_list() + if qbclient.version == 2: + torrentlist = qbclient.qb.torrents(hashes=hash.lower()) + else: + status, torrentlist = qbclient._get_list() for torrent in torrentlist: if torrent['hash'].lower() == hash.lower(): return torrent['name'] @@ -224,7 +257,10 @@ def getFolder(hash): qbclient = qbittorrentclient() try: - status, torrent_files = qbclient.getfiles(hash.lower()) + if qbclient.version == 2: + torrent_files = qbclient.qb.get_torrent_files(hash.lower()) + else: + status, torrent_files = qbclient.getfiles(hash.lower()) if torrent_files: if len(torrent_files) == 1: torrent_folder = torrent_files[0]['name'] @@ -240,6 +276,30 @@ def getFolder(hash): return torrent_folder, single_file +def setSeedRatio(hash, ratio): + logger.debug('setSeedRatio(%s)' % hash) + + qbclient = qbittorrentclient() + + if qbclient.version == 2: + ratio = -1 if ratio == 0 else ratio + return qbclient.qb.set_share_ratio(hash.lower(), ratio) + else: + logger.warn('setSeedRatio only available with qBittorrent v2 api') + return + + +def apiVersion2(): + logger.debug('getApiVersion') + + qbclient = qbittorrentclient() + + if qbclient.version == 2: + return True + else: + return False + + _BOUNDARY_CHARS = string.digits + string.ascii_letters diff --git a/headphones/searcher.py b/headphones/searcher.py index d7536b22..499b69b8 100644 --- a/headphones/searcher.py +++ b/headphones/searcher.py @@ -1007,7 +1007,10 @@ def send_to_downloader(data, bestqual, album): # Add torrent if bestqual[3] == 'rutracker.org': - ruobj.qbittorrent_add_file(data) + if qbittorrent.apiVersion2: + qbittorrent.addFile(data) + else: + ruobj.qbittorrent_add_file(data) else: qbittorrent.addTorrent(bestqual[2]) @@ -1026,6 +1029,11 @@ def send_to_downloader(data, bestqual, album): logger.error('Torrent name could not be determined') return + # Set Seed Ratio + seed_ratio = get_seed_ratio(bestqual[3]) + if seed_ratio is not None: + qbittorrent.setSeedRatio(torrentid, seed_ratio) + myDB = db.DBConnection() myDB.action('UPDATE albums SET status = "Snatched" WHERE AlbumID=?', [album['AlbumID']]) myDB.action('INSERT INTO snatched VALUES( ?, ?, ?, ?, DATETIME("NOW", "localtime"), ?, ?, ?, ?)', diff --git a/lib/qbittorrentv2/__init__.py b/lib/qbittorrentv2/__init__.py new file mode 100755 index 00000000..d3991a9f --- /dev/null +++ b/lib/qbittorrentv2/__init__.py @@ -0,0 +1 @@ +from qbittorrentv2.client import Client diff --git a/lib/qbittorrentv2/client.py b/lib/qbittorrentv2/client.py new file mode 100755 index 00000000..4131d293 --- /dev/null +++ b/lib/qbittorrentv2/client.py @@ -0,0 +1,800 @@ +import requests +import json + + +class LoginRequired(Exception): + def __str__(self): + return 'Please login first.' + + +class Client(object): + """class to interact with qBittorrent WEB API""" + def __init__(self, url, verify=True): + """ + Initialize the client + + :param url: Base URL of the qBittorrent WEB API + :param verify: Boolean to specify if SSL verification should be done. + Defaults to True. + """ + if not url.endswith('/'): + url += '/' + self.url = url + 'api/v2/' + self.verify = verify + + session = requests.Session() + prefs_url = self.url + 'app/preferences' + check_prefs = session.get(prefs_url, verify=self.verify) + if check_prefs.status_code == 200: + self._is_authenticated = True + self.session = session + + elif check_prefs.status_code == 404: + self._is_authenticated = False + raise RuntimeError(""" + This wrapper only supports qBittorrent applications + with version higher than 4.1.x. + Please use the latest qBittorrent release. + """) + + else: + self._is_authenticated = False + + def _get(self, endpoint, **kwargs): + """ + Method to perform GET request on the API. + + :param endpoint: Endpoint of the API. + :param kwargs: Other keyword arguments for requests. + + :return: Response of the GET request. + """ + return self._request(endpoint, 'get', **kwargs) + + def _post(self, endpoint, data, **kwargs): + """ + Method to perform POST request on the API. + + :param endpoint: Endpoint of the API. + :param data: POST DATA for the request. + :param kwargs: Other keyword arguments for requests. + + :return: Response of the POST request. + """ + return self._request(endpoint, 'post', data, **kwargs) + + def _request(self, endpoint, method, data=None, **kwargs): + """ + Method to hanle both GET and POST requests. + + :param endpoint: Endpoint of the API. + :param method: Method of HTTP request. + :param data: POST DATA for the request. + :param kwargs: Other keyword arguments. + + :return: Response for the request. + """ + final_url = self.url + endpoint + + if not self._is_authenticated: + raise LoginRequired + + kwargs['verify'] = self.verify + if method == 'get': + request = self.session.get(final_url, **kwargs) + else: + request = self.session.post(final_url, data, **kwargs) + + request.raise_for_status() + request.encoding = 'utf_8' + + if len(request.text) == 0: + data = json.loads('{}') + else: + try: + data = json.loads(request.text) + except ValueError: + data = request.text + + return data + + def login(self, username='admin', password='admin'): + """ + Method to authenticate the qBittorrent Client. + + Declares a class attribute named ``session`` which + stores the authenticated session if the login is correct. + Else, shows the login error. + + :param username: Username. + :param password: Password. + + :return: Response to login request to the API. + """ + self.session = requests.Session() + login = self.session.post(self.url + 'auth/login', + data={'username': username, + 'password': password}, + verify=self.verify) + if login.text == 'Ok.': + self._is_authenticated = True + else: + return login.text + + def logout(self): + """ + Logout the current session. + """ + response = self._get('auth/logout') + self._is_authenticated = False + return response + + @property + def qbittorrent_version(self): + """ + Get qBittorrent version. + """ + return self._get('app/version') + + @property + def api_version(self): + """ + Get WEB API version. + """ + return self._get('app/webapiVersion') + + def shutdown(self): + """ + Shutdown qBittorrent. + """ + return self._get('app/shutdown') + + def get_default_save_path(self): + """ + Get default save path. + """ + return self._get('app/defaultSavePath') + + def get_log(self, **params): + """ + Returns a list of log entries matching the supplied params. + + :param normal: Include normal messages (default: true). + :param info: Include info messages (default: true). + :param warning: Include warning messages (default: true). + :param critical: Include critical messages (default: true). + :param last_known_id: Exclude messages with "message id" <= last_known_id (default: -1). + + :return: list(). + For example: qb.get_log(normal='true', info='true') + """ + return self._get('log/main', params=params) + + def torrents(self, **filters): + """ + Returns a list of torrents matching the supplied filters. + + :param filter: Current status of the torrents. + :param category: Fetch all torrents with the supplied label. + :param sort: Sort torrents by. + :param reverse: Enable reverse sorting. + :param limit: Limit the number of torrents returned. + :param offset: Set offset (if less than 0, offset from end). + + :return: list() of torrent with matching filter. + For example: qb.torrents(filter='downloading', sort='ratio'). + """ + params = {} + for name, value in filters.items(): + # make sure that old 'status' argument still works + name = 'filter' if name == 'status' else name + params[name] = value + + return self._get('torrents/info', params=params) + + def get_torrent(self, infohash): + """ + Get details of the torrent. + + :param infohash: INFO HASH of the torrent. + """ + return self._get('torrents/properties?hash=' + infohash.lower()) + + def get_torrent_trackers(self, infohash): + """ + Get trackers for the torrent. + + :param infohash: INFO HASH of the torrent. + """ + return self._get('torrents/trackers?hash=' + infohash.lower()) + + def get_torrent_webseeds(self, infohash): + """ + Get webseeds for the torrent. + + :param infohash: INFO HASH of the torrent. + """ + return self._get('torrents/webseeds?hash=' + infohash.lower()) + + def get_torrent_files(self, infohash): + """ + Get list of files for the torrent. + + :param infohash: INFO HASH of the torrent. + """ + return self._get('torrents/files?hash=' + infohash.lower()) + + def get_torrent_piece_states(self, infohash): + """ + Get list of all pieces (in order) of a specific torrent. + + :param infohash: INFO HASH of the torrent. + :return: array of states (integers). + """ + return self._get('torrents/pieceStates?hash=' + infohash.lower()) + + def get_torrent_piece_hashes(self, infohash): + """ + Get list of all hashes (in order) of a specific torrent. + + :param infohash: INFO HASH of the torrent. + :return: array of hashes (strings). + """ + return self._get('torrents/pieceHashes?hash=' + infohash.lower()) + + @property + def global_transfer_info(self): + """ + :return: dict{} of the global transfer info of qBittorrent. + + """ + return self._get('transfer/info') + + @property + def preferences(self): + """ + Get the current qBittorrent preferences. + Can also be used to assign individual preferences. + For setting multiple preferences at once, + see ``set_preferences`` method. + + Note: Even if this is a ``property``, + to fetch the current preferences dict, you are required + to call it like a bound method. + + Wrong:: + + qb.preferences + + Right:: + + qb.preferences() + + """ + prefs = self._get('app/preferences') + + class Proxy(Client): + """ + Proxy class to to allow assignment of individual preferences. + this class overrides some methods to ease things. + + Because of this, settings can be assigned like:: + + In [5]: prefs = qb.preferences() + + In [6]: prefs['autorun_enabled'] + Out[6]: True + + In [7]: prefs['autorun_enabled'] = False + + In [8]: prefs['autorun_enabled'] + Out[8]: False + + """ + + def __init__(self, url, prefs, auth, session): + self.url = url + self.prefs = prefs + self._is_authenticated = auth + self.session = session + + def __getitem__(self, key): + return self.prefs[key] + + def __setitem__(self, key, value): + kwargs = {key: value} + return self.set_preferences(**kwargs) + + def __call__(self): + return self.prefs + + return Proxy(self.url, prefs, self._is_authenticated, self.session) + + def sync_main_data(self, rid=0): + """ + Sync the torrents main data by supplied LAST RESPONSE ID. + Read more @ https://git.io/fxgB8 + + :param rid: Response ID of last request. + """ + return self._get('sync/maindata', params={'rid': rid}) + + def sync_peers_data(self, infohash, rid=0): + """ + Sync the torrent peers data by supplied LAST RESPONSE ID. + Read more @ https://git.io/fxgBg + + :param infohash: INFO HASH of torrent. + :param rid: Response ID of last request. + """ + params = {'hash': infohash.lower(), 'rid': rid} + return self._get('sync/torrentPeers', params=params) + + def download_from_link(self, link, **kwargs): + """ + Download torrent using a link. + + :param link: URL Link or list of. + :param savepath: Path to download the torrent. + :param category: Label or Category of the torrent(s). + + :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] + + if isinstance(link, list): + options['urls'] = "\n".join(link) + else: + options['urls'] = link + + # workaround to send multipart/formdata request + # http://stackoverflow.com/a/23131823/4726598 + dummy_file = {'_dummy': (None, '_dummy')} + + return self._post('torrents/add', data=options, files=dummy_file) + + def download_from_file(self, file_buffer, **kwargs): + """ + Download torrent using a file. + + :param file_buffer: Single file() buffer or list of. + :param save_path: Path to download the torrent. + :param label: Label of the torrent(s). + + :return: Empty JSON data. + """ + if isinstance(file_buffer, list): + torrent_files = {} + for i, f in enumerate(file_buffer): + torrent_files.update({'torrents%s' % i: f}) + else: + torrent_files = {'torrents': file_buffer} + + data = kwargs.copy() + + if data.get('save_path'): + data.update({'savepath': data['save_path']}) + return self._post('torrents/add', data=data, files=torrent_files) + + def add_trackers(self, infohash, trackers): + """ + Add trackers to a torrent. + + :param infohash: INFO HASH of torrent. + :param trackers: Trackers. + :note %0A (aka LF newline) between trackers. Ampersand in tracker urls MUST be escaped. + """ + data = {'hash': infohash.lower(), + 'urls': trackers} + return self._post('torrents/addTrackers', data=data) + + def set_torrent_location(self, infohash_list, location): + """ + Set the location for the torrent + + :param infohash: INFO HASH of torrent. + :param location: /mnt/nfs/media. + """ + data = self._process_infohash_list(infohash_list) + data['location'] = location + return self._post('torrents/setLocation', data=data) + + def set_torrent_name(self, infohash, name): + """ + Set the name for the torrent + + :param infohash: INFO HASH of torrent. + :param name: Whatever_name_you_want. + """ + data = {'hash': infohash.lower(), + 'name': name} + return self._post('torrents/rename', data=data) + + @staticmethod + def _process_infohash_list(infohash_list): + """ + Method to convert the infohash_list to qBittorrent API friendly values. + + :param infohash_list: List of infohash. + """ + if isinstance(infohash_list, list): + data = {'hashes': '|'.join([h.lower() for h in infohash_list])} + else: + data = {'hashes': infohash_list.lower()} + return data + + def pause(self, infohash): + """ + Pause a torrent. + + :param infohash: INFO HASH of torrent. + """ + return self._post('torrents/pause', data={'hashes': infohash.lower()}) + + def pause_all(self): + """ + Pause all torrents. + """ + return self._post('torrents/pause', data={'hashes': 'all'}) + + def pause_multiple(self, infohash_list): + """ + Pause multiple torrents. + + :param infohash_list: Single or list() of infohashes. + """ + data = self._process_infohash_list(infohash_list) + return self._post('torrents/pause', data=data) + + def set_category(self, infohash_list, category): + """ + Set the category on multiple torrents. + + The category must exist before using set_category. As of v2.1.0,the API + returns a 409 Client Error for any valid category name that doesn't + already exist. + + :param infohash_list: Single or list() of infohashes. + :param category: If category is set to empty string '', + the torrent(s) specified is/are removed from all categories. + """ + data = self._process_infohash_list(infohash_list) + data['category'] = category + return self._post('torrents/setCategory', data=data) + + def create_category(self, category): + """ + Create a new category + :param category: category to create + """ + return self._post('torrents/createCategory', data={'category': category.lower()}) + + def remove_category(self, categories): + """ + Remove categories + + :param categories: can contain multiple cateogies separated by \n (%0A urlencoded). + """ + + return self._post('torrents/removeCategories', data={'categories': categories}) + + def resume(self, infohash): + """ + Resume a paused torrent. + + :param infohash: INFO HASH of torrent. + """ + return self._post('torrents/resume', data={'hashes': infohash.lower()}) + + def resume_all(self): + """ + Resume all torrents. + """ + return self._post('torrents/resume', data={'hashes': 'all'}) + + def resume_multiple(self, infohash_list): + """ + Resume multiple paused torrents. + + :param infohash_list: Single or list() of infohashes. + """ + data = self._process_infohash_list(infohash_list) + return self._post('torrents/resume', data=data) + + def delete(self, infohash_list): + """ + Delete torrents. Does not remove files. + + :param infohash_list: Single or list() of infohashes. + """ + return self._delete(infohash_list) + + def delete_all(self): + """ + Delete all torrents. Does not remove files. + """ + return self._delete('all') + + def delete_permanently(self, infohash_list): + """ + Permanently delete torrents. Removes files. + + :param infohash_list: Single or list() of infohashes. + """ + return self._delete(infohash_list, True) + + def delete_all_permanently(self): + """ + Permanently delete torrents. + """ + return self._delete('all', True) + + def _delete(self, infohash_list, delete_files=False): + """ + Delete torrents. + + :param infohash_list: Single or list() of infohashes. + :param delete_files: Whether to delete files along with torrent. + """ + data = self._process_infohash_list(infohash_list) + data['deleteFiles'] = json.dumps(delete_files) + return self._post('torrents/delete', data=data) + + def recheck(self, infohash_list): + """ + Recheck torrents. + + :param infohash_list: Single or list() of infohashes. + """ + data = self._process_infohash_list(infohash_list) + return self._post('torrents/recheck', data=data) + + def recheck_all(self): + """ + Recheck all torrents. + """ + return self._post('torrents/recheck', data={'hashes': 'all'}) + + def reannounce(self, infohash_list): + """ + Recheck all torrents. + + :param infohash_list: Single or list() of infohashes; pass 'all' for all torrents. + """ + + data = self._process_infohash_list(infohash_list) + return self._post('torrents/reannounce', data=data) + + def increase_priority(self, infohash_list): + """ + Increase priority of torrents. + + :param infohash_list: Single or list() of infohashes; pass 'all' for all torrents. + """ + data = self._process_infohash_list(infohash_list) + return self._post('torrents/increasePrio', data=data) + + def decrease_priority(self, infohash_list): + """ + Decrease priority of torrents. + + :param infohash_list: Single or list() of infohashes; pass 'all' for all torrents. + """ + data = self._process_infohash_list(infohash_list) + return self._post('torrents/decreasePrio', data=data) + + def set_max_priority(self, infohash_list): + """ + Set torrents to maximum priority level. + + :param infohash_list: Single or list() of infohashes; pass 'all' for all torrents. + """ + data = self._process_infohash_list(infohash_list) + return self._post('torrents/topPrio', data=data) + + def set_min_priority(self, infohash_list): + """ + Set torrents to minimum priority level. + + :param infohash_list: Single or list() of infohashes; pass 'all' for all torrents. + """ + data = self._process_infohash_list(infohash_list) + return self._post('torrents/bottomPrio', data=data) + + def set_file_priority(self, infohash, file_id, priority): + """ + Set file of a torrent to a supplied priority level. + + :param infohash: INFO HASH of torrent. + :param file_id: ID of the file to set priority. + :param priority: Priority level of the file. + :note priority 4 is no priority set + """ + if priority not in [0, 1, 2, 4, 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") + + data = {'hash': infohash.lower(), + 'id': file_id, + 'priority': priority} + + return self._post('torrents/filePrio', data=data) + + def set_automatic_torrent_management(self, infohash_list, enable='false'): + """ + Set the category on multiple torrents. + + :param infohash_list: Single or list() of infohashes. + :param enable: is a boolean, affects the torrents listed in infohash_list, default is 'false' + """ + data = self._process_infohash_list(infohash_list) + data['enable'] = enable + return self._post('torrents/setAutoManagement', data=data) + + # Get-set global download and upload speed limits. + + def get_global_download_limit(self): + """ + Get global download speed limit. + """ + return self._get('transfer/downloadLimit') + + def set_global_download_limit(self, limit): + """ + Set global download speed limit. + + :param limit: Speed limit in bytes. + """ + return self._post('transfer/setDownloadLimit', data={'limit': limit}) + + global_download_limit = property(get_global_download_limit, + set_global_download_limit) + + def get_global_upload_limit(self): + """ + Get global upload speed limit. + """ + return self._get('transfer/uploadLimit') + + def set_global_upload_limit(self, limit): + """ + Set global upload speed limit. + + :param limit: Speed limit in bytes. + """ + return self._post('transfer/setUploadLimit', data={'limit': limit}) + + global_upload_limit = property(get_global_upload_limit, + set_global_upload_limit) + + # Get-set download and upload speed limits of the torrents. + def get_torrent_download_limit(self, infohash_list): + """ + Get download speed limit of the supplied torrents. + + :param infohash_list: Single or list() of infohashes. + """ + data = self._process_infohash_list(infohash_list) + return self._post('torrents/downloadLimit', data=data) + + def set_torrent_download_limit(self, infohash_list, limit): + """ + Set download speed limit of the supplied torrents. + + :param infohash_list: Single or list() of infohashes. + :param limit: Speed limit in bytes. + """ + data = self._process_infohash_list(infohash_list) + data.update({'limit': limit}) + return self._post('torrents/setDownloadLimit', data=data) + + def get_torrent_upload_limit(self, infohash_list): + """ + Get upoload speed limit of the supplied torrents. + + :param infohash_list: Single or list() of infohashes. + """ + data = self._process_infohash_list(infohash_list) + return self._post('torrents/uploadLimit', data=data) + + def set_torrent_upload_limit(self, infohash_list, limit): + """ + Set upload speed limit of the supplied torrents. + + :param infohash_list: Single or list() of infohashes. + :param limit: Speed limit in bytes. + """ + data = self._process_infohash_list(infohash_list) + data.update({'limit': limit}) + return self._post('torrents/setUploadLimit', data=data) + + # setting preferences + def set_preferences(self, **kwargs): + """ + Set preferences of qBittorrent. + 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('app/setPreferences', data=json_data, + headers=headers) + + def get_alternative_speed_status(self): + """ + Get Alternative speed limits. (1/0) + """ + + return self._get('transfer/speedLimitsMode') + + alternative_speed_status = property(get_alternative_speed_status) + + def toggle_alternative_speed(self): + """ + Toggle alternative speed limits. + """ + return self._get('transfer/toggleSpeedLimitsMode') + + def toggle_sequential_download(self, infohash_list): + """ + Toggle sequential download in supplied torrents. + + :param infohash_list: Single or list() of infohashes; pass 'all' for all torrents. + """ + data = self._process_infohash_list(infohash_list) + return self._post('torrents/toggleSequentialDownload', data=data) + + def toggle_first_last_piece_priority(self, infohash_list): + """ + Toggle first/last piece priority of supplied torrents. + + :param infohash_list: Single or list() of infohashes; pass 'all' for all torrents. + """ + data = self._process_infohash_list(infohash_list) + return self._post('torrents/toggleFirstLastPiecePrio', data=data) + + def force_start(self, infohash_list, value): + """ + Force start selected torrents. + + :param infohash_list: Single or list() of infohashes; pass 'all' for all torrents. + :param value: Force start value (bool) + """ + data = self._process_infohash_list(infohash_list) + data.update({'value': json.dumps(value)}) + return self._post('torrents/setForceStart', data=data) + + def set_super_seeding(self, infohash_list, value): + """ + Set super seeding for selected torrents. + + :param infohash_list: Single or list() of infohashes; pass 'all' for all torrents. + :param value: Force start value (bool) + """ + data = self._process_infohash_list(infohash_list) + data.update({'value': json.dumps(value)}) + return self._post('torrents/setSuperSeeding', data=data) + + def set_share_ratio(self, infohash_list, ratio_limit=-2, seeding_time_limit=-2): + """ + Set the share ratio limit of the supplied torrents. + + :param infohash_list: Single or list() of infohashes. + :param ratio_limit: Ratio limit (optional 2 decimals). -2 means the global limit, -1 means no limit. + :param seeding_time_limit: Time limit in minutes. -2 means the global limit, -1 means no limit. + """ + + data = self._process_infohash_list(infohash_list) + data.update({'ratioLimit': ratio_limit, + 'seedingTimeLimit': seeding_time_limit}) + + return self._post('torrents/setShareLimits', data=data)