From a3a2c8da8eac8bfca76203e954a9f48c320cc4e9 Mon Sep 17 00:00:00 2001 From: Ruud Date: Fri, 4 Jan 2013 19:03:36 +0100 Subject: [PATCH 01/20] 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 971fece5..684f6821 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 02/20] Added Windows Phone SuperToasty Notifier --- .../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 00000000..25d27ecd --- /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 00000000..34d338f1 --- /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 03/20] 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 34d338f1..638c75dd 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 04/20] Adding itunes automation provider --- .../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 00000000..9a01f650 --- /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 00000000..466589d7 --- /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 05/20] iTunes automation cleanup --- .../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 9a01f650..a88697ef 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 466589d7..14ca2a82 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 06/20] 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 a88697ef..368c1e43 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 07/20] 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 c7c27a8a..f1f0d48e 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 08/20] 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 96bb2cf8..18f5157a 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 09/20] 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 588c5e2b..ffff772b 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 10/20] 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 ffff772b..a97dc0ad 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 11/20] 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 11714406..662b09fa 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 12/20] 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 662b09fa..bb9581db 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 13/20] Acutally use config_file variable --- init/freebsd | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/init/freebsd b/init/freebsd index bb9581db..aaeaeddb 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 14/20] Redo FreeBSD init script completely. Use rc.subr functions and proper rc.conf variables. --- init/freebsd | 108 +++++++++++++++------------------------------------ 1 file changed, 32 insertions(+), 76 deletions(-) mode change 100755 => 100644 init/freebsd diff --git a/init/freebsd b/init/freebsd old mode 100755 new mode 100644 index aaeaeddb..20de9646 --- 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"} +: ${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:=} -WGET="/usr/local/bin/wget" # You need wget for this script to safely shutdown CouchPotato. +command="${couchpotato_dir}/CouchPotato.py" +command_interpreter="/usr/local/bin/python" +command_args="--daemon --data_dir ${couchpotato_datadir}" -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 +# 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 15/20] 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 20de9646..d3899332 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 16/20] 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 | 151 ++++++++++++++++---- 1 file changed, 126 insertions(+), 25 deletions(-) diff --git a/couchpotato/core/notifications/xbmc/main.py b/couchpotato/core/notifications/xbmc/main.py index 18f5157a..412e4eb6 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 + if self.useJSONnotifications: response = self.request(host, [ - ('JSONRPC.Version', {}) - ]) - else: - 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 (result.get('error')): + log.error("XBMC error; %s: %s (%s)", (result['id'], result['error']['message'], result['error']['code'])) - 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()) 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 17/20] XBMC notifier for Frodo & Eden --- .../core/notifications/xbmc/__init__.py | 2 +- couchpotato/core/notifications/xbmc/main.py | 128 +++++++++--------- 2 files changed, 63 insertions(+), 67 deletions(-) diff --git a/couchpotato/core/notifications/xbmc/__init__.py b/couchpotato/core/notifications/xbmc/__init__.py index a97dc0ad..ee2b4cc2 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 412e4eb6..eef94695 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']) + success = False - log.debug("XBMC JSON-RPC Version: %s ; Notifications by JSON-RPC only supported for v6 [as of XBMC v12(Frodo)]", xbmc_rpc_version) + # 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']) - # disable JSON use - self.useJSONnotifications = False + log.debug('XBMC JSON-RPC Version: %s ; Notifications by JSON-RPC only supported for v6 [as of XBMC v12(Frodo)]', xbmc_rpc_version) - # 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 + # disable JSON use + self.use_json_notifications[host] = False - 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']) + # 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 - log.debug("XBMC JSON-RPC Version: %s", xbmc_rpc_version) + 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']) - # ok, XBMC version is supported - self.useJSONnotifications = True + log.debug('XBMC JSON-RPC Version: %s', xbmc_rpc_version) - # 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 + # ok, XBMC version is supported + self.use_json_notifications[host] = True - # 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'])) + # 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 - # set boolean so we only run this once after boot - # in func notify() ignored for 'test' messages - self.firstRun = False + # 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.useJSONnotifications) + 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 18/20] 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 4aa1c312..7f1398d9 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 7e0d60fc..eabbfa47 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() - 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 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 300e4a6c..539dae2a 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 19/20] Use FTDWorld temp api. closes #1243 --- .../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 81ac8edd..9a3a9f33 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 20/20] 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 213fa43f..9c143d81 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 9a3a9f33..c5a06652 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