Browse Source

Merge pull request #2 from sickness0666/temp

qbittorrent api upgrade
pull/6035/head
Jeremy 9 years ago
parent
commit
04c5211300
  1. 34
      couchpotato/core/downloaders/qbittorrent_.py
  2. 2
      libs/qbittorrent/__init__.py
  3. 62
      libs/qbittorrent/base.py
  4. 634
      libs/qbittorrent/client.py
  5. 15
      libs/qbittorrent/file.py
  6. 7
      libs/qbittorrent/helpers.py
  7. 96
      libs/qbittorrent/torrent.py

34
couchpotato/core/downloaders/qbittorrent_.py

@ -30,11 +30,8 @@ class qBittorrent(DownloaderBase):
url = cleanHost(self.conf('host'), protocol = True, ssl = False)
if self.conf('username') and self.conf('password'):
self.qb = QBittorrentClient(
url,
username = self.conf('username'),
password = self.conf('password')
)
self.qb = QBittorrentClient(url)
self.qb.login(username=self.conf('username'),password=self.conf('password'))
else:
self.qb = QBittorrentClient(url)
@ -45,10 +42,7 @@ class qBittorrent(DownloaderBase):
:return: bool
"""
if self.connect():
return True
return False
return self.qb._is_authenticated
def download(self, data = None, media = None, filedata = None):
""" Send a torrent/nzb file to the downloader
@ -95,7 +89,7 @@ class qBittorrent(DownloaderBase):
# Send request to qBittorrent
try:
self.qb.add_file(filedata)
self.qb.download_from_file(filedata, label=self.conf('label'))
return self.downloadReturnId(torrent_hash)
except Exception as e:
@ -127,14 +121,13 @@ class qBittorrent(DownloaderBase):
return []
try:
torrents = self.qb.get_torrents()
torrents = self.qb.torrents(label=self.conf('label'))
release_downloads = ReleaseDownloadList(self)
for torrent in torrents:
if torrent.hash in ids:
torrent.update_general() # get extra info
torrent_filelist = torrent.get_files()
torrent_filelist = self.qb.get_torrent_files(torrent.hash)
torrent_files = []
torrent_dir = os.path.join(torrent.save_path, torrent.name)
@ -179,8 +172,8 @@ class qBittorrent(DownloaderBase):
return False
if pause:
return torrent.pause()
return torrent.resume()
return self.qb.pause(release_download['id'])
return self.qb.resume(release_download['id'])
def removeFailed(self, release_download):
log.info('%s failed downloading, deleting...', release_download['name'])
@ -193,15 +186,15 @@ class qBittorrent(DownloaderBase):
if not self.connect():
return False
torrent = self.qb.find_torrent(release_download['id'])
torrent = self.qb.get_torrent(release_download['id'])
if torrent is None:
return False
if delete_files:
torrent.delete() # deletes torrent with data
self.qb.delete_permanently(release_download['id']) # deletes torrent with data
else:
torrent.remove() # just removes the torrent, doesn't delete data
self.qb.delete(release_download['id']) # just removes the torrent, doesn't delete data
return True
@ -236,6 +229,11 @@ config = [{
'type': 'password',
},
{
'name': 'label',
'label': 'Torrent Label',
'default': 'couchpotato',
},
{
'name': 'remove_complete',
'label': 'Remove torrent',
'default': False,

2
libs/qbittorrent/__init__.py

@ -1 +1 @@
__version__ = '0.1'
__version__ = '0.2'

62
libs/qbittorrent/base.py

@ -1,62 +0,0 @@
from urlparse import urljoin
import logging
log = logging.getLogger(__name__)
class Base(object):
properties = {}
def __init__(self, url, session, client=None):
self._client = client
self._url = url
self._session = session
@staticmethod
def _convert(response, response_type):
if response_type == 'json':
try:
return response.json()
except ValueError:
pass
return response
def _get(self, path='', response_type='json', **kwargs):
r = self._session.get(urljoin(self._url, path), **kwargs)
return self._convert(r, response_type)
def _post(self, path='', response_type='json', data=None, **kwargs):
r = self._session.post(urljoin(self._url, path), data, **kwargs)
return self._convert(r, response_type)
def _fill(self, data):
for key, value in data.items():
if self.set_property(self, key, value):
continue
log.debug('%s is missing item with key "%s" and value %s', self.__class__, key, repr(value))
@classmethod
def parse(cls, client, data):
obj = cls(client._url, client._session, client)
obj._fill(data)
return obj
@classmethod
def set_property(cls, obj, key, value):
prop = cls.properties.get(key, {})
if prop.get('key'):
key = prop['key']
if not hasattr(obj, key):
return False
if prop.get('parse'):
value = prop['parse'](value)
setattr(obj, key, value)
return True

634
libs/qbittorrent/client.py

@ -1,72 +1,610 @@
from qbittorrent.base import Base
from qbittorrent.torrent import Torrent
from requests import Session
from requests.auth import HTTPDigestAuth
import time
import requests
import json
class LoginRequired(Exception):
def __str__(self):
return 'Please login first.'
class QBittorrentClient(Base):
def __init__(self, url, username=None, password=None):
super(QBittorrentClient, self).__init__(url, Session())
if username and password:
self._session.auth = HTTPDigestAuth(username, password)
class QBittorrentClient(object):
"""class to interact with qBittorrent WEB API"""
def __init__(self, url):
if not url.endswith('/'):
url += '/'
self.url = url
def test_connection(self):
r = self._get(response_type='response')
session = requests.Session()
check_prefs = session.get(url+'query/preferences')
return r.status_code == 200
if check_prefs.status_code == 200:
self._is_authenticated = True
self.session = session
else:
self._is_authenticated = False
def add_file(self, file):
self._post('command/upload', files={'torrent': file})
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
rq = self.session
if method == 'get':
request = rq.get(final_url, **kwargs)
else:
request = rq.post(final_url, data, **kwargs)
request.raise_for_status()
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, password):
"""
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+'login',
data={'username': username,
'password': password})
if login.text == 'Ok.':
self._is_authenticated = True
else:
return login.text
def logout(self):
"""
Logout the current session.
"""
response = self._get('logout')
self._is_authenticated = False
return response
@property
def qbittorrent_version(self):
"""
Get qBittorrent version.
"""
return self._get('version/qbittorrent')
@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')
def shutdown(self):
"""
Shutdown qBittorrent.
"""
return self._get('command/shutdown')
def torrents(self, status='active', label='', sort='priority',
reverse=False, limit=10, offset=0):
"""
Returns a list of torrents matching the supplied filters.
:param status: Current status of the torrents.
:param label: 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.
"""
STATUS_LIST = ['all', 'downloading', 'completed',
'paused', 'active', 'inactive']
if status not in STATUS_LIST:
raise ValueError("Invalid status.")
params = {
'filter': status,
'label': label,
'sort': sort,
'reverse': reverse,
'limit': limit,
'offset': offset
}
return self._get('query/torrents', params=params)
def get_torrent(self, infohash):
"""
Get details of the torrent.
:param infohash: INFO HASH of the torrent.
"""
return self._get('query/propertiesGeneral/' + infohash.lower())
def get_torrent_trackers(self, infohash):
"""
Get trackers for the torrent.
:param infohash: INFO HASH of the torrent.
"""
return self._get('query/propertiesTrackers/' + infohash.lower())
def get_torrent_webseeds(self, infohash):
"""
Get webseeds for the torrent.
:param infohash: INFO HASH of the torrent.
"""
return self._get('query/propertiesWebSeeds/' + 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('query/propertiesFiles/' + infohash.lower())
@property
def global_transfer_info(self):
"""
Get JSON data of the global transfer info of qBittorrent.
"""
return self._get('query/transferInfo')
@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('query/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):
super(Proxy, self).__init__(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(self, rid=0):
"""
Sync the torrents by supplied LAST RESPONSE ID.
Read more @ http://git.io/vEgXr
:param rid: Response ID of last request.
"""
return self._get('sync/maindata', params={'rid': rid})
def download_from_link(self, link,
save_path=None, label=''):
"""
Download torrent using a link.
:param link: URL Link or list of.
:param save_path: Path to download the torrent.
:param label: Label of the torrent(s).
:return: Empty JSON data.
"""
if not isinstance(link, list):
link = [link]
data = {'urls': link}
if save_path:
data.update({'savepath': save_path})
if label:
data.update({'label': label})
return self._post('command/download', data=data)
def download_from_file(self, file_buffer,
save_path=None, label=''):
"""
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})
print torrent_files
else:
torrent_files = {'torrents': file_buffer}
def add_url(self, urls):
if type(urls) is not list:
urls = [urls]
data = {}
urls = '%0A'.join(urls)
if save_path:
data.update({'savepath': save_path})
if label:
data.update({'label': label})
return self._post('command/upload', data=data, files=torrent_files)
self._post('command/download', data={'urls': urls})
def add_trackers(self, infohash, trackers):
"""
Add trackers to a torrent.
:param infohash: INFO HASH of torrent.
:param trackers: Trackers.
"""
data = {'hash': infohash.lower(),
'urls': trackers}
return self._post('command/addTrackers', 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('command/pause', data={'hash': infohash.lower()})
def pause_all(self):
"""
Pause all torrents.
"""
return self._get('command/pauseAll')
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('command/pauseAll', data=data)
def resume(self, infohash):
"""
Resume a paused torrent.
:param infohash: INFO HASH of torrent.
"""
return self._post('command/resume', data={'hash': infohash.lower()})
def resume_all(self):
"""
Resume all torrents.
"""
return self._get('command/resumeAll')
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('command/resumeAll', data=data)
def delete(self, infohash_list):
"""
Delete torrents.
:param infohash_list: Single or list() of infohashes.
"""
data = self.process_infohash_list(infohash_list)
return self._post('command/delete', data=data)
def delete_permanently(self, infohash_list):
"""
Permanently delete torrents.
:param infohash_list: Single or list() of infohashes.
"""
data = self.process_infohash_list(infohash_list)
return self._post('command/deletePerm', 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('command/recheck', data=data)
def increase_priority(self, infohash_list):
"""
Increase priority of torrents.
:param infohash_list: Single or list() of infohashes.
"""
data = self.process_infohash_list(infohash_list)
return self._post('command/increasePrio', data=data)
def decrease_priority(self, infohash_list):
"""
Decrease priority of torrents.
:param infohash_list: Single or list() of infohashes.
"""
data = self.process_infohash_list(infohash_list)
return self._post('command/decreasePrio', data=data)
def set_max_priority(self, infohash_list):
"""
Set torrents to maximum priority level.
:param infohash_list: Single or list() of infohashes.
"""
data = self.process_infohash_list(infohash_list)
return self._post('command/topPrio', data=data)
def set_min_priority(self, infohash_list):
"""
Set torrents to minimum priority level.
:param infohash_list: Single or list() of infohashes.
"""
data = self.process_infohash_list(infohash_list)
return self._post('command/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.
"""
if priority not in [0, 1, 2, 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('command/setFilePrio', data=data)
# Get-set global download and upload speed limits.
def get_global_download_limit(self):
"""
Get global download speed limit.
"""
return self._get('command/getGlobalDlLimit')
def set_global_download_limit(self, limit):
"""
Set global download speed limit.
:param limit: Speed limit in bytes.
"""
return self._post('command/setGlobalDlLimit', 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('command/getGlobalUpLimit')
def set_global_upload_limit(self, limit):
"""
Set global upload speed limit.
:param limit: Speed limit in bytes.
"""
return self._post('command/setGlobalUpLimit', 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.
def get_torrents(self):
"""Fetch all torrents
:param infohash_list: Single or list() of infohashes.
"""
data = self.process_infohash_list(infohash_list)
return self._post('command/getTorrentsDlLimit', 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('command/setTorrentsDlLimit', 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('command/getTorrentsUpLimit', data=data)
:return: list of Torrent
def set_torrent_upload_limit(self, infohash_list, limit):
"""
r = self._get('json/torrents')
Set upload speed limit of the supplied torrents.
return [Torrent.parse(self, x) for x in r]
: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('command/setTorrentsUpLimit', data=data)
def get_torrent(self, hash, include_general=True, max_retries=5):
"""Fetch details for torrent by info_hash.
# setting preferences
def set_preferences(self, **kwargs):
"""
Set preferences of qBittorrent.
Read all possible preferences @ http://git.io/vEgDQ
:param info_hash: Torrent info hash
:param include_general: Include general torrent properties
:param max_retries: Maximum number of retries to wait for torrent to appear in client
: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,
headers=headers)
:rtype: Torrent or None
def get_alternative_speed_status(self):
"""
Get Alternative speed limits. (1/0)
"""
return self._get('command/alternativeSpeedLimitsEnabled')
torrent = None
retries = 0
alternative_speed_status = property(get_alternative_speed_status)
# Try find torrent in client
while retries < max_retries:
# TODO this wouldn't be very efficient with large numbers of torrents on the client
torrents = dict([(t.hash, t) for t in self.get_torrents()])
def toggle_alternative_speed(self):
"""
Toggle alternative speed limits.
"""
return self._get('command/toggleAlternativeSpeedLimits')
if hash in torrents:
torrent = torrents[hash]
break
def toggle_sequential_download(self, infohash_list):
"""
Toggle sequential download in supplied torrents.
retries += 1
time.sleep(1)
:param infohash_list: Single or list() of infohashes.
"""
data = self.process_infohash_list(infohash_list)
return self._post('command/toggleSequentialDownload', data=data)
def toggle_first_last_piece_priority(self, infohash_list):
"""
Toggle first/last piece priority of supplied torrents.
if torrent is None:
return None
:param infohash_list: Single or list() of infohashes.
"""
data = self.process_infohash_list(infohash_list)
return self._post('command/toggleFirstLastPiecePrio', data=data)
# Fetch general properties for torrent
if include_general:
torrent.update_general()
def force_start(self, infohash_list, value=True):
"""
Force start selected torrents.
return torrent
:param infohash_list: Single or list() of infohashes.
:param value: Force start value (bool)
"""
data = self.process_infohash_list(infohash_list)
data.update({'value': json.dumps(value)})
return self._post('command/setForceStart', data=data)

15
libs/qbittorrent/file.py

@ -1,15 +0,0 @@
from qbittorrent.base import Base
class File(Base):
def __init__(self, url, session, client=None):
super(File, self).__init__(url, session, client)
self.name = None
self.progress = None
self.priority = None
self.is_seed = None
self.size = None

7
libs/qbittorrent/helpers.py

@ -1,7 +0,0 @@
def try_convert(value, to_type, default=None):
try:
return to_type(value)
except ValueError:
return default
except TypeError:
return default

96
libs/qbittorrent/torrent.py

@ -1,96 +0,0 @@
from qbittorrent.base import Base
from qbittorrent.file import File
from qbittorrent.helpers import try_convert
class Torrent(Base):
properties = {
'num_seeds': {
'key': 'seeds',
'parse': lambda value: try_convert(value, int)
},
'num_leechs': {
'key': 'leechs',
'parse': lambda value: try_convert(value, int)
},
'ratio': {
'parse': lambda value: try_convert(value, float)
}
}
def __init__(self, url, session, client=None):
super(Torrent, self).__init__(url, session, client)
self.hash = None
self.name = None
self.state = None
self.ratio = None
self.progress = None
self.priority = None
self.seeds = None
self.leechs = None
# General properties
self.comment = None
self.save_path = None
self.eta = None
self.size = None
self.dlspeed = None
self.upspeed = None
self.nb_connections = None
self.share_ratio = None
self.piece_size = None
self.total_wasted = None
self.total_downloaded = None
self.total_uploaded = None
self.creation_date = None
self.time_elapsed = None
self.up_limit = None
self.dl_limit = None
#
# Commands
#
def pause(self):
self._post('command/pause', data={'hash': self.hash})
def resume(self):
self._post('command/resume', data={'hash': self.hash})
def remove(self):
self._post('command/delete', data={'hashes': self.hash})
def delete(self):
self._post('command/deletePerm', data={'hashes': self.hash})
def recheck(self):
self._post('command/recheck', data={'hash': self.hash})
#
# Fetch details
#
def get_files(self):
r = self._get('json/propertiesFiles/%s' % self.hash)
return [File.parse(self._client, x) for x in r]
def get_trackers(self):
pass
#
# Update torrent details
#
def update_general(self):
r = self._get('json/propertiesGeneral/%s' % self.hash)
if r:
self._fill(r)
return True
return False
Loading…
Cancel
Save