Update the Notifications and FreeSpace plugins descriptions.

On the Notifications plugin, moved the Core and GtkUi notification implementations to `core.py` and `gtkui.py` respectively.
This commit is contained in:
Pedro Algarvio 2009-11-22 06:56:50 +00:00
parent 127b577440
commit 7812f7b4e4
6 changed files with 332 additions and 333 deletions

View File

@ -77,7 +77,7 @@ class Core(CorePluginBase):
self._timer = task.LoopingCall(self.update) self._timer = task.LoopingCall(self.update)
else: else:
self._timer.stop() self._timer.stop()
self._interval = 15 #60 self._interval = 60 * 5 # every 5 minutes
if self.config['enabled']: if self.config['enabled']:
self._timer.start(self._interval, False) self._timer.start(self._interval, False)

View File

@ -22,9 +22,9 @@
# #
# You should have received a copy of the GNU General Public License # You should have received a copy of the GNU General Public License
# along with deluge. If not, write to: # along with deluge. If not, write to:
# The Free Software Foundation, Inc., # The Free Software Foundation, Inc.,
# 51 Franklin Street, Fifth Floor # 51 Franklin Street, Fifth Floor
# Boston, MA 02110-1301, USA. # Boston, MA 02110-1301, USA.
# #
# In addition, as a special exception, the copyright holders give # In addition, as a special exception, the copyright holders give
# permission to link the code of portions of this program with the OpenSSL # permission to link the code of portions of this program with the OpenSSL
@ -45,8 +45,8 @@ __author_email__ = "ufs@ufsoft.org"
__version__ = "0.1" __version__ = "0.1"
__url__ = "http://deluge.ufsoft.org/hg/Notification/" __url__ = "http://deluge.ufsoft.org/hg/Notification/"
__license__ = "GPLv3" __license__ = "GPLv3"
__description__ = "" __description__ = "Plugin which continuously checks for available free space."
__long_description__ = """""" __long_description__ = __description__
__pkg_data__ = {__plugin_name__.lower(): ["template/*", "data/*"]} __pkg_data__ = {__plugin_name__.lower(): ["template/*", "data/*"]}
setup( setup(
@ -67,7 +67,5 @@ setup(
%s = %s:CorePlugin %s = %s:CorePlugin
[deluge.plugin.gtkui] [deluge.plugin.gtkui]
%s = %s:GtkUIPlugin %s = %s:GtkUIPlugin
[deluge.plugin.webui] """ % ((__plugin_name__, __plugin_name__.lower())*2)
%s = %s:WebUIPlugin
""" % ((__plugin_name__, __plugin_name__.lower())*3)
) )

View File

@ -37,12 +37,9 @@
# statement from all source files in the program, then also delete it here. # statement from all source files in the program, then also delete it here.
# #
import smtplib from twisted.internet import defer
from twisted.internet import defer, threads
from deluge import component from deluge import component
from deluge.log import LOG as log from deluge.log import LOG as log
from deluge.ui.client import client
import deluge.common
try: try:
from deluge.event import known_events from deluge.event import known_events
@ -50,20 +47,6 @@ except ImportError:
# Old deluge version # Old deluge version
known_events = {} known_events = {}
try:
import pygame
SOUND_AVAILABLE = True
except ImportError:
SOUND_AVAILABLE = False
try:
import pynotify
POPUP_AVAILABLE = True
if deluge.common.windows_check():
POPUP_AVAILABLE = False
except ImportError:
POPUP_AVAILABLE = False
def get_resource(filename): def get_resource(filename):
import pkg_resources, os import pkg_resources, os
@ -154,304 +137,3 @@ class CustomNotifications(object):
log.debug("Notification failure using %s: %s", kind, failure) log.debug("Notification failure using %s: %s", kind, failure)
return failure return failure
class CoreNotifications(CustomNotifications):
def enable(self):
CustomNotifications.enable(self)
self.register_custom_email_notification('TorrentFinishedEvent',
self._on_torrent_finished_event)
def disable(self):
self.deregister_custom_email_notification('TorrentFinishedEvent')
CustomNotifications.disable(self)
def register_custom_email_notification(self, eventtype, handler):
"""This is used to register email notifications for custom event types.
:param event: str, the event name
:param handler: function, to be called when `:param:event` is emitted
Your handler should return a tuple of (email_subject, email_contents).
"""
self._register_custom_provider('email', eventtype, handler)
def deregister_custom_email_notification(self, eventtype):
self._deregister_custom_provider('email', eventtype)
def handle_custom_email_notification(self, result, eventtype):
if not self.config['smtp_enabled']:
return defer.succeed("SMTP notification not enabled.")
subject, message = result
log.debug("Spawning new thread to send email with subject: %s: %s",
subject, message)
# Spawn thread because we don't want Deluge to lock up while we send the
# email.
return threads.deferToThread(self._notify_email, subject, message)
def get_handled_events(self):
handled_events = []
for evt in sorted(known_events.keys()):
if known_events[evt].__module__.startswith('deluge.event'):
if evt not in ('TorrentFinishedEvent',):
# Skip all un-handled built-in events
continue
classdoc = known_events[evt].__doc__.strip()
handled_events.append((evt, classdoc))
log.debug("Handled Notification Events: %s", handled_events)
return handled_events
def _notify_email(self, subject='', message=''):
log.debug("Email prepared")
to_addrs = '; '.join(self.config['smtp_recipients'])
headers = """\
From: %(smtp_from)s
To: %(smtp_recipients)s
Subject: %(subject)s
""" % {'smtp_from': self.config['smtp_from'],
'subject': subject,
'smtp_recipients': to_addrs}
message = '\r\n'.join((headers + message).splitlines())
try:
try:
# Python 2.6
server = smtplib.SMTP(self.config["smtp_host"],
self.config["smtp_port"],
timeout=60)
except:
# Python 2.5
server = smtplib.SMTP(self.config["smtp_host"],
self.config["smtp_port"])
except Exception, err:
err_msg = _("There was an error sending the notification email:"
" %s") % err
log.error(err_msg)
return err
security_enabled = self.config['smtp_tls']
if security_enabled:
server.ehlo()
if not server.esmtp_features.has_key('starttls'):
log.warning("TLS/SSL enabled but server does not support it")
else:
server.starttls()
server.ehlo()
if self.config['smtp_user'] and self.config['smtp_pass']:
try:
server.login(self.config['smtp_user'], self.config['smtp_pass'])
except smtplib.SMTPHeloError, err:
err_msg = _("The server didn't reply properly to the helo "
"greeting: %s") % err
log.error(err_msg)
return err
except smtplib.SMTPAuthenticationError, err:
err_msg = _("The server didn't accept the username/password "
"combination: %s") % err
log.error(err_msg)
return err
try:
try:
server.sendmail(self.config['smtp_from'], to_addrs, message)
except smtplib.SMTPException, err:
err_msg = _("There was an error sending the notification email:"
" %s") % err
log.error(err_msg)
return err
finally:
if security_enabled:
# avoid false failure detection when the server closes
# the SMTP connection with TLS enabled
import socket
try:
server.quit()
except socket.sslerror:
pass
else:
server.quit()
return _("Notification email sent.")
def _on_torrent_finished_event(self, torrent_id):
log.debug("Handler for TorrentFinishedEvent called for CORE")
torrent = component.get("TorrentManager")[torrent_id]
torrent_status = torrent.get_status({})
# Email
subject = _("Finished Torrent \"%(name)s\"") % torrent_status
message = _(
"This email is to inform you that Deluge has finished "
"downloading \"%(name)s\", which includes %(num_files)i files."
"\nTo stop receiving these alerts, simply turn off email "
"notification in Deluge's preferences.\n\n"
"Thank you,\nDeluge."
) % torrent_status
return subject, message
d = defer.maybeDeferred(self.handle_custom_email_notification,
[subject, message],
"TorrentFinishedEvent")
d.addCallback(self._on_notify_sucess, 'email')
d.addErrback(self._on_notify_failure, 'email')
return d
class GtkUiNotifications(CustomNotifications):
def enable(self):
CustomNotifications.enable(self)
self.register_custom_blink_notification(
"TorrentFinishedEvent", self._on_torrent_finished_event_blink
)
self.register_custom_sound_notification(
"TorrentFinishedEvent", self._on_torrent_finished_event_sound
)
self.register_custom_popup_notification(
"TorrentFinishedEvent", self._on_torrent_finished_event_popup
)
def disable(self):
self.deregister_custom_blink_notification("TorrentFinishedEvent")
self.deregister_custom_sound_notification("TorrentFinishedEvent")
self.deregister_custom_popup_notification("TorrentFinishedEvent")
CustomNotifications.disable(self)
def register_custom_popup_notification(self, eventtype, handler):
"""This is used to register popup notifications for custom event types.
:param event: the event name
:param type: string
:param handler: function, to be called when `:param:event` is emitted
Your handler should return a tuple of (popup_title, popup_contents).
"""
self._register_custom_provider('popup', eventtype, handler)
def deregister_custom_popup_notification(self, eventtype):
self._deregister_custom_provider('popup', eventtype)
def register_custom_blink_notification(self, eventtype, handler):
"""This is used to register blink notifications for custom event types.
:param event: str, the event name
:param handler: function, to be called when `:param:event` is emitted
Your handler should return `True` or `False` to blink or not the
trayicon.
"""
self._register_custom_provider('blink', eventtype, handler)
def deregister_custom_blink_notification(self, eventtype):
self._deregister_custom_provider('blink', eventtype)
def register_custom_sound_notification(self, eventtype, handler):
"""This is used to register sound notifications for custom event types.
:param event: the event name
:type event: string
:param handler: function to be called when `:param:event` is emitted
Your handler should return either '' to use the sound defined on the
notification preferences, the path to a sound file, which will then be
played or None, where no sound will be played at all.
"""
self._register_custom_provider('sound', eventtype, handler)
def deregister_custom_sound_notification(self, eventtype):
self._deregister_custom_provider('sound', eventtype)
def handle_custom_popup_notification(self, result, eventtype):
title, message = result
return defer.maybeDeferred(self.__popup, title, message)
def handle_custom_blink_notification(self, result, eventtype):
if result:
return defer.maybeDeferred(self.__blink)
return defer.succeed("Won't blink. The returned value from the custom "
"handler was: %s", result)
def handle_custom_sound_notification(self, result, eventtype):
if isinstance(result, basestring):
if not result and eventtype in self.config['custom_sounds']:
return defer.maybeDeferred(
self.__play_sound, self.config['custom_sounds'][eventtype])
return defer.maybeDeferred(self.__play_sound, result)
return defer.succeed("Won't play sound. The returned value from the "
"custom handler was: %s", result)
def __blink(self):
self.systray.blink(True)
return defer.succeed(_("Notification Blink shown"))
def __popup(self, title='', message=''):
import gtk
if not self.config['popup_enabled']:
return defer.succeed(_("Popup notification is not enabled."))
if not POPUP_AVAILABLE:
return defer.fail(_("pynotify is not installed"))
if pynotify.init("Deluge"):
icon = gtk.gdk.pixbuf_new_from_file_at_size(
deluge.common.get_pixmap("deluge.svg"), 48, 48)
self.note = pynotify.Notification(title, message)
self.note.set_icon_from_pixbuf(icon)
if not self.note.show():
err_msg = _("pynotify failed to show notification")
log.warning(err_msg)
return defer.fail(err_msg)
return defer.succeed(_("Notification popup shown"))
def __play_sound(self, sound_path=''):
if not self.config['sound_enabled']:
return defer.succeed(_("Sound notification not enabled"))
if not SOUND_AVAILABLE:
err_msg = _("pygame is not installed")
log.warning(err_msg)
return defer.fail(err_msg)
pygame.init()
try:
if not sound_path:
sound_path = self.config['sound_path']
alert_sound = pygame.mixer.music
alert_sound.load(sound_path)
alert_sound.play()
except pygame.error, message:
err_msg = _("Sound notification failed %s") % (message)
log.warning(err_msg)
return defer.fail(err_msg)
else:
msg = _("Sound notification Success")
log.info(msg)
return defer.succeed(msg)
def _on_torrent_finished_event_blink(self, torrent_id):
return True # Yes, Blink
def _on_torrent_finished_event_sound(self, torrent_id):
# Since there's no custom sound hardcoded, just return ''
return ''
def _on_torrent_finished_event_popup(self, torrent_id):
d = client.core.get_torrent_status(torrent_id, ["name", "num_files"])
d.addCallback(self._on_torrent_finished_event_got_torrent_status)
d.addErrback(self._on_torrent_finished_event_torrent_status_failure)
return d
def _on_torrent_finished_event_torrent_status_failure(self, failure):
log.debug("Failed to get torrent status to be able to show the popup")
def _on_torrent_finished_event_got_torrent_status(self, torrent_status):
log.debug("Handler for TorrentFinishedEvent GTKUI called. "
"Got Torrent Status")
title = _("Finished Torrent")
message = _("The torrent \"%(name)s\" including %(num_files)i "
"has finished downloading.") % torrent_status
return title, message

View File

@ -37,12 +37,15 @@
# statement from all source files in the program, then also delete it here. # statement from all source files in the program, then also delete it here.
# #
import smtplib
from twisted.internet import defer, threads
from deluge.event import known_events
from deluge.log import LOG as log from deluge.log import LOG as log
from deluge.plugins.pluginbase import CorePluginBase from deluge.plugins.pluginbase import CorePluginBase
import deluge.configmanager import deluge.configmanager
from deluge.core.rpcserver import export from deluge.core.rpcserver import export
from notifications.common import CoreNotifications from notifications.common import CustomNotifications
DEFAULT_PREFS = { DEFAULT_PREFS = {
"smtp_enabled": False, "smtp_enabled": False,
@ -59,6 +62,152 @@ DEFAULT_PREFS = {
} }
} }
class CoreNotifications(CustomNotifications):
def enable(self):
CustomNotifications.enable(self)
self.register_custom_email_notification('TorrentFinishedEvent',
self._on_torrent_finished_event)
def disable(self):
self.deregister_custom_email_notification('TorrentFinishedEvent')
CustomNotifications.disable(self)
def register_custom_email_notification(self, eventtype, handler):
"""This is used to register email notifications for custom event types.
:param event: str, the event name
:param handler: function, to be called when `:param:event` is emitted
Your handler should return a tuple of (email_subject, email_contents).
"""
self._register_custom_provider('email', eventtype, handler)
def deregister_custom_email_notification(self, eventtype):
self._deregister_custom_provider('email', eventtype)
def handle_custom_email_notification(self, result, eventtype):
if not self.config['smtp_enabled']:
return defer.succeed("SMTP notification not enabled.")
subject, message = result
log.debug("Spawning new thread to send email with subject: %s: %s",
subject, message)
# Spawn thread because we don't want Deluge to lock up while we send the
# email.
return threads.deferToThread(self._notify_email, subject, message)
def get_handled_events(self):
handled_events = []
for evt in sorted(known_events.keys()):
if known_events[evt].__module__.startswith('deluge.event'):
if evt not in ('TorrentFinishedEvent',):
# Skip all un-handled built-in events
continue
classdoc = known_events[evt].__doc__.strip()
handled_events.append((evt, classdoc))
log.debug("Handled Notification Events: %s", handled_events)
return handled_events
def _notify_email(self, subject='', message=''):
log.debug("Email prepared")
to_addrs = '; '.join(self.config['smtp_recipients'])
headers = """\
From: %(smtp_from)s
To: %(smtp_recipients)s
Subject: %(subject)s
""" % {'smtp_from': self.config['smtp_from'],
'subject': subject,
'smtp_recipients': to_addrs}
message = '\r\n'.join((headers + message).splitlines())
try:
try:
# Python 2.6
server = smtplib.SMTP(self.config["smtp_host"],
self.config["smtp_port"],
timeout=60)
except:
# Python 2.5
server = smtplib.SMTP(self.config["smtp_host"],
self.config["smtp_port"])
except Exception, err:
err_msg = _("There was an error sending the notification email:"
" %s") % err
log.error(err_msg)
return err
security_enabled = self.config['smtp_tls']
if security_enabled:
server.ehlo()
if not server.esmtp_features.has_key('starttls'):
log.warning("TLS/SSL enabled but server does not support it")
else:
server.starttls()
server.ehlo()
if self.config['smtp_user'] and self.config['smtp_pass']:
try:
server.login(self.config['smtp_user'], self.config['smtp_pass'])
except smtplib.SMTPHeloError, err:
err_msg = _("The server didn't reply properly to the helo "
"greeting: %s") % err
log.error(err_msg)
return err
except smtplib.SMTPAuthenticationError, err:
err_msg = _("The server didn't accept the username/password "
"combination: %s") % err
log.error(err_msg)
return err
try:
try:
server.sendmail(self.config['smtp_from'], to_addrs, message)
except smtplib.SMTPException, err:
err_msg = _("There was an error sending the notification email:"
" %s") % err
log.error(err_msg)
return err
finally:
if security_enabled:
# avoid false failure detection when the server closes
# the SMTP connection with TLS enabled
import socket
try:
server.quit()
except socket.sslerror:
pass
else:
server.quit()
return _("Notification email sent.")
def _on_torrent_finished_event(self, torrent_id):
log.debug("Handler for TorrentFinishedEvent called for CORE")
torrent = component.get("TorrentManager")[torrent_id]
torrent_status = torrent.get_status({})
# Email
subject = _("Finished Torrent \"%(name)s\"") % torrent_status
message = _(
"This email is to inform you that Deluge has finished "
"downloading \"%(name)s\", which includes %(num_files)i files."
"\nTo stop receiving these alerts, simply turn off email "
"notification in Deluge's preferences.\n\n"
"Thank you,\nDeluge."
) % torrent_status
return subject, message
d = defer.maybeDeferred(self.handle_custom_email_notification,
[subject, message],
"TorrentFinishedEvent")
d.addCallback(self._on_notify_sucess, 'email')
d.addErrback(self._on_notify_failure, 'email')
return d
class Core(CorePluginBase, CoreNotifications): class Core(CorePluginBase, CoreNotifications):
def __init__(self, plugin_name): def __init__(self, plugin_name):
CorePluginBase.__init__(self, plugin_name) CorePluginBase.__init__(self, plugin_name)

View File

@ -49,8 +49,22 @@ import deluge.common
import deluge.configmanager import deluge.configmanager
# Relative imports # Relative imports
from notifications.common import (get_resource, GtkUiNotifications, from common import get_resource, CustomNotifications
SOUND_AVAILABLE, POPUP_AVAILABLE)
try:
import pygame
SOUND_AVAILABLE = True
except ImportError:
SOUND_AVAILABLE = False
try:
import pynotify
POPUP_AVAILABLE = True
if deluge.common.windows_check():
POPUP_AVAILABLE = False
except ImportError:
POPUP_AVAILABLE = False
DEFAULT_PREFS = { DEFAULT_PREFS = {
# BLINK # BLINK
@ -76,6 +90,158 @@ RECIPIENT_FIELD, RECIPIENT_EDIT = range(2)
SUB_NOT_SOUND) = range(6) SUB_NOT_SOUND) = range(6)
SND_EVENT, SND_EVENT_DOC, SND_NAME, SND_PATH = range(4) SND_EVENT, SND_EVENT_DOC, SND_NAME, SND_PATH = range(4)
class GtkUiNotifications(CustomNotifications):
def enable(self):
CustomNotifications.enable(self)
self.register_custom_blink_notification(
"TorrentFinishedEvent", self._on_torrent_finished_event_blink
)
self.register_custom_sound_notification(
"TorrentFinishedEvent", self._on_torrent_finished_event_sound
)
self.register_custom_popup_notification(
"TorrentFinishedEvent", self._on_torrent_finished_event_popup
)
def disable(self):
self.deregister_custom_blink_notification("TorrentFinishedEvent")
self.deregister_custom_sound_notification("TorrentFinishedEvent")
self.deregister_custom_popup_notification("TorrentFinishedEvent")
CustomNotifications.disable(self)
def register_custom_popup_notification(self, eventtype, handler):
"""This is used to register popup notifications for custom event types.
:param event: the event name
:param type: string
:param handler: function, to be called when `:param:event` is emitted
Your handler should return a tuple of (popup_title, popup_contents).
"""
self._register_custom_provider('popup', eventtype, handler)
def deregister_custom_popup_notification(self, eventtype):
self._deregister_custom_provider('popup', eventtype)
def register_custom_blink_notification(self, eventtype, handler):
"""This is used to register blink notifications for custom event types.
:param event: str, the event name
:param handler: function, to be called when `:param:event` is emitted
Your handler should return `True` or `False` to blink or not the
trayicon.
"""
self._register_custom_provider('blink', eventtype, handler)
def deregister_custom_blink_notification(self, eventtype):
self._deregister_custom_provider('blink', eventtype)
def register_custom_sound_notification(self, eventtype, handler):
"""This is used to register sound notifications for custom event types.
:param event: the event name
:type event: string
:param handler: function to be called when `:param:event` is emitted
Your handler should return either '' to use the sound defined on the
notification preferences, the path to a sound file, which will then be
played or None, where no sound will be played at all.
"""
self._register_custom_provider('sound', eventtype, handler)
def deregister_custom_sound_notification(self, eventtype):
self._deregister_custom_provider('sound', eventtype)
def handle_custom_popup_notification(self, result, eventtype):
title, message = result
return defer.maybeDeferred(self.__popup, title, message)
def handle_custom_blink_notification(self, result, eventtype):
if result:
return defer.maybeDeferred(self.__blink)
return defer.succeed("Won't blink. The returned value from the custom "
"handler was: %s", result)
def handle_custom_sound_notification(self, result, eventtype):
if isinstance(result, basestring):
if not result and eventtype in self.config['custom_sounds']:
return defer.maybeDeferred(
self.__play_sound, self.config['custom_sounds'][eventtype])
return defer.maybeDeferred(self.__play_sound, result)
return defer.succeed("Won't play sound. The returned value from the "
"custom handler was: %s", result)
def __blink(self):
self.systray.blink(True)
return defer.succeed(_("Notification Blink shown"))
def __popup(self, title='', message=''):
import gtk
if not self.config['popup_enabled']:
return defer.succeed(_("Popup notification is not enabled."))
if not POPUP_AVAILABLE:
return defer.fail(_("pynotify is not installed"))
if pynotify.init("Deluge"):
icon = gtk.gdk.pixbuf_new_from_file_at_size(
deluge.common.get_pixmap("deluge.svg"), 48, 48)
self.note = pynotify.Notification(title, message)
self.note.set_icon_from_pixbuf(icon)
if not self.note.show():
err_msg = _("pynotify failed to show notification")
log.warning(err_msg)
return defer.fail(err_msg)
return defer.succeed(_("Notification popup shown"))
def __play_sound(self, sound_path=''):
if not self.config['sound_enabled']:
return defer.succeed(_("Sound notification not enabled"))
if not SOUND_AVAILABLE:
err_msg = _("pygame is not installed")
log.warning(err_msg)
return defer.fail(err_msg)
pygame.init()
try:
if not sound_path:
sound_path = self.config['sound_path']
alert_sound = pygame.mixer.music
alert_sound.load(sound_path)
alert_sound.play()
except pygame.error, message:
err_msg = _("Sound notification failed %s") % (message)
log.warning(err_msg)
return defer.fail(err_msg)
else:
msg = _("Sound notification Success")
log.info(msg)
return defer.succeed(msg)
def _on_torrent_finished_event_blink(self, torrent_id):
return True # Yes, Blink
def _on_torrent_finished_event_sound(self, torrent_id):
# Since there's no custom sound hardcoded, just return ''
return ''
def _on_torrent_finished_event_popup(self, torrent_id):
d = client.core.get_torrent_status(torrent_id, ["name", "num_files"])
d.addCallback(self._on_torrent_finished_event_got_torrent_status)
d.addErrback(self._on_torrent_finished_event_torrent_status_failure)
return d
def _on_torrent_finished_event_torrent_status_failure(self, failure):
log.debug("Failed to get torrent status to be able to show the popup")
def _on_torrent_finished_event_got_torrent_status(self, torrent_status):
log.debug("Handler for TorrentFinishedEvent GTKUI called. "
"Got Torrent Status")
title = _("Finished Torrent")
message = _("The torrent \"%(name)s\" including %(num_files)i "
"has finished downloading.") % torrent_status
return title, message
class GtkUI(GtkPluginBase, GtkUiNotifications): class GtkUI(GtkPluginBase, GtkUiNotifications):
def __init__(self, plugin_name): def __init__(self, plugin_name):

View File

@ -45,8 +45,12 @@ __author_email__ = "ufs@ufsoft.org"
__version__ = "0.1" __version__ = "0.1"
__url__ = "http://dev.deluge-torrent.org/" __url__ = "http://dev.deluge-torrent.org/"
__license__ = "GPLv3" __license__ = "GPLv3"
__description__ = "" __description__ = "Plugin which provides notifications to Deluge."
__long_description__ = """""" __long_description__ = __description__ + """\
Email, Popup, Blink and Sound notifications are supported.
The plugin also allows other plugins to make use of itself for their own custom
notifications.
"""
__pkg_data__ = {__plugin_name__.lower(): ["template/*", "data/*"]} __pkg_data__ = {__plugin_name__.lower(): ["template/*", "data/*"]}
setup( setup(