Browse Source

Use PyObjC instead of C-module to send notifications on macOS

pull/1364/head
Safihre 5 years ago
parent
commit
e7efb3f804
  1. 1
      .travis.yml
  2. 3
      SABnzbd.py
  3. 140
      po/main/SABnzbd.pot
  4. 2
      sabnzbd/interface.py
  5. 280
      sabnzbd/notifier.py

1
.travis.yml

@ -68,6 +68,7 @@ script:
sabnzbd/misc.py
sabnzbd/lang.py
sabnzbd/nzbparser.py
sabnzbd/notifier.py
sabnzbd/rss.py
sabnzbd/par2file.py
sabnzbd/version.py

3
SABnzbd.py

@ -1384,8 +1384,7 @@ def main():
if sabnzbd.FOUNDATION:
import sabnzbd.osxmenu
sabnzbd.osxmenu.notify("SAB_Launched", None)
notifier.send_notification('SABnzbd%s' % notifier.hostname(),
T('SABnzbd %s started') % sabnzbd.__version__, 'startup')
notifier.send_notification('SABnzbd', T('SABnzbd %s started') % sabnzbd.__version__, 'startup')
# Now's the time to check for a new version
check_latest_version()
autorestarted = False

140
po/main/SABnzbd.pot

@ -12,7 +12,7 @@ msgstr ""
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=ASCII\n"
"Content-Transfer-Encoding: 7bit\n"
"POT-Creation-Date: 2020-02-22 10:13+0100\n"
"POT-Creation-Date: 2020-02-26 13:46+0100\n"
"Generated-By: pygettext.py 1.5\n"
@ -104,62 +104,6 @@ msgstr ""
msgid "SABnzbd shutdown finished"
msgstr ""
#: sabnzbd/utils/servertests.py
msgid "The hostname is not set."
msgstr ""
#: sabnzbd/utils/servertests.py
msgid "There are no connections set. Please set at least one connection."
msgstr ""
#: sabnzbd/utils/servertests.py
msgid "Password masked in ******, please re-enter"
msgstr ""
#: sabnzbd/utils/servertests.py
msgid "Invalid server details"
msgstr ""
#: sabnzbd/utils/servertests.py
msgid "Timed out: Try enabling SSL or connecting on a different port."
msgstr ""
#: sabnzbd/utils/servertests.py
msgid "Timed out"
msgstr ""
#: sabnzbd/utils/servertests.py
msgid "Unknown SSL protocol: Try disabling SSL or connecting on a different port."
msgstr ""
#: sabnzbd/utils/servertests.py
msgid "Invalid server address."
msgstr ""
#: sabnzbd/utils/servertests.py
msgid "Server quit during login sequence."
msgstr ""
#: sabnzbd/utils/servertests.py
msgid "Server requires username and password."
msgstr ""
#: sabnzbd/utils/servertests.py
msgid "Connection Successful!"
msgstr ""
#: sabnzbd/utils/servertests.py # sabnzbd/interface.py # sabnzbd/newswrapper.py
msgid "Authentication failed, check username/password."
msgstr ""
#: sabnzbd/utils/servertests.py
msgid "Too many connections, please pause downloading or try again later"
msgstr ""
#: sabnzbd/utils/servertests.py
msgid "Could not determine connection result (%s)"
msgstr ""
#: sabnzbd/__init__.py [Warning message]
msgid "Signal %s caught, saving and exiting..."
msgstr ""
@ -606,6 +550,10 @@ msgstr ""
msgid "Try our new skin Glitter! Fresh new design that is optimized for desktop and mobile devices. Go to Config -> General to change your skin."
msgstr ""
#: sabnzbd/interface.py # sabnzbd/newswrapper.py # sabnzbd/utils/servertests.py
msgid "Authentication failed, check username/password."
msgstr ""
#: sabnzbd/interface.py
msgid "Unsuccessful login attempt from %s"
msgstr ""
@ -998,6 +946,10 @@ msgstr ""
msgid "Not available"
msgstr ""
#: sabnzbd/notifier.py
msgid "Failed to send macOS notification"
msgstr ""
#: sabnzbd/notifier.py [Warning message] # sabnzbd/notifier.py
msgid "Failed to send Prowl message"
msgstr ""
@ -1470,7 +1422,7 @@ msgstr ""
msgid "Indexer id (%s) not found for ratings file"
msgstr ""
#: sabnzbd/rating.py # sabnzbd/skintext.py [Address of Growl server]
#: sabnzbd/rating.py
msgid "Server address"
msgstr ""
@ -3558,26 +3510,6 @@ msgstr ""
msgid "Notification Sent!"
msgstr ""
#: sabnzbd/skintext.py [Header Growl section]
msgid "Growl"
msgstr ""
#: sabnzbd/skintext.py [Don't translate "Growl"]
msgid "Enable Growl"
msgstr ""
#: sabnzbd/skintext.py [Don't translate "Growl"]
msgid "Only use for remote Growl server (host:port)"
msgstr ""
#: sabnzbd/skintext.py [Growl server password]
msgid "Server password"
msgstr ""
#: sabnzbd/skintext.py [Don't translate "Growl"]
msgid "Optional password for Growl server"
msgstr ""
#: sabnzbd/skintext.py [Don't translate "NotifyOSD"]
msgid "Enable NotifyOSD"
msgstr ""
@ -4510,3 +4442,55 @@ msgstr ""
msgid "URL Fetching failed; %s"
msgstr ""
#: sabnzbd/utils/servertests.py
msgid "The hostname is not set."
msgstr ""
#: sabnzbd/utils/servertests.py
msgid "There are no connections set. Please set at least one connection."
msgstr ""
#: sabnzbd/utils/servertests.py
msgid "Password masked in ******, please re-enter"
msgstr ""
#: sabnzbd/utils/servertests.py
msgid "Invalid server details"
msgstr ""
#: sabnzbd/utils/servertests.py
msgid "Timed out: Try enabling SSL or connecting on a different port."
msgstr ""
#: sabnzbd/utils/servertests.py
msgid "Timed out"
msgstr ""
#: sabnzbd/utils/servertests.py
msgid "Unknown SSL protocol: Try disabling SSL or connecting on a different port."
msgstr ""
#: sabnzbd/utils/servertests.py
msgid "Invalid server address."
msgstr ""
#: sabnzbd/utils/servertests.py
msgid "Server quit during login sequence."
msgstr ""
#: sabnzbd/utils/servertests.py
msgid "Server requires username and password."
msgstr ""
#: sabnzbd/utils/servertests.py
msgid "Connection Successful!"
msgstr ""
#: sabnzbd/utils/servertests.py
msgid "Too many connections, please pause downloading or try again later"
msgstr ""
#: sabnzbd/utils/servertests.py
msgid "Could not determine connection result (%s)"
msgstr ""

2
sabnzbd/interface.py

@ -2554,7 +2554,7 @@ class ConfigNotify:
conf['categories'] = list_cats(False)
conf['lastmail'] = self.__lastmail
conf['have_ntfosd'] = sabnzbd.notifier.have_ntfosd()
conf['have_ncenter'] = sabnzbd.DARWIN and bool(sabnzbd.notifier.ncenter_path())
conf['have_ncenter'] = sabnzbd.DARWIN and sabnzbd.FOUNDATION
conf['scripts'] = list_scripts(default=False, none=True)
for kw in LIST_EMAIL:

280
sabnzbd/notifier.py

@ -32,13 +32,16 @@ from threading import Thread
import sabnzbd
import sabnzbd.cfg
from sabnzbd.encoding import platform_btou
from sabnzbd.constants import NOTIFY_KEYS
from sabnzbd.misc import split_host
from sabnzbd.filesystem import make_script_path
from sabnzbd.newsunpack import external_script
if sabnzbd.FOUNDATION:
import Foundation
import objc
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():
@ -47,8 +50,8 @@ try:
_HAVE_NTFOSD = True
# Check for working version, not all pynotify are the same
if not hasattr(pynotify, 'init'):
_HAVE_NTFOSD = False
if not hasattr(pynotify, "init"):
_HAVE_NTFOSD = False
except:
_HAVE_NTFOSD = False
@ -58,26 +61,26 @@ except:
##############################################################################
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
"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
}
def get_icon():
icon = os.path.join(os.path.join(sabnzbd.DIR_PROG, 'icons'), 'sabnzbd.ico')
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')
icon = os.path.join(sabnzbd.DIR_PROG, "sabnzbd.ico")
if os.path.isfile(icon):
fp = open(icon, 'rb')
fp = open(icon, "rb")
icon = fp.read()
fp.close()
else:
@ -93,18 +96,18 @@ def 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
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)
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))()
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)
logging.debug("Incorrect Notify option %s:%s_prio_%s", section, section, gtype)
return -1000
@ -117,10 +120,10 @@ def check_cat(section, job_cat, keyword=None):
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
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)
logging.debug("Incorrect Notify option %s:%s_cats", section, section)
return True
@ -128,37 +131,37 @@ 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):
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):
if check_classes(gtype, "acenter") and check_cat("acenter", job_cat):
send_windows(title, msg, gtype)
# Prowl
if sabnzbd.cfg.prowl_enable() and check_cat('prowl', job_cat):
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_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'):
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_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):
if check_classes(gtype, "ntfosd") and check_cat("ntfosd", job_cat):
send_notify_osd(title, msg)
@ -166,17 +169,19 @@ def send_notification(title, msg, gtype, job_cat=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
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')
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)
logging.info("Send to NotifyOSD: %s / %s", title, message)
try:
note = pynotify.Notification(title, message, icon)
note.show()
@ -189,88 +194,64 @@ def send_notify_osd(title, message):
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', T(NOTIFICATION.get(gtype, 'other'))]
proc = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=False)
output = platform_btou(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 ''
""" Send message to macOS Notification Center """
try:
NSUserNotification = objc.lookUpClass("NSUserNotification")
NSUserNotificationCenter = objc.lookUpClass("NSUserNotificationCenter")
notification = NSUserNotification.alloc().init()
notification.setTitle_(title)
notification.setSubtitle_(T(NOTIFICATION.get(gtype, "other")))
notification.setInformativeText_(msg)
notification.setSoundName_("NSUserNotificationDefaultSoundName")
notification.setDeliveryDate_(Foundation.NSDate.dateWithTimeInterval_sinceDate_(0, Foundation.NSDate.date()))
NSUserNotificationCenter.defaultUserNotificationCenter().scheduleNotification_(notification)
except:
logging.info(T("Failed to send macOS notification"))
logging.debug("Traceback: ", exc_info=True)
return T("Failed to send macOS notification")
def send_prowl(title, msg, gtype, force=False, test=None):
""" Send message to Prowl """
if test:
apikey = test.get('prowl_apikey')
apikey = test.get("prowl_apikey")
else:
apikey = sabnzbd.cfg.prowl_apikey()
if not apikey:
return T('Cannot send, missing required data')
return T("Cannot send, missing required data")
title = T(NOTIFICATION.get(gtype, 'other'))
title = urllib.parse.quote(title.encode('utf8'))
msg = urllib.parse.quote(msg.encode('utf8'))
prio = get_prio(gtype, 'prowl')
title = T(NOTIFICATION.get(gtype, "other"))
title = urllib.parse.quote(title.encode("utf8"))
msg = urllib.parse.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)
url = (
"https://api.prowlapp.com/publicapi/add?apikey=%s&application=SABnzbd"
"&event=%s&description=%s&priority=%d" % (apikey, title, msg, prio)
)
try:
urllib.request.urlopen(url)
return ''
return ""
except:
logging.warning(T('Failed to send Prowl message'))
logging.warning(T("Failed to send Prowl message"))
logging.info("Traceback: ", exc_info=True)
return T('Failed to send Prowl message')
return ''
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')
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()
@ -278,101 +259,107 @@ def send_pushover(title, msg, gtype, force=False, test=None):
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')
return T("Cannot send, missing required data")
title = T(NOTIFICATION.get(gtype, 'other'))
prio = get_prio(gtype, 'pushover')
title = T(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
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,
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 = http.client.HTTPSConnection("api.pushover.net:443")
conn.request("POST", "/1/messages.json", urllib.parse.urlencode(body),
{"Content-type": "application/x-www-form-urlencoded"})
conn.request(
"POST",
"/1/messages.json",
urllib.parse.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')
logging.error(T("Bad response from Pushover (%s): %s"), res.status, res.read())
return T("Failed to send pushover message")
else:
return ''
return ""
except:
logging.warning(T('Failed to send pushover message'))
logging.warning(T("Failed to send pushover message"))
logging.info("Traceback: ", exc_info=True)
return T('Failed to send pushover message')
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')
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')
return T("Cannot send, missing required data")
title = 'SABnzbd: ' + T(NOTIFICATION.get(gtype, 'other'))
title = "SABnzbd: " + T(NOTIFICATION.get(gtype, "other"))
try:
conn = http.client.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'})
conn = http.client.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())
logging.error(T("Bad response from Pushbullet (%s): %s"), res.status, res.read())
else:
logging.info('Successfully sent to Pushbullet')
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 ''
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')
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 = 'SABnzbd: ' + T(NOTIFICATION.get(gtype, 'other'))
return T("Cannot send, missing required data")
title = "SABnzbd: " + T(NOTIFICATION.get(gtype, "other"))
if force or check_classes(gtype, 'nscript'):
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)
@ -380,10 +367,10 @@ def send_nscript(title, msg, gtype, force=False, test=None):
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)
logging.info("Successfully executed notification script " + script_path)
else:
return T('Notification script "%s" does not exist') % script_path
return ''
return ""
def send_windows(title, msg, gtype):
@ -391,8 +378,7 @@ def send_windows(title, msg, gtype):
try:
sabnzbd.WINTRAY.sendnotification(title, msg)
except:
logging.info(T('Failed to send Windows notification'))
logging.info(T("Failed to send Windows notification"))
logging.debug("Traceback: ", exc_info=True)
return T('Failed to send Windows notification')
return T("Failed to send Windows notification")
return None

Loading…
Cancel
Save