From ef8f40ccc4abb677e96243ab096d0c818c80e8f3 Mon Sep 17 00:00:00 2001 From: Dmitri Bogomolov <4glitch@gmail.com> Date: Fri, 10 Mar 2017 01:00:54 +0200 Subject: [PATCH] Moved notifications to plugins (including sound) --- setup.py | 17 ++- src/bitmessageqt/__init__.py | 194 +++++++++++++--------------- src/plugins/notification_notify2.py | 13 ++ src/plugins/plugin.py | 8 +- src/plugins/sound_playfile.py | 37 ++++++ 5 files changed, 160 insertions(+), 109 deletions(-) create mode 100644 src/plugins/notification_notify2.py create mode 100644 src/plugins/sound_playfile.py diff --git a/setup.py b/setup.py index 9357284b..c7f7fab5 100644 --- a/setup.py +++ b/setup.py @@ -254,7 +254,9 @@ if __name__ == "__main__": install_requires=installRequires, extras_require={ 'qrcode': ['qrcode'], - 'pyopencl': ['pyopencl'] + 'pyopencl': ['pyopencl'], + 'notify2': ['pygobject', 'notify2'], + 'sound:platform_system=="Windows"': ['winsound'] }, classifiers=[ "License :: OSI Approved :: MIT License" @@ -278,9 +280,16 @@ if __name__ == "__main__": 'popMenuYourIdentities.qrcode = ' 'pybitmessage.plugins.qrcodeui [qrcode]' ], - # 'console_scripts': [ - # 'pybitmessage = pybitmessage.bitmessagemain:main' - # ] + 'notification.message': [ + 'notify2 = pybitmessage.plugins.notification_notify2' + '[notify2]' + ], + 'notification.sound': [ + 'fallback = pybitmessage.plugins.sound_playfile [sound]' + ], + # 'console_scripts': [ + # 'pybitmessage = pybitmessage.bitmessagemain:main' + # ] }, scripts=['src/pybitmessage'], cmdclass={'install': InstallCmd} diff --git a/src/bitmessageqt/__init__.py b/src/bitmessageqt/__init__.py index 014831bf..44dcb8ec 100644 --- a/src/bitmessageqt/__init__.py +++ b/src/bitmessageqt/__init__.py @@ -4,8 +4,6 @@ try: import gi gi.require_version('MessagingMenu', '1.0') from gi.repository import MessagingMenu - gi.require_version('Notify', '0.7') - from gi.repository import Notify withMessagingMenu = True except (ImportError, ValueError): MessagingMenu = None @@ -62,9 +60,8 @@ import platform import textwrap import debug import random -import subprocess import string -import datetime +from datetime import datetime, timedelta from helper_sql import * import helper_search import l10n @@ -153,7 +150,7 @@ class MyForm(settingsmixin.SMainWindow): SOUND_CONNECTION_GREEN = 5 # the last time that a message arrival sound was played - lastSoundTime = datetime.datetime.now() - datetime.timedelta(days=1) + lastSoundTime = datetime.now() - timedelta(days=1) # the maximum frequency of message sounds in seconds maxSoundFrequencySec = 60 @@ -1368,39 +1365,41 @@ class MyForm(settingsmixin.SMainWindow): # returns true if the given sound category is a connection sound # rather than a received message sound def isConnectionSound(self, category): - if (category is self.SOUND_CONNECTED or - category is self.SOUND_DISCONNECTED or - category is self.SOUND_CONNECTION_GREEN): - return True - return False + return category in ( + self.SOUND_CONNECTED, + self.SOUND_DISCONNECTED, + self.SOUND_CONNECTION_GREEN + ) # play a sound def playSound(self, category, label): # filename of the sound to be played soundFilename = None - # whether to play a sound or not - play = True + def _choose_ext(basename): + for ext in ('.wav', '.mp3', '.oga'): + if os.path.isfile(basename + ext): + return ext # if the address had a known label in the address book - if label is not None: + if label: # Does a sound file exist for this particular contact? - if (os.path.isfile(state.appdata + 'sounds/' + label + '.wav') or - os.path.isfile(state.appdata + 'sounds/' + label + '.mp3')): - soundFilename = state.appdata + 'sounds/' + label - - # Avoid making sounds more frequently than the threshold. - # This suppresses playing sounds repeatedly when there - # are many new messages - if (soundFilename is None and - not self.isConnectionSound(category)): - # elapsed time since the last sound was played - dt = datetime.datetime.now() - self.lastSoundTime - # suppress sounds which are more frequent than the threshold - if dt.total_seconds() < self.maxSoundFrequencySec: - play = False + soundFilename = state.appdata + 'sounds/' + label + ext = _choose_ext(soundFilename) + if not ext: + soundFilename = None if soundFilename is None: + # Avoid making sounds more frequently than the threshold. + # This suppresses playing sounds repeatedly when there + # are many new messages + if not self.isConnectionSound(category): + # elapsed time since the last sound was played + dt = datetime.now() - self.lastSoundTime + # suppress sounds which are more frequent than the threshold + if dt.total_seconds() < self.maxSoundFrequencySec: + return + # the sound is for an address which exists in the address book if category is self.SOUND_KNOWN: soundFilename = state.appdata + 'sounds/known' @@ -1415,75 +1414,55 @@ class MyForm(settingsmixin.SMainWindow): soundFilename = state.appdata + 'sounds/disconnected' # sound when the connection status becomes green elif category is self.SOUND_CONNECTION_GREEN: - soundFilename = state.appdata + 'sounds/green' + soundFilename = state.appdata + 'sounds/green' - if soundFilename is not None and play is True: - if not self.isConnectionSound(category): - # record the last time that a received message sound was played - self.lastSoundTime = datetime.datetime.now() + if soundFilename is None: + return - # if not wav then try mp3 format - if not os.path.isfile(soundFilename + '.wav'): - soundFilename = soundFilename + '.mp3' - else: - soundFilename = soundFilename + '.wav' + if not self.isConnectionSound(category): + # record the last time that a received message sound was played + self.lastSoundTime = datetime.now() - if os.path.isfile(soundFilename): - if 'linux' in sys.platform: - # Note: QSound was a nice idea but it didn't work - if '.mp3' in soundFilename: - gst_available=False - try: - subprocess.call(["gst123", soundFilename], - stdin=subprocess.PIPE, - stdout=subprocess.PIPE) - gst_available=True - except: - logger.warning("WARNING: gst123 must be installed in order to play mp3 sounds") - if not gst_available: - try: - subprocess.call(["mpg123", soundFilename], - stdin=subprocess.PIPE, - stdout=subprocess.PIPE) - gst_available=True - except: - logger.warning("WARNING: mpg123 must be installed in order to play mp3 sounds") - else: - try: - subprocess.call(["aplay", soundFilename], - stdin=subprocess.PIPE, - stdout=subprocess.PIPE) - except: - logger.warning("WARNING: aplay must be installed in order to play WAV sounds") - elif sys.platform[0:3] == 'win': - # use winsound on Windows - import winsound - winsound.PlaySound(soundFilename, winsound.SND_FILENAME) + try: # try already known format + soundFilename += ext + except (TypeError, NameError): + ext = _choose_ext(soundFilename) + if not ext: + return + + soundFilename += ext + + self._player(soundFilename) # initialise the message notifier def notifierInit(self): - if withMessagingMenu: - Notify.init('pybitmessage') - - # shows a notification - def notifierShow(self, title, subtitle, fromCategory, label): - self.playSound(fromCategory, label) - - if withMessagingMenu: - n = Notify.Notification.new( - title, subtitle, 'notification-message-email') - try: - n.show() - except: - # n.show() has been known to throw this exception: - # gi._glib.GError: GDBus.Error:org.freedesktop.Notifications. - # MaxNotificationsExceeded: Exceeded maximum number of - # notifications - pass - return - else: + def _simple_notify( + title, subtitle, category, label=None, icon=None): self.tray.showMessage(title, subtitle, 1, 2000) + self._notifier = _simple_notify + self._player = QtGui.QSound.play + + if not get_plugins: + return + + for plugin in get_plugins('notification.message'): + self._notifier = plugin + break + + if not QtGui.QSound.isAvailable(): + for plugin in get_plugins('notification.sound'): + self._player = plugin + break + else: + logger.warning("No sound player plugin found!") + + def notifierShow( + self, title, subtitle, category, label=None, icon=None): + self.playSound(category, label) + self._notifier( + unicode(title), unicode(subtitle), category, label, icon) + # tree def treeWidgetKeyPressEvent(self, event): return self.handleKeyPress(event, self.getCurrentTreeWidget()) @@ -1663,15 +1642,18 @@ class MyForm(settingsmixin.SMainWindow): def setStatusIcon(self, color): # print 'setting status icon color' + _notifications_enabled = not BMConfigParser().getboolean( + 'bitmessagesettings', 'hidetrayconnectionnotifications') if color == 'red': self.pushButtonStatusIcon.setIcon( QIcon(":/newPrefix/images/redicon.png")) shared.statusIconColor = 'red' # if the connection is lost then show a notification - if self.connected and not BMConfigParser().getboolean('bitmessagesettings', 'hidetrayconnectionnotifications'): - self.notifierShow('Bitmessage', unicode(_translate( - "MainWindow", "Connection lost").toUtf8(),'utf-8'), - self.SOUND_DISCONNECTED, None) + if self.connected and _notifications_enabled: + self.notifierShow( + 'Bitmessage', + _translate("MainWindow", "Connection lost"), + self.SOUND_DISCONNECTED) if not BMConfigParser().safeGetBoolean('bitmessagesettings', 'upnp') and \ BMConfigParser().get('bitmessagesettings', 'socksproxytype') == "none": self.statusBar().showMessage(_translate( @@ -1689,10 +1671,11 @@ class MyForm(settingsmixin.SMainWindow): ":/newPrefix/images/yellowicon.png")) shared.statusIconColor = 'yellow' # if a new connection has been established then show a notification - if not self.connected and not BMConfigParser().getboolean('bitmessagesettings', 'hidetrayconnectionnotifications'): - self.notifierShow('Bitmessage', unicode(_translate( - "MainWindow", "Connected").toUtf8(),'utf-8'), - self.SOUND_CONNECTED, None) + if not self.connected and _notifications_enabled: + self.notifierShow( + 'Bitmessage', + _translate("MainWindow", "Connected"), + self.SOUND_CONNECTED) self.connected = True if self.actionStatus is not None: @@ -1705,10 +1688,11 @@ class MyForm(settingsmixin.SMainWindow): self.pushButtonStatusIcon.setIcon( QIcon(":/newPrefix/images/greenicon.png")) shared.statusIconColor = 'green' - if not self.connected and not BMConfigParser().getboolean('bitmessagesettings', 'hidetrayconnectionnotifications'): - self.notifierShow('Bitmessage', unicode(_translate( - "MainWindow", "Connected").toUtf8(),'utf-8'), - self.SOUND_CONNECTION_GREEN, None) + if not self.connected and _notifications_enabled: + self.notifierShow( + 'Bitmessage', + _translate("MainWindow", "Connected"), + self.SOUND_CONNECTION_GREEN) self.connected = True if self.actionStatus is not None: @@ -2253,8 +2237,14 @@ class MyForm(settingsmixin.SMainWindow): else: acct = ret self.propagateUnreadCount(acct.address) - if BMConfigParser().getboolean('bitmessagesettings', 'showtraynotifications'): - self.notifierShow(unicode(_translate("MainWindow",'New Message').toUtf8(),'utf-8'), unicode(_translate("MainWindow",'From ').toUtf8(),'utf-8') + unicode(acct.fromLabel, 'utf-8'), self.SOUND_UNKNOWN, None) + if BMConfigParser().getboolean( + 'bitmessagesettings', 'showtraynotifications'): + self.notifierShow( + _translate("MainWindow", "New Message"), + _translate("MainWindow", "From %1").arg( + unicode(acct.fromLabel, 'utf-8')), + self.SOUND_UNKNOWN + ) if self.getCurrentAccount() is not None and ((self.getCurrentFolder(treeWidget) != "inbox" and self.getCurrentFolder(treeWidget) is not None) or self.getCurrentAccount(treeWidget) != acct.address): # Ubuntu should notify of new message irespective of whether it's in current message list or not self.ubuntuMessagingMenuUpdate(True, None, acct.toLabel) diff --git a/src/plugins/notification_notify2.py b/src/plugins/notification_notify2.py new file mode 100644 index 00000000..90f09df3 --- /dev/null +++ b/src/plugins/notification_notify2.py @@ -0,0 +1,13 @@ +# -*- coding: utf-8 -*- + +import gi +gi.require_version('Notify', '0.7') +from gi.repository import Notify + +Notify.init('pybitmessage') + + +def connect_plugin(title, subtitle, category, label, icon): + if not icon: + icon = 'mail-message-new' if category == 2 else 'pybitmessage' + Notify.Notification.new(title, subtitle, icon).show() diff --git a/src/plugins/plugin.py b/src/plugins/plugin.py index 288be48a..395f0e9d 100644 --- a/src/plugins/plugin.py +++ b/src/plugins/plugin.py @@ -3,10 +3,12 @@ import pkg_resources -def get_plugins(group, point, name=None): +def get_plugins(group, point='', name=None): for plugin in pkg_resources.iter_entry_points(group): - if plugin.name.startswith(point): + if plugin.name == name or plugin.name.startswith(point): try: yield plugin.load().connect_plugin - except (AttributeError, pkg_resources.DistributionNotFound): + except (AttributeError, + pkg_resources.DistributionNotFound, + pkg_resources.UnknownExtra): continue diff --git a/src/plugins/sound_playfile.py b/src/plugins/sound_playfile.py new file mode 100644 index 00000000..03a164af --- /dev/null +++ b/src/plugins/sound_playfile.py @@ -0,0 +1,37 @@ +# -*- coding: utf-8 -*- + + +try: + import winsound + + def connect_plugin(sound_file): + winsound.PlaySound(sound_file, winsound.SND_FILENAME) +except ImportError: + import os + import subprocess + + play_cmd = {} + + def connect_plugin(sound_file): + global play_cmd + + ext = os.path.splitext(sound_file)[-1] + try: + subprocess.call([play_cmd[ext], sound_file]) + return + except (KeyError, AttributeError): + pass + + programs = ['gst123'] + if ext == '.wav': + programs.append('aplay') + elif ext == '.mp3': + programs += ['mpg123', 'mpg321', 'mpg321-mpg123'] + for cmd in programs: + try: + subprocess.call([cmd, sound_file]) + except OSError: + pass # log here! + else: + play_cmd[ext] = cmd + break