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/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..638c75dd
--- /dev/null
+++ b/couchpotato/core/notifications/toasty/main.py
@@ -0,0 +1,30 @@
+from couchpotato.core.helpers.encoding import toUnicode, tryUrlencode
+from couchpotato.core.logger import CPLog
+from couchpotato.core.notifications.base import Notification
+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
+
+ 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:
+ 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())
+
+ return False
diff --git a/couchpotato/core/notifications/xbmc/__init__.py b/couchpotato/core/notifications/xbmc/__init__.py
index 588c5e2b..ee2b4cc2 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': '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 96bb2cf8..eef94695 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,27 +12,147 @@ log = CPLog(__name__)
class XBMC(Notification):
listen_to = ['renamer.after']
+ use_json_notifications = {}
def notify(self, message = '', data = {}, listener = None):
if self.isDisabled(): return
hosts = splitString(self.conf('host'))
+
successful = 0
for host in hosts:
- response = self.request(host, [
- ('GUI.ShowNotification', {"title":"CouchPotato", "message":message}),
- ('VideoLibrary.Scan', {}),
- ])
+
+ 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}),
+ ('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 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']))
+
except:
log.error('Failed parsing results: %s', traceback.format_exc())
return successful == len(hosts) * 2
+ def getXBMCJSONversion(self, host, message = ''):
+
+ 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
+
+ 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
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)
diff --git a/couchpotato/core/providers/automation/itunes/__init__.py b/couchpotato/core/providers/automation/itunes/__init__.py
new file mode 100644
index 00000000..368c1e43
--- /dev/null
+++ b/couchpotato/core/providers/automation/itunes/__init__.py
@@ -0,0 +1,35 @@
+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',
+ 'default': ',',
+ },
+ {
+ 'name': 'automation_urls',
+ 'label': 'url',
+ 'type': 'combined',
+ 'combine': ['automation_urls_use', 'automation_urls'],
+ 'default': 'https://itunes.apple.com/rss/topmovies/limit=25/xml,',
+ },
+ ],
+ },
+ ],
+}]
diff --git a/couchpotato/core/providers/automation/itunes/main.py b/couchpotato/core/providers/automation/itunes/main.py
new file mode 100644
index 00000000..14ca2a82
--- /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, splitString, tryInt
+from couchpotato.core.logger import CPLog
+from couchpotato.core.providers.automation.base import Automation
+from xml.etree.ElementTree import QName
+import datetime
+import traceback
+import xml.etree.ElementTree as XMLTree
+
+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
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 81ac8edd..c5a06652 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({
@@ -82,3 +71,6 @@ class FTDWorld(NZBProvider):
'passlogin': self.conf('password'),
'submit': 'Log In',
})
+
+ def loginSuccess(self, output):
+ return 'password is incorrect' not in output
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'
diff --git a/init/freebsd b/init/freebsd
old mode 100755
new mode 100644
index 11714406..d3899332
--- a/init/freebsd
+++ b/init/freebsd
@@ -1,92 +1,49 @@
#!/bin/sh
#
# PROVIDE: couchpotato
-# REQUIRE: sabnzbd
+# REQUIRE: DAEMON
# 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
-
-# Required, for some reason, to find all our binaries when starting via service.
-PATH="/usr/bin:/usr/local/bin:$PATH"
+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.
-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}'`
+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
-
-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}"
-
-# 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
+if [ -n "${couchpotato_conf}" ]; then
+ couchpotato_flags="${couchpotato_flags} --config_file ${couchpotato_conf}"
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
-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 $?
-}
-
-# 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
- 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"
- fi
-}
-
-couchpotato_status() {
- verify_couchpotato_pid && echo "$name is running as ${pid}" || echo "$name is not running"
-}
-
run_rc_command "$1"
-
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