From 7fa08ef9b6310446cc640b139501b06b028cbc05 Mon Sep 17 00:00:00 2001 From: spion06 Date: Thu, 27 Dec 2012 19:59:25 -0600 Subject: [PATCH 01/43] Update init/freebsd to not use perl When using couchpotato on slimmed down versions of freebsd (freenas for example) sometimes perl is not available. Since the previous parsing of the INI required a "key = value" format it is pretty simple to use awk for this. --- init/freebsd | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/init/freebsd b/init/freebsd index eeba51d..c9a6530 100644 --- a/init/freebsd +++ b/init/freebsd @@ -36,9 +36,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" From b14a6c1e63147d28613a227b35e64164af270051 Mon Sep 17 00:00:00 2001 From: Ruud Date: Sat, 29 Dec 2012 20:22:42 +0100 Subject: [PATCH 02/43] nzbx description --- couchpotato/core/providers/nzb/nzbx/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/couchpotato/core/providers/nzb/nzbx/__init__.py b/couchpotato/core/providers/nzb/nzbx/__init__.py index 129fba0..f1c7d58 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', From 12a4d6a99501c4e65401f2cfb93aa15cbcb55d7d Mon Sep 17 00:00:00 2001 From: Ruud Date: Sat, 29 Dec 2012 21:23:15 +0100 Subject: [PATCH 03/43] Send proper user-agent with nzbx.co --- couchpotato/core/providers/nzb/nzbx/main.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/couchpotato/core/providers/nzb/nzbx/main.py b/couchpotato/core/providers/nzb/nzbx/main.py index 97ee9cd..ec7fbfe 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: From 041a206fb42f2a191f25263a89d56b1b40b7a69a Mon Sep 17 00:00:00 2001 From: Ruud Date: Sat, 29 Dec 2012 23:45:21 +0100 Subject: [PATCH 04/43] Rename to OMDBapi --- .../core/providers/movie/imdbapi/__init__.py | 6 - couchpotato/core/providers/movie/imdbapi/main.py | 121 --------------------- .../core/providers/movie/omdbapi/__init__.py | 6 + couchpotato/core/providers/movie/omdbapi/main.py | 121 +++++++++++++++++++++ 4 files changed, 127 insertions(+), 127 deletions(-) delete mode 100644 couchpotato/core/providers/movie/imdbapi/__init__.py delete mode 100644 couchpotato/core/providers/movie/imdbapi/main.py create mode 100644 couchpotato/core/providers/movie/omdbapi/__init__.py create mode 100644 couchpotato/core/providers/movie/omdbapi/main.py diff --git a/couchpotato/core/providers/movie/imdbapi/__init__.py b/couchpotato/core/providers/movie/imdbapi/__init__.py deleted file mode 100644 index dd1202e..0000000 --- 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/imdbapi/main.py b/couchpotato/core/providers/movie/imdbapi/main.py deleted file mode 100644 index 53f503e..0000000 --- a/couchpotato/core/providers/movie/imdbapi/main.py +++ /dev/null @@ -1,121 +0,0 @@ -from couchpotato.core.event import addEvent, fireEvent -from couchpotato.core.helpers.encoding import tryUrlencode -from couchpotato.core.helpers.variable import tryInt, tryFloat, splitString -from couchpotato.core.logger import CPLog -from couchpotato.core.providers.movie.base import MovieProvider -import json -import re -import traceback - -log = CPLog(__name__) - - -class IMDBAPI(MovieProvider): - - urls = { - 'search': 'http://www.imdbapi.com/?%s', - 'info': 'http://www.imdbapi.com/?i=%s', - } - - http_time_between_calls = 0 - - def __init__(self): - addEvent('movie.search', self.search) - addEvent('movie.info', self.getInfo) - - def search(self, q, limit = 12): - - name_year = fireEvent('scanner.name_year', q, single = True) - - if not name_year or (name_year and not name_year.get('name')): - name_year = { - 'name': q - } - - cache_key = 'imdbapi.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: - result = self.parseMovie(cached) - if result.get('titles') and len(result.get('titles')) > 0: - log.info('Found: %s', result['titles'][0] + ' (' + str(result['year']) + ')') - return [result] - - return [] - - return [] - - def getInfo(self, identifier = None): - - if not identifier: - return {} - - cache_key = 'imdbapi.cache.%s' % identifier - cached = self.getCache(cache_key, self.urls['info'] % identifier, timeout = 3) - - if cached: - result = self.parseMovie(cached) - if result.get('titles') and len(result.get('titles')) > 0: - log.info('Found: %s', result['titles'][0] + ' (' + str(result['year']) + ')') - return result - - return {} - - def parseMovie(self, movie): - - movie_data = {} - try: - - try: - if isinstance(movie, (str, unicode)): - movie = json.loads(movie) - except ValueError: - log.info('No proper json to decode') - return movie_data - - if movie.get('Response') == 'Parse Error' or movie.get('Response') == 'False': - return movie_data - - tmp_movie = movie.copy() - for key in tmp_movie: - if tmp_movie.get(key).lower() == 'n/a': - del movie[key] - - year = tryInt(movie.get('Year', '')) - - movie_data = { - 'via_imdb': True, - 'titles': [movie.get('Title')] if movie.get('Title') else [], - 'original_title': movie.get('Title', ''), - 'images': { - 'poster': [movie.get('Poster', '')] if movie.get('Poster') and len(movie.get('Poster', '')) > 4 else [], - }, - 'rating': { - 'imdb': (tryFloat(movie.get('imdbRating', 0)), tryInt(movie.get('imdbVotes', '').replace(',', ''))), - #'rotten': (tryFloat(movie.get('tomatoRating', 0)), tryInt(movie.get('tomatoReviews', '').replace(',', ''))), - }, - 'imdb': str(movie.get('imdbID', '')), - 'runtime': self.runtimeToMinutes(movie.get('Runtime', '')), - 'released': movie.get('Released', ''), - 'year': year if isinstance(year, (int)) else None, - 'plot': movie.get('Plot', ''), - 'genres': splitString(movie.get('Genre', '')), - 'directors': splitString(movie.get('Director', '')), - 'writers': splitString(movie.get('Writer', '')), - 'actors': splitString(movie.get('Actors', '')), - } - except: - log.error('Failed parsing IMDB API json: %s', traceback.format_exc()) - - return movie_data - - def runtimeToMinutes(self, runtime_str): - runtime = 0 - - regex = '(\d*.?\d+).(h|hr|hrs|mins|min)+' - matches = re.findall(regex, runtime_str) - for match in matches: - nr, size = match - runtime += tryInt(nr) * (60 if 'h' is str(size)[0] else 1) - - return runtime diff --git a/couchpotato/core/providers/movie/omdbapi/__init__.py b/couchpotato/core/providers/movie/omdbapi/__init__.py new file mode 100644 index 0000000..765662e --- /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/omdbapi/main.py b/couchpotato/core/providers/movie/omdbapi/main.py new file mode 100644 index 0000000..cdfece0 --- /dev/null +++ b/couchpotato/core/providers/movie/omdbapi/main.py @@ -0,0 +1,121 @@ +from couchpotato.core.event import addEvent, fireEvent +from couchpotato.core.helpers.encoding import tryUrlencode +from couchpotato.core.helpers.variable import tryInt, tryFloat, splitString +from couchpotato.core.logger import CPLog +from couchpotato.core.providers.movie.base import MovieProvider +import json +import re +import traceback + +log = CPLog(__name__) + + +class OMDBAPI(MovieProvider): + + urls = { + 'search': 'http://www.omdbapi.com/?%s', + 'info': 'http://www.omdbapi.com/?i=%s', + } + + http_time_between_calls = 0 + + def __init__(self): + addEvent('movie.search', self.search) + addEvent('movie.info', self.getInfo) + + def search(self, q, limit = 12): + + name_year = fireEvent('scanner.name_year', q, single = True) + + if not name_year or (name_year and not name_year.get('name')): + name_year = { + 'name': 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: + result = self.parseMovie(cached) + if result.get('titles') and len(result.get('titles')) > 0: + log.info('Found: %s', result['titles'][0] + ' (' + str(result['year']) + ')') + return [result] + + return [] + + return [] + + def getInfo(self, identifier = None): + + if not identifier: + return {} + + cache_key = 'omdbapi.cache.%s' % identifier + cached = self.getCache(cache_key, self.urls['info'] % identifier, timeout = 3) + + if cached: + result = self.parseMovie(cached) + if result.get('titles') and len(result.get('titles')) > 0: + log.info('Found: %s', result['titles'][0] + ' (' + str(result['year']) + ')') + return result + + return {} + + def parseMovie(self, movie): + + movie_data = {} + try: + + try: + if isinstance(movie, (str, unicode)): + movie = json.loads(movie) + except ValueError: + log.info('No proper json to decode') + return movie_data + + if movie.get('Response') == 'Parse Error' or movie.get('Response') == 'False': + return movie_data + + tmp_movie = movie.copy() + for key in tmp_movie: + if tmp_movie.get(key).lower() == 'n/a': + del movie[key] + + year = tryInt(movie.get('Year', '')) + + movie_data = { + 'via_imdb': True, + 'titles': [movie.get('Title')] if movie.get('Title') else [], + 'original_title': movie.get('Title', ''), + 'images': { + 'poster': [movie.get('Poster', '')] if movie.get('Poster') and len(movie.get('Poster', '')) > 4 else [], + }, + 'rating': { + 'imdb': (tryFloat(movie.get('imdbRating', 0)), tryInt(movie.get('imdbVotes', '').replace(',', ''))), + #'rotten': (tryFloat(movie.get('tomatoRating', 0)), tryInt(movie.get('tomatoReviews', '').replace(',', ''))), + }, + 'imdb': str(movie.get('imdbID', '')), + 'runtime': self.runtimeToMinutes(movie.get('Runtime', '')), + 'released': movie.get('Released', ''), + 'year': year if isinstance(year, (int)) else None, + 'plot': movie.get('Plot', ''), + 'genres': splitString(movie.get('Genre', '')), + 'directors': splitString(movie.get('Director', '')), + 'writers': splitString(movie.get('Writer', '')), + 'actors': splitString(movie.get('Actors', '')), + } + except: + log.error('Failed parsing IMDB API json: %s', traceback.format_exc()) + + return movie_data + + def runtimeToMinutes(self, runtime_str): + runtime = 0 + + regex = '(\d*.?\d+).(h|hr|hrs|mins|min)+' + matches = re.findall(regex, runtime_str) + for match in matches: + nr, size = match + runtime += tryInt(nr) * (60 if 'h' is str(size)[0] else 1) + + return runtime From 1e0267cdb5f9c192a673856ac21f55a8791d9d3c Mon Sep 17 00:00:00 2001 From: Ruud Date: Sun, 30 Dec 2012 11:21:23 +0100 Subject: [PATCH 05/43] Change OMGWTF to .org --- couchpotato/core/providers/nzb/omgwtfnzbs/main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/couchpotato/core/providers/nzb/omgwtfnzbs/main.py b/couchpotato/core/providers/nzb/omgwtfnzbs/main.py index f3de767..0a18b8f 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 From 74e4b015a954712323df6c736a13ba713db19b62 Mon Sep 17 00:00:00 2001 From: Ruud Date: Sun, 30 Dec 2012 18:38:52 +0100 Subject: [PATCH 06/43] Module update: Tornade --- libs/tornado/curl_httpclient.py | 9 ++++---- libs/tornado/ioloop.py | 46 ++++++++++++++++++++------------------- libs/tornado/iostream.py | 38 +++++++++++++++++++------------- libs/tornado/platform/twisted.py | 19 +++++++++++----- libs/tornado/process.py | 2 +- libs/tornado/simple_httpclient.py | 26 ++++++++++++---------- libs/tornado/testing.py | 16 +++++--------- libs/tornado/web.py | 15 ++++++------- 8 files changed, 95 insertions(+), 76 deletions(-) diff --git a/libs/tornado/curl_httpclient.py b/libs/tornado/curl_httpclient.py index a6c0bb0..52350d2 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 3c9b05b..7b320e5 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 40ac496..6eec2a3 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 6c3cbf9..1efc82b 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 9e048c1..fa0be55 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 faff83c..7000d98 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) - - @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, + self.io_loop.add_callback(final_callback, response) + + 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 5945643..2237662 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 7d45ce5..41ce95d 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: From 611a32d1108d6bedbbf0743dd1b5616080a75cce Mon Sep 17 00:00:00 2001 From: Ruud Date: Sun, 30 Dec 2012 18:39:16 +0100 Subject: [PATCH 07/43] Add randomstring to each internal api --- couchpotato/static/scripts/api.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/couchpotato/static/scripts/api.js b/couchpotato/static/scripts/api.js index f14eb14..5e507bc 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() }, From e92b5d95ca0c9b80b4adf2c05c7c7f27113ed125 Mon Sep 17 00:00:00 2001 From: Ruud Date: Sun, 30 Dec 2012 18:40:38 +0100 Subject: [PATCH 08/43] IOLoop cleanup --- couchpotato/api.py | 7 +++++-- couchpotato/runner.py | 2 +- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/couchpotato/api.py b/couchpotato/api.py index 15ef2b4..718527c 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/runner.py b/couchpotato/runner.py index f55c65b..c0b7eb8 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() From 6b8bca5491b80c342951bacf2c7c73ea80a6d4b8 Mon Sep 17 00:00:00 2001 From: Sander Boele Date: Sun, 30 Dec 2012 13:31:06 +0100 Subject: [PATCH 09/43] added path to the freebsd init script --- init/freebsd | 3 +++ 1 file changed, 3 insertions(+) mode change 100644 => 100755 init/freebsd diff --git a/init/freebsd b/init/freebsd old mode 100644 new mode 100755 index c9a6530..1171440 --- 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"} From 580ff38136cc438319d32ed8a6bfd105cfc5beaa Mon Sep 17 00:00:00 2001 From: Kris Kater Date: Sun, 30 Dec 2012 16:23:22 +0100 Subject: [PATCH 10/43] Added moviemeter.nl automation --- .../providers/automation/moviemeter_nl/__init__.py | 23 +++++++++++++ .../providers/automation/moviemeter_nl/main.py | 39 ++++++++++++++++++++++ 2 files changed, 62 insertions(+) create mode 100644 couchpotato/core/providers/automation/moviemeter_nl/__init__.py create mode 100644 couchpotato/core/providers/automation/moviemeter_nl/main.py diff --git a/couchpotato/core/providers/automation/moviemeter_nl/__init__.py b/couchpotato/core/providers/automation/moviemeter_nl/__init__.py new file mode 100644 index 0000000..8ea7c06 --- /dev/null +++ b/couchpotato/core/providers/automation/moviemeter_nl/__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_nl/main.py b/couchpotato/core/providers/automation/moviemeter_nl/main.py new file mode 100644 index 0000000..b9de372 --- /dev/null +++ b/couchpotato/core/providers/automation/moviemeter_nl/main.py @@ -0,0 +1,39 @@ +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__) + + +class Moviemeter(Automation, RSS): + + interval = 1800 + rss_url = 'http://www.moviemeter.nl/rss/cinema' + + def getIMDBids(self): + + if self.isDisabled(): + return + + movies = [] + + cache_key = 'moviemeter.%s' % md5(self.rss_url) + rss_data = self.getCache(cache_key, self.rss_url) + data = XMLTree.fromstring(rss_data) + + 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") + + imdb = self.search(name, year) + + if imdb and self.isMinimalMovie(imdb): + movies.append(imdb['imdb']) + + return movies From c29cb39797c6884b3b36e7a54a5356875c32f506 Mon Sep 17 00:00:00 2001 From: Ruud Date: Sun, 30 Dec 2012 21:43:13 +0100 Subject: [PATCH 11/43] Automation cleanup --- couchpotato/core/plugins/automation/main.py | 2 +- couchpotato/core/providers/automation/base.py | 7 ++- .../core/providers/automation/bluray/main.py | 35 +++++--------- couchpotato/core/providers/automation/cp/main.py | 3 -- couchpotato/core/providers/automation/imdb/main.py | 8 +--- .../core/providers/automation/kinepolis/main.py | 24 +++------- .../providers/automation/moviemeter/__init__.py | 23 +++++++++ .../core/providers/automation/moviemeter/main.py | 28 +++++++++++ .../providers/automation/moviemeter_nl/__init__.py | 23 --------- .../providers/automation/moviemeter_nl/main.py | 39 --------------- .../core/providers/automation/movies_io/main.py | 35 ++++---------- .../core/providers/automation/trakt/main.py | 29 +++-------- couchpotato/core/providers/base.py | 56 +++++++++++----------- 13 files changed, 124 insertions(+), 188 deletions(-) create mode 100644 couchpotato/core/providers/automation/moviemeter/__init__.py create mode 100644 couchpotato/core/providers/automation/moviemeter/main.py delete mode 100644 couchpotato/core/providers/automation/moviemeter_nl/__init__.py delete mode 100644 couchpotato/core/providers/automation/moviemeter_nl/main.py diff --git a/couchpotato/core/plugins/automation/main.py b/couchpotato/core/plugins/automation/main.py index f4ede40..c216688 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/providers/automation/base.py b/couchpotato/core/providers/automation/base.py index 9f86ad1..8d08f91 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 ac28152..235a1e5 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) - - if data is not None: - rss_movies = self.getElements(data, 'channel/item') + rss_movies = self.getRSSData(self.rss_url) - 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 24d6909..22b7942 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 c232485..428a8b2 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 f6633af..4158d48 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) - - if data is not None: - rss_movies = self.getElements(data, 'channel/item') + rss_movies = self.getRSSData(self.rss_url) - 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 0000000..8ea7c06 --- /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 0000000..dae764b --- /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/moviemeter_nl/__init__.py b/couchpotato/core/providers/automation/moviemeter_nl/__init__.py deleted file mode 100644 index 8ea7c06..0000000 --- a/couchpotato/core/providers/automation/moviemeter_nl/__init__.py +++ /dev/null @@ -1,23 +0,0 @@ -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_nl/main.py b/couchpotato/core/providers/automation/moviemeter_nl/main.py deleted file mode 100644 index b9de372..0000000 --- a/couchpotato/core/providers/automation/moviemeter_nl/main.py +++ /dev/null @@ -1,39 +0,0 @@ -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__) - - -class Moviemeter(Automation, RSS): - - interval = 1800 - rss_url = 'http://www.moviemeter.nl/rss/cinema' - - def getIMDBids(self): - - if self.isDisabled(): - return - - movies = [] - - cache_key = 'moviemeter.%s' % md5(self.rss_url) - rss_data = self.getCache(cache_key, self.rss_url) - data = XMLTree.fromstring(rss_data) - - 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") - - imdb = self.search(name, 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 5875a4d..0737e2e 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_data = self.getCache(cache_key, rss_url, headers = {'Referer': ''}) - data = XMLTree.fromstring(rss_data) - rss_movies = self.getElements(data, 'channel/item') + rss_movies = self.getRSSData(rss_url, headers = {'Referer': ''}) - 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 764f6cf..0109daf 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 = {} - - 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 [] + headers = {} + if self.conf('automation_password'): + headers['Authorization'] = 'Basic %s' % base64.encodestring('%s:%s' % (self.conf('automation_username'), self.conf('automation_password')))[:-1] + + 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 f16cf6e..1415286 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): @@ -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): From 0cbee01024fac001b3f7a6a5ebd1eee6dd3c8ad7 Mon Sep 17 00:00:00 2001 From: Ruud Date: Mon, 31 Dec 2012 13:10:11 +0100 Subject: [PATCH 12/43] Don't use unicode when not needed in urlopen --- couchpotato/core/downloaders/sabnzbd/main.py | 4 ++-- couchpotato/core/plugins/base.py | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/couchpotato/core/downloaders/sabnzbd/main.py b/couchpotato/core/downloaders/sabnzbd/main.py index e9b4c0a..567ea45 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/base.py b/couchpotato/core/plugins/base.py index 2392f25..61b5459 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 = {} From 615468e8e659097ad2d993ab8d98c29050eeecd0 Mon Sep 17 00:00:00 2001 From: Ruud Date: Mon, 31 Dec 2012 21:31:11 +0100 Subject: [PATCH 13/43] Make nzbget password a password field. closes #1205 --- couchpotato/core/downloaders/nzbget/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/couchpotato/core/downloaders/nzbget/__init__.py b/couchpotato/core/downloaders/nzbget/__init__.py index 8b68c95..1099429 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', }, { From f30cb9185c526d07c9fc0c7ca2313bd2254f60ad Mon Sep 17 00:00:00 2001 From: Ruud Date: Wed, 2 Jan 2013 10:40:08 +0100 Subject: [PATCH 14/43] Add nzbgeek to the defaults of newznab --- couchpotato/core/providers/nzb/newznab/__init__.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/couchpotato/core/providers/nzb/newznab/__init__.py b/couchpotato/core/providers/nzb/newznab/__init__.py index c9507f9..1e76d1c 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', From 3aea2cd96885d29ff6ca09b2b647c1b7373d15f2 Mon Sep 17 00:00:00 2001 From: Ruud Date: Wed, 2 Jan 2013 13:29:08 +0100 Subject: [PATCH 15/43] Simpler CP tag regex --- couchpotato/core/plugins/scanner/main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/couchpotato/core/plugins/scanner/main.py b/couchpotato/core/plugins/scanner/main.py index 2a1d307..971fece 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): From 2f65545086bf0f866a07cca75abd6bab6354fb0a Mon Sep 17 00:00:00 2001 From: Ruud Date: Wed, 2 Jan 2013 13:29:28 +0100 Subject: [PATCH 16/43] Extend opener with multipart --- couchpotato/core/plugins/base.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/couchpotato/core/plugins/base.py b/couchpotato/core/plugins/base.py index 61b5459..a3d5628 100644 --- a/couchpotato/core/plugins/base.py +++ b/couchpotato/core/plugins/base.py @@ -130,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: From c2382ade0599429d11125130f707435be73fb5cb Mon Sep 17 00:00:00 2001 From: Ruud Date: Wed, 2 Jan 2013 13:29:44 +0100 Subject: [PATCH 17/43] Use provider for downloader --- couchpotato/core/downloaders/base.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/couchpotato/core/downloaders/base.py b/couchpotato/core/downloaders/base.py index 98ab73b..7a695e2 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', From bf4dc62f54c2f973f004c2a2770e439ef1924274 Mon Sep 17 00:00:00 2001 From: Ruud Date: Wed, 2 Jan 2013 13:31:14 +0100 Subject: [PATCH 18/43] NZBVortex support. closes #1204 --- couchpotato/core/downloaders/nzbvortex/__init__.py | 40 +++++ couchpotato/core/downloaders/nzbvortex/main.py | 161 +++++++++++++++++++++ 2 files changed, 201 insertions(+) create mode 100644 couchpotato/core/downloaders/nzbvortex/__init__.py create mode 100644 couchpotato/core/downloaders/nzbvortex/main.py diff --git a/couchpotato/core/downloaders/nzbvortex/__init__.py b/couchpotato/core/downloaders/nzbvortex/__init__.py new file mode 100644 index 0000000..d79983f --- /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 0000000..d9078f8 --- /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) From 015675750c65d7663af529a57af1c9bcfef3d06f Mon Sep 17 00:00:00 2001 From: Ruud Date: Wed, 2 Jan 2013 13:43:24 +0100 Subject: [PATCH 19/43] Properly use imdb_results --- couchpotato/core/providers/base.py | 6 +++--- couchpotato/core/providers/nzb/newznab/main.py | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/couchpotato/core/providers/base.py b/couchpotato/core/providers/base.py index 1415286..213fa43 100644 --- a/couchpotato/core/providers/base.py +++ b/couchpotato/core/providers/base.py @@ -134,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: diff --git a/couchpotato/core/providers/nzb/newznab/main.py b/couchpotato/core/providers/nzb/newznab/main.py index c18724a..c7c27a8 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): From 96a39dbf607f2537160daa5d9e24a4e1a344f0a3 Mon Sep 17 00:00:00 2001 From: Ruud Date: Wed, 2 Jan 2013 13:52:44 +0100 Subject: [PATCH 20/43] Link to downloaders --- couchpotato/core/downloaders/nzbget/__init__.py | 2 +- couchpotato/core/downloaders/nzbvortex/__init__.py | 2 +- couchpotato/core/downloaders/pneumatic/__init__.py | 2 +- couchpotato/core/downloaders/sabnzbd/__init__.py | 2 +- couchpotato/core/downloaders/synology/__init__.py | 2 +- couchpotato/core/downloaders/transmission/__init__.py | 2 +- couchpotato/core/downloaders/utorrent/__init__.py | 2 +- 7 files changed, 7 insertions(+), 7 deletions(-) diff --git a/couchpotato/core/downloaders/nzbget/__init__.py b/couchpotato/core/downloaders/nzbget/__init__.py index 1099429..4f5afb3 100644 --- a/couchpotato/core/downloaders/nzbget/__init__.py +++ b/couchpotato/core/downloaders/nzbget/__init__.py @@ -10,7 +10,7 @@ config = [{ 'tab': 'downloaders', 'name': 'nzbget', 'label': 'NZBGet', - 'description': 'Send NZBs to your NZBGet installation.', + 'description': 'Use NZBGet to download NZBs.', 'options': [ { 'name': 'enabled', diff --git a/couchpotato/core/downloaders/nzbvortex/__init__.py b/couchpotato/core/downloaders/nzbvortex/__init__.py index d79983f..ce9c858 100644 --- a/couchpotato/core/downloaders/nzbvortex/__init__.py +++ b/couchpotato/core/downloaders/nzbvortex/__init__.py @@ -10,7 +10,7 @@ config = [{ 'tab': 'downloaders', 'name': 'nzbvortex', 'label': 'NZBVortex', - 'description': 'Send NZBs to your NZBVortex app.', + 'description': 'Use NZBVortex to download NZBs.', 'wizard': True, 'options': [ { diff --git a/couchpotato/core/downloaders/pneumatic/__init__.py b/couchpotato/core/downloaders/pneumatic/__init__.py index 004821c..f119cfc 100644 --- a/couchpotato/core/downloaders/pneumatic/__init__.py +++ b/couchpotato/core/downloaders/pneumatic/__init__.py @@ -11,7 +11,7 @@ config = [{ 'tab': 'downloaders', 'name': 'pneumatic', 'label': 'Pneumatic', - 'description': 'Download the .strm file to a specific folder.', + 'description': 'Use Pneumatic to download .strm files.', 'options': [ { 'name': 'enabled', diff --git a/couchpotato/core/downloaders/sabnzbd/__init__.py b/couchpotato/core/downloaders/sabnzbd/__init__.py index 927a9ff..e416250 100644 --- a/couchpotato/core/downloaders/sabnzbd/__init__.py +++ b/couchpotato/core/downloaders/sabnzbd/__init__.py @@ -10,7 +10,7 @@ config = [{ 'tab': 'downloaders', 'name': 'sabnzbd', 'label': 'Sabnzbd', - 'description': 'Send NZBs to your Sabnzbd installation.', + 'description': 'Use SABnzbd to download NZBs.', 'wizard': True, 'options': [ { diff --git a/couchpotato/core/downloaders/synology/__init__.py b/couchpotato/core/downloaders/synology/__init__.py index 2b4757a..2b7e861 100644 --- a/couchpotato/core/downloaders/synology/__init__.py +++ b/couchpotato/core/downloaders/synology/__init__.py @@ -10,7 +10,7 @@ config = [{ 'tab': 'downloaders', 'name': 'synology', 'label': 'Synology', - 'description': 'Send torrents to Synology\'s Download Station.', + 'description': 'Use Synology Download Station to download.', 'wizard': True, 'options': [ { diff --git a/couchpotato/core/downloaders/transmission/__init__.py b/couchpotato/core/downloaders/transmission/__init__.py index 189fcd9..0fe1184 100644 --- a/couchpotato/core/downloaders/transmission/__init__.py +++ b/couchpotato/core/downloaders/transmission/__init__.py @@ -10,7 +10,7 @@ config = [{ 'tab': 'downloaders', 'name': 'transmission', 'label': 'Transmission', - 'description': 'Send torrents to Transmission.', + 'description': 'Use Transmission to download torrents.', 'wizard': True, 'options': [ { diff --git a/couchpotato/core/downloaders/utorrent/__init__.py b/couchpotato/core/downloaders/utorrent/__init__.py index 88ceaf7..09a82a1 100644 --- a/couchpotato/core/downloaders/utorrent/__init__.py +++ b/couchpotato/core/downloaders/utorrent/__init__.py @@ -10,7 +10,7 @@ config = [{ 'tab': 'downloaders', 'name': 'utorrent', 'label': 'uTorrent', - 'description': 'Send torrents to uTorrent.', + 'description': 'Use uTorrent to download torrents.', 'wizard': True, 'options': [ { From 9a60f6001a96d1850735085591b6d180f8b737ac Mon Sep 17 00:00:00 2001 From: Ruud Date: Wed, 2 Jan 2013 14:10:41 +0100 Subject: [PATCH 21/43] Check snatched on startup --- couchpotato/core/plugins/renamer/main.py | 1 + 1 file changed, 1 insertion(+) diff --git a/couchpotato/core/plugins/renamer/main.py b/couchpotato/core/plugins/renamer/main.py index 75982ba..91dfb33 100644 --- a/couchpotato/core/plugins/renamer/main.py +++ b/couchpotato/core/plugins/renamer/main.py @@ -33,6 +33,7 @@ class Renamer(Plugin): addEvent('renamer.check_snatched', self.checkSnatched) addEvent('app.load', self.scan) + addEvent('app.load', self.checkSnatched) if self.conf('run_every') > 0: fireEvent('schedule.interval', 'renamer.check_snatched', self.checkSnatched, minutes = self.conf('run_every')) From d314a9b5b37ca03d357180699d406425949a2306 Mon Sep 17 00:00:00 2001 From: Ruud Date: Wed, 2 Jan 2013 14:11:11 +0100 Subject: [PATCH 22/43] Also check status on manual --- couchpotato/core/downloaders/nzbvortex/main.py | 2 +- couchpotato/core/downloaders/sabnzbd/main.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/couchpotato/core/downloaders/nzbvortex/main.py b/couchpotato/core/downloaders/nzbvortex/main.py index d9078f8..894cf0e 100644 --- a/couchpotato/core/downloaders/nzbvortex/main.py +++ b/couchpotato/core/downloaders/nzbvortex/main.py @@ -39,7 +39,7 @@ class NZBVortex(Downloader): def getAllDownloadStatus(self): - if self.isDisabled(manual = False): + if self.isDisabled(manual = True): return False raw_statuses = self.call('nzb') diff --git a/couchpotato/core/downloaders/sabnzbd/main.py b/couchpotato/core/downloaders/sabnzbd/main.py index 567ea45..e3848e4 100644 --- a/couchpotato/core/downloaders/sabnzbd/main.py +++ b/couchpotato/core/downloaders/sabnzbd/main.py @@ -65,7 +65,7 @@ class Sabnzbd(Downloader): return False def getAllDownloadStatus(self): - if self.isDisabled(manual = False): + if self.isDisabled(manual = True): return False log.debug('Checking SABnzbd download status.') From a1d4bab7932c984e4497542c39aca9cfccbc6125 Mon Sep 17 00:00:00 2001 From: Ruud Date: Wed, 2 Jan 2013 14:11:40 +0100 Subject: [PATCH 23/43] NZBVortex: Delete failed option --- couchpotato/core/downloaders/nzbvortex/__init__.py | 6 ++++++ couchpotato/core/downloaders/nzbvortex/main.py | 15 +++++++++++++++ 2 files changed, 21 insertions(+) diff --git a/couchpotato/core/downloaders/nzbvortex/__init__.py b/couchpotato/core/downloaders/nzbvortex/__init__.py index ce9c858..e0e7746 100644 --- a/couchpotato/core/downloaders/nzbvortex/__init__.py +++ b/couchpotato/core/downloaders/nzbvortex/__init__.py @@ -34,6 +34,12 @@ config = [{ 'advanced': True, 'description': 'Disable this downloader for automated searches, but use it when I manually send a release.', }, + { + 'name': 'delete_failed', + 'default': True, + 'type': 'bool', + 'description': 'Delete a release after the download has failed.', + }, ], } ], diff --git a/couchpotato/core/downloaders/nzbvortex/main.py b/couchpotato/core/downloaders/nzbvortex/main.py index 894cf0e..32b1e6e 100644 --- a/couchpotato/core/downloaders/nzbvortex/main.py +++ b/couchpotato/core/downloaders/nzbvortex/main.py @@ -64,6 +64,21 @@ class NZBVortex(Downloader): return statuses + def removeFailed(self, item): + + if not self.conf('delete_failed', default = True): + return False + + log.info('%s failed downloading, deleting...', item['name']) + + try: + self.call('nzb/%s/cancel' % item['id']) + except: + log.error('Failed deleting: %s', traceback.format_exc(0)) + return False + + return True + def login(self): nonce = self.call('auth/nonce', auth = False).get('authNonce') From a3a2c8da8eac8bfca76203e954a9f48c320cc4e9 Mon Sep 17 00:00:00 2001 From: Ruud Date: Fri, 4 Jan 2013 19:03:36 +0100 Subject: [PATCH 24/43] Typo --- couchpotato/core/plugins/scanner/main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/couchpotato/core/plugins/scanner/main.py b/couchpotato/core/plugins/scanner/main.py index 971fece..684f682 100644 --- a/couchpotato/core/plugins/scanner/main.py +++ b/couchpotato/core/plugins/scanner/main.py @@ -341,7 +341,7 @@ class Scanner(Plugin): group['files']['movie'] = self.getMediaFiles(group['unsorted_files']) if len(group['files']['movie']) == 0: - log.error('Couldn\t find any movie files for %s', identifier) + log.error('Couldn\'t find any movie files for %s', identifier) continue log.debug('Getting metadata for %s', identifier) From c2453bb07000bc57bc0dbb42109f1dd9db779794 Mon Sep 17 00:00:00 2001 From: Travis La Marr Date: Tue, 1 Jan 2013 17:34:53 -0500 Subject: [PATCH 25/43] Added Windows Phone SuperToasty Notifier --- couchpotato/core/notifications/toasty/__init__.py | 32 +++++++++++++++++++++++ couchpotato/core/notifications/toasty/main.py | 30 +++++++++++++++++++++ 2 files changed, 62 insertions(+) create mode 100644 couchpotato/core/notifications/toasty/__init__.py create mode 100644 couchpotato/core/notifications/toasty/main.py diff --git a/couchpotato/core/notifications/toasty/__init__.py b/couchpotato/core/notifications/toasty/__init__.py new file mode 100644 index 0000000..25d27ec --- /dev/null +++ b/couchpotato/core/notifications/toasty/__init__.py @@ -0,0 +1,32 @@ +from .main import Toasty + +def start(): + return Toasty() + +config = [{ + 'name': 'toasty', + 'groups': [ + { + 'tab': 'notifications', + 'name': 'toasty', + 'options': [ + { + 'name': 'enabled', + 'default': 0, + 'type': 'enabler', + }, + { + 'name': 'api_key', + 'label': 'Device ID', + }, + { + 'name': 'on_snatch', + 'default': 0, + 'type': 'bool', + 'advanced': True, + 'description': 'Also send message when movie is snatched.', + }, + ], + } + ], +}] diff --git a/couchpotato/core/notifications/toasty/main.py b/couchpotato/core/notifications/toasty/main.py new file mode 100644 index 0000000..34d338f --- /dev/null +++ b/couchpotato/core/notifications/toasty/main.py @@ -0,0 +1,30 @@ +from couchpotato.core.helpers.encoding import toUnicode +from couchpotato.core.logger import CPLog +from couchpotato.core.notifications.base import Notification +from httplib import HTTPConnection +from urllib import urlencode +import traceback + +log = CPLog(__name__) + +class Toasty(Notification): + + def notify(self, message = '', data = {}, listener = None): + if self.isDisabled(): return + + data = { + 'title': self.default_title, + 'text': toUnicode(message), + 'sender': toUnicode("CouchPotato"), + 'image': 'https://raw.github.com/RuudBurger/CouchPotatoServer/master/couchpotato/static/images/homescreen.png', + } + + try: + http_handler = HTTPConnection("api.supertoasty.com") + http_handler.request("GET", "/notify/"+self.conf('api_key')+"?"+urlencode(data)) + log.info('Toasty notifications sent.') + return True + except: + log.error('Toasty failed: %s', traceback.format_exc()) + + return False From 41c28453286f0ba119c85f2049fcfd9ab136fa19 Mon Sep 17 00:00:00 2001 From: Ruud Date: Fri, 4 Jan 2013 20:14:25 +0100 Subject: [PATCH 26/43] Toasty cleanup --- couchpotato/core/notifications/toasty/main.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/couchpotato/core/notifications/toasty/main.py b/couchpotato/core/notifications/toasty/main.py index 34d338f..638c75d 100644 --- a/couchpotato/core/notifications/toasty/main.py +++ b/couchpotato/core/notifications/toasty/main.py @@ -1,14 +1,16 @@ -from couchpotato.core.helpers.encoding import toUnicode +from couchpotato.core.helpers.encoding import toUnicode, tryUrlencode from couchpotato.core.logger import CPLog from couchpotato.core.notifications.base import Notification -from httplib import HTTPConnection -from urllib import urlencode import traceback log = CPLog(__name__) class Toasty(Notification): + urls = { + 'api': 'http://api.supertoasty.com/notify/%s?%s' + } + def notify(self, message = '', data = {}, listener = None): if self.isDisabled(): return @@ -20,9 +22,7 @@ class Toasty(Notification): } try: - http_handler = HTTPConnection("api.supertoasty.com") - http_handler.request("GET", "/notify/"+self.conf('api_key')+"?"+urlencode(data)) - log.info('Toasty notifications sent.') + self.urlopen(self.urls['api'] % (self.conf('api_key'), tryUrlencode(data)), show_error = False) return True except: log.error('Toasty failed: %s', traceback.format_exc()) From da429f0cb863bffd568b65676a809bbc2d5ee62f Mon Sep 17 00:00:00 2001 From: Joseph Gardner Date: Mon, 31 Dec 2012 17:03:21 -0500 Subject: [PATCH 27/43] Adding itunes automation provider --- .../core/providers/automation/itunes/__init__.py | 33 ++++++++++++ .../core/providers/automation/itunes/main.py | 63 ++++++++++++++++++++++ 2 files changed, 96 insertions(+) create mode 100644 couchpotato/core/providers/automation/itunes/__init__.py create mode 100644 couchpotato/core/providers/automation/itunes/main.py diff --git a/couchpotato/core/providers/automation/itunes/__init__.py b/couchpotato/core/providers/automation/itunes/__init__.py new file mode 100644 index 0000000..9a01f65 --- /dev/null +++ b/couchpotato/core/providers/automation/itunes/__init__.py @@ -0,0 +1,33 @@ +from .main import ITunes + +def start(): + return ITunes() + +config = [{ + 'name': 'itunes', + 'groups': [ + { + 'tab': 'automation', + 'name': 'itunes_automation', + 'label': 'iTunes', + 'description': 'From any iTunes Store feed. Url should be the RSS link. (uses minimal requirements)', + 'options': [ + { + 'name': 'automation_enabled', + 'default': False, + 'type': 'enabler', + }, + { + 'name': 'automation_urls_use', + 'label': 'Use', + }, + { + 'name': 'automation_urls', + 'label': 'url', + 'type': 'combined', + 'combine': ['automation_urls_use', 'automation_urls'], + }, + ], + }, + ], +}] diff --git a/couchpotato/core/providers/automation/itunes/main.py b/couchpotato/core/providers/automation/itunes/main.py new file mode 100644 index 0000000..466589d --- /dev/null +++ b/couchpotato/core/providers/automation/itunes/main.py @@ -0,0 +1,63 @@ +from couchpotato.core.helpers.rss import RSS +from couchpotato.core.helpers.variable import md5, getImdb, splitString, tryInt +from couchpotato.core.logger import CPLog +from couchpotato.core.providers.automation.base import Automation +import datetime +import xml.etree.ElementTree as XMLTree +from xml.etree.ElementTree import QName +import traceback + +log = CPLog(__name__) + + +class ITunes(Automation, RSS): + + interval = 1800 + + def getIMDBids(self): + + if self.isDisabled(): + return + + movies = [] + + enablers = [tryInt(x) for x in splitString(self.conf('automation_urls_use'))] + urls = splitString(self.conf('automation_urls')) + + namespace = 'http://www.w3.org/2005/Atom' + namespaceIM = 'http://itunes.apple.com/rss' + + index = -1 + for url in urls: + + index += 1 + if not enablers[index]: + continue + + try: + cache_key = 'itunes.rss.%s' % md5(url) + rss_data = self.getCache(cache_key, url) + + data = XMLTree.fromstring(rss_data) + + if data is not None: + entry_tag = str(QName(namespace, 'entry')) + rss_movies = self.getElements(data, entry_tag) + + for movie in rss_movies: + name_tag = str(QName(namespaceIM, 'name')) + name = self.getTextElement( movie, name_tag ) + + releaseDate_tag = str(QName(namespaceIM, 'releaseDate')) + releaseDateText = self.getTextElement(movie, releaseDate_tag) + year = datetime.datetime.strptime(releaseDateText, '%Y-%m-%dT00:00:00-07:00').strftime("%Y") + + imdb = self.search(name, year) + + if imdb and self.isMinimalMovie(imdb): + movies.append(imdb['imdb']) + + except: + log.error('Failed loading iTunes rss feed: %s %s', (url, traceback.format_exc())) + + return movies From 637b21cc68b6c62deb9c38271adcbd43627155f9 Mon Sep 17 00:00:00 2001 From: Ruud Date: Fri, 4 Jan 2013 20:20:52 +0100 Subject: [PATCH 28/43] iTunes automation cleanup --- .../core/providers/automation/itunes/__init__.py | 2 +- .../core/providers/automation/itunes/main.py | 24 +++++++++++----------- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/couchpotato/core/providers/automation/itunes/__init__.py b/couchpotato/core/providers/automation/itunes/__init__.py index 9a01f65..a88697e 100644 --- a/couchpotato/core/providers/automation/itunes/__init__.py +++ b/couchpotato/core/providers/automation/itunes/__init__.py @@ -1,4 +1,4 @@ -from .main import ITunes +from .main import ITunes def start(): return ITunes() diff --git a/couchpotato/core/providers/automation/itunes/main.py b/couchpotato/core/providers/automation/itunes/main.py index 466589d..14ca2a8 100644 --- a/couchpotato/core/providers/automation/itunes/main.py +++ b/couchpotato/core/providers/automation/itunes/main.py @@ -1,11 +1,11 @@ from couchpotato.core.helpers.rss import RSS -from couchpotato.core.helpers.variable import md5, getImdb, splitString, tryInt +from couchpotato.core.helpers.variable import md5, splitString, tryInt from couchpotato.core.logger import CPLog from couchpotato.core.providers.automation.base import Automation -import datetime -import xml.etree.ElementTree as XMLTree from xml.etree.ElementTree import QName +import datetime import traceback +import xml.etree.ElementTree as XMLTree log = CPLog(__name__) @@ -26,7 +26,7 @@ class ITunes(Automation, RSS): namespace = 'http://www.w3.org/2005/Atom' namespaceIM = 'http://itunes.apple.com/rss' - + index = -1 for url in urls: @@ -39,24 +39,24 @@ class ITunes(Automation, RSS): rss_data = self.getCache(cache_key, url) data = XMLTree.fromstring(rss_data) - + if data is not None: entry_tag = str(QName(namespace, 'entry')) rss_movies = self.getElements(data, entry_tag) - + for movie in rss_movies: name_tag = str(QName(namespaceIM, 'name')) - name = self.getTextElement( movie, name_tag ) - - releaseDate_tag = str(QName(namespaceIM, 'releaseDate')) + name = self.getTextElement(movie, name_tag) + + releaseDate_tag = str(QName(namespaceIM, 'releaseDate')) releaseDateText = self.getTextElement(movie, releaseDate_tag) year = datetime.datetime.strptime(releaseDateText, '%Y-%m-%dT00:00:00-07:00').strftime("%Y") - + imdb = self.search(name, year) - + if imdb and self.isMinimalMovie(imdb): movies.append(imdb['imdb']) - + except: log.error('Failed loading iTunes rss feed: %s %s', (url, traceback.format_exc())) From 4d0f8eb4ace1bf34e9bdec424ad8bc98223faedf Mon Sep 17 00:00:00 2001 From: Ruud Date: Fri, 4 Jan 2013 20:26:36 +0100 Subject: [PATCH 29/43] Default add top25 to itunes automation --- couchpotato/core/providers/automation/itunes/__init__.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/couchpotato/core/providers/automation/itunes/__init__.py b/couchpotato/core/providers/automation/itunes/__init__.py index a88697e..368c1e4 100644 --- a/couchpotato/core/providers/automation/itunes/__init__.py +++ b/couchpotato/core/providers/automation/itunes/__init__.py @@ -20,12 +20,14 @@ config = [{ { 'name': 'automation_urls_use', 'label': 'Use', + 'default': ',', }, { 'name': 'automation_urls', 'label': 'url', 'type': 'combined', 'combine': ['automation_urls_use', 'automation_urls'], + 'default': 'https://itunes.apple.com/rss/topmovies/limit=25/xml,', }, ], }, From dd9118292d3ddfd7ec6073c0d05e8fa6ec44b919 Mon Sep 17 00:00:00 2001 From: Ruud Date: Sun, 6 Jan 2013 11:20:13 +0100 Subject: [PATCH 30/43] Newznab log error --- couchpotato/core/providers/nzb/newznab/main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/couchpotato/core/providers/nzb/newznab/main.py b/couchpotato/core/providers/nzb/newznab/main.py index c7c27a8..f1f0d48 100644 --- a/couchpotato/core/providers/nzb/newznab/main.py +++ b/couchpotato/core/providers/nzb/newznab/main.py @@ -136,6 +136,6 @@ class Newznab(NZBProvider, RSS): self.limits_reached[host] = time.time() return 'try_next' - log.error('Failed download from %s', (host, traceback.format_exc())) + log.error('Failed download from %s: %s', (host, traceback.format_exc())) return 'try_next' From 383ec7e6f5fe741c21d43e386ef36a546597e7ef Mon Sep 17 00:00:00 2001 From: ikkemaniac Date: Sat, 5 Jan 2013 21:14:52 +0100 Subject: [PATCH 31/43] check for XBMC JSON-RPC version and improve logging info --- couchpotato/core/notifications/xbmc/main.py | 34 ++++++++++++++++++++++++----- 1 file changed, 29 insertions(+), 5 deletions(-) diff --git a/couchpotato/core/notifications/xbmc/main.py b/couchpotato/core/notifications/xbmc/main.py index 96bb2cf..18f5157 100755 --- a/couchpotato/core/notifications/xbmc/main.py +++ b/couchpotato/core/notifications/xbmc/main.py @@ -18,15 +18,39 @@ class XBMC(Notification): hosts = splitString(self.conf('host')) successful = 0 for host in hosts: - response = self.request(host, [ - ('GUI.ShowNotification', {"title":"CouchPotato", "message":message}), - ('VideoLibrary.Scan', {}), - ]) + if listener == "test": + # XBMC JSON-RPC version request + response = self.request(host, [ + ('JSONRPC.Version', {}) + ]) + else: + response = self.request(host, [ + ('GUI.ShowNotification', {"title":"CouchPotato", "message":message}), + ('VideoLibrary.Scan', {}), + ]) try: for result in response: - if result['result'] == "OK": + if (listener != "test" and result['result'] == "OK"): successful += 1 + elif (listener == "test"): + if (type(result['result']['version']).__name__ == 'int'): + # fail, only v2 and v4 return an int object + # v6 (as of XBMC v12(Frodo)) is required to send notifications + xbmc_rpc_version = str(result['result']['version']) + log.error("XBMC JSON-RPC Version: %s ; Notifications only supported for v6 [as of XBMC v12(Frodo)]", xbmc_rpc_version) + return False + + elif (type(result['result']['version']).__name__ == 'dict'): + # success, v6 returns an array object containing + # major, minor and patch number + xbmc_rpc_version = str(result['result']['version']['major']) + xbmc_rpc_version += "." + str(result['result']['version']['minor']) + xbmc_rpc_version += "." + str(result['result']['version']['patch']) + log.debug("XBMC JSON-RPC Version: %s", xbmc_rpc_version) + # ok, XBMC version is supported, send the text message + self.notify(message = message, data = {}, listener = 'test-rpcversion-ok') + return True except: log.error('Failed parsing results: %s', traceback.format_exc()) From f8a46ebe6dfc9771dace3ff2909ccc768f163629 Mon Sep 17 00:00:00 2001 From: ikkemaniac Date: Sat, 5 Jan 2013 21:19:33 +0100 Subject: [PATCH 32/43] clearly state XBMC version dependency for notifications --- couchpotato/core/notifications/xbmc/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/couchpotato/core/notifications/xbmc/__init__.py b/couchpotato/core/notifications/xbmc/__init__.py index 588c5e2..ffff772 100644 --- a/couchpotato/core/notifications/xbmc/__init__.py +++ b/couchpotato/core/notifications/xbmc/__init__.py @@ -10,6 +10,7 @@ config = [{ 'tab': 'notifications', 'name': 'xbmc', 'label': 'XBMC', + 'description': 'v12 (Frodo)', 'options': [ { 'name': 'enabled', From 4779265b434589a9de51bf716b91c6e6a769a7dc Mon Sep 17 00:00:00 2001 From: Ruud Date: Sun, 6 Jan 2013 11:52:05 +0100 Subject: [PATCH 33/43] Change xbmc description --- couchpotato/core/notifications/xbmc/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/couchpotato/core/notifications/xbmc/__init__.py b/couchpotato/core/notifications/xbmc/__init__.py index ffff772..a97dc0a 100644 --- a/couchpotato/core/notifications/xbmc/__init__.py +++ b/couchpotato/core/notifications/xbmc/__init__.py @@ -10,7 +10,7 @@ config = [{ 'tab': 'notifications', 'name': 'xbmc', 'label': 'XBMC', - 'description': 'v12 (Frodo)', + 'description': 'v11 (Dharma) and v12 (Frodo)', 'options': [ { 'name': 'enabled', From 3a2861f72a2dae2df5e33178fa537536884e23d8 Mon Sep 17 00:00:00 2001 From: ikkemaniac Date: Thu, 3 Jan 2013 22:13:25 +0100 Subject: [PATCH 34/43] fix FreeBSD init script -add actual start command -fix verify_couchpotato_pid function, 'ps' command failed if PID var was empty -fix verify_couchpotato_pid usage, acutally use the return of verify_couchpotato_pid in the 'stop' routine --- init/freebsd | 23 ++++++++++++++++------- 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/init/freebsd b/init/freebsd index 1171440..662b09f 100755 --- a/init/freebsd +++ b/init/freebsd @@ -49,6 +49,7 @@ stop_cmd="${name}_stop" command="/usr/sbin/daemon" command_args="-f -p ${couchpotato_pid} python ${couchpotato_dir}/CouchPotato.py ${couchpotato_flags}" +start_cmd="${command} ${command_args}" # Check for wget and refuse to start without it. if [ ! -x "${WGET}" ]; then @@ -65,8 +66,11 @@ fi verify_couchpotato_pid() { # Make sure the pid corresponds to the CouchPotato process. pid=`cat ${couchpotato_pid} 2>/dev/null` - ps -p ${pid} | grep -q "python ${couchpotato_dir}/CouchPotato.py" - return $? + if [ -n "${pid}" ]; then + ps -p ${pid} | grep -q "python ${couchpotato_dir}/CouchPotato.py" + return $? + fi + return 1 } # Try to stop CouchPotato cleanly by calling shutdown over http. @@ -75,12 +79,17 @@ couchpotato_stop() { echo "CouchPotato's settings file does not exist. Try starting CouchPotato, as this should create the file." exit 1 fi - echo "Stopping $name" verify_couchpotato_pid - ${WGET} -O - -q "http://${HOST}:${PORT}/api/${CPAPI}/app.shutdown/" >/dev/null - if [ -n "${pid}" ]; then - wait_for_pids ${pid} - echo "Stopped" + if [ "${?}" -eq 0 ]; then + echo "Stopping $name" + ${WGET} -O - -q "http://${HOST}:${PORT}/api/${CPAPI}/app.shutdown/" >/dev/null + if [ -n "${pid}" ]; then + wait_for_pids ${pid} + echo "Stopped" + fi + else + echo "$name not running?" + exit 1 fi } From 7b4924dd7a8670e2cea3a462edb1a60e640321e1 Mon Sep 17 00:00:00 2001 From: ikkemaniac Date: Sat, 5 Jan 2013 16:09:52 +0100 Subject: [PATCH 35/43] Don't influence the PATH variable in FreeBSD rc script Don't prepend the PATH variable, it's ugly, unwanted and unnecessary. Call binaries with their full path. --- init/freebsd | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/init/freebsd b/init/freebsd index 662b09f..bb9581d 100755 --- a/init/freebsd +++ b/init/freebsd @@ -25,9 +25,6 @@ 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"} @@ -38,17 +35,12 @@ load_rc_config ${name} : ${couchpotato_conf:="${couchpotato_dir}/data/settings.conf"} 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}"|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" stop_cmd="${name}_stop" command="/usr/sbin/daemon" -command_args="-f -p ${couchpotato_pid} python ${couchpotato_dir}/CouchPotato.py ${couchpotato_flags}" +command_args="-f -p ${couchpotato_pid} /usr/local/bin/python ${couchpotato_dir}/CouchPotato.py ${couchpotato_flags}" start_cmd="${command} ${command_args}" # Check for wget and refuse to start without it. From acc8ed2092b4e03ac07d620541451cb09caa17ce Mon Sep 17 00:00:00 2001 From: ikkemaniac Date: Sat, 5 Jan 2013 16:18:41 +0100 Subject: [PATCH 36/43] Acutally use config_file variable --- init/freebsd | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/init/freebsd b/init/freebsd index bb9581d..aaeaedd 100755 --- a/init/freebsd +++ b/init/freebsd @@ -40,7 +40,7 @@ status_cmd="${name}_status" stop_cmd="${name}_stop" command="/usr/sbin/daemon" -command_args="-f -p ${couchpotato_pid} /usr/local/bin/python ${couchpotato_dir}/CouchPotato.py ${couchpotato_flags}" +command_args="-f -p ${couchpotato_pid} /usr/local/bin/python ${couchpotato_dir}/CouchPotato.py --config_file ${couchpotato_conf} ${couchpotato_flags}" start_cmd="${command} ${command_args}" # Check for wget and refuse to start without it. From 1993c2b6cb1996dcf19aab2c9cee28681f5d7008 Mon Sep 17 00:00:00 2001 From: ikkemaniac Date: Sat, 5 Jan 2013 16:43:47 +0100 Subject: [PATCH 37/43] Redo FreeBSD init script completely. Use rc.subr functions and proper rc.conf variables. --- init/freebsd | 112 ++++++++++++++++++----------------------------------------- 1 file changed, 34 insertions(+), 78 deletions(-) mode change 100755 => 100644 init/freebsd diff --git a/init/freebsd b/init/freebsd old mode 100755 new mode 100644 index aaeaedd..20de964 --- a/init/freebsd +++ b/init/freebsd @@ -1,93 +1,49 @@ #!/bin/sh # # PROVIDE: couchpotato -# REQUIRE: sabnzbd +# REQUIRE: DAEMON sabnzb transmission # KEYWORD: shutdown -# -# Add the following lines to /etc/rc.conf.local or /etc/rc.conf -# to enable this service: -# -# couchpotato_enable (bool): Set to NO by default. -# Set it to YES to enable it. -# couchpotato_user: The user account CouchPotato daemon runs as what -# you want it to be. It uses '_sabnzbd' user by -# default. Do not sets it as empty or it will run -# as root. -# couchpotato_dir: Directory where CouchPotato lives. -# Default: /usr/local/couchpotato -# couchpotato_chdir: Change to this directory before running CouchPotato. -# Default is same as couchpotato_dir. -# couchpotato_pid: The name of the pidfile to create. -# Default is couchpotato.pid in couchpotato_dir. + +# Add the following lines to /etc/rc.conf to enable couchpotato: +# couchpotato_enable: Set to NO by default. Set it to YES to enable it. +# couchpotato_user: The user account CouchPotato daemon runs as what +# you want it to be. +# couchpotato_dir: Directory where CouchPotato lives. +# Default: /usr/local/CouchPotatoServer +# couchpotato_datadir: Directory where CouchPotato user data lives. +# Default: $couchpotato_dir/data +# couchpotato_conf: Directory where CouchPotato user data lives. +# Default: $couchpotato_datadir/settings.conf +# couchpotato_pid: Full path to PID file. +# Default: $couchpotato_datadir/couchpotato.pid +# couchpotato_flags: Set additonal flags as needed. . /etc/rc.subr name="couchpotato" -rcvar=${name}_enable +rcvar=couchpotato_enable load_rc_config ${name} -: ${couchpotato_enable:="NO"} -: ${couchpotato_user:="_sabnzbd"} -: ${couchpotato_dir:="/usr/local/couchpotato"} -: ${couchpotato_chdir:="${couchpotato_dir}"} -: ${couchpotato_pid:="${couchpotato_dir}/couchpotato.pid"} -: ${couchpotato_conf:="${couchpotato_dir}/data/settings.conf"} - -WGET="/usr/local/bin/wget" # You need wget for this script to safely shutdown CouchPotato. - -status_cmd="${name}_status" -stop_cmd="${name}_stop" - -command="/usr/sbin/daemon" -command_args="-f -p ${couchpotato_pid} /usr/local/bin/python ${couchpotato_dir}/CouchPotato.py --config_file ${couchpotato_conf} ${couchpotato_flags}" -start_cmd="${command} ${command_args}" - -# Check for wget and refuse to start without it. -if [ ! -x "${WGET}" ]; then - warn "couchpotato not started: You need wget to safely shut down CouchPotato." - exit 1 +: ${couchpotato_enable:=NO} +: ${couchpotato_user:=} #default is root +: ${couchpotato_dir:=/usr/local/CouchPotatoServer} +: ${couchpotato_datadir:=${couchpotato_dir}/data} +: ${couchpotato_conf:=} #default is datadir/settings.conf +: ${couchpotato_pid:=} #default is datadir/couchpotato.pid +: ${couchpotato_flags:=} + +command="${couchpotato_dir}/CouchPotato.py" +command_interpreter="/usr/local/bin/python" +command_args="--daemon --data_dir ${couchpotato_datadir}" + +# append optional flags +if [ -n "${couchpotato_pid}" ]; then + pidfile=${couchpotato_pid} + couchpotato_flags="${couchpotato_flags} --pid_file ${couchpotato_pid}" fi - -# Ensure user is root when running this script. -if [ `id -u` != "0" ]; then - echo "Oops, you should be root before running this!" - exit 1 +if [ -n "${couchpotato_conf}" ]; then + couchpotato_flags="${couchpotato_flags} --config_file ${couchpotato_conf}" fi -verify_couchpotato_pid() { - # Make sure the pid corresponds to the CouchPotato process. - pid=`cat ${couchpotato_pid} 2>/dev/null` - if [ -n "${pid}" ]; then - ps -p ${pid} | grep -q "python ${couchpotato_dir}/CouchPotato.py" - return $? - fi - return 1 -} - -# Try to stop CouchPotato cleanly by calling shutdown over http. -couchpotato_stop() { - if [ ! -e "${couchpotato_conf}" ]; then - echo "CouchPotato's settings file does not exist. Try starting CouchPotato, as this should create the file." - exit 1 - fi - verify_couchpotato_pid - if [ "${?}" -eq 0 ]; then - echo "Stopping $name" - ${WGET} -O - -q "http://${HOST}:${PORT}/api/${CPAPI}/app.shutdown/" >/dev/null - if [ -n "${pid}" ]; then - wait_for_pids ${pid} - echo "Stopped" - fi - else - echo "$name not running?" - exit 1 - fi -} - -couchpotato_status() { - verify_couchpotato_pid && echo "$name is running as ${pid}" || echo "$name is not running" -} - run_rc_command "$1" - From 9bd5688fb99cac057bc9e267a6af11b5b8ea8e2e Mon Sep 17 00:00:00 2001 From: ikkemaniac Date: Sat, 5 Jan 2013 17:05:02 +0100 Subject: [PATCH 38/43] Remove services that are not required for couchpotato to run --- init/freebsd | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/init/freebsd b/init/freebsd index 20de964..d389933 100644 --- a/init/freebsd +++ b/init/freebsd @@ -1,7 +1,7 @@ #!/bin/sh # # PROVIDE: couchpotato -# REQUIRE: DAEMON sabnzb transmission +# REQUIRE: DAEMON # KEYWORD: shutdown # Add the following lines to /etc/rc.conf to enable couchpotato: From c5cae5ab9bb98be3a4d8e82168f221ac9fe10d90 Mon Sep 17 00:00:00 2001 From: ikkemaniac Date: Sun, 6 Jan 2013 21:02:26 +0100 Subject: [PATCH 39/43] add XBMC v11 Eden notifications support This is my approach on working with Eden, maybe a little late since Frodo is almost released, but better late then never. - First detect the JSON-RPC version XBMC is running (once per boot of CouchPotatoServer on the first notification, except for sending test message then the JSON version is always checked). - Set a variable indicating whether or not to use JSON (or normal http). - If JSON should be used, proceed as before this commit. - If normal-http should be used, use 'notifyXBMCnoJSON' func - 'notifyXBMCnoJSON' just opens a specific XBMC api url, unfortunately importing urllib for this was necessary to escape the message strings. TODO: support multiple XBMC hosts, right now the last host in the hosts array will set the 'useJSONnotifications' var. Conflicts: couchpotato/core/notifications/xbmc/main.py Conflicts: couchpotato/core/notifications/xbmc/main.py --- couchpotato/core/notifications/xbmc/main.py | 153 +++++++++++++++++++++++----- 1 file changed, 127 insertions(+), 26 deletions(-) diff --git a/couchpotato/core/notifications/xbmc/main.py b/couchpotato/core/notifications/xbmc/main.py index 18f5157..412e4eb 100755 --- a/couchpotato/core/notifications/xbmc/main.py +++ b/couchpotato/core/notifications/xbmc/main.py @@ -4,6 +4,7 @@ from couchpotato.core.notifications.base import Notification from flask.helpers import json import base64 import traceback +import urllib log = CPLog(__name__) @@ -11,51 +12,151 @@ log = CPLog(__name__) class XBMC(Notification): listen_to = ['renamer.after'] + firstRun = True + useJSONnotifications = True def notify(self, message = '', data = {}, listener = None): if self.isDisabled(): return hosts = splitString(self.conf('host')) + if self.firstRun or listener == "test" : return self.getXBMCJSONversion(hosts, message=message ) + successful = 0 for host in hosts: - if listener == "test": - # XBMC JSON-RPC version request - response = self.request(host, [ - ('JSONRPC.Version', {}) - ]) - else: + if self.useJSONnotifications: response = self.request(host, [ - ('GUI.ShowNotification', {"title":"CouchPotato", "message":message}), + ('GUI.ShowNotification', {"title":self.default_title, "message":message}), ('VideoLibrary.Scan', {}), ]) + else: + response = self.notifyXBMCnoJSON(host, {'title':self.default_title,'message':message}) + response += self.request(host, [('VideoLibrary.Scan', {})]) try: for result in response: - if (listener != "test" and result['result'] == "OK"): + if (result.get('result') and result['result'] == "OK"): successful += 1 - elif (listener == "test"): - if (type(result['result']['version']).__name__ == 'int'): - # fail, only v2 and v4 return an int object - # v6 (as of XBMC v12(Frodo)) is required to send notifications - xbmc_rpc_version = str(result['result']['version']) - log.error("XBMC JSON-RPC Version: %s ; Notifications only supported for v6 [as of XBMC v12(Frodo)]", xbmc_rpc_version) - return False - - elif (type(result['result']['version']).__name__ == 'dict'): - # success, v6 returns an array object containing - # major, minor and patch number - xbmc_rpc_version = str(result['result']['version']['major']) - xbmc_rpc_version += "." + str(result['result']['version']['minor']) - xbmc_rpc_version += "." + str(result['result']['version']['patch']) - log.debug("XBMC JSON-RPC Version: %s", xbmc_rpc_version) - # ok, XBMC version is supported, send the text message - self.notify(message = message, data = {}, listener = 'test-rpcversion-ok') - return True + elif (result.get('error')): + log.error("XBMC error; %s: %s (%s)", (result['id'], result['error']['message'], result['error']['code'])) + except: log.error('Failed parsing results: %s', traceback.format_exc()) return successful == len(hosts) * 2 + # TODO: implement multiple hosts support, for now the last host of the 'hosts' array + # sets 'useJSONnotifications' + def getXBMCJSONversion(self, hosts, message=''): + + success = 0 + for host in hosts: + # XBMC JSON-RPC version request + response = self.request(host, [ + ('JSONRPC.Version', {}) + ]) + for result in response: + if (result.get('result') and type(result['result']['version']).__name__ == 'int'): + # only v2 and v4 return an int object + # v6 (as of XBMC v12(Frodo)) is required to send notifications + xbmc_rpc_version = str(result['result']['version']) + + log.debug("XBMC JSON-RPC Version: %s ; Notifications by JSON-RPC only supported for v6 [as of XBMC v12(Frodo)]", xbmc_rpc_version) + + # disable JSON use + self.useJSONnotifications = False + + # send the text message + resp = self.notifyXBMCnoJSON(host, {'title':self.default_title,'message':message}) + for result in resp: + if (result.get('result') and result['result'] == "OK"): + log.debug("Message delivered successfully!") + success = True + break + elif (result.get('error')): + log.error("XBMC error; %s: %s (%s)", (result['id'], result['error']['message'], result['error']['code'])) + break + + elif (result.get('result') and type(result['result']['version']).__name__ == 'dict'): + # XBMC JSON-RPC v6 returns an array object containing + # major, minor and patch number + xbmc_rpc_version = str(result['result']['version']['major']) + xbmc_rpc_version += "." + str(result['result']['version']['minor']) + xbmc_rpc_version += "." + str(result['result']['version']['patch']) + + log.debug("XBMC JSON-RPC Version: %s", xbmc_rpc_version) + + # ok, XBMC version is supported + self.useJSONnotifications = True + + # send the text message + resp = self.request(host, [('GUI.ShowNotification', {"title":self.default_title, "message":message})]) + for result in resp: + if (result.get('result') and result['result'] == "OK"): + log.debug("Message delivered successfully!") + success = True + break + elif (result.get('error')): + log.error("XBMC error; %s: %s (%s)", (result['id'], result['error']['message'], result['error']['code'])) + break + + # error getting version info (we do have contact with XBMC though) + elif (result.get('error')): + log.error("XBMC error; %s: %s (%s)", (result['id'], result['error']['message'], result['error']['code'])) + + # set boolean so we only run this once after boot + # in func notify() ignored for 'test' messages + self.firstRun = False + + log.debug("use JSON notifications: %s ", self.useJSONnotifications) + + return success + + def notifyXBMCnoJSON(self, host, data): + + 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'])) + server += cmd + + # I have no idea what to set to, just tried text/plain and seems to be working :) + headers = { + 'Content-Type': 'text/plain', + } + + # authentication support + if self.conf('password'): + base64string = base64.encodestring('%s:%s' % (self.conf('username'), self.conf('password'))).replace('\n', '') + headers['Authorization'] = 'Basic %s' % base64string + + try: + log.debug('Sending non-JSON-type request to %s: %s', (host, data)) + + # response wil either be 'OK': + # + #
  • OK + # + # + # or 'Error': + # + #
  • Error: + # + # + response = self.urlopen(server, headers = headers) + + if "OK" in response: + log.debug('Returned from non-JSON-type request %s: %s', (host, response)) + # manually fake expected response array + return [{"result": "OK"}] + else: + log.error('Returned from non-JSON-type request %s: %s', (host, response)) + # manually fake expected response array + return [{"result": "Error"}] + + except: + log.error('Failed sending non-JSON-type request to XBMC: %s', traceback.format_exc()) + return [{"result": "Error"}] + def request(self, host, requests): server = 'http://%s/jsonrpc' % host From 36fee6984303feaf28df0d6fd06de1fe5d9280a3 Mon Sep 17 00:00:00 2001 From: Ruud Date: Sun, 6 Jan 2013 23:06:38 +0100 Subject: [PATCH 40/43] XBMC notifier for Frodo & Eden --- couchpotato/core/notifications/xbmc/__init__.py | 2 +- couchpotato/core/notifications/xbmc/main.py | 148 ++++++++++++------------ 2 files changed, 73 insertions(+), 77 deletions(-) diff --git a/couchpotato/core/notifications/xbmc/__init__.py b/couchpotato/core/notifications/xbmc/__init__.py index a97dc0a..ee2b4cc 100644 --- a/couchpotato/core/notifications/xbmc/__init__.py +++ b/couchpotato/core/notifications/xbmc/__init__.py @@ -10,7 +10,7 @@ config = [{ 'tab': 'notifications', 'name': 'xbmc', 'label': 'XBMC', - 'description': 'v11 (Dharma) and v12 (Frodo)', + 'description': 'v11 (Eden) and v12 (Frodo)', 'options': [ { 'name': 'enabled', diff --git a/couchpotato/core/notifications/xbmc/main.py b/couchpotato/core/notifications/xbmc/main.py index 412e4eb..eef9469 100755 --- a/couchpotato/core/notifications/xbmc/main.py +++ b/couchpotato/core/notifications/xbmc/main.py @@ -12,102 +12,98 @@ log = CPLog(__name__) class XBMC(Notification): listen_to = ['renamer.after'] - firstRun = True - useJSONnotifications = True + use_json_notifications = {} def notify(self, message = '', data = {}, listener = None): if self.isDisabled(): return hosts = splitString(self.conf('host')) - if self.firstRun or listener == "test" : return self.getXBMCJSONversion(hosts, message=message ) successful = 0 for host in hosts: - if self.useJSONnotifications: + + if self.use_json_notifications.get(host) is None: + self.getXBMCJSONversion(host, message = message) + + 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}), ('VideoLibrary.Scan', {}), ]) else: - response = self.notifyXBMCnoJSON(host, {'title':self.default_title,'message':message}) + response = self.notifyXBMCnoJSON(host, {'title':self.default_title, 'message':message}) response += self.request(host, [('VideoLibrary.Scan', {})]) try: for result in response: - if (result.get('result') and result['result'] == "OK"): + if (result.get('result') and result['result'] == 'OK'): successful += 1 elif (result.get('error')): - log.error("XBMC error; %s: %s (%s)", (result['id'], result['error']['message'], result['error']['code'])) + log.error('XBMC error; %s: %s (%s)', (result['id'], result['error']['message'], result['error']['code'])) except: log.error('Failed parsing results: %s', traceback.format_exc()) return successful == len(hosts) * 2 - # TODO: implement multiple hosts support, for now the last host of the 'hosts' array - # sets 'useJSONnotifications' - def getXBMCJSONversion(self, hosts, message=''): + def getXBMCJSONversion(self, host, message = ''): - success = 0 - for host in hosts: - # XBMC JSON-RPC version request - response = self.request(host, [ - ('JSONRPC.Version', {}) - ]) - for result in response: - if (result.get('result') and type(result['result']['version']).__name__ == 'int'): - # only v2 and v4 return an int object - # v6 (as of XBMC v12(Frodo)) is required to send notifications - xbmc_rpc_version = str(result['result']['version']) - - log.debug("XBMC JSON-RPC Version: %s ; Notifications by JSON-RPC only supported for v6 [as of XBMC v12(Frodo)]", xbmc_rpc_version) - - # disable JSON use - self.useJSONnotifications = False - - # send the text message - resp = self.notifyXBMCnoJSON(host, {'title':self.default_title,'message':message}) - for result in resp: - if (result.get('result') and result['result'] == "OK"): - log.debug("Message delivered successfully!") - success = True - break - elif (result.get('error')): - log.error("XBMC error; %s: %s (%s)", (result['id'], result['error']['message'], result['error']['code'])) - break - - elif (result.get('result') and type(result['result']['version']).__name__ == 'dict'): - # XBMC JSON-RPC v6 returns an array object containing - # major, minor and patch number - xbmc_rpc_version = str(result['result']['version']['major']) - xbmc_rpc_version += "." + str(result['result']['version']['minor']) - xbmc_rpc_version += "." + str(result['result']['version']['patch']) - - log.debug("XBMC JSON-RPC Version: %s", xbmc_rpc_version) - - # ok, XBMC version is supported - self.useJSONnotifications = True - - # send the text message - resp = self.request(host, [('GUI.ShowNotification', {"title":self.default_title, "message":message})]) - for result in resp: - if (result.get('result') and result['result'] == "OK"): - log.debug("Message delivered successfully!") - success = True - break - elif (result.get('error')): - log.error("XBMC error; %s: %s (%s)", (result['id'], result['error']['message'], result['error']['code'])) - break - - # error getting version info (we do have contact with XBMC though) - elif (result.get('error')): - log.error("XBMC error; %s: %s (%s)", (result['id'], result['error']['message'], result['error']['code'])) - - # set boolean so we only run this once after boot - # in func notify() ignored for 'test' messages - self.firstRun = False - - log.debug("use JSON notifications: %s ", self.useJSONnotifications) + success = False + + # XBMC JSON-RPC version request + response = self.request(host, [ + ('JSONRPC.Version', {}) + ]) + for result in response: + if (result.get('result') and type(result['result']['version']).__name__ == 'int'): + # only v2 and v4 return an int object + # v6 (as of XBMC v12(Frodo)) is required to send notifications + xbmc_rpc_version = str(result['result']['version']) + + log.debug('XBMC JSON-RPC Version: %s ; Notifications by JSON-RPC only supported for v6 [as of XBMC v12(Frodo)]', xbmc_rpc_version) + + # disable JSON use + self.use_json_notifications[host] = False + + # send the text message + resp = self.notifyXBMCnoJSON(host, {'title':self.default_title, 'message':message}) + for result in resp: + if (result.get('result') and result['result'] == 'OK'): + log.debug('Message delivered successfully!') + success = True + break + elif (result.get('error')): + log.error('XBMC error; %s: %s (%s)', (result['id'], result['error']['message'], result['error']['code'])) + break + + elif (result.get('result') and type(result['result']['version']).__name__ == 'dict'): + # XBMC JSON-RPC v6 returns an array object containing + # major, minor and patch number + xbmc_rpc_version = str(result['result']['version']['major']) + xbmc_rpc_version += '.' + str(result['result']['version']['minor']) + xbmc_rpc_version += '.' + str(result['result']['version']['patch']) + + log.debug('XBMC JSON-RPC Version: %s', xbmc_rpc_version) + + # ok, XBMC version is supported + self.use_json_notifications[host] = True + + # send the text message + resp = self.request(host, [('GUI.ShowNotification', {'title':self.default_title, 'message':message})]) + for result in resp: + if (result.get('result') and result['result'] == 'OK'): + log.debug('Message delivered successfully!') + success = True + break + elif (result.get('error')): + log.error('XBMC error; %s: %s (%s)', (result['id'], result['error']['message'], result['error']['code'])) + break + + # error getting version info (we do have contact with XBMC though) + elif (result.get('error')): + log.error('XBMC error; %s: %s (%s)', (result['id'], result['error']['message'], result['error']['code'])) + + log.debug('Use JSON notifications: %s ', self.use_json_notifications) return success @@ -116,7 +112,7 @@ 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'])) + cmd = "xbmcHttp?command=ExecBuiltIn(Notification('%s','%s'))" % (urllib.quote(data['title']), urllib.quote(data['message'])) server += cmd # I have no idea what to set to, just tried text/plain and seems to be working :) @@ -144,18 +140,18 @@ class XBMC(Notification): # response = self.urlopen(server, headers = headers) - if "OK" in response: + if 'OK' in response: log.debug('Returned from non-JSON-type request %s: %s', (host, response)) # manually fake expected response array - return [{"result": "OK"}] + return [{'result': 'OK'}] else: log.error('Returned from non-JSON-type request %s: %s', (host, response)) # manually fake expected response array - return [{"result": "Error"}] + return [{'result': 'Error'}] except: log.error('Failed sending non-JSON-type request to XBMC: %s', traceback.format_exc()) - return [{"result": "Error"}] + return [{'result': 'Error'}] def request(self, host, requests): server = 'http://%s/jsonrpc' % host From ca08287cff0b9a0f1704b281da3995bb5e9af21f Mon Sep 17 00:00:00 2001 From: Ruud Date: Mon, 7 Jan 2013 20:54:21 +0100 Subject: [PATCH 41/43] Ignore Growl timeout. fixes #1240 --- couchpotato/core/notifications/growl/main.py | 7 +- libs/gntp/__init__.py | 100 +++++++++++++++--------- libs/gntp/notifier.py | 109 ++++++++++++++++----------- 3 files changed, 131 insertions(+), 85 deletions(-) diff --git a/couchpotato/core/notifications/growl/main.py b/couchpotato/core/notifications/growl/main.py index 4aa1c31..7f1398d 100644 --- a/couchpotato/core/notifications/growl/main.py +++ b/couchpotato/core/notifications/growl/main.py @@ -37,8 +37,11 @@ class Growl(Notification): ) self.growl.register() self.registered = True - except: - log.error('Failed register of growl: %s', traceback.format_exc()) + except Exception, e: + if 'timed out' in str(e): + self.registered = True + else: + log.error('Failed register of growl: %s', traceback.format_exc()) def notify(self, message = '', data = {}, listener = None): if self.isDisabled(): return diff --git a/libs/gntp/__init__.py b/libs/gntp/__init__.py index 7e0d60f..eabbfa4 100755 --- a/libs/gntp/__init__.py +++ b/libs/gntp/__init__.py @@ -1,8 +1,9 @@ -import hashlib import re +import hashlib import time +import StringIO -__version__ = '0.6' +__version__ = '0.8' #GNTP/ [:][ :.] GNTP_INFO_LINE = re.compile( @@ -19,7 +20,7 @@ GNTP_INFO_LINE_SHORT = re.compile( GNTP_HEADER = re.compile('([\w-]+):(.+)') -GNTP_EOL = u'\r\n' +GNTP_EOL = '\r\n' class BaseError(Exception): @@ -43,6 +44,14 @@ class UnsupportedError(BaseError): errordesc = 'Currently unsupported by gntp.py' +class _GNTPBuffer(StringIO.StringIO): + """GNTP Buffer class""" + def writefmt(self, message = "", *args): + """Shortcut function for writing GNTP Headers""" + self.write((message % args).encode('utf8', 'replace')) + self.write(GNTP_EOL) + + class _GNTPBase(object): """Base initilization @@ -206,8 +215,8 @@ class _GNTPBase(object): if not match: continue - key = match.group(1).strip() - val = match.group(2).strip() + key = unicode(match.group(1).strip(), 'utf8', 'replace') + val = unicode(match.group(2).strip(), 'utf8', 'replace') dict[key] = val return dict @@ -217,6 +226,15 @@ class _GNTPBase(object): else: self.headers[key] = unicode('%s' % value, 'utf8', 'replace') + def add_resource(self, data): + """Add binary resource + + :param string data: Binary Data + """ + identifier = hashlib.md5(data).hexdigest() + self.resources[identifier] = data + return 'x-growl-resource://%s' % identifier + def decode(self, data, password = None): """Decode GNTP Message @@ -229,19 +247,30 @@ class _GNTPBase(object): self.headers = self._parse_dict(parts[0]) def encode(self): - """Encode a GNTP Message + """Encode a generic GNTP Message - :return string: Encoded GNTP Message ready to be sent + :return string: GNTP Message ready to be sent """ - self.validate() - message = self._format_info() + GNTP_EOL + buffer = _GNTPBuffer() + + buffer.writefmt(self._format_info()) + #Headers for k, v in self.headers.iteritems(): - message += u'%s: %s%s' % (k, v, GNTP_EOL) + buffer.writefmt('%s: %s', k, v) + buffer.writefmt() - message += GNTP_EOL - return message + #Resources + for resource, data in self.resources.iteritems(): + buffer.writefmt('Identifier: %s', resource) + buffer.writefmt('Length: %d', len(data)) + buffer.writefmt() + buffer.write(data) + buffer.writefmt() + buffer.writefmt() + + return buffer.getvalue() class GNTPRegister(_GNTPBase): @@ -290,7 +319,7 @@ class GNTPRegister(_GNTPBase): for i, part in enumerate(parts): if i == 0: - continue # Skip Header + continue # Skip Header if part.strip() == '': continue notice = self._parse_dict(part) @@ -319,22 +348,33 @@ class GNTPRegister(_GNTPBase): :return string: Encoded GNTP Registration message """ - self.validate() - message = self._format_info() + GNTP_EOL + buffer = _GNTPBuffer() + + buffer.writefmt(self._format_info()) + #Headers for k, v in self.headers.iteritems(): - message += u'%s: %s%s' % (k, v, GNTP_EOL) + buffer.writefmt('%s: %s', k, v) + buffer.writefmt() #Notifications if len(self.notifications) > 0: for notice in self.notifications: - message += GNTP_EOL for k, v in notice.iteritems(): - message += u'%s: %s%s' % (k, v, GNTP_EOL) + buffer.writefmt('%s: %s', k, v) + buffer.writefmt() + + #Resources + for resource, data in self.resources.iteritems(): + buffer.writefmt('Identifier: %s', resource) + buffer.writefmt('Length: %d', len(data)) + buffer.writefmt() + buffer.write(data) + buffer.writefmt() + buffer.writefmt() - message += GNTP_EOL - return message + return buffer.getvalue() class GNTPNotice(_GNTPBase): @@ -379,7 +419,7 @@ class GNTPNotice(_GNTPBase): for i, part in enumerate(parts): if i == 0: - continue # Skip Header + continue # Skip Header if part.strip() == '': continue notice = self._parse_dict(part) @@ -388,21 +428,6 @@ class GNTPNotice(_GNTPBase): #open('notice.png','wblol').write(notice['Data']) self.resources[notice.get('Identifier')] = notice - def encode(self): - """Encode a GNTP Notification Message - - :return string: GNTP Notification Message ready to be sent - """ - self.validate() - - message = self._format_info() + GNTP_EOL - #Headers - for k, v in self.headers.iteritems(): - message += u'%s: %s%s' % (k, v, GNTP_EOL) - - message += GNTP_EOL - return message - class GNTPSubscribe(_GNTPBase): """Represents a GNTP Subscribe Command @@ -457,7 +482,8 @@ class GNTPError(_GNTPBase): self.add_header('Error-Description', errordesc) def error(self): - return self.headers['Error-Code'], self.headers['Error-Description'] + return (self.headers.get('Error-Code', None), + self.headers.get('Error-Description', None)) def parse_gntp(data, password = None): diff --git a/libs/gntp/notifier.py b/libs/gntp/notifier.py index 300e4a6..539dae2 100755 --- a/libs/gntp/notifier.py +++ b/libs/gntp/notifier.py @@ -22,43 +22,6 @@ __all__ = [ logger = logging.getLogger(__name__) -def mini(description, applicationName = 'PythonMini', noteType = "Message", - title = "Mini Message", applicationIcon = None, hostname = 'localhost', - password = None, port = 23053, sticky = False, priority = None, - callback = None): - """Single notification function - - Simple notification function in one line. Has only one required parameter - and attempts to use reasonable defaults for everything else - :param string description: Notification message - - .. warning:: - For now, only URL callbacks are supported. In the future, the - callback argument will also support a function - """ - growl = GrowlNotifier( - applicationName = applicationName, - notifications = [noteType], - defaultNotifications = [noteType], - hostname = hostname, - password = password, - port = port, - ) - result = growl.register() - if result is not True: - return result - - return growl.notify( - noteType = noteType, - title = title, - description = description, - icon = applicationIcon, - sticky = sticky, - priority = priority, - callback = callback, - ) - - class GrowlNotifier(object): """Helper class to simplfy sending Growl messages @@ -93,10 +56,12 @@ class GrowlNotifier(object): def _checkIcon(self, data): ''' Check the icon to see if it's valid - @param data: - @todo Consider checking for a valid URL + + If it's a simple URL icon, then we return True. If it's a data icon + then we return False ''' - return data + logger.info('Checking icon') + return data.startswith('http') def register(self): """Send GNTP Registration @@ -112,7 +77,11 @@ class GrowlNotifier(object): enabled = notification in self.defaultNotifications register.add_notification(notification, enabled) if self.applicationIcon: - register.add_header('Application-Icon', self.applicationIcon) + if self._checkIcon(self.applicationIcon): + register.add_header('Application-Icon', self.applicationIcon) + else: + id = register.add_resource(self.applicationIcon) + register.add_header('Application-Icon', id) if self.password: register.set_password(self.password, self.passwordHash) self.add_origin_info(register) @@ -120,7 +89,7 @@ class GrowlNotifier(object): return self._send('register', register) def notify(self, noteType, title, description, icon = None, sticky = False, - priority = None, callback = None): + priority = None, callback = None, identifier = None): """Send a GNTP notifications .. warning:: @@ -151,11 +120,18 @@ class GrowlNotifier(object): if priority: notice.add_header('Notification-Priority', priority) if icon: - notice.add_header('Notification-Icon', self._checkIcon(icon)) + if self._checkIcon(icon): + notice.add_header('Notification-Icon', icon) + else: + id = notice.add_resource(icon) + notice.add_header('Notification-Icon', id) + if description: notice.add_header('Notification-Text', description) if callback: notice.add_header('Notification-Callback-Target', callback) + if identifier: + notice.add_header('Notification-Coalescing-ID', identifier) self.add_origin_info(notice) self.notify_hook(notice) @@ -193,9 +169,10 @@ class GrowlNotifier(object): def subscribe_hook(self, packet): pass - def _send(self, type, packet): + def _send(self, messagetype, packet): """Send the GNTP Packet""" + packet.validate() data = packet.encode() logger.debug('To : %s:%s <%s>\n%s', self.hostname, self.port, packet.__class__, data) @@ -203,7 +180,7 @@ class GrowlNotifier(object): s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) s.settimeout(self.socketTimeout) s.connect((self.hostname, self.port)) - s.send(data.encode('utf8', 'replace')) + s.send(data) recv_data = s.recv(1024) while not recv_data.endswith("\r\n\r\n"): recv_data += s.recv(1024) @@ -212,11 +189,51 @@ class GrowlNotifier(object): logger.debug('From : %s:%s <%s>\n%s', self.hostname, self.port, response.__class__, response) - if response.info['messagetype'] == '-OK': + if type(response) == gntp.GNTPOK: return True logger.error('Invalid response: %s', response.error()) return response.error() + +def mini(description, applicationName = 'PythonMini', noteType = "Message", + title = "Mini Message", applicationIcon = None, hostname = 'localhost', + password = None, port = 23053, sticky = False, priority = None, + callback = None, notificationIcon = None, identifier = None, + notifierFactory = GrowlNotifier): + """Single notification function + + Simple notification function in one line. Has only one required parameter + and attempts to use reasonable defaults for everything else + :param string description: Notification message + + .. warning:: + For now, only URL callbacks are supported. In the future, the + callback argument will also support a function + """ + growl = notifierFactory( + applicationName = applicationName, + notifications = [noteType], + defaultNotifications = [noteType], + applicationIcon = applicationIcon, + hostname = hostname, + password = password, + port = port, + ) + result = growl.register() + if result is not True: + return result + + return growl.notify( + noteType = noteType, + title = title, + description = description, + icon = notificationIcon, + sticky = sticky, + priority = priority, + callback = callback, + identifier = identifier, + ) + if __name__ == '__main__': # If we're running this module directly we're likely running it as a test # so extra debugging is useful From 4d32b0b16de817231bf3a0213edc5659eb02dc26 Mon Sep 17 00:00:00 2001 From: Ruud Date: Mon, 7 Jan 2013 22:21:44 +0100 Subject: [PATCH 42/43] Use FTDWorld temp api. closes #1243 --- couchpotato/core/providers/nzb/ftdworld/main.py | 33 +++++++++---------------- 1 file changed, 11 insertions(+), 22 deletions(-) diff --git a/couchpotato/core/providers/nzb/ftdworld/main.py b/couchpotato/core/providers/nzb/ftdworld/main.py index 81ac8ed..9a3a9f3 100644 --- a/couchpotato/core/providers/nzb/ftdworld/main.py +++ b/couchpotato/core/providers/nzb/ftdworld/main.py @@ -1,12 +1,10 @@ -from bs4 import BeautifulSoup from couchpotato.core.helpers.encoding import toUnicode, 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 from dateutil.parser import parse -import re -import time +import traceback log = CPLog(__name__) @@ -14,7 +12,7 @@ log = CPLog(__name__) class FTDWorld(NZBProvider): urls = { - 'search': 'http://ftdworld.net/category.php?%s', + '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', @@ -25,7 +23,7 @@ class FTDWorld(NZBProvider): cat_ids = [ ([4, 11], ['dvdr']), ([1], ['cam', 'ts', 'dvdrip', 'tc', 'r5', 'scr', 'brrip']), - ([10, 13, 14], ['bd50', '720p', '1080p']), + ([7, 10, 13, 14], ['bd50', '720p', '1080p']), ] cat_backup_id = 1 @@ -43,38 +41,29 @@ class FTDWorld(NZBProvider): 'ctype': ','.join([str(x) for x in self.getCatId(quality['identifier'])]), }) - data = self.getHTMLData(self.urls['search'] % params, opener = self.login_opener) + data = self.getJsonData(self.urls['search'] % params, opener = self.login_opener) if data: try: - html = BeautifulSoup(data) - main_table = html.find('table', attrs = {'id':'ftdresult'}) - - if not main_table: + if data.get('numRes') == 0: return - items = main_table.find_all('tr', attrs = {'class': re.compile('tcontent')}) - - for item in items: - tds = item.find_all('td') - nzb_id = tryInt(item.attrs['data-spot']) - - up = item.find('img', attrs = {'src': re.compile('up.png')}) - down = item.find('img', attrs = {'src': re.compile('down.png')}) + for item in data.get('data'): + nzb_id = tryInt(item.get('id')) results.append({ 'id': nzb_id, - 'name': toUnicode(item.find('a', attrs = {'href': re.compile('./spotinfo')}).text.strip()), - 'age': self.calculateAge(int(time.mktime(parse(tds[2].text).timetuple()))), + 'name': toUnicode(item.get('Title')), + 'age': self.calculateAge(tryInt(item.get('Created'))), 'url': self.urls['download'] % nzb_id, 'download': self.loginDownload, 'detail_url': self.urls['detail'] % nzb_id, - 'score': (tryInt(up.attrs['title'].split(' ')[0]) * 3) - (tryInt(down.attrs['title'].split(' ')[0]) * 3) if up else 0, + 'score': (tryInt(item.get('webPlus', 0)) - tryInt(item.get('webMin', 0))) * 3, }) except: - log.error('Failed to parse HTML response from FTDWorld') + log.error('Failed to parse HTML response from FTDWorld: %s', traceback.format_exc()) def getLoginParams(self): return tryUrlencode({ From ec857a9b3d4cedd2ec48237f2b645eb1134d6995 Mon Sep 17 00:00:00 2001 From: Ruud Date: Mon, 7 Jan 2013 22:31:42 +0100 Subject: [PATCH 43/43] FTDWorld: Check for login success --- couchpotato/core/providers/base.py | 11 ++++++++--- couchpotato/core/providers/nzb/ftdworld/main.py | 3 +++ 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/couchpotato/core/providers/base.py b/couchpotato/core/providers/base.py index 213fa43..9c143d8 100644 --- a/couchpotato/core/providers/base.py +++ b/couchpotato/core/providers/base.py @@ -95,15 +95,20 @@ class YarrProvider(Provider): urllib2.install_opener(opener) log.info2('Logging into %s', self.urls['login']) f = opener.open(self.urls['login'], self.getLoginParams()) - f.read() + output = f.read() f.close() - self.login_opener = opener - return True + + if self.loginSuccess(output): + self.login_opener = opener + return True except: log.error('Failed to login %s: %s', (self.getName(), traceback.format_exc())) return False + def loginSuccess(self, output): + return True + def loginDownload(self, url = '', nzb_id = ''): try: if not self.login_opener and not self.login(): diff --git a/couchpotato/core/providers/nzb/ftdworld/main.py b/couchpotato/core/providers/nzb/ftdworld/main.py index 9a3a9f3..c5a0665 100644 --- a/couchpotato/core/providers/nzb/ftdworld/main.py +++ b/couchpotato/core/providers/nzb/ftdworld/main.py @@ -71,3 +71,6 @@ class FTDWorld(NZBProvider): 'passlogin': self.conf('password'), 'submit': 'Log In', }) + + def loginSuccess(self, output): + return 'password is incorrect' not in output