Compare commits

...

1048 Commits

Author SHA1 Message Date
d250e0a486 [Packaging] Ensure sdist tarball contains correct files 2017-06-25 19:22:13 +01:00
3f1ff54887 [Packaging] Fix using wrong commit id 2017-06-25 19:22:13 +01:00
0424543e36 Update author details 2017-06-25 19:22:13 +01:00
d0cb45808c Exclude test and plugin files from build dir 2017-06-25 19:22:13 +01:00
0cc0882ac9 [WebUI] Refactor out AuthError for NotAuthorizedError 2017-06-23 12:19:39 +01:00
a7c4228ce7 Update systemd service files
- Add man page link
 - Ensure deluge-web is not daemonised
2017-06-23 10:12:07 +01:00
ce3a8c0c17 [Travis] Pin to older trusty build 2017-06-17 04:00:22 +01:00
ace531e8ae [WebUI] Fix testing array after removing torrents 2017-06-17 03:49:30 +01:00
2c66a4c29e [#2707|WebUI] Ensure var test for getSelectedRecords value
* Fixes potential for TypeError undefined in browser console.
2017-06-17 03:47:48 +01:00
6e66452cf3 [GTKUI] Add a prefix space for seed rank k unit 2017-06-17 03:29:16 +01:00
cb646f3a4f Update changelog and gettext 2017-06-16 19:15:05 +01:00
32d5392776 [#2499|GTKUI] Add key shortcuts for changing queue position
- Ctrl+Alt+[Shift]+{Up|Down}
2017-06-16 16:56:33 +01:00
03ca4cfa46 [#2354|GTKUI] Fix alt-f conflict between Tabs and Menu 2017-06-16 14:41:54 +01:00
fb4307f748 Revert "[#2341] Use common.resource_filename in plugins"
Using common.resource_filename broke plugins so need to find a better solution for non-ascii dirs.

This reverts commit bdb3b509ad.
2017-06-16 14:23:10 +01:00
3893d3e214 Revert "[Plugins] Replace pkg_resources for abspath and decode path"
This reverts commit 85364dc8ab.
2017-06-16 11:15:19 +01:00
2b90f309a6 [WebUI] Refactor host connect methods 2017-06-16 08:03:42 +01:00
fb71d049b1 [#3068|WebUI] Fix not connecting to default daemon 2017-06-16 07:45:26 +01:00
89e245e2d3 [#3058|GTKUI] Fix unable to add user to daemon 2017-06-16 01:18:34 +01:00
3330e97d05 [#3067|GTKUI] Fix the daemon stop button sensitivity 2017-06-16 00:47:05 +01:00
15741047ed [Common] Catch tarfile exception if no filepaths exist 2017-06-16 00:31:26 +01:00
eeccc47dde Enable migration from hostlist.conf.1.2 to hostlist.conf 2017-06-16 00:31:26 +01:00
c169d8909f [#3059|GTKUI] Fix starting and autoconnecting to daemon
* Fix start_daemon using callback for non-deferred method.
 * Ensure autostart checks daemon status before attempting to start it.
 * Remove initial delay with autoconnect, it will retry anyway.
 * Found method __connect that needed renaming to _connect.
 * Tweak the timing for refresh and retries.
 * Fix use of reason.trap instead of reason.check, causing code to silently error-out
   and not retry connecting.
 * Set the stop daemon button to insensitive when not connected. The client cannot stop
   a daemon with daemon.shutdown() if daemon is not instatiated. Perhaps in future use
   config and pidfile to allow shutdown without connecting to daemon.
2017-06-15 18:45:26 +01:00
dc27d873b3 Fix metafile.makeinfo trying to decode pieces
* The use of unicode_literals causes join to decode pieces if 'b' prefix not used.
 * Added test.
 * Fixed session_id keyerror when running with tests.
2017-06-15 18:25:13 +01:00
32dc683d5c [#3037] Add creator field to details tabs 2017-06-15 16:33:24 +01:00
06dfeed750 [#3064|WebUI] Fix server not sending TLS intermediate certs
* Sending of cert chain was unintentionallly removed in commit c1902e43 (#2792).
2017-06-15 13:47:10 +01:00
60c764ac33 [Core] Save torrent state only if state has changed 2017-06-14 10:56:19 +01:00
5410c44bb1 [Lint] Fix import position 2017-06-12 21:53:25 +01:00
d372be9b65 [#3055|GTKUI] Fix unable to auto-start localhost daemon 2017-06-12 21:36:52 +01:00
131b9f86a9 [GTKUI] Fix saving host details in connection manager 2017-06-12 10:38:22 +01:00
45e0d10932 Update archiving of state file to use tarfile 2017-06-09 15:08:11 +01:00
596b5d5cd4 [GTKUI] Add prefs liststore col for translation 2017-06-09 11:31:38 +01:00
bb5dafc61d [GTKUI] Small refactor prefs remove_page 2017-06-09 11:31:38 +01:00
7e9a8b801c [#2467|GTKUI] Fix prefs tabs sync adding/removing page 2017-06-09 11:18:00 +01:00
f30f421f50 [Label] Fix common import from parent dir 2017-06-08 14:32:17 +01:00
bbce304f5a [WebUI] Fix attempting to connect to blank host_info 2017-06-08 14:31:38 +01:00
de3a6c443d [WebUI] Set debug to false for gettext script 2017-06-08 14:03:55 +01:00
7ad8431dd9 [Plugins] Update create_plugin script
* Tidyup copyright block
2017-06-08 11:48:42 +01:00
85364dc8ab [Plugins] Replace pkg_resources for abspath and decode path
- Use a BASE_PATH constant.
 - pkg_resources caches the files in python_egg_cache which is not
   required for plugins and causes issues with non-ascii paths.
2017-06-08 11:47:59 +01:00
bdb3b509ad [#2341] Use common.resource_filename in plugins 2017-06-08 11:25:11 +01:00
8326206f87 Ensure configmanager config dir path is unicode 2017-06-08 11:25:11 +01:00
85a1e478fe [#3044|Core] Stop libtorrent checking file timestamps in resume data
* The verfication of timestamps of files on disc against those in resume data
   can be buggy and sends torrents to error state for no apparent reason. It has
   also been removed in latest version of libtorrent so disabling this check.
2017-06-08 11:12:07 +01:00
29191e6a58 [Tests] Remove duplicated override code 2017-06-07 12:36:09 +01:00
15e0e0f30a [Console] Refactor quit code and cleanup conn mgr 2017-06-07 12:36:09 +01:00
d474576104 [Tests] Suppress JSONRequestFailedTestCase printing to stdout 2017-06-07 12:36:09 +01:00
31555ee5ed [UI] Further refactoring of the connection managers
* Add host Edit button to WebUI.
 * Updated and fixed associated tests.
 * Refactored related gtkui code to better understand code flow.
 * Removed dead code in gtkui.
2017-06-07 12:36:09 +01:00
2f11bb8303 [GTKUI] Update connection manager layout 2017-06-07 12:36:09 +01:00
ac48ad982e [UI] Refactor duplicated code out of connection managers 2017-06-07 12:36:09 +01:00
54a081bdfd [Tests] Remove orphaned ubuntu favicon 2017-06-07 12:25:23 +01:00
989137ddc3 [OSX] Default to 64-bit builds 2017-06-07 11:47:58 +01:00
c72459d3ef Replace pep8 with pycodestyle in tox.ini 2017-06-07 11:08:50 +01:00
5c1faf3d5e [Tests] Unpin isort as 4.2.13 contains fixes 2017-06-07 10:31:09 +01:00
4feef3be78 [GTKUI] Fix showing main window when starting in tray
* Also fixes the prefs and dialogs now showing due to first_show not
   connecting signals when starting in tray.
2017-06-07 10:24:11 +01:00
3032e5eb21 [Lint] Fix pylint warnings 2017-06-07 10:05:48 +01:00
481f779349 [Python3] Fixes to make code backward compatible
* Continuation of updating code to Python 3 with Python 2 fallback.
 * Using io.open allows files to be encoded and decoded automatically on write and read. This
 maintains the python boundaries of unicode in code and bytes for output/files so less
 explicit encoding or decoding.
 * io.StringIO is the replacement for StringIO and will only accept unicode strings.
 * io.BytesIO is used where bytes output is required by the enclosing method.
 * Update bencode for full compatibility.
2017-06-05 22:25:29 +01:00
ede0f710f8 [Core] Replace usage of deprecated new.classobj
* type is new module to use
2017-06-05 09:03:34 +01:00
66718f6d8e [Docs] Remove chardet mock to fix requests version check
* chardet is an easily installed pure python module so doesn't need mocking.
2017-06-05 08:10:13 +01:00
fd5833c1d8 Fix build failing on removed icon 2017-06-05 06:37:18 +01:00
6f8a6e5045 [Tests] Remove failing ubuntu icon test
* Changes to website make this test unusable now.
2017-06-05 00:01:39 +01:00
78dc75d55d [Tests] Specify module versions to fix tox tests 2017-06-05 00:01:07 +01:00
579f56b0bf [Lint] Fix triple-quoted to use double-quotes not single
* according to pep8 triple-quotes should use double-quote.
2017-06-05 00:01:07 +01:00
9ebe30edfc Remove unused lock icon 2017-06-05 00:01:07 +01:00
e045d5e5c2 Add new deluge icons and update icon generation script 2017-06-05 00:01:07 +01:00
fefe742ea1 [Packaging] Add a make_release file for source packaging 2017-05-17 13:56:56 +01:00
0c36b63f22 Cleanup infolist and changelog 2017-05-16 12:00:28 +01:00
c9692bb5bd [Plugins] Add webui pref pages for Label and Autoadd
* Add info-only preference pages for these plugins in WebUI.
2017-05-16 11:12:53 +01:00
0353b82c0c [GTKUI] Fix typos in text strings 2017-05-08 10:11:18 +01:00
23a34c6bac [Core] Update new release url 2017-05-03 15:19:08 +01:00
4e7b2e5b41 Update gitignore
Ignore .cache
2017-05-03 10:29:59 +01:00
3668e77a75 [GTKUI] Fix column sort state not saved in Thinclient mode
* In torrentview.stop the listview is cleared however this meant in thinclient mode
   that listview sort details are empty and overwrites existing data when save_state is
   then called in torrentview.shutdown.
2017-05-02 16:29:17 +01:00
a727ee67bc [Core] Ensure absolute dirpath for config dir 2017-03-29 21:22:33 +01:00
aa28d73f47 [Console] Fix super usage breaking console 2017-03-29 21:21:45 +01:00
d485eb5c8f [#2786] [GTKUI] Fix showing connection manager with malformed ip 2017-03-29 18:39:18 +01:00
8a0b7d970a [#2837] [WebUI] Create gettext.js when building 2017-03-22 12:55:17 +00:00
a76fde5feb [GTKUI] Enable multi-row selection with mouse 2017-03-22 12:41:25 +00:00
648dc93655 [#3003] [Label] Fix duplicate context menu entries 2017-03-22 00:21:37 +00:00
f32c8aff90 Minor gettext script improvements 2017-03-21 22:05:54 +00:00
9aed7a7f0c [AutoAdd] Handle AddTorrentError exceptions 2017-03-21 20:21:28 +00:00
c8d084c563 [AutoAdd] Fixes for adding magnets
* Combines three commits from 1.3-stable that fixed splitting magnets from file.

      b52de1549e, 8a3f15e5c0, 2d4dec669e
2017-03-21 20:08:34 +00:00
7e04abd1e5 [AutoAdd] Minor cody tidyup 2017-03-21 20:08:34 +00:00
e2e13103b8 [Core] Catch keyerror in add torrent alert 2017-03-21 18:47:31 +00:00
d8e5cbe689 [#2928] [GTKUI] Fix missing name and trackers adding magnets 2017-03-21 18:47:31 +00:00
19fdd2f88c [Core] Fix saving torrent info metadata
* Unicode dict entry should be bytes.
2017-03-21 18:46:40 +00:00
e5e49f68ba [GTKUI] Fix adding from clipboard 2017-03-20 22:53:37 +00:00
5a3625f6cd Add systemd service files to packaging dir 2017-03-20 19:16:50 +00:00
bc50f6e5f6 [#2913] [Notifications] Fix webui passing string for port value 2017-03-20 18:46:36 +00:00
8ff1bfc0b2 [Console] Add time_added to info status keys 2017-03-20 11:48:55 +00:00
744ab08dfb [GTKUI] Reduce default size of prefs dialog 2017-03-20 11:32:59 +00:00
d01100a258 [GTKUI] Implement multi-torrent support in Options Tab 2017-03-20 08:39:09 +00:00
722ca41584 [GTKUI] Cleaup/refactor mainwindow 2017-03-20 08:39:09 +00:00
1637da84e4 [GTKUI] Refactor torrent details Tabs code
* Move common code into Tab parent class
 * The Tab init now accepts creation with name, child_widget and tab_label but
   will still accept the 'oldstyle' contructor to keep backwards compatibility
   with 3rd-party plugins.
 * Create namedtuple TabWidget with widget, format func and status_keys. The
   TabWidget are stored in a tab_widgets dict making it easier to create, save
   and update large numbers of gtk widgets.
2017-03-20 08:39:09 +00:00
6d28f2c885 [GTKUI] Refactor connect_signals to use 'self'
* If the handler method names match the signal names then it is possible
   to refactor out the mappings and simply pass 'self' to connect_signals.
2017-03-20 08:39:09 +00:00
482606d80a [#2990] [Core] Fix torrent priorities mismatch
* The old priorities instead of updated call to lt were being saved to self.options.
2017-03-20 08:28:10 +00:00
73ea123f1a [#2966] [Tests] Rename deprecated method aliases 2017-03-19 22:15:07 +00:00
d09df063a5 [Stats] Remove blanket try..excepts and stop timers correctly 2017-03-18 11:54:01 +00:00
676574ff19 [Stats] Update gtkui from libglade to gtkbuilder
- Additionally fixes #2977 num_connections KeyError and #2947 AttributeError.
2017-03-18 11:34:40 +00:00
665c047541 [Label] Update gtkui from libglade to gtkbuilder 2017-03-17 23:35:03 +00:00
939835cef1 Fix warning with label glade files 2017-03-17 16:44:23 +00:00
9e303b58a0 Refactor common magnet funcs 2017-03-17 10:26:17 +00:00
112a872bc1 [#2716] [Common] Update magnet funcs for tracker tiers tr.x 2017-03-17 10:26:17 +00:00
6424333c35 [GTKUI] Refactor adding list of trackers 2017-03-17 10:26:17 +00:00
267d331fac [#2476] [GTKUI] Fix adding mistyped trackers urls 2017-03-17 10:26:17 +00:00
d1daeb4cb0 [Trans] Fix unicode issue in msgfmt
- The offset was being calculated on unicode rather than byte strings lengths.
2017-03-17 01:05:58 +00:00
7c6c9eae7f [WebUI] Fix error displaying tracker icons 2017-03-16 23:22:58 +00:00
036154fc36 [#2417] [UI] Add Last Transfer to torrent status and torrentview columns
- Create new status entry `time_since_transfer` and getter that is
   calculated from lt time_since_upload and time_since_download.
 - Add/update all UIs and formatters.
 - Included update to console layout to match other uis
2017-03-16 23:22:58 +00:00
e3abdf9901 Use super class call where possible 2017-03-16 23:20:57 +00:00
af7e83bc76 Replace/remove usage of dict.keys() 2017-03-16 23:20:56 +00:00
4a274466ac Add python 3 section to tox 2017-03-16 23:20:56 +00:00
eb38e0ffff [Py2to3] Large set of changes for Python 3 compat
- Preparation work for using six or future module for Py2/3 compat. The
   code will be written in Python 3 with Python 2 fallbacks.
 - Added some Py3 imports with Py2 fallbacks to make it easier to remove
   Py2 code in future.
 - Replace xrange with range (sort out import as top of files in future).
 - Workaround Py2to3 basestring issue with inline if in instances. This means
   every usage of basestring is more considered.
 - Replace iteritems and itervalues for items and values. There might be a
   performance penalty on Py2 so might need to revisit this change.
2017-03-16 23:20:56 +00:00
321677e05a [WebUI] Return 404.html for files not found
- This ensures a proper request response is returned.
2017-03-15 23:12:36 +00:00
960f3a6552 [WebUI] Check render template files exist and raise 404 if not
- Check render/* requests match to .html files in the 'render' dir
 - Protects against directory (path) traversal
2017-03-15 23:12:36 +00:00
35c78eee41 Update to 1.1.2 libtorrent requirement 2017-03-15 09:56:19 +00:00
5348465e60 Fix 1d1bb2a2a issue by defining init in DelugeRPCProtocol 2017-03-14 19:29:45 +00:00
1d1bb2a2a7 [#2964] Fix RPC TypeError using namedtuple 2017-03-14 18:02:33 +00:00
767503ad88 [#2979] Fix Deluge start error with missing entrypoints
- An ImportError is raised if a module is listed in the entrypoint
   but is not actually installed. The code will now catch this and
   will de-list the ui module that failed.
2017-03-14 17:22:58 +00:00
1e696fe6ca Fix type in manpage 2017-03-14 17:21:38 +00:00
90631cca88 Fix console test 2017-03-14 15:48:20 +00:00
50c476c41d Update man pages to reflect ui arg changes
- Also fixup help output details
2017-03-14 14:58:59 +00:00
8232505961 [#2996] [Console] Fix duplicate commands in help output
- The duplicate entries were actually the aliases for commands
   but the command name not the alias was being returned.
2017-03-14 14:57:53 +00:00
966678196e [WebUI] Use error logging and add exception message detail 2017-03-01 14:47:42 +00:00
11e8957dea [WebUI] Only accept application/json content-type requests
- Protects against CSRF (Cross-site request forgery)
2017-03-01 14:47:42 +00:00
ec5c8bafb6 [#2815] [Console] Fix 'add' cmd path inconsistency on windows
When adding a torrent with a download location from command prompt
2017-02-23 19:30:42 +00:00
fb8dc42acf [Core] Catch None type country in get_peers 2017-02-23 19:16:04 +00:00
4df88c0df3 [UI] Refactor appdata.xml code and markup translatable text 2017-02-23 19:02:32 +00:00
3529036f55 [UI] Add an appdata.xml file
For Linux software gallery integration.
2017-02-23 19:01:44 +00:00
f2b77c8635 [WebUI] Linting trailing comma 2017-02-23 16:52:06 +00:00
49e5611f65 Remove unicode_literal from plugins setup.py
There is a bug in Py2 setuptools where the build fails if unicode dict keys
 are passed to package_data:

    `package_data must be a dictionary mapping package names to lists of wildcard patterns`

 The easiest workaround is to remove unicode_literals use in setup.py as it is not that important.
2017-02-23 16:48:05 +00:00
d75afc007d [GTKUI] Parse u:p@host:port pasted into proxy or connection manager 2017-02-23 09:46:39 +00:00
b32c5d8247 [Daemon] Improve logging when another process is using listen port 2017-02-23 09:31:58 +00:00
b2db96e4df [Py2to3] Refactor out usage of unicode and basestring
- Python 3 renames `unicode` type to `str` and introduces `bytes` type.
 - Python 2.7 has `bytes` but is only an alias for `str` so restricted
   to comparisons but helps keep compatibility.
 - To test for unicode string on Py2 and Py3 uses the "''.__class__" type.
 - Remove usage of utf8encode and just encode, problems with bytes being passed
   in code will be picked up faster.
 - Where possible refactor out isinstance for try..except duck-typing.
2017-02-23 00:35:43 +00:00
52a85cb91c [Console] Fix showing file priorites 2017-02-23 00:30:56 +00:00
beb4f8c8f9 [Common] Rename decode_string to decode_bytes
- Switch to using Python 3 naming convention where str now refers to unicode
   and bytes are encoded strings.
 - Cleanup docs and code
 - Also rename convert_to_utf8 to utf8_encode_structure to clarify functionality.
2017-02-23 00:30:10 +00:00
3b1eeb0818 Use Ubuntu Trusty on Travis 2017-02-23 00:21:33 +00:00
9ad2f50fa4 [Common] Decode byte strings for TorrentInfo 2017-02-23 00:21:33 +00:00
bbd2661acb [WebUI] Fix missing low.png for file priority 2017-02-23 00:21:33 +00:00
b2a2995c0d [GTKUI] Use int not string number for select_path 2017-02-23 00:21:32 +00:00
5a9784ff4d Fix msgfmt for Python 3 and unicode_literal 2017-02-23 00:21:32 +00:00
ba41110c27 [WebUI] Markup byte strings for twisted request 2017-02-22 12:45:12 +00:00
608ecae5fd [Console] Use raw string for regex compile 2017-02-22 12:45:12 +00:00
04b8b14828 [Console] Use io.open to en/decode utf8 files 2017-02-22 12:45:11 +00:00
4619b31aa3 [Console] Ensure opening torrent files in binary mode 2017-02-22 12:45:11 +00:00
e3b8aaf276 [Tests] Markup byte strings to fix tests 2017-02-22 12:45:11 +00:00
1d9733014a [Core] Fixup maketorrent for unicode_literals
* Added a new convert_to_utf8 function to common that is handy for
   any nested dictionaries etc.
 * Small refactor to get rid of duplicate code and comment will be
   encoded by convert_to_utf8 function.
 * Passes test_maketorrent.
2017-02-22 12:36:33 +00:00
304ad1e72d [Lint] Add missing copyright header to files 2017-02-22 12:36:33 +00:00
ff6cec251a [Core] Markup byte-strings to fix httpdownloader and core tests
* Twisted methods require byte-string arguments.
 * The headers str conversion in httpdownloader _download_file was
   incorrent and left the dict key still as unicode so replaced with
   a dict comprehension (py2.7 requirement).
2017-02-22 12:36:33 +00:00
84802da29b [Py2to3] Force unicode_literals and fix related issues
* Added `from __future__ import unicode_literals` to every file so
   now all strings in code are forced to be unicode strings unless
   marked as byte string `b'str'` or encoded to byte string `'str'.encode('utf-8')`.

   This is a large change but we have been working towards the goal of unicode
   strings passed in the code so decoding external input and encoding
   output as byte strings (where applicable).

   Note that in Python 2 the `str` type still refers to byte strings.

 * Replaced the use of `str` for `basestring` in isinstance comparison as
   this was the original intention but breaks code when encoutering unicode strings.

 * Marked byte strings in gtkui as the conversion to utf8 is not always handled, mostly
   related to gobject signal names.
2017-02-22 12:36:32 +00:00
011afe3e89 [#2879] [OSX] Fix dyld error opening file from within Deluge
- Using DYLD_LIBRARY_PATH seems to have the unintended effect of making associated apps
   unusable (unable to locate dylds) when opening a file from within Deluge. The workaround
   for now is to switch to using DYLD_FALLBACK_LIBRARY_PATH.
2017-02-22 11:20:51 +00:00
b7162fab36 [#2956] Fix empty file_priorities with magnets 2017-02-21 10:37:09 +00:00
f6e2dab58a [#2826] Fix create_torrent filedump not encoded 2017-02-21 10:13:30 +00:00
4a62c5eac2 [Core] Switch move_storage flag to dont_replace 2017-02-21 10:10:57 +00:00
dd30bad96a [WebUI] Log successful logins with associated ip 2017-02-20 18:37:44 +00:00
c2c0fe86f9 [#2957] [GTKUI] Fix AttributeError in torrentview column sort 2017-02-20 13:28:36 +00:00
14d9b6cfcb Temp workaround for critical pickle trackers lt bug 2017-02-20 12:45:44 +00:00
df4d97c447 [Tox] Twisted version pin to 16.6
* There is a problem with Twisted 17 and the setup for travis and tox
   using site-packages with an old openssh package.

       AttributeError: 'module' object has no attribute 'OP_NO_TLSv1_1'

 * The simplest workaround for now is to pin Twisted to 16.6.
2017-02-14 18:59:34 +00:00
a7826c4f90 [Core] Use fallback message for empty tracker error status 2017-01-28 15:23:08 +00:00
8c26c83c4d [#2960] Fix typo in storage moved alert 2017-01-28 14:27:27 +00:00
6b49f844dc [GTKUI] Fix showing cache status ratio 2017-01-26 12:49:56 +00:00
4a344e382b [UI] [Core] Update and refactor proxy settings
* Combine I2P into proxy settings.
2017-01-26 12:49:55 +00:00
108dd9e4b8 Add temporary libtorent git RC_1_1 branch requirement 2017-01-26 12:49:55 +00:00
b71a2fa549 [Tox] Remove py26 and add lt version output to trial 2017-01-26 12:49:55 +00:00
3ca012ee63 Change libtorrent minimum dependency to 1.1.1
* Change PPA to develop for Travis to use libtorrent 1.1
2017-01-26 12:49:55 +00:00
706d53ab4a [Core] Move readonly key setting closer to config init 2017-01-26 12:49:38 +00:00
2321f32f84 [Core] Fix double alert messages 2017-01-26 12:49:36 +00:00
d1dd35d4b1 [Tests] Suppress file descriptor limit warning 2017-01-26 12:49:27 +00:00
a5de64a19c [Stats] Update session status keys 2017-01-26 12:49:25 +00:00
5d7c1336b9 [Core] Use new lt torrent moving_storage status 2017-01-26 12:49:04 +00:00
93898d6475 [Core] Use single underscore for private methods 2017-01-26 12:49:02 +00:00
4caf05c092 [Core] Refactor torrent.set_file_priorities 2017-01-26 12:49:02 +00:00
6083a3078e [Core] Minor cleanup of preferencesmanager 2017-01-26 12:49:01 +00:00
0160bb1c91 [Lint] Fix pylint warnings in setup.py 2017-01-26 12:48:52 +00:00
e7ce389e84 [Tests] Fix test_ui_entry failure with libtorrent 1.1
- With libtorrent 1.1 dht is enabled from startup and the returned status
   contains DHT node values (i.e. non-zero). Should be sufficient to only test for start
   and end of the status string to be correct.
2017-01-26 12:48:50 +00:00
0f2083db62 [UI] [Core] Convert to session_stats_alert for session status
* Use session disk stats for cache status
2017-01-26 12:48:50 +00:00
3ed7202253 [Core] Remove deprecated lt torrent priority 2017-01-26 12:48:43 +00:00
089c0be89b [Core] Replace deprecated lt.fingerprint with peer_fingerprint setting 2017-01-25 12:35:33 +00:00
fc902af10c [Core] Remove all lt 1.1 deprecated code references 2017-01-18 14:19:37 +00:00
a481c4d243 [UI] Update UIs for new default piece priority
- libtorrent 1.1 changes default piece priorty to 4 so changes to
   the UIs are required. Some refactoring and improvements were made as well:

    - A new 'Low' priority introduced that is values 1-3.
    - 'Normal' priority is now value 4.
    - Removed 'Highest' with the addition of 'Low' and 'High' is now values 5-7.
    - Renamed 'Do not download' to 'Ignore' so it is more succinct.
    - Moved file priority constant to ui.common.
2017-01-18 12:33:27 +00:00
bb44411a50 [Core] Check valid torrent first in alert handlers 2017-01-18 12:33:26 +00:00
1e4a24c474 [Docs] Fix docstring return type format in config.py
* With new version of Sphinx 1.5 the warning below was generated, caused
   by incorrect formatting of return type in docstring.

     docstring of deluge.config.find_json_objects:None:
     WARNING: more than one target found for cross-reference u'start'
2017-01-12 11:13:28 +00:00
993a0f71af [UI] Fix usage of 'with Image.open' in tracker_icons
* Revert changes made to fix 'too many files open' as Image.open does
      not return a file descriptor and generated the following error:

          exceptions.AttributeError: 'NoneType' object has no attribute 'startswith'
2017-01-12 10:11:08 +00:00
283ad6137d Fix isfile() typo in setup.py 2017-01-09 18:02:28 +00:00
6743b0c813 [Core] Relocate add_torrent_alert to alerts section 2016-11-30 23:21:46 +00:00
b69ba02652 [Core] Refactor default add torrent flags into a constant 2016-11-30 23:21:45 +00:00
915f2bf9e7 [Core] Move alert logging to alertmanager 2016-11-30 22:53:18 +00:00
59b01b363c [Core] Refactor TM register of alert and set funcs 2016-11-30 22:53:18 +00:00
c5e623ae45 [Core] Refactor of priorities code and test 2016-11-30 22:53:18 +00:00
425af00ebf [GTKUI] Fix the translation of tracker status 2016-11-30 20:43:34 +00:00
373c8a14b0 [WebUI] Remove duplicated translation code 2016-11-30 20:43:29 +00:00
5f4a16630e Fix a typo in minify script 2016-11-28 19:20:05 +00:00
3260db416a [Core] Add a quick fix for lt1.1 default priority change 2016-11-28 19:18:48 +00:00
9cefbc6e5d [Setup] Refactor BuildWebUI class 2016-11-28 18:59:25 +00:00
14a5156e15 [Setup] Move package variables to top of file
- Refactor _data_files list so icons paths are auto-generated.
2016-11-28 18:58:45 +00:00
259c9f11e6 [Setup] Rearrange order of some classes 2016-11-28 17:44:22 +00:00
fd1261ab65 [Setup] Create a CleanTranslations class 2016-11-28 17:44:20 +00:00
37d9e1f8fe [UI] Move and rename util/lang to translations_util
- The name needed to be more descriptive of it's function.
 - Moved into ui directory because of upcoming  changes being made to setup.py
   meant it would be easier to include all the 'common' ui files if there
   are no sub-dirs such as 'util'.
2016-11-28 17:41:56 +00:00
a924cb73b0 [Setup] Fix wording and syntax in descriptions 2016-11-28 13:00:24 +00:00
3cbafec68d [Setup] Move deluge egg-info clean to Clean 2016-11-28 13:00:09 +00:00
a48c01c3a5 Cleanup up minify js script
- The slimit package is now widely available so make the script
   only rely on it and closure. Keeping closure as it makes the most
   compact and lints the javascript but it is not as easy to install
   as slimit.
2016-11-28 12:59:03 +00:00
7468078b71 [Setup] Add a CleanDocs class 2016-11-28 12:58:34 +00:00
98add5fecd [Lint] Fix issues raised by minify script 2016-11-28 12:58:27 +00:00
5e1603317a Revert "Rename version.py to calc_version.py to fix buildd error"
This reverts commit 943a9ded00.
2016-11-26 19:35:53 +00:00
943a9ded00 Rename version.py to calc_version.py to fix buildd error
- There is a version.py included with buildd that is conflicting
   with the deluge one. Rather than messing with sys.path it is
   simplest to rename version.py to calc_version.py
2016-11-26 17:51:07 +00:00
04370b38ec Revert "[Setup] Fix buildd error with version.py import"
This reverts commit 3aff57600f.
2016-11-26 17:10:13 +00:00
3aff57600f [Setup] Fix buildd error with version.py import
- The deb buildd command also has a version.py so need to use relative import
   to specify that we really mean deluge supplied version.py.
2016-11-26 16:36:31 +00:00
874249655d [Tests] Replace isort test with flake8-isort
* Move the known_third_party back to setup.cfg with comments.
2016-11-25 12:43:50 +00:00
7ebd69218f [Common] Fix is_ipv6 when using ipaddress module
* The ipaddress module require unicode string for 'packed' addresses.
 * Also include minor corrections to the tests.
2016-11-23 11:04:12 +00:00
0edebda1c7 [WebUI] Log correct http address if listening on IPv6 2016-11-22 22:07:03 +00:00
7283e8b668 [Core] Deprecate prioritize_first_last for prioritize_first_last_pieces 2016-11-22 20:45:58 +00:00
f3f380553a [Core] Fix KeyError if only file_priorities in options 2016-11-22 20:45:57 +00:00
4a9d2d2129 [Core] Decorate methods deprecated 2016-11-22 20:45:57 +00:00
cf343c21a8 [Base] Add new deprecated decorator 2016-11-22 20:45:57 +00:00
179de3b0ff [#495] Deprecate core.set_torrent_* for core.set_torrent_options 2016-11-22 20:45:57 +00:00
720d113a9a [GTKUI] Fix typo in About dialog if-statement 2016-11-21 21:47:07 +00:00
59c9584fe0 [GTKUI] Revert use of non-existent mainwindow.get_window method 2016-11-19 10:36:51 +00:00
674610ef7d [GTKUI] Use get_window for GdkWindow instead of attribute 2016-11-19 10:32:31 +00:00
b8135617ae Revert "[GTKUI] Add MainWindow.get_window() and replace window attr usage"
Made a mistake with types of window, get_window actually refers to GdkWindow
rather than GtkWindow... The use of self.window.window is confusing and is
still deprecated in Gtk3 so will fix that in following commit.

This reverts commit ccfe6b3c80.
2016-11-19 10:27:34 +00:00
6ac296118d [GTKUI] Replace decode_string with decode(utf-8)
* We know that GTK widgets will return utf-8 encoded so no need to decode_string.
2016-11-17 12:33:23 +00:00
ecf9822ac0 [Docs] Fix example and param notation in reST docstrings
* For future parsing into other docstring styles.
2016-11-17 12:33:23 +00:00
c1249a2f3a [GTKUI] Switch to non-deprecated GTK methods 2016-11-17 12:33:23 +00:00
36cbfa8c61 [Lint] Fix files to pass Flake8 v3.2.0 2016-11-17 12:19:41 +00:00
2657cc3921 [Lint] Quote cleanup 2016-11-17 10:19:59 +00:00
441861786b [GTKUI] Add decode_string to column name comparison
* Strings from GTK are returned utf8 encoded so require decoding if
   comparing with unicode strings passed around in python code.
2016-11-11 22:22:50 +00:00
0cdf0230e9 [GTKUI] Refactor gtk imports and code
* Where possible use 'from gtk import ...', i.e. if repeated often or under 10 individual imports.
 * Remove osx_check to not show svg. It's only an issue on Windows so should work fine...
 * Rearrange and deduplicate code into d.u.g.common for getting pixbuf from files.
 * Use 'from gtk.gdk import...' to make it cleaner to apply GTK3 changes in future.
 * Move generic icon code from torrent_data_funcs to common.
 * Fix pylint import warnings and add WindowsError to pylintrc file.
2016-11-11 22:21:51 +00:00
c8e6a4476d [GTKUI] Another update of methods to latest GTK2 API 2.24
* These updates make the code more compatible with GTK3
2016-11-11 14:08:25 +00:00
ccfe6b3c80 [GTKUI] Add MainWindow.get_window() and replace window attr usage
* In GTK3 the use of `window` attribute is no longer valid so need to
   use get_window(). This updates MainWindow to follow the same convention.
2016-11-11 14:08:25 +00:00
86549eb3ee [GTKUI] Cleanup references to MainWindow 2016-11-11 14:08:25 +00:00
243004c551 [GTKUI] Remove About dialog unneeded url_hook
* This is removed in Gtk3 and clicking url in Gtk2 still works.
2016-11-11 14:08:25 +00:00
8ba8aec277 [GTKUI] Listview use lambda func for set_default_sort_func
* gi complains about `None` value being passed to set_default_sort_func
   so for compatibility use lamba func that does nothing.

   There is an unanswered question on SO about how to set the default func to None in gi:
   http://stackoverflow.com/questions/20940324/how-to-remove-the-default-sort-function-from-a-treesortable
2016-11-11 14:08:25 +00:00
a5b07aa4ef [GTKUI] Update methods to latest GTK2 API 2.24 2016-11-11 14:08:24 +00:00
c619674cf9 [GTKUI] Import gobject module names directly 2016-11-11 14:08:24 +00:00
52d591c83b [GTKUI] Remove unneeded pygtk requires check 2016-11-11 14:08:24 +00:00
4313974f07 [Lang] Add fallback dll for Windows libintl 2016-11-11 14:08:24 +00:00
bde13515e6 [Common] Only attempt dbus import on non-Win/OSX 2016-11-11 14:08:24 +00:00
93ab2445a1 [GTKUI] Update version string in ui files 2016-11-09 15:01:44 +00:00
eea3cb0553 [GTKUI] Tweak spacing of main window tabs 2016-11-08 21:09:01 +00:00
6bd3c6fa88 [#2922] [Core] Fix using incorrect type for peer_tos in lt 1.1 2016-11-08 20:25:16 +00:00
b6e2ec3a3c [Core] Remove redundant utf8 encoding
* Should only be encoding in config.py and this is already occurs.
2016-11-08 20:16:59 +00:00
bbe9de9463 [Core] Disable apply_settings for enc policy until fix can be found 2016-11-06 00:14:07 +00:00
2bdbcf9a39 [#2922] Fix interfaces and outgoing ports for lt 1.1 2016-11-05 23:30:01 +00:00
fd80ed75fe [#2922] Fix proxy settings and torrent status for lt 1.1 2016-11-05 15:49:39 +00:00
0890cc1a33 [#2922] Fix session.apply_sessings alert_mask 2016-11-05 11:30:31 +00:00
e31acfc31c [GTKUI] Restart application when switching modes 2016-11-04 23:54:46 +00:00
d0d070aaf0 [Lint] Fix flake8 2016-11-04 23:54:23 +00:00
1e41891943 [Oops] Bugfix for previous commit: e37c817 2016-11-04 18:07:59 +00:00
e37c817151 [Lint] Refactor flake8 noqa's and add msg numbers
From pep8-naming:
 * N802: function name should be lowercase
 * N803: argument name should be lowercase
2016-11-04 18:03:21 +00:00
af6b277d28 [Lint] Add flake8-quotes to tox and fix bad quotes 2016-11-04 00:10:23 +00:00
3a2ff0c188 [Lint] Convert all python double quotes to single quotes
* A rather disruptive change but for a few reasons such as easier to read,
   easier type, keep consistent and javascript code uses single quotes.
 * There are a few exceptions for the automated process:
    * Any double quotes in comments
    * Triple double quotes for docstrings
    * Strings containing single quotes are left e.g. "they're"

 * To deal with merge conflicts from feature branches it is best to follow
   these steps for each commit:
     * Create a patch: `git format-patch -1 <sha1>`
     * Edit the patch and replace double quotes with single except those in
       comments or strings containing an unescaped apostrophe.
     * Check the patch `git apply --check <patchfile>` and fix any remaining
       issues if it outputs an error.
     * Apply the patch `git am < <patchfile>`
2016-11-03 21:45:45 +00:00
d4a8a38586 [Core] Refactor out duplicate prefsmgr log lines 2016-11-03 12:05:55 +00:00
a87ce825ad [Core] Remove old geoip.dat code and fix log line 2016-11-03 11:37:20 +00:00
ac011d7f55 [Tests] Remove deprecated pip cache setting from travis config 2016-11-03 10:45:01 +00:00
03c7a2b108 [Common] Refactor unit functions 2016-11-03 10:45:01 +00:00
ca83ed79c5 [Lint] Pylint enable simple-if-statement 2016-11-03 10:45:01 +00:00
54685226c4 [Tests] Remove redundant pillow dep from tox 2016-11-03 10:45:01 +00:00
61b059f015 [Lint] Fix couple of pylint complaints 2016-11-03 10:45:01 +00:00
f96b9c8a23 [Lint] Enable pylint warning super-init-not-called 2016-11-03 10:45:00 +00:00
d8242b4ef0 [Lint] Replace R with actual symbols in Pylint rcfile 2016-11-03 10:45:00 +00:00
f664fcb7a6 [Lint] Update pylint rcfile
* Merge in pylint 1.6.4 rcfile changes.
 * Add future_builtins to redefined-builtins-modules ignore list. (should be pylint default!)
	- Removed pylint disable comments from files.
 * Rearrange disable section, comments must be at start of line but can be
   interspersed to highlight categories.
 * Conventions enabled:
	wrong-import-position
	wrong-import-order
2016-11-03 10:45:00 +00:00
5c7a4549f7 [Tests] Add pillow dep so pylint parses Win32IconImagePlugin 2016-11-03 10:45:00 +00:00
a04718ebe5 [#2797][Lint] Enable no-init pylint warning 2016-11-03 10:44:48 +00:00
59d8fc9a14 [Lint] Fix pylint warnings 2016-11-03 10:31:38 +00:00
a438f13647 Rename classic to standalone 2016-11-02 23:14:05 +00:00
23ba57313a [Core] Support new libtorrent 1.1 alert and status attributes
* Keep deprecated lt attribute support for the interim.
2016-11-02 22:08:28 +00:00
7f24a1a42d [Core] Add support for new lt settings_pack 2016-11-02 22:08:21 +00:00
05566894ad [Core] [UI] Remove deprecated lt extensions
* These extensions have been deprecated in 1.1 so simply remove usage.
2016-11-02 21:55:53 +00:00
4dc59b5255 [Core] Remove compact allocation references
* This has been removed from lt 1.1 so no longer relevant.
2016-11-02 21:55:24 +00:00
08192033fb [GTKUI] Refactor piecesbar code 2016-11-01 14:27:50 +00:00
2f4cb0156c [Tests] Fix for isort config package handling
* Force gtk modules to be third_party for tox/travis testing.
2016-11-01 14:04:14 +00:00
e26a3dc0e7 [Tests] Move test torrents into data subdir 2016-11-01 12:28:08 +00:00
e827420569 [Tests] Increase file descriptor limit
Increase the file descriptor limit to avoid 'Too many files open'
error when running tests.
2016-11-01 12:28:08 +00:00
e379e035c7 [#2849] Fix WebUI error without translation MO file 2016-11-01 12:05:36 +00:00
6de2813c3d [#2784] Fix typo in bugfix 5f92810f 2016-11-01 11:57:11 +00:00
20bae1bf90 [Console] Rewrite of the console code
This commit is a rewrite of larger parts of the console code. The
motivation behind the rewrite is to cleanup the code and reduce code
duplication to make it easier to understand and modify, and allow any
form of code reuse. Most changes are to the interactive console, but
also to how the different modes (BaseMode subclasses) are used and set
up.

* Address [#2097] - Improve match_torrent search match:
  Instead of matching e.g. torrent name with name.startswith(pattern)
  now check for asterix at beginning and end of pattern and search
  with startswith, endswith or __contains__ according to the pattern.

Various smaller fixes:
* Add errback handler to connection failed
* Fix cmd line console mixing str and unicode input
* Fix handling delete backwards with ALT+Backspace
* Fix handling resizing of message popups
* Fix docs generation warnings
* Lets not stop the reactor on exception in basemode..
* Markup for translation arg help strings

* Main functionality improvements:
 - Add support for indentation in formatting code in popup messages (like help)
 - Add filter sidebar
 - Add ComboBox and UI language selection
 - Add columnsview to allow rearranging the torrentlist columns
   and changing column widths.
 - Removed Columns pane in preferences as columnsview.py is sufficient
 - Remove torrent info panel (short cut 'i') as the torrent detail view
   is sufficient

* Cleanups and code restructuring
  - Made BaseModes subclass of Component
  - Rewrite of most of basic window/panel to allow easier code reuse
  - Implemented better handling of multple popups by stacking popups. This
    makes it easier to return to previous popup when opening multiple popups.

* Refactured console code:
  - modes/ for the different modes
    - Renamed Legacy mode to CmdLine
    - Renamed alltorrent.py to torrentlist.py and split the code into
      - torrentlist/columnsview.py
      - torrentlist/torrentsview.py
      - torrentlist/search_mode.py (minor mode)
      - torrentlist/queue_mode.py (minor mode)
  - cmdline/ for cmd line commands
  - utils/ for utility files
  - widgets/ for reusable GUI widgets
    - fields.py: Base widgets like TextInput, SelectInput, ComboInput
    - popup.py: Popup windows
    - inputpane.py: The BaseInputPane used to manage multiple base widgets in a panel
	- window.py: The BaseWindow used by all panels needing a curses screen
    - sidebar.py: The Sidebar panel
    - statusbars.py: The statusbars
  - Moved option parsing code from main.py to parser.py
2016-10-30 12:45:04 +00:00
2f8b4732b4 [#2838] [Console] Fix formatting on 'Moving' color 2016-10-30 12:45:03 +00:00
79c59a2b1e [#2099] [Console] Fix: console does not support monochrome terminals
When a terminal does not support colors we invert the
default color pair white,black to indicate selection with
white background and black foreground
2016-10-30 12:45:03 +00:00
51c44a7c5a [Console] Remove the delay after pressing ESC key
The env variable ESCDELAY specifies the time in ms which ncurses waits
for a character sequence. With a default value of 1000, it produces a
1 second delay when pressing the ESC key to close dialogs.

Set this variable to 0 to get instant respons when pressing ESC.
2016-10-30 12:45:03 +00:00
64da09675e [#1119] [Console] ignore logging when no file specified
Add wrapper around the stream passed to the loggers streamhandler
when no log file is specified. Console in interactive mode now
ignores the log output with no logfile specified.
2016-10-30 12:45:02 +00:00
82fd5e6e8a [UI] Fix sorting in ui/util/lang.py.get_languages() 2016-10-30 12:45:02 +00:00
1e183a3258 [UI] Add gettext.ngettext to __builtin__.__dict__
Handle plurality with getttext using ngettext. Added to
__builtin__.__dict__ as _n
2016-10-30 12:45:02 +00:00
891209d925 [Common] Add overrides function decorator 2016-10-30 12:45:02 +00:00
4d3cf756e4 [#2914] Fix: Specifying file version for default config 2016-10-30 12:45:02 +00:00
27c87d56bb [Config] Sort the json keys in conf files 2016-10-29 13:26:56 +01:00
72c588ad33 [WebUI] Modify UI to display single incoming port 2016-10-28 16:01:18 +01:00
41fed16d08 [#2900] Fix Error loading torrent: invalid bencoded value
* Testing the torrent with other bencode libs doesn't raise exceptions
   so just revert the 'small fix' applied in b193d87499.
 * Add BTFailure exception so bdecode issue can be caught in deluge code.
2016-10-28 14:43:06 +01:00
5607bb3d61 [GTKUI] Move Tab data funcs to new common file 2016-10-27 23:10:27 +01:00
fab0af1b40 [Oops] Remove introduced dead code by prev commit 2016-10-27 22:33:17 +01:00
b5afe90764 [GTKUI] Rearrange items in the UI tabs
* Move Private label to Trackers Tab.
 * Move Owner to Options Tab with a future plan for a dropdown box
   to change ownership.
 * Put the torrent status message into the progress bar.
 * Remove duplicate Shared label in Details Tab.
 * Details Tab allow more horizontal room for long folder paths
   and fix horizontal scrolling.
2016-10-27 22:19:00 +01:00
fef160e7a7 [Common] Use log.warning instead of error for download failure 2016-10-27 22:17:29 +01:00
e408dc14cc [#2417] [GTKUI] Add Last Active and modify layout of Status & Details Tabs 2016-10-27 13:11:50 +01:00
257c31c05f [#2846] Fix splitting IPv6 from external IP alert message 2016-10-26 23:26:01 +01:00
3f72905b3f Revert "[Py2to3] Clean-up the use of keys() on dictionary objects."
This reverts commit 8b50f3cdbd.
2016-10-26 19:14:10 +01:00
c4282f29ab [GtkUI] Refactor out duplicate code in piecesbar draw pieces 2016-10-26 10:50:22 +01:00
642913b0f8 Revert "[Py2to3] Replace iteritems and itervalues"
This reverts commit 7ad8a3cbb5.
2016-10-26 10:49:41 +01:00
2c3887ece9 [Py2to3] Use open() instead of file() 2016-10-26 09:58:44 +01:00
9fab98a6ce [Py2to3] Passes libfuturize.fixes.fix_division_safe 2016-10-26 09:58:44 +01:00
81334389a9 [Tests] Fix tests to run on Twisted < 13
* Also includes pylint fixes for W0233(non-parent-init-called)
 * Remove failing openbittorent icon test
2016-10-26 09:58:43 +01:00
d579efa041 [Lint] Fix various pylint warnings and fixup code
* Use print function
 * Fix except as statements
 * Remove old twisted 8 code
 * Remove empty docstring
 * Refactor try statement to only contain the relevant import and
   disable pylint import msgs.
 * Use flake8 noqa and pylint comment and drop pyflakes workarounds.
2016-10-26 09:58:43 +01:00
da4b2b4849 [Py2to3] Make VersionSplit Python 3 compatible.
The builtin cmp() and the __cmp__() special method is no longer used in Python 3, instead we use functools.total_ordering decorator and the __lt__/__eq__ special methods to get the same effect.
2016-10-26 09:58:43 +01:00
da51e3a3d5 [Py2to3] A group of small compatiblity code changes
* Replace the uses of long with int.
 * Replace im_func with __func__ as it has been provided for Python 3 forward-compatibility.
 * Fix next/__next__ for Python 3 compatibility.
 * Remove the long number literal
 * Ensure freespace() returns int
2016-10-26 09:58:43 +01:00
837dae242c [Py2to3] Use future_builtins zip instead of izip for Py3 compat 2016-10-26 09:57:18 +01:00
7ad8a3cbb5 [Py2to3] Replace iteritems and itervalues
* Replace the use of iteritems() and itervalues() on dictionary objects
   with items() and values() respectively for Python 3 compatibility.
2016-10-26 09:53:32 +01:00
8b50f3cdbd [Py2to3] Clean-up the use of keys() on dictionary objects.
To make the code more Python 3 compatible, I've made a few changes to how we handle keys() or iterkeys() calls on dictionaries. All functionality should remain the same.

 * Remove the use of .keys() or .iterkeys() when iterating through a dictionary.
 * Remove the use of .keys() when checking if key exists in dictionary.
 * Replace dict.keys() with list(dict) to obtain a list of dictionary keys. In Python 3 dict.keys() returns a dict_keys object, not a list.
2016-10-26 09:53:32 +01:00
16da4d851e [#2850] Fix duplicate ui log entries 2016-10-25 23:22:06 +01:00
a5bc73f0b3 [GTKUI] Use less verbose units to improve look and gain physical space
* Use markup in gtkui to reduce font size of displayed units.
 * Reduced statusbar item sizes using markup and <small> tag. Provides an
   interim solution to #1326 and has reduced width from 930px to ~600px.
 * Integer value for Progress bar value in Peers and Files Tabs.
 * Remove trailing zeros for Pieces size in Details Tab.
 * Change position of Share Ratio in Status Tab.
 * Increase minimum width of column from 10px to 20px for listview.
 * Use shortform units e.g. Columns, Title speed, Status tab, Status bar.
 * Use less precision displaying values in columns.
2016-10-25 23:22:06 +01:00
258ad95b7a [Common] Enable use of precision and shortform units in unit funcs
* Also fixes #2562; add TiB unit.
2016-10-25 23:21:18 +01:00
75714b60ca [Docs] Clean module sources dir before generating docs
If old and outdated sources are present in docs/sources/modules,
python setup.py build_docs will fail to generate docs.
2016-10-21 10:36:24 +01:00
ca7cbd291f [#2861] [Core] Switch to using python-geoip for geoip lookups
* libtorrent >= 1.1 dropped support for GeoIP so this adds support
   again using MaxMind GeoIP Legacy Python Extension API.
   For reference it is known by the following package names:
       * Maxmind: geoip-api-python
       * Linux: python-geoip
       * PyPi: GeoIP
2016-10-21 10:30:52 +01:00
d77666cd3e [GTKUI] Tidyup to use more width and no eol backslashes 2016-10-21 10:11:08 +01:00
1755347878 [GTKUI] [Console] Modify UIs to display single incoming port 2016-10-21 10:11:08 +01:00
5978b433d3 [Core] Listening ports fixes and updates
* #2133 Add flags for port reuse and disable binding to system port.
 * #2122 For random port, use single port and store it for reuse.
 * #2343 Fix 'Invalid Arg' from listen_on, likely due to whitespace as interface value.
 * Consolidate listen_on and outgoing_port into single '__set...' methods.
2016-10-20 10:57:03 +01:00
37baf3de3c [#2875][Web] Fix: WebUI Json dumps Error 2016-10-19 10:47:41 +02:00
cfdddc4469 [Web][Tests] Refactor web tests 2016-10-19 10:47:37 +02:00
d505ebe926 [Tests] Use common.rpath() in all tests 2016-10-18 21:26:36 +01:00
c8a3fd72d4 [Tests] Improve UI entry script tests
* Added parameter log.setup_logger to prevent output noise in unit tests
2016-10-18 21:26:22 +01:00
9788ca08ea [GTKUI] Autofill infohash entry from clipboard
* Create new common.is_infohash func and test.
2016-10-18 19:22:59 +01:00
b4787235b5 [#2901] [GTKUI] Strip whitespace from infohash entry before checks
* Copy-pasting from web page can include extra space at end of string.
 * Also make minor change to populate the magnet name with infohash
   for nicer UI display.
2016-10-18 18:58:49 +01:00
9dd3b1617d [#2889] Fixes for 'Too many files open' error
* Ensure all file descriptors are closed. Using the with statement ensures
   closure.
 * The main problem was with thousands of unclosed file desciptors from
   tracker_icons mkstemp.
 * Use a prefix 'deluge_ticon.' to identify created tracker_icon tmp files.
2016-10-18 18:40:25 +01:00
58835eeb2e Refactor daemon check process functions 2016-10-18 18:22:31 +01:00
3a8ed2e9cb [Core] Change deprecated lt.version to lt.__version__ 2016-10-17 12:40:28 +01:00
6b630c9fd2 [GtkUI] Fix ZeroDivisionError in piecesbar
While waiting for metadata for a magnet, self.__num_pieces is zero.
2016-10-10 18:58:28 +01:00
db1b427b3f [Tests] Fix flake8 v3 searching .tox dir 2016-10-10 18:46:09 +01:00
aa164cdbce [Core] Fix AttributeError for removed load_country_db method in lt 1.1.1 2016-09-28 10:32:35 +01:00
9c27ed29ae [#2768] [GTKUI] [OSX] Fix invalid file error at startup
When installed to the system, not using .app, error is raised on startup
as nsapp_open_file is ignoring Deluge-bin but not deluge or deluge-gtk for
potential 'filename' when connecting NSApplicationOpenFile.
2016-07-21 00:29:14 +01:00
d2385e9c75 [#2857] [Notification] Fix issues with SMTP port input 2016-07-19 15:14:47 +01:00
01d27e22f8 [#2855] [WebUI] Unable to add UDP trackers 2016-07-19 11:49:00 +01:00
abf90f1dd6 [#2784] [Execute] Escape ampersand in args for Windows
Due to the nature of passing a command and args to cmd.exe and then
to a batch file in Windows any ampersands in execute args need to be
double-escaped so prefixing with tripe-caret (^^^&) is the fix for this.
2016-06-29 23:24:23 +01:00
53215d87ee [#2077] [Extractor] Ignore the remaining rar part files
* Bump version to 0.6
2016-06-10 15:33:04 +01:00
2d5dce4954 [#2785] [Extractor] Fix successful claimed extract leaving empty folder
* The main fix here is adding os.environ to the command call otherwise in some configurations
   the extraction would fail. Was unable to reproduce locally but users confirm this fix works.
 * Refactored the code to properly report errors if the extract command fails along with actual
   command output.
 * Bump version to 0.5.
2016-06-10 15:30:52 +01:00
7e229ceb2f [Tests] Combine echo lines into python cmd for tox docs 2016-05-25 11:06:48 +01:00
2a8388d262 [UI] Fix translation setup in console
Console was incorrectly setting up pygtk translation
2016-05-24 23:59:22 +01:00
4751b33d0c [Console] Fix to console argument parsing
When starting console with './deluge-console', providing
loggin level '-L info' would fail to parse as it identified
'info' as a subcommand.
2016-05-24 23:59:22 +01:00
98eb810f89 [Docs] Minor tidyup of docstrings 2016-05-24 23:53:37 +01:00
7c07001bdc [Docs] Make tox -e docs fail on sphinx warnings
* Also cleanup isort command
2016-05-24 23:53:37 +01:00
a81f17a802 [Tests] Improve test docs 2016-05-24 23:53:14 +01:00
dbadb9b0a6 [Core] Fix core.remove_torrents return value on error 2016-05-24 23:52:24 +01:00
c204b63653 [Docs] Make tox -e docs fail on sphinx warnings
* Also cleanup isort command
2016-05-24 21:10:53 +02:00
48240db813 [Docs] Fix docs in maketorrent.py 2016-05-24 21:10:53 +02:00
94a9f17838 [Tests] Improve test docs 2016-05-24 21:10:53 +02:00
5ca7bb365e [Tests] Use tests/common.todo_test to mark tests for TODO 2016-05-24 21:10:52 +02:00
260268f62b [Tests] Inherit from BaseTestCase in testcases
* Testcases in test_torrent.py and test_torrentmanager.py creates
  components and should therefore inherit from BaseTestCase.
* Cleanup in test_json_api.py
2016-05-24 21:10:52 +02:00
a8dac9bd3a [Base] [Tests] Add more component tests 2016-05-24 21:10:52 +02:00
d1acd964a5 [Base] Fix Component docs 2016-05-24 21:10:52 +02:00
5e493f2d3f [UI] Use a shared DEFAULT_HOSTS dict in ui/common
Instead of defining a DEFAULT_HOSTS dict for each UI
use a shared dict.
2016-05-24 21:10:52 +02:00
d65ebb80c6 [UI] Reduce ui.client log verbosity 2016-05-24 21:10:52 +02:00
b9f3f549a1 [UI] Add __contains__ to deluge/ui/coreconfig.py 2016-05-24 21:10:52 +02:00
67cefb1211 [Core] Add finished_time to torrent status 2016-05-24 21:10:52 +02:00
14b576e411 [Core] Fix core.remove_torrents return value on error 2016-05-24 21:10:52 +02:00
43edea01b7 [Console] Queue prefs updated 2016-05-23 15:31:23 +01:00
262c8d71d5 [WebUI] Queue prefs updated 2016-05-23 15:19:58 +01:00
80ee713893 [#2520] [GTKUI] Queue preferences page reworked 2016-05-23 15:19:20 +01:00
3837a2c5d6 [WebUI] Constrain dialogs to browser window 2016-05-23 15:18:34 +01:00
dc56e4557b [WebUI] Create more space by removing headers from Prefs dialogs 2016-05-23 15:05:51 +01:00
717ceee0ea [Tests] Update comments in tox.ini 2016-05-22 12:43:07 +01:00
5713ff09f4 [Docs] Autogenerate module docs with apidoc
* Add sphinx-apidoc to setup.py build_docs
2016-05-22 12:11:41 +01:00
b6b1d40516 [Tox] [Travis] Fixes to test config 2016-05-22 12:11:41 +01:00
152eaa10dd [Console] Fix bug when parsing UI commands
Command line arguments like "-L info" were incorrectly
identified as console subcommands which caused parsing
to fail.
2016-05-22 02:57:40 +02:00
d689ad72e8 [UI] [#1973] Improve passing extra args to UIs
Current solution for passing arguments to UI when invoking deluge
entry script is to select an UI with the --ui option and supply quoted
arguments with the --args option.

This patch cleans this up by removing both options and change to using
subparsers for valid UIs. All command line options are now parsed
directly by the child UI which is chosen by a positional argument,
i.e. the UI name.

The help text now also shows the current default UI.
2016-05-21 15:05:01 +01:00
d6fec88932 [UI] Move Gtk console entry point class to __init__
To avoid unnecessarily importing modules from gtkui.py, move Gtk
console entry point class to __init__.py. This reduces load time
when showing help (deluge -h) with many hundred miliseconds

Also cleanup unnecessary WebUI code.
2016-05-21 15:04:59 +01:00
fd9e68e7e7 [Tests] Place logfiles from py.test run in _pytest_temp 2016-05-19 22:20:28 +01:00
6971e08b0d [#2828] [Packaging] Fix ImportError with setuptools version > 18.8 2016-05-19 17:21:19 +01:00
cea50f319d [WebUI] Print error if minify script encounters error with closure 2016-05-19 15:47:15 +01:00
6ce9f77e17 [WebUI] Handle missing script files and fallback to available files
* To help user's encountering a blank web page, log warnings if script
files for a selected mode are missing and attempt to fallback to a working mode.
 * There is no logging for dev version detection to prevent spamming output.
 * Add slimit dependency to tox
2016-05-19 15:24:37 +01:00
0f43b564c9 [WebUI] Add WebUI build class to setup.py for minifing javascript 2016-05-19 15:22:45 +01:00
6bf906a849 [Lint] Use a shorter line length for isort 2016-05-18 10:55:01 +01:00
983ee7b973 [Tests] Raise minimum isort version to 4.2.5
The use of 'isort:imports-firstparty' in gtkui.py requires version >=4.2.5
2016-05-18 10:03:06 +01:00
bd7d10b81e [Lint] [WebUI] Fix issues raised by closure 2016-05-18 09:53:09 +01:00
876e70d85f [WebUI] Remove margins from main window elements 2016-05-16 13:44:00 +01:00
590f077963 [WebUI] Tidyup Add dialog margins 2016-05-16 13:27:00 +01:00
2aa1ab2f2b [WebUI] Revert broken refactor of theme css
By combining the background-* css styles into background it overrides background
settings in ext-all-notheme.css resulting in incorrect placement of grid header gif.
2016-05-16 12:37:08 +01:00
2e08599f82 [WebUI] Disable disabling WebUi plugin in WebUI 2016-05-16 12:37:08 +01:00
b450739333 [WebUI] Remove border in Prefs for cleaner look 2016-05-16 12:37:08 +01:00
e330ff0299 [WebUI] Tidyup prefs plugins details 2016-05-16 12:37:08 +01:00
6c233da2ff [WebUI] Case-insensitive sort for plugins list 2016-05-16 12:37:08 +01:00
fa309d0d18 [WebUI] Refactor json_api._get_host 2016-05-16 12:37:07 +01:00
9f187ed027 [WebUI] Add missing deregister event handlers 2016-05-15 21:30:25 +01:00
42e5876ebe [#2293] [WebUI] Fix plugins not loading when using WebUI plugin
- Any plugins that were started before the WebUI plugin would not be loaded
   upon starting the web server and would be not show up. The fix is to use
   web.pluginmanager.start to get all enabled plugins from core.
 - Update log message output for enable/disable in pluginmanager
2016-05-15 21:20:27 +01:00
46b726a4e0 [WebUI] Fix prefs plugins page not listing enabled plugins correctly
This fixes the display of which plugins are currently running. The old
code was returned a list of enabled plugins containing WebUI code so
switched to calling the entire list of a plugins from core.

Also updated the docstring in json api to reflect actual usage.
2016-05-15 21:18:16 +01:00
0278e782e0 [#2490] Add external IP to statusbar 2016-05-14 12:29:53 +01:00
bf8f71f215 [WebUI] Update gettext script to find any missed marked-up text
Added a new function to the gettext script that will check common
extjs attributes for missing markup text strings and print the result.
2016-05-14 11:16:28 +01:00
9adc9f886c [WebUI] Add missing translation markup 2016-05-14 11:14:17 +01:00
50d504a38f [AutoAdd] Fix watch dir not accepting uppercase file extension
- Auto-add feature will now accept torrents when the .torrent extension
   has capital letters in it
2016-05-12 17:41:07 +01:00
c2d7f3c653 [#2795] [GTKUI] Reduce height of Add Torrent Dialog
- Reduced height from 575px to 495px
 - Low resolution screen users (600px high) will be unable to click
the add button with a dialog height of >550px. Keeping the height
to less than 500px leaves more room for large size themes.
2016-05-10 15:11:10 +01:00
9e92178357 [GTKUI] Fix Add Dialog tooltip text needing escaped
- An ampersand in torrent name would cause the tooltip to not be
displayed.
 - Also switched from cgi to xml.sax for escaping.
2016-05-10 15:11:03 +01:00
42c3580bf2 [Lint] [Plugins] Fix all pylint issues 2016-05-09 22:11:14 +01:00
9237c931b2 [Lint] Update pylint to only allow LF line ending 2016-05-09 20:10:51 +01:00
1a62e00066 [Lint] Add Plugins and scripts to tox pylint 2016-05-09 20:10:51 +01:00
803d94c8ac Remove old wiki_docgen script 2016-05-09 20:10:51 +01:00
ac2bbd68db [Tests] Remove flake8 complextity from travis run 2016-05-09 20:10:51 +01:00
8160cef2b3 [Lint] Enable pylint 'not-callable' 2016-05-09 20:10:51 +01:00
c7fd8f5116 [Lint] Fix redefining filename in script 2016-05-09 20:10:51 +01:00
618d2f9f58 [Lint] Enable pylint 'bad-continuation' and fix issues
There is some discrepency between pep8 and pylint for line
continuation (https://github.com/PyCQA/pylint/issues/747) but
with some minor layout changes both can pass and code looks fine,
if not better in places.
2016-05-09 20:10:51 +01:00
807d7a7aaf [Lint] Fix pylint msg for rencode 2016-05-09 19:52:47 +01:00
416fb5e1e3 [#2832] [UI] Skip blank lines in auth file 2016-05-09 16:39:10 +01:00
1fb9960168 [Base] Updated fix for missing trace with new twisted logger
Includes a commented out test to replicate the issue.
2016-05-09 13:57:54 +01:00
919e41f55e [Lint] [GTKUI] Apply isort fix for fixed placement imports
This solves the requirement for deluge imports to be placed after
installing the twisted reactor.
2016-05-09 09:44:30 +01:00
616523c732 [rencode] Update module to v1.0.4 2016-05-09 09:27:50 +01:00
bb0e699619 [UI] Add tests for ui_entry 2016-05-08 12:00:44 +01:00
d5294d5733 [Tests] Fix json_api AlreadyCancelled watchdog error 2016-05-08 10:36:44 +01:00
3769d99532 [GTKUI] Fix silly typo 2016-05-07 00:52:47 +01:00
c7b272561e [#2827] [GTKUI] Fix issue with loading GTKUI columns state
commit 1a2ff9b089 introduced a bug when loading GTKUI
column states due to changing ListViewColumnState class type.

Fixed by reverting ListViewColumnState to old style class
2016-05-06 22:59:51 +01:00
21789e0692 [#2813] [GTKUI] Fix connection manager showing daemon offline in Windows
The daemon status is not retrieved when showing the connection manager at
startup on Windows and shows it as offline.

This commit restores the removal of simulate call in commit 058b0e41d2
but applies it only to Windows OS as there were no problems in Ubuntu testing.

It would suggest then that the issue is isolated to PortableGtkReactor
but the exact cause of the problem is still unknown.
2016-05-06 22:30:20 +01:00
1a2ff9b089 [Lint] Fix and remove old-style-class from ignore warnings 2016-05-06 12:44:45 +01:00
3ec8dc6858 [Lint] Remove warnings from .pylintrc
Remove warnings from ignore list in .pylintrc:
* super-on-old-class
* pointless-except
* non-parent-init-called
2016-05-06 12:44:45 +01:00
9be1bd523a [Tests] Cancel watchdog deferreds on test completion 2016-05-06 12:44:45 +01:00
1f191c3ce1 [Base] Fix incorrect use of defer.fail in component 2016-05-06 12:44:45 +01:00
91ed621ec8 [UI] Changed ui command description and help 2016-05-06 12:44:45 +01:00
6adbd14bf8 [Base] Add custom log observer to handle twisted errors
For some reason errors are logged by twisted as
'Unhandled error in Deferred', but without a following
stacktrace. This can happen in a deferred callback that e.g.
raises an ImportError. Without an excplicit error handler for a
deferred to log such errors, finding the error can be very tricky.

Fix this by using a custom twisted.python.log.PythonLoggingObserver,
PythonLoggingObserver, that also logs the traceback in addition to
the error message.
2016-05-06 12:44:45 +01:00
5826446509 [Lint] Fix pylint signature-differs warning 2016-05-06 12:44:45 +01:00
84d2d20e13 [Web] Pylint fix for WebUtils 2016-04-29 23:23:23 +01:00
3ed4c8d636 [Lint] Add pylint to tox run 2016-04-29 23:23:18 +01:00
c15931e6f6 [Tests] Rename pylintrc and ignore .tox dir 2016-04-28 23:34:18 +01:00
7c20ed777d [Core] Save fastresume file on separate thread
To avoid blocking twisted main thread, defer file saving task to
separate thread with deferToThread.

Only queue resume data save task on shutdown
2016-04-28 23:14:29 +01:00
5d0359331b [#2821] [UI] Fix missing parameter in baseargs init 2016-04-28 22:42:59 +01:00
b255fc40af [WebUI] Remove openssl check as already a requirement 2016-04-28 11:10:09 +01:00
7b523af05b [#2819] [WebUI] Handle CannotListenError for second instance 2016-04-28 11:09:53 +01:00
bd65abd3b4 [UI] [Core] Combine common process options into baseargparser 2016-04-28 11:09:53 +01:00
b4dd90ba2b Cleanup code in web and deamon entries to match 2016-04-28 11:09:52 +01:00
c274d5114c [#2818] [WebUI] Fix AttributeError starting WebUI on windows 2016-04-28 11:09:52 +01:00
c821cdd9c7 [UI] Fix unable to use uppercase log level 2016-04-28 11:09:52 +01:00
69871506e1 Improve order of args and wording of '--help' text 2016-04-28 11:09:39 +01:00
a99e29642c [UI] Restore short arg for version '-V' (deprecate '-v') 2016-04-28 11:09:26 +01:00
acdc19df1d Add translation markup to '--help' options 2016-04-28 11:07:26 +01:00
2bad04848c [WebUI] Update gettext.js 2016-04-25 15:41:47 +01:00
bf3d6ae24b [WebUI] Add refresh dialog for language change 2016-04-25 15:35:52 +01:00
2984e2dc5d [WebUI] Fix Interface page not saving with OK button 2016-04-25 15:35:21 +01:00
3b23f69786 [WebUI] Use Apply button to change password 2016-04-25 13:26:59 +01:00
9fbc63e6fb [WebUI] Tidy Interface page layout 2016-04-25 13:26:59 +01:00
cb158ca866 [Core] Add missing warn_msg arg to set_dummy_trans() 2016-04-25 13:26:59 +01:00
857e2fd46e [#1959] [WebUI] Allow user selectable GUI language 2016-04-25 13:26:58 +01:00
74f2f45fc0 [WebUI] Fix to gen_web_gettext 2016-04-25 13:26:58 +01:00
b76d208212 [UI] Added missing languages to languages.py 2016-04-25 13:26:58 +01:00
dea43da4d2 [UI] [Daemon] Re-add --fork option 2016-04-24 22:39:16 +01:00
d32796eab0 [WebUI] Reword doctring and update gettext.js 2016-04-23 23:13:11 +01:00
1a79d7c255 [WebUI] Remove unneeded translation markup 2016-04-23 22:40:17 +01:00
1afea60c6f [UI] Indent subsequent lines in argparse help 2016-04-23 22:10:29 +01:00
a49b459a59 [UI] Remove old twisted DeprecationWarning code 2016-04-23 22:10:29 +01:00
9a051b6979 [UI] Enable translation of argparse help strings 2016-04-23 22:10:29 +01:00
64ac5fdf73 [#2677] [Web] With --base option set, serve locally on new base path
When specifying the --base option to work with reverse proxy
the WebUI is no longer accessible locally since it listens
on the server root, but serves on the path specified to work
for the reverse proxy.
Change this to also handle local requests to the base path
such that the WebUI will be available both for the reverse proxy
as well as locally on the interface/port which the twisted sever
listens on.
2016-04-22 23:04:19 +01:00
ec366c840c [Core] Fix unnecessary delay when starting components 2016-04-21 14:44:48 +02:00
edf616baca [#2805] Fix: Standalone mode not detecting local running daemon 2016-04-21 12:39:31 +02:00
58bc8b6ec7 [#2808] Fix: Deluge Log File Not Working as Intended 2016-04-20 14:09:50 +02:00
2dea6ab5a5 [Tests] Add pylint target to tox 2016-04-19 22:14:28 +02:00
c3247396f7 [Stats] Fix to tests and deleted .test.py 2016-04-19 19:00:07 +02:00
f3bfe177ce [GTKUI] Fix gtk warning on shutdown 2016-04-19 12:05:03 +01:00
7c1f39d10e [Core] Minor change to magnet info_hash fix 2016-04-19 10:50:05 +01:00
cd6669c024 [#2790] Ensure base32 magnet hash is uppercase 2016-04-19 10:44:55 +01:00
5c69b56cd5 [Core] Fix adding magnets failing 2016-04-19 10:43:59 +01:00
90d1bbbb31 [Lint] Fix pylint issues uncovered by recent changes 2016-04-18 22:54:39 +01:00
e370d7dbdd [Web] Fix error in WebApi in standalone mode
In GTKUI standalone mode, WebApi.enable would try to connect to
daemon if web.conf had the 'default_daemon' option set, causing
the client calls to break.
2016-04-18 21:03:03 +01:00
38e0bc1257 [Core] Handle error when adding torrents to session at startup 2016-04-18 19:15:27 +01:00
47f14845ca [GTKUI] Fix #2802: GTKUI classic mode shutdown procedure is broken
Fix by leaving shutdown procedure to gtkui.py:
* Daemon no longer calls component.shutdown() in GTKUI classic mode
* Mainwindow no longer calls reactor.stop but instead fires a
  'gtkui_close' signal.
* gtkui.py installs custom SIGINT handler to initiate shutdown before
  stopping reactor.
2016-04-18 16:05:46 +01:00
70d8b65f0a [WebUi] [Core] Fixes to plugin handling and WebUi plugin + tests
This should fix problems with errors occuring when failing to
enable plugins. Errors in plugin handling are handled better
and properly logged.

WebUI plugin in particular had issues when being enabled and disabled
multiple times because it was trying to create DelugeWeb component
each time it was enabled. If deluge-web is already listening on
the same port, enabling the WebUI plugin will fail, and the checkbox
will not be checked.

There are still some issues when enabling/disabling plugins by
clicking fast multiple times on the checkbox.
2016-04-18 15:49:30 +01:00
5ebe14e452 [Core] Remove old twisted DeprecationWarning code 2016-04-18 15:47:17 +01:00
36ecd5625a [UI] Cleanup logrotate option
Keeps a consistent naming for log options
2016-04-18 15:42:24 +01:00
f036c1a6c5 [Core] Fix stdout object when stopping daemon 2016-04-18 15:29:52 +01:00
64c67a07dd [WebUI] Fix #2798: WebUI plugin fails to start 2016-04-18 12:01:02 +01:00
092d496944 [WebUI] Cleanup donotdaemonize 2016-04-18 11:56:38 +01:00
5edb923904 [Base] Split main.py into ui/ui_entry.py and core/daemon_entry.py 2016-04-18 09:36:20 +02:00
6300f9154a [#1949] [UI] Allow setting max size for rotating log file 2016-04-18 09:36:13 +02:00
c90af1ce6c [UI] Add --profile to GTKUI and console and allow custom filename
Add --profile to commonoptions making the option now available for
daemon and all UIs. --profile option now prints to stdout unless an
optional filename is specified.
2016-04-18 00:54:45 +02:00
7b54a2a1ee [UI] Replace optparse with argparse for cmd arguments handling
optparse is deprecation and succeeded by argparse. See
https://www.python.org/dev/peps/pep-0389
2016-04-18 00:53:37 +02:00
aa82efd4f1 [#1974] [UI] Decouple UI selection from core.
Add entry points into setup for each of the UIs and then use this
information to determine which client UI to run.

This ensures that custom UIs may be written and run without
the need to modifify deluge source code.
2016-04-17 13:51:40 +02:00
6343f32d70 [#1973] [UI] Standardize child cmd option parsing.
Handle child args and -a args in a common way so that all children
accept the same input format. Modify UIs to pass through setup
arguments to the base class.

Instead of launching the UI directly launch the UI via the _UI
subclasses in the same way that the scripts launch the clients.
2016-04-17 13:36:15 +02:00
b86a021042 [#1972] [UI] Remove ui.UI class
The only use of the ui.UI class is a base for Web which never calls
__init__ and at the beginning when choosing which UI to launch,
however that doesn't need to be an object.
2016-04-17 13:36:15 +02:00
7af8a4cf14 [#1971] [UI] Unify common cmd options handling.
Add a CommonOptionParser which handles the standard set of options
for all UIs.
2016-04-17 13:36:15 +02:00
38a480ac14 [Core] Revert "Cache items in get_filter_tree"
This reverts commit affe47a11c
as it causes the All filter state field to be zero in classic mode.
2016-04-17 12:13:49 +01:00
3b84eb635c [GTKUI] Fix torrents not showing in classic mode
Commit 5d1aff157e implementing async_add_torrent cause torrents
not to show in classic mode.
2016-04-17 12:13:49 +01:00
6287a782a1 [Lint] Fix Redundant use of assertFalse with constant value True 2016-04-16 18:24:20 +01:00
e468436b0c [Lint] Update pylintrc and fixup code for newly introduced messages
* pylintrc is now compatible with pylint 1.6.
 * Add to ignore wrong-import-position and wrong-import-order as
   we use isort and pylint is raising too many incorrect messages.
2016-04-12 14:12:21 +01:00
10e1a2a593 [Core] Catch exception on call to lt.listen_on() 2016-04-11 11:56:34 +01:00
194d1291e1 [Core] Emit ConfigValueChangedEvent only in started state 2016-04-11 11:56:34 +01:00
085dc76e41 [Core] Set default torrent status message
Torrent status message could remain None is some cases
2016-04-11 11:56:34 +01:00
b0b9180943 [Core] Return Deferred from rename_files and rename_folder
core.rename_files and core.rename_folder now returns a Deferred
that callbacks when rename is finished.
2016-04-11 11:56:34 +01:00
af6f2b2107 [Core] Allow renaming torrent to empty string to remove the folder 2016-04-11 11:56:34 +01:00
887afa9389 [GTKUI] Fix bugs in files_tab and added tests
After renaming files/directories in GTKUI, the file list wasn't
properly updated, requiring to choose another torrent to get
a file list update.
2016-04-11 11:56:34 +01:00
d84ffa50c3 [GTKUI] Fix bug in gtkui/common.reparent_iter() 2016-04-11 11:56:34 +01:00
eda493e525 [GTKUI] Improve error handling in torrent details
If status is missing a key required for a widget a KeyError
was not always caught.
2016-04-11 11:56:34 +01:00
712b2715d4 [Tests] Fix to json tests 2016-04-11 01:36:49 +02:00
d8c4d8c1aa [Core] Fix to async_add_torrent commmit (5d1aff15) 2016-04-11 00:43:58 +02:00
5d1aff157e [Core] Implement async_add_torrent in torrentmanager 2016-04-10 11:46:22 +01:00
73220b5116 [Lint] Fix issues picked up by scrutinizer 2016-04-10 10:58:57 +01:00
d58960d723 [Tests] [Web] Make JSON independent of Web component
* Implement JSONTestCase in test_json_api.py
* Implement WebAPITestCase test case in test_web_api.py
2016-04-10 00:10:53 +02:00
bcc1db12e5 [Tests] Improved common.start_core
* Replace Popen with reactor.spawnProcess and read process
  output with twisted.internet.protocol.ProcessProtocol
* Implement support for running custom script code
* Now logs to stdout instead of stderr when not logging to file
2016-04-10 00:10:48 +02:00
533951afea [#2724] [Web] Forward exceptions in JSON-RPC back to caller
Exceptions raised by calls performed by a JSON request would
not always be handled properly resulting in no reply to be sent
leading to browser timeouts.

Fix this by including the raised error in the JSON data of a
regular (successful) HTTP response.
2016-04-09 22:19:48 +02:00
93023c5bfc [Core] Fix bug and add error testing to AuthManager 2016-04-09 22:19:48 +02:00
9319e07db5 [Webui] Show user in connection manager 2016-04-09 22:19:48 +02:00
9b18fb2b71 [Tests] Fix failing SessionProxy tests
For some reason, the time.sleep calls in the tests in
test_sessionproxy did not sleep for the expected amount
of time causing the results to differ from the expected.
Fixed by replacing time.time function with twisted's
task.clock.seconds and advancing the clock manually.

Also minor changes to test_client.py
2016-04-09 22:19:44 +02:00
cae8a18437 [Tests] Fixes to improve terminal output from unit tests
Add __str__ to WrappedException so that the stacktrace is printed when a
unit test raises a WrappedException.

Change the log output from error to warning in DelugeRPCProtocol.dispatch
when sending back a raised exception on an RPC request.
2016-04-08 16:35:09 +02:00
374989a2ad [Tests] Catch and print errors in setup/teardown 2016-04-08 16:35:09 +02:00
fc6672adda Fix #2789: Test for google tracker icon redirect is failing 2016-04-07 22:11:25 +01:00
0b17b52c9a [Tests] Consistent tox config layout 2016-04-07 19:11:32 +01:00
9d13234e23 [Tests] Fix for flake8 in tox
Force install flake8 in tox to avoid the system flake8 being used if
available.

Remove unneeded whitelist entries
2016-04-07 18:44:20 +01:00
815f67467a [Tests] Update ubuntu icon, skip google & openbt icon tests 2016-04-07 09:52:33 +01:00
bebc414136 [Core] Ensure magnet name passed to lt in string 2016-04-04 02:02:28 +02:00
d91e5d894f Add command-line option for the daemon to restrict some config keys to being read-only.
This only affects the core.set_config() RPC method which will drop items if the key
is listed as read-only.
2016-02-02 19:25:46 -08:00
d13fca251e [Core] Defer save state function to separate thread
With large amounts of torrents, saving the state file becomes
a performance bottleneck, mainly due to the required processing
in pickle.dump. When run in the main thread, the server will
hang and be unresponsive for a significant time.

Solve this issue by running the save state job in a separate thread.
2015-12-14 21:35:55 +00:00
e632ca4418 [WebUI] Use the short-form copyright text 2015-12-14 13:39:41 +00:00
a987c3ed39 [Core] Raise AttributeError on RPC call to invalid function
Also catch and log errors in rcpserver.sendData
2015-12-14 12:08:18 +00:00
382a99ad61 [GTKUI] Cleanup code duplication in Tabs 2015-12-12 22:10:53 +00:00
50bde1a607 [Core] Cleanup duplicate version callback code 2015-12-12 21:46:28 +00:00
080d137af8 [Tests] Move test_torrent_error code into test_torrent 2015-12-12 14:29:07 +00:00
02f6bfd578 [#1260] Handle redirection better with httpdownloader 2015-12-11 22:48:36 +00:00
77aa540dc3 fix isort 2015-12-11 22:34:19 +00:00
1793e36127 [Core] Fix use of parent class parameter 2015-12-11 22:30:10 +00:00
979ad972fe [#2767] [Packaging] Don't include .py files in OSX App 2015-12-11 18:51:08 +00:00
ee7e632b94 [#2783] [GTKUI] Case insensitive sort for name column 2015-12-11 18:01:54 +00:00
075542e4a5 [OSX] Fix starting deluged from connection manager 2015-12-11 12:11:38 +00:00
c1902e4396 [#2782] [WebUI] Fix HTTPS negotiating incorrect cipher 2015-12-11 11:44:37 +00:00
aaac697a98 [WebUI] Remove old code 2015-12-11 11:39:16 +00:00
ac9e11d732 [Core] Ensure valid torrent state value after init 2015-12-09 22:24:23 +00:00
f36ecc470b [Core] Fix move_storage exception handling 2015-12-09 19:00:06 +00:00
bd14657055 [GTKUI] Revert remove_column change from 550ddc010 2015-12-06 16:53:19 +00:00
6892a00b86 [GTKUI] Implement show ownership option in GTKUI 2015-12-04 19:05:59 +00:00
620a4eb409 [Base] Catch and log exceptions raised in component.update 2015-12-04 19:05:59 +00:00
ad7a1ec89f [Core] Add ClientDisconnectedEvent 2015-12-04 19:05:59 +00:00
ca1eaa5e15 [Core] Add TorrentTrackerStatusEvent 2015-12-04 19:05:14 +00:00
431357f623 [Core] [WebUI] Increase RSA key size and improve hashing
* Replace weak hashing functions, key sizes, and random number
      generation techniques with less weak versions to prevent
      crashes when running with the fips module loaded.
2015-12-04 19:04:13 +00:00
7eb037b3f4 [GTKUI] Fix import mistake 2015-12-04 17:12:28 +00:00
c619f05f94 [Label] Fix gtk warnings when removing menu 2015-11-30 23:01:19 +00:00
550ddc0109 [GTKUI] Fix treeview columns not saving 2015-11-30 22:49:42 +00:00
eaae568c7c [Core] Update tracker_host when setting new tracker status 2015-11-27 13:54:07 +00:00
d932c3ab99 [GTKUI] Fix installing plugin from non-ascii path 2015-11-27 13:41:44 +00:00
803a33efde [GTKUI] Ensure drag-n-drop urlparsed path is unicode 2015-11-26 15:07:16 +00:00
227863faf7 [#2777] Update MSVC SP1 check to latest release CLID 2015-11-23 23:30:50 +00:00
0e1582702a [#2485] [WebUI] Fix unconnected Options in context menu 2015-11-23 23:19:21 +00:00
42b9f22a81 [GTKUI] Fix for flake8 2015-11-22 14:03:14 +00:00
7e971550de Set tox version==2.1.1 due to bug in latest tox 2015-11-22 13:58:37 +00:00
6cf0ef080b [GTKUI] Fix broken sequential_download in options tab 2015-11-22 13:54:47 +00:00
c796acf791 [Core] Remove int casting as args should be int 2015-11-15 14:14:12 +00:00
27bf05f2fe [#2738] [Core] Fix illegal argument with torrent_handle.set_max_connections 2015-11-15 14:00:52 +00:00
c62c604418 [GTKUI] Fix unselect error with treeview selection returning None
In standalone mode treeview.get_selection returns None resulting
in an AttributeError for call to unselect_all.
2015-11-15 13:22:06 +00:00
fc9bc2976f [GTKUI] Fix open dialogs preventing gtk app closing 2015-11-15 12:47:07 +00:00
058b0e41d2 [GTKUI] Remove old twisted and gnome code
* Can't see any issue removing the twisted similate call and it
   seems to already be done by gtk2reactor so duplicated.
 * The gnome die handled never appears to be called and most signals
   are handled by twisted so remove this code as well.
2015-11-15 12:44:19 +00:00
0a3404fa55 [GTKUI] Move imports to top 2015-11-12 23:29:03 +00:00
ac09caefac [UI] Add Python and OS info to version output 2015-11-12 23:13:37 +00:00
ed6355fe86 [GTKUI] Refactor rpc stats code 2015-11-12 23:10:37 +00:00
471276716b [GTKUI] Refactor shutdown signal code 2015-11-12 22:03:27 +00:00
b754f9f908 [Core] Add line numbers to non-dev logging 2015-11-12 18:56:30 +00:00
cde17925fc [Lint] Autopep8 aggressive run
* Uses isinstance() instead of type()
 * Uses sorted() where possible
2015-11-04 11:54:15 +00:00
05ab06e3a5 [Console] Refactor build_file_list()
* Remove usage of sys.maxint and rename variable to make method more readable.
2015-11-04 11:06:35 +00:00
f1e70829af Fix linting mistakes
Missed renaming file to _file. This commit now uses better naming with
some minor refactoring.
2015-11-03 19:43:29 +00:00
f500d78487 [#2775] Update state and fastresume save methods
* Issue introducted in a previous commit meant the state file is never
   saved when starting with a fresh config.
2015-11-03 12:39:50 +00:00
ed48c4a0c5 [Core] Remove return true for timer from save_state
Obsolete code for old gobject timer
2015-11-03 11:36:26 +00:00
1ff189c63a [Lint] Standardise except code
* Using 'ex' variable name for exceptions.
2015-10-30 18:40:03 +00:00
2583e9d888 [Lint] Code cleanup for PyLint run by prospector tool
* Fix for pluginmanager multiple inheritance which in this case is using super incorrectly.
 * Explicitly disable pylint 'pointless-except' and 'super-on-old-class' that prospector
   tool somehow runs.
 * Make __all__ a tuple to supress pep257 warning.
 * Add a noqa for older versions of pyflakes.
2015-10-30 18:39:57 +00:00
d280fa9fbd [Lint] Cleanup helper scripts to pass PyLint 2015-10-30 18:39:52 +00:00
807fa609f9 [Lint] Cleanup code to pass PyLint Warning category
Selected Warning messages disabled in pylintrc:
  * unused-argument: Quite a large and disruptive change if enabled.
  * broad-except: Most required in-depth investigation to determine type.
  * fixme: Not important
  * protected-access: Complicated to fix
  * import-error: Too many false-positives
  * unidiomatic-typecheck: Should be fixed in the next round of checks.
  * unused-variable: Again large and disruptive changes.
  * global-statement: Most usage is required.
  * attribute-defined-outside-init: Should be fixed in next round of checks.
  * arguments-differ: Possible false-positives, needs revisited.
  * no-init, non-parent-init-called, super-init-not-called: False-positives?
  * signature-differs: False-positives?
2015-10-30 18:39:47 +00:00
ad3cba929e [Lint] Cleanup code to pass PyLint Convention category
Disabled Conventions messages:
   * missing-docstring: Not likely all methods/funcs will ever have docstrings.
   * invalid-name: Far too many too fix so will simply have to ensure submitted
     or altered code keeps to the convention.
   * old-style-class: Not a priority but would be nice to eventually fix this.
   * bad-continuation: Occasionally conflicts with pep8, not worth enabling if using
     pyflakes and pep8 as these will catch most continuation issues.
2015-10-30 18:39:42 +00:00
3288353be0 [Lint] Cleanup code to pass PyLint Error category
Disabled:

  * no-member:
  * not-callable:
  * no-name-in-module:
2015-10-30 18:39:36 +00:00
6eb46c935e [Lint] Add PyLint support and cleanup code with basic changes
* Include a pylintrc config file
 * This commit provides a basic error-only pylint config as a starting
   point with a view to adding more checks incrementally to keep the volume
   of changes low and the code able to pass pylint at each stage.
2015-10-30 18:38:56 +00:00
58388419fb [Core] Fix mistake in clear_forced_error_state 2015-10-30 18:27:38 +00:00
4ae43c5f2a [#1032] Error out torrent if data is missing on startup 2015-10-30 15:28:20 +00:00
74f5dc0a76 Add fastresume_rejected_alert 2015-10-30 15:21:07 +00:00
f4dce731e9 [Core] Supress state warnings with fresh config 2015-10-30 14:35:47 +00:00
aedb59f854 [Console] Use utf8_encoded for non-interactive mode 2015-10-29 12:22:03 +00:00
3a03bb8dd7 [GTKUI] Don't display percentage for Error'd torrents 2015-10-29 11:50:18 +00:00
e232cd812a [WebUI] Fix missing return from pep8 changes 2015-10-22 23:15:30 +01:00
ebc00f3d7c Fix config for isort 4.2 2015-10-21 01:17:08 +01:00
32bc20d8ce Fix pep8 across codebase
* Further whitespace fixes by autopep8
 * Using pep8 v1.6.2 (not currently used by pyflakes)
 * Update config for pep8 and flake8 in tox.ini
   * A separate pep8 entry for running autopep8. The ignores prevent
     blank lines being added after docstrings.
   * .tox and E133 are ignored in flake8 by default.
2015-10-21 00:06:27 +01:00
82ac1bdfe0 Use xml.sax instead of cgi for escaping 2015-10-18 18:41:58 +01:00
56f5ce6ee1 [Tests] Properly test for DeprecationWarning in test_log 2015-10-18 15:36:58 +01:00
4803600734 Remove translation markup in pluginmanagerbase 2015-10-18 15:36:58 +01:00
76cc3e79b9 [Tests] Update plugin metadata test 2015-10-18 15:36:58 +01:00
14e775cbcf Fix missing js semi-colons and refactor CSS 2015-10-07 12:50:56 +01:00
005db434f8 [#2769] [WebUI] Simplified torrent file upload UX
Previously, the process for uploading a file in the Web-UI required three
steps. Click 'File' to open the 'Add from File' window.  Click 'Browse' to
select the file. Finally, click 'Add' to upload the file. These steps have
been combined into one, making the process much easier. Now, clicking 'File'
opens the file browser directly. After a file is selected, it is uploaded
automatically.
2015-10-07 12:25:04 +01:00
d4535c6164 [GTKUI] Store width and height of 'Edit trackers' dialog in config 2015-10-07 12:17:25 +01:00
dd3aeb45ea [Core] Cleanup a few docstrings in TM 2015-10-02 19:30:04 +01:00
e4ec248eb6 [Core] Mapped files fix and torrentid correction 2015-10-02 19:30:04 +01:00
cb8e9d3018 [Core] Move add tracker merge into Torrent method 2015-10-02 19:30:04 +01:00
50200326a9 [Core] Split-up complex tm.load_state 2015-10-02 19:30:03 +01:00
40c1597c67 [Core] Split create part of save_state into create_state method 2015-10-02 19:30:03 +01:00
d34705860a [Core] Updates to writing and deleting torrentfile
* Reduces the complexity in tm.remove
2015-10-02 19:30:03 +01:00
fb95d0ef58 [Core] Fix queue_top typo in tm.add 2015-10-01 12:31:46 +01:00
0838202892 [#2703] [Core] Stop moving files if target files exist 2015-09-29 23:37:14 +01:00
7f2e06d4e2 Bump minimum version for libtorrent to 1.0.6 2015-09-29 23:30:18 +01:00
824067e238 [Core] Emit TorrentStateChangedEvent in update_state 2015-09-29 19:39:32 +01:00
084329f9f1 [#2729] [Blocklist] Fix plugin lockup with empty url 2015-09-28 12:45:39 +01:00
e1548cc974 [#1330] [Core] Fix pausing and resuming session
* The paused state of torrents is now correctly stored on shutdown if the session is paused.
 * Resume session refreshes all the torrents' state. This fixes only torrents that changed state being
   updated so queued torrents would be incorrectly displayed as paused.
2015-09-28 12:39:51 +01:00
8241b2ba3e [Core] Return all plugin status keys with empty list 2015-09-28 12:20:35 +01:00
e5e4ab4e05 [#2236] [Core] Fix filter keyerror removing plugin 2015-09-26 19:15:02 +01:00
a26101d6b9 [GTKUI] [OSX] Fix empty scrolling status (systray) menu
* Same issue as seen on Windows in #302
2015-09-26 00:03:52 +01:00
24b8baf8cc [Travis] Disable broken TODO test 2015-09-25 18:17:29 +01:00
5a6ca707e0 Fix isort and flake8 tests 2015-09-25 18:16:29 +01:00
c9d4cd2e14 [#2435] [GTKUI] Prevent user changing selection when editing tracker 2015-09-25 18:12:11 +01:00
f96f47e463 [#2705] [WebUI] Fix hostlist not being created 2015-09-25 14:02:12 +01:00
037063f24e [#2765] Add support for TLS SNI in httpdownloader 2015-09-25 14:02:07 +01:00
ca9d0abe4b [GTKUI] Fix connected issue in connection manager
* If host was not an ip address then it would not show as connected
2015-09-25 14:01:46 +01:00
a2a074fb4f osx file in wrong location 2015-09-23 00:32:54 +01:00
07fa36aa58 [GTKUI] Revert column type to uint64
* Tested fine on linux but on windows generates TypeError as this
   data is long in standalone mode.
2015-09-20 22:53:41 +01:00
774157f9b6 Fix scalable icon path 2015-09-20 21:36:46 +01:00
9df3f7b50e [Tests] Fix torrentview test 2015-09-20 19:27:03 +01:00
2c5025644c Fix data_files in setup.py 2015-09-20 18:39:20 +01:00
7b7e61485e [#2762] [GTKUI] Use correct column types for data 2015-09-20 15:58:10 +01:00
0363dddbcc [#2763] [GTKUI] Fix unhandled error with invalid magnet uri 2015-09-20 15:55:36 +01:00
356f224a25 [#2764] [Scheduler] Fix corrupt plugin prefs page on osx 2015-09-20 15:52:14 +01:00
fbf5d5287f [Packaging] Minor osx updates 2015-09-20 15:50:22 +01:00
1557bf8882 [#2754] [GTKUI] Fix Deluge isn't sorting torrents properly 2015-09-18 23:07:15 +01:00
8485fd591b [#2402] [Notification] Fix popup to show actual count of files finished 2015-09-18 23:00:00 +01:00
f834ff6ec5 [Packaging] Updates to osx scripts
* bundle_contents now appends 'Contents' without adding it twice.
 * Remove reference to non-existent gdk-pixbuf.loaders
 * Separate libtorrent in new module.
 * Update lib versions for bundle file.
2015-09-18 22:59:52 +01:00
7fccfa0651 [Packaging] Updates to the NSIS Installer script
* New message box popup if VC 2008 Redist package not detected.
 * Add Start Menu page to choose where/if to install items.
 * Add desktop shortcut install option to finish page.
 * Clean up spacing and use consistent 4 spaces to indent.
 * Exclude as many unneeded pygame libraries as possible.
2015-09-18 22:31:39 +01:00
ff6b52edc6 [Win32] Fix output exes in bbfreeze 2015-09-13 22:53:28 +01:00
7532d4d333 Fix icon paths in setup 2015-09-13 22:50:53 +01:00
40c0c8ef6a [#2325] [Packaging] Fix uninstaller deleting non-deluge files 2015-09-10 14:24:32 +01:00
3eefc81d9d [GTKUI] Select first entry in edit trackers dialog on first show. 2015-09-10 14:21:44 +01:00
da80f7cbda [Core] Only fsync the directory if GNU constant exists
* Doesn't exist on Windows.
2015-09-07 11:30:30 +01:00
e75e65b2c1 [GTKUI] Default Plugin statusbar items to the end on startup 2015-09-07 09:22:51 +01:00
0a10c8f3bf [Scheduler] Show current speed limit in statusbar
* Intercepts the updates of the statusbar and displays plugin values when in Yellow zone.
 * Core fix for resetting speed limits to core.conf values.
2015-09-07 09:21:07 +01:00
e6a6c8342f [GTKUI] Improve statusbar spacing and hide empty text labels 2015-09-07 00:59:50 +01:00
dd764a09a8 [GTKUI] Remove old and unneeded code
* Notifications now handled by plugin so remove gtkui code.
 * path_join is better done by os.path.join and replace.
2015-09-05 23:18:56 +01:00
caf35bcdf4 [Packaging] Include WebUI debug files for dev versions
* Webui will try to use debug files if deluge version contains 'dev'.
 * Include webui debug files in sdist.
 * Use exclude_package_data to remove debug files in release versions.
2015-09-04 19:43:32 +01:00
d898ba9333 [WebUI] Refactor server.get_scripts
* The directory list is now sorted so will always produce the same output.
 * Code is now shared with minify script, with some minor changes.
2015-09-04 15:02:19 +01:00
da1c07ff99 [Tests] Fixes to make tests pass
* Fix GTKUI column types mismatch (broken by 239e679)
* Updated deluge/tests/google.ico
* Remove empty line in torrentmanager.py
2015-09-02 17:14:13 +02:00
0a12d1507e [WebUI] Fix i18n of Connect button 2015-09-01 16:19:18 +01:00
1acd6e4c1c [Core] Refactor add method in tm 2015-09-01 11:28:39 +01:00
7414737cbf Tweaks to fastresume and state file saving
* Using move is not atomic on Windows so delete and rename instead.
 * Open the file with no buffering
2015-08-31 15:47:38 +01:00
bb16af3731 [GTKUI] Remove old builder file 2015-08-31 15:44:23 +01:00
239e679fee [GTKUI] Fix date columns to use int not float 2015-08-31 15:33:41 +01:00
3767a9fd27 [GTKUI] Fix issue in torrentview where columns shared datafunc 2015-08-31 15:29:59 +01:00
ff1f64d9bc [GTKUI] Fixes for tooltip deprecation warnings and signal handler warning. 2015-08-31 15:27:20 +01:00
aa5b7e7595 [#2701] [GTKUI] Fix: Move Download Folder cancel button doesn't work 2015-08-31 15:41:42 +02:00
3b82059bdb [#2731] [GTKUI] Fix potential AttributeError in is_on_active_workspace
* Without being able to replicate adding the forced updated is the likely fix for 'win'
being None but also add test in case it's not...
2015-08-31 11:30:23 +01:00
520fc23371 [Console] Remove unneeded whitespace in config output 2015-08-28 17:19:11 +01:00
62a144c730 [#2333] [Console] Fix 'set and then get' in config command
* The get method was returning old config information so use correct
 core get callback.
 * Remove redundant deferred in set method
2015-08-28 17:18:41 +01:00
f4e5fb446d Update MANIFEST and .gitattributes
* Modify `git archive` to include all source code so that creating a
release source tarball is now done with `setup.py sdist` which uses the
MANIFEST.in file to determine files to be included.
2015-08-27 22:51:43 +01:00
9e13f671ee [GTKUI] Make Add Dialog torrent name editable
* Allows copying of name and future feature of changing the torrent
display name.
2015-08-27 22:26:17 +01:00
438d49be85 [GTKUI] Fix sensitivity of indicator radio buttons 2015-08-27 22:24:55 +01:00
e883bbf10b [Core] Do not remove components from component registry on shutdown
By removing the components after they shut down, KeyErrors are raised when
trying to acccess the component. Unit tests now clear the component registry
on tear down.
2015-08-27 17:16:32 +01:00
19d1afdce0 [GTKUI] Show magnet info in Add and Queued dialogs
* Use tooltip to show orginal torrent path or magnet uri
2015-08-27 13:21:24 +01:00
8345237dcc [Packaging] bbfreeze updates
* No need for data_files to be installed on windows
2015-08-27 11:27:49 +01:00
50f6f2d3ec [Packaging] bbfreeze tweaks and comments
* Reduce output from bbfreeze and add debug option to enable again.
2015-08-26 17:25:33 +01:00
4b3684bc5d [Packaging] Fix typo in bbfreeze 2015-08-26 12:08:06 +01:00
df3a3c77eb Fix travis build version issue
* version.py script requires git tags but detached HEAD in travis clone
   requires manually creating the RELEASE-VERSION.
 * Also fix relative path issue building docs.
2015-08-26 00:27:32 +01:00
a3073c44e2 Update minify script to use closure 2015-08-25 16:18:02 +01:00
489550fd7a [WebUI] Lint js files 2015-08-25 15:43:55 +01:00
7d679eb480 Updates to helper scripts
* Python 3 compatible
 * Consistent quote symbol
2015-08-25 15:43:55 +01:00
23cbd581db Use just Taiwan in countries list 2015-08-25 11:03:20 +01:00
0466c7144c [#1389] Fix data_files installed in wrong location 2015-08-24 23:46:12 +01:00
d2a2631a70 Flake8 bbfreeze 2015-08-24 15:55:14 +01:00
1c3e14919f [Win32] Refactor bbreeze script
* In setup.py put web and deluged back into console_script as the gtkui hack in
bbfreeze is a windows only issue for popup cmd windows showing.
 * Altered the bbreeze script to find any deluge scripts in pythonpath.
 * Use setIcon now in bbfreeze and use icon from package.
 * Use version stamp from pywin32.
2015-08-24 15:35:25 +01:00
0ee8c7d70f [#2736] [Win32] Add version info to exe files 2015-08-24 14:56:53 +01:00
c55a601db9 Fix version issue with no git repo 2015-08-24 14:56:53 +01:00
71b5e0a296 [#2758] [win32] Include _cffi_backend module in bbfreeze 2015-08-24 14:56:53 +01:00
a4844f7b77 [win32] Update packaging scripts
* Update directory paths.
2015-08-24 14:56:53 +01:00
f4cb062380 [#2734] Add 256x256 to deluge.ico 2015-08-24 14:56:36 +01:00
81b3c69465 [Core] Fix set_trackers to use lt >= 0.16 tracker format 2015-08-22 15:39:22 +01:00
a36d1f6219 Exclude binary translation files in sdist 2015-08-22 15:25:56 +01:00
d7029dcfc6 [WebUI] Improve the minify script 2015-08-22 14:27:17 +01:00
24b71a400f [WebUI] Improve the gen_web_gettext script
* Create a 'minified' gettext.js by removing comments from file and simplifying js code.
 * Added creating the file to generate_pot.py, so it is not forgotten about.
2015-08-22 14:26:56 +01:00
7cc14baae3 Remove glade from package_data entry 2015-08-22 12:21:58 +01:00
9c01c87bbf Update .gitattributes
* Update path for ignoreing tests directory.
 * The deluge-all and ext-extensions source code should not be ignored.
 * Remove entries for non-existent build script and debug js files.
 * Ignore the new packaging directory.
2015-08-22 11:52:51 +01:00
941e4d7c1f Remove old glade script 2015-08-21 09:59:08 +01:00
d96633f3f7 Create a packaging directory 2015-08-21 09:58:30 +01:00
05acddcc64 Revert "Remove generated javascript gettext file from git"
This reverts commit 522815d266.
2015-08-20 22:29:13 +01:00
5c05d3d7ea [WebUI] Cleanup stray whitespace 2015-08-20 19:37:29 +01:00
7af7ecd82a [#2008] [WebUI] Fix translation marked text
* Remove labelSeparator and manually add ':' so text matches gtk translations.
 * Use consistent quotes around strings. This can affect gettext script picking up
   marked strings.
 * Added the equivalent deffered translation as gtkui for Filters and Progressbar
2015-08-20 19:37:22 +01:00
522815d266 Remove generated javascript gettext file from git 2015-08-20 18:56:17 +01:00
90db2b4c5c Minor updates to the translation scripts
* General cleanup of code.
 * Add commandline folder option to js gettext script.
 * Include webui render html files to pot template creation.
2015-08-20 18:51:09 +01:00
8dd918f2a4 [WebUI] Fix i18n issue in Connection Manager
The status strings were incorrectly marked for translation which when combined with
some translations using 'connected' and 'online' as the same word resulted in
users being unabe to connect to running daemon.

 * Removed translation markup from json_api but left as original capitalised word in
case other third-party scripts do comparison on these status strings.
 * Added translation markup prior to displaying ConnectionManager using template.
 * Reworded password prompt and added translation markup.
 * Update gettext.js
2015-08-20 13:59:02 +01:00
b1df44cf05 Update author name as per request 2015-08-17 23:03:48 +01:00
bb5f20e3de [Tests] Fixes in test_tracker_icons.py
* Removed publicbt.org test as server is down
* Replaced ubuntu.ico with ubuntu.png to make the test pass
2015-08-14 16:45:23 +01:00
9d662bf059 [Tests] Fix code for isort 4.0.0 2015-08-14 16:45:23 +01:00
379ba33bb9 [Tests] Fix Xvfb for GTKUI tests running on TravisCI 2015-08-14 16:45:23 +01:00
a39ebae0cd [#2295] [WebUI] Increased lifespan of display settings
Display settings for the WebUI are persisted using cookies created by
Ex.state.CookieProvider. When no expiration date is provided, a default
value of (now + 7 days) is used. This causes display settings to be
lost frequently.

This fix adds an 'expires' parameter with a value of (now + 10 years).
This change does not affect the lifespan of the session cookie, which
is created by a separate system.
2015-08-14 15:58:18 +01:00
cbb60e3c3a Update man pages 2015-08-14 13:27:06 +01:00
6020809462 Minor cleanup of minify js script 2015-08-14 00:17:48 +01:00
4196912966 [#2730] Fix Deluge dev versions not starting
Using latest versions of setuptools (>11.3) resulted in deluge version strings
that contain 'dev' to produce a ValueError.
2015-08-13 23:04:01 +01:00
8c4154bc1a Fix the output of minify js script
The order of the js files matters when minifying.

 * Use the '.order' files to put specified files top of the file list.
 * Sub-directory files inserted in list before root directory files.
 * Sort everything else alphabetically for consistant ordering.
2015-08-13 23:04:01 +01:00
a391bbd67b Workaround for js files generating warnings with generate_pot script
With xgettext set to python it will parse the comments in javascript files, so
single apostrophes or quotes are flagged as 'warning: untermined string'.

This change just rewrites js comments to not use apostrophes.
2015-08-13 23:04:00 +01:00
576df1f6e3 [GTKUI] Improve About dialog copyright format for translators 2015-08-13 23:03:22 +01:00
4ba98c997a Remove stray tab in label plugin text 2015-08-09 12:19:24 +01:00
9726481fb4 [#2733] [Core] Fix on_alert_performance - UnicodeDecodeError 2015-02-23 12:43:27 +00:00
2c7bbc6ade Fix for Twisted 15.0 URI class rename 2015-02-23 12:35:45 +00:00
faf3f96322 [#2250] [WebUI] [Console] Use new core.remove_torrents method 2014-12-03 17:32:47 +00:00
08363f28dd [#2250] [Core] [GTKUI] Added method remove_torrents to core
Removing multiple torrents with remove_torrents will be faster
than calling remove_torrent for each torrent, as the state file
will be written only once, after all the torrents are deleted.
2014-12-03 16:46:24 +00:00
2aaae7c6a1 [#2406] [Core] [GTKUI] Implement core.add_torrent_files
* Speeds up adding multiple torrents
2014-12-03 15:36:06 +00:00
41f08e4e29 [#2702] [GTKUI] Fix potential markup warning in Details Tab
* Comments with HTML markup cause a GTK markup warning.
 * Use cgi function to escape '&', '<' and '>' to prevent pango markup error.
2014-12-01 13:50:39 +00:00
0ea6ad0669 [GTKUI] files_tab: sort by name by default 2014-12-01 10:59:09 +00:00
cdeb3c211b [#2670] [GTKUI] optimize file trees according to pygtk tips
Use a context manager to wrap the common steps:

1) disconnect the treestore from the listview
2) disable treestore sorting
3) add rows (different in add dialog vs files tab)
4) enable treestore sorting
5) connect model to listview
2014-12-01 10:58:49 +00:00
ea028c7531 [#2670] [GTKUI] addtorrentdialog: fix O(N^2) algorithm in add_files by recalculating folder state once instead of per-child 2014-12-01 10:58:42 +00:00
8d3ba87c63 Temporarily disable testcoverage in travis build 2014-12-01 10:52:16 +00:00
448261394f [Tests] Changes to tests and test configs of Travis/tox
* Added pip chaching
* Added disable_new_release_check to tracker icons tests
* Fixed test_torrentview
* Require minimum tox version 1.8
* Fixed GTKUI tests and testcoverage by using xvfb on travis
* Separated the apt dependencies for commands requiering GTKUI deps
2014-12-01 10:52:16 +00:00
8334bf9477 [Tests] Various fixes for unit tests and tox
* Added custom trial reporter for TODO with test example in test_torrentmanager.py
* Set Stats plugin tests as todo
* Disable new_release_check when running unit tests
* Added pytest.mark.slow to test_core.test_test_listen_port
* Get rid of unit test warnings (Caused by bad names in test classes)
* Removed warnings.filterwarnings in test files.
* Added separate tox target for generating test coverage HTML report.
2014-12-01 10:52:16 +00:00
178c417fb0 [Core] [Tests] Changes to component.shutdown and unit tests
* component registry shutdown() now cleans up the component list
  this ensures that no old components are left when running unit
  tests.

* Added class BaseTestCase that all tests that create components
  should inherit from. It verifies the compoent list before and
  after the tests are run.
2014-12-01 10:52:08 +00:00
a9e7aec5b6 [#2698] [GTKUI] Fix corrupted column indexes when using multiple col_types
* Ensures that removing multiple items from liststore_columns list does not affect the index.
2014-11-29 15:40:07 +01:00
a68d836beb [#2256] [GTKUI] Indexes aren't updated properly when removing columns 2014-11-29 15:40:07 +01:00
1e75b7bd12 [#2676] Add pilow and appindicator to DEPENDS 2014-11-25 19:04:58 +00:00
dd8d2c8557 [GTKUI] [Win32] Fix 'access is denied' with magnet association
* Issue occurs with user without administrator privileges
 * See github pull request #19 for details
2014-11-25 18:51:34 +00:00
aede6f9ce5 [WebUI] Add missing column entries to Torrent Record 2014-11-21 00:12:06 +00:00
376a92f554 [WebUI] Remove unneeded grid key and fix torrent record 2014-11-21 00:06:03 +00:00
cb37198a9d [WebUI] Modify SSL Context to allow >=TLSv1 protocol
* The TLSv1_METHOD is a fixed protocol version so this change will allow higher versions to be used where possible.
2014-11-20 15:19:18 +00:00
3689eb508e [#2555] [Core] Disable use of SSLv3 for DelugeRPC 2014-11-20 15:19:09 +00:00
af95fb0828 [Core] Remove old windows cache fix
This removes a fix for #1869 (using 0.15 lt) that is now fixed properly in 0.16 lt.
2014-11-18 10:13:55 +00:00
bdca70b330 [WebUI] Security update for POODLE vulnerability
WebUI with HTTPS enabled is vulnerable to POODLE (CVE­-2014­-3566), so switch from
SSLv3 to TLSv1.
2014-10-15 19:06:35 +01:00
bd2abb0127 Update copyright year in About dialog 2014-10-04 18:37:01 +01:00
d805f99534 [#2335] [GTKUI] Fix startup failing with 'cannot acquire lock'
This issue was caused by an unclean shutdown of Deluge, usually on system shutdown, and upon rebooting
the PID stored in the lockfile is in used by another process thus the lockfile is never removed. It
affects users with Deluge set in startup applications as the PIDs are more likely to be reused.

 * Lockfile is removed if Deluge is restarted in IPC.
 * Renamed the old_tempfile variable to make it clearer as to it's role.
2014-10-02 17:34:49 +01:00
11c6e387d5 [#2510] Fix config type checking 2014-09-28 10:33:21 +01:00
3b950094af [#2510] [Tests] Add config test for overwriting None value 2014-09-28 10:33:10 +01:00
dd8e37a6ce Workaround for the isort Travis issue by using order-by-type
See isort issue: https://github.com/timothycrosley/isort/issues/185
2014-09-26 13:06:11 +01:00
62a9e3921d Revert to isort diff output 2014-09-25 23:59:49 +01:00
bdf39c1e89 Switch from PIL to pillow for tox and rtd 2014-09-25 23:57:52 +01:00
784ecb94ea Fixes for flake8 and rtd 2014-09-25 23:10:22 +01:00
23ab85e253 Refactor/cleanup of tox.ini
* Removed unused mock dep
 * Modified flake8 to run entire codebase (fixed exclude)
 * Change isort to shows files that need attention (no diff)
 * Uniform layout for tox.ini
 * Change flake8 complexity to 15
2014-09-25 22:12:09 +01:00
bb9702910b Update docs tests 2014-09-25 22:12:00 +01:00
284b86ebb6 Fix running docs build from setup 2014-09-25 21:05:57 +01:00
3d4ea71dcf Fix isort config 2014-09-25 16:12:43 +01:00
b66f313c2d minor code cleanup 2014-09-25 15:56:05 +01:00
7e86b41f92 Add isort thirdparty config for Travis 2014-09-25 15:55:32 +01:00
fedca3167d [Docs] More fixes for testing with tox 2014-09-25 15:20:15 +01:00
6a5982f3ce Small fixes for Travis and isort 2014-09-25 14:25:03 +01:00
4472c37884 Disable plugins test due to Travis issue
An unknown issue with running the Stats plugins test on travis means it
never passes.
2014-09-25 14:13:18 +01:00
66f2739be7 Added .travis.yml (for travis-ci) and tox.ini files
Targets:

* Runs the unit-tests for python 2.7
* Tests unit-test coverage
* Try to build docs
* Code style checks:
  * flake8
  * isort

Codes changes:
* Fixed tests for httpdownloader (using tmp dir)
* Implemented a couple of tests for Stats plugin but they fail to run on travis

Issues:
* Can't get py26 to work because of installing libtorrent through apt and
  the option system_site_packages fails for 2.6.
2014-09-25 14:11:51 +01:00
8dc9a0773c Fixes for building docs 2014-09-25 13:43:43 +01:00
72493e6af3 [Extractor] Add Finding Win 7z Path via Registry 2014-09-24 22:41:18 +01:00
a26c5eb56e Merge branch 'Feature/win32_associate_magnet' into develop 2014-09-23 20:23:00 -04:00
14ee13bdd4 [GTKUI] Fix magnet association button on Windows 2014-09-23 20:22:24 -04:00
3b22dcadc9 Correction for Flake8 func rename in log.py
Broke the retrieval of logging lines.
2014-09-23 10:08:50 +01:00
b19845bf93 More fixes for previously overzealous changes to setup.py 2014-09-23 09:09:49 +01:00
f2d81ff542 Update headers and isort imports 2014-09-23 08:39:29 +01:00
966f10bcb7 Fix RTD badge in readme 2014-09-22 19:18:34 -04:00
8ba0e7ce0e [Extractor] Fix absolute/relative import 2014-09-22 18:35:13 +01:00
d9522261b1 [GTKUI] Allow the Tabs area to be resized smaller
* Tabs can now be scrolled if too many for height.
 * Change window_pane_position default to 235, based on default 480 window height.
 * Set the vpaned size before show call.
 * Reduce top padding and set bottom padding to 2, to account for decender letters.
2014-09-22 15:09:21 +01:00
83262afda1 Flake8 codebase
Fixes error E265 and E714 that have been added to newest version of pep8
2014-09-22 12:46:18 +01:00
142e96b246 Autopep8 E265 2014-09-22 12:46:17 +01:00
2f68092740 Flake8 add global __request__ to config 2014-09-22 12:46:17 +01:00
c115738535 Rename README to README.rst
Update rst formatting in readme
Add RTD and Travis badges to README
2014-09-21 14:28:59 -04:00
9b2283972c Add twisted and pyopenssl to rtd_requirements.txt 2014-09-20 21:48:46 +01:00
5537d59fb8 Fix typo in rtd_requirements.txt 2014-09-20 21:32:34 +01:00
18bcf2d588 Add requirements file for readthedocs sphinx extension 2014-09-20 21:24:11 +01:00
ebcf14df06 [Console] Fix import typos 2014-09-20 20:47:13 +01:00
9a801b4b93 Fix docstring errors raised by spinx docs build 2014-09-20 20:46:03 +01:00
6b7df9ca08 Add version fallback for sphinx build with readthedocs 2014-09-20 20:44:55 +01:00
5cc5d2e811 Fix overzealous changes to setup.py 2014-09-20 18:55:21 +01:00
a4edb0080b Fix for building sphinx docs 2014-09-20 18:43:09 +01:00
fc9017cfb1 Update with new license header 2014-09-19 19:21:42 +01:00
30a0f3c9ed Flake8 pass of entire codebase
* Use the inline '# NOQA' to supress N802 lower-case warnings
2014-09-19 19:10:14 +01:00
d0b8e17873 Add workarounds for isort
* Add workaround for unicodedata issue
 * Change line length to 120
 * Skip gtkui.py to stop it moving local imports above the install reactor line
2014-09-19 15:58:43 +01:00
b8ab6e4083 [Tests] Replace module import with sys.modules 2014-09-19 13:40:39 +01:00
09c6e0cb5c Remove gtkui specific func from path_chooser_common 2014-09-19 13:39:50 +01:00
6e0e01225e [Plugins] Fix relative imports to use dot notation 2014-09-19 13:39:50 +01:00
268c8d608c [WebUI] Add reduce import for Py3 compat 2014-09-19 13:39:50 +01:00
fbcddff6ea [WebUI] Replace func_globals with __globals__ for Py3 compat 2014-09-19 13:39:50 +01:00
08b61eb50b Change imports to use absolute paths 2014-09-19 13:39:41 +01:00
45ef6ac56d [Console] Flake8 all files 2014-09-18 17:23:52 +01:00
a68c3140af [GTKUI] Fixed incorrect column for searching in treeview 2014-09-10 12:50:45 +01:00
91943ba7e3 [WebUI] Small fixes to text labels 2014-09-04 10:41:48 +01:00
403fdb31a1 [WebUI] Fix isort error in auth 2014-09-04 10:40:57 +01:00
20b05ae595 Remove unneeded ez_setup and 'isort' imports 2014-09-04 00:37:57 +01:00
5167e93d12 Flake8 core and common files
* Added N802 to flake8 ignore as certain inherited funcs cannot be changed
   to lowercase and this unresolved warning hides other errors/warnings.
 * Include new header
2014-09-03 23:48:34 +01:00
5d88504c34 [GTKUI] Fix isort error in gtkui 2014-09-03 23:30:14 +01:00
3315768b27 [GTKUI] All files Flake8'd and use new header 2014-09-03 23:30:14 +01:00
b5dcfc6f9e Sort/prettify imports with isort 2014-09-03 18:27:32 +01:00
1ca08ccf95 [Python-Modernize] Replace im_self with __self__ 2014-09-03 17:22:39 +01:00
fc7a136c70 [Python-Modernize] lib2to3.fixes.fix_numliterals 2014-09-03 17:22:39 +01:00
e24e5916e0 [Console] Replace set with list 2014-09-03 17:22:39 +01:00
4afd2513fa [Console] Fix typo in info 2014-09-03 17:22:39 +01:00
7cdedbea1f [Python-Modernize] libmodernize.fixes.fix_raise 2014-09-03 17:22:39 +01:00
38bc5d07f0 [Python-Modernize] lib2to3.fixes.fix_ws_comma
* Fixer that changes 'a ,b' into 'a, b'.
2014-09-03 17:22:38 +01:00
3a53f4002a [Python-Modernize] libmodernize.fixes.fix_print
* Replaces print with print()
2014-09-03 17:22:38 +01:00
1e6c811768 [Python-Modernize] lib2to3.fixes.fix_except
* Use 'ex' instead of 'e' to conform with pylint
 * Minimal Flake8 on some files
2014-09-03 17:22:38 +01:00
95f859673e [Python-Modernize] lib2to3.fixes.fix_has_key 2014-09-03 17:22:38 +01:00
682acc11ec [Tests] Update ubuntu icon 2014-09-03 17:22:21 +01:00
ec8d48f4fd [GTKUI] Tweaking layout of Status and Details Tabs 2014-09-02 12:48:02 +01:00
4f3fcac2bf [GTKUI] Add padding to count in sidebar 2014-09-01 22:24:19 +01:00
430f9c01d7 [#2472] [GTKUI] [WebUI] Add anonymous_mode UI prefs 2014-09-01 22:08:48 +01:00
184d6be98d [#2472] Add support for anonymous_mode 2014-09-01 21:57:09 +01:00
2a50159978 [#2497] [GTKUI] Fix the queue 'Clear' button not properly clearing. 2014-09-01 21:31:17 +01:00
5a6f202cf1 [GTKUI] Rework the sidebar layout
* Changed variable names to be less confusing.
 * Flake8'd.
 * Move the 'count' to a separate render cell.
 * Reduced size of expander icon to make it less intrusive.
 * Enabled ellipsis on labels so count is still visible.
 * Used pango markup on cell labels and count.
 * No longer set a fixed colour to fix #1193.
2014-09-01 19:25:44 +01:00
e97140cbde [GTKUI] Reduce status tab border to 1px 2014-09-01 19:06:02 +01:00
ecb4f0e9da [#2496] [GTKUI] Fix updating core_config before setting default options
* Remove duplicate entry in init.
 * Call update if empty config and prevent potential loop in update method.
 * Ensure that the queue Add button is sensitive, even when automatically adding.
2014-08-31 14:50:55 +01:00
57b594041a [#2493] [GTKUI] Fix TypeError if active workspace is None 2014-08-25 16:30:19 +01:00
2df2f882e0 Use list comprehension in get_file_progress
Should be slightly quicker with large numbers of files.

Also moved socket import to the top as it will always be imported.
2014-08-24 11:01:37 +01:00
da254a80cf [GTKUI] Add associate magnet reg in Windows 2014-08-24 10:51:47 +01:00
4ad45b2d4a No need to use get_status in Torrent class 2014-08-23 22:22:09 +01:00
a9293285a0 Flake8 rencode
Fixes a function declared twice and a few indentation issues
2014-08-23 22:22:09 +01:00
b4b58380b6 Refactor Torrent _get_pieces_info method
Code is now easier to read and should be a bit faster
2014-08-23 22:22:09 +01:00
48f79dbfca [GTKUI] Move 'Add Dialog' prefs to Download tab
Also includes more tweaks to layout for consistency and creating space.
2014-08-22 22:02:09 +01:00
5c82c144cf [GTKUI] Convert the appindicator option into a radio button 2014-08-22 22:01:23 +01:00
849101950f [GTKUI] Tidyup Preferences Dialog
* Remove unnessary page headers to save space
 * Reordered pages to be lightly grouped
 * Other changes to utilise space better
 * Fixed the plugin info panel collapsing on startup
2014-08-22 17:41:19 +01:00
210acf68c1 [GTKUI] Change tabs from top to left side 2014-08-22 17:41:13 +01:00
6bbb9832e9 [GTKUI] Remove icons from Tabs 2014-08-22 17:40:57 +01:00
1bc92ed3e3 [GTKUI] Move Piece colour chooser below checkbox 2014-08-22 17:40:25 +01:00
6496383e82 [GTKUI] Reorganise layout of tab items and add Tracker tab
* Changed layout of Status, Details and Options tabs.
 * Moved the Tracker translations to ui.common.
 * Created a new Trackers tab.
 * Added State to progressbar.
 * Translate State in piecesbar.
2014-08-22 14:43:37 +01:00
14776d86f5 Remove duplicate i2p_proxy entry 2014-08-21 13:41:33 +01:00
1de0c30bb0 Disable SSL listen port 2014-08-21 13:40:07 +01:00
b296803e01 Use int function to cast proxy type 2014-08-20 16:58:25 +01:00
68b893fa02 Log errors for invalid interface values 2014-08-20 16:55:07 +01:00
05792809b5 [GTKUI] Strip whitespace in the interface entry box 2014-08-20 16:34:43 +01:00
068cce353a Code cleanup for core files 2014-08-20 15:10:59 +01:00
82f2fc67c2 [Notifications] Small layout fixes for web page and version bump 2014-08-19 16:28:21 +01:00
b77c4682d1 [#1310] [Notifications] Add webui prefs page 2014-08-19 16:12:56 +01:00
42b3edff64 Fixed bug in Blocklist WebUI pref page 2014-08-19 16:12:55 +01:00
83c0f8a16e [WebUI] Fix tracker_host mistake 2014-08-19 15:48:25 +01:00
af9fa15636 Replace use of status key 'tracker' with 'tracker_host' in UIs
* Status key tracker can be empty so use tracker_host instead, also tracker_host
   is nicely formatted.
 * Remove unneeded tracker_host string from tracker_status.
2014-08-19 15:39:12 +01:00
061590665e Remove obsolete set_state method 2014-08-19 15:39:12 +01:00
49ed3db352 Replace try statement for LT_TORRENT_STATE_MAP lookup 2014-08-19 14:31:00 +01:00
32330f99fc Flake8 core files 2014-08-19 14:22:19 +01:00
a2c3fb3d5e [WebUI] Flake8 files
* Does not pass cleanly due to camelcase function names and __request__ global.
2014-08-19 13:01:18 +01:00
069d820d39 [WebUI] Cleanup of css files
* Consistent indent and line endings
 * Remove unused redo icon
 * Minify extensions css
2014-08-19 12:52:03 +01:00
1700b75cfe Fix firing of Finished event when moving 2014-08-16 22:49:13 +01:00
64d06f5650 Fix showing wrong state for finished torrent 2014-08-13 22:45:53 +01:00
97533145a7 Revert "Fix strange resume_data bug causing fastresume not save on shutdown"
This reverts commit 2449f5b99e.
2014-08-13 21:32:46 +01:00
8c6758720d Replace pause_all with pause_session
* Replace pause_all and resume_all with pause_session and resume_session
 * Pausing all the torrents individually loses the original paused status
   so use the libtorrent session pause instead.
 * Added a SessionPausedEvent to the method.
2014-08-12 18:21:12 +01:00
d0718df82b Refactor torrent.update_state 2014-08-12 00:31:00 +01:00
f81cc81e20 Add --sort option to deluge-console's "info" command. 2014-08-10 17:45:14 +01:00
79023eb5c6 Add seeding_time, active_time and tracker_status to deluge-console's "info" command. 2014-08-10 17:37:41 +01:00
35f7526c2a Fix spelling error in deluge-console output. 2014-08-10 17:32:16 +01:00
f8eede78ca [AutoAdd] Add Skip Hash Check option 2014-08-10 14:27:47 +01:00
a38186857d Flake8 Autoadd 2014-08-10 14:27:47 +01:00
66ae8bdd5c [#1294] [GTKUI] [WebUI] Add Skip File Hash Check
* Altered the core code so that seed_mode is now a torrent option.
 * Made some minor improvments to the Add Dialog
2014-08-10 14:27:47 +01:00
d108091511 [GTKUI] Remove dialog focus should be on Cancel button 2014-08-10 12:08:47 +01:00
29a05978ec Flake8 addtorrentdialog 2014-08-09 23:40:59 +01:00
7e4d50b406 Fix for Indicator icon label issue 2014-08-09 22:16:36 +01:00
90419a4f2d [#2450] [WebUI] Fix empty Peers tab
Also fix missing flags and tracker icons.
2014-08-09 21:54:47 +01:00
8cc96d9b89 [GTKUI] Cleanup Standalone/Thin client dialogs 2014-08-09 21:54:47 +01:00
dc7a4df39a More changes for consistent naming of download location 2014-08-09 15:04:14 +01:00
a1bc11ec09 Consistent naming of torrent download location to Download Folder
* Replaced the deprecated use of torrent status save_path with download_location.
 * UIs now use 'Download Folder', replaces 'Save Path', 'Download Path', '... Storage', etc.
2014-08-09 00:39:29 +01:00
711962da84 Flake8 files_tab 2014-08-09 00:39:29 +01:00
c5f7eeaacb [#2098] Add function to highlight the torrent folder/file
* Will show/highlight a file path in system file manager. *nix uses dbus with an xdg_open fallback.
 * [GTKUI] Open Folder still opens the download location but now shows the torrent data file/folder
 * [GTKUI] Files_tab now has a second menu item 'Show' to show a file's location
 * The open_file and show_file functions now use timestamps on *nix so that windows open in front, this fixes recent desktop changes that prevent windows randomly stealing focus.
 * Removed utf8 decode for Windows. All paths should be unicode
string, any resulting errors should be traced to source and corrected.
2014-08-09 00:39:28 +01:00
670cd21685 [GTKUI] Suppress unimportant gnome warnings 2014-08-09 00:15:47 +01:00
9ba07d3883 [GTKUI] Fix showing the open_folder menuitem
The menuitems would disappear and not reappear if switching between localhost
and remote daemons.
2014-08-09 00:07:08 +01:00
713e264061 Fix for moving progress with no data downloaded 2014-08-08 19:26:37 +01:00
21f18a75bb codepaint plugins js files 2014-08-04 23:48:35 +01:00
3e610ec5ba [#2470] [Console] Fix console parsing args
This negates the need for quoting a single command with an arg e.g.
    deluge-console del --remove_data torrrent_file

Multiple commands separated by semi-colon still require quoting.
2014-08-04 22:27:30 +01:00
e8288eec6a [WebUI] Update from config upon showing plugin page 2014-08-04 22:26:21 +01:00
936ae3b171 [Blocklist] Flake8 and bump version 2014-08-04 18:44:53 +01:00
834d30f85f [#2478] [Blocklist] Add WebUI plugin page 2014-08-04 18:44:52 +01:00
231c17f6a9 Clean up remove dialog handling a bit more 2014-08-03 21:27:46 -04:00
acf2ad2f0c Fix the remove with data checkbox not working in gtkui 2014-08-03 21:06:12 -04:00
59f82f204f Oops, accidentally renamed a variable 2014-07-31 23:20:38 -04:00
02cfc40e94 Fix move completed sometimes not moving finished torrents 2014-07-31 23:16:30 -04:00
b9338a639e Fix issue restoring torrent state 2014-07-31 22:09:23 -04:00
205444f670 [WebUI] Fix json import 2014-07-22 23:34:19 +01:00
480347296b Fix a mistake in tm finished alert 2014-07-19 20:08:47 +01:00
7b53486821 Use stdlib json
Some flake8 changes and DEPENDS tidyup
2014-07-19 11:32:17 +01:00
49c2be40ab Use flush_disk_cache as a shutdown marker
This is a hacky fix for waiting_on_resume_data not being empty on
shutdown. This issue needs further investigating.
2014-07-18 13:25:50 +01:00
a65603e10c [#1032] Keep track of torrent errors over restarts
* Add error_statusmsg to TorrentState
 * Adds a new set_error_statusmsg() method to force torrent error state.
 * Any torrent in error state will remain in that state on restart with
   additional message in status.
 * Any new libtorrent errors will override manually set ones.
2014-07-18 13:08:47 +01:00
7393d31208 Refactor TorrentState to build the class attributes 2014-07-18 12:46:56 +01:00
c82164c522 [#2161] Save magnet torrent_info to 'copy of' location
When magnet metadata is received a torrent file will also be written
to 'copy of' location if requested.
Modified the code for saving torrent file to state in Torrent class for
use by TorrentManager.
2014-07-18 00:35:26 +01:00
e30e2ef2c3 Fix mistake in convert_lt_files 2014-07-18 00:03:11 +01:00
9347a78482 Refactor and tidyup code in torrent.py 2014-07-17 21:55:57 +01:00
25c7e40574 [#2347] Add orig_files to core 2014-07-17 21:55:51 +01:00
739d8f329a [#1859] [GTKUI] Improve layout of Remove torrent dialog
* Using Shift+Del will now pre-select removing files.
 * Will now show the name of the individual torrent being removed or the
   total count if multiple torrents selected.
2014-07-17 00:12:23 +01:00
2449f5b99e Fix strange resume_data bug causing fastresume not save on shutdown 2014-07-17 00:11:15 +01:00
b3e323462c Change logging of 'creating backup' to debug level 2014-07-16 21:25:05 +01:00
bf9bd267fd Fix typo in loading peers_tab state 2014-07-16 18:21:30 +01:00
8685c7a604 Show actual error in status from storage_moved_failed_alert 2014-07-16 18:07:12 +01:00
62cca045be [#637] Add a Moving storage state along with progress
Uses attr Torrent.moving_storage for now but can be replaced with
future lt1.0 status field.
Refactored the code to use the common.TORRENT_STATE list.
Added a translating dict to ui.common to aid translation of state text.
2014-07-16 17:43:12 +01:00
bd119bccf4 Fix move storage dialog not closing 2014-07-16 17:43:11 +01:00
8920db694c Use true division 'from __future__ import division' 2014-07-16 17:43:11 +01:00
d51ad7718c Update TorrentManager docstrings and remove old load_torrent method
Passes flake8 and mostly passes pylint
2014-07-15 15:26:35 +01:00
e66c854be5 [#2238] [Scheduler] Fix undefined this.scheduleCells 2014-07-13 23:07:29 +01:00
cd8bef964a [Extractor] Tidy plugin code and add webui page 2014-07-13 14:12:31 +01:00
4d5e01abef [#1290] [Execute] Add TorrentRemoved event 2014-07-13 14:12:31 +01:00
7b44980912 [#1126] [#2322] Emit FinishedEvent after moving storage complete
Also changed the Execute and Extractor plugins to process the updated
FinishedEvent functionality.
2014-07-13 14:11:47 +01:00
7c22135bb4 [Execute] Tabs to spaces 2014-07-12 21:38:28 +01:00
21691c5cc1 [Extractor] Replace module which with twisted.python.procutils.which 2014-07-11 18:59:52 +01:00
533bdd398a [WebUI] Fix missing ext-extension files in build 2014-07-10 15:29:34 +01:00
98b54e6682 Rewrite the webui minify js script in python
Also replaced minifier 'yui-compressor' with pure-python 'slimit'.
2014-07-10 15:05:52 +01:00
5eba762a20 [GTKUI] Fix text typo and mark for translation 2014-07-09 19:39:29 +01:00
27682cb666 [#2464] [GTKUI] Fix unable add trackers in createtorrentdialog 2014-07-09 18:52:40 +01:00
67873f39dc [#2418] Fix WebUI error when adding non-ascii torrent
json.dumps attempts to decode (utf8) the 'path' entry which had a
alternative encoding. The solution is to ensure the 'path' entry is
utf8 encoded and remove the unneeded 'path.utf-8' entry.

As self.__m_metadata["info"]["files"] is updated the later code
checking and decoding the 'path' entry can be removed.
2014-07-08 15:32:03 +01:00
7aa52e5f1b Prevent private flagged torrents auto-merging trackers
When adding a torrent already in session any new trackers are merged
to the exiting torrent but this is an unwanted feature for private
flagged torrents.
2014-07-07 23:33:45 +01:00
c31c1b00b1 [#2315] [GTKUI] Potential fix for lost window on Win32 2014-07-07 23:33:36 +01:00
52db7df6d8 [GTKUI] Typo causing password dialog to show 2014-07-07 19:59:59 +01:00
c05fa40756 [GTKUI] Flake8 mainwindow 2014-07-07 19:07:35 +01:00
5fdaf73fdf [GTKUI] Fix quitting bypassing password lock 2014-07-07 18:56:47 +01:00
30e5fc83b2 [#2369] [GTKUI] Fix bypassing tray password dialog
Created a generic password dialog and moved the unlock code out of
systemtray so any call to window.present will now show the dialog.

Also fixed the appindicator not showing the correct visible status
2014-07-07 18:49:32 +01:00
4afd1fa91d Remove old sha module import code 2014-07-05 19:43:07 +01:00
c5722011e8 Pylint alertmanager 2014-07-05 16:50:14 +01:00
02592e1b5e Set alert_queue_size in AlertManager and add logging 2014-07-05 16:50:14 +01:00
ccec01b729 Minor tidyup of core code 2014-07-05 16:06:59 +01:00
6c295cd314 [#2466] [AutoAdd] Fix Copy Torrent File 2014-07-04 21:55:44 +01:00
a9274d4b52 Remove old unneeded send_redundant_have session setting 2014-07-04 21:28:03 +01:00
0a7e02bf34 [GTKUI] Hide the associate magnet button on OSX 2014-07-04 20:49:16 +01:00
19bc0fb468 [#1490] Increase the Alertmanager interval to 0.3s
The original 0.05 interval is causing excessive idle cpu usage
2014-07-04 19:15:00 +01:00
d8a00cf517 [Tests] Update ubuntu favicon 2014-07-02 19:42:57 +01:00
48fb321699 [#2463] [AutoAdd] Fix not loading torrents
The owner attribute for tm.add() method was moved to options dict
2014-07-02 19:32:38 +01:00
f1a9e2ae32 [#2462] [GTKUI] Fix clicking Edit Trackers button in Options tab 2014-07-02 18:29:51 +01:00
2c66f21cc1 [#2461] [GTKUI] Fix createtorrentdialog cell_data_size error 2014-07-02 18:20:44 +01:00
dfed6af0c0 Fixes to resume data saving routines.
* Avoid saving resume data unecessarily by checking the queue of calls
* Removed unecessary LoopingCall for resume data
2014-03-10 14:03:06 +00:00
dbf4f67c55 Update gitattributes file 2014-03-10 13:57:24 +00:00
66b54d6a27 [WebUI] Flake8 web.py 2014-03-06 19:50:40 +00:00
30705d6fc9 [WebUI] Changed --profile to use cProfile 2014-03-06 19:46:27 +00:00
6c74e2d19c [Tests] Fixed tests so that if the tcp port is used, other ports will be tested 2014-03-06 19:08:35 +00:00
973e2d2ef8 [GTKUI] Fix call on sessionproxy.get_torrent_status with bad argument 2014-03-06 19:03:12 +00:00
067ca38321 Update MANIFEST for webui file movement 2014-03-03 19:06:16 +00:00
bc7380c5d7 Add flush_disk_cache parameter to save_resume_data
Using this flag avoids potential issue with file timestamps and
is only needed when stopping the session.
2014-02-25 19:00:50 +00:00
f2535e196d Let save_resume_data build torrent list on stopping session
By having the func build the list we can skip torrents that we already have
resume data for (need_save_resume_data is False).
2014-02-25 18:57:59 +00:00
ea7ef950a3 [GTKUI] Add new OtherDialog to dialogs
This adds a new OtherDialog to dialogs so that will use Deferred to prevent
the dialog loop locking up the mainwindow.
Remove old `Other` dialog from common and cleanup up the file.
Fixes #2401, context menus for torrents not showing current value.
Fixes #2400, add a stop_at_ratio context menu.
Change the protocol rate to display as int.
2014-02-23 18:36:23 +00:00
813261df07 Update internal listview state when saving to disk
Fixes remembering sort column
2014-02-22 14:54:37 -05:00
56d216adf7 [WebUI] Fix typo in statustab 2014-02-22 18:09:26 +00:00
60e60427fc Fix for github code language detection
Moves the extjs framework into subdir that will be ignored by
github's Linguist library so that Deluge is detected correctly
as Python.
2014-02-21 22:39:25 +00:00
49d4bb4969 [#1908] [WebUI] Add bind interface option for server 2014-02-21 18:09:56 +00:00
9eb6b7c52a [WebUI] Fix custom VType 2014-02-21 18:08:06 +00:00
3d4acf7d37 Update docstrings in torrent.py 2014-02-21 10:35:31 +00:00
5e2f6b0f40 Update comment and flake8 listview.py 2014-02-20 19:16:53 +00:00
881da401e1 [GTKUI] Fix column added after restore order func 2014-02-20 19:16:12 +00:00
9290cc1f7a Fix building the code documentation with sphinx
Updated Sphinx conf and tested with Sphinx 1.2.1
Moved webui gen_gettext script
Fixed docstring warning in code
Renamed console update-tracker to update_tracker
2014-02-20 17:38:51 +00:00
c64da3ceb4 Fix missing import, pep8 2014-02-19 23:46:53 -05:00
fff75b51ce [#2373] [OSX] Fix laucher scripts for single leading slash path
On Mavericks the application path passed to scripts only has single leading slash
compared to previous double slash.
Renamed and changed shebang to bash to prevent any issues.
Updated README to rst format for display in trac wiki.
2014-02-19 19:03:53 +00:00
eb804c2a4a Remove debug log line from prev commit 2014-02-19 10:20:22 +00:00
cb87509e4f [#2082] [WebUI] [Console] Validate interface entry for IP address 2014-02-18 21:07:07 +00:00
8d74c3f22a [WebUI] Fix errors from rearranging code in 2294670 2014-02-18 20:46:53 +00:00
22946700dd [#2310] [WebUI] Fix unicode password support
hashlib requires UTF-8 encoded string
Passes flake8 but with warning for global __request__
2014-02-18 00:27:39 +00:00
32d5a0bab2 [2374][WebUI] Fix right-click selection issue 2014-02-17 22:59:10 +00:00
f415fa1a7e [WebUI] Add moved gettext file 2014-02-17 22:03:44 +00:00
66e01991d6 [GTKUI] Rearrange the Status and Details tabs 2014-02-17 22:02:55 +00:00
09b5d2252c remove unneeded gettext line from setup 2014-02-17 16:56:30 +00:00
4a917c95ab [WebUI] Move gettext.js into js dir 2014-02-17 16:52:43 +00:00
65e1f16163 [WebUI] Remove compressed web js files from repo 2014-02-17 16:52:16 +00:00
226d2bb964 [GTKUI] Remove leftover old code 2014-02-17 14:28:55 +00:00
aa5e5178d3 [WebUI][Console] Add missing columns and statuses
Rename 'Seeders' to 'Seeds'
Hide seconds from fdate unless wanted
'Last Seen Complete' renamed to 'Complete Seen'
Added columns and status for Completed date
Rename 'Seeders/Peers' to 'Seeds:Peers'
For translation added colon to WebUI status strings to match GTK
2014-02-17 14:12:32 +00:00
43f12ffdd4 [Console] Reorder prefs tabs to match gtk 2014-02-17 12:53:49 +00:00
14a55133af [#2219] Update the UIs for single Proxy and I2P Proxy 2014-02-17 01:38:02 +00:00
fa0911dbdf Convert proxy config from multi to single setting 2014-02-16 18:39:01 +00:00
a5fa5d0451 [#2219] Add i2p proxy setting to core 2014-02-16 18:34:38 +00:00
59a29d5288 Add sequential download option to webui 2014-02-16 06:12:37 +00:00
67e9787ba1 [#1923] Add pre-allocation and remove compact allocation
Compact allocation is deprecated and a new pre-allocation is available.

Any torrents already using compact will continue to do so but any newly
added only can use sparse (default) or allocate options.

The UIs have been updated to only display a checkbox in preferences for
the user to enable 'Pre-allocate disk space'.
2014-02-16 06:12:37 +00:00
4486592f04 Fix comment quotes changed by previous commit 2014-02-16 00:45:39 +00:00
2ae9a4bdbb Very large refactor and cleanup of torrent.py
Use new docstring format and added to all funcs
Pylint and flake8 fixes; only a few warnings remain for pylint
2014-02-15 23:04:58 +00:00
7dceb629ca Add name to torrent options 2014-02-15 09:27:46 +00:00
0aed074796 Move owner to torrent options 2014-02-15 09:26:08 +00:00
a9ed6fe46a refactor torrent.set_options 2014-02-15 09:26:08 +00:00
1d68579b57 Mark set_torrent_* methods deprecated 2014-02-15 09:23:18 +00:00
f66274fd9d Remove unneeded old 'public' option code 2014-02-12 11:09:39 +00:00
0a001f98e3 Add torrent bandwidth priority to core 2014-02-11 19:57:07 +00:00
0531276b9b [#2417] Add time_since_download and time_since_upload to torrent status 2014-02-11 16:52:14 +00:00
2be5474f3a [#2082] Validate ip address for listen_interface entry
This ensures that only ip addresses are accepted for
listen_interface as libtorrent cannot accept interface
names (eth0) and will cause unexpected results for user.
2014-02-11 16:37:41 +00:00
bddfb2a5c6 Change get_default_download_dir to use expanduser as fallback 2014-02-09 19:27:39 +00:00
dc0000059b GTKUI: Ensure None value from problem config is empty string for set_text 2014-02-09 19:20:06 +00:00
72753a9ccb Fix common.free_space to handle path is None 2014-02-09 19:16:34 +00:00
b193d87499 Include small upstream bencode fix and flake8 file 2014-02-03 13:37:22 +00:00
d9ce4ff634 add comment to tracker_error_alert handler 2014-01-30 13:19:21 +00:00
01ee181607 Fixed glade missing signal warning in path_combo_chooser 2014-01-30 12:01:00 +00:00
2c4af8f136 Fix empty message for certain tracker status errors
By design alert.msg will be empty if the error code is '-1' so use
a.e.message() to get the message as fallback. It was not used at
replacement because when error code is not '-1' then a.e.message()
will also include the error code, which we do not want.
2014-01-30 11:58:04 +00:00
ff8b5aca75 Fix crash if translations are missing 2014-01-27 14:44:29 +01:00
188315735b Fix #2409 : Console Non-interactive curses ImportError 2014-01-27 13:05:09 +00:00
3b79be04bc Flake8'd gtkui/path_combo_chooser 2014-01-21 10:47:16 +00:00
a47b1a28f4 Fixed small bug in path_combo_chooser
The delete-text event was triggered when setting the text in the text entry.
The completion popup was then displayed when it shouldn't. Fixed by blocking
the signal while setting the text.
2014-01-21 02:58:38 +01:00
a2fcaa15c9 Fix #2006 : Display error when moving storage location fails
Adds handler for storage_moved_failed_alert and then sets the
torrent to Error state and pauses it.
2014-01-20 18:49:22 +00:00
35842af019 Add TorrentStorageMovedEvent for #637 2014-01-20 18:49:22 +00:00
7271472e13 Ensure status message is reset after leaving Error state
Also clean up comments in core.py
2014-01-20 18:49:21 +00:00
64fd94e51e Add external ip alert handler to core 2014-01-20 18:49:21 +00:00
ce6abe0247 Fix #1466 : Performance warning: Send Buffer Watermark 2014-01-20 18:49:21 +00:00
c3477ace9b Flake8'd core files 2014-01-18 23:26:18 +00:00
ef7605f9ec Add missing entries to OPTION_FUNCS in torrent.set_options 2014-01-18 22:59:16 +00:00
58e9f66d64 flake8'd torrent.py 2014-01-18 22:14:14 +00:00
7624683710 Add torrent.get_magnet_uri() to return generated magnet uri 2014-01-18 22:07:43 +00:00
b5946c91ed Replace internal time_added with lt added_time 2014-01-18 21:10:31 +00:00
246708e222 Add super_seeding to core 2014-01-18 21:10:23 +00:00
3180bc7104 Use a tmp file when saving state and resume files 2014-01-18 15:54:43 +00:00
d12f0365d5 Handle all-zeros KeyError for removed torrents alerts 2014-01-18 14:11:44 +00:00
01d2ef84ba Fix issue with add_torrent_params flags 2014-01-17 23:43:53 +00:00
80e56eb190 Fix #1000 : GTKUI: Select first row in list if no rows are selected
Also do not remove selection when changing filter (as 1.3 works)
2014-01-17 22:51:54 +00:00
2b64d78163 Remove leftover total_uploaded state code 2014-01-17 19:28:55 +00:00
169b9b9898 Fixes for #607 : Add completed_time to core and Completed column in GTK UI 2014-01-17 19:17:18 +00:00
0da6739f94 Fixes for #367 : Don't store last_seen_complete and total_uploaded in state file 2014-01-17 19:15:38 +00:00
bcbeca4b8a Remove stray debug logging line 2014-01-17 19:08:40 +00:00
9e62304852 More removal of lt deprecated params and funcs
Due to a deprecation mistake get_settings() should be used for python
bindings and it will return a dict.
2014-01-17 18:29:53 +00:00
da6d0ba7bf Replace lt deprecated session funcs with session_settings 2014-01-17 14:18:20 +00:00
ec56ea3ebe Put setup_translation back into start_daemon
A 3rd party plugin broke because using _() in core files so reverting
to prevent issues and may also be useful in future for translating
the help text.
2014-01-17 13:37:07 +00:00
624f2f66cf Replace deprecated session.num_connections with session_status.num_peers
This commit removes get_num_connections() from core and updates UIs
to use get_session_status with num_peers key.

Extra noise is from Flake8 changes
2014-01-17 12:21:08 +00:00
b0c3c3dddc Use non-deprecated libtorrent parameters and functions
Also added support for seed_mode
2014-01-17 10:48:42 +00:00
8eb2155eac Remove torrent_status translations from Daemon and move to UI clients 2014-01-16 23:31:04 +00:00
0a41b86e47 Reinserted call to translate_strings in common and renamed to translate_size_units. 2014-01-16 23:22:28 +00:00
7c808ab4b4 Added language option to Preferences
Changing translation tested and works on:

* Windows 7
* OSX 10.8
* Ubuntu 13.04

* Updated the OSX menubar to gtkbuilder
* Added language names to the Language dropdown in Preferences
2014-01-16 19:54:20 +00:00
d260f6506f Remove unneeded LoopingCall import 2013-12-23 16:22:16 +00:00
8ecc0e11a7 Fix for #1885 and add simple caching to the data funcs for the torrentview
* Fix for #1885 (Wrong tracker icon in torrent list)
* Moved the data functions from torrentview/listview into
  torrentview_data_funcs.py
* Added caching the current value of the cell renderer for the data functions
* Reordered if-tests in deluge.common.fsize
* Disable data funcs when column is hidden
2013-12-23 16:21:24 +00:00
feaeee0379 Removed LoopingCall from torrent.py
Having a LoopingCall for each torrent is expencive with a lot of torrents.
The LoopingCall in torrent.py has been moved to torrentmanager.py
which runs through all the torrents and calls cleanup_prev_status.
2013-11-21 10:48:55 +00:00
de3740fa70 Remove old code line 2013-11-20 23:26:29 +00:00
fe1f620731 Fix web ui showing total_size rather than total_wanted for Size column 2013-11-20 19:36:34 +00:00
63329e7199 Fix #265 : Add new Remaining column 2013-11-20 19:31:04 +00:00
e79cc6cd2d Fix #2381 : Allow silent uninstall for Windows package 2013-11-19 23:03:31 +00:00
6376c49441 Fix #2371 : Add StartupWMClass to desktop file 2013-11-19 23:03:31 +00:00
aafd31b552 Fix #2367 : Private Flag not showing as ticked/checked in DelugeStart theme 2013-11-19 23:03:31 +00:00
2520093b3e Fix #2335 : IPC lockfile issue preventing start of deluge-gtk 2013-11-19 23:03:31 +00:00
3d569b23d6 Merge branch 'master' into HEAD 2013-11-19 23:03:14 +00:00
4ab4998bf7 Fix #2355 previous fix was incorrect - thanks Thomas Hipp 2013-11-12 14:04:01 -08:00
60c53b0ec1 Fix broken last commit to make bbfreeze script work again 2013-09-08 04:13:36 -04:00
852f6049bd Fix twisted 13.1 compat -- the _parse() function was replaced by the _URI class 2013-08-06 18:51:33 -07:00
1daaad422b Improve the bbfreeze script a bit (and hopefully not break it) 2013-07-09 23:48:42 -04:00
f5d8cce4a2 Fix donot option check for windows/osx 2013-06-18 18:46:38 +01:00
9d48d04f0f Fix #2338 : Spelling mistake with occurred 2013-06-09 02:30:26 +01:00
d91a9504aa Fix bug introduced in 19234d 2013-05-27 23:39:43 +02:00
19234d6565 Added some tests for rpcserver 2013-05-26 23:09:02 +01:00
affe47a11c Cache items in get_filter_tree 2013-05-26 21:45:00 +01:00
899c575ae8 Speedup in tracker_error_filter 2013-05-26 21:44:04 +01:00
b76cdc2227 Fix 2247: AttributeError in deluge.error.DaemonRunningError
* Removed all the properties in error.py and added more tests
* Handle failure in client.py handling RPC_ERROR (From older daemons)
2013-05-26 15:25:39 +01:00
a27b479f06 Fix some typos in my previous commits 2013-05-26 14:55:36 +01:00
533228ff5e Fix overzealous legacy code removal from torrentmanager 2013-05-26 01:27:21 +01:00
7dd276631a Add flake8 to setup.cfg and add missed change to #2303 fix 2013-05-22 23:33:06 +01:00
e3f3b6d751 Cleanup torrentmanger code
* Remove legacy/old state file code
 * Passes flake8 cleanly
 * Most pylint messages dealt with
 * Code now uses >=Python 2.6 'with' and 'as' statements
2013-05-22 22:46:01 +01:00
77cb794e4d Fix #2303 : Torrent state not updated until after emitting TorrentFinishedEvent 2013-05-22 19:09:56 +01:00
45f898870f Fix mistake in torrentmanager 2013-05-22 03:01:25 +01:00
5ae74f4017 Fix typo in authmanager 2013-05-22 02:12:00 +01:00
2c4ef9dbb3 Fixup saving and loading state files
* All state files have a backup created before saving
 * The backup will now be used if saving or loading fails
 * GTKUI state files stored in new gtkui_state dir and common load/save functions created
 * Detects bad shutdown and archives timestamped state files in separate config directory.
2013-05-22 01:25:25 +01:00
2bbc1013be Removed call to save_config in path-chooser (obsolete) 2013-05-22 01:10:28 +01:00
4b49e456dd Handle plugin info not available 2013-05-22 01:10:28 +01:00
e2e09200c4 Fix gettext setup in test_common and log in test_transfer so tests run standalone
Add extra tests to test_config
Run the test files through flake8 to tidy up code
2013-05-22 01:06:32 +01:00
28d7c5d44a lower case 'l' for libtorrent in user_agent string 2013-05-21 18:16:06 +01:00
85f9247fd7 GTKUI: Fix Show Zero Hits to only apply to state, owner and tracker_host
There is an issue with the Label plugin where a new label would 'disappear'
and you could not change the options for the new label with zero hits hidden.
2013-05-20 15:41:20 +01:00
f8fbda97cd Fix daemon log being clobbered by running another instance with same config dir
Also includes small fixes and code cleanup
2013-05-20 01:28:31 +01:00
86ac98b9f9 GTKUI: Simplify the quit code for mainwindow 2013-05-19 01:51:55 +01:00
35c85c6d1d daemon code and comment cleanup, removed double call to _shutdown 2013-05-19 01:23:29 +01:00
02bc00bfa5 Small fixes to path chooser
* Handle completion when removing characteres while the popup is visible
* Set Apply button sensitive when changing move completed path in options tab
2013-05-18 22:11:29 +02:00
19184518e9 Fix torrentview exception for self.columns_to_update 2013-05-18 18:27:08 +01:00
85c5449ba8 GTKUI: Display only folder not full path on path chooser 2013-05-18 17:56:57 +01:00
957f04912f GTKUI: Fix layout issues with new path chooser in options tab 2013-05-18 17:56:57 +01:00
e5f7042d00 GTKUI: Asthetic updates to new path chooser options dialog 2013-05-18 17:56:39 +01:00
1596475db2 GTKUI: New path chooser to handle remote paths and store favorite paths 2013-05-18 17:55:25 +01:00
42f5cc382d GTKUI: Modify Show Zero Hits to apply to all filter categories 2013-05-18 02:16:16 +01:00
5479bdd85c GTKUI: More refactoring of filtertree translation text 2013-05-18 02:10:03 +01:00
b4f266457f GTKUI: Remove add_row and use add_rows instead 2013-05-17 17:01:48 +01:00
77bdcfa7a4 Fix seeing double torrents in classic mode
Need to ignore torrent added events from state as they are already
loaded by _on_session_state
2013-05-17 16:15:23 +01:00
235b7348ae Fix test_client test 2013-05-16 12:54:33 +01:00
cf669f3cfa Fix LP#1004793 : Console: Enable use of connect in non-interactive mode 2013-05-16 02:41:59 +01:00
e1b09f2694 Only add quit to deluge-console args if it isn't already present
This prevents an error from being raised due to trying to stop
a reactor which is no longer running
2013-05-16 02:39:55 +01:00
88dd64e795 Ensure console commands are executed in order 2013-05-16 02:39:55 +01:00
f077030dfc ConsoleUI: quit command now gracefully handles stopping a stopped reactor 2013-05-16 02:37:06 +01:00
836acbf02b ConsoleUI: Tidy up help output and limit command usage message to one line 2013-05-16 02:35:28 +01:00
3101104738 UI client.connect() with no credentials/username now attempts connect using localhost auth file details 2013-05-16 02:31:56 +01:00
391513d378 Remove old plugins 2013-05-15 16:56:31 +01:00
6bed403412 Move stray extractor plugin file to correct location 2013-05-15 16:46:37 +01:00
bc91804996 Remove unneeded dht saving state to separate file
The dht state is now saved by default in the session state by libtorrent
2013-05-15 16:27:58 +01:00
a754f1303f Gtkui: Add an alignment container to radio buttons in interface preferences 2013-05-15 15:18:03 +01:00
65f3c12d2d Move Interface page to top in preferences 2013-05-15 14:47:09 +01:00
879cf1b53c change 'classic mode' checkbox for standalone/thinclient radio buttons 2013-05-15 14:44:02 +01:00
105e4c0555 refactor owner code in torrentmanager add method 2013-05-14 01:59:51 +01:00
f7888757aa Refactor translation code in GTK torrent and filtertree views 2013-05-13 19:43:38 +01:00
1a0ca9edbe Alter return value to -1 for invalid freespace path
Updated GTK and Web UI to display error if value is -1
Added set_markup method to gtk statusbaritem class
2013-05-13 19:07:49 +01:00
a49d558aaf Add default localclient info to localhost items in gtk connection manager 2013-05-11 22:54:38 -04:00
502f135b15 Make sure auth file is closed during get_localhost_auth 2013-05-11 22:54:36 -04:00
e263db90ce Console UI: Fix problem displaying first-run message 2013-05-12 02:56:51 +01:00
7bd53903a4 Rearrange the Network tab in GTK and Web UIs
Also applied the suggestions in #2055 including removing 'Encrypt Entire Stream'
and default it (prefer_rc4) to True
2013-05-12 00:15:15 +01:00
8d63ce3ce5 GTKUI: Add a separator after Plugins in Preferences
To help distinguish where the plugin pages start a horizontal seperator
is now included after Plugins.
2013-05-12 00:10:32 +01:00
bf77f42674 Fix #2217 - handling exceptions with authentication 2013-05-10 16:30:35 +01:00
7424cf2834 Fix error in previous commit (b6a3161) and added test 2013-05-09 22:29:04 +01:00
b6a3161280 Add a get method to config so a default value can be returned 2013-05-09 19:13:20 +01:00
daba92b992 Fix #2324 : Encryption level set by deluge does not match libtorrent values
The clients are using range (0-2) whereas actual bit values are (1-3)
2013-05-09 19:12:38 +01:00
5503f90473 Fix #2285 : Speed optimizations for sessionproxy 2013-05-09 12:10:02 +01:00
d5a3851eef Remove develop_plugins, replace with --develop and --install-dir options for build_plugins 2013-05-06 22:14:52 +01:00
8c6d37d9bd Fix tracker_error_filter typo and tidyup commenting in filtermanager 2013-05-03 19:37:38 +01:00
e6498b6864 Fix #2277 : Plugins keys weren't fetched in filter_torrent_ids
Fixed bug introduced in 8c106ce8c4
where keys for plugins weren't fetched in filter_torrent_ids.
2013-05-03 18:00:37 +01:00
4b31061037 Reword gtk prefs tooltip 2013-05-03 14:01:16 +01:00
6e8e7a63cc #2218 #2254 : Re-enable utpex control and add lt_tex (tracker exchange) 2013-05-03 01:15:38 +01:00
53db149b12 Replace Markup with gtkbuilder atttributes for simpler translation labels 2013-05-02 21:28:47 +01:00
d455d03608 Create new generate_pot.py script for translations
* glade3 files require workaround with intltool-extract
 * webui javascript files are now included
 * fix multiline string for parse
2013-05-02 19:21:46 +01:00
3959b67cc0 move and update createicons & check_glade scripts 2013-05-02 19:01:06 +01:00
04bc23abe9 Change Web interface to fork by default
Implemented to match the daemon's default action and option to not fork/daemonize.
2013-05-02 00:56:35 +01:00
a60dc95fed Fixed tests: sessionproxy/torrent/tracker_icon
* Fixed sessionproxy tests
* Fixed test_torrent messing up component for other tests
* Updated tracker_icon test to use unresized google icon
2013-05-02 01:20:05 +02:00
607be461e0 Fix and update tests 2013-05-01 05:24:36 +01:00
765 changed files with 55016 additions and 62593 deletions

21
.gitattributes vendored
View File

@ -1,24 +1,3 @@
/libtorrent/ export-ignore
/win32/ export-ignore
/osx/ export-ignore
docs/build/ export-ignore
docs/source/ export-ignore
/tests/ export-ignore
deluge/scripts/ export-ignore
setup.cfg export-ignore
check_glade.sh export-ignore
createicons.sh export-ignore
create_potfiles_in.py export-ignore
gettextize.sh export-ignore
deluge/i18n/deluge.pot export-ignore
deluge/ui/web/css/*-debug.css export-ignore
deluge/ui/web/js/*-debug.js export-ignore
deluge/ui/web/js/deluge-all/ export-ignore
deluge/ui/web/js/ext-extensions/ export-ignore
deluge/ui/web/gen_gettext.py export-ignore
deluge/ui/web/build export-ignore
deluge/ui/web/docs/ export-ignore
.gitattributes export-ignore
.gitmodules export-ignore
.gitignore export-ignore

5
.gitignore vendored
View File

@ -1,14 +1,19 @@
*~
build
.cache
dist
docs/source/modules
*egg-info
*.egg
*.log
*.pyc
*.tar.*
_trial_temp
.tox/
deluge/i18n/*/
deluge.pot
*.desktop
*.appdata.xml
.build_data*
osx/app
RELEASE-VERSION

425
.pylintrc Normal file
View File

@ -0,0 +1,425 @@
[MASTER]
# Specify a configuration file.
#rcfile=
# Python code to execute, usually for sys.path manipulation such as
# pygtk.require().
#init-hook=
# Add files or directories to the blacklist. They should be base names, not
# paths.
ignore=CVS
# Add files or directories matching the regex patterns to the blacklist. The
# regex matches against base names, not paths.
ignore-patterns=
# Pickle collected data for later comparisons.
persistent=yes
# List of plugins (as comma separated values of python modules names) to load,
# usually to register additional checkers.
load-plugins=
# Use multiple processes to speed up Pylint.
jobs=2
# Allow loading of arbitrary C extensions. Extensions are imported into the
# active Python interpreter and may run arbitrary code.
unsafe-load-any-extension=no
# A comma-separated list of package or module names from where C extensions may
# be loaded. Extensions are loading into the active Python interpreter and may
# run arbitrary code
extension-pkg-whitelist=
# Allow optimization of some AST trees. This will activate a peephole AST
# optimizer, which will apply various small optimizations. For instance, it can
# be used to obtain the result of joining multiple strings with the addition
# operator. Joining a lot of strings can lead to a maximum recursion error in
# Pylint and this flag can prevent that. It has one side effect, the resulting
# AST will be different than the one from reality. This option is deprecated
# and it will be removed in Pylint 2.0.
optimize-ast=no
[MESSAGES CONTROL]
# Only show warnings with the listed confidence levels. Leave empty to show
# all. Valid levels: HIGH, INFERENCE, INFERENCE_FAILURE, UNDEFINED
confidence=
# Enable the message, report, category or checker with the given id(s). You can
# either give multiple identifier separated by comma (,) or put this option
# multiple time (only on the command line, not in the configuration file where
# it should appear only once). See also the "--disable" option for examples.
#enable=
# Disable the message, report, category or checker with the given id(s). You
# can either give multiple identifiers separated by comma (,) or put this
# option multiple times (only on the command line, not in the configuration
# file where it should appear only once).You can also use "--disable=all" to
# disable everything first and then reenable specific checks. For example, if
# you want to run only the similarities checker, you can use "--disable=all
# --enable=similarities". If you want to run only the classes checker, but have
# no Warning level messages displayed, use"--disable=all --enable=classes
# --disable=W"
#
# Arranged by category and use symbolic names instead of ids.
disable=
# Convention
missing-docstring, invalid-name,
# Error
no-member, no-name-in-module,
# Information
locally-disabled,
# Refactor
no-self-use, too-many-arguments, too-many-branches, too-many-instance-attributes,
too-many-locals, too-few-public-methods, too-many-public-methods, too-many-statements,
# Refactor msgs that should eventually be enabled:
redefined-variable-type, too-many-ancestors,
too-many-nested-blocks, too-many-return-statements,
# Warning
unused-argument, protected-access, import-error, unused-variable,
attribute-defined-outside-init,
# Warning msgs that should eventually be enabled:
arguments-differ, global-statement, fixme, broad-except
[REPORTS]
# Set the output format. Available formats are text, parseable, colorized, msvs
# (visual studio) and html. You can also give a reporter class, eg
# mypackage.mymodule.MyReporterClass.
output-format=parseable
# Put messages in a separate file for each module / package specified on the
# command line instead of printing them on stdout. Reports (if any) will be
# written in a file name "pylint_global.[txt|html]". This option is deprecated
# and it will be removed in Pylint 2.0.
files-output=no
# Tells whether to display a full report or only the messages
reports=no
# Python expression which should return a note less than 10 (10 is the highest
# note). You have access to the variables errors warning, statement which
# respectively contain the number of errors / warnings messages and the total
# number of statements analyzed. This is used by the global evaluation report
# (RP0004).
evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10)
# Template used to display messages. This is a python new-style format string
# used to format the message information. See doc for all details
#msg-template=
[SPELLING]
# Spelling dictionary name. Available dictionaries: none. To make it working
# install python-enchant package.
spelling-dict=
# List of comma separated words that should not be checked.
spelling-ignore-words=
# A path to a file that contains private dictionary; one word per line.
spelling-private-dict-file=
# Tells whether to store unknown words to indicated private dictionary in
# --spelling-private-dict-file option instead of raising a message.
spelling-store-unknown-words=no
[BASIC]
# Good variable names which should always be accepted, separated by a comma
good-names=d,i,j,k,ex,Run,_,log
# Bad variable names which should always be refused, separated by a comma
bad-names=foo,bar,baz,toto,tutu,tata
# Colon-delimited sets of names that determine each other's naming style when
# the name regexes allow several styles.
name-group=
# Include a hint for the correct naming format with invalid-name
include-naming-hint=no
# List of decorators that produce properties, such as abc.abstractproperty. Add
# to this list to register other decorators that produce valid properties.
property-classes=abc.abstractproperty
# Regular expression matching correct function names
function-rgx=[a-z_][a-z0-9_]{2,30}$
# Naming hint for function names
function-name-hint=[a-z_][a-z0-9_]{2,30}$
# Regular expression matching correct variable names
variable-rgx=[a-z_][a-z0-9_]{2,30}$
# Naming hint for variable names
variable-name-hint=[a-z_][a-z0-9_]{2,30}$
# Regular expression matching correct constant names
const-rgx=(([A-Z_][A-Z0-9_]*)|(__.*__))$
# Naming hint for constant names
const-name-hint=(([A-Z_][A-Z0-9_]*)|(__.*__))$
# Regular expression matching correct attribute names
attr-rgx=[a-z_][a-z0-9_]{2,30}$
# Naming hint for attribute names
attr-name-hint=[a-z_][a-z0-9_]{2,30}$
# Regular expression matching correct argument names
argument-rgx=[a-z_][a-z0-9_]{2,30}$
# Naming hint for argument names
argument-name-hint=[a-z_][a-z0-9_]{2,30}$
# Regular expression matching correct class attribute names
class-attribute-rgx=([A-Za-z_][A-Za-z0-9_]{2,30}|(__.*__))$
# Naming hint for class attribute names
class-attribute-name-hint=([A-Za-z_][A-Za-z0-9_]{2,30}|(__.*__))$
# Regular expression matching correct inline iteration names
inlinevar-rgx=[A-Za-z_][A-Za-z0-9_]*$
# Naming hint for inline iteration names
inlinevar-name-hint=[A-Za-z_][A-Za-z0-9_]*$
# Regular expression matching correct class names
class-rgx=[A-Z_][a-zA-Z0-9]+$
# Naming hint for class names
class-name-hint=[A-Z_][a-zA-Z0-9]+$
# Regular expression matching correct module names
module-rgx=(([a-z_][a-z0-9_]*)|([A-Z][a-zA-Z0-9]+))$
# Naming hint for module names
module-name-hint=(([a-z_][a-z0-9_]*)|([A-Z][a-zA-Z0-9]+))$
# Regular expression matching correct method names
method-rgx=[a-z_][a-z0-9_]{2,40}$
# Naming hint for method names
method-name-hint=[a-z_][a-z0-9_]{2,30}$
# Regular expression which should only match function or class names that do
# not require a docstring.
no-docstring-rgx=__.*__
# Minimum line length for functions/classes that require docstrings, shorter
# ones are exempt.
docstring-min-length=-1
[ELIF]
# Maximum number of nested blocks for function / method body
max-nested-blocks=5
[LOGGING]
# Logging modules to check that the string format arguments are in logging
# function parameter format
logging-modules=logging
[FORMAT]
# Maximum number of characters on a single line.
max-line-length=120
# Regexp for a line that is allowed to be longer than the limit.
ignore-long-lines=^\s*(# )?<?https?://\S+>?$
# Allow the body of an if to be on the same line as the test if there is no
# else.
single-line-if-stmt=no
# List of optional constructs for which whitespace checking is disabled. `dict-
# separator` is used to allow tabulation in dicts, etc.: {1 : 1,\n222: 2}.
# `trailing-comma` allows a space between comma and closing bracket: (a, ).
# `empty-line` allows space-only lines.
no-space-check=trailing-comma,dict-separator
# Maximum number of lines in a module
max-module-lines=1550
# String used as indentation unit. This is usually " " (4 spaces) or "\t" (1
# tab).
indent-string=' '
# Number of spaces of indent required inside a hanging or continued line.
indent-after-paren=4
# Expected format of line ending, e.g. empty (any line ending), LF or CRLF.
expected-line-ending-format=LF
[MISCELLANEOUS]
# List of note tags to take in consideration, separated by a comma.
notes=FIXME,XXX,TODO
[VARIABLES]
# Tells whether we should check for unused import in __init__ files.
init-import=no
# A regular expression matching the name of dummy variables (i.e. expectedly
# not used).
dummy-variables-rgx=_$|dummy
# List of additional names supposed to be defined in builtins. Remember that
# you should avoid to define new builtins when possible.
additional-builtins=_,_n,__request__,WindowsError
# List of strings which can identify a callback function by name. A callback
# name must start or end with one of those strings.
callbacks=cb_,_cb
# List of qualified module names which can have objects that can redefine
# builtins.
redefining-builtins-modules=six.moves,future.builtins,future_builtins
[TYPECHECK]
# Tells whether missing members accessed in mixin class should be ignored. A
# mixin class is detected if its name ends with "mixin" (case insensitive).
ignore-mixin-members=yes
# List of module names for which member attributes should not be checked
# (useful for modules/projects where namespaces are manipulated during runtime
# and thus existing member attributes cannot be deduced by static analysis. It
# supports qualified module names, as well as Unix pattern matching.
ignored-modules=
# List of class names for which member attributes should not be checked (useful
# for classes with dynamically set attributes). This supports the use of
# qualified names.
ignored-classes=SQLObject,twisted.internet.reactor
# List of members which are set dynamically and missed by pylint inference
# system, and so shouldn't trigger E1101 when accessed. Python regular
# expressions are accepted.
generated-members=REQUEST,acl_users,aq_parent
# List of decorators that produce context managers, such as
# contextlib.contextmanager. Add to this list to register other decorators that
# produce valid context managers.
contextmanager-decorators=contextlib.contextmanager
[SIMILARITIES]
# Minimum lines number of a similarity.
min-similarity-lines=4
# Ignore comments when computing similarities.
ignore-comments=yes
# Ignore docstrings when computing similarities.
ignore-docstrings=yes
# Ignore imports when computing similarities.
ignore-imports=no
[IMPORTS]
# Deprecated modules which should not be used, separated by a comma
deprecated-modules=regsub,TERMIOS,Bastion,rexec
# Create a graph of every (i.e. internal and external) dependencies in the
# given file (report RP0402 must not be disabled)
import-graph=
# Create a graph of external dependencies in the given file (report RP0402 must
# not be disabled)
ext-import-graph=
# Create a graph of internal dependencies in the given file (report RP0402 must
# not be disabled)
int-import-graph=
# Force import order to recognize a module as part of the standard
# compatibility libraries.
known-standard-library=
# Force import order to recognize a module as part of a third party library.
known-third-party=enchant
# Analyse import fallback blocks. This can be used to support both Python 2 and
# 3 compatible code, which means that the block might have code that exists
# only in one or another interpreter, leading to false positives when analysed.
analyse-fallback-blocks=no
[DESIGN]
# Maximum number of arguments for function / method
max-args=7
# Argument names that match this expression will be ignored. Default to name
# with leading underscore
ignored-argument-names=_.*
# Maximum number of locals for function / method body
max-locals=15
# Maximum number of return / yield for function / method body
max-returns=6
# Maximum number of branch for function / method body
max-branches=12
# Maximum number of statements in function / method body
max-statements=50
# Maximum number of parents for a class (see R0901).
max-parents=7
# Maximum number of attributes for a class (see R0902).
max-attributes=7
# Minimum number of public methods for a class (see R0903).
min-public-methods=2
# Maximum number of public methods for a class (see R0904).
max-public-methods=20
# Maximum number of boolean expressions in a if statement
max-bool-expr=5
[CLASSES]
# List of method names used to declare (i.e. assign) instance attributes.
defining-attr-methods=__init__,__new__,setUp
# List of valid names for the first argument in a class method.
valid-classmethod-first-arg=cls
# List of valid names for the first argument in a metaclass class method.
valid-metaclass-classmethod-first-arg=mcs
# List of member names, which should be excluded from the protected access
# warning.
exclude-protected=_asdict,_fields,_replace,_source,_make
[EXCEPTIONS]
# Exceptions that will emit a warning when being caught. Defaults to
# "Exception"
overgeneral-exceptions=Exception

51
.travis.yml Normal file
View File

@ -0,0 +1,51 @@
dist: trusty
sudo: required
group: deprecated-2017Q2
language: python
python:
- "2.7"
cache: pip
before_install:
- lsb_release -a
- sudo add-apt-repository ppa:deluge-team/develop -y
- sudo apt-get update
# command to install dependencies
install:
- bash -c "echo $APTPACKAGES"
- sudo apt-get install $APTPACKAGES
- pip install "tox==2.1.1"
env:
global:
- APTPACKAGES="python-libtorrent"
- APTPACKAGES_GTKUI="python-gobject python-glade2"
- DISPLAY=:99.0
matrix:
- TOX_ENV=pydef
- TOX_ENV=flake8
# - TOX_ENV=flake8-complexity
- TOX_ENV=docs
# - TOX_ENV=todo
- TOX_ENV=trial APTPACKAGES="$APTPACKAGES $APTPACKAGES_GTKUI"
- TOX_ENV=pygtkui APTPACKAGES="$APTPACKAGES $APTPACKAGES_GTKUI"
# - TOX_ENV=testcoverage APTPACKAGES="$APTPACKAGES $APTPACKAGES_GTKUI"
- TOX_ENV=plugins
virtualenv:
system_site_packages: true
# We use xvfb for the GTKUI tests
before_script:
- export PYTHONPATH=$PYTHONPATH:$PWD
- python -c "import libtorrent as lt; print lt.__version__"
- "/sbin/start-stop-daemon --start --quiet --pidfile /tmp/custom_xvfb_99.pid --make-pidfile --background --exec /usr/bin/Xvfb -- :99 -ac -screen 0 1280x1024x16"
- echo '2.0.0.dev0' > RELEASE-VERSION
script:
- bash -c "echo $DISPLAY"
- tox -e $TOX_ENV

View File

@ -14,7 +14,7 @@ libtorrent (http://www.libtorrent.org):
Contributors (and Past Developers):
* Zach Tibbitts <zach@collegegeek.org>
* Alon Zakai ('Kripken') <kripkensteiner@gmail.com>
* Marcos Pinto ('markybob') <markybob@gmail.com>
* Marcos Mobley ('markybob') <markybob@gmail.com>
* Alex Dedul
* Sadrul Habib Chowdhury
* Ido Abramovich <ido.deluge@gmail.com>
@ -457,7 +457,7 @@ Translation Contributors:
Marco Rodrigues
Marcos
Marcos Escalier
Marcos Pinto
Marcos Mobley
Marcus Ekstrom
Marek Dębowski
Mário Buči

559
ChangeLog
View File

@ -1,521 +1,50 @@
=== Deluge 1.4.0 (In Development) ===
* Improved Logging
* Removed the AutoAdd feature on the core. It's now handled with the AutoAdd
plugin, which is also shipped with Deluge, and it does a better job and
now, it even supports multiple users perfectly.
* Authentication/Permission exceptions are now sent to clients and recreated
there to allow acting upon them.
* Enforced the use of the "deluge.plugins" namespace to reduce package
names clashing beetween regular packages and deluge plugins.
=== Deluge 2.0 (In Development) ===
* Improved Logging
* Removed the AutoAdd feature on the core. It's now handled with the AutoAdd
plugin, which is also shipped with Deluge, and it does a better job and
now, it even supports multiple users perfectly.
* Authentication/Permission exceptions are now sent to clients and recreated
there to allow acting upon them.
* Enforced the use of the "deluge.plugins" namespace to reduce package
names clashing beetween regular packages and deluge plugins.
==== Core ====
* Make the distinction between adding to the session new unmanaged torrents
and torrents loaded from state. This will break backwards compatability.
* Pass a copy of an event instead of passing the event arguments to the
event handlers. This will break backwards compatability.
* Allow changing ownership of torrents.
* File modifications on the auth file are now detected and when they happen,
the file is reloaded. Upon finding an old auth file with an old format, an
upgrade to the new format is made, file saved, and reloaded.
* Authentication no longer requires a username/password. If one or both of
these is missing, an authentication error will be sent to the client
which sould then ask the username/password to the user.
* Implemented sequential downloads.
* #378: Provide information about a torrent's pieces states
* Make the distinction between adding to the session new unmanaged torrents
and torrents loaded from state. This will break backwards compatability.
* Pass a copy of an event instead of passing the event arguments to the
event handlers. This will break backwards compatability.
* Allow changing ownership of torrents.
* File modifications on the auth file are now detected and when they happen,
the file is reloaded. Upon finding an old auth file with an old format, an
upgrade to the new format is made, file saved, and reloaded.
* Authentication no longer requires a username/password. If one or both of
these is missing, an authentication error will be sent to the client
which sould then ask the username/password to the user.
* Implemented sequential downloads.
* Provide information about a torrent's pieces states
==== GtkUI ====
* Fix uncaught exception when closing deluge in classic mode
* Allow changing ownership of torrents
* Host entries in the Connection Manager UI are now editable. They're
now also migrated from the old format were automatic localhost logins were
possible, which no longer is, this fixes #1814.
* Implemented sequential downloads UI handling.
* #378: Allow showing a pieces bar instead of a regular progress bar in a
torrent's status tab.
* #2093: Make torrent opening compatible with all unicode paths.
* Allow changing ownership of torrents.
* Host entries in the Connection Manager UI are now editable.
* Implemented sequential downloads UI handling.
* Add optional pieces bar instead of a regular progress bar in torrent status tab.
* Make torrent opening compatible with all unicode paths.
* Fix magnet association button on Windows.
* Add keyboard shortcuts for changing queue position:
- Up: Ctrl+Alt+Up
- Down: Ctrl+Alt+Down
- Top: Ctrl+Alt+Shift+Up
- Bottom: Ctrl+Alt+Shift+Down
==== WebUI ====
* Server (deluge-web) now daemonizes by default, use '-d' or '--do-not-daemonize' to disable.
* Fixed the '--base' option to work for regular use, not just with reverse proxies.
==== Blocklist Plugin ====
* #1382: Implemented whitelist support to both core and GTK UI.
* Implemented ip filter cleaning before each update. Restarting the deluge
daemon is no longer needed.
* If "check_after_days" is 0(zero), the timer is not started anymore. It
would keep updating one call after the other. If the value changed, the
timer is now stopped and restarted using the new value.
=== Deluge 1.3.7 (In Development) ===
==== GtkUI ====
* Fix issue with Plugins that add Tab to torrentdetails
* Fix the scalable icon install directory
==== Extractor ====
* #2290: Fix dotted filenames being rejected
=== Deluge 1.3.6 (25 Feburary 2013) ===
==== Core ====
* Catch & log KeyError when removing a torrent from the queued torrents set
* Fix moving/renaming torrents issues when using libtorrent 0.16
* Make sure queue order is preserved when restarting
* #2160: Disable use of python bindings for libtorrent extensions and replace with session flag
* #2163: Fix unable add torrent file with empty (0:) encoding tag
* #2201: Fix error in authmanager if auth file has extra newlines
* #2109: Fix the Proxy settings not being cleared by setting None
* #2110: Fix accepting magnet uris with xt param anywhere within them
* #2204: Fix daemon shutdown hang with large numbers of torrents
==== Client ====
* Fix keyerrors after removing torrents from UIs
==== GtkUI ====
* Add move completed option to add torrent dialog
* Prevent jitter in torrent view
* Fix torrent creation with non-ascii characters
* Fix #2100 : Add option not to bring main window to front when adding torrents through ipcinterface
* Add Quit Dialog when toggling classic mode in preferences and only show connection manager when not in classic mode.
* #2169: Fix 'Download Location' in the Add Torrent Dialog not set correctly when folder typed into Other->Location field
* #2171: Fix the Add Peer dialog not responding if empty or invalid values entered
* #2104: Fix no title set for the appindicator
* #2086: Fix submenus and icons for appindicator
* #2146: Fix missing translations in View|Tabs submenu
* Fix torrent names on libtorrent 0.16 on windows
* #2147: Fix missing translations for plugin preferences page
* #1474: Fix the on_show_prefs hook not being executed immediatly after enabling a plugin
* #1946: Fix ReactorNotRestartable error when set as startup application
* #2130: Fix same name can be given to different files in Add Torrent dialog
* #2129: Fix empty filename able to be set in AddTorrent dialog
* #2228: Fix Apply-To-All in AddTorrent Dialog copying file renames to other torrents
* #2260: Fix the Add Torrent dialog also bringing the main window to active workspace
* Fix showing exception error to user in Classic Mode with no libtorrent installed
==== Console ====
* LP#1004793: Enable use of connect command in non-interactive mode
* Ensure console commands are executed in order
* #2065: Fix crash with missing closing quote
* #1397: Add support for -s STATE in info command
==== WebUI ====
* Add move completed option to add torrent dialog
* #2112: Fix world readable tmp directory in json_api
* #2069: Fix login window layout problem when using larger than default font size
* #1890: Fix columns in files and peers view could use some spacing
* #2103: Fix sorting by name is case-sensitive [sedulous]
* #2120: Fix manually entered values not being saved in spinners
* #2212: Fix unable to scroll in proxy preferences page
* Fix autoconnecting to the default host
* #2046: Fix plugins not enabling properly until after refreshing page
* #2125: Fix plugin methods not being available when enabled until restart
* #2085: Fix not showing torrents in sidebar for categories other than 'All' in classic mode
* #2232: Fix flag icon path in Peers Tab missing deluge.config.base
* Fix submenus closing upon mouse click
* Add failed login log message, including IP address, to enable use with fail2ban
* #2261: Fix Proxy settings not being saved in preferences
==== Windows OS ====
* Hide the cmd windows when running deluged.exe or deluge-web.exe
* Add deluged-debug.exe and deluge-web-debug.exe that still show the cmd window
* Add gtk locale files to fix untranslated text
* Fix the Open Folder option not working with non-ascii paths
* Fix the daemon starting with config dir containing spaces
* Fix Windows tray submenu items requiring right-click instead of left-click
* Fix issue with adding some torrents with illegal characters via url in gtk client
* #2240: Fix freespace issue with large capacity drives
==== OS X ====
* Fix Open File/Folder option
* Add OS X Menu for GTK Quartz
==== Execute ====
* Fix execute plugin not working with unicode torrent names
==== Extractor ====
* Add Windows support, using 7-zip
* Added support for more extensions
* Disabled extracting 'Move Completed' torrents due to race condition
=== Deluge 1.3.5 (09 April 2012) ===
==== GtkUI ====
* Modified fix for #1957, keyerror with non-acsii columns
* Fix translation of items in Sidebar and Torrent Menu
* #2052: Fix translation of Progress bar text
* #2071: Fix KeyError in gtkui when file priority set to value '3'
* #2064: Fix files treeview height in Create Dialog
* Fix missing semi-colon in deluge.desktop
* Disable setting file priorities for seeding torrents
* Bring MainWindow to front when opening another instance
==== WebUI ====
* #2050: Fix 'Up Speed' column not sorting
* Hide unused Infohash button in WebUI
==== Label ====
* Disable unusable items for 'All' in sidebar menu
* Fix items for translation
==== Console ====
* Fix prefixed space for tab completing commands
* Fix missing trailing space for command options with tab complete
==== Blocklist ====
* Use (documented) formatdate over format_date_time
=== Deluge 1.3.4 (03 March 2012) ===
==== Core ====
* #1921: Free disk space reporting incorrectly in FreeBSD
* #1964: Fix unhandled UnpicklingErrors
* #1967: Fix unhandled IndexError when trying to open a non-json conf file
* Fix setting daemon listen interface from command line
* #2021: Fix share ratio limit not obeyed for seeded torrents added to session
* Add optparse custom version to prevent unnecessary loading of libtorrent
* #1554: Fix seeding on share ratio failing for partially selected torrents
* Add proper process title naming in ps, top etc. (Depends: setproctitle)
==== GtkUI ====
* #1918: Fix Drag'n'Drop not working in Windows
* #1941: Increase maximum Cache Size to 999999 (15GiB)
* #1940: File & folder renaming issue when using Add Torrent dialog in Windows
* LP#821577: Fix UnpicklingError when external selection dragged onto Files Tab
* #1934: Fix Unicode error in AddTorrent Dialog
* #1957: Fix keyerror when adding columns for non-latin languages
* #1969: Fix menu item 'Quit & Shutdown' still available when not connected to daemon
* #1895: Fix Files Tab showing wrong files due to torrent_info race condition
* #2010: Move speed text in titlebar to the beginning
* #2032: Wait for client to shutdown/disconnect before stopping reactor
* Fix compatibility with Python 2.5
* Fix collapsed treeview in Create Torrent dialog
* Ignore unmaximise event when window isn't visible
* #1976: Fixed text entry with trailing newline characters causing issues for Move Storage
==== WebUI ====
* Fix Webui files-tab menu setting wrong priority
* Update to ExtJS 3.4.0
* #1960: Fix statustab showing total_payload_download for upload as well
* Remove uneeded Titlebar to save space
* Fix clipped Browse button in WebUI
* #1915: Fix being unable to stop the status bar from autohiding
* Fix password box focus issue in Firefox
* Fix plugin uploads from behind a reverse proxy
* #2010: Move speed text in titlebar to the beginning
* #1936: Fix Referenced before assignment error in json_api
* Changes are now applied when clicking OK in Preferences
* Added Download,Uploaded,Down Limit, Up Limit & Seeder/Peeds columns
* Add magnet uri support to Add Url
* Add keymaps for torrents - Ctrl-A (select all) and Delete
* #2037: Fix 'Add Torrents' torrents list not scrolling
* #2038: Fix Chrome 17 disconnecting from webui
==== Console ====
* #1953: Fix flickering on every update
* #1954: Fix 'invalid literal for float' when setting listen interface
* #1945: Fix UnicodeDecodeError when using non-ascii chars in info
==== Label ====
* #1961: Add missing 'All' filter option
* #2035: Fix label options dialog in webui
* #2036: Fix newly added labels not being sorted in torrent right click menu
==== Notification ====
* #1905: Fix no email sent to second email address
* #1898: Fix email notifications not including date/time they were sent
==== Scheduler ====
* Add plugin page for WebUi
==== Execute ====
* Commands now run scripts asynchronous to prevent Deluge from hanging
==== AutoAdd ====
* Added watch folder support for '.magnet' text file containing single or multiple magnet uris
* Fix glade object issue when re-enabling plugin in same session
* Fix plugin not showing as enabled in webui
=== Deluge 1.3.3 (22 July 2011) ===
==== Core ====
* Properly show the 'Checking Resume Data' state instead of just 7
* #1788: Added ability to use XDG_DOWNLOAD_DIR as default download folder
* Fix path error with torrent files prefixed with 'file://' from Firefox
* #1869: Fix setting the disk io read/write to bypass OS cache in Windows
* #1504: Fix win32 running deluged as not logged in user via runas or service
* #890: If added torrent already exists, append extra trackers to it
* #1338: Fix Seeds and Peers totals not updating
* #1239: Fix translated Tracker Error text not counted in sidebar Error status
* Fix httpdownloader error with existing filename
* #1505: Add libtorrent info to version output
* #1637 Fix UnicodeDecodeError from 'deluge-* --help' with non-english languages
* #1714 Fix handling of backslashes when renaming files/folders
==== GtkUI ====
* Show the checking icon for torrents in the 'Checking Resume Data' state
* #1195: Fix right-click selecting issue when switching between folders and files
* Add F2 key shortcut for renaming filenames in the Files Tab
* Increase max piece size to 16 MiB in create torrent dialog
* #1475: Fix save and restore Preferences dialog size from config
* Add search as you type to the torrent view
* #1456: Fix no ETA showing with multiple files
* #1560: Fix FilesTab Progress value sorting by int instead of float
* #1263: Fix not remembering column widths
* #948: New Release Dialog now shows the server version
* Fix peers in PeersTab showing non-zero download rate when seeding
==== AutoAdd ====
* #1861: Fix AutoAdd Warning (column number is a boolean)
==== Label ====
* #1246: Fix losing Labels upon restart
==== Execute ====
* #1477: Fix ignore Added events from state file on startup
==== ConsoleUI ====
* #1258: Add support for urls and magnet uris in add command
* #1801: Fix unhandled defered error and missing error message upon failed connect
=== Deluge 1.3.2 (24 May 2011) ===
==== Core ====
* #1527: Fix Converting unicode to unicode error in move_storage
* #1373: Fix creating and moving non-ascii folder names in MS Windows
* #1507: Fix temporary file race condition in core/core.py:add_torrent_url
* Fix a bug that can occur when upgrading 1.1 config files
* #1517: Fix isohunt urls not loading
* Handle redirection when adding a torrent by url
* #1614: Fix autoadd matching a directory called "torrent"
* #1742: Fix failure in Event handler prevents further emissions
==== GtkUI ====
* #1514: Added Indicator Applet
* #1494: Add torrent columns Downloaded and Uploaded
* #1308: Add torrent column Seeds/Peers ratio
* #1646: Add torrent columns for per torrent upload and download speed limits
* Add missing icons for Trackers filter
* Fix inconsistancies in the text for translation
* #1510: Fix cannot create a torrent with only non-zero tier trackers
* #1513: Fix unhandled Twisted Error in test_listen_port
* #690: Fix renaming folders does not remove old empty folders
* #1336: Fix uneeded horizontal scrollbar showing in Files & Peers Tab
* #1508: Fix TypeError in cell_data_queue() could not convert argument to correct param type
* #1498: Fix double slashes appearing when renaming
* #1283: Fix consistent icons for Files tab
* #1282: Text for AutoManaged changed to 'On/Off' and localized
* Fix Up/Down buttons in Edit Trackers Dialog
* Add Key Shortcuts for main menu functions
==== WebUI ====
* #1194: Fix infinite login prompt in web ui through reverse proxy
* #1355: Fix slow changing states in webUI
* #1536: Fix Edit Trackers window not scrolling and not being resizable
* #1799: Fix Missing textbox for "Move completed" in torrent options
* #1562: Fix Javascript error in Web UI when re-opening preferences
* #1567: Fix js from plugins does not work with different 'base' setting
* #1268: Fix torrent errors not displayed in webui
* #1323: Fix filter panels not scrollable
* Fix file uploads from behind a reverse proxy.
* #1333: Fix peer list doesn't update automatically
* #1537: Fix editing trackers list, trackers have to be reselected
==== ConsoleUI ====
* #755: Fix can't set listen_ports through console UI
* #1500: Fix Console crashes on command longer than terminal width
* #1248: Fix deluge-console unicode support on redirected stdout
* Fix for deluge-console not adding torrent files on MS Windows
* #1450: Fix trailing white space in paths
* Misc: Updated help text for deluge-console on MS Windows
* #1484: Fix trying to access the screen object when not using interactive mode
* #1548: Fix cli argument processing
* #1856: Add --sort option to info command
* #1857: Add seeding_time, active_time and tracker_status to info command
==== Scheduler ====
* #1506: Fix max speed not restored on a yellow->green transition
=== 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
* Implement #1012 httpdownloader supports gzip decoding
* #496: Remove deprecated functions in favour of get_session_status()
* #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 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 ====
* Implement new RPC protocol DelugeRPC replacing XMLRPC
* Move to a twisted framework
* Add an 'Error' filter for Trackers to show trackers that currently have a tracker error
* Use system GeoIP database if available, this is now an optional dependency
==== GtkUI ====
* Remove SignalReceiver
* Implemented a cross-platform IPC method thus removing the DBUS dependency
* Implement a "True" Classic Mode where there is no longer a separate daemon process
* Add preferences option "Add torrent in paused state"
* Add tracker icons to the Tracker column
* Implement #259 show tooltip with country name in the peers tab
* Add an error category to the tracker sidebar list
* Add Find More Plugins button to Plugins preference page
* Fix #518 remove header in add torrent dialog to save vertical space
* Add a Cache preferences page to adjust cache settings and examine cache status
* Add ability to rename files prior to adding them
* Fix shutdown handler with GNOME session manager
* Allow 4 MiB piece sizes when creating a torrent
==== ConsoleUI ====
* Changed to use curses for a more interactive client
==== WebUI ====
* Move over to using Twisted-Web for the webserver.
* Move to only AJAX interface built upon Ext-JS.
==== Plugins ====
* Add Scheduler plugin
* Add Extractor plugin
==== Misc ====
* PyGTK dependency bumped to => 2.12 to use new tooltip system
* Add new scripts for invoking UIs: deluge-gtk, deluge-web, deluge-console
* Remove GeoIP database from the source tree
=== Deluge 1.1.0 - "Time gas!" (10 January 2009) ===
==== Core ====
* Implement #79 ability to change outgoing port range
* Implement #296 ability to change peer TOS byte
* Add per-torrent move on completed settings
* Implement #414 use async save_resume_data method
* Filter Manager with torrent filtering in get_torrents_status , for sidebar and plugins.
* Implement #368 add torrents by infohash/magnet uri (trackerless torrents)
* Remove remaining gtk functions in common
* Tracker icons.
* Add ETA for torrents with stop at seed ratio set
* Fix #47 the state and config files are no longer invalidated when there is no diskspace
* Fix #619 return "" instead of "Infinity" if seconds == 0 in ftime
* Add -P, --pidfile option to deluged
==== GtkUI ====
* Add peer progress to the peers tab
* Add ability to manually add peers
* Sorting # column will place downloaders above seeds
* Remove dependency on libtorrent for add torrent dialog
* Allow adding multiple trackers at once in the edit tracker dialog
* Implement #28 Create Torrent Dialog
* Redesiged sidebar with filters for Active and Tracker (see Filter Manager)
* Implement #428 the ability to rename files and directories
* Implement #229 add date added column
* Implement #596 show speeds in title
* Fix #636 not setting the daemon's config directory when using --config= with the UI in classic mode.
* Fix #624 do not allow changing file priorities when using compact allocation
* Fix #602 re-did files/peers tab state saving/loading
* Fix gtk warnings
* Add protocol traffic statusbar item
* Rework the Remove Torrent Dialog to only have 2 options, remove data and remove from session.
* Add "Install Plugin" and "Rescan Plugins" buttons to the Plugins preferences
* Make active port test use internal graphic instead of launching browser
==== WebUI ====
* Lots of smaller tweaks.
* All details tabs have the same features as in gtk-ui 1.0.x
* Persistent sessions #486
* Plugin improvements for easy use of templates and images in eggs. #497
* Classic template takes over some style elements from white template.
* https (for users that know how to create certificates)
* Easier apache mod_proxy use.
* Redesigned sidebar
==== AjaxUI ====
* Hosted in a webui template.
==== ConsoleUI ====
* New ConsoleUI written by Idoa01
* Callable from command-line for scripts.
==== Plugins ====
* Stats plugin for graphs.
* Label plugin for grouping torrents and per torrent settings.
==== Misc ====
* Implement #478 display UI options in usage help
* Fix #547 add description to name field per HIG entry 2.1.1.1
* Fix #531 set default log level to ERROR and add 2 command-line options, "-L, --loglevel" and "-q, --quiet".
* Implemented whitelist support to both core and GTK UI.
* Implemented ip filter cleaning before each update. Restarting the deluge
daemon is no longer needed.
* If "check_after_days" is 0(zero), the timer is not started anymore. It
would keep updating one call after the other. If the value changed, the
timer is now stopped and restarted using the new value.

29
DEPENDS
View File

@ -1,30 +1,29 @@
=== Core ===
* python >= 2.6
* twisted >= 8.1
* twisted-web >= 8.1
* pyopenssl
* libtorrent (rasterbar) >= 1.1.1
* python >= 2.7.7
* setuptools
* gettext
* intltool
* twisted >= 11.1
* pyopenssl
* pyxdg
* chardet
* gettext
* python-geoip (optional)
* geoip-database (optional)
* setproctitle (optional)
* rencode >= 1.0.2 (optional), a Python port is already included
* pillow (optional)
* py2-ipaddress (optional, required for Windows IPv6)
* rencode >= 1.0.2 (optional), python port bundled.
* libtorrent (rasterbar) >= 0.16.7
* If building libtorrent:
* boost >= 1.40
* openssl
* zlib
=== Gtk ===
=== Gtk UI ===
* pygtk >= 2.16
* librsvg
* xdg-utils
* intltool
* python-notify (optional)
* pygame (optional)
* python-appindicator (optional)
=== Web ===
=== Web UI ===
* mako
* slimit (optional), minifies JS files.

View File

@ -1,22 +1,29 @@
include AUTHORS ChangeLog DEPENDS ez_setup.py LICENSE msgfmt.py RELEASE-VERSION version.py
include AUTHORS ChangeLog DEPENDS LICENSE RELEASE-VERSION README.rst
include msgfmt.py minify_web_js.py version.py
exclude setup.cfg
graft docs/man
include deluge/i18n/*.po
recursive-exclude deluge/i18n LC_MESSAGES *.mo
graft deluge/plugins
recursive-exclude deluge/plugins create_dev_link.sh *.pyc
recursive-exclude deluge/plugins create_dev_link.sh *.pyc *.egg
prune deluge/plugins/*/build
prune deluge/plugins/*/*.egg-info
graft deluge/tests/data
graft deluge/tests/twisted
prune deluge/tests
graft deluge/ui/data
recursive-exclude deluge/ui/data *.desktop *.xml
graft deluge/ui/gtkui/glade
include deluge/ui/web/index.html
include deluge/ui/web/gettext.js
include deluge/ui/web/css/*.css
exclude deluge/ui/web/css/*-debug.css
include deluge/ui/web/js/*.js
exclude deluge/ui/web/js/*-debug.js
exclude deluge/ui/web/gen_gettext.py
graft deluge/ui/web/js/deluge-all/
graft deluge/ui/web/js/extjs/
graft deluge/ui/web/themes
graft deluge/ui/web/render
graft deluge/ui/web/icons

View File

@ -1,6 +1,8 @@
==========================
=========================
Deluge BitTorrent Client
==========================
=========================
|build-status| |docs|
Homepage: http://deluge-torrent.org
@ -19,39 +21,48 @@ For detailed instructions see: http://dev.deluge-torrent.org/wiki/Installing/Sou
Ensure build dependencies are installed, see DEPENDS for a full listing.
Build and install by running:
Build and install by running::
$ python setup.py build
$ sudo python setup.py install
==========================
================
Contact/Support:
==========================
================
Forum: http://forum.deluge-torrent.org
IRC Channel: #deluge on irc.freenode.net
:Forum: http://forum.deluge-torrent.org
:IRC Channel: #deluge on irc.freenode.net
==========================
===
FAQ
==========================
===
For the full FAQ see: http://dev.deluge-torrent.org/wiki/Faq
How to start the various user-interfaces
How to start the various user-interfaces:
Gtk::
Gtk:
deluge or deluge-gtk
Console:
Console::
deluge-console
Web:
Web::
deluge-web
Go to http://localhost:8112/ default-password = "deluge"
How do I start the daemon?
How do I start the daemon?:
deluged
I can't connect to the daemon from another machine
I can't connect to the daemon from another machine:
See: http://dev.deluge-torrent.org/wiki/UserGuide/ThinClient
.. |build-status| image:: https://travis-ci.org/deluge-torrent/deluge.svg
:target: https://travis-ci.org/deluge-torrent/deluge
.. |docs| image:: https://readthedocs.org/projects/deluge/badge/?version=develop
:target: https://readthedocs.org/projects/deluge/?badge=develop
:alt: Documentation Status

View File

@ -1,21 +0,0 @@
#!/bin/sh
# Fixes glade files which may have set gtk stock labels set to translatable
for x in `find . -name '*.glade' |grep -v '.git\|build'` ; do \
for y in gtk-add gtk-apply gtk-bold gtk-cancel gtk-cdrom gtk-clear \
gtk-close gtk-color-picker gtk-connect gtk-convert gtk-copy gtk-cut \
gtk-delete gtk-dialog-error gtk-dialog-info gtk-dialog-question \
gtk-dialog-warning gtk-dnd gtk-dnd-multiple gtk-edit gtk-execute gtk-find \
gtk-find-and-replace gtk-floppy gtk-goto-bottom gtk-goto-first \
gtk-goto-last gtk-goto-top gtk-go-back gtk-go-down gtk-go-forward \
gtk-go-up gtk-help gtk-home gtk-index gtk-italic gtk-jump-to \
gtk-justify-center gtk-justify-fill gtk-justify-left gtk-missing-image \
gtk-new gtk-no gtk-ok gtk-open gtk-paste gtk-preferences gtk-print \
gtk-print-preview gtk-properties gtk-quit gtk-redo gtk-refresh \
gtk-remove gtk-revert-to-saved gtk-save gtk-save-as gtk-select-color \
gtk-select-font gtk-sort-descending gtk-spell-check gtk-stop \
gtk-strikethrough gtk-undelete gtk-underline gtk-undo gtk-yes \
gtk-zoom-100 gtk-zoom-fit gtk-zoom-in gtk-zoom-out; do \
sed -i "s/<property\ name\=\"label\"\ translatable\=\"yes\">$y<\/property>/<property\ name\=\"label\"\ translatable\=\"no\">$y<\/property>/g" $x; \
done;\
done

View File

@ -1,32 +0,0 @@
#!/usr/bin/env python
import os
import re
import sys
# Paths to exclude
EXCLUSIONS = [
"deluge/scripts",
"deluge/i18n",
]
POTFILE_IN = "deluge/i18n/POTFILES.in"
pattern = "deluge\/plugins\/.*\/build"
compiled = re.compile(pattern)
sys.stdout.write("Creating " + POTFILE_IN + " ... ")
sys.stdout.flush()
to_translate = []
for (dirpath, dirnames, filenames) in os.walk("deluge"):
for filename in filenames:
if os.path.splitext(filename)[1] in (".py", ".glade", ".in") \
and dirpath not in EXCLUSIONS \
and not compiled.match(dirpath):
to_translate.append(os.path.join(dirpath, filename))
f = open(POTFILE_IN, "wb")
for line in to_translate:
f.write(line + "\n")
f.close()
print "Done"

View File

@ -1,6 +0,0 @@
#!/bin/bash
for size in 16 22 24 32 36 48 64 72 96 128 192 256; do mkdir -p deluge/ui/data/\
icons/hicolor/${size}x${size}/apps; rsvg-convert -w ${size} -h ${size} \
-o deluge/ui/data/icons/hicolor/${size}x${size}/apps/deluge.png deluge/ui/data/pixmaps\
/deluge.svg; mkdir -p deluge/ui/data/icons/scalable/apps/; cp deluge/ui/data/pixmaps/\
deluge.svg deluge/ui/data/icons/scalable/apps/deluge.svg; done

View File

@ -1,4 +1,7 @@
"""Deluge"""
from __future__ import unicode_literals
# this is a namespace package
import pkg_resources
pkg_resources.declare_namespace(__name__)

View File

@ -1,20 +1,33 @@
from new import classobj
# -*- coding: utf-8 -*-
#
# Copyright (C) 2009 Damien Churchill <damoxc@gmail.com>
#
# This file is part of Deluge and is licensed under GNU General Public License 3.0, or later, with
# the additional special exception to link portions of this program with the OpenSSL library.
# See LICENSE for more details.
#
from __future__ import unicode_literals
from deluge.core.core import Core
from deluge.core.daemon import Daemon
class RpcApi:
class RpcApi(object):
pass
def scan_for_methods(obj):
methods = {
'__doc__': 'Methods available in %s' % obj.__name__.lower()
}
for d in dir(obj):
if not hasattr(getattr(obj,d), '_rpcserver_export'):
if not hasattr(getattr(obj, d), '_rpcserver_export'):
continue
methods[d] = getattr(obj, d)
cobj = classobj(obj.__name__.lower(), (object,), methods)
cobj = type(obj.__name__.lower(), (object,), methods)
setattr(RpcApi, obj.__name__.lower(), cobj)
scan_for_methods(Core)
scan_for_methods(Daemon)
scan_for_methods(Daemon)

View File

@ -1,60 +1,30 @@
#
# _libtorrent.py
# -*- coding: utf-8 -*-
#
# 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.
#
# This file is part of Deluge and is licensed under GNU General Public License 3.0, or later, with
# the additional special exception to link portions of this program with the OpenSSL library.
# See LICENSE for more details.
#
"""
This module is used to handle the importing of libtorrent.
This module is used to handle the importing of libtorrent and also controls
the minimum versions of libtorrent that this version of Deluge supports.
We use this module to control what versions of libtorrent this version of Deluge
supports.
** Usage **
>>> from deluge._libtorrent import lt
Example:
>>> from deluge._libtorrent import lt
"""
from __future__ import unicode_literals
REQUIRED_VERSION = "0.16.7.0"
def check_version(lt):
from deluge.common import VersionSplit
if VersionSplit(lt.version) < VersionSplit(REQUIRED_VERSION):
raise ImportError("This version of Deluge requires libtorrent >=%s!" % REQUIRED_VERSION)
from deluge.common import VersionSplit, get_version
try:
import deluge.libtorrent as lt
check_version(lt)
except ImportError:
import libtorrent as lt
check_version(lt)
REQUIRED_VERSION = '1.1.2.0'
if VersionSplit(lt.__version__) < VersionSplit(REQUIRED_VERSION):
raise ImportError('Deluge %s requires libtorrent >= %s' % (get_version(), REQUIRED_VERSION))

View File

@ -9,66 +9,83 @@
# License.
# Written by Petru Paler
# Updated by Calum Lind to support both Python 2 and Python 3.
from sys import version_info
PY2 = version_info.major == 2
class BTFailure(Exception):
pass
DICT_DELIM = b'd'
END_DELIM = b'e'
INT_DELIM = b'i'
LIST_DELIM = b'l'
BYTE_SEP = b':'
# Minor modifications made by Andrew Resch to replace the BTFailure errors with Exceptions
def decode_int(x, f):
f += 1
newf = x.index('e', f)
newf = x.index(END_DELIM, f)
n = int(x[f:newf])
if x[f] == '-':
if x[f + 1] == '0':
raise ValueError
elif x[f] == '0' and newf != f+1:
if x[f:f+1] == b'-' and x[f+1:f+2] == b'0':
raise ValueError
return (n, newf+1)
elif x[f:f+1] == b'0' and newf != f + 1:
raise ValueError
return (n, newf + 1)
def decode_string(x, f):
colon = x.index(':', f)
colon = x.index(BYTE_SEP, f)
n = int(x[f:colon])
if x[f] == '0' and colon != f+1:
if x[f:f+1] == b'0' and colon != f + 1:
raise ValueError
colon += 1
return (x[colon:colon+n], colon+n)
return (x[colon:colon + n], colon + n)
def decode_list(x, f):
r, f = [], f+1
while x[f] != 'e':
v, f = decode_func[x[f]](x, f)
r, f = [], f + 1
while x[f:f+1] != END_DELIM:
v, f = decode_func[x[f:f+1]](x, f)
r.append(v)
return (r, f + 1)
def decode_dict(x, f):
r, f = {}, f+1
while x[f] != 'e':
r, f = {}, f + 1
while x[f:f+1] != END_DELIM:
k, f = decode_string(x, f)
r[k], f = decode_func[x[f]](x, f)
r[k], f = decode_func[x[f:f+1]](x, f)
return (r, f + 1)
decode_func = {}
decode_func['l'] = decode_list
decode_func['d'] = decode_dict
decode_func['i'] = decode_int
decode_func['0'] = decode_string
decode_func['1'] = decode_string
decode_func['2'] = decode_string
decode_func['3'] = decode_string
decode_func['4'] = decode_string
decode_func['5'] = decode_string
decode_func['6'] = decode_string
decode_func['7'] = decode_string
decode_func['8'] = decode_string
decode_func['9'] = decode_string
decode_func[LIST_DELIM] = decode_list
decode_func[DICT_DELIM] = decode_dict
decode_func[INT_DELIM] = decode_int
decode_func[b'0'] = decode_string
decode_func[b'1'] = decode_string
decode_func[b'2'] = decode_string
decode_func[b'3'] = decode_string
decode_func[b'4'] = decode_string
decode_func[b'5'] = decode_string
decode_func[b'6'] = decode_string
decode_func[b'7'] = decode_string
decode_func[b'8'] = decode_string
decode_func[b'9'] = decode_string
def bdecode(x):
try:
r, l = decode_func[x[0]](x, 0)
r, l = decode_func[x[0:1]](x, 0)
except (IndexError, KeyError, ValueError):
raise Exception("not a valid bencoded string")
return r
from types import StringType, IntType, LongType, DictType, ListType, TupleType
raise BTFailure('Not a valid bencoded string')
else:
return r
class Bencached(object):
@ -78,52 +95,58 @@ class Bencached(object):
def __init__(self, s):
self.bencoded = s
def encode_bencached(x,r):
def encode_bencached(x, r):
r.append(x.bencoded)
def encode_int(x, r):
r.extend(('i', str(x), 'e'))
r.extend((INT_DELIM, str(x).encode('utf8'), END_DELIM))
def encode_bool(x, r):
if x:
encode_int(1, r)
else:
encode_int(0, r)
encode_int(1 if x else 0, r)
def encode_string(x, r):
r.extend((str(len(x)), ':', x))
encode_string(x.encode('utf8'), r)
def encode_bytes(x, r):
r.extend((str(len(x)).encode('utf8'), BYTE_SEP, x))
def encode_list(x, r):
r.append('l')
r.append(LIST_DELIM)
for i in x:
encode_func[type(i)](i, r)
r.append('e')
r.append(END_DELIM)
def encode_dict(x,r):
r.append('d')
ilist = x.items()
ilist.sort()
for k, v in ilist:
r.extend((str(len(k)), ':', k))
def encode_dict(x, r):
r.append(DICT_DELIM)
for k, v in sorted(x.items()):
r.extend((str(len(k)).encode('utf8'), BYTE_SEP, k))
encode_func[type(v)](v, r)
r.append('e')
r.append(END_DELIM)
encode_func = {}
encode_func[Bencached] = encode_bencached
encode_func[IntType] = encode_int
encode_func[LongType] = encode_int
encode_func[StringType] = encode_string
encode_func[ListType] = encode_list
encode_func[TupleType] = encode_list
encode_func[DictType] = encode_dict
encode_func[int] = encode_int
encode_func[list] = encode_list
encode_func[tuple] = encode_list
encode_func[dict] = encode_dict
encode_func[bool] = encode_bool
encode_func[str] = encode_string
encode_func[bytes] = encode_bytes
if PY2:
encode_func[long] = encode_int
encode_func[str] = encode_bytes
encode_func[unicode] = encode_string
try:
from types import BooleanType
encode_func[BooleanType] = encode_bool
except ImportError:
pass
def bencode(x):
r = []
encode_func[type(x)](x, r)
return ''.join(r)
return b''.join(r)

File diff suppressed because it is too large Load Diff

View File

@ -1,51 +1,55 @@
#
# component.py
# -*- coding: utf-8 -*-
#
# Copyright (C) 2007-2010 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.
#
# This file is part of Deluge and is licensed under GNU General Public License 3.0, or later, with
# the additional special exception to link portions of this program with the OpenSSL library.
# See LICENSE for more details.
#
from __future__ import unicode_literals
import logging
import traceback
from collections import defaultdict
from twisted.internet.defer import maybeDeferred, succeed, DeferredList, fail
from twisted.internet.task import LoopingCall
from twisted.internet import reactor
from twisted.internet.defer import DeferredList, fail, maybeDeferred, succeed
from twisted.internet.task import LoopingCall, deferLater
from deluge.common import PY2
log = logging.getLogger(__name__)
class ComponentAlreadyRegistered(Exception):
pass
class ComponentException(Exception):
def __init__(self, message, tb):
super(ComponentException, self).__init__(message)
self.message = message
self.tb = tb
def __str__(self):
s = super(ComponentException, self).__str__()
return '%s\n%s' % (s, ''.join(self.tb))
def __eq__(self, other):
if isinstance(other, self.__class__):
return self.message == other.message
else:
return False
def __ne__(self, other):
return not self.__eq__(other)
class Component(object):
"""
Component objects are singletons managed by the :class:`ComponentRegistry`.
"""Component objects are singletons managed by the :class:`ComponentRegistry`.
When a new Component object is instantiated, it will be automatically
registered with the :class:`ComponentRegistry`.
@ -90,10 +94,18 @@ class Component(object):
"""
def __init__(self, name, interval=1, depend=None):
"""Initialize component.
Args:
name (str): Name of component.
interval (int, optional): The interval in seconds to call the update function.
depend (list, optional): The names of components this component depends on.
"""
self._component_name = name
self._component_interval = interval
self._component_depend = depend
self._component_state = "Stopped"
self._component_state = 'Stopped'
self._component_timer = None
self._component_starting_deferred = None
self._component_stopping_deferred = None
@ -104,57 +116,58 @@ class Component(object):
_ComponentRegistry.deregister(self)
def _component_start_timer(self):
if hasattr(self, "update"):
if hasattr(self, 'update'):
self._component_timer = LoopingCall(self.update)
self._component_timer.start(self._component_interval)
def _component_start(self):
def on_start(result):
self._component_state = "Started"
self._component_state = 'Started'
self._component_starting_deferred = None
self._component_start_timer()
return True
def on_start_fail(result):
self._component_state = "Stopped"
self._component_state = 'Stopped'
self._component_starting_deferred = None
log.error(result)
return result
return fail(result)
if self._component_state == "Stopped":
if hasattr(self, "start"):
self._component_state = "Starting"
d = maybeDeferred(self.start)
d.addCallback(on_start)
d.addErrback(on_start_fail)
if self._component_state == 'Stopped':
if hasattr(self, 'start'):
self._component_state = 'Starting'
d = deferLater(reactor, 0, self.start)
d.addCallbacks(on_start, on_start_fail)
self._component_starting_deferred = d
else:
d = maybeDeferred(on_start, None)
elif self._component_state == "Starting":
elif self._component_state == 'Starting':
return self._component_starting_deferred
elif self._component_state == "Started":
elif self._component_state == 'Started':
d = succeed(True)
else:
d = fail("Cannot start a component not in a Stopped state!")
d = fail(ComponentException('Trying to start component "%s" but it is '
'not in a stopped state. Current state: %s' %
(self._component_name, self._component_state),
traceback.format_stack(limit=4)))
return d
def _component_stop(self):
def on_stop(result):
self._component_state = "Stopped"
self._component_state = 'Stopped'
if self._component_timer and self._component_timer.running:
self._component_timer.stop()
return True
def on_stop_fail(result):
self._component_state = "Started"
self._component_state = 'Started'
self._component_stopping_deferred = None
log.error(result)
return result
if self._component_state != "Stopped" and self._component_state != "Stopping":
if hasattr(self, "stop"):
self._component_state = "Stopping"
if self._component_state != 'Stopped' and self._component_state != 'Stopping':
if hasattr(self, 'stop'):
self._component_state = 'Stopping'
d = maybeDeferred(self.stop)
d.addCallback(on_stop)
d.addErrback(on_stop_fail)
@ -162,43 +175,47 @@ class Component(object):
else:
d = maybeDeferred(on_stop, None)
if self._component_state == "Stopping":
if self._component_state == 'Stopping':
return self._component_stopping_deferred
return succeed(None)
def _component_pause(self):
def on_pause(result):
self._component_state = "Paused"
self._component_state = 'Paused'
if self._component_state == "Started":
if self._component_state == 'Started':
if self._component_timer and self._component_timer.running:
d = maybeDeferred(self._component_timer.stop)
d.addCallback(on_pause)
else:
d = succeed(None)
elif self._component_state == "Paused":
elif self._component_state == 'Paused':
d = succeed(None)
else:
d = fail("Cannot pause a component in a non-Started state!")
d = fail(ComponentException('Trying to pause component "%s" but it is '
'not in a started state. Current state: %s' %
(self._component_name, self._component_state),
traceback.format_stack(limit=4)))
return d
def _component_resume(self):
def on_resume(result):
self._component_state = "Started"
self._component_state = 'Started'
if self._component_state == "Paused":
if self._component_state == 'Paused':
d = maybeDeferred(self._component_start_timer)
d.addCallback(on_resume)
else:
d = fail("Component cannot be resumed from a non-Paused state!")
d = fail(ComponentException('Trying to resume component "%s" but it is '
'not in a paused state. Current state: %s' %
(self._component_name, self._component_state),
traceback.format_stack(limit=4)))
return d
def _component_shutdown(self):
def on_stop(result):
if hasattr(self, "shutdown"):
if hasattr(self, 'shutdown'):
return maybeDeferred(self.shutdown)
return succeed(None)
@ -206,6 +223,9 @@ class Component(object):
d.addCallback(on_stop)
return d
def get_state(self):
return self._component_state
def start(self):
pass
@ -218,11 +238,11 @@ class Component(object):
def shutdown(self):
pass
class ComponentRegistry(object):
"""
The ComponentRegistry holds a list of currently registered
:class:`Component` objects. It is used to manage the Components by
starting, stopping, pausing and shutting them down.
"""The ComponentRegistry holds a list of currently registered :class:`Component` objects.
It is used to manage the Components by starting, stopping, pausing and shutting them down.
"""
def __init__(self):
self.components = {}
@ -230,20 +250,21 @@ class ComponentRegistry(object):
self.dependents = defaultdict(list)
def register(self, obj):
"""
Registers a component object with the registry. This is done
automatically when a Component object is instantiated.
"""Register a component object with the registry.
:param obj: the Component object
:type obj: object
Note:
This is done automatically when a Component object is instantiated.
:raises ComponentAlreadyRegistered: if a component with the same name is already registered.
Args:
obj (Component): A component object to register.
Raises:
ComponentAlreadyRegistered: If a component with the same name is already registered.
"""
name = obj._component_name
if name in self.components:
raise ComponentAlreadyRegistered(
"Component already registered with name %s" % name)
raise ComponentAlreadyRegistered('Component already registered with name %s' % name)
self.components[obj._component_name] = obj
if obj._component_depend:
@ -251,42 +272,44 @@ class ComponentRegistry(object):
self.dependents[depend].append(name)
def deregister(self, obj):
"""
Deregisters a component from the registry. A stop will be
"""Deregister a component from the registry. A stop will be
issued to the component prior to deregistering it.
:param obj: the Component object
:type obj: object
Args:
obj (Component): a component object to deregister
Returns:
Deferred: a deferred object that will fire once the Component has been sucessfully deregistered
"""
if obj in self.components.values():
log.debug("Deregistering Component: %s", obj._component_name)
log.debug('Deregistering Component: %s', obj._component_name)
d = self.stop([obj._component_name])
def on_stop(result, name):
del self.components[name]
# Component may have been removed, so pop to ensure it doesn't fail
self.components.pop(name, None)
return d.addCallback(on_stop, obj._component_name)
else:
return succeed(None)
def start(self, names=[]):
"""
Starts Components that are currently in a Stopped state and their
dependencies. If *names* is specified, will only start those
Components and their dependencies and if not it will start all
registered components.
def start(self, names=None):
"""Start Components, and their dependencies, that are currently in a Stopped state.
:param names: a list of Components to start
:type names: list
Note:
If no names are specified then all registered components will be started.
:returns: a Deferred object that will fire once all Components have been sucessfully started
:rtype: twisted.internet.defer.Deferred
Args:
names (list): A list of Components to start and their dependencies.
Returns:
Deferred: Fired once all Components have been successfully started.
"""
# Start all the components if names is empty
if not names:
names = self.components.keys()
elif isinstance(names, str):
names = list(self.components)
elif isinstance(names, str if not PY2 else basestring):
names = [names]
def on_depends_started(result, name):
@ -305,22 +328,22 @@ class ComponentRegistry(object):
return DeferredList(deferreds)
def stop(self, names=[]):
"""
Stops Components that are currently not in a Stopped state. If
*names* is specified, then it will only stop those Components,
and if not it will stop all the registered Components.
def stop(self, names=None):
"""Stop Components that are currently not in a Stopped state.
:param names: a list of Components to start
:type names: list
Note:
If no names are specified then all registered components will be stopped.
:returns: a Deferred object that will fire once all Components have been sucessfully stopped
:rtype: twisted.internet.defer.Deferred
Args:
names (list): A list of Components to stop.
Returns:
Deferred: Fired once all Components have been successfully stopped.
"""
if not names:
names = self.components.keys()
elif isinstance(names, str):
names = list(self.components)
elif isinstance(names, str if not PY2 else basestring):
names = [names]
def on_dependents_stopped(result, name):
@ -343,81 +366,81 @@ class ComponentRegistry(object):
return DeferredList(deferreds)
def pause(self, names=[]):
"""
Pauses Components that are currently in a Started state. If
*names* is specified, then it will only pause those Components,
and if not it will pause all the registered Components.
def pause(self, names=None):
"""Pause Components that are currently in a Started state.
:param names: a list of Components to pause
:type names: list
Note:
If no names are specified then all registered components will be paused.
:returns: a Deferred object that will fire once all Components have been sucessfully paused
:rtype: twisted.internet.defer.Deferred
Args:
names (list): A list of Components to pause.
Returns:
Deferred: Fired once all Components have been successfully paused.
"""
if not names:
names = self.components.keys()
elif isinstance(names, str):
names = list(self.components)
elif isinstance(names, str if not PY2 else basestring):
names = [names]
deferreds = []
for name in names:
if self.components[name]._component_state == "Started":
if self.components[name]._component_state == 'Started':
deferreds.append(self.components[name]._component_pause())
return DeferredList(deferreds)
def resume(self, names=[]):
"""
Resumes Components that are currently in a Paused state. If
*names* is specified, then it will only resume those Components,
and if not it will resume all the registered Components.
def resume(self, names=None):
"""Resume Components that are currently in a Paused state.
:param names: a list of Components to resume
:type names: list
Note:
If no names are specified then all registered components will be resumed.
:returns: a Deferred object that will fire once all Components have been successfully resumed
:rtype: twisted.internet.defer.Deferred
Args:
names (list): A list of Components to to resume.
Returns:
Deferred: Fired once all Components have been successfully resumed.
"""
if not names:
names = self.components.keys()
elif isinstance(names, str):
names = list(self.components)
elif isinstance(names, str if not PY2 else basestring):
names = [names]
deferreds = []
for name in names:
if self.components[name]._component_state == "Paused":
if self.components[name]._component_state == 'Paused':
deferreds.append(self.components[name]._component_resume())
return DeferredList(deferreds)
def shutdown(self):
"""
Shutdowns all Components regardless of state. This will call
:meth:`stop` on call the components prior to shutting down. This should
be called when the program is exiting to ensure all Components have a
chance to properly shutdown.
"""Shutdown all Components regardless of state.
:returns: a Deferred object that will fire once all Components have been successfully shut down
:rtype: twisted.internet.defer.Deferred
This will call stop() on all the components prior to shutting down. This should be called
when the program is exiting to ensure all Components have a chance to properly shutdown.
Returns:
Deferred: Fired once all Components have been successfully shut down.
"""
def on_stopped(result):
return DeferredList(map(lambda c: c._component_shutdown(), self.components.values()))
return DeferredList([comp._component_shutdown() for comp in self.components.values()])
return self.stop(self.components.keys()).addCallback(on_stopped)
return self.stop(list(self.components)).addCallback(on_stopped)
def update(self):
"""
Updates all Components that are in a Started state.
"""
"""Update all Components that are in a Started state."""
for component in self.components.items():
component.update()
try:
component.update()
except BaseException as ex:
log.exception(ex)
_ComponentRegistry = ComponentRegistry()
@ -429,17 +452,18 @@ resume = _ComponentRegistry.resume
update = _ComponentRegistry.update
shutdown = _ComponentRegistry.shutdown
def get(name):
"""
Return a reference to a component.
"""Return a reference to a component.
:param name: the Component name to get
:type name: string
Args:
name (str): The Component name to get.
:returns: the Component object
:rtype: object
Returns:
Component: The Component object.
:raises KeyError: if the Component does not exist
Raises:
KeyError: If the Component does not exist.
"""
return _ComponentRegistry.components[name]

View File

@ -1,38 +1,11 @@
#
# config.py
# -*- coding: utf-8 -*-
#
# Copyright (C) 2008 Andrew Resch <andrewresch@gmail.com>
#
# Deluge is free software.
# This file is part of Deluge and is licensed under GNU General Public License 3.0, or later, with
# the additional special exception to link portions of this program with the OpenSSL library.
# See LICENSE for more details.
#
# 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.
#
#
"""
Deluge Config Module
@ -66,81 +39,91 @@ this can only be done for the 'config file version' and not for the 'format'
version as this will be done internally.
"""
from __future__ import unicode_literals
import cPickle as pickle
import json
import logging
import shutil
import os
import shutil
from codecs import getwriter
from io import open
import deluge.common
json = deluge.common.json
from deluge.common import JSON_FORMAT, get_default_config_dir
log = logging.getLogger(__name__)
callLater = None # Necessary for the config tests
def prop(func):
"""Function decorator for defining property attributes
The decorated function is expected to return a dictionary
containing one or more of the following pairs:
fget - function for getting attribute value
fset - function for setting attribute value
fdel - function for deleting attribute
This can be conveniently constructed by the locals() builtin
function; see:
http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/205183
"""
return property(doc=func.__doc__, **func())
def find_json_objects(s):
"""
Find json objects in a string.
"""Find json objects in a string.
:param s: the string to find json objects in
:type s: string
Args:
s (str): the string to find json objects in
:returns: a list of tuples containing start and end locations of json objects in the string `s`
:rtype: [(start, end), ...]
Returns:
list: A list of tuples containing start and end locations of json
objects in string `s`. e.g. [(start, end), ...]
"""
objects = []
opens = 0
start = s.find("{")
start = s.find('{')
offset = start
if start < 0:
return []
for index, c in enumerate(s[offset:]):
if c == "{":
if c == '{':
opens += 1
elif c == "}":
elif c == '}':
opens -= 1
if opens == 0:
objects.append((start, index+offset+1))
objects.append((start, index + offset + 1))
start = index + offset + 1
return objects
class Config(object):
"""
This class is used to access/create/modify config files
"""This class is used to access/create/modify config files.
:param filename: the name of the config file
:param defaults: dictionary of default values
:param config_dir: the path to the config directory
Args:
filename (str): The config filename.
defaults (dict): The default config values to insert before loading the config file.
config_dir (str): the path to the config directory.
file_version (int): The file format for the default config values when creating
a fresh config. This value should be increased whenever a new migration function is
setup to convert old config files. (default: 1)
"""
def __init__(self, filename, defaults=None, config_dir=None):
def __init__(self, filename, defaults=None, config_dir=None, file_version=1):
self.__config = {}
self.__set_functions = {}
self.__change_callbacks = []
# These hold the version numbers and they will be set when loaded
self.__version = {
"format": 1,
"file": 1
'format': 1,
'file': file_version
}
# This will get set with a reactor.callLater whenever a config option
@ -148,14 +131,14 @@ class Config(object):
self._save_timer = None
if defaults:
for key, value in defaults.iteritems():
for key, value in defaults.items():
self.set_item(key, value)
# Load the config from file in the config_dir
if config_dir:
self.__config_file = os.path.join(config_dir, filename)
else:
self.__config_file = deluge.common.get_default_config_dir(filename)
self.__config_file = get_default_config_dir(filename)
self.load()
@ -163,110 +146,129 @@ class Config(object):
return item in self.__config
def __setitem__(self, key, value):
"""
See
:meth:`set_item`
"""
"""See set_item"""
return self.set_item(key, value)
def set_item(self, key, value):
"""
Sets item 'key' to 'value' in the config dictionary, but does not allow
changing the item's type unless it is None. If the types do not match,
it will attempt to convert it to the set type before raising a ValueError.
"""Sets item 'key' to 'value' in the config dictionary.
:param key: string, item to change to change
:param value: the value to change item to, must be same type as what is currently in the config
Does not allow changing the item's type unless it is None.
:raises ValueError: raised when the type of value is not the same as\
what is currently in the config and it could not convert the value
If the types do not match, it will attempt to convert it to the
set type before raising a ValueError.
**Usage**
Args:
key (str): Item to change to change.
value (any): The value to change item to, must be same type as what is
currently in the config.
>>> config = Config("test.conf")
>>> config["test"] = 5
>>> config["test"]
5
Raises:
ValueError: Raised when the type of value is not the same as what is
currently in the config and it could not convert the value.
Examples:
>>> config = Config('test.conf')
>>> config['test'] = 5
>>> config['test']
5
"""
if isinstance(value, basestring):
value = deluge.common.utf8_encoded(value)
if not self.__config.has_key(key):
if key not in self.__config:
self.__config[key] = value
log.debug("Setting '%s' to %s of %s", key, value, type(value))
log.debug('Setting key "%s" to: %s (of type: %s)', key, value, type(value))
return
if self.__config[key] == value:
return
# Do not allow the type to change unless it is None
oldtype, newtype = type(self.__config[key]), type(value)
if value is not None and oldtype != type(None) and oldtype != newtype:
if value is not None and not isinstance(
self.__config[key], type(None)) and not isinstance(self.__config[key], type(value)):
try:
if oldtype == unicode:
value = oldtype(value, "utf8")
else:
value = oldtype(value)
oldtype = type(self.__config[key])
value = oldtype(value)
except ValueError:
log.warning("Type '%s' invalid for '%s'", newtype, key)
log.warning('Value Type "%s" invalid for key: %s', type(value), key)
raise
log.debug("Setting '%s' to %s of %s", key, value, type(value))
if isinstance(value, bytes):
value.decode('utf8')
log.debug('Setting key "%s" to: %s (of type: %s)', key, value, type(value))
self.__config[key] = value
global callLater
if callLater is None:
# Must import here and not at the top or it will throw ReactorAlreadyInstalledError
from twisted.internet.reactor import callLater # pylint: disable=redefined-outer-name
# Run the set_function for this key if any
from twisted.internet import reactor
try:
for func in self.__set_functions[key]:
reactor.callLater(0, func, key, value)
callLater(0, func, key, value)
except KeyError:
pass
try:
def do_change_callbacks(key, value):
for func in self.__change_callbacks:
func(key, value)
reactor.callLater(0, do_change_callbacks, key, value)
except:
callLater(0, do_change_callbacks, key, value)
except Exception:
pass
# We set the save_timer for 5 seconds if not already set
if not self._save_timer or not self._save_timer.active():
self._save_timer = reactor.callLater(5, self.save)
self._save_timer = callLater(5, self.save)
def __getitem__(self, key):
"""
See
:meth:`get_item`
"""
"""See get_item """
return self.get_item(key)
def get_item(self, key):
"""
Gets the value of item 'key'
"""Gets the value of item 'key'.
:param key: the item for which you want it's value
:return: the value of item 'key'
Args:
key (str): The item for which you want it's value.
:raises KeyError: if 'key' is not in the config dictionary
Returns:
any: The value of item 'key'.
**Usage**
Raises:
ValueError: If 'key' is not in the config dictionary.
>>> config = Config("test.conf", defaults={"test": 5})
>>> config["test"]
5
Examples:
>>> config = Config('test.conf', defaults={'test': 5})
>>> config['test']
5
"""
if isinstance(self.__config[key], str):
try:
return self.__config[key].decode("utf8")
except UnicodeDecodeError:
return self.__config[key]
else:
return self.__config[key]
return self.__config[key]
def get(self, key, default=None):
"""Gets the value of item 'key' if key is in the config, else default.
If default is not given, it defaults to None, so that this method
never raises a KeyError.
Args:
key (str): the item for which you want it's value
default (any): the default value if key is missing
Returns:
any: The value of item 'key' or default.
Examples:
>>> config = Config('test.conf', defaults={'test': 5})
>>> config.get('test', 10)
5
>>> config.get('bad_key', 10)
10
"""
try:
return self.get_item(key)
except KeyError:
return default
def __delitem__(self, key):
"""
@ -276,59 +278,67 @@ what is currently in the config and it could not convert the value
self.del_item(key)
def del_item(self, key):
"""
Deletes item with a specific key from the configuration.
"""Deletes item with a specific key from the configuration.
:param key: the item which you wish to delete.
:raises KeyError: if 'key' is not in the config dictionary
Args:
key (str): The item which you wish to delete.
Raises:
ValueError: If 'key' is not in the config dictionary.
Examples:
>>> config = Config('test.conf', defaults={'test': 5})
>>> del config['test']
**Usage**
>>> config = Config("test.conf", defaults={"test": 5})
>>> del config["test"]
"""
del self.__config[key]
# We set the save_timer for 5 seconds if not already set
from twisted.internet import reactor
if not self._save_timer or not self._save_timer.active():
self._save_timer = reactor.callLater(5, self.save)
global callLater
if callLater is None:
# Must import here and not at the top or it will throw ReactorAlreadyInstalledError
from twisted.internet.reactor import callLater # pylint: disable=redefined-outer-name
# We set the save_timer for 5 seconds if not already set
if not self._save_timer or not self._save_timer.active():
self._save_timer = callLater(5, self.save)
def register_change_callback(self, callback):
"""
Registers a callback function that will be called when a value is changed in the config dictionary
"""Registers a callback function for any changed value.
:param callback: the function, callback(key, value)
Will be called when any value is changed in the config dictionary.
**Usage**
Args:
callback (func): The function to call with parameters: f(key, value).
>>> config = Config("test.conf", defaults={"test": 5})
>>> def cb(key, value):
... print key, value
...
>>> config.register_change_callback(cb)
Examples:
>>> config = Config('test.conf', defaults={'test': 5})
>>> def cb(key, value):
... print key, value
...
>>> config.register_change_callback(cb)
"""
self.__change_callbacks.append(callback)
def register_set_function(self, key, function, apply_now=True):
"""
Register a function to be called when a config value changes
"""Register a function to be called when a config value changes.
:param key: the item to monitor for change
:param function: the function to call when the value changes, f(key, value)
:keyword apply_now: if True, the function will be called after it's registered
Args:
key (str): The item to monitor for change.
function (func): The function to call when the value changes, f(key, value).
apply_now (bool): If True, the function will be called immediately after it's registered.
**Usage**
>>> config = Config("test.conf", defaults={"test": 5})
>>> def cb(key, value):
... print key, value
...
>>> config.register_set_function("test", cb, apply_now=True)
test 5
Examples:
>>> config = Config('test.conf', defaults={'test': 5})
>>> def cb(key, value):
... print key, value
...
>>> config.register_set_function('test', cb, apply_now=True)
test 5
"""
log.debug("Registering function for %s key..", key)
log.debug('Registering function for %s key..', key)
if key not in self.__set_functions:
self.__set_functions[key] = []
@ -340,52 +350,50 @@ what is currently in the config and it could not convert the value
return
def apply_all(self):
"""
Calls all set functions
"""Calls all set functions.
**Usage**
>>> config = Config("test.conf", defaults={"test": 5})
>>> def cb(key, value):
... print key, value
...
>>> config.register_set_function("test", cb, apply_now=False)
>>> config.apply_all()
test 5
Examples:
>>> config = Config('test.conf', defaults={'test': 5})
>>> def cb(key, value):
... print key, value
...
>>> config.register_set_function('test', cb, apply_now=False)
>>> config.apply_all()
test 5
"""
log.debug("Calling all set functions..")
for key, value in self.__set_functions.iteritems():
log.debug('Calling all set functions..')
for key, value in self.__set_functions.items():
for func in value:
func(key, self.__config[key])
def apply_set_functions(self, key):
"""
Calls set functions for `:param:key`.
"""Calls set functions for `:param:key`.
:param key: str, the config key
Args:
key (str): the config key
"""
log.debug("Calling set functions for key %s..", key)
log.debug('Calling set functions for key %s..', key)
if key in self.__set_functions:
for func in self.__set_functions[key]:
func(key, self.__config[key])
def load(self, filename=None):
"""
Load a config file
:param filename: if None, uses filename set in object initialization
"""Load a config file.
Args:
filename (str): If None, uses filename set in object initialization
"""
if not filename:
filename = self.__config_file
try:
data = open(filename, "rb").read()
except IOError, e:
log.warning("Unable to open config file %s: %s", filename, e)
with open(filename, 'r', encoding='utf8') as _file:
data = _file.read()
except IOError as ex:
log.warning('Unable to open config file %s: %s', filename, ex)
return
objects = find_json_objects(data)
@ -394,36 +402,37 @@ what is currently in the config and it could not convert the value
# 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)
except Exception as ex:
log.exception(ex)
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)
except Exception as ex:
log.exception(ex)
log.warning('Unable to load config file: %s', filename)
elif len(objects) == 2:
try:
start, end = objects[0]
self.__version.update(json.loads(data[start:end]))
start, end = objects[1]
self.__config.update(json.loads(data[start:end]))
except Exception, e:
log.exception(e)
log.warning("Unable to load config file: %s", filename)
except Exception as ex:
log.exception(ex)
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)
log.debug('Config %s version: %s.%s loaded: %s', filename,
self.__version['format'], self.__version['file'], self.__config)
def save(self, filename=None):
"""
Save configuration to disk
"""Save configuration to disk.
:param filename: if None, uses filename set in object initiliazation
:rtype bool:
:return: whether or not the save succeeded.
Args:
filename (str): If None, uses filename set in object initialization
Returns:
bool: Whether or not the save succeeded.
"""
if not filename:
@ -431,7 +440,8 @@ what is currently in the config and it could not convert the value
# Check to see if the current config differs from the one on disk
# We will only write a new config file if there is a difference
try:
data = open(filename, "rb").read()
with open(filename, 'r', encoding='utf8') as _file:
data = _file.read()
objects = find_json_objects(data)
start, end = objects[0]
version = json.loads(data[start:end])
@ -442,36 +452,35 @@ what is currently in the config and it could not convert the value
if self._save_timer and self._save_timer.active():
self._save_timer.cancel()
return True
except (IOError, IndexError), e:
log.warning("Unable to open config file: %s because: %s", filename, e)
except (IOError, IndexError) as ex:
log.warning('Unable to open config file: %s because: %s', filename, ex)
# 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.__config, f, indent=2)
f.flush()
os.fsync(f.fileno())
f.close()
except IOError, e:
log.error("Error writing new config file: %s", e)
log.debug('Saving new config file %s', filename + '.new')
with open(filename + '.new', 'wb') as _file:
json.dump(self.__version, getwriter('utf8')(_file), **JSON_FORMAT)
json.dump(self.__config, getwriter('utf8')(_file), **JSON_FORMAT)
_file.flush()
os.fsync(_file.fileno())
except IOError as ex:
log.error('Error writing new config file: %s', ex)
return False
# Make a backup of the old config
try:
log.debug("Backing up old config file to %s~", filename)
shutil.move(filename, filename + "~")
except Exception, e:
log.warning("Unable to backup old config...")
log.debug('Backing up old config file to %s.bak', filename)
shutil.move(filename, filename + '.bak')
except IOError as ex:
log.warning('Unable to backup old config: %s', ex)
# The new config file has been written successfully, so let's move it over
# the existing one.
try:
log.debug("Moving new config file %s to %s..", filename + ".new", filename)
shutil.move(filename + ".new", filename)
except Exception, e:
log.error("Error moving new config file: %s", e)
log.debug('Moving new config file %s to %s..', filename + '.new', filename)
shutil.move(filename + '.new', filename)
except IOError as ex:
log.error('Error moving new config file: %s', ex)
return False
else:
return True
@ -480,36 +489,35 @@ what is currently in the config and it could not convert the value
self._save_timer.cancel()
def run_converter(self, input_range, output_version, func):
"""
Runs a function that will convert file versions in the `:param:input_range`
to the `:param:output_version`.
"""Runs a function that will convert file versions.
:param input_range: tuple, (int, int) the range of input versions this
function will accept
:param output_version: int, the version this function will return
:param func: func, the function that will do the conversion, it will take
the config dict as an argument and return the augmented dict
Args:
input_range (tuple): (int, int) The range of input versions this function will accept.
output_version (int): The version this function will convert to.
func (func): The function that will do the conversion, it will take the config
dict as an argument and return the augmented dict.
:raises ValueError: if the output_version is less than the input_range
Raises:
ValueError: If output_version is less than the input_range.
"""
if output_version in input_range or output_version <= max(input_range):
raise ValueError("output_version needs to be greater than input_range")
raise ValueError('output_version needs to be greater than input_range')
if self.__version["file"] not in input_range:
log.debug("File version %s is not in input_range %s, ignoring converter function..",
self.__version["file"], input_range)
if self.__version['file'] not in input_range:
log.debug('File version %s is not in input_range %s, ignoring converter function..',
self.__version['file'], input_range)
return
try:
self.__config = func(self.__config)
except Exception, e:
log.exception(e)
log.error("There was an exception try to convert config file %s %s to %s",
self.__config_file, self.__version["file"], output_version)
raise e
except Exception as ex:
log.exception(ex)
log.error('There was an exception try to convert config file %s %s to %s',
self.__config_file, self.__version['file'], output_version)
raise ex
else:
self.__version["file"] = output_version
self.__version['file'] = output_version
self.save()
@property
@ -517,10 +525,11 @@ what is currently in the config and it could not convert the value
return self.__config_file
@prop
def config():
def config(): # pylint: disable=no-method-argument
"""The config dictionary"""
def fget(self):
return self.__config
def fdel(self):
return self.save()
return locals()

View File

@ -1,40 +1,16 @@
#
# configmanager.py
# -*- coding: utf-8 -*-
#
# Copyright (C) 2007 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.
#
# This file is part of Deluge and is licensed under GNU General Public License 3.0, or later, with
# the additional special exception to link portions of this program with the OpenSSL library.
# See LICENSE for more details.
#
import os
from __future__ import unicode_literals
import logging
import os
import deluge.common
import deluge.log
@ -42,9 +18,10 @@ from deluge.config import Config
log = logging.getLogger(__name__)
class _ConfigManager:
class _ConfigManager(object):
def __init__(self):
log.debug("ConfigManager started..")
log.debug('ConfigManager started..')
self.config_files = {}
self.__config_directory = None
@ -69,16 +46,19 @@ class _ConfigManager:
if not directory:
return False
log.info("Setting config directory to: %s", directory)
# Ensure absolute dirpath
directory = os.path.abspath(directory)
log.info('Setting config directory to: %s', directory)
if not os.path.exists(directory):
# Try to create the config folder if it doesn't exist
try:
os.makedirs(directory)
except Exception, e:
log.error("Unable to make config directory: %s", e)
except OSError as ex:
log.error('Unable to make config directory: %s', ex)
return False
elif not os.path.isdir(directory):
log.error("Config directory needs to be a directory!")
log.error('Config directory needs to be a directory!')
return False
self.__config_directory = directory
@ -109,30 +89,37 @@ class _ConfigManager:
# We need to return True to keep the timer active
return True
def get_config(self, config_file, defaults=None):
def get_config(self, config_file, defaults=None, file_version=1):
"""Get a reference to the Config object for this filename"""
log.debug("Getting config '%s'", config_file)
log.debug('Getting config: %s', config_file)
# Create the config object if not already created
if config_file not in self.config_files.keys():
self.config_files[config_file] = Config(config_file, defaults, self.config_directory)
if config_file not in self.config_files:
self.config_files[config_file] = Config(config_file, defaults,
config_dir=self.config_directory,
file_version=file_version)
return self.config_files[config_file]
# Singleton functions
_configmanager = _ConfigManager()
def ConfigManager(config, defaults=None):
return _configmanager.get_config(config, defaults)
def ConfigManager(config, defaults=None, file_version=1): # NOQA: N802
return _configmanager.get_config(config, defaults=defaults, file_version=file_version)
def set_config_dir(directory):
"""Sets the config directory, else just uses default"""
return _configmanager.set_config_dir(directory)
return _configmanager.set_config_dir(deluge.common.decode_bytes(directory))
def get_config_dir(filename=None):
if filename != None:
if filename is not None:
return os.path.join(_configmanager.get_config_dir(), filename)
else:
return _configmanager.get_config_dir()
def close(config):
return _configmanager.close(config)

View File

@ -1,83 +1,66 @@
#
# alertmanager.py
# -*- coding: utf-8 -*-
#
# 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.
# This file is part of Deluge and is licensed under GNU General Public License 3.0, or later, with
# the additional special exception to link portions of this program with the OpenSSL library.
# See LICENSE for more details.
#
"""
The AlertManager handles all the libtorrent alerts.
This should typically only be used by the Core. Plugins should utilize the
This should typically only be used by the Core. Plugins should utilize the
`:mod:EventManager` for similar functionality.
"""
from __future__ import unicode_literals
import logging
from twisted.internet import reactor
import deluge.component as component
from deluge._libtorrent import lt
from deluge.common import decode_string
from deluge.common import decode_bytes
log = logging.getLogger(__name__)
class AlertManager(component.Component):
def __init__(self):
log.debug("AlertManager initialized..")
component.Component.__init__(self, "AlertManager", interval=0.05)
self.session = component.get("Core").session
self.session.set_alert_mask(
lt.alert.category_t.error_notification |
lt.alert.category_t.port_mapping_notification |
lt.alert.category_t.storage_notification |
lt.alert.category_t.tracker_notification |
lt.alert.category_t.status_notification |
lt.alert.category_t.ip_block_notification |
lt.alert.category_t.performance_warning)
class AlertManager(component.Component):
"""AlertManager fetches and processes libtorrent alerts"""
def __init__(self):
log.debug('AlertManager init...')
component.Component.__init__(self, 'AlertManager', interval=0.3)
self.session = component.get('Core').session
# Increase the alert queue size so that alerts don't get lost.
self.alert_queue_size = 10000
self.set_alert_queue_size(self.alert_queue_size)
alert_mask = (lt.alert.category_t.error_notification |
lt.alert.category_t.port_mapping_notification |
lt.alert.category_t.storage_notification |
lt.alert.category_t.tracker_notification |
lt.alert.category_t.status_notification |
lt.alert.category_t.ip_block_notification |
lt.alert.category_t.performance_warning)
self.session.apply_settings({'alert_mask': alert_mask})
# handlers is a dictionary of lists {"alert_type": [handler1,h2,..]}
self.handlers = {}
self.delayed_calls = []
self.wait_on_handler = False
def update(self):
self.delayed_calls = [dc for dc in self.delayed_calls if dc.active()]
self.handle_alerts(wait=self.wait_on_handler)
self.handle_alerts()
def stop(self):
for dc in self.delayed_calls:
if dc.active():
dc.cancel()
for delayed_call in self.delayed_calls:
if delayed_call.active():
delayed_call.cancel()
self.delayed_calls = []
def register_handler(self, alert_type, handler):
@ -96,7 +79,7 @@ class AlertManager(component.Component):
# Append the handler to the list in the handlers dictionary
self.handlers[alert_type].append(handler)
log.debug("Registered handler for alert %s", alert_type)
log.debug('Registered handler for alert %s', alert_type)
def deregister_handler(self, handler):
"""
@ -105,30 +88,40 @@ class AlertManager(component.Component):
:param handler: func, the handler function to deregister
"""
# Iterate through all handlers and remove 'handler' where found
for (key, value) in self.handlers.items():
for (dummy_key, value) in self.handlers.items():
if handler in value:
# Handler is in this alert type list
value.remove(handler)
def handle_alerts(self, wait=False):
def handle_alerts(self):
"""
Pops all libtorrent alerts in the session queue and handles them
appropriately.
:param wait: bool, if True then the handler functions will be run right
away and waited to return before processing the next alert
Pops all libtorrent alerts in the session queue and handles them appropriately.
"""
alerts = self.session.pop_alerts()
if not alerts:
return
num_alerts = len(alerts)
if log.isEnabledFor(logging.DEBUG):
log.debug('Alerts queued: %s', num_alerts)
if num_alerts > 0.9 * self.alert_queue_size:
log.warning('Warning total alerts queued, %s, passes 90%% of queue size.', num_alerts)
# Loop through all alerts in the queue
for alert in alerts:
alert_type = type(alert).__name__
# Display the alert message
if log.isEnabledFor(logging.DEBUG):
log.debug("%s: %s", alert_type, decode_string(alert.message()))
log.debug('%s: %s', alert_type, decode_bytes(alert.message()))
# Call any handlers for this alert type
if alert_type in self.handlers:
for handler in self.handlers[alert_type]:
if not wait:
self.delayed_calls.append(reactor.callLater(0, handler, alert))
else:
handler(alert)
if log.isEnabledFor(logging.DEBUG):
log.debug('Handling alert: %s', alert_type)
self.delayed_calls.append(reactor.callLater(0, handler, alert))
def set_alert_queue_size(self, queue_size):
"""Sets the maximum size of the libtorrent alert queue"""
log.info('Alert Queue Size set to %s', queue_size)
self.alert_queue_size = queue_size
component.get('Core').apply_session_setting('alert_queue_size', self.alert_queue_size)

View File

@ -1,52 +1,25 @@
#
# authmanager.py
# -*- coding: utf-8 -*-
#
# Copyright (C) 2009 Andrew Resch <andrewresch@gmail.com>
# Copyright (C) 2011 Pedro Algarvio <pedro@algarvio.me>
#
# 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.
#
# This file is part of Deluge and is licensed under GNU General Public License 3.0, or later, with
# the additional special exception to link portions of this program with the OpenSSL library.
# See LICENSE for more details.
#
import os
import random
import stat
import shutil
from __future__ import unicode_literals
import logging
import os
import shutil
from io import open
import deluge.component as component
import deluge.configmanager as configmanager
from deluge.common import (AUTH_LEVEL_ADMIN, AUTH_LEVEL_NONE, AUTH_LEVEL_NORMAL,
AUTH_LEVEL_READONLY, AUTH_LEVEL_DEFAULT,
create_localclient_account)
from deluge.error import AuthManagerError, AuthenticationRequired, BadLoginError
from deluge.common import (AUTH_LEVEL_ADMIN, AUTH_LEVEL_DEFAULT, AUTH_LEVEL_NONE, AUTH_LEVEL_NORMAL,
AUTH_LEVEL_READONLY, create_localclient_account)
from deluge.error import AuthenticationRequired, AuthManagerError, BadLoginError
log = logging.getLogger(__name__)
@ -55,15 +28,13 @@ AUTH_LEVELS_MAPPING = {
'READONLY': AUTH_LEVEL_READONLY,
'DEFAULT': AUTH_LEVEL_NORMAL,
'NORMAL': AUTH_LEVEL_DEFAULT,
'ADMIN': AUTH_LEVEL_ADMIN
}
'ADMIN': AUTH_LEVEL_ADMIN}
AUTH_LEVELS_MAPPING_REVERSE = {v: k for k, v in AUTH_LEVELS_MAPPING.items()}
AUTH_LEVELS_MAPPING_REVERSE = {}
for key, value in AUTH_LEVELS_MAPPING.iteritems():
AUTH_LEVELS_MAPPING_REVERSE[value] = key
class Account(object):
__slots__ = ('username', 'password', 'authlevel')
def __init__(self, username, password, authlevel):
self.username = username
self.password = password
@ -79,12 +50,12 @@ class Account(object):
def __repr__(self):
return ('<Account username="%(username)s" authlevel=%(authlevel)s>' %
self.__dict__)
{'username': self.username, 'authlevel': self.authlevel})
class AuthManager(component.Component):
def __init__(self):
component.Component.__init__(self, "AuthManager", interval=10)
component.Component.__init__(self, 'AuthManager', interval=10)
self.__auth = {}
self.__auth_modification_time = None
@ -98,93 +69,95 @@ class AuthManager(component.Component):
pass
def update(self):
auth_file = configmanager.get_config_dir("auth")
auth_file = configmanager.get_config_dir('auth')
# Check for auth file and create if necessary
if not os.path.exists(auth_file):
log.info("Authfile not found, recreating it.")
if not os.path.isfile(auth_file):
log.info('Authfile not found, recreating it.')
self.__load_auth_file()
return
auth_file_modification_time = os.stat(auth_file).st_mtime
if self.__auth_modification_time != auth_file_modification_time:
log.info("Auth file changed, reloading it!")
log.info('Auth file changed, reloading it!')
self.__load_auth_file()
def authorize(self, username, password):
"""
Authorizes users based on username and password
"""Authorizes users based on username and password.
:param username: str, username
:param password: str, password
:returns: int, the auth level for this user
:rtype: int
Args:
username (str): Username
password (str): Password
:raises AuthenticationRequired: if aditional details are required to
authenticate.
:raises BadLoginError: if the username does not exist or password does
not match.
Returns:
int: The auth level for this user.
Raises:
AuthenticationRequired: If aditional details are required to authenticate.
BadLoginError: If the username does not exist or password does not match.
"""
if not username:
raise AuthenticationRequired(
"Username and Password are required.", username
'Username and Password are required.', username
)
if username not in self.__auth:
# Let's try to re-load the file.. Maybe it's been updated
self.__load_auth_file()
if username not in self.__auth:
raise BadLoginError("Username does not exist", username)
raise BadLoginError('Username does not exist', username)
if self.__auth[username].password == password:
# Return the users auth level
return self.__auth[username].authlevel
elif not password and self.__auth[username].password:
raise AuthenticationRequired("Password is required", username)
raise AuthenticationRequired('Password is required', username)
else:
raise BadLoginError("Password does not match", username)
raise BadLoginError('Password does not match', username)
def has_account(self, username):
return username in self.__auth
def get_known_accounts(self):
"""
Returns a list of known deluge usernames.
"""
"""Returns a list of known deluge usernames."""
self.__load_auth_file()
return [account.data() for account in self.__auth.values()]
def create_account(self, username, password, authlevel):
if username in self.__auth:
raise AuthManagerError("Username in use.", username)
raise AuthManagerError('Username in use.', username)
if authlevel not in AUTH_LEVELS_MAPPING:
raise AuthManagerError('Invalid auth level: %s' % authlevel)
try:
self.__auth[username] = Account(username, password,
AUTH_LEVELS_MAPPING[authlevel])
self.write_auth_file()
return True
except Exception, err:
log.exception(err)
raise err
except Exception as ex:
log.exception(ex)
raise ex
def update_account(self, username, password, authlevel):
if username not in self.__auth:
raise AuthManagerError("Username not known", username)
raise AuthManagerError('Username not known', username)
if authlevel not in AUTH_LEVELS_MAPPING:
raise AuthManagerError('Invalid auth level: %s' % authlevel)
try:
self.__auth[username].username = username
self.__auth[username].password = password
self.__auth[username].authlevel = AUTH_LEVELS_MAPPING[authlevel]
self.write_auth_file()
return True
except Exception, err:
log.exception(err)
raise err
except Exception as ex:
log.exception(ex)
raise ex
def remove_account(self, username):
if username not in self.__auth:
raise AuthManagerError("Username not known", username)
elif username == component.get("RPCServer").get_session_user():
raise AuthManagerError('Username not known', username)
elif username == component.get('RPCServer').get_session_user():
raise AuthManagerError(
"You cannot delete your own account while logged in!", username
'You cannot delete your own account while logged in!', username
)
del self.__auth[username]
@ -192,36 +165,42 @@ class AuthManager(component.Component):
return True
def write_auth_file(self):
old_auth_file = configmanager.get_config_dir("auth")
new_auth_file = old_auth_file + '.new'
bak_auth_file = old_auth_file + '.bak'
# Let's first create a backup
if os.path.exists(old_auth_file):
shutil.copy2(old_auth_file, bak_auth_file)
filename = 'auth'
filepath = os.path.join(configmanager.get_config_dir(), filename)
filepath_bak = filepath + '.bak'
filepath_tmp = filepath + '.tmp'
try:
fd = open(new_auth_file, "w")
for account in self.__auth.values():
fd.write(
"%(username)s:%(password)s:%(authlevel_int)s\n" %
account.data()
)
fd.flush()
os.fsync(fd.fileno())
fd.close()
os.rename(new_auth_file, old_auth_file)
except:
# Something failed, let's restore the previous file
if os.path.exists(bak_auth_file):
os.rename(bak_auth_file, old_auth_file)
if os.path.isfile(filepath):
log.debug('Creating backup of %s at: %s', filename, filepath_bak)
shutil.copy2(filepath, filepath_bak)
except IOError as ex:
log.error('Unable to backup %s to %s: %s', filepath, filepath_bak, ex)
else:
log.info('Saving the %s at: %s', filename, filepath)
try:
with open(filepath_tmp, 'w', encoding='utf8') as _file:
for account in self.__auth.values():
_file.write('%(username)s:%(password)s:%(authlevel_int)s\n' % account.data())
_file.flush()
os.fsync(_file.fileno())
shutil.move(filepath_tmp, filepath)
except IOError as ex:
log.error('Unable to save %s: %s', filename, ex)
if os.path.isfile(filepath_bak):
log.info('Restoring backup of %s from: %s', filename, filepath_bak)
shutil.move(filepath_bak, filepath)
self.__load_auth_file()
def __load_auth_file(self):
save_and_reload = False
auth_file = configmanager.get_config_dir("auth")
filename = 'auth'
auth_file = configmanager.get_config_dir(filename)
auth_file_bak = auth_file + '.bak'
# Check for auth file and create if necessary
if not os.path.exists(auth_file):
if not os.path.isfile(auth_file):
create_localclient_account()
return self.__load_auth_file()
@ -232,24 +211,29 @@ class AuthManager(component.Component):
# File didn't change, no need for re-parsing's
return
# Load the auth file into a dictionary: {username: Account(...)}
f = open(auth_file, "r").readlines()
for _filepath in (auth_file, auth_file_bak):
log.info('Opening %s for load: %s', filename, _filepath)
try:
with open(_filepath, 'r', encoding='utf8') as _file:
file_data = _file.readlines()
except IOError as ex:
log.warning('Unable to load %s: %s', _filepath, ex)
file_data = []
else:
log.info('Successfully loaded %s: %s', filename, _filepath)
break
for line in f:
# Load the auth file into a dictionary: {username: Account(...)}
for line in file_data:
line = line.strip()
if line.startswith("#") or not line:
if line.startswith('#') or not line:
# This line is a comment or empty
continue
try:
lsplit = line.split(":")
except Exception, e:
log.error("Your auth file is malformed: %s", e)
continue
lsplit = line.split(':')
if len(lsplit) == 2:
username, password = lsplit
log.warning("Your auth entry for %s contains no auth level, "
"using AUTH_LEVEL_DEFAULT(%s)..", username,
AUTH_LEVEL_DEFAULT)
log.warning('Your auth entry for %s contains no auth level, '
'using AUTH_LEVEL_DEFAULT(%s)..', username, AUTH_LEVEL_DEFAULT)
if username == 'localclient':
authlevel = AUTH_LEVEL_ADMIN
else:
@ -259,8 +243,7 @@ class AuthManager(component.Component):
elif len(lsplit) == 3:
username, password, authlevel = lsplit
else:
log.error("Your auth file is malformed: "
"Incorrect number of fields!")
log.error('Your auth file is malformed: Incorrect number of fields!')
continue
username = username.strip()
@ -271,19 +254,16 @@ class AuthManager(component.Component):
try:
authlevel = AUTH_LEVELS_MAPPING[authlevel]
except KeyError:
log.error("Your auth file is malformed: %r is not a valid auth "
"level" % authlevel)
log.error('Your auth file is malformed: %r is not a valid auth level', authlevel)
continue
self.__auth[username] = Account(username, password, authlevel)
if "localclient" not in self.__auth:
if 'localclient' not in self.__auth:
create_localclient_account(True)
return self.__load_auth_file()
if save_and_reload:
log.info("Re-writing auth file (upgrade)")
log.info('Re-writing auth file (upgrade)')
self.write_auth_file()
self.__auth_modification_time = auth_file_modification_time

File diff suppressed because it is too large Load Diff

View File

@ -1,200 +1,173 @@
#
# daemon.py
# -*- coding: utf-8 -*-
#
# 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.
# This file is part of Deluge and is licensed under GNU General Public License 3.0, or later, with
# the additional special exception to link portions of this program with the OpenSSL library.
# See LICENSE for more details.
#
import os
"""The Deluge daemon"""
from __future__ import unicode_literals
import logging
import os
import socket
from twisted.internet import reactor
import twisted.internet.error
import deluge.component as component
import deluge.configmanager
import deluge.common
from deluge.common import get_version, is_ip, is_process_running, windows_check
from deluge.configmanager import get_config_dir
from deluge.core.core import Core
from deluge.core.rpcserver import RPCServer, export
import deluge.error
from deluge.error import DaemonRunningError
if windows_check():
from win32api import SetConsoleCtrlHandler
from win32con import CTRL_CLOSE_EVENT, CTRL_SHUTDOWN_EVENT
log = logging.getLogger(__name__)
def is_daemon_running(pid_file):
"""
Check for another running instance of the daemon using the same pid file.
Args:
pid_file: The location of the file with pid, port values.
Returns:
bool: True is daemon is running, False otherwise.
"""
try:
with open(pid_file) as _file:
pid, port = [int(x) for x in _file.readline().strip().split(';')]
except (EnvironmentError, ValueError):
return False
if is_process_running(pid):
# Ensure it's a deluged process by trying to open a socket to it's port.
_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
try:
_socket.connect(('127.0.0.1', port))
except socket.error:
# Can't connect, so pid is not a deluged process.
return False
else:
# This is a deluged process!
_socket.close()
return True
class Daemon(object):
def __init__(self, options=None, args=None, classic=False):
# Check for another running instance of the daemon
if os.path.isfile(deluge.configmanager.get_config_dir("deluged.pid")):
# Get the PID and the port of the supposedly running daemon
try:
(pid, port) = open(
deluge.configmanager.get_config_dir("deluged.pid")
).read().strip().split(";")
pid = int(pid)
port = int(port)
except ValueError:
pid = None
port = None
"""The Deluge Daemon class"""
def __init__(self, listen_interface=None, interface=None, port=None, standalone=False,
read_only_config_keys=None):
"""
Args:
listen_interface (str, optional): The IP address to listen to bittorrent connections on.
interface (str, optional): The IP address the daemon will listen for UI connections on.
port (int, optional): The port the daemon will listen for UI connections on.
standalone (bool, optional): If True the client is in Standalone mode otherwise, if
False, start the daemon as separate process.
read_only_config_keys (list of str, optional): A list of config keys that will not be
altered by core.set_config() RPC method.
"""
self.standalone = standalone
self.pid_file = get_config_dir('deluged.pid')
log.info('Deluge daemon %s', get_version())
if is_daemon_running(self.pid_file):
raise DaemonRunningError('Deluge daemon already running with this config directory!')
def process_running(pid):
if deluge.common.windows_check():
import win32process
return pid in win32process.EnumProcesses()
else:
# We can just use os.kill on UNIX to test if the process is running
try:
os.kill(pid, 0)
except OSError:
return False
else:
return True
if pid is not None and process_running(pid):
# Ok, so a process is running with this PID, let's make doubly-sure
# it's a deluged process by trying to open a socket to it's port.
import socket
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
try:
s.connect(("127.0.0.1", port))
except socket.error:
# Can't connect, so it must not be a deluged process..
pass
else:
# This is a deluged!
s.close()
raise deluge.error.DaemonRunningError(
"There is a deluge daemon running with this config "
"directory!"
)
# Twisted catches signals to terminate, so just have it call the shutdown
# method.
reactor.addSystemEventTrigger("before", "shutdown", self._shutdown)
# Twisted catches signals to terminate, so just have it call the shutdown method.
reactor.addSystemEventTrigger('before', 'shutdown', self._shutdown)
# Catch some Windows specific signals
if deluge.common.windows_check():
from win32api import SetConsoleCtrlHandler
from win32con import CTRL_CLOSE_EVENT
from win32con import CTRL_SHUTDOWN_EVENT
if windows_check():
def win_handler(ctrl_type):
log.debug("ctrl_type: %s", ctrl_type)
"""Handle the Windows shutdown or close events."""
log.debug('windows handler ctrl_type: %s', ctrl_type)
if ctrl_type == CTRL_CLOSE_EVENT or ctrl_type == CTRL_SHUTDOWN_EVENT:
self._shutdown()
return 1
SetConsoleCtrlHandler(win_handler)
version = deluge.common.get_version()
log.info("Deluge daemon %s", version)
log.debug("options: %s", options)
log.debug("args: %s", args)
# Set the config directory
if options and options.config:
deluge.configmanager.set_config_dir(options.config)
if options and options.listen_interface:
listen_interface = options.listen_interface
else:
listen_interface = ""
from deluge.core.core import Core
# Start the core as a thread and join it until it's done
self.core = Core(listen_interface=listen_interface)
self.core = Core(listen_interface=listen_interface,
read_only_config_keys=read_only_config_keys)
port = self.core.config["daemon_port"]
if options and options.port:
port = options.port
if options and options.ui_interface:
interface = options.ui_interface
else:
interface = ""
if port is None:
port = self.core.config['daemon_port']
self.port = port
if interface and not is_ip(interface):
log.error('Invalid UI interface (must be IP Address): %s', interface)
interface = None
self.rpcserver = RPCServer(
port=port,
allow_remote=self.core.config["allow_remote"],
listen=not classic,
allow_remote=self.core.config['allow_remote'],
listen=not standalone,
interface=interface
)
log.debug('Listening to UI on: %s:%s and bittorrent on: %s', interface, port, listen_interface)
def start(self):
# Register the daemon and the core RPCs
self.rpcserver.register_object(self.core)
self.rpcserver.register_object(self)
# Make sure we start the PreferencesManager first
component.start("PreferencesManager")
component.start('PreferencesManager')
if not classic:
# Write out a pid file all the time, we use this to see if a deluged is running
# We also include the running port number to do an additional test
open(deluge.configmanager.get_config_dir("deluged.pid"), "wb").write(
"%s;%s\n" % (os.getpid(), port))
if not self.standalone:
log.info('Deluge daemon starting...')
# Create pid file to track if deluged is running, also includes the port number.
pid = os.getpid()
log.debug('Storing pid %s & port %s in: %s', pid, self.port, self.pid_file)
with open(self.pid_file, 'w') as _file:
_file.write('%s;%s\n' % (pid, self.port))
component.start()
try:
reactor.run()
finally:
self._shutdown()
log.debug('Remove pid file: %s', self.pid_file)
os.remove(self.pid_file)
log.info('Deluge daemon shutdown successfully')
@export()
def shutdown(self, *args, **kwargs):
log.debug('Deluge daemon shutdown requested...')
reactor.callLater(0, reactor.stop)
def _shutdown(self, *args, **kwargs):
if os.path.exists(deluge.configmanager.get_config_dir("deluged.pid")):
try:
os.remove(deluge.configmanager.get_config_dir("deluged.pid"))
except Exception, e:
log.exception(e)
log.error("Error removing deluged.pid!")
log.info("Waiting for components to shutdown..")
d = component.shutdown()
return d
log.info('Deluge daemon shutting down, waiting for components to shutdown...')
if not self.standalone:
return component.shutdown()
@export()
def get_method_list(self):
"""
Returns a list of the exported methods.
"""
"""Returns a list of the exported methods."""
return self.rpcserver.get_method_list()
@export(1)
def authorized_call(self, rpc):
"""
Returns True if authorized to call rpc.
"""Determines if session auth_level is authorized to call RPC.
:param rpc: a rpc, eg, "core.get_torrents_status"
:type rpc: string
Args:
rpc (str): A RPC, e.g. core.get_torrents_status
Returns:
bool: True if authorized to call RPC, otherwise False.
"""
if not rpc in self.get_method_list():
if rpc not in self.get_method_list():
return False
auth_level = self.rpcserver.get_session_auth_level()
return auth_level >= self.rpcserver.get_rpc_auth_level()
return self.rpcserver.get_session_auth_level() >= self.rpcserver.get_rpc_auth_level(rpc)

View File

@ -0,0 +1,98 @@
# -*- coding: utf-8 -*-
#
# Copyright (C) 2007 Andrew Resch <andrewresch@gmail.com>
# Copyright (C) 2010 Pedro Algarvio <pedro@algarvio.me>
#
# This file is part of Deluge and is licensed under GNU General Public License 3.0, or later, with
# the additional special exception to link portions of this program with the OpenSSL library.
# See LICENSE for more details.
#
from __future__ import print_function, unicode_literals
import os
import sys
from logging import DEBUG, FileHandler, getLogger
from twisted.internet.error import CannotListenError
from deluge.common import run_profiled
from deluge.configmanager import get_config_dir
from deluge.ui.baseargparser import BaseArgParser
from deluge.ui.translations_util import set_dummy_trans
def add_daemon_options(parser):
group = parser.add_argument_group(_('Daemon Options'))
group.add_argument('-u', '--ui-interface', metavar='<ip-addr>', action='store',
help=_('IP address to listen for UI connections'))
group.add_argument('-p', '--port', metavar='<port>', action='store', type=int,
help=_('Port to listen for UI connections on'))
group.add_argument('-i', '--interface', metavar='<ip-addr>', dest='listen_interface', action='store',
help=_('IP address to listen for BitTorrent connections'))
group.add_argument('--read-only-config-keys', metavar='<comma-separated-keys>', action='store',
help=_('Config keys to be unmodified by `set_config` RPC'), type=str, default='')
parser.add_process_arg_group()
def start_daemon(skip_start=False):
"""
Entry point for daemon script
Args:
skip_start (bool): If starting daemon should be skipped.
Returns:
deluge.core.daemon.Daemon: A new daemon object
"""
set_dummy_trans(warn_msg=True)
# Setup the argument parser
parser = BaseArgParser()
add_daemon_options(parser)
options = parser.parse_args()
# Check for any daemons running with this same config
from deluge.core.daemon import is_daemon_running
pid_file = get_config_dir('deluged.pid')
if is_daemon_running(pid_file):
print('Cannot run multiple daemons with same config directory.\n'
'If you believe this is an error, force starting by deleting: %s' % pid_file)
sys.exit(1)
log = getLogger(__name__)
# If no logfile specified add logging to default location (as well as stdout)
if not options.logfile:
options.logfile = get_config_dir('deluged.log')
file_handler = FileHandler(options.logfile)
log.addHandler(file_handler)
def run_daemon(options):
try:
from deluge.core.daemon import Daemon
daemon = Daemon(listen_interface=options.listen_interface,
interface=options.ui_interface,
port=options.port,
read_only_config_keys=options.read_only_config_keys.split(','))
if skip_start:
return daemon
else:
daemon.start()
except CannotListenError as ex:
log.error('Cannot start deluged, listen port in use.\n'
' Check for other running daemons or services using this port: %s:%s',
ex.interface, ex.port)
sys.exit(1)
except Exception as ex:
log.error('Unable to start deluged: %s', ex)
if log.isEnabledFor(DEBUG):
log.exception(ex)
sys.exit(1)
finally:
log.info('Exiting...')
if options.pidfile:
os.remove(options.pidfile)
return run_profiled(run_daemon, options, output_file=options.profile, do_profile=options.profile)

View File

@ -1,46 +1,24 @@
#
# eventmanager.py
# -*- coding: utf-8 -*-
#
# 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.
#
# This file is part of Deluge and is licensed under GNU General Public License 3.0, or later, with
# the additional special exception to link portions of this program with the OpenSSL library.
# See LICENSE for more details.
#
from __future__ import unicode_literals
import logging
import deluge.component as component
log = logging.getLogger(__name__)
class EventManager(component.Component):
def __init__(self):
component.Component.__init__(self, "EventManager")
component.Component.__init__(self, 'EventManager')
self.handlers = {}
def emit(self, event):
@ -50,15 +28,15 @@ class EventManager(component.Component):
:param event: DelugeEvent
"""
# Emit the event to the interested clients
component.get("RPCServer").emit_event(event)
component.get('RPCServer').emit_event(event)
# Call any handlers for the event
if event.name in self.handlers:
for handler in self.handlers[event.name]:
#log.debug("Running handler %s for event %s with args: %s", event.name, handler, event.args)
# log.debug('Running handler %s for event %s with args: %s', event.name, handler, event.args)
try:
handler(*event.args)
except Exception, e:
log.error("Event handler %s failed in %s with exception %s", event.name, handler, e)
except Exception as ex:
log.error('Event handler %s failed in %s with exception %s', event.name, handler, ex)
def register_event_handler(self, event, handler):
"""

View File

@ -1,85 +1,64 @@
#
# core.py
# -*- coding: utf-8 -*-
#
# Copyright (C) 2008 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.
#
# This file is part of Deluge and is licensed under GNU General Public License 3.0, or later, with
# the additional special exception to link portions of this program with the OpenSSL library.
# See LICENSE for more details.
#
from __future__ import unicode_literals
import logging
import deluge.component as component
STATE_SORT = ["All", "Downloading", "Seeding", "Active", "Paused", "Queued"]
import deluge.component as component
from deluge.common import PY2, TORRENT_STATE
log = logging.getLogger(__name__)
#special purpose filters:
def filter_keywords(torrent_ids, values):
#cleanup.
keywords = ",".join([v.lower() for v in values])
keywords = keywords.split(",")
STATE_SORT = ['All', 'Active'] + TORRENT_STATE
# Special purpose filters:
def filter_keywords(torrent_ids, values):
# Cleanup
keywords = ','.join([v.lower() for v in values])
keywords = keywords.split(',')
for keyword in keywords:
torrent_ids = filter_one_keyword(torrent_ids, keyword)
return torrent_ids
def filter_one_keyword(torrent_ids, keyword):
"""
search torrent on keyword.
searches title,state,tracker-status,tracker,files
"""
all_torrents = component.get("TorrentManager").torrents
#filter:
found = False
all_torrents = component.get('TorrentManager').torrents
for torrent_id in torrent_ids:
torrent = all_torrents[torrent_id]
if keyword in torrent.filename.lower():
yield torrent_id
elif keyword in torrent.state.lower():
yield torrent_id
elif torrent.trackers and keyword in torrent.trackers[0]["url"]:
elif torrent.trackers and keyword in torrent.trackers[0]['url']:
yield torrent_id
elif keyword in torrent_id:
yield torrent_id
#i want to find broken torrents (search on "error", or "unregistered")
# Want to find broken torrents (search on "error", or "unregistered")
elif keyword in torrent.tracker_status.lower():
yield torrent_id
else:
for t_file in torrent.get_files():
if keyword in t_file["path"].lower():
if keyword in t_file['path'].lower():
yield torrent_id
break
def filter_by_name(torrent_ids, search_string):
all_torrents = component.get("TorrentManager").torrents
all_torrents = component.get('TorrentManager').torrents
try:
search_string, match_case = search_string[0].split('::match')
except ValueError:
@ -99,48 +78,50 @@ def filter_by_name(torrent_ids, search_string):
if search_string in torrent_name:
yield torrent_id
def tracker_error_filter(torrent_ids, values):
filtered_torrent_ids = []
tm = component.get("TorrentManager")
tm = component.get('TorrentManager')
# If this is a tracker_host, then we need to filter on it
if values[0] != "Error":
if values[0] != 'Error':
for torrent_id in torrent_ids:
if values[0] == tm[torrent_id].get_status(["tracker_host"])["tracker_host"]:
if values[0] == tm[torrent_id].get_status(['tracker_host'])['tracker_host']:
filtered_torrent_ids.append(torrent_id)
return filtered_torrent_ids
# Check all the torrent's tracker_status for 'Error:' and only return torrent_ids
# that have this substring in their tracker_status
# Check torrent's tracker_status for 'Error:' and return those torrent_ids
for torrent_id in torrent_ids:
if _("Error") + ":" in tm[torrent_id].get_status(["tracker_host"])["tracker_host"]:
if 'Error:' in tm[torrent_id].get_status(['tracker_status'])['tracker_status']:
filtered_torrent_ids.append(torrent_id)
return filtered_torrent_ids
class FilterManager(component.Component):
"""FilterManager
"""
def __init__(self, core):
component.Component.__init__(self, "FilterManager")
log.debug("FilterManager init..")
component.Component.__init__(self, 'FilterManager')
log.debug('FilterManager init..')
self.core = core
self.torrents = core.torrentmanager
self.registered_filters = {}
self.register_filter("keyword", filter_keywords)
self.register_filter("name", filter_by_name)
self.register_filter('keyword', filter_keywords)
self.register_filter('name', filter_by_name)
self.tree_fields = {}
self.register_tree_field("state", self._init_state_tree)
def _init_tracker_tree():
return {"Error": 0}
self.register_tree_field("tracker_host", _init_tracker_tree)
self.register_tree_field('state', self._init_state_tree)
self.register_filter("tracker_host", tracker_error_filter)
def _init_tracker_tree():
return {'Error': 0}
self.register_tree_field('tracker_host', _init_tracker_tree)
self.register_filter('tracker_host', tracker_error_filter)
def _init_users_tree():
return {"": 0}
self.register_tree_field("owner", _init_users_tree)
return {'': 0}
self.register_tree_field('owner', _init_users_tree)
def filter_torrent_ids(self, filter_dict):
"""
@ -150,51 +131,54 @@ class FilterManager(component.Component):
if not filter_dict:
return self.torrents.get_torrent_list()
#sanitize input: filter-value must be a list of strings
# Sanitize input: filter-value must be a list of strings
for key, value in filter_dict.items():
if isinstance(value, basestring):
if isinstance(value, str if not PY2 else basestring):
filter_dict[key] = [value]
if "id"in filter_dict: #optimized filter for id:
torrent_ids = list(filter_dict["id"])
del filter_dict["id"]
# Optimized filter for id
if 'id' in filter_dict:
torrent_ids = list(filter_dict['id'])
del filter_dict['id']
else:
torrent_ids = self.torrents.get_torrent_list()
if not filter_dict: #return if there's nothing more to filter
# Return if there's nothing more to filter
if not filter_dict:
return torrent_ids
#special purpose: state=Active.
if "state" in filter_dict:
# Special purpose, state=Active.
if 'state' in filter_dict:
# We need to make sure this is a list for the logic below
filter_dict["state"] = list(filter_dict["state"])
filter_dict['state'] = list(filter_dict['state'])
if "state" in filter_dict and "Active" in filter_dict["state"]:
filter_dict["state"].remove("Active")
if not filter_dict["state"]:
del filter_dict["state"]
if 'state' in filter_dict and 'Active' in filter_dict['state']:
filter_dict['state'].remove('Active')
if not filter_dict['state']:
del filter_dict['state']
torrent_ids = self.filter_state_active(torrent_ids)
if not filter_dict: #return if there's nothing more to filter
if not filter_dict:
return torrent_ids
#Registered filters:
# Registered filters
for field, values in filter_dict.items():
if field in self.registered_filters:
# a set filters out the doubles.
# Filters out doubles
torrent_ids = list(set(self.registered_filters[field](torrent_ids, values)))
del filter_dict[field]
if not filter_dict: #return if there's nothing more to filter
if not filter_dict:
return torrent_ids
#leftover filter arguments:
#default filter on status fields.
torrent_keys, plugin_keys = self.torrents.separate_keys(list(filter_dict), torrent_ids)
# Leftover filter arguments, default filter on status fields.
for torrent_id in list(torrent_ids):
status = self.torrents[torrent_id].get_status(filter_dict.keys()) #status={key:value}
for field, values in filter_dict.iteritems():
if (not status[field] in values) and torrent_id in torrent_ids:
status = self.core.create_torrent_status(torrent_id, torrent_keys, plugin_keys)
for field, values in filter_dict.items():
if field in status and status[field] in values:
continue
elif torrent_id in torrent_ids:
torrent_ids.remove(torrent_id)
return torrent_ids
@ -204,7 +188,7 @@ class FilterManager(component.Component):
for use in sidebar.
"""
torrent_ids = self.torrents.get_torrent_list()
tree_keys = list(self.tree_fields.keys())
tree_keys = list(self.tree_fields)
if hide_cat:
for cat in hide_cat:
tree_keys.remove(cat)
@ -213,46 +197,43 @@ class FilterManager(component.Component):
items = dict((field, self.tree_fields[field]()) for field in tree_keys)
for torrent_id in list(torrent_ids):
status = self.core.create_torrent_status(torrent_id, torrent_keys, plugin_keys) #status={key:value}
status = self.core.create_torrent_status(torrent_id, torrent_keys, plugin_keys) # status={key:value}
for field in tree_keys:
value = status[field]
items[field][value] = items[field].get(value, 0) + 1
if "tracker_host" in items:
items["tracker_host"]["All"] = len(torrent_ids)
items["tracker_host"]["Error"] = len(tracker_error_filter(torrent_ids, ("Error",)))
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:
self._hide_state_items(items["state"])
if not show_zero_hits:
for cat in ['state', 'owner', 'tracker_host']:
if cat in tree_keys:
self._hide_state_items(items[cat])
#return a dict of tuples:
sorted_items = {}
for field in tree_keys:
sorted_items[field] = sorted(items[field].iteritems())
# Return a dict of tuples:
sorted_items = {field: sorted(items[field].items()) for field in tree_keys}
if "state" in tree_keys:
sorted_items["state"].sort(self._sort_state_items)
if 'state' in tree_keys:
sorted_items['state'].sort(self._sort_state_items)
return sorted_items
def _init_state_tree(self):
return {"All":len(self.torrents.get_torrent_list()),
"Downloading":0,
"Seeding":0,
"Paused":0,
"Checking":0,
"Queued":0,
"Error":0,
"Active":len(self.filter_state_active(self.torrents.get_torrent_list()))
}
init_state = {}
init_state['All'] = len(self.torrents.get_torrent_list())
for state in TORRENT_STATE:
init_state[state] = 0
init_state['Active'] = len(self.filter_state_active(self.torrents.get_torrent_list()))
return init_state
def register_filter(self, id, filter_func, filter_value = None):
self.registered_filters[id] = filter_func
def register_filter(self, filter_id, filter_func, filter_value=None):
self.registered_filters[filter_id] = filter_func
def deregister_filter(self, id):
del self.registered_filters[id]
def deregister_filter(self, filter_id):
del self.registered_filters[filter_id]
def register_tree_field(self, field, init_func = lambda : {}):
def register_tree_field(self, field, init_func=lambda: {}):
self.tree_fields[field] = init_func
def deregister_tree_field(self, field):
@ -261,21 +242,20 @@ class FilterManager(component.Component):
def filter_state_active(self, torrent_ids):
for torrent_id in list(torrent_ids):
status = self.torrents[torrent_id].get_status(["download_payload_rate", "upload_payload_rate"])
if status["download_payload_rate"] or status["upload_payload_rate"]:
pass #ok
status = self.torrents[torrent_id].get_status(['download_payload_rate', 'upload_payload_rate'])
if status['download_payload_rate'] or status['upload_payload_rate']:
pass
else:
torrent_ids.remove(torrent_id)
return torrent_ids
def _hide_state_items(self, state_items):
"for hide(show)-zero hits"
for (value, count) in state_items.items():
if value != "All" and count == 0:
"""For hide(show)-zero hits"""
for (value, count) in state_items.items():
if value != 'All' and count == 0:
del state_items[value]
def _sort_state_items(self, x, y):
""
if x[0] in STATE_SORT:
ix = STATE_SORT.index(x[0])
else:

View File

@ -1,143 +0,0 @@
#
# oldstateupgrader.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 os
import os.path
import pickle
import cPickle
import shutil
import logging
from deluge._libtorrent import lt
from deluge.configmanager import ConfigManager, get_config_dir
import deluge.core.torrentmanager
log = logging.getLogger(__name__)
#start : http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/286203
def makeFakeClass(module, name):
class FakeThing(object):
pass
FakeThing.__name__ = name
FakeThing.__module__ = '(fake)' + module
return FakeThing
class PickleUpgrader(pickle.Unpickler):
def find_class(self, module, cname):
# Pickle tries to load a couple things like copy_reg and
# __builtin__.object even though a pickle file doesn't
# explicitly reference them (afaict): allow them to be loaded
# normally.
if module in ('copy_reg', '__builtin__'):
thing = pickle.Unpickler.find_class(self, module, cname)
return thing
return makeFakeClass(module, cname)
# end: http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/286203
class OldStateUpgrader:
def __init__(self):
self.config = ConfigManager("core.conf")
self.state05_location = os.path.join(get_config_dir(), "persistent.state")
self.state10_location = os.path.join(get_config_dir(), "state", "torrents.state")
if os.path.exists(self.state05_location) and not os.path.exists(self.state10_location):
# If the 0.5 state file exists and the 1.0 doesn't, then let's upgrade it
self.upgrade05()
def upgrade05(self):
try:
state = PickleUpgrader(open(self.state05_location, "rb")).load()
except Exception, e:
log.debug("Unable to open 0.5 state file: %s", e)
return
# Check to see if we can upgrade this file
if type(state).__name__ == 'list':
log.warning("0.5 state file is too old to upgrade")
return
new_state = deluge.core.torrentmanager.TorrentManagerState()
for ti, uid in state.torrents.items():
torrent_path = os.path.join(get_config_dir(), "torrentfiles", ti.filename)
try:
torrent_info = None
log.debug("Attempting to create torrent_info from %s", torrent_path)
_file = open(torrent_path, "rb")
torrent_info = lt.torrent_info(lt.bdecode(_file.read()))
_file.close()
except (IOError, RuntimeError), e:
log.warning("Unable to open %s: %s", torrent_path, e)
# Copy the torrent file to the new location
import shutil
shutil.copyfile(torrent_path, os.path.join(get_config_dir(), "state", str(torrent_info.info_hash()) + ".torrent"))
# Set the file prioritiy property if not already there
if not hasattr(ti, "priorities"):
ti.priorities = [1] * torrent_info.num_files()
# Create the new TorrentState object
new_torrent = deluge.core.torrentmanager.TorrentState(
torrent_id=str(torrent_info.info_hash()),
filename=ti.filename,
save_path=ti.save_dir,
compact=ti.compact,
paused=ti.user_paused,
total_uploaded=ti.uploaded_memory,
max_upload_speed=ti.upload_rate_limit,
max_download_speed=ti.download_rate_limit,
file_priorities=ti.priorities,
queue=state.queue.index(ti)
)
# Append the object to the state list
new_state.torrents.append(new_torrent)
# Now we need to write out the new state file
try:
log.debug("Saving torrent state file.")
state_file = open(
os.path.join(get_config_dir(), "state", "torrents.state"), "wb")
cPickle.dump(new_state, state_file)
state_file.close()
except IOError, e:
log.warning("Unable to save state file: %s", e)
return
# Rename the persistent.state file
try:
os.rename(self.state05_location, self.state05_location + ".old")
except Exception, e:
log.debug("Unable to rename old persistent.state file! %s", e)

View File

@ -1,62 +1,39 @@
#
# pluginmanager.py
# -*- coding: utf-8 -*-
#
# Copyright (C) 2007 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.
#
# This file is part of Deluge and is licensed under GNU General Public License 3.0, or later, with
# the additional special exception to link portions of this program with the OpenSSL library.
# See LICENSE for more details.
#
"""PluginManager for Core"""
from __future__ import unicode_literals
import logging
from deluge.event import PluginEnabledEvent, PluginDisabledEvent
import deluge.pluginmanagerbase
from twisted.internet import defer
import deluge.component as component
import deluge.pluginmanagerbase
from deluge.event import PluginDisabledEvent, PluginEnabledEvent
log = logging.getLogger(__name__)
class PluginManager(deluge.pluginmanagerbase.PluginManagerBase,
component.Component):
class PluginManager(deluge.pluginmanagerbase.PluginManagerBase, component.Component):
"""PluginManager handles the loading of plugins and provides plugins with
functions to access parts of the core."""
def __init__(self, core):
component.Component.__init__(self, "CorePluginManager")
component.Component.__init__(self, 'CorePluginManager')
self.status_fields = {}
# Call the PluginManagerBase constructor
deluge.pluginmanagerbase.PluginManagerBase.__init__(
self, "core.conf", "deluge.plugin.core")
self, 'core.conf', 'deluge.plugin.core')
def start(self):
# Enable plugins that are enabled in the config
@ -70,28 +47,43 @@ class PluginManager(deluge.pluginmanagerbase.PluginManagerBase,
self.stop()
def update_plugins(self):
for plugin in self.plugins.keys():
if hasattr(self.plugins[plugin], "update"):
for plugin in self.plugins:
if hasattr(self.plugins[plugin], 'update'):
try:
self.plugins[plugin].update()
except Exception, e:
log.exception(e)
except Exception as ex:
log.exception(ex)
def enable_plugin(self, name):
d = defer.succeed(True)
if name not in self.plugins:
super(PluginManager, self).enable_plugin(name)
if name in self.plugins:
component.get("EventManager").emit(PluginEnabledEvent(name))
d = deluge.pluginmanagerbase.PluginManagerBase.enable_plugin(self, name)
def on_enable_plugin(result):
if result is True and name in self.plugins:
component.get('EventManager').emit(PluginEnabledEvent(name))
return result
d.addBoth(on_enable_plugin)
return d
def disable_plugin(self, name):
d = defer.succeed(True)
if name in self.plugins:
super(PluginManager, self).disable_plugin(name)
if name not in self.plugins:
component.get("EventManager").emit(PluginDisabledEvent(name))
d = deluge.pluginmanagerbase.PluginManagerBase.disable_plugin(self, name)
def on_disable_plugin(result):
if name not in self.plugins:
component.get('EventManager').emit(PluginDisabledEvent(name))
return result
d.addBoth(on_disable_plugin)
return d
def get_status(self, torrent_id, fields):
"""Return the value of status fields for the selected torrent_id."""
status = {}
if len(fields) == 0:
fields = list(self.status_fields)
for field in fields:
try:
status[field] = self.status_fields[field](torrent_id)
@ -102,13 +94,13 @@ class PluginManager(deluge.pluginmanagerbase.PluginManagerBase,
def register_status_field(self, field, function):
"""Register a new status field. This can be used in the same way the
client requests other status information from core."""
log.debug("Registering status field %s with PluginManager", field)
log.debug('Registering status field %s with PluginManager', field)
self.status_fields[field] = function
def deregister_status_field(self, field):
"""Deregisters a status field"""
log.debug("Deregistering status field %s with PluginManager", field)
log.debug('Deregistering status field %s with PluginManager', field)
try:
del self.status_fields[field]
except:
log.warning("Unable to deregister status field %s", field)
except Exception:
log.warning('Unable to deregister status field %s', field)

View File

@ -1,165 +1,156 @@
#
# preferencesmanager.py
# -*- coding: utf-8 -*-
#
# Copyright (C) 2008-2010 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.
#
# This file is part of Deluge and is licensed under GNU General Public License 3.0, or later, with
# the additional special exception to link portions of this program with the OpenSSL library.
# See LICENSE for more details.
#
import os
from __future__ import unicode_literals
import logging
import os
import platform
import random
import threading
from twisted.internet.task import LoopingCall
from deluge._libtorrent import lt
from deluge.event import *
import deluge.configmanager
import deluge.common
import deluge.component as component
import deluge.configmanager
from deluge._libtorrent import lt
from deluge.event import ConfigValueChangedEvent
try:
import GeoIP
except ImportError:
GeoIP = None
try:
from urllib.parse import quote_plus
from urllib.request import urlopen
except ImportError:
from urllib import quote_plus
from urllib2 import urlopen
log = logging.getLogger(__name__)
DEFAULT_PREFS = {
"send_info": False,
"info_sent": 0.0,
"daemon_port": 58846,
"allow_remote": False,
"compact_allocation": False,
"download_location": deluge.common.get_default_download_dir(),
"listen_ports": [6881, 6891],
"listen_interface": "",
"copy_torrent_file": False,
"del_copy_torrent_file": False,
"torrentfiles_location": deluge.common.get_default_download_dir(),
"plugins_location": os.path.join(deluge.configmanager.get_config_dir(), "plugins"),
"prioritize_first_last_pieces": False,
"sequential_download": False,
"random_port": True,
"dht": True,
"upnp": True,
"natpmp": True,
"utpex": True,
"lsd": True,
"enc_in_policy": 1,
"enc_out_policy": 1,
"enc_level": 2,
"enc_prefer_rc4": True,
"max_connections_global": 200,
"max_upload_speed": -1.0,
"max_download_speed": -1.0,
"max_upload_slots_global": 4,
"max_half_open_connections": (lambda: deluge.common.windows_check() and
(lambda: deluge.common.vista_check() and 4 or 8)() or 50)(),
"max_connections_per_second": 20,
"ignore_limits_on_local_network": True,
"max_connections_per_torrent": -1,
"max_upload_slots_per_torrent": -1,
"max_upload_speed_per_torrent": -1,
"max_download_speed_per_torrent": -1,
"enabled_plugins": [],
"add_paused": False,
"max_active_seeding": 5,
"max_active_downloading": 3,
"max_active_limit": 8,
"dont_count_slow_torrents": False,
"queue_new_to_top": False,
"stop_seed_at_ratio": False,
"remove_seed_at_ratio": False,
"stop_seed_ratio": 2.00,
"share_ratio_limit": 2.00,
"seed_time_ratio_limit": 7.00,
"seed_time_limit": 180,
"auto_managed": True,
"move_completed": False,
"move_completed_path": deluge.common.get_default_download_dir(),
"new_release_check": True,
"proxies": {
"peer": {
"type": 0,
"hostname": "",
"username": "",
"password": "",
"port": 8080
},
"web_seed": {
"type": 0,
"hostname": "",
"username": "",
"password": "",
"port": 8080
},
"tracker": {
"type": 0,
"hostname": "",
"username": "",
"password": "",
"port": 8080
},
"dht": {
"type": 0,
"hostname": "",
"username": "",
"password": "",
"port": 8080
},
'send_info': False,
'info_sent': 0.0,
'daemon_port': 58846,
'allow_remote': False,
'pre_allocate_storage': False,
'download_location': deluge.common.get_default_download_dir(),
'listen_ports': [6881, 6891],
'listen_interface': '',
'random_port': True,
'listen_random_port': None,
'listen_use_sys_port': False,
'listen_reuse_port': True,
'outgoing_ports': [0, 0],
'random_outgoing_ports': True,
'copy_torrent_file': False,
'del_copy_torrent_file': False,
'torrentfiles_location': deluge.common.get_default_download_dir(),
'plugins_location': os.path.join(deluge.configmanager.get_config_dir(), 'plugins'),
'prioritize_first_last_pieces': False,
'sequential_download': False,
'dht': True,
'upnp': True,
'natpmp': True,
'utpex': True,
'lsd': True,
'enc_in_policy': 1,
'enc_out_policy': 1,
'enc_level': 2,
'max_connections_global': 200,
'max_upload_speed': -1.0,
'max_download_speed': -1.0,
'max_upload_slots_global': 4,
'max_half_open_connections': (lambda: deluge.common.windows_check() and
(lambda: deluge.common.vista_check() and 4 or 8)() or 50)(),
'max_connections_per_second': 20,
'ignore_limits_on_local_network': True,
'max_connections_per_torrent': -1,
'max_upload_slots_per_torrent': -1,
'max_upload_speed_per_torrent': -1,
'max_download_speed_per_torrent': -1,
'enabled_plugins': [],
'add_paused': False,
'max_active_seeding': 5,
'max_active_downloading': 3,
'max_active_limit': 8,
'dont_count_slow_torrents': False,
'queue_new_to_top': False,
'stop_seed_at_ratio': False,
'remove_seed_at_ratio': False,
'stop_seed_ratio': 2.00,
'share_ratio_limit': 2.00,
'seed_time_ratio_limit': 7.00,
'seed_time_limit': 180,
'auto_managed': True,
'move_completed': False,
'move_completed_path': deluge.common.get_default_download_dir(),
'move_completed_paths_list': [],
'download_location_paths_list': [],
'path_chooser_show_chooser_button_on_localhost': True,
'path_chooser_auto_complete_enabled': True,
'path_chooser_accelerator_string': 'Tab',
'path_chooser_max_popup_rows': 20,
'path_chooser_show_hidden_files': False,
'new_release_check': True,
'proxy': {
'type': 0,
'hostname': '',
'username': '',
'password': '',
'port': 8080,
'proxy_hostnames': True,
'proxy_peer_connections': True,
'proxy_tracker_connections': True,
'force_proxy': False,
'anonymous_mode': False,
},
"outgoing_ports": [0, 0],
"random_outgoing_ports": True,
"peer_tos": "0x00",
"rate_limit_ip_overhead": True,
"geoip_db_location": "/usr/share/GeoIP/GeoIP.dat",
"cache_size": 512,
"cache_expiry": 60,
"auto_manage_prefer_seeds": False,
"shared": False
'peer_tos': '0x00',
'rate_limit_ip_overhead': True,
'geoip_db_location': '/usr/share/GeoIP/GeoIP.dat',
'cache_size': 512,
'cache_expiry': 60,
'auto_manage_prefer_seeds': False,
'shared': False,
'super_seeding': False
}
class PreferencesManager(component.Component):
def __init__(self):
component.Component.__init__(self, "PreferencesManager")
component.Component.__init__(self, 'PreferencesManager')
self.config = deluge.configmanager.ConfigManager('core.conf', DEFAULT_PREFS)
if 'proxies' in self.config:
log.warning('Updating config file for proxy, using "peer" values to fill new "proxy" setting')
self.config['proxy'].update(self.config['proxies']['peer'])
log.warning('New proxy config is: %s', self.config['proxy'])
del self.config['proxies']
if 'i2p_proxy' in self.config and self.config['i2p_proxy']['hostname']:
self.config['proxy'].update(self.config['i2p_proxy'])
self.config['proxy']['type'] = 6
del self.config['i2p_proxy']
if 'anonymous_mode' in self.config:
self.config['proxy']['anonymous_mode'] = self.config['anonymous_mode']
del self.config['anonymous_mode']
if 'proxy' in self.config:
for key in DEFAULT_PREFS['proxy']:
if key not in self.config['proxy']:
self.config['proxy'][key] = DEFAULT_PREFS['proxy'][key]
self.config = deluge.configmanager.ConfigManager("core.conf", DEFAULT_PREFS)
if 'public' in self.config:
log.debug("Updating configuration file: Renamed torrent's public "
"attribute to shared.")
self.config["shared"] = self.config["public"]
del self.config["public"]
def start(self):
self.core = component.get("Core")
self.session = component.get("Core").session
self.core = component.get('Core')
self.new_release_timer = None
def start(self):
# Set the initial preferences on start-up
for key in DEFAULT_PREFS:
self.do_config_set_func(key, self.config[key])
@ -172,126 +163,91 @@ class PreferencesManager(component.Component):
# Config set functions
def do_config_set_func(self, key, value):
on_set_func = getattr(self, "_on_set_" + key, None)
on_set_func = getattr(self, '_on_set_' + key, None)
if on_set_func:
if log.isEnabledFor(logging.DEBUG):
log.debug('Config key: %s set to %s..', key, value)
on_set_func(key, value)
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):
self.do_config_set_func(key, value)
component.get("EventManager").emit(ConfigValueChangedEvent(key, value))
if self.get_state() == 'Started':
self.do_config_set_func(key, value)
component.get('EventManager').emit(ConfigValueChangedEvent(key, value))
def _on_set_torrentfiles_location(self, key, value):
if self.config["copy_torrent_file"]:
if self.config['copy_torrent_file']:
try:
os.makedirs(value)
except Exception, e:
log.debug("Unable to make directory: %s", e)
except OSError as ex:
log.debug('Unable to make directory: %s', ex)
def _on_set_listen_ports(self, key, value):
# Only set the listen ports if random_port is not true
if self.config["random_port"] is not True:
log.debug("listen port range set to %s-%s", value[0], value[1])
self.session.listen_on(
value[0], value[1], str(self.config["listen_interface"])
)
self.__set_listen_on()
def _on_set_listen_interface(self, key, value):
# Call the random_port callback since it'll do what we need
self._on_set_random_port("random_port", self.config["random_port"])
self.__set_listen_on()
def _on_set_random_port(self, key, value):
log.debug("random port value set to %s", value)
# We need to check if the value has been changed to true and false
# and then handle accordingly.
if value:
import random
listen_ports = []
randrange = lambda: random.randrange(49152, 65525)
listen_ports.append(randrange())
listen_ports.append(listen_ports[0]+10)
else:
listen_ports = self.config["listen_ports"]
self.__set_listen_on()
# Set the listen ports
log.debug("listen port range set to %s-%s", listen_ports[0],
listen_ports[1])
self.session.listen_on(
listen_ports[0], listen_ports[1],
str(self.config["listen_interface"])
)
def __set_listen_on(self):
""" Set the ports and interface address to listen for incoming connections on."""
if self.config['random_port']:
if not self.config['listen_random_port']:
self.config['listen_random_port'] = random.randrange(49152, 65525)
listen_ports = [self.config['listen_random_port']] * 2 # use single port range
else:
self.config['listen_random_port'] = None
listen_ports = self.config['listen_ports']
interface = str(self.config['listen_interface'].strip())
interface = interface if interface else '0.0.0.0'
log.debug('Listen Interface: %s, Ports: %s with use_sys_port: %s',
interface, listen_ports, self.config['listen_use_sys_port'])
interfaces = ['%s:%s' % (interface, port) for port in range(listen_ports[0], listen_ports[1]+1)]
self.core.apply_session_settings(
{'listen_system_port_fallback': self.config['listen_use_sys_port'],
'listen_interfaces': ''.join(interfaces)})
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.session_set_setting("outgoing_ports", (value[0], value[1]))
self.__set_outgoing_ports()
def _on_set_random_outgoing_ports(self, key, value):
if value:
self.session.outgoing_ports(0, 0)
self.__set_outgoing_ports()
def __set_outgoing_ports(self):
port = 0 if self.config['random_outgoing_ports'] else self.config['outgoing_ports'][0]
if port:
num_ports = self.config['outgoing_ports'][1] - self.config['outgoing_ports'][0]
num_ports = num_ports if num_ports > 1 else 5
else:
num_ports = 0
log.debug('Outgoing port set to %s with range: %s', port, num_ports)
self.core.apply_session_settings({'outgoing_port': port, 'num_outgoing_ports': num_ports})
def _on_set_peer_tos(self, key, value):
log.debug("setting peer_tos to: %s", value)
try:
self.session_set_setting("peer_tos", chr(int(value, 16)))
except ValueError, e:
log.debug("Invalid tos byte: %s", e)
return
self.core.apply_session_setting('peer_tos', int(value, 16))
except ValueError as ex:
log.error('Invalid tos byte: %s', ex)
def _on_set_dht(self, key, value):
log.debug("dht value set to %s", value)
state_file = deluge.configmanager.get_config_dir("dht.state")
if value:
state = None
try:
state = lt.bdecode(open(state_file, "rb").read())
except Exception, e:
log.warning("Unable to read DHT state file: %s", e)
try:
self.session.start_dht(state)
except Exception, e:
log.warning("Restoring old DHT state failed: %s", e)
self.session.start_dht(None)
self.session.add_dht_router("router.bittorrent.com", 6881)
self.session.add_dht_router("router.utorrent.com", 6881)
self.session.add_dht_router("router.bitcomet.com", 6881)
else:
self.core.save_dht_state()
self.session.stop_dht()
dht_bootstraps = 'router.bittorrent.com:6881,router.utorrent.com:6881,router.bitcomet.com:6881'
self.core.apply_session_settings({'dht_bootstrap_nodes': dht_bootstraps, 'enable_dht': value})
def _on_set_upnp(self, key, value):
log.debug("upnp value set to %s", value)
if value:
self.session.start_upnp()
else:
self.session.stop_upnp()
self.core.apply_session_setting('enable_upnp', value)
def _on_set_natpmp(self, key, value):
log.debug("natpmp value set to %s", value)
if value:
self.session.start_natpmp()
else:
self.session.stop_natpmp()
self.core.apply_session_setting('enable_natpmp', value)
def _on_set_lsd(self, key, value):
log.debug("lsd value set to %s", value)
if value:
self.session.start_lsd()
else:
self.session.stop_lsd()
self.core.apply_session_setting('enable_lsd', value)
def _on_set_utpex(self, key, value):
log.debug("utpex value set to %s", value)
if value:
# Note: All libtorrent python bindings to set plugins/extensions need to be disabled
# due to GIL issue. https://code.google.com/p/libtorrent/issues/detail?id=369
#self.session.add_extension(lt.create_ut_pex_plugin)
pass
self.core.session.add_extension('ut_pex')
def _on_set_enc_in_policy(self, key, value):
self._on_set_encryption(key, value)
@ -302,179 +258,155 @@ class PreferencesManager(component.Component):
def _on_set_enc_level(self, key, value):
self._on_set_encryption(key, value)
def _on_set_enc_prefer_rc4(self, key, value):
self._on_set_encryption(key, value)
def _on_set_encryption(self, key, value):
log.debug("encryption value %s set to %s..", key, value)
pe_settings = lt.pe_settings()
pe_settings.out_enc_policy = \
lt.enc_policy(self.config["enc_out_policy"])
pe_settings.in_enc_policy = lt.enc_policy(self.config["enc_in_policy"])
pe_settings.allowed_enc_level = lt.enc_level(self.config["enc_level"])
pe_settings.prefer_rc4 = self.config["enc_prefer_rc4"]
self.session.set_pe_settings(pe_settings)
set = self.session.get_pe_settings()
log.debug("encryption settings:\n\t\t\tout_policy: %s\n\t\t\
in_policy: %s\n\t\t\tlevel: %s\n\t\t\tprefer_rc4: %s",
set.out_enc_policy,
set.in_enc_policy,
set.allowed_enc_level,
set.prefer_rc4)
# Convert Deluge enc_level values to libtorrent enc_level values.
pe_enc_level = {0: lt.enc_level.plaintext, 1: lt.enc_level.rc4, 2: lt.enc_level.both}
self.core.apply_session_settings(
{'out_enc_policy': lt.enc_policy(self.config['enc_out_policy']),
'in_enc_policy': lt.enc_policy(self.config['enc_in_policy']),
'allowed_enc_level': lt.enc_level(pe_enc_level[self.config['enc_level']]),
'prefer_rc4': True})
def _on_set_max_connections_global(self, key, value):
log.debug("max_connections_global set to %s..", value)
self.session.set_max_connections(value)
self.core.apply_session_setting('connections_limit', value)
def _on_set_max_upload_speed(self, key, value):
log.debug("max_upload_speed set to %s..", value)
# We need to convert Kb/s to B/s
if value < 0:
v = -1
else:
v = int(value * 1024)
self.session.set_upload_rate_limit(v)
value = -1 if value < 0 else int(value * 1024)
self.core.apply_session_setting('upload_rate_limit', value)
def _on_set_max_download_speed(self, key, value):
log.debug("max_download_speed set to %s..", value)
# We need to convert Kb/s to B/s
if value < 0:
v = -1
else:
v = int(value * 1024)
self.session.set_download_rate_limit(v)
value = -1 if value < 0 else int(value * 1024)
self.core.apply_session_setting('download_rate_limit', value)
def _on_set_max_upload_slots_global(self, key, value):
log.debug("max_upload_slots_global set to %s..", value)
self.session.set_max_uploads(value)
self.core.apply_session_setting('unchoke_slots_limit', value)
def _on_set_max_half_open_connections(self, key, value):
self.session.set_max_half_open_connections(value)
self.core.apply_session_setting('half_open_limit', value)
def _on_set_max_connections_per_second(self, key, value):
self.session_set_setting("connection_speed", value)
self.core.apply_session_setting('connection_speed', value)
def _on_set_ignore_limits_on_local_network(self, key, value):
self.session_set_setting("ignore_limits_on_local_network", value)
self.core.apply_session_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.session_set_setting("share_ratio_limit", value)
# This value is a float percentage in deluge, but libtorrent needs int percentage.
self.core.apply_session_setting('share_ratio_limit', int(value * 100))
def _on_set_seed_time_ratio_limit(self, key, value):
log.debug("%s set to %s..", key, value)
self.session_set_setting("seed_time_ratio_limit", value)
# This value is a float percentage in deluge, but libtorrent needs int percentage.
self.core.apply_session_setting('seed_time_ratio_limit', int(value * 100))
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.session_set_setting("seed_time_limit", int(value * 60))
self.core.apply_session_setting('seed_time_limit', int(value * 60))
def _on_set_max_active_downloading(self, key, value):
log.debug("%s set to %s..", key, value)
self.session_set_setting("active_downloads", value)
self.core.apply_session_setting('active_downloads', value)
def _on_set_max_active_seeding(self, key, value):
log.debug("%s set to %s..", key, value)
self.session_set_setting("active_seeds", value)
self.core.apply_session_setting('active_seeds', value)
def _on_set_max_active_limit(self, key, value):
log.debug("%s set to %s..", key, value)
self.session_set_setting("active_limit", value)
self.core.apply_session_setting('active_limit', value)
def _on_set_dont_count_slow_torrents(self, key, value):
log.debug("%s set to %s..", key, value)
self.session_set_setting("dont_count_slow_torrents", value)
self.core.apply_session_setting('dont_count_slow_torrents', value)
def _on_set_send_info(self, key, value):
log.debug("Sending anonymous stats..")
"""sends anonymous stats home"""
class Send_Info_Thread(threading.Thread):
log.debug('Sending anonymous stats..')
class SendInfoThread(threading.Thread):
def __init__(self, config):
self.config = config
threading.Thread.__init__(self)
def run(self):
import time
now = time.time()
# check if we've done this within the last week or never
if (now - self.config["info_sent"]) >= (60 * 60 * 24 * 7):
import deluge.common
from urllib import quote_plus
from urllib2 import urlopen
import platform
if (now - self.config['info_sent']) >= (60 * 60 * 24 * 7):
try:
url = "http://deluge-torrent.org/stats_get.php?processor=" + \
platform.machine() + "&python=" + platform.python_version() \
+ "&deluge=" + deluge.common.get_version() \
+ "&os=" + platform.system() \
+ "&plugins=" + quote_plus(":".join(self.config["enabled_plugins"]))
url = 'http://deluge-torrent.org/stats_get.php?processor=' + \
platform.machine() + '&python=' + platform.python_version() \
+ '&deluge=' + deluge.common.get_version() \
+ '&os=' + platform.system() \
+ '&plugins=' + quote_plus(':'.join(self.config['enabled_plugins']))
urlopen(url)
except IOError, e:
log.debug("Network error while trying to send info: %s", e)
except IOError as ex:
log.debug('Network error while trying to send info: %s', ex)
else:
self.config["info_sent"] = now
self.config['info_sent'] = now
if value:
Send_Info_Thread(self.config).start()
SendInfoThread(self.config).start()
def _on_set_new_release_check(self, key, value):
if value:
log.debug("Checking for new release..")
log.debug('Checking for new release..')
threading.Thread(target=self.core.get_new_release).start()
if self.new_release_timer and self.new_release_timer.running:
self.new_release_timer.stop()
# Set a timer to check for a new release every 3 days
self.new_release_timer = LoopingCall(
self._on_set_new_release_check, "new_release_check", True)
self._on_set_new_release_check, 'new_release_check', True)
self.new_release_timer.start(72 * 60 * 60, False)
else:
if self.new_release_timer and self.new_release_timer.running:
self.new_release_timer.stop()
def _on_set_proxies(self, key, value):
for k, v in value.items():
if v["type"]:
proxy_settings = lt.proxy_settings()
proxy_settings.type = lt.proxy_type(v["type"])
proxy_settings.username = str(v["username"])
proxy_settings.password = str(v["password"])
proxy_settings.hostname = str(v["hostname"])
proxy_settings.port = v["port"]
log.debug("setting %s proxy settings", k)
getattr(self.session, "set_%s_proxy" % k)(proxy_settings)
def _on_set_proxy(self, key, value):
# Initialise with type none and blank hostnames.
proxy_settings = {
'proxy_type': lt.proxy_type.none,
'i2p_hostname': '',
'proxy_hostname': '',
'proxy_hostnames': value['proxy_hostnames'],
'proxy_peer_connections': value['proxy_peer_connections'],
'proxy_tracker_connections': value['proxy_tracker_connections'],
'force_proxy': value['force_proxy'],
'anonymous_mode': value['anonymous_mode']
}
if value['type'] == lt.proxy_type.i2p_proxy:
proxy_settings.update({
'proxy_type': lt.proxy_type.i2p_proxy,
'i2p_hostname': value['hostname'],
'i2p_port': value['port'],
})
elif value['type'] != lt.proxy_type.none:
proxy_settings.update({
'proxy_type': value['type'],
'proxy_hostname': value['hostname'],
'proxy_port': value['port'],
'proxy_username': value['username'],
'proxy_password': value['password'],
})
self.core.apply_session_settings(proxy_settings)
def _on_set_rate_limit_ip_overhead(self, key, value):
log.debug("%s: %s", key, value)
self.session_set_setting("rate_limit_ip_overhead", value)
self.core.apply_session_setting('rate_limit_ip_overhead', value)
def _on_set_geoip_db_location(self, key, value):
log.debug("%s: %s", key, value)
def _on_set_geoip_db_location(self, key, geoipdb_path):
# Load the GeoIP DB for country look-ups if available
geoip_db = ""
if os.path.exists(value):
geoip_db = value
elif os.path.exists(deluge.common.resource_filename("deluge", os.path.join("data", "GeoIP.dat"))):
geoip_db = deluge.common.resource_filename(
"deluge", os.path.join("data", "GeoIP.dat")
)
else:
log.warning("Unable to find GeoIP database file!")
if geoip_db:
if os.path.exists(geoipdb_path):
try:
self.session.load_country_db(str(geoip_db))
except Exception, e:
log.error("Unable to load geoip database!")
log.exception(e)
self.core.geoip_instance = GeoIP.open(geoipdb_path, GeoIP.GEOIP_STANDARD)
except AttributeError:
log.warning('GeoIP Unavailable')
else:
log.warning('Unable to find GeoIP database file: %s', geoipdb_path)
def _on_set_cache_size(self, key, value):
log.debug("%s: %s", key, value)
self.session_set_setting("cache_size", value)
self.core.apply_session_setting('cache_size', value)
def _on_set_cache_expiry(self, key, value):
log.debug("%s: %s", key, value)
self.session_set_setting("cache_expiry", value)
self.core.apply_session_setting('cache_expiry', value)
def _on_auto_manage_prefer_seeds(self, key, value):
log.debug("%s set to %s..", key, value)
self.session_set_setting("auto_manage_prefer_seeds", value)
self.core.apply_session_setting('auto_manage_prefer_seeds', value)

View File

@ -1,65 +1,32 @@
#
# rpcserver.py
# -*- coding: utf-8 -*-
#
# Copyright (C) 2008,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.
#
# This file is part of Deluge and is licensed under GNU General Public License 3.0, or later, with
# the additional special exception to link portions of this program with the OpenSSL library.
# See LICENSE for more details.
#
"""RPCServer Module"""
from __future__ import unicode_literals
import sys
import zlib
import logging
import os
import stat
import logging
import sys
import traceback
from twisted.internet.protocol import Factory, Protocol
from twisted.internet import reactor, defer
from OpenSSL import crypto, SSL
from collections import namedtuple
from types import FunctionType
try:
import rencode
except ImportError:
import deluge.rencode as rencode
from OpenSSL import SSL, crypto
from twisted.internet import defer, reactor
from twisted.internet.protocol import Factory, connectionDone
import deluge.component as component
import deluge.configmanager
from deluge.core.authmanager import (AUTH_LEVEL_NONE, AUTH_LEVEL_DEFAULT,
AUTH_LEVEL_ADMIN)
from deluge.error import (DelugeError, NotAuthorizedError, WrappedException,
_ClientSideRecreateError, IncompatibleClient)
from deluge.core.authmanager import AUTH_LEVEL_ADMIN, AUTH_LEVEL_DEFAULT, AUTH_LEVEL_NONE
from deluge.error import DelugeError, IncompatibleClient, NotAuthorizedError, WrappedException, _ClientSideRecreateError
from deluge.event import ClientDisconnectedEvent
from deluge.transfer import DelugeTransferProtocol
RPC_RESPONSE = 1
@ -68,6 +35,7 @@ RPC_EVENT = 3
log = logging.getLogger(__name__)
def export(auth_level=AUTH_LEVEL_DEFAULT):
"""
Decorator function to register an object's method as an RPC. The object
@ -83,13 +51,13 @@ def export(auth_level=AUTH_LEVEL_DEFAULT):
func._rpcserver_export = True
func._rpcserver_auth_level = auth_level
doc = func.__doc__
func.__doc__ = "**RPC Exported Function** (*Auth Level: %s*)\n\n" % auth_level
func.__doc__ = '**RPC Exported Function** (*Auth Level: %s*)\n\n' % auth_level
if doc:
func.__doc__ += doc
return func
if type(auth_level) is FunctionType:
if isinstance(auth_level, FunctionType):
func = auth_level
auth_level = AUTH_LEVEL_DEFAULT
return wrap(func)
@ -109,34 +77,41 @@ def format_request(call):
"""
try:
s = call[1] + "("
s = call[1] + '('
if call[2]:
s += ", ".join([str(x) for x in call[2]])
s += ', '.join([str(x) for x in call[2]])
if call[3]:
if call[2]:
s += ", "
s += ", ".join([key + "=" + str(value) for key, value in call[3].items()])
s += ")"
s += ', '
s += ', '.join([key + '=' + str(value) for key, value in call[3].items()])
s += ')'
except UnicodeEncodeError:
return "UnicodeEncodeError, call: %s" % call
return 'UnicodeEncodeError, call: %s' % call
else:
return s
class ServerContextFactory(object):
def getContext(self):
def getContext(self): # NOQA: N802
"""
Create an SSL context.
This loads the servers cert/private key SSL files for use with the
SSL transport.
"""
ssl_dir = deluge.configmanager.get_config_dir("ssl")
ctx = SSL.Context(SSL.SSLv3_METHOD)
ctx.use_certificate_file(os.path.join(ssl_dir, "daemon.cert"))
ctx.use_privatekey_file(os.path.join(ssl_dir, "daemon.pkey"))
ssl_dir = deluge.configmanager.get_config_dir('ssl')
ctx = SSL.Context(SSL.SSLv23_METHOD)
ctx.set_options(SSL.OP_NO_SSLv2 | SSL.OP_NO_SSLv3)
ctx.use_certificate_file(os.path.join(ssl_dir, 'daemon.cert'))
ctx.use_privatekey_file(os.path.join(ssl_dir, 'daemon.pkey'))
return ctx
class DelugeRPCProtocol(DelugeTransferProtocol):
def __init__(self):
super(DelugeRPCProtocol, self).__init__()
# namedtuple subclass with auth_level, username for the connected session.
self.AuthLevel = namedtuple('SessionAuthlevel', 'auth_level, username')
def message_received(self, request):
"""
@ -144,28 +119,28 @@ class DelugeRPCProtocol(DelugeTransferProtocol):
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
:meth:`dispatch`.
:param request: the request from the client.
:type data: tuple
"""
if type(request) is not tuple:
log.debug("Received invalid message: type is not tuple")
if not isinstance(request, tuple):
log.debug('Received invalid message: type is not tuple')
return
if len(request) < 1:
log.debug("Received invalid message: there are no items")
log.debug('Received invalid message: there are no items')
return
for call in request:
if len(call) != 4:
log.debug("Received invalid rpc request: number of items "
"in request is %s", len(call))
log.debug('Received invalid rpc request: number of items '
'in request is %s', len(call))
continue
#log.debug("RPCRequest: %s", format_request(call))
# log.debug('RPCRequest: %s', format_request(call))
reactor.callLater(0, self.dispatch, *call)
def sendData(self, data):
def sendData(self, data): # NOQA: N802
"""
Sends the data to the client.
@ -174,19 +149,25 @@ class DelugeRPCProtocol(DelugeTransferProtocol):
:type data: object
"""
self.transfer_message(data)
try:
self.transfer_message(data)
except Exception as ex:
log.warn('Error occurred when sending message: %s.', ex)
log.exception(ex)
raise
def connectionMade(self):
def connectionMade(self): # NOQA: N802
"""
This method is called when a new client connects.
"""
peer = self.transport.getPeer()
log.info("Deluge Client connection made from: %s:%s",
log.info('Deluge Client connection made from: %s:%s',
peer.host, peer.port)
# Set the initial auth level of this session to AUTH_LEVEL_NONE
self.factory.authorized_sessions[self.transport.sessionno] = AUTH_LEVEL_NONE
self.factory.authorized_sessions[
self.transport.sessionno] = self.AuthLevel(AUTH_LEVEL_NONE, '')
def connectionLost(self, reason):
def connectionLost(self, reason=connectionDone): # NOQA: N802
"""
This method is called when the client is disconnected.
@ -202,7 +183,9 @@ class DelugeRPCProtocol(DelugeTransferProtocol):
if self.transport.sessionno in self.factory.interested_events:
del self.factory.interested_events[self.transport.sessionno]
log.info("Deluge client disconnected: %s", reason.value)
if self.factory.state == 'running':
component.get('EventManager').emit(ClientDisconnectedEvent(self.factory.session_id))
log.info('Deluge client disconnected: %s', reason.value)
def valid_session(self):
return self.transport.sessionno in self.factory.authorized_sessions
@ -223,12 +206,12 @@ class DelugeRPCProtocol(DelugeTransferProtocol):
:type kwargs: dict
"""
def sendError():
def send_error():
"""
Sends an error response with the contents of the exception that was raised.
"""
exceptionType, exceptionValue, exceptionTraceback = sys.exc_info()
formated_tb = "".join(traceback.format_tb(exceptionTraceback))
exceptionType, exceptionValue, dummy_exceptionTraceback = sys.exc_info()
formated_tb = traceback.format_exc()
try:
self.sendData((
RPC_ERROR,
@ -238,45 +221,51 @@ class DelugeRPCProtocol(DelugeTransferProtocol):
exceptionValue._kwargs,
formated_tb
))
except Exception, err:
# This most likely not a deluge exception, let's wrap it
log.error("An exception occurred while sending RPC_ERROR to "
"client. Wrapping it and resending. Error to "
"send(causing exception goes next):\n%s", formated_tb)
log.exception(err)
except AttributeError:
# This is not a deluge exception (object has no attribute '_args), let's wrap it
log.warning('An exception occurred while sending RPC_ERROR to '
'client. Wrapping it and resending. Error to '
'send(causing exception goes next):\n%s', formated_tb)
try:
raise WrappedException(str(exceptionValue), exceptionType.__name__, formated_tb)
except:
sendError()
except WrappedException:
send_error()
except Exception as ex:
log.error('An exception occurred while sending RPC_ERROR to client: %s', ex)
if method == "daemon.info":
if method == 'daemon.info':
# This is a special case and used in the initial connection process
self.sendData((RPC_RESPONSE, request_id, deluge.common.get_version()))
return
elif method == "daemon.login":
elif method == 'daemon.login':
# This is a special case and used in the initial connection process
# We need to authenticate the user here
log.debug("RPC dispatch daemon.login")
log.debug('RPC dispatch daemon.login')
try:
client_version = kwargs.pop('client_version', None)
if client_version is None:
raise IncompatibleClient(deluge.common.get_version())
ret = component.get("AuthManager").authorize(*args, **kwargs)
ret = component.get('AuthManager').authorize(*args, **kwargs)
if ret:
self.factory.authorized_sessions[self.transport.sessionno] = (ret, args[0])
self.factory.authorized_sessions[
self.transport.sessionno] = self.AuthLevel(ret, args[0])
self.factory.session_protocols[self.transport.sessionno] = self
except Exception, e:
sendError()
if not isinstance(e, _ClientSideRecreateError):
log.exception(e)
except Exception as ex:
send_error()
if not isinstance(ex, _ClientSideRecreateError):
log.exception(ex)
else:
self.sendData((RPC_RESPONSE, request_id, (ret)))
if not ret:
self.transport.loseConnection()
finally:
return
elif method == "daemon.set_event_interest" and self.valid_session():
log.debug("RPC dispatch daemon.set_event_interest")
return
# Anything below requires a valid session
if not self.valid_session():
return
if method == 'daemon.set_event_interest':
log.debug('RPC dispatch daemon.set_event_interest')
# This special case is to allow clients to set which events they are
# interested in receiving.
# We are expecting a sequence from the client.
@ -284,51 +273,61 @@ class DelugeRPCProtocol(DelugeTransferProtocol):
if self.transport.sessionno not in self.factory.interested_events:
self.factory.interested_events[self.transport.sessionno] = []
self.factory.interested_events[self.transport.sessionno].extend(args[0])
except Exception, e:
sendError()
except Exception:
send_error()
else:
self.sendData((RPC_RESPONSE, request_id, (True)))
finally:
return
if method not in self.factory.methods:
try:
# Raise exception to be sent back to client
raise AttributeError('RPC call on invalid function: %s' % method)
except AttributeError:
send_error()
return
if method in self.factory.methods and self.valid_session():
log.debug("RPC dispatch %s", method)
try:
method_auth_requirement = self.factory.methods[method]._rpcserver_auth_level
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)
raise NotAuthorizedError(auth_level, method_auth_requirement)
# Set the session_id in the factory so that methods can know
# which session is calling it.
self.factory.session_id = self.transport.sessionno
ret = self.factory.methods[method](*args, **kwargs)
except Exception, e:
sendError()
# Don't bother printing out DelugeErrors, because they are just
# for the client
if not isinstance(e, DelugeError):
log.exception("Exception calling RPC request: %s", e)
else:
# Check if the return value is a deferred, since we'll need to
# wait for it to fire before sending the RPC_RESPONSE
if isinstance(ret, defer.Deferred):
def on_success(result):
log.debug('RPC dispatch %s', method)
try:
method_auth_requirement = self.factory.methods[method]._rpcserver_auth_level
auth_level = self.factory.authorized_sessions[self.transport.sessionno].auth_level
if auth_level < method_auth_requirement:
# This session is not allowed to call this method
log.debug('Session %s is attempting an unauthorized method call!',
self.transport.sessionno)
raise NotAuthorizedError(auth_level, method_auth_requirement)
# Set the session_id in the factory so that methods can know
# which session is calling it.
self.factory.session_id = self.transport.sessionno
ret = self.factory.methods[method](*args, **kwargs)
except Exception as ex:
send_error()
# Don't bother printing out DelugeErrors, because they are just
# for the client
if not isinstance(ex, DelugeError):
log.exception('Exception calling RPC request: %s', ex)
else:
# Check if the return value is a deferred, since we'll need to
# wait for it to fire before sending the RPC_RESPONSE
if isinstance(ret, defer.Deferred):
def on_success(result):
try:
self.sendData((RPC_RESPONSE, request_id, result))
return result
except Exception:
send_error()
return result
def on_fail(failure):
try:
failure.raiseException()
except Exception, e:
sendError()
return failure
def on_fail(failure):
try:
failure.raiseException()
except Exception:
send_error()
return failure
ret.addCallbacks(on_success, on_fail)
else:
self.sendData((RPC_RESPONSE, request_id, ret))
ret.addCallbacks(on_success, on_fail)
else:
self.sendData((RPC_RESPONSE, request_id, ret))
class RPCServer(component.Component):
"""
@ -346,12 +345,13 @@ class RPCServer(component.Component):
:type listen: bool
"""
def __init__(self, port=58846, interface="", allow_remote=False, listen=True):
component.Component.__init__(self, "RPCServer")
def __init__(self, port=58846, interface='', allow_remote=False, listen=True):
component.Component.__init__(self, 'RPCServer')
self.factory = Factory()
self.factory.protocol = DelugeRPCProtocol
self.factory.session_id = -1
self.factory.state = 'running'
# Holds the registered methods
self.factory.methods = {}
@ -367,24 +367,23 @@ class RPCServer(component.Component):
return
if allow_remote:
hostname = ""
hostname = ''
else:
hostname = "localhost"
hostname = 'localhost'
if interface:
hostname = interface
log.info("Starting DelugeRPC server %s:%s", hostname, port)
log.info('Starting DelugeRPC server %s:%s', hostname, port)
# Check for SSL keys and generate some if needed
check_ssl_keys()
try:
reactor.listenSSL(port, self.factory, ServerContextFactory(), interface=hostname)
except Exception, e:
log.info("Daemon already running or port not available..")
log.error(e)
sys.exit(0)
except Exception as ex:
log.debug('Daemon already running or port not available.: %s', ex)
raise
def register_object(self, obj, name=None):
"""
@ -400,11 +399,11 @@ class RPCServer(component.Component):
name = obj.__class__.__name__.lower()
for d in dir(obj):
if d[0] == "_":
if d[0] == '_':
continue
if getattr(getattr(obj, d), '_rpcserver_export', False):
log.debug("Registering method: %s", name + "." + d)
self.factory.methods[name + "." + d] = getattr(obj, d)
log.debug('Registering method: %s', name + '.' + d)
self.factory.methods[name + '.' + d] = getattr(obj, d)
def deregister_object(self, obj):
"""
@ -414,7 +413,7 @@ class RPCServer(component.Component):
"""
for key, value in self.factory.methods.items():
if value.im_self == obj:
if value.__self__ == obj:
del self.factory.methods[key]
def get_object_method(self, name):
@ -438,7 +437,7 @@ class RPCServer(component.Component):
:returns: the exported methods
:rtype: list
"""
return self.factory.methods.keys()
return list(self.factory.methods)
def get_session_id(self):
"""
@ -459,13 +458,13 @@ class RPCServer(component.Component):
"""
if not self.listen:
return "localclient"
return 'localclient'
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]
return self.factory.authorized_sessions[session_id].username
else:
# No connections made yet
return ""
return ''
def get_session_auth_level(self):
"""
@ -476,7 +475,7 @@ class RPCServer(component.Component):
"""
if not self.listen or not self.is_session_valid(self.get_session_id()):
return AUTH_LEVEL_ADMIN
return self.factory.authorized_sessions[self.get_session_id()][0]
return self.factory.authorized_sessions[self.get_session_id()].auth_level
def get_rpc_auth_level(self, rpc):
"""
@ -485,7 +484,7 @@ class RPCServer(component.Component):
:returns: the auth level
:rtype: int
"""
self.factory.methods[rpc]._rpcserver_auth_level
return self.factory.methods[rpc]._rpcserver_auth_level
def is_session_valid(self, session_id):
"""
@ -507,11 +506,11 @@ class RPCServer(component.Component):
:param event: the event to emit
:type event: :class:`deluge.event.DelugeEvent`
"""
log.debug("intevents: %s", self.factory.interested_events)
log.debug('intevents: %s', self.factory.interested_events)
# Find sessions interested in this event
for session_id, interest in self.factory.interested_events.items():
if event.name in interest:
log.debug("Emit Event: %s %s", event.name, event.args)
log.debug('Emit Event: %s %s', event.name, event.args)
# This session is interested so send a RPC_EVENT
self.factory.session_protocols[session_id].sendData(
(RPC_EVENT, event.name, event.args)
@ -527,47 +526,54 @@ class RPCServer(component.Component):
:type event: :class:`deluge.event.DelugeEvent`
"""
if not self.is_session_valid(session_id):
log.debug("Session ID %s is not valid. Not sending event \"%s\".", session_id, event.name)
log.debug('Session ID %s is not valid. Not sending event "%s".', session_id, event.name)
return
if session_id not in self.factory.interested_events:
log.debug("Session ID %s is not interested in any events. Not sending event \"%s\".", session_id, event.name)
log.debug('Session ID %s is not interested in any events. Not sending event "%s".',
session_id, event.name)
return
if event.name not in self.factory.interested_events[session_id]:
log.debug("Session ID %s is not interested in event \"%s\". Not sending it.", session_id, event.name)
log.debug('Session ID %s is not interested in event "%s". Not sending it.', session_id, event.name)
return
log.debug("Sending event \"%s\" with args \"%s\" to session id \"%s\".",
log.debug('Sending event "%s" with args "%s" to session id "%s".',
event.name, event.args, session_id)
self.factory.session_protocols[session_id].sendData((RPC_EVENT, event.name, event.args))
def stop(self):
self.factory.state = 'stopping'
def check_ssl_keys():
"""
Check for SSL cert/key and create them if necessary
"""
ssl_dir = deluge.configmanager.get_config_dir("ssl")
ssl_dir = deluge.configmanager.get_config_dir('ssl')
if not os.path.exists(ssl_dir):
# The ssl folder doesn't exist so we need to create it
os.makedirs(ssl_dir)
generate_ssl_keys()
else:
for f in ("daemon.pkey", "daemon.cert"):
for f in ('daemon.pkey', 'daemon.cert'):
if not os.path.exists(os.path.join(ssl_dir, f)):
generate_ssl_keys()
break
def generate_ssl_keys():
"""
This method generates a new SSL key/cert.
"""
digest = "md5"
from deluge.common import PY2
digest = 'sha256' if not PY2 else b'sha256'
# Generate key pair
pkey = crypto.PKey()
pkey.generate_key(crypto.TYPE_RSA, 1024)
pkey.generate_key(crypto.TYPE_RSA, 2048)
# Generate cert request
req = crypto.X509Req()
subj = req.get_subject()
setattr(subj, "CN", "Deluge Daemon")
setattr(subj, 'CN', 'Deluge Daemon')
req.set_pubkey(pkey)
req.sign(pkey, digest)
@ -575,20 +581,18 @@ def generate_ssl_keys():
cert = crypto.X509()
cert.set_serial_number(0)
cert.gmtime_adj_notBefore(0)
cert.gmtime_adj_notAfter(60*60*24*365*5) # Five Years
cert.gmtime_adj_notAfter(60 * 60 * 24 * 365 * 3) # Three Years
cert.set_issuer(req.get_subject())
cert.set_subject(req.get_subject())
cert.set_pubkey(req.get_pubkey())
cert.sign(pkey, digest)
# Write out files
ssl_dir = deluge.configmanager.get_config_dir("ssl")
open(os.path.join(ssl_dir, "daemon.pkey"), "w").write(
crypto.dump_privatekey(crypto.FILETYPE_PEM, pkey)
)
open(os.path.join(ssl_dir, "daemon.cert"), "w").write(
crypto.dump_certificate(crypto.FILETYPE_PEM, cert)
)
ssl_dir = deluge.configmanager.get_config_dir('ssl')
with open(os.path.join(ssl_dir, 'daemon.pkey'), 'wb') as _file:
_file.write(crypto.dump_privatekey(crypto.FILETYPE_PEM, pkey))
with open(os.path.join(ssl_dir, 'daemon.cert'), 'wb') as _file:
_file.write(crypto.dump_certificate(crypto.FILETYPE_PEM, cert))
# Make the files only readable by this user
for f in ("daemon.pkey", "daemon.cert"):
for f in ('daemon.pkey', 'daemon.cert'):
os.chmod(os.path.join(ssl_dir, f), stat.S_IREAD | stat.S_IWRITE)

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -1,40 +1,20 @@
#
# decorators.py
# -*- coding: utf-8 -*-
#
# Copyright (C) 2010 John Garland <johnnybg+deluge@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.
#
# This file is part of Deluge and is licensed under GNU General Public License 3.0, or later, with
# the additional special exception to link portions of this program with the OpenSSL library.
# See LICENSE for more details.
#
from __future__ import unicode_literals
import inspect
import re
import warnings
from functools import wraps
def proxy(proxy_func):
"""
Factory class which returns a decorator that passes
@ -49,3 +29,109 @@ def proxy(proxy_func):
return proxy_func(func, *args, **kwargs)
return wrapper
return decorator
def overrides(*args):
"""
Decorater function to specify when class methods override
super class methods.
When used as
@overrides
def funcname
the argument will be the funcname function.
When used as
@overrides(BaseClass)
def funcname
the argument will be the BaseClass
"""
stack = inspect.stack()
if inspect.isfunction(args[0]):
return _overrides(stack, args[0])
else:
# One or more classes are specifed, so return a function that will be
# called with the real function as argument
def ret_func(func, **kwargs):
return _overrides(stack, func, explicit_base_classes=args)
return ret_func
def _overrides(stack, method, explicit_base_classes=None):
# stack[0]=overrides, stack[1]=inside class def'n, stack[2]=outside class def'n
classes = {}
derived_class_locals = stack[2][0].f_locals
# Find all super classes
m = re.search(r'class\s(.+)\((.+)\)\s*\:', stack[2][4][0])
class_name = m.group(1)
base_classes = m.group(2)
# Handle multiple inheritance
base_classes = [s.strip() for s in base_classes.split(',')]
check_classes = base_classes
if not base_classes:
raise ValueError('overrides decorator: unable to determine base class of class "%s"' % class_name)
def get_class(cls_name):
if '.' not in cls_name:
return derived_class_locals[cls_name]
else:
components = cls_name.split('.')
# obj is either a module or a class
obj = derived_class_locals[components[0]]
for c in components[1:]:
assert inspect.ismodule(obj) or inspect.isclass(obj)
obj = getattr(obj, c)
return obj
if explicit_base_classes:
# One or more base classes are explicitly given, check only those classes
override_classes = re.search(r'\s*@overrides\((.+)\)\s*', stack[1][4][0]).group(1)
override_classes = [c.strip() for c in override_classes.split(',')]
check_classes = override_classes
for c in base_classes + check_classes:
classes[c] = get_class(c)
# Verify that the excplicit override class is one of base classes
if explicit_base_classes:
from itertools import product
for bc, cc in product(base_classes, check_classes):
if issubclass(classes[bc], classes[cc]):
break
else:
raise Exception('Excplicit override class "%s" is not a super class of: %s'
% (explicit_base_classes, class_name))
if not all(hasattr(classes[cls], method.__name__) for cls in check_classes):
for cls in check_classes:
if not hasattr(classes[cls], method.__name__):
raise Exception('Function override "%s" not found in superclass: %s\n%s'
% (method.__name__, cls, 'File: %s:%s' % (stack[1][1], stack[1][2])))
if not any(hasattr(classes[cls], method.__name__) for cls in check_classes):
raise Exception('Function override "%s" not found in any superclass: %s\n%s'
% (method.__name__, check_classes, 'File: %s:%s' % (stack[1][1], stack[1][2])))
return method
def deprecated(func):
"""This is a decorator which can be used to mark function as deprecated.
It will result in a warning being emmitted when the function is used.
"""
@wraps(func)
def depr_func(*args, **kwargs):
warnings.simplefilter('always', DeprecationWarning) # Turn off filter
warnings.warn('Call to deprecated function {}.'.format(func.__name__),
category=DeprecationWarning, stacklevel=2)
warnings.simplefilter('default', DeprecationWarning) # Reset filter
return func(*args, **kwargs)
return depr_func

View File

@ -1,50 +1,18 @@
#
# error.py
# -*- coding: utf-8 -*-
#
# Copyright (C) 2008 Andrew Resch <andrewresch@gmail.com>
# Copyright (C) 2011 Pedro Algarvio <pedro@algarvio.me>
#
# 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.
#
# This file is part of Deluge and is licensed under GNU General Public License 3.0, or later, with
# the additional special exception to link portions of this program with the OpenSSL library.
# See LICENSE for more details.
#
from __future__ import unicode_literals
class DelugeError(Exception):
def _get_message(self):
return self._message
def _set_message(self, message):
self._message = message
message = property(_get_message, _set_message)
del _get_message, _set_message
def __str__(self):
return self.message
def __new__(cls, *args, **kwargs):
inst = super(DelugeError, cls).__new__(cls, *args, **kwargs)
@ -52,80 +20,79 @@ class DelugeError(Exception):
inst._kwargs = kwargs
return inst
class NoCoreError(DelugeError):
pass
def __init__(self, message=None):
super(DelugeError, self).__init__(message)
self.message = message
def __str__(self):
return self.message
class DaemonRunningError(DelugeError):
pass
class InvalidTorrentError(DelugeError):
pass
class AddTorrentError(DelugeError):
pass
class InvalidPathError(DelugeError):
pass
class WrappedException(DelugeError):
def _get_traceback(self):
return self._traceback
def _set_traceback(self, traceback):
self._traceback = traceback
traceback = property(_get_traceback, _set_traceback)
del _get_traceback, _set_traceback
def _get_type(self):
return self._type
def _set_type(self, type):
self._type = type
type = property(_get_type, _set_type)
del _get_type, _set_type
class WrappedException(DelugeError):
def __init__(self, message, exception_type, traceback):
self.message = message
super(WrappedException, self).__init__(message)
self.type = exception_type
self.traceback = traceback
def __str__(self):
return '%s\n%s' % (self.message, self.traceback)
class _ClientSideRecreateError(DelugeError):
pass
class IncompatibleClient(_ClientSideRecreateError):
def __init__(self, daemon_version):
self.daemon_version = daemon_version
self.message = _(
"Your deluge client is not compatible with the daemon. "
"Please upgrade your client to %(daemon_version)s"
) % dict(daemon_version=self.daemon_version)
msg = 'Your deluge client is not compatible with the daemon. '\
'Please upgrade your client to %(daemon_version)s' % \
dict(daemon_version=self.daemon_version)
super(IncompatibleClient, self).__init__(message=msg)
class NotAuthorizedError(_ClientSideRecreateError):
def __init__(self, current_level, required_level):
self.message = _(
"Auth level too low: %(current_level)s < %(required_level)s" %
msg = 'Auth level too low: %(current_level)s < %(required_level)s' % \
dict(current_level=current_level, required_level=required_level)
)
super(NotAuthorizedError, self).__init__(message=msg)
self.current_level = current_level
self.required_level = required_level
class _UsernameBasedPasstroughError(_ClientSideRecreateError):
def _get_username(self):
return self._username
def _set_username(self, username):
self._username = username
username = property(_get_username, _set_username)
del _get_username, _set_username
def __init__(self, message, username):
super(_UsernameBasedPasstroughError, self).__init__(message)
self.message = message
self.username = username
class BadLoginError(_UsernameBasedPasstroughError):
pass
class AuthenticationRequired(_UsernameBasedPasstroughError):
pass
class AuthManagerError(_UsernameBasedPasstroughError):
pass

View File

@ -1,36 +1,10 @@
#
# event.py
# -*- coding: utf-8 -*-
#
# 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.
#
# This file is part of Deluge and is licensed under GNU General Public License 3.0, or later, with
# the additional special exception to link portions of this program with the OpenSSL library.
# See LICENSE for more details.
#
"""
@ -40,17 +14,20 @@ This module describes the types of events that can be generated by the daemon
and subsequently emitted to the clients.
"""
from __future__ import unicode_literals
known_events = {}
class DelugeEventMetaClass(type):
"""
This metaclass simply keeps a list of all events classes created.
"""
def __init__(cls, name, bases, dct):
super(DelugeEventMetaClass, cls).__init__(name, bases, dct)
if name != "DelugeEvent":
known_events[name] = cls
def __init__(self, name, bases, dct): # pylint: disable=bad-mcs-method-argument
super(DelugeEventMetaClass, self).__init__(name, bases, dct)
if name != 'DelugeEvent':
known_events[name] = self
class DelugeEvent(object):
"""
@ -68,13 +45,14 @@ class DelugeEvent(object):
return self.__class__.__name__
def _get_args(self):
if not hasattr(self, "_args"):
if not hasattr(self, '_args'):
return []
return self._args
name = property(fget=_get_name)
args = property(fget=_get_args)
class TorrentAddedEvent(DelugeEvent):
"""
Emitted when a new torrent is successfully added to the session.
@ -88,6 +66,7 @@ class TorrentAddedEvent(DelugeEvent):
"""
self._args = [torrent_id, from_state]
class TorrentRemovedEvent(DelugeEvent):
"""
Emitted when a torrent has been removed from the session.
@ -99,6 +78,7 @@ class TorrentRemovedEvent(DelugeEvent):
"""
self._args = [torrent_id]
class PreTorrentRemovedEvent(DelugeEvent):
"""
Emitted when a torrent is about to be removed from the session.
@ -110,6 +90,7 @@ class PreTorrentRemovedEvent(DelugeEvent):
"""
self._args = [torrent_id]
class TorrentStateChangedEvent(DelugeEvent):
"""
Emitted when a torrent changes state.
@ -123,12 +104,27 @@ class TorrentStateChangedEvent(DelugeEvent):
"""
self._args = [torrent_id, state]
class TorrentTrackerStatusEvent(DelugeEvent):
"""
Emitted when a torrents tracker status changes.
"""
def __init__(self, torrent_id, status):
"""
Args:
torrent_id (str): the torrent_id
status (str): the new status
"""
self._args = [torrent_id, status]
class TorrentQueueChangedEvent(DelugeEvent):
"""
Emitted when the queue order has changed.
"""
pass
class TorrentFolderRenamedEvent(DelugeEvent):
"""
Emitted when a folder within a torrent has been renamed.
@ -144,6 +140,7 @@ class TorrentFolderRenamedEvent(DelugeEvent):
"""
self._args = [torrent_id, old, new]
class TorrentFileRenamedEvent(DelugeEvent):
"""
Emitted when a file within a torrent has been renamed.
@ -159,6 +156,7 @@ class TorrentFileRenamedEvent(DelugeEvent):
"""
self._args = [torrent_id, index, name]
class TorrentFinishedEvent(DelugeEvent):
"""
Emitted when a torrent finishes downloading.
@ -170,6 +168,7 @@ class TorrentFinishedEvent(DelugeEvent):
"""
self._args = [torrent_id]
class TorrentResumedEvent(DelugeEvent):
"""
Emitted when a torrent resumes from a paused state.
@ -181,12 +180,10 @@ 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):
"""
@ -197,6 +194,21 @@ class TorrentFileCompletedEvent(DelugeEvent):
"""
self._args = [torrent_id, index]
class TorrentStorageMovedEvent(DelugeEvent):
"""
Emitted when the storage location for a torrent has been moved.
"""
def __init__(self, torrent_id, path):
"""
:param torrent_id: the torrent_id
:type torrent_id: string
:param path: the new location
:type path: string
"""
self._args = [torrent_id, path]
class CreateTorrentProgressEvent(DelugeEvent):
"""
Emitted when creating a torrent file remotely.
@ -204,6 +216,7 @@ class CreateTorrentProgressEvent(DelugeEvent):
def __init__(self, piece_count, num_pieces):
self._args = [piece_count, num_pieces]
class NewVersionAvailableEvent(DelugeEvent):
"""
Emitted when a more recent version of Deluge is available.
@ -215,6 +228,7 @@ class NewVersionAvailableEvent(DelugeEvent):
"""
self._args = [new_release]
class SessionStartedEvent(DelugeEvent):
"""
Emitted when a session has started. This typically only happens once when
@ -222,18 +236,21 @@ class SessionStartedEvent(DelugeEvent):
"""
pass
class SessionPausedEvent(DelugeEvent):
"""
Emitted when the session has been paused.
"""
pass
class SessionResumedEvent(DelugeEvent):
"""
Emitted when the session has been resumed.
"""
pass
class ConfigValueChangedEvent(DelugeEvent):
"""
Emitted when a config value changes in the Core.
@ -246,6 +263,7 @@ class ConfigValueChangedEvent(DelugeEvent):
"""
self._args = [key, value]
class PluginEnabledEvent(DelugeEvent):
"""
Emitted when a plugin is enabled in the Core.
@ -253,6 +271,7 @@ class PluginEnabledEvent(DelugeEvent):
def __init__(self, plugin_name):
self._args = [plugin_name]
class PluginDisabledEvent(DelugeEvent):
"""
Emitted when a plugin is disabled in the Core.
@ -260,3 +279,22 @@ class PluginDisabledEvent(DelugeEvent):
def __init__(self, plugin_name):
self._args = [plugin_name]
class ClientDisconnectedEvent(DelugeEvent):
"""
Emitted when a client disconnects.
"""
def __init__(self, session_id):
self._args = [session_id]
class ExternalIPEvent(DelugeEvent):
"""
Emitted when the external ip address is received from libtorrent.
"""
def __init__(self, external_ip):
"""
Args:
external_ip (str): The IP address.
"""
self._args = [external_ip]

View File

@ -1,48 +1,34 @@
#
# httpdownloader.py
# -*- coding: utf-8 -*-
#
# 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.
# This file is part of Deluge and is licensed under GNU General Public License 3.0, or later, with
# the additional special exception to link portions of this program with the OpenSSL library.
# See LICENSE for more details.
#
from twisted.web import client, http
from twisted.web.error import PageRedirect
from twisted.python.failure import Failure
from twisted.internet import reactor
from common import get_version
from __future__ import unicode_literals
import logging
import os.path
import zlib
from twisted.internet import reactor
from twisted.python.failure import Failure
from twisted.web import client, http
from twisted.web.error import PageRedirect
from deluge.common import get_version, utf8_encode_structure
try:
from urllib.parse import urljoin
except ImportError:
# PY2 fallback
from urlparse import urljoin # pylint: disable=ungrouped-imports
log = logging.getLogger(__name__)
class HTTPDownloader(client.HTTPDownloader):
"""
Factory class for downloading files and keeping track of progress.
@ -62,56 +48,60 @@ class HTTPDownloader(client.HTTPDownloader):
:param headers: any optional headers to send
:type headers: dictionary
"""
self.part_callback = part_callback
self.current_length = 0
self.total_length = 0
self.decoder = None
self.value = filename
self.force_filename = force_filename
self.allow_compression = allow_compression
agent = "Deluge/%s (http://deluge-torrent.org)" % get_version()
self.code = None
agent = b'Deluge/%s (http://deluge-torrent.org)' % get_version().encode('utf8')
client.HTTPDownloader.__init__(self, url, filename, headers=headers, agent=agent)
def gotStatus(self, version, status, message):
def gotStatus(self, version, status, message): # NOQA: N802
self.code = int(status)
client.HTTPDownloader.gotStatus(self, version, status, message)
def gotHeaders(self, headers):
def gotHeaders(self, headers): # NOQA: N802
if self.code == http.OK:
if "content-length" in headers:
self.total_length = int(headers["content-length"][0])
if 'content-length' in headers:
self.total_length = int(headers['content-length'][0])
else:
self.total_length = 0
if self.allow_compression and "content-encoding" in headers and \
headers["content-encoding"][0] in ("gzip", "x-gzip", "deflate"):
if self.allow_compression and 'content-encoding' in headers and \
headers['content-encoding'][0] in ('gzip', 'x-gzip', 'deflate'):
# Adding 32 to the wbits enables gzip & zlib decoding (with automatic header detection)
# Adding 16 just enables gzip decoding (no zlib)
self.decoder = zlib.decompressobj(zlib.MAX_WBITS + 32)
if "content-disposition" in headers and not self.force_filename:
new_file_name = str(headers["content-disposition"][0]).split(";")[1].split("=")[1]
if 'content-disposition' in headers and not self.force_filename:
new_file_name = str(headers['content-disposition'][0]).split(';')[1].split('=')[1]
new_file_name = sanitise_filename(new_file_name)
new_file_name = os.path.join(os.path.split(self.fileName)[0], new_file_name)
new_file_name = os.path.join(os.path.split(self.value)[0], new_file_name)
count = 1
fileroot = os.path.splitext(new_file_name)[0]
fileext = os.path.splitext(new_file_name)[1]
while os.path.isfile(new_file_name):
# Increment filename if already exists
new_file_name = "%s-%s%s" % (fileroot, count, fileext)
new_file_name = '%s-%s%s' % (fileroot, count, fileext)
count += 1
self.fileName = new_file_name
self.value = new_file_name
elif self.code in (http.MOVED_PERMANENTLY, http.FOUND, http.SEE_OTHER, http.TEMPORARY_REDIRECT):
location = headers["location"][0]
location = headers['location'][0]
error = PageRedirect(self.code, location=location)
self.noPage(Failure(error))
return client.HTTPDownloader.gotHeaders(self, headers)
def pagePart(self, data):
def pagePart(self, data): # NOQA: N802
if self.code == http.OK:
self.current_length += len(data)
if self.decoder:
@ -121,7 +111,7 @@ class HTTPDownloader(client.HTTPDownloader):
return client.HTTPDownloader.pagePart(self, data)
def pageEnd(self):
def pageEnd(self): # NOQA: N802
if self.decoder:
data = self.decoder.flush()
self.current_length -= len(data)
@ -130,6 +120,7 @@ class HTTPDownloader(client.HTTPDownloader):
return client.HTTPDownloader.pageEnd(self)
def sanitise_filename(filename):
"""
Sanitises a filename to use as a download destination file.
@ -142,66 +133,140 @@ def sanitise_filename(filename):
"""
# Remove any quotes
filename = filename.strip("'\"")
filename = filename.strip('\'"')
if os.path.basename(filename) != filename:
# Dodgy server, log it
log.warning("Potentially malicious server: trying to write to file '%s'" % filename)
log.warning('Potentially malicious server: trying to write to file: %s', filename)
# Only use the basename
filename = os.path.basename(filename)
filename = filename.strip()
if filename.startswith(".") or ";" in filename or "|" in filename:
if filename.startswith('.') or ';' in filename or '|' in filename:
# Dodgy server, log it
log.warning("Potentially malicious server: trying to write to file '%s'" % filename)
log.warning('Potentially malicious server: trying to write to file: %s', filename)
return filename
def download_file(url, filename, callback=None, headers=None,
force_filename=False, allow_compression=True):
def _download_file(url, filename, callback=None, headers=None, force_filename=False, allow_compression=True):
"""
Downloads a file from a specific URL and returns a Deferred. You can also
specify a callback function to be called as parts are received.
Downloads a file from a specific URL and returns a Deferred. A callback
function can be specified to be called as parts are received.
:param url: the url to download from
:type url: string
:param filename: the filename to save the file as
:type filename: string
:param callback: a function to be called when a part of data is received,
it's signature should be: func(data, current_length, total_length)
:type callback: function
:param headers: any optional headers to send
:type headers: dictionary
:param force_filename: force us to use the filename specified rather than
one the server may suggest
:type force_filename: boolean
:param allow_compression: allows gzip & deflate decoding
:type allow_compression: boolean
Args:
url (str): The url to download from
filename (str): The filename to save the file as
callback (func): A function to be called when a part of data is received,
it's signature should be: func(data, current_length, total_length)
headers (dict): Any optional headers to send
force_filename (bool): force us to use the filename specified rather than
one the server may suggest
allow_compression (bool): Allows gzip & deflate decoding
:returns: the filename of the downloaded file
:rtype: Deferred
Returns:
Deferred: the filename of the downloaded file
Raises:
t.w.e.PageRedirect
t.w.e.Error: for all other HTTP response errors
:raises t.w.e.PageRedirect: when server responds with a temporary redirect
or permanently moved.
:raises t.w.e.Error: for all other HTTP response errors (besides OK)
"""
url = str(url)
filename = str(filename)
if headers:
for key, value in headers.items():
headers[str(key)] = str(value)
if allow_compression:
if not headers:
headers = {}
headers["accept-encoding"] = "deflate, gzip, x-gzip"
headers['accept-encoding'] = 'deflate, gzip, x-gzip'
scheme, host, port, path = client._parse(url)
url = url.encode('utf8')
filename = filename.encode('utf8')
headers = utf8_encode_structure(headers) if headers else headers
factory = HTTPDownloader(url, filename, callback, headers, force_filename, allow_compression)
if scheme == "https":
# In Twisted 13.1.0 _parse() function replaced by _URI class.
# In Twisted 15.0.0 _URI class renamed to URI.
if hasattr(client, '_parse'):
scheme, host, port, dummy_path = client._parse(url)
else:
try:
from twisted.web.client import _URI as URI
except ImportError:
from twisted.web.client import URI
finally:
uri = URI.fromBytes(url)
scheme = uri.scheme
host = uri.host
port = uri.port
if scheme == 'https':
from twisted.internet import ssl
reactor.connectSSL(host, port, factory, ssl.ClientContextFactory())
# ClientTLSOptions in Twisted >= 14, see ticket #2765 for details on this addition.
try:
from twisted.internet._sslverify import ClientTLSOptions
except ImportError:
ctx_factory = ssl.ClientContextFactory()
else:
class TLSSNIContextFactory(ssl.ClientContextFactory): # pylint: disable=no-init
"""
A custom context factory to add a server name for TLS connections.
"""
def getContext(self): # NOQA: N802
ctx = ssl.ClientContextFactory.getContext(self)
ClientTLSOptions(host, ctx)
return ctx
ctx_factory = TLSSNIContextFactory()
reactor.connectSSL(host, port, factory, ctx_factory)
else:
reactor.connectTCP(host, port, factory)
return factory.deferred
def download_file(url, filename, callback=None, headers=None, force_filename=False,
allow_compression=True, handle_redirects=True):
"""
Downloads a file from a specific URL and returns a Deferred. A callback
function can be specified to be called as parts are received.
Args:
url (str): The url to download from
filename (str): The filename to save the file as
callback (func): A function to be called when a part of data is received,
it's signature should be: func(data, current_length, total_length)
headers (dict): Any optional headers to send
force_filename (bool): force us to use the filename specified rather than
one the server may suggest
allow_compression (bool): Allows gzip & deflate decoding
handle_redirects (bool): If HTTP redirects should be handled automatically
Returns:
Deferred: the filename of the downloaded file
Raises:
t.w.e.PageRedirect: Unless handle_redirects=True
t.w.e.Error: for all other HTTP response errors
"""
def on_download_success(result):
log.debug('Download success!')
return result
def on_download_fail(failure):
if failure.check(PageRedirect) and handle_redirects:
new_url = urljoin(url, failure.getErrorMessage().split(' to ')[1])
result = _download_file(new_url, filename, callback=callback, headers=headers,
force_filename=force_filename,
allow_compression=allow_compression)
result.addCallbacks(on_download_success, on_download_fail)
else:
# Log the failure and pass to the caller
log.warning('Error occurred downloading file from "%s": %s',
url, failure.getErrorMessage())
result = failure
return result
d = _download_file(url, filename, callback=callback, headers=headers,
force_filename=force_filename, allow_compression=allow_compression)
d.addCallbacks(on_download_success, on_download_fail)
return d

View File

@ -1,61 +1,41 @@
#
# log.py
# -*- coding: utf-8 -*-
#
# Copyright (C) 2007 Andrew Resch <andrewresch@gmail.com>
# Copyright (C) 2010 Pedro Algarvio <pedro@algarvio.me>
#
# 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.
#
# This file is part of Deluge and is licensed under GNU General Public License 3.0, or later, with
# the additional special exception to link portions of this program with the OpenSSL library.
# See LICENSE for more details.
#
"""Logging functions"""
from __future__ import unicode_literals
import os
import inspect
import logging
from deluge import common
import logging.handlers
import os
import sys
from twisted.internet import defer
from twisted.python.log import PythonLoggingObserver
__all__ = ["setupLogger", "setLoggerLevel", "getPluginLogger", "LOG"]
from deluge import common
__all__ = ('setup_logger', 'set_logger_level', 'get_plugin_logger', 'LOG')
LoggingLoggerClass = logging.getLoggerClass()
if 'dev' in common.get_version():
DEFAULT_LOGGING_FORMAT = "%%(asctime)s.%%(msecs)03.0f [%%(levelname)-8s][%%(name)-%ds:%%(lineno)-4d] %%(message)s"
DEFAULT_LOGGING_FORMAT = '%%(asctime)s.%%(msecs)03.0f [%%(levelname)-8s][%%(name)-%ds:%%(lineno)-4d] %%(message)s'
else:
DEFAULT_LOGGING_FORMAT = "%%(asctime)s [%%(levelname)-8s][%%(name)-%ds] %%(message)s"
DEFAULT_LOGGING_FORMAT = '%%(asctime)s [%%(levelname)-8s][%%(name)-%ds:%%(lineno)-4d] %%(message)s'
MAX_LOGGER_NAME_LENGTH = 10
class Logging(LoggingLoggerClass):
def __init__(self, logger_name):
LoggingLoggerClass.__init__(self, logger_name)
super(Logging, self).__init__(logger_name)
# This makes module name padding increase to the biggest module name
# so that logs keep readability.
@ -65,7 +45,7 @@ class Logging(LoggingLoggerClass):
for handler in logging.getLogger().handlers:
handler.setFormatter(logging.Formatter(
DEFAULT_LOGGING_FORMAT % MAX_LOGGER_NAME_LENGTH,
datefmt="%H:%M:%S"
datefmt='%H:%M:%S'
))
@defer.inlineCallbacks
@ -102,10 +82,10 @@ class Logging(LoggingLoggerClass):
def exception(self, msg, *args, **kwargs):
yield LoggingLoggerClass.exception(self, msg, *args, **kwargs)
def findCaller(self):
def findCaller(self, stack_info=False): # NOQA: N802
f = logging.currentframe().f_back
rv = "(unknown file)", 0, "(unknown function)"
while hasattr(f, "f_code"):
rv = '(unknown file)', 0, '(unknown function)'
while hasattr(f, 'f_code'):
co = f.f_code
filename = os.path.normcase(co.co_filename)
if filename in (__file__.replace('.pyc', '.py'),
@ -116,29 +96,35 @@ class Logging(LoggingLoggerClass):
break
return rv
levels = {
"none": logging.NOTSET,
"info": logging.INFO,
"warn": logging.WARNING,
"warning": logging.WARNING,
"error": logging.ERROR,
"none": logging.CRITICAL,
"debug": logging.DEBUG,
"trace": 5,
"garbage": 1
'info': logging.INFO,
'warn': logging.WARNING,
'warning': logging.WARNING,
'error': logging.ERROR,
'none': logging.CRITICAL,
'debug': logging.DEBUG,
'trace': 5,
'garbage': 1
}
def setupLogger(level="error", filename=None, filemode="w"):
def setup_logger(level='error', filename=None, filemode='w', logrotate=None,
output_stream=sys.stdout, twisted_observer=True):
"""
Sets up the basic logger and if `:param:filename` is set, then it will log
to that file instead of stdout.
:param level: str, the level to log
:param filename: str, the file to log to
Args:
level (str): The log level to use (Default: 'error')
filename (str, optional): The log filename. Default is None meaning log
to terminal
filemode (str): The filemode to use when opening the log file
logrotate (int, optional): The size of the logfile in bytes when enabling
log rotation (Default is None meaning disabled)
output_stream (file descriptor): File descriptor to log to if not logging to file
twisted_observer (bool): Whether to setup the custom twisted logging observer.
"""
import logging
if logging.getLoggerClass() is not Logging:
logging.setLoggerClass(Logging)
logging.addLevelName(5, 'TRACE')
@ -146,40 +132,64 @@ def setupLogger(level="error", filename=None, filemode="w"):
level = levels.get(level, logging.ERROR)
rootLogger = logging.getLogger()
root_logger = logging.getLogger()
if filename and filemode=='a':
import logging.handlers
if filename and logrotate:
handler = logging.handlers.RotatingFileHandler(
filename, filemode,
maxBytes=50*1024*1024, # 50 Mb
backupCount=3,
encoding='utf-8',
delay=0
)
elif filename and filemode=='w':
import logging.handlers
handler = getattr(
logging.handlers, 'WatchedFileHandler', logging.FileHandler)(
filename, filemode, 'utf-8', delay=0
filename, maxBytes=logrotate,
backupCount=5, encoding='utf-8'
)
elif filename and filemode == 'w':
handler_cls = logging.FileHandler
if not common.windows_check():
handler_cls = getattr(logging.handlers, 'WatchedFileHandler', logging.FileHandler)
handler = handler_cls(filename, mode=filemode, encoding='utf-8')
else:
handler = logging.StreamHandler()
handler = logging.StreamHandler(stream=output_stream)
handler.setLevel(level)
formatter = logging.Formatter(
DEFAULT_LOGGING_FORMAT % MAX_LOGGER_NAME_LENGTH,
datefmt="%H:%M:%S"
datefmt='%H:%M:%S'
)
handler.setFormatter(formatter)
rootLogger.addHandler(handler)
rootLogger.setLevel(level)
twisted_logging = PythonLoggingObserver('twisted')
twisted_logging.start()
logging.getLogger("twisted").setLevel(level)
# Check for existing handler to prevent duplicate logging.
if root_logger.handlers:
for handle in root_logger.handlers:
if not isinstance(handle, type(handler)):
root_logger.addHandler(handler)
else:
root_logger.addHandler(handler)
root_logger.setLevel(level)
if twisted_observer:
twisted_logging = TwistedLoggingObserver()
twisted_logging.start()
class TwistedLoggingObserver(PythonLoggingObserver):
"""
Custom logging class to fix missing exception tracebacks in log output with new
twisted.logger module in twisted version >= 15.2.
Related twisted bug ticket: https://twistedmatrix.com/trac/ticket/7927
"""
def __init__(self):
PythonLoggingObserver.__init__(self, loggerName='twisted')
def emit(self, event_dict):
log = logging.getLogger(__name__)
if 'log_failure' in event_dict:
fmt = '%(log_namespace)s \n%(log_failure)s'
getattr(LoggingLoggerClass, event_dict['log_level'].name)(log, fmt % (event_dict))
else:
PythonLoggingObserver.emit(self, event_dict)
def tweak_logging_levels():
"""This function allows tweaking the logging levels for all or some loggers.
@ -200,25 +210,25 @@ def tweak_logging_levels():
the command line.
"""
from deluge import configmanager
logging_config_file = os.path.join(configmanager.get_config_dir(),
'logging.conf')
logging_config_file = os.path.join(configmanager.get_config_dir(), 'logging.conf')
if not os.path.isfile(logging_config_file):
return
log = logging.getLogger(__name__)
log.warn("logging.conf found! tweaking logging levels from %s",
log.warn('logging.conf found! tweaking logging levels from %s',
logging_config_file)
for line in open(logging_config_file, 'r').readlines():
if line.strip().startswith("#"):
continue
name, level = line.strip().split(':')
if level not in levels:
continue
with open(logging_config_file, 'r') as _file:
for line in _file:
if line.strip().startswith('#'):
continue
name, level = line.strip().split(':')
if level not in levels:
continue
log.warn("Setting logger \"%s\" to logging level \"%s\"", name, level)
setLoggerLevel(level, name)
log.warn('Setting logger "%s" to logging level "%s"', name, level)
set_logger_level(level, name)
def setLoggerLevel(level, logger_name=None):
def set_logger_level(level, logger_name=None):
"""
Sets the logger level.
@ -228,34 +238,34 @@ def setLoggerLevel(level, logger_name=None):
tweak the root logger level.
"""
logging.getLogger(logger_name).setLevel(levels.get(level, "error"))
logging.getLogger(logger_name).setLevel(levels.get(level, 'error'))
def getPluginLogger(logger_name):
def get_plugin_logger(logger_name):
import warnings
stack = inspect.stack()
stack.pop(0) # The logging call from this module
module_stack = stack.pop(0) # The module that called the log function
module_stack = stack.pop(0) # The module that called the log function
caller_module = inspect.getmodule(module_stack[0])
# In some weird cases caller_module might be None, try to continue
caller_module_name = getattr(caller_module, '__name__', '')
warnings.warn_explicit(DEPRECATION_WARNING, DeprecationWarning,
module_stack[1], module_stack[2],
caller_module_name)
caller_module_name)
if 'deluge.plugins.' in logger_name:
return logging.getLogger(logger_name)
return logging.getLogger("deluge.plugin.%s" % logger_name)
return logging.getLogger('deluge.plugin.%s' % logger_name)
DEPRECATION_WARNING = """You seem to be using old style logging on your code, ie:
from deluge.log import LOG as log
or:
from deluge.log import getPluginLogger
from deluge.log import get_plugin_logger
This has been deprecated in favour of an enhanced logging system and both "LOG"
and "getPluginLogger" will be removed on the next major version release of Deluge,
and "get_plugin_logger" will be removed on the next major version release of Deluge,
meaning, code will break, specially plugins.
If you're seeing this message and you're not the developer of the plugin which
triggered this warning, please report to it's author.
@ -270,13 +280,14 @@ The above will result in, regarding the "Label" plugin for example a log message
Triggering code:"""
class __BackwardsCompatibleLOG(object):
class _BackwardsCompatibleLOG(object):
def __getattribute__(self, name):
import warnings
logger_name = 'deluge'
stack = inspect.stack()
stack.pop(0) # The logging call from this module
module_stack = stack.pop(0) # The module that called the log function
stack.pop(0) # The logging call from this module
module_stack = stack.pop(0) # The module that called the log function
caller_module = inspect.getmodule(module_stack[0])
# In some weird cases caller_module might be None, try to continue
caller_module_name = getattr(caller_module, '__name__', '')
@ -297,8 +308,9 @@ class __BackwardsCompatibleLOG(object):
else:
logging.getLogger(logger_name).warning(
"Unable to monkey-patch the calling module's `log` attribute! "
"You should really update and rebuild your plugins..."
'You should really update and rebuild your plugins...'
)
return getattr(logging.getLogger(logger_name), name)
LOG = __BackwardsCompatibleLOG()
LOG = _BackwardsCompatibleLOG()

View File

@ -1,278 +0,0 @@
#
# main.py
#
# Copyright (C) 2007 Andrew Resch <andrewresch@gmail.com>
# Copyright (C) 2010 Pedro Algarvio <pedro@algarvio.me>
#
# 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.
#
#
# The main starting point for the program. This function is called when the
# user runs the command 'deluge'.
"""Main starting point for Deluge. Contains the main() entry point."""
import os
import sys
from optparse import OptionParser
from logging import FileHandler, getLogger
from errno import EEXIST
from deluge.log import setupLogger
import deluge.error
def version_callback(option, opt_str, value, parser):
print os.path.basename(sys.argv[0]) + ": " + deluge.common.get_version()
try:
from deluge._libtorrent import lt
print "libtorrent: %s" % lt.version
except ImportError:
pass
raise SystemExit
def start_ui():
"""Entry point for ui script"""
import deluge.common
deluge.common.setup_translations()
# Setup the argument parser
parser = OptionParser(usage="%prog [options] [actions]")
parser.add_option("-v", "--version", action="callback", callback=version_callback,
help="Show program's version number and exit")
parser.add_option("-u", "--ui", dest="ui",
help="""The UI that you wish to launch. The UI choices are:\n
\t gtk -- A GTK-based graphical user interface (default)\n
\t web -- A web-based interface (http://localhost:8112)\n
\t console -- A console or command-line interface""", action="store", type="str")
parser.add_option("-s", "--set-default-ui", dest="default_ui",
help="Sets the default UI to be run when no UI is specified", action="store", type="str")
parser.add_option("-a", "--args", dest="args",
help="Arguments to pass to UI, -a '--option args'", action="store", type="str")
parser.add_option("-c", "--config", dest="config",
help="Set the config folder location", action="store", type="str")
parser.add_option("-l", "--logfile", dest="logfile",
help="Output to designated logfile instead of stdout", action="store", type="str")
parser.add_option("-L", "--loglevel", dest="loglevel",
help="Set the log level: none, info, warning, error, critical, debug", action="store", type="str")
parser.add_option("-q", "--quiet", dest="quiet",
help="Sets the log level to 'none', this is the same as `-L none`", action="store_true", default=False)
parser.add_option("-r", "--rotate-logs",
help="Rotate logfiles.", action="store_true", default=False)
# Get the options and args from the OptionParser
(options, args) = parser.parse_args(deluge.common.unicode_argv()[1:])
# Setup the logger
if options.quiet:
options.loglevel = "none"
if options.loglevel:
options.loglevel = options.loglevel.lower()
logfile_mode = 'w'
if options.rotate_logs:
logfile_mode = 'a'
setupLogger(level=options.loglevel, filename=options.logfile, filemode=logfile_mode)
log = getLogger(__name__)
if options.config:
if not os.path.exists(options.config):
# Try to create the config folder if it doesn't exist
try:
os.makedirs(options.config)
except Exception, e:
pass
elif not os.path.isdir(options.config):
log.error("Config option needs to be a directory!")
sys.exit(1)
else:
if not os.path.exists(deluge.common.get_default_config_dir()):
os.makedirs(deluge.common.get_default_config_dir())
if options.default_ui:
import deluge.configmanager
if options.config:
deluge.configmanager.set_config_dir(options.config)
config = deluge.configmanager.ConfigManager("ui.conf")
config["default_ui"] = options.default_ui
config.save()
print "The default UI has been changed to", options.default_ui
sys.exit(0)
version = deluge.common.get_version()
log.info("Deluge ui %s", version)
log.debug("options: %s", options)
log.debug("args: %s", args)
log.debug("ui_args: %s", args)
from deluge.ui.ui import UI
log.info("Starting ui..")
UI(options, args, options.args)
def start_daemon():
"""Entry point for daemon script"""
import deluge.common
deluge.common.setup_translations()
if 'dev' not in deluge.common.get_version():
import warnings
warnings.filterwarnings('ignore', category=DeprecationWarning, module='twisted')
# Setup the argument parser
parser = OptionParser(usage="%prog [options] [actions]")
parser.add_option("-v", "--version", action="callback", callback=version_callback,
help="Show program's version number and exit")
parser.add_option("-p", "--port", dest="port",
help="Port daemon will listen on", action="store", type="int")
parser.add_option("-i", "--interface", dest="listen_interface",
help="Interface daemon will listen for bittorrent connections on, \
this should be an IP address", metavar="IFACE",
action="store", type="str")
parser.add_option("-u", "--ui-interface", dest="ui_interface",
help="Interface daemon will listen for UI connections on, this should be\
an IP address", metavar="IFACE", action="store", type="str")
if not (deluge.common.windows_check() or deluge.common.osx_check()):
parser.add_option("-d", "--do-not-daemonize", dest="donot",
help="Do not daemonize", action="store_true", default=False)
parser.add_option("-c", "--config", dest="config",
help="Set the config location", action="store", type="str")
parser.add_option("-P", "--pidfile", dest="pidfile",
help="Use pidfile to store process id", action="store", type="str")
if not deluge.common.windows_check():
parser.add_option("-U", "--user", dest="user",
help="User to switch to. Only use it when starting as root", action="store", type="str")
parser.add_option("-g", "--group", dest="group",
help="Group to switch to. Only use it when starting as root", action="store", type="str")
parser.add_option("-l", "--logfile", dest="logfile",
help="Set the logfile location", action="store", type="str")
parser.add_option("-L", "--loglevel", dest="loglevel",
help="Set the log level: none, info, warning, error, critical, debug", action="store", type="str")
parser.add_option("-q", "--quiet", dest="quiet",
help="Sets the log level to 'none', this is the same as `-L none`", action="store_true", default=False)
parser.add_option("-r", "--rotate-logs",
help="Rotate logfiles.", action="store_true", default=False)
parser.add_option("--profile", dest="profile", action="store_true", default=False,
help="Profiles the daemon")
# Get the options and args from the OptionParser
(options, args) = parser.parse_args()
# Setup the logger
if options.quiet:
options.loglevel = "none"
if options.logfile:
# Try to create the logfile's directory if it doesn't exist
try:
os.makedirs(os.path.abspath(os.path.dirname(options.logfile)))
except OSError, e:
if e.errno != EEXIST:
print "There was an error creating the log directory, exiting... (%s)" % e
sys.exit(1)
logfile_mode = 'w'
if options.rotate_logs:
logfile_mode = 'a'
setupLogger(level=options.loglevel, filename=options.logfile, filemode=logfile_mode)
log = getLogger(__name__)
import deluge.configmanager
if options.config:
if not deluge.configmanager.set_config_dir(options.config):
log.error("There was an error setting the config directory! Exiting...")
sys.exit(1)
# Sets the options.logfile to point to the default location
def open_logfile():
if not options.logfile:
options.logfile = deluge.configmanager.get_config_dir("deluged.log")
file_handler = FileHandler(options.logfile)
log.addHandler(file_handler)
# Writes out a pidfile if necessary
def write_pidfile():
if options.pidfile:
open(options.pidfile, "wb").write("%s\n" % os.getpid())
# If the donot daemonize is set, then we just skip the forking
if not (deluge.common.windows_check() or deluge.common.osx_check() or options.donot):
if os.fork():
# We've forked and this is now the parent process, so die!
os._exit(0)
os.setsid()
# Do second fork
if os.fork():
os._exit(0)
# Write pid file before chuid
write_pidfile()
if not deluge.common.windows_check():
if options.user:
if not options.user.isdigit():
import pwd
options.user = pwd.getpwnam(options.user)[2]
os.setuid(options.user)
if options.group:
if not options.group.isdigit():
import grp
options.group = grp.getgrnam(options.group)[2]
os.setuid(options.group)
open_logfile()
def run_daemon(options, args):
try:
from deluge.core.daemon import Daemon
Daemon(options, args)
except deluge.error.DaemonRunningError, e:
log.error(e)
log.error("You cannot run multiple daemons with the same config directory set.")
log.error("If you believe this is an error, you can force a start by deleting %s.", deluge.configmanager.get_config_dir("deluged.pid"))
sys.exit(1)
except Exception, e:
log.exception(e)
sys.exit(1)
if options.profile:
import cProfile
profiler = cProfile.Profile()
profile_output = deluge.configmanager.get_config_dir("deluged.profile")
# Twisted catches signals to terminate
def save_profile_stats():
profiler.dump_stats(profile_output)
print "Profile stats saved to %s" % profile_output
from twisted.internet import reactor
reactor.addSystemEventTrigger("before", "shutdown", save_profile_stats)
print "Running with profiler..."
profiler.runcall(run_daemon, options, args)
else:
run_daemon(options, args)

View File

@ -1,146 +1,122 @@
#
# maketorrent.py
# -*- coding: utf-8 -*-
#
# 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.
#
# This file is part of Deluge and is licensed under GNU General Public License 3.0, or later, with
# the additional special exception to link portions of this program with the OpenSSL library.
# See LICENSE for more details.
#
import sys
from __future__ import division, unicode_literals
import os
import sys
from hashlib import sha1 as sha
from deluge.common import get_path_size
from deluge.bencode import bencode
from deluge.common import get_path_size, utf8_encode_structure
class InvalidPath(Exception):
"""
Raised when an invalid path is supplied
"""
"""Raised when an invalid path is supplied."""
pass
class InvalidPieceSize(Exception):
"""
Raised when an invalid piece size is set. Piece sizes must be multiples of
16KiB.
"""Raised when an invalid piece size is set.
Note:
Piece sizes must be multiples of 16KiB.
"""
pass
class TorrentMetadata(object):
"""
This class is used to create .torrent files.
"""This class is used to create .torrent files.
** Usage **
Examples:
>>> t = TorrentMetadata()
>>> t.data_path = "/tmp/torrent"
>>> t.comment = "My Test Torrent"
>>> t.trackers = [["http://tracker.openbittorent.com"]]
>>> t.save("/tmp/test.torrent")
>>> t = TorrentMetadata()
>>> t.data_path = '/tmp/torrent'
>>> t.comment = 'My Test Torrent'
>>> t.trackers = [['http://tracker.openbittorent.com']]
>>> t.save('/tmp/test.torrent')
"""
def __init__(self):
self.__data_path = None
self.__piece_size = 0
self.__comment = ""
self.__comment = ''
self.__private = False
self.__trackers = []
self.__webseeds = []
self.__pad_files = False
def save(self, torrent_path, progress=None):
"""
Creates and saves the torrent file to `path`.
"""Creates and saves the torrent file to `path`.
:param torrent_path: where to save the torrent file
:type torrent_path: string
Args:
torrent_path (str): Location to save the torrent file.
progress(func, optional): The function to be called when a piece is hashed. The
provided function should be in the format `func(num_completed, num_pieces)`.
:param progress: a function to be called when a piece is hashed
:type progress: function(num_completed, num_pieces)
:raises InvalidPath: if the data_path has not been set
Raises:
InvalidPath: If the data_path has not been set.
"""
if not self.data_path:
raise InvalidPath("Need to set a data_path!")
raise InvalidPath('Need to set a data_path!')
torrent = {
"info": {}
}
'info': {}
}
if self.comment:
torrent["comment"] = self.comment.encode("UTF-8")
torrent['comment'] = self.comment
if self.private:
torrent["info"]["private"] = True
torrent['info']['private'] = True
if self.trackers:
torrent["announce"] = self.trackers[0][0]
torrent["announce-list"] = self.trackers
torrent['announce'] = self.trackers[0][0]
torrent['announce-list'] = self.trackers
else:
torrent["announce"] = ""
torrent['announce'] = ''
if self.webseeds:
httpseeds = []
webseeds = []
for w in self.webseeds:
if w.endswith(".php"):
if w.endswith('.php'):
httpseeds.append(w)
else:
webseeds.append(w)
if httpseeds:
torrent["httpseeds"] = httpseeds
torrent['httpseeds'] = httpseeds
if webseeds:
torrent["url-list"] = webseeds
torrent['url-list'] = webseeds
datasize = get_path_size(self.data_path)
if self.piece_size:
piece_size = piece_size * 1024
piece_size = self.piece_size * 1024
else:
# We need to calculate a piece size
piece_size = 16384
while (datasize / piece_size) > 1024 and piece_size < (8192 * 1024):
while (datasize // piece_size) > 1024 and piece_size < (8192 * 1024):
piece_size *= 2
# Calculate the number of pieces we will require for the data
num_pieces = datasize / piece_size
num_pieces = datasize // piece_size
if datasize % piece_size:
num_pieces += 1
torrent["info"]["piece length"] = piece_size
torrent['info']['piece length'] = piece_size
torrent['info']['name'] = os.path.split(self.data_path)[1]
# Create the info
if os.path.isdir(self.data_path):
torrent["info"]["name"] = os.path.split(self.data_path)[1]
files = []
padding_count = 0
# Collect a list of file paths and add padding files if necessary
@ -148,8 +124,8 @@ class TorrentMetadata(object):
for index, filename in enumerate(filenames):
size = get_path_size(os.path.join(self.data_path, dirpath, filename))
p = dirpath[len(self.data_path):]
p = p.lstrip("/")
p = p.split("/")
p = p.lstrip('/')
p = p.split('/')
if p[0]:
p += [filename]
else:
@ -160,7 +136,7 @@ class TorrentMetadata(object):
left = size % piece_size
if left:
p = list(p)
p[-1] = "_____padding_file_" + str(padding_count)
p[-1] = '_____padding_file_' + str(padding_count)
files.append((piece_size - left, p))
padding_count += 1
@ -171,179 +147,222 @@ class TorrentMetadata(object):
fs = []
pieces = []
# Create the piece hashes
buf = ""
buf = ''
for size, path in files:
path = [s.decode(sys.getfilesystemencoding()).encode("UTF-8") for s in path]
fs.append({"length": size, "path": path})
if path[-1].startswith("_____padding_file_"):
buf += "\0" * size
path = [s.decode(sys.getfilesystemencoding()).encode('UTF-8') for s in path]
fs.append({'length': size, 'path': path})
if path[-1].startswith('_____padding_file_'):
buf += '\0' * size
pieces.append(sha(buf).digest())
buf = ""
fs[-1]["attr"] = "p"
buf = ''
fs[-1]['attr'] = 'p'
else:
fd = open(os.path.join(self.data_path, *path), "rb")
r = fd.read(piece_size - len(buf))
while r:
buf += r
if len(buf) == piece_size:
pieces.append(sha(buf).digest())
# Run the progress function if necessary
if progress:
progress(len(pieces), num_pieces)
buf = ""
else:
break
r = fd.read(piece_size - len(buf))
fd.close()
with open(os.path.join(self.data_path, *path), 'rb') as _file:
r = _file.read(piece_size - len(buf))
while r:
buf += r
if len(buf) == piece_size:
pieces.append(sha(buf).digest())
# Run the progress function if necessary
if progress:
progress(len(pieces), num_pieces)
buf = ''
else:
break
r = _file.read(piece_size - len(buf))
torrent['info']['files'] = fs
if buf:
pieces.append(sha(buf).digest())
if progress:
progress(len(pieces), num_pieces)
buf = ""
torrent["info"]["pieces"] = "".join(pieces)
torrent["info"]["files"] = fs
buf = ''
elif os.path.isfile(self.data_path):
torrent["info"]["name"] = os.path.split(self.data_path)[1]
torrent["info"]["length"] = get_path_size(self.data_path)
torrent['info']['length'] = get_path_size(self.data_path)
pieces = []
fd = open(self.data_path, "rb")
r = fd.read(piece_size)
while r:
pieces.append(sha(r).digest())
if progress:
progress(len(pieces), num_pieces)
with open(self.data_path, 'rb') as _file:
r = _file.read(piece_size)
while r:
pieces.append(sha(r).digest())
if progress:
progress(len(pieces), num_pieces)
r = fd.read(piece_size)
r = _file.read(piece_size)
torrent["info"]["pieces"] = "".join(pieces)
torrent['info']['pieces'] = b''.join(pieces)
# Write out the torrent file
open(torrent_path, "wb").write(bencode(torrent))
with open(torrent_path, 'wb') as _file:
_file.write(bencode(utf8_encode_structure(torrent)))
def get_data_path(self):
"""
The path to the files that the torrent will contain. It can be either
a file or a folder. This property needs to be set before the torrent
file can be created and saved.
"""Get the path to the files that the torrent will contain.
Note:
It can be either a file or a folder.
Returns:
str: The torrent data path, either a file or a folder.
"""
return self.__data_path
def set_data_path(self, path):
"""
:param path: the path to the data
:type path: string
"""Set the path to the files (data) that the torrent will contain.
:raises InvalidPath: if the path is not found
Note:
This property needs to be set before the torrent file can be created and saved.
Args:
path (str): The path to the torrent data and can be either a file or a folder.
Raises:
InvalidPath: If the path is not found.
"""
if os.path.exists(path) and (os.path.isdir(path) or os.path.isfile(path)):
self.__data_path = os.path.abspath(path)
else:
raise InvalidPath("No such file or directory: %s" % path)
raise InvalidPath('No such file or directory: %s' % path)
def get_piece_size(self):
"""
The size of pieces in bytes. The size must be a multiple of 16KiB.
If you don't set a piece size, one will be automatically selected to
produce a torrent with less than 1024 pieces or the smallest possible
with a 8192KiB piece size.
"""The size of the pieces.
Returns:
int: The piece size in multiples of 16 KiBs.
"""
return self.__piece_size
def set_piece_size(self, size):
"""
:param size: the desired piece size in KiBs
:type size: int
"""Set piece size.
:raises InvalidPieceSize: if the piece size is not a multiple of 16 KiB
Note:
If no piece size is set, one will be automatically selected to
produce a torrent with less than 1024 pieces or the smallest possible
with a 8192KiB piece size.
Args:
size (int): The desired piece size in multiples of 16 KiBs.
Raises:
InvalidPieceSize: If the piece size is not a valid multiple of 16 KiB.
"""
if size % 16 and size:
raise InvalidPieceSize("Piece size must be a multiple of 16 KiB")
raise InvalidPieceSize('Piece size must be a multiple of 16 KiB')
self.__piece_size = size
def get_comment(self):
"""
Comment is some extra info to be stored in the torrent. This is
typically an informational string.
"""Get the torrent comment.
Returns:
str: An informational string about the torrent.
"""
return self.__comment
def set_comment(self, comment):
"""
:param comment: an informational string
:type comment: string
"""Set the comment for the torrent.
Args:
comment (str): An informational string about the torrent.
"""
self.__comment = comment
def get_private(self):
"""
Private torrents only announce to the tracker and will not use DHT or
Peer Exchange.
"""Get the private flag of the torrent.
See: http://bittorrent.org/beps/bep_0027.html
Returns:
bool: True if private flag has been set, else False.
"""
return self.__private
def set_private(self, private):
"""
:param private: True if the torrent is to be private
:type private: bool
"""Set the torrent private flag.
Note:
Private torrents only announce to trackers and will not use DHT or
Peer Exchange. See http://bittorrent.org/beps/bep_0027.html
Args:
private (bool): True if the torrent is to be private.
"""
self.__private = private
def get_trackers(self):
"""
The announce trackers is a list of lists.
"""Get the announce trackers.
See: http://bittorrent.org/beps/bep_0012.html
Note:
See http://bittorrent.org/beps/bep_0012.html
Returns:
list of lists: A list containing a list of trackers.
"""
return self.__trackers
def set_trackers(self, trackers):
"""
:param trackers: a list of lists of trackers, each list is a tier
:type trackers: list of list of strings
"""Set the announce trackers.
Args:
private (list of lists): A list containing lists of trackers as strings, each list is a tier.
"""
self.__trackers = trackers
def get_webseeds(self):
"""
The web seeds can either be:
Hoffman-style: http://bittorrent.org/beps/bep_0017.html
or,
GetRight-style: http://bittorrent.org/beps/bep_0019.html
"""Get the webseeds.
Note:
The web seeds can either be:
Hoffman-style: http://bittorrent.org/beps/bep_0017.html
GetRight-style: http://bittorrent.org/beps/bep_0019.html
If the url ends in '.php' then it will be considered Hoffman-style, if
not it will be considered GetRight-style.
Returns:
list: The webseeds.
If the url ends in '.php' then it will be considered Hoffman-style, if
not it will be considered GetRight-style.
"""
return self.__webseeds
def set_webseeds(self, webseeds):
"""
:param webseeds: the webseeds which can be either Hoffman or GetRight style
:type webseeds: list of urls
"""Set webseeds.
Note:
The web seeds can either be:
Hoffman-style: http://bittorrent.org/beps/bep_0017.html
GetRight-style: http://bittorrent.org/beps/bep_0019.html
If the url ends in '.php' then it will be considered Hoffman-style, if
not it will be considered GetRight-style.
Args:
private (list): The webseeds URLs which can be either Hoffman or GetRight style.
"""
self.__webseeds = webseeds
def get_pad_files(self):
"""
If this is True, padding files will be added to align files on piece
boundaries.
"""Get status of padding files for the torrent.
Returns:
bool: True if padding files have been enabled to align files on piece boundaries.
"""
return self.__pad_files
def set_pad_files(self, pad):
"""
:param pad: set True to align files on piece boundaries
:type pad: bool
"""Enable padding files for the torrent.
Args:
private (bool): True adds padding files to align files on piece boundaries.
"""
self.__pad_files = pad

View File

@ -1,29 +1,26 @@
# Taken from http://download.bittorrent.com/dl/BitTorrent-5.3-GPL.tar.gz
# -*- coding: utf-8 -*-
#
# 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 of the License, or
# (at your option) any later version.
# Original file from BitTorrent-5.3-GPL.tar.gz
# Copyright (C) Bram Cohen
#
# 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.
# Modifications for use in Deluge:
# Copyright (C) 2007 Andrew Resch <andrewresch@gmail.com>
#
# This file is part of Deluge and is licensed under GNU General Public License 3.0, or later, with
# the additional special exception to link portions of this program with the OpenSSL library.
# See LICENSE for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
# Written by Bram Cohen
# Modifications for use in Deluge by Andrew Resch 2008
from __future__ import division, unicode_literals
import os.path
import sys
import time
import logging
import os.path
import time
from hashlib import sha1 as sha
import deluge.component as component
from deluge.bencode import bencode
from deluge.common import utf8_encode_structure
from deluge.event import CreateTorrentProgressEvent
log = logging.getLogger(__name__)
@ -31,42 +28,32 @@ log = logging.getLogger(__name__)
ignore = ['core', 'CVS', 'Thumbs.db', 'desktop.ini']
noncharacter_translate = {}
for i in xrange(0xD800, 0xE000):
for i in range(0xD800, 0xE000):
noncharacter_translate[i] = ord('-')
for i in xrange(0xFDD0, 0xFDF0):
for i in range(0xFDD0, 0xFDF0):
noncharacter_translate[i] = ord('-')
for i in (0xFFFE, 0xFFFF):
noncharacter_translate[i] = ord('-')
def gmtime():
return time.mktime(time.gmtime())
def get_filesystem_encoding():
return sys.getfilesystemencoding()
def decode_from_filesystem(path):
encoding = get_filesystem_encoding()
if encoding == None:
assert isinstance(path, unicode), "Path should be unicode not %s" % type(path)
decoded_path = path
else:
assert isinstance(path, str), "Path should be str not %s" % type(path)
decoded_path = path.decode(encoding)
return decoded_path
def dummy(*v):
pass
class RemoteFileProgress(object):
def __init__(self, session_id):
self.session_id = session_id
def __call__(self, piece_count, num_pieces):
component.get("RPCServer").emit_event_for_session_id(
component.get('RPCServer').emit_event_for_session_id(
self.session_id, CreateTorrentProgressEvent(piece_count, num_pieces)
)
def make_meta_file(path, url, piece_length, progress=None, title=None, comment=None,
safe=None, content_type=None, target=None, webseeds=None, name=None,
private=False, created_by=None, trackers=None):
@ -83,31 +70,34 @@ def make_meta_file(path, url, piece_length, progress=None, title=None, comment=N
f = target
if progress is None:
session_id = component.get("RPCServer").get_session_id()
if not session_id:
progress = dummy
progress = dummy
try:
session_id = component.get('RPCServer').get_session_id()
except KeyError:
pass
else:
progress = RemoteFileProgress(component.get("RPCServer").get_session_id())
if session_id:
progress = RemoteFileProgress(session_id)
info = makeinfo(path, piece_length, progress, name, content_type, private)
#check_info(info)
h = file(f, 'wb')
# check_info(info)
h = open(f, 'wb')
data['info'] = info
if title:
data['title'] = title.encode("utf8")
data['title'] = title.encode('utf8')
if comment:
data['comment'] = comment.encode("utf8")
data['comment'] = comment.encode('utf8')
if safe:
data['safe'] = safe.encode("utf8")
data['safe'] = safe.encode('utf8')
httpseeds = []
url_list = []
if webseeds:
for webseed in webseeds:
if webseed.endswith(".php"):
if webseed.endswith('.php'):
httpseeds.append(webseed)
else:
url_list.append(webseed)
@ -117,47 +107,30 @@ def make_meta_file(path, url, piece_length, progress=None, title=None, comment=N
if httpseeds:
data['httpseeds'] = httpseeds
if created_by:
data['created by'] = created_by.encode("utf8")
data['created by'] = created_by.encode('utf8')
if trackers and (len(trackers[0]) > 1 or len(trackers) > 1):
data['announce-list'] = trackers
data["encoding"] = "UTF-8"
data['encoding'] = 'UTF-8'
h.write(bencode(data))
h.write(bencode(utf8_encode_structure(data)))
h.close()
def calcsize(path):
total = 0
for s in subfiles(os.path.abspath(path)):
total += os.path.getsize(s[1])
return total
def makeinfo(path, piece_length, progress, name = None,
content_type = None, private=False): # HEREDAVE. If path is directory,
# how do we assign content type?
def to_utf8(name):
if isinstance(name, unicode):
u = name
else:
try:
u = decode_from_filesystem(name)
except Exception:
raise Exception('Could not convert file/directory name %r to '
'Unicode. Either the assumed filesystem '
'encoding "%s" is wrong or the filename contains '
'illegal bytes.' % (name, get_filesystem_encoding()))
if u.translate(noncharacter_translate) != u:
raise Exception('File/directory name "%s" contains reserved '
'unicode values that do not correspond to '
'characters.' % name)
return u.encode('utf-8')
def makeinfo(path, piece_length, progress, name=None, content_type=None, private=False):
# HEREDAVE. If path is directory, how do we assign content type?
path = os.path.abspath(path)
piece_count = 0
if os.path.isdir(path):
subs = subfiles(path)
subs.sort()
subs = sorted(subfiles(path))
pieces = []
sh = sha()
done = 0
@ -168,20 +141,20 @@ def makeinfo(path, piece_length, progress, name = None,
totalsize += os.path.getsize(f)
if totalsize >= piece_length:
import math
num_pieces = math.ceil(float(totalsize) / float(piece_length))
num_pieces = math.ceil(totalsize / piece_length)
else:
num_pieces = 1
for p, f in subs:
pos = 0
size = os.path.getsize(f)
p2 = [to_utf8(n) for n in p]
p2 = [n.encode('utf8') for n in p]
if content_type:
fs.append({'length': size, 'path': p2,
'content_type' : content_type}) # HEREDAVE. bad for batch!
'content_type': content_type}) # HEREDAVE. bad for batch!
else:
fs.append({'length': size, 'path': p2})
h = file(f, 'rb')
h = open(f, 'rb')
while pos < size:
a = min(size - pos, piece_length - done)
sh.update(h.read(a))
@ -201,26 +174,24 @@ def makeinfo(path, piece_length, progress, name = None,
piece_count += 1
progress(piece_count, num_pieces)
if name is not None:
assert isinstance(name, unicode)
name = to_utf8(name)
else:
name = to_utf8(os.path.split(path)[1])
if not name:
name = os.path.split(path)[1]
return {'pieces': ''.join(pieces),
'piece length': piece_length, 'files': fs,
'name': name,
'private': private}
return {'pieces': b''.join(pieces),
'piece length': piece_length,
'files': fs,
'name': name.encode('utf8'),
'private': private}
else:
size = os.path.getsize(path)
if size >= piece_length:
num_pieces = size / piece_length
num_pieces = size // piece_length
else:
num_pieces = 1
pieces = []
p = 0
h = file(path, 'rb')
h = open(path, 'rb')
while p < size:
x = h.read(min(piece_length, size - p))
pieces.append(sha(x).digest())
@ -230,16 +201,18 @@ def makeinfo(path, piece_length, progress, name = None,
p = size
progress(piece_count, num_pieces)
h.close()
name = os.path.split(path)[1].encode('utf8')
if content_type is not None:
return {'pieces': ''.join(pieces),
return {'pieces': b''.join(pieces),
'piece length': piece_length, 'length': size,
'name': name,
'content_type': content_type,
'private': private}
return {'pieces': b''.join(pieces),
'piece length': piece_length, 'length': size,
'name': to_utf8(os.path.split(path)[1]),
'content_type' : content_type,
'private': private }
return {'pieces': ''.join(pieces),
'piece length': piece_length, 'length': size,
'name': to_utf8(os.path.split(path)[1]),
'private': private}
'name': name,
'private': private}
def subfiles(d):
r = []

View File

@ -0,0 +1,84 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
#
# Copyright (C) 2013 Bro <bro.development@gmail.com>
#
# This file is part of Deluge and is licensed under GNU General Public License 3.0, or later, with
# the additional special exception to link portions of this program with the OpenSSL library.
# See LICENSE for more details.
#
from __future__ import unicode_literals
import os
def is_hidden(filepath):
def has_hidden_attribute(filepath):
import win32api
import win32con
try:
attribute = win32api.GetFileAttributes(filepath)
return attribute & (win32con.FILE_ATTRIBUTE_HIDDEN | win32con.FILE_ATTRIBUTE_SYSTEM)
except (AttributeError, AssertionError):
return False
name = os.path.basename(os.path.abspath(filepath))
# Windows
if os.name == 'nt':
return has_hidden_attribute(filepath)
return name.startswith('.')
def get_completion_paths(args):
"""
Takes a path value and returns the available completions.
If the path_value is a valid path, return all sub-directories.
If the path_value is not a valid path, remove the basename from the
path and return all sub-directories of path that start with basename.
:param args: options
:type args: dict
:returns: the args argument containing the available completions for the completion_text
:rtype: list
"""
args['paths'] = []
path_value = args['completion_text']
hidden_files = args['show_hidden_files']
def get_subdirs(dirname):
try:
return os.walk(dirname).next()[1]
except StopIteration:
# Invalid dirname
return []
dirname = os.path.dirname(path_value)
basename = os.path.basename(path_value)
dirs = get_subdirs(dirname)
# No completions available
if not dirs:
return args
# path_value ends with path separator so
# we only want all the subdirectories
if not basename:
# Lets remove hidden files
if not hidden_files:
old_dirs = dirs
dirs = []
for d in old_dirs:
if not is_hidden(os.path.join(dirname, d)):
dirs.append(d)
matching_dirs = []
for s in dirs:
if s.startswith(basename):
p = os.path.join(dirname, s)
if not p.endswith(os.path.sep):
p += os.path.sep
matching_dirs.append(p)
args['paths'] = sorted(matching_dirs)
return args

View File

@ -1,62 +1,39 @@
#
# pluginmanagerbase.py
# -*- coding: utf-8 -*-
#
# Copyright (C) 2007 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.
#
# This file is part of Deluge and is licensed under GNU General Public License 3.0, or later, with
# the additional special exception to link portions of this program with the OpenSSL library.
# See LICENSE for more details.
#
"""PluginManagerBase"""
from __future__ import unicode_literals
import os.path
import logging
import os.path
import pkg_resources
from twisted.internet import defer
from twisted.python.failure import Failure
import deluge.common
import deluge.configmanager
import deluge.component as component
import deluge.configmanager
log = logging.getLogger(__name__)
METADATA_KEYS = [
"Name",
"License",
"Author",
"Home-page",
"Summary",
"Platform",
"Version",
"Author-email",
"Description",
'Name',
'License',
'Author',
'Home-page',
'Summary',
'Platform',
'Version',
'Author-email',
'Description',
]
DEPRECATION_WARNING = """
@ -69,17 +46,18 @@ If you're the developer, please take a look at the plugins hosted on deluge's
git repository to have an idea of what needs to be changed.
"""
class PluginManagerBase:
class PluginManagerBase(object):
"""PluginManagerBase is a base class for PluginManagers to inherit"""
def __init__(self, config_file, entry_name):
log.debug("Plugin manager init..")
log.debug('Plugin manager init..')
self.config = deluge.configmanager.ConfigManager(config_file)
# Create the plugins folder if it doesn't exist
if not os.path.exists(os.path.join(deluge.configmanager.get_config_dir(), "plugins")):
os.mkdir(os.path.join(deluge.configmanager.get_config_dir(), "plugins"))
if not os.path.exists(os.path.join(deluge.configmanager.get_config_dir(), 'plugins')):
os.mkdir(os.path.join(deluge.configmanager.get_config_dir(), 'plugins'))
# This is the entry we want to load..
self.entry_name = entry_name
@ -92,12 +70,13 @@ class PluginManagerBase:
def enable_plugins(self):
# Load plugins that are enabled in the config.
for name in self.config["enabled_plugins"]:
for name in self.config['enabled_plugins']:
self.enable_plugin(name)
def disable_plugins(self):
# Disable all plugins that are enabled
for key in self.plugins.keys():
"""Disable all plugins that are enabled"""
# Dict will be modified so iterate over generated list
for key in list(self.plugins):
self.disable_plugin(key)
def __getitem__(self, key):
@ -109,13 +88,13 @@ class PluginManagerBase:
def get_enabled_plugins(self):
"""Returns a list of enabled plugins"""
return self.plugins.keys()
return list(self.plugins)
def scan_for_plugins(self):
"""Scans for available plugins"""
base_plugin_dir = deluge.common.resource_filename("deluge", "plugins")
base_plugin_dir = deluge.common.resource_filename('deluge', 'plugins')
pkg_resources.working_set.add_entry(base_plugin_dir)
user_plugin_dir = os.path.join(deluge.configmanager.get_config_dir(), "plugins")
user_plugin_dir = os.path.join(deluge.configmanager.get_config_dir(), 'plugins')
plugins_dirs = [base_plugin_dir]
for dirname in os.listdir(base_plugin_dir):
@ -129,82 +108,152 @@ class PluginManagerBase:
self.available_plugins = []
for name in self.pkg_env:
log.debug("Found plugin: %s %s at %s",
self.pkg_env[name][0].project_name,
self.pkg_env[name][0].version,
self.pkg_env[name][0].location)
log.debug('Found plugin: %s %s at %s',
self.pkg_env[name][0].project_name,
self.pkg_env[name][0].version,
self.pkg_env[name][0].location)
self.available_plugins.append(self.pkg_env[name][0].project_name)
def enable_plugin(self, plugin_name):
"""Enables a plugin"""
"""Enable a plugin.
Args:
plugin_name (str): The plugin name.
Returns:
Deferred: A deferred with callback value True or False indicating
whether the plugin is enabled or not.
"""
if plugin_name not in self.available_plugins:
log.warning("Cannot enable non-existant plugin %s", plugin_name)
return
log.warning('Cannot enable non-existant plugin %s', plugin_name)
return defer.succeed(False)
if plugin_name in self.plugins:
log.warning("Cannot enable already enabled plugin %s", plugin_name)
return
log.warning('Cannot enable already enabled plugin %s', plugin_name)
return defer.succeed(True)
plugin_name = plugin_name.replace(" ", "-")
plugin_name = plugin_name.replace(' ', '-')
egg = self.pkg_env[plugin_name][0]
egg.activate()
return_d = defer.succeed(True)
for name in egg.get_entry_map(self.entry_name):
entry_point = egg.get_entry_info(self.entry_name, name)
try:
cls = entry_point.load()
instance = cls(plugin_name.replace("-", "_"))
except Exception, e:
log.error("Unable to instantiate plugin %r from %r!",
name, egg.location)
log.exception(e)
instance = cls(plugin_name.replace('-', '_'))
except component.ComponentAlreadyRegistered as ex:
log.error(ex)
return defer.succeed(False)
except Exception as ex:
log.error('Unable to instantiate plugin %r from %r!', name, egg.location)
log.exception(ex)
continue
instance.enable()
if not instance.__module__.startswith("deluge.plugins."):
try:
return_d = defer.maybeDeferred(instance.enable)
except Exception as ex:
log.error('Unable to enable plugin: %s', name)
log.exception(ex)
return_d = defer.fail(False)
if not instance.__module__.startswith('deluge.plugins.'):
import warnings
warnings.warn_explicit(
DEPRECATION_WARNING % name,
DeprecationWarning,
instance.__module__, 0
)
if self._component_state == "Started":
component.start([instance.plugin._component_name])
plugin_name = plugin_name.replace("-", " ")
self.plugins[plugin_name] = instance
if plugin_name not in self.config["enabled_plugins"]:
log.debug("Adding %s to enabled_plugins list in config",
plugin_name)
self.config["enabled_plugins"].append(plugin_name)
log.info("Plugin %s enabled..", plugin_name)
if self._component_state == 'Started':
def on_enabled(result, instance):
return component.start([instance.plugin._component_name])
return_d.addCallback(on_enabled, instance)
def on_started(result, instance):
plugin_name_space = plugin_name.replace('-', ' ')
self.plugins[plugin_name_space] = instance
if plugin_name_space not in self.config['enabled_plugins']:
log.debug('Adding %s to enabled_plugins list in config', plugin_name_space)
self.config['enabled_plugins'].append(plugin_name_space)
log.info('Plugin %s enabled...', plugin_name_space)
return True
def on_started_error(result, instance):
log.error('Failed to start plugin: %s\n%s', plugin_name,
result.getTraceback(elideFrameworkCode=1, detail='brief'))
self.plugins[plugin_name.replace('-', ' ')] = instance
self.disable_plugin(plugin_name)
return False
return_d.addCallbacks(on_started, on_started_error, callbackArgs=[instance], errbackArgs=[instance])
return return_d
return defer.succeed(False)
def disable_plugin(self, name):
"""Disables a plugin"""
try:
self.plugins[name].disable()
component.deregister(self.plugins[name].plugin)
del self.plugins[name]
self.config["enabled_plugins"].remove(name)
except KeyError:
log.warning("Plugin %s is not enabled..", name)
"""Disable a plugin.
log.info("Plugin %s disabled..", name)
Args:
plugin_name (str): The plugin name.
Returns:
Deferred: A deferred with callback value True or False indicating
whether the plugin is disabled or not.
"""
if name not in self.plugins:
log.warning('Plugin "%s" is not enabled...', name)
return defer.succeed(True)
try:
d = defer.maybeDeferred(self.plugins[name].disable)
except Exception as ex:
log.error('Error when disabling plugin: %s', self.plugin._component_name)
log.debug(ex)
d = defer.succeed(False)
def on_disabled(result):
ret = True
if isinstance(result, Failure):
log.debug('Error when disabling plugin %s: %s', name, result.getTraceback())
ret = False
try:
component.deregister(self.plugins[name].plugin)
del self.plugins[name]
self.config['enabled_plugins'].remove(name)
except Exception as ex:
log.warning('Problems occured disabling plugin: %s', name)
log.debug(ex)
ret = False
else:
log.info('Plugin %s disabled...', name)
return ret
d.addBoth(on_disabled)
return d
def get_plugin_info(self, name):
"""Returns a dictionary of plugin info from the metadata"""
info = {}.fromkeys(METADATA_KEYS)
last_header = ""
last_header = ''
cont_lines = []
for line in self.pkg_env[name][0].get_metadata("PKG-INFO").splitlines():
# Missing plugin info
if not self.pkg_env[name]:
log.warn('Failed to retrive info for plugin: %s', name)
for k in info:
info[k] = 'not available'
return info
for line in self.pkg_env[name][0].get_metadata('PKG-INFO').splitlines():
if not line:
continue
if line[0] in ' \t' and (len(line.split(":", 1)) == 1 or line.split(":", 1)[0] not in info.keys()):
if line[0] in ' \t' and (len(line.split(':', 1)) == 1 or line.split(':', 1)[0] not in info):
# This is a continuation
cont_lines.append(line.strip())
else:
if cont_lines:
info[last_header] = "\n".join(cont_lines).strip()
info[last_header] = '\n'.join(cont_lines).strip()
cont_lines = []
if line.split(":", 1)[0] in info.keys():
last_header = line.split(":", 1)[0]
info[last_header] = line.split(":", 1)[1].strip()
if line.split(':', 1)[0] in info:
last_header = line.split(':', 1)[0]
info[last_header] = line.split(':', 1)[1].strip()
return info

View File

@ -1,3 +1,6 @@
from __future__ import unicode_literals
# this is a namespace package
import pkg_resources
pkg_resources.declare_namespace(__name__)

View File

@ -1,3 +1,6 @@
from __future__ import unicode_literals
# this is a namespace package
import pkg_resources
pkg_resources.declare_namespace(__name__)

View File

@ -1,5 +1,4 @@
#
# __init__.py
# -*- coding: utf-8 -*-
#
# Copyright (C) 2009 GazpachoKing <chase.sterling@gmail.com>
#
@ -8,51 +7,32 @@
# 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.
# This file is part of Deluge and is licensed under GNU General Public License 3.0, or later, with
# the additional special exception to link portions of this program with the OpenSSL library.
# See LICENSE for more details.
#
from __future__ import unicode_literals
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
from .core import Core as _pluginCls
self._plugin_cls = _pluginCls
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
from .gtkui import GtkUI as _pluginCls
self._plugin_cls = _pluginCls
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
from .webui import WebUI as _pluginCls
self._plugin_cls = _pluginCls
super(WebUIPlugin, self).__init__(plugin_name)

View File

@ -1,43 +1,23 @@
#
# common.py
#
# Copyright (C) 2009 GazpachoKing <chase.sterling@gmail.com>
# -*- coding: utf-8 -*-
#
# 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>
# 2007-2009 Andrew Resch <andrewresch@gmail.com>
# 2009 Damien Churchill <damoxc@gmail.com>
# 2010 Pedro Algarvio <pedro@algarvio.me>
# 2017 Calum Lind <calumlind+deluge@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.
# This file is part of Deluge and is licensed under GNU General Public License 3.0, or later, with
# the additional special exception to link portions of this program with the OpenSSL library.
# See LICENSE for more details.
#
from __future__ import unicode_literals
import os.path
from pkg_resources import resource_filename
def get_resource(filename):
import pkg_resources, os
return pkg_resources.resource_filename("deluge.plugins.autoadd",
os.path.join("data", filename))
return resource_filename('deluge.plugins.autoadd', os.path.join('data', filename))

View File

@ -1,5 +1,4 @@
#
# core.py
# -*- coding: utf-8 -*-
#
# Copyright (C) 2009 GazpachoKing <chase.sterling@gmail.com>
# Copyright (C) 2011 Pedro Algarvio <pedro@algarvio.me>
@ -9,101 +8,90 @@
# 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.
# This file is part of Deluge and is licensed under GNU General Public License 3.0, or later, with
# the additional special exception to link portions of this program with the OpenSSL library.
# See LICENSE for more details.
#
from deluge._libtorrent import lt
import os
from __future__ import unicode_literals
import base64
import logging
from deluge.plugins.pluginbase import CorePluginBase
import os
import shutil
from twisted.internet import reactor
from twisted.internet.task import LoopingCall, deferLater
import deluge.component as component
import deluge.configmanager
from deluge.common import AUTH_LEVEL_ADMIN
from deluge._libtorrent import lt
from deluge.common import AUTH_LEVEL_ADMIN, is_magnet
from deluge.core.rpcserver import export
from twisted.internet.task import LoopingCall, deferLater
from twisted.internet import reactor
from deluge.error import AddTorrentError
from deluge.event import DelugeEvent
from deluge.plugins.pluginbase import CorePluginBase
log = logging.getLogger(__name__)
DEFAULT_PREFS = {
"watchdirs":{},
"next_id":1
'watchdirs': {},
'next_id': 1
}
OPTIONS_AVAILABLE = { #option: builtin
"enabled":False,
"path":False,
"append_extension":False,
"copy_torrent": False,
"delete_copy_torrent_toggle": False,
"abspath":False,
"download_location":True,
"max_download_speed":True,
"max_upload_speed":True,
"max_connections":True,
"max_upload_slots":True,
"prioritize_first_last":True,
"auto_managed":True,
"stop_at_ratio":True,
"stop_ratio":True,
"remove_at_ratio":True,
"move_completed":True,
"move_completed_path":True,
"label":False,
"add_paused":True,
"queue_to_top":False,
"owner": "localclient"
OPTIONS_AVAILABLE = { # option: builtin
'enabled': False,
'path': False,
'append_extension': False,
'copy_torrent': False,
'delete_copy_torrent_toggle': False,
'abspath': False,
'download_location': True,
'max_download_speed': True,
'max_upload_speed': True,
'max_connections': True,
'max_upload_slots': True,
'prioritize_first_last': True,
'auto_managed': True,
'stop_at_ratio': True,
'stop_ratio': True,
'remove_at_ratio': True,
'move_completed': True,
'move_completed_path': True,
'label': False,
'add_paused': True,
'queue_to_top': False,
'owner': True,
'seed_mode': True
}
MAX_NUM_ATTEMPTS = 10
class AutoaddOptionsChangedEvent(DelugeEvent):
"""Emitted when the options for the plugin are changed."""
def __init__(self):
pass
def CheckInput(cond, message):
def check_input(cond, message):
if not cond:
raise Exception(message)
class Core(CorePluginBase):
def enable(self):
#reduce typing, assigning some values to self...
self.config = deluge.configmanager.ConfigManager("autoadd.conf", DEFAULT_PREFS)
# reduce typing, assigning some values to self...
self.config = deluge.configmanager.ConfigManager('autoadd.conf', DEFAULT_PREFS)
self.config.run_converter((0, 1), 2, self.__migrate_config_1_to_2)
self.config.save()
self.watchdirs = self.config["watchdirs"]
self.watchdirs = self.config['watchdirs']
component.get("EventManager").register_event_handler(
"PreTorrentRemovedEvent", self.__on_pre_torrent_removed
component.get('EventManager').register_event_handler(
'PreTorrentRemovedEvent', self.__on_pre_torrent_removed
)
# Dict of Filename:Attempts
@ -114,16 +102,16 @@ class Core(CorePluginBase):
def enable_looping(self):
# Enable all looping calls for enabled watchdirs here
for watchdir_id, watchdir in self.watchdirs.iteritems():
for watchdir_id, watchdir in self.watchdirs.items():
if watchdir['enabled']:
self.enable_watchdir(watchdir_id)
def disable(self):
#disable all running looping calls
component.get("EventManager").deregister_event_handler(
"PreTorrentRemovedEvent", self.__on_pre_torrent_removed
# disable all running looping calls
component.get('EventManager').deregister_event_handler(
'PreTorrentRemovedEvent', self.__on_pre_torrent_removed
)
for loopingcall in self.update_timers.itervalues():
for loopingcall in self.update_timers.values():
loopingcall.stop()
self.config.save()
@ -135,236 +123,214 @@ class Core(CorePluginBase):
"""Update the options for a watch folder."""
watchdir_id = str(watchdir_id)
options = self._make_unicode(options)
CheckInput(
watchdir_id in self.watchdirs, _("Watch folder does not exist.")
check_input(
watchdir_id in self.watchdirs, _('Watch folder does not exist.')
)
if options.has_key('path'):
if 'path' in options:
options['abspath'] = os.path.abspath(options['path'])
CheckInput(
os.path.isdir(options['abspath']), _("Path does not exist.")
check_input(
os.path.isdir(options['abspath']), _('Path does not exist.')
)
for w_id, w in self.watchdirs.iteritems():
for w_id, w in self.watchdirs.items():
if options['abspath'] == w['abspath'] and watchdir_id != w_id:
raise Exception("Path is already being watched.")
for key in options.keys():
raise Exception('Path is already being watched.')
for key in options:
if key not in OPTIONS_AVAILABLE:
if key not 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 key not in [key2 + '_toggle' for key2 in OPTIONS_AVAILABLE]:
raise Exception('autoadd: Invalid options key:%s' % key)
# disable the watch loop if it was active
if watchdir_id in self.update_timers:
self.disable_watchdir(watchdir_id)
self.watchdirs[watchdir_id].update(options)
#re-enable watch loop if appropriate
# re-enable watch loop if appropriate
if self.watchdirs[watchdir_id]['enabled']:
self.enable_watchdir(watchdir_id)
self.config.save()
component.get("EventManager").emit(AutoaddOptionsChangedEvent())
component.get('EventManager').emit(AutoaddOptionsChangedEvent())
def load_torrent(self, filename, magnet):
log.debug('Attempting to open %s for add.', filename)
file_mode = 'r' if magnet else 'rb'
try:
log.debug("Attempting to open %s for add.", filename)
if magnet == False:
_file = open(filename, "rb")
elif magnet == True:
_file = open(filename, "r")
filedump = _file.read()
if not filedump:
raise RuntimeError, "Torrent is 0 bytes!"
_file.close()
except IOError, e:
log.warning("Unable to open %s: %s", filename, e)
raise e
with open(filename, file_mode) as _file:
filedump = _file.read()
except IOError as ex:
log.warning('Unable to open %s: %s', filename, ex)
raise ex
if not filedump:
raise EOFError('Torrent is 0 bytes!')
# Get the info to see if any exceptions are raised
if magnet == False:
if not magnet:
lt.torrent_info(lt.bdecode(filedump))
return filedump
def split_magnets(self, filename):
log.debug("Attempting to open %s for splitting magnets.", filename)
log.debug('Attempting to open %s for splitting magnets.', filename)
magnets = []
try:
_file = open(filename, "r")
except IOError, e:
log.warning("Unable to open %s: %s", filename, e)
raise e
else:
magnets = list(filter(len, _file.readlines()))
_file.close()
if len(magnets) < 2:
return
n = 0
path = filename.rsplit(os.sep, 1)[0]
for magnet in magnets:
for part in magnet.split('&'):
if part.startswith("dn="):
mname = os.sep.join([path, part[3:] + ".magnet"])
with open(filename, 'r') as _file:
magnets = list(filter(len, _file.read().splitlines()))
except IOError as ex:
log.warning('Unable to open %s: %s', filename, ex)
if len(magnets) < 2:
return []
path = filename.rsplit(os.sep, 1)[0]
for magnet in magnets:
if not is_magnet(magnet):
log.warning('Found line which is not a magnet: %s', magnet)
continue
for part in magnet.split('&'):
if part.startswith('dn='):
name = part[3:].strip()
if name:
mname = os.sep.join([path, name + '.magnet'])
break
else:
mname = '.'.join([filename, str(n), "magnet"])
n += 1
try:
_mfile = open(mname, "w")
except IOError, e:
log.warning("Unable to open %s: %s", mname, e)
else:
else:
short_hash = magnet.split('btih:')[1][:8]
mname = '.'.join([os.path.splitext(filename)[0], short_hash, 'magnet'])
try:
with open(mname, 'w') as _mfile:
_mfile.write(magnet)
_mfile.close()
return magnets
except IOError as ex:
log.warning('Unable to open %s: %s', mname, ex)
return magnets
def update_watchdir(self, watchdir_id):
"""Check the watch folder for new torrents to add."""
log.trace("Updating watchdir id: %s", watchdir_id)
log.trace('Updating watchdir id: %s', watchdir_id)
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
log.debug("Watchdir id %s is not enabled. Disabling it.",
log.debug('Watchdir id %s is not enabled. Disabling it.',
watchdir_id)
self.disable_watchdir(watchdir_id)
return
if not os.path.isdir(watchdir["abspath"]):
log.warning("Invalid AutoAdd folder: %s", watchdir["abspath"])
if not os.path.isdir(watchdir['abspath']):
log.warning('Invalid AutoAdd folder: %s', watchdir['abspath'])
self.disable_watchdir(watchdir_id)
return
# Generate options dict for watchdir
opts = {}
options = {}
if 'stop_at_ratio_toggle' in watchdir:
watchdir['stop_ratio_toggle'] = watchdir['stop_at_ratio_toggle']
# We default to True when reading _toggle values, so a config
# without them is valid, and applies all its settings.
for option, value in watchdir.iteritems():
for option, value in watchdir.items():
if OPTIONS_AVAILABLE.get(option):
if watchdir.get(option+'_toggle', True):
opts[option] = value
if watchdir.get(option + '_toggle', True) or option in ['owner', 'seed_mode']:
options[option] = value
# Check for .magnet files containing multiple magnet links and
# create a new .magnet file for each of them.
for filename in os.listdir(watchdir["abspath"]):
for filename in os.listdir(watchdir['abspath']):
try:
filepath = os.path.join(watchdir["abspath"], filename)
except UnicodeDecodeError, e:
log.error("Unable to auto add torrent due to improper "
"filename encoding: %s", e)
filepath = os.path.join(watchdir['abspath'], filename)
except UnicodeDecodeError as ex:
log.error('Unable to auto add torrent due to improper filename encoding: %s', ex)
continue
if os.path.isdir(filepath):
# Skip directories
continue
elif os.path.splitext(filename)[1] == ".magnet" and \
self.split_magnets(filepath):
elif os.path.splitext(filename)[1] == '.magnet' and self.split_magnets(filepath):
os.remove(filepath)
for filename in os.listdir(watchdir["abspath"]):
for filename in os.listdir(watchdir['abspath']):
try:
filepath = os.path.join(watchdir["abspath"], filename)
except UnicodeDecodeError, e:
log.error("Unable to auto add torrent due to improper "
"filename encoding: %s", e)
filepath = os.path.join(watchdir['abspath'], filename)
except UnicodeDecodeError as ex:
log.error('Unable to auto add torrent due to improper filename encoding: %s', ex)
continue
if os.path.isdir(filepath):
# Skip directories
continue
else:
ext = os.path.splitext(filename)[1]
if ext == ".torrent":
magnet = False
elif ext == ".magnet":
magnet = True
else:
ext = os.path.splitext(filename)[1].lower()
magnet = ext == '.magnet'
if not magnet and not ext == '.torrent':
log.debug('File checked for auto-loading is invalid: %s', filename)
continue
try:
filedump = self.load_torrent(filepath, magnet)
except (RuntimeError, Exception), e:
# If the torrent is invalid, we keep track of it so that we
# can try again on the next pass. This is because some
# torrents may not be fully saved during the pass.
log.debug("Torrent is invalid: %s", e)
except (IOError, EOFError) as ex:
# If torrent is invalid, keep track of it so can try again on the next pass.
# This catches torrent files that may not be fully saved to disk at load time.
log.debug('Torrent is invalid: %s', ex)
if filename in self.invalid_torrents:
self.invalid_torrents[filename] += 1
if self.invalid_torrents[filename] >= MAX_NUM_ATTEMPTS:
log.warning(
"Maximum attempts reached while trying to add the "
"torrent file with the path %s", filepath
)
os.rename(filepath, filepath + ".invalid")
log.warning('Maximum attempts reached while trying to add the '
'torrent file with the path %s', filepath)
os.rename(filepath, filepath + '.invalid')
del self.invalid_torrents[filename]
else:
self.invalid_torrents[filename] = 1
continue
# The torrent looks good, so lets add it to the session.
if magnet == False:
torrent_id = component.get("TorrentManager").add(
filedump=filedump, filename=filename, options=opts,
owner=watchdir.get("owner", "localclient")
)
elif magnet == True:
torrent_id = component.get("TorrentManager").add(
magnet=filedump, options=opts,
owner=watchdir.get("owner", "localclient")
)
try:
# The torrent looks good, so lets add it to the session.
if magnet:
torrent_id = component.get('Core').add_torrent_magnet(
filedump.strip(), options)
else:
torrent_id = component.get('Core').add_torrent_file(
filename, base64.encodestring(filedump), options)
except AddTorrentError as ex:
log.error(ex)
os.rename(filepath, filepath + '.invalid')
continue
# If the torrent added successfully, set the extra options.
if torrent_id:
if 'Label' in component.get("CorePluginManager").get_enabled_plugins():
if 'Label' in component.get('CorePluginManager').get_enabled_plugins():
if watchdir.get('label_toggle', True) and watchdir.get('label'):
label = component.get("CorePlugin.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)
component.get('TorrentManager').queue_top(torrent_id)
else:
component.get("TorrentManager").queue_bottom(torrent_id)
component.get('TorrentManager').queue_bottom(torrent_id)
else:
# torrent handle is invalid and so is the magnet link
if magnet == True:
log.debug("invalid magnet link")
os.rename(filepath, filepath + ".invalid")
if magnet:
log.debug('invalid magnet link')
os.rename(filepath, filepath + '.invalid')
continue
# Rename, copy or delete the torrent once added to deluge.
if watchdir.get('append_extension_toggle'):
if not watchdir.get('append_extension'):
watchdir['append_extension'] = ".added"
watchdir['append_extension'] = '.added'
os.rename(filepath, filepath + watchdir['append_extension'])
elif watchdir.get('copy_torrent_toggle'):
copy_torrent_path = watchdir['copy_torrent']
copy_torrent_file = os.path.join(copy_torrent_path, filename)
log.debug("Moving added torrent file \"%s\" to \"%s\"",
log.debug('Moving added torrent file "%s" to "%s"',
os.path.basename(filepath), copy_torrent_path)
try:
os.rename(filepath, copy_torrent_file)
except OSError, why:
from errno import EXDEV
if why.errno == errno.EXDEV:
# This can happen for different mount points
from shutil import copyfile
try:
copyfile(filepath, copy_torrent_file)
os.remove(filepath)
except OSError:
# Last Resort!
try:
open(copy_torrent_file, 'wb').write(
open(filepath, 'rb').read()
)
os.remove(filepath)
except OSError, why:
raise why
else:
raise why
shutil.move(filepath, copy_torrent_file)
else:
os.remove(filepath)
def on_update_watchdir_error(self, failure, watchdir_id):
"""Disables any watch folders with un-handled exceptions."""
self.disable_watchdir(watchdir_id)
log.error("Disabling '%s', error during update: %s",
self.watchdirs[watchdir_id]["path"], failure)
log.error('Disabling "%s", error during update: %s',
self.watchdirs[watchdir_id]['path'], failure)
@export
def enable_watchdir(self, watchdir_id):
@ -379,7 +345,7 @@ class Core(CorePluginBase):
if not self.watchdirs[w_id]['enabled']:
self.watchdirs[w_id]['enabled'] = True
self.config.save()
component.get("EventManager").emit(AutoaddOptionsChangedEvent())
component.get('EventManager').emit(AutoaddOptionsChangedEvent())
@export
def disable_watchdir(self, watchdir_id):
@ -393,16 +359,16 @@ class Core(CorePluginBase):
if self.watchdirs[w_id]['enabled']:
self.watchdirs[w_id]['enabled'] = False
self.config.save()
component.get("EventManager").emit(AutoaddOptionsChangedEvent())
component.get('EventManager').emit(AutoaddOptionsChangedEvent())
@export
def set_config(self, config):
"""Sets the config dictionary."""
config = self._make_unicode(config)
for key in config.keys():
for key in config:
self.config[key] = config[key]
self.config.save()
component.get("EventManager").emit(AutoaddOptionsChangedEvent())
component.get('EventManager').emit(AutoaddOptionsChangedEvent())
@export
def get_config(self):
@ -411,43 +377,45 @@ class Core(CorePluginBase):
@export
def get_watchdirs(self):
rpcserver = component.get("RPCServer")
rpcserver = component.get('RPCServer')
session_user = rpcserver.get_session_user()
session_auth_level = rpcserver.get_session_auth_level()
if session_auth_level == AUTH_LEVEL_ADMIN:
log.debug("Current logged in user %s is an ADMIN, send all "
"watchdirs", session_user)
log.debug('Current logged in user %s is an ADMIN, send all '
'watchdirs', session_user)
return self.watchdirs
watchdirs = {}
for watchdir_id, watchdir in self.watchdirs.iteritems():
if watchdir.get("owner", "localclient") == session_user:
for watchdir_id, watchdir in self.watchdirs.items():
if watchdir.get('owner', 'localclient') == session_user:
watchdirs[watchdir_id] = watchdir
log.debug("Current logged in user %s is not an ADMIN, send only "
"his watchdirs: %s", session_user, watchdirs.keys())
log.debug('Current logged in user %s is not an ADMIN, send only '
'their watchdirs: %s', session_user, list(watchdirs))
return watchdirs
def _make_unicode(self, options):
opts = {}
for key in options:
if isinstance(options[key], str):
options[key] = unicode(options[key], "utf8")
if isinstance(options[key], bytes):
options[key] = options[key].decode('utf8')
opts[key] = options[key]
return opts
@export
def add(self, options={}):
def add(self, options=None):
"""Add a watch folder."""
if options is None:
options = {}
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."
check_input(os.path.isdir(abswatchdir), _('Path does not exist.'))
check_input(
os.access(abswatchdir, os.R_OK | os.W_OK),
'You must have read and write access to watch folder.'
)
if abswatchdir in [wd['abspath'] for wd in self.watchdirs.itervalues()]:
raise Exception("Path is already being watched.")
if abswatchdir in [wd['abspath'] for wd in self.watchdirs.values()]:
raise Exception('Path is already being watched.')
options.setdefault('enabled', False)
options['abspath'] = abswatchdir
watchdir_id = self.config['next_id']
@ -456,36 +424,35 @@ class Core(CorePluginBase):
self.enable_watchdir(watchdir_id)
self.config['next_id'] = watchdir_id + 1
self.config.save()
component.get("EventManager").emit(AutoaddOptionsChangedEvent())
component.get('EventManager').emit(AutoaddOptionsChangedEvent())
return watchdir_id
@export
def remove(self, watchdir_id):
"""Remove a watch folder."""
watchdir_id = str(watchdir_id)
CheckInput(watchdir_id in self.watchdirs,
"Unknown Watchdir: %s" % self.watchdirs)
check_input(watchdir_id in self.watchdirs, 'Unknown Watchdir: %s' % self.watchdirs)
if self.watchdirs[watchdir_id]['enabled']:
self.disable_watchdir(watchdir_id)
del self.watchdirs[watchdir_id]
self.config.save()
component.get("EventManager").emit(AutoaddOptionsChangedEvent())
component.get('EventManager').emit(AutoaddOptionsChangedEvent())
def __migrate_config_1_to_2(self, config):
for watchdir_id in config['watchdirs'].iterkeys():
for watchdir_id in config['watchdirs']:
config['watchdirs'][watchdir_id]['owner'] = 'localclient'
return config
def __on_pre_torrent_removed(self, torrent_id):
try:
torrent = component.get("TorrentManager")[torrent_id]
torrent = component.get('TorrentManager')[torrent_id]
except KeyError:
log.warning("Unable to remove torrent file for torrent id %s. It"
"was already deleted from the TorrentManager",
log.warning('Unable to remove torrent file for torrent id %s. It'
'was already deleted from the TorrentManager',
torrent_id)
return
torrent_fname = torrent.filename
for watchdir in self.watchdirs.itervalues():
for watchdir in self.watchdirs.values():
if not watchdir.get('copy_torrent_toggle', False):
# This watchlist does copy torrents
continue
@ -497,9 +464,9 @@ class Core(CorePluginBase):
if os.path.isfile(torrent_fname_path):
try:
os.remove(torrent_fname_path)
log.info("Removed torrent file \"%s\" from \"%s\"",
log.info('Removed torrent file "%s" from "%s"',
torrent_fname, copy_torrent_path)
break
except OSError, e:
log.info("Failed to removed torrent file \"%s\" from "
"\"%s\": %s", torrent_fname, copy_torrent_path, e)
except OSError as ex:
log.info('Failed to removed torrent file "%s" from "%s": %s',
torrent_fname, copy_torrent_path, ex)

View File

@ -1,50 +1,57 @@
/*
Script: autoadd.js
The client-side javascript code for the AutoAdd plugin.
/*!
* Script: autoadd.js
* The client-side javascript code for the AutoAdd plugin.
*
* Copyright (C) 2009 GazpachoKing <chase.sterling@gmail.com>
*
* This file is part of Deluge and is licensed under GNU General Public License 3.0, or later, with
* the additional special exception to link portions of this program with the OpenSSL library.
* See LICENSE for more details.
*/
Copyright:
(C) GazpachoKing 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.
Ext.ns('Deluge.ux.preferences');
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.
/**
* @class Deluge.ux.preferences.AutoAddPage
* @extends Ext.Panel
*/
Deluge.ux.preferences.AutoAddPage = Ext.extend(Ext.Panel, {
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.
title: _('AutoAdd'),
layout: 'fit',
border: false,
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.
*/
AutoAddPlugin = Ext.extend(Deluge.Plugin, {
constructor: function(config) {
config = Ext.apply({
name: "AutoAdd"
}, config);
AutoAddPlugin.superclass.constructor.call(this, config);
},
onDisable: function() {
},
onEnable: function() {
}
initComponent: function() {
Deluge.ux.preferences.AutoAddPage.superclass.initComponent.call(this);
fieldset = this.add({
xtype: 'fieldset',
border: false,
title: _('AutoAdd Preferences'),
autoHeight: true,
labelWidth: 1,
defaultType: 'panel'
});
fieldset.add({
border: false,
bodyCfg: {
html: _('<p>The AutoAdd plugin is enabled however there is no WebUI ' +
'preferences page implemented yet for this plugin.</p><br>' +
'<p>In the meantime please use GtkUI preference page to configure this plugin.<p>')
}
});
}
});
new AutoAddPlugin();
Deluge.plugins.AutoAddPlugin = Ext.extend(Deluge.Plugin, {
name: 'AutoAdd',
onDisable: function() {
deluge.preferences.removePage(this.prefsPage);
},
onEnable: function() {
this.prefsPage = deluge.preferences.addPage(new Deluge.ux.preferences.AutoAddPage());
}
});
Deluge.registerPlugin('AutoAdd', Deluge.plugins.AutoAddPlugin);

View File

@ -25,7 +25,6 @@
<property name="can_focus">True</property>
<property name="can_default">True</property>
<property name="receives_default">False</property>
<property name="use_action_appearance">False</property>
<property name="use_stock">True</property>
<signal name="clicked" handler="on_opts_cancel"/>
</widget>
@ -42,7 +41,6 @@
<property name="can_focus">True</property>
<property name="can_default">True</property>
<property name="receives_default">False</property>
<property name="use_action_appearance">False</property>
<property name="use_stock">True</property>
<signal name="clicked" handler="on_opts_add"/>
</widget>
@ -59,7 +57,6 @@
<property name="can_focus">True</property>
<property name="can_default">True</property>
<property name="receives_default">False</property>
<property name="use_action_appearance">False</property>
<property name="use_stock">True</property>
<signal name="clicked" handler="on_opts_apply"/>
</widget>
@ -155,7 +152,6 @@ it will be added to the session.</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">False</property>
<property name="use_action_appearance">False</property>
<property name="use_underline">True</property>
<property name="draw_indicator">True</property>
<signal name="toggled" handler="on_enabled_toggle_toggled"/>
@ -189,42 +185,7 @@ it will be added to the session.</property>
</packing>
</child>
<child>
<widget class="GtkFrame" id="OwnerFrame">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label_xalign">0</property>
<property name="shadow_type">none</property>
<child>
<widget class="GtkAlignment" id="alignment5">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="left_padding">12</property>
<child>
<widget class="GtkComboBox" id="OwnerCombobox">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="tooltip" translatable="yes">The user selected here will be the owner of the torrent.</property>
</widget>
</child>
</widget>
</child>
<child>
<widget class="GtkLabel" id="label3">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label" translatable="yes">&lt;b&gt;Owner&lt;/b&gt;</property>
<property name="use_markup">True</property>
</widget>
<packing>
<property name="type">label_item</property>
</packing>
</child>
</widget>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
<placeholder/>
</child>
<child>
<widget class="GtkFrame" id="frame1">
@ -251,9 +212,8 @@ it will be added to the session.</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">False</property>
<property name="tooltip" translatable="yes">Once the torrent is added to the session,
<property name="tooltip" translatable="yes">Once the torrent is added to the session,
the .torrent will be deleted.</property>
<property name="use_action_appearance">False</property>
<property name="draw_indicator">True</property>
<signal name="toggled" handler="on_toggle_toggled"/>
</widget>
@ -273,10 +233,9 @@ the .torrent will be deleted.</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">False</property>
<property name="tooltip" translatable="yes">Once the torrent is added to the session,
<property name="tooltip" translatable="yes">Once the torrent is added to the session,
an extension will be appended to the .torrent
and it will remain in the same directory.</property>
<property name="use_action_appearance">False</property>
<property name="draw_indicator">True</property>
<property name="group">isnt_append_extension</property>
<signal name="toggled" handler="on_toggle_toggled"/>
@ -323,10 +282,9 @@ and it will remain in the same directory.</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">False</property>
<property name="tooltip" translatable="yes">Once the torrent is added to the session,
<property name="tooltip" translatable="yes">Once the torrent is added to the session,
the .torrent will copied to the chosen directory
and deleted from the watch folder.</property>
<property name="use_action_appearance">False</property>
<property name="draw_indicator">True</property>
<property name="group">isnt_append_extension</property>
<signal name="toggled" handler="on_toggle_toggled"/>
@ -378,9 +336,8 @@ and deleted from the watch folder.</property>
<property name="can_focus">True</property>
<property name="receives_default">False</property>
<property name="has_tooltip">True</property>
<property name="tooltip" translatable="yes">Once the torrent is deleted from the session,
<property name="tooltip" translatable="yes">Once the torrent is deleted from the session,
also delete the .torrent file used to add it.</property>
<property name="use_action_appearance">False</property>
<property name="draw_indicator">True</property>
</widget>
<packing>
@ -438,12 +395,11 @@ also delete the .torrent file used to add it.</property>
<property name="can_focus">False</property>
<child>
<widget class="GtkCheckButton" id="download_location_toggle">
<property name="label" translatable="yes">Set download location</property>
<property name="label" translatable="yes">Set download folder</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">False</property>
<property name="tooltip" translatable="yes">This directory will be the download location</property>
<property name="use_action_appearance">False</property>
<property name="tooltip" translatable="yes">This folder will be where the torrent data is downloaded to.</property>
<property name="use_underline">True</property>
<property name="draw_indicator">True</property>
<signal name="toggled" handler="on_toggle_toggled"/>
@ -502,7 +458,7 @@ also delete the .torrent file used to add it.</property>
<widget class="GtkLabel" id="label7">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label" translatable="yes">&lt;b&gt;Download Location&lt;/b&gt;</property>
<property name="label" translatable="yes">&lt;b&gt;Download Folder&lt;/b&gt;</property>
<property name="use_markup">True</property>
</widget>
<packing>
@ -533,11 +489,10 @@ also delete the .torrent file used to add it.</property>
<property name="can_focus">False</property>
<child>
<widget class="GtkCheckButton" id="move_completed_toggle">
<property name="label" translatable="yes">Set move completed location</property>
<property name="label" translatable="yes">Set move completed folder</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">False</property>
<property name="use_action_appearance">False</property>
<property name="use_underline">True</property>
<property name="draw_indicator">True</property>
<signal name="toggled" handler="on_toggle_toggled"/>
@ -586,7 +541,6 @@ also delete the .torrent file used to add it.</property>
<property name="sensitive">False</property>
<property name="can_focus">False</property>
<property name="receives_default">False</property>
<property name="use_action_appearance">False</property>
<property name="use_underline">True</property>
<property name="active">True</property>
<property name="draw_indicator">True</property>
@ -646,7 +600,6 @@ also delete the .torrent file used to add it.</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">False</property>
<property name="use_action_appearance">False</property>
<property name="use_underline">True</property>
<property name="draw_indicator">True</property>
<signal name="toggled" handler="on_toggle_toggled"/>
@ -661,15 +614,6 @@ also delete the .torrent file used to add it.</property>
<widget class="GtkComboBoxEntry" id="label">
<property name="visible">True</property>
<property name="can_focus">False</property>
<child internal-child="entry">
<widget class="GtkEntry" id="comboboxentry-entry2">
<property name="can_focus">False</property>
<property name="primary_icon_activatable">False</property>
<property name="secondary_icon_activatable">False</property>
<property name="primary_icon_sensitive">True</property>
<property name="secondary_icon_sensitive">True</property>
</widget>
</child>
</widget>
<packing>
<property name="expand">True</property>
@ -717,8 +661,45 @@ also delete the .torrent file used to add it.</property>
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="border_width">6</property>
<property name="spacing">5</property>
<child>
<placeholder/>
<widget class="GtkFrame" id="OwnerFrame">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label_xalign">0</property>
<property name="shadow_type">none</property>
<child>
<widget class="GtkAlignment" id="alignment5">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="left_padding">12</property>
<child>
<widget class="GtkComboBox" id="OwnerCombobox">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="has_tooltip">True</property>
<property name="tooltip" translatable="yes">The user selected here will be the owner of the torrent.</property>
</widget>
</child>
</widget>
</child>
<child>
<widget class="GtkLabel" id="label3">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label" translatable="yes">&lt;b&gt;Owner&lt;/b&gt;</property>
<property name="use_markup">True</property>
</widget>
<packing>
<property name="type">label_item</property>
</packing>
</child>
</widget>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<widget class="GtkFrame" id="frame5">
@ -746,7 +727,6 @@ also delete the .torrent file used to add it.</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">False</property>
<property name="use_action_appearance">False</property>
<property name="use_underline">True</property>
<property name="draw_indicator">True</property>
<signal name="toggled" handler="on_toggle_toggled"/>
@ -764,7 +744,6 @@ also delete the .torrent file used to add it.</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">False</property>
<property name="use_action_appearance">False</property>
<property name="use_underline">True</property>
<property name="draw_indicator">True</property>
<signal name="toggled" handler="on_toggle_toggled"/>
@ -782,7 +761,6 @@ also delete the .torrent file used to add it.</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">False</property>
<property name="use_action_appearance">False</property>
<property name="use_underline">True</property>
<property name="draw_indicator">True</property>
<signal name="toggled" handler="on_toggle_toggled"/>
@ -908,7 +886,6 @@ also delete the .torrent file used to add it.</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">False</property>
<property name="use_action_appearance">False</property>
<property name="use_underline">True</property>
<property name="draw_indicator">True</property>
<signal name="toggled" handler="on_toggle_toggled"/>
@ -961,8 +938,8 @@ also delete the .torrent file used to add it.</property>
<widget class="GtkTable" id="table2">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="n_rows">5</property>
<property name="n_columns">4</property>
<property name="n_rows">6</property>
<property name="n_columns">3</property>
<property name="column_spacing">2</property>
<property name="row_spacing">4</property>
<child>
@ -975,7 +952,6 @@ also delete the .torrent file used to add it.</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">False</property>
<property name="use_action_appearance">False</property>
<property name="use_underline">True</property>
<property name="draw_indicator">True</property>
<signal name="toggled" handler="on_toggle_toggled"/>
@ -1001,7 +977,6 @@ also delete the .torrent file used to add it.</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">False</property>
<property name="use_action_appearance">False</property>
<property name="use_underline">True</property>
<property name="draw_indicator">True</property>
</widget>
@ -1018,7 +993,6 @@ also delete the .torrent file used to add it.</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">False</property>
<property name="use_action_appearance">False</property>
<property name="use_underline">True</property>
<property name="draw_indicator">True</property>
<signal name="toggled" handler="on_toggle_toggled"/>
@ -1035,7 +1009,6 @@ also delete the .torrent file used to add it.</property>
<property name="sensitive">False</property>
<property name="can_focus">True</property>
<property name="receives_default">False</property>
<property name="use_action_appearance">False</property>
<property name="use_underline">True</property>
<property name="active">True</property>
<property name="draw_indicator">True</property>
@ -1054,7 +1027,6 @@ also delete the .torrent file used to add it.</property>
<property name="sensitive">False</property>
<property name="can_focus">True</property>
<property name="receives_default">False</property>
<property name="use_action_appearance">False</property>
<property name="use_underline">True</property>
<property name="active">True</property>
<property name="draw_indicator">True</property>
@ -1096,11 +1068,11 @@ also delete the .torrent file used to add it.</property>
<property name="homogeneous">True</property>
<child>
<widget class="GtkRadioButton" id="auto_managed">
<property name="label" translatable="no">gtk-yes</property>
<property name="label">gtk-yes</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">False</property>
<property name="use_action_appearance">False</property>
<property name="use_stock">True</property>
<property name="active">True</property>
<property name="draw_indicator">True</property>
</widget>
@ -1112,11 +1084,11 @@ also delete the .torrent file used to add it.</property>
</child>
<child>
<widget class="GtkRadioButton" id="isnt_auto_managed">
<property name="label" translatable="no">gtk-no</property>
<property name="label">gtk-no</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">False</property>
<property name="use_action_appearance">False</property>
<property name="use_stock">True</property>
<property name="draw_indicator">True</property>
<property name="group">auto_managed</property>
</widget>
@ -1141,7 +1113,6 @@ also delete the .torrent file used to add it.</property>
<property name="sensitive">False</property>
<property name="can_focus">True</property>
<property name="receives_default">False</property>
<property name="use_action_appearance">False</property>
<property name="use_underline">True</property>
<property name="active">True</property>
<property name="draw_indicator">True</property>
@ -1161,7 +1132,6 @@ also delete the .torrent file used to add it.</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">False</property>
<property name="use_action_appearance">False</property>
<property name="draw_indicator">True</property>
<signal name="toggled" handler="on_toggle_toggled"/>
</widget>
@ -1173,11 +1143,11 @@ also delete the .torrent file used to add it.</property>
<property name="homogeneous">True</property>
<child>
<widget class="GtkRadioButton" id="add_paused">
<property name="label" translatable="no">gtk-yes</property>
<property name="label">gtk-yes</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">False</property>
<property name="use_action_appearance">False</property>
<property name="use_stock">True</property>
<property name="active">True</property>
<property name="draw_indicator">True</property>
</widget>
@ -1189,11 +1159,11 @@ also delete the .torrent file used to add it.</property>
</child>
<child>
<widget class="GtkRadioButton" id="isnt_add_paused">
<property name="label" translatable="no">gtk-no</property>
<property name="label">gtk-no</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">False</property>
<property name="use_action_appearance">False</property>
<property name="use_stock">True</property>
<property name="draw_indicator">True</property>
<property name="group">add_paused</property>
</widget>
@ -1215,7 +1185,6 @@ also delete the .torrent file used to add it.</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">False</property>
<property name="use_action_appearance">False</property>
<property name="draw_indicator">True</property>
<signal name="toggled" handler="on_toggle_toggled"/>
</widget>
@ -1235,7 +1204,6 @@ also delete the .torrent file used to add it.</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">False</property>
<property name="use_action_appearance">False</property>
<property name="active">True</property>
<property name="draw_indicator">True</property>
</widget>
@ -1251,7 +1219,6 @@ also delete the .torrent file used to add it.</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">False</property>
<property name="use_action_appearance">False</property>
<property name="draw_indicator">True</property>
<property name="group">queue_to_top</property>
</widget>
@ -1270,13 +1237,20 @@ also delete the .torrent file used to add it.</property>
</packing>
</child>
<child>
<placeholder/>
</child>
<child>
<placeholder/>
</child>
<child>
<placeholder/>
<widget class="GtkCheckButton" id="seed_mode">
<property name="label" translatable="yes">Skip File Hash Check</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">False</property>
<property name="use_underline">True</property>
<property name="draw_indicator">True</property>
</widget>
<packing>
<property name="top_attach">5</property>
<property name="bottom_attach">6</property>
<property name="x_options">GTK_FILL</property>
<property name="y_options"></property>
</packing>
</child>
<child>
<placeholder/>

View File

@ -1,5 +1,4 @@
#
# gtkui.py
# -*- coding: utf-8 -*-
#
# Copyright (C) 2009 GazpachoKing <chase.sterling@gmail.com>
#
@ -8,82 +7,66 @@
# 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.
# This file is part of Deluge and is licensed under GNU General Public License 3.0, or later, with
# the additional special exception to link portions of this program with the OpenSSL library.
# See LICENSE for more details.
#
from __future__ import unicode_literals
import logging
import os
import gtk
import gtk.glade
import logging
import deluge.common
import deluge.component as component
from deluge.plugins.pluginbase import GtkPluginBase
from deluge.ui.client import client
from deluge.ui.gtkui import dialogs
from deluge.plugins.pluginbase import GtkPluginBase
import deluge.component as component
import deluge.common
import os
from common import get_resource
from .common import get_resource
log = logging.getLogger(__name__)
class IncompatibleOption(Exception):
pass
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", "queue_to_top"]
class OptionsDialog(object):
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', 'queue_to_top']
def __init__(self):
self.accounts = gtk.ListStore(str)
self.labels = gtk.ListStore(str)
self.core_config = {}
def show(self, options={}, watchdir_id=None):
self.glade = gtk.glade.XML(get_resource("autoadd_options.glade"))
def show(self, options=None, watchdir_id=None):
if options is None:
options = {}
self.glade = gtk.glade.XML(get_resource('autoadd_options.glade'))
self.glade.signal_autoconnect({
"on_opts_add":self.on_add,
"on_opts_apply":self.on_apply,
"on_opts_cancel":self.on_cancel,
"on_options_dialog_close":self.on_cancel,
"on_toggle_toggled":self.on_toggle_toggled
'on_opts_add': self.on_add,
'on_opts_apply': self.on_apply,
'on_opts_cancel': self.on_cancel,
'on_options_dialog_close': self.on_cancel,
'on_toggle_toggled': self.on_toggle_toggled
})
self.dialog = self.glade.get_widget("options_dialog")
self.dialog.set_transient_for(component.get("Preferences").pref_dialog)
self.dialog = self.glade.get_widget('options_dialog')
self.dialog.set_transient_for(component.get('Preferences').pref_dialog)
if watchdir_id:
#We have an existing watchdir_id, we are editing
# We have an existing watchdir_id, we are editing
self.glade.get_widget('opts_add_button').hide()
self.glade.get_widget('opts_apply_button').show()
self.watchdir_id = watchdir_id
else:
#We don't have an id, adding
# We don't have an id, adding
self.glade.get_widget('opts_add_button').show()
self.glade.get_widget('opts_apply_button').hide()
self.watchdir_id = None
@ -108,6 +91,9 @@ class OptionsDialog():
self.glade.get_widget('delete_copy_torrent_toggle').set_active(
options.get('delete_copy_torrent_toggle', False)
)
self.glade.get_widget('seed_mode').set_active(
options.get('seed_mode', False)
)
self.accounts.clear()
self.labels.clear()
combobox = self.glade.get_widget('OwnerCombobox')
@ -122,12 +108,12 @@ class OptionsDialog():
label_widget.set_text_column(0)
self.glade.get_widget('label_toggle').set_active(options.get('label_toggle', False))
for id in self.spin_ids + self.spin_int_ids:
self.glade.get_widget(id).set_value(options.get(id, 0))
self.glade.get_widget(id+'_toggle').set_active(options.get(id+'_toggle', False))
for id in self.chk_ids:
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))
for spin_id in self.spin_ids + self.spin_int_ids:
self.glade.get_widget(spin_id).set_value(options.get(spin_id, 0))
self.glade.get_widget(spin_id + '_toggle').set_active(options.get(spin_id + '_toggle', False))
for chk_id in self.chk_ids:
self.glade.get_widget(chk_id).set_active(bool(options.get(chk_id, True)))
self.glade.get_widget(chk_id + '_toggle').set_active(options.get(chk_id + '_toggle', False))
if not options.get('add_paused', True):
self.glade.get_widget('isnt_add_paused').set_active(True)
if not options.get('queue_to_top', True):
@ -137,78 +123,78 @@ class OptionsDialog():
for field in ['move_completed_path', 'path', 'download_location',
'copy_torrent']:
if client.is_localhost():
self.glade.get_widget(field+"_chooser").set_current_folder(
options.get(field, os.path.expanduser("~"))
self.glade.get_widget(field + '_chooser').set_current_folder(
options.get(field, os.path.expanduser('~'))
)
self.glade.get_widget(field+"_chooser").show()
self.glade.get_widget(field+"_entry").hide()
self.glade.get_widget(field + '_chooser').show()
self.glade.get_widget(field + '_entry').hide()
else:
self.glade.get_widget(field+"_entry").set_text(
options.get(field, "")
self.glade.get_widget(field + '_entry').set_text(
options.get(field, '')
)
self.glade.get_widget(field+"_entry").show()
self.glade.get_widget(field+"_chooser").hide()
self.glade.get_widget(field + '_entry').show()
self.glade.get_widget(field + '_chooser').hide()
self.set_sensitive()
def on_core_config(config):
if client.is_localhost():
self.glade.get_widget('download_location_chooser').set_current_folder(
options.get('download_location', config["download_location"])
options.get('download_location', config['download_location'])
)
if options.get('move_completed_toggle', config["move_completed"]):
if options.get('move_completed_toggle', config['move_completed']):
self.glade.get_widget('move_completed_toggle').set_active(True)
self.glade.get_widget('move_completed_path_chooser').set_current_folder(
options.get('move_completed_path', config["move_completed_path"])
options.get('move_completed_path', config['move_completed_path'])
)
if options.get('copy_torrent_toggle', config["copy_torrent_file"]):
if options.get('copy_torrent_toggle', config['copy_torrent_file']):
self.glade.get_widget('copy_torrent_toggle').set_active(True)
self.glade.get_widget('copy_torrent_chooser').set_current_folder(
options.get('copy_torrent', config["torrentfiles_location"])
options.get('copy_torrent', config['torrentfiles_location'])
)
else:
self.glade.get_widget('download_location_entry').set_text(
options.get('download_location', config["download_location"])
options.get('download_location', config['download_location'])
)
if options.get('move_completed_toggle', config["move_completed"]):
if options.get('move_completed_toggle', config['move_completed']):
self.glade.get_widget('move_completed_toggle').set_active(
options.get('move_completed_toggle', False)
)
self.glade.get_widget('move_completed_path_entry').set_text(
options.get('move_completed_path', config["move_completed_path"])
options.get('move_completed_path', config['move_completed_path'])
)
if options.get('copy_torrent_toggle', config["copy_torrent_file"]):
if options.get('copy_torrent_toggle', config['copy_torrent_file']):
self.glade.get_widget('copy_torrent_toggle').set_active(True)
self.glade.get_widget('copy_torrent_entry').set_text(
options.get('copy_torrent', config["torrentfiles_location"])
options.get('copy_torrent', config['torrentfiles_location'])
)
if options.get('delete_copy_torrent_toggle', config["del_copy_torrent_file"]):
if options.get('delete_copy_torrent_toggle', config['del_copy_torrent_file']):
self.glade.get_widget('delete_copy_torrent_toggle').set_active(True)
if not options:
client.core.get_config().addCallback(on_core_config)
def on_accounts(accounts, owner):
log.debug("Got Accounts")
log.debug('Got Accounts')
selected_iter = None
for account in accounts:
iter = self.accounts.append()
acc_iter = self.accounts.append()
self.accounts.set_value(
iter, 0, account['username']
acc_iter, 0, account['username']
)
if account['username'] == owner:
selected_iter = iter
selected_iter = acc_iter
self.glade.get_widget('OwnerCombobox').set_active_iter(selected_iter)
def on_accounts_failure(failure):
log.debug("Failed to get accounts!!! %s", failure)
iter = self.accounts.append()
self.accounts.set_value(iter, 0, client.get_auth_user())
log.debug('Failed to get accounts!!! %s', failure)
acc_iter = self.accounts.append()
self.accounts.set_value(acc_iter, 0, client.get_auth_user())
self.glade.get_widget('OwnerCombobox').set_active(0)
self.glade.get_widget('OwnerCombobox').set_sensitive(False)
def on_labels(labels):
log.debug("Got Labels: %s", labels)
log.debug('Got Labels: %s', labels)
for label in labels:
self.labels.set_value(self.labels.append(), 0, label)
label_widget = self.glade.get_widget('label')
@ -232,8 +218,8 @@ class OptionsDialog():
on_accounts, options.get('owner', client.get_auth_user())
).addErrback(on_accounts_failure)
else:
iter = self.accounts.append()
self.accounts.set_value(iter, 0, client.get_auth_user())
acc_iter = self.accounts.append()
self.accounts.set_value(acc_iter, 0, client.get_auth_user())
self.glade.get_widget('OwnerCombobox').set_active(0)
self.glade.get_widget('OwnerCombobox').set_sensitive(False)
@ -243,10 +229,11 @@ class OptionsDialog():
'max_upload_speed', 'max_connections',
'max_upload_slots', 'add_paused', 'auto_managed',
'stop_at_ratio', 'queue_to_top', 'copy_torrent']
[self.on_toggle_toggled(self.glade.get_widget(x+'_toggle')) for x in maintoggles]
for maintoggle in maintoggles:
self.on_toggle_toggled(self.glade.get_widget(maintoggle + '_toggle'))
def on_toggle_toggled(self, tb):
toggle = str(tb.name).replace("_toggle", "")
toggle = str(tb.name).replace('_toggle', '')
isactive = tb.get_active()
if toggle == 'download_location':
self.glade.get_widget('download_location_chooser').set_sensitive(isactive)
@ -287,32 +274,31 @@ class OptionsDialog():
self.glade.get_widget('stop_ratio').set_sensitive(isactive)
self.glade.get_widget('remove_at_ratio').set_sensitive(isactive)
def on_apply(self, Event=None):
def on_apply(self, event=None):
try:
options = self.generate_opts()
client.autoadd.set_options(
str(self.watchdir_id), options
).addCallbacks(self.on_added, self.on_error_show)
except IncompatibleOption, err:
dialogs.ErrorDialog(_("Incompatible Option"), str(err), self.dialog).run()
except IncompatibleOption as ex:
dialogs.ErrorDialog(_('Incompatible Option'), str(ex), self.dialog).run()
def on_error_show(self, result):
d = dialogs.ErrorDialog(_("Error"), result.value.exception_msg, self.dialog)
d = dialogs.ErrorDialog(_('Error'), result.value.exception_msg, self.dialog)
result.cleanFailure()
d.run()
def on_added(self, result):
self.dialog.destroy()
def on_add(self, Event=None):
def on_add(self, event=None):
try:
options = self.generate_opts()
client.autoadd.add(options).addCallbacks(self.on_added, self.on_error_show)
except IncompatibleOption, err:
dialogs.ErrorDialog(_("Incompatible Option"), str(err), self.dialog).run()
except IncompatibleOption as ex:
dialogs.ErrorDialog(_('Incompatible Option'), str(ex), self.dialog).run()
def on_cancel(self, Event=None):
def on_cancel(self, event=None):
self.dialog.destroy()
def generate_opts(self):
@ -343,49 +329,49 @@ class OptionsDialog():
for key in ['append_extension_toggle', 'download_location_toggle',
'label_toggle', 'copy_torrent_toggle',
'delete_copy_torrent_toggle']:
'delete_copy_torrent_toggle', 'seed_mode']:
options[key] = self.glade.get_widget(key).get_active()
for id in self.spin_ids:
options[id] = self.glade.get_widget(id).get_value()
options[id+'_toggle'] = self.glade.get_widget(id+'_toggle').get_active()
for id in self.spin_int_ids:
options[id] = self.glade.get_widget(id).get_value_as_int()
options[id+'_toggle'] = self.glade.get_widget(id+'_toggle').get_active()
for id in self.chk_ids:
options[id] = self.glade.get_widget(id).get_active()
options[id+'_toggle'] = self.glade.get_widget(id+'_toggle').get_active()
for spin_id in self.spin_ids:
options[spin_id] = self.glade.get_widget(spin_id).get_value()
options[spin_id + '_toggle'] = self.glade.get_widget(spin_id + '_toggle').get_active()
for spin_int_id in self.spin_int_ids:
options[spin_int_id] = self.glade.get_widget(spin_int_id).get_value_as_int()
options[spin_int_id + '_toggle'] = self.glade.get_widget(spin_int_id + '_toggle').get_active()
for chk_id in self.chk_ids:
options[chk_id] = self.glade.get_widget(chk_id).get_active()
options[chk_id + '_toggle'] = self.glade.get_widget(chk_id + '_toggle').get_active()
if options['copy_torrent_toggle'] and options['path'] == options['copy_torrent']:
raise IncompatibleOption(_("\"Watch Folder\" directory and \"Copy of .torrent"
" files to\" directory cannot be the same!"))
raise IncompatibleOption(_('"Watch Folder" directory and "Copy of .torrent'
' files to" directory cannot be the same!'))
return options
class GtkUI(GtkPluginBase):
def enable(self):
self.glade = gtk.glade.XML(get_resource("config.glade"))
self.glade = gtk.glade.XML(get_resource('config.glade'))
self.glade.signal_autoconnect({
"on_add_button_clicked": self.on_add_button_clicked,
"on_edit_button_clicked": self.on_edit_button_clicked,
"on_remove_button_clicked": self.on_remove_button_clicked
'on_add_button_clicked': self.on_add_button_clicked,
'on_edit_button_clicked': self.on_edit_button_clicked,
'on_remove_button_clicked': self.on_remove_button_clicked
})
self.opts_dialog = OptionsDialog()
component.get("PluginManager").register_hook(
"on_apply_prefs", self.on_apply_prefs
component.get('PluginManager').register_hook(
'on_apply_prefs', self.on_apply_prefs
)
component.get("PluginManager").register_hook(
"on_show_prefs", self.on_show_prefs
component.get('PluginManager').register_hook(
'on_show_prefs', self.on_show_prefs
)
client.register_event_handler(
"AutoaddOptionsChangedEvent", self.on_options_changed_event
'AutoaddOptionsChangedEvent', self.on_options_changed_event
)
self.watchdirs = {}
vbox = self.glade.get_widget("watchdirs_vbox")
vbox = self.glade.get_widget('watchdirs_vbox')
sw = gtk.ScrolledWindow()
sw.set_shadow_type(gtk.SHADOW_ETCHED_IN)
sw.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
@ -395,61 +381,61 @@ class GtkUI(GtkPluginBase):
self.store = self.create_model()
self.treeView = gtk.TreeView(self.store)
self.treeView.connect("cursor-changed", self.on_listitem_activated)
self.treeView.connect("row-activated", self.on_edit_button_clicked)
self.treeView.connect('cursor-changed', self.on_listitem_activated)
self.treeView.connect('row-activated', self.on_edit_button_clicked)
self.treeView.set_rules_hint(True)
self.create_columns(self.treeView)
sw.add(self.treeView)
sw.show_all()
component.get("Preferences").add_page(
_("AutoAdd"), self.glade.get_widget("prefs_box")
component.get('Preferences').add_page(
_('AutoAdd'), self.glade.get_widget('prefs_box')
)
def disable(self):
component.get("Preferences").remove_page(_("AutoAdd"))
component.get("PluginManager").deregister_hook(
"on_apply_prefs", self.on_apply_prefs
component.get('Preferences').remove_page(_('AutoAdd'))
component.get('PluginManager').deregister_hook(
'on_apply_prefs', self.on_apply_prefs
)
component.get("PluginManager").deregister_hook(
"on_show_prefs", self.on_show_prefs
component.get('PluginManager').deregister_hook(
'on_show_prefs', self.on_show_prefs
)
def create_model(self):
store = gtk.ListStore(str, bool, str, str)
for watchdir_id, watchdir in self.watchdirs.iteritems():
for watchdir_id, watchdir in self.watchdirs.items():
store.append([
watchdir_id, watchdir['enabled'],
watchdir.get('owner', 'localclient'), watchdir['path']
])
return store
def create_columns(self, treeView):
rendererToggle = gtk.CellRendererToggle()
def create_columns(self, treeview):
renderer_toggle = gtk.CellRendererToggle()
column = gtk.TreeViewColumn(
_("Active"), rendererToggle, activatable=1, active=1
_('Active'), renderer_toggle, activatable=1, active=1
)
column.set_sort_column_id(1)
treeView.append_column(column)
treeview.append_column(column)
tt = gtk.Tooltip()
tt.set_text(_('Double-click to toggle'))
treeView.set_tooltip_cell(tt, None, None, rendererToggle)
treeview.set_tooltip_cell(tt, None, None, renderer_toggle)
rendererText = gtk.CellRendererText()
column = gtk.TreeViewColumn(_("Owner"), rendererText, text=2)
renderertext = gtk.CellRendererText()
column = gtk.TreeViewColumn(_('Owner'), renderertext, text=2)
column.set_sort_column_id(2)
treeView.append_column(column)
treeview.append_column(column)
tt2 = gtk.Tooltip()
tt2.set_text(_('Double-click to edit'))
treeView.set_has_tooltip(True)
treeview.set_has_tooltip(True)
rendererText = gtk.CellRendererText()
column = gtk.TreeViewColumn(_("Path"), rendererText, text=3)
renderertext = gtk.CellRendererText()
column = gtk.TreeViewColumn(_('Path'), renderertext, text=3)
column.set_sort_column_id(3)
treeView.append_column(column)
treeview.append_column(column)
tt2 = gtk.Tooltip()
tt2.set_text(_('Double-click to edit'))
treeView.set_has_tooltip(True)
treeview.set_has_tooltip(True)
def load_watchdir_list(self):
pass
@ -457,21 +443,21 @@ class GtkUI(GtkPluginBase):
def add_watchdir_entry(self):
pass
def on_add_button_clicked(self, Event=None):
#display options_window
def on_add_button_clicked(self, event=None):
# display options_window
self.opts_dialog.show()
def on_remove_button_clicked(self, Event=None):
def on_remove_button_clicked(self, event=None):
tree, tree_id = self.treeView.get_selection().get_selected()
watchdir_id = str(self.store.get_value(tree_id, 0))
if watchdir_id:
client.autoadd.remove(watchdir_id)
def on_edit_button_clicked(self, Event=None, a=None, col=None):
def on_edit_button_clicked(self, event=None, a=None, col=None):
tree, tree_id = self.treeView.get_selection().get_selected()
watchdir_id = str(self.store.get_value(tree_id, 0))
if watchdir_id:
if col and col.get_title() == _("Active"):
if col and col.get_title() == _('Active'):
if self.watchdirs[watchdir_id]['enabled']:
client.autoadd.disable_watchdir(watchdir_id)
else:
@ -489,8 +475,8 @@ class GtkUI(GtkPluginBase):
self.glade.get_widget('remove_button').set_sensitive(False)
def on_apply_prefs(self):
log.debug("applying prefs for AutoAdd")
for watchdir_id, watchdir in self.watchdirs.iteritems():
log.debug('applying prefs for AutoAdd')
for watchdir_id, watchdir in self.watchdirs.items():
client.autoadd.set_options(watchdir_id, watchdir)
def on_show_prefs(self):
@ -501,10 +487,10 @@ class GtkUI(GtkPluginBase):
def cb_get_config(self, watchdirs):
"""callback for on show_prefs"""
log.trace("Got whatchdirs from core: %s", watchdirs)
log.trace('Got whatchdirs from core: %s', watchdirs)
self.watchdirs = watchdirs or {}
self.store.clear()
for watchdir_id, watchdir in self.watchdirs.iteritems():
for watchdir_id, watchdir in self.watchdirs.items():
self.store.append([
watchdir_id, watchdir['enabled'],
watchdir.get('owner', 'localclient'), watchdir['path']

View File

@ -1,5 +1,4 @@
#
# webui.py
# -*- coding: utf-8 -*-
#
# Copyright (C) 2009 GazpachoKing <chase.sterling@gmail.com>
#
@ -8,47 +7,25 @@
# 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.
# This file is part of Deluge and is licensed under GNU General Public License 3.0, or later, with
# the additional special exception to link portions of this program with the OpenSSL library.
# See LICENSE for more details.
#
from __future__ import unicode_literals
import logging
from deluge.ui.client import client
from deluge import component
from deluge.plugins.pluginbase import WebPluginBase
from common import get_resource
from .common import get_resource
log = logging.getLogger(__name__)
class WebUI(WebPluginBase):
scripts = [get_resource("autoadd.js")]
scripts = [get_resource('autoadd.js')]
def enable(self):
pass

View File

@ -1,5 +1,4 @@
#
# setup.py
# -*- coding: utf-8 -*-
#
# Copyright (C) 2009 GazpachoKing <chase.sterling@gmail.com>
# Copyright (C) 2011 Pedro Algarvio <pedro@algarvio.me>
@ -9,46 +8,22 @@
# 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.
# This file is part of Deluge and is licensed under GNU General Public License 3.0, or later, with
# the additional special exception to link portions of this program with the OpenSSL library.
# See LICENSE for more details.
#
from setuptools import setup, find_packages
from setuptools import find_packages, setup
__plugin_name__ = "AutoAdd"
__author__ = "Chase Sterling, Pedro Algarvio"
__author_email__ = "chase.sterling@gmail.com, pedro@algarvio.me"
__version__ = "1.04"
__url__ = "http://dev.deluge-torrent.org/wiki/Plugins/AutoAdd"
__license__ = "GPLv3"
__description__ = "Monitors folders for .torrent files."
__plugin_name__ = 'AutoAdd'
__author__ = 'Chase Sterling, Pedro Algarvio'
__author_email__ = 'chase.sterling@gmail.com, pedro@algarvio.me'
__version__ = '1.06'
__url__ = 'http://dev.deluge-torrent.org/wiki/Plugins/AutoAdd'
__license__ = 'GPLv3'
__description__ = 'Monitors folders for .torrent files.'
__long_description__ = """"""
__pkg_data__ = {'deluge.plugins.'+__plugin_name__.lower(): ["template/*", "data/*"]}
__pkg_data__ = {'deluge.plugins.' + __plugin_name__.lower(): ['template/*', 'data/*']}
setup(
name=__plugin_name__,
@ -60,8 +35,8 @@ setup(
license=__license__,
long_description=__long_description__ if __long_description__ else __description__,
packages=find_packages(),
namespace_packages = ["deluge", "deluge.plugins"],
package_data = __pkg_data__,
namespace_packages=['deluge', 'deluge.plugins'],
package_data=__pkg_data__,
entry_points="""
[deluge.plugin.core]
@ -70,5 +45,5 @@ setup(
%s = deluge.plugins.%s:GtkUIPlugin
[deluge.plugin.web]
%s = deluge.plugins.%s:WebUIPlugin
""" % ((__plugin_name__, __plugin_name__.lower())*3)
""" % ((__plugin_name__, __plugin_name__.lower()) * 3)
)

View File

@ -1,2 +1,4 @@
from __future__ import unicode_literals
# this is a namespace package
__import__('pkg_resources').declare_namespace(__name__)

View File

@ -1,2 +1,4 @@
from __future__ import unicode_literals
# this is a namespace package
__import__('pkg_resources').declare_namespace(__name__)

View File

@ -1,55 +1,33 @@
#
# blocklist/__init__.py
# -*- coding: utf-8 -*-
#
# Copyright (C) 2007-2009 Andrew Resch <andrewresch@gmail.com>
#
# This file is part of Deluge and is licensed under GNU General Public License 3.0, or later, with
# the additional special exception to link portions of this program with the OpenSSL library.
# See LICENSE for more details.
#
# 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 __future__ import unicode_literals
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
from .core import Core as _pluginCls
self._plugin_cls = _pluginCls
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
from .gtkui import GtkUI as _pluginCls
self._plugin_cls = _pluginCls
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
from .webui import WebUI as _pluginCls
self._plugin_cls = _pluginCls
super(WebUIPlugin, self).__init__(plugin_name)

View File

@ -1,67 +1,44 @@
# -*- coding: utf-8 -*-
#
# 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.
# Basic plugin template created by:
# Copyright (C) 2008 Martijn Voncken <mvoncken@gmail.com>
# 2007-2009 Andrew Resch <andrewresch@gmail.com>
# 2009 Damien Churchill <damoxc@gmail.com>
# 2010 Pedro Algarvio <pedro@algarvio.me>
# 2017 Calum Lind <calumlind+deluge@gmail.com>
#
# This file is part of Deluge and is licensed under GNU General Public License 3.0, or later, with
# the additional special exception to link portions of this program with the OpenSSL library.
# See LICENSE for more details.
#
from __future__ import unicode_literals
import pkg_resources
import os.path
from functools import wraps
from sys import exc_info
def get_resource(filename):
return pkg_resources.resource_filename("deluge.plugins.blocklist",
os.path.join("data", filename))
from pkg_resources import resource_filename
def get_resource(filename):
return resource_filename('deluge.plugins.blocklist', os.path.join('data', filename))
def raises_errors_as(error):
"""Factory class that returns a decorator which wraps the decorated
function to raise all exceptions as the specified error type.
def raisesErrorsAs(error):
"""
Factory class that returns a decorator which wraps
the decorated function to raise all exceptions as
the specified error type
"""
def decorator(func):
"""
Returns a function which wraps the given func
to raise all exceptions as error
"""
"""Returns a function which wraps the given func to raise all exceptions as error."""
@wraps(func)
def wrapper(self, *args, **kwargs):
"""
Wraps the function in a try..except block
and calls it with the specified args
"""Wraps the function in a try..except block and calls it with the specified args.
Raises:
Any exceptions as error preserving the message and traceback.
Raises any exceptions as error preserving the
message and traceback
"""
try:
return func(self, *args, **kwargs)
@ -71,25 +48,28 @@ def raisesErrorsAs(error):
return wrapper
return decorator
def remove_zeros(ip):
"""
Removes unneeded zeros from ip addresses.
"""Removes unneeded zeros from ip addresses.
Example: 000.000.000.003 -> 0.0.0.3
Args:
ip (str): The ip address.
:param ip: the ip address
:type ip: string
Returns:
str: The ip address without the unneeded zeros.
:returns: the ip address without the unneeded zeros
:rtype: string
Example:
000.000.000.003 -> 0.0.0.3
"""
return ".".join([part.lstrip("0").zfill(1) for part in ip.split(".")])
return '.'.join([part.lstrip('0').zfill(1) for part in ip.split('.')])
class BadIP(Exception):
_message = None
def __init__(self, message):
self.message = message
super(BadIP, self).__init__(message)
def __set_message(self, message):
self._message = message
@ -100,8 +80,10 @@ class BadIP(Exception):
message = property(__get_message, __set_message)
del __get_message, __set_message
class IP(object):
__slots__ = ('q1', 'q2', 'q3', 'q4', '_long')
def __init__(self, q1, q2, q3, q4):
self.q1 = q1
self.q2 = q2
@ -124,11 +106,11 @@ class IP(object):
try:
q1, q2, q3, q4 = [int(q) for q in ip.split('.')]
except ValueError:
raise BadIP(_("The IP address \"%s\" is badly formed" % ip))
if q1<0 or q2<0 or q3<0 or q4<0:
raise BadIP(_("The IP address \"%s\" is badly formed" % ip))
elif q1>255 or q2>255 or q3>255 or q4>255:
raise BadIP(_("The IP address \"%s\" is badly formed" % ip))
raise BadIP(_('The IP address "%s" is badly formed' % ip))
if q1 < 0 or q2 < 0 or q3 < 0 or q4 < 0:
raise BadIP(_('The IP address "%s" is badly formed' % ip))
elif q1 > 255 or q2 > 255 or q3 > 255 or q4 > 255:
raise BadIP(_('The IP address "%s" is badly formed' % ip))
return cls(q1, q2, q3, q4)
def quadrants(self):
@ -140,7 +122,7 @@ class IP(object):
# if q3 >= 255:
# if q2 >= 255:
# if q1 >= 255:
# raise BadIP(_("There ain't a next IP address"))
# raise BadIP(_('There is not a next IP address'))
# q1 += 1
# else:
# q2 += 1
@ -156,7 +138,7 @@ class IP(object):
# if q3 <= 1:
# if q2 <= 1:
# if q1 <= 1:
# raise BadIP(_("There ain't a previous IP address"))
# raise BadIP(_('There is not a previous IP address'))
# q1 -= 1
# else:
# q2 -= 1
@ -167,17 +149,17 @@ class IP(object):
# return IP(q1, q2, q3, q4)
def __lt__(self, other):
if isinstance(other, basestring):
if isinstance(other, ''.__class__):
other = IP.parse(other)
return self.long < other.long
def __gt__(self, other):
if isinstance(other, basestring):
if isinstance(other, ''.__class__):
other = IP.parse(other)
return self.long > other.long
def __eq__(self, other):
if isinstance(other, basestring):
if isinstance(other, ''.__class__):
other = IP.parse(other)
return self.long == other.long

View File

@ -1,60 +1,42 @@
#
# core.py
# -*- coding: utf-8 -*-
#
# Copyright (C) 2008 Andrew Resch <andrewresch@gmail.com>
# Copyright (C) 2009-2010 John Garland <johnnybg+deluge@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.
#
# This file is part of Deluge and is licensed under GNU General Public License 3.0, or later, with
# the additional special exception to link portions of this program with the OpenSSL library.
# See LICENSE for more details.
#
import os
import time
from __future__ import division, unicode_literals
import logging
import os
import shutil
import time
from datetime import datetime, timedelta
from email.utils import formatdate
from urlparse import urljoin
import shutil
from twisted.internet import defer, threads
from twisted.internet.task import LoopingCall
from twisted.internet import threads, defer
from twisted.web import error
from deluge.plugins.pluginbase import CorePluginBase
import deluge.component as component
import deluge.configmanager
from deluge.common import is_url
from deluge.core.rpcserver import export
from deluge.httpdownloader import download_file
from common import IP, BadIP
from detect import detect_compression, detect_format, create_reader, UnknownFormatError
from readers import ReaderParseError
from deluge.plugins.pluginbase import CorePluginBase
from .common import IP, BadIP
from .detect import UnknownFormatError, create_reader, detect_compression, detect_format
from .readers import ReaderParseError
try:
from urllib.parse import urljoin
except ImportError:
# PY2 fallback
from urlparse import urljoin # pylint: disable=ungrouped-imports
# TODO: check return values for deferred callbacks
# TODO: review class attributes for redundancy
@ -62,22 +44,23 @@ from readers import ReaderParseError
log = logging.getLogger(__name__)
DEFAULT_PREFS = {
"url": "",
"load_on_start": False,
"check_after_days": 4,
"list_compression": "",
"list_type": "",
"last_update": 0.0,
"list_size": 0,
"timeout": 180,
"try_times": 3,
"whitelisted": [],
'url': '',
'load_on_start': False,
'check_after_days': 4,
'list_compression': '',
'list_type': '',
'last_update': 0.0,
'list_size': 0,
'timeout': 180,
'try_times': 3,
'whitelisted': [],
}
# Constants
ALLOW_RANGE = 0
BLOCK_RANGE = 1
class Core(CorePluginBase):
def enable(self):
log.debug('Blocklist: Plugin enabled...')
@ -92,26 +75,26 @@ class Core(CorePluginBase):
self.num_blocked = 0
self.file_progress = 0.0
self.core = component.get("Core")
self.config = deluge.configmanager.ConfigManager("blocklist.conf", DEFAULT_PREFS)
if "whitelisted" not in self.config:
self.config["whitelisted"] = []
self.core = component.get('Core')
self.config = deluge.configmanager.ConfigManager('blocklist.conf', DEFAULT_PREFS)
if 'whitelisted' not in self.config:
self.config['whitelisted'] = []
self.reader = create_reader(self.config["list_type"], self.config["list_compression"])
self.reader = create_reader(self.config['list_type'], self.config['list_compression'])
if type(self.config["last_update"]) is not float:
self.config.config["last_update"] = 0.0
if not isinstance(self.config['last_update'], float):
self.config.config['last_update'] = 0.0
update_now = False
if self.config["load_on_start"]:
if self.config['load_on_start']:
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"])
if not self.config["last_update"] or last_update + check_period < datetime.now():
if self.config['last_update']:
last_update = datetime.fromtimestamp(self.config['last_update'])
check_period = timedelta(days=self.config['check_after_days'])
if not self.config['last_update'] or last_update + check_period < datetime.now():
update_now = True
else:
d = self.import_list(deluge.configmanager.get_config_dir("blocklist.cache"))
d = self.import_list(deluge.configmanager.get_config_dir('blocklist.cache'))
d.addCallbacks(self.on_import_complete, self.on_import_error)
if self.need_to_resume_session:
d.addBoth(self.resume_session)
@ -119,34 +102,36 @@ class Core(CorePluginBase):
# This function is called every 'check_after_days' days, to download
# and import a new list if needed.
self.update_timer = LoopingCall(self.check_import)
if self.config["check_after_days"] > 0:
if self.config['check_after_days'] > 0:
self.update_timer.start(
self.config["check_after_days"] * 24 * 60 * 60, update_now
self.config['check_after_days'] * 24 * 60 * 60, update_now
)
def disable(self):
self.config.save()
log.debug("Reset IP filter")
log.debug('Reset IP filter')
self.core.session.get_ip_filter().add_rule(
"0.0.0.0", "255.255.255.255", ALLOW_RANGE
'0.0.0.0', '255.255.255.255', ALLOW_RANGE
)
log.debug('Blocklist: Plugin disabled')
def update(self):
pass
## Exported RPC methods ###
# Exported RPC methods #
@export
def check_import(self, force=False):
"""
Imports latest blocklist specified by blocklist url
Only downloads/imports if necessary or forced
"""Imports latest blocklist specified by blocklist url.
Args:
force (bool, optional): Force the download/import, default is False.
Returns:
Deferred: A Deferred which fires when the blocklist has been imported.
:param force: optional argument to force download/import
:type force: boolean
:returns: a Deferred which fires when the blocklist has been imported
:rtype: Deferred
"""
if not self.config['url']:
return
# Reset variables
self.filename = None
@ -156,7 +141,7 @@ class Core(CorePluginBase):
self.up_to_date = False
if force:
self.reader = None
self.is_url = is_url(self.config["url"])
self.is_url = is_url(self.config['url'])
# Start callback chain
if self.is_url:
@ -164,7 +149,7 @@ class Core(CorePluginBase):
d.addCallbacks(self.on_download_complete, self.on_download_error)
d.addCallback(self.import_list)
else:
d = self.import_list(self.config["url"])
d = self.import_list(self.config['url'])
d.addCallbacks(self.on_import_complete, self.on_import_error)
if self.need_to_resume_session:
d.addBoth(self.resume_session)
@ -173,30 +158,30 @@ class Core(CorePluginBase):
@export
def get_config(self):
"""
Returns the config dictionary
"""Gets the blocklist config dictionary.
Returns:
dict: The config dictionary.
:returns: the config dictionary
:rtype: dict
"""
return self.config.config
@export
def set_config(self, config):
"""
Sets the config based on values in 'config'
"""Sets the blocklist config.
Args:
config (dict): config to set.
:param config: config to set
:type config: dictionary
"""
needs_blocklist_import = False
for key in config.keys():
for key in config:
if key == 'whitelisted':
saved = set(self.config[key])
update = set(config[key])
diff = saved.symmetric_difference(update)
if diff:
log.debug("Whitelist changed. Updating...")
log.debug('Whitelist changed. Updating...')
added = update.intersection(diff)
removed = saved.intersection(diff)
if added:
@ -207,10 +192,10 @@ class Core(CorePluginBase):
ip.address, ip.address, ALLOW_RANGE
)
saved.add(ip.address)
log.debug("Added %s to whitelisted", ip)
log.debug('Added %s to whitelisted', ip)
self.num_whited += 1
except BadIP, e:
log.error("Bad IP: %s", e)
except BadIP as ex:
log.error('Bad IP: %s', ex)
continue
if removed:
needs_blocklist_import = True
@ -218,96 +203,99 @@ class Core(CorePluginBase):
try:
ip = IP.parse(ip)
saved.remove(ip.address)
log.debug("Removed %s from whitelisted", ip)
except BadIP, e:
log.error("Bad IP: %s", e)
log.debug('Removed %s from whitelisted', ip)
except BadIP as ex:
log.error('Bad IP: %s', ex)
continue
self.config[key] = list(saved)
continue
elif key == "check_after_days":
elif key == 'check_after_days':
if self.config[key] != config[key]:
self.config[key] = config[key]
update_now = False
if self.config["last_update"]:
last_update = datetime.fromtimestamp(self.config["last_update"])
check_period = timedelta(days=self.config["check_after_days"])
if not self.config["last_update"] or last_update + check_period < datetime.now():
if self.config['last_update']:
last_update = datetime.fromtimestamp(self.config['last_update'])
check_period = timedelta(days=self.config['check_after_days'])
if not self.config['last_update'] or last_update + check_period < datetime.now():
update_now = True
self.update_timer.running and self.update_timer.stop()
if self.config["check_after_days"] > 0:
if self.update_timer.running:
self.update_timer.stop()
if self.config['check_after_days'] > 0:
self.update_timer.start(
self.config["check_after_days"] * 24 * 60 * 60, update_now
self.config['check_after_days'] * 24 * 60 * 60, update_now
)
continue
self.config[key] = config[key]
if needs_blocklist_import:
log.debug("IP addresses were removed from the whitelist. Since we "
"don't know if they were blocked before. Re-import "
"current blocklist and re-add whitelisted.")
log.debug('IP addresses were removed from the whitelist. Since we '
'do not know if they were blocked before. Re-import '
'current blocklist and re-add whitelisted.')
self.has_imported = False
d = self.import_list(deluge.configmanager.get_config_dir("blocklist.cache"))
d = self.import_list(deluge.configmanager.get_config_dir('blocklist.cache'))
d.addCallbacks(self.on_import_complete, self.on_import_error)
@export
def get_status(self):
"""
Returns the status of the plugin
"""Get the status of the plugin.
Returns:
dict: The status dict of the plugin.
:returns: the status dict of the plugin
:rtype: dict
"""
status = {}
if self.is_downloading:
status["state"] = "Downloading"
status['state'] = 'Downloading'
elif self.is_importing:
status["state"] = "Importing"
status['state'] = 'Importing'
else:
status["state"] = "Idle"
status['state'] = 'Idle'
status["up_to_date"] = self.up_to_date
status["num_whited"] = self.num_whited
status["num_blocked"] = self.num_blocked
status["file_progress"] = self.file_progress
status["file_url"] = self.config["url"]
status["file_size"] = self.config["list_size"]
status["file_date"] = self.config["last_update"]
status["file_type"] = self.config["list_type"]
status["whitelisted"] = self.config["whitelisted"]
if self.config["list_compression"]:
status["file_type"] += " (%s)" % self.config["list_compression"]
status['up_to_date'] = self.up_to_date
status['num_whited'] = self.num_whited
status['num_blocked'] = self.num_blocked
status['file_progress'] = self.file_progress
status['file_url'] = self.config['url']
status['file_size'] = self.config['list_size']
status['file_date'] = self.config['last_update']
status['file_type'] = self.config['list_type']
status['whitelisted'] = self.config['whitelisted']
if self.config['list_compression']:
status['file_type'] += ' (%s)' % self.config['list_compression']
return status
####
def update_info(self, blocklist):
"""
Updates blocklist info
"""Updates blocklist info.
Args:
blocklist (str): Path of blocklist.
Returns:
str: Path of blocklist.
:param blocklist: path of blocklist
:type blocklist: string
:returns: path of blocklist
:rtype: string
"""
log.debug("Updating blocklist info: %s", blocklist)
self.config["last_update"] = time.time()
self.config["list_size"] = os.path.getsize(blocklist)
log.debug('Updating blocklist info: %s', blocklist)
self.config['last_update'] = time.time()
self.config['list_size'] = os.path.getsize(blocklist)
self.filename = blocklist
return blocklist
def download_list(self, url=None):
"""
Downloads the blocklist specified by 'url' in the config
"""Downloads the blocklist specified by 'url' in the config.
Args:
url (str, optional): url to download from, defaults to config value.
Returns:
Deferred: a Deferred which fires once the blocklist has been downloaded.
:param url: optional url to download from, defaults to config value
:type url: string
:returns: a Deferred which fires once the blocklist has been downloaded
:rtype: Deferred
"""
def on_retrieve_data(data, current_length, total_length):
if total_length:
fp = float(current_length) / total_length
fp = current_length / total_length
if fp > 1.0:
fp = 1.0
else:
@ -316,85 +304,89 @@ class Core(CorePluginBase):
self.file_progress = fp
import socket
socket.setdefaulttimeout(self.config["timeout"])
socket.setdefaulttimeout(self.config['timeout'])
if not url:
url = self.config["url"]
url = self.config['url']
headers = {}
if self.config["last_update"] and not self.force_download:
headers['If-Modified-Since'] = formatdate(self.config["last_update"], usegmt=True)
if self.config['last_update'] and not self.force_download:
headers['If-Modified-Since'] = formatdate(self.config['last_update'], usegmt=True)
log.debug("Attempting to download blocklist %s", url)
log.debug("Sending headers: %s", headers)
log.debug('Attempting to download blocklist %s', url)
log.debug('Sending headers: %s', headers)
self.is_downloading = True
return download_file(
url, deluge.configmanager.get_config_dir("blocklist.download"),
url, deluge.configmanager.get_config_dir('blocklist.download'),
on_retrieve_data, headers
)
def on_download_complete(self, blocklist):
"""
Runs any download clean up functions
"""Runs any download clean up functions.
Args:
blocklist (str): Path of blocklist.
Returns:
Deferred: a Deferred which fires when clean up is done.
:param blocklist: path of blocklist
:type blocklist: string
:returns: a Deferred which fires when clean up is done
:rtype: Deferred
"""
log.debug("Blocklist download complete: %s", blocklist)
log.debug('Blocklist download complete: %s', blocklist)
self.is_downloading = False
return threads.deferToThread(self.update_info, blocklist)
def on_download_error(self, f):
"""
Recovers from download error
"""Recovers from download error.
Args:
f (Failure): Failure that occurred.
Returns:
Deferred or Failure: A Deferred if recovery was possible else original Failure.
:param f: failure that occured
:type f: Failure
:returns: a Deferred if recovery was possible
else the original failure
:rtype: Deferred or Failure
"""
self.is_downloading = False
error_msg = f.getErrorMessage()
d = f
if f.check(error.PageRedirect):
# Handle redirect errors
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
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
d = self.download_list(location)
d.addCallbacks(self.on_download_complete, self.on_download_error)
else:
if "Not Modified" in error_msg:
log.debug("Blocklist is up-to-date!")
if 'Not Modified' in error_msg:
log.debug('Blocklist is up-to-date!')
self.up_to_date = True
blocklist = deluge.configmanager.get_config_dir("blocklist.cache")
blocklist = deluge.configmanager.get_config_dir('blocklist.cache')
d = threads.deferToThread(self.update_info, blocklist)
else:
log.warning("Blocklist download failed: %s", error_msg)
if self.failed_attempts < self.config["try_times"]:
log.debug("Let's try again")
log.warning('Blocklist download failed: %s', error_msg)
if self.failed_attempts < self.config['try_times']:
log.debug('Try downloading blocklist again... (%s/%s)',
self.failed_attempts, self.config['try_times'])
self.failed_attempts += 1
d = self.download_list()
d.addCallbacks(self.on_download_complete, self.on_download_error)
return d
def import_list(self, blocklist):
"""
Imports the downloaded blocklist into the session
"""Imports the downloaded blocklist into the session.
Args:
blocklist (str): path of blocklist.
Returns:
Deferred: A Deferred that fires when the blocklist has been imported.
:param blocklist: path of blocklist
:type blocklist: string
:returns: a Deferred that fires when the blocklist has been imported
:rtype: Deferred
"""
log.trace("on import_list")
log.trace('on import_list')
def on_read_ip_range(start, end):
"""Add ip range to blocklist"""
# log.trace("Adding ip range %s - %s to ipfilter as blocked", start, end)
# log.trace('Adding ip range %s - %s to ipfilter as blocked', start, end)
self.blocklist.add_rule(start.address, end.address, BLOCK_RANGE)
self.num_blocked += 1
@ -402,19 +394,19 @@ class Core(CorePluginBase):
"""Add any whitelisted IP's and add the blocklist to session"""
# White listing happens last because the last rules added have
# priority
log.info("Added %d ranges to ipfilter as blocked", self.num_blocked)
for ip in self.config["whitelisted"]:
log.info('Added %d ranges to ipfilter as blocked', self.num_blocked)
for ip in self.config['whitelisted']:
ip = IP.parse(ip)
self.blocklist.add_rule(ip.address, ip.address, ALLOW_RANGE)
self.num_whited += 1
log.trace("Added %s to the ipfiler as white-listed", ip.address)
log.info("Added %d ranges to ipfilter as white-listed", self.num_whited)
log.trace('Added %s to the ipfiler as white-listed', ip.address)
log.info('Added %d ranges to ipfilter as white-listed', self.num_whited)
self.core.session.set_ip_filter(self.blocklist)
return result
# TODO: double check logic
if self.up_to_date and self.has_imported:
log.debug("Latest blocklist is already imported")
log.debug('Latest blocklist is already imported')
return defer.succeed(blocklist)
self.is_importing = True
@ -430,67 +422,68 @@ class Core(CorePluginBase):
self.auto_detected = True
def on_reader_failure(failure):
log.error("Failed to read!!!!!!")
log.error('Failed to read!!!!!!')
log.exception(failure)
log.debug("Importing using reader: %s", self.reader)
log.debug("Reader type: %s compression: %s", self.config["list_type"], self.config["list_compression"])
log.debug("Clearing current ip filtering")
# self.blocklist.add_rule("0.0.0.0", "255.255.255.255", ALLOW_RANGE)
log.debug('Importing using reader: %s', self.reader)
log.debug('Reader type: %s compression: %s', self.config['list_type'], self.config['list_compression'])
log.debug('Clearing current ip filtering')
# self.blocklist.add_rule('0.0.0.0', '255.255.255.255', ALLOW_RANGE)
d = threads.deferToThread(self.reader(blocklist).read, on_read_ip_range)
d.addCallback(on_finish_read).addErrback(on_reader_failure)
return d
def on_import_complete(self, blocklist):
"""
Runs any import clean up functions
"""Runs any import clean up functions.
Args:
blocklist (str): Path of blocklist.
Returns:
Deferred: A Deferred that fires when clean up is done.
:param blocklist: path of blocklist
:type blocklist: string
:returns: a Deferred that fires when clean up is done
:rtype: Deferred
"""
log.trace("on_import_list_complete")
log.trace('on_import_list_complete')
d = blocklist
self.is_importing = False
self.has_imported = True
log.debug("Blocklist import complete!")
cache = deluge.configmanager.get_config_dir("blocklist.cache")
log.debug('Blocklist import complete!')
cache = deluge.configmanager.get_config_dir('blocklist.cache')
if blocklist != cache:
if self.is_url:
log.debug("Moving %s to %s", blocklist, cache)
log.debug('Moving %s to %s', blocklist, cache)
d = threads.deferToThread(shutil.move, blocklist, cache)
else:
log.debug("Copying %s to %s", blocklist, cache)
log.debug('Copying %s to %s', blocklist, cache)
d = threads.deferToThread(shutil.copy, blocklist, cache)
return d
def on_import_error(self, f):
"""
Recovers from import error
"""Recovers from import error.
Args:
f (Failure): Failure that occurred.
Returns:
Deferred or Failure: A Deferred if recovery was possible else original Failure.
:param f: failure that occured
:type f: Failure
:returns: a Deferred if recovery was possible
else the original failure
:rtype: Deferred or Failure
"""
log.trace("on_import_error: %s", f)
log.trace('on_import_error: %s', f)
d = f
self.is_importing = False
try_again = False
cache = deluge.configmanager.get_config_dir("blocklist.cache")
cache = deluge.configmanager.get_config_dir('blocklist.cache')
if f.check(ReaderParseError) and not self.auto_detected:
# Invalid / corrupt list, let's detect it
log.warning("Invalid / corrupt blocklist")
log.warning('Invalid / corrupt blocklist')
self.reader = None
blocklist = None
try_again = True
elif self.filename != cache and os.path.exists(cache):
# If we have a backup and we haven't already used it
log.warning("Error reading blocklist: %s", f.getErrorMessage())
log.warning('Error reading blocklist: %s', f.getErrorMessage())
blocklist = cache
try_again = True
@ -501,31 +494,29 @@ class Core(CorePluginBase):
return d
def auto_detect(self, blocklist):
"""
Tries to auto-detect the blocklist type
"""Attempts to auto-detect the blocklist type.
Args:
blocklist (str): Path of blocklist.
Raises:
UnknownFormatError: If the format cannot be detected.
:param blocklist: path of blocklist to auto-detect
:type blocklist: string
:raises UnknownFormatError: if the format cannot be detected
"""
self.config["list_compression"] = detect_compression(blocklist)
self.config["list_type"] = detect_format(blocklist, self.config["list_compression"])
log.debug("Auto-detected type: %s compression: %s", self.config["list_type"], self.config["list_compression"])
if not self.config["list_type"]:
self.config["list_compression"] = ""
self.config['list_compression'] = detect_compression(blocklist)
self.config['list_type'] = detect_format(blocklist, self.config['list_compression'])
log.debug('Auto-detected type: %s compression: %s', self.config['list_type'], self.config['list_compression'])
if not self.config['list_type']:
self.config['list_compression'] = ''
raise UnknownFormatError
else:
self.reader = create_reader(self.config["list_type"], self.config["list_compression"])
self.reader = create_reader(self.config['list_type'], self.config['list_compression'])
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
self.need_to_resume_session = not self.core.session.is_paused()
self.core.pause_session()
def resume_session(self, result):
self.core.session.resume()
self.core.resume_session()
self.need_to_resume_session = False
return result

View File

@ -0,0 +1,408 @@
/*!
* blocklist.js
*
* Copyright (C) Omar Alvarez 2014 <omar.alvarez@udc.es>
*
* This file is part of Deluge and is licensed under GNU General Public License 3.0, or later, with
* the additional special exception to link portions of this program with the OpenSSL library.
* See LICENSE for more details.
*
*/
Ext.ns('Deluge.ux.preferences');
/**
* @class Deluge.ux.preferences.BlocklistPage
* @extends Ext.Panel
*/
Deluge.ux.preferences.BlocklistPage = Ext.extend(Ext.Panel, {
title: _('Blocklist'),
header: false,
layout: 'fit',
border: false,
autoScroll: true,
initComponent: function() {
Deluge.ux.preferences.BlocklistPage.superclass.initComponent.call(this);
this.URLFset = this.add({
xtype: 'fieldset',
border: false,
title: _('General'),
autoHeight: true,
defaultType: 'textfield',
style: 'margin-top: 3px; margin-bottom: 0px; padding-bottom: 0px;',
autoWidth: true,
labelWidth: 40
});
this.URL = this.URLFset.add({
fieldLabel: _('URL:'),
labelSeparator: '',
name: 'url',
width: '80%'
});
this.SettingsFset = this.add({
xtype: 'fieldset',
border: false,
title: _('Settings'),
autoHeight: true,
defaultType: 'spinnerfield',
style: 'margin-top: 3px; margin-bottom: 0px; padding-bottom: 0px;',
autoWidth: true,
labelWidth: 160
});
this.checkListDays = this.SettingsFset.add({
fieldLabel: _('Check for new list every:'),
labelSeparator: '',
name: 'check_list_days',
value: 4,
decimalPrecision: 0,
width: 80
});
this.chkImportOnStart = this.SettingsFset.add({
xtype: 'checkbox',
fieldLabel: _('Import blocklist on startup'),
name: 'check_import_startup'
});
this.OptionsFset = this.add({
xtype: 'fieldset',
border: false,
title: _('Options'),
autoHeight: true,
defaultType: 'button',
style: 'margin-top: 3px; margin-bottom: 0px; padding-bottom: 0px;',
autoWidth: false,
width: '80%',
labelWidth: 0
});
this.checkDownload = this.OptionsFset.add({
fieldLabel: _(''),
name: 'check_download',
xtype: 'container',
layout: 'hbox',
margins: '4 0 0 5',
items: [{
xtype: 'button',
text: ' Check Download and Import ',
scale: 'medium'
}, {
xtype: 'box',
autoEl: {
tag: 'img',
src: '../icons/ok.png'
},
margins: '4 0 0 3'
}]
});
this.forceDownload = this.OptionsFset.add({
fieldLabel: _(''),
name: 'force_download',
text: ' Force Download and Import ',
margins: '2 0 0 0',
//icon: '../icons/blocklist_import24.png',
scale: 'medium'
});
this.ProgressFset = this.add({
xtype: 'fieldset',
border: false,
title: _('Info'),
autoHeight: true,
defaultType: 'progress',
style: 'margin-top: 1px; margin-bottom: 0px; padding-bottom: 0px;',
autoWidth: true,
labelWidth: 0,
hidden: true
});
this.downProgBar = this.ProgressFset.add({
fieldLabel: _(''),
name: 'progress_bar',
width: '90%'
});
this.InfoFset = this.add({
xtype: 'fieldset',
border: false,
title: _('Info'),
autoHeight: true,
defaultType: 'label',
style: 'margin-top: 0px; margin-bottom: 0px; padding-bottom: 0px;',
labelWidth: 60
});
this.lblFileSize = this.InfoFset.add({
fieldLabel: _('File Size:'),
labelSeparator: '',
name: 'file_size'
});
this.lblDate = this.InfoFset.add({
fieldLabel: _('Date:'),
labelSeparator: '',
name: 'date'
});
this.lblType = this.InfoFset.add({
fieldLabel: _('Type:'),
labelSeparator: '',
name: 'type'
});
this.lblURL = this.InfoFset.add({
fieldLabel: _('URL:'),
labelSeparator: '',
name: 'lbl_URL'
});
this.WhitelistFset = this.add({
xtype: 'fieldset',
border: false,
title: _('Whitelist'),
autoHeight: true,
defaultType: 'editorgrid',
style: 'margin-top: 3px; margin-bottom: 0px; padding-bottom: 0px;',
autoWidth: true,
labelWidth: 0,
items: [{
fieldLabel: _(''),
name: 'whitelist',
margins: '2 0 5 5',
height: 100,
width: 260,
autoExpandColumn: 'ip',
viewConfig: {
emptyText: _('Add an IP...'),
deferEmptyText: false
},
colModel: new Ext.grid.ColumnModel({
columns: [{
id: 'ip',
header: _('IP'),
dataIndex: 'ip',
sortable: true,
hideable: false,
editable: true,
editor: {
xtype: 'textfield'
}
}]
}),
selModel: new Ext.grid.RowSelectionModel({
singleSelect: false,
moveEditorOnEnter: false
}),
store: new Ext.data.ArrayStore({
autoDestroy: true,
fields: [{name: 'ip'}]
}),
listeners: {
afteredit: function(e) {
e.record.commit();
}
},
setEmptyText: function(text) {
if (this.viewReady) {
this.getView().emptyText = text;
this.getView().refresh();
} else {
Ext.apply(this.viewConfig, {emptyText: text});
}
},
loadData: function(data) {
this.getStore().loadData(data);
if (this.viewReady) {
this.getView().updateHeaders();
}
}
}]
});
this.ipButtonsContainer = this.WhitelistFset.add({
xtype: 'container',
layout: 'hbox',
margins: '4 0 0 5',
items: [{
xtype: 'button',
text: ' Add IP ',
margins: '0 5 0 0'
},{
xtype: 'button',
text: ' Delete IP '
}]
});
this.updateTask = Ext.TaskMgr.start({
interval: 2000,
run: this.onUpdate,
scope: this
});
this.on('show', this.updateConfig, this);
this.ipButtonsContainer.getComponent(0).setHandler(this.addIP, this);
this.ipButtonsContainer.getComponent(1).setHandler(this.deleteIP, this);
this.checkDownload.getComponent(0).setHandler(this.checkDown, this);
this.forceDownload.setHandler(this.forceDown, this);
},
onApply: function() {
var config = {};
config['url'] = this.URL.getValue();
config['check_after_days'] = this.checkListDays.getValue();
config['load_on_start'] = this.chkImportOnStart.getValue();
var ipList = [];
var store = this.WhitelistFset.getComponent(0).getStore();
for (var i = 0; i < store.getCount(); i++) {
var record = store.getAt(i);
var ip = record.get('ip');
ipList.push(ip);
}
config['whitelisted'] = ipList;
deluge.client.blocklist.set_config(config);
},
onOk: function() {
this.onApply();
},
onUpdate: function() {
deluge.client.blocklist.get_status({
success: function(status) {
if (status['state'] == 'Downloading') {
this.InfoFset.hide();
this.checkDownload.getComponent(0).setDisabled(true);
this.checkDownload.getComponent(1).hide();
this.forceDownload.setDisabled(true);
this.ProgressFset.show();
this.downProgBar.updateProgress(status['file_progress'],'Downloading '.concat((status['file_progress'] * 100).toFixed(2)).concat('%'),true);
} else if (status['state'] == 'Importing') {
this.InfoFset.hide();
this.checkDownload.getComponent(0).setDisabled(true);
this.checkDownload.getComponent(1).hide();
this.forceDownload.setDisabled(true);
this.ProgressFset.show();
this.downProgBar.updateText('Importing '.concat(status['num_blocked']));
} else if (status['state'] == 'Idle') {
this.ProgressFset.hide();
this.checkDownload.getComponent(0).setDisabled(false);
this.forceDownload.setDisabled(false);
if (status['up_to_date']) {
this.checkDownload.getComponent(1).show();
this.checkDownload.doLayout();
} else {
this.checkDownload.getComponent(1).hide();
}
this.InfoFset.show();
this.lblFileSize.setText(fsize(status['file_size']));
this.lblDate.setText(fdate(status['file_date']));
this.lblType.setText(status['file_type']);
this.lblURL.setText(status['file_url'].substr(0,40).concat('...'));
}
},
scope: this
});
},
checkDown: function() {
this.onApply();
deluge.client.blocklist.check_import();
},
forceDown: function() {
this.onApply();
deluge.client.blocklist.check_import(force = true);
},
updateConfig: function() {
deluge.client.blocklist.get_config({
success: function(config) {
this.URL.setValue(config['url']);
this.checkListDays.setValue(config['check_after_days']);
this.chkImportOnStart.setValue(config['load_on_start']);
var data = [];
var keys = Ext.keys(config['whitelisted']);
for (var i = 0; i < keys.length; i++) {
var key = keys[i];
data.push([config['whitelisted'][key]]);
}
this.WhitelistFset.getComponent(0).loadData(data);
},
scope: this
});
deluge.client.blocklist.get_status({
success: function(status) {
this.lblFileSize.setText(fsize(status['file_size']));
this.lblDate.setText(fdate(status['file_date']));
this.lblType.setText(status['file_type']);
this.lblURL.setText(status['file_url'].substr(0,40).concat('...'));
},
scope: this
});
},
addIP: function() {
var store = this.WhitelistFset.getComponent(0).getStore();
var IP = store.recordType;
var i = new IP({
ip: ''
});
this.WhitelistFset.getComponent(0).stopEditing();
store.insert(0, i);
this.WhitelistFset.getComponent(0).startEditing(0, 0);
},
deleteIP: function() {
var selections = this.WhitelistFset.getComponent(0).getSelectionModel().getSelections();
var store = this.WhitelistFset.getComponent(0).getStore();
this.WhitelistFset.getComponent(0).stopEditing();
for (var i = 0; i < selections.length; i++)
store.remove(selections[i]);
store.commitChanges();
},
onDestroy: function() {
Ext.TaskMgr.stop(this.updateTask);
deluge.preferences.un('show', this.updateConfig, this);
Deluge.ux.preferences.BlocklistPage.superclass.onDestroy.call(this);
}
});
Deluge.plugins.BlocklistPlugin = Ext.extend(Deluge.Plugin, {
name: 'Blocklist',
onDisable: function() {
deluge.preferences.removePage(this.prefsPage);
},
onEnable: function() {
this.prefsPage = deluge.preferences.addPage(new Deluge.ux.preferences.BlocklistPage());
}
});
Deluge.registerPlugin('Blocklist', Deluge.plugins.BlocklistPlugin);

View File

@ -1,62 +1,39 @@
#
# decompressers.py
# -*- coding: utf-8 -*-
#
# Copyright (C) 2009-2010 John Garland <johnnybg+deluge@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.
#
# This file is part of Deluge and is licensed under GNU General Public License 3.0, or later, with
# the additional special exception to link portions of this program with the OpenSSL library.
# See LICENSE for more details.
#
# pylint: disable=redefined-builtin
import gzip, zipfile, bz2
from __future__ import unicode_literals
def Zipped(reader):
import bz2
import gzip
import zipfile
def Zipped(reader): # NOQA: N802
"""Blocklist reader for zipped blocklists"""
def open(self):
z = zipfile.ZipFile(self.file)
if hasattr(z, 'open'):
f = z.open(z.namelist()[0])
else:
# Handle python 2.5
import cStringIO
f = cStringIO.StringIO(z.read(z.namelist()[0]))
f = z.open(z.namelist()[0])
return f
reader.open = open
return reader
def GZipped(reader):
def GZipped(reader): # NOQA: N802
"""Blocklist reader for gzipped blocklists"""
def open(self):
return gzip.open(self.file)
reader.open = open
return reader
def BZipped2(reader):
def BZipped2(reader): # NOQA: N802
"""Blocklist reader for bzipped2 blocklists"""
def open(self):
return bz2.BZ2File(self.file)

View File

@ -1,78 +1,57 @@
#
# detect.py
# -*- coding: utf-8 -*-
#
# Copyright (C) 2009-2010 John Garland <johnnybg+deluge@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.
#
# This file is part of Deluge and is licensed under GNU General Public License 3.0, or later, with
# the additional special exception to link portions of this program with the OpenSSL library.
# See LICENSE for more details.
#
from decompressers import Zipped, GZipped, BZipped2
from readers import EmuleReader, SafePeerReader, PeerGuardianReader
from __future__ import unicode_literals
from .decompressers import BZipped2, GZipped, Zipped
from .readers import EmuleReader, PeerGuardianReader, SafePeerReader
COMPRESSION_TYPES = {
"PK" : "Zip",
"\x1f\x8b" : "GZip",
"BZ" : "BZip2"
'PK': 'Zip',
'\x1f\x8b': 'GZip',
'BZ': 'BZip2'
}
DECOMPRESSERS = {
"Zip" : Zipped,
"GZip" : GZipped,
"BZip2" : BZipped2
'Zip': Zipped,
'GZip': GZipped,
'BZip2': BZipped2
}
READERS = {
"Emule" : EmuleReader,
"SafePeer" : SafePeerReader,
"PeerGuardian" : PeerGuardianReader
'Emule': EmuleReader,
'SafePeer': SafePeerReader,
'PeerGuardian': PeerGuardianReader
}
class UnknownFormatError(Exception):
pass
def detect_compression(filename):
f = open(filename, "rb")
magic_number = f.read(2)
f.close()
return COMPRESSION_TYPES.get(magic_number, "")
def detect_format(filename, compression=""):
format = ""
def detect_compression(filename):
with open(filename, 'rb') as _file:
magic_number = _file.read(2)
return COMPRESSION_TYPES.get(magic_number, '')
def detect_format(filename, compression=''):
file_format = ''
for reader in READERS:
if create_reader(reader, compression)(filename).is_valid():
format = reader
file_format = reader
break
return format
return file_format
def create_reader(format, compression=""):
reader = READERS.get(format)
def create_reader(file_format, compression=''):
reader = READERS.get(file_format)
if reader and compression:
decompressor = DECOMPRESSERS.get(compression)
if decompressor:

View File

@ -1,153 +1,128 @@
#
# gtkui.py
# -*- coding: utf-8 -*-
#
# 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.
#
# This file is part of Deluge and is licensed under GNU General Public License 3.0, or later, with
# the additional special exception to link portions of this program with the OpenSSL library.
# See LICENSE for more details.
#
from __future__ import unicode_literals
import logging
from datetime import datetime
import gtk
import gtk.glade
from deluge.ui.client import client
import deluge.component as component
import deluge.common
import deluge.component as component
from deluge.plugins.pluginbase import GtkPluginBase
import common
from deluge.ui.client import client
from . import common
log = logging.getLogger(__name__)
class GtkUI(GtkPluginBase):
def enable(self):
log.debug("Blocklist GtkUI enable..")
self.plugin = component.get("PluginManager")
log.debug('Blocklist GtkUI enable..')
self.plugin = component.get('PluginManager')
try:
self.load_preferences_page()
except Exception, err:
log.exception(err)
raise
self.load_preferences_page()
self.status_item = component.get("StatusBar").add_item(
image=common.get_resource("blocklist16.png"),
text="",
self.status_item = component.get('StatusBar').add_item(
image=common.get_resource('blocklist16.png'),
text='',
callback=self._on_status_item_clicked,
tooltip=_("Blocked IP Ranges /Whitelisted IP Ranges")
tooltip=_('Blocked IP Ranges /Whitelisted IP Ranges')
)
# Register some hooks
self.plugin.register_hook("on_apply_prefs", self._on_apply_prefs)
self.plugin.register_hook("on_show_prefs", self._on_show_prefs)
self.plugin.register_hook('on_apply_prefs', self._on_apply_prefs)
self.plugin.register_hook('on_show_prefs', self._on_show_prefs)
def disable(self):
log.debug("Blocklist GtkUI disable..")
log.debug('Blocklist GtkUI disable..')
# Remove the preferences page
self.plugin.remove_preferences_page(_("Blocklist"))
self.plugin.remove_preferences_page(_('Blocklist'))
# Remove status item
component.get("StatusBar").remove_item(self.status_item)
component.get('StatusBar').remove_item(self.status_item)
del self.status_item
# Deregister the hooks
self.plugin.deregister_hook("on_apply_prefs", self._on_apply_prefs)
self.plugin.deregister_hook("on_show_prefs", self._on_show_prefs)
self.plugin.deregister_hook('on_apply_prefs', self._on_apply_prefs)
self.plugin.deregister_hook('on_show_prefs', self._on_show_prefs)
del self.glade
def update(self):
def _on_get_status(status):
if status["state"] == "Downloading":
if status['state'] == 'Downloading':
self.table_info.hide()
self.glade.get_widget("button_check_download").set_sensitive(False)
self.glade.get_widget("button_force_download").set_sensitive(False)
self.glade.get_widget("image_up_to_date").hide()
self.glade.get_widget('button_check_download').set_sensitive(False)
self.glade.get_widget('button_force_download').set_sensitive(False)
self.glade.get_widget('image_up_to_date').hide()
self.status_item.set_text(
"Downloading %.2f%%" % (status["file_progress"] * 100))
self.progress_bar.set_text("Downloading %.2f%%" % (status["file_progress"] * 100))
self.progress_bar.set_fraction(status["file_progress"])
'Downloading %.2f%%' % (status['file_progress'] * 100))
self.progress_bar.set_text('Downloading %.2f%%' % (status['file_progress'] * 100))
self.progress_bar.set_fraction(status['file_progress'])
self.progress_bar.show()
elif status["state"] == "Importing":
elif status['state'] == 'Importing':
self.table_info.hide()
self.glade.get_widget("button_check_download").set_sensitive(False)
self.glade.get_widget("button_force_download").set_sensitive(False)
self.glade.get_widget("image_up_to_date").hide()
self.glade.get_widget('button_check_download').set_sensitive(False)
self.glade.get_widget('button_force_download').set_sensitive(False)
self.glade.get_widget('image_up_to_date').hide()
self.status_item.set_text(
"Importing " + str(status["num_blocked"]))
self.progress_bar.set_text("Importing %s" % (status["num_blocked"]))
'Importing ' + str(status['num_blocked']))
self.progress_bar.set_text('Importing %s' % (status['num_blocked']))
self.progress_bar.pulse()
self.progress_bar.show()
elif status["state"] == "Idle":
elif status['state'] == 'Idle':
self.progress_bar.hide()
self.glade.get_widget("button_check_download").set_sensitive(True)
self.glade.get_widget("button_force_download").set_sensitive(True)
if status["up_to_date"]:
self.glade.get_widget("image_up_to_date").show()
self.glade.get_widget('button_check_download').set_sensitive(True)
self.glade.get_widget('button_force_download').set_sensitive(True)
if status['up_to_date']:
self.glade.get_widget('image_up_to_date').show()
else:
self.glade.get_widget("image_up_to_date").hide()
self.glade.get_widget('image_up_to_date').hide()
self.table_info.show()
self.status_item.set_text("%(num_blocked)s/%(num_whited)s" % status)
self.status_item.set_text('%(num_blocked)s/%(num_whited)s' % status)
self.glade.get_widget("label_filesize").set_text(
deluge.common.fsize(status["file_size"]))
self.glade.get_widget("label_modified").set_text(
datetime.fromtimestamp(status["file_date"]).strftime("%c"))
self.glade.get_widget("label_type").set_text(status["file_type"])
self.glade.get_widget("label_url").set_text(
status["file_url"])
self.glade.get_widget('label_filesize').set_text(
deluge.common.fsize(status['file_size']))
self.glade.get_widget('label_modified').set_text(
datetime.fromtimestamp(status['file_date']).strftime('%c'))
self.glade.get_widget('label_type').set_text(status['file_type'])
self.glade.get_widget('label_url').set_text(
status['file_url'])
client.blocklist.get_status().addCallback(_on_get_status)
def _on_show_prefs(self):
def _on_get_config(config):
log.trace("Loaded config: %s", config)
self.glade.get_widget("entry_url").set_text(config["url"])
self.glade.get_widget("spin_check_days").set_value(config["check_after_days"])
self.glade.get_widget("chk_import_on_start").set_active(config["load_on_start"])
self.populate_whitelist(config["whitelisted"])
log.trace('Loaded config: %s', config)
self.glade.get_widget('entry_url').set_text(config['url'])
self.glade.get_widget('spin_check_days').set_value(config['check_after_days'])
self.glade.get_widget('chk_import_on_start').set_active(config['load_on_start'])
self.populate_whitelist(config['whitelisted'])
client.blocklist.get_config().addCallback(_on_get_config)
def _on_apply_prefs(self):
config = {}
config["url"] = self.glade.get_widget("entry_url").get_text()
config["check_after_days"] = self.glade.get_widget("spin_check_days").get_value_as_int()
config["load_on_start"] = self.glade.get_widget("chk_import_on_start").get_active()
config["whitelisted"] = [ip[0] for ip in self.whitelist_model if ip[0]!='IP HERE']
config['url'] = self.glade.get_widget('entry_url').get_text().strip()
config['check_after_days'] = self.glade.get_widget('spin_check_days').get_value_as_int()
config['load_on_start'] = self.glade.get_widget('chk_import_on_start').get_active()
config['whitelisted'] = [ip[0] for ip in self.whitelist_model if ip[0] != 'IP HERE']
client.blocklist.set_config(config)
def _on_button_check_download_clicked(self, widget):
@ -159,16 +134,16 @@ class GtkUI(GtkPluginBase):
client.blocklist.check_import(force=True)
def _on_status_item_clicked(self, widget, event):
component.get("Preferences").show(_("Blocklist"))
component.get('Preferences').show(_('Blocklist'))
def load_preferences_page(self):
"""Initializes the preferences page and adds it to the preferences dialog"""
# Load the preferences page
self.glade = gtk.glade.XML(common.get_resource("blocklist_pref.glade"))
self.glade = gtk.glade.XML(common.get_resource('blocklist_pref.glade'))
self.whitelist_frame = self.glade.get_widget("whitelist_frame")
self.progress_bar = self.glade.get_widget("progressbar")
self.table_info = self.glade.get_widget("table_info")
self.whitelist_frame = self.glade.get_widget('whitelist_frame')
self.progress_bar = self.glade.get_widget('progressbar')
self.table_info = self.glade.get_widget('table_info')
# Hide the progress bar initially
self.progress_bar.hide()
@ -178,8 +153,8 @@ class GtkUI(GtkPluginBase):
self.build_whitelist_model_treeview()
self.glade.signal_autoconnect({
"on_button_check_download_clicked": self._on_button_check_download_clicked,
"on_button_force_download_clicked": self._on_button_force_download_clicked,
'on_button_check_download_clicked': self._on_button_check_download_clicked,
'on_button_force_download_clicked': self._on_button_force_download_clicked,
'on_whitelist_add_clicked': (self.on_add_button_clicked,
self.whitelist_treeview),
'on_whitelist_remove_clicked': (self.on_delete_button_clicked,
@ -187,72 +162,71 @@ class GtkUI(GtkPluginBase):
})
# Set button icons
self.glade.get_widget("image_download").set_from_file(
common.get_resource("blocklist_download24.png"))
self.glade.get_widget('image_download').set_from_file(
common.get_resource('blocklist_download24.png'))
self.glade.get_widget("image_import").set_from_file(
common.get_resource("blocklist_import24.png"))
self.glade.get_widget('image_import').set_from_file(
common.get_resource('blocklist_import24.png'))
# Update the preferences page with config values from the core
self._on_show_prefs()
# Add the page to the preferences dialog
self.plugin.add_preferences_page(
_("Blocklist"),
self.glade.get_widget("blocklist_prefs_box"))
_('Blocklist'),
self.glade.get_widget('blocklist_prefs_box'))
def build_whitelist_model_treeview(self):
self.whitelist_treeview = self.glade.get_widget("whitelist_treeview")
self.whitelist_treeview = self.glade.get_widget('whitelist_treeview')
treeview_selection = self.whitelist_treeview.get_selection()
treeview_selection.connect(
"changed", self.on_whitelist_treeview_selection_changed
'changed', self.on_whitelist_treeview_selection_changed
)
self.whitelist_model = gtk.ListStore(str, bool)
renderer = gtk.CellRendererText()
renderer.connect("edited", self.on_cell_edited, self.whitelist_model)
renderer.set_data("ip", 0)
renderer.connect('edited', self.on_cell_edited, self.whitelist_model)
renderer.set_data('ip', 0)
column = gtk.TreeViewColumn("IPs", renderer, text=0, editable=1)
column = gtk.TreeViewColumn('IPs', renderer, text=0, editable=1)
column.set_expand(True)
self.whitelist_treeview.append_column(column)
self.whitelist_treeview.set_model(self.whitelist_model)
def on_cell_edited(self, cell, path_string, new_text, model):
# iter = model.get_iter_from_string(path_string)
# path = model.get_path(iter)[0]
# iter = model.get_iter_from_string(path_string)
# path = model.get_path(iter)[0]
try:
ip = common.IP.parse(new_text)
model.set(model.get_iter_from_string(path_string), 0, ip.address)
except common.BadIP, e:
except common.BadIP as ex:
model.remove(model.get_iter_from_string(path_string))
from deluge.ui.gtkui import dialogs
d = dialogs.ErrorDialog(_("Bad IP address"), e.message)
d = dialogs.ErrorDialog(_('Bad IP address'), ex.message)
d.run()
def on_whitelist_treeview_selection_changed(self, selection):
model, selected_connection_iter = selection.get_selected()
if selected_connection_iter:
self.glade.get_widget("whitelist_delete").set_property('sensitive',
self.glade.get_widget('whitelist_delete').set_property('sensitive',
True)
else:
self.glade.get_widget("whitelist_delete").set_property('sensitive',
self.glade.get_widget('whitelist_delete').set_property('sensitive',
False)
def on_add_button_clicked(self, widget, treeview):
model = treeview.get_model()
model.set(model.append(), 0, "IP HERE", 1, True)
model.set(model.append(), 0, 'IP HERE', 1, True)
def on_delete_button_clicked(self, widget, treeview):
selection = treeview.get_selection()
model, iter = selection.get_selected()
if iter:
# path = model.get_path(iter)[0]
model.remove(iter)
model, selected_iter = selection.get_selected()
if selected_iter:
# path = model.get_path(iter)[0]
model.remove(selected_iter)
def populate_whitelist(self, whitelist):
self.whitelist_model.clear()
for ip in whitelist:
self.whitelist_model.set(
self.whitelist_model.append(),0, ip, 1, True
self.whitelist_model.append(), 0, ip, 1, True
)

View File

@ -1,53 +1,60 @@
##
# Copyright 2007 Steve 'Tarka' Smith (tarka@internode.on.net)
# Distributed under the same terms as Deluge
##
# -*- coding: utf-8 -*-
#
# Copyright (C) 2007 Steve 'Tarka' Smith (tarka@internode.on.net)
#
# This file is part of Deluge and is licensed under GNU General Public License 3.0, or later, with
# the additional special exception to link portions of this program with the OpenSSL library.
# See LICENSE for more details.
#
from __future__ import unicode_literals
import gzip
import logging
from exceptions import Exception
import socket
from struct import unpack
import gzip, socket
log = logging.getLogger(__name__)
class PGException(Exception):
pass
# Incrementally reads PeerGuardian blocklists v1 and v2.
# See http://wiki.phoenixlabs.org/wiki/P2B_Format
class PGReader:
class PGReader(object):
def __init__(self, filename):
log.debug("PGReader loading: %s", filename)
log.debug('PGReader loading: %s', filename)
try:
self.fd = gzip.open(filename, "rb")
except IOError, e:
log.debug("Blocklist: PGReader: Incorrect file type or list is corrupt")
with gzip.open(filename, 'rb') as _file:
self.fd = _file
except IOError:
log.debug('Blocklist: PGReader: Incorrect file type or list is corrupt')
# 4 bytes, should be 0xffffffff
buf = self.fd.read(4)
hdr = unpack("l", buf)[0]
hdr = unpack('l', buf)[0]
if hdr != -1:
raise PGException(_("Invalid leader") + " %d"%hdr)
raise PGException(_('Invalid leader') + ' %d' % hdr)
magic = self.fd.read(3)
if magic != "P2B":
raise PGException(_("Invalid magic code"))
if magic != 'P2B':
raise PGException(_('Invalid magic code'))
buf = self.fd.read(1)
ver = ord(buf)
if ver != 1 and ver != 2:
raise PGException(_("Invalid version") + " %d" % ver)
def next(self):
raise PGException(_('Invalid version') + ' %d' % ver)
def __next__(self):
# Skip over the string
buf = -1
while buf != 0:
buf = self.fd.read(1)
if buf == "": # EOF
if buf == '': # EOF
return False
buf = ord(buf)
@ -59,5 +66,8 @@ class PGReader:
return (start, end)
# Python 2 compatibility
next = __next__
def close(self):
self.fd.close()

View File

@ -1,52 +1,31 @@
#
# readers.py
# -*- coding: utf-8 -*-
#
# Copyright (C) 2009-2010 John Garland <johnnybg+deluge@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.
#
# This file is part of Deluge and is licensed under GNU General Public License 3.0, or later, with
# the additional special exception to link portions of this program with the OpenSSL library.
# See LICENSE for more details.
#
from __future__ import unicode_literals
import logging
from common import raisesErrorsAs, IP, BadIP
import re
from .common import IP, BadIP, raises_errors_as
log = logging.getLogger(__name__)
class ReaderParseError(Exception):
pass
class BaseReader(object):
"""Base reader for blocklist files"""
def __init__(self, file):
def __init__(self, _file):
"""Creates a new BaseReader given a file"""
self.file = file
self.file = _file
def open(self):
"""Opens the associated file for reading"""
@ -61,9 +40,8 @@ class BaseReader(object):
for start, end in self.readranges():
try:
callback(IP.parse(start), IP.parse(end))
except BadIP, e:
log.error("Failed to parse IP: %s", e)
# log.exception(e)
except BadIP as ex:
log.error('Failed to parse IP: %s', ex)
return self.file
def is_ignored(self, line):
@ -79,17 +57,16 @@ class BaseReader(object):
if not self.is_ignored(line):
try:
(start, end) = self.parse(line)
if not re.match("^(\d{1,3}\.){4}$", start + ".") or \
not re.match("^(\d{1,3}\.){4}$", end + "."):
if not re.match(r'^(\d{1,3}\.){4}$', start + '.') or \
not re.match(r'^(\d{1,3}\.){4}$', end + '.'):
valid = False
except:
except Exception:
valid = False
finally:
break
break
blocklist.close()
return valid
@raisesErrorsAs(ReaderParseError)
@raises_errors_as(ReaderParseError)
def readranges(self):
"""Yields each ip range from the file"""
blocklist = self.open()
@ -98,15 +75,18 @@ class BaseReader(object):
yield self.parse(line)
blocklist.close()
class EmuleReader(BaseReader):
"""Blocklist reader for emule style blocklists"""
def parse(self, line):
return line.strip().split(" , ")[0].split(" - ")
return line.strip().split(' , ')[0].split(' - ')
class SafePeerReader(BaseReader):
"""Blocklist reader for SafePeer style blocklists"""
def parse(self, line):
return line.strip().split(":")[-1].split("-")
return line.strip().split(':')[-1].split('-')
class PeerGuardianReader(SafePeerReader):
"""Blocklist reader for PeerGuardian style blocklists"""

View File

@ -1,64 +1,31 @@
#
# blocklist/webui.py
# -*- coding: utf-8 -*-
#
# Copyright (C) 2008 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.
#
# This file is part of Deluge and is licensed under GNU General Public License 3.0, or later, with
# the additional special exception to link portions of this program with the OpenSSL library.
# See LICENSE for more details.
#
from __future__ import unicode_literals
import os
import logging
from deluge.ui.client import client
from deluge import component
from deluge.plugins.pluginbase import WebPluginBase
from .common import get_resource
log = logging.getLogger(__name__)
#import deluge.ui.webui.lib.newforms_plus as forms
#config_page_manager = component.get("ConfigPageManager")
FORMAT_LIST = [
('gzmule',_("Emule IP list (GZip)")),
('spzip',_("SafePeer Text (Zipped)")),
('pgtext',_("PeerGuardian Text (Uncompressed)")),
('p2bgz',_("PeerGuardian P2B (GZip)"))
FORMAT_LIST = [
('gzmule', _('Emule IP list (GZip)')),
('spzip', _('SafePeer Text (Zipped)')),
('pgtext', _('PeerGuardian Text (Uncompressed)')),
('p2bgz', _('PeerGuardian P2B (GZip)'))
]
class WebUI(WebPluginBase):
def enable(self):
#config_page_manager.register('plugins','blocklist',BlockListCfgForm)
pass
def disable(self):
#config_page_manager.deregister('blocklist')
pass
class WebUI(WebPluginBase):
scripts = [get_resource('blocklist.js')]
debug_scripts = scripts

View File

@ -1,47 +1,23 @@
# setup.py
# -*- coding: utf-8 -*-
#
# 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.
# Copyright (C) 2009 Andrew Resch <andrewresch@gmail.com>
#
# This file is part of Deluge and is licensed under GNU General Public License 3.0, or later, with
# the additional special exception to link portions of this program with the OpenSSL library.
# See LICENSE for more details.
#
from setuptools import find_packages, setup
from setuptools import setup, find_packages
__plugin_name__ = "Blocklist"
__author__ = "John Garland"
__author_email__ = "johnnybg+deluge@gmail.com"
__version__ = "1.2"
__url__ = "http://deluge-torrent.org"
__license__ = "GPLv3"
__description__ = "Download and import IP blocklists"
__plugin_name__ = 'Blocklist'
__author__ = 'John Garland'
__author_email__ = 'johnnybg+deluge@gmail.com'
__version__ = '1.3'
__url__ = 'http://deluge-torrent.org'
__license__ = 'GPLv3'
__description__ = 'Download and import IP blocklists'
__long_description__ = __description__
__pkg_data__ = {'deluge.plugins.'+__plugin_name__.lower(): ["data/*"]}
__pkg_data__ = {'deluge.plugins.' + __plugin_name__.lower(): ['data/*']}
setup(
name=__plugin_name__,
@ -53,11 +29,9 @@ setup(
license=__license__,
zip_safe=False,
long_description=__long_description__,
packages=find_packages(),
namespace_packages = ["deluge", "deluge.plugins"],
package_data = __pkg_data__,
namespace_packages=['deluge', 'deluge.plugins'],
package_data=__pkg_data__,
entry_points="""
[deluge.plugin.core]
%s = deluge.plugins.%s:CorePlugin
@ -65,5 +39,5 @@ setup(
%s = deluge.plugins.%s:GtkUIPlugin
[deluge.plugin.web]
%s = deluge.plugins.%s:WebUIPlugin
""" % ((__plugin_name__, __plugin_name__.lower())*3)
""" % ((__plugin_name__, __plugin_name__.lower()) * 3)
)

View File

@ -1,3 +0,0 @@
# this is a namespace package
import pkg_resources
pkg_resources.declare_namespace(__name__)

View File

@ -1,3 +0,0 @@
# this is a namespace package
import pkg_resources
pkg_resources.declare_namespace(__name__)

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,41 +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("deluge.plugins.example",
os.path.join("data", filename))

View File

@ -1,57 +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.
#
#
import logging
from deluge.plugins.pluginbase import CorePluginBase
import deluge.component as component
import deluge.configmanager
from deluge.core.rpcserver import export
log = logging.getLogger(__name__)
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,50 +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
import logging
from deluge.ui.client import client
from deluge.plugins.pluginbase import GtkPluginBase
import deluge.component as component
import deluge.common
log = logging.getLogger(__name__)
class GtkUI(GtkPluginBase):
def enable(self):
pass
def disable(self):
pass

View File

@ -1,56 +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.
#
#
import logging
from deluge.ui.client import client
from deluge import component
from deluge.plugins.pluginbase import WebPluginBase
from common import get_resource
log = logging.getLogger(__name__)
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,68 +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, find_packages
__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__ = {"deluge.plugins."+__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=find_packages(),
namespace_packages = ["deluge", "deluge.plugins"],
package_data = __pkg_data__,
entry_points="""
[deluge.plugin.core]
%s = deluge.plugins.%s:CorePlugin
[deluge.plugin.gtkui]
%s = deluge.plugins.%s:GtkUIPlugin
[deluge.plugin.web]
%s = deluge.plugins.%s:WebUIPlugin
""" % ((__plugin_name__, __plugin_name__.lower())*3)
)

View File

@ -1,3 +1,6 @@
from __future__ import unicode_literals
# this is a namespace package
import pkg_resources
pkg_resources.declare_namespace(__name__)

View File

@ -1,3 +1,6 @@
from __future__ import unicode_literals
# this is a namespace package
import pkg_resources
pkg_resources.declare_namespace(__name__)

View File

@ -1,54 +1,33 @@
#
# __init__.py
# -*- coding: utf-8 -*-
#
# 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.
#
# This file is part of Deluge and is licensed under GNU General Public License 3.0, or later, with
# the additional special exception to link portions of this program with the OpenSSL library.
# See LICENSE for more details.
#
from __future__ import unicode_literals
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
from .core import Core as _pluginCls
self._plugin_cls = _pluginCls
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
from .gtkui import GtkUI as _pluginCls
self._plugin_cls = _pluginCls
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)
from .webui import WebUI as _pluginCls
self._plugin_cls = _pluginCls
super(WebUIPlugin, self).__init__(plugin_name)

View File

@ -1,41 +1,23 @@
# -*- coding: utf-8 -*-
#
# 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.
# Basic plugin template created by:
# Copyright (C) 2008 Martijn Voncken <mvoncken@gmail.com>
# 2007-2009 Andrew Resch <andrewresch@gmail.com>
# 2009 Damien Churchill <damoxc@gmail.com>
# 2010 Pedro Algarvio <pedro@algarvio.me>
# 2017 Calum Lind <calumlind+deluge@gmail.com>
#
# This file is part of Deluge and is licensed under GNU General Public License 3.0, or later, with
# the additional special exception to link portions of this program with the OpenSSL library.
# See LICENSE for more details.
#
import pkg_resources
from __future__ import unicode_literals
import os.path
from pkg_resources import resource_filename
def get_resource(filename):
return pkg_resources.resource_filename("deluge.plugins.execute",
os.path.join("data", filename))
return resource_filename('deluge.plugins.execute', os.path.join('data', filename))

View File

@ -1,54 +1,32 @@
#
# core.py
# -*- coding: utf-8 -*-
#
# 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.
#
# This file is part of Deluge and is licensed under GNU General Public License 3.0, or later, with
# the additional special exception to link portions of this program with the OpenSSL library.
# See LICENSE for more details.
#
import os
import time
from __future__ import unicode_literals
import hashlib
import logging
import os
import time
from twisted.internet.utils import getProcessOutputAndValue
from deluge.plugins.pluginbase import CorePluginBase
import deluge.component as component
from deluge.common import windows_check
from deluge.configmanager import ConfigManager
from deluge.core.rpcserver import export
from deluge.event import DelugeEvent
from deluge.plugins.pluginbase import CorePluginBase
log = logging.getLogger(__name__)
DEFAULT_CONFIG = {
"commands": []
'commands': []
}
EXECUTE_ID = 0
@ -56,10 +34,12 @@ EXECUTE_EVENT = 1
EXECUTE_COMMAND = 2
EVENT_MAP = {
"complete": "TorrentFinishedEvent",
"added": "TorrentAddedEvent"
'complete': 'TorrentFinishedEvent',
'added': 'TorrentAddedEvent',
'removed': 'TorrentRemovedEvent'
}
class ExecuteCommandAddedEvent(DelugeEvent):
"""
Emitted when a new command is added.
@ -67,6 +47,7 @@ class ExecuteCommandAddedEvent(DelugeEvent):
def __init__(self, command_id, event, command):
self._args = [command_id, event, command]
class ExecuteCommandRemovedEvent(DelugeEvent):
"""
Emitted when a command is removed.
@ -74,14 +55,16 @@ class ExecuteCommandRemovedEvent(DelugeEvent):
def __init__(self, command_id):
self._args = [command_id]
class Core(CorePluginBase):
def enable(self):
self.config = ConfigManager("execute.conf", DEFAULT_CONFIG)
event_manager = component.get("EventManager")
self.config = ConfigManager('execute.conf', DEFAULT_CONFIG)
event_manager = component.get('EventManager')
self.registered_events = {}
self.preremoved_cache = {}
# Go through the commands list and register event handlers
for command in self.config["commands"]:
for command in self.config['commands']:
event = command[EXECUTE_EVENT]
if event in self.registered_events:
continue
@ -92,84 +75,94 @@ class Core(CorePluginBase):
return event_handler
event_handler = create_event_handler(event)
event_manager.register_event_handler(EVENT_MAP[event], event_handler)
if event == 'removed':
event_manager.register_event_handler('PreTorrentRemovedEvent', self.on_preremoved)
self.registered_events[event] = event_handler
log.debug("Execute core plugin enabled!")
log.debug('Execute core plugin enabled!')
def on_preremoved(self, torrent_id):
# Get and store the torrent info before it is removed
torrent = component.get('TorrentManager').torrents[torrent_id]
info = torrent.get_status(['name', 'download_location'])
self.preremoved_cache[torrent_id] = [torrent_id, info['name'], info['download_location']]
def execute_commands(self, torrent_id, event, *arg):
torrent = component.get("TorrentManager").torrents[torrent_id]
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 == "complete":
save_path = info["move_on_completed_path"] if info ["move_on_completed"] else info["save_path"]
elif event == "added" and arg[0]:
if event == 'added' and arg[0]:
# No futher action as from_state (arg[0]) is True
return
elif event == 'removed':
torrent_id, torrent_name, download_location = self.preremoved_cache.pop(torrent_id)
else:
save_path = info["save_path"]
torrent = component.get('TorrentManager').torrents[torrent_id]
info = torrent.get_status(['name', 'download_location'])
# Grab the torrent name and download location
# getProcessOutputAndValue requires args to be str
torrent_name = info['name']
download_location = info['download_location']
# getProcessOutputAndValue requires args to be str
if isinstance(torrent_id, unicode):
torrent_id = torrent_id.encode("utf-8", "ignore")
if isinstance(torrent_name, unicode):
torrent_name = torrent_name.encode("utf-8", "ignore")
if isinstance(save_path, unicode):
save_path = save_path.encode("utf-8", "ignore")
log.debug("[execute] Running commands for %s", event)
log.debug('Running commands for %s', event)
def log_error(result, command):
(stdout, stderr, exit_code) = result
if exit_code:
log.warn("[execute] command '%s' failed with exit code %d", command, exit_code)
log.warn('Command "%s" failed with exit code %d', command, exit_code)
if stdout:
log.warn("[execute] stdout: %s", stdout)
log.warn('stdout: %s', stdout)
if stderr:
log.warn("[execute] stderr: %s", stderr)
log.warn('stderr: %s', stderr)
# Go through and execute all the commands
for command in self.config["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)
d = getProcessOutputAndValue(command, (torrent_id, torrent_name, save_path), env=os.environ)
d.addCallback(log_error, command)
cmd_args = [torrent_id.encode('utf8'), torrent_name.encode('utf8'),
download_location.encode('utf8')]
if windows_check():
# Escape ampersand on windows (see #2784)
cmd_args = [cmd_arg.replace('&', '^^^&') for cmd_arg in cmd_args]
if os.path.isfile(command) and os.access(command, os.X_OK):
log.debug('Running %s with args: %s', command, cmd_args)
d = getProcessOutputAndValue(command, cmd_args, env=os.environ)
d.addCallback(log_error, command)
else:
log.error('Execute script not found or not executable')
def disable(self):
self.config.save()
event_manager = component.get("EventManager")
for event, handler in self.registered_events.iteritems():
event_manager = component.get('EventManager')
for event, handler in self.registered_events.items():
event_manager.deregister_event_handler(event, handler)
log.debug("Execute core plugin disabled!")
log.debug('Execute core plugin disabled!')
### Exported RPC methods ###
# Exported RPC methods #
@export
def add_command(self, event, command):
command_id = hashlib.sha1(str(time.time())).hexdigest()
self.config["commands"].append((command_id, event, command))
self.config['commands'].append((command_id, event, command))
self.config.save()
component.get("EventManager").emit(ExecuteCommandAddedEvent(command_id, event, command))
component.get('EventManager').emit(ExecuteCommandAddedEvent(command_id, event, command))
@export
def get_commands(self):
return self.config["commands"]
return self.config['commands']
@export
def remove_command(self, command_id):
for command in self.config["commands"]:
for command in self.config['commands']:
if command[EXECUTE_ID] == command_id:
self.config["commands"].remove(command)
component.get("EventManager").emit(ExecuteCommandRemovedEvent(command_id))
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):
for i, command in enumerate(self.config["commands"]):
for i, command in enumerate(self.config['commands']):
if command[EXECUTE_ID] == command_id:
self.config["commands"][i] = (command_id, event, cmd)
self.config['commands'][i] = (command_id, event, cmd)
break
self.config.save()

View File

@ -1,138 +1,120 @@
/*
Script: execute.js
The client-side javascript code for the Execute plugin.
/*!
* execute.js
* The client-side javascript code for the Execute plugin.
*
* Copyright (C) Damien Churchill 2010 <damoxc@gmail.com>
*
* This file is part of Deluge and is licensed under GNU General Public License 3.0, or later, with
* the additional special exception to link portions of this program with the OpenSSL library.
* See LICENSE for more details.
*
*/
Copyright:
(C) Damien Churchill 2009-2010 <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.
*/
Ext.ns('Deluge.ux');
Deluge.ux.ExecuteWindowBase = Ext.extend(Ext.Window, {
layout: 'fit',
width: 400,
height: 130,
closeAction: 'hide',
initComponent: function() {
Deluge.ux.ExecuteWindowBase.superclass.initComponent.call(this);
this.addButton(_('Cancel'), this.onCancelClick, this);
layout: 'fit',
width: 400,
height: 130,
closeAction: 'hide',
this.form = this.add({
xtype: 'form',
baseCls: 'x-plain',
bodyStyle: 'padding: 5px',
items: [{
xtype: 'combo',
width: 270,
fieldLabel: _('Event'),
store: new Ext.data.ArrayStore({
fields: ['id', 'text'],
data: [
['complete', _('Torrent Complete')],
['added', _('Torrent Added')]
]
}),
name: 'event',
mode: 'local',
editable: false,
triggerAction: 'all',
valueField: 'id',
displayField: 'text'
}, {
xtype: 'textfield',
fieldLabel: _('Command'),
name: 'command',
width: 270
}]
});
},
initComponent: function() {
Deluge.ux.ExecuteWindowBase.superclass.initComponent.call(this);
this.addButton(_('Cancel'), this.onCancelClick, this);
onCancelClick: function() {
this.hide();
}
this.form = this.add({
xtype: 'form',
baseCls: 'x-plain',
bodyStyle: 'padding: 5px',
items: [{
xtype: 'combo',
width: 270,
fieldLabel: _('Event'),
store: new Ext.data.ArrayStore({
fields: ['id', 'text'],
data: [
['complete', _('Torrent Complete')],
['added', _('Torrent Added')],
['removed', _('Torrent Removed')]
]
}),
name: 'event',
mode: 'local',
editable: false,
triggerAction: 'all',
valueField: 'id',
displayField: 'text'
}, {
xtype: 'textfield',
fieldLabel: _('Command'),
name: 'command',
width: 270
}]
});
},
onCancelClick: function() {
this.hide();
}
});
Deluge.ux.EditExecuteCommandWindow = Ext.extend(Deluge.ux.ExecuteWindowBase, {
title: _('Edit Command'),
title: _('Edit Command'),
initComponent: function() {
Deluge.ux.EditExecuteCommandWindow.superclass.initComponent.call(this);
this.addButton(_('Save'), this.onSaveClick, this);
this.addEvents({
'commandedit': true
});
},
initComponent: function() {
Deluge.ux.EditExecuteCommandWindow.superclass.initComponent.call(this);
this.addButton(_('Save'), this.onSaveClick, this);
this.addEvents({
'commandedit': true
});
},
show: function(command) {
Deluge.ux.EditExecuteCommandWindow.superclass.show.call(this);
this.command = command;
this.form.getForm().setValues({
event: command.get('event'),
command: command.get('name')
});
},
show: function(command) {
Deluge.ux.EditExecuteCommandWindow.superclass.show.call(this);
this.command = command;
this.form.getForm().setValues({
event: command.get('event'),
command: command.get('name')
});
},
onSaveClick: function() {
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);
},
scope: this
});
this.hide();
}
onSaveClick: function() {
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);
},
scope: this
});
this.hide();
}
});
Deluge.ux.AddExecuteCommandWindow = Ext.extend(Deluge.ux.ExecuteWindowBase, {
title: _('Add Command'),
title: _('Add Command'),
initComponent: function() {
Deluge.ux.AddExecuteCommandWindow.superclass.initComponent.call(this);
this.addButton(_('Add'), this.onAddClick, this);
this.addEvents({
'commandadd': true
});
},
initComponent: function() {
Deluge.ux.AddExecuteCommandWindow.superclass.initComponent.call(this);
this.addButton(_('Add'), this.onAddClick, this);
this.addEvents({
'commandadd': true
});
},
onAddClick: function() {
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);
},
scope: this
});
this.hide();
}
onAddClick: function() {
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);
},
scope: this
});
this.hide();
}
});
@ -144,144 +126,146 @@ Ext.ns('Deluge.ux.preferences');
*/
Deluge.ux.preferences.ExecutePage = Ext.extend(Ext.Panel, {
title: _('Execute'),
layout: 'fit',
border: false,
initComponent: function() {
Deluge.ux.preferences.ExecutePage.superclass.initComponent.call(this);
var event_map = this.event_map = {
'complete': _('Torrent Complete'),
'added': _('Torrent Added')
}
title: _('Execute'),
header: false,
layout: 'fit',
border: false,
this.list = new Ext.list.ListView({
store: new Ext.data.SimpleStore({
fields: [
{name: 'event', mapping: 1},
{name: 'name', mapping: 2}
],
id: 0
}),
columns: [{
width: .3,
header: _('Event'),
sortable: true,
dataIndex: 'event',
tpl: new Ext.XTemplate('{[this.getEvent(values.event)]}', {
getEvent: function(e) {
return (event_map[e]) ? event_map[e] : e;
}
})
}, {
id: 'name',
header: _('Command'),
sortable: true,
dataIndex: 'name'
}],
singleSelect: true,
autoExpandColumn: 'name'
});
this.list.on('selectionchange', this.onSelectionChange, this);
initComponent: function() {
Deluge.ux.preferences.ExecutePage.superclass.initComponent.call(this);
var event_map = this.event_map = {
'complete': _('Torrent Complete'),
'added': _('Torrent Added'),
'removed': _('Torrent Removed')
};
this.panel = this.add({
items: [this.list],
bbar: {
items: [{
text: _('Add'),
iconCls: 'icon-add',
handler: this.onAddClick,
scope: this
}, {
text: _('Edit'),
iconCls: 'icon-edit',
handler: this.onEditClick,
scope: this,
disabled: true
}, '->', {
text: _('Remove'),
iconCls: 'icon-remove',
handler: this.onRemoveClick,
scope: this,
disabled: true
}]
}
});
deluge.preferences.on('show', this.onPreferencesShow, this);
},
this.list = new Ext.list.ListView({
store: new Ext.data.SimpleStore({
fields: [
{name: 'event', mapping: 1},
{name: 'name', mapping: 2}
],
id: 0
}),
columns: [{
width: .3,
header: _('Event'),
sortable: true,
dataIndex: 'event',
tpl: new Ext.XTemplate('{[this.getEvent(values.event)]}', {
getEvent: function(e) {
return (event_map[e]) ? event_map[e] : e;
}
})
}, {
id: 'name',
header: _('Command'),
sortable: true,
dataIndex: 'name'
}],
singleSelect: true,
autoExpandColumn: 'name'
});
this.list.on('selectionchange', this.onSelectionChange, this);
updateCommands: function() {
deluge.client.execute.get_commands({
success: function(commands) {
this.list.getStore().loadData(commands);
},
scope: this
});
},
this.panel = this.add({
items: [this.list],
bbar: {
items: [{
text: _('Add'),
iconCls: 'icon-add',
handler: this.onAddClick,
scope: this
}, {
text: _('Edit'),
iconCls: 'icon-edit',
handler: this.onEditClick,
scope: this,
disabled: true
}, '->', {
text: _('Remove'),
iconCls: 'icon-remove',
handler: this.onRemoveClick,
scope: this,
disabled: true
}]
}
});
onAddClick: function() {
if (!this.addWin) {
this.addWin = new Deluge.ux.AddExecuteCommandWindow();
this.addWin.on('commandadd', function() {
this.updateCommands();
}, this);
}
this.addWin.show();
},
this.on('show', this.onPreferencesShow, this);
},
onCommandAdded: function(win, evt, cmd) {
var record = new this.list.getStore().recordType({
event: evt,
command: cmd
});
},
updateCommands: function() {
deluge.client.execute.get_commands({
success: function(commands) {
this.list.getStore().loadData(commands);
},
scope: this
});
},
onEditClick: function() {
if (!this.editWin) {
this.editWin = new Deluge.ux.EditExecuteCommandWindow();
this.editWin.on('commandedit', function() {
this.updateCommands();
}, this);
}
this.editWin.show(this.list.getSelectedRecords()[0]);
},
onAddClick: function() {
if (!this.addWin) {
this.addWin = new Deluge.ux.AddExecuteCommandWindow();
this.addWin.on('commandadd', function() {
this.updateCommands();
}, this);
}
this.addWin.show();
},
onPreferencesShow: function() {
this.updateCommands();
},
onCommandAdded: function(win, evt, cmd) {
var record = new this.list.getStore().recordType({
event: evt,
command: cmd
});
},
onRemoveClick: function() {
var record = this.list.getSelectedRecords()[0];
deluge.client.execute.remove_command(record.id, {
success: function() {
this.updateCommands();
},
scope: this
});
},
onEditClick: function() {
if (!this.editWin) {
this.editWin = new Deluge.ux.EditExecuteCommandWindow();
this.editWin.on('commandedit', function() {
this.updateCommands();
}, this);
}
this.editWin.show(this.list.getSelectedRecords()[0]);
},
onSelectionChange: function(dv, selections) {
if (selections.length) {
this.panel.getBottomToolbar().items.get(1).enable();
this.panel.getBottomToolbar().items.get(3).enable();
} else {
this.panel.getBottomToolbar().items.get(1).disable();
this.panel.getBottomToolbar().items.get(3).disable();
}
}
onPreferencesShow: function() {
this.updateCommands();
},
onRemoveClick: function() {
var record = this.list.getSelectedRecords()[0];
deluge.client.execute.remove_command(record.id, {
success: function() {
this.updateCommands();
},
scope: this
});
},
onSelectionChange: function(dv, selections) {
if (selections.length) {
this.panel.getBottomToolbar().items.get(1).enable();
this.panel.getBottomToolbar().items.get(3).enable();
} else {
this.panel.getBottomToolbar().items.get(1).disable();
this.panel.getBottomToolbar().items.get(3).disable();
}
}
});
Deluge.plugins.ExecutePlugin = Ext.extend(Deluge.Plugin, {
name: 'Execute',
onDisable: function() {
deluge.preferences.removePage(this.prefsPage);
},
onEnable: function() {
this.prefsPage = deluge.preferences.addPage(new Deluge.ux.preferences.ExecutePage());
}
name: 'Execute',
onDisable: function() {
deluge.preferences.removePage(this.prefsPage);
},
onEnable: function() {
this.prefsPage = deluge.preferences.addPage(new Deluge.ux.preferences.ExecutePage());
}
});
Deluge.registerPlugin('Execute', Deluge.plugins.ExecutePlugin);

View File

@ -1,48 +1,26 @@
#
# gtkui.py
# -*- coding: utf-8 -*-
#
# 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.
#
# This file is part of Deluge and is licensed under GNU General Public License 3.0, or later, with
# the additional special exception to link portions of this program with the OpenSSL library.
# See LICENSE for more details.
#
from __future__ import unicode_literals
import logging
import gtk
import gtk.glade
import logging
from deluge.ui.client import client
from deluge.plugins.pluginbase import GtkPluginBase
import deluge.component as component
from deluge.plugins.pluginbase import GtkPluginBase
from deluge.ui.client import client
from . import common
# Relative import
from . import common
log = logging.getLogger(__name__)
@ -51,24 +29,26 @@ EXECUTE_EVENT = 1
EXECUTE_COMMAND = 2
EVENT_MAP = {
"complete": _("Torrent Complete"),
"added": _("Torrent Added")
'complete': _('Torrent Complete'),
'added': _('Torrent Added'),
'removed': _('Torrent Removed')
}
EVENTS = ["complete", "added"]
EVENTS = ['complete', 'added', 'removed']
class ExecutePreferences(object):
def __init__(self, plugin):
self.plugin = plugin
def load(self):
log.debug("Adding Execute Preferences page")
self.glade = gtk.glade.XML(common.get_resource("execute_prefs.glade"))
log.debug('Adding Execute Preferences page')
self.glade = gtk.glade.XML(common.get_resource('execute_prefs.glade'))
self.glade.signal_autoconnect({
"on_add_button_clicked": self.on_add_button_clicked
'on_add_button_clicked': self.on_add_button_clicked
})
events = self.glade.get_widget("event_combobox")
events = self.glade.get_widget('event_combobox')
store = gtk.ListStore(str, str)
for event in EVENTS:
@ -77,32 +57,31 @@ class ExecutePreferences(object):
events.set_model(store)
events.set_active(0)
self.plugin.add_preferences_page(_("Execute"),
self.glade.get_widget("execute_box"))
self.plugin.register_hook("on_show_prefs", self.load_commands)
self.plugin.register_hook("on_apply_prefs", self.on_apply_prefs)
self.plugin.add_preferences_page(_('Execute'), self.glade.get_widget('execute_box'))
self.plugin.register_hook('on_show_prefs', self.load_commands)
self.plugin.register_hook('on_apply_prefs', self.on_apply_prefs)
self.load_commands()
client.register_event_handler("ExecuteCommandAddedEvent", self.on_command_added_event)
client.register_event_handler("ExecuteCommandRemovedEvent", self.on_command_removed_event)
client.register_event_handler('ExecuteCommandAddedEvent', self.on_command_added_event)
client.register_event_handler('ExecuteCommandRemovedEvent', self.on_command_removed_event)
def unload(self):
self.plugin.remove_preferences_page(_("Execute"))
self.plugin.deregister_hook("on_apply_prefs", self.on_apply_prefs)
self.plugin.deregister_hook("on_show_prefs", self.load_commands)
self.plugin.remove_preferences_page(_('Execute'))
self.plugin.deregister_hook('on_apply_prefs', self.on_apply_prefs)
self.plugin.deregister_hook('on_show_prefs', self.load_commands)
def add_command(self, command_id, event, command):
log.debug("Adding command `%s`", command_id)
vbox = self.glade.get_widget("commands_vbox")
log.debug('Adding command `%s`', command_id)
vbox = self.glade.get_widget('commands_vbox')
hbox = gtk.HBox(False, 5)
hbox.set_name(command_id + "_" + event)
hbox.set_name(command_id + '_' + event)
label = gtk.Label(EVENT_MAP[event])
entry = gtk.Entry()
entry.set_text(command)
button = gtk.Button()
button.set_name("remove_%s" % command_id)
button.connect("clicked", self.on_remove_button_clicked)
button.set_name('remove_%s' % command_id)
button.connect('clicked', self.on_remove_button_clicked)
img = gtk.Image()
img.set_from_stock(gtk.STOCK_REMOVE, gtk.ICON_SIZE_BUTTON)
@ -115,15 +94,15 @@ class ExecutePreferences(object):
vbox.pack_start(hbox)
def remove_command(self, command_id):
vbox = self.glade.get_widget("commands_vbox")
vbox = self.glade.get_widget('commands_vbox')
children = vbox.get_children()
for child in children:
if child.get_name().split("_")[0] == command_id:
if child.get_name().split('_')[0] == command_id:
vbox.remove(child)
break
def clear_commands(self):
vbox = self.glade.get_widget("commands_vbox")
vbox = self.glade.get_widget('commands_vbox')
children = vbox.get_children()
for child in children:
vbox.remove(child)
@ -131,7 +110,7 @@ class ExecutePreferences(object):
def load_commands(self):
def on_get_commands(commands):
self.clear_commands()
log.debug("on_get_commands: %s", commands)
log.debug('on_get_commands: %s', commands)
for command in commands:
command_id, event, command = command
self.add_command(command_id, event, command)
@ -139,37 +118,38 @@ class ExecutePreferences(object):
client.execute.get_commands().addCallback(on_get_commands)
def on_add_button_clicked(self, *args):
command = self.glade.get_widget("command_entry").get_text()
events = self.glade.get_widget("event_combobox")
command = self.glade.get_widget('command_entry').get_text()
events = self.glade.get_widget('event_combobox')
event = events.get_model()[events.get_active()][1]
client.execute.add_command(event, command)
def on_remove_button_clicked(self, widget, *args):
command_id = widget.get_name().replace("remove_", "")
command_id = widget.get_name().replace('remove_', '')
client.execute.remove_command(command_id)
def on_apply_prefs(self):
vbox = self.glade.get_widget("commands_vbox")
vbox = self.glade.get_widget('commands_vbox')
children = vbox.get_children()
for child in children:
command_id, event = child.get_name().split("_")
command_id, event = child.get_name().split('_')
for widget in child.get_children():
if type(widget) == gtk.Entry:
if isinstance(widget, gtk.Entry):
command = widget.get_text()
client.execute.save_command(command_id, event, command)
def on_command_added_event(self, command_id, event, command):
log.debug("Adding command %s: %s", event, command)
log.debug('Adding command %s: %s', event, command)
self.add_command(command_id, event, command)
def on_command_removed_event(self, command_id):
log.debug("Removing command %s", command_id)
log.debug('Removing command %s', command_id)
self.remove_command(command_id)
class GtkUI(GtkPluginBase):
def enable(self):
self.plugin = component.get("PluginManager")
self.plugin = component.get('PluginManager')
self.preferences = ExecutePreferences(self.plugin)
self.preferences.load()

View File

@ -1,49 +1,24 @@
#
# webui.py
# -*- coding: utf-8 -*-
#
# 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.
#
# This file is part of Deluge and is licensed under GNU General Public License 3.0, or later, with
# the additional special exception to link portions of this program with the OpenSSL library.
# See LICENSE for more details.
#
from __future__ import unicode_literals
import logging
from deluge.ui.client import client
from deluge import component
from deluge.plugins.pluginbase import WebPluginBase
from common import get_resource
from .common import get_resource
log = logging.getLogger(__name__)
class WebUI(WebPluginBase):
scripts = [get_resource("execute.js")]
scripts = [get_resource('execute.js')]
debug_scripts = scripts

View File

@ -1,47 +1,23 @@
#
# setup.py
# -*- coding: utf-8 -*-
#
# Copyright (C) 2009 Damien Churchill <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.
#
# This file is part of Deluge and is licensed under GNU General Public License 3.0, or later, with
# the additional special exception to link portions of this program with the OpenSSL library.
# See LICENSE for more details.
#
from setuptools import setup, find_packages
from setuptools import find_packages, setup
__plugin_name__ = "Execute"
__author__ = "Damien Churchill"
__author_email__ = "damoxc@gmail.com"
__version__ = "1.2"
__url__ = "http://deluge-torrent.org"
__license__ = "GPLv3"
__description__ = "Plugin to execute a command upon an event"
__plugin_name__ = 'Execute'
__author__ = 'Damien Churchill'
__author_email__ = 'damoxc@gmail.com'
__version__ = '1.2'
__url__ = 'http://deluge-torrent.org'
__license__ = 'GPLv3'
__description__ = 'Plugin to execute a command upon an event'
__long_description__ = __description__
__pkg_data__ = {"deluge.plugins."+__plugin_name__.lower(): ["data/*"]}
__pkg_data__ = {'deluge.plugins.' + __plugin_name__.lower(): ['data/*']}
setup(
name=__plugin_name__,
@ -54,8 +30,8 @@ setup(
long_description=__long_description__,
packages=find_packages(),
namespace_packages = ["deluge", "deluge.plugins"],
package_data = __pkg_data__,
namespace_packages=['deluge', 'deluge.plugins'],
package_data=__pkg_data__,
entry_points="""
[deluge.plugin.core]
@ -64,5 +40,5 @@ setup(
%s = deluge.plugins.%s:GtkUIPlugin
[deluge.plugin.web]
%s = deluge.plugins.%s:WebUIPlugin
""" % ((__plugin_name__, __plugin_name__.lower())*3)
""" % ((__plugin_name__, __plugin_name__.lower()) * 3)
)

View File

@ -1,3 +1,6 @@
from __future__ import unicode_literals
# this is a namespace package
import pkg_resources
pkg_resources.declare_namespace(__name__)

View File

@ -1,3 +1,6 @@
from __future__ import unicode_literals
# this is a namespace package
import pkg_resources
pkg_resources.declare_namespace(__name__)

View File

@ -1,5 +1,4 @@
#
# __init__.py
# -*- coding: utf-8 -*-
#
# Copyright (C) 2009 Andrew Resch <andrewresch@gmail.com>
#
@ -7,52 +6,32 @@
# 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.
#
# This file is part of Deluge and is licensed under GNU General Public License 3.0, or later, with
# the additional special exception to link portions of this program with the OpenSSL library.
# See LICENSE for more details.
#
from __future__ import unicode_literals
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
from .core import Core as _pluginCls
self._plugin_cls = _pluginCls
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
from .gtkui import GtkUI as _pluginCls
self._plugin_cls = _pluginCls
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
from .webui import WebUI as _pluginCls
self._plugin_cls = _pluginCls
super(WebUIPlugin, self).__init__(plugin_name)

View File

@ -1,39 +1,23 @@
# -*- coding: utf-8 -*-
#
# 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.
# Basic plugin template created by:
# Copyright (C) 2008 Martijn Voncken <mvoncken@gmail.com>
# 2007-2009 Andrew Resch <andrewresch@gmail.com>
# 2009 Damien Churchill <damoxc@gmail.com>
# 2010 Pedro Algarvio <pedro@algarvio.me>
# 2017 Calum Lind <calumlind+deluge@gmail.com>
#
# This file is part of Deluge and is licensed under GNU General Public License 3.0, or later, with
# the additional special exception to link portions of this program with the OpenSSL library.
# See LICENSE for more details.
#
from __future__ import unicode_literals
import os.path
from pkg_resources import resource_filename
def get_resource(filename):
import pkg_resources, os
return pkg_resources.resource_filename("deluge.plugins.extractor",
os.path.join("data", filename))
return resource_filename('deluge.plugins.extractor', os.path.join('data', filename))

View File

@ -1,5 +1,4 @@
#
# core.py
# -*- coding: utf-8 -*-
#
# Copyright (C) 2009 Andrew Resch <andrewresch@gmail.com>
#
@ -7,53 +6,31 @@
# 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.
#
# This file is part of Deluge and is licensed under GNU General Public License 3.0, or later, with
# the additional special exception to link portions of this program with the OpenSSL library.
# See LICENSE for more details.
#
import os
from __future__ import unicode_literals
import errno
import logging
import os
from twisted.internet.utils import getProcessValue
from twisted.internet.utils import getProcessOutputAndValue
from twisted.python.procutils import which
from deluge.plugins.pluginbase import CorePluginBase
import deluge.component as component
import deluge.configmanager
from deluge.core.rpcserver import export
from deluge.common import windows_check
from extractor.which import which
from deluge.core.rpcserver import export
from deluge.plugins.pluginbase import CorePluginBase
log = logging.getLogger(__name__)
DEFAULT_PREFS = {
"extract_path": "",
"use_name_folder": True
'extract_path': '',
'use_name_folder': True
}
if windows_check():
@ -62,60 +39,76 @@ if windows_check():
'C:\\Program Files\\7-Zip\\7z.exe',
'C:\\Program Files (x86)\\7-Zip\\7z.exe',
]
switch_7z = "x -y"
## Future suport:
## 7-zip cannot extract tar.* with single command.
try:
import winreg
except ImportError:
import _winreg as winreg # For Python 2.
try:
hkey = winreg.OpenKey(winreg.HKEY_CURRENT_USER, 'Software\\7-Zip')
except WindowsError:
pass
else:
win_7z_path = os.path.join(winreg.QueryValueEx(hkey, 'Path')[0], '7z.exe')
winreg.CloseKey(hkey)
win_7z_exes.insert(1, win_7z_path)
switch_7z = 'x -y'
# Future suport:
# 7-zip cannot extract tar.* with single command.
# ".tar.gz", ".tgz",
# ".tar.bz2", ".tbz",
# ".tar.lzma", ".tlz",
# ".tar.xz", ".txz",
exts_7z = [
".rar", ".zip", ".tar",
".7z", ".xz", ".lzma",
'.rar', '.zip', '.tar',
'.7z', '.xz', '.lzma',
]
for win_7z_exe in win_7z_exes:
if which(win_7z_exe):
EXTRACT_COMMANDS = dict.fromkeys(exts_7z, [win_7z_exe, switch_7z])
break
else:
required_cmds=["unrar", "unzip", "tar", "unxz", "unlzma", "7zr", "bunzip2"]
## Possible future suport:
required_cmds = ['unrar', 'unzip', 'tar', 'unxz', 'unlzma', '7zr', 'bunzip2']
# Possible future suport:
# gunzip: gz (cmd will delete original archive)
## the following do not extract to dest dir
# the following do not extract to dest dir
# ".xz": ["xz", "-d --keep"],
# ".lzma": ["xz", "-d --format=lzma --keep"],
# ".bz2": ["bzip2", "-d --keep"],
EXTRACT_COMMANDS = {
".rar": ["unrar", "x -o+ -y"],
".tar": ["tar", "-xf"],
".zip": ["unzip", ""],
".tar.gz": ["tar", "-xzf"], ".tgz": ["tar", "-xzf"],
".tar.bz2": ["tar", "-xjf"], ".tbz": ["tar", "-xjf"],
".tar.lzma": ["tar", "--lzma -xf"], ".tlz": ["tar", "--lzma -xf"],
".tar.xz": ["tar", "--xz -xf"], ".txz": ["tar", "--xz -xf"],
".7z": ["7zr", "x"],
'.rar': ['unrar', 'x -o+ -y'],
'.tar': ['tar', '-xf'],
'.zip': ['unzip', ''],
'.tar.gz': ['tar', '-xzf'], '.tgz': ['tar', '-xzf'],
'.tar.bz2': ['tar', '-xjf'], '.tbz': ['tar', '-xjf'],
'.tar.lzma': ['tar', '--lzma -xf'], '.tlz': ['tar', '--lzma -xf'],
'.tar.xz': ['tar', '--xz -xf'], '.txz': ['tar', '--xz -xf'],
'.7z': ['7zr', 'x'],
}
# Test command exists and if not, remove.
for cmd in required_cmds:
if not which(cmd):
for k,v in EXTRACT_COMMANDS.items():
if cmd in v[0]:
log.warning("%s not found, disabling support for %s", cmd, k)
for command in required_cmds:
if not which(command):
for k, v in EXTRACT_COMMANDS.items():
if command in v[0]:
log.warning('%s not found, disabling support for %s', command, k)
del EXTRACT_COMMANDS[k]
if not EXTRACT_COMMANDS:
raise Exception("No archive extracting programs found, plugin will be disabled")
raise Exception('No archive extracting programs found, plugin will be disabled')
class Core(CorePluginBase):
def enable(self):
self.config = deluge.configmanager.ConfigManager("extractor.conf", DEFAULT_PREFS)
if not self.config["extract_path"]:
self.config["extract_path"] = deluge.configmanager.ConfigManager("core.conf")["download_location"]
component.get("EventManager").register_event_handler("TorrentFinishedEvent", self._on_torrent_finished)
self.config = deluge.configmanager.ConfigManager('extractor.conf', DEFAULT_PREFS)
if not self.config['extract_path']:
self.config['extract_path'] = deluge.configmanager.ConfigManager('core.conf')['download_location']
component.get('EventManager').register_event_handler('TorrentFinishedEvent', self._on_torrent_finished)
def disable(self):
component.get("EventManager").deregister_event_handler("TorrentFinishedEvent", self._on_torrent_finished)
component.get('EventManager').deregister_event_handler('TorrentFinishedEvent', self._on_torrent_finished)
def update(self):
pass
@ -124,63 +117,57 @@ class Core(CorePluginBase):
"""
This is called when a torrent finishes and checks if any files to extract.
"""
tid = component.get("TorrentManager").torrents[torrent_id]
tid_status = tid.get_status(["save_path", "move_completed", "name"])
if tid_status["move_completed"]:
log.warning("Cannot extract torrents with 'Move Completed' enabled")
return
tid = component.get('TorrentManager').torrents[torrent_id]
tid_status = tid.get_status(['download_location', 'name'])
files = tid.get_files()
for f in files:
file_root, file_ext = os.path.splitext(f["path"])
file_root, file_ext = os.path.splitext(f['path'])
file_ext_sec = os.path.splitext(file_root)[1]
if file_ext_sec and file_ext_sec + file_ext in EXTRACT_COMMANDS:
file_ext = file_ext_sec + file_ext
elif file_ext not in EXTRACT_COMMANDS or file_ext_sec == '.tar':
log.warning("Can't extract file with unknown file type: %s" % f["path"])
log.debug('Cannot extract file with unknown file type: %s', f['path'])
continue
elif file_ext == '.rar' and 'part' in file_ext_sec:
part_num = file_ext_sec.split('part')[1]
if part_num.isdigit() and int(part_num) != 1:
log.debug('Skipping remaining multi-part rar files: %s', f['path'])
continue
cmd = EXTRACT_COMMANDS[file_ext]
fpath = os.path.join(tid_status['download_location'], os.path.normpath(f['path']))
dest = os.path.normpath(self.config['extract_path'])
if self.config['use_name_folder']:
dest = os.path.join(dest, tid_status['name'])
# Now that we have the cmd, lets run it to extract the files
fpath = os.path.join(tid_status["save_path"], os.path.normpath(f["path"]))
try:
os.makedirs(dest)
except OSError as ex:
if not (ex.errno == errno.EEXIST and os.path.isdir(dest)):
log.error('Error creating destination folder: %s', ex)
break
# Get the destination path
dest = os.path.normpath(self.config["extract_path"])
if self.config["use_name_folder"]:
name = tid_status["name"]
dest = os.path.join(dest, name)
def on_extract(result, torrent_id, fpath):
# Check command exit code.
if not result[2]:
log.info('Extract successful: %s (%s)', fpath, torrent_id)
else:
log.error('Extract failed: %s (%s) %s', fpath, torrent_id, result[1])
# Create the destination folder if it doesn't exist
if not os.path.exists(dest):
try:
os.makedirs(dest)
except Exception, e:
log.error("Error creating destination folder: %s", e)
return
def on_extract_success(result, torrent_id, fpath):
# XXX: Emit an event
log.info("Extract successful: %s (%s)", fpath, torrent_id)
def on_extract_failed(result, torrent_id, fpath):
# XXX: Emit an event
log.error("Extract failed: %s (%s)", fpath, torrent_id)
# Run the command and add some callbacks
log.debug("Extracting %s with %s %s to %s", fpath, cmd[0], cmd[1], dest)
d = getProcessValue(cmd[0], cmd[1].split() + [str(fpath)], {}, str(dest))
d.addCallback(on_extract_success, torrent_id, fpath)
d.addErrback(on_extract_failed, torrent_id, fpath)
# Run the command and add callback.
log.debug('Extracting %s from %s with %s %s to %s', fpath, torrent_id, cmd[0], cmd[1], dest)
d = getProcessOutputAndValue(cmd[0], cmd[1].split() + [str(fpath)], os.environ, str(dest))
d.addCallback(on_extract, torrent_id, fpath)
@export
def set_config(self, config):
"sets the config dictionary"
for key in config.keys():
'sets the config dictionary'
for key in config:
self.config[key] = config[key]
self.config.save()
@export
def get_config(self):
"returns the config dictionary"
'returns the config dictionary'
return self.config.config

View File

@ -0,0 +1,102 @@
/*!
* extractor.js
*
* Copyright (C) Calum Lind 2014 <calumlind@gmail.com>
*
* This file is part of Deluge and is licensed under GNU General Public License 3.0, or later, with
* the additional special exception to link portions of this program with the OpenSSL library.
* See LICENSE for more details.
*
*/
Ext.ns('Deluge.ux.preferences');
/**
* @class Deluge.ux.preferences.ExtractorPage
* @extends Ext.Panel
*/
Deluge.ux.preferences.ExtractorPage = Ext.extend(Ext.Panel, {
title: _('Extractor'),
header: false,
layout: 'fit',
border: false,
initComponent: function() {
Deluge.ux.preferences.ExtractorPage.superclass.initComponent.call(this);
this.form = this.add({
xtype: 'form',
layout: 'form',
border: false,
autoHeight: true
});
fieldset = this.form.add({
xtype: 'fieldset',
border: false,
title: '',
autoHeight: true,
labelAlign: 'top',
labelWidth: 80,
defaultType: 'textfield'
});
this.extract_path = fieldset.add({
fieldLabel: _('Extract to:'),
labelSeparator: '',
name: 'extract_path',
width: '97%'
});
this.use_name_folder = fieldset.add({
xtype: 'checkbox',
name: 'use_name_folder',
height: 22,
hideLabel: true,
boxLabel: _('Create torrent name sub-folder')
});
this.on('show', this.updateConfig, this);
},
onApply: function() {
// build settings object
var config = {};
config['extract_path'] = this.extract_path.getValue();
config['use_name_folder'] = this.use_name_folder.getValue();
deluge.client.extractor.set_config(config);
},
onOk: function() {
this.onApply();
},
updateConfig: function() {
deluge.client.extractor.get_config({
success: function(config) {
this.extract_path.setValue(config['extract_path']);
this.use_name_folder.setValue(config['use_name_folder']);
},
scope: this
});
}
});
Deluge.plugins.ExtractorPlugin = Ext.extend(Deluge.Plugin, {
name: 'Extractor',
onDisable: function() {
deluge.preferences.removePage(this.prefsPage);
},
onEnable: function() {
this.prefsPage = deluge.preferences.addPage(new Deluge.ux.preferences.ExtractorPage());
}
});
Deluge.registerPlugin('Extractor', Deluge.plugins.ExtractorPlugin);

View File

@ -1,5 +1,4 @@
#
# gtkui.py
# -*- coding: utf-8 -*-
#
# Copyright (C) 2009 Andrew Resch <andrewresch@gmail.com>
#
@ -7,92 +6,70 @@
# 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.
#
# This file is part of Deluge and is licensed under GNU General Public License 3.0, or later, with
# the additional special exception to link portions of this program with the OpenSSL library.
# See LICENSE for more details.
#
from __future__ import unicode_literals
import logging
import gtk
import gtk.glade
import logging
from deluge.ui.client import client
from deluge.plugins.pluginbase import GtkPluginBase
import deluge.component as component
import deluge.common
from deluge.plugins.pluginbase import GtkPluginBase
from deluge.ui.client import client
from common import get_resource
from .common import get_resource
log = logging.getLogger(__name__)
class GtkUI(GtkPluginBase):
def enable(self):
self.glade = gtk.glade.XML(get_resource("extractor_prefs.glade"))
self.glade = gtk.glade.XML(get_resource('extractor_prefs.glade'))
component.get("Preferences").add_page(_("Extractor"), self.glade.get_widget("extractor_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)
component.get('Preferences').add_page(_('Extractor'), self.glade.get_widget('extractor_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()
def disable(self):
component.get("Preferences").remove_page(_("Extractor"))
component.get("PluginManager").deregister_hook("on_apply_prefs", self.on_apply_prefs)
component.get("PluginManager").deregister_hook("on_show_prefs", self.on_show_prefs)
component.get('Preferences').remove_page(_('Extractor'))
component.get('PluginManager').deregister_hook('on_apply_prefs', self.on_apply_prefs)
component.get('PluginManager').deregister_hook('on_show_prefs', self.on_show_prefs)
del self.glade
def on_apply_prefs(self):
log.debug("applying prefs for Extractor")
log.debug('applying prefs for Extractor')
if client.is_localhost():
path = self.glade.get_widget("folderchooser_path").get_filename()
path = self.glade.get_widget('folderchooser_path').get_filename()
else:
path = self.glade.get_widget("entry_path").get_text()
path = self.glade.get_widget('entry_path').get_text()
config = {
"extract_path": path,
"use_name_folder": self.glade.get_widget("chk_use_name").get_active()
'extract_path': path,
'use_name_folder': self.glade.get_widget('chk_use_name').get_active()
}
client.extractor.set_config(config)
def on_show_prefs(self):
if client.is_localhost():
self.glade.get_widget("folderchooser_path").show()
self.glade.get_widget("entry_path").hide()
self.glade.get_widget('folderchooser_path').show()
self.glade.get_widget('entry_path').hide()
else:
self.glade.get_widget("folderchooser_path").hide()
self.glade.get_widget("entry_path").show()
self.glade.get_widget('folderchooser_path').hide()
self.glade.get_widget('entry_path').show()
def on_get_config(config):
if client.is_localhost():
self.glade.get_widget("folderchooser_path").set_current_folder(config["extract_path"])
self.glade.get_widget('folderchooser_path').set_current_folder(config['extract_path'])
else:
self.glade.get_widget("entry_path").set_text(config["extract_path"])
self.glade.get_widget('entry_path').set_text(config['extract_path'])
self.glade.get_widget("chk_use_name").set_active(config["use_name_folder"])
self.glade.get_widget('chk_use_name').set_active(config['use_name_folder'])
client.extractor.get_config().addCallback(on_get_config)

View File

@ -1,5 +1,4 @@
#
# webui.py
# -*- coding: utf-8 -*-
#
# Copyright (C) 2009 Andrew Resch <andrewresch@gmail.com>
#
@ -7,46 +6,23 @@
# 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.
#
# This file is part of Deluge and is licensed under GNU General Public License 3.0, or later, with
# the additional special exception to link portions of this program with the OpenSSL library.
# See LICENSE for more details.
#
from __future__ import unicode_literals
import logging
from deluge.ui.client import client
from deluge import component
from deluge.plugins.pluginbase import WebPluginBase
from .common import get_resource
log = logging.getLogger(__name__)
class WebUI(WebPluginBase):
def enable(self):
pass
def disable(self):
pass
class WebUI(WebPluginBase):
scripts = [get_resource('extractor.js')]
debug_scripts = scripts

View File

@ -1,5 +1,4 @@
#
# setup.py
# -*- coding: utf-8 -*-
#
# Copyright (C) 2009 Andrew Resch <andrewresch@gmail.com>
#
@ -7,45 +6,20 @@
# 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.
#
# This file is part of Deluge and is licensed under GNU General Public License 3.0, or later, with
# the additional special exception to link portions of this program with the OpenSSL library.
# See LICENSE for more details.
#
from setuptools import setup, find_packages
from setuptools import find_packages, setup
__plugin_name__ = "Extractor"
__author__ = "Andrew Resch"
__author_email__ = "andrewresch@gmail.com"
__version__ = "0.3"
__url__ = "http://deluge-torrent.org"
__license__ = "GPLv3"
__description__ = "Extract files upon torrent completion"
__plugin_name__ = 'Extractor'
__author__ = 'Andrew Resch'
__author_email__ = 'andrewresch@gmail.com'
__version__ = '0.6'
__url__ = 'http://deluge-torrent.org'
__license__ = 'GPLv3'
__description__ = 'Extract files upon torrent completion'
__long_description__ = """
Extract files upon torrent completion
@ -56,7 +30,7 @@ Windows support: .rar, .zip, .tar, .7z, .xz, .lzma
Note: Will not extract with 'Move Completed' enabled
"""
__pkg_data__ = {"deluge.plugins."+__plugin_name__.lower(): ["template/*", "data/*"]}
__pkg_data__ = {'deluge.plugins.' + __plugin_name__.lower(): ['template/*', 'data/*']}
setup(
name=__plugin_name__,
@ -69,21 +43,15 @@ setup(
long_description=__long_description__ if __long_description__ else __description__,
packages=find_packages(),
namespace_packages = ["deluge", "deluge.plugins"],
package_data = __pkg_data__,
namespace_packages=['deluge', 'deluge.plugins'],
package_data=__pkg_data__,
entry_points="""
[deluge.plugin.core]
%s = deluge.plugins.%s:CorePlugin
[deluge.plugin.gtkui]
<<<<<<< HEAD:deluge/plugins/Extractor/setup.py
%s = deluge.plugins.%s:GtkUIPlugin
[deluge.plugin.web]
%s = deluge.plugins.%s:WebUIPlugin
=======
%s = %s:GtkUIPlugin
[deluge.plugin.web]
%s = %s:WebUIPlugin
>>>>>>> fa7edd0... Fix plugins not showing enabled in webui:deluge/plugins/extractor/setup.py
""" % ((__plugin_name__, __plugin_name__.lower())*3)
""" % ((__plugin_name__, __plugin_name__.lower()) * 3)
)

View File

@ -1,3 +0,0 @@
# this is a namespace package
import pkg_resources
pkg_resources.declare_namespace(__name__)

View File

@ -1,3 +0,0 @@
# this is a namespace package
import pkg_resources
pkg_resources.declare_namespace(__name__)

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,434 +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 logging
import feedparser # for parsing rss feeds
import threading # for threaded updates
import re # for regular expressions
from twisted.internet.task import LoopingCall
from deluge.plugins.pluginbase import CorePluginBase
import deluge.component as component
import deluge.configmanager
from deluge.core.rpcserver import export
log = logging.getLogger(__name__)
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,50 +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
import logging
from deluge.ui.client import client
from deluge.plugins.pluginbase import GtkPluginBase
import deluge.component as component
import deluge.common
log = logging.getLogger(__name__)
class GtkUI(GtkPluginBase):
def enable(self):
pass
def disable(self):
pass

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>

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