Merge branch 'refs/heads/develop'

This commit is contained in:
Ruud
2013-01-07 22:40:37 +01:00
14 changed files with 475 additions and 193 deletions
+5 -2
View File
@@ -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
@@ -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.',
},
],
}
],
}]
@@ -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
@@ -10,6 +10,7 @@ config = [{
'tab': 'notifications',
'name': 'xbmc',
'label': 'XBMC',
'description': 'v11 (Eden) and v12 (Frodo)',
'options': [
{
'name': 'enabled',
+126 -5
View File
@@ -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':
# <html>
# <li>OK
# </html>
#
# or 'Error':
# <html>
# <li>Error:<message>
# </html>
#
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
+1 -1
View File
@@ -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)
@@ -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 <a href="http://itunes.apple.com/rss">iTunes</a> 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,',
},
],
},
],
}]
@@ -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
+8 -3
View File
@@ -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():
+14 -22
View File
@@ -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
@@ -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'
Executable → Regular
+33 -76
View File
@@ -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"
+63 -37
View File
@@ -1,8 +1,9 @@
import hashlib
import re
import hashlib
import time
import StringIO
__version__ = '0.6'
__version__ = '0.8'
#GNTP/<version> <messagetype> <encryptionAlgorithmID>[:<ivValue>][ <keyHashAlgorithmID>:<keyHash>.<salt>]
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):
+63 -46
View File
@@ -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