Browse Source

Merge remote-tracking branch 'upstream/master' into utf8

pull/7306/head
AnilDaoud 5 years ago
parent
commit
74f004ebd6
  1. 2
      CouchPotato.py
  2. 39
      couchpotato/core/downloaders/deluge.py
  3. 6
      couchpotato/core/helpers/variable.py
  4. 45
      couchpotato/core/media/_base/providers/torrent/iptorrents.py
  5. 2
      couchpotato/core/media/movie/providers/automation/base.py
  6. 10
      couchpotato/core/media/movie/providers/automation/trakt/main.py
  7. 81
      couchpotato/core/media/movie/providers/automation/yifypopular.py
  8. 6
      couchpotato/core/media/movie/providers/torrent/iptorrents.py
  9. 13
      couchpotato/core/notifications/plex/__init__.py
  10. 8
      couchpotato/core/notifications/plex/server.py
  11. 11
      couchpotato/core/plugins/automation.py
  12. 2
      couchpotato/core/plugins/userscript/template.js_tmpl
  13. 2
      couchpotato/static/style/settings.scss
  14. 2
      init/couchpotato.service
  15. 0
      libs/deluge_client/__init__.py
  16. 275
      libs/deluge_client/client.py
  17. 474
      libs/deluge_client/rencode.py
  18. 65
      libs/deluge_client/tests.py
  19. 2
      libs/synchronousdeluge/transfer.py

2
CouchPotato.py

@ -1,4 +1,4 @@
#!/usr/bin/env python #!/usr/bin/env python2
from __future__ import print_function from __future__ import print_function
from logging import handlers from logging import handlers
from os.path import dirname from os.path import dirname

39
couchpotato/core/downloaders/deluge.py

@ -10,8 +10,7 @@ from couchpotato.core._base.downloader.main import DownloaderBase, ReleaseDownlo
from couchpotato.core.helpers.encoding import isInt, sp from couchpotato.core.helpers.encoding import isInt, sp
from couchpotato.core.helpers.variable import tryFloat, cleanHost from couchpotato.core.helpers.variable import tryFloat, cleanHost
from couchpotato.core.logger import CPLog from couchpotato.core.logger import CPLog
from synchronousdeluge import DelugeClient from deluge_client.client import DelugeRPCClient
log = CPLog(__name__) log = CPLog(__name__)
@ -80,17 +79,17 @@ class Deluge(DownloaderBase):
} }
if self.conf('directory'): if self.conf('directory'):
if os.path.isdir(self.conf('directory')): #if os.path.isdir(self.conf('directory')):
options['download_location'] = self.conf('directory') options['download_location'] = self.conf('directory')
else: #else:
log.error('Download directory from Deluge settings: %s doesn\'t exist', self.conf('directory')) # log.error('Download directory from Deluge settings: %s doesn\'t exist', self.conf('directory'))
if self.conf('completed_directory'): if self.conf('completed_directory'):
if os.path.isdir(self.conf('completed_directory')): #if os.path.isdir(self.conf('completed_directory')):
options['move_completed'] = 1 options['move_completed'] = 1
options['move_completed_path'] = self.conf('completed_directory') options['move_completed_path'] = self.conf('completed_directory')
else: #else:
log.error('Download directory from Deluge settings: %s doesn\'t exist', self.conf('directory')) # log.error('Download directory from Deluge settings: %s doesn\'t exist', self.conf('directory'))
if data.get('seed_ratio'): if data.get('seed_ratio'):
options['stop_at_ratio'] = 1 options['stop_at_ratio'] = 1
@ -221,8 +220,10 @@ class DelugeRPC(object):
self.password = password self.password = password
def connect(self): def connect(self):
self.client = DelugeClient() #self.client = DelugeClient()
self.client.connect(self.host, int(self.port), self.username, self.password) #self.client.connect(self.host, int(self.port), self.username, self.password)
self.client = DelugeRPCClient(self.host, int(self.port), self.username, self.password)
self.client.connect()
def test(self): def test(self):
try: try:
@ -235,12 +236,12 @@ class DelugeRPC(object):
torrent_id = False torrent_id = False
try: try:
self.connect() self.connect()
torrent_id = self.client.core.add_torrent_magnet(torrent, options).get() torrent_id = self.client.core.add_torrent_magnet(torrent, options)
if not torrent_id: if not torrent_id:
torrent_id = self._check_torrent(True, torrent) torrent_id = self._check_torrent(True, torrent)
if torrent_id and options['label']: if torrent_id and options['label']:
self.client.label.set_torrent(torrent_id, options['label']).get() self.client.label.set_torrent(torrent_id, options['label'])
except Exception as err: except Exception as err:
log.error('Failed to add torrent magnet %s: %s %s', (torrent, err, traceback.format_exc())) log.error('Failed to add torrent magnet %s: %s %s', (torrent, err, traceback.format_exc()))
finally: finally:
@ -253,12 +254,12 @@ class DelugeRPC(object):
torrent_id = False torrent_id = False
try: try:
self.connect() self.connect()
torrent_id = self.client.core.add_torrent_file(filename, b64encode(torrent), options).get() torrent_id = self.client.core.add_torrent_file(filename, b64encode(torrent), options)
if not torrent_id: if not torrent_id:
torrent_id = self._check_torrent(False, torrent) torrent_id = self._check_torrent(False, torrent)
if torrent_id and options['label']: if torrent_id and options['label']:
self.client.label.set_torrent(torrent_id, options['label']).get() self.client.label.set_torrent(torrent_id, options['label'])
except Exception as err: except Exception as err:
log.error('Failed to add torrent file %s: %s %s', (filename, err, traceback.format_exc())) log.error('Failed to add torrent file %s: %s %s', (filename, err, traceback.format_exc()))
finally: finally:
@ -271,7 +272,7 @@ class DelugeRPC(object):
ret = False ret = False
try: try:
self.connect() self.connect()
ret = self.client.core.get_torrents_status({'id': ids}, ('name', 'hash', 'save_path', 'move_completed_path', 'progress', 'state', 'eta', 'ratio', 'stop_ratio', 'is_seed', 'is_finished', 'paused', 'move_on_completed', 'files')).get() ret = self.client.core.get_torrents_status({'id': ids}, ('name', 'hash', 'save_path', 'move_completed_path', 'progress', 'state', 'eta', 'ratio', 'stop_ratio', 'is_seed', 'is_finished', 'paused', 'move_on_completed', 'files'))
except Exception as err: except Exception as err:
log.error('Failed to get all torrents: %s %s', (err, traceback.format_exc())) log.error('Failed to get all torrents: %s %s', (err, traceback.format_exc()))
finally: finally:
@ -282,7 +283,7 @@ class DelugeRPC(object):
def pause_torrent(self, torrent_ids): def pause_torrent(self, torrent_ids):
try: try:
self.connect() self.connect()
self.client.core.pause_torrent(torrent_ids).get() self.client.core.pause_torrent(torrent_ids)
except Exception as err: except Exception as err:
log.error('Failed to pause torrent: %s %s', (err, traceback.format_exc())) log.error('Failed to pause torrent: %s %s', (err, traceback.format_exc()))
finally: finally:
@ -292,7 +293,7 @@ class DelugeRPC(object):
def resume_torrent(self, torrent_ids): def resume_torrent(self, torrent_ids):
try: try:
self.connect() self.connect()
self.client.core.resume_torrent(torrent_ids).get() self.client.core.resume_torrent(torrent_ids)
except Exception as err: except Exception as err:
log.error('Failed to resume torrent: %s %s', (err, traceback.format_exc())) log.error('Failed to resume torrent: %s %s', (err, traceback.format_exc()))
finally: finally:
@ -303,7 +304,7 @@ class DelugeRPC(object):
ret = False ret = False
try: try:
self.connect() self.connect()
ret = self.client.core.remove_torrent(torrent_id, remove_local_data).get() ret = self.client.core.remove_torrent(torrent_id, remove_local_data)
except Exception as err: except Exception as err:
log.error('Failed to remove torrent: %s %s', (err, traceback.format_exc())) log.error('Failed to remove torrent: %s %s', (err, traceback.format_exc()))
finally: finally:
@ -327,7 +328,7 @@ class DelugeRPC(object):
torrent_hash = b16encode(b32decode(torrent_hash)) torrent_hash = b16encode(b32decode(torrent_hash))
torrent_hash = torrent_hash.lower() torrent_hash = torrent_hash.lower()
torrent_check = self.client.core.get_torrent_status(torrent_hash, {}).get() torrent_check = self.client.core.get_torrent_status(torrent_hash, {})
if torrent_check['hash']: if torrent_check['hash']:
return torrent_hash return torrent_hash

6
couchpotato/core/helpers/variable.py

@ -194,12 +194,12 @@ def getImdb(txt, check_inside = False, multiple = False):
output.close() output.close()
try: try:
ids = re.findall('(tt\d{4,7})', txt) ids = re.findall('(tt\d{4,8})', txt)
if multiple: if multiple:
return removeDuplicate(['tt%07d' % tryInt(x[2:]) for x in ids]) if len(ids) > 0 else [] return removeDuplicate(['tt%s' % str(tryInt(x[2:])).rjust(7, '0') for x in ids]) if len(ids) > 0 else []
return 'tt%07d' % tryInt(ids[0][2:]) return 'tt%s' % str(tryInt(ids[0][2:])).rjust(7, '0')
except IndexError: except IndexError:
pass pass

45
couchpotato/core/media/_base/providers/torrent/iptorrents.py

@ -16,15 +16,14 @@ class Base(TorrentProvider):
urls = { urls = {
'test': 'https://iptorrents.com/', 'test': 'https://iptorrents.com/',
'base_url': 'https://iptorrents.com', 'base_url': 'https://iptorrents.com',
'login': 'https://iptorrents.com/take_login.php',
'login_check': 'https://iptorrents.com/oldinbox.php',
'search': 'https://iptorrents.com/t?%s%%s&q=%s&qf=ti#torrents&p=%%d', 'search': 'https://iptorrents.com/t?%s%%s&q=%s&qf=ti#torrents&p=%%d',
} }
http_time_between_calls = 1 # Seconds http_time_between_calls = 1 # Seconds
login_fail_msg = 'Invalid username and password combination' login_fail_msg = 'Invalid username and cookie combination'
cat_backup_id = None cat_backup_id = None
def buildUrl(self, title, media, quality): def buildUrl(self, title, media, quality):
return self._buildUrl(title.replace(':', ''), quality) return self._buildUrl(title.replace(':', ''), quality)
@ -37,7 +36,7 @@ class Base(TorrentProvider):
return None return None
query = query.replace('"', '') query = query.replace('"', '')
return self.urls['search'] % ("&".join(("%d=" % x) for x in cat_ids), tryUrlencode(query).replace('%', '%%')) return self.urls['search'] % ("&".join(("%d=" % x) for x in cat_ids), tryUrlencode(query).replace('%', '%%'))
def _searchOnTitle(self, title, media, quality, results): def _searchOnTitle(self, title, media, quality, results):
@ -50,8 +49,8 @@ class Base(TorrentProvider):
pages = 1 pages = 1
current_page = 1 current_page = 1
while current_page <= pages and not self.shuttingDown(): while current_page <= pages and not self.shuttingDown():
data = self.getHTMLData(base_url % (freeleech, current_page)) data = self.getHTMLData(base_url % (freeleech, current_page), headers = self.getRequestHeaders())
if data: if data:
html = BeautifulSoup(data) html = BeautifulSoup(data)
@ -63,8 +62,8 @@ class Base(TorrentProvider):
final_page_link = next_link.previous_sibling.previous_sibling final_page_link = next_link.previous_sibling.previous_sibling
pages = int(final_page_link.string) pages = int(final_page_link.string)
result_table = html.find('table', attrs={'id': 'torrents'}) result_table = html.find('table', id="torrents")
if not result_table or 'nothing found!' in data.lower(): if not result_table or 'nothing found!' in data.lower():
return return
@ -102,18 +101,18 @@ class Base(TorrentProvider):
current_page += 1 current_page += 1
def getLoginParams(self): def getRequestHeaders(self):
return { return {
'username': self.conf('username'), 'Cookie': self.conf('cookiesetting') or ''
'password': self.conf('password'),
'login': 'submit',
} }
def download(self, url = '', nzb_id = ''):
try:
return self.urlopen(url, headers=self.getRequestHeaders())
except:
log.error('Failed getting release from %s: %s', (self.getName(), traceback.format_exc()))
def loginSuccess(self, output): return 'try_next'
return 'don\'t have an account' not in output.lower()
def loginCheckSuccess(self, output):
return '/logout.php' in output.lower()
config = [{ config = [{
@ -137,11 +136,6 @@ config = [{
'default': '', 'default': '',
}, },
{ {
'name': 'password',
'default': '',
'type': 'password',
},
{
'name': 'freeleech', 'name': 'freeleech',
'default': 0, 'default': 0,
'type': 'bool', 'type': 'bool',
@ -161,6 +155,12 @@ config = [{
'default': 40, 'default': 40,
'description': 'Will not be (re)moved until this seed time (in hours) is met.', 'description': 'Will not be (re)moved until this seed time (in hours) is met.',
}, },
{
'name': 'cookiesetting',
'label': 'Cookies',
'default': 'uid=1234;pass=567845439634987',
'description': 'Use DevTools or Firebug to get these values after logging in on your browser',
},
{ {
'name': 'extra_score', 'name': 'extra_score',
'advanced': True, 'advanced': True,
@ -173,3 +173,4 @@ config = [{
}, },
], ],
}] }]

2
couchpotato/core/media/movie/providers/automation/base.py

@ -71,7 +71,7 @@ class Automation(AutomationBase):
log.info('ignoring %s as no rating is available for.', (movie['original_title'])) log.info('ignoring %s as no rating is available for.', (movie['original_title']))
return False return False
if movie['rating'] and movie['rating'].get('imdb'): if movie['rating'] and type(movie['rating']) is not float and movie['rating'].get('imdb'):
movie['votes'] = movie['rating']['imdb'][1] movie['votes'] = movie['rating']['imdb'][1]
movie['rating'] = movie['rating']['imdb'][0] movie['rating'] = movie['rating']['imdb'][0]

10
couchpotato/core/media/movie/providers/automation/trakt/main.py

@ -37,7 +37,7 @@ class TraktBase(Provider):
class Trakt(Automation, TraktBase): class Trakt(Automation, TraktBase):
urls = { urls = {
'watchlist': 'sync/watchlist/movies/', 'watchlist': 'sync/watchlist/movies?extended=full',
'oauth': 'https://api.couchpota.to/authorize/trakt/', 'oauth': 'https://api.couchpota.to/authorize/trakt/',
'refresh_token': 'https://api.couchpota.to/authorize/trakt_refresh/', 'refresh_token': 'https://api.couchpota.to/authorize/trakt_refresh/',
} }
@ -79,7 +79,13 @@ class Trakt(Automation, TraktBase):
def getIMDBids(self): def getIMDBids(self):
movies = [] movies = []
for movie in self.getWatchlist(): for movie in self.getWatchlist():
movies.append(movie.get('movie').get('ids').get('imdb')) m = movie.get('movie')
m['original_title'] = m['title']
log.debug("Movie: %s", m)
if self.isMinimalMovie(m):
log.info("Trakt automation: %s satisfies requirements, added", m.get('title'))
movies.append(m.get('ids').get('imdb'))
continue
return movies return movies

81
couchpotato/core/media/movie/providers/automation/yifypopular.py

@ -0,0 +1,81 @@
import HTMLParser
from couchpotato import fireEvent
from couchpotato.core.logger import CPLog
from couchpotato.core.media.movie.providers.automation.base import Automation
log = CPLog(__name__)
autoload = 'YTSPopular'
class YTSPopular(Automation):
interval = 1800
url = 'https://yts.lt/'
def getIMDBids(self):
movies = []
source = self.getHTMLData(self.url)
class MyHTMLParser(HTMLParser):
doparse = False
dotitle = False
doyear = False
currentmovie = {'title':"", 'year':""}
movies = []
def handle_starttag(self, tag, attrs):
for attr in attrs:
self.doparse = (attr[0] == "id" and attr[1] == "popular-downloads") or self.doparse
self.dotitle = (attr[0] == "class" and attr[1] == "browse-movie-title" and self.doparse)
self.doyear = (attr[0] == "class" and attr[1] == "browse-movie-year" and self.doparse)
if (attr[0] == "class" and attr[1] == "home-movies"):
self.doparse = False
def handle_endtag(self, tag):
self.dotitle = False
self.doyear = False
def handle_data(self, data):
if (self.doparse):
if (self.dotitle):
self.dotitle = False
self.currentmovie['title'] = data
if (self.doyear):
self.doyear = False
self.currentmovie['year'] = data
self.movies.append(self.currentmovie)
self.currentmovie = {'title':"", 'year':""}
def getMovies(self):
return self.movies
parser = MyHTMLParser()
parser.feed(source)
for el in parser.getMovies():
imdb = self.search(el['title'], el['year'])
if imdb and self.isMinimalMovie(imdb):
movies.append(imdb['imdb'])
return movies
config = [{
'name': 'ytspopular',
'groups': [
{
'tab': 'automation',
'list': 'automation_providers',
'name': 'ytspopular_automation',
'label': 'YTS Popular',
'description': 'Imports popular downloas as currently listed on YTS.',
'options': [
{
'name': 'automation_enabled',
'default': False,
'type': 'enabler',
},
],
},
],
}]

6
couchpotato/core/media/movie/providers/torrent/iptorrents.py

@ -11,9 +11,9 @@ class IPTorrents(MovieProvider, Base):
cat_ids = [ cat_ids = [
([87], ['3d']), ([87], ['3d']),
([89], ['bd50']), ([89, 90], ['bd50']),
([48], ['720p', '1080p']), ([48, 20, 62], ['720p', '1080p']),
([101], ['2160p']), ([100, 101], ['2160p']),
([48, 20], ['brrip']), ([48, 20], ['brrip']),
([7, 77], ['dvdrip']), ([7, 77], ['dvdrip']),
([6], ['dvdr']), ([6], ['dvdr']),

13
couchpotato/core/notifications/plex/__init__.py

@ -24,6 +24,19 @@ config = [{
'description': 'Hostname/IP, default localhost' 'description': 'Hostname/IP, default localhost'
}, },
{ {
'name': 'media_server_port',
'label': 'Port',
'default': '32400',
'description': 'Connection to the Media Server should use this port'
},
{
'name': 'use_https',
'label': 'Use HTTPS',
'default': '0',
'type': 'bool',
'description': 'Connection to the Media Server should use HTTPS instead of HTTP'
},
{
'name': 'username', 'name': 'username',
'label': 'Username', 'label': 'Username',
'default': '', 'default': '',

8
couchpotato/core/notifications/plex/server.py

@ -38,7 +38,7 @@ class PlexServer(object):
#Maintain support for older Plex installations without myPlex #Maintain support for older Plex installations without myPlex
if not self.plex.conf('auth_token') and not self.plex.conf('username') and not self.plex.conf('password'): if not self.plex.conf('auth_token') and not self.plex.conf('username') and not self.plex.conf('password'):
data = self.plex.urlopen('%s/%s' % ( data = self.plex.urlopen('%s/%s' % (
self.createHost(self.plex.conf('media_server'), port = 32400), self.createHost(self.plex.conf('media_server'), port = self.plex.conf('media_server_port'), use_https = self.plex.conf('use_https')),
path path
)) ))
else: else:
@ -71,7 +71,7 @@ class PlexServer(object):
#Add X-Plex-Token header for myPlex support workaround #Add X-Plex-Token header for myPlex support workaround
data = self.plex.urlopen('%s/%s?X-Plex-Token=%s' % ( data = self.plex.urlopen('%s/%s?X-Plex-Token=%s' % (
self.createHost(self.plex.conf('media_server'), port = 32400), self.createHost(self.plex.conf('media_server'), port = self.plex.conf('media_server_port'), use_https = self.plex.conf('use_https')),
path, path,
self.plex.conf('auth_token') self.plex.conf('auth_token')
)) ))
@ -139,9 +139,9 @@ class PlexServer(object):
return True return True
def createHost(self, host, port = None): def createHost(self, host, port = None, use_https = False):
h = cleanHost(host) h = cleanHost(host, True, use_https)
p = urlparse(h) p = urlparse(h)
h = h.rstrip('/') h = h.rstrip('/')

11
couchpotato/core/plugins/automation.py

@ -1,3 +1,4 @@
from couchpotato.api import addApiView
from couchpotato.core.event import addEvent, fireEvent from couchpotato.core.event import addEvent, fireEvent
from couchpotato.core.logger import CPLog from couchpotato.core.logger import CPLog
from couchpotato.core.plugins.base import Plugin from couchpotato.core.plugins.base import Plugin
@ -17,11 +18,21 @@ class Automation(Plugin):
if not Env.get('dev'): if not Env.get('dev'):
addEvent('app.load', self.addMovies) addEvent('app.load', self.addMovies)
addApiView('automation.add_movies', self.addMoviesFromApi, docs = {
'desc': 'Manually trigger the automation scan. Hangs until scan is complete. Useful for webhooks.',
'return': {'type': 'object: {"success": true}'},
})
addEvent('setting.save.automation.hour.after', self.setCrons) addEvent('setting.save.automation.hour.after', self.setCrons)
def setCrons(self): def setCrons(self):
fireEvent('schedule.interval', 'automation.add_movies', self.addMovies, hours = self.conf('hour', default = 12)) fireEvent('schedule.interval', 'automation.add_movies', self.addMovies, hours = self.conf('hour', default = 12))
def addMoviesFromApi(self, **kwargs):
self.addMovies()
return {
'success': True
}
def addMovies(self): def addMovies(self):
movies = fireEvent('automation.get_movies', merge = True) movies = fireEvent('automation.get_movies', merge = True)

2
couchpotato/core/plugins/userscript/template.js_tmpl

@ -100,7 +100,7 @@ var osd = function(){
// Try and get imdb url // Try and get imdb url
try { try {
var regex = new RegExp(/tt(\d{7})/); var regex = new RegExp(/tt(\d{8})/);
var imdb_id = document.body.innerHTML.match(regex)[0]; var imdb_id = document.body.innerHTML.match(regex)[0];
if (imdb_id) if (imdb_id)
iframe.setAttribute('src', createApiUrl('http://imdb.com/title/'+imdb_id+'/')) iframe.setAttribute('src', createApiUrl('http://imdb.com/title/'+imdb_id+'/'))

2
couchpotato/static/style/settings.scss

@ -704,7 +704,7 @@
z-index: 2; z-index: 2;
position: absolute; position: absolute;
width: 450px; width: 450px;
margin: 28px 0 20px 0; margin: 75px 0 20px 0;
@include theme(background, primary); @include theme(background, primary);
box-shadow: 0 0 15px 2px rgba(0,0,0,.15); box-shadow: 0 0 15px 2px rgba(0,0,0,.15);
border-radius: $border_radius $border_radius 0 0; border-radius: $border_radius $border_radius 0 0;

2
init/couchpotato.service

@ -7,6 +7,8 @@ ExecStart=/var/lib/CouchPotatoServer/CouchPotato.py
Type=simple Type=simple
User=couchpotato User=couchpotato
Group=couchpotato Group=couchpotato
Restart=always
RestartSec=2s
[Install] [Install]
WantedBy=multi-user.target WantedBy=multi-user.target

0
libs/deluge_client/__init__.py

275
libs/deluge_client/client.py

@ -0,0 +1,275 @@
import logging
import socket
import ssl
import struct
import warnings
import zlib
from .rencode import dumps, loads
RPC_RESPONSE = 1
RPC_ERROR = 2
RPC_EVENT = 3
MESSAGE_HEADER_SIZE = 5
READ_SIZE = 10
logger = logging.getLogger(__name__)
class DelugeClientException(Exception):
"""Base exception for all deluge client exceptions"""
class ConnectionLostException(DelugeClientException):
pass
class CallTimeoutException(DelugeClientException):
pass
class InvalidHeaderException(DelugeClientException):
pass
class FailedToReconnectException(DelugeClientException):
pass
class RemoteException(DelugeClientException):
pass
class DelugeRPCClient(object):
timeout = 20
def __init__(self, host, port, username, password, decode_utf8=False, automatic_reconnect=True):
self.host = host
self.port = port
self.username = username
self.password = password
self.deluge_version = None
# This is only applicable if deluge_version is 2
self.deluge_protocol_version = None
self.decode_utf8 = decode_utf8
if not self.decode_utf8:
warnings.warn('Using `decode_utf8=False` is deprecated, please set it to True.'
'The argument will be removed in a future release where it will be always True', DeprecationWarning)
self.automatic_reconnect = automatic_reconnect
self.request_id = 1
self.connected = False
self._create_socket()
def _create_socket(self, ssl_version=None):
if ssl_version is not None:
self._socket = ssl.wrap_socket(socket.socket(socket.AF_INET, socket.SOCK_STREAM), ssl_version=ssl_version)
else:
self._socket = ssl.wrap_socket(socket.socket(socket.AF_INET, socket.SOCK_STREAM))
self._socket.settimeout(self.timeout)
def connect(self):
"""
Connects to the Deluge instance
"""
self._connect()
logger.debug('Connected to Deluge, detecting daemon version')
self._detect_deluge_version()
logger.debug('Daemon version {} detected, logging in'.format(self.deluge_version))
if self.deluge_version == 2:
result = self.call('daemon.login', self.username, self.password, client_version='deluge-client')
else:
result = self.call('daemon.login', self.username, self.password)
logger.debug('Logged in with value %r' % result)
self.connected = True
def _connect(self):
logger.info('Connecting to %s:%s' % (self.host, self.port))
try:
self._socket.connect((self.host, self.port))
except ssl.SSLError as e:
# Note: have not verified that we actually get errno 258 for this error
if (hasattr(ssl, 'PROTOCOL_SSLv3') and
(getattr(e, 'reason', None) == 'UNSUPPORTED_PROTOCOL' or e.errno == 258)):
logger.warning('Was unable to ssl handshake, trying to force SSLv3 (insecure)')
self._create_socket(ssl_version=ssl.PROTOCOL_SSLv3)
self._socket.connect((self.host, self.port))
else:
raise
def disconnect(self):
"""
Disconnect from deluge
"""
if self.connected:
self._socket.close()
self._socket = None
self.connected = False
def _detect_deluge_version(self):
if self.deluge_version is not None:
return
self._send_call(1, None, 'daemon.info')
self._send_call(2, None, 'daemon.info')
self._send_call(2, 1, 'daemon.info')
result = self._socket.recv(1)
if result[:1] == b'D':
# This is a protocol deluge 2.0 was using before release
self.deluge_version = 2
self.deluge_protocol_version = None
# If we need the specific version of deluge 2, this is it.
daemon_version = self._receive_response(2, None, partial_data=result)
elif ord(result[:1]) == 1:
self.deluge_version = 2
self.deluge_protocol_version = 1
# If we need the specific version of deluge 2, this is it.
daemon_version = self._receive_response(2, 1, partial_data=result)
else:
self.deluge_version = 1
# Deluge 1 doesn't recover well from the bad request. Re-connect the socket.
self._socket.close()
self._create_socket()
self._connect()
def _send_call(self, deluge_version, protocol_version, method, *args, **kwargs):
self.request_id += 1
if method == 'daemon.login':
debug_args = list(args)
if len(debug_args) >= 2:
debug_args[1] = '<password hidden>'
logger.debug('Calling reqid %s method %r with args:%r kwargs:%r' % (self.request_id, method, debug_args, kwargs))
else:
logger.debug('Calling reqid %s method %r with args:%r kwargs:%r' % (self.request_id, method, args, kwargs))
req = ((self.request_id, method, args, kwargs), )
req = zlib.compress(dumps(req))
if deluge_version == 2:
if protocol_version is None:
# This was a protocol for deluge 2 before they introduced protocol version numbers
self._socket.send(b'D' + struct.pack("!i", len(req)))
elif protocol_version == 1:
self._socket.send(struct.pack('!BI', protocol_version, len(req)))
else:
raise Exception('Deluge protocol version {} is not (yet) supported.'.format(protocol_version))
self._socket.send(req)
def _receive_response(self, deluge_version, protocol_version, partial_data=b''):
expected_bytes = None
data = partial_data
while True:
try:
d = self._socket.recv(READ_SIZE)
except ssl.SSLError:
raise CallTimeoutException()
data += d
if deluge_version == 2:
if expected_bytes is None:
if len(data) < 5:
continue
header = data[:MESSAGE_HEADER_SIZE]
data = data[MESSAGE_HEADER_SIZE:]
if protocol_version is None:
if header[0] != b'D'[0]:
raise InvalidHeaderException('Expected D as first byte in reply')
elif ord(header[:1]) != protocol_version:
raise InvalidHeaderException(
'Expected protocol version ({}) as first byte in reply'.format(protocol_version)
)
if protocol_version is None:
expected_bytes = struct.unpack('!i', header[1:])[0]
else:
expected_bytes = struct.unpack('!I', header[1:])[0]
if len(data) >= expected_bytes:
data = zlib.decompress(data)
break
else:
try:
data = zlib.decompress(data)
except zlib.error:
if not d:
raise ConnectionLostException()
continue
break
data = list(loads(data, decode_utf8=self.decode_utf8))
msg_type = data.pop(0)
request_id = data.pop(0)
if msg_type == RPC_ERROR:
if self.deluge_version == 2:
exception_type, exception_msg, _, traceback = data
# On deluge 2, exception arguments are sent as tuple
if self.decode_utf8:
exception_msg = ', '.join(exception_msg)
else:
exception_msg = b', '.join(exception_msg)
else:
exception_type, exception_msg, traceback = data[0]
if self.decode_utf8:
exception = type(str(exception_type), (RemoteException, ), {})
exception_msg = '%s\n%s' % (exception_msg,
traceback)
else:
exception = type(str(exception_type.decode('utf-8', 'ignore')), (RemoteException, ), {})
exception_msg = '%s\n%s' % (exception_msg.decode('utf-8', 'ignore'),
traceback.decode('utf-8', 'ignore'))
raise exception(exception_msg)
elif msg_type == RPC_RESPONSE:
retval = data[0]
return retval
def reconnect(self):
"""
Reconnect
"""
self.disconnect()
self._create_socket()
self.connect()
def call(self, method, *args, **kwargs):
"""
Calls an RPC function
"""
tried_reconnect = False
for _ in range(2):
try:
self._send_call(self.deluge_version, self.deluge_protocol_version, method, *args, **kwargs)
return self._receive_response(self.deluge_version, self.deluge_protocol_version)
except (socket.error, ConnectionLostException, CallTimeoutException):
if self.automatic_reconnect:
if tried_reconnect:
raise FailedToReconnectException()
else:
try:
self.reconnect()
except (socket.error, ConnectionLostException, CallTimeoutException):
raise FailedToReconnectException()
tried_reconnect = True
else:
raise
def __getattr__(self, item):
return RPCCaller(self.call, item)
class RPCCaller(object):
def __init__(self, caller, method=''):
self.caller = caller
self.method = method
def __getattr__(self, item):
return RPCCaller(self.caller, self.method+'.'+item)
def __call__(self, *args, **kwargs):
return self.caller(self.method, *args, **kwargs)

474
libs/deluge_client/rencode.py

@ -0,0 +1,474 @@
# Original bencode module by Petru Paler, et al.
#
# Modifications by Connelly Barnes:
#
# - Added support for floats (sent as 32-bit or 64-bit in network
# order), bools, None.
# - Allowed dict keys to be of any serializable type.
# - Lists/tuples are always decoded as tuples (thus, tuples can be
# used as dict keys).
# - Embedded extra information in the 'typecodes' to save some space.
# - Added a restriction on integer length, so that malicious hosts
# cannot pass us large integers which take a long time to decode.
#
# Licensed by Bram Cohen under the "MIT license":
#
# "Copyright (C) 2001-2002 Bram Cohen
#
# Permission is hereby granted, free of charge, to any person
# obtaining a copy of this software and associated documentation files
# (the "Software"), to deal in the Software without restriction,
# including without limitation the rights to use, copy, modify, merge,
# publish, distribute, sublicense, and/or sell copies of the Software,
# and to permit persons to whom the Software is furnished to do so,
# subject to the following conditions:
#
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.
#
# The Software is provided "AS IS", without warranty of any kind,
# express or implied, including but not limited to the warranties of
# merchantability, fitness for a particular purpose and
# noninfringement. In no event shall the authors or copyright holders
# be liable for any claim, damages or other liability, whether in an
# action of contract, tort or otherwise, arising from, out of or in
# connection with the Software or the use or other dealings in the
# Software."
#
# (The rencode module is licensed under the above license as well).
#
"""
rencode -- Web safe object pickling/unpickling.
Public domain, Connelly Barnes 2006-2007.
The rencode module is a modified version of bencode from the
BitTorrent project. For complex, heterogeneous data structures with
many small elements, r-encodings take up significantly less space than
b-encodings:
>>> len(rencode.dumps({'a':0, 'b':[1,2], 'c':99}))
13
>>> len(bencode.bencode({'a':0, 'b':[1,2], 'c':99}))
26
The rencode format is not standardized, and may change with different
rencode module versions, so you should check that you are using the
same rencode version throughout your project.
"""
import struct
import sys
from threading import Lock
try:
from future_builtins import zip
except ImportError:
# Ignore on Py3.
pass
__version__ = ('Python', 1, 0, 4)
__all__ = ['dumps', 'loads']
py3 = sys.version_info[0] >= 3
if py3:
long = int # pylint: disable=redefined-builtin
unicode = str # pylint: disable=redefined-builtin
def int2byte(c):
return bytes([c])
else:
def int2byte(c):
return chr(c)
# Default number of bits for serialized floats, either 32 or 64 (also a parameter for dumps()).
DEFAULT_FLOAT_BITS = 32
# Maximum length of integer when written as base 10 string.
MAX_INT_LENGTH = 64
# The bencode 'typecodes' such as i, d, etc have been extended and
# relocated on the base-256 character set.
CHR_LIST = int2byte(59)
CHR_DICT = int2byte(60)
CHR_INT = int2byte(61)
CHR_INT1 = int2byte(62)
CHR_INT2 = int2byte(63)
CHR_INT4 = int2byte(64)
CHR_INT8 = int2byte(65)
CHR_FLOAT32 = int2byte(66)
CHR_FLOAT64 = int2byte(44)
CHR_TRUE = int2byte(67)
CHR_FALSE = int2byte(68)
CHR_NONE = int2byte(69)
CHR_TERM = int2byte(127)
# Positive integers with value embedded in typecode.
INT_POS_FIXED_START = 0
INT_POS_FIXED_COUNT = 44
# Dictionaries with length embedded in typecode.
DICT_FIXED_START = 102
DICT_FIXED_COUNT = 25
# Negative integers with value embedded in typecode.
INT_NEG_FIXED_START = 70
INT_NEG_FIXED_COUNT = 32
# Strings with length embedded in typecode.
STR_FIXED_START = 128
STR_FIXED_COUNT = 64
# Lists with length embedded in typecode.
LIST_FIXED_START = STR_FIXED_START + STR_FIXED_COUNT
LIST_FIXED_COUNT = 64
# Whether strings should be decoded when loading
_decode_utf8 = False
def decode_int(x, f):
f += 1
newf = x.index(CHR_TERM, f)
if newf - f >= MAX_INT_LENGTH:
raise ValueError('overflow')
try:
n = int(x[f:newf])
except (OverflowError, ValueError):
n = long(x[f:newf])
if x[f:f + 1] == '-':
if x[f + 1:f + 2] == '0':
raise ValueError
elif x[f:f + 1] == '0' and newf != f + 1:
raise ValueError
return (n, newf + 1)
def decode_intb(x, f):
f += 1
return (struct.unpack('!b', x[f:f + 1])[0], f + 1)
def decode_inth(x, f):
f += 1
return (struct.unpack('!h', x[f:f + 2])[0], f + 2)
def decode_intl(x, f):
f += 1
return (struct.unpack('!l', x[f:f + 4])[0], f + 4)
def decode_intq(x, f):
f += 1
return (struct.unpack('!q', x[f:f + 8])[0], f + 8)
def decode_float32(x, f):
f += 1
n = struct.unpack('!f', x[f:f + 4])[0]
return (n, f + 4)
def decode_float64(x, f):
f += 1
n = struct.unpack('!d', x[f:f + 8])[0]
return (n, f + 8)
def decode_string(x, f):
colon = x.index(b':', f)
try:
n = int(x[f:colon])
except (OverflowError, ValueError):
n = long(x[f:colon])
if x[f] == '0' and colon != f + 1:
raise ValueError
colon += 1
s = x[colon:colon + n]
if _decode_utf8:
s = s.decode('utf8')
return (s, colon + n)
def decode_list(x, f):
r, f = [], f + 1
while x[f:f + 1] != CHR_TERM:
v, f = decode_func[x[f:f + 1]](x, f)
r.append(v)
return (tuple(r), f + 1)
def decode_dict(x, f):
r, f = {}, f + 1
while x[f:f + 1] != CHR_TERM:
k, f = decode_func[x[f:f + 1]](x, f)
r[k], f = decode_func[x[f:f + 1]](x, f)
return (r, f + 1)
def decode_true(x, f):
return (True, f + 1)
def decode_false(x, f):
return (False, f + 1)
def decode_none(x, f):
return (None, f + 1)
decode_func = {}
decode_func[b'0'] = decode_string
decode_func[b'1'] = decode_string
decode_func[b'2'] = decode_string
decode_func[b'3'] = decode_string
decode_func[b'4'] = decode_string
decode_func[b'5'] = decode_string
decode_func[b'6'] = decode_string
decode_func[b'7'] = decode_string
decode_func[b'8'] = decode_string
decode_func[b'9'] = decode_string
decode_func[CHR_LIST] = decode_list
decode_func[CHR_DICT] = decode_dict
decode_func[CHR_INT] = decode_int
decode_func[CHR_INT1] = decode_intb
decode_func[CHR_INT2] = decode_inth
decode_func[CHR_INT4] = decode_intl
decode_func[CHR_INT8] = decode_intq
decode_func[CHR_FLOAT32] = decode_float32
decode_func[CHR_FLOAT64] = decode_float64
decode_func[CHR_TRUE] = decode_true
decode_func[CHR_FALSE] = decode_false
decode_func[CHR_NONE] = decode_none
def make_fixed_length_string_decoders():
def make_decoder(slen):
def f(x, f):
s = x[f + 1:f + 1 + slen]
if _decode_utf8:
s = s.decode('utf8')
return (s, f + 1 + slen)
return f
for i in range(STR_FIXED_COUNT):
decode_func[int2byte(STR_FIXED_START + i)] = make_decoder(i)
make_fixed_length_string_decoders()
def make_fixed_length_list_decoders():
def make_decoder(slen):
def f(x, f):
r, f = [], f + 1
for _ in range(slen):
v, f = decode_func[x[f:f + 1]](x, f)
r.append(v)
return (tuple(r), f)
return f
for i in range(LIST_FIXED_COUNT):
decode_func[int2byte(LIST_FIXED_START + i)] = make_decoder(i)
make_fixed_length_list_decoders()
def make_fixed_length_int_decoders():
def make_decoder(j):
def f(x, f):
return (j, f + 1)
return f
for i in range(INT_POS_FIXED_COUNT):
decode_func[int2byte(INT_POS_FIXED_START + i)] = make_decoder(i)
for i in range(INT_NEG_FIXED_COUNT):
decode_func[int2byte(INT_NEG_FIXED_START + i)] = make_decoder(-1 - i)
make_fixed_length_int_decoders()
def make_fixed_length_dict_decoders():
def make_decoder(slen):
def f(x, f):
r, f = {}, f + 1
for _ in range(slen):
k, f = decode_func[x[f:f + 1]](x, f)
r[k], f = decode_func[x[f:f + 1]](x, f)
return (r, f)
return f
for i in range(DICT_FIXED_COUNT):
decode_func[int2byte(DICT_FIXED_START + i)] = make_decoder(i)
make_fixed_length_dict_decoders()
def loads(x, decode_utf8=False):
global _decode_utf8
_decode_utf8 = decode_utf8
try:
r, l = decode_func[x[0:1]](x, 0)
except (IndexError, KeyError):
raise ValueError
if l != len(x):
raise ValueError
return r
def encode_int(x, r):
if 0 <= x < INT_POS_FIXED_COUNT:
r.append(int2byte(INT_POS_FIXED_START + x))
elif -INT_NEG_FIXED_COUNT <= x < 0:
r.append(int2byte(INT_NEG_FIXED_START - 1 - x))
elif -128 <= x < 128:
r.extend((CHR_INT1, struct.pack('!b', x)))
elif -32768 <= x < 32768:
r.extend((CHR_INT2, struct.pack('!h', x)))
elif -2147483648 <= x < 2147483648:
r.extend((CHR_INT4, struct.pack('!l', x)))
elif -9223372036854775808 <= x < 9223372036854775808:
r.extend((CHR_INT8, struct.pack('!q', x)))
else:
s = str(x)
if py3:
s = bytes(s, 'ascii')
if len(s) >= MAX_INT_LENGTH:
raise ValueError('overflow')
r.extend((CHR_INT, s, CHR_TERM))
def encode_float32(x, r):
r.extend((CHR_FLOAT32, struct.pack('!f', x)))
def encode_float64(x, r):
r.extend((CHR_FLOAT64, struct.pack('!d', x)))
def encode_bool(x, r):
r.append({False: CHR_FALSE, True: CHR_TRUE}[bool(x)])
def encode_none(x, r):
r.append(CHR_NONE)
def encode_string(x, r):
if len(x) < STR_FIXED_COUNT:
r.extend((int2byte(STR_FIXED_START + len(x)), x))
else:
s = str(len(x))
if py3:
s = bytes(s, 'ascii')
r.extend((s, b':', x))
def encode_unicode(x, r):
encode_string(x.encode('utf8'), r)
def encode_list(x, r):
if len(x) < LIST_FIXED_COUNT:
r.append(int2byte(LIST_FIXED_START + len(x)))
for i in x:
encode_func[type(i)](i, r)
else:
r.append(CHR_LIST)
for i in x:
encode_func[type(i)](i, r)
r.append(CHR_TERM)
def encode_dict(x, r):
if len(x) < DICT_FIXED_COUNT:
r.append(int2byte(DICT_FIXED_START + len(x)))
for k, v in x.items():
encode_func[type(k)](k, r)
encode_func[type(v)](v, r)
else:
r.append(CHR_DICT)
for k, v in x.items():
encode_func[type(k)](k, r)
encode_func[type(v)](v, r)
r.append(CHR_TERM)
encode_func = {}
encode_func[int] = encode_int
encode_func[long] = encode_int
encode_func[bytes] = encode_string
encode_func[list] = encode_list
encode_func[tuple] = encode_list
encode_func[dict] = encode_dict
encode_func[type(None)] = encode_none
encode_func[unicode] = encode_unicode
encode_func[bool] = encode_bool
lock = Lock()
def dumps(x, float_bits=DEFAULT_FLOAT_BITS):
"""
Dump data structure to str.
Here float_bits is either 32 or 64.
"""
with lock:
if float_bits == 32:
encode_func[float] = encode_float32
elif float_bits == 64:
encode_func[float] = encode_float64
else:
raise ValueError('Float bits (%d) is not 32 or 64' % float_bits)
r = []
encode_func[type(x)](x, r)
return b''.join(r)
def test():
f1 = struct.unpack('!f', struct.pack('!f', 25.5))[0]
f2 = struct.unpack('!f', struct.pack('!f', 29.3))[0]
f3 = struct.unpack('!f', struct.pack('!f', -0.6))[0]
ld = (({b'a': 15, b'bb': f1, b'ccc': f2, b'': (f3, (), False, True, b'')}, (b'a', 10**20),
tuple(range(-100000, 100000)), b'b' * 31, b'b' * 62, b'b' * 64, 2**30, 2**33, 2**62,
2**64, 2**30, 2**33, 2**62, 2**64, False, False, True, -1, 2, 0),)
assert loads(dumps(ld)) == ld
d = dict(zip(range(-100000, 100000), range(-100000, 100000)))
d.update({b'a': 20, 20: 40, 40: 41, f1: f2, f2: f3, f3: False, False: True, True: False})
ld = (d, {}, {5: 6}, {7: 7, True: 8}, {9: 10, 22: 39, 49: 50, 44: b''})
assert loads(dumps(ld)) == ld
ld = (b'', b'a' * 10, b'a' * 100, b'a' * 1000, b'a' * 10000, b'a' * 100000, b'a' * 1000000, b'a' * 10000000)
assert loads(dumps(ld)) == ld
ld = tuple([dict(zip(range(n), range(n))) for n in range(100)]) + (b'b',)
assert loads(dumps(ld)) == ld
ld = tuple([dict(zip(range(n), range(-n, 0))) for n in range(100)]) + (b'b',)
assert loads(dumps(ld)) == ld
ld = tuple([tuple(range(n)) for n in range(100)]) + (b'b',)
assert loads(dumps(ld)) == ld
ld = tuple([b'a' * n for n in range(1000)]) + (b'b',)
assert loads(dumps(ld)) == ld
ld = tuple([b'a' * n for n in range(1000)]) + (None, True, None)
assert loads(dumps(ld)) == ld
assert loads(dumps(None)) is None
assert loads(dumps({None: None})) == {None: None}
assert 1e-10 < abs(loads(dumps(1.1)) - 1.1) < 1e-6
assert 1e-10 < abs(loads(dumps(1.1, 32)) - 1.1) < 1e-6
assert abs(loads(dumps(1.1, 64)) - 1.1) < 1e-12
assert loads(dumps('Hello World!!'), decode_utf8=True)
try:
import psyco
psyco.bind(dumps)
psyco.bind(loads)
except ImportError:
pass
if __name__ == '__main__':
test()

65
libs/deluge_client/tests.py

@ -0,0 +1,65 @@
import os
import sys
import pytest
from .client import DelugeRPCClient, RemoteException
if sys.version_info > (3,):
long = int
@pytest.fixture
def client(request):
if sys.platform.startswith('win'):
auth_path = os.path.join(os.getenv('APPDATA'), 'deluge', 'auth')
else:
auth_path = os.path.expanduser("~/.config/deluge/auth")
with open(auth_path, 'rb') as f:
filedata = f.read().decode("utf-8").split('\n')[0].split(':')
username, password = filedata[:2]
ip = '127.0.0.1'
port = 58846
kwargs = {'decode_utf8': True}
if hasattr(request, 'param'):
kwargs.update(request.param)
client = DelugeRPCClient(ip, port, username, password, **kwargs)
client.connect()
yield client
try:
client.disconnect()
except:
pass
def test_connect(client):
assert client.connected
def test_call_method(client):
assert isinstance(client.call('core.get_free_space'), (int, long))
def test_call_method_arguments(client):
assert isinstance(client.call('core.get_free_space', '/'), (int, long))
@pytest.mark.parametrize('client',
[{'decode_utf8': True}, {'decode_utf8': False}],
ids=['decode_utf8_on', 'decode_utf8_off'],
indirect=True)
def test_call_method_exception(client):
with pytest.raises(RemoteException) as ex_info:
client.call('core.get_free_space', '1', '2')
assert ('takes at most 2 arguments' in str(ex_info.value) or
'takes from 1 to 2 positional arguments' in str(ex_info.value)) # deluge 2.0
def test_attr_caller(client):
assert isinstance(client.core.get_free_space(), (int, long))
assert isinstance(client.core.get_free_space('/'), (int, long))

2
libs/synchronousdeluge/transfer.py

@ -19,7 +19,7 @@ class DelugeTransfer(object):
self.disconnect() self.disconnect()
self.sock = socket.create_connection(hostport) self.sock = socket.create_connection(hostport)
self.conn = ssl.wrap_socket(self.sock, None, None, False, ssl.CERT_NONE, ssl.PROTOCOL_TLSv1) self.conn = ssl.wrap_socket(self.sock, None, None, False, ssl.CERT_NONE, ssl.PROTOCOL_TLSv1_2)
self.connected = True self.connected = True
def disconnect(self): def disconnect(self):

Loading…
Cancel
Save