Browse Source
- Windows: through GNTP library - OSX: through Growl library for local server and GTNP for remote server - Unix/Linux: through GNTP library For Ubuntu there's support for NotifyOSD. Made Growl class texts translatable.pull/7/head
22 changed files with 986 additions and 95 deletions
@ -0,0 +1,443 @@ |
|||
import re |
|||
import hashlib |
|||
import time |
|||
import platform |
|||
|
|||
__version__ = '0.4' |
|||
|
|||
class BaseError(Exception): |
|||
pass |
|||
|
|||
class ParseError(BaseError): |
|||
def gntp_error(self): |
|||
error = GNTPError(errorcode=500,errordesc='Error parsing the message') |
|||
return error.encode() |
|||
|
|||
class AuthError(BaseError): |
|||
def gntp_error(self): |
|||
error = GNTPError(errorcode=400,errordesc='Error with authorization') |
|||
return error.encode() |
|||
|
|||
class UnsupportedError(BaseError): |
|||
def gntp_error(self): |
|||
error = GNTPError(errorcode=500,errordesc='Currently unsupported by gntp.py') |
|||
return error.encode() |
|||
|
|||
class _GNTPBase(object): |
|||
info = { |
|||
'version':'1.0', |
|||
'messagetype':None, |
|||
'encryptionAlgorithmID':None |
|||
} |
|||
_requiredHeaders = [] |
|||
headers = {} |
|||
resources = {} |
|||
def add_origin_info(self): |
|||
self.add_header('Origin-Machine-Name',platform.node()) |
|||
self.add_header('Origin-Software-Name','gntp.py') |
|||
self.add_header('Origin-Software-Version',__version__) |
|||
self.add_header('Origin-Platform-Name',platform.system()) |
|||
self.add_header('Origin-Platform-Version',platform.platform()) |
|||
def __str__(self): |
|||
return self.encode() |
|||
def _parse_info(self,data): |
|||
''' |
|||
Parse the first line of a GNTP message to get security and other info values |
|||
@param data: GNTP Message |
|||
@return: GNTP Message information in a dictionary |
|||
''' |
|||
#GNTP/<version> <messagetype> <encryptionAlgorithmID>[:<ivValue>][ <keyHashAlgorithmID>:<keyHash>.<salt>] |
|||
match = re.match('GNTP/(?P<version>\d+\.\d+) (?P<messagetype>REGISTER|NOTIFY|SUBSCRIBE|\-OK|\-ERROR)'+ |
|||
' (?P<encryptionAlgorithmID>[A-Z0-9]+(:(?P<ivValue>[A-F0-9]+))?) ?'+ |
|||
'((?P<keyHashAlgorithmID>[A-Z0-9]+):(?P<keyHash>[A-F0-9]+).(?P<salt>[A-F0-9]+))?\r\n', data,re.IGNORECASE) |
|||
|
|||
if not match: |
|||
raise ParseError('ERROR_PARSING_INFO_LINE') |
|||
|
|||
info = match.groupdict() |
|||
if info['encryptionAlgorithmID'] == 'NONE': |
|||
info['encryptionAlgorithmID'] = None |
|||
|
|||
return info |
|||
def set_password(self,password,encryptAlgo='MD5'): |
|||
''' |
|||
Set a password for a GNTP Message |
|||
@param password: Null to clear password |
|||
@param encryptAlgo: Supports MD5,SHA1,SHA256,SHA512 |
|||
@todo: Support other hash functions |
|||
''' |
|||
hash = { |
|||
'MD5': hashlib.md5, |
|||
'SHA1': hashlib.sha1, |
|||
'SHA256': hashlib.sha256, |
|||
'SHA512': hashlib.sha512, |
|||
} |
|||
|
|||
self.password = password |
|||
self.encryptAlgo = encryptAlgo.upper() |
|||
if not password: |
|||
self.info['encryptionAlgorithmID'] = None |
|||
self.info['keyHashAlgorithm'] = None; |
|||
return |
|||
if not self.encryptAlgo in hash.keys(): |
|||
raise UnsupportedError('INVALID HASH "%s"'%self.encryptAlgo) |
|||
|
|||
hashfunction = hash.get(self.encryptAlgo) |
|||
|
|||
password = password.encode('utf8') |
|||
seed = time.ctime() |
|||
salt = hashfunction(seed).hexdigest() |
|||
saltHash = hashfunction(seed).digest() |
|||
keyBasis = password+saltHash |
|||
key = hashfunction(keyBasis).digest() |
|||
keyHash = hashfunction(key).hexdigest() |
|||
|
|||
self.info['keyHashAlgorithmID'] = self.encryptAlgo |
|||
self.info['keyHash'] = keyHash.upper() |
|||
self.info['salt'] = salt.upper() |
|||
def _decode_hex(self,value): |
|||
''' |
|||
Helper function to decode hex string to `proper` hex string |
|||
@param value: Value to decode |
|||
@return: Hex string |
|||
''' |
|||
result = '' |
|||
for i in range(0,len(value),2): |
|||
tmp = int(value[i:i+2],16) |
|||
result += chr(tmp) |
|||
return result |
|||
def _decode_binary(self,rawIdentifier,identifier): |
|||
rawIdentifier += '\r\n\r\n' |
|||
dataLength = int(identifier['Length']) |
|||
pointerStart = self.raw.find(rawIdentifier)+len(rawIdentifier) |
|||
pointerEnd = pointerStart + dataLength |
|||
data = self.raw[pointerStart:pointerEnd] |
|||
if not len(data) == dataLength: |
|||
raise ParseError('INVALID_DATA_LENGTH Expected: %s Recieved %s'%(dataLength,len(data))) |
|||
return data |
|||
def _validate_password(self,password): |
|||
''' |
|||
Validate GNTP Message against stored password |
|||
''' |
|||
self.password = password |
|||
if password == None: raise Exception() |
|||
keyHash = self.info.get('keyHash',None) |
|||
if keyHash is None and self.password is None: |
|||
return True |
|||
if keyHash is None: |
|||
raise AuthError('Invalid keyHash') |
|||
if self.password is None: |
|||
raise AuthError('Missing password') |
|||
|
|||
password = self.password.encode('utf8') |
|||
saltHash = self._decode_hex(self.info['salt']) |
|||
|
|||
keyBasis = password+saltHash |
|||
key = hashlib.md5(keyBasis).digest() |
|||
keyHash = hashlib.md5(key).hexdigest() |
|||
|
|||
if not keyHash.upper() == self.info['keyHash'].upper(): |
|||
raise AuthError('Invalid Hash') |
|||
return True |
|||
def validate(self): |
|||
''' |
|||
Verify required headers |
|||
''' |
|||
for header in self._requiredHeaders: |
|||
if not self.headers.get(header,False): |
|||
raise ParseError('Missing Notification Header: '+header) |
|||
|
|||
def _format_info(self): |
|||
''' |
|||
Generate info line for GNTP Message |
|||
@return: Info line string |
|||
''' |
|||
info = u'GNTP/%s %s'%( |
|||
self.info.get('version'), |
|||
self.info.get('messagetype'), |
|||
) |
|||
if self.info.get('encryptionAlgorithmID',None): |
|||
info += ' %s:%s'%( |
|||
self.info.get('encryptionAlgorithmID'), |
|||
self.info.get('ivValue'), |
|||
) |
|||
else: |
|||
info+=' NONE' |
|||
|
|||
if self.info.get('keyHashAlgorithmID',None): |
|||
info += ' %s:%s.%s'%( |
|||
self.info.get('keyHashAlgorithmID'), |
|||
self.info.get('keyHash'), |
|||
self.info.get('salt') |
|||
) |
|||
|
|||
return info |
|||
def _parse_dict(self,data): |
|||
''' |
|||
Helper function to parse blocks of GNTP headers into a dictionary |
|||
@param data: |
|||
@return: Dictionary of headers |
|||
''' |
|||
dict = {} |
|||
for line in data.split('\r\n'): |
|||
match = re.match('([\w-]+):(.+)', line) |
|||
if not match: continue |
|||
|
|||
key = match.group(1).strip() |
|||
val = match.group(2).strip() |
|||
dict[key] = val |
|||
return dict |
|||
def add_header(self,key,value): |
|||
if isinstance(value, unicode): |
|||
self.headers[key] = value |
|||
else: |
|||
self.headers[key] = unicode('%s'%value,'utf8','replace') |
|||
def decode(self,data,password=None): |
|||
''' |
|||
Decode GNTP Message |
|||
@param data: |
|||
''' |
|||
self.password = password |
|||
self.raw = data |
|||
parts = self.raw.split('\r\n\r\n') |
|||
self.info = self._parse_info(data) |
|||
self.headers = self._parse_dict(parts[0]) |
|||
def encode(self): |
|||
''' |
|||
Encode a GNTP Message |
|||
@return: GNTP Message ready to be sent |
|||
''' |
|||
self.validate() |
|||
EOL = u'\r\n' |
|||
|
|||
message = self._format_info() + EOL |
|||
#Headers |
|||
for k,v in self.headers.iteritems(): |
|||
message += u'%s: %s%s'%(k,v,EOL) |
|||
|
|||
message += EOL |
|||
return message.encode('utf8') |
|||
class GNTPRegister(_GNTPBase): |
|||
''' |
|||
GNTP Registration Message |
|||
''' |
|||
notifications = [] |
|||
_requiredHeaders = [ |
|||
'Application-Name', |
|||
'Notifications-Count' |
|||
] |
|||
_requiredNotificationHeaders = ['Notification-Name'] |
|||
def __init__(self,data=None,password=None): |
|||
''' |
|||
@param data: (Optional) See decode() |
|||
@param password: (Optional) Password to use while encoding/decoding messages |
|||
''' |
|||
self.info['messagetype'] = 'REGISTER' |
|||
|
|||
if data: |
|||
self.decode(data,password) |
|||
else: |
|||
self.set_password(password) |
|||
self.add_header('Application-Name', 'pygntp') |
|||
self.add_header('Notifications-Count', 0) |
|||
self.add_origin_info() |
|||
def validate(self): |
|||
''' |
|||
Validate required headers and validate notification headers |
|||
''' |
|||
for header in self._requiredHeaders: |
|||
if not self.headers.get(header,False): |
|||
raise ParseError('Missing Registration Header: '+header) |
|||
for notice in self.notifications: |
|||
for header in self._requiredNotificationHeaders: |
|||
if not notice.get(header,False): |
|||
raise ParseError('Missing Notification Header: '+header) |
|||
def decode(self,data,password): |
|||
''' |
|||
Decode existing GNTP Registration message |
|||
@param data: Message to decode. |
|||
''' |
|||
self.raw = data |
|||
parts = self.raw.split('\r\n\r\n') |
|||
self.info = self._parse_info(data) |
|||
self._validate_password(password) |
|||
self.headers = self._parse_dict(parts[0]) |
|||
|
|||
for i,part in enumerate(parts): |
|||
if i==0: continue #Skip Header |
|||
if part.strip()=='': continue |
|||
notice = self._parse_dict(part) |
|||
if notice.get('Notification-Name',False): |
|||
self.notifications.append(notice) |
|||
elif notice.get('Identifier',False): |
|||
notice['Data'] = self._decode_binary(part,notice) |
|||
#open('register.png','wblol').write(notice['Data']) |
|||
self.resources[ notice.get('Identifier') ] = notice |
|||
|
|||
def add_notification(self,name,enabled=True): |
|||
''' |
|||
Add new Notification to Registration message |
|||
@param name: Notification Name |
|||
@param enabled: Default Notification to Enabled |
|||
''' |
|||
notice = {} |
|||
notice['Notification-Name'] = u'%s'%name |
|||
notice['Notification-Enabled'] = u'%s'%enabled |
|||
|
|||
self.notifications.append(notice) |
|||
self.add_header('Notifications-Count', len(self.notifications)) |
|||
def encode(self): |
|||
''' |
|||
Encode a GNTP Registration Message |
|||
@return: GNTP Registration Message ready to be sent |
|||
''' |
|||
self.validate() |
|||
EOL = u'\r\n' |
|||
|
|||
message = self._format_info() + EOL |
|||
#Headers |
|||
for k,v in self.headers.iteritems(): |
|||
message += u'%s: %s%s'%(k,v,EOL) |
|||
|
|||
#Notifications |
|||
if len(self.notifications)>0: |
|||
for notice in self.notifications: |
|||
message += EOL |
|||
for k,v in notice.iteritems(): |
|||
message += u'%s: %s%s'%(k,v,EOL) |
|||
|
|||
message += EOL |
|||
return message |
|||
|
|||
class GNTPNotice(_GNTPBase): |
|||
''' |
|||
GNTP Notification Message |
|||
''' |
|||
_requiredHeaders = [ |
|||
'Application-Name', |
|||
'Notification-Name', |
|||
'Notification-Title' |
|||
] |
|||
def __init__(self,data=None,app=None,name=None,title=None,password=None): |
|||
''' |
|||
|
|||
@param data: (Optional) See decode() |
|||
@param app: (Optional) Set Application-Name |
|||
@param name: (Optional) Set Notification-Name |
|||
@param title: (Optional) Set Notification Title |
|||
@param password: (Optional) Password to use while encoding/decoding messages |
|||
''' |
|||
self.info['messagetype'] = 'NOTIFY' |
|||
|
|||
if data: |
|||
self.decode(data,password) |
|||
else: |
|||
self.set_password(password) |
|||
if app: |
|||
self.add_header('Application-Name', app) |
|||
if name: |
|||
self.add_header('Notification-Name', name) |
|||
if title: |
|||
self.add_header('Notification-Title', title) |
|||
self.add_origin_info() |
|||
def decode(self,data,password): |
|||
''' |
|||
Decode existing GNTP Notification message |
|||
@param data: Message to decode. |
|||
''' |
|||
self.raw = data |
|||
parts = self.raw.split('\r\n\r\n') |
|||
self.info = self._parse_info(data) |
|||
self._validate_password(password) |
|||
self.headers = self._parse_dict(parts[0]) |
|||
|
|||
for i,part in enumerate(parts): |
|||
if i==0: continue #Skip Header |
|||
if part.strip()=='': continue |
|||
notice = self._parse_dict(part) |
|||
if notice.get('Identifier',False): |
|||
notice['Data'] = self._decode_binary(part,notice) |
|||
#open('notice.png','wblol').write(notice['Data']) |
|||
self.resources[ notice.get('Identifier') ] = notice |
|||
def encode(self): |
|||
''' |
|||
Encode a GNTP Notification Message |
|||
@return: GNTP Notification Message ready to be sent |
|||
''' |
|||
self.validate() |
|||
EOL = u'\r\n' |
|||
|
|||
message = self._format_info() + EOL |
|||
#Headers |
|||
for k,v in self.headers.iteritems(): |
|||
message += u'%s: %s%s'%(k,v,EOL) |
|||
|
|||
message += EOL |
|||
return message.encode('utf8') |
|||
|
|||
class GNTPSubscribe(_GNTPBase): |
|||
def __init__(self,data=None,password=None): |
|||
self.info['messagetype'] = 'SUBSCRIBE' |
|||
self._requiredHeaders = [ |
|||
'Subscriber-ID', |
|||
'Subscriber-Name', |
|||
] |
|||
if data: |
|||
self.decode(data,password) |
|||
else: |
|||
self.set_password(password) |
|||
self.add_origin_info() |
|||
|
|||
class GNTPOK(_GNTPBase): |
|||
_requiredHeaders = ['Response-Action'] |
|||
def __init__(self,data=None,action=None): |
|||
''' |
|||
@param data: (Optional) See _GNTPResponse.decode() |
|||
@param action: (Optional) Set type of action the OK Response is for |
|||
''' |
|||
self.info['messagetype'] = '-OK' |
|||
if data: |
|||
self.decode(data) |
|||
if action: |
|||
self.add_header('Response-Action', action) |
|||
self.add_origin_info() |
|||
|
|||
class GNTPError(_GNTPBase): |
|||
_requiredHeaders = ['Error-Code','Error-Description'] |
|||
def __init__(self,data=None,errorcode=None,errordesc=None): |
|||
''' |
|||
@param data: (Optional) See _GNTPResponse.decode() |
|||
@param errorcode: (Optional) Error code |
|||
@param errordesc: (Optional) Error Description |
|||
''' |
|||
self.info['messagetype'] = '-ERROR' |
|||
if data: |
|||
self.decode(data) |
|||
if errorcode: |
|||
self.add_header('Error-Code', errorcode) |
|||
self.add_header('Error-Description', errordesc) |
|||
self.add_origin_info() |
|||
def error(self): |
|||
return self.headers['Error-Code'],self.headers['Error-Description'] |
|||
|
|||
def parse_gntp(data,password=None): |
|||
''' |
|||
Attempt to parse a message as a GNTP message |
|||
@param data: Message to be parsed |
|||
@param password: Optional password to be used to verify the message |
|||
''' |
|||
match = re.match('GNTP/(?P<version>\d+\.\d+) (?P<messagetype>REGISTER|NOTIFY|SUBSCRIBE|\-OK|\-ERROR)',data,re.IGNORECASE) |
|||
if not match: |
|||
raise ParseError('INVALID_GNTP_INFO') |
|||
info = match.groupdict() |
|||
if info['messagetype'] == 'REGISTER': |
|||
return GNTPRegister(data,password=password) |
|||
elif info['messagetype'] == 'NOTIFY': |
|||
return GNTPNotice(data,password=password) |
|||
elif info['messagetype'] == 'SUBSCRIBE': |
|||
return GNTPSubscribe(data,password=password) |
|||
elif info['messagetype'] == '-OK': |
|||
return GNTPOK(data) |
|||
elif info['messagetype'] == '-ERROR': |
|||
return GNTPError(data) |
|||
raise ParseError('INVALID_GNTP_MESSAGE') |
@ -0,0 +1,130 @@ |
|||
""" |
|||
A Python module that uses GNTP to post messages |
|||
Mostly mirrors the Growl.py file that comes with Mac Growl |
|||
http://code.google.com/p/growl/source/browse/Bindings/python/Growl.py |
|||
""" |
|||
import gntp |
|||
import socket |
|||
import logging |
|||
|
|||
logger = logging.getLogger(__name__) |
|||
|
|||
class GrowlNotifier(object): |
|||
applicationName = 'Python GNTP' |
|||
notifications = [] |
|||
defaultNotifications = [] |
|||
applicationIcon = None |
|||
passwordHash = 'MD5' |
|||
|
|||
#GNTP Specific |
|||
password = None |
|||
hostname = 'localhost' |
|||
port = 23053 |
|||
|
|||
def __init__(self, applicationName=None, notifications=None, defaultNotifications=None, applicationIcon=None, hostname=None, password=None, port=None): |
|||
if applicationName: |
|||
self.applicationName = applicationName |
|||
assert self.applicationName, 'An application name is required.' |
|||
|
|||
if notifications: |
|||
self.notifications = list(notifications) |
|||
assert self.notifications, 'A sequence of one or more notification names is required.' |
|||
|
|||
if defaultNotifications is not None: |
|||
self.defaultNotifications = list(defaultNotifications) |
|||
elif not self.defaultNotifications: |
|||
self.defaultNotifications = list(self.notifications) |
|||
|
|||
if applicationIcon is not None: |
|||
self.applicationIcon = self._checkIcon(applicationIcon) |
|||
elif self.applicationIcon is not None: |
|||
self.applicationIcon = self._checkIcon(self.applicationIcon) |
|||
|
|||
#GNTP Specific |
|||
if password: |
|||
self.password = password |
|||
|
|||
if hostname: |
|||
self.hostname = hostname |
|||
assert self.hostname, 'Requires valid hostname' |
|||
|
|||
if port: |
|||
self.port = int(port) |
|||
assert isinstance(self.port,int), 'Requires valid port' |
|||
|
|||
def _checkIcon(self, data): |
|||
''' |
|||
Check the icon to see if it's valid |
|||
@param data: |
|||
@todo Consider checking for a valid URL |
|||
''' |
|||
return data |
|||
|
|||
def register(self): |
|||
''' |
|||
Send GNTP Registration |
|||
''' |
|||
logger.info('Sending registration to %s:%s',self.hostname,self.port) |
|||
register = gntp.GNTPRegister() |
|||
register.add_header('Application-Name',self.applicationName) |
|||
for notification in self.notifications: |
|||
enabled = notification in self.defaultNotifications |
|||
register.add_notification(notification,enabled) |
|||
if self.applicationIcon: |
|||
register.add_header('Application-Icon',self.applicationIcon) |
|||
if self.password: |
|||
register.set_password(self.password,self.passwordHash) |
|||
response = self.send('register',register.encode()) |
|||
if isinstance(response,gntp.GNTPOK): return True |
|||
logger.debug('Invalid response %s',response.error()) |
|||
return response.error() |
|||
|
|||
def notify(self, noteType, title, description, icon=None, sticky=False, priority=None): |
|||
''' |
|||
Send a GNTP notifications |
|||
''' |
|||
logger.info('Sending notification [%s] to %s:%s',noteType,self.hostname,self.port) |
|||
assert noteType in self.notifications |
|||
notice = gntp.GNTPNotice() |
|||
notice.add_header('Application-Name',self.applicationName) |
|||
notice.add_header('Notification-Name',noteType) |
|||
notice.add_header('Notification-Title',title) |
|||
if self.password: |
|||
notice.set_password(self.password,self.passwordHash) |
|||
if sticky: |
|||
notice.add_header('Notification-Sticky',sticky) |
|||
if priority: |
|||
notice.add_header('Notification-Priority',priority) |
|||
if icon: |
|||
notice.add_header('Notification-Icon',self._checkIcon(icon)) |
|||
if description: |
|||
notice.add_header('Notification-Text',description) |
|||
response = self.send('notify',notice.encode()) |
|||
if isinstance(response,gntp.GNTPOK): return True |
|||
logger.debug('Invalid response %s',response.error()) |
|||
return response.error() |
|||
def subscribe(self,id,name,port): |
|||
sub = gntp.GNTPSubscribe() |
|||
sub.add_header('Subscriber-ID',id) |
|||
sub.add_header('Subscriber-Name',name) |
|||
sub.add_header('Subscriber-Port',port) |
|||
if self.password: |
|||
sub.set_password(self.password,self.passwordHash) |
|||
response = self.send('subscribe',sub.encode()) |
|||
if isinstance(response,gntp.GNTPOK): return True |
|||
logger.debug('Invalid response %s',response.error()) |
|||
return response.error() |
|||
def send(self,type,data): |
|||
''' |
|||
Send the GNTP Packet |
|||
''' |
|||
logger.debug('To : %s:%s <%s>\n%s',self.hostname,self.port,type,data) |
|||
|
|||
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) |
|||
s.connect((self.hostname,self.port)) |
|||
s.send(data.encode('utf-8', 'replace')) |
|||
response = gntp.parse_gntp(s.recv(1024)) |
|||
s.close() |
|||
|
|||
logger.debug('From : %s:%s <%s>\n%s',self.hostname,self.port,response.__class__,response) |
|||
return response |
@ -0,0 +1,223 @@ |
|||
#!/usr/bin/python -OO |
|||
# Copyright 2008-2011 The SABnzbd-Team <team@sabnzbd.org> |
|||
# |
|||
# This program is free software; you can redistribute it and/or |
|||
# modify it under the terms of the GNU General Public License |
|||
# as published by the Free Software Foundation; either version 2 |
|||
# of the License, or (at your option) any later version. |
|||
# |
|||
# This program is distributed in the hope that it will be useful, |
|||
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
|||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|||
# GNU General Public License for more details. |
|||
# |
|||
# You should have received a copy of the GNU General Public License |
|||
# along with this program; if not, write to the Free Software |
|||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
|||
# |
|||
""" |
|||
sabnzbd.growler - Send notifications to Growl |
|||
""" |
|||
#------------------------------------------------------------------------------ |
|||
|
|||
import os.path |
|||
import logging |
|||
import socket |
|||
|
|||
import sabnzbd |
|||
import sabnzbd.cfg |
|||
from sabnzbd.encoding import unicoder, latin1 |
|||
from gntp import GNTPRegister |
|||
from gntp.notifier import GrowlNotifier |
|||
try: |
|||
import Growl |
|||
_HAVE_OSX_GROWL = True |
|||
except ImportError: |
|||
_HAVE_OSX_GROWL = False |
|||
try: |
|||
import pynotify |
|||
_HAVE_NTFOSD = True |
|||
except ImportError: |
|||
_HAVE_NTFOSD = False |
|||
|
|||
#------------------------------------------------------------------------------ |
|||
# Define translatable message table |
|||
TT = lambda x:x |
|||
_NOTIFICATION = { |
|||
'startup' : TT('Startup/Shutdown'), #: Message class for Growl server |
|||
'download' : TT('Added NZB'), #: Message class for Growl server |
|||
'pp' : TT('Post-processing started'), #: Message class for Growl server |
|||
'complete' : TT('Job finished'), #: Message class for Growl server |
|||
'other' : TT('Other Messages') #: Message class for Growl server |
|||
} |
|||
_KEYS = ('startup', 'download', 'pp', 'complete', 'other') |
|||
|
|||
#------------------------------------------------------------------------------ |
|||
# Setup platform dependent Growl support |
|||
# |
|||
_GROWL_ICON = None # Platform-dependant icon path |
|||
_GROWL = None # Instance of the Notifier after registration |
|||
_GROWL_REG = False # Succesful registration |
|||
|
|||
|
|||
#------------------------------------------------------------------------------ |
|||
def get_icon(): |
|||
icon = os.path.join(sabnzbd.DIR_PROG, 'sabnzbd.ico') |
|||
if not os.path.isfile(icon): |
|||
icon = None |
|||
return icon |
|||
|
|||
|
|||
#------------------------------------------------------------------------------ |
|||
def change_value(): |
|||
""" Signal that we should register with a new Growl server |
|||
""" |
|||
global _GROWL_REG |
|||
_GROWL_REG = False |
|||
|
|||
|
|||
#------------------------------------------------------------------------------ |
|||
def have_growl(): |
|||
""" Return if any Growl support is present |
|||
""" |
|||
return True |
|||
|
|||
def have_ntfosd(): |
|||
""" Return if any PyNotify support is present |
|||
""" |
|||
return bool(_HAVE_NTFOSD) |
|||
|
|||
|
|||
#------------------------------------------------------------------------------ |
|||
def send_notification(title , msg, gtype): |
|||
""" Send Notification message |
|||
""" |
|||
if have_growl(): |
|||
send_growl(title, msg, gtype) |
|||
if have_ntfosd(): |
|||
send_notify_osd(title, msg) |
|||
|
|||
|
|||
#------------------------------------------------------------------------------ |
|||
def register_growl(): |
|||
""" Register this app with Growl |
|||
""" |
|||
host, port = sabnzbd.misc.split_host(sabnzbd.cfg.growl_server()) |
|||
|
|||
if host: |
|||
sys_name = '@' + sabnzbd.misc.hostname().lower() |
|||
else: |
|||
sys_name = '' |
|||
|
|||
# Clean up persistent data in GNTP to make re-registration work |
|||
GNTPRegister.notifications = [] |
|||
GNTPRegister.headers = {} |
|||
|
|||
growler = GrowlNotifier( |
|||
applicationName = 'SABnzbd%s' % sys_name, |
|||
applicationIcon = get_icon(), |
|||
notifications = [Tx(_NOTIFICATION[key]) for key in _KEYS], |
|||
hostname = host or None, |
|||
port = port or 23053, |
|||
password = sabnzbd.cfg.growl_password() or None |
|||
) |
|||
|
|||
try: |
|||
ret = growler.register() |
|||
if ret is None or isinstance(ret, bool): |
|||
logging.info('Registered with Growl') |
|||
ret = growler |
|||
else: |
|||
logging.debug('Cannot register with Growl %s', ret) |
|||
del growler |
|||
ret = None |
|||
except socket.error, err: |
|||
logging.debug('Cannot register with Growl %s', err) |
|||
del growler |
|||
ret = None |
|||
return ret |
|||
|
|||
|
|||
#------------------------------------------------------------------------------ |
|||
def send_growl(title , msg, gtype): |
|||
""" Send Growl message |
|||
""" |
|||
global _GROWL, _GROWL_REG |
|||
if not sabnzbd.cfg.growl_enable(): |
|||
return |
|||
|
|||
if _HAVE_OSX_GROWL and not sabnzbd.cfg.growl_server(): |
|||
send_local_growl(title, msg, gtype) |
|||
return |
|||
|
|||
for n in (0, 1): |
|||
if not _GROWL_REG: _GROWL = None |
|||
_GROWL = _GROWL or register_growl() |
|||
if _GROWL: |
|||
assert isinstance(_GROWL, GrowlNotifier) |
|||
_GROWL_REG = True |
|||
logging.debug('Send to Growl: %s %s %s', gtype, latin1(title), latin1(msg)) |
|||
try: |
|||
ret = _GROWL.notify( |
|||
noteType = Tx(_NOTIFICATION.get(gtype, 'other')), |
|||
title = title, |
|||
description = unicoder(msg), |
|||
#icon = options.icon, |
|||
#sticky = options.sticky, |
|||
#priority = options.priority |
|||
) |
|||
if ret is None or isinstance(ret, bool): |
|||
return |
|||
elif ret[0] == '401': |
|||
_GROWL = False |
|||
else: |
|||
logging.debug('Growl error %s', ret) |
|||
return |
|||
except socket.error, err: |
|||
logging.debug('Growl error %s', err) |
|||
return |
|||
else: |
|||
return |
|||
|
|||
|
|||
#------------------------------------------------------------------------------ |
|||
# Local OSX Growl support |
|||
# |
|||
if _HAVE_OSX_GROWL: |
|||
|
|||
if os.path.isfile('sabnzbdplus.icns'): |
|||
_OSX_ICON = Growl.Image.imageFromPath('sabnzbdplus.icns') |
|||
elif os.path.isfile('osx/resources/sabnzbdplus.icns'): |
|||
_OSX_ICON = Growl.Image.imageFromPath('osx/resources/sabnzbdplus.icns') |
|||
else: |
|||
_OSX_ICON = Growl.Image.imageWithIconForApplication('Terminal') |
|||
|
|||
def send_local_growl(title , msg, gtype): |
|||
""" Send to local Growl server, OSX-only """ |
|||
notes = [Tx(_NOTIFICATION[key]) for key in _KEYS] |
|||
growler = Growl.GrowlNotifier( |
|||
applicationName = 'SABnzbd', |
|||
applicationIcon = _OSX_ICON, |
|||
notifications = notes, |
|||
defaultNotifications = notes |
|||
) |
|||
growler.register() |
|||
growler.notify(Tx(_NOTIFICATION.get(gtype, 'other')), title, msg) |
|||
|
|||
|
|||
#------------------------------------------------------------------------------ |
|||
# Ubuntu NotifyOSD Support |
|||
# |
|||
if _HAVE_NTFOSD: |
|||
_NTFOSD = False |
|||
def send_notify_osd(title, message): |
|||
""" Send a message to NotifyOSD |
|||
""" |
|||
global _NTFOSD |
|||
if sabnzbd.cfg.ntfosd_enable(): |
|||
icon = os.path.join(sabnzbd.DIR_PROG, 'sabnzbd.ico') |
|||
_NTFOSD = _NTFOSD or pynotify.init('icon-summary-body') |
|||
if _NTFOSD: |
|||
logging.info('Send to NotifyOSD: %s / %s', latin1(title), latin1(message)) |
|||
note = pynotify.Notification(title, message, icon) |
|||
note.show() |
@ -1,65 +0,0 @@ |
|||
#!/usr/bin/python -OO |
|||
# Copyright 2008-2011 The SABnzbd-Team <team@sabnzbd.org> |
|||
# |
|||
# This program is free software; you can redistribute it and/or |
|||
# modify it under the terms of the GNU General Public License |
|||
# as published by the Free Software Foundation; either version 2 |
|||
# of the License, or (at your option) any later version. |
|||
# |
|||
# This program is distributed in the hope that it will be useful, |
|||
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
|||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|||
# GNU General Public License for more details. |
|||
# |
|||
# You should have received a copy of the GNU General Public License |
|||
# along with this program; if not, write to the Free Software |
|||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
|||
# |
|||
#""" |
|||
#TO FIX : Translations are not working with this implementation |
|||
# Growl Registration may only be done once per run ? |
|||
# Registration is made too early, the language module has not read the text file yet |
|||
#NOTIFICATION = {'startup':'grwl-notif-startup','download':'grwl-notif-dl','pp':'grwl-notif-pp','other':'grwl-notif-other'} |
|||
NOTIFICATION = {'startup':'1. On Startup/Shutdown','download':'2. On adding NZB','pp':'3. On post-processing','complete':'4. On download terminated','other':'5. Other Messages'} |
|||
|
|||
# For a future release, make texts translatable. |
|||
if 0: |
|||
#------------------------------------------------------------------------------ |
|||
# Define translatable message table |
|||
TT = lambda x:x |
|||
_NOTIFICATION = { |
|||
'startup' : TT('Startup/Shutdown'), #: Message class for Growl server |
|||
'download' : TT('Added NZB'), #: Message class for Growl server |
|||
'pp' : TT('Post-processing started'), #: Message class for Growl server |
|||
'complete' : TT('Job finished'), #: Message class for Growl server |
|||
'other' : TT('Other Messages') #: Message class for Growl server |
|||
} |
|||
|
|||
try: |
|||
import Growl |
|||
import os.path |
|||
import logging |
|||
|
|||
if os.path.isfile('sabnzbdplus.icns'): |
|||
nIcon = Growl.Image.imageFromPath('sabnzbdplus.icns') |
|||
elif os.path.isfile('osx/resources/sabnzbdplus.icns'): |
|||
nIcon = Growl.Image.imageFromPath('osx/resources/sabnzbdplus.icns') |
|||
else: |
|||
nIcon = Growl.Image.imageWithIconForApplication('Terminal') |
|||
|
|||
def sendGrowlMsg(nTitle , nMsg, nType=NOTIFICATION['other']): |
|||
gnotifier = SABGrowlNotifier(applicationIcon=nIcon) |
|||
gnotifier.register() |
|||
#TO FIX |
|||
#gnotifier.notify(T(nType), nTitle, nMsg) |
|||
gnotifier.notify(nType, nTitle, nMsg) |
|||
|
|||
class SABGrowlNotifier(Growl.GrowlNotifier): |
|||
applicationName = "SABnzbd" |
|||
#TO FIX |
|||
#notifications = [T(notification) for notification in NOTIFICATION.values()] |
|||
notifications = NOTIFICATION.values() |
|||
|
|||
except ImportError: |
|||
def sendGrowlMsg(nTitle , nMsg, nType): |
|||
pass |
Loading…
Reference in new issue