diff --git a/CouchPotato.py b/CouchPotato.py index b4a6421..d3492cf 100755 --- a/CouchPotato.py +++ b/CouchPotato.py @@ -1,4 +1,4 @@ -#!/usr/bin/env python +#!/usr/bin/env python2 from __future__ import print_function from logging import handlers from os.path import dirname diff --git a/couchpotato/core/downloaders/deluge.py b/couchpotato/core/downloaders/deluge.py index aaca40e..d788bcd 100644 --- a/couchpotato/core/downloaders/deluge.py +++ b/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.variable import tryFloat, cleanHost from couchpotato.core.logger import CPLog -from synchronousdeluge import DelugeClient - +from deluge_client.client import DelugeRPCClient log = CPLog(__name__) @@ -80,17 +79,17 @@ class Deluge(DownloaderBase): } if self.conf('directory'): - if os.path.isdir(self.conf('directory')): + #if os.path.isdir(self.conf('directory')): options['download_location'] = self.conf('directory') - else: - log.error('Download directory from Deluge settings: %s doesn\'t exist', self.conf('directory')) + #else: + # log.error('Download directory from Deluge settings: %s doesn\'t exist', self.conf('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_path'] = self.conf('completed_directory') - else: - log.error('Download directory from Deluge settings: %s doesn\'t exist', self.conf('directory')) + #else: + # log.error('Download directory from Deluge settings: %s doesn\'t exist', self.conf('directory')) if data.get('seed_ratio'): options['stop_at_ratio'] = 1 @@ -221,8 +220,10 @@ class DelugeRPC(object): self.password = password def connect(self): - self.client = DelugeClient() - self.client.connect(self.host, int(self.port), self.username, self.password) + #self.client = DelugeClient() + #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): try: @@ -235,12 +236,12 @@ class DelugeRPC(object): torrent_id = False try: 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: torrent_id = self._check_torrent(True, torrent) 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: log.error('Failed to add torrent magnet %s: %s %s', (torrent, err, traceback.format_exc())) finally: @@ -253,12 +254,12 @@ class DelugeRPC(object): torrent_id = False try: 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: torrent_id = self._check_torrent(False, torrent) 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: log.error('Failed to add torrent file %s: %s %s', (filename, err, traceback.format_exc())) finally: @@ -271,7 +272,7 @@ class DelugeRPC(object): ret = False try: 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: log.error('Failed to get all torrents: %s %s', (err, traceback.format_exc())) finally: @@ -282,7 +283,7 @@ class DelugeRPC(object): def pause_torrent(self, torrent_ids): try: self.connect() - self.client.core.pause_torrent(torrent_ids).get() + self.client.core.pause_torrent(torrent_ids) except Exception as err: log.error('Failed to pause torrent: %s %s', (err, traceback.format_exc())) finally: @@ -292,7 +293,7 @@ class DelugeRPC(object): def resume_torrent(self, torrent_ids): try: self.connect() - self.client.core.resume_torrent(torrent_ids).get() + self.client.core.resume_torrent(torrent_ids) except Exception as err: log.error('Failed to resume torrent: %s %s', (err, traceback.format_exc())) finally: @@ -303,7 +304,7 @@ class DelugeRPC(object): ret = False try: 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: log.error('Failed to remove torrent: %s %s', (err, traceback.format_exc())) finally: @@ -327,7 +328,7 @@ class DelugeRPC(object): torrent_hash = b16encode(b32decode(torrent_hash)) 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']: return torrent_hash diff --git a/couchpotato/core/helpers/variable.py b/couchpotato/core/helpers/variable.py index a3dfff5..0db7248 100755 --- a/couchpotato/core/helpers/variable.py +++ b/couchpotato/core/helpers/variable.py @@ -194,12 +194,12 @@ def getImdb(txt, check_inside = False, multiple = False): output.close() try: - ids = re.findall('(tt\d{4,7})', txt) + ids = re.findall('(tt\d{4,8})', txt) if multiple: - return removeDuplicate(['tt%07d' % tryInt(x[2:]) for x in ids]) if len(ids) > 0 else [] + return removeDuplicate(['tt%08d' % tryInt(x[2:]) for x in ids]) if len(ids) > 0 else [] - return 'tt%07d' % tryInt(ids[0][2:]) + return 'tt%08d' % tryInt(ids[0][2:]) except IndexError: pass diff --git a/couchpotato/core/media/_base/providers/torrent/iptorrents.py b/couchpotato/core/media/_base/providers/torrent/iptorrents.py index e3331ef..c25d686 100644 --- a/couchpotato/core/media/_base/providers/torrent/iptorrents.py +++ b/couchpotato/core/media/_base/providers/torrent/iptorrents.py @@ -16,15 +16,14 @@ class Base(TorrentProvider): urls = { 'test': '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', } 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 + def buildUrl(self, title, media, quality): return self._buildUrl(title.replace(':', ''), quality) @@ -37,7 +36,7 @@ class Base(TorrentProvider): return None query = query.replace('"', '') - + return self.urls['search'] % ("&".join(("%d=" % x) for x in cat_ids), tryUrlencode(query).replace('%', '%%')) def _searchOnTitle(self, title, media, quality, results): @@ -50,8 +49,8 @@ class Base(TorrentProvider): pages = 1 current_page = 1 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: html = BeautifulSoup(data) @@ -63,8 +62,8 @@ class Base(TorrentProvider): final_page_link = next_link.previous_sibling.previous_sibling 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(): return @@ -102,18 +101,18 @@ class Base(TorrentProvider): current_page += 1 - def getLoginParams(self): + def getRequestHeaders(self): return { - 'username': self.conf('username'), - 'password': self.conf('password'), - 'login': 'submit', + 'Cookie': self.conf('cookiesetting') or '' } + + 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 'don\'t have an account' not in output.lower() - - def loginCheckSuccess(self, output): - return '/logout.php' in output.lower() + return 'try_next' config = [{ @@ -137,11 +136,6 @@ config = [{ 'default': '', }, { - 'name': 'password', - 'default': '', - 'type': 'password', - }, - { 'name': 'freeleech', 'default': 0, 'type': 'bool', @@ -161,6 +155,12 @@ config = [{ 'default': 40, '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', 'advanced': True, @@ -173,3 +173,4 @@ config = [{ }, ], }] + diff --git a/couchpotato/core/media/movie/providers/automation/base.py b/couchpotato/core/media/movie/providers/automation/base.py index ee19649..113191f 100644 --- a/couchpotato/core/media/movie/providers/automation/base.py +++ b/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'])) 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['rating'] = movie['rating']['imdb'][0] diff --git a/couchpotato/core/media/movie/providers/automation/trakt/main.py b/couchpotato/core/media/movie/providers/automation/trakt/main.py index fcec75e..dbfc11d 100644 --- a/couchpotato/core/media/movie/providers/automation/trakt/main.py +++ b/couchpotato/core/media/movie/providers/automation/trakt/main.py @@ -37,7 +37,7 @@ class TraktBase(Provider): class Trakt(Automation, TraktBase): urls = { - 'watchlist': 'sync/watchlist/movies/', + 'watchlist': 'sync/watchlist/movies?extended=full', 'oauth': 'https://api.couchpota.to/authorize/trakt/', 'refresh_token': 'https://api.couchpota.to/authorize/trakt_refresh/', } @@ -79,7 +79,13 @@ class Trakt(Automation, TraktBase): def getIMDBids(self): movies = [] 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 diff --git a/couchpotato/core/media/movie/providers/torrent/iptorrents.py b/couchpotato/core/media/movie/providers/torrent/iptorrents.py index 699d5b9..72d96bd 100644 --- a/couchpotato/core/media/movie/providers/torrent/iptorrents.py +++ b/couchpotato/core/media/movie/providers/torrent/iptorrents.py @@ -11,9 +11,9 @@ class IPTorrents(MovieProvider, Base): cat_ids = [ ([87], ['3d']), - ([89], ['bd50']), - ([48], ['720p', '1080p']), - ([101], ['2160p']), + ([89, 90], ['bd50']), + ([48, 20, 62], ['720p', '1080p']), + ([100, 101], ['2160p']), ([48, 20], ['brrip']), ([7, 77], ['dvdrip']), ([6], ['dvdr']), diff --git a/couchpotato/core/notifications/plex/__init__.py b/couchpotato/core/notifications/plex/__init__.py index 957369b..4e2a4fd 100755 --- a/couchpotato/core/notifications/plex/__init__.py +++ b/couchpotato/core/notifications/plex/__init__.py @@ -24,6 +24,19 @@ config = [{ 'description': 'Hostname/IP, default localhost' }, { + 'name': 'media_server_port', + 'label': 'Port', + 'default': '32400', + 'description': 'Connection tot he 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', 'label': 'Username', 'default': '', diff --git a/couchpotato/core/notifications/plex/server.py b/couchpotato/core/notifications/plex/server.py index 9c8df76..9c52e6d 100644 --- a/couchpotato/core/notifications/plex/server.py +++ b/couchpotato/core/notifications/plex/server.py @@ -38,7 +38,7 @@ class PlexServer(object): #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'): 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 )) else: @@ -71,7 +71,7 @@ class PlexServer(object): #Add X-Plex-Token header for myPlex support workaround 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, self.plex.conf('auth_token') )) @@ -139,9 +139,9 @@ class PlexServer(object): 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) h = h.rstrip('/') diff --git a/couchpotato/core/plugins/automation.py b/couchpotato/core/plugins/automation.py index e98a00a..110d4c4 100644 --- a/couchpotato/core/plugins/automation.py +++ b/couchpotato/core/plugins/automation.py @@ -1,3 +1,4 @@ +from couchpotato.api import addApiView from couchpotato.core.event import addEvent, fireEvent from couchpotato.core.logger import CPLog from couchpotato.core.plugins.base import Plugin @@ -17,11 +18,21 @@ class Automation(Plugin): if not Env.get('dev'): 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) def setCrons(self): 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): movies = fireEvent('automation.get_movies', merge = True) diff --git a/couchpotato/core/plugins/userscript/template.js_tmpl b/couchpotato/core/plugins/userscript/template.js_tmpl index 25e1842..8023b18 100644 --- a/couchpotato/core/plugins/userscript/template.js_tmpl +++ b/couchpotato/core/plugins/userscript/template.js_tmpl @@ -100,7 +100,7 @@ var osd = function(){ // Try and get imdb url try { - var regex = new RegExp(/tt(\d{7})/); + var regex = new RegExp(/tt(\d{8})/); var imdb_id = document.body.innerHTML.match(regex)[0]; if (imdb_id) iframe.setAttribute('src', createApiUrl('http://imdb.com/title/'+imdb_id+'/')) diff --git a/couchpotato/static/style/settings.scss b/couchpotato/static/style/settings.scss index 0724ada..ad19dcd 100644 --- a/couchpotato/static/style/settings.scss +++ b/couchpotato/static/style/settings.scss @@ -704,7 +704,7 @@ z-index: 2; position: absolute; width: 450px; - margin: 28px 0 20px 0; + margin: 75px 0 20px 0; @include theme(background, primary); box-shadow: 0 0 15px 2px rgba(0,0,0,.15); border-radius: $border_radius $border_radius 0 0; diff --git a/init/couchpotato.service b/init/couchpotato.service index c9a0c47..c767c97 100644 --- a/init/couchpotato.service +++ b/init/couchpotato.service @@ -7,6 +7,8 @@ ExecStart=/var/lib/CouchPotatoServer/CouchPotato.py Type=simple User=couchpotato Group=couchpotato +Restart=always +RestartSec=2s [Install] WantedBy=multi-user.target diff --git a/libs/deluge_client/__init__.py b/libs/deluge_client/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/libs/deluge_client/client.py b/libs/deluge_client/client.py new file mode 100644 index 0000000..827fcd7 --- /dev/null +++ b/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] = '