You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
442 lines
15 KiB
442 lines
15 KiB
from base64 import b16encode, b32decode
|
|
from datetime import timedelta
|
|
from hashlib import sha1
|
|
from urlparse import urlparse
|
|
import os
|
|
import re
|
|
|
|
from couchpotato.core._base.downloader.main import DownloaderBase, ReleaseDownloadList
|
|
from couchpotato.core.event import addEvent
|
|
from couchpotato.core.helpers.encoding import sp
|
|
from couchpotato.core.helpers.variable import cleanHost, splitString
|
|
from couchpotato.core.logger import CPLog
|
|
from bencode import bencode, bdecode
|
|
from rtorrent import RTorrent
|
|
|
|
|
|
log = CPLog(__name__)
|
|
|
|
autoload = 'rTorrent'
|
|
|
|
|
|
class rTorrent(DownloaderBase):
|
|
|
|
protocol = ['torrent', 'torrent_magnet']
|
|
rt = None
|
|
error_msg = ''
|
|
|
|
# Migration url to host options
|
|
def __init__(self):
|
|
super(rTorrent, self).__init__()
|
|
|
|
addEvent('app.load', self.migrate)
|
|
addEvent('setting.save.rtorrent.*.after', self.settingsChanged)
|
|
|
|
def migrate(self):
|
|
|
|
url = self.conf('url')
|
|
if url:
|
|
host_split = splitString(url.split('://')[-1], split_on = '/')
|
|
|
|
self.conf('ssl', value = url.startswith('https'))
|
|
self.conf('host', value = host_split[0].strip())
|
|
self.conf('rpc_url', value = '/'.join(host_split[1:]))
|
|
|
|
self.deleteConf('url')
|
|
|
|
def settingsChanged(self):
|
|
# Reset active connection if settings have changed
|
|
if self.rt:
|
|
log.debug('Settings have changed, closing active connection')
|
|
|
|
self.rt = None
|
|
return True
|
|
|
|
def getAuth(self):
|
|
if not self.conf('username') or not self.conf('password'):
|
|
# Missing username or password parameter
|
|
return None
|
|
|
|
# Build authentication tuple
|
|
return (
|
|
self.conf('authentication'),
|
|
self.conf('username'),
|
|
self.conf('password')
|
|
)
|
|
|
|
def getVerifySsl(self):
|
|
# Ensure verification has been enabled
|
|
if not self.conf('ssl_verify'):
|
|
return False
|
|
|
|
# Use ca bundle if defined
|
|
ca_bundle = self.conf('ssl_ca_bundle')
|
|
|
|
if ca_bundle and os.path.exists(ca_bundle):
|
|
return ca_bundle
|
|
|
|
# Use default ssl verification
|
|
return True
|
|
|
|
def connect(self, reconnect = False):
|
|
# Already connected?
|
|
if not reconnect and self.rt is not None:
|
|
return self.rt
|
|
|
|
url = cleanHost(self.conf('host'), protocol = True, ssl = self.conf('ssl'))
|
|
|
|
# Automatically add '+https' to 'httprpc' protocol if SSL is enabled
|
|
if self.conf('ssl') and url.startswith('httprpc://'):
|
|
url = url.replace('httprpc://', 'httprpc+https://')
|
|
|
|
parsed = urlparse(url)
|
|
|
|
# rpc_url is only used on http/https scgi pass-through
|
|
if parsed.scheme in ['http', 'https']:
|
|
url += self.conf('rpc_url')
|
|
|
|
# Construct client
|
|
self.rt = RTorrent(
|
|
url, self.getAuth(),
|
|
verify_ssl=self.getVerifySsl()
|
|
)
|
|
|
|
self.error_msg = ''
|
|
try:
|
|
self.rt.connection.verify()
|
|
except AssertionError as e:
|
|
self.error_msg = e.message
|
|
self.rt = None
|
|
|
|
return self.rt
|
|
|
|
def test(self):
|
|
""" Check if connection works
|
|
:return: bool
|
|
"""
|
|
|
|
if self.connect(True):
|
|
return True
|
|
|
|
if self.error_msg:
|
|
return False, 'Connection failed: ' + self.error_msg
|
|
|
|
return False
|
|
|
|
|
|
def download(self, data = None, media = None, filedata = None):
|
|
""" Send a torrent/nzb file to the downloader
|
|
|
|
:param data: dict returned from provider
|
|
Contains the release information
|
|
:param media: media dict with information
|
|
Used for creating the filename when possible
|
|
:param filedata: downloaded torrent/nzb filedata
|
|
The file gets downloaded in the searcher and send to this function
|
|
This is done to have failed checking before using the downloader, so the downloader
|
|
doesn't need to worry about that
|
|
:return: boolean
|
|
One faile returns false, but the downloaded should log his own errors
|
|
"""
|
|
|
|
if not media: media = {}
|
|
if not data: data = {}
|
|
|
|
log.debug('Sending "%s" to rTorrent.', (data.get('name')))
|
|
|
|
if not self.connect():
|
|
return False
|
|
|
|
torrent_hash = 0
|
|
torrent_params = {}
|
|
if self.conf('label'):
|
|
torrent_params['label'] = self.conf('label')
|
|
|
|
if not filedata and data.get('protocol') == 'torrent':
|
|
log.error('Failed sending torrent, no data')
|
|
return False
|
|
|
|
# Try download magnet torrents
|
|
if data.get('protocol') == 'torrent_magnet':
|
|
# Send magnet to rTorrent
|
|
torrent_hash = re.findall('urn:btih:([\w]{32,40})', data.get('url'))[0].upper()
|
|
# Send request to rTorrent
|
|
try:
|
|
torrent = self.rt.load_magnet(data.get('url'), torrent_hash)
|
|
|
|
if not torrent:
|
|
log.error('Unable to find the torrent, did it fail to load?')
|
|
return False
|
|
|
|
except Exception as err:
|
|
log.error('Failed to send magnet to rTorrent: %s', err)
|
|
return False
|
|
|
|
if data.get('protocol') == 'torrent':
|
|
info = bdecode(filedata)["info"]
|
|
torrent_hash = sha1(bencode(info)).hexdigest().upper()
|
|
|
|
# Convert base 32 to hex
|
|
if len(torrent_hash) == 32:
|
|
torrent_hash = b16encode(b32decode(torrent_hash))
|
|
|
|
# Send request to rTorrent
|
|
try:
|
|
# Send torrent to rTorrent
|
|
torrent = self.rt.load_torrent(filedata, verify_retries=10)
|
|
|
|
if not torrent:
|
|
log.error('Unable to find the torrent, did it fail to load?')
|
|
return False
|
|
|
|
except Exception as err:
|
|
log.error('Failed to send torrent to rTorrent: %s', err)
|
|
return False
|
|
|
|
try:
|
|
# Set label
|
|
if self.conf('label'):
|
|
torrent.set_custom(1, self.conf('label'))
|
|
|
|
if self.conf('directory'):
|
|
torrent.set_directory(self.conf('directory'))
|
|
|
|
# Start torrent
|
|
if not self.conf('paused', default = 0):
|
|
torrent.start()
|
|
|
|
return self.downloadReturnId(torrent_hash)
|
|
|
|
except Exception as err:
|
|
log.error('Failed to send torrent to rTorrent: %s', err)
|
|
return False
|
|
|
|
|
|
def getTorrentStatus(self, torrent):
|
|
if not torrent.complete:
|
|
return 'busy'
|
|
|
|
if torrent.open:
|
|
return 'seeding'
|
|
|
|
return 'completed'
|
|
|
|
def getAllDownloadStatus(self, ids):
|
|
""" Get status of all active downloads
|
|
|
|
:param ids: list of (mixed) downloader ids
|
|
Used to match the releases for this downloader as there could be
|
|
other downloaders active that it should ignore
|
|
:return: list of releases
|
|
"""
|
|
|
|
log.debug('Checking rTorrent download status.')
|
|
|
|
if not self.connect():
|
|
return []
|
|
|
|
try:
|
|
torrents = self.rt.get_torrents()
|
|
|
|
release_downloads = ReleaseDownloadList(self)
|
|
|
|
for torrent in torrents:
|
|
if torrent.info_hash in ids:
|
|
torrent_directory = os.path.normpath(torrent.directory)
|
|
torrent_files = []
|
|
|
|
for file in torrent.get_files():
|
|
if not os.path.normpath(file.path).startswith(torrent_directory):
|
|
file_path = os.path.join(torrent_directory, file.path.lstrip('/'))
|
|
else:
|
|
file_path = file.path
|
|
|
|
torrent_files.append(sp(file_path))
|
|
|
|
release_downloads.append({
|
|
'id': torrent.info_hash,
|
|
'name': torrent.name,
|
|
'status': self.getTorrentStatus(torrent),
|
|
'seed_ratio': torrent.ratio,
|
|
'original_status': torrent.state,
|
|
'timeleft': str(timedelta(seconds = float(torrent.left_bytes) / torrent.down_rate)) if torrent.down_rate > 0 else -1,
|
|
'folder': sp(torrent.directory),
|
|
'files': torrent_files
|
|
})
|
|
|
|
return release_downloads
|
|
|
|
except Exception as err:
|
|
log.error('Failed to get status from rTorrent: %s', err)
|
|
return []
|
|
|
|
def pause(self, release_download, pause = True):
|
|
if not self.connect():
|
|
return False
|
|
|
|
torrent = self.rt.find_torrent(release_download['id'])
|
|
if torrent is None:
|
|
return False
|
|
|
|
if pause:
|
|
return torrent.pause()
|
|
return torrent.resume()
|
|
|
|
def removeFailed(self, release_download):
|
|
log.info('%s failed downloading, deleting...', release_download['name'])
|
|
return self.processComplete(release_download, delete_files = True)
|
|
|
|
def processComplete(self, release_download, delete_files):
|
|
log.debug('Requesting rTorrent to remove the torrent %s%s.',
|
|
(release_download['name'], ' and cleanup the downloaded files' if delete_files else ''))
|
|
|
|
if not self.connect():
|
|
return False
|
|
|
|
torrent = self.rt.find_torrent(release_download['id'])
|
|
|
|
if torrent is None:
|
|
return False
|
|
|
|
if delete_files:
|
|
for file_item in torrent.get_files(): # will only delete files, not dir/sub-dir
|
|
os.unlink(os.path.join(torrent.directory, file_item.path))
|
|
|
|
if torrent.is_multi_file() and torrent.directory.endswith(torrent.name):
|
|
# Remove empty directories bottom up
|
|
try:
|
|
for path, _, _ in os.walk(sp(torrent.directory), topdown = False):
|
|
os.rmdir(path)
|
|
except OSError:
|
|
log.info('Directory "%s" contains extra files, unable to remove', torrent.directory)
|
|
|
|
torrent.erase() # just removes the torrent, doesn't delete data
|
|
|
|
return True
|
|
|
|
|
|
config = [{
|
|
'name': 'rtorrent',
|
|
'groups': [
|
|
{
|
|
'tab': 'downloaders',
|
|
'list': 'download_providers',
|
|
'name': 'rtorrent',
|
|
'label': 'rTorrent',
|
|
'description': 'Use <a href="https://rakshasa.github.io/rtorrent/" target="_blank">rTorrent</a> to download torrents.',
|
|
'wizard': True,
|
|
'options': [
|
|
{
|
|
'name': 'enabled',
|
|
'default': 0,
|
|
'type': 'enabler',
|
|
'radio_group': 'torrent',
|
|
},
|
|
{
|
|
'name': 'ssl',
|
|
'label': 'SSL Enabled',
|
|
'order': 1,
|
|
'default': 0,
|
|
'type': 'bool',
|
|
'advanced': True,
|
|
'description': 'Use HyperText Transfer Protocol Secure, or <strong>https</strong>',
|
|
},
|
|
{
|
|
'name': 'ssl_verify',
|
|
'label': 'SSL Verify',
|
|
'order': 2,
|
|
'default': 1,
|
|
'type': 'bool',
|
|
'advanced': True,
|
|
'description': 'Verify SSL certificate on https connections',
|
|
},
|
|
{
|
|
'name': 'ssl_ca_bundle',
|
|
'label': 'SSL CA Bundle',
|
|
'order': 3,
|
|
'type': 'string',
|
|
'advanced': True,
|
|
'description': 'Path to a directory (or file) containing trusted certificate authorities',
|
|
},
|
|
{
|
|
'name': 'host',
|
|
'order': 4,
|
|
'default': 'localhost:80',
|
|
'description': 'RPC Communication URI. Usually <strong>scgi://localhost:5000</strong>, '
|
|
'<strong>httprpc://localhost/rutorrent</strong> or <strong>localhost:80</strong>',
|
|
},
|
|
{
|
|
'name': 'rpc_url',
|
|
'order': 5,
|
|
'default': 'RPC2',
|
|
'type': 'string',
|
|
'advanced': True,
|
|
'description': 'Change if your RPC mount is at a different path.',
|
|
},
|
|
{
|
|
'name': 'authentication',
|
|
'order': 6,
|
|
'default': 'basic',
|
|
'type': 'dropdown',
|
|
'advanced': True,
|
|
'values': [('Basic', 'basic'), ('Digest', 'digest')],
|
|
'description': 'Authentication method used for http(s) connections',
|
|
},
|
|
{
|
|
'name': 'username',
|
|
'order': 7,
|
|
},
|
|
{
|
|
'name': 'password',
|
|
'order': 8,
|
|
'type': 'password',
|
|
},
|
|
{
|
|
'name': 'label',
|
|
'order': 9,
|
|
'description': 'Label to apply on added torrents.',
|
|
},
|
|
{
|
|
'name': 'directory',
|
|
'order': 10,
|
|
'type': 'directory',
|
|
'description': 'Download to this directory. Keep empty for default rTorrent download directory.',
|
|
},
|
|
{
|
|
'name': 'remove_complete',
|
|
'label': 'Remove torrent',
|
|
'order': 11,
|
|
'default': False,
|
|
'type': 'bool',
|
|
'advanced': True,
|
|
'description': 'Remove the torrent after it finishes seeding.',
|
|
},
|
|
{
|
|
'name': 'delete_files',
|
|
'label': 'Remove files',
|
|
'order': 12,
|
|
'default': True,
|
|
'type': 'bool',
|
|
'advanced': True,
|
|
'description': 'Also remove the leftover files.',
|
|
},
|
|
{
|
|
'name': 'paused',
|
|
'order': 13,
|
|
'type': 'bool',
|
|
'advanced': True,
|
|
'default': False,
|
|
'description': 'Add the torrent paused.',
|
|
},
|
|
{
|
|
'name': 'manual',
|
|
'order': 14,
|
|
'default': 0,
|
|
'type': 'bool',
|
|
'advanced': True,
|
|
'description': 'Disable this downloader for automated searches, but use it when I manually send a release.',
|
|
},
|
|
],
|
|
}
|
|
],
|
|
}]
|
|
|