You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 

518 lines
19 KiB

#!/usr/bin/python -OO
# Copyright 2008-2015 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
"""
#------------------------------------------------------------------------------
from __future__ import with_statement
import os.path
import logging
import socket
import urllib2
import httplib
import urllib
import time
import subprocess
import json
from threading import Thread
import sabnzbd
import sabnzbd.cfg
from sabnzbd.encoding import unicoder
from sabnzbd.constants import NOTIFY_KEYS
from gntp import GNTPRegister
from gntp.notifier import GrowlNotifier
try:
import Growl
# Detect classic Growl (older than 1.3)
_HAVE_CLASSIC_GROWL = os.path.isfile('/Library/PreferencePanes/Growl.prefPane/Contents/MacOS/Growl')
except ImportError:
_HAVE_CLASSIC_GROWL = False
try:
import warnings
# Make any warnings exceptions, so that pynotify is ignored
# PyNotify will not work with Python 2.5 (due to next three lines)
with warnings.catch_warnings():
warnings.simplefilter("error")
import pynotify
_HAVE_NTFOSD = True
except:
_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
'failed' : TT('Job failed'), #: Message class for Growl server
'warning' : TT('Warning'), #: Message class for Growl server
'error' : TT('Error'), #: Message class for Growl server
'disk_full' : TT('Disk full'), #: Message class for Growl server
'queue_done': TT('Queue finished'), #: Message class for Growl server
'other' : TT('Other Messages') #: Message class for Growl server
}
#------------------------------------------------------------------------------
# Setup platform dependent Growl support
#
_GROWL = None # Instance of the Notifier after registration
_GROWL_REG = False # Succesful registration
_GROWL_DATA = (None, None) # Address and password
#------------------------------------------------------------------------------
def get_icon():
icon = os.path.join(os.path.join(sabnzbd.DIR_PROG, 'icons'), 'sabnzbd.ico')
if not os.path.isfile(icon):
icon = os.path.join(sabnzbd.DIR_PROG, 'sabnzbd.ico')
if os.path.isfile(icon):
if sabnzbd.WIN32 or sabnzbd.DARWIN:
fp = open(icon, 'rb')
icon = fp.read()
fp.close
else:
# Due to a bug in GNTP, need this work-around for Linux/Unix
icon = 'http://sabnzbdplus.sourceforge.net/version/sabnzbd.ico'
else:
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_ntfosd():
""" Return if any PyNotify support is present
"""
return bool(_HAVE_NTFOSD)
#------------------------------------------------------------------------------
def check_classes(gtype, section):
""" Check if `gtype` is enabled in `section` """
try:
return sabnzbd.config.get_config(section, '%s_prio_%s' % (section, gtype))() > 0
except TypeError:
logging.debug('Incorrect Notify option %s:%s_prio_%s', section, section, gtype)
#------------------------------------------------------------------------------
def send_notification(title , msg, gtype):
""" Send Notification message
"""
# Notification Center
if sabnzbd.DARWIN_VERSION > 7 and sabnzbd.cfg.ncenter_enable():
if check_classes(gtype, 'ncenter'):
send_notification_center(title, msg, gtype)
# Growl
if sabnzbd.cfg.growl_enable() and check_classes(gtype, 'growl'):
if _HAVE_CLASSIC_GROWL and not sabnzbd.cfg.growl_server():
return send_local_growl(title, msg, gtype)
else:
Thread(target=send_growl, args=(title, msg, gtype)).start()
time.sleep(0.5)
# Prowl
if sabnzbd.cfg.prowl_enable():
if sabnzbd.cfg.prowl_apikey():
Thread(target=send_prowl, args=(title, msg, gtype)).start()
time.sleep(0.5)
# Pushover
if sabnzbd.cfg.pushover_enable():
if sabnzbd.cfg.pushover_token():
Thread(target=send_pushover, args=(title, msg, gtype)).start()
time.sleep(0.5)
# Pushbullet
if sabnzbd.cfg.pushbullet_enable():
if sabnzbd.cfg.pushbullet_apikey():
Thread(target=send_pushbullet, args=(title, msg, gtype)).start()
time.sleep(0.5)
# NTFOSD
if have_ntfosd() and sabnzbd.cfg.ntfosd_enable() and check_classes(gtype, 'ntfosd'):
send_notify_osd(title, msg)
#------------------------------------------------------------------------------
def reset_growl():
""" Reset Growl (after changing language)
"""
global _GROWL, _GROWL_REG
_GROWL = None
_GROWL_REG = False
#------------------------------------------------------------------------------
def register_growl(growl_server, growl_password):
""" Register this app with Growl
"""
error = None
host, port = sabnzbd.misc.split_host(growl_server or '')
sys_name = hostname(host)
# 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 NOTIFY_KEYS],
hostname = host or 'localhost',
port = port or 23053,
password = growl_password or None
)
try:
ret = growler.register()
if ret is None or isinstance(ret, bool):
logging.info('Registered with Growl')
ret = growler
else:
error = 'Cannot register with Growl %s' % str(ret)
logging.debug(error)
del growler
ret = None
except socket.error, err:
error = 'Cannot register with Growl %s' % str(err)
logging.debug(error)
del growler
ret = None
except:
error = 'Unknown Growl registration error'
logging.debug(error)
logging.info("Traceback: ", exc_info = True)
del growler
ret = None
return ret, error
#------------------------------------------------------------------------------
def send_growl(title , msg, gtype, test=None):
""" Send Growl message
"""
global _GROWL, _GROWL_REG, _GROWL_DATA
# support testing values from UI
if test:
growl_server = test.get('growl_server') or None
growl_password = test.get('growl_password') or None
else:
growl_server = sabnzbd.cfg.growl_server()
growl_password = sabnzbd.cfg.growl_password()
for n in (0, 1):
if not _GROWL_REG: _GROWL = None
if (growl_server, growl_password) != _GROWL_DATA:
reset_growl()
if not _GROWL:
_GROWL, error = register_growl(growl_server, growl_password)
if _GROWL:
assert isinstance(_GROWL, GrowlNotifier)
_GROWL_REG = True
if isinstance(msg, unicode):
msg = msg.decode('utf-8')
elif not isinstance(msg, str):
msg = str(msg)
logging.debug('Send to Growl: %s %s %s', gtype, title, msg)
try:
ret = _GROWL.notify(
noteType = Tx(NOTIFICATION.get(gtype, 'other')),
title = title,
description = unicoder(msg),
)
if ret is None or isinstance(ret, bool):
return None
elif ret[0] == '401':
_GROWL = False
else:
logging.debug('Growl error %s', ret)
return 'Growl error %s', ret
except socket.error, err:
error = 'Growl error %s' % err
logging.debug(error)
return error
except:
error = 'Growl error (unknown)'
logging.debug(error)
return error
else:
return error
return None
#------------------------------------------------------------------------------
# Local OSX Growl support
#
if _HAVE_CLASSIC_GROWL:
_local_growl = None
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 """
global _local_growl
if not _local_growl:
notes = [Tx(NOTIFICATION[key]) for key in NOTIFY_KEYS]
_local_growl = Growl.GrowlNotifier(
applicationName = 'SABnzbd',
applicationIcon = _OSX_ICON,
notifications = notes,
defaultNotifications = notes
)
_local_growl.register()
_local_growl.notify(Tx(NOTIFICATION.get(gtype, 'other')), title, msg)
return None
#------------------------------------------------------------------------------
# Ubuntu NotifyOSD Support
#
_NTFOSD = False
def send_notify_osd(title, message):
""" Send a message to NotifyOSD
"""
global _NTFOSD
if not _HAVE_NTFOSD:
return T('Not available') #: Function is not available on this OS
error = 'NotifyOSD not working'
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', title, message)
try:
note = pynotify.Notification(title, message, icon)
note.show()
except:
# Apparently not implemented on this system
logging.info(error)
return error
return None
else:
return error
def ncenter_path():
""" Return path of Notification Center tool, if it exists """
tool = os.path.normpath(os.path.join(sabnzbd.DIR_PROG, '../Resources/SABnzbd.app/Contents/MacOS/SABnzbd'))
if os.path.exists(tool):
return tool
else:
return None
def send_notification_center(title, msg, gtype):
""" Send message to Mountain Lion's Notification Center """
if sabnzbd.DARWIN_VERSION < 8:
return T('Not available') #: Function is not available on this OS
tool = ncenter_path()
if tool:
try:
command = [tool, '-title', title, '-message', msg, '-group', Tx(NOTIFICATION.get(gtype, 'other')),
'-sender', 'org.sabnzbd.team']
proc = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=False)
output = proc.stdout.read()
proc.wait()
if 'Notification delivered' in output or 'Removing previously' in output:
output = ''
except:
logging.info('Cannot run notifier "%s"', tool)
logging.debug("Traceback: ", exc_info = True)
output = 'Notifier tool crashed'
else:
output = 'Notifier app not found'
return output.strip('*\n ')
#------------------------------------------------------------------------------
def hostname(host=True):
""" Return host's pretty name """
if sabnzbd.WIN32:
sys_name = os.environ.get('computername', 'unknown')
else:
try:
sys_name = os.uname()[1]
except:
sys_name = 'unknown'
if host:
return '@%s' % sys_name.lower()
else:
return ''
#------------------------------------------------------------------------------
def send_prowl(title, msg, gtype, force=False, test=None):
""" Send message to Prowl """
if test:
apikey = test.get('prowl_apikey')
else:
apikey = sabnzbd.cfg.prowl_apikey()
if not apikey:
return T('Cannot send, missing required data')
title = Tx(NOTIFICATION.get(gtype, 'other'))
title = urllib2.quote(title.encode('utf8'))
msg = urllib2.quote(msg.encode('utf8'))
prio = -3
if gtype == 'startup' : prio = sabnzbd.cfg.prowl_prio_startup()
if gtype == 'download' : prio = sabnzbd.cfg.prowl_prio_download()
if gtype == 'pp' : prio = sabnzbd.cfg.prowl_prio_pp()
if gtype == 'complete' : prio = sabnzbd.cfg.prowl_prio_complete()
if gtype == 'failed' : prio = sabnzbd.cfg.prowl_prio_failed()
if gtype == 'disk-full': prio = sabnzbd.cfg.prowl_prio_disk_full()
if gtype == 'warning': prio = sabnzbd.cfg.prowl_prio_warning()
if gtype == 'error': prio = sabnzbd.cfg.prowl_prio_error()
if gtype == 'queue_done': prio = sabnzbd.cfg.prowl_prio_queue_done()
if gtype == 'other': prio = sabnzbd.cfg.prowl_prio_other()
if force: prio = 0
if prio > -3:
url = 'https://api.prowlapp.com/publicapi/add?apikey=%s&application=SABnzbd' \
'&event=%s&description=%s&priority=%d' % (apikey, title, msg, prio)
try:
urllib2.urlopen(url)
return ''
except:
logging.warning(T('Failed to send Prowl message'))
logging.info("Traceback: ", exc_info = True)
return T('Failed to send Prowl message')
return ''
#------------------------------------------------------------------------------
def send_pushover(title, msg, gtype, force=False, test=None):
""" Send message to pushover """
if test:
apikey = test.get('pushover_token')
userkey = test.get('pushover_userkey')
device = test.get('pushover_device')
else:
apikey = sabnzbd.cfg.pushover_token()
userkey = sabnzbd.cfg.pushover_userkey()
device = sabnzbd.cfg.pushover_device()
if not apikey or not userkey:
return T('Cannot send, missing required data')
title = Tx(NOTIFICATION.get(gtype, 'other'))
prio = -2
if gtype == 'startup' : prio = sabnzbd.cfg.pushover_prio_startup()
if gtype == 'download' : prio = sabnzbd.cfg.pushover_prio_download()
if gtype == 'pp' : prio = sabnzbd.cfg.pushover_prio_pp()
if gtype == 'complete' : prio = sabnzbd.cfg.pushover_prio_complete()
if gtype == 'failed' : prio = sabnzbd.cfg.pushover_prio_failed()
if gtype == 'disk-full': prio = sabnzbd.cfg.pushover_prio_disk_full()
if gtype == 'warning': prio = sabnzbd.cfg.pushover_prio_warning()
if gtype == 'error': prio = sabnzbd.cfg.pushover_prio_error()
if gtype == 'queue_done': prio = sabnzbd.cfg.pushover_prio_queue_done()
if gtype == 'other': prio = sabnzbd.cfg.pushover_prio_other()
if force: prio = 1
if prio > -2:
try:
conn = httplib.HTTPSConnection("api.pushover.net:443")
conn.request("POST", "/1/messages.json", urllib.urlencode({
"token": apikey,
"user": userkey,
"device": device,
"title": title,
"message": msg,
"priority": prio
}), { "Content-type": "application/x-www-form-urlencoded" })
res = conn.getresponse()
if res.status != 200:
logging.error(T('Bad response from Pushover (%s): %s'), res.status, res.read())
except:
logging.warning(T('Failed to send pushover message'))
logging.info("Traceback: ", exc_info = True)
return T('Failed to send pushover message')
return ''
#------------------------------------------------------------------------------
def send_pushbullet(title, msg, gtype, force=False, test=None):
""" Send message to Pushbullet """
if test:
apikey = test.get('pushbullet_apikey')
device = test.get('pushbullet_device')
else:
apikey = sabnzbd.cfg.pushbullet_apikey()
device = sabnzbd.cfg.pushbullet_device()
if not apikey:
return T('Cannot send, missing required data')
title = u'SABnzbd: ' + Tx(NOTIFICATION.get(gtype, 'other'))
prio = 0
if gtype == 'startup' : prio = sabnzbd.cfg.pushbullet_prio_startup()
if gtype == 'download' : prio = sabnzbd.cfg.pushbullet_prio_download()
if gtype == 'pp' : prio = sabnzbd.cfg.pushbullet_prio_pp()
if gtype == 'complete' : prio = sabnzbd.cfg.pushbullet_prio_complete()
if gtype == 'failed' : prio = sabnzbd.cfg.pushbullet_prio_failed()
if gtype == 'disk-full': prio = sabnzbd.cfg.pushbullet_prio_disk_full()
if gtype == 'warning': prio = sabnzbd.cfg.pushbullet_prio_warning()
if gtype == 'error': prio = sabnzbd.cfg.pushbullet_prio_error()
if gtype == 'queue_done': prio = sabnzbd.cfg.pushbullet_prio_queue_done()
if gtype == 'other': prio = sabnzbd.cfg.pushbullet_prio_other()
if force: prio = 1
if prio > 0:
try:
conn = httplib.HTTPSConnection('api.pushbullet.com:443')
conn.request('POST', '/v2/pushes', \
json.dumps({
'type' : 'note',
'device' : device,
'title' : title,
'body' : msg }),
headers={'Authorization' : 'Bearer ' + apikey,
'Content-type': 'application/json'})
res = conn.getresponse()
if res.status != 200:
logging.error(T('Bad response from Pushbullet (%s): %s'), res.status, res.read())
else:
logging.info('Successfully sent to Pushbullet')
except:
logging.warning(T('Failed to send pushbullet message'))
logging.info('Traceback: ', exc_info = True)
return T('Failed to send pushbullet message')
return ''