diff --git a/couchpotato/core/_base/_core/__init__.py b/couchpotato/core/_base/_core/__init__.py index 8d702f18..c8c3fda6 100644 --- a/couchpotato/core/_base/_core/__init__.py +++ b/couchpotato/core/_base/_core/__init__.py @@ -23,20 +23,22 @@ config = [{ 'default': '', 'type': 'password', }, - { - 'name': 'host', - 'advanced': True, - 'default': '0.0.0.0', - 'hidden': True, - 'label': 'IP', - 'description': 'Host that I should listen to. "0.0.0.0" listens to all ips.', - }, { 'name': 'port', 'default': 5050, 'type': 'int', 'description': 'The port I should listen to.', }, + { + 'name': 'ssl_cert', + 'description': 'Path to SSL server.crt', + 'advanced': True, + }, + { + 'name': 'ssl_key', + 'description': 'Path to SSL server.key', + 'advanced': True, + }, { 'name': 'launch_browser', 'default': True, diff --git a/couchpotato/core/downloaders/base.py b/couchpotato/core/downloaders/base.py index 7a695e21..377e6062 100644 --- a/couchpotato/core/downloaders/base.py +++ b/couchpotato/core/downloaders/base.py @@ -33,17 +33,36 @@ class Downloader(Provider): ] def __init__(self): - addEvent('download', self.download) - addEvent('download.status', self.getAllDownloadStatus) - addEvent('download.remove_failed', self.removeFailed) + addEvent('download', self._download) + addEvent('download.enabled', self._isEnabled) + addEvent('download.enabled_types', self.getEnabledDownloadType) + addEvent('download.status', self._getAllDownloadStatus) + addEvent('download.remove_failed', self._removeFailed) - def download(self, data = {}, movie = {}, manual = False, filedata = None): - pass + def getEnabledDownloadType(self): + for download_type in self.type: + if self.isEnabled(manual = True, data = {'type': download_type}): + return self.type - def getAllDownloadStatus(self): + return [] + + def _download(self, data = {}, movie = {}, manual = False, filedata = None): + if self.isDisabled(manual, data): + return + return self.download(data = data, movie = movie, filedata = filedata) + + def _getAllDownloadStatus(self): + if self.isDisabled(manual = True, data = {}): + return + + return self.getAllDownloadStatus() + + def _removeFailed(self, item): + if self.conf('delete_failed', default = True): + return self.removeFailed(self, item) return False - def removeFailed(self, name = {}, nzo_id = {}): + def removeFailed(self, item): return False def isCorrectType(self, item_type): @@ -77,9 +96,14 @@ class Downloader(Provider): log.error('Failed converting magnet url to torrent: %s', (torrent_hash)) return False - def isDisabled(self, manual): - return not self.isEnabled(manual) + def isDisabled(self, manual, data): + return not self.isEnabled(manual, data) - def isEnabled(self, manual): + def _isEnabled(self, manual, data = {}): + if not self.isEnabled(manual, data): + return + return True + + def isEnabled(self, manual, data = {}): d_manual = self.conf('manual', default = False) - return super(Downloader, self).isEnabled() and ((d_manual and manual) or (d_manual is False)) + return super(Downloader, self).isEnabled() and ((d_manual and manual) or (d_manual is False)) and self.isCorrectType(data.get('type')) diff --git a/couchpotato/core/downloaders/blackhole/main.py b/couchpotato/core/downloaders/blackhole/main.py index ca38fc49..402e607a 100644 --- a/couchpotato/core/downloaders/blackhole/main.py +++ b/couchpotato/core/downloaders/blackhole/main.py @@ -10,11 +10,7 @@ class Blackhole(Downloader): type = ['nzb', 'torrent', 'torrent_magnet'] - def download(self, data = {}, movie = {}, manual = False, filedata = None): - if self.isDisabled(manual) or \ - (not self.isCorrectType(data.get('type')) or \ - (not self.conf('use_for') in ['both', 'torrent' if 'torrent' in data.get('type') else data.get('type')])): - return + def download(self, data = {}, movie = {}, filedata = None): directory = self.conf('directory') if not directory or not os.path.isdir(directory): @@ -52,4 +48,17 @@ class Blackhole(Downloader): except: log.info('Failed to download file %s: %s', (data.get('name'), traceback.format_exc())) return False + return False + + def getEnabledDownloadType(self): + if self.conf('use_for') == 'both': + return super(Blackhole, self).getEnabledDownloadType() + elif self.conf('use_for') == 'torrent': + return ['torrent', 'torrent_magnet'] + else: + return ['nzb'] + + def isEnabled(self, manual, data = {}): + return super(Blackhole, self).isEnabled(manual, data) and \ + ((self.conf('use_for') in ['both', 'torrent' if 'torrent' in data.get('type') else data.get('type')])) diff --git a/couchpotato/core/downloaders/nzbget/__init__.py b/couchpotato/core/downloaders/nzbget/__init__.py index 4f5afb36..4fdac265 100644 --- a/couchpotato/core/downloaders/nzbget/__init__.py +++ b/couchpotato/core/downloaders/nzbget/__init__.py @@ -33,6 +33,13 @@ config = [{ 'default': 'Movies', 'description': 'The category CP places the nzb in. Like movies or couchpotato', }, + { + 'name': 'priority', + 'default': '0', + 'type': 'dropdown', + 'values': [('Very Low', -100), ('Low', -50), ('Normal', 0), ('High', 50), ('Very High', 100)], + 'description': 'Only change this if you are using NZBget 9.0 or higher', + }, { 'name': 'manual', 'default': 0, diff --git a/couchpotato/core/downloaders/nzbget/main.py b/couchpotato/core/downloaders/nzbget/main.py index 0d9c52aa..c3b9703e 100644 --- a/couchpotato/core/downloaders/nzbget/main.py +++ b/couchpotato/core/downloaders/nzbget/main.py @@ -1,7 +1,8 @@ from base64 import standard_b64encode from couchpotato.core.downloaders.base import Downloader +from couchpotato.core.helpers.variable import tryInt from couchpotato.core.logger import CPLog -from inspect import isfunction +import re import socket import traceback import xmlrpclib @@ -14,10 +15,7 @@ class NZBGet(Downloader): url = 'http://nzbget:%(password)s@%(host)s/xmlrpc' - def download(self, data = {}, movie = {}, manual = False, filedata = None): - - if self.isDisabled(manual) or not self.isCorrectType(data.get('type')): - return + def download(self, data = {}, movie = {}, filedata = None): if not filedata: log.error('Unable to get NZB file: %s', traceback.format_exc()) @@ -44,7 +42,12 @@ class NZBGet(Downloader): log.error('Protocol Error: %s', e) return False - if rpc.append(nzb_name, self.conf('category'), False, standard_b64encode(filedata.strip())): + if re.search(r"^0", rpc.version()): + xml_response = rpc.append(nzb_name, self.conf('category'), False, standard_b64encode(filedata.strip())) + else: + xml_response = rpc.append(nzb_name, self.conf('category'), tryInt(self.conf('priority')), False, standard_b64encode(filedata.strip())) + + if xml_response: log.info('NZB sent successfully to NZBGet') return True else: diff --git a/couchpotato/core/downloaders/nzbvortex/main.py b/couchpotato/core/downloaders/nzbvortex/main.py index 32b1e6e8..1462c678 100644 --- a/couchpotato/core/downloaders/nzbvortex/main.py +++ b/couchpotato/core/downloaders/nzbvortex/main.py @@ -22,10 +22,7 @@ class NZBVortex(Downloader): api_level = None session_id = None - def download(self, data = {}, movie = {}, manual = False, filedata = None): - - if self.isDisabled(manual) or not self.isCorrectType(data.get('type')) or not self.getApiLevel(): - return + def download(self, data = {}, movie = {}, filedata = None): # Send the nzb try: @@ -39,9 +36,6 @@ class NZBVortex(Downloader): def getAllDownloadStatus(self): - if self.isDisabled(manual = True): - return False - raw_statuses = self.call('nzb') statuses = [] @@ -66,9 +60,6 @@ class NZBVortex(Downloader): def removeFailed(self, item): - if not self.conf('delete_failed', default = True): - return False - log.info('%s failed downloading, deleting...', item['name']) try: @@ -153,6 +144,9 @@ class NZBVortex(Downloader): return self.api_level + def isEnabled(self, manual, data): + return super(NZBVortex, self).isEnabled(manual, data) and self.getApiLevel() + class HTTPSConnection(httplib.HTTPSConnection): def __init__(self, *args, **kwargs): diff --git a/couchpotato/core/downloaders/pneumatic/main.py b/couchpotato/core/downloaders/pneumatic/main.py index 57e47e60..5e2b7854 100644 --- a/couchpotato/core/downloaders/pneumatic/main.py +++ b/couchpotato/core/downloaders/pneumatic/main.py @@ -11,9 +11,7 @@ class Pneumatic(Downloader): type = ['nzb'] strm_syntax = 'plugin://plugin.program.pneumatic/?mode=strm&type=add_file&nzb=%s&nzbname=%s' - def download(self, data = {}, movie = {}, manual = False, filedata = None): - if self.isDisabled(manual) or (not self.isCorrectType(data.get('type'))): - return + def download(self, data = {}, movie = {}, filedata = None): directory = self.conf('directory') if not directory or not os.path.isdir(directory): diff --git a/couchpotato/core/downloaders/sabnzbd/main.py b/couchpotato/core/downloaders/sabnzbd/main.py index e3848e40..91302780 100644 --- a/couchpotato/core/downloaders/sabnzbd/main.py +++ b/couchpotato/core/downloaders/sabnzbd/main.py @@ -12,10 +12,7 @@ class Sabnzbd(Downloader): type = ['nzb'] - def download(self, data = {}, movie = {}, manual = False, filedata = None): - - if self.isDisabled(manual) or not self.isCorrectType(data.get('type')): - return + def download(self, data = {}, movie = {}, filedata = None): log.info('Sending "%s" to SABnzbd.', data.get('name')) @@ -65,8 +62,6 @@ class Sabnzbd(Downloader): return False def getAllDownloadStatus(self): - if self.isDisabled(manual = True): - return False log.debug('Checking SABnzbd download status.') @@ -122,9 +117,6 @@ class Sabnzbd(Downloader): def removeFailed(self, item): - if not self.conf('delete_failed', default = True): - return False - log.info('%s failed downloading, deleting...', item['name']) try: diff --git a/couchpotato/core/downloaders/synology/main.py b/couchpotato/core/downloaders/synology/main.py index 02d91d56..6e405980 100644 --- a/couchpotato/core/downloaders/synology/main.py +++ b/couchpotato/core/downloaders/synology/main.py @@ -14,10 +14,7 @@ class Synology(Downloader): type = ['torrent_magnet'] log = CPLog(__name__) - def download(self, data, movie, manual = False, filedata = None): - - if self.isDisabled(manual) or not self.isCorrectType(data.get('type')): - return + def download(self, data, movie, filedata = None): log.error('Sending "%s" (%s) to Synology.', (data.get('name'), data.get('type'))) diff --git a/couchpotato/core/downloaders/transmission/main.py b/couchpotato/core/downloaders/transmission/main.py index 22e01c2f..63c9de9d 100644 --- a/couchpotato/core/downloaders/transmission/main.py +++ b/couchpotato/core/downloaders/transmission/main.py @@ -16,10 +16,7 @@ class Transmission(Downloader): type = ['torrent', 'torrent_magnet'] log = CPLog(__name__) - def download(self, data, movie, manual = False, filedata = None): - - if self.isDisabled(manual) or not self.isCorrectType(data.get('type')): - return + def download(self, data, movie, filedata = None): log.debug('Sending "%s" (%s) to Transmission.', (data.get('name'), data.get('type'))) diff --git a/couchpotato/core/downloaders/utorrent/main.py b/couchpotato/core/downloaders/utorrent/main.py index 983afbf9..e1e67de1 100644 --- a/couchpotato/core/downloaders/utorrent/main.py +++ b/couchpotato/core/downloaders/utorrent/main.py @@ -1,6 +1,6 @@ from bencode import bencode, bdecode from couchpotato.core.downloaders.base import Downloader -from couchpotato.core.helpers.encoding import isInt +from couchpotato.core.helpers.encoding import isInt, ss from couchpotato.core.logger import CPLog from hashlib import sha1 from multipartpost import MultipartPostHandler @@ -20,10 +20,7 @@ class uTorrent(Downloader): type = ['torrent', 'torrent_magnet'] utorrent_api = None - def download(self, data, movie, manual = False, filedata = None): - - if self.isDisabled(manual) or not self.isCorrectType(data.get('type')): - return + def download(self, data, movie, filedata = None): log.debug('Sending "%s" (%s) to uTorrent.', (data.get('name'), data.get('type'))) @@ -125,7 +122,7 @@ class uTorrentAPI(object): def add_torrent_file(self, filename, filedata): action = "action=add-file" - return self._request(action, {"torrent_file": (filename, filedata)}) + return self._request(action, {"torrent_file": (ss(filename), filedata)}) def set_torrent(self, hash, params): action = "action=setprops&hash=%s" % hash diff --git a/couchpotato/core/event.py b/couchpotato/core/event.py index 8365ae57..aa05ce0f 100644 --- a/couchpotato/core/event.py +++ b/couchpotato/core/event.py @@ -115,7 +115,8 @@ def fireEvent(name, *args, **kwargs): elif isinstance(results[0], list): merged = [] for result in results: - merged += result + if result not in merged: + merged += result results = merged diff --git a/couchpotato/core/notifications/xbmc/main.py b/couchpotato/core/notifications/xbmc/main.py index eef94695..a1987bfa 100755 --- a/couchpotato/core/notifications/xbmc/main.py +++ b/couchpotato/core/notifications/xbmc/main.py @@ -13,6 +13,7 @@ class XBMC(Notification): listen_to = ['renamer.after'] use_json_notifications = {} + couch_logo_url = 'https://raw.github.com/RuudBurger/CouchPotatoServer/master/couchpotato/static/images/xbmc-notify.png' def notify(self, message = '', data = {}, listener = None): if self.isDisabled(): return @@ -27,7 +28,7 @@ class XBMC(Notification): if self.use_json_notifications.get(host): response = self.request(host, [ - ('GUI.ShowNotification', {'title':self.default_title, 'message':message}), + ('GUI.ShowNotification', {'title': self.default_title, 'message': message, 'image': self.couch_logo_url}), ('VideoLibrary.Scan', {}), ]) else: @@ -89,7 +90,7 @@ class XBMC(Notification): self.use_json_notifications[host] = True # send the text message - resp = self.request(host, [('GUI.ShowNotification', {'title':self.default_title, 'message':message})]) + resp = self.request(host, [('GUI.ShowNotification', {'title':self.default_title, 'message':message, 'image':self.couch_logo_url})]) for result in resp: if (result.get('result') and result['result'] == 'OK'): log.debug('Message delivered successfully!') @@ -111,8 +112,8 @@ class XBMC(Notification): server = 'http://%s/xbmcCmds/' % host - # title, message [, timeout , image #can be added!] - cmd = "xbmcHttp?command=ExecBuiltIn(Notification('%s','%s'))" % (urllib.quote(data['title']), urllib.quote(data['message'])) + # Notification(title, message [, timeout , image]) + cmd = "xbmcHttp?command=ExecBuiltIn(Notification(%s,%s,'',%s))" % (urllib.quote(data['title']), urllib.quote(data['message']), urllib.quote(self.couch_logo_url)) server += cmd # I have no idea what to set to, just tried text/plain and seems to be working :) diff --git a/couchpotato/core/plugins/manage/main.py b/couchpotato/core/plugins/manage/main.py index 40336e5f..ead5b6ea 100644 --- a/couchpotato/core/plugins/manage/main.py +++ b/couchpotato/core/plugins/manage/main.py @@ -135,7 +135,6 @@ class Manage(Plugin): already_used = used_files.get(release_file['path']) if already_used: - print already_used, release['id'] if already_used < release['id']: fireEvent('release.delete', release['id'], single = True) # delete this one else: diff --git a/couchpotato/core/plugins/searcher/main.py b/couchpotato/core/plugins/searcher/main.py index 805affcf..eb953cd1 100644 --- a/couchpotato/core/plugins/searcher/main.py +++ b/couchpotato/core/plugins/searcher/main.py @@ -12,6 +12,7 @@ from couchpotato.environment import Env from inspect import ismethod, isfunction from sqlalchemy.exc import InterfaceError import datetime +import random import re import time import traceback @@ -83,37 +84,51 @@ class Searcher(Plugin): movies = db.query(Movie).filter( Movie.status.has(identifier = 'active') ).all() + random.shuffle(movies) self.in_progress = { 'total': len(movies), 'to_go': len(movies), } - for movie in movies: - movie_dict = movie.to_dict({ - 'profile': {'types': {'quality': {}}}, - 'releases': {'status': {}, 'quality': {}}, - 'library': {'titles': {}, 'files':{}}, - 'files': {} - }) + try: + search_types = self.getSearchTypes() - try: - self.single(movie_dict) - except IndexError: - log.error('Forcing library update for %s, if you see this often, please report: %s', (movie_dict['library']['identifier'], traceback.format_exc())) - fireEvent('library.update', movie_dict['library']['identifier'], force = True) - except: - log.error('Search failed for %s: %s', (movie_dict['library']['identifier'], traceback.format_exc())) + for movie in movies: + movie_dict = movie.to_dict({ + 'profile': {'types': {'quality': {}}}, + 'releases': {'status': {}, 'quality': {}}, + 'library': {'titles': {}, 'files':{}}, + 'files': {} + }) - self.in_progress['to_go'] -= 1 + try: + self.single(movie_dict, search_types) + except IndexError: + log.error('Forcing library update for %s, if you see this often, please report: %s', (movie_dict['library']['identifier'], traceback.format_exc())) + fireEvent('library.update', movie_dict['library']['identifier'], force = True) + except: + log.error('Search failed for %s: %s', (movie_dict['library']['identifier'], traceback.format_exc())) - # Break if CP wants to shut down - if self.shuttingDown(): - break + self.in_progress['to_go'] -= 1 + + # Break if CP wants to shut down + if self.shuttingDown(): + break + + except SearchSetupError: + pass self.in_progress = False - def single(self, movie): + def single(self, movie, search_types = None): + + # Find out search type + try: + if not search_types: + search_types = self.getSearchTypes() + except SearchSetupError: + return done_status = fireEvent('status.get', 'done', single = True) @@ -128,6 +143,8 @@ class Searcher(Plugin): available_status = fireEvent('status.get', 'available', single = True) ignored_status = fireEvent('status.get', 'ignored', single = True) + found_releases = [] + default_title = getTitle(movie['library']) if not default_title: log.error('No proper info found for movie, removing it from library to cause it from having more issues.') @@ -136,6 +153,7 @@ class Searcher(Plugin): fireEvent('notify.frontend', type = 'searcher.started.%s' % movie['id'], data = True, message = 'Searching for "%s"' % default_title) + ret = False for quality_type in movie['profile']['types']: if not self.couldBeReleased(quality_type['quality']['identifier'], release_dates, pre_releases): @@ -155,7 +173,11 @@ class Searcher(Plugin): log.info('Search for %s in %s', (default_title, quality_type['quality']['label'])) quality = fireEvent('quality.single', identifier = quality_type['quality']['identifier'], single = True) - results = fireEvent('yarr.search', movie, quality, merge = True) + results = [] + for search_type in search_types: + type_results = fireEvent('%s.search' % search_type, movie, quality, merge = True) + if type_results: + results += type_results sorted_results = sorted(results, key = lambda k: k['score'], reverse = True) if len(sorted_results) == 0: @@ -172,10 +194,13 @@ class Searcher(Plugin): # Add them to this movie releases list for nzb in sorted_results: - rls = db.query(Release).filter_by(identifier = md5(nzb['url'])).first() + nzb_identifier = md5(nzb['url']) + found_releases.append(nzb_identifier) + + rls = db.query(Release).filter_by(identifier = nzb_identifier).first() if not rls: rls = Release( - identifier = md5(nzb['url']), + identifier = nzb_identifier, movie_id = movie.get('id'), quality_id = quality_type.get('quality_id'), status_id = available_status.get('id') @@ -223,6 +248,12 @@ class Searcher(Plugin): break elif downloaded != 'try_next': break + + # Remove releases that aren't found anymore + for release in movie.get('releases', []): + if release.get('identifier') not in found_releases: + fireEvent('release.delete', release.get('id'), single = True) + else: log.info('Better quality (%s) already available or snatched for %s', (quality_type['quality']['label'], default_title)) fireEvent('movie.restatus', movie['id']) @@ -238,61 +269,87 @@ class Searcher(Plugin): def download(self, data, movie, manual = False): - snatched_status = fireEvent('status.get', 'snatched', single = True) + # Test to see if any downloaders are enabled for this type + downloader_enabled = fireEvent('download.enabled', manual, data, single = True) - # Download movie to temp - filedata = None - if data.get('download') and (ismethod(data.get('download')) or isfunction(data.get('download'))): - filedata = data.get('download')(url = data.get('url'), nzb_id = data.get('id')) - if filedata == 'try_next': - return filedata + if downloader_enabled: - successful = fireEvent('download', data = data, movie = movie, manual = manual, filedata = filedata, single = True) + snatched_status = fireEvent('status.get', 'snatched', single = True) - if successful: + # Download movie to temp + filedata = None + if data.get('download') and (ismethod(data.get('download')) or isfunction(data.get('download'))): + filedata = data.get('download')(url = data.get('url'), nzb_id = data.get('id')) + if filedata == 'try_next': + return filedata - try: - # Mark release as snatched - db = get_session() - rls = db.query(Release).filter_by(identifier = md5(data['url'])).first() - if rls: - rls.status_id = snatched_status.get('id') - db.commit() + successful = fireEvent('download', data = data, movie = movie, manual = manual, filedata = filedata, single = True) - log_movie = '%s (%s) in %s' % (getTitle(movie['library']), movie['library']['year'], rls.quality.label) - snatch_message = 'Snatched "%s": %s' % (data.get('name'), log_movie) - log.info(snatch_message) - fireEvent('movie.snatched', message = snatch_message, data = rls.to_dict()) + if successful: - # If renamer isn't used, mark movie done - if not Env.setting('enabled', 'renamer'): - active_status = fireEvent('status.get', 'active', single = True) - done_status = fireEvent('status.get', 'done', single = True) - try: - if movie['status_id'] == active_status.get('id'): - for profile_type in movie['profile']['types']: - if rls and profile_type['quality_id'] == rls.quality.id and profile_type['finish']: - log.info('Renamer disabled, marking movie as finished: %s', log_movie) + try: + # Mark release as snatched + db = get_session() + rls = db.query(Release).filter_by(identifier = md5(data['url'])).first() + if rls: + rls.status_id = snatched_status.get('id') + db.commit() - # Mark release done - rls.status_id = done_status.get('id') - db.commit() + log_movie = '%s (%s) in %s' % (getTitle(movie['library']), movie['library']['year'], rls.quality.label) + snatch_message = 'Snatched "%s": %s' % (data.get('name'), log_movie) + log.info(snatch_message) + fireEvent('movie.snatched', message = snatch_message, data = rls.to_dict()) - # Mark movie done - mvie = db.query(Movie).filter_by(id = movie['id']).first() - mvie.status_id = done_status.get('id') - db.commit() - except: - log.error('Failed marking movie finished, renamer disabled: %s', traceback.format_exc()) + # If renamer isn't used, mark movie done + if not Env.setting('enabled', 'renamer'): + active_status = fireEvent('status.get', 'active', single = True) + done_status = fireEvent('status.get', 'done', single = True) + try: + if movie['status_id'] == active_status.get('id'): + for profile_type in movie['profile']['types']: + if rls and profile_type['quality_id'] == rls.quality.id and profile_type['finish']: + log.info('Renamer disabled, marking movie as finished: %s', log_movie) - except: - log.error('Failed marking movie finished: %s', traceback.format_exc()) + # Mark release done + rls.status_id = done_status.get('id') + db.commit() - return True + # Mark movie done + mvie = db.query(Movie).filter_by(id = movie['id']).first() + mvie.status_id = done_status.get('id') + db.commit() + except: + log.error('Failed marking movie finished, renamer disabled: %s', traceback.format_exc()) + + except: + log.error('Failed marking movie finished: %s', traceback.format_exc()) + + return True + + log.info('Tried to download, but none of the "%s" downloaders are enabled', (data.get('type', ''))) - log.info('Tried to download, but none of the downloaders are enabled') return False + def getSearchTypes(self): + + download_types = fireEvent('download.enabled_types', merge = True) + provider_types = fireEvent('provider.enabled_types', merge = True) + + if download_types and len(list(set(provider_types) & set(download_types))) == 0: + log.error('There aren\'t any providers enabled for your downloader (%s). Check your settings.', ','.join(download_types)) + raise NoProviders + + for useless_provider in list(set(provider_types) - set(download_types)): + log.debug('Provider for "%s" enabled, but no downloader.', useless_provider) + + search_types = download_types + + if len(search_types) == 0: + log.error('There aren\'t any downloaders enabled. Please pick one in settings.') + raise NoDownloaders + + return search_types + def correctMovie(self, nzb = {}, movie = {}, quality = {}, **kwargs): imdb_results = kwargs.get('imdb_results', False) @@ -536,3 +593,12 @@ class Searcher(Plugin): except: log.error('Failed searching for next release: %s', traceback.format_exc()) return False + +class SearchSetupError(Exception): + pass + +class NoDownloaders(SearchSetupError): + pass + +class NoProviders(SearchSetupError): + pass diff --git a/couchpotato/core/plugins/subtitle/main.py b/couchpotato/core/plugins/subtitle/main.py index cde1b929..73ead087 100644 --- a/couchpotato/core/plugins/subtitle/main.py +++ b/couchpotato/core/plugins/subtitle/main.py @@ -49,6 +49,7 @@ class Subtitle(Plugin): available_languages = sum(group['subtitle_language'].itervalues(), []) downloaded = [] files = [toUnicode(x) for x in group['files']['movie']] + log.debug('Searching for subtitles for: %s', files) for lang in self.getLanguages(): if lang not in available_languages: @@ -57,6 +58,7 @@ class Subtitle(Plugin): downloaded.extend(download[subtitle]) for d_sub in downloaded: + log.info('Found subtitle (%s): %s', (d_sub.language.alpha2, files)) group['files']['subtitle'].add(d_sub.path) group['subtitle_language'][d_sub.path] = [d_sub.language.alpha2] diff --git a/couchpotato/core/providers/automation/rottentomatoes/__init__.py b/couchpotato/core/providers/automation/rottentomatoes/__init__.py new file mode 100644 index 00000000..f3c12919 --- /dev/null +++ b/couchpotato/core/providers/automation/rottentomatoes/__init__.py @@ -0,0 +1,28 @@ +from .main import Rottentomatoes + +def start(): + return Rottentomatoes() + +config = [{ + 'name': 'rottentomatoes', + 'groups': [ + { + 'tab': 'automation', + 'name': 'rottentomatoes_automation', + 'label': 'Rottentomatoes', + 'description': 'Imports movies from the rottentomatoes "in theaters"-feed.', + 'options': [ + { + 'name': 'automation_enabled', + 'default': False, + 'type': 'enabler', + }, + { + 'name': 'tomatometer_percent', + 'default': '80', + 'label': 'Tomatometer' + } + ], + }, + ], +}] diff --git a/couchpotato/core/providers/automation/rottentomatoes/main.py b/couchpotato/core/providers/automation/rottentomatoes/main.py new file mode 100644 index 00000000..053b79e8 --- /dev/null +++ b/couchpotato/core/providers/automation/rottentomatoes/main.py @@ -0,0 +1,48 @@ +from couchpotato.core.helpers.rss import RSS +from couchpotato.core.helpers.variable import tryInt +from couchpotato.core.logger import CPLog +from couchpotato.core.providers.automation.base import Automation +from xml.etree.ElementTree import QName +import datetime +import re + +log = CPLog(__name__) + +class Rottentomatoes(Automation, RSS): + + interval = 1800 + urls = { + 'namespace': 'http://www.rottentomatoes.com/xmlns/rtmovie/', + 'theater': 'http://www.rottentomatoes.com/syndication/rss/in_theaters.xml', + } + + def getIMDBids(self): + + movies = [] + + rss_movies = self.getRSSData(self.urls['theater']) + rating_tag = str(QName(self.urls['namespace'], 'tomatometer_percent')) + + for movie in rss_movies: + + value = self.getTextElement(movie, "title") + result = re.search('(?<=%\s).*', value) + + if result: + + log.info2('Something smells...') + rating = tryInt(self.getTextElement(movie, rating_tag)) + name = result.group(0) + + if rating < tryInt(self.conf('tomatometer_percent')): + log.info2('%s seems to be rotten...' % name) + else: + + log.info2('Found %s fresh enough movies, enqueuing: %s' % (rating, name)) + year = datetime.datetime.now().strftime("%Y") + imdb = self.search(name, year) + + if imdb: + movies.append(imdb['imdb']) + + return movies diff --git a/couchpotato/core/providers/base.py b/couchpotato/core/providers/base.py index 9c143d81..a07282af 100644 --- a/couchpotato/core/providers/base.py +++ b/couchpotato/core/providers/base.py @@ -84,8 +84,16 @@ class YarrProvider(Provider): login_opener = None def __init__(self): + addEvent('provider.enabled_types', self.getEnabledProviderType) addEvent('provider.belongs_to', self.belongsTo) addEvent('yarr.search', self.search) + addEvent('%s.search' % self.type, self.search) + + def getEnabledProviderType(self): + if self.isEnabled(): + return self.type + else: + return [] def login(self): diff --git a/couchpotato/core/providers/nzb/ftdworld/main.py b/couchpotato/core/providers/nzb/ftdworld/main.py index c5a06652..bd2a3ee2 100644 --- a/couchpotato/core/providers/nzb/ftdworld/main.py +++ b/couchpotato/core/providers/nzb/ftdworld/main.py @@ -4,6 +4,7 @@ from couchpotato.core.logger import CPLog from couchpotato.core.providers.nzb.base import NZBProvider from couchpotato.environment import Env from dateutil.parser import parse +import json import traceback log = CPLog(__name__) @@ -15,7 +16,7 @@ class FTDWorld(NZBProvider): 'search': 'http://ftdworld.net/api/index.php?%s', 'detail': 'http://ftdworld.net/spotinfo.php?id=%s', 'download': 'http://ftdworld.net/cgi-bin/nzbdown.pl?fileID=%s', - 'login': 'http://ftdworld.net/index.php', + 'login': 'http://ftdworld.net/api/login.php', } http_time_between_calls = 3 #seconds @@ -56,6 +57,7 @@ class FTDWorld(NZBProvider): 'id': nzb_id, 'name': toUnicode(item.get('Title')), 'age': self.calculateAge(tryInt(item.get('Created'))), + 'size': item.get('Size', 0), 'url': self.urls['download'] % nzb_id, 'download': self.loginDownload, 'detail_url': self.urls['detail'] % nzb_id, @@ -73,4 +75,7 @@ class FTDWorld(NZBProvider): }) def loginSuccess(self, output): - return 'password is incorrect' not in output + try: + return json.loads(output).get('goodToGo', False) + except: + return False diff --git a/couchpotato/core/providers/nzb/newznab/main.py b/couchpotato/core/providers/nzb/newznab/main.py index b1e93bcd..f80e07cc 100644 --- a/couchpotato/core/providers/nzb/newznab/main.py +++ b/couchpotato/core/providers/nzb/newznab/main.py @@ -104,12 +104,23 @@ class Newznab(NZBProvider, RSS): return result def getUrl(self, host, type): + if '?page=newznabapi' in host: + return cleanHost(host)[:-1] + '&t=' + type + return cleanHost(host) + 'api?t=' + type - def isDisabled(self, host): + def isDisabled(self, host = None): return not self.isEnabled(host) - def isEnabled(self, host): + def isEnabled(self, host = None): + + # Return true if at least one is enabled and no host is given + if host is None: + for host in self.getHosts(): + if self.isEnabled(host): + return True + return False + return NZBProvider.isEnabled(self) and host['host'] and host['api_key'] and int(host['use']) def getApiExt(self, host): diff --git a/couchpotato/core/providers/nzb/omgwtfnzbs/__init__.py b/couchpotato/core/providers/nzb/omgwtfnzbs/__init__.py index 8e8af499..089b72c6 100644 --- a/couchpotato/core/providers/nzb/omgwtfnzbs/__init__.py +++ b/couchpotato/core/providers/nzb/omgwtfnzbs/__init__.py @@ -10,7 +10,7 @@ config = [{ 'tab': 'searcher', 'subtab': 'nzb_providers', 'name': 'OMGWTFNZBs', - 'description': 'See OMGWTFNZBs', + 'description': 'See OMGWTFNZBs', 'options': [ { 'name': 'enabled', diff --git a/couchpotato/core/providers/torrent/torrentday/main.py b/couchpotato/core/providers/torrent/torrentday/main.py index 8b29f320..5e207c1e 100644 --- a/couchpotato/core/providers/torrent/torrentday/main.py +++ b/couchpotato/core/providers/torrent/torrentday/main.py @@ -59,3 +59,6 @@ class TorrentDay(TorrentProvider): 'password': self.conf('password'), 'submit': 'submit', }) + + def loginSuccess(self, output): + return 'Password not correct' not in output diff --git a/couchpotato/runner.py b/couchpotato/runner.py index c0b7eb86..d3a49f02 100644 --- a/couchpotato/runner.py +++ b/couchpotato/runner.py @@ -4,6 +4,7 @@ from couchpotato.api import api, NonBlockHandler from couchpotato.core.event import fireEventAsync, fireEvent from couchpotato.core.helpers.variable import getDataDir, tryInt from logging import handlers +from tornado.httpserver import HTTPServer from tornado.web import Application, FallbackHandler from tornado.wsgi import WSGIContainer from werkzeug.contrib.cache import FileSystemCache @@ -210,8 +211,9 @@ def runCouchPotato(options, base_path, args, data_dir = None, log_dir = None, En # app.debug = development config = { 'use_reloader': reloader, - 'host': Env.setting('host', default = '0.0.0.0'), - 'port': tryInt(Env.setting('port', default = 5000)) + 'port': tryInt(Env.setting('port', default = 5000)), + 'ssl_cert': Env.setting('ssl_cert', default = None), + 'ssl_key': Env.setting('ssl_key', default = None), } # Static path @@ -243,12 +245,20 @@ def runCouchPotato(options, base_path, args, data_dir = None, log_dir = None, En debug = config['use_reloader'] ) + if config['ssl_cert'] and config['ssl_key']: + server = HTTPServer(application, no_keep_alive = True, ssl_options = { + "certfile": config['ssl_cert'], + "keyfile": config['ssl_key'], + }) + else: + server = HTTPServer(application, no_keep_alive = True) + try_restart = True restart_tries = 5 while try_restart: try: - application.listen(config['port'], config['host'], no_keep_alive = True) + server.listen(config['port']) loop.start() except Exception, e: try: diff --git a/couchpotato/static/images/xbmc-notify.png b/couchpotato/static/images/xbmc-notify.png new file mode 100644 index 00000000..6b7959f4 Binary files /dev/null and b/couchpotato/static/images/xbmc-notify.png differ diff --git a/couchpotato/static/scripts/page/settings.js b/couchpotato/static/scripts/page/settings.js index 44ae0f6b..11f1aa44 100644 --- a/couchpotato/static/scripts/page/settings.js +++ b/couchpotato/static/scripts/page/settings.js @@ -1311,7 +1311,7 @@ Option.Combined = new Class({ if(has_empty > 0) return; self.add_empty_timeout = setTimeout(function(){ - self.createItem(false, null); + self.createItem({'use': true}); }, 10); },