First notifications plugin commit.

Working when not in classic mode. In classic mode, needs some more coding.
This commit is contained in:
Pedro Algarvio 2009-11-22 02:34:23 +00:00
commit 0723a77214
29 changed files with 1482 additions and 164 deletions

View file

@ -101,13 +101,6 @@ class ComponentRegistry:
if depend != None: if depend != None:
self.depend[name] = depend self.depend[name] = depend
def deregister(self, name):
"""Deregisters a component"""
if name in self.components:
log.debug("Deregistering Component: %s", name)
self.stop_component(name)
del self.components[name]
def get(self, name): def get(self, name):
"""Returns a reference to the component 'name'""" """Returns a reference to the component 'name'"""
return self.components[name] return self.components[name]
@ -133,13 +126,7 @@ class ComponentRegistry:
def stop(self): def stop(self):
"""Stops all components""" """Stops all components"""
# We create a separate list of the keys and do an additional check to for component in self.components.keys():
# make sure the key still exists in the components dict.
# This is because components could be deregistered during a stop and
# the dictionary would get modified while iterating through it.
components = self.components.keys()
for component in components:
if component in self.components:
self.stop_component(component) self.stop_component(component)
def stop_component(self, component): def stop_component(self, component):
@ -200,10 +187,6 @@ def register(name, obj, depend=None):
"""Registers a component with the registry""" """Registers a component with the registry"""
_ComponentRegistry.register(name, obj, depend) _ComponentRegistry.register(name, obj, depend)
def deregister(name):
"""Deregisters a component"""
_ComponentRegistry.deregister(name)
def start(component=None): def start(component=None):
"""Starts all components""" """Starts all components"""
if component == None: if component == None:

View file

@ -131,7 +131,7 @@ class DelugeRPCProtocol(Protocol):
try: try:
request = rencode.loads(dobj.decompress(data)) request = rencode.loads(dobj.decompress(data))
except Exception, e: except Exception, e:
#log.debug("Received possible invalid message (%r): %s", data, e) log.debug("Received possible invalid message (%r): %s", data, e)
# This could be cut-off data, so we'll save this in the buffer # This could be cut-off data, so we'll save this in the buffer
# and try to prepend it on the next dataReceived() # and try to prepend it on the next dataReceived()
self.__buffer = data self.__buffer = data

View file

@ -210,10 +210,7 @@ class TorrentManager(component.Component):
def stop(self): def stop(self):
# Stop timers # Stop timers
if self.save_state_timer.running:
self.save_state_timer.stop() self.save_state_timer.stop()
if self.save_resume_data_timer.running:
self.save_resume_data_timer.stop() self.save_resume_data_timer.stop()
# Save state on shutdown # Save state on shutdown

View file

@ -41,21 +41,6 @@ and subsequently emitted to the clients.
""" """
known_events = {}
class DelugeEventMetaClass(type):
"""
This metaclass simply keeps a list of all events classes created.
"""
def __init__(cls, name, bases, dct):
super(DelugeEventMetaClass, cls).__init__(name, bases, dct)
if name != "DelugeEvent":
classdoc = cls.__doc__.splitlines()
if classdoc[0].strip():
known_events[name] = classdoc[0].strip()
else:
known_events[name] = classdoc[1].strip()
class DelugeEvent(object): class DelugeEvent(object):
""" """
The base class for all events. The base class for all events.
@ -64,8 +49,6 @@ class DelugeEvent(object):
:prop args: a list of the attribute values :prop args: a list of the attribute values
""" """
__metaclass__ = DelugeEventMetaClass
def _get_name(self): def _get_name(self):
return self.__class__.__name__ return self.__class__.__name__

View file

@ -87,7 +87,7 @@ class PluginManagerBase:
def disable_plugins(self): def disable_plugins(self):
# Disable all plugins that are enabled # Disable all plugins that are enabled
for key in self.plugins.keys(): for key in self.plugins.keys():
self.disable_plugin(key) self.plugins[key].disable()
def __getitem__(self, key): def __getitem__(self, key):
return self.plugins[key] return self.plugins[key]
@ -153,7 +153,6 @@ class PluginManagerBase:
"""Disables a plugin""" """Disables a plugin"""
try: try:
self.plugins[name].disable() self.plugins[name].disable()
component.deregister(self.plugins[name].plugin.get_component_name())
del self.plugins[name] del self.plugins[name]
self.config["enabled_plugins"].remove(name) self.config["enabled_plugins"].remove(name)
except KeyError: except KeyError:

View file

@ -49,18 +49,3 @@ def raiseError(error):
raise error raise error
return new return new
return safer return safer
def remove_zeros(ip):
"""
Removes unneeded zeros from ip addresses.
Example: 000.000.000.003 -> 0.0.0.3
:param ip: the ip address
:type ip: string
:returns: the ip address without the unneeded zeros
:rtype: string
"""
return ".".join([part.lstrip("0").zfill(1) for part in ip.split(".")])

View file

@ -128,8 +128,6 @@ class Core(CorePluginBase):
self.use_cache = False self.use_cache = False
self.failed_attempts = 0 self.failed_attempts = 0
self.auto_detected = False self.auto_detected = False
if force:
self.reader = None
# Start callback chain # Start callback chain
d = self.download_list() d = self.download_list()
@ -220,8 +218,8 @@ class Core(CorePluginBase):
if self.config["last_update"] and not self.force_download: if self.config["last_update"] and not self.force_download:
headers['If-Modified-Since'] = self.config["last_update"] headers['If-Modified-Since'] = self.config["last_update"]
log.debug("Attempting to download blocklist %s", url) log.debug("Attempting to download blocklist %s" % url)
log.debug("Sending headers: %s", headers) log.debug("Sending headers: %s" % headers)
self.up_to_date = False self.up_to_date = False
self.is_downloading = True self.is_downloading = True
return download_file(url, deluge.configmanager.get_config_dir("blocklist.download"), on_retrieve_data, headers) return download_file(url, deluge.configmanager.get_config_dir("blocklist.download"), on_retrieve_data, headers)
@ -241,7 +239,7 @@ class Core(CorePluginBase):
# Handle redirect errors # Handle redirect errors
location = error_msg.split(" to ")[1] location = error_msg.split(" to ")[1]
if "Moved Permanently" in error_msg: if "Moved Permanently" in error_msg:
log.debug("Setting blocklist url to %s", location) log.debug("Setting blocklist url to %s" % location)
self.config["url"] = location self.config["url"] = location
f.trap(f.type) f.trap(f.type)
d = self.download_list(url=location) d = self.download_list(url=location)
@ -329,7 +327,7 @@ class Core(CorePluginBase):
elif os.path.exists(blocklist) and not self.use_cache: elif os.path.exists(blocklist) and not self.use_cache:
# If we have a backup and we haven't already used it # If we have a backup and we haven't already used it
e = f.trap(Exception) e = f.trap(Exception)
log.warning("Error reading blocklist: %s", e) log.warning("Error reading blocklist: ", e)
self.use_cache = True self.use_cache = True
try_again = True try_again = True

View file

@ -77,4 +77,5 @@ def create_reader(format, compression=""):
decompressor = DECOMPRESSERS.get(compression) decompressor = DECOMPRESSERS.get(compression)
if decompressor: if decompressor:
reader = decompressor(reader) reader = decompressor(reader)
return reader return reader

View file

@ -33,8 +33,28 @@
# #
# #
from common import raiseError, remove_zeros from deluge.log import LOG as log
import re from common import raiseError
def remove_zeros(ip):
"""
Removes unneeded zeros from ip addresses.
Example: 000.000.000.003 -> 0.0.0.3
:param ip: the ip address
:type ip: string
:returns: the ip address without the unneeded zeros
:rtype: string
"""
new_ip = []
for part in ip.split("."):
while part[0] == "0" and len(part) > 1:
part = part[1:]
new_ip.append(part)
return ".".join(new_ip)
class ReaderParseError(Exception): class ReaderParseError(Exception):
pass pass
@ -70,9 +90,6 @@ class BaseReader(object):
if not self.is_ignored(line): if not self.is_ignored(line):
try: try:
(start, end) = self.parse(line) (start, end) = self.parse(line)
if not re.match("^(\d{1,3}\.){4}$", start + ".") or \
not re.match("^(\d{1,3}\.){4}$", end + "."):
valid = False
except: except:
valid = False valid = False
finally: finally:
@ -98,7 +115,7 @@ class SafePeerReader(BaseReader):
"""Blocklist reader for SafePeer style blocklists""" """Blocklist reader for SafePeer style blocklists"""
@raiseError(ReaderParseError) @raiseError(ReaderParseError)
def parse(self, line): def parse(self, line):
return line.strip().split(":")[-1].split("-") return line.strip().split(":")[1].split("-")
class PeerGuardianReader(SafePeerReader): class PeerGuardianReader(SafePeerReader):
"""Blocklist reader for PeerGuardian style blocklists""" """Blocklist reader for PeerGuardian style blocklists"""

View file

@ -0,0 +1,7 @@
#!/bin/bash
cd /home/vampas/projects/DelugeNotify/deluge/plugins/notifications
mkdir temp
export PYTHONPATH=./temp
python setup.py build develop --install-dir ./temp
cp ./temp/Notifications.egg-link .config//plugins
rm -fr ./temp

View file

@ -0,0 +1,58 @@
#
# __init__.py
#
# Copyright (C) 2009 Pedro Algarvio <ufs@ufsoft.org>
#
# Basic plugin template created by:
# Copyright (C) 2008 Martijn Voncken <mvoncken@gmail.com>
# Copyright (C) 2007-2009 Andrew Resch <andrewresch@gmail.com>
# Copyright (C) 2009 Damien Churchill <damoxc@gmail.com>
#
# Deluge is free software.
#
# You may redistribute it and/or modify it under the terms of the
# GNU General Public License, as published by the Free Software
# Foundation; either version 3 of the License, or (at your option)
# any later version.
#
# deluge 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 deluge. If not, write to:
# The Free Software Foundation, Inc.,
# 51 Franklin Street, Fifth Floor
# Boston, MA 02110-1301, USA.
#
# In addition, as a special exception, the copyright holders give
# permission to link the code of portions of this program with the OpenSSL
# library.
# You must obey the GNU General Public License in all respects for all of
# the code used other than OpenSSL. If you modify file(s) with this
# exception, you may extend this exception to your version of the file(s),
# but you are not obligated to do so. If you do not wish to do so, delete
# this exception statement from your version. If you delete this exception
# statement from all source files in the program, then also delete it here.
#
from deluge.plugins.init import PluginInitBase
class CorePlugin(PluginInitBase):
def __init__(self, plugin_name):
from core import Core as _plugin_cls
self._plugin_cls = _plugin_cls
super(CorePlugin, self).__init__(plugin_name)
class GtkUIPlugin(PluginInitBase):
def __init__(self, plugin_name):
from gtkui import GtkUI as _plugin_cls
self._plugin_cls = _plugin_cls
super(GtkUIPlugin, self).__init__(plugin_name)
class WebUIPlugin(PluginInitBase):
def __init__(self, plugin_name):
from webui import WebUI as _plugin_cls
self._plugin_cls = _plugin_cls
super(WebUIPlugin, self).__init__(plugin_name)

View file

@ -0,0 +1,42 @@
#
# common.py
#
# Copyright (C) 2009 Pedro Algarvio <ufs@ufsoft.org>
#
# Basic plugin template created by:
# Copyright (C) 2008 Martijn Voncken <mvoncken@gmail.com>
# Copyright (C) 2007-2009 Andrew Resch <andrewresch@gmail.com>
# Copyright (C) 2009 Damien Churchill <damoxc@gmail.com>
#
# Deluge is free software.
#
# You may redistribute it and/or modify it under the terms of the
# GNU General Public License, as published by the Free Software
# Foundation; either version 3 of the License, or (at your option)
# any later version.
#
# deluge 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 deluge. If not, write to:
# The Free Software Foundation, Inc.,
# 51 Franklin Street, Fifth Floor
# Boston, MA 02110-1301, USA.
#
# In addition, as a special exception, the copyright holders give
# permission to link the code of portions of this program with the OpenSSL
# library.
# You must obey the GNU General Public License in all respects for all of
# the code used other than OpenSSL. If you modify file(s) with this
# exception, you may extend this exception to your version of the file(s),
# but you are not obligated to do so. If you do not wish to do so, delete
# this exception statement from your version. If you delete this exception
# statement from all source files in the program, then also delete it here.
#
def get_resource(filename):
import pkg_resources, os
return pkg_resources.resource_filename("notifications", os.path.join("data", filename))

View file

@ -0,0 +1,205 @@
#
# core.py
#
# Copyright (C) 2009 Pedro Algarvio <ufs@ufsoft.org>
#
# Basic plugin template created by:
# Copyright (C) 2008 Martijn Voncken <mvoncken@gmail.com>
# Copyright (C) 2007-2009 Andrew Resch <andrewresch@gmail.com>
# Copyright (C) 2009 Damien Churchill <damoxc@gmail.com>
#
# Deluge is free software.
#
# You may redistribute it and/or modify it under the terms of the
# GNU General Public License, as published by the Free Software
# Foundation; either version 3 of the License, or (at your option)
# any later version.
#
# deluge 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 deluge. If not, write to:
# The Free Software Foundation, Inc.,
# 51 Franklin Street, Fifth Floor
# Boston, MA 02110-1301, USA.
#
# In addition, as a special exception, the copyright holders give
# permission to link the code of portions of this program with the OpenSSL
# library.
# You must obey the GNU General Public License in all respects for all of
# the code used other than OpenSSL. If you modify file(s) with this
# exception, you may extend this exception to your version of the file(s),
# but you are not obligated to do so. If you do not wish to do so, delete
# this exception statement from your version. If you delete this exception
# statement from all source files in the program, then also delete it here.
#
import smtplib
from twisted.internet import defer, threads
from deluge.log import LOG as log
from deluge.plugins.pluginbase import CorePluginBase
import deluge.component as component
import deluge.configmanager
from deluge.core.rpcserver import export
# Relative imports
from manager import Notifications
import events
DEFAULT_PREFS = {
# BLINK
"blink_enabled": False,
# EMAIL
"smtp_enabled": False,
"smtp_host": "",
"smtp_port": 25,
"smtp_user": "",
"smtp_pass": "",
"smtp_from": "",
"smtp_tls": False, # SSL or TLS
"smtp_recipients": [],
# FLASH
"flash_enabled": False,
# POPUP
"popup_enabled": False,
# SOUND
"sound_enabled": False,
"sound_path": ""
}
class Core(CorePluginBase, Notifications):
def enable(self):
Notifications.enable(self)
self.config = deluge.configmanager.ConfigManager("notifications.conf",
DEFAULT_PREFS)
component.get("EventManager").register_event_handler(
"TorrentFinishedEvent", self._on_torrent_finished_event
)
log.debug("\n\nENABLING CORE NOTIFICATIONS\n\n")
def disable(self):
Notifications.disable(self)
log.debug("\n\nDISABLING CORE NOTIFICATIONS\n\n")
def update(self):
pass
@export
def set_config(self, config):
"sets the config dictionary"
for key in config.keys():
self.config[key] = config[key]
self.config.save()
@export
def get_config(self):
"returns the config dictionary"
return self.config.config
# Notification methods
@export
def notify_blink(self):
if not self.config["blink_enabled"]:
return defer.succeed("Blink notification not enabled")
return defer.maybeDeferred(
component.get("EventManager").emit, events.NotificationBlinkEvent())
@export
def notify_email(self, title='', message='', smtp_from='', recipients=[]):
if not self.config['smtp_enabled']:
return defer.succeed("SMTP notification not enabled")
d = threads.deferToThread(self._notify_email, title, message, smtp_from,
recipients)
d.addCallback(self._on_notify_sucess, 'email')
d.addErrback(self._on_notify_failure, 'email')
return d
@export
def notify_flash(self, title='', message=''):
if not self.config["flash_enabled"]:
return defer.succeed("Flash notification not enabled")
return defer.maybeDeferred(
component.get("EventManager").emit,
events.NotificationFlashEvent(title, message)
)
@export
def notify_popup(self, title='', message=''):
if not self.config["popup_enabled"]:
return defer.succeed("Popup notification not enabled")
return defer.maybeDeferred(
component.get("EventManager").emit,
events.NotificationPopupEvent(title, message)
)
@export
def notify_sound(self, sound_path=''):
if not self.config["sound_enabled"]:
return defer.succeed("Sound notification not enabled")
return defer.maybeDeferred(
component.get("EventManager").emit,
events.NotificationSoundEvent(sound_path))
def _notify_email(self, title='', message='', smtp_from='', recipients=[]):
config = self.config
to_addrs = '; '.join(config['smtp_recipients']+recipients)
headers = """\
From: %(smtp_from)s
To: %(smtp_recipients)s
Subject: %(title)s
""" % {'smtp_from': smtp_from and smtp_from or config['smtp_from'],
'title': title,
'smtp_recipients': to_addrs}
message = '\r\n'.join((headers + message).splitlines())
try:
server = smtplib.SMTP(config["smtp_host"], config["smtp_port"])
except Exception, err:
log.error("There was an error sending the notification email: %s",
err)
security_enabled = 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 config['smtp_user'] and config['smtp_pass']:
try:
server.login(config['smtp_user'], config['smtp_pass'])
except smtplib.SMTPHeloError:
log.warning("The server didn't reply properly to the helo "
"greeting")
except smtplib.SMTPAuthenticationError:
log.warning("The server didn't accept the username/password "
"combination")
try:
try:
server.sendmail(config['smtp_from'], to_addrs, message)
except smtplib.SMTPException, err:
log.error("There was an error sending the notification email: "
"%s", 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."

View file

@ -0,0 +1,398 @@
<?xml version="1.0"?>
<glade-interface>
<!-- interface-requires gtk+ 2.6 -->
<!-- interface-naming-policy toplevel-contextual -->
<widget class="GtkWindow" id="window">
<child>
<widget class="GtkVBox" id="prefs_box">
<property name="visible">True</property>
<property name="orientation">vertical</property>
<child>
<widget class="GtkFrame" id="frame2">
<property name="visible">True</property>
<property name="label_xalign">0</property>
<property name="shadow_type">none</property>
<child>
<widget class="GtkAlignment" id="alignment2">
<property name="visible">True</property>
<property name="left_padding">12</property>
<child>
<widget class="GtkVBox" id="vbox1">
<property name="visible">True</property>
<property name="orientation">vertical</property>
<child>
<widget class="GtkCheckButton" id="blink_enabled">
<property name="label" translatable="yes">Blink tray icon</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">False</property>
<property name="draw_indicator">True</property>
</widget>
<packing>
<property name="position">0</property>
</packing>
</child>
<child>
<widget class="GtkCheckButton" id="popup_enabled">
<property name="label" translatable="yes">Show popup</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">False</property>
<property name="draw_indicator">True</property>
</widget>
<packing>
<property name="position">1</property>
</packing>
</child>
<child>
<widget class="GtkHBox" id="hbox2">
<property name="visible">True</property>
<child>
<widget class="GtkCheckButton" id="sound_enabled">
<property name="label" translatable="yes">Play sound</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">False</property>
<property name="draw_indicator">True</property>
<signal name="toggled" handler="on_sound_enabled_toggled"/>
</widget>
<packing>
<property name="expand">False</property>
<property name="position">0</property>
</packing>
</child>
<child>
<widget class="GtkFileChooserButton" id="sound_path">
<property name="visible">True</property>
<property name="create_folders">False</property>
</widget>
<packing>
<property name="padding">2</property>
<property name="position">1</property>
</packing>
</child>
</widget>
<packing>
<property name="position">2</property>
</packing>
</child>
</widget>
</child>
</widget>
</child>
<child>
<widget class="GtkLabel" id="label6">
<property name="visible">True</property>
<property name="label" translatable="yes">&lt;b&gt;UI Notifications&lt;/b&gt;</property>
<property name="use_markup">True</property>
</widget>
<packing>
<property name="type">label_item</property>
</packing>
</child>
</widget>
<packing>
<property name="position">0</property>
</packing>
</child>
<child>
<widget class="GtkFrame" id="frame3">
<property name="visible">True</property>
<property name="label_xalign">0</property>
<property name="shadow_type">none</property>
<child>
<widget class="GtkAlignment" id="alignment3">
<property name="visible">True</property>
<property name="left_padding">12</property>
<child>
<widget class="GtkTable" id="prefs_table">
<property name="visible">True</property>
<property name="n_rows">7</property>
<property name="n_columns">4</property>
<property name="column_spacing">2</property>
<property name="row_spacing">2</property>
<child>
<widget class="GtkLabel" id="label1">
<property name="visible">True</property>
<property name="label" translatable="yes">Hostname:</property>
<property name="justify">right</property>
</widget>
<packing>
<property name="top_attach">1</property>
<property name="bottom_attach">2</property>
</packing>
</child>
<child>
<widget class="GtkEntry" id="smtp_host">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="invisible_char">&#x25CF;</property>
</widget>
<packing>
<property name="left_attach">1</property>
<property name="right_attach">2</property>
<property name="top_attach">1</property>
<property name="bottom_attach">2</property>
</packing>
</child>
<child>
<widget class="GtkLabel" id="label2">
<property name="visible">True</property>
<property name="label" translatable="yes">Port:</property>
<property name="justify">right</property>
</widget>
<packing>
<property name="left_attach">2</property>
<property name="right_attach">3</property>
<property name="top_attach">1</property>
<property name="bottom_attach">2</property>
</packing>
</child>
<child>
<widget class="GtkSpinButton" id="smtp_port">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="max_length">65535</property>
<property name="invisible_char">&#x25CF;</property>
<property name="width_chars">5</property>
<property name="adjustment">25 1 100 1 10 10</property>
<property name="climb_rate">1</property>
<property name="snap_to_ticks">True</property>
<property name="numeric">True</property>
</widget>
<packing>
<property name="left_attach">3</property>
<property name="right_attach">4</property>
<property name="top_attach">1</property>
<property name="bottom_attach">2</property>
</packing>
</child>
<child>
<widget class="GtkLabel" id="label3">
<property name="visible">True</property>
<property name="label" translatable="yes">Username:</property>
<property name="justify">right</property>
</widget>
<packing>
<property name="top_attach">2</property>
<property name="bottom_attach">3</property>
</packing>
</child>
<child>
<widget class="GtkEntry" id="smtp_user">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="invisible_char">&#x25CF;</property>
</widget>
<packing>
<property name="left_attach">1</property>
<property name="right_attach">4</property>
<property name="top_attach">2</property>
<property name="bottom_attach">3</property>
</packing>
</child>
<child>
<widget class="GtkLabel" id="label4">
<property name="visible">True</property>
<property name="label" translatable="yes">Password:</property>
</widget>
<packing>
<property name="top_attach">3</property>
<property name="bottom_attach">4</property>
</packing>
</child>
<child>
<widget class="GtkEntry" id="smtp_pass">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="visibility">False</property>
<property name="invisible_char">&#x25CF;</property>
</widget>
<packing>
<property name="left_attach">1</property>
<property name="right_attach">4</property>
<property name="top_attach">3</property>
<property name="bottom_attach">4</property>
</packing>
</child>
<child>
<widget class="GtkFrame" id="frame1">
<property name="visible">True</property>
<property name="label_xalign">0</property>
<property name="shadow_type">none</property>
<child>
<widget class="GtkAlignment" id="alignment1">
<property name="visible">True</property>
<property name="left_padding">12</property>
<child>
<widget class="GtkHBox" id="hbox1">
<property name="visible">True</property>
<property name="spacing">2</property>
<child>
<widget class="GtkScrolledWindow" id="scrolledwindow1">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="hscrollbar_policy">automatic</property>
<property name="vscrollbar_policy">automatic</property>
<child>
<widget class="GtkTreeView" id="smtp_recipients">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="headers_visible">False</property>
<property name="enable_grid_lines">horizontal</property>
</widget>
</child>
</widget>
<packing>
<property name="position">0</property>
</packing>
</child>
<child>
<widget class="GtkVButtonBox" id="vbuttonbox1">
<property name="visible">True</property>
<property name="orientation">vertical</property>
<property name="spacing">5</property>
<property name="layout_style">start</property>
<child>
<widget class="GtkButton" id="add_button">
<property name="label">gtk-add</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">True</property>
<property name="use_stock">True</property>
<signal name="clicked" handler="on_add_button_clicked"/>
</widget>
<packing>
<property name="expand">False</property>
<property name="fill">False</property>
<property name="position">0</property>
</packing>
</child>
<child>
<widget class="GtkButton" id="delete_button">
<property name="label">gtk-delete</property>
<property name="visible">True</property>
<property name="sensitive">False</property>
<property name="can_focus">True</property>
<property name="receives_default">True</property>
<property name="use_stock">True</property>
<signal name="clicked" handler="on_delete_button_clicked"/>
</widget>
<packing>
<property name="expand">False</property>
<property name="fill">False</property>
<property name="position">1</property>
</packing>
</child>
</widget>
<packing>
<property name="expand">False</property>
<property name="padding">3</property>
<property name="position">1</property>
</packing>
</child>
</widget>
</child>
</widget>
</child>
<child>
<widget class="GtkLabel" id="label5">
<property name="visible">True</property>
<property name="label" translatable="yes">&lt;b&gt;Recipients&lt;/b&gt;</property>
<property name="use_markup">True</property>
</widget>
<packing>
<property name="type">label_item</property>
</packing>
</child>
</widget>
<packing>
<property name="right_attach">4</property>
<property name="top_attach">6</property>
<property name="bottom_attach">7</property>
</packing>
</child>
<child>
<widget class="GtkCheckButton" id="smtp_tls">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">False</property>
<property name="draw_indicator">True</property>
<child>
<widget class="GtkLabel" id="label9">
<property name="visible">True</property>
<property name="label" translatable="yes">Server requires TLS/SSL</property>
</widget>
</child>
</widget>
<packing>
<property name="left_attach">1</property>
<property name="right_attach">4</property>
<property name="top_attach">5</property>
<property name="bottom_attach">6</property>
<property name="x_padding">10</property>
</packing>
</child>
<child>
<widget class="GtkLabel" id="label8">
<property name="visible">True</property>
<property name="label" translatable="yes">From:</property>
<property name="justify">right</property>
</widget>
<packing>
<property name="top_attach">4</property>
<property name="bottom_attach">5</property>
</packing>
</child>
<child>
<widget class="GtkEntry" id="smtp_from">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="invisible_char">&#x25CF;</property>
</widget>
<packing>
<property name="left_attach">1</property>
<property name="right_attach">4</property>
<property name="top_attach">4</property>
<property name="bottom_attach">5</property>
</packing>
</child>
<child>
<widget class="GtkCheckButton" id="smtp_enabled">
<property name="label" translatable="yes">Enabled</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">False</property>
<property name="draw_indicator">True</property>
<signal name="toggled" handler="on_enabled_toggled"/>
</widget>
<packing>
<property name="right_attach">4</property>
</packing>
</child>
<child>
<placeholder/>
</child>
</widget>
</child>
</widget>
</child>
<child>
<widget class="GtkLabel" id="label7">
<property name="visible">True</property>
<property name="label" translatable="yes">&lt;b&gt;Email Notifications&lt;/b&gt;</property>
<property name="use_markup">True</property>
</widget>
<packing>
<property name="type">label_item</property>
</packing>
</child>
</widget>
<packing>
<property name="position">1</property>
</packing>
</child>
</widget>
</child>
</widget>
</glade-interface>

View file

@ -0,0 +1,50 @@
/*
Script: notifications.js
The client-side javascript code for the Notifications plugin.
Copyright:
(C) Pedro Algarvio 2009 <damoxc@gmail.com>
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 3, 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.
In addition, as a special exception, the copyright holders give
permission to link the code of portions of this program with the OpenSSL
library.
You must obey the GNU General Public License in all respects for all of
the code used other than OpenSSL. If you modify file(s) with this
exception, you may extend this exception to your version of the file(s),
but you are not obligated to do so. If you do not wish to do so, delete
this exception statement from your version. If you delete this exception
statement from all source files in the program, then also delete it here.
*/
NotificationsPlugin = Ext.extend(Deluge.Plugin, {
constructor: function(config) {
config = Ext.apply({
name: "Notifications"
}, config);
NotificationsPlugin.superclass.constructor.call(this, config);
},
onDisable: function() {
},
onEnable: function() {
}
});
new NotificationsPlugin();

View file

@ -0,0 +1,73 @@
#
# events.py
#
# Copyright (C) 2009 Pedro Algarvio <ufs@ufsoft.org>
#
# Deluge is free software.
#
# You may redistribute it and/or modify it under the terms of the
# GNU General Public License, as published by the Free Software
# Foundation; either version 3 of the License, or (at your option)
# any later version.
#
# deluge 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 deluge. If not, write to:
# The Free Software Foundation, Inc.,
# 51 Franklin Street, Fifth Floor
# Boston, MA 02110-1301, USA.
#
# In addition, as a special exception, the copyright holders give
# permission to link the code of portions of this program with the OpenSSL
# library.
# You must obey the GNU General Public License in all respects for all of
# the code used other than OpenSSL. If you modify file(s) with this
# exception, you may extend this exception to your version of the file(s),
# but you are not obligated to do so. If you do not wish to do so, delete
# this exception statement from your version. If you delete this exception
# statement from all source files in the program, then also delete it here.
#
#
from deluge.event import DelugeEvent
class NotificationEvent(DelugeEvent):
"""Emitted when a notification is suposed to happen."""
def __init__(self, title=None, message=None):
"""
:param title: the notification title
:type title: string
:param message: the notification message
:type message: string
"""
self._args = [title, message]
class NotificationBlinkEvent(DelugeEvent):
"""Emitted when a tray icon blink should occur."""
class NotificationPopupEvent(DelugeEvent):
"""Emitted when a popup notification is required"""
def __init__(self, title="", message=""):
"""
:param title: the notification title
:type title: string
:param message: the notification message
:type message: string
"""
self._args = [title, message]
class NotificationFlashEvent(NotificationPopupEvent):
"""Emmited when a flash on the web front-end should occur."""
class NotificationSoundEvent(DelugeEvent):
"""Emitted when a sound notification is required"""
def __init__(self, sound_path=""):
"""
:param sound_path: the path to the notification sound
:type title: string
"""
self._args = [sound_path]

View file

@ -0,0 +1,282 @@
#
# gtkui.py
#
# Copyright (C) 2009 Pedro Algarvio <ufs@ufsoft.org>
#
# Basic plugin template created by:
# Copyright (C) 2008 Martijn Voncken <mvoncken@gmail.com>
# Copyright (C) 2007-2009 Andrew Resch <andrewresch@gmail.com>
# Copyright (C) 2009 Damien Churchill <damoxc@gmail.com>
#
# Deluge is free software.
#
# You may redistribute it and/or modify it under the terms of the
# GNU General Public License, as published by the Free Software
# Foundation; either version 3 of the License, or (at your option)
# any later version.
#
# deluge 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 deluge. If not, write to:
# The Free Software Foundation, Inc.,
# 51 Franklin Street, Fifth Floor
# Boston, MA 02110-1301, USA.
#
# In addition, as a special exception, the copyright holders give
# permission to link the code of portions of this program with the OpenSSL
# library.
# You must obey the GNU General Public License in all respects for all of
# the code used other than OpenSSL. If you modify file(s) with this
# exception, you may extend this exception to your version of the file(s),
# but you are not obligated to do so. If you do not wish to do so, delete
# this exception statement from your version. If you delete this exception
# statement from all source files in the program, then also delete it here.
#
import gtk
from twisted.internet import defer
from deluge.log import LOG as log
from deluge.ui.client import client
from deluge.plugins.pluginbase import GtkPluginBase
import deluge.component as component
import deluge.common
try:
import pygame
SOUND_AVAILABLE = True
except ImportError:
SOUND_AVAILABLE = False
try:
import pynotify
POPUP_ENABLED = True
except ImportError:
POPUP_ENABLED = False
# Relative imports
from common import get_resource
from manager import Notifications
RECIPIENT_FIELD, RECIPIENT_EDIT = range(2)
class GtkUI(GtkPluginBase, Notifications):
def enable(self):
Notifications.enable(self)
self.glade = gtk.glade.XML(get_resource("config.glade"))
self.glade.get_widget("smtp_port").set_value(25)
self.prefs = self.glade.get_widget("prefs_box")
self.prefs.show_all()
self.treeview = self.glade.get_widget("smtp_recipients")
treeview_selection = self.treeview.get_selection()
treeview_selection.connect("changed", self.on_treeview_selection_changed)
self.model = gtk.ListStore(str, bool)
renderer = gtk.CellRendererText()
renderer.connect("edited", self.on_cell_edited, self.model)
renderer.set_data("recipient", RECIPIENT_FIELD)
column = gtk.TreeViewColumn("Recipients", renderer,
text=RECIPIENT_FIELD,
editable=RECIPIENT_EDIT)
column.set_expand(True)
self.treeview.append_column(column)
self.treeview.set_model(self.model)
deluge.common.get_default_download_dir()
self.glade.signal_autoconnect({
'on_add_button_clicked': (self.on_add_button_clicked,
self.treeview),
'on_delete_button_clicked': (self.on_delete_button_clicked,
self.treeview),
'on_enabled_toggled': self.on_enabled_toggled,
'on_sound_enabled_toggled': self.on_sound_enabled_toggled
})
component.get("Preferences").add_page("Notifications", self.prefs)
component.get("PluginManager").register_hook("on_apply_prefs",
self.on_apply_prefs)
component.get("PluginManager").register_hook("on_show_prefs",
self.on_show_prefs)
if not POPUP_ENABLED:
self.glade.get_widget("popup_enabled").set_property('sensitive',
False)
else:
client.register_event_handler("NotificationPopupEvent",
self.notify_popup)
client.register_event_handler("NotificationBlinkEvent",
self.notify_blink)
self.tray = component.get("SystemTray")
if not SOUND_AVAILABLE:
self.glade.get_widget("sound_enabled").set_property('sensitive',
False)
self.glade.get_widget('sound_path').set_property('sensitive', False)
else:
client.register_event_handler("NotificationSoundEvent",
self.notify_sound)
# Force config populate
client.notifications.get_config().addCallback(self.cb_get_config)
def disable(self):
Notifications.disable(self)
component.get("Preferences").remove_page("Notifications")
component.get("PluginManager").deregister_hook("on_apply_prefs",
self.on_apply_prefs)
component.get("PluginManager").deregister_hook("on_show_prefs",
self.on_show_prefs)
def on_apply_prefs(self):
log.debug("applying prefs for Notifications")
config = {
"smtp_enabled": self.glade.get_widget("smtp_enabled").get_active(),
"smtp_host": self.glade.get_widget("smtp_host").get_text(),
"smtp_port": self.glade.get_widget("smtp_port").get_value(),
"smtp_user": self.glade.get_widget("smtp_user").get_text(),
"smtp_pass": self.glade.get_widget("smtp_pass").get_text(),
"smtp_from": self.glade.get_widget("smtp_from").get_text(),
"smtp_tls": self.glade.get_widget("smtp_tls").get_active(),
"smtp_recipients": [dest[0] for dest in self.model if
dest[0]!='USER@HOST'],
"blink_enabled": self.glade.get_widget("blink_enabled").get_active(),
"sound_enabled": self.glade.get_widget("sound_enabled").get_active(),
"sound_path": self.glade.get_widget("sound_path").get_filename(),
"popup_enabled": self.glade.get_widget("popup_enabled").get_active()
}
client.notifications.set_config(config)
def on_show_prefs(self):
client.notifications.get_config().addCallback(self.cb_get_config)
def cb_get_config(self, config):
"callback for on show_prefs"
self.config = config
self.glade.get_widget("smtp_host").set_text(config["smtp_host"])
self.glade.get_widget("smtp_port").set_value(config["smtp_port"])
self.glade.get_widget("smtp_user").set_text(config["smtp_user"])
self.glade.get_widget("smtp_pass").set_text(config["smtp_pass"])
self.glade.get_widget("smtp_from").set_text(config["smtp_from"])
self.glade.get_widget("smtp_tls").set_active(config["smtp_tls"])
self.model.clear()
for recipient in config['smtp_recipients']:
self.model.set(self.model.append(),
RECIPIENT_FIELD, recipient,
RECIPIENT_EDIT, False)
self.glade.get_widget("smtp_enabled").set_active(config['smtp_enabled'])
self.glade.get_widget("sound_enabled").set_active(
config['sound_enabled']
)
self.glade.get_widget("popup_enabled").set_active(
config['popup_enabled']
)
self.glade.get_widget("blink_enabled").set_active(
config['blink_enabled']
)
if config['sound_path']:
sound_path = config['sound_path']
else:
sound_path = deluge.common.get_default_download_dir()
self.glade.get_widget("sound_path").set_filename(sound_path)
# Force toggle
self.on_enabled_toggled(self.glade.get_widget("smtp_enabled"))
self.on_sound_enabled_toggled(self.glade.get_widget('sound_enabled'))
def on_add_button_clicked(self, widget, treeview):
model = treeview.get_model()
model.set(model.append(),
RECIPIENT_FIELD, "USER@HOST",
RECIPIENT_EDIT, True)
def on_delete_button_clicked(self, widget, treeview):
selection = treeview.get_selection()
model, iter = selection.get_selected()
if iter:
path = model.get_path(iter)[0]
model.remove(iter)
def on_cell_edited(self, cell, path_string, new_text, model):
log.debug("%s %s %s %s", cell, path_string, new_text, model)
iter = model.get_iter_from_string(path_string)
path = model.get_path(iter)[0]
model.set(iter, RECIPIENT_FIELD, new_text)
def on_treeview_selection_changed(self, selection):
model, selected_connection_iter = selection.get_selected()
if selected_connection_iter:
self.glade.get_widget("delete_button").set_property('sensitive',
True)
else:
self.glade.get_widget("delete_button").set_property('sensitive',
False)
def on_enabled_toggled(self, widget):
if widget.get_active():
for widget in ('smtp_host', 'smtp_port', 'smtp_user', 'smtp_pass',
'smtp_pass', 'smtp_tls', 'smtp_from',
'smtp_recipients'):
self.glade.get_widget(widget).set_property('sensitive', True)
else:
for widget in ('smtp_host', 'smtp_port', 'smtp_user', 'smtp_pass',
'smtp_pass', 'smtp_tls', 'smtp_from',
'smtp_recipients'):
self.glade.get_widget(widget).set_property('sensitive', False)
def on_sound_enabled_toggled(self, widget):
if widget.get_active():
self.glade.get_widget('sound_path').set_property('sensitive', True)
else:
self.glade.get_widget('sound_path').set_property('sensitive', False)
def notify_blink(self):
return defer.maybeDeferred(self.tray.blink, True)
def notify_email(self, title='', message='', smtp_from='', recipients=[]):
client.notifications.notify_email(title, message, smtp_from, recipients)
def notify_flash(self, title='', message=''):
client.notifications.notify_flash(title, message)
def notify_popup(self, title='', message=''):
if not self.config['popup_enabled']:
return defer.succeed("Popup notification is not enabled.")
if not POPUP_ENABLED:
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():
log.warning("pynotify failed to show notification")
return defer.fail("pynotify failed to show notification")
return defer.succeed("Notification popup shown")
def notify_sound(self, sound_path=''):
if not self.config['sound_enabled']:
return defer.succeed("Sound notification not enabled")
if not SOUND_AVAILABLE:
log.warning("pygame is not installed")
return defer.fail("pygame is not installed")
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:
log.warning("pygame failed to play because %s" % (message))
return defer.fail("Sound notification failed %s" % (message))
else:
log.info("sound notification played successfully")
return defer.succeed("Sound notification Success")

View file

@ -0,0 +1,149 @@
#
# notifications.py
#
# Copyright (C) 2009 Pedro Algarvio <ufs@ufsoft.org>
#
# Deluge is free software.
#
# You may redistribute it and/or modify it under the terms of the
# GNU General Public License, as published by the Free Software
# Foundation; either version 3 of the License, or (at your option)
# any later version.
#
# deluge 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 deluge. If not, write to:
# The Free Software Foundation, Inc.,
# 51 Franklin Street, Fifth Floor
# Boston, MA 02110-1301, USA.
#
# In addition, as a special exception, the copyright holders give
# permission to link the code of portions of this program with the OpenSSL
# library.
# You must obey the GNU General Public License in all respects for all of
# the code used other than OpenSSL. If you modify file(s) with this
# exception, you may extend this exception to your version of the file(s),
# but you are not obligated to do so. If you do not wish to do so, delete
# this exception statement from your version. If you delete this exception
# statement from all source files in the program, then also delete it here.
#
#
from twisted.internet import defer, threads
from deluge import component
from deluge.core.rpcserver import export
from deluge.log import LOG as log
from deluge.ui.client import client
class Notifications(component.Component):
def __init__(self, name):
component.Component.__init__(self, "Notifications")
log.debug("\n\nSTARTING NOTIFICATIONS\n\n")
def enable(self):
log.debug("\n\nENABLING NOTIFICATIONS\n\n")
def disable(self):
log.debug("\n\nDISABLING NOTIFICATIONS\n\n")
def notify_blink(self):
raise NotImplementedError("%s has not implemented this method" %
self.__class__.__name__)
def notify_email(self, title='', message='', smtp_from='', recipients=[]):
raise NotImplementedError("%s has not implemented this method" %
self.__class__.__name__)
def notify_flash(self, title='', message=''):
raise NotImplementedError("%s has not implemented this method" %
self.__class__.__name__)
def notify_popup(self, title='', message=''):
raise NotImplementedError("%s has not implemented this method" %
self.__class__.__name__)
def notify_sound(self, sound_path=''):
raise NotImplementedError("%s has not implemented this method" %
self.__class__.__name__)
def notify(self,
# COMMON
title = '', message='',
# EMAIL
smtp_from='', recipients=[],
# SOUND
sound_path=''):
self.notify_blink()
self.notify_email(title, message, smtp_from, recipients)
self.notify_flash(title, message)
self.notify_popup(title, message)
self.notify_sound(sound_path)
def _on_notify_sucess(self, result, kind):
log.debug("Notification success using %s: %s", kind, result)
def _on_notify_failure(self, failure, kind):
log.debug("Notification failure using %s: %s", kind, failure)
# def _on_torrent_finished_event(self, torrent_id):
# log.debug("\n\nhandler for TorrentFinishedEvent called")
# torrent = component.get("TorrentManager")[torrent_id]
# torrent_status = torrent.get_status({})
# # Email
# title = _("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
#
# d0 = defer.maybeDeferred(self.notify_blink)
# d0.addCallback(self._on_notify_sucess, 'blink')
# d0.addErrback(self._on_notify_failure, 'blink')
# log.debug("Blink notification callback yielded")
#
## self.notify_email(title, message)
# d1 = defer.maybeDeferred(self.notify_email, title, message)
# d1.addCallback(self._on_notify_sucess, 'email')
# d1.addErrback(self._on_notify_failure, 'email')
## d.
## yield d
# log.debug("Email notification callback yielded")
#
# d2 = defer.maybeDeferred(self.notify_flash, title, message)
# d2.addCallback(self._on_notify_sucess, 'flash')
# d2.addErrback(self._on_notify_failure, 'flash')
## d.
## yield d
# log.debug("Flash notification callback yielded")
# # Sound
## self.notify_sound()
# d3 = defer.maybeDeferred(self.notify_sound)
# d3.addCallback(self._on_notify_sucess, 'sound')
# d3.addErrback(self._on_notify_failure, 'sound')
## yield d
# log.debug("Sound notification callback yielded")
#
# # Popup
# title = _("Finished Torrent")
# message = _("The torrent \"%(name)s\" including %(num_files)i "
# "has finished downloading.") % torrent_status
## self.notify_popup(title, message)
# d4 = defer.maybeDeferred(self.notify_popup, title, message)
# d4.addCallback(self._on_notify_sucess, 'popup')
# d4.addErrback(self._on_notify_failure, 'popup')
## yield d
# log.debug("Popup notification callback yielded")
#
# d5 = defer.maybeDeferred(self.notify_sound)
# d5.addCallback(self._on_notify_sucess, 'sound')
# d5.addErrback(self._on_notify_failure, 'sound')

View file

@ -0,0 +1,57 @@
#
# webui.py
#
# Copyright (C) 2009 Pedro Algarvio <ufs@ufsoft.org>
#
# Basic plugin template created by:
# Copyright (C) 2008 Martijn Voncken <mvoncken@gmail.com>
# Copyright (C) 2007-2009 Andrew Resch <andrewresch@gmail.com>
# Copyright (C) 2009 Damien Churchill <damoxc@gmail.com>
#
# Deluge is free software.
#
# You may redistribute it and/or modify it under the terms of the
# GNU General Public License, as published by the Free Software
# Foundation; either version 3 of the License, or (at your option)
# any later version.
#
# deluge 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 deluge. If not, write to:
# The Free Software Foundation, Inc.,
# 51 Franklin Street, Fifth Floor
# Boston, MA 02110-1301, USA.
#
# In addition, as a special exception, the copyright holders give
# permission to link the code of portions of this program with the OpenSSL
# library.
# You must obey the GNU General Public License in all respects for all of
# the code used other than OpenSSL. If you modify file(s) with this
# exception, you may extend this exception to your version of the file(s),
# but you are not obligated to do so. If you do not wish to do so, delete
# this exception statement from your version. If you delete this exception
# statement from all source files in the program, then also delete it here.
#
from deluge.log import LOG as log
from deluge.ui.client import client
from deluge import component
from deluge.plugins.pluginbase import WebPluginBase
# Relative imports
from common import get_resource
from manager import Notifications
class WebUI(WebPluginBase, Notifications):
scripts = [get_resource("notifications.js")]
def enable(self):
Notifications.enable(self)
def disable(self):
Notifications.disable(self)

View file

@ -0,0 +1,73 @@
#
# setup.py
#
# Copyright (C) 2009 Pedro Algarvio <ufs@ufsoft.org>
#
# Basic plugin template created by:
# Copyright (C) 2008 Martijn Voncken <mvoncken@gmail.com>
# Copyright (C) 2007-2009 Andrew Resch <andrewresch@gmail.com>
# Copyright (C) 2009 Damien Churchill <damoxc@gmail.com>
#
# Deluge is free software.
#
# You may redistribute it and/or modify it under the terms of the
# GNU General Public License, as published by the Free Software
# Foundation; either version 3 of the License, or (at your option)
# any later version.
#
# deluge 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 deluge. If not, write to:
# The Free Software Foundation, Inc.,
# 51 Franklin Street, Fifth Floor
# Boston, MA 02110-1301, USA.
#
# In addition, as a special exception, the copyright holders give
# permission to link the code of portions of this program with the OpenSSL
# library.
# You must obey the GNU General Public License in all respects for all of
# the code used other than OpenSSL. If you modify file(s) with this
# exception, you may extend this exception to your version of the file(s),
# but you are not obligated to do so. If you do not wish to do so, delete
# this exception statement from your version. If you delete this exception
# statement from all source files in the program, then also delete it here.
#
from setuptools import setup
__plugin_name__ = "Notifications"
__author__ = "Pedro Algarvio"
__author_email__ = "ufs@ufsoft.org"
__version__ = "0.1"
__url__ = "http://dev.deluge-torrent.org/"
__license__ = "GPLv3"
__description__ = ""
__long_description__ = """"""
__pkg_data__ = {__plugin_name__.lower(): ["template/*", "data/*"]}
setup(
name=__plugin_name__,
version=__version__,
description=__description__,
author=__author__,
author_email=__author_email__,
url=__url__,
license=__license__,
long_description=__long_description__ if __long_description__ else __description__,
packages=[__plugin_name__.lower()],
package_data = __pkg_data__,
entry_points="""
[deluge.plugin.core]
%s = %s:CorePlugin
[deluge.plugin.gtkui]
%s = %s:GtkUIPlugin
[deluge.plugin.webui]
%s = %s:WebUIPlugin
""" % ((__plugin_name__, __plugin_name__.lower())*3)
)

View file

@ -84,8 +84,7 @@ class TorrentInfo(object):
# Get the torrent data from the torrent file # Get the torrent data from the torrent file
try: try:
log.debug("Attempting to open %s.", filename) log.debug("Attempting to open %s.", filename)
self.__m_filedata = open(filename, "rb").read() self.__m_metadata = bencode.bdecode(open(filename, "rb").read())
self.__m_metadata = bencode.bdecode(self.__m_filedata)
except Exception, e: except Exception, e:
log.warning("Unable to open %s: %s", filename, e) log.warning("Unable to open %s: %s", filename, e)
raise e raise e
@ -213,16 +212,6 @@ class TorrentInfo(object):
""" """
return self.__m_metadata return self.__m_metadata
@property
def filedata(self):
"""
The torrents file data. This will be the bencoded dictionary read
from the torrent file.
:rtype: string
"""
return self.__m_filedata
class FileTree(object): class FileTree(object):
""" """
Convert a list of paths in a file tree. Convert a list of paths in a file tree.

View file

@ -249,7 +249,7 @@ class ConsoleUI(component.Component):
""" """
self.batch_write = batch self.batch_write = batch
if not batch and self.interactive: if not batch:
self.screen.refresh() self.screen.refresh()
def write(self, line): def write(self, line):

View file

@ -211,7 +211,7 @@ class AddTorrentDialog(component.Component):
new_row = self.torrent_liststore.append( new_row = self.torrent_liststore.append(
[info.info_hash, info.name, filename]) [info.info_hash, info.name, filename])
self.files[info.info_hash] = info.files self.files[info.info_hash] = info.files
self.infos[info.info_hash] = info.filedata self.infos[info.info_hash] = info.metadata
self.listview_torrents.get_selection().select_iter(new_row) self.listview_torrents.get_selection().select_iter(new_row)
self.set_default_options() self.set_default_options()
@ -226,11 +226,7 @@ class AddTorrentDialog(component.Component):
new_row = None new_row = None
for uri in uris: for uri in uris:
s = uri.split("&")[0][20:] info_hash = base64.b32decode(uri.split("&")[0][20:]).encode("hex")
if len(s) == 32:
info_hash = base64.b32decode(s).encode("hex")
elif len(s) == 40:
info_hash = s
if info_hash in self.infos: if info_hash in self.infos:
log.debug("Torrent already in list!") log.debug("Torrent already in list!")
continue continue
@ -720,6 +716,11 @@ class AddTorrentDialog(component.Component):
if row is not None: if row is not None:
self.save_torrent_options(row) self.save_torrent_options(row)
torrent_filenames = []
torrent_magnets = []
torrent_magnet_options = []
torrent_options = []
row = self.torrent_liststore.get_iter_first() row = self.torrent_liststore.get_iter_first()
while row != None: while row != None:
torrent_id = self.torrent_liststore.get_value(row, 0) torrent_id = self.torrent_liststore.get_value(row, 0)
@ -734,16 +735,26 @@ class AddTorrentDialog(component.Component):
options["file_priorities"] = file_priorities options["file_priorities"] = file_priorities
if deluge.common.is_magnet(filename): if deluge.common.is_magnet(filename):
torrent_magnets.append(filename)
del options["file_priorities"] del options["file_priorities"]
client.core.add_torrent_magnet(filename, options) torrent_magnet_options.append(options)
else: else:
client.core.add_torrent_file( torrent_filenames.append(filename)
os.path.split(filename)[-1], torrent_options.append(options)
base64.encodestring(self.infos[torrent_id]),
options)
row = self.torrent_liststore.iter_next(row) row = self.torrent_liststore.iter_next(row)
if torrent_filenames:
for i, f in enumerate(torrent_filenames):
client.core.add_torrent_file(
os.path.split(f)[-1],
base64.encodestring(open(f, "rb").read()),
torrent_options[i])
if torrent_magnets:
for i, m in enumerate(torrent_magnets):
client.core.add_torrent_magnet(m, torrent_magnet_options[i])
client.force_call(False)
self.hide() self.hide()
def _on_button_apply_clicked(self, widget): def _on_button_apply_clicked(self, widget):

View file

@ -35,8 +35,6 @@
"""Common functions for various parts of gtkui to use.""" """Common functions for various parts of gtkui to use."""
import os
import pygtk import pygtk
pygtk.require('2.0') pygtk.require('2.0')
import gtk, gtk.glade import gtk, gtk.glade

View file

@ -3,6 +3,7 @@
<!-- interface-requires gtk+ 2.12 --> <!-- interface-requires gtk+ 2.12 -->
<!-- interface-naming-policy toplevel-contextual --> <!-- interface-naming-policy toplevel-contextual -->
<widget class="GtkDialog" id="dialog_add_torrent"> <widget class="GtkDialog" id="dialog_add_torrent">
<property name="height_request">560</property>
<property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property> <property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property>
<property name="border_width">5</property> <property name="border_width">5</property>
<property name="title" translatable="yes">Add Torrents</property> <property name="title" translatable="yes">Add Torrents</property>
@ -13,14 +14,12 @@
<widget class="GtkVBox" id="dialog-vbox1"> <widget class="GtkVBox" id="dialog-vbox1">
<property name="visible">True</property> <property name="visible">True</property>
<property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property> <property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property>
<property name="orientation">vertical</property>
<property name="spacing">2</property> <property name="spacing">2</property>
<child> <child>
<widget class="GtkVPaned" id="vpaned1"> <widget class="GtkVPaned" id="vpaned1">
<property name="visible">True</property> <property name="visible">True</property>
<property name="can_focus">True</property> <property name="can_focus">True</property>
<property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property> <property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property>
<property name="orientation">vertical</property>
<child> <child>
<widget class="GtkFrame" id="frame2"> <widget class="GtkFrame" id="frame2">
<property name="visible">True</property> <property name="visible">True</property>
@ -38,7 +37,6 @@
<widget class="GtkVBox" id="vbox2"> <widget class="GtkVBox" id="vbox2">
<property name="visible">True</property> <property name="visible">True</property>
<property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property> <property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property>
<property name="orientation">vertical</property>
<child> <child>
<widget class="GtkScrolledWindow" id="scrolledwindow1"> <widget class="GtkScrolledWindow" id="scrolledwindow1">
<property name="visible">True</property> <property name="visible">True</property>
@ -347,7 +345,6 @@
<property name="visible">True</property> <property name="visible">True</property>
<property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property> <property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property>
<property name="border_width">5</property> <property name="border_width">5</property>
<property name="orientation">vertical</property>
<property name="spacing">5</property> <property name="spacing">5</property>
<child> <child>
<widget class="GtkFrame" id="frame7"> <widget class="GtkFrame" id="frame7">
@ -428,7 +425,6 @@
<widget class="GtkVBox" id="vbox4"> <widget class="GtkVBox" id="vbox4">
<property name="visible">True</property> <property name="visible">True</property>
<property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property> <property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property>
<property name="orientation">vertical</property>
<child> <child>
<widget class="GtkRadioButton" id="radio_full"> <widget class="GtkRadioButton" id="radio_full">
<property name="label" translatable="yes">Full</property> <property name="label" translatable="yes">Full</property>
@ -661,7 +657,6 @@
<widget class="GtkVBox" id="vbox5"> <widget class="GtkVBox" id="vbox5">
<property name="visible">True</property> <property name="visible">True</property>
<property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property> <property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property>
<property name="orientation">vertical</property>
<property name="spacing">5</property> <property name="spacing">5</property>
<child> <child>
<widget class="GtkCheckButton" id="chk_paused"> <widget class="GtkCheckButton" id="chk_paused">
@ -906,8 +901,6 @@
<signal name="clicked" handler="on_button_cancel_clicked"/> <signal name="clicked" handler="on_button_cancel_clicked"/>
</widget> </widget>
<packing> <packing>
<property name="expand">False</property>
<property name="fill">False</property>
<property name="position">0</property> <property name="position">0</property>
</packing> </packing>
</child> </child>
@ -922,8 +915,6 @@
<signal name="clicked" handler="on_button_add_clicked"/> <signal name="clicked" handler="on_button_add_clicked"/>
</widget> </widget>
<packing> <packing>
<property name="expand">False</property>
<property name="fill">False</property>
<property name="position">1</property> <property name="position">1</property>
</packing> </packing>
</child> </child>
@ -951,13 +942,11 @@
<widget class="GtkVBox" id="dialog-vbox4"> <widget class="GtkVBox" id="dialog-vbox4">
<property name="visible">True</property> <property name="visible">True</property>
<property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property> <property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property>
<property name="orientation">vertical</property>
<property name="spacing">2</property> <property name="spacing">2</property>
<child> <child>
<widget class="GtkVBox" id="vbox6"> <widget class="GtkVBox" id="vbox6">
<property name="visible">True</property> <property name="visible">True</property>
<property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property> <property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property>
<property name="orientation">vertical</property>
<property name="spacing">5</property> <property name="spacing">5</property>
<child> <child>
<widget class="GtkHBox" id="hbox7"> <widget class="GtkHBox" id="hbox7">
@ -1064,8 +1053,6 @@
<property name="use_stock">True</property> <property name="use_stock">True</property>
</widget> </widget>
<packing> <packing>
<property name="expand">False</property>
<property name="fill">False</property>
<property name="position">0</property> <property name="position">0</property>
</packing> </packing>
</child> </child>
@ -1082,8 +1069,6 @@
<property name="use_stock">True</property> <property name="use_stock">True</property>
</widget> </widget>
<packing> <packing>
<property name="expand">False</property>
<property name="fill">False</property>
<property name="position">1</property> <property name="position">1</property>
</packing> </packing>
</child> </child>
@ -1111,13 +1096,11 @@
<widget class="GtkVBox" id="dialog-vbox5"> <widget class="GtkVBox" id="dialog-vbox5">
<property name="visible">True</property> <property name="visible">True</property>
<property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property> <property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property>
<property name="orientation">vertical</property>
<property name="spacing">2</property> <property name="spacing">2</property>
<child> <child>
<widget class="GtkVBox" id="vbox7"> <widget class="GtkVBox" id="vbox7">
<property name="visible">True</property> <property name="visible">True</property>
<property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property> <property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property>
<property name="orientation">vertical</property>
<property name="spacing">5</property> <property name="spacing">5</property>
<child> <child>
<widget class="GtkHBox" id="hbox8"> <widget class="GtkHBox" id="hbox8">
@ -1263,8 +1246,6 @@
<property name="use_stock">True</property> <property name="use_stock">True</property>
</widget> </widget>
<packing> <packing>
<property name="expand">False</property>
<property name="fill">False</property>
<property name="position">0</property> <property name="position">0</property>
</packing> </packing>
</child> </child>
@ -1281,8 +1262,6 @@
<property name="use_stock">True</property> <property name="use_stock">True</property>
</widget> </widget>
<packing> <packing>
<property name="expand">False</property>
<property name="fill">False</property>
<property name="position">1</property> <property name="position">1</property>
</packing> </packing>
</child> </child>

View file

@ -310,6 +310,9 @@ Please see the details below for more information."), details=traceback.format_e
_("Error Starting Daemon"), _("Error Starting Daemon"),
_("There was an error starting the daemon process. Try running it from a console to see if there is an error.")).run() _("There was an error starting the daemon process. Try running it from a console to see if there is an error.")).run()
# We'll try 30 reconnects at 500ms intervals
try_counter = 30
def on_connect(connector): def on_connect(connector):
component.start() component.start()
def on_connect_fail(result, try_counter): def on_connect_fail(result, try_counter):
@ -320,15 +323,15 @@ Please see the details below for more information."), details=traceback.format_e
try_counter -= 1 try_counter -= 1
import time import time
time.sleep(0.5) time.sleep(0.5)
do_connect(try_counter) do_connect()
return result return result
def do_connect(try_counter): def do_connect():
client.connect(*host[1:]).addCallback(on_connect).addErrback(on_connect_fail, try_counter) client.connect(*host[1:]).addCallback(on_connect).addErrback(on_connect_fail, try_counter)
if try_connect: if try_connect:
do_connect(6) do_connect()
break
if self.config["show_connection_manager_on_start"]: if self.config["show_connection_manager_on_start"]:
# XXX: We need to call a simulate() here, but this could be a bug in twisted # XXX: We need to call a simulate() here, but this could be a bug in twisted

View file

@ -35,7 +35,7 @@
import sys import sys
import os import os.path
import base64 import base64
import deluge.rencode import deluge.rencode
@ -71,9 +71,7 @@ class IPCInterface(component.Component):
_args = [] _args = []
for arg in args: for arg in args:
if arg.strip(): if arg.strip():
if not deluge.common.is_magnet(arg) and not deluge.common.is_url(arg): _args.append(os.path.abspath(arg))
arg = os.path.abspath(arg)
_args.append(arg)
args = _args args = _args
socket = os.path.join(deluge.configmanager.get_config_dir("ipc"), "deluge-gtk") socket = os.path.join(deluge.configmanager.get_config_dir("ipc"), "deluge-gtk")
@ -106,20 +104,6 @@ class IPCInterface(component.Component):
reactor.run() reactor.run()
sys.exit(0) sys.exit(0)
else: else:
lockfile = socket + ".lock"
log.debug("Checking if lockfile exists: %s", lockfile)
if os.path.lexists(lockfile):
try:
os.kill(int(os.readlink(lockfile)), 0)
except OSError:
log.debug("Removing lockfile since it's stale.")
try:
os.remove(lockfile)
os.remove(socket)
except Exception, e:
log.error("Problem deleting lockfile or socket file!")
log.exception(e)
try: try:
self.factory = Factory() self.factory = Factory()
self.factory.protocol = IPCProtocolServer self.factory.protocol = IPCProtocolServer

View file

@ -196,9 +196,6 @@ class SystemTray(component.Component):
self.upload_rate = deluge.common.fsize(upload_rate) self.upload_rate = deluge.common.fsize(upload_rate)
def update(self): def update(self):
if not self.config["enable_system_tray"]:
return
# Set the tool tip text # Set the tool tip text
max_download_speed = self.max_download_speed max_download_speed = self.max_download_speed
max_upload_speed = self.max_upload_speed max_upload_speed = self.max_upload_speed

View file

@ -1,7 +1,7 @@
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"> <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml"> <html xmlns="http://www.w3.org/1999/xhtml">
<head> <head>
<title>Deluge: Web UI ${version}</title> <title>Deluge: Web UI (alpha) ${version}</title>
<link rel="shortcut icon" href="/icons/deluge.png" type="image/png" /> <link rel="shortcut icon" href="/icons/deluge.png" type="image/png" />
<link rel="icon" href="/icons/deluge.png" type="image/png" /> <link rel="icon" href="/icons/deluge.png" type="image/png" />