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.
563 lines
19 KiB
563 lines
19 KiB
#!/usr/bin/python -OO
|
|
# Copyright 2007-2018 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.notifier - Send notifications to any notification services
|
|
"""
|
|
|
|
from __future__ import with_statement
|
|
import os.path
|
|
import logging
|
|
import urllib2
|
|
import httplib
|
|
import urllib
|
|
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 sabnzbd.misc import split_host, make_script_path
|
|
from sabnzbd.newsunpack import external_script
|
|
|
|
from gntp.core import GNTPRegister
|
|
from gntp.notifier import GrowlNotifier
|
|
import gntp.errors
|
|
|
|
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
|
|
|
|
# Check for working version, not all pynotify are the same
|
|
if not hasattr(pynotify, 'init'):
|
|
_HAVE_NTFOSD = False
|
|
except:
|
|
_HAVE_NTFOSD = False
|
|
|
|
|
|
##############################################################################
|
|
# Define translatable message table
|
|
##############################################################################
|
|
TT = lambda x: x
|
|
NOTIFICATION = {
|
|
'startup': TT('Startup/Shutdown'), #: Notification
|
|
'download': TT('Added NZB'), #: Notification
|
|
'pp': TT('Post-processing started'), # : Notification
|
|
'complete': TT('Job finished'), #: Notification
|
|
'failed': TT('Job failed'), #: Notification
|
|
'warning': TT('Warning'), #: Notification
|
|
'error': TT('Error'), #: Notification
|
|
'disk_full': TT('Disk full'), #: Notification
|
|
'queue_done': TT('Queue finished'), #: Notification
|
|
'new_login': TT('User logged in'), #: Notification
|
|
'other': TT('Other Messages') #: Notification
|
|
}
|
|
|
|
##############################################################################
|
|
# 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):
|
|
fp = open(icon, 'rb')
|
|
icon = fp.read()
|
|
fp.close()
|
|
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)
|
|
return False
|
|
|
|
|
|
def get_prio(gtype, section):
|
|
""" Check prio of `gtype` in `section` """
|
|
try:
|
|
return sabnzbd.config.get_config(section, '%s_prio_%s' % (section, gtype))()
|
|
except TypeError:
|
|
logging.debug('Incorrect Notify option %s:%s_prio_%s', section, section, gtype)
|
|
return -1000
|
|
|
|
|
|
def check_cat(section, job_cat, keyword=None):
|
|
""" Check if `job_cat` is enabled in `section`.
|
|
* = All, if no other categories selected.
|
|
"""
|
|
if not job_cat:
|
|
return True
|
|
try:
|
|
if not keyword:
|
|
keyword = section
|
|
section_cats = sabnzbd.config.get_config(section, '%s_cats' % keyword)()
|
|
return ['*'] == section_cats or job_cat in section_cats
|
|
except TypeError:
|
|
logging.debug('Incorrect Notify option %s:%s_cats', section, section)
|
|
return True
|
|
|
|
|
|
def send_notification(title, msg, gtype, job_cat=None):
|
|
""" Send Notification message """
|
|
# Notification Center
|
|
if sabnzbd.DARWIN and sabnzbd.cfg.ncenter_enable():
|
|
if check_classes(gtype, 'ncenter') and check_cat('ncenter', job_cat):
|
|
send_notification_center(title, msg, gtype)
|
|
|
|
# Windows
|
|
if sabnzbd.WIN32 and sabnzbd.cfg.acenter_enable():
|
|
if check_classes(gtype, 'acenter') and check_cat('acenter', job_cat):
|
|
send_windows(title, msg, gtype)
|
|
|
|
# Growl
|
|
if sabnzbd.cfg.growl_enable() and check_classes(gtype, 'growl') and check_cat('growl', job_cat):
|
|
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()
|
|
|
|
# Prowl
|
|
if sabnzbd.cfg.prowl_enable() and check_cat('prowl', job_cat):
|
|
if sabnzbd.cfg.prowl_apikey():
|
|
Thread(target=send_prowl, args=(title, msg, gtype)).start()
|
|
|
|
# Pushover
|
|
if sabnzbd.cfg.pushover_enable() and check_cat('pushover', job_cat):
|
|
if sabnzbd.cfg.pushover_token():
|
|
Thread(target=send_pushover, args=(title, msg, gtype)).start()
|
|
|
|
# Pushbullet
|
|
if sabnzbd.cfg.pushbullet_enable() and check_cat('pushbullet', job_cat):
|
|
if sabnzbd.cfg.pushbullet_apikey() and check_classes(gtype, 'pushbullet'):
|
|
Thread(target=send_pushbullet, args=(title, msg, gtype)).start()
|
|
|
|
# Notification script.
|
|
if sabnzbd.cfg.nscript_enable() and check_cat('nscript', job_cat):
|
|
if sabnzbd.cfg.nscript_script():
|
|
Thread(target=send_nscript, args=(title, msg, gtype)).start()
|
|
|
|
# NTFOSD
|
|
if have_ntfosd() and sabnzbd.cfg.ntfosd_enable():
|
|
if check_classes(gtype, 'ntfosd') and check_cat('ntfosd', job_cat):
|
|
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 = split_host(growl_server or '')
|
|
|
|
sys_name = hostname(host)
|
|
|
|
# Reduce logging of Growl in Debug/Info mode
|
|
logging.getLogger('gntp').setLevel(logging.WARNING)
|
|
|
|
# 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 (gntp.errors.NetworkError, gntp.errors.AuthError) as 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:
|
|
_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 (gntp.errors.NetworkError, gntp.errors.AuthError) as 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 """
|
|
tool = ncenter_path()
|
|
if tool:
|
|
try:
|
|
command = [tool, '-title', title, '-message', msg, '-group', Tx(NOTIFICATION.get(gtype, 'other'))]
|
|
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 = get_prio(gtype, 'prowl')
|
|
|
|
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()
|
|
emergency_retry = sabnzbd.cfg.pushover_emergency_retry()
|
|
emergency_expire = sabnzbd.cfg.pushover_emergency_expire()
|
|
if not apikey or not userkey:
|
|
return T('Cannot send, missing required data')
|
|
|
|
title = Tx(NOTIFICATION.get(gtype, 'other'))
|
|
prio = get_prio(gtype, 'pushover')
|
|
|
|
if force:
|
|
prio = 1
|
|
|
|
if prio == 2:
|
|
body = { "token": apikey,
|
|
"user": userkey,
|
|
"device": device,
|
|
"title": title,
|
|
"message": msg,
|
|
"priority": prio,
|
|
"retry": emergency_retry,
|
|
"expire": emergency_expire
|
|
}
|
|
return do_send_pushover(body)
|
|
if -3 < prio < 2:
|
|
body = { "token": apikey,
|
|
"user": userkey,
|
|
"device": device,
|
|
"title": title,
|
|
"message": msg,
|
|
"priority": prio,
|
|
}
|
|
return do_send_pushover(body)
|
|
|
|
def do_send_pushover(body):
|
|
try:
|
|
conn = httplib.HTTPSConnection("api.pushover.net:443")
|
|
conn.request("POST", "/1/messages.json", urllib.urlencode(body),
|
|
{"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())
|
|
return T('Failed to send pushover message')
|
|
else:
|
|
return ''
|
|
except:
|
|
logging.warning(T('Failed to send pushover message'))
|
|
logging.info("Traceback: ", exc_info=True)
|
|
return T('Failed to send pushover message')
|
|
|
|
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'))
|
|
|
|
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 ''
|
|
|
|
|
|
def send_nscript(title, msg, gtype, force=False, test=None):
|
|
""" Run user's notification script """
|
|
if test:
|
|
script = test.get('nscript_script')
|
|
parameters = test.get('nscript_parameters')
|
|
else:
|
|
script = sabnzbd.cfg.nscript_script()
|
|
parameters = sabnzbd.cfg.nscript_parameters()
|
|
if not script:
|
|
return T('Cannot send, missing required data')
|
|
title = u'SABnzbd: ' + Tx(NOTIFICATION.get(gtype, 'other'))
|
|
|
|
if force or check_classes(gtype, 'nscript'):
|
|
script_path = make_script_path(script)
|
|
if script_path:
|
|
output, ret = external_script(script_path, gtype, title, msg, parameters)
|
|
if ret:
|
|
logging.error(T('Script returned exit code %s and output "%s"') % (ret, output))
|
|
return T('Script returned exit code %s and output "%s"') % (ret, output)
|
|
else:
|
|
logging.info('Successfully executed notification script ' + script_path)
|
|
else:
|
|
return T('Notification script "%s" does not exist') % script_path
|
|
return ''
|
|
|
|
|
|
def send_windows(title, msg, gtype):
|
|
if sabnzbd.WINTRAY and not sabnzbd.WINTRAY.terminate:
|
|
try:
|
|
sabnzbd.WINTRAY.sendnotification(title, msg)
|
|
except:
|
|
logging.info(T('Failed to send Windows notification'))
|
|
logging.debug("Traceback: ", exc_info=True)
|
|
return T('Failed to send Windows notification')
|
|
return None
|
|
|
|
|