diff --git a/couchpotato/api.py b/couchpotato/api.py index 15ef2b4c..718527c9 100644 --- a/couchpotato/api.py +++ b/couchpotato/api.py @@ -1,6 +1,5 @@ from flask.blueprints import Blueprint from flask.helpers import url_for -from tornado.ioloop import IOLoop from tornado.web import RequestHandler, asynchronous from werkzeug.utils import redirect @@ -11,7 +10,11 @@ api_nonblock = {} class NonBlockHandler(RequestHandler): - stoppers = [] + + def __init__(self, application, request, **kwargs): + cls = NonBlockHandler + cls.stoppers = [] + super(NonBlockHandler, self).__init__(application, request, **kwargs) @asynchronous def get(self, route): diff --git a/couchpotato/core/downloaders/base.py b/couchpotato/core/downloaders/base.py index 98ab73bb..7a695e21 100644 --- a/couchpotato/core/downloaders/base.py +++ b/couchpotato/core/downloaders/base.py @@ -1,16 +1,17 @@ from base64 import b32decode, b16encode from couchpotato.core.event import addEvent from couchpotato.core.logger import CPLog -from couchpotato.core.plugins.base import Plugin +from couchpotato.core.providers.base import Provider import random import re log = CPLog(__name__) -class Downloader(Plugin): +class Downloader(Provider): type = [] + http_time_between_calls = 0 torrent_sources = [ 'http://torrage.com/torrent/%s.torrent', diff --git a/couchpotato/core/downloaders/nzbget/__init__.py b/couchpotato/core/downloaders/nzbget/__init__.py index 8b68c95f..10994299 100644 --- a/couchpotato/core/downloaders/nzbget/__init__.py +++ b/couchpotato/core/downloaders/nzbget/__init__.py @@ -25,6 +25,7 @@ config = [{ }, { 'name': 'password', + 'type': 'password', 'description': 'Default NZBGet password is tegbzn6789', }, { diff --git a/couchpotato/core/downloaders/nzbvortex/__init__.py b/couchpotato/core/downloaders/nzbvortex/__init__.py new file mode 100644 index 00000000..d79983f4 --- /dev/null +++ b/couchpotato/core/downloaders/nzbvortex/__init__.py @@ -0,0 +1,40 @@ +from .main import NZBVortex + +def start(): + return NZBVortex() + +config = [{ + 'name': 'nzbvortex', + 'groups': [ + { + 'tab': 'downloaders', + 'name': 'nzbvortex', + 'label': 'NZBVortex', + 'description': 'Send NZBs to your NZBVortex app.', + 'wizard': True, + 'options': [ + { + 'name': 'enabled', + 'default': 0, + 'type': 'enabler', + 'radio_group': 'nzb', + }, + { + 'name': 'host', + 'default': 'https://localhost:4321', + }, + { + 'name': 'api_key', + 'label': 'Api Key', + }, + { + 'name': 'manual', + 'default': False, + 'type': 'bool', + 'advanced': True, + 'description': 'Disable this downloader for automated searches, but use it when I manually send a release.', + }, + ], + } + ], +}] diff --git a/couchpotato/core/downloaders/nzbvortex/main.py b/couchpotato/core/downloaders/nzbvortex/main.py new file mode 100644 index 00000000..d9078f8d --- /dev/null +++ b/couchpotato/core/downloaders/nzbvortex/main.py @@ -0,0 +1,161 @@ +from base64 import b64encode +from couchpotato.core.downloaders.base import Downloader +from couchpotato.core.helpers.encoding import tryUrlencode, ss +from couchpotato.core.helpers.variable import cleanHost +from couchpotato.core.logger import CPLog +from urllib2 import URLError +from uuid import uuid4 +import hashlib +import httplib +import json +import socket +import ssl +import sys +import traceback +import urllib2 + +log = CPLog(__name__) + +class NZBVortex(Downloader): + + type = ['nzb'] + 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 + + # Send the nzb + try: + nzb_filename = self.createFileName(data, filedata, movie) + self.call('nzb/add', params = {'file': (ss(nzb_filename), filedata)}, multipart = True) + + return True + except: + log.error('Something went wrong sending the NZB file: %s', traceback.format_exc()) + return False + + def getAllDownloadStatus(self): + + if self.isDisabled(manual = False): + return False + + raw_statuses = self.call('nzb') + + statuses = [] + for item in raw_statuses.get('nzbs', []): + + # Check status + status = 'busy' + if item['state'] == 20: + status = 'completed' + elif item['state'] in [21, 22, 24]: + status = 'failed' + + statuses.append({ + 'id': item['id'], + 'name': item['uiTitle'], + 'status': status, + 'original_status': item['state'], + 'timeleft':-1, + }) + + return statuses + + def login(self): + + nonce = self.call('auth/nonce', auth = False).get('authNonce') + cnonce = uuid4().hex + hashed = b64encode(hashlib.sha256('%s:%s:%s' % (nonce, cnonce, self.conf('api_key'))).digest()) + + params = { + 'nonce': nonce, + 'cnonce': cnonce, + 'hash': hashed + } + + login_data = self.call('auth/login', parameters = params, auth = False) + + # Save for later + if login_data.get('loginResult') == 'successful': + self.session_id = login_data.get('sessionID') + return True + + log.error('Login failed, please check you api-key') + return False + + + def call(self, call, parameters = {}, repeat = False, auth = True, *args, **kwargs): + + # Login first + if not self.session_id and auth: + self.login() + + # Always add session id to request + if self.session_id: + parameters['sessionid'] = self.session_id + + params = tryUrlencode(parameters) + + url = cleanHost(self.conf('host')) + 'api/' + call + url_opener = urllib2.build_opener(HTTPSHandler()) + + try: + data = self.urlopen('%s?%s' % (url, params), opener = url_opener, *args, **kwargs) + + if data: + return json.loads(data) + except URLError, e: + if hasattr(e, 'code') and e.code == 403: + # Try login and do again + if not repeat: + self.login() + return self.call(call, parameters = parameters, repeat = True, *args, **kwargs) + + log.error('Failed to parsing %s: %s', (self.getName(), traceback.format_exc())) + except: + log.error('Failed to parsing %s: %s', (self.getName(), traceback.format_exc())) + + return {} + + def getApiLevel(self): + + if not self.api_level: + + url = cleanHost(self.conf('host')) + 'api/app/apilevel' + url_opener = urllib2.build_opener(HTTPSHandler()) + + try: + data = self.urlopen(url, opener = url_opener, show_error = False) + self.api_level = float(json.loads(data).get('apilevel')) + except URLError, e: + if hasattr(e, 'code') and e.code == 403: + log.error('This version of NZBVortex isn\'t supported. Please update to 2.8.6 or higher') + else: + log.error('NZBVortex doesn\'t seem to be running or maybe the remote option isn\'t enabled yet: %s', traceback.format_exc(1)) + + return self.api_level + + +class HTTPSConnection(httplib.HTTPSConnection): + def __init__(self, *args, **kwargs): + httplib.HTTPSConnection.__init__(self, *args, **kwargs) + + def connect(self): + sock = socket.create_connection((self.host, self.port), self.timeout) + if sys.version_info < (2, 6, 7): + if hasattr(self, '_tunnel_host'): + self.sock = sock + self._tunnel() + else: + if self._tunnel_host: + self.sock = sock + self._tunnel() + + self.sock = ssl.wrap_socket(sock, self.key_file, self.cert_file, ssl_version = ssl.PROTOCOL_TLSv1) + +class HTTPSHandler(urllib2.HTTPSHandler): + def https_open(self, req): + return self.do_open(HTTPSConnection, req) diff --git a/couchpotato/core/downloaders/sabnzbd/main.py b/couchpotato/core/downloaders/sabnzbd/main.py index e9b4c0ae..567ea455 100644 --- a/couchpotato/core/downloaders/sabnzbd/main.py +++ b/couchpotato/core/downloaders/sabnzbd/main.py @@ -1,5 +1,5 @@ from couchpotato.core.downloaders.base import Downloader -from couchpotato.core.helpers.encoding import tryUrlencode +from couchpotato.core.helpers.encoding import tryUrlencode, ss from couchpotato.core.helpers.variable import cleanHost, mergeDicts from couchpotato.core.logger import CPLog from urllib2 import URLError @@ -41,7 +41,7 @@ class Sabnzbd(Downloader): try: if params.get('mode') is 'addfile': - sab = self.urlopen(url, timeout = 60, params = {'nzbfile': (nzb_filename, filedata)}, multipart = True, show_error = False) + sab = self.urlopen(url, timeout = 60, params = {'nzbfile': (ss(nzb_filename), filedata)}, multipart = True, show_error = False) else: sab = self.urlopen(url, timeout = 60, show_error = False) except URLError: diff --git a/couchpotato/core/plugins/automation/main.py b/couchpotato/core/plugins/automation/main.py index f4ede40d..c216688c 100644 --- a/couchpotato/core/plugins/automation/main.py +++ b/couchpotato/core/plugins/automation/main.py @@ -12,7 +12,7 @@ class Automation(Plugin): fireEvent('schedule.interval', 'automation.add_movies', self.addMovies, hours = self.conf('hour', default = 12)) - if not Env.get('dev'): + if Env.get('dev'): addEvent('app.load', self.addMovies) def addMovies(self): diff --git a/couchpotato/core/plugins/base.py b/couchpotato/core/plugins/base.py index 2392f251..a3d5628a 100644 --- a/couchpotato/core/plugins/base.py +++ b/couchpotato/core/plugins/base.py @@ -98,6 +98,7 @@ class Plugin(object): # http request def urlopen(self, url, timeout = 30, params = None, headers = None, opener = None, multipart = False, show_error = True): + url = ss(url) if not headers: headers = {} if not params: params = {} @@ -129,8 +130,11 @@ class Plugin(object): log.info('Opening multipart url: %s, params: %s', (url, [x for x in params.iterkeys()] if isinstance(params, dict) else 'with data')) request = urllib2.Request(url, params, headers) - cookies = cookielib.CookieJar() - opener = urllib2.build_opener(urllib2.HTTPCookieProcessor(cookies), MultipartPostHandler) + if opener: + opener.add_handler(MultipartPostHandler()) + else: + cookies = cookielib.CookieJar() + opener = urllib2.build_opener(urllib2.HTTPCookieProcessor(cookies), MultipartPostHandler) response = opener.open(request, timeout = timeout) else: diff --git a/couchpotato/core/plugins/scanner/main.py b/couchpotato/core/plugins/scanner/main.py index 2a1d3078..971fece5 100644 --- a/couchpotato/core/plugins/scanner/main.py +++ b/couchpotato/core/plugins/scanner/main.py @@ -89,7 +89,7 @@ class Scanner(Plugin): '()([ab])(\.....?)$' #*a.mkv ] - cp_imdb = '(\.cp\((?Ptt[0-9{7}]+)\))' + cp_imdb = '(.cp.(?Ptt[0-9{7}]+).)' def __init__(self): diff --git a/couchpotato/core/providers/automation/base.py b/couchpotato/core/providers/automation/base.py index 9f86ad12..8d08f913 100644 --- a/couchpotato/core/providers/automation/base.py +++ b/couchpotato/core/providers/automation/base.py @@ -1,13 +1,13 @@ from couchpotato.core.event import addEvent, fireEvent from couchpotato.core.logger import CPLog -from couchpotato.core.plugins.base import Plugin +from couchpotato.core.providers.base import Provider from couchpotato.environment import Env import time log = CPLog(__name__) -class Automation(Plugin): +class Automation(Provider): enabled_option = 'automation_enabled' @@ -19,6 +19,9 @@ class Automation(Plugin): def _getMovies(self): + if self.isDisabled(): + return + if not self.canCheck(): log.debug('Just checked, skipping %s', self.getName()) return [] diff --git a/couchpotato/core/providers/automation/bluray/main.py b/couchpotato/core/providers/automation/bluray/main.py index ac28152c..235a1e5f 100644 --- a/couchpotato/core/providers/automation/bluray/main.py +++ b/couchpotato/core/providers/automation/bluray/main.py @@ -1,8 +1,7 @@ from couchpotato.core.helpers.rss import RSS -from couchpotato.core.helpers.variable import md5, tryInt +from couchpotato.core.helpers.variable import tryInt from couchpotato.core.logger import CPLog from couchpotato.core.providers.automation.base import Automation -import xml.etree.ElementTree as XMLTree log = CPLog(__name__) @@ -14,32 +13,24 @@ class Bluray(Automation, RSS): def getIMDBids(self): - if self.isDisabled(): - return - movies = [] - cache_key = 'bluray.%s' % md5(self.rss_url) - rss_data = self.getCache(cache_key, self.rss_url) - data = XMLTree.fromstring(rss_data) + rss_movies = self.getRSSData(self.rss_url) - if data is not None: - rss_movies = self.getElements(data, 'channel/item') + for movie in rss_movies: + name = self.getTextElement(movie, 'title').lower().split('blu-ray')[0].strip('(').rstrip() + year = self.getTextElement(movie, 'description').split('|')[1].strip('(').strip() - for movie in rss_movies: - name = self.getTextElement(movie, "title").lower().split("blu-ray")[0].strip("(").rstrip() - year = self.getTextElement(movie, "description").split("|")[1].strip("(").strip() + if not name.find('/') == -1: # make sure it is not a double movie release + continue - if not name.find("/") == -1: # make sure it is not a double movie release - continue + if tryInt(year) < self.getMinimal('year'): + continue - if tryInt(year) < self.getMinimal('year'): - continue + imdb = self.search(name, year) - imdb = self.search(name, year) - - if imdb: - if self.isMinimalMovie(imdb): - movies.append(imdb['imdb']) + if imdb: + if self.isMinimalMovie(imdb): + movies.append(imdb['imdb']) return movies diff --git a/couchpotato/core/providers/automation/cp/main.py b/couchpotato/core/providers/automation/cp/main.py index 24d69092..22b7942a 100644 --- a/couchpotato/core/providers/automation/cp/main.py +++ b/couchpotato/core/providers/automation/cp/main.py @@ -8,7 +8,4 @@ class CP(Automation): def getMovies(self): - if self.isDisabled(): - return - return [] diff --git a/couchpotato/core/providers/automation/imdb/main.py b/couchpotato/core/providers/automation/imdb/main.py index c2324856..428a8b2b 100644 --- a/couchpotato/core/providers/automation/imdb/main.py +++ b/couchpotato/core/providers/automation/imdb/main.py @@ -1,5 +1,5 @@ from couchpotato.core.helpers.rss import RSS -from couchpotato.core.helpers.variable import md5, getImdb, splitString, tryInt +from couchpotato.core.helpers.variable import getImdb, splitString, tryInt from couchpotato.core.logger import CPLog from couchpotato.core.providers.automation.base import Automation import traceback @@ -13,9 +13,6 @@ class IMDB(Automation, RSS): def getIMDBids(self): - if self.isDisabled(): - return - movies = [] enablers = [tryInt(x) for x in splitString(self.conf('automation_urls_use'))] @@ -29,8 +26,7 @@ class IMDB(Automation, RSS): continue try: - cache_key = 'imdb.rss.%s' % md5(url) - rss_data = self.getCache(cache_key, url) + rss_data = self.getHTMLData(url) imdbs = getImdb(rss_data, multiple = True) for imdb in imdbs: diff --git a/couchpotato/core/providers/automation/kinepolis/main.py b/couchpotato/core/providers/automation/kinepolis/main.py index f6633af7..4158d488 100644 --- a/couchpotato/core/providers/automation/kinepolis/main.py +++ b/couchpotato/core/providers/automation/kinepolis/main.py @@ -1,9 +1,7 @@ from couchpotato.core.helpers.rss import RSS -from couchpotato.core.helpers.variable import md5 from couchpotato.core.logger import CPLog from couchpotato.core.providers.automation.base import Automation import datetime -import xml.etree.ElementTree as XMLTree log = CPLog(__name__) @@ -15,25 +13,17 @@ class Kinepolis(Automation, RSS): def getIMDBids(self): - if self.isDisabled(): - return - movies = [] - cache_key = 'kinepolis.%s' % md5(self.rss_url) - rss_data = self.getCache(cache_key, self.rss_url) - data = XMLTree.fromstring(rss_data) + rss_movies = self.getRSSData(self.rss_url) - if data is not None: - rss_movies = self.getElements(data, 'channel/item') + for movie in rss_movies: + name = self.getTextElement(movie, 'title') + year = datetime.datetime.now().strftime('%Y') - for movie in rss_movies: - name = self.getTextElement(movie, "title") - year = datetime.datetime.now().strftime("%Y") + imdb = self.search(name, year) - imdb = self.search(name, year) - - if imdb and self.isMinimalMovie(imdb): - movies.append(imdb['imdb']) + if imdb and self.isMinimalMovie(imdb): + movies.append(imdb['imdb']) return movies diff --git a/couchpotato/core/providers/automation/moviemeter/__init__.py b/couchpotato/core/providers/automation/moviemeter/__init__.py new file mode 100644 index 00000000..8ea7c06d --- /dev/null +++ b/couchpotato/core/providers/automation/moviemeter/__init__.py @@ -0,0 +1,23 @@ +from .main import Moviemeter + +def start(): + return Moviemeter() + +config = [{ + 'name': 'moviemeter', + 'groups': [ + { + 'tab': 'automation', + 'name': 'moviemeter_automation', + 'label': 'Moviemeter', + 'description': 'Imports movies from the current top 10 of moviemeter.nl. (uses minimal requirements)', + 'options': [ + { + 'name': 'automation_enabled', + 'default': False, + 'type': 'enabler', + }, + ], + }, + ], +}] diff --git a/couchpotato/core/providers/automation/moviemeter/main.py b/couchpotato/core/providers/automation/moviemeter/main.py new file mode 100644 index 00000000..dae764bf --- /dev/null +++ b/couchpotato/core/providers/automation/moviemeter/main.py @@ -0,0 +1,28 @@ +from couchpotato.core.event import fireEvent +from couchpotato.core.helpers.rss import RSS +from couchpotato.core.logger import CPLog +from couchpotato.core.providers.automation.base import Automation + +log = CPLog(__name__) + + +class Moviemeter(Automation, RSS): + + interval = 1800 + rss_url = 'http://www.moviemeter.nl/rss/cinema' + + def getIMDBids(self): + + movies = [] + + rss_movies = self.getRSSData(self.rss_url) + + for movie in rss_movies: + + name_year = fireEvent('scanner.name_year', self.getTextElement(movie, 'title'), single = True) + imdb = self.search(name_year.get('name'), name_year.get('year')) + + if imdb and self.isMinimalMovie(imdb): + movies.append(imdb['imdb']) + + return movies diff --git a/couchpotato/core/providers/automation/movies_io/main.py b/couchpotato/core/providers/automation/movies_io/main.py index 5875a4d9..0737e2e6 100644 --- a/couchpotato/core/providers/automation/movies_io/main.py +++ b/couchpotato/core/providers/automation/movies_io/main.py @@ -1,11 +1,8 @@ from couchpotato.core.event import fireEvent from couchpotato.core.helpers.rss import RSS -from couchpotato.core.helpers.variable import md5 +from couchpotato.core.helpers.variable import tryInt, splitString from couchpotato.core.logger import CPLog from couchpotato.core.providers.automation.base import Automation -from xml.etree.ElementTree import ParseError -import traceback -import xml.etree.ElementTree as XMLTree log = CPLog(__name__) @@ -16,39 +13,27 @@ class MoviesIO(Automation, RSS): def getIMDBids(self): - if self.isDisabled(): - return - movies = [] - enablers = self.conf('automation_urls_use').split(',') + enablers = [tryInt(x) for x in splitString(self.conf('automation_urls_use'))] index = -1 - for rss_url in self.conf('automation_urls').split(','): + for rss_url in splitString(self.conf('automation_urls')): index += 1 if not enablers[index]: continue - try: - cache_key = 'imdb.rss.%s' % md5(rss_url) + rss_movies = self.getRSSData(rss_url, headers = {'Referer': ''}) - rss_data = self.getCache(cache_key, rss_url, headers = {'Referer': ''}) - data = XMLTree.fromstring(rss_data) - rss_movies = self.getElements(data, 'channel/item') + for movie in rss_movies: - for movie in rss_movies: + nameyear = fireEvent('scanner.name_year', self.getTextElement(movie, 'title'), single = True) + imdb = self.search(nameyear.get('name'), nameyear.get('year'), imdb_only = True) - nameyear = fireEvent('scanner.name_year', self.getTextElement(movie, "title"), single = True) - imdb = self.search(nameyear.get('name'), nameyear.get('year'), imdb_only = True) + if not imdb: + continue - if not imdb: - continue - - movies.append(imdb) - except ParseError: - log.debug('Failed loading Movies.io watchlist, probably empty: %s', (rss_url)) - except: - log.error('Failed loading Movies.io watchlist: %s %s', (rss_url, traceback.format_exc())) + movies.append(imdb) return movies diff --git a/couchpotato/core/providers/automation/trakt/main.py b/couchpotato/core/providers/automation/trakt/main.py index 764f6cfc..0109daf3 100644 --- a/couchpotato/core/providers/automation/trakt/main.py +++ b/couchpotato/core/providers/automation/trakt/main.py @@ -1,9 +1,8 @@ from couchpotato.core.event import addEvent -from couchpotato.core.helpers.variable import md5, sha1 +from couchpotato.core.helpers.variable import sha1 from couchpotato.core.logger import CPLog from couchpotato.core.providers.automation.base import Automation import base64 -import json log = CPLog(__name__) @@ -25,9 +24,6 @@ class Trakt(Automation): def getIMDBids(self): - if self.isDisabled(): - return - movies = [] for movie in self.getWatchlist(): movies.append(movie.get('imdb_id')) @@ -38,22 +34,11 @@ class Trakt(Automation): method = (self.urls['watchlist'] % self.conf('automation_api_key')) + self.conf('automation_username') return self.call(method) - def call(self, method_url): - try: - if self.conf('automation_password'): - headers = { - 'Authorization': 'Basic %s' % base64.encodestring('%s:%s' % (self.conf('automation_username'), self.conf('automation_password')))[:-1] - } - else: - headers = {} + headers = {} + if self.conf('automation_password'): + headers['Authorization'] = 'Basic %s' % base64.encodestring('%s:%s' % (self.conf('automation_username'), self.conf('automation_password')))[:-1] - cache_key = 'trakt.%s' % md5(method_url) - json_string = self.getCache(cache_key, self.urls['base'] + method_url, headers = headers) - if json_string: - return json.loads(json_string) - except: - log.error('Failed to get data from trakt, check your login.') - - return [] + data = self.getJsonData(self.urls['base'] + method_url, headers = headers) + return data if data else [] diff --git a/couchpotato/core/providers/base.py b/couchpotato/core/providers/base.py index f16cf6ea..213fa43f 100644 --- a/couchpotato/core/providers/base.py +++ b/couchpotato/core/providers/base.py @@ -44,6 +44,34 @@ class Provider(Plugin): return self.is_available.get(host, False) + def getJsonData(self, url, **kwargs): + + data = self.getCache(md5(url), url, **kwargs) + + if data: + try: + return json.loads(data) + except: + log.error('Failed to parsing %s: %s', (self.getName(), traceback.format_exc())) + + return [] + + def getRSSData(self, url, **kwargs): + + data = self.getCache(md5(url), url, **kwargs) + + if data: + try: + data = XMLTree.fromstring(data) + return self.getElements(data, 'channel/item') + except: + log.error('Failed to parsing %s: %s', (self.getName(), traceback.format_exc())) + + return [] + + def getHTMLData(self, url, **kwargs): + return self.getCache(md5(url), url, **kwargs) + class YarrProvider(Provider): @@ -106,11 +134,11 @@ class YarrProvider(Provider): return [] # Create result container - imdb_result = hasattr(self, '_search') - results = ResultList(self, movie, quality, imdb_result = imdb_result) + imdb_results = hasattr(self, '_search') + results = ResultList(self, movie, quality, imdb_results = imdb_results) # Do search based on imdb id - if imdb_result: + if imdb_results: self._search(movie, quality, results) # Search possible titles else: @@ -165,34 +193,6 @@ class YarrProvider(Provider): return [self.cat_backup_id] - def getJsonData(self, url, **kwargs): - - data = self.getCache(md5(url), url, **kwargs) - - if data: - try: - return json.loads(data) - except: - log.error('Failed to parsing %s: %s', (self.getName(), traceback.format_exc())) - - return [] - - def getRSSData(self, url, **kwargs): - - data = self.getCache(md5(url), url, **kwargs) - - if data: - try: - data = XMLTree.fromstring(data) - return self.getElements(data, 'channel/item') - except: - log.error('Failed to parsing %s: %s', (self.getName(), traceback.format_exc())) - - return [] - - def getHTMLData(self, url, **kwargs): - return self.getCache(md5(url), url, **kwargs) - class ResultList(list): diff --git a/couchpotato/core/providers/movie/imdbapi/__init__.py b/couchpotato/core/providers/movie/imdbapi/__init__.py deleted file mode 100644 index dd1202ea..00000000 --- a/couchpotato/core/providers/movie/imdbapi/__init__.py +++ /dev/null @@ -1,6 +0,0 @@ -from .main import IMDBAPI - -def start(): - return IMDBAPI() - -config = [] diff --git a/couchpotato/core/providers/movie/omdbapi/__init__.py b/couchpotato/core/providers/movie/omdbapi/__init__.py new file mode 100644 index 00000000..765662e9 --- /dev/null +++ b/couchpotato/core/providers/movie/omdbapi/__init__.py @@ -0,0 +1,6 @@ +from .main import OMDBAPI + +def start(): + return OMDBAPI() + +config = [] diff --git a/couchpotato/core/providers/movie/imdbapi/main.py b/couchpotato/core/providers/movie/omdbapi/main.py similarity index 94% rename from couchpotato/core/providers/movie/imdbapi/main.py rename to couchpotato/core/providers/movie/omdbapi/main.py index 53f503e1..cdfece0a 100644 --- a/couchpotato/core/providers/movie/imdbapi/main.py +++ b/couchpotato/core/providers/movie/omdbapi/main.py @@ -10,11 +10,11 @@ import traceback log = CPLog(__name__) -class IMDBAPI(MovieProvider): +class OMDBAPI(MovieProvider): urls = { - 'search': 'http://www.imdbapi.com/?%s', - 'info': 'http://www.imdbapi.com/?i=%s', + 'search': 'http://www.omdbapi.com/?%s', + 'info': 'http://www.omdbapi.com/?i=%s', } http_time_between_calls = 0 @@ -32,7 +32,7 @@ class IMDBAPI(MovieProvider): 'name': q } - cache_key = 'imdbapi.cache.%s' % q + cache_key = 'omdbapi.cache.%s' % q cached = self.getCache(cache_key, self.urls['search'] % tryUrlencode({'t': name_year.get('name'), 'y': name_year.get('year', '')}), timeout = 3) if cached: @@ -50,7 +50,7 @@ class IMDBAPI(MovieProvider): if not identifier: return {} - cache_key = 'imdbapi.cache.%s' % identifier + cache_key = 'omdbapi.cache.%s' % identifier cached = self.getCache(cache_key, self.urls['info'] % identifier, timeout = 3) if cached: diff --git a/couchpotato/core/providers/nzb/newznab/__init__.py b/couchpotato/core/providers/nzb/newznab/__init__.py index c9507f91..1e76d1ca 100644 --- a/couchpotato/core/providers/nzb/newznab/__init__.py +++ b/couchpotato/core/providers/nzb/newznab/__init__.py @@ -13,7 +13,7 @@ config = [{ 'order': 10, 'description': 'Enable NewzNab providers such as NZB.su, \ NZBs.org, DOGnzb.cr, \ - Spotweb', + Spotweb or NZBGeek', 'wizard': True, 'options': [ { @@ -22,16 +22,16 @@ config = [{ }, { 'name': 'use', - 'default': '0,0,0' + 'default': '0,0,0,0' }, { 'name': 'host', - 'default': 'nzb.su,dognzb.cr,nzbs.org', + 'default': 'nzb.su,dognzb.cr,nzbs.org,https://index.nzbgeek.info', 'description': 'The hostname of your newznab provider', }, { 'name': 'api_key', - 'default': ',,', + 'default': ',,,', 'label': 'Api Key', 'description': 'Can be found on your profile page', 'type': 'combined', diff --git a/couchpotato/core/providers/nzb/newznab/main.py b/couchpotato/core/providers/nzb/newznab/main.py index c18724a9..c7c27a8a 100644 --- a/couchpotato/core/providers/nzb/newznab/main.py +++ b/couchpotato/core/providers/nzb/newznab/main.py @@ -29,7 +29,7 @@ class Newznab(NZBProvider, RSS): def search(self, movie, quality): hosts = self.getHosts() - results = ResultList(self, movie, quality, imdb_result = True) + results = ResultList(self, movie, quality, imdb_results = True) for host in hosts: if self.isDisabled(host): diff --git a/couchpotato/core/providers/nzb/nzbx/__init__.py b/couchpotato/core/providers/nzb/nzbx/__init__.py index 129fba02..f1c7d588 100644 --- a/couchpotato/core/providers/nzb/nzbx/__init__.py +++ b/couchpotato/core/providers/nzb/nzbx/__init__.py @@ -10,7 +10,7 @@ config = [{ 'tab': 'searcher', 'subtab': 'nzb_providers', 'name': 'nzbX', - 'description': 'Free provider, less accurate. See nzbX', + 'description': 'Free provider. See nzbX', 'options': [ { 'name': 'enabled', diff --git a/couchpotato/core/providers/nzb/nzbx/main.py b/couchpotato/core/providers/nzb/nzbx/main.py index 97ee9cd2..ec7fbfe2 100644 --- a/couchpotato/core/providers/nzb/nzbx/main.py +++ b/couchpotato/core/providers/nzb/nzbx/main.py @@ -2,6 +2,7 @@ from couchpotato.core.helpers.encoding import tryUrlencode from couchpotato.core.helpers.variable import tryInt from couchpotato.core.logger import CPLog from couchpotato.core.providers.nzb.base import NZBProvider +from couchpotato.environment import Env log = CPLog(__name__) @@ -22,7 +23,7 @@ class Nzbx(NZBProvider): 'q': movie['library']['identifier'].replace('tt', ''), 'sf': quality.get('size_min'), }) - nzbs = self.getJsonData(self.urls['search'] % arguments) + nzbs = self.getJsonData(self.urls['search'] % arguments, headers = {'User-Agent': Env.getIdentifier()}) for nzb in nzbs: diff --git a/couchpotato/core/providers/nzb/omgwtfnzbs/main.py b/couchpotato/core/providers/nzb/omgwtfnzbs/main.py index f3de7675..0a18b8f4 100644 --- a/couchpotato/core/providers/nzb/omgwtfnzbs/main.py +++ b/couchpotato/core/providers/nzb/omgwtfnzbs/main.py @@ -14,7 +14,7 @@ log = CPLog(__name__) class OMGWTFNZBs(NZBProvider, RSS): urls = { - 'search': 'http://rss.omgwtfnzbs.com/rss-search.php?%s', + 'search': 'http://rss.omgwtfnzbs.org/rss-search.php?%s', } http_time_between_calls = 1 #seconds diff --git a/couchpotato/runner.py b/couchpotato/runner.py index f55c65b6..c0b7eb86 100644 --- a/couchpotato/runner.py +++ b/couchpotato/runner.py @@ -4,7 +4,6 @@ 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.ioloop import IOLoop from tornado.web import Application, FallbackHandler from tornado.wsgi import WSGIContainer from werkzeug.contrib.cache import FileSystemCache @@ -231,6 +230,7 @@ def runCouchPotato(options, base_path, args, data_dir = None, log_dir = None, En fireEventAsync('app.load') # Go go go! + from tornado.ioloop import IOLoop web_container = WSGIContainer(app) web_container._log = _log loop = IOLoop.instance() diff --git a/couchpotato/static/scripts/api.js b/couchpotato/static/scripts/api.js index f14eb14d..5e507bc1 100644 --- a/couchpotato/static/scripts/api.js +++ b/couchpotato/static/scripts/api.js @@ -13,7 +13,7 @@ var ApiClass = new Class({ return new Request[r_type](Object.merge({ 'callbackKey': 'callback_func', 'method': 'get', - 'url': self.createUrl(type), + 'url': self.createUrl(type, {'t': randomString()}), }, options)).send() }, diff --git a/init/freebsd b/init/freebsd old mode 100644 new mode 100755 index eeba51d3..11714406 --- a/init/freebsd +++ b/init/freebsd @@ -25,6 +25,9 @@ name="couchpotato" rcvar=${name}_enable +# Required, for some reason, to find all our binaries when starting via service. +PATH="/usr/bin:/usr/local/bin:$PATH" + load_rc_config ${name} : ${couchpotato_enable:="NO"} @@ -36,9 +39,9 @@ load_rc_config ${name} WGET="/usr/local/bin/wget" # You need wget for this script to safely shutdown CouchPotato. if [ -e "${couchpotato_conf}" ]; then - HOST=`grep -A14 "\[core\]" "${couchpotato_conf}"|egrep "^host"|perl -wple 's/^host = (.*)$/$1/'` - PORT=`grep -A14 "\[core\]" "${couchpotato_conf}"|egrep "^port"|perl -wple 's/^port = (.*)$/$1/'` - CPAPI=`grep -A14 "\[core\]" "${couchpotato_conf}"|egrep "^api_key"|perl -wple 's/^api_key = (.*)$/$1/'` + HOST=`grep -A14 "\[core\]" "${couchpotato_conf}"|awk -F" = " '/^host/ {print $2}'` + PORT=`grep -A14 "\[core\]" "${couchpotato_conf}"|awk -F" = " '/^port/ {print $2}'` + CPAPI=`grep -A14 "\[core\]" "${couchpotato_conf}"|awk -F" = " '/^api_key/ {print $2}'` fi status_cmd="${name}_status" diff --git a/libs/tornado/curl_httpclient.py b/libs/tornado/curl_httpclient.py index a6c0bb0d..52350d24 100755 --- a/libs/tornado/curl_httpclient.py +++ b/libs/tornado/curl_httpclient.py @@ -96,17 +96,18 @@ class CurlAsyncHTTPClient(AsyncHTTPClient): pycurl.POLL_INOUT: ioloop.IOLoop.READ | ioloop.IOLoop.WRITE } if event == pycurl.POLL_REMOVE: - self.io_loop.remove_handler(fd) - del self._fds[fd] + if fd in self._fds: + self.io_loop.remove_handler(fd) + del self._fds[fd] else: ioloop_event = event_map[event] if fd not in self._fds: - self._fds[fd] = ioloop_event self.io_loop.add_handler(fd, self._handle_events, ioloop_event) - else: self._fds[fd] = ioloop_event + else: self.io_loop.update_handler(fd, ioloop_event) + self._fds[fd] = ioloop_event def _set_timeout(self, msecs): """Called by libcurl to schedule a timeout.""" diff --git a/libs/tornado/ioloop.py b/libs/tornado/ioloop.py index 3c9b05b9..7b320e59 100755 --- a/libs/tornado/ioloop.py +++ b/libs/tornado/ioloop.py @@ -194,7 +194,7 @@ class IOLoop(Configurable): def initialize(self): pass - def close(self, all_fds=False): + def close(self, all_fds = False): """Closes the IOLoop, freeing any resources used. If ``all_fds`` is true, all file descriptors registered on the @@ -320,7 +320,7 @@ class IOLoop(Configurable): """ raise NotImplementedError() - def add_callback(self, callback): + def add_callback(self, callback, *args, **kwargs): """Calls the given callback on the next I/O loop iteration. It is safe to call this method from any thread at any time, @@ -335,7 +335,7 @@ class IOLoop(Configurable): """ raise NotImplementedError() - def add_callback_from_signal(self, callback): + def add_callback_from_signal(self, callback, *args, **kwargs): """Calls the given callback on the next I/O loop iteration. Safe for use from a Python signal handler; should not be used @@ -359,8 +359,7 @@ class IOLoop(Configurable): assert isinstance(future, IOLoop._FUTURE_TYPES) callback = stack_context.wrap(callback) future.add_done_callback( - lambda future: self.add_callback( - functools.partial(callback, future))) + lambda future: self.add_callback(callback, future)) def _run_callback(self, callback): """Runs a callback with error handling. @@ -382,7 +381,7 @@ class IOLoop(Configurable): The exception itself is not passed explicitly, but is available in sys.exc_info. """ - app_log.error("Exception in callback %r", callback, exc_info=True) + app_log.error("Exception in callback %r", callback, exc_info = True) @@ -393,7 +392,7 @@ class PollIOLoop(IOLoop): (Linux), `tornado.platform.kqueue.KQueueIOLoop` (BSD and Mac), or `tornado.platform.select.SelectIOLoop` (all platforms). """ - def initialize(self, impl, time_func=None): + def initialize(self, impl, time_func = None): super(PollIOLoop, self).initialize() self._impl = impl if hasattr(self._impl, 'fileno'): @@ -417,7 +416,7 @@ class PollIOLoop(IOLoop): lambda fd, events: self._waker.consume(), self.READ) - def close(self, all_fds=False): + def close(self, all_fds = False): with self._callback_lock: self._closing = True self.remove_handler(self._waker.fileno()) @@ -426,7 +425,7 @@ class PollIOLoop(IOLoop): try: os.close(fd) except Exception: - gen_log.debug("error closing fd %s", fd, exc_info=True) + gen_log.debug("error closing fd %s", fd, exc_info = True) self._waker.close() self._impl.close() @@ -442,8 +441,8 @@ class PollIOLoop(IOLoop): self._events.pop(fd, None) try: self._impl.unregister(fd) - except (OSError, IOError): - gen_log.debug("Error deleting fd from IOLoop", exc_info=True) + except Exception: + gen_log.debug("Error deleting fd from IOLoop", exc_info = True) def set_blocking_signal_threshold(self, seconds, action): if not hasattr(signal, "setitimer"): @@ -501,7 +500,7 @@ class PollIOLoop(IOLoop): # IOLoop is just started once at the beginning. signal.set_wakeup_fd(old_wakeup_fd) old_wakeup_fd = None - except ValueError: # non-main thread + except ValueError: # non-main thread pass while True: @@ -569,17 +568,18 @@ class PollIOLoop(IOLoop): while self._events: fd, events = self._events.popitem() try: - self._handlers[fd](fd, events) + hdlr = self._handlers.get(fd) + if hdlr: hdlr(fd, events) except (OSError, IOError), e: if e.args[0] == errno.EPIPE: # Happens when the client closes the connection pass else: app_log.error("Exception in I/O handler for fd %s", - fd, exc_info=True) + fd, exc_info = True) except Exception: app_log.error("Exception in I/O handler for fd %s", - fd, exc_info=True) + fd, exc_info = True) # reset the stopped flag so another start/stop pair can be issued self._stopped = False if self._blocking_signal_threshold is not None: @@ -609,12 +609,13 @@ class PollIOLoop(IOLoop): # collection pass whenever there are too many dead timeouts. timeout.callback = None - def add_callback(self, callback): + def add_callback(self, callback, *args, **kwargs): with self._callback_lock: if self._closing: raise RuntimeError("IOLoop is closing") list_empty = not self._callbacks - self._callbacks.append(stack_context.wrap(callback)) + self._callbacks.append(functools.partial( + stack_context.wrap(callback), *args, **kwargs)) if list_empty and thread.get_ident() != self._thread_ident: # If we're in the IOLoop's thread, we know it's not currently # polling. If we're not, and we added the first callback to an @@ -624,12 +625,12 @@ class PollIOLoop(IOLoop): # avoid it when we can. self._waker.wake() - def add_callback_from_signal(self, callback): + def add_callback_from_signal(self, callback, *args, **kwargs): with stack_context.NullContext(): if thread.get_ident() != self._thread_ident: # if the signal is handled on another thread, we can add # it normally (modulo the NullContext) - self.add_callback(callback) + self.add_callback(callback, *args, **kwargs) else: # If we're on the IOLoop's thread, we cannot use # the regular add_callback because it may deadlock on @@ -639,7 +640,8 @@ class PollIOLoop(IOLoop): # _callback_lock block in IOLoop.start, we may modify # either the old or new version of self._callbacks, # but either way will work. - self._callbacks.append(stack_context.wrap(callback)) + self._callbacks.append(functools.partial( + stack_context.wrap(callback), *args, **kwargs)) class _Timeout(object): @@ -682,7 +684,7 @@ class PeriodicCallback(object): `start` must be called after the PeriodicCallback is created. """ - def __init__(self, callback, callback_time, io_loop=None): + def __init__(self, callback, callback_time, io_loop = None): self.callback = callback if callback_time <= 0: raise ValueError("Periodic callback must have a positive callback_time") @@ -710,7 +712,7 @@ class PeriodicCallback(object): try: self.callback() except Exception: - app_log.error("Error in periodic callback", exc_info=True) + app_log.error("Error in periodic callback", exc_info = True) self._schedule_next() def _schedule_next(self): diff --git a/libs/tornado/iostream.py b/libs/tornado/iostream.py index 40ac4964..6eec2a35 100755 --- a/libs/tornado/iostream.py +++ b/libs/tornado/iostream.py @@ -209,11 +209,19 @@ class BaseIOStream(object): """Call the given callback when the stream is closed.""" self._close_callback = stack_context.wrap(callback) - def close(self): - """Close this stream.""" + def close(self, exc_info=False): + """Close this stream. + + If ``exc_info`` is true, set the ``error`` attribute to the current + exception from `sys.exc_info()` (or if ``exc_info`` is a tuple, + use that instead of `sys.exc_info`). + """ if not self.closed(): - if any(sys.exc_info()): - self.error = sys.exc_info()[1] + if exc_info: + if not isinstance(exc_info, tuple): + exc_info = sys.exc_info() + if any(exc_info): + self.error = exc_info[1] if self._read_until_close: callback = self._read_callback self._read_callback = None @@ -285,7 +293,7 @@ class BaseIOStream(object): except Exception: gen_log.error("Uncaught exception, closing connection.", exc_info=True) - self.close() + self.close(exc_info=True) raise def _run_callback(self, callback, *args): @@ -300,7 +308,7 @@ class BaseIOStream(object): # (It would eventually get closed when the socket object is # gc'd, but we don't want to rely on gc happening before we # run out of file descriptors) - self.close() + self.close(exc_info=True) # Re-raise the exception so that IOLoop.handle_callback_exception # can see it and log the error raise @@ -348,7 +356,7 @@ class BaseIOStream(object): self._pending_callbacks -= 1 except Exception: gen_log.warning("error on read", exc_info=True) - self.close() + self.close(exc_info=True) return if self._read_from_buffer(): return @@ -397,9 +405,9 @@ class BaseIOStream(object): # Treat ECONNRESET as a connection close rather than # an error to minimize log spam (the exception will # be available on self.error for apps that care). - self.close() + self.close(exc_info=True) return - self.close() + self.close(exc_info=True) raise if chunk is None: return 0 @@ -503,7 +511,7 @@ class BaseIOStream(object): else: gen_log.warning("Write error on %d: %s", self.fileno(), e) - self.close() + self.close(exc_info=True) return if not self._write_buffer and self._write_callback: callback = self._write_callback @@ -664,7 +672,7 @@ class IOStream(BaseIOStream): if e.args[0] not in (errno.EINPROGRESS, errno.EWOULDBLOCK): gen_log.warning("Connect error on fd %d: %s", self.socket.fileno(), e) - self.close() + self.close(exc_info=True) return self._connect_callback = stack_context.wrap(callback) self._add_io_state(self.io_loop.WRITE) @@ -733,7 +741,7 @@ class SSLIOStream(IOStream): return elif err.args[0] in (ssl.SSL_ERROR_EOF, ssl.SSL_ERROR_ZERO_RETURN): - return self.close() + return self.close(exc_info=True) elif err.args[0] == ssl.SSL_ERROR_SSL: try: peer = self.socket.getpeername() @@ -741,11 +749,11 @@ class SSLIOStream(IOStream): peer = '(not connected)' gen_log.warning("SSL Error on %d %s: %s", self.socket.fileno(), peer, err) - return self.close() + return self.close(exc_info=True) raise except socket.error, err: if err.args[0] in (errno.ECONNABORTED, errno.ECONNRESET): - return self.close() + return self.close(exc_info=True) else: self._ssl_accepting = False if self._ssl_connect_callback is not None: @@ -842,7 +850,7 @@ class PipeIOStream(BaseIOStream): elif e.args[0] == errno.EBADF: # If the writing half of a pipe is closed, select will # report it as readable but reads will fail with EBADF. - self.close() + self.close(exc_info=True) return None else: raise diff --git a/libs/tornado/platform/twisted.py b/libs/tornado/platform/twisted.py index 6c3cbf96..1efc82b7 100755 --- a/libs/tornado/platform/twisted.py +++ b/libs/tornado/platform/twisted.py @@ -431,6 +431,8 @@ class TwistedIOLoop(tornado.ioloop.IOLoop): self.reactor.removeWriter(self.fds[fd]) def remove_handler(self, fd): + if fd not in self.fds: + return self.fds[fd].lost = True if self.fds[fd].reading: self.reactor.removeReader(self.fds[fd]) @@ -444,6 +446,12 @@ class TwistedIOLoop(tornado.ioloop.IOLoop): def stop(self): self.reactor.crash() + def _run_callback(self, callback, *args, **kwargs): + try: + callback(*args, **kwargs) + except Exception: + self.handle_callback_exception(callback) + def add_timeout(self, deadline, callback): if isinstance(deadline, (int, long, float)): delay = max(deadline - self.time(), 0) @@ -451,13 +459,14 @@ class TwistedIOLoop(tornado.ioloop.IOLoop): delay = deadline.total_seconds() else: raise TypeError("Unsupported deadline %r") - return self.reactor.callLater(delay, wrap(callback)) + return self.reactor.callLater(delay, self._run_callback, wrap(callback)) def remove_timeout(self, timeout): timeout.cancel() - def add_callback(self, callback): - self.reactor.callFromThread(wrap(callback)) + def add_callback(self, callback, *args, **kwargs): + self.reactor.callFromThread(self._run_callback, + wrap(callback), *args, **kwargs) - def add_callback_from_signal(self, callback): - self.add_callback(callback) + def add_callback_from_signal(self, callback, *args, **kwargs): + self.add_callback(callback, *args, **kwargs) diff --git a/libs/tornado/process.py b/libs/tornado/process.py index 9e048c19..fa0be555 100755 --- a/libs/tornado/process.py +++ b/libs/tornado/process.py @@ -268,7 +268,7 @@ class Subprocess(object): assert ret_pid == pid subproc = cls._waiting.pop(pid) subproc.io_loop.add_callback_from_signal( - functools.partial(subproc._set_returncode, status)) + subproc._set_returncode, status) def _set_returncode(self, status): if os.WIFSIGNALED(status): diff --git a/libs/tornado/simple_httpclient.py b/libs/tornado/simple_httpclient.py index faff83c8..7000d987 100755 --- a/libs/tornado/simple_httpclient.py +++ b/libs/tornado/simple_httpclient.py @@ -12,7 +12,6 @@ from tornado.util import b, GzipDecompressor import base64 import collections -import contextlib import copy import functools import os.path @@ -134,7 +133,7 @@ class _HTTPConnection(object): self._decompressor = None # Timeout handle returned by IOLoop.add_timeout self._timeout = None - with stack_context.StackContext(self.cleanup): + with stack_context.ExceptionStackContext(self._handle_exception): self.parsed = urlparse.urlsplit(_unicode(self.request.url)) if ssl is None and self.parsed.scheme == "https": raise ValueError("HTTPS requires either python2.6+ or " @@ -309,19 +308,24 @@ class _HTTPConnection(object): if self.final_callback is not None: final_callback = self.final_callback self.final_callback = None - final_callback(response) + self.io_loop.add_callback(final_callback, response) - @contextlib.contextmanager - def cleanup(self): - try: - yield - except Exception, e: - gen_log.warning("uncaught exception", exc_info=True) - self._run_callback(HTTPResponse(self.request, 599, error=e, + def _handle_exception(self, typ, value, tb): + if self.final_callback: + gen_log.warning("uncaught exception", exc_info=(typ, value, tb)) + self._run_callback(HTTPResponse(self.request, 599, error=value, request_time=self.io_loop.time() - self.start_time, )) + if hasattr(self, "stream"): self.stream.close() + return True + else: + # If our callback has already been called, we are probably + # catching an exception that is not caused by us but rather + # some child of our callback. Rather than drop it on the floor, + # pass it along. + return False def _on_close(self): if self.final_callback is not None: diff --git a/libs/tornado/testing.py b/libs/tornado/testing.py index 59456433..22376627 100755 --- a/libs/tornado/testing.py +++ b/libs/tornado/testing.py @@ -36,9 +36,8 @@ except ImportError: netutil = None SimpleAsyncHTTPClient = None from tornado.log import gen_log -from tornado.stack_context import StackContext +from tornado.stack_context import ExceptionStackContext from tornado.util import raise_exc_info -import contextlib import logging import os import re @@ -167,13 +166,10 @@ class AsyncTestCase(unittest.TestCase): ''' return IOLoop() - @contextlib.contextmanager - def _stack_context(self): - try: - yield - except Exception: - self.__failure = sys.exc_info() - self.stop() + def _handle_exception(self, typ, value, tb): + self.__failure = sys.exc_info() + self.stop() + return True def __rethrow(self): if self.__failure is not None: @@ -182,7 +178,7 @@ class AsyncTestCase(unittest.TestCase): raise_exc_info(failure) def run(self, result=None): - with StackContext(self._stack_context): + with ExceptionStackContext(self._handle_exception): super(AsyncTestCase, self).run(result) # In case an exception escaped super.run or the StackContext caught # an exception when there wasn't a wait() to re-raise it, do so here. diff --git a/libs/tornado/web.py b/libs/tornado/web.py index 7d45ce53..41ce95d4 100755 --- a/libs/tornado/web.py +++ b/libs/tornado/web.py @@ -1317,10 +1317,8 @@ class Application(object): def add_handlers(self, host_pattern, host_handlers): """Appends the given handlers to our handler list. - Note that host patterns are processed sequentially in the - order they were added, and only the first matching pattern is - used. This means that all handlers for a given host must be - added in a single add_handlers call. + Host patterns are processed sequentially in the order they were + added. All matching patterns will be considered. """ if not host_pattern.endswith("$"): host_pattern += "$" @@ -1365,15 +1363,16 @@ class Application(object): def _get_host_handlers(self, request): host = request.host.lower().split(':')[0] + matches = [] for pattern, handlers in self.handlers: if pattern.match(host): - return handlers + matches.extend(handlers) # Look for default host if not behind load balancer (for debugging) - if "X-Real-Ip" not in request.headers: + if not matches and "X-Real-Ip" not in request.headers: for pattern, handlers in self.handlers: if pattern.match(self.default_host): - return handlers - return None + matches.extend(handlers) + return matches or None def _load_ui_methods(self, methods): if type(methods) is types.ModuleType: