Compare commits

...

137 Commits

Author SHA1 Message Date
b0ceae8d28 Fix copying scripts 2010-10-31 10:50:51 -07:00
dc0bf3bc88 Update versions and changelog 2010-10-31 10:15:02 -07:00
3b9d7ff9c3 remove the convert conf script that won't actually work anymore 2010-10-31 14:35:12 +00:00
a165d5d746 fix a silly bug 2010-10-31 10:13:22 +00:00
cc02ebea6a Merge branch '1.3-stable' of deluge-torrent.org:deluge into 1.3-stable 2010-10-31 09:18:36 +00:00
41ffee5d8a change entry_points to a dictionary and split it up into console and gui scripts 2010-10-31 09:16:18 +00:00
14a89b3f8a don't depend on being executed from a specific directory 2010-10-29 10:09:15 +01:00
6f0c2af58a Fix up Changelog (entry was in wrong section) 2010-10-25 09:59:06 +11:00
84cccabf19 update changelog 2010-10-24 23:44:40 +01:00
7fb483adde fix a bug in the MultiOptionsManager that didn't fire the right arguments in the initial event fire 2010-10-24 23:42:29 +01:00
28ce7a70a0 apply patch from #1377 2010-10-24 13:30:39 +01:00
14565977fa include a file that fixes the SpinnerField onBlur method (no idea why it is set to emptyFn) 2010-10-23 22:22:00 +01:00
e4420ef354 fix the path to the loading gif (not that its actually used) 2010-10-23 21:24:05 +01:00
02ad0b93ab Fix hang on quit 2010-10-23 01:14:48 +11:00
6d2a001635 Fix #1373 use of cyrllic paths 2010-10-16 12:56:29 -07:00
2a3eb0578c Fix #1349 force a theme style with expander-size = 15 to show entries in the sidebar properly. 2010-10-15 19:31:36 -07:00
60fac28217 Keep a torrent paused after a forced recheck if it was paused to start. 2010-10-10 12:34:13 -07:00
59e01e7ecf add a check to ensure that the events loop doesn't continue indefinitely 2010-10-10 19:51:50 +01:00
4c52ee4229 Update ChangeLog for previous commit 2010-10-07 22:46:49 +11:00
8428524793 wrap client.disconnect() call with a check to see if its classic mode 2010-10-07 00:14:55 +01:00
21c8d02d9a Update ChangeLog for previous commit 2010-10-03 19:52:33 +11:00
0c687c7684 Make sure config value strings are utf8 encoded (fixes #1369) 2010-10-03 19:24:29 +11:00
78f9efefd9 Move decode_string/utf8_encoded to common 2010-10-03 19:24:27 +11:00
6b228ce31f Fix sidebar not updating (#1365) 2010-10-03 00:01:29 +10:00
40ce4ec731 Use better attribute / method names in blocklist 2010-09-26 11:39:47 +10:00
c029c312e4 Fix attribute error in blocklist plugin 2010-09-26 11:37:29 +10:00
16c38cd027 Set locale to the user's default settings in the gtkui 2010-09-20 02:44:18 +10:00
e23a6b852a Organise latest changes into appropriate sections 2010-09-19 20:59:27 +10:00
90e4de54e9 Do not include unnecessary dlls in windows build 2010-09-18 16:03:21 -07:00
c1505bea3a Update versions 2010-09-18 11:31:31 -07:00
6235e832fe include missing theme images 2010-09-18 00:48:22 +10:00
a71f14c47e include the .order files 2010-09-16 09:23:35 -07:00
ed3b23b0fc add all the other scripts to package_data 2010-09-16 09:23:19 -07:00
6402634ec1 Update win32 build files 2010-09-14 11:48:05 -07:00
3e68733cfd More clean-up of setup.py 2010-09-14 11:40:41 -07:00
f847a7dc4e Remove the custom 'install' class and include_package_data 2010-09-14 11:40:34 -07:00
c7954c20eb Fix preference page index when removing a preference page 2010-09-13 18:22:08 -07:00
dc7ed11601 Update ChangeLog 2010-09-13 16:11:57 -07:00
d898def9ec Fix bugs with unicode torrents in AutoAdd plugin. 2010-09-13 02:22:18 -04:00
3e2f6c4060 Fix bug in AutoAdd plugin where watchdirs would not display in gtkui when first enabled. 2010-09-13 02:22:17 -04:00
321a22a6f0 Increase max piece size to 8 MiB in create torrent dialog (closes #1358) 2010-09-13 08:53:19 +10:00
b4774af2f3 Fix VersionSplit behavior when comparing to a dev version. 2010-09-11 05:39:40 -04:00
d0fd709c74 AutoAdd plugin can now recover when one of the watchfolders has an unhandled exception. 2010-09-03 22:30:24 -04:00
e24212b3f8 Fix "adjustment with non-zero page size" deprication warning in autoadd plugin. 2010-09-03 22:28:41 -04:00
f8f72af6dc Add TorrentFileCompleted event. 2010-09-03 17:11:57 -07:00
b9caa4eeeb Fix issue when adding torrents without a 'session'. This can happen
when a plugin adds a torrent, like how the AutoAdd plugin works.  The
user that adds this torrent will be an empty string.
2010-09-03 14:29:36 -07:00
6c3b216b40 Use a temp filename with add_torrent_url 2010-08-31 00:11:58 +10:00
eaad867885 Update get_free_space test 2010-08-31 00:11:54 +10:00
f6b9f67df8 Fix error in last commit. 2010-08-26 02:33:24 -04:00
24fe3f7fd5 Ensure preferencesmanager only changes intended libtorrent session settings. 2010-08-26 02:23:40 -04:00
da2fb41a3a Fix scheduler so that it keeps current state, even after global settings change. 2010-08-26 01:39:40 -04:00
f8d7f22167 Ignore global stop ratio related settings in logic, so per torrent ones are used. 2010-08-24 22:47:24 -04:00
b75abc70e5 Add max active downloading and seeding options to scheduler. 2010-08-24 00:58:28 -04:00
2d821bd79a Merge branch '1.3-stable' of deluge-torrent.org:deluge into 1.3-stable 2010-08-23 17:35:31 -07:00
12d9a7a5bd Fix key error after enabling a plugin that introduces a new status key 2010-08-23 17:35:19 -07:00
c118fa36a9 Moved xdg import so it is not called on Windows, where it is unused. fixes #1343 2010-08-22 15:38:22 -04:00
82c91cdc51 AutoAdd plugin changes
adds queue to top option
adds ability to append extension instead of deleting torrent once added
2010-08-22 00:01:58 -04:00
5501094214 Fix unhandled exception when adding a torrent to the session 2010-08-21 12:54:19 -07:00
b41aa808be Fix issue where the save_timer is cancelled when it's not active 2010-08-21 12:54:13 -07:00
b9336889f5 Update version 2010-08-20 14:20:51 -07:00
995f5387eb Update windows files 2010-08-20 14:19:42 -07:00
38958d3c4f Update ChangeLog 2010-08-20 14:15:21 -07:00
b45e019f08 Fix man deluged not showing '-u' on its own line 2010-08-20 01:16:09 +10:00
d93fcf6eea Fix #1341 issue where Config would try to cancel the save_timer when it is None. 2010-08-18 12:32:11 -07:00
a2d75a5274 Add cache expiry check by key update times to fix issue where some status updates would not return
correctly if they were done < cache_time from the previous status request
2010-08-18 12:17:31 -07:00
8797c3ce1b Add additional test for get_torrents_status and fix the other one to properly invalidate the cache
time from startup before proceeding
2010-08-18 12:17:23 -07:00
79749cca03 Add test to demonstrate flaw in SessionProxy design. Need to keep track of update times for each
status key individually to fix this.
2010-08-18 11:05:21 -07:00
5fd8628761 fix the script resource on windows 2010-08-14 17:38:31 +01:00
0e80b3ea0a add the apple iOS bookmark icons from #1339 2010-08-14 16:15:18 +01:00
aa61d33ee2 Merge branch '1.3-stable' of deluge-torrent.org:deluge into 1.3-stable 2010-08-14 16:06:00 +01:00
13f29a77dd fix the system.listMethods json call when running in classic mode 2010-08-14 16:05:41 +01:00
97453d1411 Add test suite for SessionProxy 2010-08-10 09:58:28 -07:00
62d02091b3 Fix getting a torrent's status with an empty key list to return all the
torrent's status keys instead of an empty dict
2010-08-06 17:26:23 -07:00
161ad0ff0d use the get_libtorrent.sh script to get libtorrent if it is missing 2010-07-22 21:13:27 +01:00
7f323ec0fc add libtorrent fetch script 2010-07-22 18:17:51 +01:00
05aebbb575 remove the libtorrent submodule 2010-07-22 18:04:49 +01:00
de85e1dcdc a couple of fixes to stop the webui crashing when running within the gtkui 2010-07-18 23:11:02 +01:00
1ce480ff23 Only use an icon if it passes some sanity checks 2010-07-17 17:13:15 +10:00
007a9912d2 Use a blank icon when the tracker icon downloaded isn't a proper image 2010-07-15 19:17:48 -07:00
d793b9e6b8 Attempt to create a move_storage destination path if it doesn't exist 2010-07-15 10:50:15 -07:00
72ec926c1a Do not attempt to move a torrents storage if the destination path does
not exist
2010-07-12 14:45:51 -07:00
0d431ae7db Try to import system rencode before deluge.rencode to allow the use of the new rencode library at: http://code.google.com/p/rencode/ 2010-07-08 16:39:53 -07:00
f1e0e3be15 Add logging the user when a torrent is added or removed 2010-07-05 21:10:16 -07:00
e8bf5eb592 Fix ChangeLog 2010-07-04 02:13:31 +10:00
d9a2c4db72 Fix uncaught exception when closing deluge in classic mode 2010-07-02 02:43:07 +10:00
8fb7277a82 Fix typo 2010-07-02 02:41:30 +10:00
35128cf18f more improvements to the shift select 2010-07-01 14:21:37 +01:00
6ff1da2391 fix select 'upwards' 2010-07-01 14:08:20 +01:00
4614188c62 update changelog 2010-07-01 13:46:27 +01:00
80297b8e45 allow for shift selecting in tree grids 2010-07-01 13:45:22 +01:00
9173a9cfdd Update Changelog 2010-06-22 18:26:43 -07:00
571d1079f6 Fix #1302 an uncaught exception in an state_changed event handler in SessionProxy was preventing the
TorrentManager's stop method from properly saving all the resume data.
2010-06-22 18:25:50 -07:00
0497c407e1 Always look for -mt boost libraries first 2010-06-18 09:51:50 -07:00
8b58c960f3 fix typo 2010-06-12 22:48:00 +01:00
b615ebe1b8 change bits to bytes, thanks to charles 2010-06-11 15:59:33 +01:00
3ed8279219 Fix typo in label plugin - thanks konti 2010-06-11 00:51:46 +10:00
f2944bdeef Handle os.remove failing on windows 2010-06-08 03:20:48 +10:00
0fcd90ee2c Python independent version of previous commit 2010-06-08 01:57:13 +10:00
26460808e7 Fix console ui not liking paths with backslashes on windows (#1293) 2010-06-08 01:30:10 +10:00
7aba1af0b2 Print a more informative error message if the torrent file doesn't exist 2010-06-08 01:26:49 +10:00
4d2b7df49d Fix execute plugin only executing last event (#1306) 2010-06-08 00:20:07 +10:00
bd775d0d40 Only encode if necessary 2010-06-07 20:18:37 +10:00
7fb3c3c04c Fix unicode support in console ui (#1307) 2010-06-07 20:18:35 +10:00
19c27ee8c5 Add some debug logging statements 2010-06-04 18:05:27 +01:00
d69b8e1099 Fix an error in the key 2010-06-04 17:37:45 +01:00
88daf82cb0 Fix saving the correct event name 2010-06-04 16:37:50 +01:00
99c1a61383 Save the execute config after adding/removing/saving commands 2010-06-04 16:30:51 +01:00
2e55769c18 Fix typo in execute plugin 2010-05-20 00:06:18 +10:00
259d2633e7 Fix man deluged not showing '-d' on its own line 2010-05-16 22:32:59 +10:00
8e5aab660c Fix remote save path dialog not disappearing after creating a torrent 2010-05-16 18:13:34 +10:00
fc96e9d02c Fix only being able to click "remote path" once when creating a torrent 2010-05-16 18:13:31 +10:00
821d403a6c Fix deluged crashing on windows when logfile's directory doesn't exist 2010-05-16 13:12:51 +10:00
5e0d988ef0 Revert "Fix trac wiki turning CamelCase words into broken links"
This reverts commit 925ac42f7c.
2010-05-11 23:46:48 +10:00
925ac42f7c Fix trac wiki turning CamelCase words into broken links 2010-05-11 23:29:02 +10:00
1ac72b81b6 Update email address and copyright 2010-05-11 03:52:17 +10:00
3417caf1d2 Fix label plugin not remembering newly created labels 2010-05-09 17:52:29 +10:00
1bcfc91c35 Remove unused code from label plugin 2010-05-09 17:52:25 +10:00
6ee0e5b6be Update docstrings to use names from previous commit 2010-05-09 17:07:11 +10:00
58a74202e1 Use better names for TrackerIcons' args 2010-05-09 17:01:47 +10:00
a4c6f4e8c9 Return the noIcon for empty strings as well 2010-05-09 16:44:12 +10:00
60f3d32de7 Raise IconsError instead of IndexError (fixes infinite looping) 2010-05-09 16:36:54 +10:00
b3eed8a1f0 Add test for tracker_icons for when requesting an icon for host that is "". This test results in an infinite loop. 2010-05-08 20:10:12 -07:00
37137d9b54 Return 0 in get_free_space if the download_location is invalid 2010-05-08 20:09:57 -07:00
4fb14b581d Use previously defined host variable instead of getting the tracker host from the TreeModel 2010-05-08 20:09:49 -07:00
98da4d0291 Do not request a tracker icon if the host is "" 2010-05-08 20:09:37 -07:00
f0c06f4bc5 Update Changelog 2010-05-08 11:50:19 -07:00
63d701305c rebuild deluge-all and ext-extensions 2010-05-08 16:19:40 +01:00
99396afa0c update the build files for deluge-all and ext-extensions 2010-05-08 16:19:40 +01:00
6231dbd1ca fix the null comparison 2010-05-08 16:19:40 +01:00
8f021c7f06 set the baseCls for the add label form panel to x-plain 2010-05-08 16:19:40 +01:00
6bb4559d18 Make host_to_url support redirection and add another test 2010-05-08 16:26:08 +10:00
7c9eea0361 Try favicon.ico if there's a HTMLParseError 2010-05-08 15:50:57 +10:00
15247507d4 Fix relative redirecting in blocklist plugin 2010-05-06 23:31:25 +10:00
10de8d5475 Remove plugins that aren't release ready 2010-05-05 17:48:04 -07:00
e304c1f719 update the ChangeLog 2010-05-05 23:03:19 +01:00
48d3e89d84 Update windows build files 2010-05-05 15:01:52 -07:00
44f9e17a09 Update version
Update ChangeLog
2010-05-05 14:52:18 -07:00
120 changed files with 12527 additions and 11584 deletions

3
.gitmodules vendored
View File

@ -1,3 +0,0 @@
[submodule "libtorrent"]
path = libtorrent
url = git://deluge-torrent.org/libtorrent

View File

@ -1,4 +1,63 @@
=== Deluge 1.3.0 (In Development) ===
=== Deluge 1.3.1 (31 October 2010) ===
==== Core ====
* #1369: Fix non-ascii config folders not working in windows
==== GtkUI ====
* #1365: Fix sidebar not updating show/hide trackers
* #1247: Fix hang on quit
==== WebUI ====
* #1364: Fix preferences not saving when the web ui plugin is enabled in classic mode
* #1377: Fix bug when enabling plugins
* #1370: Fix issues with preferences
* #1312: Fix deluge-web using 100% CPU
=== Deluge 1.3.0 (18 September 2010) ===
==== Core ====
* Fix issue where the save_timer is cancelled when it's not active
* Fix unhandled exception when adding a torrent to the session
* Moved xdg import so it is not called on Windows, where it is unused. fixes #1343
* Fix key error after enabling a plugin that introduces a new status key
* Ignore global stop ratio related settings in logic, so per torrent ones are used.
* Ensure preferencesmanager only changes intended libtorrent session settings.
* Fix issue when adding torrents without a 'session'. This can happen when a plugin adds a torrent, like how the AutoAdd plugin works. The user that adds this torrent will be an empty string.
* Add TorrentFileCompleted event
==== GtkUI ====
* Increase max piece size to 8 MiB in create torrent dialog (closes #1358)
==== Scheduler ====
* Add max active downloading and seeding options to scheduler.
* Fix scheduler so that it keeps current state, even after global settings change.
==== AutoAdd ====
* AutoAdd plugin can now recover when one of the watchfolders has an unhandled exception.
* Fix bug in AutoAdd plugin where watchdirs would not display in gtkui when first enabled.
* Fix bugs with unicode torrents in AutoAdd plugin.
=== Deluge 1.3.0-rc2 (20 August 2010) ===
==== Core ====
* Fix tracker_icons failing on windows
* Fix #1302 an uncaught exception in an state_changed event handler in SessionProxy was preventing the TorrentManager's stop method from properly saving all the resume data
* Fix issue with SessionProxy not updating the torrent status correctly when get_torrent_status calls take place within the cache_expiry time
==== ConsoleUI ====
* #1307: Fix not being able to add torrents
* #1293: Fix not being able to add paths that contain backslashes
==== GtkUI ====
* Fix uncaught exception when closing deluge in classic mode
==== Execute ====
* #1306: Fix always executing last event
==== Label ====
* Fix being able to remove labels in web ui
==== WebUI ====
* #1319: Fix shift selecting in file trees
=== Deluge 1.3.0-rc1 (08 May 2010) ===
==== Core ====
* Implement #1063 option to delete torrent file copy on torrent removal - patch from Ghent
* Implement #457 progress bars for folders
@ -7,15 +66,33 @@
* #1112: Fix renaming files in add torrent dialog
* #1247: Fix deluge-gtk from hanging on shutdown
* #995: Rewrote tracker_icons
* Add AutoAdd plugin
* Add Notifications plugin
==== GtkUI ====
* Use new SessionProxy class for caching torrent status client-side
* Use torrent status diffs to reduce RPC traffic
==== Blocklist ====
* Implement local blocklist support
* #861: Pause transfers until blocklist is imported
* Fix redirection not working with relative paths
==== Execute ====
* Fix running commands with the TorrentAdded event
* Fix the web interface
==== Label ====
* Fix the web interface (#733)
==== Web ====
* Migrate to ExtJS 3.1
* Add gzip compression of HTTP data to the server
* Improve the efficiency of the TorrentGrid
* Improve the efficiency of the TorrentGrid with lots of torrents (#1026)
* Add a base parameter to allow reverse proxying (#1076)
* Fix showing all the peers in the details tab (#1054)
* Fix uploading torrent files in Opera or IE (#1087)
* Complete IE support
=== Deluge 1.2.0 - "Bursting like an infected kidney" (10 January 2010) ===
==== Core ====

View File

@ -7,6 +7,7 @@
* setuptools
* gettext
* pyxdg
* chardet
* geoip-database (optional)
* libtorrent >= 0.14, or build the included version
@ -16,9 +17,6 @@
* openssl
* zlib
=== UIs ===
* chardet
=== Gtk ===
* python-notify (libnotify python wrapper)
* pygame

View File

@ -41,6 +41,7 @@ import time
import subprocess
import platform
import sys
import chardet
try:
import json
@ -63,7 +64,6 @@ if not hasattr(json, "dumps"):
json.load = load
import pkg_resources
import xdg, xdg.BaseDirectory
import gettext
import locale
@ -150,6 +150,7 @@ def get_default_config_dir(filename=None):
else:
return os.path.join(os.environ.get("APPDATA"), "deluge")
else:
import xdg.BaseDirectory
if filename:
return os.path.join(xdg.BaseDirectory.save_config_path("deluge"), filename)
else:
@ -474,7 +475,7 @@ def free_space(path):
sectors, bytes, free, total = map(long, win32file.GetDiskFreeSpace(path))
return (free * sectors * bytes)
else:
disk_data = os.statvfs(path)
disk_data = os.statvfs(path.encode("utf8"))
block_size = disk_data.f_bsize
return disk_data.f_bavail * block_size
@ -560,6 +561,41 @@ def xml_encode(string):
string = string.replace(char, escape)
return string
def decode_string(s, encoding="utf8"):
"""
Decodes a string and re-encodes it in utf8. If it cannot decode using
`:param:encoding` then it will try to detect the string encoding and
decode it.
:param s: string to decode
:type s: string
:keyword encoding: the encoding to use in the decoding
:type encoding: string
"""
try:
s = s.decode(encoding).encode("utf8", "ignore")
except UnicodeDecodeError:
s = s.decode(chardet.detect(s)["encoding"], "ignore").encode("utf8", "ignore")
return s
def utf8_encoded(s):
"""
Returns a utf8 encoded string of s
:param s: (unicode) string to (re-)encode
:type s: basestring
:returns: a utf8 encoded string of s
:rtype: str
"""
if isinstance(s, str):
s = decode_string(s, locale.getpreferredencoding())
elif isinstance(s, unicode):
s = s.encode("utf8", "ignore")
return s
class VersionSplit(object):
"""
Used for comparing version numbers.
@ -570,13 +606,15 @@ class VersionSplit(object):
"""
def __init__(self, ver):
ver = ver.lower()
vs = ver.split("_") if "_" in ver else ver.split("-")
vs = ver.replace("_", "-").split("-")
self.version = [int(x) for x in vs[0].split(".")]
self.suffix = None
self.dev = False
if len(vs) > 1:
for s in ("rc", "alpha", "beta", "dev"):
if s in vs[1][:len(s)]:
self.suffix = vs[1]
if vs[1].startswith(("rc", "alpha", "beta")):
self.suffix = vs[1]
if vs[-1] == 'dev':
self.dev = True
def __cmp__(self, ver):
"""
@ -587,19 +625,8 @@ class VersionSplit(object):
"""
if self.version > ver.version or (self.suffix and self.suffix[:3] == "dev"):
return 1
if self.version < ver.version:
return -1
if self.version == ver.version:
if self.suffix == ver.suffix:
return 0
if self.suffix is None:
return 1
if ver.suffix is None:
return -1
if self.suffix < ver.suffix:
return -1
if self.suffix > ver.suffix:
return 1
# If there is no suffix we use z because we want final
# to appear after alpha, beta, and rc alphabetically.
v1 = [self.version, self.suffix or 'z', self.dev]
v2 = [ver.version, ver.suffix or 'z', ver.dev]
return cmp(v1, v2)

View File

@ -45,9 +45,9 @@ The format of the config file is two json encoded dicts:
<version dict>
<content dict>
The version dict contains two keys: file and format. The format version is
controlled by the Config class. It should only be changed when anything below
it is changed directly by the Config class. An example of this would be if we
The version dict contains two keys: file and format. The format version is
controlled by the Config class. It should only be changed when anything below
it is changed directly by the Config class. An example of this would be if we
changed the serializer for the content to something different.
The config file version is changed by the 'owner' of the config file. This is
@ -93,13 +93,13 @@ def prop(func):
def find_json_objects(s):
"""
Find json objects in a string.
:param s: the string to find json objects in
:type s: string
:returns: a list of tuples containing start and end locations of json objects in the string `s`
:rtype: [(start, end), ...]
"""
objects = []
opens = 0
@ -119,8 +119,8 @@ def find_json_objects(s):
start = index + offset + 1
return objects
class Config(object):
"""
This class is used to access/create/modify config files
@ -146,7 +146,8 @@ class Config(object):
self._save_timer = None
if defaults:
self.__config = dict(defaults)
for key, value in defaults.iteritems():
self.set_item(key, value)
# Load the config from file in the config_dir
if config_dir:
@ -187,6 +188,10 @@ what is currently in the config and it could not convert the value
5
"""
if isinstance(value, basestring):
value = deluge.common.utf8_encoded(value)
if not self.__config.has_key(key):
self.__config[key] = value
log.debug("Setting '%s' to %s of %s", key, value, type(value))
@ -200,7 +205,10 @@ what is currently in the config and it could not convert the value
if value is not None and oldtype != type(None) and oldtype != newtype:
try:
value = oldtype(value)
if oldtype == unicode:
value = oldtype(value, "utf8")
else:
value = oldtype(value)
except ValueError:
log.warning("Type '%s' invalid for '%s'", newtype, key)
raise
@ -250,7 +258,10 @@ what is currently in the config and it could not convert the value
5
"""
return self.__config[key]
if isinstance(self.__config[key], str):
return self.__config[key].decode("utf8")
else:
return self.__config[key]
def register_change_callback(self, callback):
"""
@ -348,21 +359,21 @@ what is currently in the config and it could not convert the value
return
objects = find_json_objects(data)
if not len(objects):
# No json objects found, try depickling it
try:
self.__config.update(pickle.loads(data))
except Exception, e:
log.exception(e)
log.warning("Unable to load config file: %s", filename)
log.warning("Unable to load config file: %s", filename)
elif len(objects) == 1:
start, end = objects[0]
try:
self.__config.update(json.loads(data[start:end]))
except Exception, e:
log.exception(e)
log.warning("Unable to load config file: %s", filename)
log.warning("Unable to load config file: %s", filename)
elif len(objects) == 2:
try:
start, end = objects[0]
@ -371,8 +382,8 @@ what is currently in the config and it could not convert the value
self.__config.update(json.loads(data[start:end]))
except Exception, e:
log.exception(e)
log.warning("Unable to load config file: %s", filename)
log.warning("Unable to load config file: %s", filename)
log.debug("Config %s version: %s.%s loaded: %s", filename,
self.__version["format"], self.__version["file"], self.__config)
@ -396,26 +407,24 @@ what is currently in the config and it could not convert the value
version = json.loads(data[start:end])
start, end = objects[1]
loaded_data = json.loads(data[start:end])
if self.__config == loaded_data and self.__version == version:
# The config has not changed so lets just return
self._save_timer.cancel()
return
except Exception, e:
log.warning("Unable to open config file: %s", filename)
if self._save_timer and self._save_timer.active():
self._save_timer.cancel()
return True
except IOError, e:
log.warning("Unable to open config file: %s because: %s", filename, e)
# Save the new config and make sure it's written to disk
try:
log.debug("Saving new config file %s", filename + ".new")
f = open(filename + ".new", "wb")
json.dump(self.__version, f, indent=2)
json.dump(self.__version, f, indent=2)
json.dump(self.__config, f, indent=2)
f.flush()
os.fsync(f.fileno())
f.close()
except Exception, e:
except IOError, e:
log.error("Error writing new config file: %s", e)
return False

View File

@ -42,6 +42,7 @@ import shutil
import threading
import pkg_resources
import warnings
import tempfile
from twisted.internet import reactor, defer
@ -238,7 +239,13 @@ class Core(component.Component):
log.info("Attempting to add url %s", url)
def on_get_file(filename):
# We got the file, so add it to the session
data = open(filename, "rb").read()
f = open(filename, "rb")
data = f.read()
f.close()
try:
os.remove(filename)
except Exception, e:
log.warning("Couldn't remove temp file: %s", e)
return self.add_torrent_file(filename, base64.encodestring(data), options)
def on_get_file_error(failure):
@ -247,7 +254,7 @@ class Core(component.Component):
log.error("Reason: %s", failure.getErrorMessage())
return failure
d = download_file(url, url.split("/")[-1], headers=headers)
d = download_file(url, tempfile.mkstemp()[1], headers=headers)
d.addCallback(on_get_file)
d.addErrback(on_get_file_error)
return d
@ -768,7 +775,10 @@ class Core(component.Component):
"""
if not path:
path = self.config["download_location"]
return deluge.common.free_space(path)
try:
return deluge.common.free_space(path)
except InvalidPathError:
return 0
@export
def get_libtorrent_version(self):

View File

@ -196,9 +196,8 @@ class FilterManager(component.Component):
value = status[field]
items[field][value] = items[field].get(value, 0) + 1
items["tracker_host"]["All"] = len(torrent_ids)
if "tracker_host" in items:
items["tracker_host"]["All"] = len(torrent_ids)
items["tracker_host"]["Error"] = len(tracker_error_filter(torrent_ids, ("Error",)))
if "state" in tree_keys and not show_zero_hits:

View File

@ -152,7 +152,6 @@ class PreferencesManager(component.Component):
def start(self):
self.core = component.get("Core")
self.session = component.get("Core").session
self.settings = component.get("Core").settings
# Register set functions in the Config
self.config.register_set_function("torrentfiles_location",
@ -233,6 +232,11 @@ class PreferencesManager(component.Component):
self.new_release_timer.stop()
# Config set functions
def session_set_setting(self, key, value):
settings = self.session.settings()
setattr(settings, key, value)
self.session.set_settings(settings)
def _on_config_value_change(self, key, value):
component.get("EventManager").emit(ConfigValueChangedEvent(key, value))
@ -274,8 +278,7 @@ class PreferencesManager(component.Component):
def _on_set_outgoing_ports(self, key, value):
if not self.config["random_outgoing_ports"]:
log.debug("outgoing port range set to %s-%s", value[0], value[1])
self.settings.outgoing_ports = value[0], value[1]
self.session.set_settings(self.settings)
self.session_set_setting("outgoing_ports", (value[0], value[1]))
def _on_set_random_outgoing_ports(self, key, value):
if value:
@ -284,13 +287,11 @@ class PreferencesManager(component.Component):
def _on_set_peer_tos(self, key, value):
log.debug("setting peer_tos to: %s", value)
try:
self.settings.peer_tos = chr(int(value, 16))
self.session_set_setting("peer_tos", chr(int(value, 16)))
except ValueError, e:
log.debug("Invalid tos byte: %s", e)
return
self.session.set_settings(self.settings)
def _on_set_dht(self, key, value):
log.debug("dht value set to %s", value)
state_file = deluge.configmanager.get_config_dir("dht.state")
@ -387,51 +388,39 @@ class PreferencesManager(component.Component):
self.session.set_max_half_open_connections(value)
def _on_set_max_connections_per_second(self, key, value):
self.settings.connection_speed = value
self.session.set_settings(self.settings)
self.session_set_setting("connection_speed", value)
def _on_ignore_limits_on_local_network(self, key, value):
self.settings.ignore_limits_on_local_network = value
self.session.set_settings(self.settings)
self.session_set_setting("ignore_limits_on_local_network", value)
def _on_set_share_ratio_limit(self, key, value):
log.debug("%s set to %s..", key, value)
self.settings.share_ratio_limit = value
self.session.set_settings(self.settings)
self.session_set_setting("share_ratio_limit", value)
def _on_set_seed_time_ratio_limit(self, key, value):
log.debug("%s set to %s..", key, value)
self.settings.seed_time_ratio_limit = value
self.session.set_settings(self.settings)
self.session_set_setting("seed_time_ratio_limit", value)
def _on_set_seed_time_limit(self, key, value):
log.debug("%s set to %s..", key, value)
# This value is stored in minutes in deluge, but libtorrent wants seconds
self.settings.seed_time_limit = int(value * 60)
self.session.set_settings(self.settings)
self.session_set_setting("seed_time_limit", int(value * 60))
def _on_set_max_active_downloading(self, key, value):
log.debug("%s set to %s..", key, value)
log.debug("active_downloads: %s", self.settings.active_downloads)
self.settings.active_downloads = value
self.session.set_settings(self.settings)
self.session_set_setting("active_downloads", value)
def _on_set_max_active_seeding(self, key, value):
log.debug("%s set to %s..", key, value)
log.debug("active_seeds: %s", self.settings.active_seeds)
self.settings.active_seeds = value
self.session.set_settings(self.settings)
self.session_set_setting("active_seeds", value)
def _on_set_max_active_limit(self, key, value):
log.debug("%s set to %s..", key, value)
log.debug("active_limit: %s", self.settings.active_limit)
self.settings.active_limit = value
self.session.set_settings(self.settings)
self.session_set_setting("active_limit", value)
def _on_set_dont_count_slow_torrents(self, key, value):
log.debug("%s set to %s..", key, value)
self.settings.dont_count_slow_torrents = value
self.session.set_settings(self.settings)
self.session_set_setting("dont_count_slow_torrents", value)
def _on_send_info(self, key, value):
log.debug("Sending anonymous stats..")
@ -491,8 +480,7 @@ class PreferencesManager(component.Component):
def _on_rate_limit_ip_overhead(self, key, value):
log.debug("%s: %s", key, value)
self.settings.rate_limit_ip_overhead = value
self.session.set_settings(self.settings)
self.session_set_setting("rate_limit_ip_overhead", value)
def _on_geoip_db_location(self, key, value):
log.debug("%s: %s", key, value)
@ -514,10 +502,8 @@ class PreferencesManager(component.Component):
def _on_cache_size(self, key, value):
log.debug("%s: %s", key, value)
self.settings.cache_size = value
self.session.set_settings(self.settings)
self.session_set_setting("cache_size", value)
def _on_cache_expiry(self, key, value):
log.debug("%s: %s", key, value)
self.settings.cache_expiry = value
self.session.set_settings(self.settings)
self.session_set_setting("cache_expiry", value)

View File

@ -47,7 +47,11 @@ from twisted.internet import ssl, reactor, defer
from OpenSSL import crypto, SSL
from types import FunctionType
import deluge.rencode as rencode
try:
import rencode
except ImportError:
import deluge.rencode as rencode
from deluge.log import LOG as log
import deluge.component as component
@ -90,13 +94,13 @@ def export(auth_level=AUTH_LEVEL_DEFAULT):
def format_request(call):
"""
Format the RPCRequest message for debug printing
:param call: the request
:type call: a RPCRequest
:returns: a formatted string for printing
:rtype: str
"""
try:
s = call[1] + "("
@ -111,7 +115,7 @@ def format_request(call):
return "UnicodeEncodeError, call: %s" % call
else:
return s
class DelugeError(Exception):
pass
@ -139,7 +143,7 @@ class DelugeRPCProtocol(Protocol):
"""
This method is called whenever data is received from a client. The
only message that a client sends to the server is a RPC Request message.
If the RPC Request message is valid, then the method is called in
If the RPC Request message is valid, then the method is called in
:meth:`dispatch`.
:param data: the data from the client. It should be a zlib compressed
@ -187,7 +191,7 @@ class DelugeRPCProtocol(Protocol):
:param data: the object that is to be sent to the client. This should
be one of the RPC message types.
:type data: object
"""
self.transport.write(zlib.compress(rencode.dumps(data)))
@ -254,7 +258,7 @@ class DelugeRPCProtocol(Protocol):
try:
ret = component.get("AuthManager").authorize(*args, **kwargs)
if ret:
self.factory.authorized_sessions[self.transport.sessionno] = ret
self.factory.authorized_sessions[self.transport.sessionno] = (ret, args[0])
self.factory.session_protocols[self.transport.sessionno] = self
except Exception, e:
sendError()
@ -283,7 +287,7 @@ class DelugeRPCProtocol(Protocol):
if method in self.factory.methods and self.transport.sessionno in self.factory.authorized_sessions:
try:
method_auth_requirement = self.factory.methods[method]._rpcserver_auth_level
auth_level = self.factory.authorized_sessions[self.transport.sessionno]
auth_level = self.factory.authorized_sessions[self.transport.sessionno][0]
if auth_level < method_auth_requirement:
# This session is not allowed to call this method
log.debug("Session %s is trying to call a method it is not authorized to call!", self.transport.sessionno)
@ -338,7 +342,7 @@ class RPCServer(component.Component):
self.factory = Factory()
self.factory.protocol = DelugeRPCProtocol
self.factory.session_id = -1
# Holds the registered methods
self.factory.methods = {}
# Holds the session_ids and auth levels
@ -417,26 +421,41 @@ class RPCServer(component.Component):
def get_session_id(self):
"""
Returns the session id of the current RPC.
:returns: the session id, this will be -1 if no connections have been made
:rtype: int
"""
return self.factory.session_id
def get_session_user(self):
"""
Returns the username calling the current RPC.
:returns: the username of the user calling the current RPC
:rtype: string
"""
session_id = self.get_session_id()
if session_id > -1 and session_id in self.factory.authorized_sessions:
return self.factory.authorized_sessions[session_id][1]
else:
# No connections made yet
return ""
def is_session_valid(self, session_id):
"""
Checks if the session is still valid, eg, if the client is still connected.
:param session_id: the session id
:type session_id: int
:returns: True if the session is valid
:rtype: bool
"""
return session_id in self.factory.authorized_sessions
def emit_event(self, event):
"""
Emits the event to interested clients.

View File

@ -179,6 +179,11 @@ class Torrent(object):
else:
self.time_added = time.time()
# Keep track if we're forcing a recheck of the torrent so that we can
# repause it after its done if necessary
self.forcing_recheck = False
self.forcing_recheck_paused = False
log.debug("Torrent object created.")
## Options methods ##
@ -393,12 +398,11 @@ class Torrent(object):
else:
status = self.status
if self.is_finished and (self.options["stop_at_ratio"] or self.config["stop_seed_at_ratio"]):
if self.is_finished and self.options["stop_at_ratio"]:
# We're a seed, so calculate the time to the 'stop_share_ratio'
if not status.upload_payload_rate:
return 0
stop_ratio = self.config["stop_seed_ratio"] if self.config["stop_seed_at_ratio"] else self.options["stop_ratio"]
stop_ratio = self.options["stop_ratio"]
return ((status.all_time_download * stop_ratio) - status.all_time_upload) / status.upload_payload_rate
left = status.total_wanted - status.total_done
@ -761,13 +765,8 @@ class Torrent(object):
if self.handle.is_finished():
# If the torrent has already reached it's 'stop_seed_ratio' then do not do anything
if self.config["stop_seed_at_ratio"] or self.options["stop_at_ratio"]:
if self.options["stop_at_ratio"]:
ratio = self.options["stop_ratio"]
else:
ratio = self.config["stop_seed_ratio"]
if self.get_ratio() >= ratio:
if self.options["stop_at_ratio"]:
if self.get_ratio() >= self.options["stop_ratio"]:
#XXX: This should just be returned in the RPC Response, no event
#self.signals.emit_event("torrent_resume_at_stop_ratio")
return
@ -794,6 +793,14 @@ class Torrent(object):
def move_storage(self, dest):
"""Move a torrent's storage location"""
if not os.path.exists(dest):
try:
# Try to make the destination path if it doesn't exist
os.makedirs(dest)
except IOError, e:
log.exception(e)
log.error("Could not move storage for torrent %s since %s does not exist and could not create the directory.", self.torrent_id, dest)
return False
try:
self.handle.move_storage(dest.encode("utf8"))
except:
@ -857,12 +864,15 @@ class Torrent(object):
def force_recheck(self):
"""Forces a recheck of the torrents pieces"""
paused = self.handle.is_paused()
try:
self.handle.force_recheck()
self.handle.resume()
except Exception, e:
log.debug("Unable to force recheck: %s", e)
return False
self.forcing_recheck = True
self.forcing_recheck_paused = paused
return True
def rename_files(self, filenames):

View File

@ -47,16 +47,14 @@ from twisted.internet.task import LoopingCall
from deluge._libtorrent import lt
from deluge.event import *
from deluge.error import *
import deluge.common
import deluge.component as component
from deluge.configmanager import ConfigManager, get_config_dir
from deluge.core.torrent import Torrent
from deluge.core.torrent import TorrentOptions
import deluge.core.oldstateupgrader
from deluge.ui.common import utf8_encoded
from deluge.common import utf8_encoded
from deluge.log import LOG as log
@ -192,6 +190,8 @@ class TorrentManager(component.Component):
self.on_alert_metadata_received)
self.alerts.register_handler("file_error_alert",
self.on_alert_file_error)
self.alerts.register_handler("file_completed_alert",
self.on_alert_file_completed)
def start(self):
# Get the pluginmanager reference
@ -256,16 +256,13 @@ class TorrentManager(component.Component):
def update(self):
for torrent_id, torrent in self.torrents.items():
if self.config["stop_seed_at_ratio"] or torrent.options["stop_at_ratio"] and torrent.state not in ("Checking", "Allocating", "Paused", "Queued"):
if torrent.options["stop_at_ratio"] and torrent.state not in ("Checking", "Allocating", "Paused", "Queued"):
# If the global setting is set, but the per-torrent isn't.. Just skip to the next torrent
# This is so that a user can turn-off the stop at ratio option on a per-torrent basis
if self.config["stop_seed_at_ratio"] and not torrent.options["stop_at_ratio"]:
if not torrent.options["stop_at_ratio"]:
continue
stop_ratio = self.config["stop_seed_ratio"]
if torrent.options["stop_at_ratio"]:
stop_ratio = torrent.options["stop_ratio"]
if torrent.get_ratio() >= stop_ratio and torrent.is_finished:
if self.config["remove_seed_at_ratio"] or torrent.options["remove_at_ratio"]:
if torrent.get_ratio() >= torrent.options["stop_ratio"] and torrent.is_finished:
if torrent.options["remove_at_ratio"]:
self.remove(torrent_id)
break
if not torrent.handle.is_paused():
@ -476,6 +473,7 @@ class TorrentManager(component.Component):
# Emit the torrent_added signal
component.get("EventManager").emit(TorrentAddedEvent(torrent.torrent_id))
log.info("Torrent %s added by user: %s", torrent.get_status(["name"])["name"], component.get("RPCServer").get_session_user())
return torrent.torrent_id
def load_torrent(self, torrent_id):
@ -515,6 +513,8 @@ class TorrentManager(component.Component):
if torrent_id not in self.torrents:
raise InvalidTorrentError("torrent_id not in session")
torrent_name = self.torrents[torrent_id].get_status(["name"])["name"]
# Emit the signal to the clients
component.get("EventManager").emit(PreTorrentRemovedEvent(torrent_id))
@ -562,7 +562,7 @@ class TorrentManager(component.Component):
# Emit the signal to the clients
component.get("EventManager").emit(TorrentRemovedEvent(torrent_id))
log.info("Torrent %s removed by user: %s", torrent_name, component.get("RPCServer").get_session_user())
return True
def load_state(self):
@ -849,7 +849,14 @@ class TorrentManager(component.Component):
torrent = self.torrents[str(alert.handle.info_hash())]
except:
return
# Check to see if we're forcing a recheck and set it back to paused
# if necessary
if torrent.forcing_recheck:
torrent.forcing_recheck = False
if torrent.forcing_recheck_paused:
torrent.handle.pause()
# Set the torrent state
torrent.update_state()
@ -1012,3 +1019,9 @@ class TorrentManager(component.Component):
except:
return
torrent.update_state()
def on_alert_file_completed(self, alert):
log.debug("file_completed_alert: %s", alert.message())
torrent_id = str(alert.handle.info_hash())
component.get("EventManager").emit(
TorrentFileCompletedEvent(torrent_id, alert.index))

View File

@ -164,6 +164,22 @@ class TorrentResumedEvent(DelugeEvent):
"""
self._args = [torrent_id]
class TorrentFileCompletedEvent(DelugeEvent):
"""
Emitted when a file completes.
This will only work with libtorrent 0.15 or greater.
"""
def __init__(self, torrent_id, index):
"""
:param torrent_id: the torrent_id
:type torrent_id: string
:param index: the file index
:type index: int
"""
self._args = [torrent_id, index]
class NewVersionAvailableEvent(DelugeEvent):
"""
Emitted when a more recent version of Deluge is available.

View File

@ -197,6 +197,11 @@ this should be an IP address", metavar="IFACE",
write_pidfile()
# Setup the logger
try:
# Try to make the logfile's directory if it doesn't exist
os.makedirs(os.path.abspath(os.path.dirname(options.logfile)))
except:
pass
deluge.log.setupLogger(level=options.loglevel, filename=options.logfile)
from deluge.log import LOG as log

View File

@ -56,6 +56,7 @@ DEFAULT_PREFS = {
OPTIONS_AVAILABLE = { #option: builtin
"enabled":False,
"path":False,
"append_extension":False,
"abspath":False,
"download_location":True,
"max_download_speed":True,
@ -70,15 +71,14 @@ OPTIONS_AVAILABLE = { #option: builtin
"move_completed":True,
"move_completed_path":True,
"label":False,
"add_paused":True
"add_paused":True,
"queue_to_top":False
}
MAX_NUM_ATTEMPTS = 10
class AutoaddOptionsChangedEvent(DelugeEvent):
"""
Emitted when a new command is added.
"""
"""Emitted when the options for the plugin are changed."""
def __init__(self):
pass
@ -93,21 +93,20 @@ class Core(CorePluginBase):
self.config = deluge.configmanager.ConfigManager("autoadd.conf", DEFAULT_PREFS)
self.watchdirs = self.config["watchdirs"]
self.core_cfg = deluge.configmanager.ConfigManager("core.conf")
# A list of filenames
self.invalid_torrents = []
# Filename:Attempts
self.attempts = {}
# Dict of Filename:Attempts
self.invalid_torrents = {}
# Loopingcall timers for each enabled watchdir
self.update_timers = {}
# If core autoadd folder is enabled, move it to the plugin
if self.core_cfg.config.get('autoadd_enable'):
# Disable core autoadd
self.core_cfg['autoadd_enable'] = False
self.core_cfg.save()
# Check if core autoadd folder is already added in plugin
for watchdir in self.watchdirs:
if os.path.abspath(self.core_cfg['autoadd_location']) == watchdir['abspath']:
watchdir['enabled'] = True
break
else:
# didn't find core watchdir, add it
@ -125,18 +124,15 @@ class Core(CorePluginBase):
for loopingcall in self.update_timers.itervalues():
loopingcall.stop()
self.config.save()
pass
def update(self):
pass
@export()
def set_options(self, watchdir_id, options):
"""
update the options for a watch folder
"""
"""Update the options for a watch folder."""
watchdir_id = str(watchdir_id)
options = self._clean_unicode(options)
options = self._make_unicode(options)
CheckInput(watchdir_id in self.watchdirs , _("Watch folder does not exist."))
if options.has_key('path'):
options['abspath'] = os.path.abspath(options['path'])
@ -146,7 +142,7 @@ class Core(CorePluginBase):
raise Exception("Path is already being watched.")
for key in options.keys():
if not key in OPTIONS_AVAILABLE:
if not key in [key+'_toggle' for key in OPTIONS_AVAILABLE.iterkeys()]:
if not key in [key2+'_toggle' for key2 in OPTIONS_AVAILABLE.iterkeys()]:
raise Exception("autoadd: Invalid options key:%s" % key)
#disable the watch loop if it was active
if watchdir_id in self.update_timers:
@ -177,33 +173,29 @@ class Core(CorePluginBase):
return filedump
def update_watchdir(self, watchdir_id):
"""Check the watch folder for new torrents to add."""
watchdir_id = str(watchdir_id)
watchdir = self.watchdirs[watchdir_id]
if not watchdir['enabled']:
# We shouldn't be updating because this watchdir is not enabled
#disable the looping call
self.update_timers[watchdir_id].stop()
del self.update_timers[watchdir_id]
self.disable_watchdir(watchdir_id)
return
# Check the auto add folder for new torrents to add
if not os.path.isdir(watchdir["abspath"]):
log.warning("Invalid AutoAdd folder: %s", watchdir["abspath"])
#disable the looping call
watchdir['enabled'] = False
self.update_timers[watchdir_id].stop()
del self.update_timers[watchdir_id]
self.disable_watchdir(watchdir_id)
return
#Generate options dict for watchdir
opts={}
if watchdir.get('stop_at_ratio_toggle'):
watchdir['stop_ratio_toggle'] = True
# Generate options dict for watchdir
opts = {}
if 'stop_at_ratio_toggle' in watchdir:
watchdir['stop_ratio_toggle'] = watchdir['stop_at_ratio_toggle']
# We default to True wher reading _toggle values, so a config
# without them is valid, and applies all its settings.
for option, value in watchdir.iteritems():
if OPTIONS_AVAILABLE.get(option):
if watchdir.get(option+'_toggle', True):
opts[option] = value
opts = self._clean_unicode(opts)
for filename in os.listdir(watchdir["abspath"]):
if filename.split(".")[-1] == "torrent":
try:
@ -219,49 +211,73 @@ class Core(CorePluginBase):
# torrents may not be fully saved during the pass.
log.debug("Torrent is invalid: %s", e)
if filename in self.invalid_torrents:
self.attempts[filename] += 1
if self.attempts[filename] >= MAX_NUM_ATTEMPTS:
self.invalid_torrents[filename] += 1
if self.invalid_torrents[filename] >= MAX_NUM_ATTEMPTS:
os.rename(filepath, filepath + ".invalid")
del self.attempts[filename]
self.invalid_torrents.remove(filename)
del self.invalid_torrents[filename]
else:
self.invalid_torrents.append(filename)
self.attempts[filename] = 1
self.invalid_torrents[filename] = 1
continue
# The torrent looks good, so lets add it to the session
# The torrent looks good, so lets add it to the session.
torrent_id = component.get("TorrentManager").add(filedump=filedump, filename=filename, options=opts)
if ('Label' in component.get("CorePluginManager").get_enabled_plugins()) and torrent_id:
if watchdir.get('label_toggle', True) and watchdir.get('label'):
label = component.get("CorePlugin.Label")
if not watchdir['label'] in label.get_labels():
label.add(watchdir['label'])
label.set_torrent(torrent_id, watchdir['label'])
os.remove(filepath)
# If the torrent added successfully, set the extra options.
if torrent_id:
if 'Label' in component.get("CorePluginManager").get_enabled_plugins():
if watchdir.get('label_toggle', True) and watchdir.get('label'):
label = component.get("CorePlugin.Label")
if not watchdir['label'] in label.get_labels():
label.add(watchdir['label'])
label.set_torrent(torrent_id, watchdir['label'])
if watchdir.get('queue_to_top_toggle', True) and 'queue_to_top' in watchdir:
if watchdir['queue_to_top']:
component.get("TorrentManager").queue_top(torrent_id)
else:
component.get("TorrentManager").queue_bottom(torrent_id)
# Rename or delete the torrent once added to deluge.
if watchdir.get('append_extension_toggle'):
if not watchdir.get('append_extension'):
watchdir['append_extension'] = ".added"
os.rename(filepath, filepath + watchdir['append_extension'])
else:
os.remove(filepath)
def on_update_watchdir_error(self, failure, watchdir_id):
"""Disables any watch folders with unhandled exceptions."""
self.disable_watchdir(watchdir_id)
log.error("Disabling '%s', error during update: %s" % (self.watchdirs[watchdir_id]["path"], failure))
@export
def enable_watchdir(self, watchdir_id):
watchdir_id = str(watchdir_id)
self.watchdirs[watchdir_id]['enabled'] = True
#Enable the looping call
self.update_timers[watchdir_id] = LoopingCall(self.update_watchdir, watchdir_id)
self.update_timers[watchdir_id].start(5)
self.config.save()
component.get("EventManager").emit(AutoaddOptionsChangedEvent())
# Enable the looping call
if watchdir_id not in self.update_timers or not self.update_timers[watchdir_id].running:
self.update_timers[watchdir_id] = LoopingCall(self.update_watchdir, watchdir_id)
self.update_timers[watchdir_id].start(5).addErrback(self.on_update_watchdir_error, watchdir_id)
# Update the config
if not self.watchdirs[watchdir_id]['enabled']:
self.watchdirs[watchdir_id]['enabled'] = True
self.config.save()
component.get("EventManager").emit(AutoaddOptionsChangedEvent())
@export
def disable_watchdir(self, watchdir_id):
watchdir_id = str(watchdir_id)
self.watchdirs[watchdir_id]['enabled'] = False
#disable the looping call here
self.update_timers[watchdir_id].stop()
del self.update_timers[watchdir_id]
self.config.save()
component.get("EventManager").emit(AutoaddOptionsChangedEvent())
# Disable the looping call
if watchdir_id in self.update_timers:
if self.update_timers[watchdir_id].running:
self.update_timers[watchdir_id].stop()
del self.update_timers[watchdir_id]
# Update the config
if self.watchdirs[watchdir_id]['enabled']:
self.watchdirs[watchdir_id]['enabled'] = False
self.config.save()
component.get("EventManager").emit(AutoaddOptionsChangedEvent())
@export
def set_config(self, config):
"""sets the config dictionary"""
"""Sets the config dictionary."""
config = self._make_unicode(config)
for key in config.keys():
self.config[key] = config[key]
self.config.save()
@ -269,35 +285,30 @@ class Core(CorePluginBase):
@export
def get_config(self):
"""returns the config dictionary"""
"""Returns the config dictionary."""
return self.config.config
@export()
def get_watchdirs(self):
return self.watchdirs.keys()
def _clean_unicode(self, options):
def _make_unicode(self, options):
opts = {}
for key, value in options.iteritems():
if isinstance(key, unicode):
key = str(key)
if isinstance(value, unicode):
value = str(value)
opts[key] = value
for key in options:
if isinstance(options[key], str):
options[key] = unicode(options[key], "utf8")
opts[key] = options[key]
return opts
#Labels:
@export()
def add(self, options={}):
"""add a watchdir
"""
options = self._clean_unicode(options)
"""Add a watch folder."""
options = self._make_unicode(options)
abswatchdir = os.path.abspath(options['path'])
CheckInput(os.path.isdir(abswatchdir) , _("Path does not exist."))
CheckInput(os.access(abswatchdir, os.R_OK|os.W_OK), "You must have read and write access to watch folder.")
for watchdir_id, watchdir in self.watchdirs.iteritems():
if watchdir['abspath'] == abswatchdir:
raise Exception("Path is already being watched.")
if abswatchdir in [wd['abspath'] for wd in self.watchdirs.itervalues()]:
raise Exception("Path is already being watched.")
options.setdefault('enabled', False)
options['abspath'] = abswatchdir
watchdir_id = self.config['next_id']
@ -311,7 +322,7 @@ class Core(CorePluginBase):
@export
def remove(self, watchdir_id):
"""remove a label"""
"""Remove a watch folder."""
watchdir_id = str(watchdir_id)
CheckInput(watchdir_id in self.watchdirs, "Unknown Watchdir: %s" % self.watchdirs)
if self.watchdirs[watchdir_id]['enabled']:

View File

@ -83,9 +83,11 @@
<child internal-child="vbox">
<widget class="GtkVBox" id="dialog-vbox1">
<property name="visible">True</property>
<property name="orientation">vertical</property>
<child>
<widget class="GtkVBox" id="vbox1">
<property name="visible">True</property>
<property name="orientation">vertical</property>
<child>
<widget class="GtkNotebook" id="notebook1">
<property name="visible">True</property>
@ -94,6 +96,7 @@
<widget class="GtkVBox" id="vbox2">
<property name="visible">True</property>
<property name="border_width">6</property>
<property name="orientation">vertical</property>
<child>
<widget class="GtkFrame" id="frame2">
<property name="visible">True</property>
@ -106,6 +109,7 @@
<child>
<widget class="GtkVBox" id="vbox6">
<property name="visible">True</property>
<property name="orientation">vertical</property>
<child>
<widget class="GtkHBox" id="hbox3">
<property name="visible">True</property>
@ -170,6 +174,89 @@
<property name="position">0</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="GtkAlignment" id="alignment2">
<property name="visible">True</property>
<child>
<widget class="GtkVBox" id="vbox7">
<property name="visible">True</property>
<property name="orientation">vertical</property>
<child>
<widget class="GtkRadioButton" id="isnt_append_extension">
<property name="label" translatable="yes">Delete .torrent after adding</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">False</property>
<property name="active">True</property>
<property name="draw_indicator">True</property>
</widget>
<packing>
<property name="position">0</property>
</packing>
</child>
<child>
<widget class="GtkHBox" id="hbox1">
<property name="visible">True</property>
<child>
<widget class="GtkRadioButton" id="append_extension_toggle">
<property name="label" translatable="yes">Append extension after adding:</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">False</property>
<property name="draw_indicator">True</property>
<property name="group">isnt_append_extension</property>
<signal name="toggled" handler="on_toggle_toggled"/>
</widget>
<packing>
<property name="position">0</property>
</packing>
</child>
<child>
<widget class="GtkEntry" id="append_extension">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="invisible_char">&#x2022;</property>
<property name="text" translatable="yes">.added</property>
</widget>
<packing>
<property name="position">1</property>
</packing>
</child>
</widget>
<packing>
<property name="position">1</property>
</packing>
</child>
</widget>
</child>
</widget>
</child>
</widget>
</child>
<child>
<widget class="GtkLabel" id="label2">
<property name="visible">True</property>
<property name="label" translatable="yes">&lt;b&gt;Torrent File Action&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>
<child>
<widget class="GtkFrame" id="frame3">
<property name="visible">True</property>
@ -182,6 +269,7 @@
<child>
<widget class="GtkVBox" id="vbox3">
<property name="visible">True</property>
<property name="orientation">vertical</property>
<child>
<widget class="GtkCheckButton" id="download_location_toggle">
<property name="label" translatable="yes">Set download location</property>
@ -244,7 +332,7 @@
</widget>
<packing>
<property name="fill">False</property>
<property name="position">1</property>
<property name="position">2</property>
</packing>
</child>
<child>
@ -259,6 +347,7 @@
<child>
<widget class="GtkVBox" id="vbox4">
<property name="visible">True</property>
<property name="orientation">vertical</property>
<child>
<widget class="GtkCheckButton" id="move_completed_toggle">
<property name="label" translatable="yes">Set move completed location</property>
@ -336,7 +425,7 @@
</widget>
<packing>
<property name="fill">False</property>
<property name="position">2</property>
<property name="position">3</property>
</packing>
</child>
<child>
@ -392,7 +481,7 @@
</child>
</widget>
<packing>
<property name="position">3</property>
<property name="position">4</property>
</packing>
</child>
</widget>
@ -411,6 +500,7 @@
<widget class="GtkVBox" id="vbox5">
<property name="visible">True</property>
<property name="border_width">6</property>
<property name="orientation">vertical</property>
<child>
<placeholder/>
</child>
@ -484,7 +574,7 @@
<widget class="GtkSpinButton" id="max_download_speed">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="adjustment">-1 -1 10000 1 10 10</property>
<property name="adjustment">-1 -1 10000 1 10 0</property>
<property name="climb_rate">1</property>
<property name="digits">1</property>
</widget>
@ -498,7 +588,7 @@
<widget class="GtkSpinButton" id="max_upload_speed">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="adjustment">-1 -1 10000 1 10 10</property>
<property name="adjustment">-1 -1 10000 1 10 0</property>
<property name="climb_rate">1</property>
<property name="digits">1</property>
</widget>
@ -514,7 +604,7 @@
<widget class="GtkSpinButton" id="max_connections">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="adjustment">-1 -1 10000 1 10 10</property>
<property name="adjustment">-1 -1 10000 1 10 0</property>
<property name="climb_rate">1</property>
</widget>
<packing>
@ -529,7 +619,7 @@
<widget class="GtkSpinButton" id="max_upload_slots">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="adjustment">-1 -1 10000 1 10 10</property>
<property name="adjustment">-1 -1 10000 1 10 0</property>
<property name="climb_rate">1</property>
</widget>
<packing>
@ -640,14 +730,16 @@
</child>
</widget>
<packing>
<property name="top_attach">2</property>
<property name="bottom_attach">3</property>
<property name="top_attach">3</property>
<property name="bottom_attach">4</property>
<property name="x_options">GTK_FILL</property>
</packing>
</child>
<child>
<widget class="GtkAlignment" id="alignment14">
<property name="visible">True</property>
<property name="xalign">0</property>
<property name="yalign">0</property>
<property name="left_padding">12</property>
<child>
<widget class="GtkCheckButton" id="remove_at_ratio">
@ -661,8 +753,8 @@
</child>
</widget>
<packing>
<property name="top_attach">3</property>
<property name="bottom_attach">4</property>
<property name="top_attach">4</property>
<property name="bottom_attach">5</property>
</packing>
</child>
<child>
@ -676,8 +768,8 @@
<signal name="toggled" handler="on_toggle_toggled"/>
</widget>
<packing>
<property name="top_attach">1</property>
<property name="bottom_attach">2</property>
<property name="top_attach">2</property>
<property name="bottom_attach">3</property>
<property name="x_options">GTK_FILL</property>
<property name="y_options"></property>
</packing>
@ -694,8 +786,8 @@
<packing>
<property name="left_attach">2</property>
<property name="right_attach">3</property>
<property name="top_attach">3</property>
<property name="bottom_attach">4</property>
<property name="top_attach">4</property>
<property name="bottom_attach">5</property>
<property name="x_options">GTK_FILL</property>
<property name="y_options"></property>
</packing>
@ -712,8 +804,8 @@
<packing>
<property name="left_attach">1</property>
<property name="right_attach">2</property>
<property name="top_attach">3</property>
<property name="bottom_attach">4</property>
<property name="top_attach">4</property>
<property name="bottom_attach">5</property>
<property name="x_options">GTK_FILL</property>
<property name="y_options"></property>
</packing>
@ -723,21 +815,22 @@
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="invisible_char">&#x25CF;</property>
<property name="adjustment">2 0 100 0.10000000149 10 10</property>
<property name="adjustment">2 0 100 0.10000000149 10 0</property>
<property name="climb_rate">1</property>
<property name="digits">1</property>
</widget>
<packing>
<property name="left_attach">1</property>
<property name="right_attach">2</property>
<property name="top_attach">2</property>
<property name="bottom_attach">3</property>
<property name="top_attach">3</property>
<property name="bottom_attach">4</property>
<property name="y_options"></property>
</packing>
</child>
<child>
<widget class="GtkHBox" id="auto_managed_box">
<property name="visible">True</property>
<property name="homogeneous">True</property>
<child>
<widget class="GtkRadioButton" id="auto_managed">
<property name="label" translatable="yes">Yes</property>
@ -768,8 +861,8 @@
<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>
<property name="top_attach">2</property>
<property name="bottom_attach">3</property>
<property name="x_options">GTK_FILL</property>
<property name="y_options">GTK_FILL</property>
</packing>
@ -786,8 +879,8 @@
<packing>
<property name="left_attach">2</property>
<property name="right_attach">3</property>
<property name="top_attach">2</property>
<property name="bottom_attach">3</property>
<property name="top_attach">3</property>
<property name="bottom_attach">4</property>
<property name="x_options">GTK_FILL</property>
<property name="y_options"></property>
</packing>
@ -805,6 +898,7 @@
<child>
<widget class="GtkHBox" id="add_paused_box">
<property name="visible">True</property>
<property name="homogeneous">True</property>
<child>
<widget class="GtkRadioButton" id="add_paused">
<property name="label" translatable="yes">Yes</property>
@ -824,7 +918,6 @@
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">False</property>
<property name="active">True</property>
<property name="draw_indicator">True</property>
<property name="group">add_paused</property>
</widget>
@ -839,10 +932,56 @@
</packing>
</child>
<child>
<placeholder/>
<widget class="GtkCheckButton" id="queue_to_top_toggle">
<property name="label" translatable="yes">Queue to:</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_toggle_toggled"/>
</widget>
<packing>
<property name="top_attach">1</property>
<property name="bottom_attach">2</property>
</packing>
</child>
<child>
<placeholder/>
<widget class="GtkHBox" id="hbox2">
<property name="visible">True</property>
<property name="homogeneous">True</property>
<child>
<widget class="GtkRadioButton" id="queue_to_top">
<property name="label" translatable="yes">Top</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">False</property>
<property name="active">True</property>
<property name="draw_indicator">True</property>
</widget>
<packing>
<property name="position">0</property>
</packing>
</child>
<child>
<widget class="GtkRadioButton" id="isnt_queue_to_top">
<property name="label" translatable="yes">Bottom</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">False</property>
<property name="draw_indicator">True</property>
<property name="group">queue_to_top</property>
</widget>
<packing>
<property name="position">1</property>
</packing>
</child>
</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>
<placeholder/>
@ -911,6 +1050,7 @@
<child>
<widget class="GtkHButtonBox" id="hbuttonbox2">
<property name="visible">True</property>
<property name="orientation">vertical</property>
</widget>
<packing>
<property name="position">1</property>
@ -943,7 +1083,7 @@
</child>
<child>
<widget class="GtkButton" id="opts_add_button">
<property name="label" translatable="no">gtk-add</property>
<property name="label">gtk-add</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="can_default">True</property>
@ -959,7 +1099,7 @@
</child>
<child>
<widget class="GtkButton" id="opts_apply_button">
<property name="label" translatable="no">gtk-apply</property>
<property name="label">gtk-apply</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="can_default">True</property>

View File

@ -51,11 +51,10 @@ from common import get_resource
class OptionsDialog():
spin_ids = ["max_download_speed", "max_upload_speed", "stop_ratio"]
spin_int_ids = ["max_upload_slots", "max_connections"]
chk_ids = ["stop_at_ratio", "remove_at_ratio", "move_completed", "add_paused", "auto_managed"]
chk_ids = ["stop_at_ratio", "remove_at_ratio", "move_completed", "add_paused", "auto_managed", "queue_to_top"]
def __init__(self):
pass
def show(self, options={}, watchdir_id=None):
self.glade = gtk.glade.XML(get_resource("autoadd_options.glade"))
self.glade.signal_autoconnect({
@ -87,6 +86,8 @@ class OptionsDialog():
def load_options(self, options):
self.glade.get_widget('enabled').set_active(options.get('enabled', False))
self.glade.get_widget('append_extension_toggle').set_active(options.get('append_extension_toggle', False))
self.glade.get_widget('append_extension').set_text(options.get('append_extension', '.added'))
self.glade.get_widget('download_location_toggle').set_active(options.get('download_location_toggle', False))
self.glade.get_widget('label').set_text(options.get('label', ''))
self.glade.get_widget('label_toggle').set_active(options.get('label_toggle', False))
@ -97,7 +98,9 @@ class OptionsDialog():
self.glade.get_widget(id).set_active(bool(options.get(id, True)))
self.glade.get_widget(id+'_toggle').set_active(options.get(id+'_toggle', False))
if not options.get('add_paused', True):
self.glade.get_widget('isnt_add_paused').set_active(True)
self.glade.get_widget('isnt_add_paused').set_active(True)
if not options.get('queue_to_top', True):
self.glade.get_widget('isnt_queue_to_top').set_active(True)
if not options.get('auto_managed', True):
self.glade.get_widget('isnt_auto_managed').set_active(True)
for field in ['move_completed_path', 'path', 'download_location']:
@ -121,9 +124,9 @@ class OptionsDialog():
client.core.get_enabled_plugins().addCallback(on_get_enabled_plugins)
def set_sensitive(self):
maintoggles = ['download_location', 'move_completed', 'label', \
maintoggles = ['download_location', 'append_extension', 'move_completed', 'label', \
'max_download_speed', 'max_upload_speed', 'max_connections', \
'max_upload_slots', 'add_paused', 'auto_managed', 'stop_at_ratio']
'max_upload_slots', 'add_paused', 'auto_managed', 'stop_at_ratio', 'queue_to_top']
[self.on_toggle_toggled(self.glade.get_widget(x+'_toggle')) for x in maintoggles]
def on_toggle_toggled(self, tb):
@ -132,6 +135,8 @@ class OptionsDialog():
if toggle == 'download_location':
self.glade.get_widget('download_location_chooser').set_sensitive(isactive)
self.glade.get_widget('download_location_entry').set_sensitive(isactive)
elif toggle == 'append_extension':
self.glade.get_widget('append_extension').set_sensitive(isactive)
elif toggle == 'move_completed':
self.glade.get_widget('move_completed_path_chooser').set_sensitive(isactive)
self.glade.get_widget('move_completed_path_entry').set_sensitive(isactive)
@ -149,6 +154,9 @@ class OptionsDialog():
elif toggle == 'add_paused':
self.glade.get_widget('add_paused').set_sensitive(isactive)
self.glade.get_widget('isnt_add_paused').set_sensitive(isactive)
elif toggle == 'queue_to_top':
self.glade.get_widget('queue_to_top').set_sensitive(isactive)
self.glade.get_widget('isnt_queue_to_top').set_sensitive(isactive)
elif toggle == 'auto_managed':
self.glade.get_widget('auto_managed').set_sensitive(isactive)
self.glade.get_widget('isnt_auto_managed').set_sensitive(isactive)
@ -193,10 +201,11 @@ class OptionsDialog():
options['path'] = self.glade.get_widget('path_entry').get_text()
options['download_location'] = self.glade.get_widget('download_location_entry').get_text()
options['move_completed_path'] = self.glade.get_widget('move_completed_path_entry').get_text()
options['append_extension_toggle'] = self.glade.get_widget('append_extension_toggle').get_active()
options['append_extension'] = self.glade.get_widget('append_extension').get_text()
options['download_location_toggle'] = self.glade.get_widget('download_location_toggle').get_active()
options['label'] = self.glade.get_widget('label').get_text().lower()
options['label_toggle'] = self.glade.get_widget('label_toggle').get_active()
for id in self.spin_ids:
options[id] = self.glade.get_widget(id).get_value()
@ -245,6 +254,7 @@ class GtkUI(GtkPluginBase):
sw.add(self.treeView)
sw.show_all()
component.get("Preferences").add_page("AutoAdd", self.glade.get_widget("prefs_box"))
self.on_show_prefs()
def disable(self):
@ -320,7 +330,6 @@ class GtkUI(GtkPluginBase):
client.autoadd.set_options(watchdir_id, watchdir)
def on_show_prefs(self):
client.autoadd.get_config().addCallback(self.cb_get_config)
def on_options_changed_event(self):

View File

@ -42,7 +42,7 @@ from setuptools import setup
__plugin_name__ = "AutoAdd"
__author__ = "Chase Sterling"
__author_email__ = "chase.sterling@gmail.com"
__version__ = "0.28"
__version__ = "1.02"
__url__ = "http://forum.deluge-torrent.org/viewtopic.php?f=9&t=26775"
__license__ = "GPLv3"
__description__ = "Monitors folders for .torrent files."

View File

@ -2,7 +2,7 @@
# core.py
#
# Copyright (C) 2008 Andrew Resch <andrewresch@gmail.com>
# Copyright (C) 2009 John Garland <johnnybg@gmail.com>
# Copyright (C) 2009-2010 John Garland <johnnybg+deluge@gmail.com>
#
# Deluge is free software.
#
@ -38,6 +38,7 @@ import os
import time
from datetime import datetime, timedelta
from wsgiref.handlers import format_date_time
from urlparse import urljoin
import shutil
from twisted.internet.task import LoopingCall
@ -81,6 +82,7 @@ class Core(CorePluginBase):
self.is_importing = False
self.has_imported = False
self.up_to_date = False
self.need_to_resume_session = False
self.num_blocked = 0
self.file_progress = 0.0
@ -94,7 +96,7 @@ class Core(CorePluginBase):
update_now = False
if self.config["load_on_start"]:
self.pause_transfers()
self.pause_session()
if self.config["last_update"]:
last_update = datetime.fromtimestamp(self.config["last_update"])
check_period = timedelta(days=self.config["check_after_days"])
@ -103,7 +105,8 @@ class Core(CorePluginBase):
else:
d = self.import_list(deluge.configmanager.get_config_dir("blocklist.cache"))
d.addCallbacks(self.on_import_complete, self.on_import_error)
d.addBoth(self.resume_transfers)
if self.need_to_resume_session:
d.addBoth(self.resume_session)
# This function is called every 'check_after_days' days, to download
# and import a new list if needed.
@ -149,7 +152,8 @@ class Core(CorePluginBase):
else:
d = self.import_list(self.config["url"])
d.addCallbacks(self.on_import_complete, self.on_import_error)
d.addBoth(self.resume_transfers)
if self.need_to_resume_session:
d.addBoth(self.resume_session)
return d
@ -281,7 +285,7 @@ class Core(CorePluginBase):
d = f
if f.check(error.PageRedirect):
# Handle redirect errors
location = error_msg.split(" to ")[1]
location = urljoin(self.config["url"], error_msg.split(" to ")[1])
if "Moved Permanently" in error_msg:
log.debug("Setting blocklist url to %s", location)
self.config["url"] = location
@ -417,13 +421,14 @@ class Core(CorePluginBase):
else:
self.reader = create_reader(self.config["list_type"], self.config["list_compression"])
def pause_transfers(self):
self.session_was_paused = self.core.session.is_paused()
if not self.session_was_paused:
def pause_session(self):
if not self.core.session.is_paused():
self.core.session.pause()
self.need_to_resume_session = True
else:
self.need_to_resume_session = False
def resume_transfers(self, result):
if not self.session_was_paused:
self.session_was_paused = True
self.core.session.resume()
def resume_session(self, result):
self.core.session.resume()
self.need_to_resume_session = False
return result

View File

@ -1,7 +1,7 @@
#
# decompressers.py
#
# Copyright (C) 2009 John Garland <johnnybg@gmail.com>
# Copyright (C) 2009-2010 John Garland <johnnybg+deluge@gmail.com>
#
# Deluge is free software.
#

View File

@ -1,7 +1,7 @@
#
# detect.py
#
# Copyright (C) 2009 John Garland <johnnybg@gmail.com>
# Copyright (C) 2009-2010 John Garland <johnnybg+deluge@gmail.com>
#
# Deluge is free software.
#

View File

@ -1,7 +1,7 @@
#
# readers.py
#
# Copyright (C) 2009 John Garland <johnnybg@gmail.com>
# Copyright (C) 2009-2010 John Garland <johnnybg+deluge@gmail.com>
#
# Deluge is free software.
#

View File

@ -35,7 +35,7 @@ from setuptools import setup
__plugin_name__ = "Blocklist"
__author__ = "John Garland"
__author_email__ = "johnnybg@gmail.com"
__author_email__ = "johnnybg+deluge@gmail.com"
__version__ = "1.2"
__url__ = "http://deluge-torrent.org"
__license__ = "GPLv3"

View File

@ -1,54 +0,0 @@
#
# __init__.py
#
# Copyright (C) 2009 Andrew Resch <andrewresch@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

@ -1,40 +0,0 @@
#
# common.py
#
# Copyright (C) 2009 Andrew Resch <andrewresch@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 pkg_resources
import os.path
def get_resource(filename):
return pkg_resources.resource_filename("example", os.path.join("data", filename))

View File

@ -1,55 +0,0 @@
#
# core.py
#
# Copyright (C) 2009 Andrew Resch <andrewresch@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.plugins.pluginbase import CorePluginBase
import deluge.component as component
import deluge.configmanager
from deluge.core.rpcserver import export
class Core(CorePluginBase):
def enable(self):
log.debug("Example core plugin enabled!")
def disable(self):
log.debug("Example core plugin disabled!")
def update(self):
pass
### Exported RPC methods ###
@export()
def example_method(self):
pass

View File

@ -1,51 +0,0 @@
/*
Script: example.js
The client-side javascript code for the Example plugin.
Copyright:
(C) Damien Churchill 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.
*/
ExamplePlugin = Ext.extend(Deluge.Plugin, {
constructor: function(config) {
config = Ext.apply({
name: "Example"
}, config);
ExamplePlugin.superclass.constructor.call(this, config);
},
onDisable: function() {
Deluge.Preferences.removePage(this.prefsPage);
},
onEnable: function() {
this.prefsPage = new ExamplePreferencesPanel();
this.prefsPage = Deluge.Preferences.addPage(this.prefsPage);
}
});
new ExamplePlugin();

View File

@ -1,48 +0,0 @@
#
# gtkui.py
#
# Copyright (C) 2008 Andrew Resch <andrewresch@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 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
class GtkUI(GtkPluginBase):
def enable(self):
pass
def disable(self):
pass

View File

@ -1,54 +0,0 @@
#
# webui.py
#
# Copyright (C) 2009 Martijn Voncken <mvoncken@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
from common import get_resource
class WebUI(WebPluginBase):
scripts = [get_resource("example.js")]
# The enable and disable methods are not scrictly required on the WebUI
# plugins. They are only here if you need to register images/stylesheets
# with the webserver.
def enable(self):
log.debug("Example Web plugin enabled!")
def disable(self):
log.debug("Example Web plugin disabled!")

View File

@ -1,67 +0,0 @@
#
# setup.py
#
# Copyright (C) 2008 Andrew Resch <andrewresch@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.
#
#
from setuptools import setup
__plugin_name__ = "Example"
__author__ = "Andrew Resch"
__author_email__ = "andrewresch@gmail.com"
__version__ = "1.2"
__url__ = "http://deluge-torrent.org"
__license__ = "GPLv3"
__description__ = "Example plugin"
__long_description__ = __description__
__pkg_data__ = {__plugin_name__.lower(): []}
setup(
name=__plugin_name__,
version=__version__,
description=__description__,
author=__author__,
author_email=__author_email__,
url=__url__,
license=__license__,
long_description=__long_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,11 @@ class Core(CorePluginBase):
if event in self.registered_events:
continue
def event_handler(torrent_id):
self.execute_commands(torrent_id, command[EXECUTE_EVENT])
def create_event_handler(event):
def event_handler(torrent_id):
self.execute_commands(torrent_id, event)
return event_handler
event_handler = create_event_handler(event)
event_manager.register_event_handler(EVENT_MAP[event], event_handler)
self.registered_events[event] = event_handler
@ -93,20 +96,23 @@ class Core(CorePluginBase):
def execute_commands(self, torrent_id, event):
torrent = component.get("TorrentManager").torrents[torrent_id]
info = torrent.get_status(["name", "save_path", "move_completed", "move_on_completed_path"])
info = torrent.get_status(["name", "save_path", "move_on_completed", "move_on_completed_path"])
# Grab the torrent name and save path
torrent_name = info["name"]
if event == "completed":
save_path = info["move_on_completed_path"] if info ["move_completed"] else info["save_path"]
if event == "complete":
save_path = info["move_on_completed_path"] if info ["move_on_completed"] else info["save_path"]
else:
save_path = info["save_path"]
log.debug("[execute] Running commands for %s", event)
# Go through and execute all the commands
for command in self.config["commands"]:
if command[EXECUTE_EVENT] == event:
command = os.path.expandvars(command[EXECUTE_COMMAND])
command = os.path.expanduser(command)
log.debug("[execute] running %s", command)
p = Popen([command, torrent_id, torrent_name, save_path], stdin=PIPE, stdout=PIPE, stderr=PIPE)
if p.wait() != 0:
log.warn("Execute command failed with exit code %d", p.returncode)
@ -123,6 +129,7 @@ class Core(CorePluginBase):
def add_command(self, event, command):
command_id = hashlib.sha1(str(time.time())).hexdigest()
self.config["commands"].append((command_id, event, command))
self.config.save()
component.get("EventManager").emit(ExecuteCommandAddedEvent(command_id, event, command))
@export
@ -136,6 +143,7 @@ class Core(CorePluginBase):
self.config["commands"].remove(command)
component.get("EventManager").emit(ExecuteCommandRemovedEvent(command_id))
break
self.config.save()
@export
def save_command(self, command_id, event, cmd):
@ -143,3 +151,4 @@ class Core(CorePluginBase):
if command[EXECUTE_ID] == command_id:
self.config["commands"][i] = (command_id, event, cmd)
break
self.config.save()

View File

@ -99,7 +99,7 @@ Deluge.ux.EditExecuteCommandWindow = Ext.extend(Deluge.ux.ExecuteWindowBase, {
},
onSaveClick: function() {
var values = this.form.getForm().getValues();
var values = this.form.getForm().getFieldValues();
deluge.client.execute.save_command(this.command.id, values.event, values.command, {
success: function() {
this.fireEvent('commandedit', this, values.event, values.command);
@ -124,7 +124,7 @@ Deluge.ux.AddExecuteCommandWindow = Ext.extend(Deluge.ux.ExecuteWindowBase, {
},
onAddClick: function() {
var values = this.form.getForm().getValues();
var values = this.form.getForm().getFieldValues();
deluge.client.execute.add_command(values.event, values.command, {
success: function() {
this.fireEvent('commandadd', this, values.event, values.command);

View File

@ -1,55 +0,0 @@
#
# feeder/__init__.py
#
# Copyright (C) 2007-2009 Andrew Resch <andrewresch@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

@ -1,432 +0,0 @@
#
# core.py
#
# Copyright (C) 2008-2009 Fredrik Eriksson <feeder@winterbird.org>
# Copyright (C) 2009 David Mohr <david@mcbf.net>
#
# Basic plugin template created by:
# Copyright (C) 2008 Martijn Voncken <mvoncken@gmail.com>
# Copyright (C) 2007-2009 Andrew Resch <andrewresch@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 feedparser # for parsing rss feeds
import threading # for threaded updates
import re # for regular expressions
from twisted.internet.task import LoopingCall
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
DEFAULT_PREFS = {
"feeds": {},
"filters": {},
"updatetime": 15,
"history": []
}
# Helper classes
class Feed:
"""
Class for the Feed object (containging feed configurations)
"""
def __init__(self):
self.url = ""
self.cookies = {}
self.updatetime = 15
def get_config(self):
try:
tmp = self.cookies
except Exception, e:
log.debug("Old feed without cookies... updating")
self.cookies = {}
return {'url': self.url, 'updatetime': self.updatetime, 'cookies': self.cookies}
def set_config(self, config):
self.url = config['url']
self.updatetime = config['updatetime']
self.cookies = config['cookies']
class Filter:
"""
Class for the Filter object (containing filter configurations)
"""
def __init__(self):
self.regex = ""
self.feeds = [] #TODO activate filter per feed
self.all_feeds = True
self.active = True
# by default, set the configuration to match
# the per-torrent settings in deluge
def_conf = component.get("Core").get_config()
self.max_download_speed = def_conf['max_download_speed_per_torrent']
self.max_upload_speed = def_conf['max_upload_speed_per_torrent']
self.max_connections = def_conf['max_connections_per_torrent']
self.max_upload_slots = def_conf['max_upload_slots_per_torrent']
self.prioritize_first_last_pieces = def_conf['prioritize_first_last_pieces']
self.auto_managed = def_conf['auto_managed']
self.download_location = def_conf['download_location']
self.stop_at_ratio = def_conf['stop_seed_at_ratio']
self.stop_ratio = def_conf['stop_seed_ratio']
self.remove_at_ratio = def_conf['remove_seed_at_ratio']
def get_config(self):
def_conf = component.get("Core").get_config()
try:
tmp = self.active
except Exception, e:
log.debug("Old filter detected (pre 0.3), updating...")
self.active = True
try:
tmp = self.stop_at_ratio
tmp = self.stop_ratio
tmp = self.remove_at_ratio
except:
log.debug("Old filter detected (pre 0.4), updating...")
self.stop_at_ratio = def_conf['stop_seed_at_ratio']
self.stop_ratio = def_conf['stop_seed_ratio']
self.remove_at_ratio = def_conf['remove_seed_at_ratio']
conf = {
'regex': self.regex,
'feeds': self.feeds,
'all_feeds': self.all_feeds,
'active' : self.active,
'max_download_speed': self.max_download_speed,
'max_upload_speed': self.max_upload_speed,
'max_connections': self.max_connections,
'max_upload_slots': self.max_upload_slots,
'prioritize_first_last_pieces': self.prioritize_first_last_pieces,
'auto_managed': self.auto_managed,
'download_location':self.download_location,
'remove_at_ratio':self.remove_at_ratio,
'stop_ratio': self.stop_ratio,
'stop_at_ratio': self.stop_at_ratio }
return conf
def set_config(self, conf):
self.regex = conf['regex']
self.feeds = conf['feeds']
self.all_feeds = conf['all_feeds']
self.active = conf['active']
self.max_download_speed = int(conf['max_download_speed'])
self.max_upload_speed = int(conf['max_upload_speed'])
self.max_connections = int(conf['max_connections'])
self.max_upload_slots = int(conf['max_upload_slots'])
self.prioritize_first_last_pieces = conf['prioritize_first_last_pieces']
self.auto_managed = conf['auto_managed']
self.download_location = conf['download_location']
self.remove_at_ratio = conf['remove_at_ratio']
self.stop_ratio = float(conf['stop_ratio'])
self.stop_at_ratio = conf['stop_at_ratio']
class Core(CorePluginBase):
def enable(self):
self.config = deluge.configmanager.ConfigManager("feeder.conf", DEFAULT_PREFS)
self.feeds = {}
self.timers = {}
self.history = self.config['history']
self.time = 0
# Setting default timer to configured update time
for feed in self.config['feeds']:
self.timers[feed] = LoopingCall(self.update_feed, feed)
self.timers[feed].start( self.config['feeds'][feed].updatetime * 60)
def disable(self):
self.config['history'] = self.history
self.config.save()
def update(self):
pass
#=================Exported functions==================
@export
def set_config(self, config):
"""sets the config dictionary"""
for key in config.keys():
self.config[key] = config[key]
self.config.save()
####################Configuration Getters##################
@export
def get_config(self):
"""returns the config dictionary"""
return self.config.config
@export
def get_feed_config(self, feedname):
"""Returns configuration for a feed"""
return self.config['feeds'][feedname].get_config()
@export
def get_filter_config(self, filtername):
"""Returns a configuration for a filter"""
return self.config['filters'][filtername].get_config()
####################Information Getters####################
@export
def get_feeds(self):
"""Returns a list of the configured feeds"""
feeds = []
for feedname in self.config['feeds']:
feeds.append(feedname)
feeds.sort(key=string.lower)
return feeds
@export
def get_filters(self):
"""Returns a list of all available filters"""
filters = []
for filter in self.config['filters']:
filters.append(filter)
filters.sort(key=string.lower)
return filters
@export
def get_items(self, feedname):
"""Returns a dictionary with feedname:link"""
try:
items = {}
feed = self.feeds[feedname]
for entry in feed['entries']:
items[entry.title] = entry.link
except Exception, e:
items = {}
log.warning("Feed '%s' not loaded", feedname)
return items
@export
def test_filter(self, regex):
filters = { "to_test":Filter() }
conf = filters["to_test"].get_config()
conf["regex"] = regex
filters["to_test"].set_config(conf)
hits = {}
for feed in self.feeds:
hits.update(self.run_filters(feed, filters, test=True))
return hits
@export
def add_feed(self, config):
"""adds/updates a feed and, for whatever reason, sets the default timeout"""
# save the feedname and remove it from the config
feedname = config['name']
del config['name']
# check if the feed already exists and save config
try:
conf = self.config['feeds'][feedname].get_config()
del self.config['feeds'][feedname]
except Exception, e:
conf = {}
# update configuration
for var in config:
conf[var] = config[var]
# save as default update time
try:
self.config['updatetime'] = config['updatetime']
except Exception, e:
log.warning("updatetime not set when adding feed %s", feedname)
# Create the new feed
newfeed = Feed()
newfeed.set_config(conf)
# Add a timer (with default timer for now, since we can't get ttl just yet)...
self.timers[feedname] = LoopingCall(self.update_feed, feedname)
# Save the new feed
self.config['feeds'].update({feedname: newfeed })
self.config.save()
# Start the timeout, which will also update the new feed
self.timers[feedname].start(newfeed.updatetime * 60)
@export
def remove_feed(self, feedname):
"""Remove a feed"""
if self.feeds.has_key(feedname): # Check if we have the feed saved and remove it
del self.feeds[feedname]
if self.timers.has_key(feedname): # Check if we have a timer for this feed and remove it
self.timers[feedname].stop()
del self.timers[feedname]
if self.config['feeds'].has_key(feedname): # Check if we have the feed in the configuration and remove it
del self.config['feeds'][feedname]
self.config.save()
@export
def add_filter(self, name):
"""Adds a new filter to the configuration"""
if not self.config['filters'].has_key(name): # we don't want to add a filter that already exists
self.config['filters'][name] = Filter()
self.config.save()
@export
def set_filter_config(self, filtername, conf):
"""Changes the options for a filter"""
oldconf = self.config['filters'][filtername].get_config()
for item in conf:
oldconf[item] = conf[item]
self.config['filters'][filtername].set_config(oldconf)
self.config.save()
for feed in self.config['feeds']: # we would like to check if the filter now matches something new
self.run_filters(feed)
@export
def remove_filter(self, name):
"""Removes a filter"""
if self.config['filters'].has_key(name): # Can't remove a filter that doesn't exists
del self.config['filters'][name]
self.config.save()
#=================Internal functions================
def update_feed(self, feedname):
"""Start a thread to update a single feed"""
threading.Thread(
target=self.update_feed_thread,
args=(self.on_feed_updated, feedname)).start()
# Need to return true to not destoy timer...
return True
def update_feed_thread(self, callback, feedname):
"""updates a feed"""
feed = self.config['feeds'][feedname]
try:
self.feeds[feedname] = feedparser.parse(feed.url)
except Exception, e:
log.warning("Error parsing feed %s: %s", feedname, e)
else:
callback(feedname)
def on_feed_updated(self, feedname):
"""Run stuff when a feed has been updated"""
# Not all feeds contain a ttl value, but if it does
# we would like to obey it
try:
if not self.feeds[feedname].ttl == self.config['feeds'][feedname].updatetime:
log.debug("feed '%s' request a ttl of %s, updating timer", feedname, self.feeds[feedname].ttl)
self.config['feeds'][feedname].updatetime = self.feeds[feedname].ttl
self.timers[feedname].stop()
self.timers[feedname].start(self.config['feeds'][feedname].updatetime * 60)
except Exception, e:
log.debug("feed '%s' has no ttl set, will use default timer", feedname)
# Run filters on the feed
self.run_filters(feedname)
def run_filters(self, feedname, filters={}, test=False):
"""Test all available filters on the given feed"""
if not filters:
filters = self.config['filters']
log.debug("will test filters %s", filters)
hits = {}
# Test every entry...
for entry in self.feeds[feedname]['entries']:
# ...and every filter
for filter in filters:
# We need to be able to run feeds saved before implementation of actiave/deactivate filter (pre 0.3) TODO
try:
if not filters[filter].active:
continue
except:
log.debug("old filter, will assume filter is activated")
if filters[filter].regex == "": # we don't want a empty regex...
log.warning("Filter '%s' has not been configured, ignoring!", filter)
continue
# if the filter isn't supposed to be run on this feed we don't want to run it...
# if filter.all_feeds or self.config['filters'][filter].feeds.has_element(feedname) : # ...apparently has_element doesn't work on arrays... TODO
if self.test_filter(entry, filters[filter].regex):
if test:
hits[entry.title] = entry.link
else:
opts = filters[filter].get_config()
#remove filter options that should not be passed on to the torrent.
del opts['regex']
del opts['feeds']
del opts['all_feeds']
# history patch from Darrell Enns, slightly modified :)
# check history to prevent multiple adds of the same torrent
log.debug("testing %s", entry.link)
if not entry.link in self.history:
self.add_torrent(entry.link, opts, self.feeds[feedname].cookies)
self.history.append(entry.link)
#limit history to 50 entries
if len(self.history)>50:
self.history=self.history[-50:]
log.debug("wrapping history")
else:
log.debug("'%s' is in history, will not download", entry.link)
return hits
def test_filter(self, entry, filter):
"""Tests a filter to a given rss entry"""
f = re.compile(filter, re.IGNORECASE)
if f.search(entry.title) or f.search(entry.link):
log.debug("RSS item '%s' matches filter '%s'", entry.title, filter)
return True
else:
return False
def add_torrent(self, url, torrent_options, headers):
log.debug("Attempting to add torrent %s", url)
component.get("Core").add_torrent_url(url, torrent_options, headers)

View File

@ -1,13 +0,0 @@
$def with (entries, feedname)
$:render.header("things", '')
<div class="panel" >
<div>
<h3>Feed items for feed $feedname</h3>
<ul>
$entries
</ul>
</div>
<a href="/config/feeder">back to config</a>
</div>
$:render.footer()

View File

@ -1,50 +0,0 @@
$def with (filter_settings_form, filter)
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html>
<head>
<title>Deluge:things</title>
<link rel="icon" href="/static/images/deluge-icon.png" type="image/png" />
<link rel="shortcut icon" href="/static/images/deluge-icon.png" type="image/png" />
<link rel="stylesheet" type="text/css" href="/template_style.css" />
<script language="javascript" src="/static/deluge.js"></script>
<script language="javascript" src="/static/mootools-1.2-core.js"></script>
<script language="javascript" src="/static/mootools-1.2-more.js"></script>
<script language="javascript" src="/static/mooui.js"></script>
<script language="javascript" src="/static/deluge-moo.js"></script>
<script language="javascript" src="/gettext.js"></script>
<script language="javascript" src="/label/data/label.js"></script>
</head>
<body>
<form method="post" action='$base/feeder/filter_settings/$filter'>
<div class="info">
<h3>Filter</h3>
<table>
$:(filter_settings_form.as_table(["regex", "active"]))
</table>
<h3>Speed</h3>
<table>
$:(filter_settings_form.as_table(["max_download_speed", "max_upload_speed", "max_upload_slots", "max_connections"]))
</table>
<h3>Seed options</h3>
<table>
$:(filter_settings_form.as_table(["stop_ratio", "stop_at_ratio", "remove_at_ratio"]))
</table>
<h3>Other options</h3>
<table>
$:(filter_settings_form.as_table(["prioritize_first_last_pieces", "auto_managed", "download_location"]))
</table>
<input type="submit" name="submit" value="$_('Save')" />
</form>
<h3>Matches</h3>
$:filter_settings_form.post_html()
</div>
</body>
</html>

View File

@ -1,39 +0,0 @@
$def with (filters, new_filter_form)
$:render.header("things", '')
<table><tr>
<td>
<table><tr></td>
<div class="info">
<h3>Filters</h3>
</div></td></tr>
<tr><td>
<div class="info">
<ul>
$filters
</ul>
<form method="post" action='$base/feeder/filters'>
$:(new_filter_form.as_p(["name"]))
<input type="submit" name="submit" class="form_input" value="$_('Add filter')" />
</form>
<p><a href="/config/feeder">back to config</a>
</div></td></tr></table>
</td>
<td>
<div class="panel">
<iframe style="border-style:hidden;" id="filter_settings" width=100% height=600>
</iframe>
</div>
</td>
</tr></table>
<script language="javascript">
function load_options(filter){
\$('filter_settings').src = state.base_url + '/feeder/filter_settings/' + filter;
}
</script>
<script language="javascript">
new InputSensitivitySetter({prefix:"id_",groups:[
["name"]
]});
</script>
$:render.footer()

View File

@ -1,277 +0,0 @@
#
# webui.py
#
# Copyright (C) 2008 Fredrik Eriksson <feeder@winterbird.org>
#
# Basic plugin template created by:
# Copyright (C) 2008 Martijn Voncken <mvoncken@gmail.com>
# Copyright (C) 2007, 2008 Andrew Resch <andrewresch@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
import feedparser # for proccessing feed entries
import os
from deluge.log import LOG as log
from deluge.ui.client import sclient, aclient
from deluge.plugins.webuipluginbase import WebUIPluginBase
from deluge import component
api = component.get("WebPluginApi")
forms = api.forms
class feed_page:
"Class for showing feed items"
@api.deco.deluge_page
def GET(self, feedname):
entries = sclient.feeder_get_items(feedname)
items = ""
for item in entries:
items = """%(old)s
<a href="%(link)s">
<li>%(entry)s</li>
</a>""" % { "old":items, "entry":item, "link":entries[item]}
return api.render.feeder.feeds(items, feedname)
class filter_page:
"Class for showing filters / filter settings"
@api.deco.deluge_page
def GET(self, args):
new_filter = new_filter_form()
filters = sclient.feeder_get_filters()
# List filters
txt = ""
for filter in filters:
txt = """%(old)s
<li onclick=\"load_options('%(new)s')\">
%(new)s
</li>""" % {'old':txt, 'new':filter}
return api.render.feeder.filters(txt, new_filter)
def POST(self):
"Saves the new filter"
name = api.utils.get_newforms_data(new_filter_form)['name']
sclient.feeder_add_filter(name)
return self.GET(name)
class new_filter_form(forms.Form):
"basic form for a new label"
name = forms.CharField(label="")
class filter_settings_page:
"Class for showing filter settings"
@api.deco.deluge_page
def GET(self, filter):
form = filter_settings_form(filter)
return api.render.feeder.filter_settings(form, filter)
def POST(self, filter):
opts = api.utils.get_newforms_data(filter_settings_form)
# apparently the "Unlimited" options still have to be changed
# to -1 (wtf?)
# FIXME there is probably a very much better way to ensure that
# all values have the right types... not to mention to convert "Unlimited"
# to -1...
try:
opts['max_upload_speed'] = int(opts['max_upload_speed'])
except:
opts['max_upload_speed'] = int(-1)
try:
opts['max_download_speed'] = int(opts['max_download_speed'])
except:
opts['max_download_speed'] = int(-1)
try:
opts['max_connections'] = int(opts['max_connections'])
except:
opts['max_connections'] = int(-1)
try:
opts['max_upload_slots'] = int(opts['max_upload_slots'])
except:
opts['max_upload_slots'] = int(-1)
"""opts['max_upload_slots'] = long(opts['max_upload_slots'])
opts['max_connections'] = long(opts['max_connections'])"""
# TODO filter settings per feed not implemented.
opts['feeds'] = []
sclient.feeder_set_filter_config(filter, opts)
return self.GET(filter)
class filter_settings_form(forms.Form):
"form for filter settings"
def __init__(self, filter, test=False):
self.filtername = filter # We want to save our filtername
forms.Form.__init__(self)
def initial_data(self):
self.conf = sclient.feeder_get_filter_config(self.filtername)
return self.conf
def post_html(self):
regex = self.conf["regex"]
hits = sclient.feeder_test_filter(regex)
if not hits:
return "No hits"
list = ""
for hit in hits:
list = """%(old)s
<li><a href="%(link)s" >%(name)s</a></li>
""" % { "old":list, "link":hits[hit], "name":hit }
return """
<ul>
%s
</ul>
""" % list
regex = forms.CharField(_("regular_expression"))
all_feeds = forms.CheckBox(_("all_feeds"))
active = forms.CheckBox(_("active"))
#maximum:
max_download_speed = forms.DelugeFloat(_("max_download_speed"))
max_upload_speed = forms.DelugeFloat(_("max_upload_speed"))
max_upload_slots = forms.DelugeInt(_("max_upload_slots"))
max_connections = forms.DelugeInt(_("max_connections"))
stop_ratio = forms.DelugeFloat(_("stop_ratio"))
stop_at_ratio = forms.CheckBox(_("stop_at_ratio"))
remove_at_ratio = forms.CheckBox(_("remove_at_ratio"))
#queue:
auto_managed = forms.CheckBox(_("is_auto_managed"))
prioritize_first_last_pieces = forms.CheckBox(_("prioritize_first_last_pieces"))
download_location = forms.ServerFolder(_("download_location"))
class remove_feed_page:
"Class for deleting feeds, redirects to setting page"
@api.deco.deluge_page
def GET(self, feedname):
sclient.feeder_remove_feed(feedname)
return """<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">
<html>
<head>
<title>Redirecting back to settings</title>
<meta http-equiv="refresh" content="0; URL=/config/feeder">
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
</head>
<body>
</body>
</html>"""
class remove_filter_page:
"Class for deleting filters, redirects to setting page"
@api.deco.deluge_page
def GET(self, name):
sclient.feeder_remove_filter(name)
return """<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">
<html>
<head>
<title>Redirecting back to settings</title>
<meta http-equiv="refresh" content="0; URL=/config/feeder">
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
</head>
<body>
</body>
</html>"""
class WebUI(WebUIPluginBase):
#map url's to classes: [(url,class), ..]
urls = [('/feeder/filters', filter_page),
('/feeder/filter_settings/(.*)', filter_settings_page),
('/feeder/feed_remove/(.*)', remove_feed_page),
('/feeder/filter_remove/(.*)', remove_filter_page),
('/feeder/feed/(.*)', feed_page)]
def enable(self):
api.config_page_manager.register('plugins', 'feeder' ,ConfigForm)
def disable(self):
api.config_page_manager.deregister('feeder')
class ConfigForm(forms.Form):
#meta:
title = _("feeder")
#load/save:
def initial_data(self):
return sclient.feeder_get_config()
def save(self, data):
cfg = dict(data)
sclient.feeder_add_feed(cfg)
def pre_html(self):
feeds = sclient.feeder_get_feeds()
filters = sclient.feeder_get_filters()
filterlist = ""
for filter in filters:
filterlist = """ %(old)s <li>%(new)s
<a href="/feeder/filter_remove/%(new)s">
<img src="/static/images/16/list-remove.png" alt="Remove" />
</a></li>""" % {'old':filterlist, 'new':filter}
feedlist = ""
for feed in feeds:
feedlist = """%(old)s
<li> <a href="/feeder/feed/%(new)s"> %(new)s (%(entrys)s torrents)</a>
<a href="/feeder/feed_remove/%(new)s">
<img src="/static/images/16/list-remove.png" alt="Remove" />
</a></li>""" % {'old':feedlist, 'new':feed, 'entrys':len(sclient.feeder_get_items(feed))}
return """
<table width=100%%><tr><td>
<h3>Feeds</h3>
</td>
<td>
<h3>Filters</h3>
</td></tr>
<tr><td>
<div class="info">
<ul>
%(feeds)s
</ul></div>
</td><td>
<div class="info">
<ul>
%(filters)s
</ul></div>
<a href="/feeder/filters">Add/modify filters</a>
</td></tr>
</table>
<h3>Add/change feed settings</h3>""" % {'feeds':feedlist, 'filters':filterlist}
name = forms.CharField(label=_("Name of feed"))
url = forms.URLField(label=_("URL of feed"))
updatetime = forms.IntegerField(label=_("Defualt refresh time"))

View File

@ -1,70 +0,0 @@
#
# setup.py
#
# Copyright (C) 2008 Fredrik Eriksson <feeder@winterbird.org>
#
# Basic plugin template created by:
# Copyright (C) 2008 Martijn Voncken <mvoncken@gmail.com>
# Copyright (C) 2007, 2008 Andrew Resch <andrewresch@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
from setuptools import setup
__plugin_name__ = "feeder"
__author__ = "Fredrik Eriksson"
__author_email__ = "feeder@winterbird.org"
__version__ = "0.4"
__url__ = ""
__license__ = "GPLv3"
__description__ = "A plugin for automatically downloadning torrents from a RSS-feed"
__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__,
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

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

View File

@ -1,58 +0,0 @@
#
# __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

@ -1,42 +0,0 @@
#
# 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("freespace", os.path.join("data", filename))

View File

@ -1,201 +0,0 @@
#
# 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 os, statvfs
from datetime import datetime, timedelta
from twisted.internet import task
from deluge.log import LOG as log
from deluge.plugins.pluginbase import CorePluginBase
from deluge.event import DelugeEvent
import deluge.component as component
import deluge.configmanager
from deluge.core.rpcserver import export
class LowDiskSpaceEvent(DelugeEvent):
"""Triggered when the available space for a specific path is getting
too low.
"""
def __init__(self, percents_dict):
"""
:param percents: dictionary of path keys with their respecive
occupation percentages.
"""
self._args = [percents_dict]
DEFAULT_PREFS = {
"enabled": False,
"percent": 90
}
class Core(CorePluginBase):
CLEANUP_TIMEOUT_SECS = 3600 # One hour
def enable(self):
self.config = deluge.configmanager.ConfigManager("freespace.conf",
DEFAULT_PREFS)
self.notifications_sent = {}
self._timer = task.LoopingCall(self.update)
self._interval = 60 * 5 # every 5 minutes
if self.config['enabled']:
self._timer.start(self._interval, False)
self._cleanup = task.LoopingCall(self.__cleanup_notifications)
self._cleanup.start(self._interval, False)
try:
component.get("CorePlugin.Notifications"). \
register_custom_email_notification(
"LowDiskSpaceEvent", self.__custom_email_notification
)
except KeyError:
pass
component.get("EventManager").register_event_handler(
"PluginEnabledEvent", self.__on_plugin_enabled
)
component.get("EventManager").register_event_handler(
"PluginDisabledEvent", self.__on_plugin_disabled
)
def disable(self):
try:
component.get("CorePlugin.Notifications"). \
deregister_custom_email_notification("LowDiskSpaceEvent")
except KeyError:
pass
component.get("EventManager").deregister_event_handler(
"PluginEnabledEvent", self.__on_plugin_enabled
)
component.get("EventManager").deregister_event_handler(
"PluginDisabledEvent", self.__on_plugin_disabled
)
self._cleanup.stop()
if self._timer.running:
self._timer.stop()
def update(self):
log.debug('Updating %s FreeSpace', self.__class__.__name__)
nots = {}
for path in self.__gather_paths_to_check():
log.debug("Checking path %s", path)
if os.path.exists(path):
free_percent = self.__get_free_space(path)
if (100 - free_percent) > self.config['percent']:
if path not in self.notifications_sent:
self.notifications_sent[path] = datetime.utcnow()
nots[path] = (100 - free_percent)
else:
log.warning("Running low on disk space on %s but "
"notifications were already triggered.",
path)
if nots:
component.get("EventManager").emit(LowDiskSpaceEvent(nots))
@export
def set_config(self, config):
"sets the config dictionary"
if not self.config['enabled'] and config['enabled']:
self._timer.start(self._interval, False)
elif self.config['enabled'] and not config['enabled']:
self._timer.stop()
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
def __gather_paths_to_check(self):
self.over_percentage = {}
torrent_manager = component.get('TorrentManager')
paths = set()
for torrent_id in torrent_manager.get_torrent_list():
status = torrent_manager[torrent_id].get_status([
'is_finished',
'move_on_completed_path',
'save_path'
])
if not status['is_finished']:
paths.add(status['move_on_completed_path'])
paths.add(status['save_path'])
return paths
def __get_free_space(self, path):
log.debug("Calculating free space on %s", path)
stat = os.statvfs(path)
free_blocks = stat[statvfs.F_BAVAIL]
total_blocks = stat[statvfs.F_BLOCKS]
free_percent = free_blocks * 100 / total_blocks
return free_percent
def __custom_email_notification(self, ocupied_percents):
subject = _("Low Disk Space Warning")
message = _("You're running low on disk space:\n")
for path, ocupied_percent in ocupied_percents.iteritems():
message += _(' %s%% ocupation in %s\n') % (ocupied_percent, path)
# "\"%s\"%% space occupation on %s") % (ocupied_percent, path)
return subject, message
def __on_plugin_enabled(self, plugin_name):
if plugin_name == 'Notifications':
component.get("CorePlugin.Notifications"). \
register_custom_email_notification(
"LowDiskSpaceEvent", self.__custom_email_notification
)
def __on_plugin_disabled(self, plugin_name):
if plugin_name == 'Notifications':
component.get("CorePlugin.Notifications"). \
deregister_custom_email_notification("LowDiskSpaceEvent")
def __cleanup_notifications(self):
now = datetime.now()
for path, when in self.notifications_sent.copy().iteritems():
if when <= (now -timedelta(seconds=self.CLEANUP_TIMEOUT_SECS)):
log.debug("Removing old(%s) path from notified paths: %s",
when, path)
self.notifications_sent.pop(path)

View File

@ -1,100 +0,0 @@
<?xml version="1.0"?>
<glade-interface>
<!-- interface-requires gtk+ 2.6 -->
<!-- interface-naming-policy toplevel-contextual -->
<widget class="GtkWindow" id="window1">
<child>
<widget class="GtkHBox" id="prefs_box">
<property name="visible">True</property>
<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="GtkVBox" id="vbox2">
<property name="visible">True</property>
<property name="orientation">vertical</property>
<child>
<widget class="GtkHBox" id="hbox2">
<property name="visible">True</property>
<child>
<widget class="GtkCheckButton" id="enabled">
<property name="label" translatable="yes">Consider low when</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="expand">False</property>
<property name="position">0</property>
</packing>
</child>
<child>
<widget class="GtkSpinButton" id="percent">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="max_length">2</property>
<property name="invisible_char">&#x25CF;</property>
<property name="adjustment">90 0 99 1 10 0</property>
</widget>
<packing>
<property name="expand">False</property>
<property name="padding">5</property>
<property name="position">1</property>
</packing>
</child>
<child>
<widget class="GtkLabel" id="label14">
<property name="visible">True</property>
<property name="label">%</property>
</widget>
<packing>
<property name="expand">False</property>
<property name="position">2</property>
</packing>
</child>
<child>
<widget class="GtkLabel" id="label4">
<property name="visible">True</property>
<property name="label" translatable="yes">of the disk is occupied.</property>
</widget>
<packing>
<property name="expand">False</property>
<property name="padding">5</property>
<property name="position">3</property>
</packing>
</child>
</widget>
<packing>
<property name="position">0</property>
</packing>
</child>
</widget>
</child>
</widget>
</child>
<child>
<widget class="GtkLabel" id="label2">
<property name="visible">True</property>
<property name="label" translatable="yes">&lt;b&gt;Free Space Checking&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>
</widget>
</child>
</widget>
</glade-interface>

View File

@ -1,50 +0,0 @@
/*
Script: freespace.js
The client-side javascript code for the FreeSpace 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.
*/
FreeSpacePlugin = Ext.extend(Deluge.Plugin, {
constructor: function(config) {
config = Ext.apply({
name: "FreeSpace"
}, config);
FreeSpacePlugin.superclass.constructor.call(this, config);
},
onDisable: function() {
},
onEnable: function() {
}
});
new FreeSpacePlugin();

View File

@ -1,174 +0,0 @@
#
# 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 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
from common import get_resource
class GtkUI(GtkPluginBase):
def enable(self):
log.debug('Enabling %s FreeSpace', self.__class__.__name__)
self.glade = gtk.glade.XML(get_resource("config.glade"))
self.prefs = self.glade.get_widget('prefs_box')
parent = self.prefs.get_parent()
if parent:
parent.remove(self.prefs)
# chk_ap = component.get("Preferences").glade.get_widget('chk_add_paused')
# downloads_vbox = chk_ap.get_parent().get_parent().get_parent().get_parent()
downloads_vbox = component.get("Preferences").glade.get_widget('vbox1')
downloads_vbox.pack_start(self.prefs, False, True, 0)
# self.prefs.set_parent(frame)
# component.get("Preferences").add_page("FreeSpace", self.glade.get_widget("prefs_box"))
component.get("PluginManager").register_hook("on_apply_prefs",
self.on_apply_prefs)
component.get("PluginManager").register_hook("on_show_prefs",
self.on_show_prefs)
try:
notifications = component.get("GtkPlugin.Notifications")
notifications.register_custom_popup_notification(
"LowDiskSpaceEvent", self.__custom_popup_notification
)
notifications.register_custom_blink_notification(
"LowDiskSpaceEvent", self.__custom_blink_notification
)
notifications.register_custom_sound_notification(
"LowDiskSpaceEvent", self.__custom_sound_notification
)
except KeyError:
pass
client.register_event_handler("PluginEnabledEvent",
self.__on_plugin_enabled)
client.register_event_handler("PluginDisabledEvent",
self.__on_plugin_disabled)
def disable(self):
component.get("Preferences").remove_page("FreeSpace")
component.get("PluginManager").deregister_hook("on_apply_prefs",
self.on_apply_prefs)
component.get("PluginManager").deregister_hook("on_show_prefs",
self.on_show_prefs)
try:
notifications = component.get("GtkPlugin.Notifications")
notifications.deregister_custom_popup_notification(
"LowDiskSpaceEvent"
)
notifications.deregister_custom_blink_notification(
"LowDiskSpaceEvent"
)
notifications.deregister_custom_sound_notification(
"LowDiskSpaceEvent"
)
except KeyError:
pass
client.deregister_event_handler("PluginEnabledEvent",
self.__on_plugin_enabled)
client.deregister_event_handler("PluginDisabledEvent",
self.__on_plugin_disabled)
def on_apply_prefs(self):
log.debug("applying prefs for FreeSpace")
config = {
"enabled": self.glade.get_widget('enabled').get_active(),
"percent": self.glade.get_widget('percent').get_value()
}
client.freespace.set_config(config)
def on_show_prefs(self):
client.freespace.get_config().addCallback(self.cb_get_config)
def cb_get_config(self, config):
"callback for on show_prefs"
self.glade.get_widget('enabled').set_active(config['enabled'])
self.glade.get_widget('percent').set_value(config['percent'])
def __custom_popup_notification(self, ocupied_percents):
title = _("Low Free Space")
message = ''
for path, percent in ocupied_percents.iteritems():
message += '%s%% %s\n' % (percent, path)
message += '\n'
return title, message
def __custom_blink_notification(self, ocupied_percents):
return True # Yes, do blink
def __custom_sound_notification(self, ocupied_percents):
return '' # Use default sound
def __on_plugin_enabled(self, plugin_name):
if plugin_name == 'Notifications':
notifications = component.get("GtkPlugin.Notifications")
notifications.register_custom_popup_notification(
"LowDiskSpaceEvent", self.__custom_popup_notification
)
notifications.register_custom_blink_notification(
"LowDiskSpaceEvent", self.__custom_blink_notification
)
notifications.register_custom_sound_notification(
"LowDiskSpaceEvent", self.__custom_sound_notification
)
def __on_plugin_disabled(self, plugin_name):
pass
# if plugin_name == 'Notifications':
# notifications = component.get("GtkPlugin.Notifications")
# notifications.deregister_custom_popup_notification(
# "LowDiskSpaceEvent"
# )
# notifications.deregister_custom_blink_notification(
# "LowDiskSpaceEvent"
# )
# notifications.deregister_custom_sound_notification(
# "LowDiskSpaceEvent"
# )

View File

@ -1,55 +0,0 @@
#
# 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
from common import get_resource
class WebUI(WebPluginBase):
scripts = [get_resource("freespace.js")]
def enable(self):
pass
def disable(self):
pass

View File

@ -1,71 +0,0 @@
#
# 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__ = "FreeSpace"
__author__ = "Pedro Algarvio"
__author_email__ = "ufs@ufsoft.org"
__version__ = "0.1"
__url__ = "http://deluge.ufsoft.org/hg/Notification/"
__license__ = "GPLv3"
__description__ = "Plugin which continuously checks for available free space."
__long_description__ = __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
""" % ((__plugin_name__, __plugin_name__.lower())*2)
)

View File

@ -188,6 +188,7 @@ class Core(CorePluginBase):
CheckInput(not (label_id in self.labels) , _("Label already exists"))
self.labels[label_id] = dict(OPTIONS_DEFAULTS)
self.config.save()
@export
def remove(self, label_id):
@ -259,7 +260,7 @@ class Core(CorePluginBase):
return False
@export
def set_options(self, label_id, options_dict , apply = False):
def set_options(self, label_id, options_dict):
"""update the label options
options_dict :
@ -271,8 +272,6 @@ class Core(CorePluginBase):
"apply_max":bool(),
"move_completed_to":string() or None
}
apply : applies download-options to all torrents currently labelled by label_id
"""
CheckInput(label_id in self.labels , _("Unknown Label"))
for key in options_dict.keys():

View File

@ -49,6 +49,7 @@ Deluge.ux.AddLabelWindow = Ext.extend(Ext.Window, {
this.form = this.add({
xtype: 'form',
height: 35,
baseCls: 'x-plain',
bodyStyle:'padding:5px 5px 0',
defaultType: 'textfield',
labelWidth: 50,
@ -496,7 +497,7 @@ Deluge.plugins.LabelPlugin = Ext.extend(Deluge.Plugin, {
},
onLabelRemoveClick: function() {
var state = this.filter.getFilter();
var state = this.filter.getState();
deluge.client.label.remove(state, {
success: function() {
deluge.ui.update();

View File

@ -46,11 +46,8 @@ import sidebar_menu
import label_config
import submenu
from deluge.configmanager import ConfigManager
config = ConfigManager("label.conf")
NO_LABEL = "No Label"
def cell_data_label(column, cell, model, row, data):
cell.set_property('text', str(model.get_value(row, data)))

View File

@ -41,8 +41,6 @@ from deluge import component # for systray
import gtk, gobject
from deluge.ui.client import client
from deluge.configmanager import ConfigManager
config = ConfigManager("label.conf")
NO_LABEL = "No Label"
class LabelMenu(gtk.MenuItem):

View File

@ -51,6 +51,8 @@ DEFAULT_PREFS = {
"low_down": -1.0,
"low_up": -1.0,
"low_active": -1,
"low_active_down": -1,
"low_active_up": -1,
"button_state": [[0] * 7 for dummy in xrange(24)]
}
@ -60,6 +62,14 @@ STATES = {
2: "Red"
}
CONTROLLED_SETTINGS = [
"max_download_speed",
"max_download_speed",
"max_active_limit",
"max_active_downloading",
"max_active_seeding"
]
class SchedulerEvent(DelugeEvent):
"""
Emitted when a schedule state changes.
@ -77,6 +87,8 @@ class Core(CorePluginBase):
DEFAULT_PREFS["low_down"] = core_config["max_download_speed"]
DEFAULT_PREFS["low_up"] = core_config["max_upload_speed"]
DEFAULT_PREFS["low_active"] = core_config["max_active_limit"]
DEFAULT_PREFS["low_active_down"] = core_config["max_active_downloading"]
DEFAULT_PREFS["low_active_up"] = core_config["max_active_seeding"]
self.config = deluge.configmanager.ConfigManager("scheduler.conf", DEFAULT_PREFS)
@ -90,26 +102,32 @@ class Core(CorePluginBase):
secs_to_next_hour = ((60 - now[4]) * 60) + (60 - now[5])
self.timer = reactor.callLater(secs_to_next_hour, self.do_schedule)
# Register for config changes so state isn't overridden
component.get("EventManager").register_event_handler("ConfigValueChangedEvent", self.on_config_value_changed)
def disable(self):
try:
self.timer.cancel()
except:
pass
component.get("EventManager").deregister_event_handler("ConfigValueChangedEvent", self.on_config_value_changed)
self.__apply_set_functions()
def update(self):
pass
def on_config_value_changed(self, key, value):
if key in CONTROLLED_SETTINGS:
self.do_schedule(False)
def __apply_set_functions(self):
"""
Have the core apply it's bandwidth settings as specified in core.conf.
"""
core_config = deluge.configmanager.ConfigManager("core.conf")
core_config.apply_set_functions("max_download_speed")
core_config.apply_set_functions("max_upload_speed")
core_config.apply_set_functions("max_active_limit")
for setting in CONTROLLED_SETTINGS:
core_config.apply_set_functions(setting)
# Resume the session if necessary
component.get("Core").session.resume()
@ -131,6 +149,8 @@ class Core(CorePluginBase):
session.set_upload_rate_limit(int(self.config["low_up"] * 1024))
settings = session.settings()
settings.active_limit = self.config["low_active"]
settings.active_downloads = self.config["low_active_down"]
settings.active_seeds = self.config["low_active_up"]
session.set_settings(settings)
# Resume the session if necessary
component.get("Core").session.resume()

View File

@ -183,6 +183,8 @@ class GtkUI(GtkPluginBase):
config["low_down"] = self.spin_download.get_value()
config["low_up"] = self.spin_upload.get_value()
config["low_active"] = self.spin_active.get_value_as_int()
config["low_active_down"] = self.spin_active_down.get_value_as_int()
config["low_active_up"] = self.spin_active_up.get_value_as_int()
config["button_state"] = self.scheduler_select.button_state
client.scheduler.set_config(config)
@ -193,6 +195,8 @@ class GtkUI(GtkPluginBase):
self.spin_download.set_value(config["low_down"])
self.spin_upload.set_value(config["low_up"])
self.spin_active.set_value(config["low_active"])
self.spin_active_down.set_value(config["low_active_down"])
self.spin_active_up.set_value(config["low_active_up"])
client.scheduler.get_config().addCallback(on_get_config)
@ -229,7 +233,7 @@ class GtkUI(GtkPluginBase):
vbox.pack_start(frame, True, True)
vbox.pack_start(hover)
table = gtk.Table(3, 2)
table = gtk.Table(3, 4)
label = gtk.Label(_("Download Limit:"))
label.set_alignment(0.0, 0.6)
@ -251,12 +255,30 @@ class GtkUI(GtkPluginBase):
label = gtk.Label(_("Active Torrents:"))
label.set_alignment(0.0, 0.6)
table.attach(label, 0, 1, 2, 3, gtk.FILL)
table.attach(label, 2, 3, 0, 1, gtk.FILL)
self.spin_active = gtk.SpinButton()
self.spin_active.set_numeric(True)
self.spin_active.set_range(-1, 9999)
self.spin_active.set_increments(1, 10)
table.attach(self.spin_active, 1, 2, 2, 3, gtk.FILL)
table.attach(self.spin_active, 3, 4, 0, 1, gtk.FILL)
label = gtk.Label(_("Active Downloading:"))
label.set_alignment(0.0, 0.6)
table.attach(label, 2, 3, 1, 2, gtk.FILL)
self.spin_active_down = gtk.SpinButton()
self.spin_active_down.set_numeric(True)
self.spin_active_down.set_range(-1, 9999)
self.spin_active_down.set_increments(1, 10)
table.attach(self.spin_active_down, 3, 4, 1, 2, gtk.FILL)
label = gtk.Label(_("Active Seeding:"))
label.set_alignment(0.0, 0.6)
table.attach(label, 2, 3, 2, 3, gtk.FILL)
self.spin_active_up = gtk.SpinButton()
self.spin_active_up.set_numeric(True)
self.spin_active_up.set_range(-1, 9999)
self.spin_active_up.set_increments(1, 10)
table.attach(self.spin_active_up, 3, 4, 2, 3, gtk.FILL)
eventbox = gtk.EventBox()
eventbox.modify_bg(gtk.STATE_NORMAL, gtk.gdk.color_parse("#EDD400"))

View File

@ -41,7 +41,7 @@ from setuptools import setup
__plugin_name__ = "Scheduler"
__author__ = "Andrew Resch"
__author_email__ = "andrewresch@gmail.com"
__version__ = "0.1"
__version__ = "0.2"
__url__ = "http://deluge-torrent.org"
__license__ = "GPLv3"
__description__ = "Schedule limits on a per-hour per-day basis."

View File

@ -1,6 +0,0 @@
#!/bin/bash
mkdir temp
export PYTHONPATH=./temp
python setup.py develop --install-dir ./temp
cp ./temp/Stats.egg-link ~/.config/deluge/plugins
rm -fr ./temp

View File

@ -1,81 +0,0 @@
#
# setup.py
#
# Copyright (C) 2008 Martijn Voncken <mvoncken@gmail.com>
#
# Basic plugin template created by:
# Copyright (C) 2008 Martijn Voncken <mvoncken@gmail.com>
# Copyright (C) 2007, 2008 Andrew Resch <andrewresch@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.
#
#
# 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
from setuptools import setup
__plugin_name__ = "Stats"
__author__ = "Martijn Voncken"
__author_email__ = "mvoncken@gmail.com"
__version__ = "0.1"
__url__ = "http://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__,
packages=[__plugin_name__.lower()],
package_data = __pkg_data__,
entry_points="""
[deluge.plugin.core]
%s = %s:CorePlugin
[deluge.plugin.gtkui]
%s = %s:GtkUIPlugin
[deluge.plugin.web]
%s = %s:WebUIPlugin
""" % ((__plugin_name__, __plugin_name__.lower())*3)
)

View File

@ -1,57 +0,0 @@
#
# __init__.py
#
# Copyright (C) 2008 Martijn Voncken <mvoncken@gmail.com>
#
# Basic plugin template created by:
# Copyright (C) 2008 Martijn Voncken <mvoncken@gmail.com>
# Copyright (C) 2007, 2008 Andrew Resch <andrewresch@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

@ -1,39 +0,0 @@
#
# common.py
#
# Copyright (C) 2009 Andrew Resch <andrewresch@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 pkg_resources
import os.path
def get_resource(filename):
return pkg_resources.resource_filename("stats", os.path.join("data", filename))

View File

@ -1,169 +0,0 @@
#
# core.py
#
# Copyright (C) 2009 Ian Martin <ianmartin@cantab.net>
# Copyright (C) 2008 Damien Churchill <damoxc@gmail.com>
# Copyright (C) 2008 Martijn Voncken <mvoncken@gmail.com>
# Copyright (C) Marcos Pinto 2007 <markybob@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.
#
#
# 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
from twisted.internet.task import LoopingCall
import time
import deluge
from deluge.log import LOG as log
from deluge.plugins.pluginbase import CorePluginBase
from deluge import component
from deluge import configmanager
from deluge.core.rpcserver import export
DEFAULT_PREFS = {
"test": "NiNiNi",
"update_interval": 2, #2 seconds.
"length": 150, # 2 seconds * 150 --> 5 minutes.
}
DEFAULT_TOTALS = {
"total_upload": 0,
"total_download": 0,
"total_payload_upload": 0,
"total_payload_download": 0,
"stats": {}
}
class Core(CorePluginBase):
totals = {} #class var to catch only updating this once per session in enable.
def enable(self):
self.core = component.get("Core")
self.stats ={}
self.config = configmanager.ConfigManager("stats.conf", DEFAULT_PREFS)
self.saved_stats = configmanager.ConfigManager("stats.totals", DEFAULT_TOTALS)
if self.totals == {}:
self.totals.update(self.saved_stats.config)
self.stats = self.saved_stats["stats"] or {}
self.stats_keys = [
"payload_download_rate",
"payload_upload_rate"
]
self.update_stats()
self.update_timer = LoopingCall(self.update_stats)
self.update_timer.start(self.config["update_interval"])
self.save_timer = LoopingCall(self.save_stats)
self.save_timer.start(60)
def disable(self):
self.save_stats()
try:
self.update_timer.stop()
self.save_timer.stop()
except:
pass
def update_stats(self):
try:
status = self.core.get_session_status(self.stats_keys)
for key, value in status.items():
if key not in self.stats:
self.stats[key] = []
self.stats[key].insert(0, value)
for stat_list in self.stats.values():
if len(stat_list) > self.config["length"]:
stat_list.pop()
self.last_update = time.time()
except Exception, e:
log.exception(e)
def save_stats(self):
try:
self.saved_stats["stats"] = self.stats
self.saved_stats.config.update(self.get_totals())
self.saved_stats.save()
except Exception,e:
log.exception(e)
return True
# export:
@export
def get_stats(self, keys):
stats_dict = {}
for key in keys:
if key in self.stats:
stats_dict[key] = self.stats[key]
stats_dict["_last_update"] = self.last_update
return stats_dict
@export
def get_totals(self):
result = {}
session_totals = self.get_session_totals()
for key in session_totals:
result[key] = self.totals[key] + session_totals[key]
return result
@export
def get_session_totals(self):
status = self.core.session.status()
return {
"total_upload": status.total_upload,
"total_download": status.total_download,
"total_payload_upload": status.total_payload_upload,
"total_payload_download": status.total_payload_download
}
@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

View File

@ -1,27 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE glade-interface SYSTEM "glade-2.0.dtd">
<!--Generated with glade3 3.4.5 on Fri Aug 8 23:34:44 2008 -->
<glade-interface>
<widget class="GtkWindow" id="window1">
<child>
<widget class="GtkHBox" id="prefs_box">
<property name="visible">True</property>
<child>
<widget class="GtkLabel" id="label1">
<property name="visible">True</property>
<property name="label" translatable="yes">Test config value:</property>
</widget>
</child>
<child>
<widget class="GtkEntry" id="txt_test">
<property name="visible">True</property>
<property name="can_focus">True</property>
</widget>
<packing>
<property name="position">1</property>
</packing>
</child>
</widget>
</child>
</widget>
</glade-interface>

View File

@ -1,48 +0,0 @@
/*
Script: stats.js
The javascript client-side code for the Stats plugin.
Copyright:
(C) Damien Churchill 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.
*/
StatsPlugin = Ext.extend(Deluge.Plugin, {
constructor: function(config) {
config = Ext.apply({
name: "Stats"
}, config);
StatsPlugin.superclass.constructor.call(this, config);
},
onDisable: function() {
},
onEnable: function() {
}
});
new StatsPlugin();

View File

@ -1,103 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE glade-interface SYSTEM "glade-2.0.dtd">
<!--Generated with glade3 3.4.5 on Mon Oct 13 20:17:39 2008 -->
<glade-interface>
<widget class="GtkWindow" id="window1">
<child>
<widget class="GtkVBox" id="vbox1">
<property name="visible">True</property>
<child>
<widget class="GtkHBox" id="graph_label">
<property name="visible">True</property>
<child>
<widget class="GtkImage" id="image1">
<property name="visible">True</property>
<property name="stock">gtk-page-setup</property>
</widget>
</child>
<child>
<widget class="GtkLabel" id="graph_label_text">
<property name="visible">True</property>
<property name="label" translatable="yes">Graphs</property>
</widget>
<packing>
<property name="position">1</property>
</packing>
</child>
</widget>
</child>
<child>
<widget class="GtkScrolledWindow" id="graph_tab">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="hscrollbar_policy">GTK_POLICY_AUTOMATIC</property>
<property name="vscrollbar_policy">GTK_POLICY_AUTOMATIC</property>
<child>
<widget class="GtkNotebook" id="graph_notebook">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="tab_pos">GTK_POS_LEFT</property>
<child>
<widget class="GtkDrawingArea" id="bandwidth_graph">
<property name="visible">True</property>
</widget>
</child>
<child>
<widget class="GtkLabel" id="bandwidth_label">
<property name="visible">True</property>
<property name="label" translatable="yes">Bandwidth</property>
</widget>
<packing>
<property name="type">tab</property>
<property name="tab_fill">False</property>
</packing>
</child>
<child>
<widget class="GtkDrawingArea" id="connections_graph">
<property name="visible">True</property>
</widget>
<packing>
<property name="position">1</property>
</packing>
</child>
<child>
<widget class="GtkLabel" id="connections_label">
<property name="visible">True</property>
<property name="label" translatable="yes">Connections</property>
</widget>
<packing>
<property name="type">tab</property>
<property name="position">1</property>
<property name="tab_fill">False</property>
</packing>
</child>
<child>
<widget class="GtkDrawingArea" id="seeds_graph">
<property name="visible">True</property>
</widget>
<packing>
<property name="position">2</property>
</packing>
</child>
<child>
<widget class="GtkLabel" id="seeds_label">
<property name="visible">True</property>
<property name="label" translatable="yes">Seeds/Peers</property>
</widget>
<packing>
<property name="type">tab</property>
<property name="position">2</property>
<property name="tab_fill">False</property>
</packing>
</child>
</widget>
</child>
</widget>
<packing>
<property name="position">1</property>
</packing>
</child>
</widget>
</child>
</widget>
</glade-interface>

View File

@ -1,262 +0,0 @@
#
# graph.py
#
# Copyright (C) 2008 Damien Churchill <damoxc@gmail.com>
# Copyright (C) 2008 Martijn Voncken <mvoncken@gmail.com>
# Copyright (C) Marcos Pinto 2007 <markybob@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.
#
#
# 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
"""
port of old plugin by markybob.
"""
import time
import cairo
from deluge.log import LOG as log
from deluge.ui.client import client
black = (0, 0, 0)
gray = (0.75, 0.75, 0.75)
white = (1.0, 1.0, 1.0)
darkred = (0.65, 0, 0)
red = (1.0, 0, 0)
green = (0, 1.0, 0)
blue = (0, 0, 1.0)
orange = (1.0, 0.74, 0)
def default_formatter(value):
return str(value)
def change_opacity(color, opactiy):
"""A method to assist in changing the opactiy of a color inorder to draw the
fills.
"""
color = list(color)
if len(color) == 4:
color[3] = opactiy
else:
color.append(opactiy)
return tuple(color)
class Graph:
def __init__(self):
self.width = 100
self.height = 100
self.length = 150
self.stat_info = {}
self.line_size = 2
self.mean_selected = True
self.legend_selected = True
self.max_selected = True
self.black = (0, 0 , 0,)
self.interval = 2 # 2 secs
self.text_bg = (255, 255 , 255, 128) # prototyping
self.set_left_axis()
def set_left_axis(self, **kargs):
self.left_axis = kargs
def add_stat(self, stat, label='', axis='left', line=True, fill=True, color=None):
self.stat_info[stat] = {
'axis': axis,
'label': label,
'line': line,
'fill': fill,
'color': color
}
def set_stats(self, stats):
self.last_update = stats["_last_update"]
log.debug("Last update: %s" % self.last_update)
del stats["_last_update"]
self.stats = stats
def set_config(self, config):
self.length = config["length"]
self.interval = config["update_interval"]
def draw_to_context(self, context, width, height):
self.ctx = context
self.width, self.height = width, height
try:
self.draw_rect(white, 0, 0, self.width, self.height)
self.draw_x_axis()
self.draw_left_axis()
if self.legend_selected:
self.draw_legend()
except cairo.Error, e:
log.exception(e)
return self.ctx
def draw(self, width, height):
self.width = width
self.height = height
self.surface = cairo.ImageSurface(cairo.FORMAT_ARGB32, self.width, self.height)
self.ctx = cairo.Context(self.surface)
self.draw_rect(white, 0, 0, self.width, self.height)
self.draw_x_axis()
self.draw_left_axis()
if self.legend_selected:
self.draw_legend()
return self.surface
def draw_x_axis(self):
duration = float(self.length * self.interval)
start = self.last_update - duration
ratio = (self.width - 40) / duration
seconds_to_minute = 60 - time.localtime(start)[5]
for i in xrange(0, 5):
text = time.strftime('%H:%M', time.localtime(start + seconds_to_minute + (60*i)))
x = int(ratio * (seconds_to_minute + (60*i)))
self.draw_text(text, x + 46, self.height - 20)
x = x + 59.5
self.draw_dotted_line(gray, x, 20, x, self.height - 20)
y = self.height - 22.5
self.draw_dotted_line(gray, 60, y, int(self.width), y)
def draw_left_axis(self):
stats = {}
max_values = []
for stat in self.stat_info:
if self.stat_info[stat]['axis'] == 'left':
stats[stat] = self.stat_info[stat]
stats[stat]['values'] = self.stats[stat]
stats[stat]['fill_color'] = change_opacity(stats[stat]['color'], 0.5)
stats[stat]['color'] = change_opacity(stats[stat]['color'], 0.8)
stats[stat]['max_value'] = max(self.stats[stat])
max_values.append(stats[stat]['max_value'])
if len(max_values) > 1:
max_value = max(*max_values)
else:
max_value = max_values[0]
if max_value < self.left_axis['min']:
max_value = self.left_axis['min']
height = self.height - self.line_size - 22
#max_value = float(round(max_value, len(str(max_value)) * -1))
max_value = float(max_value)
ratio = height / max_value
for i in xrange(1, 6):
y = int(ratio * ((max_value / 5) * i)) - 0.5
if i < 5:
self.draw_dotted_line(gray, 60, y, self.width, y)
text = self.left_axis['formatter']((max_value / 5) * (5 - i))
self.draw_text(text, 0, y - 6)
self.draw_dotted_line(gray, 60.5, 20, 60.5, self.height - 20)
for stat, info in stats.iteritems():
self.draw_value_poly(info['values'], info['color'], max_value)
self.draw_value_poly(info['values'], info['fill_color'], max_value, info['fill'])
def draw_legend(self):
pass
def trace_path(self, values, max_value):
height = self.height - 24
width = self.width
line_width = self.line_size
self.ctx.set_line_width(line_width)
self.ctx.move_to(width, height)
self.ctx.line_to(width,
int(height - ((height - 28) * values[0] / max_value)))
x = width
step = (width - 60) / float(self.length)
for i, value in enumerate(values):
if i == self.length - 1:
x = 62
self.ctx.line_to(x,
int(height - 1 - ((height - 28) * value / max_value))
)
x -= step
self.ctx.line_to(
int(width + 62 - (((len(values) - 1) * width) / (self.length - 1))),
height)
self.ctx.close_path()
def draw_value_poly(self, values, color, max_value, fill=False):
self.trace_path(values, max_value)
self.ctx.set_source_rgba(*color)
if fill:
self.ctx.fill()
else:
self.ctx.stroke()
def draw_text(self, text, x, y):
self.ctx.set_font_size(9)
self.ctx.move_to(x, y + 9)
self.ctx.set_source_rgba(*self.black)
self.ctx.show_text(text)
def draw_rect(self, color, x, y, height, width):
self.ctx.set_source_rgba(*color)
self.ctx.rectangle(x, y, height, width)
self.ctx.fill()
def draw_line(self, color, x1, y1, x2, y2):
self.ctx.set_source_rgba(*color)
self.ctx.set_line_width(1)
self.ctx.move_to(x1, y1)
self.ctx.line_to(x2, y2)
self.ctx.stroke()
def draw_dotted_line(self, color, x1, y1, x2, y2):
self.ctx.set_source_rgba(*color)
self.ctx.set_line_width(1)
self.ctx.move_to(x1, y1)
self.ctx.line_to(x2, y2)
#self.ctx.stroke_preserve()
#self.ctx.set_source_rgba(*white)
#self.ctx.set_dash((1, 1), 4)
self.ctx.stroke()
#self.ctx.set_dash((1, 1), 0)
if __name__ == "__main__":
import test

View File

@ -1,151 +0,0 @@
#
# gtkui.py
#
# Copyright (C) 2008 Martijn Voncken <mvoncken@gmail.com>
#
# Basic plugin template created by:
# Copyright (C) 2008 Martijn Voncken <mvoncken@gmail.com>
# Copyright (C) 2007, 2008 Andrew Resch <andrewresch@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.
#
#
# 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
import gtk
import gobject
from gtk.glade import XML
from twisted.internet import defer
import graph
from deluge import component
from deluge.log import LOG as log
from deluge.common import fspeed
from deluge.ui.client import client
from deluge.ui.gtkui.torrentdetails import Tab
from deluge.plugins.pluginbase import GtkPluginBase
class GraphsTab(Tab):
def __init__(self, glade):
Tab.__init__(self)
self._name = 'Graphs'
self.glade = glade
self.window = self.glade.get_widget('graph_tab')
self._child_widget = self.window
self.notebook = self.glade.get_widget('graph_notebook')
self.label = self.glade.get_widget('graph_label')
self._tab_label = self.label
self.bandwidth_graph = self.glade.get_widget('bandwidth_graph')
self.bandwidth_graph.connect('expose_event', self.expose)
self.window.unparent()
self.label.unparent()
self.graph_widget = self.bandwidth_graph
self.graph = graph.Graph()
self.graph.add_stat('payload_download_rate', label='Download Rate', color=graph.green)
self.graph.add_stat('payload_upload_rate', label='Upload Rate', color=graph.blue)
self.graph.set_left_axis(formatter=fspeed, min=10240)
def expose(self, widget, event):
"""Redraw"""
context = self.graph_widget.window.cairo_create()
# set a clip region
context.rectangle(event.area.x, event.area.y,
event.area.width, event.area.height)
context.clip()
width, height = self.graph_widget.allocation.width, self.graph_widget.allocation.height
self.graph.draw_to_context(context, width, height)
#Do not propagate the event
return False
def update(self):
log.debug("getstat keys: %s", self.graph.stat_info.keys())
d1 = client.stats.get_stats(self.graph.stat_info.keys())
d1.addCallback(self.graph.set_stats)
d2 = client.stats.get_config()
d2.addCallback(self.graph.set_config)
dl = defer.DeferredList([d1, d2])
def _on_update(result):
width, height = self.graph_widget.allocation.width, self.graph_widget.allocation.height
rect = gtk.gdk.Rectangle(0, 0, width, height)
self.graph_widget.window.invalidate_rect(rect, True)
dl.addCallback(_on_update)
def clear(self):
pass
class GtkUI(GtkPluginBase):
def enable(self):
log.debug("Stats plugin enable called")
self.glade = XML(self.get_resource("config.glade"))
component.get("Preferences").add_page("Stats", self.glade.get_widget("prefs_box"))
component.get("PluginManager").register_hook("on_apply_prefs", self.on_apply_prefs)
component.get("PluginManager").register_hook("on_show_prefs", self.on_show_prefs)
self.on_show_prefs()
self.graphs_tab = GraphsTab(XML(self.get_resource("tabs.glade")))
self.torrent_details = component.get('TorrentDetails')
self.torrent_details.add_tab(self.graphs_tab)
def disable(self):
component.get("Preferences").remove_page("Stats")
component.get("PluginManager").deregister_hook("on_apply_prefs", self.on_apply_prefs)
component.get("PluginManager").deregister_hook("on_show_prefs", self.on_show_prefs)
self.torrent_details.remove_tab(self.graphs_tab.get_name())
def on_apply_prefs(self):
log.debug("applying prefs for Stats")
config = {
"test":self.glade.get_widget("txt_test").get_text()
}
client.stats.set_config(config)
def on_show_prefs(self):
client.stats.get_config().addCallback(self.cb_get_config)
def cb_get_config(self, config):
"callback for on show_prefs"
self.glade.get_widget("txt_test").set_text(config["test"])
def get_resource(self, filename):
import pkg_resources, os
return pkg_resources.resource_filename("stats", os.path.join("data", filename))

View File

@ -1,9 +0,0 @@
<html>
<head>
<meta http-equiv="refresh" content="2" />
</head>
<body>
<img src="output_async.png" /> <br />
<img src="output_dht.png" />
</body>
</html>

View File

@ -1,78 +0,0 @@
from deluge.ui.client import sclient, aclient
sclient.set_core_uri()
import graph
import deluge
def test_sync():
if 1:
upload = sclient.graph_get_upload()
download = sclient.graph_get_download()
print upload
print download
else:
upload = [66804, 66915, 66974, 67447, 67540, 67318, 67320, 67249, 66659, 66489, 67027, 66914, 66802, 67303, 67654, 67643, 67763, 67528, 67523, 67431, 67214, 66939, 67316, 67020, 66881, 67103, 67377, 67141, 67366, 67492, 67375, 67203, 67056, 67010, 67029, 66741, 66695, 66868, 66805, 66264, 66249, 66317, 66459, 66306, 66681, 66954, 66662, 66278, 65921, 65695, 65681, 65942, 66000, 66140, 66424, 66480, 66257, 66271, 66145, 65854, 65568, 65268, 65112, 65050, 65027, 64676, 64655, 64178, 64386, 63979, 63271, 62746, 62337, 62297, 62496, 62902, 63801, 64121, 62957, 62921, 63051, 62644, 63240, 64107, 63968, 63987, 63644, 63263, 63153, 62999, 62843, 62777, 63101, 63078, 63178, 63126, 63401, 62630, 62451, 62505, 62254, 61485, 61264, 60937, 60568, 61011, 61109, 60325, 60196, 59640, 59619, 59514, 60813, 60572, 61632, 61689, 63365, 64583, 66396, 67179, 68209, 68295, 67674, 67559, 67195, 66178, 65632, 66124, 66456, 66676, 67183, 67620, 66960, 66347, 65925, 65907, 65896, 66738, 66703, 67060, 67004, 67007, 66329, 65304, 52002, 38969, 25433, 12426, 0, 0]
download = [42926, 43853, 43157, 45470, 44254, 46272, 45083, 47344, 46716, 51963, 50112, 52334, 55525, 57545, 53691, 51637, 49574, 49836, 48295, 49843, 52878, 56014, 56966, 56938, 60065, 60461, 56542, 59526, 58678, 54424, 51862, 55109, 52132, 53783, 51687, 56567, 52182, 50758, 46714, 50511, 48161, 50920, 48694, 50528, 55074, 55420, 55882, 59268, 59958, 57938, 57115, 51424, 51180, 53184, 52879, 51177, 54417, 51097, 47901, 49870, 55865, 61118, 61476, 63498, 58878, 49630, 45975, 45632, 45892, 44855, 49495, 48304, 45829, 42152, 39403, 37574, 32384, 34933, 34901, 33492, 31953, 36271, 33826, 34515, 36408, 41106, 43054, 44110, 40810, 41383, 37267, 35881, 38660, 37525, 34857, 36718, 36842, 34281, 39528, 41854, 42952, 40021, 41722, 41045, 42917, 39287, 38672, 32824, 28765, 22686, 18490, 15714, 15268, 14793, 15305, 16354, 16720, 17502, 17857, 16622, 18447, 19929, 31138, 36965, 36158, 32795, 30445, 21997, 18100, 22491, 27227, 29317, 32436, 35700, 39140, 36258, 33697, 24751, 20354, 8211, 3836, 1560, 834, 2034, 1744, 1637, 1637, 1637, 0, 0]
from graph import NetworkGraph
n = NetworkGraph()
n.savedUpSpeeds = upload
n.savedDownSpeeds = download
n.draw(800,200)
n.surface.write_to_png('output_sync.png')
def test_async():
g = graph.Graph()
g.add_stat('download_rate', color=graph.green)
g.add_stat('upload_rate', color=graph.blue)
g.set_left_axis(formatter=deluge.common.fspeed, min=10240)
g.async_request()
aclient.force_call(True)
surface = g.draw(600, 300)
surface.write_to_png('output_async.png')
def test_dht():
"""'boring graph, but testing if it works'"""
g = graph.Graph()
g.add_stat('dht_nodes', color=graph.orange)
g.add_stat('dht_cache_nodes', color=graph.blue)
g.add_stat('dht_torrents', color=graph.green)
g.add_stat('num_connections', color=graph.darkred) #testing : non dht
g.set_left_axis(formatter=str, min=10)
g.async_request()
aclient.force_call(True)
surface = g.draw(600, 300)
surface.write_to_png('output_dht.png')
def test_write():
"""
writing to a file-like object; need this for webui.
"""
class fake_file:
def __init__(self):
self.data = []
def write(self, str):
self.data.append(str)
g = graph.Graph()
g.add_stat('download_rate', color=graph.green)
g.add_stat('upload_rate', color=graph.blue)
g.set_left_axis(formatter=deluge.common.fspeed, min=10240)
g.async_request()
aclient.force_call(True)
surface = g.draw(900, 150)
file_like = fake_file()
surface.write_to_png(file_like)
data = "".join(file_like.data)
f = open("file_like.png","wb")
f.write(data)
f.close()
#test_sync()
test_async()
test_dht()
#test_write()

View File

@ -1,6 +0,0 @@
#!/bin/sh
while true; do
python test.py
sleep 2
done;

View File

@ -1,23 +0,0 @@
from deluge.ui.client import sclient, aclient
from deluge.common import fsize
sclient.set_core_uri()
def print_totals(totals):
for name, value in totals.iteritems():
print name , fsize(value)
print "overhead:"
print "up:", fsize(totals["total_upload"] - totals["total_payload_upload"] )
print "down:", fsize(totals["total_download"] - totals["total_payload_download"] )
print "==totals=="
print_totals(sclient.stats_get_totals())
print "==session totals=="
print_totals(sclient.stats_get_session_totals())

View File

@ -1,54 +0,0 @@
#
# webui.py
#
# Copyright (C) 2009 Damien Churchill <mvoncken@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
from common import get_resource
class WebUI(WebPluginBase):
scripts = [get_resource("stats.js")]
# The enable and disable methods are not scrictly required on the WebUI
# plugins. They are only here if you need to register images/stylesheets
# with the webserver.
def enable(self):
log.debug("Stats Web plugin enabled!")
def disable(self):
log.debug("Stats Web plugin disabled!")

View File

@ -35,7 +35,11 @@
from twisted.internet.protocol import Protocol, ClientFactory
from twisted.internet import reactor, ssl, defer
import deluge.rencode as rencode
try:
import rencode
except ImportError:
import deluge.rencode as rencode
import zlib
import deluge.common
@ -430,7 +434,7 @@ class DaemonClassicProxy(DaemonProxy):
self.connected = True
self.host = "localhost"
self.port = 58846
self.user = "localclient"
self.username = "localclient"
# Register the event handlers
for event in event_handlers:
for handler in event_handlers[event]:

View File

@ -42,7 +42,6 @@ import os
import sys
import urlparse
import chardet
import locale
try:
@ -50,45 +49,11 @@ try:
except ImportError:
from sha import sha
from deluge import bencode, common
from deluge import bencode
from deluge.common import decode_string, path_join
from deluge.log import LOG as log
import deluge.configmanager
def decode_string(s, encoding="utf8"):
"""
Decodes a string and re-encodes it in utf8. If it cannot decode using
`:param:encoding` then it will try to detect the string encoding and
decode it.
:param s: string to decode
:type s: string
:keyword encoding: the encoding to use in the decoding
:type encoding: string
"""
try:
s = s.decode(encoding).encode("utf8", "ignore")
except UnicodeDecodeError:
s = s.decode(chardet.detect(s)["encoding"], "ignore").encode("utf8", "ignore")
return s
def utf8_encoded(s):
"""
Returns a utf8 encoded string of s
:param s: (unicode) string to (re-)encode
:type s: basestring
:returns: a utf8 encoded string of s
:rtype: str
"""
if isinstance(s, str):
s = decode_string(s, locale.getpreferredencoding())
elif isinstance(s, unicode):
s = s.encode("utf8", "ignore")
return s
class TorrentInfo(object):
"""
Collects information about a torrent file.
@ -336,7 +301,7 @@ class FileTree2(object):
"""
def walk(directory, parent_path):
for path in directory["contents"].keys():
full_path = common.path_join(parent_path, path)
full_path = path_join(parent_path, path)
if directory["contents"][path]["type"] == "dir":
directory["contents"][path] = callback(full_path, directory["contents"][path]) or \
directory["contents"][path]

View File

@ -126,7 +126,8 @@ def get_line_length(line, encoding="UTF-8"):
if line.count("{!") != line.count("!}"):
raise BadColorString("Number of {! is not equal to number of !}")
line = line.encode(encoding, "replace")
if isinstance(line, unicode):
line = line.encode(encoding, "replace")
# Remove all the color tags
line = strip_colors(line)
@ -146,7 +147,8 @@ def parse_color_string(s, encoding="UTF-8"):
if s.count("{!") != s.count("!}"):
raise BadColorString("Number of {! is not equal to number of !}")
s = s.encode(encoding, "replace")
if isinstance(s, unicode):
s = s.encode(encoding, "replace")
ret = []
# Keep track of where the strings

View File

@ -63,6 +63,9 @@ class Command(BaseCommand):
# Keep a list of deferreds to make a DeferredList
deferreds = []
for arg in args:
if not os.path.exists(arg):
self.console.write("{!error!}%s doesn't exist!" % arg)
continue
if not os.path.isfile(arg):
self.console.write("{!error!}This is a directory!")
continue

View File

@ -109,6 +109,8 @@ class BaseCommand(object):
return self.__doc__
def split(self, text):
if deluge.common.windows_check():
text = text.replace('\\', '\\\\')
return shlex.split(text)
def create_parser(self):

View File

@ -308,7 +308,7 @@ class Screen(CursesStdIO):
if c == curses.KEY_ENTER or c == 10:
if self.input:
self.add_line(">>> " + self.input)
self.command_parser(self.input)
self.command_parser(self.input.encode(self.encoding))
if len(self.input_history) == INPUT_HISTORY_SIZE:
# Remove the oldest input history if the max history size
# is reached.
@ -404,21 +404,13 @@ class Screen(CursesStdIO):
if c > 31 and c < 256:
# Emulate getwch
stroke = chr(c)
uchar = None
while 1:
uchar = ""
while not uchar:
try:
uchar = stroke.decode(self.encoding)
except UnicodeDecodeError:
pass
c = self.stdscr.getch()
if c == -1:
break
stroke += chr(c)
c = self.stdscr.getch()
stroke += chr(c)
if uchar:
if self.input_cursor == len(self.input):
@ -426,7 +418,7 @@ class Screen(CursesStdIO):
else:
# Insert into string
self.input = self.input[:self.input_cursor] + uchar + self.input[self.input_cursor:]
# Move the cursor forward
self.input_cursor += 1

View File

@ -214,7 +214,7 @@ class CreateTorrentDialog:
client.core.get_path_size(result).addCallback(_on_get_path_size)
client.force_call(True)
dialog.destroy()
dialog.hide()
def _on_button_cancel_clicked(self, widget):
log.debug("_on_button_cancel_clicked")
@ -228,11 +228,14 @@ class CreateTorrentDialog:
is_remote = self.files_treestore[0][1] == gtk.STOCK_NETWORK
if is_remote:
# This is a remote path
response = self.glade.get_widget("remote_save_dialog").run()
dialog = self.glade.get_widget("remote_save_dialog")
response = dialog.run()
if response == gtk.RESPONSE_OK:
result = self.glade.get_widget("entry_save_path").get_text()
else:
dialog.hide()
return
dialog.hide()
else:
# Setup the filechooserdialog
chooser = gtk.FileChooserDialog(_("Save .torrent file"),
@ -344,7 +347,6 @@ class CreateTorrentDialog:
trackers,
add_to_session).addCallback(hide_progress)
chooser.destroy()
self.dialog.destroy()
def create_torrent(self, path, tracker, piece_length, progress, comment, target,

View File

@ -122,7 +122,10 @@ class FilterTreeView(component.Component):
self.label_view.set_show_expanders(True)
self.label_view.set_headers_visible(False)
self.label_view.set_level_indentation(-35)
# Force the theme to use an expander-size of 15 so that we don't cut out
# entries due to our indentation hack.
gtk.rc_parse_string('style "treeview-style" { GtkTreeView::expander-size = 15 } class "GtkTreeView" style "treeview-style"')
self.label_view.set_model(self.treestore)
self.label_view.get_selection().connect("changed", self.on_selection_changed)
self.create_model_filter()
@ -210,7 +213,7 @@ class FilterTreeView(component.Component):
row = self.treestore.append(self.cat_nodes[cat],[cat, value, label, count , pix, True])
self.filters[(cat, value)] = row
if cat == "tracker_host" and value not in ("All", "Error"):
if cat == "tracker_host" and value not in ("All", "Error") and value:
d = self.tracker_icons.get(value)
d.addCallback(on_get_icon)

View File

@ -557,6 +557,7 @@
1 MiB
2 MiB
4 MiB
8 MiB
</property>
</widget>
<packing>

View File

@ -47,6 +47,7 @@ import sys
# Initialize gettext
try:
locale.setlocale(locale.LC_ALL, '')
if hasattr(locale, "bindtextdomain"):
locale.bindtextdomain("deluge", pkg_resources.resource_filename("deluge", "i18n"))
if hasattr(locale, "textdomain"):
@ -252,12 +253,6 @@ class GtkUI(object):
# Shutdown all components
component.shutdown()
if self.started_in_classic:
try:
client.daemon.shutdown()
except:
pass
# Make sure the config is saved.
self.config.save()

View File

@ -38,7 +38,11 @@ import sys
import os
import base64
import deluge.rencode
try:
import rencode
except ImportError:
import deluge.rencode as rencode
import deluge.component as component
from deluge.ui.client import client
import deluge.common
@ -51,12 +55,12 @@ import twisted.internet.error
class IPCProtocolServer(Protocol):
def dataReceived(self, data):
data = deluge.rencode.loads(data)
data = rencode.loads(data)
process_args(data)
class IPCProtocolClient(Protocol):
def connectionMade(self):
self.transport.write(deluge.rencode.dumps(self.factory.args))
self.transport.write(rencode.dumps(self.factory.args))
self.transport.loseConnection()
def connectionLost(self, reason):

View File

@ -152,7 +152,15 @@ class MainWindow(component.Component):
"""Returns a reference to the main window glade object."""
return self.main_glade
def quit(self):
def quit(self, shutdown=False):
"""
Quits the GtkUI
:param shutdown: whether or not to shutdown the daemon as well
:type shutdown: boolean
"""
if shutdown:
client.daemon.shutdown()
reactor.stop()
def load_window_state(self):

View File

@ -253,15 +253,10 @@ class MenuBar(component.Component):
def on_menuitem_quitdaemon_activate(self, data=None):
log.debug("on_menuitem_quitdaemon_activate")
# Tell the core to shutdown
def on_shutdown(result):
self.window.quit()
client.daemon.shutdown().addCallback(on_shutdown)
self.window.quit(shutdown=True)
def on_menuitem_quit_activate(self, data=None):
log.debug("on_menuitem_quit_activate")
if self.config["classic_mode"] and client.is_classicmode():
client.daemon.shutdown()
self.window.quit()
## Edit Menu ##

View File

@ -173,6 +173,10 @@ class Preferences(component.Component):
if self.iter_to_remove != None:
self.liststore.remove(self.iter_to_remove)
# We need to re-adjust the index values for the remaining pages
for i, (index, name) in enumerate(self.liststore):
self.liststore[i][0] = i
def show(self, page=None):
"""Page should be the string in the left list.. ie, 'Network' or
'Bandwidth'"""

View File

@ -323,9 +323,6 @@ class SystemTray(component.Component):
if self.config["lock_tray"] and not self.window.visible():
self.unlock_tray()
if self.config["classic_mode"]:
client.daemon.shutdown()
self.window.quit()
def on_menuitem_quitdaemon_activate(self, menuitem):
@ -333,8 +330,7 @@ class SystemTray(component.Component):
if self.config["lock_tray"] and not self.window.visible():
self.unlock_tray()
client.daemon.shutdown()
self.window.quit()
self.window.quit(shutdown=True)
def tray_setbwdown(self, widget, data=None):
self.setbwlimit(widget, _("Set Maximum Download Speed"), "max_download_speed",

View File

@ -88,21 +88,25 @@ def cell_data_statusicon(column, cell, model, row, data):
def cell_data_trackericon(column, cell, model, row, data):
def on_get_icon(icon):
def create_blank_icon():
i = gtk.gdk.Pixbuf(gtk.gdk.COLORSPACE_RGB, True, 8, 16, 16)
i.fill(0x00000000)
return i
if icon:
try:
icon = gtk.gdk.pixbuf_new_from_file_at_size(icon.get_filename(), 16, 16)
except Exception, e:
pass
icon = create_blank_icon()
else:
icon = gtk.gdk.Pixbuf(gtk.gdk.COLORSPACE_RGB, True, 8, 16, 16)
icon.fill(0x00000000)
icon = create_blank_icon()
if cell.get_property("pixbuf") != icon:
cell.set_property("pixbuf", icon)
host = model[row][data]
if host:
d = component.get("TrackerIcons").get(model[row][data])
d = component.get("TrackerIcons").get(host)
d.addCallback(on_get_icon)
else:
on_get_icon(None)

View File

@ -62,6 +62,9 @@ class SessionProxy(component.Component):
# Hold the torrents' status.. {torrent_id: [time, {status_dict}], ...}
self.torrents = {}
# Holds the time of the last key update.. {torrent_id: {key1, time, ...}, ...}
self.cache_times = {}
client.register_event_handler("TorrentStateChangedEvent", self.on_torrent_state_changed)
client.register_event_handler("TorrentRemovedEvent", self.on_torrent_removed)
client.register_event_handler("TorrentAddedEvent", self.on_torrent_added)
@ -72,10 +75,16 @@ class SessionProxy(component.Component):
t = time.time()
for key, value in status.items():
self.torrents[key] = [t, value]
self.cache_times[key] = {}
for k, v in value.items():
self.cache_times[key][k] = t
return client.core.get_torrents_status({}, [], True).addCallback(on_torrent_status)
def stop(self):
client.deregister_event_handler("TorrentStateChangedEvent", self.on_torrent_state_changed)
client.deregister_event_handler("TorrentRemovedEvent", self.on_torrent_removed)
client.deregister_event_handler("TorrentAddedEvent", self.on_torrent_added)
self.torrents = {}
def create_status_dict(self, torrent_ids, keys):
@ -93,7 +102,11 @@ class SessionProxy(component.Component):
"""
sd = {}
for torrent_id in torrent_ids:
sd[torrent_id] = dict([(x, y) for x, y in self.torrents[torrent_id][1].iteritems() if x in keys])
if keys:
sd[torrent_id] = dict([(x, y) for x, y in self.torrents[torrent_id][1].iteritems() if x in keys])
else:
sd[torrent_id] = dict(self.torrents[torrent_id][1])
return sd
def get_torrent_status(self, torrent_id, keys):
@ -110,20 +123,37 @@ class SessionProxy(component.Component):
"""
if torrent_id in self.torrents:
if time.time() - self.torrents[torrent_id][0] < self.cache_time:
return succeed(self.create_status_dict([torrent_id], keys)[torrent_id])
# Keep track of keys we need to request from the core
keys_to_get = []
if not keys:
keys = self.torrents[torrent_id][1].keys()
for key in keys:
if time.time() - self.cache_times[torrent_id][key] > self.cache_time:
keys_to_get.append(key)
if not keys_to_get:
return succeed(self.create_status_dict([torrent_id], keys)[torrent_id])
else:
d = client.core.get_torrent_status(torrent_id, keys, True)
d = client.core.get_torrent_status(torrent_id, keys_to_get, True)
def on_status(result, torrent_id):
self.torrents[torrent_id][0] = time.time()
t = time.time()
self.torrents[torrent_id][0] = t
self.torrents[torrent_id][1].update(result)
for key in keys_to_get:
self.cache_times[torrent_id][key] = t
return self.create_status_dict([torrent_id], keys)[torrent_id]
return d.addCallback(on_status, torrent_id)
else:
d = client.core.get_torrent_status(torrent_id, keys, True)
def on_status(result):
if result:
self.torrents[torrent_id] = (time.time(), result)
t = time.time()
self.torrents[torrent_id] = (t, result)
self.cache_times[torrent_id] = {}
for key in result:
self.cache_times[torrent_id][key] = t
return result
return d.addCallback(on_status)
@ -151,6 +181,8 @@ class SessionProxy(component.Component):
for key, value in result.items():
self.torrents[key][0] = t
self.torrents[key][1].update(value)
for k in value:
self.cache_times[key][k] = t
# Create the status dict
if not torrent_ids:
@ -161,10 +193,16 @@ class SessionProxy(component.Component):
def find_torrents_to_fetch(torrent_ids):
to_fetch = []
t = time.time()
for key in torrent_ids:
torrent = self.torrents[key]
for torrent_id in torrent_ids:
torrent = self.torrents[torrent_id]
if t - torrent[0] > self.cache_time:
to_fetch.append(key)
to_fetch.append(torrent_id)
else:
# We need to check if a key is expired
for key in keys:
if t - self.cache_times[torrent_id].get(key, 0.0) > self.cache_time:
to_fetch.append(torrent_id)
break
return to_fetch
#-----------------------------------------------------------------------
@ -197,13 +235,20 @@ class SessionProxy(component.Component):
return d.addCallback(on_status, None, keys)
def on_torrent_state_changed(self, torrent_id, state):
self.torrents[torrent_id][1]["state"] = state
if torrent_id in self.torrents:
self.torrents[torrent_id][1]["state"] = state
self.cache_times[torrent_id]["state"] = time.time()
def on_torrent_added(self, torrent_id):
self.torrents[torrent_id] = [time.time() - self.cache_time - 1, {}]
self.cache_times[torrent_id] = {}
def on_status(status):
self.torrents[torrent_id][1].update(status)
t = time.time()
for key in status:
self.cache_times[torrent_id][key] = t
client.core.get_torrent_status(torrent_id, []).addCallback(on_status)
def on_torrent_removed(self, torrent_id):
del self.torrents[torrent_id]
del self.cache_times[torrent_id]

View File

@ -34,7 +34,7 @@
#
import os
from HTMLParser import HTMLParser
from HTMLParser import HTMLParser, HTMLParseError
from urlparse import urljoin, urlparse
from tempfile import mkstemp
@ -121,37 +121,39 @@ class TrackerIcons(Component):
"""
A TrackerIcon factory class
"""
def __init__(self, dir=None, noIcon=None):
def __init__(self, icon_dir=None, no_icon=None):
"""
Initialises a new TrackerIcons object
:param dir: the (optional) directory of where to store the icons
:type dir: string
:param noIcon: the (optional) path name of the icon to show when no icon
:param icon_dir: the (optional) directory of where to store the icons
:type icon_dir: string
:param no_icon: the (optional) path name of the icon to show when no icon
can be fetched
:type noIcon: string
:type no_icon: string
"""
Component.__init__(self, "TrackerIcons")
if not dir:
dir = get_config_dir("icons")
self.dir = dir
if not icon_dir:
icon_dir = get_config_dir("icons")
self.dir = icon_dir
if not os.path.isdir(self.dir):
os.makedirs(self.dir)
self.icons = {}
for icon in os.listdir(self.dir):
if icon != noIcon:
if icon != no_icon:
host = icon_name_to_host(icon)
try:
self.icons[host] = TrackerIcon(os.path.join(self.dir, icon))
except KeyError:
log.warning("invalid icon %s", icon)
if noIcon:
self.icons[None] = TrackerIcon(noIcon)
if no_icon:
self.icons[None] = TrackerIcon(no_icon)
else:
self.icons[None] = None
self.icons[''] = self.icons[None]
self.pending = {}
self.redirects = {}
def get(self, host):
"""
@ -202,7 +204,7 @@ class TrackerIcons(Component):
:rtype: Deferred
"""
if not url:
url = host_to_url(host)
url = self.host_to_url(host)
log.debug("Downloading %s", url)
return download_file(url, mkstemp()[1], force_filename=True)
@ -235,7 +237,8 @@ class TrackerIcons(Component):
d = f
if f.check(error.PageRedirect):
# Handle redirect errors
location = urljoin(host_to_url(host), error_msg.split(" to ")[1])
location = urljoin(self.host_to_url(host), error_msg.split(" to ")[1])
self.redirects[host] = url_to_host(location)
d = self.download_page(host, url=location)
d.addCallbacks(self.on_download_page_complete, self.on_download_page_fail,
errbackArgs=(host,))
@ -260,7 +263,10 @@ class TrackerIcons(Component):
break
parser.close()
f.close()
os.remove(page)
try:
os.remove(page)
except Exception, e:
log.warning("Couldn't remove temp file: %s", e)
return parser.get_icons()
def on_parse_complete(self, icons, host):
@ -275,7 +281,7 @@ class TrackerIcons(Component):
:rtype: list
"""
log.debug("Got icons for %s: %s", host, icons)
url = host_to_url(host)
url = self.host_to_url(host)
icons = [(urljoin(url, icon), mimetype) for icon, mimetype in icons]
log.debug("Icon urls: %s", icons)
return icons
@ -304,13 +310,39 @@ class TrackerIcons(Component):
:returns: a Deferred which fires with the downloaded icon's filename
:rtype: Deferred
"""
if len(icons) == 0:
raise NoIconsError, "empty icons list"
(url, mimetype) = icons.pop(0)
d = download_file(url, os.path.join(self.dir, host_to_icon_name(host, mimetype)),
force_filename=True)
d.addCallback(self.check_icon_is_valid)
if icons:
d.addErrback(self.on_download_icon_fail, host, icons)
return d
@proxy(threads.deferToThread)
def check_icon_is_valid(self, icon_name):
"""
Performs a sanity check on icon_name
:param icon_name: the name of the icon to check
:type icon_name: string
:returns: the name of the validated icon
:rtype: string
:raises: InvalidIconError
"""
if PIL_INSTALLED:
try:
Image.open(icon_name)
except IOError, e:
raise InvalidIconError(e)
else:
if os.stat(icon_name).st_size == 0L:
raise InvalidIconError, "empty icon"
return icon_name
def on_download_icon_complete(self, icon_name, host):
"""
Runs any download cleanup functions
@ -345,16 +377,16 @@ class TrackerIcons(Component):
d = f
if f.check(error.PageRedirect):
# Handle redirect errors
location = urljoin(host_to_url(host), error_msg.split(" to ")[1])
location = urljoin(self.host_to_url(host), error_msg.split(" to ")[1])
d = self.download_icon([(location, extension_to_mimetype(location.rpartition('.')[2]))] + icons, host)
if not icons:
d.addCallbacks(self.on_download_icon_complete, self.on_download_icon_fail,
callbackArgs=(host,), errbackArgs=(host,))
elif f.check(error.NoResource, error.ForbiddenResource) and icons:
d = self.download_icon(icons, host)
elif f.check(IndexError):
elif f.check(NoIconsError, HTMLParseError):
# No icons, try favicon.ico as an act of desperation
d = self.download_icon([(urljoin(host_to_url(host), "favicon.ico"), extension_to_mimetype("ico"))], host)
d = self.download_icon([(urljoin(self.host_to_url(host), "favicon.ico"), extension_to_mimetype("ico"))], host)
d.addCallbacks(self.on_download_icon_complete, self.on_download_icon_fail,
callbackArgs=(host,), errbackArgs=(host,))
else:
@ -404,6 +436,19 @@ class TrackerIcons(Component):
del self.pending[host]
return icon
def host_to_url(self, host):
"""
Given a host, returns the URL to fetch
:param host: the tracker host
:type host: string
:returns: the url of the tracker
:rtype: string
"""
if host in self.redirects:
host = self.redirects[host]
return "http://%s/" % host
################################ HELPER CLASSES ###############################
class FaviconParser(HTMLParser):
@ -450,17 +495,6 @@ class FaviconParser(HTMLParser):
############################### HELPER FUNCTIONS ##############################
def host_to_url(host):
"""
Given a host, returns the URL to fetch
:param host: the tracker host
:type host: string
:returns: the url of the tracker
:rtype: string
"""
return "http://%s/" % host
def url_to_host(url):
"""
Given a URL, returns the host it belongs to
@ -532,3 +566,11 @@ def extension_to_mimetype(extension):
:raises KeyError: if given an invalid filename extension
"""
return MIME_MAP[extension.lower()]
################################## EXCEPTIONS #################################
class NoIconsError(Exception):
pass
class InvalidIconError(Exception):
pass

View File

@ -1,3 +1,20 @@
Icons in this folder are copied from the fruge icon set, taken from
http://www.pinvoke.com, licensed under the Creative Common
Attribution-ShareAlike 3.0 License.
Exceptions
==========
apple-pre-57.png
apple-pre-72.png
apple-pre-114.png
active.png
alert.png
all.png
deluge.png
dht.png
downloading.png
inactive.png
queued.png
seeding.png
traffic.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.4 KiB

View File

@ -1,37 +1,40 @@
<!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">
<head>
<title>Deluge: Web UI ${version}</title>
<link rel="shortcut icon" href="${base}icons/deluge.png" type="image/png" />
<link rel="icon" href="${base}icons/deluge.png" type="image/png" />
<!-- Stylesheets -->
% for stylesheet in stylesheets:
<link rel="stylesheet" type="text/css" href="${base}${stylesheet}" />
% endfor
<head>
<title>Deluge: Web UI ${version}</title>
<link rel="shortcut icon" href="${base}icons/deluge.png" type="image/png" />
<link rel="icon" href="${base}icons/deluge.png" type="image/png" />
<link rel="apple-touch-icon-precomposed" media="screen and (resolution: 163dpi)" href="${base}icons/apple-pre-57.png" />
<link rel="apple-touch-icon-precomposed" media="screen and (resolution: 132dpi)" href="${base}icons/apple-pre-72.png" />
<link rel="apple-touch-icon-precomposed" media="screen and (resolution: 326dpi)" href="${base}icons/apple-pre-114.png" />
<!-- Stylesheets -->
% for stylesheet in stylesheets:
<link rel="stylesheet" type="text/css" href="${base}${stylesheet}" />
% endfor
<script type="text/javascript">
<script type="text/javascript">
deluge = {
author: 'Damien Churchill <damoxc@gmail.com>',
version: '${version}',
config: ${js_config}
}
</script>
</script>
<!-- Javascript -->
% for script in scripts:
<script type="text/javascript" src="${base}${script}"></script>
% endfor
<script type="text/javascript">
Deluge.debug = ${str(debug).lower()};
</script>
</head>
<body>
<div style="background-image: url('${base}themes/default/tree/loading.gif');"></div>
<!-- Javascript -->
% for script in scripts:
<script type="text/javascript" src="${base}${script}"></script>
% endfor
<script type="text/javascript">
Deluge.debug = ${str(debug).lower()};
</script>
</head>
<body>
<div style="background-image: url('${base}themes/images/default/tree/loading.gif');"></div>
<!-- Preload icon classes -->
<div class="ext-mb-error"></div>
<div class="icon-ok"></div>
</body>
</body>
</html>

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

View File

@ -1,45 +1,59 @@
#!/bin/sh
add_file "data/SortTypes.js"
add_file "data/PeerRecord.js"
add_file "data/TorrentRecord.js"
add_file "details/DetailsPanel.js"
add_file "details/DetailsTab.js"
add_file "details/FilesTab.js"
add_file "details/OptionsTab.js"
add_file "details/PeersTab.js"
add_file "details/StatusTab.js"
add_file "add/Window.js"
add_file "add/AddWindow.js"
add_file "add/FileWindow.js"
add_file "add/FilesTab.js"
add_file "add/Infohash.js"
add_file "add/OptionsPanel.js"
add_file "add/OptionsTab.js"
add_file "add/UrlWindow.js"
add_file "preferences/BandwidthPage.js"
add_file "preferences/CachePage.js"
add_file "preferences/DaemonPage.js"
add_file "preferences/DownloadsPage.js"
add_file "preferences/EncryptionPage.js"
add_file "preferences/InstallPluginWindow.js"
add_file "preferences/InterfacePage.js"
add_file "preferences/NetworkPage.js"
add_file "preferences/OtherPage.js"
add_file "preferences/PluginsPage.js"
add_file "preferences/PreferencesWindow.js"
add_file "preferences/ProxyField.js"
add_file "preferences/ProxyPage.js"
add_file "preferences/QueuePage.js"
add_file "StatusbarMenu.js"
add_file "OptionsManager.js"
add_file "AddConnectionWindow.js"
add_file "AddTrackerWindow.js"
add_file "Client.js"
add_file "ConnectionManager.js"
add_file "Deluge.js"
add_file "Deluge.Formatters.js"
add_file "Deluge.Keys.js"
add_file "Deluge.Menus.js"
add_file "Deluge.data.SortTypes.js"
add_file "Deluge.EventsManager.js"
add_file "Deluge.OptionsManager.js"
add_file "Deluge.MultiOptionsManager.js"
add_file "Deluge.Add.js"
add_file "Deluge.Add.File.js"
add_file "Deluge.Add.Url.js"
add_file "Deluge.Client.js"
add_file "Deluge.ConnectionManager.js"
add_file "Deluge.Details.js"
add_file "Deluge.Details.Status.js"
add_file "Deluge.Details.Details.js"
add_file "Deluge.Details.Files.js"
add_file "Deluge.Details.Peers.js"
add_file "Deluge.Details.Options.js"
add_file "Deluge.EditTrackers.js"
add_file "Deluge.FileBrowser.js"
add_file "Deluge.Login.js"
add_file "Deluge.MoveStorage.js"
add_file "Deluge.Plugin.js"
add_file "Deluge.Preferences.js"
add_file "Deluge.Preferences.Downloads.js"
add_file "Deluge.Preferences.Network.js"
add_file "Deluge.Preferences.Encryption.js"
add_file "Deluge.Preferences.Bandwidth.js"
add_file "Deluge.Preferences.Interface.js"
add_file "Deluge.Preferences.Other.js"
add_file "Deluge.Preferences.Daemon.js"
add_file "Deluge.Preferences.Queue.js"
add_file "Deluge.Preferences.Proxy.js"
add_file "Deluge.Preferences.Cache.js"
add_file "Deluge.Preferences.Plugins.js"
add_file "Deluge.Remove.js"
add_file "Deluge.Sidebar.js"
add_file "Deluge.Statusbar.js"
add_file "Deluge.Toolbar.js"
add_file "Deluge.Torrent.js"
add_file "Deluge.Torrents.js"
add_file "Deluge.UI.js"
add_file "EditTrackerWindow.js"
add_file "EditTrackersWindow.js"
add_file "EventsManager.js"
add_file "FileBrowser.js"
add_file "FilterPanel.js"
add_file "Formatters.js"
add_file "Keys.js"
add_file "LoginWindow.js"
add_file "Menus.js"
add_file "MoveStorage.js"
add_file "MultiOptionsManager.js"
add_file "OtherLimitWindow.js"
add_file "Plugin.js"
add_file "RemoveWindow.js"
add_file "Sidebar.js"
add_file "Statusbar.js"
add_file "Toolbar.js"
add_file "TorrentGrid.js"
add_file "UI.js"

View File

@ -84,12 +84,12 @@ Deluge.Formatters = {
/**
* Formats a string to display a transfer speed utilizing {@link #size}
*
* @param {Number} bits the number of bits per second
* @param {Number} bytes the number of bytes per second
* @param {Boolean} showZero pass in true to displays 0 values
* @return {String} formatted string with KiB, MiB or GiB units.
*/
speed: function(bits, showZero) {
return (!bits && !showZero) ? '' : fsize(bits, showZero) + '/s';
speed: function(bytes, showZero) {
return (!bytes && !showZero) ? '' : fsize(bytes, showZero) + '/s';
},
/**

View File

@ -174,7 +174,7 @@ Deluge.MultiOptionsManager = Ext.extend(Deluge.OptionsManager, {
this.stored[this.currentId][option] = value;
if (!this.isDirty(option)) {
this.fireEvent('changed', this.currentId, option, value, oldValue);
this.fireEvent('changed', option, value, oldValue);
}
}
},

View File

@ -107,7 +107,7 @@ Deluge.Sidebar = Ext.extend(Ext.Panel, {
// Grab the filters from each of the filter panels
this.items.each(function(panel) {
var state = panel.getState();
if (!state == null) return;
if (state == null) return;
states[panel.filterType] = state;
}, this);
} else {

View File

@ -232,9 +232,9 @@ Deluge.preferences.Plugins = Ext.extend(Ext.Panel, {
},
onPluginEnabled: function(pluginName) {
var index = this.grid.getStore().find('plugin', pluginName);
var index = this.list.getStore().find('plugin', pluginName);
if (index == -1) return;
var plugin = this.grid.getStore().getAt(index);
var plugin = this.list.getStore().getAt(index);
plugin.set('enabled', true);
plugin.commit();
},

File diff suppressed because it is too large Load Diff

Some files were not shown because too many files have changed in this diff Show More